Summary
Agent usage is currently written from both the main-process chunk sink and the renderer IPC path. The ledger attempts to deduplicate those writes by comparing a new event only with the immediately preceding event within 250 ms.
That is not concurrency-safe. With two interleaved runs, the ordering can be:
- main A
- main B
- renderer A
- renderer B
Neither renderer copy matches the last ledger entry, so both duplicates are retained.
The main writer also derives projectId from the globally active project at record time. A background run that finishes after the user switches projects can therefore be attributed to the wrong project.
Finally, opening the Usage view calls loadAllProjectThreads(), folding every message in every configured project even though aggregation only consumes per-thread usage metadata. The event array is also cloned and rewritten in full for every event.
Relevant code
src/main/services/agent-chunk-sink.ts
src/main/services/storage/usage-ledger.ts
src/renderer/controller/agent.ts
src/shared/usage/usage-event.ts
src/shared/usage/aggregate-usage.ts
src/main/services/thread-store.ts
Plan
-
Choose one authoritative writer
- Record agent-stream usage in the main process only.
- Keep renderer IPC only for genuinely renderer-originated legacy sources, or remove it if no such source remains.
-
Add stable identity
- Give each usage delta a run-scoped/event-scoped idempotency key.
- Deduplicate by that key rather than timestamp adjacency.
- Persist the owning
projectId and threadId from the run context, never from activeProjectId.
-
Make storage scale with events
- Use an append-oriented ledger with bounded compaction, or a small indexed store.
- Maintain daily/model/project aggregates if needed for fast views.
- Keep the existing 90-day retention behavior explicit and testable.
-
Aggregate from metadata
- Read thread catalog/meta usage fields without folding messages, tool results, images, or subagents.
- Prefer a project-level usage projection if repeatedly scanning metadata is still material.
-
Cover concurrency
- Add deterministic tests with interleaved usage chunks from multiple simultaneous runs and a project switch during a run.
Acceptance criteria
Related
Summary
Agent usage is currently written from both the main-process chunk sink and the renderer IPC path. The ledger attempts to deduplicate those writes by comparing a new event only with the immediately preceding event within 250 ms.
That is not concurrency-safe. With two interleaved runs, the ordering can be:
Neither renderer copy matches the last ledger entry, so both duplicates are retained.
The main writer also derives
projectIdfrom the globally active project at record time. A background run that finishes after the user switches projects can therefore be attributed to the wrong project.Finally, opening the Usage view calls
loadAllProjectThreads(), folding every message in every configured project even though aggregation only consumes per-thread usage metadata. The event array is also cloned and rewritten in full for every event.Relevant code
src/main/services/agent-chunk-sink.tssrc/main/services/storage/usage-ledger.tssrc/renderer/controller/agent.tssrc/shared/usage/usage-event.tssrc/shared/usage/aggregate-usage.tssrc/main/services/thread-store.tsPlan
Choose one authoritative writer
Add stable identity
projectIdandthreadIdfrom the run context, never fromactiveProjectId.Make storage scale with events
Aggregate from metadata
Cover concurrency
Acceptance criteria
loadAllProjectThreadsor read message/blob contents.Related