From 48c120e5cd400b2e2b8afae0afcc7c8bc4d2ccb4 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Tue, 2 Jun 2026 13:34:33 +0900 Subject: fix: reconcile live cacheStats to DB truth on turn-sealed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the live-accumulator overshoot a Gemini review surfaced: the frontend adds every streamed usage event to cacheStats, but a rate-limited fallback attempt's usage is discarded server-side (never persisted). Live numbers overshot until a reload re-seeded from the DB aggregate. Fix: turn-sealed (emitted AFTER the atomic usage-row write) now carries the authoritative getUsageStatsForTab aggregate. The store REPLACES (not adds) cacheStats with it every turn — landing the just-sealed turn's usage AND self-healing any live drift, including the discarded-fallback overshoot. No extra round-trip (piggybacks turn-sealed); idempotent in the happy path. - core: add UsageStats type; getUsageStatsForTab returns it; turn-sealed gains optional usageStats field. - api: agent-manager reads getUsageStatsForTab post-flush and attaches it to the turn-sealed emit (try/catch: omit on DB error). - frontend: turn-sealed handler replaces cacheStats (undefined ⇒ untouched back-compat; null ⇒ clear). Tests: frontend reconcile/self-heal/back-compat/null-clear; api turn-sealed carries aggregate. 509 -> 514 passing; typecheck + biome green. --- packages/api/src/agent-manager.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'packages/api/src') diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts index 1db9a04..9d7300a 100644 --- a/packages/api/src/agent-manager.ts +++ b/packages/api/src/agent-manager.ts @@ -35,6 +35,7 @@ import { getMessagesForTab, getSetting, getTab, + getUsageStatsForTab, listOpenTabs, loadAgent, loadAgents, @@ -55,6 +56,7 @@ import { toAvailableSubagents, toAvailableUserAgents, type UsageData, + type UsageStats, validateConfig, } from "@dispatch/core"; import type { PermissionManager } from "./permission-manager.js"; @@ -1639,7 +1641,16 @@ export class AgentManager { // above). Signal the frontend that the turn's rows — with real seqs — are // durable so it can fold its live representation into the sealed log. // Emitted AFTER status:idle/error (which fire before the DB write). - this.emit({ type: "turn-sealed", turnId }, tabId); + // Carry the authoritative usage aggregate (read AFTER the usage rows were + // persisted) so the frontend reconciles its live cacheStats to the DB truth + // — self-healing the live overshoot from a discarded rate-limited attempt. + let usageStats: UsageStats | null = null; + try { + usageStats = getUsageStatsForTab(tabId); + } catch { + // DB read failed — omit reconciliation rather than crash the turn. + } + this.emit({ type: "turn-sealed", turnId, usageStats }, tabId); // Turn fully settled — clear the shared turn id. tabAgent.currentTurnId = null; -- cgit v1.2.3