Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/session/compaction-handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,31 @@ describe("extractHandoffArtifact", () => {
"lint: unused import in src/auth.ts",
);
});

test("keeps a standing output token that straddles the 500-character file-goal cap", () => {
const token = "FILES_DONE=src/session/compaction-handoff.ts";
const prefix = `${"W".repeat(500 - "FILES_DO".length - 1)} `;
const goal = `${prefix}${token}`;
const sliced = goal.replace(/\s+/g, " ").trim().slice(0, 500);
expect(sliced.endsWith("FILES_DO")).toBe(true);
expect(sliced.includes("FILES_DONE")).toBe(false);

const { artifact, spine } = extractHandoffArtifact(
[userTurn(goal)],
"narrative",
);
expect(artifact.goal).toContain(token);
expect(artifact.goal).not.toBe(sliced);
expect(artifact.goal.endsWith("FILES_DO")).toBe(false);
expect(spine.goal).toContain(token);

const rendered = renderHandoffSpine(
spine,
handoffBlobUri(HANDOFF_LATEST_KEY),
);
expect(rendered).toContain(token);
expect(rendered).toContain(`Output: ${token}`);
});
});

describe("recoverEvidenceMarkers", () => {
Expand Down Expand Up @@ -379,6 +404,40 @@ describe("renderHandoffSpine", () => {
expect(rendered).toMatch(/operator ask|output contract/);
});

test("does not render Goal: ... only when the first 160 characters have no space", () => {
const goal = "W".repeat(200);
const rendered = renderHandoffSpine(
{
goal,
constraints: [],
decisions: [],
evidenceMarkers: [],
activatedTools: [],
},
handoffBlobUri(HANDOFF_LATEST_KEY),
);
expect(rendered).not.toMatch(/^Goal: \.\.\.$/m);
expect(rendered).toContain(`Goal: ${"W".repeat(160)}...`);
});

test("keeps a standing output token after a spaceless 160-character prefix", () => {
const token = "FILES_DONE=src/a.ts";
const goal = `${"W".repeat(160)} ${token}`;
const rendered = renderHandoffSpine(
{
goal,
constraints: [],
decisions: [],
evidenceMarkers: [],
activatedTools: [],
},
handoffBlobUri(HANDOFF_LATEST_KEY),
);
expect(rendered).not.toMatch(/^Goal: \.\.\.$/m);
expect(rendered).toContain(`Goal: ${"W".repeat(160)}...`);
expect(rendered).toContain(`Output: ${token}`);
});

test("leaves 79- and 80-char items unmarked and marks 81 with an ellipsis", () => {
const unmarked79 = `Must never write to ${"a".repeat(59)}`;
const unmarked80 = `Must never write to ${"a".repeat(60)}`;
Expand Down Expand Up @@ -627,6 +686,16 @@ describe("iterative folding", () => {
);
expect(withoutFile.spineText).not.toContain(midToken);
expect(withoutFile.spineText).toContain(token);
expect(withoutFile.artifact.goal).toBe(goal);
expect(withoutFile.artifact.goal).not.toContain("...");

const again = buildHandoffFold(
[spineTurn(withoutFile.spineText), userTurn("Continue.")],
"narrative",
);
expect(again.artifact.goal).toBe(goal);
expect(again.artifact.goal).not.toContain("...");
expect(again.spineText).toContain(token);
});

test("a truncated spine fragment collapses into the full prior-file constraint", () => {
Expand Down
58 changes: 48 additions & 10 deletions src/session/compaction-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,65 @@ const VERIFICATION_SIGNAL = /test|check|lint|build|typecheck|verify/i;
const EVIDENCE_TOKEN = /\[\[evidence:[^[\]\r\n]+\]\]/g;

// Standing reply-contract tokens (FILES_DONE=..., SUMMARY=...). The spine
// must keep these whole; a char slice is how they become FILES_DO.
const OUTPUT_FORMAT_TOKEN = /\b[A-Z][A-Z0-9_]+=\S+/g;
// and the file goal must keep these whole; a char slice is how they become
// FILES_DO. Harvest from the uncapped source so a 500-char cap cannot drop
// a token that starts just past the cut.
function outputFormatTokenRe(): RegExp {
return /\b[A-Z][A-Z0-9_]+=\S+/g;
}

function oneLine(text: string, maxChars: number): string {
return text.replace(/\s+/g, " ").trim().slice(0, maxChars);
}

function outputFormatTokens(text: string): string[] {
const found: string[] = [];
for (const match of text.matchAll(OUTPUT_FORMAT_TOKEN)) {
for (const match of text.matchAll(outputFormatTokenRe())) {
const token = match[0];
if (token !== undefined && !found.includes(token)) found.push(token);
}
return found;
}

function capGoal(text: string, maxChars: number): string {
const line = text.replace(/\s+/g, " ").trim();
const tokens = outputFormatTokens(line);
let end = Math.min(maxChars, line.length);
for (const match of line.matchAll(outputFormatTokenRe())) {
const start = match.index ?? 0;
const tokenEnd = start + match[0].length;
if (start < end && tokenEnd > end) end = tokenEnd;
}
let capped = line.slice(0, end);
for (const token of tokens) {
if (!capped.includes(token)) capped = `${capped} ${token}`;
}
return capped;
}

function cutSpineGoal(text: string): string {
const line = text.replace(/\s+/g, " ").trim();
if (line.length <= SPINE_GOAL_CHARS) return line;
const head = line.slice(0, SPINE_GOAL_CHARS);
let cap = SPINE_GOAL_CHARS;
for (const match of line.matchAll(outputFormatTokenRe())) {
const start = match.index ?? 0;
const tokenEnd = start + match[0].length;
if (start < SPINE_GOAL_CHARS && tokenEnd > SPINE_GOAL_CHARS) {
if (start === 0) {
const kept = match[0];
return kept.length < line.length
? `${kept}${SPINE_CUT_SENTINEL}`
: kept;
}
cap = start;
break;
}
}
const head = line.slice(0, cap);
const lastSpace = head.lastIndexOf(" ");
const kept = lastSpace > 0 ? head.slice(0, lastSpace) : "";
return kept.length > 0 ? `${kept}${SPINE_CUT_SENTINEL}` : SPINE_CUT_SENTINEL;
const kept = lastSpace > 0 ? head.slice(0, lastSpace) : head.trimEnd();
if (kept.length === 0) return SPINE_CUT_SENTINEL;
return `${kept}${SPINE_CUT_SENTINEL}`;
}

function cutSpineItem(text: string): string {
Expand Down Expand Up @@ -235,7 +271,9 @@ function parseSpineText(text: string): CarriedFacts {
for (const line of text.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("Goal: ")) {
const goal = trimmed.slice("Goal: ".length).trim();
let goal = trimmed.slice("Goal: ".length).trim();
if (goal.endsWith(SPINE_CUT_SENTINEL))
goal = goal.slice(0, -SPINE_CUT_SENTINEL.length);
if (goal.length > 0) carried.goal = goal;
} else if (trimmed.startsWith(HANDOFF_OUTPUT_LINE_PREFIX)) {
for (const token of outputFormatTokens(
Expand Down Expand Up @@ -524,14 +562,14 @@ export function extractHandoffArtifact(
// instead of dropping it. The verify-repair appendix is stripped first: it
// carries exact file lists that stay in the fat file by design (CL-8744) and
// must not leak into the thin spine.
const narrativeGoal = oneLine(
const narrativeGoal = capGoal(
narrative.split(VERIFY_REPAIR_HEADING)[0] ?? "",
MAX_GOAL_CHARS,
);
const extractedGoal =
carried.goal ??
(nonEmptyUserTexts.length > 0
? oneLine(nonEmptyUserTexts[0] ?? "", MAX_GOAL_CHARS)
? capGoal(nonEmptyUserTexts[0] ?? "", MAX_GOAL_CHARS)
: narrativeGoal.length > 0
? narrativeGoal
: "Unknown (no user message in folded turns)");
Expand All @@ -541,7 +579,7 @@ export function extractHandoffArtifact(
const lastUserText = [...nonEmptyUserTexts].pop();
const lastUserAsNext =
lastUserText !== undefined &&
oneLine(lastUserText, MAX_GOAL_CHARS) !== extractedGoal &&
capGoal(lastUserText, MAX_GOAL_CHARS) !== extractedGoal &&
oneLine(lastUserText, SPINE_GOAL_CHARS) !== carried.goal;

const freshDecisions: string[] = [];
Expand Down
Loading