fix: canonicalize workspace tab routes and prevent orphan runs

This commit is contained in:
Mohamed Boudra
2026-03-04 17:39:19 +07:00
parent 7578846c27
commit 26ea214d64
16 changed files with 723 additions and 88 deletions
@@ -824,6 +824,161 @@ describe("ClaudeAgentSession interrupt restart regression", () => {
await session.close();
});
test("does not create an orphan autonomous run from pre-replay task_started metadata", async () => {
const logger = createTestLogger();
const keepQueryAlive = deferred<void>();
sdkMocks.query.mockImplementation(({ prompt }: { prompt: AsyncIterable<unknown> }) => {
const readPromptUuid = createPromptUuidReader(prompt);
let step = 0;
return {
next: vi.fn(async () => {
if (step === 0) {
step += 1;
return {
done: false,
value: {
type: "system",
subtype: "init",
session_id: "task-started-fallback-session",
permissionMode: "default",
model: "opus",
},
};
}
if (step === 1) {
step += 1;
return {
done: false,
value: {
type: "assistant",
message: {
id: "tool-call-msg",
content: [
{
type: "tool_use",
id: "toolu_1",
name: "Agent",
input: { description: "verify", prompt: "sub-task" },
},
],
},
},
};
}
if (step === 2) {
step += 1;
return {
done: false,
value: {
type: "system",
subtype: "task_started",
task_id: "task-1",
tool_use_id: "toolu_1",
description: "verify",
task_type: "local_agent",
session_id: "task-started-fallback-session",
uuid: "task-started-1",
},
};
}
if (step === 3) {
step += 1;
return {
done: false,
value: {
type: "stream_event",
event: {
type: "message_delta",
delta: { stop_reason: "tool_use", stop_sequence: null },
usage: buildUsage(),
},
session_id: "task-started-fallback-session",
parent_tool_use_id: null,
uuid: "msg-delta-tool-use",
},
};
}
if (step === 4) {
step += 1;
const promptUuid = (await readPromptUuid()) ?? "missing-prompt-uuid";
return {
done: false,
value: {
type: "user",
message: { role: "user", content: "current prompt" },
parent_tool_use_id: null,
uuid: promptUuid,
session_id: "task-started-fallback-session",
isReplay: true,
},
};
}
if (step === 5) {
step += 1;
return {
done: false,
value: {
type: "assistant",
message: {
content: "FOREGROUND_DONE",
},
},
};
}
if (step === 6) {
step += 1;
return {
done: false,
value: {
type: "result",
subtype: "success",
usage: buildUsage(),
total_cost_usd: 0,
},
};
}
if (step === 7) {
await keepQueryAlive.promise;
return { done: true, value: undefined };
}
return { done: true, value: undefined };
}),
interrupt: vi.fn(async () => undefined),
return: vi.fn(async () => undefined),
setPermissionMode: vi.fn(async () => undefined),
setModel: vi.fn(async () => undefined),
supportedModels: vi.fn(async () => [{ value: "opus", displayName: "Opus" }]),
supportedCommands: vi.fn(async () => []),
rewindFiles: vi.fn(async () => ({ canRewind: true })),
} satisfies QueryMock;
});
const client = new ClaudeAgentClient({ logger });
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
});
const events = await collectUntilTerminal(session.stream("current prompt"));
const assistantText = collectAssistantText(events);
expect(assistantText).toContain("FOREGROUND_DONE");
expect(
(session as unknown as { turnState?: string }).turnState ?? null
).toBe("idle");
expect(
(
session as unknown as {
runTracker?: { listActiveRuns: (owner?: "foreground" | "autonomous") => unknown[] };
}
).runTracker?.listActiveRuns("autonomous") ?? []
).toHaveLength(0);
keepQueryAlive.resolve(undefined);
await session.close();
});
test("emits autonomous live events from SDK stream when Claude wakes itself", async () => {
const logger = createTestLogger();
let queryCreateCount = 0;
@@ -2693,6 +2693,9 @@ class ClaudeAgentSession implements AgentSession {
// first turn cannot be stranded in autonomous fallback. If metadata churn
// was observed pre-replay, stay conservative and wait for replay.
if (!run.promptReplaySeen) {
if (this.isToolUseBoundaryStreamEvent(input.message)) {
return true;
}
// Keep pre-replay result events with the foreground run so stale result
// bursts cannot consume autonomous wake reservations.
if (message.type === "result") {
@@ -2718,6 +2721,21 @@ class ClaudeAgentSession implements AgentSession {
return true;
}
private isToolUseBoundaryStreamEvent(message: SDKMessage): boolean {
if (message.type !== "stream_event") {
return false;
}
const event = (message as unknown as { event?: Record<string, unknown> }).event;
if (!event || event.type !== "message_delta") {
return false;
}
const delta =
"delta" in event && event.delta && typeof event.delta === "object"
? (event.delta as Record<string, unknown>)
: null;
return delta?.stop_reason === "tool_use";
}
private notePreReplayMetadata(message: SDKMessage): void {
if (this.turnState !== "foreground") {
return;