From a7b54d91185f2ede9d1d6fc624f4caed82831d83 Mon Sep 17 00:00:00 2001 From: Gagancreates Date: Tue, 26 May 2026 01:54:25 +0530 Subject: [PATCH] fix: reliable Claude Code session resume on Windows (avoid claude.cmd EINVAL) Resuming a code-mode chat after restarting the app spawns a fresh ACP agent. On Windows + Node >=20.12 the bridge spawning claude.cmd throws EINVAL, so the session queue owner fails to start. Rowboat injects CLAUDE_CODE_EXECUTABLE=claude.exe to dodge this, but the override didn't reliably reach the spawn. Windows-only; no-op on macOS/Linux. - executeCommand now accepts an env override and the non-abortable fallback path passes it through (was silently dropped) - resolveClaudeExeOnWindows also scans known npm/pnpm/volta global bin dirs, not just PATH (Electron's runtime PATH can omit them) - add --timeout 600 to acpx prompt commands so a genuine stall fails cleanly instead of hanging on 'Running' forever --- apps/x/packages/core/src/agents/runtime.ts | 4 +- .../skills/code-with-agents/skill.ts | 12 ++--- .../core/src/application/lib/builtin-tools.ts | 44 +++++++++++++++---- .../src/application/lib/command-executor.ts | 2 + 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index 20242113..0b260eec 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -1344,13 +1344,13 @@ npx acpx@latest --approve-all --cwd sessions ensure --name ${s **2. Then run the prompt:** \`\`\` -npx acpx@latest --approve-all --cwd -s ${sessionName} "" +npx acpx@latest --approve-all --timeout 600 --cwd -s ${sessionName} "" \`\`\` **3. Every follow-up coding request in this chat — reuse the same session (do NOT create again):** \`\`\` -npx acpx@latest --approve-all --cwd -s ${sessionName} "" +npx acpx@latest --approve-all --timeout 600 --cwd -s ${sessionName} "" \`\`\` Run these as **separate, sequential** \`executeCommand\` calls — issue the \`sessions ensure\` call first and WAIT for it to finish, then issue the prompt call. Do NOT fire both in the same turn / batch. diff --git a/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts b/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts index e98c05e6..d8e81a58 100644 --- a/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts +++ b/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts @@ -75,13 +75,13 @@ npx acpx@latest --approve-all --cwd sessions ensure --name -s "" +npx acpx@latest --approve-all --timeout 600 --cwd -s "" \`\`\` **3. Every follow-up coding request in this chat — reuse the same session (do NOT create again):** \`\`\` -npx acpx@latest --approve-all --cwd -s "" +npx acpx@latest --approve-all --timeout 600 --cwd -s "" \`\`\` **Run steps 1 and 2 as separate, sequential \`executeCommand\` calls.** Issue the \`sessions ensure\` call FIRST, wait for it to finish, and only THEN issue the prompt call. Do NOT fire both in the same turn / batch — each must surface and complete its own permission + command block before the next begins. @@ -93,17 +93,17 @@ Concrete example: \`\`\` # First coding message in the chat — ensure the session, then prompt: npx acpx@latest --approve-all --cwd "G:\\Blogging\\myblog" claude sessions ensure --name diskspace-check -npx acpx@latest --approve-all --cwd "G:\\Blogging\\myblog" claude -s diskspace-check "Check the system disk space and report total, used, and free space." +npx acpx@latest --approve-all --timeout 600 --cwd "G:\\Blogging\\myblog" claude -s diskspace-check "Check the system disk space and report total, used, and free space." # Follow-up in the same chat — reuse the session, no create: -npx acpx@latest --approve-all --cwd "G:\\Blogging\\myblog" claude -s diskspace-check "Summarize what we did and the final findings." +npx acpx@latest --approve-all --timeout 600 --cwd "G:\\Blogging\\myblog" claude -s diskspace-check "Summarize what we did and the final findings." \`\`\` ### Critical: flag order -\`--approve-all\` and \`--cwd\` are GLOBAL flags and MUST appear BEFORE the agent name. \`sessions ensure --name \` and \`-s \` come AFTER the agent name: +\`--approve-all\`, \`--timeout\`, and \`--cwd\` are GLOBAL flags and MUST appear BEFORE the agent name. \`sessions ensure --name \` and \`-s \` come AFTER the agent name: -- ✓ Correct: \`npx acpx@latest --approve-all --cwd -s ""\` +- ✓ Correct: \`npx acpx@latest --approve-all --timeout 600 --cwd -s ""\` - ✗ Wrong: \`npx acpx@latest --approve-all -s "..."\` (will fail) ### Writing good prompts for the agent diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index e3622b01..9bfb4250 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -95,21 +95,47 @@ const LLMPARSE_MIME_TYPES: Record = { // When the LLM invokes acpx via executeCommand, pre-resolve claude's real .exe // from the npm-shim layout and inject it via env so the bridge can spawn it. function resolveClaudeExeOnWindows(): string | undefined { - const pathDirs = (process.env.PATH ?? '').split(';'); - for (const dir of pathDirs) { - const trimmed = dir.trim(); - if (!trimmed) continue; - const cmdPath = path.join(trimmed, 'claude.cmd'); - if (!existsSync(cmdPath)) continue; - const exeFromLayout = path.join(trimmed, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'); + // Candidate dirs = everything on PATH, plus well-known npm/pnpm/volta global + // bin dirs. Electron's runtime PATH can omit these even when the user's shell + // includes them, which would otherwise leave us unable to find claude.exe and + // force a fallback to claude.cmd (which Node refuses to spawn — EINVAL). + const home = process.env.USERPROFILE ?? ''; + const appData = process.env.APPDATA || (home && path.join(home, 'AppData', 'Roaming')); + const localAppData = process.env.LOCALAPPDATA || (home && path.join(home, 'AppData', 'Local')); + const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; + const knownDirs = [ + appData && path.join(appData, 'npm'), + localAppData && path.join(localAppData, 'npm'), + appData && path.join(appData, 'pnpm'), + localAppData && path.join(localAppData, 'pnpm'), + home && path.join(home, '.volta', 'bin'), + path.join(programFiles, 'nodejs'), + ].filter(Boolean) as string[]; + + const pathDirs = (process.env.PATH ?? '').split(';').map((d) => d.trim()).filter(Boolean); + const seen = new Set(); + const candidates = [...pathDirs, ...knownDirs].filter((d) => { + const key = d.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + for (const dir of candidates) { + // Direct npm-shim layout: \node_modules\@anthropic-ai\claude-code\bin\claude.exe + const exeFromLayout = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'); if (existsSync(exeFromLayout)) return exeFromLayout; + + // Otherwise parse the claude.cmd shim for the real exe path. + const cmdPath = path.join(dir, 'claude.cmd'); + if (!existsSync(cmdPath)) continue; try { const content = readFileSync(cmdPath, 'utf-8'); const absMatch = content.match(/[A-Z]:[\\/][^\s"]*claude\.exe/i); if (absMatch && existsSync(absMatch[0])) return absMatch[0]; const relMatch = content.match(/%~dp0[\\/]?([^\s"%]+claude\.exe)/i); if (relMatch) { - const resolved = path.join(trimmed, relMatch[1]); + const resolved = path.join(dir, relMatch[1]); if (existsSync(resolved)) return resolved; } } catch { @@ -825,7 +851,7 @@ export const BuiltinTools: z.infer = { } // Fallback to original for backward compatibility - const result = await executeCommand(command, { cwd: workingDir }); + const result = await executeCommand(command, { cwd: workingDir, env: envOverride }); return { success: result.exitCode === 0, diff --git a/apps/x/packages/core/src/application/lib/command-executor.ts b/apps/x/packages/core/src/application/lib/command-executor.ts index 150c8f72..6be3c0e6 100644 --- a/apps/x/packages/core/src/application/lib/command-executor.ts +++ b/apps/x/packages/core/src/application/lib/command-executor.ts @@ -80,6 +80,7 @@ export async function executeCommand( cwd?: string; timeout?: number; // timeout in milliseconds maxBuffer?: number; // max buffer size in bytes + env?: NodeJS.ProcessEnv; // override environment (e.g. CLAUDE_CODE_EXECUTABLE for acpx) } ): Promise { try { @@ -89,6 +90,7 @@ export async function executeCommand( timeout: options?.timeout, maxBuffer: options?.maxBuffer || 1024 * 1024, // default 1MB shell, + env: options?.env, }); return {