Skip to content

fix(ui): refresh session usage info on step and usage events - #731

Merged
pascalandr merged 6 commits into
NeuralNomadsAI:devfrom
markerikson:fix/status-tab-session-usage
Sep 21, 2026
Merged

pascalandr merged 6 commits into
NeuralNomadsAI:devfrom
markerikson:fix/status-tab-session-usage

Conversation

@markerikson

@markerikson markerikson commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

The Status tab's INPUT / OUTPUT / COST chips show 0 / 0 / $0.00 for sessions that were empty when opened, and never update while a session runs.

The Status tab reads sessionInfoByInstance, which is only written by updateSessionInfo(). The V2 migration (#647) removed the calls to it from the live SSE handlers, leaving only the message-load and agent/model-switch paths. updateSessionInfo() also summed usage from resident messages only, which in V2 is a bounded window rather than the whole session.

The displayed totals are now the session's lifetime usage, taken from the server's cumulative Session.cost / Session.tokens counters:

  • updateSessionInfo() uses session.cost / session.tokens whenever the server reports them, independent of which messages are loaded. Message sums only stand in when the server sends no session usage.
  • It is called from the session.usage.updated handler (outside the active-transcript gate, so inactive sessions update too), after session.step.ended / session.step.failed projection, and on load for an already-hydrated session that has no info yet.
  • Reverts do not change the displayed totals. OpenCode's projector does not decrement session usage on session.revert.committed, so subtracting client-side was only correct for messages that happened to be resident and was lost on reload. Lifetime totals are the same number in every window and after every reload.

stores/session-usage-contract.test.ts (added to the force-exit CI list) covers: active step completion, usage-only event with an empty transcript, inactive session, initial hydration from the session record, authoritative totals over a partial window, revert with resident messages, revert from an anchored historical window with the newer message not resident, and post-reload hydration after a revert.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES_REQUESTED

The active session.step.ended / session.step.failed projection can refresh the chips, but this does not yet restore the complete V2 usage contract:

  1. session.usage.updated carries the authoritative cumulative cost/tokens, yet the new path does not consume that payload and cannot update inactive sessions or initial hydration. This is the same first-load gap explicitly noted in the PR description.
  2. session.revert.committed removes normalized messages after the new refresh point, so sessionInfoByInstance and thread totals retain the removed usage.
  3. No regression test was added for this central event-ordering path. Please cover active step completion, usage-only/inactive or initial hydration, and a usage-decreasing revert in one of the runnable store suites (for example runtime-contract.test.ts).

I reproduced both stale-state cases locally: an authoritative session with cost 2.5 and tokens 1200/300 still published 0/0/$0, and removing a 10/5/$1 assistant message cleared the message-store usage while the displayed session info remained 10/5/$1.

The test/typecheck jobs and completed builds are green; the Tauri Windows and macOS x64 builds were still running when this review was submitted. No merge performed.

Comment thread packages/ui/src/stores/instances.ts
@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35537978024

Artifacts expire in 7 days.
Artifacts:

  • pr-731-958eef2e2188c79457e80b857282c23a41758813-tauri-macos
  • pr-731-958eef2e2188c79457e80b857282c23a41758813-tauri-windows
  • pr-731-958eef2e2188c79457e80b857282c23a41758813-tauri-linux
  • pr-731-958eef2e2188c79457e80b857282c23a41758813-electron-macos
  • pr-731-958eef2e2188c79457e80b857282c23a41758813-electron-windows
  • pr-731-958eef2e2188c79457e80b857282c23a41758813-tauri-macos-arm64
  • session-pruning-plugin-ubuntu-latest
  • pr-731-958eef2e2188c79457e80b857282c23a41758813-electron-linux
  • session-pruning-plugin-windows-latest
  • session-pruning-plugin-macos-latest

@markerikson
markerikson force-pushed the fix/status-tab-session-usage branch from 958eef2 to a7df40c Compare September 20, 2026 23:35
The V2 migration dropped the live updateSessionInfo() calls from the SSE handlers, so the Status tab's cost/token chips were only written at message load (and stayed at 0 for sessions that were empty when opened). Recompute them after projecting messages for session.step.ended, session.step.failed, session.usage.updated, and forced resyncs.
- fall back to the session record's cost/tokens when no message usage is loaded
- refresh on session.usage.updated for any session, after the record is updated
- refresh after session.revert.committed removes messages
- initialize info for already-loaded sessions on open
- add session-usage-contract.test.ts to the force-exit CI list
@markerikson
markerikson force-pushed the fix/status-tab-session-usage branch from 7d2a4d3 to 1085812 Compare September 20, 2026 23:44

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES_REQUESTED on 10858125.

The new event ordering, inactive-session path, hydration fallback, and runnable regression file address much of the earlier review. One correctness gap still leaves the Status totals dependent on the bounded resident transcript: any single loaded usage entry wins over the authoritative session totals, and an all-message revert falls back to the same non-decrementing native counters. Please address the inline P1 and cover both boundaries. No merge performed.

Comment thread packages/ui/src/stores/message-v2/session-info.ts Outdated
…parately

The loaded transcript is a bounded window, so message sums cannot stand in
for the session totals when the server reports them. Reverted messages are
recorded as a separate adjustment because the server's usage counters do
not decrement on session.revert.committed.
@markerikson

Copy link
Copy Markdown
Contributor Author

Addressed in 489e8a9c:

  • session-info.ts: when the session record carries tokens, the server's cumulative cost/tokens are the base totals regardless of which messages are resident. Message sums only stand in when the server reports no session usage.
  • Reverts are tracked as a separate adjustment (recordRevertedUsage): the usage entries of the removed messages are recorded before removal and subtracted from the authoritative totals, clamped at zero. A later session.usage.updated with the same non-decremented counters keeps the adjustment. Cleared on instance removal.
  • New tests in session-usage-contract.test.ts: native totals $20 with one resident $1 message shows $20; revert of the only message shows 0; revert followed by an unchanged usage.updated stays lowered; message-sum fallback when no session usage exists. Both new boundary cases fail without the change; 13/13 pass with it.

Known limit: session.revert.committed carries only { sessionID, to }, so the adjustment can count only reverted messages that were resident in the loaded window, and it is in-memory (a reload shows the server's lifetime counters until the next revert). Both would need the server to either decrement its counters or include the removed usage in the event.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES_REQUESTED on 489e8a9c.

The two boundaries from the previous review are fixed, and the submitted usage tests pass. However, the acknowledged limitations are normal CodeNomad flows and leave the same displayed totals with different values depending on transcript residency and renderer lifetime:

  1. Bounded historical window: with native totals 50 / 25 / $5, resident boundary message m1 = 10 / 5 / $1, and newer m2 = 40 / 20 / $4 outside the resident window, committing the revert to m1 leaves 40 / 20 / $4 instead of zero. removed can only enumerate resident records.
  2. Reload after revert: reverting the only 10 / 5 / $1 message shows zero until the renderer state is rebuilt; hydration then restores 10 / 5 / $1 from OpenCode's persisted cumulative counters because the module-local adjustment is gone.

OpenCode 2.0.11 deleting messages without decrementing SessionInfo.cost/tokens explains the constraint, but it does not make the inconsistent result safe to ship. Please choose one stable contract: either lifetime usage (never subtract on revert) or current-transcript usage backed by complete, reload-reconstructible authority. Add regressions for a revert from an anchored historical window and post-reload hydration.

Local validation: UI typecheck passed; 81/81 relevant native-event, request-authority, and usage tests passed; both additional boundary reproductions failed as described. No merge performed.

Comment thread packages/ui/src/stores/instances.ts Outdated
Comment thread packages/ui/src/stores/message-v2/session-info.ts Outdated
- session totals come from the server's cumulative usage counters and are
  not adjusted on revert, so the display matches across windows and reloads
- tests cover revert from an anchored window and post-reload hydration
@markerikson

Copy link
Copy Markdown
Contributor Author

Switched to the lifetime-usage contract in 761968a2.

  • updateSessionInfo() now uses session.cost / session.tokens as-is whenever the server reports them. The client-side revert adjustment (revertedUsage map and the revert-handler bookkeeping) is removed, so the displayed number no longer depends on which messages are resident or on renderer lifetime.
  • Both reproductions from the review are now tests in session-usage-contract.test.ts: a revert from an anchored window with only the $1 boundary message resident keeps showing the $5 native total, and reverting the only message then re-hydrating in a fresh setup shows the same totals before and after.
  • Message sums still stand in only when the server sends no session usage at all.

PR description updated to match. UI typecheck clean; the usage, runtime-contract, and native-events suites pass (22/22) under the force-exit command.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new lifetime-usage contract resolves the two production inconsistencies from the prior review: anchored-window reverts and renderer reloads now intentionally retain the same native cumulative totals. One focused regression-test flaw remains before signoff; the current reload case never discards the first in-memory session-info value. The known system-message timeouts are ignored per project-owner direction.

Comment thread packages/ui/src/stores/session-usage-contract.test.ts Outdated
@markerikson

Copy link
Copy Markdown
Contributor Author

Fixed the reload test in 16d70b9f: the re-hydration now runs under a fresh instance id (removeInstance() leaves sessionInfoByInstance entries in place), asserts the info is absent and the transcript empty before loadMessages(), then checks the hydrated record. I confirmed it now tracks the persisted record by temporarily changing the second record to 90 / 45 / $9: the test fails with those values and passes with the matching ones.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @markerikson !

@pascalandr
pascalandr merged commit 0fa18c0 into NeuralNomadsAI:dev Sep 21, 2026
12 of 14 checks passed
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.

2 participants