Skip to content

Commit 4832427

Browse files
fix(compaction): keep operator ellipsis and harvest substring tokens (#1246)
Strip only the spine cut sentinel when restoring Output tokens, so operator-authored trailing ellipsis stays on a without-file re-fold. Harvest standing KEY=value tokens by exact match so a later token is not dropped because it is a substring of an in-window token.
1 parent 4c5fb28 commit 4832427

2 files changed

Lines changed: 85 additions & 7 deletions

File tree

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,26 @@ describe("extractHandoffArtifact", () => {
292292
expect(rendered).toContain(token);
293293
expect(rendered).toContain(`Output: ${token}`);
294294
});
295+
296+
test("harvests a later standing token that is a substring of an in-window token", () => {
297+
const inWindow = "FILES_DONE=src/a.ts.bak";
298+
const later = "FILES_DONE=src/a.ts";
299+
const prefix = `${"W".repeat(500 - inWindow.length - 1)} `;
300+
const goal = `${prefix}${inWindow} ${later}`;
301+
const window = goal.replace(/\s+/g, " ").trim().slice(0, 500);
302+
expect(window.endsWith(inWindow)).toBe(true);
303+
expect(window.includes(later)).toBe(true);
304+
expect(window.includes(`${later} `)).toBe(false);
305+
306+
const { artifact, spine } = extractHandoffArtifact(
307+
[userTurn(goal)],
308+
"narrative",
309+
);
310+
expect(artifact.goal).toBe(goal.replace(/\s+/g, " ").trim());
311+
expect(artifact.goal.endsWith(` ${later}`)).toBe(true);
312+
expect(spine.goal).toContain(inWindow);
313+
expect(spine.goal.endsWith(` ${later}`)).toBe(true);
314+
});
295315
});
296316

297317
describe("recoverEvidenceMarkers", () => {
@@ -698,6 +718,37 @@ describe("iterative folding", () => {
698718
expect(again.spineText).toContain(token);
699719
});
700720

721+
test("without-file re-fold keeps an operator Goal that ends with ellipsis", () => {
722+
const goal = "Fix the tests...";
723+
const first = buildHandoffFold([userTurn(goal)], "narrative");
724+
expect(first.spineText).toContain(`Goal: ${goal}`);
725+
expect(first.artifact.goal).toBe(goal);
726+
727+
const withoutFile = buildHandoffFold(
728+
[spineTurn(first.spineText), userTurn("Continue.")],
729+
"narrative",
730+
);
731+
expect(withoutFile.artifact.goal).toBe(goal);
732+
expect(withoutFile.spineText).toContain(`Goal: ${goal}`);
733+
});
734+
735+
test("without-file re-fold keeps FILES_DONE=src/foo... as one token", () => {
736+
const token = "FILES_DONE=src/foo...";
737+
const first = buildHandoffFold([userTurn(token)], "narrative");
738+
expect(first.spineText).toContain(`Goal: ${token}`);
739+
expect(first.spineText).toContain(`Output: ${token}`);
740+
expect(first.artifact.goal).toBe(token);
741+
742+
const withoutFile = buildHandoffFold(
743+
[spineTurn(first.spineText), userTurn("Continue.")],
744+
"narrative",
745+
);
746+
expect(withoutFile.artifact.goal).toBe(token);
747+
expect(withoutFile.spineText).toContain(`Goal: ${token}`);
748+
expect(withoutFile.spineText).toContain(`Output: ${token}`);
749+
expect(withoutFile.artifact.goal).not.toContain(`${token} ${token}`);
750+
});
751+
701752
test("a truncated spine fragment collapses into the full prior-file constraint", () => {
702753
const full = `Must never ship without ${"x".repeat(80)}`;
703754
const first = buildHandoffFold([userTurn(full)], "narrative");

‎src/session/compaction-handoff.ts‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,39 @@ function capGoal(text: string, maxChars: number): string {
127127
if (start < end && tokenEnd > end) end = tokenEnd;
128128
}
129129
let capped = line.slice(0, end);
130+
const present = new Set(outputFormatTokens(capped));
130131
for (const token of tokens) {
131-
if (!capped.includes(token)) capped = `${capped} ${token}`;
132+
if (present.has(token)) continue;
133+
capped = `${capped} ${token}`;
134+
present.add(token);
132135
}
133136
return capped;
134137
}
135138

139+
function hasStandingOutputToken(goal: string, token: string): boolean {
140+
return outputFormatTokens(goal).includes(token);
141+
}
142+
143+
function trailingEllipsisIsStandingToken(goal: string): boolean {
144+
if (!goal.endsWith(SPINE_CUT_SENTINEL)) return false;
145+
return outputFormatTokens(goal).some(
146+
(token) => token.endsWith(SPINE_CUT_SENTINEL) && goal.endsWith(token),
147+
);
148+
}
149+
150+
function mergeStandingOutputToken(
151+
goal: string | undefined,
152+
token: string,
153+
): string {
154+
if (goal === undefined || goal.length === 0) return token;
155+
if (hasStandingOutputToken(goal, token)) return goal;
156+
const prefix =
157+
goal.endsWith(SPINE_CUT_SENTINEL) && !trailingEllipsisIsStandingToken(goal)
158+
? goal.slice(0, -SPINE_CUT_SENTINEL.length)
159+
: goal;
160+
return `${prefix} ${token}`;
161+
}
162+
136163
function cutSpineGoal(text: string): string {
137164
const line = text.replace(/\s+/g, " ").trim();
138165
if (line.length <= SPINE_GOAL_CHARS) return line;
@@ -271,17 +298,17 @@ function parseSpineText(text: string): CarriedFacts {
271298
for (const line of text.split("\n")) {
272299
const trimmed = line.trim();
273300
if (trimmed.startsWith("Goal: ")) {
274-
let goal = trimmed.slice("Goal: ".length).trim();
275-
if (goal.endsWith(SPINE_CUT_SENTINEL))
276-
goal = goal.slice(0, -SPINE_CUT_SENTINEL.length);
301+
// Keep operator-authored trailing "..." (Fix the tests...,
302+
// FILES_DONE=src/foo...). The spine cut sentinel is stripped only when
303+
// mergeStandingOutputToken restores an Output token onto a truncated
304+
// Goal prefix.
305+
const goal = trimmed.slice("Goal: ".length).trim();
277306
if (goal.length > 0) carried.goal = goal;
278307
} else if (trimmed.startsWith(HANDOFF_OUTPUT_LINE_PREFIX)) {
279308
for (const token of outputFormatTokens(
280309
trimmed.slice(HANDOFF_OUTPUT_LINE_PREFIX.length),
281310
)) {
282-
if (carried.goal === undefined) carried.goal = token;
283-
else if (!carried.goal.includes(token))
284-
carried.goal = `${carried.goal} ${token}`;
311+
carried.goal = mergeStandingOutputToken(carried.goal, token);
285312
}
286313
} else if (trimmed.startsWith("Constraints: ")) {
287314
for (const constraint of trimmed

0 commit comments

Comments
 (0)