Skip to content

Commit 7ab4711

Browse files
committed
fix(compaction): drop dead cache-ttl fold state and stale tests
## Summary The TTL fold path is gone, so the governor's outstandingToolCalls tracking was write-only and the stall-ping test merged from main still expected a cache-ttl-recompress compact. Rewrite the provider-aware describe block to pin the new contract — idle re-entry never folds, whatever the provider window. ## Verification - bun test src/agent/compaction.test.ts src/subagent/nudge-director.test.ts (117 pass) - bun run check (the one local failure is a /tmp symlink artifact of the worktree path; the same test passes on a real checkout) Refs CL-8914
1 parent d2759a6 commit 7ab4711

3 files changed

Lines changed: 61 additions & 247 deletions

File tree

‎src/agent/compaction.test.ts‎

Lines changed: 46 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ describe("compaction governor", () => {
936936
});
937937
});
938938

939-
describe("provider-aware idle recompress (CL-8745)", () => {
939+
describe("cache expiry never folds (CL-8914)", () => {
940940
const MINUTE_MS = 60_000;
941941

942942
function ttlInferenceDone(
@@ -984,142 +984,48 @@ describe("provider-aware idle recompress (CL-8745)", () => {
984984
} as ReactorInboundEvent;
985985
}
986986

987-
test("fires past the provider TTL while under threshold, meter-only on empty", () => {
987+
test("idle pings past any provider cache window return null", () => {
988+
// The governor holds no TTL table: an unarmed idle re-entry never
989+
// produces a compact, whatever the provider's cache economics. Staleness
990+
// on the outgoing prompt is the anthropic-cache-prompt transform's job.
988991
let continuations = 0;
989992
let nowMs = 10_000_000;
990-
const governor = createCompactionGovernor(
991-
() => continuations++,
992-
"",
993-
[],
994-
() => nowMs,
995-
);
996-
governor.noteInferenceDone(
997-
ttlInferenceDone(
998-
{ provider: "anthropic", model: "claude-opus-4-6" },
999-
false,
1000-
),
1001-
tenTurns,
1002-
);
1003-
1004-
// Inside the 5-minute Anthropic window: no fire.
1005-
nowMs += 4 * MINUTE_MS;
1006-
expect(
1007-
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1008-
).toBeNull();
1009-
1010-
// Past the window: the same fold as the threshold path (same compactor,
1011-
// so the fresh tail stays raw) with an attributable reason.
1012-
nowMs += MINUTE_MS + 1;
1013-
expect(
1014-
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1015-
).toBeNull();
1016-
expect(continuations).toBe(0);
1017-
});
1018-
1019-
test("production LastCycleSource: anthropic fires at 5m, codex never does", () => {
1020-
// Harness stamps { sourceId, provider, model } with a bare model, not
1021-
// slash-form "anthropic/claude-opus-4-6". Anthropic's 5-minute window
1022-
// must come from provider, not a dummy model string. Codex has no
1023-
// published 5-minute expiry, so it must stay quiet past the old 10-minute
1024-
// guess as well.
1025-
let nowMs = 15_000_000;
1026-
const clock = () => nowMs;
1027-
const anthropic = createCompactionGovernor(() => undefined, "", [], clock);
1028-
const codex = createCompactionGovernor(() => undefined, "", [], clock);
1029-
anthropic.noteInferenceDone(
1030-
ttlInferenceDone(
1031-
{ provider: "anthropic", model: "claude-opus-4-6" },
1032-
false,
1033-
),
1034-
tenTurns,
1035-
);
1036-
codex.noteInferenceDone(
1037-
ttlInferenceDone(
1038-
{
1039-
sourceId: "codex/work",
1040-
provider: "codex-responses",
1041-
model: "gpt-5.6-luna",
1042-
},
1043-
false,
1044-
),
1045-
tenTurns,
1046-
);
1047-
1048-
nowMs += 5 * MINUTE_MS + 1;
1049-
expect(
1050-
anthropic.interceptIdleContinuation(emptyMessage(), capabilities),
1051-
).toBeNull();
1052-
expect(
1053-
codex.interceptIdleContinuation(emptyMessage(), capabilities),
1054-
).toBeNull();
1055-
1056-
nowMs += 30 * MINUTE_MS;
1057-
expect(
1058-
codex.interceptIdleContinuation(emptyMessage(), capabilities),
1059-
).toBeNull();
1060-
});
1061-
1062-
test("does not idle-recompress DeepSeek or ollama", () => {
1063-
let nowMs = 20_000_000;
1064993
const clock = () => nowMs;
1065-
const deepseek = createCompactionGovernor(() => undefined, "", [], clock);
1066-
const local = createCompactionGovernor(() => undefined, "", [], clock);
1067-
deepseek.noteInferenceDone(
1068-
ttlInferenceDone("deepseek/deepseek-chat", false),
1069-
tenTurns,
1070-
);
1071-
local.noteInferenceDone(
1072-
ttlInferenceDone("ollama/llama3.1", false),
1073-
tenTurns,
1074-
);
1075-
1076-
nowMs += 90 * MINUTE_MS;
1077-
expect(
1078-
deepseek.interceptIdleContinuation(emptyMessage(), capabilities),
1079-
).toBeNull();
1080-
expect(
1081-
local.interceptIdleContinuation(emptyMessage(), capabilities),
1082-
).toBeNull();
1083-
});
1084-
1085-
test("production ollama LastCycleSource never fires cache-ttl-recompress", () => {
1086-
// Harness stamps { sourceId, provider, model } with a bare model. Ollama is
1087-
// buildOpenAISource: sourceId "ollama/default", provider openai-compatible,
1088-
// Keying TTL off the bare model would miss the ollama sourceId. Local
1089-
// inference stays disabled.
1090-
let nowMs = 25_000_000;
1091-
const governor = createCompactionGovernor(
1092-
() => undefined,
1093-
"",
1094-
[],
1095-
() => nowMs,
1096-
);
1097-
governor.noteInferenceDone(
1098-
ttlInferenceDone(
1099-
{
1100-
sourceId: "ollama/default",
1101-
provider: "openai-compatible",
1102-
model: "llama3",
1103-
},
1104-
false,
1105-
),
1106-
tenTurns,
1107-
);
1108-
1109-
nowMs += 5 * MINUTE_MS + 1;
1110-
expect(
1111-
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1112-
).toBeNull();
1113-
});
1114-
1115-
test("stays inert with no observed cache write", () => {
1116-
const governor = createCompactionGovernor(() => undefined);
1117-
expect(
1118-
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1119-
).toBeNull();
994+
const sources = [
995+
{ provider: "anthropic", model: "claude-opus-4-6" },
996+
{
997+
sourceId: "codex/work",
998+
provider: "codex-responses",
999+
model: "gpt-5.6-luna",
1000+
},
1001+
{ provider: "deepseek", model: "deepseek-chat" },
1002+
{
1003+
sourceId: "ollama/default",
1004+
provider: "openai-compatible",
1005+
model: "llama3",
1006+
},
1007+
{ provider: "custom-proxy", model: "unknown-model" },
1008+
];
1009+
for (const source of sources) {
1010+
const governor = createCompactionGovernor(
1011+
() => continuations++,
1012+
"",
1013+
[],
1014+
clock,
1015+
);
1016+
governor.noteInferenceDone(ttlInferenceDone(source, false), tenTurns);
1017+
expect(
1018+
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1019+
).toBeNull();
1020+
nowMs += 90 * MINUTE_MS;
1021+
expect(
1022+
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1023+
).toBeNull();
1024+
}
1025+
expect(continuations).toBe(0);
11201026
});
11211027

1122-
test("defers to the armed threshold path while over threshold", () => {
1028+
test("an armed threshold fold is not disturbed by idle re-entry", () => {
11231029
let nowMs = 30_000_000;
11241030
const governor = createCompactionGovernor(
11251031
() => undefined,
@@ -1128,9 +1034,9 @@ describe("provider-aware idle recompress (CL-8745)", () => {
11281034
() => nowMs,
11291035
);
11301036
governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns);
1131-
// Fixture provider "p" has no TTL. Threshold arming still owns this session.
1037+
// Threshold arming owns the over-threshold session and fires at the tool
1038+
// pause; a message.received re-entry is not its trigger.
11321039
nowMs += 5 * MINUTE_MS + 1;
1133-
// Threshold arming owns the over-threshold session: no TTL double-fold.
11341040
expect(
11351041
governor.interceptIdleContinuation(emptyMessage(), capabilities),
11361042
).toBeNull();
@@ -1139,11 +1045,10 @@ describe("provider-aware idle recompress (CL-8745)", () => {
11391045
).toBeNull();
11401046
});
11411047

1142-
test("fires cache-ttl-recompress in the hysteresis gap (over-threshold, no growth)", () => {
1143-
// Characterization, not a bug: after a threshold compact, a post-compact
1144-
// infer at the same usage clears `pending` via growth hysteresis, so the
1145-
// threshold path no longer owns the session. Idle past the provider TTL
1146-
// still folds — same window- and cap-bounded path as under-threshold.
1048+
test("the hysteresis gap does not fold on cache expiry", () => {
1049+
// After a threshold compact, a post-compact infer at the same usage
1050+
// clears `pending` via growth hysteresis — the exact re-entry where the
1051+
// removed TTL path used to fire.
11471052
let nowMs = 70_000_000;
11481053
const governor = createCompactionGovernor(
11491054
() => undefined,
@@ -1174,37 +1079,7 @@ describe("provider-aware idle recompress (CL-8745)", () => {
11741079
).toBeNull();
11751080
});
11761081

1177-
test("after threshold compact and gap TTL, growth-armed compact stays blocked until a tool_call", () => {
1178-
// Threshold compact (consecutive=1) plus TTL fire in the hysteresis gap
1179-
// (consecutive=2) fills the shared cap. Later growth that would re-arm
1180-
// the threshold path stays blocked until a tool_call occupancy resets it.
1181-
let nowMs = 80_000_000;
1182-
const governor = createCompactionGovernor(
1183-
() => undefined,
1184-
"",
1185-
[],
1186-
() => nowMs,
1187-
);
1188-
governor.noteInferenceDone(
1189-
inferenceDone(overThreshold, "", "anthropic"),
1190-
tenTurns,
1191-
);
1192-
expect(
1193-
governor.interceptActions(toolDone(), inferAction, capabilities),
1194-
).not.toBeNull();
1195-
expect(governor.resumeAfterCompact(emptyMessage())).toBe("infer");
1196-
1197-
governor.noteInferenceDone(
1198-
inferenceDone(overThreshold, "", "anthropic"),
1199-
tenTurns,
1200-
);
1201-
nowMs += 5 * MINUTE_MS + 1;
1202-
expect(
1203-
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1204-
).toBeNull();
1205-
});
1206-
1207-
test("does not fold under an outstanding tool batch, fires once it settles", () => {
1082+
test("an outstanding tool batch does not change idle re-entry", () => {
12081083
let continuations = 0;
12091084
let nowMs = 40_000_000;
12101085
const governor = createCompactionGovernor(
@@ -1221,7 +1096,6 @@ describe("provider-aware idle recompress (CL-8745)", () => {
12211096
expect(
12221097
governor.interceptIdleContinuation(emptyMessage(), capabilities),
12231098
).toBeNull();
1224-
// The batch settles (threshold path uninvolved: under threshold).
12251099
expect(
12261100
governor.interceptActions(toolDone(), inferAction, capabilities),
12271101
).toBeNull();
@@ -1231,28 +1105,7 @@ describe("provider-aware idle recompress (CL-8745)", () => {
12311105
expect(continuations).toBe(0);
12321106
});
12331107

1234-
test("one fire per window, then the shared consecutive-compact cap stops the spiral", () => {
1235-
let continuations = 0;
1236-
let nowMs = 50_000_000;
1237-
const governor = createCompactionGovernor(
1238-
() => continuations++,
1239-
"",
1240-
[],
1241-
() => nowMs,
1242-
);
1243-
governor.noteInferenceDone(
1244-
ttlInferenceDone("anthropic/claude-opus-4-6", false),
1245-
tenTurns,
1246-
);
1247-
1248-
nowMs += 5 * MINUTE_MS + 1;
1249-
expect(
1250-
governor.interceptIdleContinuation(emptyMessage(), capabilities),
1251-
).toBeNull();
1252-
expect(continuations).toBe(0);
1253-
});
1254-
1255-
test("a raced operator message past the TTL still folds, then re-infers", () => {
1108+
test("a raced operator message past the window does not fold", () => {
12561109
let continuations = 0;
12571110
let nowMs = 60_000_000;
12581111
const governor = createCompactionGovernor(

‎src/agent/compaction.ts‎

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,6 @@ export function createCompactionGovernor(
163163
let usingEstimate = false;
164164
let lastModel: string | undefined;
165165
let turnCount = 0;
166-
// Tool calls issued by the last inference.done and not yet settled. A TTL
167-
// recompress must never fold while a batch is outstanding — the stall ping
168-
// that triggers it can arrive mid-work, and folding under it would rewrite
169-
// turns the pending results still belong to. Assigned (not incremented) on
170-
// every inference.done so the serial loop self-heals a miscount.
171-
let outstandingToolCalls = 0;
172166
// Growth hysteresis after a compact that remained over the high watermark:
173167
// snapshot the post-compact infer's usage, then do not re-arm until usage
174168
// grows by resumeDelta. Cleared once usage drops back to or under high.
@@ -267,15 +261,6 @@ export function createCompactionGovernor(
267261
}
268262
syncFromTurns(turns);
269263
lastModel = event.source?.model;
270-
// The terminal reply ends the previous tool batch (its results are
271-
// already in the turns) and opens the batch the reply just issued. A TTL
272-
// recompress must never fold while a batch is outstanding — the stall
273-
// ping that triggers it can arrive mid-work, and folding under it would
274-
// rewrite turns the pending results still belong to. Assigned, not
275-
// incremented, so the serial loop self-heals a miscount.
276-
outstandingToolCalls = event.turn.content.filter(
277-
(block) => block.type === "tool_call",
278-
).length;
279264
const reportedTokens = contextTokensFromUsage(event.usage);
280265
usingEstimate = reportedTokens <= 0;
281266
const contextTokens = usingEstimate ? estimate.tokens : reportedTokens;
@@ -315,7 +300,6 @@ export function createCompactionGovernor(
315300
capabilities: ReactorCapabilities,
316301
): ReactorAction[] | null {
317302
if (event.type !== "tool.done") return null;
318-
if (outstandingToolCalls > 0) outstandingToolCalls -= 1;
319303
const operator = manualPending;
320304
if (
321305
!operator &&
@@ -421,17 +405,11 @@ export function createCompactionGovernor(
421405
}
422406
return issueIdleFold(content, capabilities, THRESHOLD_COMPACT_REASON);
423407
}
424-
// Unarmed idle re-entry past the provider TTL: same fold, same
425-
// keep-recent tail, same cap — but a "cache-ttl-recompress" reason so the
426-
// fold is attributable. No arming: every live re-entry re-checks the
427-
// window, so a sub-agent stall ping or operator message is the trigger.
428-
// In-flight `/compact` (manualPending without idlePending) waits on
429-
// interceptActions; do not steal that hop with a TTL fold.
430-
if (manualPending) return null;
431-
// Cache expiry is a prompt transform, not a fold. Compacting here rewrites
432-
// turns.jsonl and drops the history the transform is supposed to leave
433-
// stored. The Anthropic prompt transform stubs tool bodies on the request
434-
// when this stamp is expired.
408+
// Cache expiry is a prompt transform, not a fold. Compacting on an
409+
// unarmed idle re-entry rewrites turns.jsonl and drops the history the
410+
// transform is supposed to leave stored; the Anthropic prompt transform
411+
// stubs tool bodies on the outgoing request instead. In-flight `/compact`
412+
// (manualPending without idlePending) waits on interceptActions.
435413
return null;
436414
}
437415

@@ -486,10 +464,6 @@ export function createCompactionGovernor(
486464
function notePostCompact(turns: readonly ConversationTurn[]): void {
487465
syncFromTurns(turns);
488466
usingEstimate = true;
489-
// A fold rewrites the turns: results already applied vanish from the
490-
// live set, and post-compact stall pings (empty continuations) carry no
491-
// tool traffic. Reset so a stale count cannot pin the TTL window shut.
492-
outstandingToolCalls = 0;
493467
}
494468

495469
// True while the governor expects the host to answer a continuation emit.

0 commit comments

Comments
 (0)