@love-moon/conductor-cli 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -366,6 +366,7 @@ function normalizeRole(role) {
366
366
  function normalizeTaskStatus(status) {
367
367
  const normalized = cleanText(status).toLowerCase();
368
368
  if (normalized === "completed") return "completed";
369
+ if (normalized === "init") return "init";
369
370
  if (normalized === "running") return "running";
370
371
  if (normalized === "killed" || normalized === "failed" || normalized === "cancelled") return "killed";
371
372
  return normalized || "unknown";
@@ -427,13 +427,12 @@ async function main() {
427
427
 
428
428
  let resumeContext = null;
429
429
  if (cliArgs.resumeSessionId) {
430
- resumeContext = await resolveResumeContext(cliArgs.backend, cliArgs.resumeSessionId);
431
- log(
432
- `Validated --resume ${resumeContext.sessionId} (${resumeContext.provider}) at ${resumeContext.sessionPath}`,
433
- );
434
- log(`Resume will run backend from ${resumeContext.cwd}`);
435
- runtimeProjectPath = await applyWorkingDirectory(resumeContext.cwd);
436
- log(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
430
+ const bootstrap = await bootstrapResumeContextForFire({
431
+ backend: cliArgs.backend,
432
+ resumeSessionId: cliArgs.resumeSessionId,
433
+ });
434
+ resumeContext = bootstrap.resumeContext;
435
+ runtimeProjectPath = bootstrap.runtimeProjectPath;
437
436
  }
438
437
 
439
438
  const env = buildEnv();
@@ -1224,6 +1223,40 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
1224
1223
  return resolveCliResumeContext(backend, sessionId, options);
1225
1224
  }
1226
1225
 
1226
+ export async function bootstrapResumeContextForFire({
1227
+ backend,
1228
+ resumeSessionId,
1229
+ env = process.env,
1230
+ resolveResumeContextFn = resolveResumeContext,
1231
+ applyWorkingDirectoryFn = applyWorkingDirectory,
1232
+ logger = log,
1233
+ }) {
1234
+ let runtimeProjectPath = process.cwd();
1235
+ let resumeContext = null;
1236
+
1237
+ if (!resumeSessionId) {
1238
+ return { resumeContext, runtimeProjectPath };
1239
+ }
1240
+
1241
+ const overrideResumeCwd =
1242
+ typeof env?.CONDUCTOR_RESUME_CWD === "string" ? env.CONDUCTOR_RESUME_CWD.trim() : "";
1243
+ if (overrideResumeCwd) {
1244
+ logger(`Using CONDUCTOR_RESUME_CWD override for --resume ${resumeSessionId}: ${overrideResumeCwd}`);
1245
+ runtimeProjectPath = await applyWorkingDirectoryFn(overrideResumeCwd);
1246
+ logger(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
1247
+ return { resumeContext, runtimeProjectPath };
1248
+ }
1249
+
1250
+ resumeContext = await resolveResumeContextFn(backend, resumeSessionId);
1251
+ logger(
1252
+ `Validated --resume ${resumeContext.sessionId} (${resumeContext.provider}) at ${resumeContext.sessionPath}`,
1253
+ );
1254
+ logger(`Resume will run backend from ${resumeContext.cwd}`);
1255
+ runtimeProjectPath = await applyWorkingDirectoryFn(resumeContext.cwd);
1256
+ logger(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
1257
+ return { resumeContext, runtimeProjectPath };
1258
+ }
1259
+
1227
1260
  export async function applyWorkingDirectory(targetPath) {
1228
1261
  const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
1229
1262
  if (!normalizedPath) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.27",
4
- "gitCommitId": "76b37c9",
3
+ "version": "0.2.29",
4
+ "gitCommitId": "56ce873",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -17,16 +17,17 @@
17
17
  "test": "node --test test/*.test.js"
18
18
  },
19
19
  "dependencies": {
20
- "@love-moon/ai-sdk": "0.2.27",
21
- "@love-moon/conductor-sdk": "0.2.27",
20
+ "@love-moon/ai-bridge": "0.1.4",
21
+ "@love-moon/ai-sdk": "0.2.29",
22
+ "@love-moon/conductor-sdk": "0.2.29",
23
+ "chrome-launcher": "^1.2.1",
24
+ "chrome-remote-interface": "^0.33.0",
22
25
  "dotenv": "^16.4.5",
23
26
  "enquirer": "^2.4.1",
24
27
  "js-yaml": "^4.1.1",
25
28
  "node-pty": "^1.0.0",
26
29
  "ws": "^8.18.0",
27
- "yargs": "^17.7.2",
28
- "chrome-launcher": "^1.2.1",
29
- "chrome-remote-interface": "^0.33.0"
30
+ "yargs": "^17.7.2"
30
31
  },
31
32
  "optionalDependencies": {
32
33
  "@roamhq/wrtc": "^0.10.0"
package/src/daemon.js CHANGED
@@ -3,13 +3,14 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { createRequire } from "node:module";
5
5
  import { spawn } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  import dotenv from "dotenv";
9
9
  import yaml from "js-yaml";
10
10
 
11
11
  import { ConductorWebSocketClient, ConductorConfig, loadConfig, ConfigFileNotFound } from "@love-moon/conductor-sdk";
12
12
  import { DaemonLogCollector } from "./log-collector.js";
13
+ import { resolveResumeContext } from "./fire/resume.js";
13
14
  import { filterRuntimeSupportedAllowCliList, normalizeRuntimeBackendName } from "./runtime-backends.js";
14
15
  import {
15
16
  PACKAGE_NAME,
@@ -30,6 +31,7 @@ dotenv.config();
30
31
  const __filename = fileURLToPath(import.meta.url);
31
32
  const __dirname = path.dirname(__filename);
32
33
  const PACKAGE_ROOT = path.join(__dirname, "..");
34
+ const DEFAULT_AI_BRIDGE_API_SPECIFIER = "@love-moon/ai-bridge/dist/api.js";
33
35
  const moduleRequire = createRequire(import.meta.url);
34
36
  const CLI_PATH = path.resolve(PACKAGE_ROOT, "bin", "conductor-fire.js");
35
37
  const DAEMON_LOG_DIR = path.join(os.homedir(), ".conductor", "logs");
@@ -283,6 +285,24 @@ function normalizeOptionalString(value) {
283
285
  return normalized || null;
284
286
  }
285
287
 
288
+ function resolveImportTarget(specifierOrPath) {
289
+ const normalized = normalizeOptionalString(specifierOrPath);
290
+ if (!normalized) {
291
+ return null;
292
+ }
293
+ if (
294
+ normalized.startsWith("file:") ||
295
+ normalized.startsWith("node:") ||
296
+ normalized.startsWith("data:")
297
+ ) {
298
+ return normalized;
299
+ }
300
+ if (path.isAbsolute(normalized) || normalized.startsWith("./") || normalized.startsWith("../")) {
301
+ return pathToFileURL(path.resolve(normalized)).href;
302
+ }
303
+ return normalized;
304
+ }
305
+
286
306
  function normalizeTerminalResumeStrategy(value) {
287
307
  const normalized = normalizeOptionalString(value);
288
308
  if (!normalized) {
@@ -2518,6 +2538,16 @@ export function startDaemon(config = {}, deps = {}) {
2518
2538
  rejectCreateTaskDuringShutdown(event.payload);
2519
2539
  return;
2520
2540
  }
2541
+ if (event.type === "restart_task") {
2542
+ reportRestartFailure({
2543
+ taskId: event?.payload?.target_task_id ? String(event.payload.target_task_id) : "",
2544
+ projectId: event?.payload?.project_id ? String(event.payload.project_id) : "",
2545
+ requestId: event?.payload?.request_id ? String(event.payload.request_id) : "",
2546
+ mode: event?.payload?.mode ? String(event.payload.mode) : "",
2547
+ error: new Error("daemon shutting down"),
2548
+ });
2549
+ return;
2550
+ }
2521
2551
  if (event.type === "create_pty_task") {
2522
2552
  rejectCreatePtyTaskDuringShutdown(event.payload);
2523
2553
  return;
@@ -2528,6 +2558,10 @@ export function startDaemon(config = {}, deps = {}) {
2528
2558
  handleCreateTask(event.payload);
2529
2559
  return;
2530
2560
  }
2561
+ if (event.type === "restart_task") {
2562
+ void handleRestartTask(event.payload);
2563
+ return;
2564
+ }
2531
2565
  if (event.type === "create_pty_task") {
2532
2566
  void handleCreatePtyTask(event.payload);
2533
2567
  return;
@@ -2792,6 +2826,111 @@ export function startDaemon(config = {}, deps = {}) {
2792
2826
  }
2793
2827
  }
2794
2828
 
2829
+ let bridgeSessionHelperPromise = null;
2830
+ async function getBridgeSessionHelper() {
2831
+ if (typeof deps.bridgeSessionBetweenBackends === "function") {
2832
+ return deps.bridgeSessionBetweenBackends;
2833
+ }
2834
+ if (!bridgeSessionHelperPromise) {
2835
+ bridgeSessionHelperPromise = (async () => {
2836
+ try {
2837
+ const bridgeImportTarget =
2838
+ resolveImportTarget(process.env.CONDUCTOR_AI_BRIDGE_API_PATH) ||
2839
+ DEFAULT_AI_BRIDGE_API_SPECIFIER;
2840
+ const bridgeModule = await importOptionalModule(bridgeImportTarget);
2841
+ if (typeof bridgeModule.bridgeSessionBetweenBackends !== "function") {
2842
+ throw new Error("bridgeSessionBetweenBackends is not available");
2843
+ }
2844
+ return bridgeModule.bridgeSessionBetweenBackends;
2845
+ } catch (error) {
2846
+ bridgeSessionHelperPromise = null;
2847
+ throw error;
2848
+ }
2849
+ })();
2850
+ }
2851
+ return bridgeSessionHelperPromise;
2852
+ }
2853
+
2854
+ function reportRestartFailure({ taskId, projectId, requestId, mode, error, sendAck = true }) {
2855
+ const prefix =
2856
+ mode === "bridge_to_new_task" || mode === "fork_to_new_task"
2857
+ ? "new task failed"
2858
+ : "restart failed";
2859
+ const summary = `${prefix}: ${error?.message || error}`;
2860
+ if (sendAck) {
2861
+ sendAgentCommandAck({
2862
+ requestId,
2863
+ taskId,
2864
+ eventType: "restart_task",
2865
+ accepted: false,
2866
+ }).catch(() => {});
2867
+ }
2868
+ client
2869
+ .sendJson({
2870
+ type: "task_status_update",
2871
+ payload: {
2872
+ task_id: taskId,
2873
+ project_id: projectId,
2874
+ status: "KILLED",
2875
+ summary,
2876
+ },
2877
+ })
2878
+ .catch((err) => {
2879
+ logError(`Failed to report restart_task failure for ${taskId}: ${err?.message || err}`);
2880
+ });
2881
+ }
2882
+
2883
+ async function resolveRestartCwd({
2884
+ projectId,
2885
+ preferredCwd = "",
2886
+ backendType,
2887
+ sessionId,
2888
+ sourceSessionFilePath = "",
2889
+ }) {
2890
+ const normalizedPreferredCwd = typeof preferredCwd === "string" ? preferredCwd.trim() : "";
2891
+ if (normalizedPreferredCwd) {
2892
+ return normalizedPreferredCwd;
2893
+ }
2894
+
2895
+ const boundPath = await getProjectLocalPath(projectId);
2896
+ if (boundPath) {
2897
+ return boundPath;
2898
+ }
2899
+
2900
+ const normalizedBackend = normalizeRuntimeBackendName(backendType);
2901
+ const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
2902
+ if (normalizedSessionId && normalizedBackend && normalizedBackend !== "opencode") {
2903
+ try {
2904
+ const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
2905
+ normalizedBackend,
2906
+ normalizedSessionId,
2907
+ { cwd: process.cwd() },
2908
+ );
2909
+ if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
2910
+ return resumeContext.cwd.trim();
2911
+ }
2912
+ } catch {
2913
+ // ignore provider-specific fallback failure here; we'll try the remaining fallbacks
2914
+ }
2915
+ }
2916
+
2917
+ const normalizedSessionPath =
2918
+ typeof sourceSessionFilePath === "string" ? sourceSessionFilePath.trim() : "";
2919
+ if (normalizedSessionPath) {
2920
+ try {
2921
+ const stats = fs.statSync(normalizedSessionPath);
2922
+ if (stats.isDirectory()) {
2923
+ return normalizedSessionPath;
2924
+ }
2925
+ return path.dirname(normalizedSessionPath);
2926
+ } catch {
2927
+ // ignore missing local path
2928
+ }
2929
+ }
2930
+
2931
+ return "";
2932
+ }
2933
+
2795
2934
  async function handleCreateTask(payload) {
2796
2935
  const {
2797
2936
  task_id: taskId,
@@ -2888,11 +3027,11 @@ export function startDaemon(config = {}, deps = {}) {
2888
3027
  payload: {
2889
3028
  task_id: taskId,
2890
3029
  project_id: projectId,
2891
- status: "UNKNOWN",
3030
+ status: "INIT",
2892
3031
  },
2893
3032
  })
2894
3033
  .catch((err) => {
2895
- logError(`Failed to report task status (UNKNOWN) for ${taskId}: ${err?.message || err}`);
3034
+ logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
2896
3035
  });
2897
3036
 
2898
3037
  // Check if project has a bound local path for this daemon
@@ -3085,6 +3224,375 @@ export function startDaemon(config = {}, deps = {}) {
3085
3224
  });
3086
3225
  }
3087
3226
 
3227
+ async function handleRestartTask(payload) {
3228
+ const {
3229
+ mode,
3230
+ source_task_id: sourceTaskId,
3231
+ target_task_id: targetTaskId,
3232
+ project_id: projectId,
3233
+ title,
3234
+ source_backend_type: sourceBackendType,
3235
+ source_session_id: sourceSessionId,
3236
+ source_session_file_path: sourceSessionFilePath,
3237
+ target_backend_type: targetBackendType,
3238
+ request_id: requestIdRaw,
3239
+ } = payload || {};
3240
+
3241
+ const requestId = requestIdRaw ? String(requestIdRaw) : "";
3242
+ const normalizedMode = typeof mode === "string" ? mode.trim() : "";
3243
+ const normalizedSourceTaskId = sourceTaskId ? String(sourceTaskId) : "";
3244
+ const normalizedTargetTaskId = targetTaskId ? String(targetTaskId) : "";
3245
+ const normalizedProjectId = projectId ? String(projectId) : "";
3246
+ const normalizedSourceSessionId = sourceSessionId ? String(sourceSessionId).trim() : "";
3247
+
3248
+ if (
3249
+ !normalizedMode ||
3250
+ !normalizedSourceTaskId ||
3251
+ !normalizedTargetTaskId ||
3252
+ !normalizedProjectId ||
3253
+ !normalizedSourceSessionId
3254
+ ) {
3255
+ logError(`Invalid restart_task payload: ${JSON.stringify(payload)}`);
3256
+ sendAgentCommandAck({
3257
+ requestId,
3258
+ taskId: normalizedTargetTaskId || normalizedSourceTaskId,
3259
+ eventType: "restart_task",
3260
+ accepted: false,
3261
+ }).catch(() => {});
3262
+ return;
3263
+ }
3264
+
3265
+ if (requestId && !markRequestSeen(requestId)) {
3266
+ log(
3267
+ `Duplicate restart_task ignored for ${normalizedTargetTaskId} (request_id=${requestId})`,
3268
+ );
3269
+ sendAgentCommandAck({
3270
+ requestId,
3271
+ taskId: normalizedTargetTaskId,
3272
+ eventType: "restart_task",
3273
+ accepted: true,
3274
+ }).catch(() => {});
3275
+ return;
3276
+ }
3277
+
3278
+ if (daemonShuttingDown) {
3279
+ reportRestartFailure({
3280
+ taskId: normalizedTargetTaskId,
3281
+ projectId: normalizedProjectId,
3282
+ requestId,
3283
+ mode: normalizedMode,
3284
+ error: new Error("daemon shutting down"),
3285
+ });
3286
+ return;
3287
+ }
3288
+
3289
+ const activeTarget = activeTaskProcesses.get(normalizedTargetTaskId);
3290
+ if (activeTarget?.child) {
3291
+ reportRestartFailure({
3292
+ taskId: normalizedTargetTaskId,
3293
+ projectId: normalizedProjectId,
3294
+ requestId,
3295
+ mode: normalizedMode,
3296
+ error: new Error(`task already active (pid=${activeTarget.child.pid ?? "unknown"})`),
3297
+ });
3298
+ return;
3299
+ }
3300
+
3301
+ const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
3302
+ if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
3303
+ reportRestartFailure({
3304
+ taskId: normalizedTargetTaskId,
3305
+ projectId: normalizedProjectId,
3306
+ requestId,
3307
+ mode: normalizedMode,
3308
+ error: new Error(`Unsupported backend: ${effectiveBackend}`),
3309
+ });
3310
+ return;
3311
+ }
3312
+
3313
+ if (normalizedMode === "resume_inplace") {
3314
+ if (normalizedTargetTaskId !== normalizedSourceTaskId) {
3315
+ reportRestartFailure({
3316
+ taskId: normalizedTargetTaskId,
3317
+ projectId: normalizedProjectId,
3318
+ requestId,
3319
+ mode: normalizedMode,
3320
+ error: new Error("In-place restart must reuse the same task"),
3321
+ });
3322
+ return;
3323
+ }
3324
+ if (effectiveBackend !== sourceBackendType) {
3325
+ reportRestartFailure({
3326
+ taskId: normalizedTargetTaskId,
3327
+ projectId: normalizedProjectId,
3328
+ requestId,
3329
+ mode: normalizedMode,
3330
+ error: new Error("In-place restart must reuse the same backend"),
3331
+ });
3332
+ return;
3333
+ }
3334
+ }
3335
+
3336
+ sendAgentCommandAck({
3337
+ requestId,
3338
+ taskId: normalizedTargetTaskId,
3339
+ eventType: "restart_task",
3340
+ accepted: true,
3341
+ }).catch((err) => {
3342
+ logError(`Failed to report agent_command_ack(restart_task) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3343
+ });
3344
+
3345
+ let resolvedResumeSessionId = normalizedSourceSessionId;
3346
+ let resolvedResumeCwd = "";
3347
+ try {
3348
+ if (normalizedMode === "bridge_to_new_task" || normalizedMode === "fork_to_new_task") {
3349
+ const sourceResumeCwd = await resolveRestartCwd({
3350
+ projectId: normalizedProjectId,
3351
+ backendType: sourceBackendType,
3352
+ sessionId: normalizedSourceSessionId,
3353
+ sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3354
+ });
3355
+ const bridgeSession = await getBridgeSessionHelper();
3356
+ const bridgeResult = await bridgeSession({
3357
+ sourceTool: sourceBackendType,
3358
+ sourceSessionId: normalizedSourceSessionId,
3359
+ sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3360
+ sourceSessionInfo: {
3361
+ tool: sourceBackendType,
3362
+ sessionId: normalizedSourceSessionId,
3363
+ path: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3364
+ cwd: sourceResumeCwd || undefined,
3365
+ },
3366
+ targetTool: effectiveBackend,
3367
+ targetCwdFallback: sourceResumeCwd || undefined,
3368
+ });
3369
+ resolvedResumeSessionId = bridgeResult.sessionId;
3370
+ resolvedResumeCwd = await resolveRestartCwd({
3371
+ projectId: normalizedProjectId,
3372
+ preferredCwd: bridgeResult.cwd,
3373
+ backendType: effectiveBackend,
3374
+ sessionId: bridgeResult.sessionId,
3375
+ sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3376
+ });
3377
+ } else if (normalizedMode === "resume_inplace") {
3378
+ resolvedResumeCwd = await resolveRestartCwd({
3379
+ projectId: normalizedProjectId,
3380
+ backendType: effectiveBackend,
3381
+ sessionId: normalizedSourceSessionId,
3382
+ sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3383
+ });
3384
+ } else {
3385
+ throw new Error(`Unsupported restart mode: ${normalizedMode}`);
3386
+ }
3387
+ } catch (error) {
3388
+ reportRestartFailure({
3389
+ taskId: normalizedTargetTaskId,
3390
+ projectId: normalizedProjectId,
3391
+ requestId,
3392
+ mode: normalizedMode,
3393
+ error,
3394
+ sendAck: false,
3395
+ });
3396
+ return;
3397
+ }
3398
+
3399
+ if (!resolvedResumeCwd) {
3400
+ reportRestartFailure({
3401
+ taskId: normalizedTargetTaskId,
3402
+ projectId: normalizedProjectId,
3403
+ requestId,
3404
+ mode: normalizedMode,
3405
+ error: new Error("Could not resolve resume cwd"),
3406
+ sendAck: false,
3407
+ });
3408
+ return;
3409
+ }
3410
+
3411
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3412
+
3413
+ log("");
3414
+ log(
3415
+ `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
3416
+ );
3417
+ log(`CLI command: ${cliCommand}`);
3418
+
3419
+ if (normalizedMode !== "resume_inplace") {
3420
+ client
3421
+ .sendJson({
3422
+ type: "task_status_update",
3423
+ payload: {
3424
+ task_id: normalizedTargetTaskId,
3425
+ project_id: normalizedProjectId,
3426
+ status: "INIT",
3427
+ },
3428
+ })
3429
+ .catch((err) => {
3430
+ logError(`Failed to report task status (INIT) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3431
+ });
3432
+ }
3433
+
3434
+ if (daemonShuttingDown) {
3435
+ reportRestartFailure({
3436
+ taskId: normalizedTargetTaskId,
3437
+ projectId: normalizedProjectId,
3438
+ requestId,
3439
+ mode: normalizedMode,
3440
+ error: new Error("daemon shutting down"),
3441
+ sendAck: false,
3442
+ });
3443
+ return;
3444
+ }
3445
+
3446
+ let taskDir = resolvedResumeCwd;
3447
+ let logPath = path.join(taskDir, "conductor.log");
3448
+
3449
+ try {
3450
+ mkdirSyncFn(taskDir, { recursive: true });
3451
+ } catch (err) {
3452
+ reportRestartFailure({
3453
+ taskId: normalizedTargetTaskId,
3454
+ projectId: normalizedProjectId,
3455
+ requestId,
3456
+ mode: normalizedMode,
3457
+ error: new Error(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`),
3458
+ sendAck: false,
3459
+ });
3460
+ return;
3461
+ }
3462
+
3463
+ const args = [];
3464
+ if (effectiveBackend) {
3465
+ args.push("--backend", effectiveBackend);
3466
+ }
3467
+ args.push("--resume", resolvedResumeSessionId);
3468
+ args.push("--");
3469
+
3470
+ const env = {
3471
+ ...process.env,
3472
+ CONDUCTOR_PROJECT_ID: normalizedProjectId,
3473
+ CONDUCTOR_TASK_ID: normalizedTargetTaskId,
3474
+ CONDUCTOR_CLI_COMMAND: cliCommand,
3475
+ CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
3476
+ };
3477
+ if (config.CONFIG_FILE) {
3478
+ env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
3479
+ }
3480
+ if (AGENT_TOKEN) {
3481
+ env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
3482
+ }
3483
+ if (BACKEND_HTTP) {
3484
+ env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
3485
+ }
3486
+
3487
+ const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3488
+ cwd: taskDir,
3489
+ env,
3490
+ stdio: ["inherit", "pipe", "pipe"],
3491
+ });
3492
+
3493
+ let logStream;
3494
+ try {
3495
+ logStream = createWriteStreamFn(logPath, { flags: "a" });
3496
+ if (logStream && typeof logStream.on === "function") {
3497
+ const logPathSnapshot = logPath;
3498
+ logStream.on("error", (err) => {
3499
+ logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
3500
+ });
3501
+ }
3502
+ } catch (err) {
3503
+ logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3504
+ }
3505
+
3506
+ log(`Task title: ${title || normalizedTargetTaskId}`);
3507
+ log(`Resume session: ${resolvedResumeSessionId}`);
3508
+ log(`Resume cwd: ${resolvedResumeCwd}`);
3509
+ log(`Logs: ${logPath}`);
3510
+
3511
+ activeTaskProcesses.set(normalizedTargetTaskId, {
3512
+ child,
3513
+ projectId: normalizedProjectId,
3514
+ logPath,
3515
+ stopForceKillTimer: null,
3516
+ });
3517
+
3518
+ client
3519
+ .sendJson({
3520
+ type: "task_status_update",
3521
+ payload: {
3522
+ task_id: normalizedTargetTaskId,
3523
+ project_id: normalizedProjectId,
3524
+ status: "RUNNING",
3525
+ },
3526
+ })
3527
+ .catch((err) => {
3528
+ logError(`Failed to report task status (RUNNING) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3529
+ });
3530
+
3531
+ if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
3532
+ child.stdout.pipe(logStream, { end: false });
3533
+ } else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
3534
+ child.stdout.on("data", (chunk) => logStream.write(chunk));
3535
+ }
3536
+ if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
3537
+ child.stderr.pipe(logStream, { end: false });
3538
+ } else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
3539
+ child.stderr.on("data", (chunk) => logStream.write(chunk));
3540
+ }
3541
+
3542
+ child.on("error", (err) => {
3543
+ logError(`Failed to spawn restart CLI for ${normalizedTargetTaskId}: ${err.message}`);
3544
+ if (logStream) {
3545
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3546
+ logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
3547
+ }
3548
+ });
3549
+
3550
+ child.on("exit", (code, signal) => {
3551
+ const active = activeTaskProcesses.get(normalizedTargetTaskId);
3552
+ if (active?.stopForceKillTimer) {
3553
+ clearTimeout(active.stopForceKillTimer);
3554
+ }
3555
+ activeTaskProcesses.delete(normalizedTargetTaskId);
3556
+ const suppressExitStatusReport = suppressedExitStatusReports.has(normalizedTargetTaskId);
3557
+ suppressedExitStatusReports.delete(normalizedTargetTaskId);
3558
+ if (logStream) {
3559
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3560
+ if (signal) {
3561
+ logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3562
+ } else {
3563
+ logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3564
+ }
3565
+ logStream.end();
3566
+ }
3567
+
3568
+ const isKilledBySignal = Boolean(signal);
3569
+ const isKilledByExitCode = code === 130 || code === 143;
3570
+ const isKilled = isKilledBySignal || isKilledByExitCode;
3571
+ const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
3572
+ const summary = isKilled
3573
+ ? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
3574
+ : code === 0
3575
+ ? "completed"
3576
+ : `exited with code ${code}`;
3577
+
3578
+ if (!suppressExitStatusReport) {
3579
+ client
3580
+ .sendJson({
3581
+ type: "task_status_update",
3582
+ payload: {
3583
+ task_id: normalizedTargetTaskId,
3584
+ project_id: normalizedProjectId,
3585
+ status,
3586
+ summary,
3587
+ },
3588
+ })
3589
+ .catch((err) => {
3590
+ logError(`Failed to report task status (${status}) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3591
+ });
3592
+ }
3593
+ });
3594
+ }
3595
+
3088
3596
  let closePromise = null;
3089
3597
  async function shutdownDaemon(reason = "manual close") {
3090
3598
  if (closePromise) {
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import process from "node:process";
3
4
  import { spawn as spawnProcess } from "node:child_process";
@@ -166,6 +167,38 @@ export async function resolveGlobalPackageDirectory({
166
167
  return path.join(normalizeRoot(rawRoot), packageName);
167
168
  }
168
169
 
170
+ export function ensureNodePtySpawnHelperExecutableForPackageDirectory({
171
+ packageDirectory,
172
+ platform = process.platform,
173
+ arch = process.arch,
174
+ existsSync = fs.existsSync,
175
+ statSync = fs.statSync,
176
+ chmodSync = fs.chmodSync,
177
+ } = {}) {
178
+ if (!packageDirectory || platform === "win32") {
179
+ return null;
180
+ }
181
+
182
+ const helperCandidates = [
183
+ path.join(packageDirectory, "node_modules", "node-pty", "build", "Release", "spawn-helper"),
184
+ path.join(packageDirectory, "node_modules", "node-pty", "build", "Debug", "spawn-helper"),
185
+ path.join(packageDirectory, "node_modules", "node-pty", "prebuilds", `${platform}-${arch}`, "spawn-helper"),
186
+ ];
187
+ const helperPath = helperCandidates.find((candidate) => existsSync(candidate));
188
+ if (!helperPath) {
189
+ return null;
190
+ }
191
+
192
+ const currentMode = statSync(helperPath).mode & 0o777;
193
+ if ((currentMode & 0o111) !== 0) {
194
+ return { helperPath, updated: false };
195
+ }
196
+
197
+ const nextMode = currentMode | 0o111;
198
+ chmodSync(helperPath, nextMode);
199
+ return { helperPath, updated: true };
200
+ }
201
+
169
202
  export function buildNodePtyVerificationScript() {
170
203
  return String.raw`
171
204
  const fs = require('node:fs');
@@ -252,6 +285,7 @@ export async function verifyNodePtyForPackageDirectory({
252
285
  if (!packageDirectory) {
253
286
  throw new Error("packageDirectory is required");
254
287
  }
288
+ ensureNodePtySpawnHelperExecutableForPackageDirectory({ packageDirectory });
255
289
  const result = await runCommand(nodeExecutable, ["-e", buildNodePtyVerificationScript(), packageDirectory], {
256
290
  timeoutMs: 15_000,
257
291
  });