Skip to content

Commit 1d7933c

Browse files
committed
fix(compaction): strip glued spine sentinel from long standing tokens
cutSpineGoal glues "..." onto a start-at-0 token longer than the cap. outputFormatTokenRe eats that sentinel, so Goal and Output harvests diverge and a without-file re-fold duplicates the token. Strip the glued sentinel when restoring Output, and do not recut a cap-length prefix that already ends with the sentinel.
1 parent f80ae24 commit 1d7933c

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,74 @@ describe("iterative folding", () => {
749749
expect(withoutFile.artifact.goal).not.toContain(`${token} ${token}`);
750750
});
751751

752+
test("without-file re-fold of a start-at-0 FILES_DONE longer than 160 keeps one token", () => {
753+
const token = `FILES_DONE=${"src/session/compaction-handoff.ts/".repeat(8)}end.ts`;
754+
expect(token.length).toBeGreaterThan(160);
755+
const goal = `${token} trailing prose after the standing token`;
756+
const first = buildHandoffFold([userTurn(goal)], "narrative");
757+
expect(first.artifact.goal).toBe(goal);
758+
expect(first.spineText).toContain(`Goal: ${token}...`);
759+
expect(first.spineText).toContain(`Output: ${token}`);
760+
761+
const withoutFile = buildHandoffFold(
762+
[spineTurn(first.spineText), userTurn("Continue.")],
763+
"narrative",
764+
);
765+
expect(withoutFile.artifact.goal).toBe(token);
766+
expect(withoutFile.spineText).toContain(`Output: ${token}`);
767+
expect(
768+
withoutFile.spineText
769+
.split("\n")
770+
.find((line) => line.startsWith("Output: ")),
771+
).toBe(`Output: ${token}`);
772+
expect(withoutFile.artifact.goal).not.toContain(`${token}... ${token}`);
773+
expect(withoutFile.artifact.goal).not.toContain(`${token} ${token}`);
774+
});
775+
776+
test("without-file re-fold of a start-at-0 comma-joined FILES_DONE keeps one token", () => {
777+
const token = `FILES_DONE=${Array.from({ length: 20 }, (_, i) => `src/session/file${i}.ts`).join(",")}`;
778+
expect(token.length).toBeGreaterThan(160);
779+
const goal = `${token} trailing prose after the standing token`;
780+
const first = buildHandoffFold([userTurn(goal)], "narrative");
781+
expect(first.artifact.goal).toBe(goal);
782+
783+
const withoutFile = buildHandoffFold(
784+
[spineTurn(first.spineText), userTurn("Continue.")],
785+
"narrative",
786+
);
787+
expect(withoutFile.artifact.goal).toBe(token);
788+
expect(
789+
withoutFile.spineText
790+
.split("\n")
791+
.find((line) => line.startsWith("Output: ")),
792+
).toBe(`Output: ${token}`);
793+
expect(withoutFile.artifact.goal).not.toContain(`${token}... ${token}`);
794+
expect(withoutFile.artifact.goal).not.toContain(`${token} ${token}`);
795+
});
796+
797+
test("two without-file re-folds of a 159-char spaceless prefix do not grow a fourth dot", () => {
798+
const prefix = "A".repeat(159);
799+
const goal = `${prefix} extra words beyond the spine cap`;
800+
const first = buildHandoffFold([userTurn(goal)], "narrative");
801+
expect(first.spineText).toContain(`Goal: ${prefix}...`);
802+
expect(first.artifact.goal).toBe(goal);
803+
804+
const withoutFile = buildHandoffFold(
805+
[spineTurn(first.spineText), userTurn("Continue.")],
806+
"narrative",
807+
);
808+
const again = buildHandoffFold(
809+
[spineTurn(withoutFile.spineText), userTurn("Continue.")],
810+
"narrative",
811+
);
812+
expect(withoutFile.spineText).toContain(`Goal: ${prefix}...`);
813+
expect(again.spineText).toContain(`Goal: ${prefix}...`);
814+
expect(withoutFile.spineText).not.toContain(`${prefix}....`);
815+
expect(again.spineText).not.toContain(`${prefix}....`);
816+
expect(withoutFile.artifact.goal).not.toContain("....");
817+
expect(again.artifact.goal).not.toContain("....");
818+
});
819+
752820
test("a truncated spine fragment collapses into the full prior-file constraint", () => {
753821
const full = `Must never ship without ${"x".repeat(80)}`;
754822
const first = buildHandoffFold([userTurn(full)], "narrative");

‎src/session/compaction-handoff.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,21 @@ function mergeStandingOutputToken(
153153
): string {
154154
if (goal === undefined || goal.length === 0) return token;
155155
if (hasStandingOutputToken(goal, token)) return goal;
156+
// cutSpineGoal glues "..." onto a start-at-0 token longer than the cap.
157+
// outputFormatTokenRe (\S+) eats that sentinel, so Goal's harvested token
158+
// is token+"..." and never equals the Output token. Strip the glued
159+
// sentinel and keep the standing token once.
160+
if (
161+
goal.endsWith(SPINE_CUT_SENTINEL) &&
162+
!token.endsWith(SPINE_CUT_SENTINEL)
163+
) {
164+
const prefix = goal.slice(0, -SPINE_CUT_SENTINEL.length);
165+
if (
166+
hasStandingOutputToken(prefix, token) ||
167+
outputFormatTokens(goal).includes(`${token}${SPINE_CUT_SENTINEL}`)
168+
)
169+
return prefix;
170+
}
156171
const prefix =
157172
goal.endsWith(SPINE_CUT_SENTINEL) && !trailingEllipsisIsStandingToken(goal)
158173
? goal.slice(0, -SPINE_CUT_SENTINEL.length)
@@ -163,6 +178,13 @@ function mergeStandingOutputToken(
163178
function cutSpineGoal(text: string): string {
164179
const line = text.replace(/\s+/g, " ").trim();
165180
if (line.length <= SPINE_GOAL_CHARS) return line;
181+
// A prior spine cut already ended with the sentinel and sits at the cap.
182+
// Recutting a 159-char spaceless prefix plus "..." eats the first dot and
183+
// glues another sentinel, growing "..." into "....".
184+
if (line.endsWith(SPINE_CUT_SENTINEL)) {
185+
const without = line.slice(0, -SPINE_CUT_SENTINEL.length);
186+
if (without.length <= SPINE_GOAL_CHARS) return line;
187+
}
166188
let cap = SPINE_GOAL_CHARS;
167189
for (const match of line.matchAll(outputFormatTokenRe())) {
168190
const start = match.index ?? 0;

0 commit comments

Comments
 (0)