Complete (not cancel) autonomous turns on interrupt

When a foreground message interrupts an autonomous turn, the notification
content has already been dispatched to subscribers. Canceling says "this
turn didn't happen" — but it did. Complete it instead, preserving the
lifecycle semantics and preventing notification loss.

- Change interrupt() to call completeAutonomousTurn instead of
  cancelAutonomousTurn, with flushPendingToolCalls before completion
- Remove dead cancelAutonomousTurn method (no remaining callers)
- Fix autonomous wake tests B and C to handle notification/foreground
  timing race: when task_notification arrives during a foreground turn,
  there is no separate autonomous running edge afterwards
This commit is contained in:
Mohamed Boudra
2026-03-25 17:37:00 +07:00
parent 4c1b11f733
commit 253319ee50
2 changed files with 42 additions and 33 deletions
@@ -1381,7 +1381,8 @@ class ClaudeAgentSession implements AgentSession {
}
if (this.autonomousTurn) {
this.cancelAutonomousTurn("Interrupted");
this.flushPendingToolCalls();
this.completeAutonomousTurn();
}
await this.interruptActiveTurn();
@@ -2154,20 +2155,6 @@ class ClaudeAgentSession implements AgentSession {
this.syncTurnState("autonomous turn completed");
}
private cancelAutonomousTurn(reason: string): void {
if (!this.autonomousTurn) {
return;
}
this.flushPendingToolCalls();
this.notifySubscribers({
type: "turn_canceled",
provider: "claude",
reason,
});
this.autonomousTurn = null;
this.syncTurnState("autonomous turn canceled");
}
private failActiveTurns(errorMessage: string): void {
const failure = this.buildTurnFailedEvent(errorMessage);
this.flushPendingToolCalls();
@@ -463,13 +463,27 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
expect(helloFinish.status).toBe("idle");
expect((helloFinish.lastMessage ?? "").trim().toUpperCase()).toContain("HELLO");
await client.waitForAgentUpsert(
agent.id,
(snapshot) => snapshot.status === "running",
30_000,
);
const autonomousFinish = await client.waitForFinish(agent.id, 120_000);
expect(autonomousFinish.status).toBe("idle");
// The background task may complete before, during, or after the HELLO
// turn. When the task_notification races with HELLO, the notification
// is handled during the foreground turn and there is no separate
// autonomous running edge afterwards. Wait for a possible autonomous
// wake; if none arrives within the expected sleep window, verify the
// agent settled to idle (notification was already processed).
const autonomousWake = await client
.waitForAgentUpsert(
agent.id,
(snapshot) => snapshot.status === "running",
15_000,
)
.catch(() => null);
if (autonomousWake) {
const autonomousFinish = await client.waitForFinish(agent.id, 120_000);
expect(autonomousFinish.status).toBe("idle");
} else {
const current = await client.fetchAgent(agent.id);
expect(current.agent.status).toBe("idle");
}
} finally {
await client.close();
await daemon.close();
@@ -521,19 +535,27 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
const helloFinish = await client.waitForFinish(agent.id, 180_000);
expect(helloFinish.status).toBe("idle");
await client.waitForAgentUpsert(
agent.id,
(snapshot) => snapshot.status === "running",
30_000,
);
// The second background task may complete before, during, or after
// HELLO. When it races with HELLO, the notification is handled during
// the foreground turn and there is no separate autonomous running edge.
const autonomousWake = await client
.waitForAgentUpsert(
agent.id,
(snapshot) => snapshot.status === "running",
15_000,
)
.catch(() => null);
const startedAt = Date.now();
await client.cancelAgent(agent.id);
const interruptDurationMs = Date.now() - startedAt;
expect(interruptDurationMs).toBeLessThan(10_000);
if (autonomousWake) {
const startedAt = Date.now();
await client.cancelAgent(agent.id);
const interruptDurationMs = Date.now() - startedAt;
expect(interruptDurationMs).toBeLessThan(10_000);
const settled = await client.waitForFinish(agent.id, 20_000);
expect(settled.status).toBe("idle");
}
const settled = await client.waitForFinish(agent.id, 20_000);
expect(settled.status).toBe("idle");
const finalResult = await client.fetchAgent(agent.id);
expect(finalResult?.agent.status).toBe("idle");
} finally {