Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/agent/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,21 @@ describe("handoff arming (/handoff)", () => {
).toBeNull();
});

test("a failed handoff compact does not schedule the post-compact infer", () => {
const governor = createCompactionGovernor(undefined);
governor.syncFromTurns(tenTurns);
expect(governor.requestHandoff("now do the UI audit")).toBe("armed");
const actions = governor.interceptIdleContinuation(
pivot("now do the UI audit"),
capabilities,
);
expect(actions?.some((a) => a.type === "compact")).toBe(true);
expect(governor.hasOutstandingContinuation()).toBe(true);
governor.noteKeptPrior();
expect(governor.hasOutstandingContinuation()).toBe(false);
expect(governor.resumeAfterCompact(emptyMessage())).toBeNull();
});

test("instructions stay sticky for the summary after the fold fires", () => {
const governor = createCompactionGovernor(undefined);
governor.syncFromTurns(tenTurns);
Expand Down
9 changes: 9 additions & 0 deletions src/agent/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,14 @@ export function createCompactionGovernor(
];
}

// A keep-prior compact (verify abort, summarizer failure) must not resume
// into inference: the continuation is already queued, and inferring on the
// full transcript is what 401/429s the handoff.
function noteKeptPrior(): void {
postCompactInfer = false;
postCompactMeter = false;
}

// After compact, a content-less continuation re-enters decide. "infer"
// resumes the interrupted loop; "meter" adopts the shrunk turns for the
// Ctx display and stays idle (idle empty compact has nothing to answer).
Expand Down Expand Up @@ -559,6 +567,7 @@ export function createCompactionGovernor(
interceptIdleContinuation,
interceptOverflow,
resumeAfterCompact,
noteKeptPrior,
hasOutstandingContinuation,
};
}
30 changes: 30 additions & 0 deletions src/agent/director-compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,36 @@ test("compaction is self-regulating: a cycle back under threshold does not re-co
expect(hasContinuationEmit(arr)).toBe(false);
});

test("a failed handoff compact does not schedule the post-compact infer", async () => {
const director = makeChatDirector();
director.restoreCacheWrite({
at: 0,
source,
turns: manyTurnsState.turns,
});
expect(director.requestHandoff("now do the UI audit")).toBe("armed");
const armed = await director.decide(
{
type: "message.received",
message: { content: "now do the UI audit" },
} as ReactorInboundEvent,
manyTurnsState,
makeCapabilities(),
);
const armedArr = Array.isArray(armed) ? armed : [armed];
expect(armedArr.some((a) => a.type === "compact")).toBe(true);
expect(hasContinuationEmit(armedArr)).toBe(true);
director.noteCompactionKeptPrior();
const resume = await director.decide(
emptyMessageReceived(),
manyTurnsState,
makeCapabilities(),
);
const resumeArr = Array.isArray(resume) ? resume : [resume];
expect(resumeArr.some((a) => a.type === "infer")).toBe(false);
expect(resumeArr.some((a) => a.type === "compact")).toBe(false);
});

test("a fresh director answers an unsolicited empty continuation without inferring", async () => {
const director = makeChatDirector();

Expand Down
10 changes: 10 additions & 0 deletions src/agent/director.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,11 @@ class ChatDirectorImpl extends DefaultDirector {
return this.compaction.requestHandoff(instructions);
}

/** A compact that kept prior context: do not infer on the queued continuation. */
noteCompactionKeptPrior(): void {
this.compaction.noteKeptPrior();
}

// Drop a handoff arming whose pivot never delivered, so the next operator
// message does not fold unexpectedly.
cancelManualCompact(): void {
Expand Down Expand Up @@ -888,6 +893,9 @@ class ChatDirectorImpl extends DefaultDirector {
capabilities: ReactorCapabilities,
): Promise<ReactorAction | ReactorAction[]> {
const afterCompact = this.compaction.resumeAfterCompact(event);
// noteKeptPrior clears the resume flags during apply, before this
// continuation is decided. A keep-prior handoff must wait, not infer
// on the unchanged transcript.
if (afterCompact !== null) {
// Compacted history is the live occupancy until the next
// inference.done; paint from the estimate meanwhile.
Expand Down Expand Up @@ -1358,4 +1366,6 @@ export interface ChatDirector extends ReactorDirector {
requestHandoff(instructions: string): HandoffArming;
/** Drop a handoff arming whose pivot never delivered. */
cancelManualCompact(): void;
/** Compact kept the prior transcript; the queued continuation must not infer. */
noteCompactionKeptPrior(): void;
}
3 changes: 3 additions & 0 deletions src/exec/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,9 @@ export async function runExec(config: Config): Promise<ExecResult> {
onFailure: (text) => {
stderr.write(`${text}\n`);
},
onKeptPrior: () => {
directorHolder.instance?.noteCompactionKeptPrior();
},
onFolded: () => {
// Fold restarts the cached prefix: drop idle execute-promoted
// schemas and persist, so a crash resume cannot re-flush them.
Expand Down
Loading
Loading