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
This commit is contained in:
Gagancreates 2026-05-26 01:54:25 +05:30
parent 54ab7bfefc
commit a7b54d9118
4 changed files with 45 additions and 17 deletions

View file

@ -1344,13 +1344,13 @@ npx acpx@latest --approve-all --cwd <workdir> <agent> sessions ensure --name ${s
**2. Then run the prompt:**
\`\`\`
npx acpx@latest --approve-all --cwd <workdir> <agent> -s ${sessionName} "<prompt>"
npx acpx@latest --approve-all --timeout 600 --cwd <workdir> <agent> -s ${sessionName} "<prompt>"
\`\`\`
**3. Every follow-up coding request in this chat reuse the same session (do NOT create again):**
\`\`\`
npx acpx@latest --approve-all --cwd <workdir> <agent> -s ${sessionName} "<prompt>"
npx acpx@latest --approve-all --timeout 600 --cwd <workdir> <agent> -s ${sessionName} "<prompt>"
\`\`\`
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.

View file

@ -75,13 +75,13 @@ npx acpx@latest --approve-all --cwd <folder> <agent> sessions ensure --name <ses
**2. Then run the prompt:**
\`\`\`
npx acpx@latest --approve-all --cwd <folder> <agent> -s <session-name> "<prompt>"
npx acpx@latest --approve-all --timeout 600 --cwd <folder> <agent> -s <session-name> "<prompt>"
\`\`\`
**3. Every follow-up coding request in this chat reuse the same session (do NOT create again):**
\`\`\`
npx acpx@latest --approve-all --cwd <folder> <agent> -s <session-name> "<prompt>"
npx acpx@latest --approve-all --timeout 600 --cwd <folder> <agent> -s <session-name> "<prompt>"
\`\`\`
**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 <name>\` and \`-s <session-name>\` come AFTER the agent name:
\`--approve-all\`, \`--timeout\`, and \`--cwd\` are GLOBAL flags and MUST appear BEFORE the agent name. \`sessions ensure --name <name>\` and \`-s <session-name>\` come AFTER the agent name:
- Correct: \`npx acpx@latest --approve-all --cwd <folder> <agent> -s <session-name> "<prompt>"\`
- Correct: \`npx acpx@latest --approve-all --timeout 600 --cwd <folder> <agent> -s <session-name> "<prompt>"\`
- Wrong: \`npx acpx@latest <agent> --approve-all -s <name> "..."\` (will fail)
### Writing good prompts for the agent

View file

@ -95,21 +95,47 @@ const LLMPARSE_MIME_TYPES: Record<string, string> = {
// 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<string>();
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: <dir>\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<typeof BuiltinToolsSchema> = {
}
// 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,

View file

@ -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<CommandResult> {
try {
@ -89,6 +90,7 @@ export async function executeCommand(
timeout: options?.timeout,
maxBuffer: options?.maxBuffer || 1024 * 1024, // default 1MB
shell,
env: options?.env,
});
return {