Skip to content

Commit 0333c14

Browse files
fix(compaction): keep standing output-format tokens whole on the spine (#1236)
A hard 160-character slice was splitting reply-contract tokens (FILES_DONE became FILES_DO), so a fold-then-continue run no longer saw the original reply contract. Cut the live goal at a token boundary, echo output-format tokens uncut, and point the spine at the handoff for the full operator ask.
1 parent 9cee7e4 commit 0333c14

2 files changed

Lines changed: 102 additions & 14 deletions

File tree

‎src/session/compaction-handoff.test.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,31 @@ describe("renderHandoffSpine", () => {
354354
expect(rendered.split("\n").length).toBeLessThanOrEqual(10);
355355
});
356356

357+
test("does not mid-token truncate a standing output contract", () => {
358+
const token = "FILES_DONE=src/session/compaction-handoff.ts";
359+
const splitAt = "FILES_DO".length;
360+
const prefix = `${"W".repeat(160 - splitAt - 1)} `;
361+
const goal = `${prefix}${token}`;
362+
const midToken = goal.slice(0, 160);
363+
expect(midToken.endsWith("FILES_DO")).toBe(true);
364+
expect(midToken.includes("FILES_DONE")).toBe(false);
365+
366+
const rendered = renderHandoffSpine(
367+
{
368+
goal,
369+
constraints: [],
370+
decisions: [],
371+
evidenceMarkers: [],
372+
activatedTools: [],
373+
},
374+
handoffBlobUri(HANDOFF_LATEST_KEY),
375+
);
376+
expect(rendered).not.toContain(midToken);
377+
expect(rendered).toContain(token);
378+
expect(rendered).toMatch(/re-read/);
379+
expect(rendered).toMatch(/operator ask|output contract/);
380+
});
381+
357382
test("leaves 79- and 80-char items unmarked and marks 81 with an ellipsis", () => {
358383
const unmarked79 = `Must never write to ${"a".repeat(59)}`;
359384
const unmarked80 = `Must never write to ${"a".repeat(60)}`;
@@ -574,6 +599,36 @@ describe("iterative folding", () => {
574599
);
575600
});
576601

602+
test("a fold-then-continue keeps the standing output contract uncut", () => {
603+
const token = "FILES_DONE=src/a.ts";
604+
const goal = `${"W".repeat(151)} ${token}`;
605+
const midToken = goal.replace(/\s+/g, " ").trim().slice(0, 160);
606+
expect(midToken.endsWith("FILES_DO")).toBe(true);
607+
608+
const first = buildHandoffFold([userTurn(goal)], "narrative");
609+
expect(first.spineText).not.toContain(midToken);
610+
expect(first.spineText).toContain(token);
611+
const file = new TextDecoder().decode(first.blob.bytes);
612+
expect(file).toContain(token);
613+
expect(first.artifact.goal).toContain(token);
614+
615+
const second = buildHandoffFold(
616+
[spineTurn(first.spineText), userTurn("Continue.")],
617+
"narrative",
618+
{ priorFileText: file },
619+
);
620+
expect(second.spineText).not.toContain(midToken);
621+
expect(second.spineText).toContain(token);
622+
expect(second.artifact.goal).toContain(token);
623+
624+
const withoutFile = buildHandoffFold(
625+
[spineTurn(first.spineText), userTurn("Continue.")],
626+
"narrative",
627+
);
628+
expect(withoutFile.spineText).not.toContain(midToken);
629+
expect(withoutFile.spineText).toContain(token);
630+
});
631+
577632
test("a truncated spine fragment collapses into the full prior-file constraint", () => {
578633
const full = `Must never ship without ${"x".repeat(80)}`;
579634
const first = buildHandoffFold([userTurn(full)], "narrative");

‎src/session/compaction-handoff.ts‎

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
// markers, files/commands, verification, dead ends, next actions, plus a
1010
// verbatim exact-facts appendix), persisted as a context-store blob by the
1111
// reactor under one STABLE key that every fold overwrites;
12-
// - a thin spine that stays in the live prompt: goal one-liner, top
13-
// constraints/decisions, a cumulative evidence echo, activated tools, and
14-
// an explicit pointer (tool-output:/// URI) so the agent can re-read the
15-
// full file when a detail is missing.
12+
// - a thin spine that stays in the live prompt: goal one-liner (cut at a
13+
// token boundary, never mid-token), standing output-format tokens uncut,
14+
// top constraints/decisions, a cumulative evidence echo, activated tools,
15+
// and an explicit pointer (tool-output:/// URI) so the agent can re-read
16+
// the full operator ask and output contract when a detail is missing.
1617
//
1718
// Everything the file carries is copied verbatim out of the folded turns —
1819
// never paraphrased — so exact-required facts (paths, commands, counts, user
@@ -44,6 +45,7 @@ export const HANDOFF_LATEST_KEY = "compaction-handoff-latest.md";
4445

4546
const HANDOFF_TOOLS_LINE_PREFIX =
4647
"Tools still activated and callable directly (no tool_search needed): ";
48+
const HANDOFF_OUTPUT_LINE_PREFIX = "Output: ";
4749

4850
// Structured handoff artifact: the fat file's sections. Every entry is a
4951
// verbatim excerpt from the folded turns (or carried verbatim from a prior
@@ -94,10 +96,32 @@ const VERIFICATION_SIGNAL = /test|check|lint|build|typecheck|verify/i;
9496
// the lines between.
9597
const EVIDENCE_TOKEN = /\[\[evidence:[^[\]\r\n]+\]\]/g;
9698

99+
// Standing reply-contract tokens (FILES_DONE=..., SUMMARY=...). The spine
100+
// must keep these whole; a char slice is how they become FILES_DO.
101+
const OUTPUT_FORMAT_TOKEN = /\b[A-Z][A-Z0-9_]+=\S+/g;
102+
97103
function oneLine(text: string, maxChars: number): string {
98104
return text.replace(/\s+/g, " ").trim().slice(0, maxChars);
99105
}
100106

107+
function outputFormatTokens(text: string): string[] {
108+
const found: string[] = [];
109+
for (const match of text.matchAll(OUTPUT_FORMAT_TOKEN)) {
110+
const token = match[0];
111+
if (token !== undefined && !found.includes(token)) found.push(token);
112+
}
113+
return found;
114+
}
115+
116+
function cutSpineGoal(text: string): string {
117+
const line = text.replace(/\s+/g, " ").trim();
118+
if (line.length <= SPINE_GOAL_CHARS) return line;
119+
const head = line.slice(0, SPINE_GOAL_CHARS);
120+
const lastSpace = head.lastIndexOf(" ");
121+
const kept = lastSpace > 0 ? head.slice(0, lastSpace) : "";
122+
return kept.length > 0 ? `${kept}${SPINE_CUT_SENTINEL}` : SPINE_CUT_SENTINEL;
123+
}
124+
101125
function cutSpineItem(text: string): string {
102126
const line = text.replace(/\s+/g, " ").trim();
103127
if (line.length <= MAX_SPINE_ITEM_CHARS) return line;
@@ -213,6 +237,14 @@ function parseSpineText(text: string): CarriedFacts {
213237
if (trimmed.startsWith("Goal: ")) {
214238
const goal = trimmed.slice("Goal: ".length).trim();
215239
if (goal.length > 0) carried.goal = goal;
240+
} else if (trimmed.startsWith(HANDOFF_OUTPUT_LINE_PREFIX)) {
241+
for (const token of outputFormatTokens(
242+
trimmed.slice(HANDOFF_OUTPUT_LINE_PREFIX.length),
243+
)) {
244+
if (carried.goal === undefined) carried.goal = token;
245+
else if (!carried.goal.includes(token))
246+
carried.goal = `${carried.goal} ${token}`;
247+
}
216248
} else if (trimmed.startsWith("Constraints: ")) {
217249
for (const constraint of trimmed
218250
.slice("Constraints: ".length)
@@ -691,7 +723,7 @@ export function extractHandoffArtifact(
691723
return {
692724
artifact: checked,
693725
spine: {
694-
goal: extractedGoal,
726+
goal: fileGoal,
695727
constraints: mergeUnique(
696728
carried.constraints,
697729
freshConstraints,
@@ -739,19 +771,20 @@ export function renderHandoffFile(
739771
}
740772

741773
/**
742-
* Render the thin live spine. Goal, constraints/decisions, the cumulative
743-
* evidence echo, activated tools, and the explicit file pointer. Starts with
744-
* COMPACTED_PREFIX so the next fold treats it as a foldable handoff turn.
745-
* Counts, file lists, and next actions stay in the fat file.
774+
* Render the thin live spine. Goal (token-boundary cut), standing output
775+
* contract tokens uncut, constraints/decisions, the cumulative evidence echo,
776+
* activated tools, and the explicit file pointer. Starts with COMPACTED_PREFIX
777+
* so the next fold treats it as a foldable handoff turn. Counts, file lists,
778+
* and next actions stay in the fat file.
746779
*/
747780
export function renderHandoffSpine(
748781
spine: SpineFacts,
749782
pointerUri: string,
750783
): string {
751-
const lines = [
752-
COMPACTED_PREFIX,
753-
`Goal: ${oneLine(spine.goal, SPINE_GOAL_CHARS)}`,
754-
];
784+
const lines = [COMPACTED_PREFIX, `Goal: ${cutSpineGoal(spine.goal)}`];
785+
const output = outputFormatTokens(spine.goal);
786+
if (output.length > 0)
787+
lines.push(`${HANDOFF_OUTPUT_LINE_PREFIX}${output.join(" ")}`);
755788
if (spine.constraints.length > 0)
756789
lines.push(
757790
`Constraints: ${spine.constraints
@@ -774,7 +807,7 @@ export function renderHandoffSpine(
774807
`${HANDOFF_TOOLS_LINE_PREFIX}${spine.activatedTools.join(", ")}`,
775808
);
776809
lines.push(
777-
`Handoff: ${pointerUri} — re-read with read_file (offset/limit) for full detail: decisions, verification, dead ends, next actions.`,
810+
`Handoff: ${pointerUri} — re-read with read_file (offset/limit) for the full operator ask, output contract, decisions, verification, dead ends, next actions.`,
778811
);
779812
return lines.join("\n");
780813
}

0 commit comments

Comments
 (0)