Skip to content

Commit d4e2cb2

Browse files
committed
fix(session): abort residual repair misses and bound scorer tokens
A cap that slices the repair tail used to ship a spine that still missed exact names, and the scorer treated auth as present inside authored. Abort any residual after the cap, and match tokens on word boundaries.
1 parent eb6f353 commit d4e2cb2

3 files changed

Lines changed: 64 additions & 9 deletions

File tree

‎src/context-compactor.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,11 +1220,12 @@ describe("buildTurnSummary via createPruningCompactor", () => {
12201220
const turns: ConversationTurn[] = [
12211221
makeTurn({
12221222
role: "user",
1223-
content: [{ type: "text", text: "a".repeat(500) }],
1223+
// Short tokens so the verify pass is vacuous; length still overflows.
1224+
content: [{ type: "text", text: "yes ".repeat(200) }],
12241225
}),
12251226
makeTurn({
12261227
role: "assistant",
1227-
content: [{ type: "text", text: "b".repeat(500) }],
1228+
content: [{ type: "text", text: "ok ".repeat(200) }],
12281229
}),
12291230
makeTurn({ role: "user", content: [{ type: "text", text: "recent" }] }),
12301231
];

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ describe("verifyCompactionSummary", () => {
139139
expect(kinds).toContain("exactName");
140140
});
141141

142+
test("auth as a goal token does not match authored", () => {
143+
const facts = extractContinuationFacts([textTurn("user", "Fix auth now")]);
144+
const report = verifyCompactionSummary(
145+
"The authored notes: next step module plan is set.",
146+
facts,
147+
);
148+
expect(report.supported).toBe(false);
149+
expect(report.misses.some((m) => m.kind === "goal")).toBe(true);
150+
});
151+
142152
test("denying failure while errors were dropped is a contradiction", () => {
143153
const facts = extractContinuationFacts(droppedTurns());
144154
const report = verifyCompactionSummary(
@@ -176,6 +186,29 @@ describe("verifyOrRepair", () => {
176186
expect(outcome.repaired).toBe(false);
177187
});
178188

189+
test("truncating repair that still misses exactName aborts", () => {
190+
const facts = extractContinuationFacts([
191+
textTurn("user", "Fix auth now"),
192+
{
193+
role: "assistant",
194+
content: [
195+
{
196+
type: "tool_call",
197+
id: "c1",
198+
name: "read_file",
199+
arguments: { path: "src/very-long-unique-path/exact-file.ts" },
200+
},
201+
],
202+
timestamp: 2,
203+
},
204+
]);
205+
const outcome = verifyOrRepair("Fix auth now. Work continues.", facts, 90);
206+
expect(outcome.aborted).toBe(true);
207+
expect(outcome.repaired).toBe(false);
208+
const shipped = verifyCompactionSummary(outcome.summary, facts);
209+
expect(shipped.misses.some((m) => m.kind === "exactName")).toBe(true);
210+
});
211+
179212
test("repairSummary names only what the handoff missed", () => {
180213
const facts = extractContinuationFacts(droppedTurns());
181214
const repaired = repairSummary(

‎src/session/compaction-verify.ts‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,34 @@ function significantTokens(text: string, cap = 24): string[] {
127127

128128
// Half (rounded up) of the fact's content words must appear in the summary.
129129
// Short facts need all of their words: one shared word proves nothing.
130+
// Match on token boundaries so "auth" does not score against "authored".
131+
function isTokenChar(ch: string | undefined): boolean {
132+
if (ch === undefined) return false;
133+
const code = ch.charCodeAt(0);
134+
return (
135+
(code >= 48 && code <= 57) || (code >= 97 && code <= 122) || code === 95
136+
);
137+
}
138+
139+
function tokenAppears(token: string, lowered: string): boolean {
140+
let from = 0;
141+
for (;;) {
142+
const i = lowered.indexOf(token, from);
143+
if (i < 0) return false;
144+
if (
145+
!isTokenChar(i === 0 ? undefined : lowered[i - 1]) &&
146+
!isTokenChar(lowered[i + token.length])
147+
)
148+
return true;
149+
from = i + 1;
150+
}
151+
}
152+
130153
function tokensSupported(fact: string, summary: string): boolean {
131154
const tokens = significantTokens(fact);
132155
if (tokens.length === 0) return true;
133156
const lowered = summary.toLowerCase();
134-
const hits = tokens.filter((t) => lowered.includes(t)).length;
157+
const hits = tokens.filter((t) => tokenAppears(t, lowered)).length;
135158
const needed =
136159
tokens.length <= 2 ? tokens.length : Math.ceil(tokens.length / 2);
137160
return hits >= needed;
@@ -404,8 +427,8 @@ export function repairSummary(
404427
/**
405428
* Verify a candidate handoff, repairing once or aborting the fold.
406429
* A contradiction aborts outright: an appended correction cannot retract the
407-
* handoff's false denial. A truncating repair that still drops the goal
408-
* aborts too — the fold ships the goal or it does not ship.
430+
* handoff's false denial. A truncating repair that still misses any fact
431+
* aborts too — never ship a lying spine.
409432
*/
410433
export function verifyOrRepair(
411434
summary: string,
@@ -424,10 +447,8 @@ export function verifyOrRepair(
424447
? repairedFull.slice(0, maxChars)
425448
: repairedFull;
426449
const second = verifyCompactionSummary(repaired, facts);
427-
if (
428-
second.misses.some((m) => m.kind === "contradiction" || m.kind === "goal")
429-
)
430-
return { summary, repaired: false, aborted: true, misses: first.misses };
450+
if (second.misses.length > 0)
451+
return { summary, repaired: false, aborted: true, misses: second.misses };
431452
return {
432453
summary: repaired,
433454
repaired: true,

0 commit comments

Comments
 (0)