From 038d2aeeaddbb0f9fa7d9d772ef296a2e8661bfc Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 29 Sep 2026 17:53:17 -0700 Subject: [PATCH] fix(compaction): strip glued sentinel even when the token ends with dots The skip existed so operator-authored trailing ellipsis stayed intact. A long start-at-0 token that itself ends with "..." still gets a glued spine sentinel, so Goal and Output harvests diverge the same way as a token without ellipsis. Short operator cases already match as one token and do not need the skip. --- src/session/compaction-handoff.test.ts | 28 ++++++++++++++++++++++++++ src/session/compaction-handoff.ts | 8 +++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/session/compaction-handoff.test.ts b/src/session/compaction-handoff.test.ts index 77f7c474e..454cf65fb 100644 --- a/src/session/compaction-handoff.test.ts +++ b/src/session/compaction-handoff.test.ts @@ -773,6 +773,34 @@ describe("iterative folding", () => { expect(withoutFile.artifact.goal).not.toContain(`${token} ${token}`); }); + test("two without-file re-folds of a start-at-0 token ending with ellipsis keep one Output token", () => { + const token = `FILES_DONE=${"src/session/compaction-handoff.ts/".repeat(8)}end...`; + expect(token.length).toBeGreaterThan(160); + expect(token.endsWith("...")).toBe(true); + const goal = `${token} trailing prose after the standing token`; + const first = buildHandoffFold([userTurn(goal)], "narrative"); + expect(first.artifact.goal).toBe(goal); + expect(first.spineText).toContain(`Output: ${token}`); + + const withoutFile = buildHandoffFold( + [spineTurn(first.spineText), userTurn("Continue.")], + "narrative", + ); + const again = buildHandoffFold( + [spineTurn(withoutFile.spineText), userTurn("Continue.")], + "narrative", + ); + + for (const fold of [withoutFile, again]) { + expect( + fold.spineText.split("\n").find((line) => line.startsWith("Output: ")), + ).toBe(`Output: ${token}`); + expect(fold.artifact.goal).toBe(token); + expect(fold.artifact.goal).not.toContain(`${token} ${token}`); + expect(fold.artifact.goal).not.toContain(`${token}... ${token}`); + } + }); + test("without-file re-fold of a start-at-0 comma-joined FILES_DONE keeps one token", () => { const token = `FILES_DONE=${Array.from({ length: 20 }, (_, i) => `src/session/file${i}.ts`).join(",")}`; expect(token.length).toBeGreaterThan(160); diff --git a/src/session/compaction-handoff.ts b/src/session/compaction-handoff.ts index 95b1dfb29..89b715444 100644 --- a/src/session/compaction-handoff.ts +++ b/src/session/compaction-handoff.ts @@ -156,11 +156,9 @@ function mergeStandingOutputToken( // cutSpineGoal glues "..." onto a start-at-0 token longer than the cap. // outputFormatTokenRe (\S+) eats that sentinel, so Goal's harvested token // is token+"..." and never equals the Output token. Strip the glued - // sentinel and keep the standing token once. - if ( - goal.endsWith(SPINE_CUT_SENTINEL) && - !token.endsWith(SPINE_CUT_SENTINEL) - ) { + // sentinel and keep the standing token once, including when the standing + // token already ends with "...". + if (goal.endsWith(SPINE_CUT_SENTINEL)) { const prefix = goal.slice(0, -SPINE_CUT_SENTINEL.length); if ( hasStandingOutputToken(prefix, token) ||