@love-moon/conductor-cli 0.2.28 → 0.2.30

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.28",
4
- "gitCommitId": "c690738",
3
+ "version": "0.2.30",
4
+ "gitCommitId": "e6a71ad",
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.28",
21
- "@love-moon/conductor-sdk": "0.2.28",
20
+ "@love-moon/ai-bridge": "0.1.4",
21
+ "@love-moon/ai-sdk": "0.2.30",
22
+ "@love-moon/conductor-sdk": "0.2.30",
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) {
@@ -1154,13 +1174,18 @@ export function startDaemon(config = {}, deps = {}) {
1154
1174
  });
1155
1175
  }
1156
1176
 
1157
- function runCommand(command, args, timeoutMs = 120_000) {
1177
+ function runCommand(command, args, options = 120_000) {
1158
1178
  return new Promise((resolve) => {
1159
1179
  let stdout = "";
1160
1180
  let stderr = "";
1181
+ const normalizedOptions =
1182
+ typeof options === "number"
1183
+ ? { timeoutMs: options }
1184
+ : (options || {});
1161
1185
  const child = spawnFn(command, args, {
1162
1186
  stdio: ["ignore", "pipe", "pipe"],
1163
- env: { ...process.env },
1187
+ env: normalizedOptions.env || { ...process.env },
1188
+ cwd: normalizedOptions.cwd || process.cwd(),
1164
1189
  });
1165
1190
  const timer = setTimeout(() => {
1166
1191
  try {
@@ -1168,7 +1193,7 @@ export function startDaemon(config = {}, deps = {}) {
1168
1193
  } catch {
1169
1194
  /* ignore */
1170
1195
  }
1171
- }, timeoutMs);
1196
+ }, normalizedOptions.timeoutMs ?? 120_000);
1172
1197
  child.stdout?.on("data", (chunk) => {
1173
1198
  if (stdout.length < 4000) stdout += chunk.toString().slice(0, 2000);
1174
1199
  });
@@ -1187,11 +1212,7 @@ export function startDaemon(config = {}, deps = {}) {
1187
1212
  }
1188
1213
 
1189
1214
  function runBufferedCommand(command, args, options = {}) {
1190
- return runCommand(
1191
- command,
1192
- args,
1193
- typeof options === "number" ? options : options?.timeoutMs ?? 120_000,
1194
- );
1215
+ return runCommand(command, args, options);
1195
1216
  }
1196
1217
 
1197
1218
  async function readInstalledCliVersion() {
@@ -2518,6 +2539,16 @@ export function startDaemon(config = {}, deps = {}) {
2518
2539
  rejectCreateTaskDuringShutdown(event.payload);
2519
2540
  return;
2520
2541
  }
2542
+ if (event.type === "restart_task") {
2543
+ reportRestartFailure({
2544
+ taskId: event?.payload?.target_task_id ? String(event.payload.target_task_id) : "",
2545
+ projectId: event?.payload?.project_id ? String(event.payload.project_id) : "",
2546
+ requestId: event?.payload?.request_id ? String(event.payload.request_id) : "",
2547
+ mode: event?.payload?.mode ? String(event.payload.mode) : "",
2548
+ error: new Error("daemon shutting down"),
2549
+ });
2550
+ return;
2551
+ }
2521
2552
  if (event.type === "create_pty_task") {
2522
2553
  rejectCreatePtyTaskDuringShutdown(event.payload);
2523
2554
  return;
@@ -2528,6 +2559,10 @@ export function startDaemon(config = {}, deps = {}) {
2528
2559
  handleCreateTask(event.payload);
2529
2560
  return;
2530
2561
  }
2562
+ if (event.type === "restart_task") {
2563
+ void handleRestartTask(event.payload);
2564
+ return;
2565
+ }
2531
2566
  if (event.type === "create_pty_task") {
2532
2567
  void handleCreatePtyTask(event.payload);
2533
2568
  return;
@@ -2792,6 +2827,111 @@ export function startDaemon(config = {}, deps = {}) {
2792
2827
  }
2793
2828
  }
2794
2829
 
2830
+ let bridgeSessionHelperPromise = null;
2831
+ async function getBridgeSessionHelper() {
2832
+ if (typeof deps.bridgeSessionBetweenBackends === "function") {
2833
+ return deps.bridgeSessionBetweenBackends;
2834
+ }
2835
+ if (!bridgeSessionHelperPromise) {
2836
+ bridgeSessionHelperPromise = (async () => {
2837
+ try {
2838
+ const bridgeImportTarget =
2839
+ resolveImportTarget(process.env.CONDUCTOR_AI_BRIDGE_API_PATH) ||
2840
+ DEFAULT_AI_BRIDGE_API_SPECIFIER;
2841
+ const bridgeModule = await importOptionalModule(bridgeImportTarget);
2842
+ if (typeof bridgeModule.bridgeSessionBetweenBackends !== "function") {
2843
+ throw new Error("bridgeSessionBetweenBackends is not available");
2844
+ }
2845
+ return bridgeModule.bridgeSessionBetweenBackends;
2846
+ } catch (error) {
2847
+ bridgeSessionHelperPromise = null;
2848
+ throw error;
2849
+ }
2850
+ })();
2851
+ }
2852
+ return bridgeSessionHelperPromise;
2853
+ }
2854
+
2855
+ function reportRestartFailure({ taskId, projectId, requestId, mode, error, sendAck = true }) {
2856
+ const prefix =
2857
+ mode === "bridge_to_new_task" || mode === "fork_to_new_task"
2858
+ ? "new task failed"
2859
+ : "restart failed";
2860
+ const summary = `${prefix}: ${error?.message || error}`;
2861
+ if (sendAck) {
2862
+ sendAgentCommandAck({
2863
+ requestId,
2864
+ taskId,
2865
+ eventType: "restart_task",
2866
+ accepted: false,
2867
+ }).catch(() => {});
2868
+ }
2869
+ client
2870
+ .sendJson({
2871
+ type: "task_status_update",
2872
+ payload: {
2873
+ task_id: taskId,
2874
+ project_id: projectId,
2875
+ status: "KILLED",
2876
+ summary,
2877
+ },
2878
+ })
2879
+ .catch((err) => {
2880
+ logError(`Failed to report restart_task failure for ${taskId}: ${err?.message || err}`);
2881
+ });
2882
+ }
2883
+
2884
+ async function resolveRestartCwd({
2885
+ projectId,
2886
+ preferredCwd = "",
2887
+ backendType,
2888
+ sessionId,
2889
+ sourceSessionFilePath = "",
2890
+ }) {
2891
+ const normalizedPreferredCwd = typeof preferredCwd === "string" ? preferredCwd.trim() : "";
2892
+ if (normalizedPreferredCwd) {
2893
+ return normalizedPreferredCwd;
2894
+ }
2895
+
2896
+ const boundPath = await getProjectLocalPath(projectId);
2897
+ if (boundPath) {
2898
+ return boundPath;
2899
+ }
2900
+
2901
+ const normalizedBackend = normalizeRuntimeBackendName(backendType);
2902
+ const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
2903
+ if (normalizedSessionId && normalizedBackend && normalizedBackend !== "opencode") {
2904
+ try {
2905
+ const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
2906
+ normalizedBackend,
2907
+ normalizedSessionId,
2908
+ { cwd: process.cwd() },
2909
+ );
2910
+ if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
2911
+ return resumeContext.cwd.trim();
2912
+ }
2913
+ } catch {
2914
+ // ignore provider-specific fallback failure here; we'll try the remaining fallbacks
2915
+ }
2916
+ }
2917
+
2918
+ const normalizedSessionPath =
2919
+ typeof sourceSessionFilePath === "string" ? sourceSessionFilePath.trim() : "";
2920
+ if (normalizedSessionPath) {
2921
+ try {
2922
+ const stats = fs.statSync(normalizedSessionPath);
2923
+ if (stats.isDirectory()) {
2924
+ return normalizedSessionPath;
2925
+ }
2926
+ return path.dirname(normalizedSessionPath);
2927
+ } catch {
2928
+ // ignore missing local path
2929
+ }
2930
+ }
2931
+
2932
+ return "";
2933
+ }
2934
+
2795
2935
  async function handleCreateTask(payload) {
2796
2936
  const {
2797
2937
  task_id: taskId,
@@ -2888,11 +3028,11 @@ export function startDaemon(config = {}, deps = {}) {
2888
3028
  payload: {
2889
3029
  task_id: taskId,
2890
3030
  project_id: projectId,
2891
- status: "UNKNOWN",
3031
+ status: "INIT",
2892
3032
  },
2893
3033
  })
2894
3034
  .catch((err) => {
2895
- logError(`Failed to report task status (UNKNOWN) for ${taskId}: ${err?.message || err}`);
3035
+ logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
2896
3036
  });
2897
3037
 
2898
3038
  // Check if project has a bound local path for this daemon
@@ -3085,6 +3225,375 @@ export function startDaemon(config = {}, deps = {}) {
3085
3225
  });
3086
3226
  }
3087
3227
 
3228
+ async function handleRestartTask(payload) {
3229
+ const {
3230
+ mode,
3231
+ source_task_id: sourceTaskId,
3232
+ target_task_id: targetTaskId,
3233
+ project_id: projectId,
3234
+ title,
3235
+ source_backend_type: sourceBackendType,
3236
+ source_session_id: sourceSessionId,
3237
+ source_session_file_path: sourceSessionFilePath,
3238
+ target_backend_type: targetBackendType,
3239
+ request_id: requestIdRaw,
3240
+ } = payload || {};
3241
+
3242
+ const requestId = requestIdRaw ? String(requestIdRaw) : "";
3243
+ const normalizedMode = typeof mode === "string" ? mode.trim() : "";
3244
+ const normalizedSourceTaskId = sourceTaskId ? String(sourceTaskId) : "";
3245
+ const normalizedTargetTaskId = targetTaskId ? String(targetTaskId) : "";
3246
+ const normalizedProjectId = projectId ? String(projectId) : "";
3247
+ const normalizedSourceSessionId = sourceSessionId ? String(sourceSessionId).trim() : "";
3248
+
3249
+ if (
3250
+ !normalizedMode ||
3251
+ !normalizedSourceTaskId ||
3252
+ !normalizedTargetTaskId ||
3253
+ !normalizedProjectId ||
3254
+ !normalizedSourceSessionId
3255
+ ) {
3256
+ logError(`Invalid restart_task payload: ${JSON.stringify(payload)}`);
3257
+ sendAgentCommandAck({
3258
+ requestId,
3259
+ taskId: normalizedTargetTaskId || normalizedSourceTaskId,
3260
+ eventType: "restart_task",
3261
+ accepted: false,
3262
+ }).catch(() => {});
3263
+ return;
3264
+ }
3265
+
3266
+ if (requestId && !markRequestSeen(requestId)) {
3267
+ log(
3268
+ `Duplicate restart_task ignored for ${normalizedTargetTaskId} (request_id=${requestId})`,
3269
+ );
3270
+ sendAgentCommandAck({
3271
+ requestId,
3272
+ taskId: normalizedTargetTaskId,
3273
+ eventType: "restart_task",
3274
+ accepted: true,
3275
+ }).catch(() => {});
3276
+ return;
3277
+ }
3278
+
3279
+ if (daemonShuttingDown) {
3280
+ reportRestartFailure({
3281
+ taskId: normalizedTargetTaskId,
3282
+ projectId: normalizedProjectId,
3283
+ requestId,
3284
+ mode: normalizedMode,
3285
+ error: new Error("daemon shutting down"),
3286
+ });
3287
+ return;
3288
+ }
3289
+
3290
+ const activeTarget = activeTaskProcesses.get(normalizedTargetTaskId);
3291
+ if (activeTarget?.child) {
3292
+ reportRestartFailure({
3293
+ taskId: normalizedTargetTaskId,
3294
+ projectId: normalizedProjectId,
3295
+ requestId,
3296
+ mode: normalizedMode,
3297
+ error: new Error(`task already active (pid=${activeTarget.child.pid ?? "unknown"})`),
3298
+ });
3299
+ return;
3300
+ }
3301
+
3302
+ const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
3303
+ if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
3304
+ reportRestartFailure({
3305
+ taskId: normalizedTargetTaskId,
3306
+ projectId: normalizedProjectId,
3307
+ requestId,
3308
+ mode: normalizedMode,
3309
+ error: new Error(`Unsupported backend: ${effectiveBackend}`),
3310
+ });
3311
+ return;
3312
+ }
3313
+
3314
+ if (normalizedMode === "resume_inplace") {
3315
+ if (normalizedTargetTaskId !== normalizedSourceTaskId) {
3316
+ reportRestartFailure({
3317
+ taskId: normalizedTargetTaskId,
3318
+ projectId: normalizedProjectId,
3319
+ requestId,
3320
+ mode: normalizedMode,
3321
+ error: new Error("In-place restart must reuse the same task"),
3322
+ });
3323
+ return;
3324
+ }
3325
+ if (effectiveBackend !== sourceBackendType) {
3326
+ reportRestartFailure({
3327
+ taskId: normalizedTargetTaskId,
3328
+ projectId: normalizedProjectId,
3329
+ requestId,
3330
+ mode: normalizedMode,
3331
+ error: new Error("In-place restart must reuse the same backend"),
3332
+ });
3333
+ return;
3334
+ }
3335
+ }
3336
+
3337
+ sendAgentCommandAck({
3338
+ requestId,
3339
+ taskId: normalizedTargetTaskId,
3340
+ eventType: "restart_task",
3341
+ accepted: true,
3342
+ }).catch((err) => {
3343
+ logError(`Failed to report agent_command_ack(restart_task) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3344
+ });
3345
+
3346
+ let resolvedResumeSessionId = normalizedSourceSessionId;
3347
+ let resolvedResumeCwd = "";
3348
+ try {
3349
+ if (normalizedMode === "bridge_to_new_task" || normalizedMode === "fork_to_new_task") {
3350
+ const sourceResumeCwd = await resolveRestartCwd({
3351
+ projectId: normalizedProjectId,
3352
+ backendType: sourceBackendType,
3353
+ sessionId: normalizedSourceSessionId,
3354
+ sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3355
+ });
3356
+ const bridgeSession = await getBridgeSessionHelper();
3357
+ const bridgeResult = await bridgeSession({
3358
+ sourceTool: sourceBackendType,
3359
+ sourceSessionId: normalizedSourceSessionId,
3360
+ sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3361
+ sourceSessionInfo: {
3362
+ tool: sourceBackendType,
3363
+ sessionId: normalizedSourceSessionId,
3364
+ path: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3365
+ cwd: sourceResumeCwd || undefined,
3366
+ },
3367
+ targetTool: effectiveBackend,
3368
+ targetCwdFallback: sourceResumeCwd || undefined,
3369
+ });
3370
+ resolvedResumeSessionId = bridgeResult.sessionId;
3371
+ resolvedResumeCwd = await resolveRestartCwd({
3372
+ projectId: normalizedProjectId,
3373
+ preferredCwd: bridgeResult.cwd,
3374
+ backendType: effectiveBackend,
3375
+ sessionId: bridgeResult.sessionId,
3376
+ sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3377
+ });
3378
+ } else if (normalizedMode === "resume_inplace") {
3379
+ resolvedResumeCwd = await resolveRestartCwd({
3380
+ projectId: normalizedProjectId,
3381
+ backendType: effectiveBackend,
3382
+ sessionId: normalizedSourceSessionId,
3383
+ sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3384
+ });
3385
+ } else {
3386
+ throw new Error(`Unsupported restart mode: ${normalizedMode}`);
3387
+ }
3388
+ } catch (error) {
3389
+ reportRestartFailure({
3390
+ taskId: normalizedTargetTaskId,
3391
+ projectId: normalizedProjectId,
3392
+ requestId,
3393
+ mode: normalizedMode,
3394
+ error,
3395
+ sendAck: false,
3396
+ });
3397
+ return;
3398
+ }
3399
+
3400
+ if (!resolvedResumeCwd) {
3401
+ reportRestartFailure({
3402
+ taskId: normalizedTargetTaskId,
3403
+ projectId: normalizedProjectId,
3404
+ requestId,
3405
+ mode: normalizedMode,
3406
+ error: new Error("Could not resolve resume cwd"),
3407
+ sendAck: false,
3408
+ });
3409
+ return;
3410
+ }
3411
+
3412
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3413
+
3414
+ log("");
3415
+ log(
3416
+ `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
3417
+ );
3418
+ log(`CLI command: ${cliCommand}`);
3419
+
3420
+ if (normalizedMode !== "resume_inplace") {
3421
+ client
3422
+ .sendJson({
3423
+ type: "task_status_update",
3424
+ payload: {
3425
+ task_id: normalizedTargetTaskId,
3426
+ project_id: normalizedProjectId,
3427
+ status: "INIT",
3428
+ },
3429
+ })
3430
+ .catch((err) => {
3431
+ logError(`Failed to report task status (INIT) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3432
+ });
3433
+ }
3434
+
3435
+ if (daemonShuttingDown) {
3436
+ reportRestartFailure({
3437
+ taskId: normalizedTargetTaskId,
3438
+ projectId: normalizedProjectId,
3439
+ requestId,
3440
+ mode: normalizedMode,
3441
+ error: new Error("daemon shutting down"),
3442
+ sendAck: false,
3443
+ });
3444
+ return;
3445
+ }
3446
+
3447
+ let taskDir = resolvedResumeCwd;
3448
+ let logPath = path.join(taskDir, "conductor.log");
3449
+
3450
+ try {
3451
+ mkdirSyncFn(taskDir, { recursive: true });
3452
+ } catch (err) {
3453
+ reportRestartFailure({
3454
+ taskId: normalizedTargetTaskId,
3455
+ projectId: normalizedProjectId,
3456
+ requestId,
3457
+ mode: normalizedMode,
3458
+ error: new Error(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`),
3459
+ sendAck: false,
3460
+ });
3461
+ return;
3462
+ }
3463
+
3464
+ const args = [];
3465
+ if (effectiveBackend) {
3466
+ args.push("--backend", effectiveBackend);
3467
+ }
3468
+ args.push("--resume", resolvedResumeSessionId);
3469
+ args.push("--");
3470
+
3471
+ const env = {
3472
+ ...process.env,
3473
+ CONDUCTOR_PROJECT_ID: normalizedProjectId,
3474
+ CONDUCTOR_TASK_ID: normalizedTargetTaskId,
3475
+ CONDUCTOR_CLI_COMMAND: cliCommand,
3476
+ CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
3477
+ };
3478
+ if (config.CONFIG_FILE) {
3479
+ env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
3480
+ }
3481
+ if (AGENT_TOKEN) {
3482
+ env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
3483
+ }
3484
+ if (BACKEND_HTTP) {
3485
+ env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
3486
+ }
3487
+
3488
+ const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3489
+ cwd: taskDir,
3490
+ env,
3491
+ stdio: ["inherit", "pipe", "pipe"],
3492
+ });
3493
+
3494
+ let logStream;
3495
+ try {
3496
+ logStream = createWriteStreamFn(logPath, { flags: "a" });
3497
+ if (logStream && typeof logStream.on === "function") {
3498
+ const logPathSnapshot = logPath;
3499
+ logStream.on("error", (err) => {
3500
+ logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
3501
+ });
3502
+ }
3503
+ } catch (err) {
3504
+ logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3505
+ }
3506
+
3507
+ log(`Task title: ${title || normalizedTargetTaskId}`);
3508
+ log(`Resume session: ${resolvedResumeSessionId}`);
3509
+ log(`Resume cwd: ${resolvedResumeCwd}`);
3510
+ log(`Logs: ${logPath}`);
3511
+
3512
+ activeTaskProcesses.set(normalizedTargetTaskId, {
3513
+ child,
3514
+ projectId: normalizedProjectId,
3515
+ logPath,
3516
+ stopForceKillTimer: null,
3517
+ });
3518
+
3519
+ client
3520
+ .sendJson({
3521
+ type: "task_status_update",
3522
+ payload: {
3523
+ task_id: normalizedTargetTaskId,
3524
+ project_id: normalizedProjectId,
3525
+ status: "RUNNING",
3526
+ },
3527
+ })
3528
+ .catch((err) => {
3529
+ logError(`Failed to report task status (RUNNING) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3530
+ });
3531
+
3532
+ if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
3533
+ child.stdout.pipe(logStream, { end: false });
3534
+ } else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
3535
+ child.stdout.on("data", (chunk) => logStream.write(chunk));
3536
+ }
3537
+ if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
3538
+ child.stderr.pipe(logStream, { end: false });
3539
+ } else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
3540
+ child.stderr.on("data", (chunk) => logStream.write(chunk));
3541
+ }
3542
+
3543
+ child.on("error", (err) => {
3544
+ logError(`Failed to spawn restart CLI for ${normalizedTargetTaskId}: ${err.message}`);
3545
+ if (logStream) {
3546
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3547
+ logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
3548
+ }
3549
+ });
3550
+
3551
+ child.on("exit", (code, signal) => {
3552
+ const active = activeTaskProcesses.get(normalizedTargetTaskId);
3553
+ if (active?.stopForceKillTimer) {
3554
+ clearTimeout(active.stopForceKillTimer);
3555
+ }
3556
+ activeTaskProcesses.delete(normalizedTargetTaskId);
3557
+ const suppressExitStatusReport = suppressedExitStatusReports.has(normalizedTargetTaskId);
3558
+ suppressedExitStatusReports.delete(normalizedTargetTaskId);
3559
+ if (logStream) {
3560
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3561
+ if (signal) {
3562
+ logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3563
+ } else {
3564
+ logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3565
+ }
3566
+ logStream.end();
3567
+ }
3568
+
3569
+ const isKilledBySignal = Boolean(signal);
3570
+ const isKilledByExitCode = code === 130 || code === 143;
3571
+ const isKilled = isKilledBySignal || isKilledByExitCode;
3572
+ const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
3573
+ const summary = isKilled
3574
+ ? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
3575
+ : code === 0
3576
+ ? "completed"
3577
+ : `exited with code ${code}`;
3578
+
3579
+ if (!suppressExitStatusReport) {
3580
+ client
3581
+ .sendJson({
3582
+ type: "task_status_update",
3583
+ payload: {
3584
+ task_id: normalizedTargetTaskId,
3585
+ project_id: normalizedProjectId,
3586
+ status,
3587
+ summary,
3588
+ },
3589
+ })
3590
+ .catch((err) => {
3591
+ logError(`Failed to report task status (${status}) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3592
+ });
3593
+ }
3594
+ });
3595
+ }
3596
+
3088
3597
  let closePromise = null;
