diff --git a/packages/server/src/server/agent/providers/claude/agent.interrupt-restart-regression.test.ts b/packages/server/src/server/agent/providers/claude/agent.interrupt-restart-regression.test.ts index e90907cad8..153f6c632a 100644 --- a/packages/server/src/server/agent/providers/claude/agent.interrupt-restart-regression.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.interrupt-restart-regression.test.ts @@ -2,7 +2,6 @@ import { afterEach, expect, test, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; import { ClaudeAgentClient } from "./agent.js"; -import { AgentManager } from "../../agent-manager.js"; import { streamSession } from "../test-utils/session-stream-adapter.js"; import type { AgentSession, AgentStreamEvent } from "../../agent-sdk-types.js"; @@ -319,86 +318,6 @@ test.each([false, true])( }, ); -test("publishes the submitted prompt identity before an immediate provider failure", async () => { - queryFactory.mockImplementation(({ prompt }: { prompt: AsyncIterable }) => - createScriptedQuery({ - prompt, - sessionId: "immediate-failure-session", - handlePrompt: ({ query }) => - query.emit({ - type: "result", - subtype: "error_during_execution", - session_id: "immediate-failure-session", - errors: ["session limit reached"], - }), - }), - ); - const session = await new ClaudeAgentClient({ - logger: createTestLogger(), - queryFactory, - resolveBinary: async () => "/test/claude/bin", - }).createSession({ provider: "claude", cwd: process.cwd() }); - try { - const events = await collectUntilTerminal( - streamSession(session, "continue", { clientMessageId: "client-failed-prompt" }), - ); - expect(events.at(-1)?.type).toBe("turn_failed"); - expect(events).toContainEqual( - expect.objectContaining({ - type: "timeline", - item: expect.objectContaining({ - type: "user_message", - text: "continue", - clientMessageId: "client-failed-prompt", - messageId: expect.any(String), - }), - }), - ); - } finally { - await session.close(); - } -}); - -test("rewind resolves a failed prompt's client identity to its Claude checkpoint", async () => { - const queries: ScriptedQuery[] = []; - queryFactory.mockImplementation(({ prompt }: { prompt: AsyncIterable }) => { - const query = createScriptedQuery({ - prompt, - sessionId: "failed-prompt-rewind-session", - handlePrompt: ({ query: activeQuery }) => - activeQuery.emit({ - type: "result", - subtype: "error_during_execution", - session_id: "failed-prompt-rewind-session", - errors: ["session limit reached"], - }), - }); - queries.push(query); - return query; - }); - const client = new ClaudeAgentClient({ - logger: createTestLogger(), - queryFactory, - resolveBinary: async () => "/test/claude/bin", - }); - const manager = new AgentManager({ clients: { claude: client }, logger: createTestLogger() }); - const agent = await manager.createAgent({ provider: "claude", cwd: process.cwd() }, undefined, { - workspaceId: undefined, - }); - try { - await collectUntilTerminal( - manager.streamAgent(agent.id, "continue", { clientMessageId: "client-failed-prompt" }), - ); - expect(manager.getAgent(agent.id)?.lastError).toBe("session limit reached"); - await manager.rewind(agent.id, "client-failed-prompt", "files"); - expect(queries[0].rewindFiles).toHaveBeenCalledWith(queries[0].prompts[0].uuid, { - dryRun: false, - }); - } finally { - await manager.closeAgent(agent.id); - } -}); - test("interrupt only calls query.interrupt and leaves the query open", async () => { const logger = createTestLogger(); const queries: ScriptedQuery[] = []; diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 925a8412de..935bd94be5 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -2403,9 +2403,13 @@ class ClaudeAgentSession implements AgentSession { } this.activeForegroundQuery = this.query; this.activeForegroundInput = this.input; - this.input.push(sdkMessage); - this.emitSubmittedUserMessage(sdkMessage, turnId, options?.clientMessageId); this.startQueryPump(); + this.input.push(sdkMessage); + setTimeout(() => { + if (this.activeForegroundTurnId === turnId) { + this.emitSubmittedUserMessage(sdkMessage, turnId, options?.clientMessageId); + } + }, 0); } catch (error) { if (sdkUserMessageId) this.unstartedMessageUuids.delete(sdkUserMessageId); this.finishForegroundTurn( diff --git a/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts index c1640a7d2d..03fd284c0d 100644 --- a/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts @@ -115,4 +115,43 @@ describe("daemon E2E (real claude) - thinking effort memory", () => { rmSync(cwd, { recursive: true, force: true }); } }, 420_000); + + test("a thinking change before the first turn does not fail that turn", async () => { + const logger = pino({ level: "silent" }); + const cwd = tmpCwd(); + const daemon = await createTestPaseoDaemon({ + agentClients: createRealProviderClients(["claude"], logger), + logger, + }); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + + try { + await client.connect(); + await client.fetchAgents({ subscribe: {} }); + + const modelList = await client.listProviderModels("claude", { cwd }); + const model = modelList.models.find(modelHasLowThinkingOption); + if (!model) { + throw new Error("No Claude Sonnet model with low thinking effort returned"); + } + + const agent = await client.createAgent({ + cwd, + title: "claude-thinking-before-first-turn-real", + ...getRealProviderConfig("claude"), + model: model.id, + }); + await client.setAgentThinkingOption(agent.id, "low"); + + await client.sendMessage(agent.id, "Reply exactly: ACK_FIRST"); + const finish = await client.waitForFinish(agent.id, 180_000); + expect(finish.final?.lastError).toBeUndefined(); + expect(finish.status).toBe("idle"); + expect(compactText(await getAssistantText(client, agent.id))).toContain("ack_first"); + } finally { + await client.close().catch(() => undefined); + await daemon.close().catch(() => undefined); + rmSync(cwd, { recursive: true, force: true }); + } + }, 300_000); });