diff --git a/packages/pi-coding-agent/src/core/agent-session.ts b/packages/pi-coding-agent/src/core/agent-session.ts index a5dfa2335..285e32516 100644 --- a/packages/pi-coding-agent/src/core/agent-session.ts +++ b/packages/pi-coding-agent/src/core/agent-session.ts @@ -246,6 +246,12 @@ export class AgentSession { private _retryPromise: Promise | undefined = undefined; private _retryResolve: (() => void) | undefined = undefined; + // Cumulative session stats — survives compaction (#1423) + private _cumulativeCost = 0; + private _cumulativeInputTokens = 0; + private _cumulativeOutputTokens = 0; + private _cumulativeToolCalls = 0; + // Bash execution state private _bashAbortController: AbortController | undefined = undefined; private _pendingBashMessages: BashExecutionMessage[] = []; @@ -438,7 +444,13 @@ export class AgentSession { if (event.message.role === "assistant") { this._lastAssistantMessage = event.message; + // Accumulate session stats that survive compaction (#1423) const assistantMsg = event.message as AssistantMessage; + this._cumulativeCost += assistantMsg.usage?.cost?.total ?? 0; + this._cumulativeInputTokens += assistantMsg.usage?.input ?? 0; + this._cumulativeOutputTokens += assistantMsg.usage?.output ?? 0; + this._cumulativeToolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length; + if (assistantMsg.stopReason !== "error") { this._overflowRecoveryAttempted = false; } @@ -3230,17 +3242,17 @@ export class AgentSession { sessionId: this.sessionId, userMessages, assistantMessages, - toolCalls, + toolCalls: Math.max(toolCalls, this._cumulativeToolCalls), toolResults, totalMessages: state.messages.length, tokens: { - input: totalInput, - output: totalOutput, + input: Math.max(totalInput, this._cumulativeInputTokens), + output: Math.max(totalOutput, this._cumulativeOutputTokens), cacheRead: totalCacheRead, cacheWrite: totalCacheWrite, - total: totalInput + totalOutput + totalCacheRead + totalCacheWrite, + total: Math.max(totalInput + totalOutput, this._cumulativeInputTokens + this._cumulativeOutputTokens) + totalCacheRead + totalCacheWrite, }, - cost: totalCost, + cost: Math.max(totalCost, this._cumulativeCost), }; } diff --git a/src/tests/file-watcher.test.ts b/src/tests/file-watcher.test.ts index e8dc7fd00..38040cdc6 100644 --- a/src/tests/file-watcher.test.ts +++ b/src/tests/file-watcher.test.ts @@ -54,10 +54,11 @@ test("settings.json change emits settings-changed event", async () => { const bus = createMockEventBus(); await startFileWatcher(dir, bus); + await delay(200); writeFileSync(join(dir, "settings.json"), JSON.stringify({ updated: true })); // Wait for debounce (300ms) + filesystem propagation - await delay(600); + await delay(800); const matched = bus.events.filter((e) => e.channel === "settings-changed"); assert.ok(matched.length > 0, "should emit settings-changed event"); @@ -68,9 +69,10 @@ test("auth.json change emits auth-changed event", async () => { const bus = createMockEventBus(); await startFileWatcher(dir, bus); + await delay(200); writeFileSync(join(dir, "auth.json"), JSON.stringify({ token: "new" })); - await delay(600); + await delay(800); const matched = bus.events.filter((e) => e.channel === "auth-changed"); assert.ok(matched.length > 0, "should emit auth-changed event"); @@ -81,9 +83,10 @@ test("models.json change emits models-changed event", async () => { const bus = createMockEventBus(); await startFileWatcher(dir, bus); + await delay(200); writeFileSync(join(dir, "models.json"), JSON.stringify({ model: "new" })); - await delay(600); + await delay(800); const matched = bus.events.filter((e) => e.channel === "models-changed"); assert.ok(matched.length > 0, "should emit models-changed event"); @@ -133,7 +136,7 @@ test("debouncing coalesces rapid changes into one event", async () => { for (let i = 0; i < 5; i++) { writeFileSync(join(dir, "settings.json"), JSON.stringify({ i })); } - await delay(600); + await delay(800); const matched = bus.events.filter((e) => e.channel === "settings-changed"); assert.strictEqual(