3089
3598
  async function shutdownDaemon(reason = "manual close") {
3090
3599
  if (closePromise) {
@@ -315,8 +315,16 @@ export async function repairAndVerifyGlobalNodePty({
315
315
  await ensurePnpmOnlyBuiltDependencies({ runCommand, dependencies, global: true });
316
316
  }
317
317
 
318
+ const packageDirectory = await resolveGlobalPackageDirectory({
319
+ packageManager,
320
+ packageName,
321
+ runCommand,
322
+ });
323
+
318
324
  if (packageManager === "pnpm") {
319
- const rebuildResult = await runCommand("pnpm", ["rebuild", "-g", ...dependencies]);
325
+ const rebuildResult = await runCommand("pnpm", ["rebuild", ...dependencies], {
326
+ cwd: packageDirectory,
327
+ });
320
328
  if (!rebuildResult.success) {
321
329
  throw new Error(
322
330
  `pnpm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
@@ -336,12 +344,6 @@ export async function repairAndVerifyGlobalNodePty({
336
344
  );
337
345
  }
338
346
  }
339
-
340
- const packageDirectory = await resolveGlobalPackageDirectory({
341
- packageManager,
342
- packageName,
343
- runCommand,
344
- });
345
347
  await verifyNodePtyForPackageDirectory({
346
348
  packageDirectory,
347
349
  runCommand,