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
4 changes: 2 additions & 2 deletions packages/core/src/runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ const PAUSE_GRACE_MS = 8000;
/** Continuations a Stop hook (`/goal`) forces in a row without a tool call before Godmode ends the run. */
const GOAL_IDLE_LIMIT = 2;
const SESSION_MISSING = /no conversation found|session(?: id)? [^\n]{0,80}not found|could not find session|no such session/i;
const AUTH_PROBLEM = /not logged in|please run \/login|invalid api key|authentication_error|oauth token (?:has )?expired|credit balance is too low/i;
const AUTH_PROBLEM = /not logged in|please run \/login|invalid api key|authentication_error|oauth (?:token|session) (?:has )?expired|credit balance is too low/i;
/** Built-in Claude Code tools of a dream run: reading and editing the memory files, nothing else. */
const DREAM_TOOLS = "Read,Write,Edit,Glob,Grep";
/**
Expand Down Expand Up @@ -2786,7 +2786,7 @@ async function finalize(job: Job, outcome: Outcome, agent: Agent | null, started
if (!wasRunning || !finished || !agent) return;
// A dream's summary may talk about past login trouble — that is no new missing login. Neither is Claude Code's own
// error ("Failed to authenticate. API Error: 403") standing in for the answer.
const claudeError = outcome.status === "failed" && (text.trim() === error?.trim() || /^(?:Failed to authenticate\.|API Error:)/.test(text.trim()));
const claudeError = outcome.status === "failed" && (text.trim() === error?.trim() || /^(?:Failed to authenticate[.:]|API Error:)/.test(text.trim()));
if (outcome.status !== "cancelled" && job.trigger !== "dream" && !claudeError) await detectMissingLogin(job, agent, text);

// Condition checks change nothing worth a commit; the task run that follows commits as usual. Dreams commit their
Expand Down
7 changes: 7 additions & 0 deletions packages/core/test/fixtures/fake-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* SLEEP emit init, then hang (cancel / timeout tests)
* LOGIN_FAIL answer with a login-failure sentence
* AUTH_403 fail like Claude Code when Claude's API refuses the sign-in ("Failed to authenticate. API Error: 403 …")
* AUTH_EXPIRED fail like Claude Code when its sign-in ran out ("Failed to authenticate: OAuth session expired …")
* CALL_MCP call the Godmode MCP gateway from --mcp-config (initialize, tools/list, report_missing_login)
* CALL_COMPUTER call the `computer` MCP server from --mcp-config (initialize, tools/list, computer_info) and answer
* with a JSON summary; "no computer server" when the run has none
Expand Down Expand Up @@ -854,6 +855,12 @@ if (slash?.[1] === "clear") {
out({ type: "assistant", message: { id: crypto.randomUUID(), model: "<synthetic>", role: "assistant", content: [{ type: "text", text }] }, parent_tool_use_id: null, session_id: sessionId });
out({ type: "result", subtype: "success", is_error: true, api_error_status: 403, result: text, session_id: sessionId, total_cost_usd: 0, duration_ms: 7, num_turns: 1 });
process.exit(1);
} else if (prompt.includes("AUTH_EXPIRED")) {
out(init);
const text = "Failed to authenticate: OAuth session expired and could not be refreshed";
out({ type: "assistant", message: { id: crypto.randomUUID(), model: "<synthetic>", role: "assistant", content: [{ type: "text", text }] }, parent_tool_use_id: null, session_id: sessionId });
out({ type: "result", subtype: "success", is_error: true, api_error_status: 401, result: text, session_id: sessionId, total_cost_usd: 0, duration_ms: 7, num_turns: 1 });
process.exit(1);
} else if (prompt.includes("LOGIN_FAIL")) {
out(init);
const text = "I opened the dashboard. I couldn't log in to example.com because there are no saved credentials for it.";
Expand Down
2 changes: 2 additions & 0 deletions packages/core/test/frame.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe("a turn that ended early", () => {
expect(runEndOf("Timed out after 60 minutes")).toEqual({ kind: "timeout", minutes: 60 });
expect(runEndOf("Cancelled by user")).toEqual({ kind: "stopped", byUser: true });
expect(runEndOf("Claude Code is not signed in (or the API key is invalid).")?.kind).toBe("auth");
expect(runEndOf("Failed to authenticate: OAuth session expired and could not be refreshed")?.kind).toBe("auth");
expect(runEndOf("Failed to authenticate. API Error: 403 Request not allowed")).toBeNull();
expect(runEndOf("Prompt is too long")?.kind).toBe("context");
expect(runEndOf("This work is set to run in a virtual machine, but virtual machines are turned off (Settings → Virtual machines).")).toEqual({ kind: "vm", off: true });
expect(runEndOf("The virtual machine can't be used: boot timed out")).toEqual({ kind: "vm" });
Expand Down
9 changes: 9 additions & 0 deletions packages/core/test/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,15 @@ describe("runner end-to-end with fake claude", () => {
expect(listMissingLogins({}).filter((m) => m.runId === run.id)).toHaveLength(0);
});

test("an expired Claude sign-in asks to sign in again", async () => {
const { run } = await startChat({ agentId: agent.id, content: "AUTH_EXPIRED" });
const finished = await waitForRun(run.id, 20_000);
expect(finished.status).toBe("failed");
expect(finished.error).toStartWith("Claude Code is not signed in");
await new Promise((r) => setTimeout(r, 200));
expect(listMissingLogins({}).filter((m) => m.runId === run.id)).toHaveLength(0);
});

test("runs reach the MCP gateway; an explicit report disables the heuristic", async () => {
const { run } = await startChat({ agentId: agent.id, content: "CALL_MCP" });
const finished = await waitForRun(run.id, 20_000);
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function runEndOf(text: string): RunEnd | null {
if (t.startsWith(RUN_WATCHDOG)) return { kind: "stalled" };
if (/prompt is too long|context (?:length|window)|conversation is too long/i.test(t)) return { kind: "context" };
if (t.startsWith(RUN_COST_LIMIT)) return { kind: "budget" };
if (/^Claude Code is not signed in/.test(t) || /^(?:invalid api key|authentication_error|oauth token (?:has )?expired|credit balance is too low)/i.test(t)) return { kind: "auth" };
if (/^Claude Code is not signed in/.test(t) || /^(?:failed to authenticate: )?(?:invalid api key|authentication_error|oauth (?:token|session) (?:has )?expired|credit balance is too low)/i.test(t)) return { kind: "auth" };
if (t.startsWith(RUN_CLI_MISSING)) return { kind: "cli" };
if (/Pick another folder for this chat\.|Change the default folder in /.test(t)) return { kind: "folder" };
if (/virtual machines are turned off/i.test(t)) return { kind: "vm", off: true };
Expand Down
Loading