Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<repo-slug>/<session-id>/<Agent>.jsonl`) and are scanned too;
- **subagent and advisor spend is included**: subagent transcripts persist as session-format JSONL artifacts (`<repo-slug>/<session-id>/<Agent>.jsonl`) and advisor consults as `__advisor.jsonl` — beside the session (`<session-id>/__advisor.jsonl`) and nested per agent (`<session-id>/<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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
47 changes: 34 additions & 13 deletions src/money.ts
Original file line number Diff line number Diff line change
@@ -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 (<session-dir>/<Agent>.jsonl artifacts carry usage), and every
// covers subagent spend (<session-dir>/<Agent>.jsonl artifacts carry usage) and advisor
// consults (<session-dir>/__advisor.jsonl, and per-agent <session-dir>/<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";
Expand All @@ -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.
Expand Down Expand Up @@ -77,6 +78,33 @@ function parseSessionFile(path: string, sub: boolean, out: CostEntry[], seen: Se
}
}

// Session artifact directories hold *.jsonl at their own level (subagent <Agent>.jsonl,
// main __advisor.jsonl) and per-agent subtrees one level deeper (<Agent>/__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<string>): 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(),
Expand All @@ -103,16 +131,9 @@ export function scanMoney(
// main session file: <repoSlug>/<ts-id>.jsonl
parseSessionFile(join(repoDir, f.name), false, entries, seen);
} else if (f.isDirectory()) {
// subagent artifacts: <repoSlug>/<session-dir>/*.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: <repoSlug>/<session-dir>/ — subagent <Agent>.jsonl,
// main/per-agent __advisor.jsonl (nested one level deeper)
scanSessionDir(join(repoDir, f.name), entries, seen);
}
}
}
Expand Down
26 changes: 14 additions & 12 deletions test/money.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
Loading