Skip to content

Commit 6387f15

Browse files
raymondginger2018-sudoRaymo
authored andcommitted
fix(core): auto-compaction never fires because activeTokens uses single-response total
activeTokens was set to getTotalTokens(responseUsage), i.e. the token count of only the latest response. Since each individual call stays below the autoCompactWindow threshold, session.activeTokens never crosses it and auto-compaction never triggers. Long sessions keep resending their full history on every turn, causing runaway token usage. Fix: - activeTokens now accumulates: (entry.activeTokens ?? 0) + getTotalTokens(responseUsage) - reset activeTokens to 0 after compaction so it does not re-trigger immediately Add regression tests (auto-compact.test.ts) and update the two existing session tests that asserted the old single-response behavior.
1 parent d489e0c commit 6387f15

3 files changed

Lines changed: 71 additions & 4 deletions

File tree

packages/core/src/session.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,7 +1567,7 @@ ${agentInstructions}
15671567
toolCalls,
15681568
usage: accumulateUsage(entry.usage, responseUsage),
15691569
usagePerModel: accumulateUsagePerModel(entry.usagePerModel, model, responseUsage),
1570-
activeTokens: getTotalTokens(responseUsage),
1570+
activeTokens: (entry.activeTokens ?? 0) + getTotalTokens(responseUsage),
15711571
status: "ask_permission",
15721572
failReason: null,
15731573
askPermissions: permissionPlan.askPermissions,
@@ -1593,7 +1593,7 @@ ${agentInstructions}
15931593
toolCalls,
15941594
usage: accumulateUsage(entry.usage, responseUsage),
15951595
usagePerModel: accumulateUsagePerModel(entry.usagePerModel, model, responseUsage),
1596-
activeTokens: getTotalTokens(responseUsage),
1596+
activeTokens: (entry.activeTokens ?? 0) + getTotalTokens(responseUsage),
15971597
status: refusal ? "failed" : waitingForUser ? "waiting_for_user" : toolCalls ? "processing" : "completed",
15981598
failReason: refusal ? refusal : entry.failReason,
15991599
askPermissions: undefined,
@@ -1706,7 +1706,8 @@ ${agentInstructions}
17061706
...entry,
17071707
usage: accumulateUsage(entry.usage, responseUsage),
17081708
usagePerModel: accumulateUsagePerModel(entry.usagePerModel, model, responseUsage),
1709-
activeTokens: getTotalTokens(responseUsage),
1709+
// 压缩后上下文已精简, 重置 activeTokens 避免立即再次触发压缩
1710+
activeTokens: 0,
17101711
updateTime: now,
17111712
}));
17121713

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { getCompactPromptTokenThreshold } from "../session";
4+
5+
/**
6+
* Regression test for auto-compaction bug:
7+
* activeTokens was set to the *single response* total_tokens instead of a
8+
* running counter. Because each individual call stays below the threshold,
9+
* auto-compaction never fired, so long sessions kept resending their full
10+
* history on every turn and blew up token usage.
11+
*
12+
* The fix (session.ts):
13+
* activeTokens = (entry.activeTokens ?? 0) + getTotalTokens(responseUsage)
14+
* → running context counter that grows with each response
15+
* activeTokens = 0 after compaction
16+
* → reset so it does not re-trigger immediately on the next turn
17+
*/
18+
describe("auto-compact activeTokens accumulation", () => {
19+
it("accumulated activeTokens eventually crosses the threshold (buggy single-response never does)", () => {
20+
const threshold = getCompactPromptTokenThreshold("deepseek-v4-flash");
21+
// Each single response is far below the threshold
22+
const singleTotal = 30_000;
23+
assert.ok(singleTotal < threshold, "single response must stay under threshold");
24+
25+
// Buggy behavior: activeTokens = getTotalTokens(responseUsage) — single response, never crosses
26+
const buggyActive = singleTotal;
27+
// Fixed behavior: activeTokens = previous + responseTotal (running counter)
28+
let activeTokens = 0;
29+
let fixedCrossed = false;
30+
31+
// Simulate many turns in one long session
32+
for (let i = 0; i < 50; i += 1) {
33+
activeTokens += singleTotal;
34+
if (activeTokens > threshold) {
35+
fixedCrossed = true;
36+
activeTokens = 0; // reset after compaction
37+
}
38+
}
39+
40+
assert.equal(fixedCrossed, true, "fixed logic must trigger compaction at some point");
41+
assert.ok(buggyActive < threshold, "buggy single-response activeTokens stays under threshold forever");
42+
});
43+
44+
it("resetting activeTokens after compaction prevents immediate re-trigger", () => {
45+
const threshold = getCompactPromptTokenThreshold("deepseek-v4-flash");
46+
const bigTotal = 120_000;
47+
let activeTokens = 0;
48+
let compactions = 0;
49+
50+
for (let i = 0; i < 100; i += 1) {
51+
activeTokens += bigTotal;
52+
if (activeTokens > threshold) {
53+
compactions += 1;
54+
activeTokens = 0; // reset after compaction
55+
}
56+
}
57+
58+
assert.ok(compactions >= 1, "should have compacted at least once");
59+
// Reset prevents runaway: with 120k/call and ~524k threshold, at least 4 calls
60+
// must pass before the next compaction. 100 calls → at most ~25 compactions,
61+
// and never one on every call.
62+
const maxBounded = Math.ceil(100 / 4);
63+
assert.ok(compactions <= maxBounded, `compactions=${compactions} should be bounded by ${maxBounded}`);
64+
});
65+
});

packages/core/src/tests/session.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3260,7 +3260,8 @@ test("SessionManager accumulates response usage while active tokens track the la
32603260
const session = manager.getSession(sessionId);
32613261
const usage = session?.usage as Record<string, any>;
32623262
const usagePerModel = session?.usagePerModel?.["test-model"] as Record<string, any>;
3263-
assert.equal(session?.activeTokens, 27);
3263+
// activeTokens 现在跟踪累计 total_tokens (修复: 之前是单次响应的 27)
3264+
assert.equal(session?.activeTokens, 42);
32643265
assert.equal(usage.prompt_tokens, 30);
32653266
assert.equal(usage.completion_tokens, 12);
32663267
assert.equal(usage.total_tokens, 42);

0 commit comments

Comments
 (0)