summaryrefslogtreecommitdiffhomepage
path: root/packages/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/api')
-rw-r--r--packages/api/src/agent-manager.ts13
-rw-r--r--packages/api/tests/agent-manager.test.ts39
2 files changed, 51 insertions, 1 deletions
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;
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts
index 95fb558..6d7d66f 100644
--- a/packages/api/tests/agent-manager.test.ts
+++ b/packages/api/tests/agent-manager.test.ts
@@ -104,6 +104,13 @@ function resetAppendChunksCalls(): void {
appendChunksCalls.length = 0;
}
+// Seedable return value for the mocked getUsageStatsForTab — what the backend
+// reads (post-write) to attach to the `turn-sealed` event.
+const fakeUsageStatsByTab = new Map<string, unknown>();
+function resetFakeUsageStats(): void {
+ fakeUsageStatsByTab.clear();
+}
+
// Allow tests to swap in a custom `run` generator (e.g. to simulate
// a fallback failure mid-stream). Returning to undefined restores
// the default.
@@ -371,6 +378,9 @@ vi.mock("@dispatch/core", () => ({
getMessagesForTab(tabId: string) {
return fakeMessagesByTab.get(tabId) ?? [];
},
+ getUsageStatsForTab(tabId: string) {
+ return fakeUsageStatsByTab.get(tabId) ?? null;
+ },
appendEventToChunks: appendEventToChunksSpy,
applySystemEvent(_messages: unknown[], _event: unknown) {
return { messageId: "mock-system-msg" };
@@ -421,6 +431,7 @@ describe("AgentManager", () => {
setRunImpl(null);
appendEventToChunksSpy.mockClear();
resetAppendChunksCalls();
+ resetFakeUsageStats();
});
it("initial status is idle", () => {
@@ -1402,6 +1413,34 @@ describe("AgentManager", () => {
});
});
+ it("attaches the DB usage aggregate to the turn-sealed event for live reconciliation", async () => {
+ const manager = new AgentManager();
+ const aggregate = {
+ inputTokens: 222,
+ outputTokens: 22,
+ cacheReadTokens: 100,
+ cacheWriteTokens: 5,
+ requests: 1,
+ last: { inputTokens: 222, outputTokens: 22, cacheReadTokens: 100, cacheWriteTokens: 5 },
+ };
+ fakeUsageStatsByTab.set("tab-sealed-usage", aggregate);
+
+ const events: AgentEvent[] = [];
+ manager.onEvent((event) => {
+ events.push(event);
+ });
+
+ await manager.processMessage("tab-sealed-usage", "go");
+
+ const sealed = events.find((e) => e.type === "turn-sealed") as
+ | Extract<AgentEvent, { type: "turn-sealed" }>
+ | undefined;
+ expect(sealed).toBeDefined();
+ // The aggregate read AFTER the write is carried on the event so the
+ // frontend can REPLACE its live cacheStats with the DB truth.
+ expect(sealed?.usageStats).toEqual(aggregate);
+ });
+
it("emits usage rows in the SAME appendChunks call as the turn's content (one atomic write)", async () => {
const manager = new AgentManager();
setRunImpl(async function* () {