Skip to content

fix: parse bare-today headline in aggregate mode - #1503

Open
iliaal wants to merge 13 commits into
getagentseal:mainfrom
iliaal:fix/overview-aggregate-mode
Open

iliaal wants to merge 13 commits into
getagentseal:mainfrom
iliaal:fix/overview-aggregate-mode

Conversation

@iliaal

@iliaal iliaal commented Sep 19, 2026

Copy link
Copy Markdown

Fixes #1505 (stacked on #1509; restacked onto the split stack).

Stacks on #1506 (review that first; this diff includes it until it merges). Fixes the remaining codeburn overview -p today heap crash at the parse layer.

What was wrong

After the load-phase bounds from #1502, the same-day parse still retained ~1-2GB: every provider's full turns co-resident through merge, day aggregation, and rendering. Capped at 512MB the command still exited 134; the heap floor measured between 512MB and 1024MB.

What changed

  • New opt-in stripForAggregate parse mode: per-call payloads the headline never reads (tool sequences, dedup keys, per-call paths, bash/skill/mcp/subagent arrays) are stripped per session inside buildSessionSummary the moment it resolves (in place, single-owner — the shared memo is bypassed both ways, so peak never holds more than one session's full turns).
  • Shell commands are extracted first (call.commands); PR launch matching reads them through a derivation-equivalence-tested helper. userMessage is emptied and replaced with a precomputed 160-char promptPrefix (the full text averaged ~115B/turn but totaled ~40MB serialized / ~100MB heap); the matcher reads the prefix first with the userMessage fallback, so attribution is identical.
  • The lock reload is skipped when the envelope nonce is unchanged: every saveCache mints a fresh nonce, so an unchanged nonce proves byte-identical shards and re-streaming them only doubled peak heap for zero new data. Any publication, torn read, or missing envelope keeps the reload (pinned by parser-cache-refresh-reload-skip.test.ts).
  • Engages only for the narrow shape that needs no session payloads (today-only, all-provider, no project filters or day selection); every other caller keeps full sessions.

The detach-only-retained-strings and size-gate commits now live down the stack in #1506, so no step here regresses on its own.

Verification

  • overview -p today completes with real data (was: FATAL ERROR: JavaScript heap out of memory, exit 134); lite-parity suite proves byte-identical rendered output and equal totals/days/models/categories on an isolated corpus.
  • Uncapped lite vs full on the live corpus: identical row structure, mix-consistent (frozen claude rows, growing omp rows).
  • Full suite + locks green, tsc clean.

Timings (warm cache, medians of 3, 1.2GB shard corpus with one 697MB shard)

command main #1506 (base of stack) this PR (stack top)
overview -p today 13.8s / 3.1GB 17.6s / 2.7GB 26.5s / 1.8GB
report --period today --format json 13.2s / 2.5GB 17.3s / 2.6GB 26.6s / 1.6GB

The strip trades ~9s for ~1GB less peak on this corpus (the lite sessions replace full turns through merge/aggregate/render). report takes the full-session path and still benefits from the gate plus the skipped reload below it.

Notes for review

  • The strip is a denylist (toolSequence/spawn ids/paths/name arrays/dedup key out, everything else spread through), so future ParsedApiCall fields stay populated.
  • Single gate: main.ts requests lite unconditionally for overview; buildDurablePeriod is the only decider (today-only, all-provider, no filters/selection).
  • serve dispatches overview in-process, so the strip also runs in the resident desktop/menubar process, not only one-shot CLI runs. Verified: a lite overview -p today followed by a full report --period today --format json in one serve --stdio process returns identical full data (memo bypass is per-parse, not process-global).

@iamtoruk

Copy link
Copy Markdown
Member

The parse-layer work is good and the numbers are clean.

On a frozen copy of a real corpus I diffed the full JSON export leaf by leaf against main for today, week, 30days, month and all: zero differences from this PR. The single all-period difference (shellCommands "ls" 6300 vs 6299) is the branch predating main's antigravity extractBashCommands change, and it disappears with that one line applied. No contamination either: a cache directory that served three lite overview -p today runs produces identical full reports afterwards, and inside one serve --stdio process a lite overview -p today followed by a full report --period today --format json returns identical full data. Suite green (4336 passed), locks 36/36, tsc clean.

The memory result is real: overview -p today runs at a 128MB heap cap where main needs more than 256MB.

Before merge:

  1. Blocked on fix: bound session-shard memory for ranged queries #1506, which does not compile on its own. See the review there.
  2. Please replace the allowlist in stripCallForAggregate with a denylist. It is complete today, but it is 31 hand-copied fields, and the next optional field added to ParsedApiCall becomes undefined on the today headline with no compile error. Destructure out toolSequence and spawnToolUseIds, spread the rest, then override the emptied arrays. Shorter, and it cannot silently drop a money field.
  3. Say in the PR body that serve dispatches overview in-process, so the strip runs in the resident desktop and menu bar process, not only in one-shot CLI runs. It behaves correctly there (tested), but the PR reads as CLI-only.
  4. The narrowToday gate is computed in main.ts and re-derived in buildDurablePeriod. One is enough.
  5. Timing: warm overview -p today goes from 2s to 14s here. Part is fix: bound session-shard memory for ranged queries #1506's streaming, part is the memo bypass making today's sources parse twice (the daily backfill parses them in full, then the lite pass cannot reuse it). Please say which is which, with numbers.
  6. Same rebase and indentation points as fix: bound session-shard memory for ranged queries #1506. Please rebase after 0.9.25 lands.

Session month shards decode incrementally (one turn at a time,
keeping only in-range turns with the exact kept/dropped/carry
contract); shard loads run serially with the pre-lock snapshot
released before the canonical reload. Out-of-range turns
contribute raw dedup keys to cross-file suppression markers;
retained strings detach from tokenizer buffers. The codex result
cache rewrite, digest wiring, and aggregate parse mode follow as
stacked PRs.
@iliaal
iliaal force-pushed the fix/overview-aggregate-mode branch from 2682736 to 702bd19 Compare September 20, 2026 02:10
@iamtoruk

Copy link
Copy Markdown
Member

The memory result is real and the output is exact. Thank you.

overview -p today goes from 1006MB to 383MB max RSS and report --period today from 1007MB to 576MB. The overview text is byte-identical to main for today, week, month, all and today with --provider claude, including the shape that engages the strip. The full JSON leaf diff against #1509 is identical on every period, cold and warm.

I re-checked the three hazards. In one resident serve --stdio: overview today, then the full today report, then overview again, then the full week report. Both full reports are leaf-identical to main's standalone exports. A cache that served three lite overview runs then produced a full all report identical to a full-only cache. No rangeFiltered or droppedKeys marker reaches any shard on disk. All pass. The denylist is right, and letting buildDurablePeriod be the single decider is cleaner than what I suggested.

Still open:

  1. The PR body says "userMessage stays (bounded, needed by prompts/corrections)", but commit 8ca6235 empties it and replaces it with a 160-char promptPrefix (src/parser.ts:1795). The behaviour is correct; the body says the opposite of what the code does.
  2. src/parser.ts:5688 ends mid-sentence again ("Never a correctness gate: on any"). The closing line is present on main, fix: bound session-shard memory for ranged queries #1506, fix: stream the codex result cache instead of whole-parsing #1508 and fix: hash cross-file dedup keys with SHA-256 instead of raw strings #1509; this PR's own diff deletes it.
  3. The timing split. What I measure, warm, medians of 3, report today: main 1.83s, fix: bound session-shard memory for ranged queries #1506 21.19s, fix: stream the codex result cache instead of whole-parsing #1508 23.08s, fix: hash cross-file dedup keys with SHA-256 instead of raw strings #1509 23.14s, this PR 13.37s. overview today: main 1.13s, fix: bound session-shard memory for ranged queries #1506 10.94s, this PR 8.21s. So nearly all the cost is fix: bound session-shard memory for ranged queries #1506's streaming and this PR gives about 8 seconds back through 1b30177. Even at the top of the stack it is still 7x main on the path the desktop app polls, which we cannot take. The size gate I asked for on fix: bound session-shard memory for ranged queries #1506 is the fix, and 1b30177 belongs down there.

Conflicts for the rebase. Against main: CHANGELOG.md, src/codex-cache.ts, src/parser.ts, src/providers/types.ts, src/session-cache.ts. Against 0.9.25 you also pick up src/providers/warp.ts and src/usage-aggregator.ts, and the usage-aggregator one is semantic: 0.9.25 changes provider exclusion through buildDurablePeriod, right where your stripForAggregate gate goes. Please rebase after 0.9.25 lands. Nothing in the stack bumps a cache version or changes a cache filename, so that part is safe.

Per-string flattening at assembly copied every string of every turn
(kept or dropped): ~829k turns x dozens of strings of Buffer + string
churn that GC could not retire under a heap cap, dying in the sept
shard at ~500MB live. streamShardArrayField takes detachStrings
(default true); the filtered loader passes false and flattens only
what it keeps (kept turns + meta at fold time, carry refs + first
project in finishSlice).

(cherry picked from commit 1b30177)
The streaming walk costs 15-20s where JSON.parse needs milliseconds
(525MB of shards in ~11s on this corpus); it exists for the one
shard past V8's max string length. Shards at or under 256MB now take
plain JSON.parse through the same per-record projection
(filterShardFile / validateCachedFile); larger ones stream exactly
as before. A single readFile is already an atomic snapshot, so the
small path needs no fingerprint guard. Byte-identical results on
both paths are pinned by the size-gate parity suite (kept, sliced,
PR-whole, key-overlap, all-dropped, invalid).
The single result file decodes entry by entry; discovery labels
come from calls-free metadata; publishes merge dirty entries;
single-flight loads are shared and retry on mid-decode publish,
with a global clear epoch.
@iliaal
iliaal force-pushed the fix/overview-aggregate-mode branch from bb2415b to bfe4f62 Compare September 20, 2026 14:04
…reaming

Same size gate as session shards (SHARD_STREAM_GATE_BYTES now lives
in shard-stream.ts, shared by both): the timestamp scan reuses the
exported retainCodexEntry rule per entry so the paths cannot drift,
and the entry load applies the same retain rule in memory. The
streaming mid-flush rendezvous test pins the stream path explicitly.
Parity between paths is pinned by the new codex size-gate test.
Dropped-key markers and shared dedup sets store 32-character
SHA-256 digests (~140MB of raw key characters on the reporter
corpus); every insert and lookup hashes uniformly. The set is a
plain class so unadapted consumers fail at compile time;
Antigravity RPC conversations ride along as paired bare keys.
overview -p today (all-provider, unfiltered) strips per-call
payloads the headline never reads (tool sequences, dedup keys,
per-call paths, name arrays; shell commands extracted first
for PR launch matching) per provider right after parsing, so
peak tracks one provider instead of the corpus. Denylist strip,
single narrow-shape gate in the durable builder, memo bypassed
both ways. Byte-identical output proven by lite-parity suite.
project/prLinks ride on calls outside the ParsedApiCall type and
survive a pure type-keyed denylist; they are heavy, so they drop
explicitly. Covered by extended strip assertions.
Tool-name strings like 'Bash' repeat ~1.5M times as distinct
objects (~90MB heap); the tool breakdown is precomputed before
stripping and the edit-time median is unrendered here. Model,
provider, speed, category, and project names intern to one
shared ref per provider batch.
userMessage averages ~115B per turn but totals ~40MB serialized
(~100MB heap); launch matching needs only the first qualifying
160-char prefix. Precompute turn.promptPrefix at strip time and
drop the full text; the matcher reads the prefix first with the
userMessage fallback, so attribution is identical. Correction
scans tolerate empty messages (their stats are unrendered here).
Per-provider stripping left the provider's full summaries resident
through its whole parse (death in early provider parsing at cap).
buildSessionSummary now strips each session the moment it resolves
under an AsyncLocalStorage-gated per-parse store (safe for
concurrent mixed parses), so peak never holds more than one
session's full turns.
A complete-cache refresh streamed every in-scope shard twice: once
before lock acquisition and once after. The pre-lock snapshot stays
referenced until the reload replaces it, so peak held two filtered
copies plus a 730MB shard transient and died near 500MB on overview
-p today. Every saveCache mints a fresh envelope nonce, so an
unchanged nonce proves byte-identical shards and the reload is
skipped; any publication, torn read, or missing envelope keeps the
reload.
@iliaal
iliaal force-pushed the fix/overview-aggregate-mode branch from bfe4f62 to 4dc5436 Compare September 20, 2026 14:33
@iliaal

iliaal commented Sep 20, 2026

Copy link
Copy Markdown
Author

Body fixed: userMessage is emptied, matcher reads the precomputed 160-char promptPrefix first with fallback. The dropped comment closing line is restored (folded into the commit that dropped it). 1b30177 is out of this stack (lives in 1506 now). Timings in body: strip trades about 9s for about 1GB less peak. Full suite plus locks green. Rebase after 0.9.25; noted the warp and usage-aggregator semantic conflicts for then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

overview -p today still OOMs at 512MB after cache-load bounds (parse retention)

2 participants