From 213f155c9ce25bf13f1af3864de905f0ccbee050 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 12 Mar 2026 13:16:08 +0700 Subject: [PATCH] fix(desktop): fix Claude agent spawn from managed runtime and rotate logs on restart - Always override spawnClaudeCodeProcess to use process.execPath instead of SDK's PATH-based "node" lookup, which fails in the managed runtime bundle - Fix append-mode arg ordering in resolveClaudeSpawnCommand so extra CLI args (e.g. --chrome) go after cli.js, not before it (Node exit code 9) - Rotate daemon.log on every daemon restart for clean startup logs - Remove dead dev_resource_root fallback from runtime_manager.rs - Canonicalize current_exe path in CLI runner --- .../desktop/src-tauri/src/runtime_manager.rs | 16 ++------- .../server/agent/providers/claude-agent.ts | 19 +++++----- packages/server/src/server/logger.ts | 36 +++++++++++++++++-- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/desktop/src-tauri/src/runtime_manager.rs b/packages/desktop/src-tauri/src/runtime_manager.rs index 7bebb7cc7..7b239e7bb 100644 --- a/packages/desktop/src-tauri/src/runtime_manager.rs +++ b/packages/desktop/src-tauri/src/runtime_manager.rs @@ -312,10 +312,6 @@ fn read_json_file Deserialize<'de>>(path: &Path) -> Result PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources") -} - fn bundled_runtime_root_from_resource_dir(resource_dir: &Path) -> Option { for candidate in [ resource_dir.join("resources").join("managed-runtime"), @@ -337,12 +333,7 @@ fn bundled_runtime_root(app: &AppHandle) -> Result { return Ok(candidate); } } else { - log::info!("[runtime] resource_dir() unavailable, checking dev path"); - } - let dev = dev_resource_root().join("managed-runtime"); - if dev.exists() { - log::info!("[runtime] using dev runtime at {}", dev.display()); - return Ok(dev); + log::info!("[runtime] resource_dir() unavailable"); } log::error!("[runtime] no managed runtime found"); Err("Managed runtime resources are not bundled with this desktop build.".to_string()) @@ -786,6 +777,7 @@ pub async fn update_managed_daemon_tcp_settings( pub fn run_managed_cli_from_current_process(args: Vec) -> Result { let current_exe = std::env::current_exe() + .and_then(|p| p.canonicalize()) .map_err(|error| format!("Failed to resolve desktop executable path: {error}"))?; let current_exe = dunce::simplified(¤t_exe).to_path_buf(); let exe_dir = current_exe @@ -799,10 +791,6 @@ pub fn run_managed_cli_from_current_process(args: Vec) -> Result 0; - const commandMode = runtimeSettings?.command?.mode; - const needsCustomSpawn = - hasEnvOverrides || commandMode === "append" || commandMode === "replace"; - - if (!needsCustomSpawn) { - return options; - } - return { ...options, spawnClaudeCodeProcess: (spawnOptions) => { @@ -406,7 +397,13 @@ function applyRuntimeSettingsToClaudeOptions( spawnOptions, runtimeSettings ); - return spawn(resolved.command, resolved.args, { + // The SDK defaults to spawning "node" via PATH lookup, which fails when + // running from the managed runtime bundle where node isn't in PATH. + // Always use process.execPath — the actual node binary running the daemon. + const command = resolved.command === spawnOptions.command + ? process.execPath + : resolved.command; + return spawn(command, resolved.args, { cwd: spawnOptions.cwd, env: applyProviderEnv(spawnOptions.env, runtimeSettings), signal: spawnOptions.signal, diff --git a/packages/server/src/server/logger.ts b/packages/server/src/server/logger.ts index 0035ac23b..917c2f254 100644 --- a/packages/server/src/server/logger.ts +++ b/packages/server/src/server/logger.ts @@ -1,4 +1,4 @@ -import { mkdirSync } from "node:fs"; +import { existsSync, mkdirSync, readdirSync, renameSync, unlinkSync } from "node:fs"; import path from "node:path"; import pino from "pino"; import pretty from "pino-pretty"; @@ -138,6 +138,37 @@ function normalizeLoggerConfigInput(config: LoggerConfigInput): PersistedConfig return config as PersistedConfig; } +function rotateOnRestart(filePath: string, maxFiles: number): void { + if (!existsSync(filePath)) return; + + const dir = path.dirname(filePath); + const base = path.basename(filePath); + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, "0"); + const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`; + + try { + renameSync(filePath, path.join(dir, `${ts}-00-${base}`)); + } catch { + return; + } + + // Clean up old rotated logs beyond maxFiles. + // Both our restart-rotated files (YYYYMMDD-HHMM-00-daemon.log) and + // rotating-file-stream's size-rotated files (YYYYMMDD-HHMM-NN-daemon.log) + // end with -${base} and sort chronologically by name. + const rotatedFiles = readdirSync(dir) + .filter((f) => f.endsWith(`-${base}`) && f !== base) + .sort() + .reverse(); + + for (const file of rotatedFiles.slice(maxFiles)) { + try { + unlinkSync(path.join(dir, file)); + } catch {} + } +} + function toRotatingFileStreamSize(size: string): string { const trimmed = size.trim(); const match = trimmed.match(/^(\d+)\s*([bBkKmMgG])?$/); @@ -230,11 +261,12 @@ export function createRootLogger( }) : pino.destination({ dest: 1, sync: false }); + rotateOnRestart(config.file.path, config.file.rotate.maxFiles); + const fileStream = createRotatingFileStream(path.basename(config.file.path), { path: path.dirname(config.file.path), size: toRotatingFileStreamSize(config.file.rotate.maxSize), maxFiles: config.file.rotate.maxFiles, - initialRotation: true, }); return pino(