From 464a04a16e7fbac2c43dd4c8386d11c3e61441c1 Mon Sep 17 00:00:00 2001 From: RECTOR Date: Mon, 12 Oct 2026 10:59:44 +0700 Subject: [PATCH 1/2] fix(money): count per-agent advisor artifacts in sessions scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scan walked //*.jsonl (main sessions, subagent artifacts, main __advisor.jsonl) but stopped one level short of ///__advisor.jsonl — advisor consults inside subagent trees carry usage.cost.total like any other artifact and were silently dropped. Measured on the live sessions tree: 157 files / ~6.8k entries missed, ~22% under 7DAY, ~28% under DAY. Scan now covers session-dir subtrees one level deeper; omp writes no .jsonl below that (verified: zero files at depth 5), so no recursion. --- README.md | 4 ++-- src/money.ts | 47 +++++++++++++++++++++++++++++++++------------- test/money.test.ts | 26 +++++++++++++------------ 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 5f61f3c..f380c08 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Render order (top → bottom): **info · prayers · money · zai (one row per wi Costs are disk-scanned from omp's own session tree (`~/.omp/agent/sessions/`), not tracked live: - every session file embeds its `cwd` and per-assistant-message `usage.cost.total` — the tree is a complete ledger; -- **subagent spend is included**: subagent transcripts persist as session-format JSONL artifacts (`//.jsonl`) and are scanned too; +- **subagent and advisor spend is included**: subagent transcripts persist as session-format JSONL artifacts (`//.jsonl`) and advisor consults as `__advisor.jsonl` — beside the session (`/__advisor.jsonl`) and nested per agent (`//__advisor.jsonl`) — and are scanned too; - entry ids are deduped globally, so branched/resumed session copies never double-count; - the sessions tree (including history migrated from pi) supersedes the retired pi-statusline ledger. @@ -79,7 +79,7 @@ State (deen cache) lives beside the config in `~/.omp/agent/omp-statusline/`. ## `/sl` — force refresh & full report -Forces a zai + deen + money refresh and notifies full source state: quota freshness, today's plan credits, 7-day per-model split, usage streaks, cache-hit rate, prayer city/hijri, and the spend breakdown including subagent share and entry count: +Forces a zai + deen + money refresh and notifies full source state: quota freshness, today's plan credits, 7-day per-model split, usage streaks, cache-hit rate, prayer city/hijri, and the spend breakdown including subagent + advisor share and entry count: ``` zai key omp credentials · 5h 16% · weekly 33% · fetched 0m ago · today 7.7K · models 7d glm-5.3 28K · gpt-5.4 9.2K · streak 46d (best 61d) · cache 71% | deen Jakarta · 1 Jumādá al-ūlá 1448 (11 Oct 2026) · fresh | money REPO $68.36 · DAY $26.50 · 7DAY $315.27 · 30DAY $492.88 · sub $11.02 · 1234 entries diff --git a/src/money.ts b/src/money.ts index a4a2a08..0c97156 100644 --- a/src/money.ts +++ b/src/money.ts @@ -1,11 +1,12 @@ // money.ts — omp sessions disk-scan ledger. Sole money source (decision 2026-09-07): // the sessions tree is a superset of the retired pi ledger (migrated history inside), -// covers subagent spend (/.jsonl artifacts carry usage), and every +// covers subagent spend (/.jsonl artifacts carry usage) and advisor +// consults (/__advisor.jsonl, and per-agent //__advisor.jsonl), and every // file embeds its cwd in the {"type":"session"} line → repo attribution without guesses. // Windows: DAY = since local midnight; 7DAY/30DAY = rolling, aligned to the current hour // (hourFloor(now) − N×24h — RECTOR semantics, NOT pi-statusline's calendar-day buckets). // Entry ids are uuid7; branched/resumed session copies share ids → dedupe on the bare id -// so a re-persisted message is never counted twice. Full rescan (~0.9s over ~560 files) +// so a re-persisted message is never counted twice. Full rescan (~4s over ~150k entries) // per poll cycle; renders between polls use the cached snapshot. import { homedir } from "node:os"; import { join } from "node:path"; @@ -16,7 +17,7 @@ export interface MoneySnapshot { day: number; // since 00:00 local week: number; // hourFloor(now) − 7×24h month: number; // hourFloor(now) − 30×24h - sub: number; // subagent-artifact share of those windows' union (reporting only) + sub: number; // subagent + advisor artifact share of those windows' union (reporting only) entries: number; // v0.3.0: token volumes per window (RECTOR) — totalTokens from the same scan; // falls back to the token-component sum on legacy entries lacking the field. @@ -77,6 +78,33 @@ function parseSessionFile(path: string, sub: boolean, out: CostEntry[], seen: Se } } +// Session artifact directories hold *.jsonl at their own level (subagent .jsonl, +// main __advisor.jsonl) and per-agent subtrees one level deeper (/__advisor.jsonl). +// omp writes nothing below that (verified 2026-10-12: zero .jsonl at depth 5), so no recursion. +function scanSessionDir(dir: string, entries: CostEntry[], seen: Set): void { + let dirents; + try { + dirents = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const g of dirents) { + if (g.isFile() && g.name.endsWith(".jsonl")) { + parseSessionFile(join(dir, g.name), true, entries, seen); + } else if (g.isDirectory()) { + let nested; + try { + nested = readdirSync(join(dir, g.name), { withFileTypes: true }); + } catch { + continue; + } + for (const h of nested) { + if (h.isFile() && h.name.endsWith(".jsonl")) parseSessionFile(join(dir, g.name, h.name), true, entries, seen); + } + } + } +} + export function scanMoney( currentRepo: string, now = Date.now(), @@ -103,16 +131,9 @@ export function scanMoney( // main session file: /.jsonl parseSessionFile(join(repoDir, f.name), false, entries, seen); } else if (f.isDirectory()) { - // subagent artifacts: //*.jsonl - let nested; - try { - nested = readdirSync(join(repoDir, f.name), { withFileTypes: true }); - } catch { - continue; - } - for (const g of nested) { - if (g.isFile() && g.name.endsWith(".jsonl")) parseSessionFile(join(repoDir, f.name, g.name), true, entries, seen); - } + // session artifacts: // — subagent .jsonl, + // main/per-agent __advisor.jsonl (nested one level deeper) + scanSessionDir(join(repoDir, f.name), entries, seen); } } } diff --git a/test/money.test.ts b/test/money.test.ts index b85b052..18165d6 100644 --- a/test/money.test.ts +++ b/test/money.test.ts @@ -53,27 +53,29 @@ test("scanMoney: windows, dedupe, repo attribution, subagent share", () => { fixture(root, "slugA", "sess1.jsonl", "/home/u/repoA", repoA); fixture(root, "slugA", "sess1-branch.jsonl", "/home/u/repoA", repoA); // duplicate ids fixture(root, "slugA/sessdir", "Scout.jsonl", "/home/u/repoA", [{ id: "e3", ts: todayA + HOUR, cost: 0.5 }]); // subagent artifact, one level deeper + // per-agent advisor artifact, two levels deeper (the level that used to be skipped) + fixture(root, "slugA/sessdir/Scout", "__advisor.jsonl", "/home/u/repoA", [{ id: "e9", ts: todayA + HOUR, cost: 0.75 }]); // repoB session: windows yes, repoA attribution no fixture(root, "slugB", "sess2.jsonl", "/home/u/repoB", [{ id: "e4", ts: todayA, cost: 10 }]); // no session header → repo "unknown", never counted toward a named repo fixture(root, "slugC", "sess3.jsonl", null, [{ id: "e8", ts: todayA, cost: 100 }]); - // DAY: today's entries from every repo incl. subagent (e1, e3, e4, e8) — yesterday excluded + // DAY: today's entries from every repo incl. subagent + advisor (e1, e3, e9, e4, e8) — yesterday excluded const snap = scanMoney("repoA", now, root); - // REPO: e1+e2+e5+e6+e7 (+subagent e3) — duplicates counted once, other repos excluded - assert.equal(snap.repo, 1 + 2 + 5 + 0.25 + 5 + 0.5); - // DAY: today's entries from every repo (e1, e4, e8) — yesterday excluded - assert.equal(snap.day, 1 + 0.5 + 10 + 100); - // WEEK (hourFloor − 7d, inclusive): e1,e3,e4,e8 + e2(yesterday) + e5(edge) + // REPO: e1+e2+e5+e6+e7 (+subagent e3, +advisor e9) — duplicates counted once, other repos excluded + assert.equal(snap.repo, 1 + 2 + 5 + 0.25 + 5 + 0.5 + 0.75); + // DAY: today's entries from every repo (e1, e3, e9, e4, e8) — yesterday excluded + assert.equal(snap.day, 1 + 0.5 + 0.75 + 10 + 100); + // WEEK (hourFloor − 7d, inclusive): e1,e3,e9,e4,e8 + e2(yesterday) + e5(edge) // e6 is 1ms past the edge; e7 far out - assert.equal(snap.week, 1 + 0.5 + 10 + 100 + 2 + 5); + assert.equal(snap.week, 1 + 0.5 + 0.75 + 10 + 100 + 2 + 5); // MONTH (hourFloor − 30d): week + e6 - assert.equal(snap.month, 1 + 0.5 + 10 + 100 + 2 + 5 + 0.25); - // subagent share over the 30d window - assert.equal(snap.sub, 0.5); - // unique entries: 8 ids (branch copy deduped, noise/no-usage/malformed skipped) - assert.equal(snap.entries, 8); + assert.equal(snap.month, 1 + 0.5 + 0.75 + 10 + 100 + 2 + 5 + 0.25); + // subagent share over the 30d window (subagent artifact + advisor consult) + assert.equal(snap.sub, 0.5 + 0.75); + // unique entries: 9 ids (branch copy deduped, noise/no-usage/malformed skipped) + assert.equal(snap.entries, 9); } finally { rmSync(root, { recursive: true, force: true }); } From 42dbec19cc1f113efd1fbbbdac9a7c7c010c3731 Mon Sep 17 00:00:00 2001 From: RECTOR Date: Mon, 12 Oct 2026 11:08:29 +0700 Subject: [PATCH 2/2] chore: release 0.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36d2855..710dd99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getpipher/omp-statusline", - "version": "0.8.1", + "version": "0.8.2", "description": "Six-line omp statusline widget: hijri clock, prayer times, API-spend ledger (REPO/DAY/7DAY/30DAY, subagent-inclusive), per-window zai coding-plan quota rows (provider-gated), and an omp/sl/tern version row.", "keywords": [ "pi-package",