Skip to content

Commit cdb71f5

Browse files
committed
fix(compaction): restore idle arming and prior fold guidance on cancel
1 parent 5ccec45 commit cdb71f5

3 files changed

Lines changed: 84 additions & 3 deletions

File tree

‎src/agent/compaction.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
LEGACY_COMPACT_SPACER_TEXT,
2222
compactorNoOpFloor,
2323
} from "../session/compactor.js";
24+
import { buildSummaryPrompt } from "../session/summarizer.js";
2425

2526
const capabilities = {
2627
infer: (options?: unknown) => ({
@@ -903,6 +904,20 @@ describe("handoff arming (/handoff)", () => {
903904
).toBeNull();
904905
});
905906

907+
test("cancelManual restores extraInstructions from a prior successful fold", () => {
908+
const governor = createCompactionGovernor(undefined);
909+
governor.syncFromTurns(tenTurns);
910+
expect(governor.requestHandoff("keep the UI audit")).toBe("armed");
911+
governor.interceptIdleContinuation(
912+
pivot("keep the UI audit"),
913+
capabilities,
914+
);
915+
expect(governor.extraInstructions).toBe("keep the UI audit");
916+
expect(governor.requestHandoff("failed pivot: drop this")).toBe("armed");
917+
governor.cancelManual();
918+
expect(governor.extraInstructions).toBe("keep the UI audit");
919+
});
920+
906921
test("cancelManual clears sticky extraInstructions from a failed pivot", () => {
907922
const governor = createCompactionGovernor(undefined);
908923
governor.syncFromTurns(tenTurns);
@@ -911,6 +926,55 @@ describe("handoff arming (/handoff)", () => {
911926
expect(governor.extraInstructions).toBeUndefined();
912927
});
913928

929+
test("failed-pivot instructions are not in a later threshold summary prompt", () => {
930+
const governor = createCompactionGovernor(undefined);
931+
governor.syncFromTurns(tenTurns);
932+
expect(governor.requestHandoff("now do the UI audit")).toBe("armed");
933+
governor.cancelManual();
934+
governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns);
935+
expect(
936+
governor.interceptActions(toolDone(), inferAction, capabilities),
937+
).not.toBeNull();
938+
const prompt = buildSummaryPrompt(
939+
tenTurns,
940+
governor.extraInstructions !== undefined
941+
? { extraInstructions: governor.extraInstructions }
942+
: undefined,
943+
);
944+
expect(prompt).not.toContain("now do the UI audit");
945+
expect(prompt).not.toContain("Operator compact instructions");
946+
});
947+
948+
test("cancelManual does not invent pending", () => {
949+
const governor = createCompactionGovernor(undefined);
950+
governor.syncFromTurns(tenTurns);
951+
expect(governor.requestHandoff("now do the UI audit")).toBe("armed");
952+
governor.cancelManual();
953+
expect(
954+
governor.interceptActions(toolDone(), inferAction, capabilities),
955+
).toBeNull();
956+
});
957+
958+
test("cancelManual restores idlePending so an idle threshold fold still fires", () => {
959+
const governor = createCompactionGovernor(undefined);
960+
governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns);
961+
expect(
962+
governor.noteIdleTurn(inferenceDone(overThreshold), [
963+
{ type: "reply", content: "done" },
964+
]),
965+
).toBe(true);
966+
expect(governor.requestHandoff("now do the UI audit")).toBe("armed");
967+
governor.cancelManual();
968+
const actions = governor.interceptIdleContinuation(
969+
emptyMessage(),
970+
capabilities,
971+
);
972+
expect(actions).not.toBeNull();
973+
expect(actions?.find((a) => a.type === "compact")).toMatchObject({
974+
reason: "context-threshold",
975+
});
976+
});
977+
914978
test("threshold pending survives cancelManual so a tool pause still folds", () => {
915979
const governor = createCompactionGovernor(undefined);
916980
governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns);

‎src/agent/compaction.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export function createCompactionGovernor(
7878
// instructions still fold with the default structured summary; a non-empty
7979
// argument is kept for later auto-folds and written into the compact record.
8080
let extraInstructions: string | undefined;
81+
// Snapshots taken at requestHandoff so cancelManual can restore the
82+
// pre-pivot idle arming and a prior successful fold's guidance.
83+
let idlePendingAtHandoff = false;
84+
let extraInstructionsAtHandoff: string | undefined;
8185
let postCompactInfer = false;
8286
// Idle empty compact needs a post-compact decide cycle to adopt the shrunk
8387
// turns for the meter, but must not start a new inference (there is no
@@ -386,6 +390,8 @@ export function createCompactionGovernor(
386390
// the single operator fold, because firing clears the arming.
387391
function requestHandoff(instructions: string): HandoffArming {
388392
if (turnCount <= MIN_TURNS_TO_COMPACT) return "noop";
393+
idlePendingAtHandoff = idlePending;
394+
extraInstructionsAtHandoff = extraInstructions;
389395
const trimmed = instructions.trim();
390396
if (trimmed.length > 0) extraInstructions = trimmed;
391397
manualPending = true;
@@ -398,13 +404,16 @@ export function createCompactionGovernor(
398404

399405
// Disarm after a pivot send that never delivered: without this the next
400406
// operator message would fold unexpectedly. Threshold `pending` is independent
401-
// of the failed pivot and must still fire at the next tool pause. Sticky
402-
// extraInstructions belong to a successful fold, not a cancelled one.
407+
// of the failed pivot and must still fire at the next tool pause. Restore
408+
// idlePending and extraInstructions from the requestHandoff snapshots so a
409+
// cancelled pivot neither invents an idle fold nor wipes a prior successful
410+
// fold's guidance.
403411
function cancelManual(): void {
404412
const thresholdPending = pending;
405413
clearManualArming();
406414
pending = thresholdPending;
407-
extraInstructions = undefined;
415+
idlePending = idlePendingAtHandoff;
416+
extraInstructions = extraInstructionsAtHandoff;
408417
}
409418

410419
return {

‎tests/unit/summarizer.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ test("buildSummaryPrompt omits workflow preamble when none active", () => {
8585
expect(prompt).toContain("Session excerpt");
8686
});
8787

88+
test("buildSummaryPrompt injects extra operator compact instructions", () => {
89+
const prompt = buildSummaryPrompt(turns(), {
90+
extraInstructions: "now do the UI audit",
91+
});
92+
expect(prompt).toContain("Operator compact instructions");
93+
expect(prompt).toContain("now do the UI audit");
94+
});
95+
8896
test("model summarizer returns the model output", async () => {
8997
const summarize = createModelSummarizer({
9098
getSource: () => source,

0 commit comments

Comments
 (0)