Skip to content

Commit 5a251e1

Browse files
Merge pull request #1463 from corbitsdev/fix/compaction-verify-overflow-fold
fix(session): commit folds when handoff carries verify overflow
2 parents 4d5c550 + 893c90e commit 5a251e1

5 files changed

Lines changed: 440 additions & 61 deletions

File tree

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

Lines changed: 227 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
repairSummary,
1515
verifyCompactionSummary,
1616
verifyOrRepair,
17+
verifyAbortNoticeLabel,
1718
VERIFY_REPAIR_HEADING,
1819
} from "./compaction-verify.js";
1920
import type {
@@ -421,7 +422,7 @@ describe("verifyCompactionSummary", () => {
421422
}
422423
});
423424

424-
test("uppercase HTTP URL scores hostname case-insensitively", () => {
425+
test("a URL path is required; hostname alone does not cover it", () => {
425426
const facts = extractContinuationFacts([
426427
textTurn("user", "Fix auth now"),
427428
{
@@ -442,6 +443,12 @@ describe("verifyCompactionSummary", () => {
442443
"Fix auth now. Called api.com.",
443444
facts,
444445
).misses.some((m) => m.kind === "exactName"),
446+
).toBe(true);
447+
expect(
448+
verifyCompactionSummary(
449+
"Fix auth now. Called API.COM/v1.",
450+
facts,
451+
).misses.some((m) => m.kind === "exactName"),
445452
).toBe(false);
446453
expect(
447454
verifyCompactionSummary(
@@ -453,6 +460,12 @@ describe("verifyCompactionSummary", () => {
453460
});
454461

455462
describe("verifyOrRepair", () => {
463+
test("names contradiction and repair overflow instead of a bare verify", () => {
464+
expect(verifyAbortNoticeLabel("contradiction")).toBe("contradiction");
465+
expect(verifyAbortNoticeLabel("repair-overflow")).toBe("repair overflow");
466+
expect(verifyAbortNoticeLabel(undefined)).toBe("kept-prior");
467+
});
468+
456469
test("repairs a lossy handoff instead of shipping it", () => {
457470
const facts = extractContinuationFacts(droppedTurns());
458471
const outcome = verifyOrRepair(
@@ -477,6 +490,7 @@ describe("verifyOrRepair", () => {
477490
);
478491
expect(outcome.aborted).toBe(true);
479492
expect(outcome.repaired).toBe(false);
493+
expect(outcome.abortReason).toBe("contradiction");
480494
});
481495

482496
test("truncating repair that still misses exactName aborts", () => {
@@ -495,9 +509,10 @@ describe("verifyOrRepair", () => {
495509
timestamp: 2,
496510
},
497511
]);
498-
const outcome = verifyOrRepair("Fix auth now. Work continues.", facts, 90);
512+
const outcome = verifyOrRepair("Fix auth now. Work continues.", facts, 80);
499513
expect(outcome.aborted).toBe(true);
500514
expect(outcome.repaired).toBe(false);
515+
expect(outcome.abortReason).toBe("repair-overflow");
501516
expect(outcome.misses.some((m) => m.kind === "exactName")).toBe(true);
502517
});
503518

@@ -527,6 +542,26 @@ describe("verifyOrRepair", () => {
527542
expect(outcome.summary).toContain("exact-file.ts");
528543
});
529544

545+
test("shrinks model prose so a carry-forward that fits the cap still verifies", () => {
546+
const facts = extractContinuationFacts(droppedTurns());
547+
const prose = "Padded model prose. ".repeat(40);
548+
const misses = verifyCompactionSummary(prose, facts).misses;
549+
const full = repairSummary(prose, facts, misses);
550+
const appendixAt = full.indexOf(VERIFY_REPAIR_HEADING);
551+
expect(appendixAt).toBeGreaterThan(200);
552+
const appendix = full.slice(appendixAt).replace(/^\n+/, "");
553+
const cap = appendix.length + 8;
554+
expect(full.length).toBeGreaterThan(cap);
555+
const outcome = verifyOrRepair(prose, facts, cap);
556+
expect(outcome.aborted).toBe(false);
557+
expect(outcome.repaired).toBe(true);
558+
expect(outcome.summary.length).toBeLessThanOrEqual(cap);
559+
expect(outcome.summary).toContain(VERIFY_REPAIR_HEADING);
560+
expect(verifyCompactionSummary(outcome.summary, facts).supported).toBe(
561+
true,
562+
);
563+
});
564+
530565
test("a constraint-only residual after a sliced repair aborts", () => {
531566
const facts = extractContinuationFacts([
532567
textTurn("user", "Fix auth now"),
@@ -662,10 +697,200 @@ describe("pruning compactor verify pass", () => {
662697
expect(allText(result.output)).toContain("auth.ts");
663698
} else if (kind === "aborted") {
664699
expect(result.output).toBe(turns);
700+
expect(result.record.decisions.verifyAbortReason).toBe("contradiction");
665701
} else {
666702
expect(allText(result.output)).not.toContain(VERIFY_REPAIR_HEADING);
667703
}
668704
});
705+
706+
test("an over-cap repair commits the fold and the handoff keeps critical facts", async () => {
707+
const folded: ConversationTurn[] = [
708+
textTurn("user", "Fix auth now"),
709+
{
710+
role: "assistant",
711+
content: [
712+
{
713+
type: "tool_call",
714+
id: "c1",
715+
name: "read_file",
716+
arguments: { path: "src/very-long-unique-path/exact-file.ts" },
717+
},
718+
],
719+
timestamp: 2,
720+
},
721+
{
722+
role: "user",
723+
content: [
724+
{
725+
type: "tool_result",
726+
callId: "c1",
727+
isError: true,
728+
content: [
729+
{
730+
type: "text",
731+
text: "ZEBRA-BLOCKER-UNIQUE token refresh exploded",
732+
},
733+
],
734+
},
735+
],
736+
timestamp: 3,
737+
},
738+
textTurn(
739+
"assistant",
740+
"Next step is to patch the zebra handler QUOKKA-NEXT",
741+
),
742+
textTurn("user", "Do not commit generated artifacts ever."),
743+
];
744+
const turns: ConversationTurn[] = [
745+
...folded,
746+
textTurn("user", "recent ask"),
747+
textTurn("assistant", "recent reply"),
748+
];
749+
const compactor = createPruningCompactor({
750+
summaryMaxChars: 80,
751+
compactionShape: { tailBudgetTokens: 10 },
752+
summarize: async () => "Work continues.",
753+
});
754+
const result = await compactor.apply(turns, mockStrategyCtx);
755+
expect(result.record.decisions.verifyAborted).toBeUndefined();
756+
expect(result.record.reason.startsWith("compacted")).toBe(true);
757+
expect(result.output.length).toBeLessThan(turns.length);
758+
const file = handoffFileText(result);
759+
expect(file).toContain("exact-file.ts");
760+
expect(file).toContain("ZEBRA-BLOCKER-UNIQUE");
761+
expect(file).toContain("QUOKKA-NEXT");
762+
expect(file).toContain("generated artifacts");
763+
expect(file.toLowerCase()).toContain("fix auth");
764+
const spine = allText(result.output);
765+
expect(spine).toContain(COMPACTED_PREFIX);
766+
expect(spine).not.toMatch(/\bno\s+errors?\b/i);
767+
});
768+
769+
test("shrink-to-fit does not ship a spine that denies recorded failures", async () => {
770+
const folded: ConversationTurn[] = [
771+
textTurn("user", "Fix auth now"),
772+
{
773+
role: "assistant",
774+
content: [
775+
{
776+
type: "tool_call",
777+
id: "c1",
778+
name: "read_file",
779+
arguments: { path: "src/auth.ts" },
780+
},
781+
],
782+
timestamp: 2,
783+
},
784+
{
785+
role: "user",
786+
content: [
787+
{
788+
type: "tool_result",
789+
callId: "c1",
790+
isError: true,
791+
content: [{ type: "text", text: "token refresh assertion failed" }],
792+
},
793+
],
794+
timestamp: 3,
795+
},
796+
textTurn("assistant", "Fix the token refresh assertion next."),
797+
textTurn("user", "No errors remain"),
798+
textTurn("user", "Keep going on the handler"),
799+
];
800+
const turns: ConversationTurn[] = [
801+
...folded,
802+
textTurn("user", "recent ask"),
803+
textTurn("assistant", "recent reply"),
804+
];
805+
const prose = "Padded model prose. ".repeat(40);
806+
const facts = extractContinuationFacts(folded);
807+
const misses = verifyCompactionSummary(prose, facts).misses;
808+
const full = repairSummary(prose, facts, misses);
809+
const appendixAt = full.indexOf(VERIFY_REPAIR_HEADING);
810+
const appendix = full.slice(appendixAt).replace(/^\n+/, "");
811+
const cap = appendix.length + 8;
812+
expect(full.length).toBeGreaterThan(cap);
813+
const shrunk = verifyOrRepair(prose, facts, cap);
814+
expect(shrunk.aborted).toBe(false);
815+
expect(shrunk.summary.length).toBeLessThanOrEqual(cap);
816+
817+
const apply = (summaryMaxChars: number) =>
818+
createPruningCompactor({
819+
summaryMaxChars,
820+
compactionShape: { tailBudgetTokens: 10 },
821+
summarize: async () => prose,
822+
}).apply(turns, mockStrategyCtx);
823+
824+
const fitted = await apply(cap);
825+
const fittedSpine = allText(fitted.output);
826+
const committed = fitted.output.length < turns.length;
827+
if (committed) {
828+
expect(fittedSpine).not.toMatch(/\bno\s+errors?\s+remain\b/i);
829+
} else {
830+
expect(fitted.output).toBe(turns);
831+
expect(fitted.record.decisions.verifyAbortReason).toBe("contradiction");
832+
}
833+
expect(committed && /\bno\s+errors?\s+remain\b/i.test(fittedSpine)).toBe(
834+
false,
835+
);
836+
837+
const tight = await apply(80);
838+
expect(tight.output).toBe(turns);
839+
expect(tight.record.decisions.verifyAbortReason).toBe("contradiction");
840+
});
841+
842+
test("an over-cap fold does not commit without the exact URL path", async () => {
843+
const url = "https://api.example/hooks/QUOKKA-CALLBACK";
844+
const folded: ConversationTurn[] = [
845+
textTurn("user", "Register the callback host api.example"),
846+
{
847+
role: "assistant",
848+
content: [
849+
{
850+
type: "tool_call",
851+
id: "c1",
852+
name: "web_fetch",
853+
arguments: { url },
854+
},
855+
],
856+
timestamp: 2,
857+
},
858+
textTurn("assistant", "Next step is to patch the handler QUOKKA-NEXT"),
859+
textTurn("user", "Do not commit generated artifacts ever."),
860+
];
861+
const turns: ConversationTurn[] = [
862+
...folded,
863+
textTurn("user", "recent ask"),
864+
textTurn("assistant", "recent reply"),
865+
];
866+
const prose = "Work continues.";
867+
const facts = extractContinuationFacts(folded);
868+
expect(facts.exactNames).toContain(url);
869+
const misses = verifyCompactionSummary(prose, facts).misses;
870+
const full = repairSummary(prose, facts, misses);
871+
const exactAt = full.indexOf("Exact references:");
872+
expect(exactAt).toBeGreaterThan(0);
873+
expect(full.slice(0, exactAt)).toContain("api.example");
874+
expect(full.slice(0, exactAt)).not.toContain("QUOKKA-CALLBACK");
875+
876+
const result = await createPruningCompactor({
877+
summaryMaxChars: exactAt,
878+
compactionShape: { tailBudgetTokens: 10 },
879+
summarize: async () => prose,
880+
}).apply(turns, mockStrategyCtx);
881+
882+
const file = handoffFileText(result);
883+
const spine = allText(result.output);
884+
const keptPrior = result.output === turns;
885+
const pathKept =
886+
file.includes("hooks/QUOKKA-CALLBACK") ||
887+
spine.includes("hooks/QUOKKA-CALLBACK");
888+
expect(keptPrior || pathKept).toBe(true);
889+
if (!keptPrior) {
890+
expect(result.record.decisions.verifyAborted).toBeUndefined();
891+
expect(result.record.reason.startsWith("compacted")).toBe(true);
892+
}
893+
});
669894
});
670895

671896
describe("CL-9007 budgeted tail (shared auto+manual pipeline)", () => {

0 commit comments

Comments
 (0)