Skip to content

fix: keep session token/cost counters monotonic across compaction - #557

Open
synth-mania wants to merge 1 commit into
agegr:mainfrom
synth-mania:fix/compaction-token-counters
Open

fix: keep session token/cost counters monotonic across compaction#557
synth-mania wants to merge 1 commit into
agegr:mainfrom
synth-mania:fix/compaction-token-counters

Conversation

@synth-mania

@synth-mania synth-mania commented Aug 20, 2026

Copy link
Copy Markdown

Disclosure: This PR was authored autonomously by pi (the coding agent) via the pi-web webui, on behalf of synth-mania.

Problem

The top-bar tokens in/out (and cache read/write) counters — and the cost figure — appear to be reset by pi-web's compaction process. After a session compacts, the numbers visibly drop.

Root cause

Those counters are computed in hooks/useAgentSession.ts by summing usage over the current context messages only. When compaction finishes, the client calls loadSession(), which rebuilds the message list through the SDK's buildContextEntries(). That function drops the summarized pre-compaction entries (they stay in the JSONL file but are excluded from context). So the summed token/cost counters collapse to the post-compaction usage only.

The SDK's own AgentSession.getSessionStats() does the opposite and correct thing: it aggregates over all session-file entries, including the usage recorded on compaction/branch-summary entries — because compaction only appends a summary entry and never deletes history.

Fix

Make the reported stats use the same all-entries aggregation the SDK uses, while still tracking live streaming usage:

  • lib/session-stats.ts (new): computeSessionStats(entries) mirrors the SDK's aggregation — assistant + tool-result message usage, plus usage on compaction and branch_summary entries.
  • app/api/sessions/[id]/route.ts: GET now returns a cumulative stats block computed over all entries.
  • hooks/useAgentSession.ts: the per-message live sum is merged with the file stats using a per-field max(). This keeps counters monotonic across compaction, page reloads, and branch navigation, and still reflects in-flight streaming usage before it's persisted.
  • lib/types.ts: shared AgentUsage type; adds usage? to compaction/branch-summary/tool-result entries.

Because session entries are only ever appended, max() guarantees the counters can only ever increase.

Verification

  • New unit tests in lib/session-stats.test.mjs (6), including a monotonicity check against the SDK's real buildContextEntries.
  • Full suite: 592 pass. (One unrelated pre-existing failure in web-auth.test.mjs — it depends on PI_WEB_PASSWORD being unset, which isn't true in my env; it fails identically on a clean tree.)
  • tsc --noEmit and eslint . clean.
  • Real-world check on an actually-compacted session: full-file stats (4.14M tokens / $0.0086) ≥ post-compact context only (1.82M / $0.003), monotonic ✓.

The top-bar token (in/out/cache) and cost counters were computed by
summing usage over the current context messages only. After compaction,
pi's buildContextEntries() drops the summarized pre-compaction entries,
so loadSession() replaced the message list and the counters visibly
reset (and cost dropped with them).

The SDK's own getSessionStats() instead aggregates over ALL session-file
entries (including usage recorded on compaction/branch-summary entries),
because compaction only appends a summary — it never deletes history.

- lib/session-stats.ts: computeSessionStats() mirrors that all-entries
  aggregation (assistant + tool-result messages, compaction and branch
  summary usage).
- GET /api/sessions/[id] now returns a cumulative `stats` block computed
  over all entries.
- useAgentSession merges file stats into the live per-message sum with a
  per-field max(), so counters only ever increase — across compaction,
  page reloads, and branch navigation — while still tracking live
  streaming usage before it is persisted.
- types.ts: shared AgentUsage type; usage? on compaction/branch-summary/
  tool-result entries.

Adds unit tests (incl. a monotonicity check against the SDK's real
buildContextEntries) and a real-world check on a compacted session.
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.

1 participant