mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
fix(claude): keep the first turn working after a settings change before it
Changing thinking (or another query setting) on a Claude agent that has no live query left the restart flag set after a fresh query was created, so the next prompt failed with "Cannot read properties of null (reading 'push')". A fresh query now consumes the pending restart, and the query pump drains the query the turn created instead of re-running ensureQuery. Keeps the submitted-message emission as it was on main; the immediate- failure rewind change from #5950 is not part of this fix. Adds a real Claude daemon test for the reported path: create an agent, set thinking, then send the first message. Co-authored-by: louis1618 <louis.devop@gmail.com> Co-authored-by: oyzh888 <oyzh888@users.noreply.github.com>
This commit is contained in:
co-authored by
louis1618
oyzh888
parent
d3c1cc54bb
commit
870c8dc9c5
-81
@@ -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<unknown> }) =>
|
||||
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<unknown> }) => {
|
||||
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[] = [];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user