Skip to content

Commit 7703277

Browse files
committed
feat(session): fold a structured summary plus a small budgeted tail
Route every pruning-compactor fold through one budgeted tail selector: the tail holds at least the last keepRecentTurns turns and extends older while the next whole pick fits tailBudgetTokens. Tail tool outputs ride head/tail excerpted with a sentinel; cuts never split a tool pair; newest user messages stay whole; prior spines fold into the next handoff while the summary updates them via priorSummary.
1 parent 9e20da7 commit 7703277

9 files changed

Lines changed: 558 additions & 50 deletions

‎src/context-compactor.test.ts‎

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ function hasConsecutiveSameRole(turns: ConversationTurn[]): boolean {
5959
describe("createPruningCompactor", () => {
6060
test("returns turns unchanged when under the keep threshold", async () => {
6161
const compactor = createPruningCompactor({
62+
// CL-9007: pin a tiny tail budget so the fold covers the same older
63+
// region the old keepRecentTurns cut folded.
64+
compactionShape: { tailBudgetTokens: 10 },
6265
keepRecentTurns: 5,
6366
summaryMaxChars: 500,
6467
});
@@ -78,16 +81,28 @@ describe("createPruningCompactor", () => {
7881
// compactorNoOpFloor accordingly breaks that guarantee silently.
7982
const keepRecentTurns = 3;
8083
const compactor = createPruningCompactor({
84+
// CL-9007: pin a tiny tail budget so the fold covers the same older
85+
// region the old keepRecentTurns cut folded.
86+
compactionShape: { tailBudgetTokens: 10 },
8187
keepRecentTurns,
8288
summaryMaxChars: 500,
8389
});
8490
const floor = compactorNoOpFloor(keepRecentTurns);
8591

92+
// Bodies exceed the pinned tail budget, so past-floor always has a folded
93+
// region while at-floor still no-ops on the count check alone.
94+
const body = (i: number): string => `body ${i} ` + "x".repeat(100);
8695
const atFloor = Array.from({ length: floor }, (_, i) =>
87-
makeTurn({ role: i % 2 === 0 ? "user" : "assistant" }),
96+
makeTurn({
97+
role: i % 2 === 0 ? "user" : "assistant",
98+
content: [{ type: "text", text: body(i) }],
99+
}),
88100
);
89101
const pastFloor = Array.from({ length: floor + 1 }, (_, i) =>
90-
makeTurn({ role: i % 2 === 0 ? "user" : "assistant" }),
102+
makeTurn({
103+
role: i % 2 === 0 ? "user" : "assistant",
104+
content: [{ type: "text", text: body(i) }],
105+
}),
91106
);
92107

93108
expect(
@@ -100,6 +115,9 @@ describe("createPruningCompactor", () => {
100115

101116
test("compacts old turns and preserves recent ones", async () => {
102117
const compactor = createPruningCompactor({
118+
// CL-9007: pin a tiny tail budget so the fold covers the same older
119+
// region the old keepRecentTurns cut folded.
120+
compactionShape: { tailBudgetTokens: 10 },
103121
keepRecentTurns: 2,
104122
summaryMaxChars: 500,
105123
});
@@ -150,6 +168,9 @@ describe("createPruningCompactor", () => {
150168

151169
test("preserves tool_call and tool_result blocks in recent turns", async () => {
152170
const compactor = createPruningCompactor({
171+
// CL-9007: pin a tiny tail budget so the fold covers the same older
172+
// region the old keepRecentTurns cut folded.
173+
compactionShape: { tailBudgetTokens: 10 },
153174
keepRecentTurns: 1,
154175
summaryMaxChars: 500,
155176
});
@@ -183,6 +204,9 @@ describe("createPruningCompactor", () => {
183204
describe("createPruningCompactor — initiating task preservation", () => {
184205
test("keeps the initiating task verbatim even when it is far outside the recent window", async () => {
185206
const compactor = createPruningCompactor({
207+
// CL-9007: pin a tiny tail budget so the fold covers the same older
208+
// region the old keepRecentTurns cut folded.
209+
compactionShape: { tailBudgetTokens: 10 },
186210
keepRecentTurns: 2,
187211
maxAnchorTurns: 1,
188212
summaryMaxChars: 500,
@@ -232,6 +256,9 @@ describe("createPruningCompactor — initiating task preservation", () => {
232256

233257
test("emits the compaction summary as a user turn, never system", async () => {
234258
const compactor = createPruningCompactor({
259+
// CL-9007: pin a one-token tail budget — only the mandatory floor stays
260+
// live, so this tiny fixture still folds the same older region.
261+
compactionShape: { tailBudgetTokens: 1 },
235262
keepRecentTurns: 1,
236263
summaryMaxChars: 500,
237264
});
@@ -247,6 +274,9 @@ describe("createPruningCompactor — initiating task preservation", () => {
247274

248275
test("never emits consecutive same-role turns, even with adjacent user anchors", async () => {
249276
const compactor = createPruningCompactor({
277+
// CL-9007: pin a one-token tail budget — only the mandatory floor stays
278+
// live, so this tiny fixture still folds the same older region.
279+
compactionShape: { tailBudgetTokens: 1 },
250280
keepRecentTurns: 2,
251281
summaryMaxChars: 500,
252282
});
@@ -270,6 +300,9 @@ describe("createPruningCompactor — initiating task preservation", () => {
270300

271301
test("keeps alternating roles when a tool_result user turn abuts a plain user turn", async () => {
272302
const compactor = createPruningCompactor({
303+
// CL-9007: pin a tiny tail budget so the fold covers the same older
304+
// region the old keepRecentTurns cut folded.
305+
compactionShape: { tailBudgetTokens: 10 },
273306
keepRecentTurns: 1,
274307
maxAnchorTurns: 3,
275308
summaryMaxChars: 500,
@@ -338,6 +371,9 @@ describe("createPruningCompactor — image aging", () => {
338371

339372
test("strips image bytes from an anchored (aged) turn but keeps its text", async () => {
340373
const compactor = createPruningCompactor({
374+
// CL-9007: pin a tiny tail budget so the fold covers the same older
375+
// region the old keepRecentTurns cut folded.
376+
compactionShape: { tailBudgetTokens: 10 },
341377
keepRecentTurns: 2,
342378
maxAnchorTurns: 1,
343379
summaryMaxChars: 500,
@@ -405,6 +441,9 @@ describe("createPruningCompactor — image aging", () => {
405441

406442
test("keeps an image intact when its turn is still within the recent window", async () => {
407443
const compactor = createPruningCompactor({
444+
// CL-9007: pin a tiny tail budget so the fold covers the same older
445+
// region the old keepRecentTurns cut folded.
446+
compactionShape: { tailBudgetTokens: 10 },
408447
keepRecentTurns: 3,
409448
summaryMaxChars: 500,
410449
});
@@ -437,6 +476,9 @@ describe("createPruningCompactor — image aging", () => {
437476
// With few turns, full pruning is a no-op, but images outside keepRecentTurns
438477
// must still spill so they are not resent as base64 forever.
439478
const compactor = createPruningCompactor({
479+
// CL-9007: pin a tiny tail budget so the fold covers the same older
480+
// region the old keepRecentTurns cut folded.
481+
compactionShape: { tailBudgetTokens: 10 },
440482
keepRecentTurns: 2,
441483
summaryMaxChars: 500,
442484
});
@@ -478,6 +520,9 @@ describe("createPruningCompactor — image aging", () => {
478520

479521
test("records the number of turns aged out in the transform record", async () => {
480522
const compactor = createPruningCompactor({
523+
// CL-9007: pin a tiny tail budget so the fold covers the same older
524+
// region the old keepRecentTurns cut folded.
525+
compactionShape: { tailBudgetTokens: 10 },
481526
keepRecentTurns: 1,
482527
maxAnchorTurns: 1,
483528
summaryMaxChars: 500,
@@ -540,6 +585,9 @@ describe("createPruningCompactor — error anchoring (CL-6906)", () => {
540585
...padding(8, "after"),
541586
];
542587
const compactor = createPruningCompactor({
588+
// CL-9007: pin a tiny tail budget so the fold covers the same older
589+
// region the old keepRecentTurns cut folded.
590+
compactionShape: { tailBudgetTokens: 10 },
543591
keepRecentTurns: 6,
544592
maxAnchorTurns: 8,
545593
summaryMaxChars: 2000,
@@ -587,6 +635,9 @@ describe("createPruningCompactor — error anchoring (CL-6906)", () => {
587635
...padding(8, "after"),
588636
];
589637
const compactor = createPruningCompactor({
638+
// CL-9007: pin a tiny tail budget so the fold covers the same older
639+
// region the old keepRecentTurns cut folded.
640+
compactionShape: { tailBudgetTokens: 10 },
590641
keepRecentTurns: 6,
591642
maxAnchorTurns: 8,
592643
summaryMaxChars: 2000,
@@ -648,6 +699,9 @@ describe("createPruningCompactor — error anchoring (CL-6906)", () => {
648699
...padding(8, "after"),
649700
];
650701
const compactor = createPruningCompactor({
702+
// CL-9007: pin a tiny tail budget so the fold covers the same older
703+
// region the old keepRecentTurns cut folded.
704+
compactionShape: { tailBudgetTokens: 10 },
651705
keepRecentTurns: 6,
652706
maxAnchorTurns: 8,
653707
summaryMaxChars: 2000,
@@ -734,6 +788,9 @@ describe("createPruningCompactor — maxAnchorTurns caps pairing pulls (CL-6906)
734788

735789
const maxAnchorTurns = 4;
736790
const compactor = createPruningCompactor({
791+
// CL-9007: pin a tiny tail budget so the fold covers the same older
792+
// region the old keepRecentTurns cut folded.
793+
compactionShape: { tailBudgetTokens: 10 },
737794
keepRecentTurns: 6,
738795
maxAnchorTurns,
739796
summaryMaxChars: 2000,
@@ -755,6 +812,9 @@ describe("createPruningCompactor — summarize receives the workflow context (CL
755812
let capturedCtx: unknown = "not called";
756813
const workflowCtx = { workflow: { name: "build", stepIndex: 2, total: 7 } };
757814
const compactor = createPruningCompactor({
815+
// CL-9007: pin a one-token tail budget — only the mandatory floor stays
816+
// live, so this tiny fixture still folds the same older region.
817+
compactionShape: { tailBudgetTokens: 1 },
758818
keepRecentTurns: 1,
759819
summaryMaxChars: 500,
760820
summaryContext: () => workflowCtx,
@@ -776,6 +836,9 @@ describe("createPruningCompactor — summarize receives the workflow context (CL
776836
describe("createPruningCompactor — operator extra instructions", () => {
777837
test("stores extra instructions on the compact record", async () => {
778838
const compactor = createPruningCompactor({
839+
// CL-9007: pin a tiny tail budget so the fold covers the same older
840+
// region the old keepRecentTurns cut folded.
841+
compactionShape: { tailBudgetTokens: 10 },
779842
keepRecentTurns: 1,
780843
summaryMaxChars: 500,
781844
summaryContext: () => ({ extraInstructions: "keep the auth discussion" }),
@@ -799,6 +862,9 @@ describe("createPruningCompactor — operator extra instructions", () => {
799862
makeTurn({ role: "user", content: [{ type: "text", text: "recent" }] }),
800863
];
801864
const written = await createPruningCompactor({
865+
// CL-9007: pin a one-token tail budget — only the mandatory floor stays
866+
// live, so this tiny fixture still folds the same older region.
867+
compactionShape: { tailBudgetTokens: 1 },
802868
keepRecentTurns: 1,
803869
summaryMaxChars: 500,
804870
summaryContext: () => ({ extraInstructions: "keep the auth discussion" }),
@@ -812,6 +878,9 @@ describe("createPruningCompactor — operator extra instructions", () => {
812878

813879
let captured: { extraInstructions?: string } | undefined;
814880
const next = await createPruningCompactor({
881+
// CL-9007: pin a one-token tail budget — only the mandatory floor stays
882+
// live, so this tiny fixture still folds the same older region.
883+
compactionShape: { tailBudgetTokens: 1 },
815884
keepRecentTurns: 1,
816885
summaryMaxChars: 500,
817886
summaryContext: () => {
@@ -860,6 +929,9 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
860929

861930
test("second apply replaces the prior summary instead of accumulating", async () => {
862931
const compactor = createPruningCompactor({
932+
// CL-9007: pin a tiny tail budget so the fold covers the same older
933+
// region the old keepRecentTurns cut folded.
934+
compactionShape: { tailBudgetTokens: 10 },
863935
keepRecentTurns: 2,
864936
summaryMaxChars: 500,
865937
});
@@ -880,6 +952,9 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
880952

881953
test("second apply keeps the initiating task as its own user turn", async () => {
882954
const compactor = createPruningCompactor({
955+
// CL-9007: pin a tiny tail budget so the fold covers the same older
956+
// region the old keepRecentTurns cut folded.
957+
compactionShape: { tailBudgetTokens: 10 },
883958
keepRecentTurns: 2,
884959
maxAnchorTurns: 1,
885960
summaryMaxChars: 500,
@@ -941,6 +1016,9 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
9411016

9421017
test("harness spacer is stamped with the reserved producer id and a visible sentinel", async () => {
9431018
const compactor = createPruningCompactor({
1019+
// CL-9007: pin a tiny tail budget so the fold covers the same older
1020+
// region the old keepRecentTurns cut folded.
1021+
compactionShape: { tailBudgetTokens: 10 },
9441022
keepRecentTurns: 2,
9451023
summaryMaxChars: 500,
9461024
});
@@ -983,6 +1061,9 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
9831061

9841062
test("empty-fold keep-set returns the input unchanged", async () => {
9851063
const compactor = createPruningCompactor({
1064+
// CL-9007: pin a tiny tail budget so the fold covers the same older
1065+
// region the old keepRecentTurns cut folded.
1066+
compactionShape: { tailBudgetTokens: 10 },
9861067
keepRecentTurns: 1,
9871068
maxAnchorTurns: 8,
9881069
summaryMaxChars: 500,
@@ -1028,6 +1109,9 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
10281109
},
10291110
});
10301111
const compactor = createPruningCompactor({
1112+
// CL-9007: pin a tiny tail budget so the fold covers the same older
1113+
// region the old keepRecentTurns cut folded.
1114+
compactionShape: { tailBudgetTokens: 10 },
10311115
keepRecentTurns: 2,
10321116
summaryMaxChars: 500,
10331117
summarize,
@@ -1248,6 +1332,9 @@ describe("buildLLMTurnSummary", () => {
12481332
describe("buildTurnSummary via createPruningCompactor", () => {
12491333
test("summarizes tool_call and tool_result blocks in compacted turns", async () => {
12501334
const compactor = createPruningCompactor({
1335+
// CL-9007: pin a tiny tail budget so the fold covers the same older
1336+
// region the old keepRecentTurns cut folded.
1337+
compactionShape: { tailBudgetTokens: 10 },
12511338
keepRecentTurns: 1,
12521339
summaryMaxChars: 2000,
12531340
});
@@ -1319,6 +1406,9 @@ describe("buildTurnSummary via createPruningCompactor", () => {
13191406
test("a truncated lying spine aborts instead of shipping", async () => {
13201407
const maxChars = 20;
13211408
const compactor = createPruningCompactor({
1409+
// CL-9007: pin a tiny tail budget so the fold covers the same older
1410+
// region the old keepRecentTurns cut folded.
1411+
compactionShape: { tailBudgetTokens: 10 },
13221412
keepRecentTurns: 1,
13231413
summaryMaxChars: maxChars,
13241414
});

‎src/session/compaction-handoff.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import { ArkErrors, type } from "arktype";
3232
import type { ConversationTurn, StrategyBlob } from "@intx/types/runtime";
33+
import { VERIFY_REPAIR_HEADING } from "./compaction-verify.js";
3334

3435
// Canonical home of the fold marker. compactor.ts re-exports it so existing
3536
// importers keep working; this module owns the literal.
@@ -483,11 +484,25 @@ export function extractHandoffArtifact(
483484
const nonEmptyUserTexts = freshUserTexts.filter(
484485
(text) => text.trim().length > 0,
485486
);
487+
// CL-9007: the budgeted tail can lift every user turn out of the folded
488+
// region (anchored initiating task, whole newest messages), leaving no fresh
489+
// user text to name the goal. Fall back to the fold's own summary narrative
490+
// — the model's statement of what the region was about — before Unknown, so
491+
// the next fold's "update this" context still names the work (e.g. src/a.ts)
492+
// instead of dropping it. The verify-repair appendix is stripped first: it
493+
// carries exact file lists that stay in the fat file by design (CL-8744) and
494+
// must not leak into the thin spine.
495+
const narrativeGoal = oneLine(
496+
narrative.split(VERIFY_REPAIR_HEADING)[0] ?? "",
497+
MAX_GOAL_CHARS,
498+
);
486499
const extractedGoal =
487500
carried.goal ??
488501
(nonEmptyUserTexts.length > 0
489502
? oneLine(nonEmptyUserTexts[0] ?? "", MAX_GOAL_CHARS)
490-
: "Unknown (no user message in folded turns)");
503+
: narrativeGoal.length > 0
504+
? narrativeGoal
505+
: "Unknown (no user message in folded turns)");
491506
const goalFromFreshIndex = carried.goal === undefined ? 0 : -1;
492507
const fileGoal = preferFull(priorFile.goal, extractedGoal);
493508

0 commit comments

Comments
 (0)