Skip to content

Commit 51d08f4

Browse files
feat(compaction): fold to a token-capped tail, not recent turns (#1227)
* feat(compaction): fold to a token-capped tail, not recent turns * fix(compaction): fold fat anchors into the handoff not the live prompt Unexcerpted edit_file pairs were riding the live fold as score-10 anchors and holding occupancy over the 60% trigger. The live set is now the thin spine plus a token-capped tail: file-edit bodies go in the fat handoff, images count against the tail budget, superseded reads still stub when the tail is the whole transcript, and a still- over fold notifies the operator without weakening the occupancy latch. # Conflicts: # src/session/compactor.ts * fix(compaction): keep CI folds on the token-capped tail Tiny fixtures sat inside the 2500-token live tail and no-oped. The paging e2e echoed the summarizer prompt, so verify truncated the repair and aborted the fold. Point the auth store test at the testkit mock helper.
1 parent 3913876 commit 51d08f4

20 files changed

Lines changed: 812 additions & 591 deletions

‎e2e/compaction-read-paging.test.ts‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,18 @@ function logLines(count: number): string {
4141
async function openFoldSession(): Promise<E2ESession> {
4242
const session = await openE2ESession({
4343
permissionGate: e2ePermissionGate(),
44-
// A tiny tail budget keeps the fold honest: the default 7500-token
44+
// A tiny tail budget keeps the fold honest: the default 2500-token
4545
// budget would absorb this small scenario into the live tail and the
4646
// compactor would correctly no-op.
47-
compactionShape: { tailBudgetTokens: 10 },
48-
// Echo the summarized turns verbatim: the spine must exist, and this
49-
// scenario asserts on the kept live bodies, not the summary text.
50-
compactionCompletion: async (turns) =>
51-
turns
52-
.flatMap((turn) =>
53-
turn.content.flatMap((block) =>
54-
block.type === "text" ? [block.text] : [],
55-
),
56-
)
57-
.join("\n"),
47+
// Budget fits the paged read windows in the live tail but not the bulky
48+
// pad turns, so the fold still fires.
49+
compactionShape: { tailBudgetTokens: 500 },
50+
// A short handoff, not a prompt echo: complete() receives the summarizer
51+
// prompt turns, and a 4k echo gets truncated off the verify-repair tail
52+
// so the fold aborts. This scenario asserts on kept live bodies, not the
53+
// summary text.
54+
compactionCompletion: async () =>
55+
"Goal: page through var/log/big.log. Next: keep reading remaining windows.",
5856
});
5957
seedFile(session, "var/log/big.log", `${logLines(80)}\n`);
6058
return session;
@@ -78,7 +76,7 @@ async function lastRequestBody(session: E2ESession): Promise<string> {
7876
async function padTurns(session: E2ESession, count: number): Promise<void> {
7977
for (let i = 0; i < count; i++) {
8078
session.harness.scenario.replyOnce("anthropic", {
81-
text: `Acknowledged ${i}.`,
79+
text: `Acknowledged ${i}. ${"pad".repeat(800)}`,
8280
headUsage: LOW_USAGE,
8381
});
8482
await runUntilDone(session, `Pad turn ${i}.`);

‎evals/compaction/fixtures.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const BASELINE = {
1111
syntheticLowInput: 100,
1212
syntheticTriggerInput: 200000,
1313
outputTokens: 1,
14-
keepRecentTurns: 6,
14+
tailBudgetTokens: 2500,
1515
summaryMaxChars: 4000,
1616
wallTimeoutMs: 30000,
1717
} as const;

‎src/agent/chat-event-subscribers.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
22
import {
33
CHAT_TASKS_CHANGED_EVENT,
44
CHAT_TOOLS_ACTIVATE_EVENT,
5+
COMPACTION_FOLD_NONCONVERGED_EVENT,
56
} from "./director.js";
67
import { handleChatDirectorEvent } from "./chat-event-subscribers.js";
78
import type { Task } from "./tasks.js";
@@ -119,4 +120,28 @@ describe("handleChatDirectorEvent", () => {
119120
expect(dispatched).toBe(false);
120121
expect(log.calls).toEqual([]);
121122
});
123+
124+
test("dispatches a fold-nonconverged notice", () => {
125+
const seen: string[] = [];
126+
const log = makeLog();
127+
const handled = handleChatDirectorEvent(
128+
{
129+
type: COMPACTION_FOLD_NONCONVERGED_EVENT,
130+
data: { notice: "Context fold did not reduce occupancy." },
131+
},
132+
{
133+
onTasksChanged: () => {
134+
throw new Error("unexpected tasks-changed dispatch");
135+
},
136+
onToolsActivate: () => {
137+
throw new Error("unexpected tools-activate dispatch");
138+
},
139+
onFoldNonConverged: (notice) => seen.push(notice),
140+
},
141+
log.log,
142+
);
143+
expect(handled).toBe(true);
144+
expect(seen).toEqual(["Context fold did not reduce occupancy."]);
145+
expect(log.calls).toEqual([]);
146+
});
122147
});

‎src/agent/chat-event-subscribers.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import { type } from "arktype";
1010
import {
1111
CHAT_TASKS_CHANGED_EVENT,
1212
CHAT_TOOLS_ACTIVATE_EVENT,
13+
COMPACTION_FOLD_NONCONVERGED_EVENT,
1314
ChatTasksChangedDataSchema,
1415
ChatToolsActivateDataSchema,
16+
CompactionFoldNonConvergedDataSchema,
1517
} from "./director.js";
1618
import type { Task } from "./tasks.js";
1719

1820
export interface ChatDirectorEventHandlers {
1921
onTasksChanged: (tasks: Task[]) => void;
2022
onToolsActivate: (names: string[]) => void;
23+
onFoldNonConverged?: (notice: string) => void;
2124
}
2225

2326
export type ChatDirectorEventDebugLog = (
@@ -58,5 +61,17 @@ export function handleChatDirectorEvent(
5861
handlers.onToolsActivate(parsed.names);
5962
return true;
6063
}
64+
if (event.type === COMPACTION_FOLD_NONCONVERGED_EVENT) {
65+
const parsed = CompactionFoldNonConvergedDataSchema(event.data);
66+
if (parsed instanceof type.errors) {
67+
logDebug(
68+
"chat fold-nonconverged event dropped invalid payload: {error}",
69+
{ error: parsed.summary },
70+
);
71+
return true;
72+
}
73+
handlers.onFoldNonConverged?.(parsed.notice);
74+
return true;
75+
}
6176
return false;
6277
}

0 commit comments

Comments
 (0)