Merge pull request #1430 from trek-e/fix/1423-session-cost-compaction

fix: accumulate session cost independently of message array (#1423)
This commit is contained in:
TÂCHES 2026-03-19 15:39:19 -06:00 committed by GitHub
commit 364bd5b65b
2 changed files with 24 additions and 9 deletions

View file

@ -246,6 +246,12 @@ export class AgentSession {
private _retryPromise: Promise<void> | 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),
};
}

View file

@ -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(