Skip to content

Commit 0cdb84a

Browse files
committed
feat(session): fold compaction into a fat handoff file with a thin live spine
A fold writes the full structured record to one stable handoff file and keeps only a short spine plus an explicit pointer in the prompt. The completeness gate rejects folds that drop novel text, so the spine re-renders carried facts byte-identical and every fold overwrites the same latest key instead of chaining per-fold files.
1 parent fc5e047 commit 0cdb84a

5 files changed

Lines changed: 1169 additions & 29 deletions

File tree

‎src/context-compactor.test.ts‎

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,17 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
982982
mockStrategyCtx,
983983
);
984984
expect(compactedTurns(result2.output)).toHaveLength(1);
985-
expect(allText(result2.output)).toContain("UNIQUE_SUCCESS_SUMMARY");
985+
// CL-8744: the narrative lives in the fat handoff file, not the prompt.
986+
// The live output carries only the thin spine plus its pointer.
987+
expect(allText(result2.output)).not.toContain("UNIQUE_SUCCESS_SUMMARY");
988+
const handoffBlob = defined(defined(result2.blobs)[0]);
989+
expect(handoffBlob.contentType).toBe("text/markdown");
990+
expect(new TextDecoder().decode(handoffBlob.bytes)).toContain(
991+
"UNIQUE_SUCCESS_SUMMARY",
992+
);
993+
expect(allText(result2.output)).toContain(
994+
`Handoff: tool-output:///${handoffBlob.key}`,
995+
);
986996
expect(hasConsecutiveSameRole(result2.output)).toBe(false);
987997
});
988998
});
@@ -1204,11 +1214,24 @@ describe("buildTurnSummary via createPruningCompactor", () => {
12041214
];
12051215

12061216
const result = await compactor.apply(turns, mockStrategyCtx);
1207-
const summaryText = (
1217+
const spineText = (
12081218
defined(defined(result.output[0]).content[0]) as { text: string }
12091219
).text;
1210-
expect(summaryText).toContain("read_file");
1211-
expect(summaryText).toContain("Total tool calls: 1");
1220+
// CL-8744: the live output carries only the thin spine (goal one-liner,
1221+
// evidence echo, explicit pointer) — file lists and counts stay in the
1222+
// fat handoff file, where they cannot make the next spine novel.
1223+
expect(spineText).toContain("[Compacted prior context]");
1224+
expect(spineText).toContain("Handoff: tool-output:///");
1225+
expect(spineText).not.toContain("src/foo.ts");
1226+
// The structured tool memory lives in the fat handoff file.
1227+
const file = new TextDecoder().decode(
1228+
defined(defined(result.blobs)[0]).bytes,
1229+
);
1230+
expect(file).toContain("src/foo.ts");
1231+
expect(file).toContain("paths: src/foo.ts");
1232+
expect(file).toContain("turns: 2, tool calls: 1");
1233+
expect(file).toContain("read_file");
1234+
expect(file).toContain("Total tool calls: 1");
12121235
});
12131236

12141237
test("truncates summary when it exceeds maxChars", async () => {
@@ -1230,17 +1253,17 @@ describe("buildTurnSummary via createPruningCompactor", () => {
12301253
];
12311254

12321255
const result = await compactor.apply(turns, mockStrategyCtx);
1233-
const summaryBlock = defined(defined(result.output[0]).content[0]) as {
1234-
text: string;
1235-
};
1236-
// The summary portion of the block is extracted from after the header line.
1237-
// The header itself is "---..." so we look at the full block text — the
1238-
// embedded buildTurnSummary output must end with "..." when truncated.
1239-
expect(summaryBlock.text).toContain("...");
1240-
// And the truncated summary must not exceed maxChars + 3 (for the "..." suffix)
1241-
const summaryStart = summaryBlock.text.indexOf("[Compacted prior context]");
1242-
const rawSummary = summaryBlock.text.slice(summaryStart);
1243-
// The raw summary lines are bounded by maxChars
1244-
expect(rawSummary.length).toBeLessThan(maxChars + 200); // header text + bounded summary
1256+
// CL-8744: the deterministic narrative lives in the fat handoff file's
1257+
// Summary section; the live output carries only the thin spine.
1258+
const file = new TextDecoder().decode(
1259+
defined(defined(result.blobs)[0]).bytes,
1260+
);
1261+
const narrative = defined(
1262+
file.split("## Summary (this fold — may paraphrase)\n")[1],
1263+
).split("## Exact facts")[0];
1264+
// The embedded buildTurnSummary output ends with "..." when truncated...
1265+
expect(narrative).toContain("...");
1266+
// ...and the truncated narrative stays within maxChars + 3 ("..." suffix).
1267+
expect(defined(narrative?.trim()).length).toBeLessThanOrEqual(maxChars + 3);
12451268
});
12461269
});

0 commit comments

Comments
 (0)