@love-moon/conductor-cli 0.2.30 → 0.2.32

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.
package/src/daemon.js CHANGED
@@ -11,7 +11,13 @@ import yaml from "js-yaml";
11
11
  import { ConductorWebSocketClient, ConductorConfig, loadConfig, ConfigFileNotFound } from "@love-moon/conductor-sdk";
12
12
  import { DaemonLogCollector } from "./log-collector.js";
13
13
  import { resolveResumeContext } from "./fire/resume.js";
14
- import { filterRuntimeSupportedAllowCliList, normalizeRuntimeBackendName } from "./runtime-backends.js";
14
+ import {
15
+ RUNTIME_SUPPORTED_BACKENDS,
16
+ isRuntimeSupportedBackend,
17
+ listRuntimeSupportedBackends,
18
+ normalizeRuntimeBackendAlias,
19
+ normalizeRuntimeBackendName,
20
+ } from "./runtime-backends.js";
15
21
  import {
16
22
  PACKAGE_NAME,
17
23
  fetchLatestVersion,
@@ -53,6 +59,7 @@ const DEFAULT_TERMINAL_ROWS = 40;
53
59
  const DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES = 2 * 1024 * 1024;
54
60
  const DEFAULT_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES = 128 * 1024;
55
61
  const DEFAULT_RTC_MODULE_CANDIDATES = ["@roamhq/wrtc", "wrtc"];
62
+ const BLOCKING_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
56
63
  let nodePtySpawnPromise = null;
57
64
 
58
65
  function resolveNodePtySpawnExport(mod) {
@@ -111,6 +118,20 @@ function logError(message) {
111
118
  appendDaemonLog(line);
112
119
  }
113
120
 
121
+ function sleepSync(ms) {
122
+ if (!Number.isFinite(ms) || ms <= 0) {
123
+ return;
124
+ }
125
+ try {
126
+ Atomics.wait(BLOCKING_SLEEP_BUFFER, 0, 0, ms);
127
+ } catch {
128
+ const deadline = Date.now() + ms;
129
+ while (Date.now() < deadline) {
130
+ // best-effort fallback for runtimes that disallow Atomics.wait
131
+ }
132
+ }
133
+ }
134
+
114
135
  function getUserConfig(configFilePath) {
115
136
  try {
116
137
  const home = os.homedir();
@@ -180,14 +201,46 @@ const DEFAULT_CLI_LIST = {
180
201
  opencode: "opencode",
181
202
  };
182
203
 
204
+ const LEGACY_RUNTIME_BACKEND_ALIASES = new Set(["code", "claude-code", "open-code", "open_code", "kimi-cli", "kimi-code"]);
205
+
206
+ function filterConfiguredAllowCliList(allowCliList) {
207
+ if (!allowCliList || typeof allowCliList !== "object") {
208
+ return {};
209
+ }
210
+ const builtInBackends = new Set(RUNTIME_SUPPORTED_BACKENDS);
211
+ const filtered = {};
212
+ for (const [backend, command] of Object.entries(allowCliList)) {
213
+ const normalizedBackend = normalizeRuntimeBackendName(backend);
214
+ if (
215
+ !normalizedBackend ||
216
+ LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend) ||
217
+ !builtInBackends.has(normalizedBackend)
218
+ ) {
219
+ continue;
220
+ }
221
+ if (typeof command !== "string" || !command.trim()) {
222
+ continue;
223
+ }
224
+ if (filtered[normalizedBackend] !== undefined) {
225
+ continue;
226
+ }
227
+ filtered[normalizedBackend] = command.trim();
228
+ }
229
+ return filtered;
230
+ }
231
+
183
232
  function getAllowCliList(userConfig) {
184
233
  // If user has configured allow_cli_list, use it; otherwise use defaults
185
234
  if (userConfig.allow_cli_list && typeof userConfig.allow_cli_list === "object") {
186
- return filterRuntimeSupportedAllowCliList(userConfig.allow_cli_list);
235
+ return filterConfiguredAllowCliList(userConfig.allow_cli_list);
187
236
  }
188
237
  return DEFAULT_CLI_LIST;
189
238
  }
190
239
 
240
+ function formatBackendLaunchCommand(cliCommand) {
241
+ return typeof cliCommand === "string" && cliCommand.trim() ? cliCommand.trim() : "ai-sdk-managed";
242
+ }
243
+
191
244
  async function defaultCreatePty(command, args, options) {
192
245
  if (!nodePtySpawnPromise) {
193
246
  const spawnHelperInfo = ensureNodePtySpawnHelperExecutable();
@@ -458,7 +511,7 @@ export function startDaemon(config = {}, deps = {}) {
458
511
 
459
512
  // Get allow_cli_list from config
460
513
  const ALLOW_CLI_LIST = getAllowCliList(userConfig);
461
- const SUPPORTED_BACKENDS = Object.keys(ALLOW_CLI_LIST);
514
+ let SUPPORTED_BACKENDS = Object.keys(ALLOW_CLI_LIST);
462
515
  const fetchLatestVersionFn = deps.fetchLatestVersion || fetchLatestVersion;
463
516
  const isNewerVersionFn = deps.isNewerVersion || isNewerVersion;
464
517
  const detectPackageManagerFn = deps.detectPackageManager || detectPackageManager;
@@ -522,6 +575,18 @@ export function startDaemon(config = {}, deps = {}) {
522
575
  process.env.CONDUCTOR_STOP_FORCE_KILL_TIMEOUT_MS,
523
576
  5000,
524
577
  );
578
+ const DAEMON_FORCE_STOP_GRACE_MS = parsePositiveInt(
579
+ process.env.CONDUCTOR_DAEMON_FORCE_STOP_GRACE_MS,
580
+ 15_000,
581
+ );
582
+ const DAEMON_FORCE_STOP_POLL_INTERVAL_MS = parsePositiveInt(
583
+ process.env.CONDUCTOR_DAEMON_FORCE_STOP_POLL_INTERVAL_MS,
584
+ 100,
585
+ );
586
+ const DAEMON_FORCE_KILL_WAIT_MS = parsePositiveInt(
587
+ process.env.CONDUCTOR_DAEMON_FORCE_KILL_WAIT_MS,
588
+ 2_000,
589
+ );
525
590
  const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
526
591
  process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
527
592
  1000,
@@ -610,6 +675,20 @@ export function startDaemon(config = {}, deps = {}) {
610
675
  return true;
611
676
  };
612
677
 
678
+ const waitForProcessExitSync = (pid, timeoutMs) => {
679
+ const deadline = Date.now() + timeoutMs;
680
+ while (true) {
681
+ if (!isProcessAlive(pid)) {
682
+ return true;
683
+ }
684
+ const remainingMs = deadline - Date.now();
685
+ if (remainingMs <= 0) {
686
+ return false;
687
+ }
688
+ sleepSync(Math.min(DAEMON_FORCE_STOP_POLL_INTERVAL_MS, remainingMs));
689
+ }
690
+ };
691
+
613
692
  try {
614
693
  mkdirSyncFn(WORKSPACE_ROOT, { recursive: true });
615
694
  } catch (err) {
@@ -632,17 +711,35 @@ export function startDaemon(config = {}, deps = {}) {
632
711
  if (alive) {
633
712
  if (config.FORCE) {
634
713
  log(`Force enabled: stopping existing daemon PID ${pid}`);
714
+ let alreadyExited = false;
635
715
  try {
636
716
  killFn(pid, "SIGTERM");
637
717
  } catch (killErr) {
638
- if (!killErr || killErr.code !== "ESRCH") {
718
+ if (killErr?.code === "ESRCH") {
719
+ alreadyExited = true;
720
+ } else {
639
721
  logError(`Failed to stop existing daemon PID ${pid}: ${killErr.message}`);
640
722
  return exitAndReturn(1);
641
723
  }
642
724
  }
643
725
  try {
644
- if (isProcessAlive(pid)) {
645
- logError(`Existing daemon PID ${pid} is still running; please stop it manually.`);
726
+ let exited = alreadyExited || waitForProcessExitSync(pid, DAEMON_FORCE_STOP_GRACE_MS);
727
+ if (!exited) {
728
+ log(
729
+ `Existing daemon PID ${pid} did not exit within ${DAEMON_FORCE_STOP_GRACE_MS}ms; sending SIGKILL`,
730
+ );
731
+ try {
732
+ killFn(pid, "SIGKILL");
733
+ } catch (killErr) {
734
+ if (killErr?.code !== "ESRCH") {
735
+ logError(`Failed to force kill existing daemon PID ${pid}: ${killErr.message}`);
736
+ return exitAndReturn(1);
737
+ }
738
+ }
739
+ exited = waitForProcessExitSync(pid, DAEMON_FORCE_KILL_WAIT_MS);
740
+ }
741
+ if (!exited) {
742
+ logError(`Existing daemon PID ${pid} is still running after force restart; please stop it manually.`);
646
743
  return exitAndReturn(1);
647
744
  }
648
745
  } catch (checkErr) {
@@ -650,7 +747,9 @@ export function startDaemon(config = {}, deps = {}) {
650
747
  return exitAndReturn(1);
651
748
  }
652
749
  log("Removing lock file after force stop");
653
- unlinkSyncFn(LOCK_FILE);
750
+ if (existsSyncFn(LOCK_FILE)) {
751
+ unlinkSyncFn(LOCK_FILE);
752
+ }
654
753
  } else {
655
754
  logError(`Daemon already running with PID ${pid}`);
656
755
  return exitAndReturn(1);
@@ -770,13 +869,6 @@ export function startDaemon(config = {}, deps = {}) {
770
869
  return { close: detachProcessHandlers };
771
870
  }
772
871
 
773
- log("Daemon starting...");
774
- log(`Backend: ${BACKEND_URL}`);
775
- log(`Workspace: ${WORKSPACE_ROOT}`);
776
- log(`CLI Path: ${CLI_PATH_VAL}`);
777
- log(`Daemon Name: ${AGENT_NAME}`);
778
- log(`Supported Backends: ${SUPPORTED_BACKENDS.join(", ")}`);
779
-
780
872
  const sdkConfig = new ConductorConfig({
781
873
  agentToken: AGENT_TOKEN,
782
874
  backendUrl: BACKEND_HTTP,
@@ -787,6 +879,7 @@ export function startDaemon(config = {}, deps = {}) {
787
879
  let didRecoverStaleTasks = false;
788
880
  let daemonShuttingDown = false;
789
881
  const activeTaskProcesses = new Map();
882
+ const pendingTaskStarts = new Set();
790
883
  const activePtySessions = new Map();
791
884
  const activePtyRtcTransports = new Map();
792
885
  const suppressedExitStatusReports = new Set();
@@ -903,23 +996,48 @@ export function startDaemon(config = {}, deps = {}) {
903
996
  handleEvent(payload);
904
997
  });
905
998
 
906
- client.connect().catch((err) => {
907
- logError(`Failed to connect: ${err}`);
908
- });
999
+ void (async () => {
1000
+ try {
1001
+ const runtimeBackends = await listRuntimeSupportedBackends({ configFilePath: config.CONFIG_FILE });
1002
+ const externalBackends = runtimeBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
1003
+ SUPPORTED_BACKENDS = [...new Set([...Object.keys(ALLOW_CLI_LIST), ...externalBackends])];
1004
+ } catch (error) {
1005
+ logError(`Failed to discover external backends: ${error?.message || error}`);
1006
+ }
909
1007
 
910
- if (!AUTO_UPDATE_ENABLED && autoUpdateSupportedInstall === false) {
911
- log("[auto-update] Disabled for local/dev install; set CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL=true to override");
912
- }
1008
+ extraHeaders["x-conductor-backends"] = SUPPORTED_BACKENDS.join(",");
1009
+ if (typeof client?.setExtraHeaders === "function") {
1010
+ client.setExtraHeaders(extraHeaders);
1011
+ }
1012
+ if (daemonShuttingDown) {
1013
+ return;
1014
+ }
913
1015
 
914
- watchdogTimer = setInterval(() => {
915
- void runDaemonWatchdog();
916
- // Auto-update checks (internally throttled)
917
- void checkForUpdate().catch(() => {});
918
- void tryAutoUpdate().catch(() => {});
919
- }, DAEMON_WATCHDOG_INTERVAL_MS);
920
- if (typeof watchdogTimer?.unref === "function") {
921
- watchdogTimer.unref();
922
- }
1016
+ log("Daemon starting...");
1017
+ log(`Backend: ${BACKEND_URL}`);
1018
+ log(`Workspace: ${WORKSPACE_ROOT}`);
1019
+ log(`CLI Path: ${CLI_PATH_VAL}`);
1020
+ log(`Daemon Name: ${AGENT_NAME}`);
1021
+ log(`Supported Backends: ${SUPPORTED_BACKENDS.join(", ")}`);
1022
+
1023
+ client.connect().catch((err) => {
1024
+ logError(`Failed to connect: ${err}`);
1025
+ });
1026
+
1027
+ if (!AUTO_UPDATE_ENABLED && autoUpdateSupportedInstall === false) {
1028
+ log("[auto-update] Disabled for local/dev install; set CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL=true to override");
1029
+ }
1030
+
1031
+ watchdogTimer = setInterval(() => {
1032
+ void runDaemonWatchdog();
1033
+ // Auto-update checks (internally throttled)
1034
+ void checkForUpdate().catch(() => {});
1035
+ void tryAutoUpdate().catch(() => {});
1036
+ }, DAEMON_WATCHDOG_INTERVAL_MS);
1037
+ if (typeof watchdogTimer?.unref === "function") {
1038
+ watchdogTimer.unref();
1039
+ }
1040
+ })();
923
1041
 
924
1042
  function markBackendHttpSuccess(at = Date.now()) {
925
1043
  lastSuccessfulHttpAt = at;
@@ -2556,7 +2674,9 @@ export function startDaemon(config = {}, deps = {}) {
2556
2674
  }
2557
2675
 
2558
2676
  if (event.type === "create_task") {
2559
- handleCreateTask(event.payload);
2677
+ void handleCreateTask(event.payload).catch((error) => {
2678
+ logError(`Unhandled create_task failure: ${error?.message || error}`);
2679
+ });
2560
2680
  return;
2561
2681
  }
2562
2682
  if (event.type === "restart_task") {
@@ -2881,6 +3001,39 @@ export function startDaemon(config = {}, deps = {}) {
2881
3001
  });
2882
3002
  }
2883
3003
 
3004
+ function reportCreateTaskFailure({ taskId, projectId, requestId, error, sendAck = true }) {
3005
+ const normalizedTaskId = taskId ? String(taskId) : "";
3006
+ const normalizedProjectId = projectId ? String(projectId) : "";
3007
+ const message = error instanceof Error ? error.message : String(error);
3008
+ logError(`Failed to create task ${normalizedTaskId || "unknown"}: ${message}`);
3009
+ if (sendAck) {
3010
+ sendAgentCommandAck({
3011
+ requestId,
3012
+ taskId: normalizedTaskId,
3013
+ eventType: "create_task",
3014
+ accepted: false,
3015
+ }).catch((err) => {
3016
+ logError(`Failed to report agent_command_ack(create_task) for ${normalizedTaskId}: ${err?.message || err}`);
3017
+ });
3018
+ }
3019
+ if (!normalizedTaskId || !normalizedProjectId) {
3020
+ return;
3021
+ }
3022
+ client
3023
+ .sendJson({
3024
+ type: "task_status_update",
3025
+ payload: {
3026
+ task_id: normalizedTaskId,
3027
+ project_id: normalizedProjectId,
3028
+ status: "KILLED",
3029
+ summary: message,
3030
+ },
3031
+ })
3032
+ .catch((err) => {
3033
+ logError(`Failed to report task status (KILLED) for ${normalizedTaskId}: ${err?.message || err}`);
3034
+ });
3035
+ }
3036
+
2884
3037
  async function resolveRestartCwd({
2885
3038
  projectId,
2886
3039
  preferredCwd = "",
@@ -2905,7 +3058,10 @@ export function startDaemon(config = {}, deps = {}) {
2905
3058
  const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
2906
3059
  normalizedBackend,
2907
3060
  normalizedSessionId,
2908
- { cwd: process.cwd() },
3061
+ {
3062
+ cwd: process.cwd(),
3063
+ configFilePath: config.CONFIG_FILE,
3064
+ },
2909
3065
  );
2910
3066
  if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
2911
3067
  return resumeContext.cwd.trim();
@@ -2971,9 +3127,9 @@ export function startDaemon(config = {}, deps = {}) {
2971
3127
  }
2972
3128
 
2973
3129
  const existingTaskRecord = activeTaskProcesses.get(taskId);
2974
- if (existingTaskRecord?.child) {
3130
+ if (existingTaskRecord?.child || pendingTaskStarts.has(taskId)) {
2975
3131
  log(
2976
- `Duplicate create_task ignored for ${taskId}: task already active (pid=${existingTaskRecord.child.pid ?? "unknown"})`,
3132
+ `Duplicate create_task ignored for ${taskId}: task already active (pid=${existingTaskRecord?.child?.pid ?? "unknown"})`,
2977
3133
  );
2978
3134
  sendAgentCommandAck({
2979
3135
  requestId,
@@ -2984,245 +3140,256 @@ export function startDaemon(config = {}, deps = {}) {
2984
3140
  return;
2985
3141
  }
2986
3142
 
2987
- // Validate and get CLI command for the backend
2988
- const effectiveBackend = normalizeRuntimeBackendName(backendType || SUPPORTED_BACKENDS[0]);
2989
- if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
2990
- logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
3143
+ pendingTaskStarts.add(taskId);
3144
+ let acceptedAckSent = false;
3145
+ try {
3146
+ const effectiveBackend = await normalizeRuntimeBackendAlias(backendType || SUPPORTED_BACKENDS[0], {
3147
+ configFilePath: config.CONFIG_FILE,
3148
+ });
3149
+ if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
3150
+ logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
3151
+ sendAgentCommandAck({
3152
+ requestId,
3153
+ taskId,
3154
+ eventType: "create_task",
3155
+ accepted: false,
3156
+ }).catch(() => {});
3157
+ client
3158
+ .sendJson({
3159
+ type: "task_status_update",
3160
+ payload: {
3161
+ task_id: taskId,
3162
+ project_id: projectId,
3163
+ status: "KILLED",
3164
+ summary: `Unsupported backend: ${effectiveBackend}`,
3165
+ },
3166
+ })
3167
+ .catch(() => {});
3168
+ return;
3169
+ }
3170
+
2991
3171
  sendAgentCommandAck({
2992
3172
  requestId,
2993
3173
  taskId,
2994
3174
  eventType: "create_task",
2995
- accepted: false,
2996
- }).catch(() => {});
3175
+ accepted: true,
3176
+ }).catch((err) => {
3177
+ logError(`Failed to report agent_command_ack(create_task) for ${taskId}: ${err?.message || err}`);
3178
+ });
3179
+ acceptedAckSent = true;
3180
+
3181
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3182
+
3183
+ log("");
3184
+ log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
3185
+ log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
2997
3186
  client
2998
3187
  .sendJson({
2999
3188
  type: "task_status_update",
3000
3189
  payload: {
3001
3190
  task_id: taskId,
3002
3191
  project_id: projectId,
3003
- status: "KILLED",
3004
- summary: `Unsupported backend: ${effectiveBackend}`,
3192
+ status: "INIT",
3005
3193
  },
3006
3194
  })
3007
- .catch(() => {});
3008
- return;
3009
- }
3010
-
3011
- sendAgentCommandAck({
3012
- requestId,
3013
- taskId,
3014
- eventType: "create_task",
3015
- accepted: true,
3016
- }).catch((err) => {
3017
- logError(`Failed to report agent_command_ack(create_task) for ${taskId}: ${err?.message || err}`);
3018
- });
3019
-
3020
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3021
-
3022
- log("");
3023
- log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
3024
- log(`CLI command: ${cliCommand}`);
3025
- client
3026
- .sendJson({
3027
- type: "task_status_update",
3028
- payload: {
3029
- task_id: taskId,
3030
- project_id: projectId,
3031
- status: "INIT",
3032
- },
3033
- })
3034
- .catch((err) => {
3035
- logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
3036
- });
3037
-
3038
- // Check if project has a bound local path for this daemon
3039
- const boundPath = await getProjectLocalPath(projectId);
3040
- if (daemonShuttingDown) {
3041
- rejectCreateTaskDuringShutdown(payload, { sendAck: false });
3042
- return;
3043
- }
3044
- let taskDir;
3045
- let logPath;
3046
- let runTimestampPart = null;
3195
+ .catch((err) => {
3196
+ logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
3197
+ });
3047
3198
 
3048
- if (boundPath) {
3049
- // Use the bound path directly (don't create subdirectory)
3050
- taskDir = boundPath;
3051
- log(`Using project bound path: ${taskDir}`);
3052
- // Create log file in the bound path
3053
- logPath = path.join(taskDir, "conductor.log");
3054
- } else {
3055
- // Use Beijing timestamp + process id workspace structure:
3056
- // YYYY-MM-DD/HH-mm-ss_pid_<fire-pid>/
3057
- // Child pid is only known after spawn; start with daemon pid and rename after spawn.
3058
- const now = new Date();
3059
- const dayDir = path.join(WORKSPACE_ROOT, formatWorkspaceDate(now));
3060
- runTimestampPart = formatWorkspaceRunTimestamp(now);
3061
- const pendingRunDir = `${runTimestampPart}_pid_${process.pid}`;
3062
- taskDir = path.join(dayDir, pendingRunDir);
3063
- mkdirSyncFn(taskDir, { recursive: true });
3064
- logPath = path.join(taskDir, "conductor.log");
3065
- }
3199
+ const boundPath = await getProjectLocalPath(projectId);
3200
+ if (daemonShuttingDown) {
3201
+ rejectCreateTaskDuringShutdown(payload, { sendAck: false });
3202
+ return;
3203
+ }
3066
3204
 
3067
- const args = [];
3068
- if (effectiveBackend) {
3069
- args.push("--backend", effectiveBackend);
3070
- }
3071
- if (initialContent) {
3072
- args.push("--prefill", initialContent);
3073
- }
3074
- // Explicitly separate conductor flags from backend args so they don't leak into messages
3075
- args.push("--");
3205
+ let taskDir;
3206
+ let logPath;
3207
+ let runTimestampPart = null;
3076
3208
 
3077
- const env = {
3078
- ...process.env,
3079
- CONDUCTOR_PROJECT_ID: projectId,
3080
- CONDUCTOR_TASK_ID: taskId,
3081
- CONDUCTOR_CLI_COMMAND: cliCommand,
3082
- };
3083
- if (config.CONFIG_FILE) {
3084
- env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
3085
- }
3086
- if (AGENT_TOKEN) {
3087
- env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
3088
- }
3089
- if (BACKEND_HTTP) {
3090
- env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
3091
- }
3209
+ if (boundPath) {
3210
+ taskDir = boundPath;
3211
+ log(`Using project bound path: ${taskDir}`);
3212
+ logPath = path.join(taskDir, "conductor.log");
3213
+ } else {
3214
+ const now = new Date();
3215
+ const dayDir = path.join(WORKSPACE_ROOT, formatWorkspaceDate(now));
3216
+ runTimestampPart = formatWorkspaceRunTimestamp(now);
3217
+ const pendingRunDir = `${runTimestampPart}_pid_${process.pid}`;
3218
+ taskDir = path.join(dayDir, pendingRunDir);
3219
+ mkdirSyncFn(taskDir, { recursive: true });
3220
+ logPath = path.join(taskDir, "conductor.log");
3221
+ }
3222
+
3223
+ const args = [];
3224
+ if (effectiveBackend) {
3225
+ args.push("--backend", effectiveBackend);
3226
+ }
3227
+ if (initialContent) {
3228
+ args.push("--prefill", initialContent);
3229
+ }
3230
+ args.push("--");
3231
+
3232
+ const env = {
3233
+ ...process.env,
3234
+ CONDUCTOR_PROJECT_ID: projectId,
3235
+ CONDUCTOR_TASK_ID: taskId,
3236
+ CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
3237
+ ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
3238
+ };
3239
+ if (config.CONFIG_FILE) {
3240
+ env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
3241
+ }
3242
+ if (AGENT_TOKEN) {
3243
+ env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
3244
+ }
3245
+ if (BACKEND_HTTP) {
3246
+ env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
3247
+ }
3092
3248
 
3093
- const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3094
- cwd: taskDir,
3095
- env,
3096
- stdio: ["inherit", "pipe", "pipe"],
3097
- });
3249
+ const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3250
+ cwd: taskDir,
3251
+ env,
3252
+ stdio: ["inherit", "pipe", "pipe"],
3253
+ });
3098
3254
 
3099
- if (!boundPath && runTimestampPart && Number.isInteger(child?.pid) && child.pid > 0) {
3100
- const desiredTaskDir = path.join(path.dirname(taskDir), `${runTimestampPart}_pid_${child.pid}`);
3101
- if (desiredTaskDir !== taskDir) {
3102
- try {
3103
- renameSyncFn(taskDir, desiredTaskDir);
3104
- taskDir = desiredTaskDir;
3105
- logPath = path.join(taskDir, "conductor.log");
3106
- } catch (err) {
3107
- logError(
3108
- `Failed to rename workspace dir from ${taskDir} to ${desiredTaskDir}: ${err?.message || err}`,
3109
- );
3255
+ if (!boundPath && runTimestampPart && Number.isInteger(child?.pid) && child.pid > 0) {
3256
+ const desiredTaskDir = path.join(path.dirname(taskDir), `${runTimestampPart}_pid_${child.pid}`);
3257
+ if (desiredTaskDir !== taskDir) {
3258
+ try {
3259
+ renameSyncFn(taskDir, desiredTaskDir);
3260
+ taskDir = desiredTaskDir;
3261
+ logPath = path.join(taskDir, "conductor.log");
3262
+ } catch (err) {
3263
+ logError(
3264
+ `Failed to rename workspace dir from ${taskDir} to ${desiredTaskDir}: ${err?.message || err}`,
3265
+ );
3266
+ }
3110
3267
  }
3111
3268
  }
3112
- }
3113
-
3114
- try {
3115
- mkdirSyncFn(taskDir, { recursive: true });
3116
- } catch (err) {
3117
- logError(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`);
3118
- }
3119
3269
 
3120
- let logStream;
3121
- try {
3122
- logStream = createWriteStreamFn(logPath, { flags: "a" });
3123
- if (logStream && typeof logStream.on === "function") {
3124
- const logPathSnapshot = logPath;
3125
- logStream.on("error", (err) => {
3126
- logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
3127
- });
3270
+ try {
3271
+ mkdirSyncFn(taskDir, { recursive: true });
3272
+ } catch (err) {
3273
+ logError(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`);
3128
3274
  }
3129
- } catch (err) {
3130
- logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3131
- }
3132
3275
 
3133
- log(`New task workspace: ${taskDir}`);
3134
- log(`Logs: ${logPath}`);
3276
+ let logStream;
3277
+ try {
3278
+ logStream = createWriteStreamFn(logPath, { flags: "a" });
3279
+ if (logStream && typeof logStream.on === "function") {
3280
+ const logPathSnapshot = logPath;
3281
+ logStream.on("error", (err) => {
3282
+ logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
3283
+ });
3284
+ }
3285
+ } catch (err) {
3286
+ logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3287
+ }
3135
3288
 
3136
- activeTaskProcesses.set(taskId, {
3137
- child,
3138
- projectId,
3139
- logPath,
3140
- stopForceKillTimer: null,
3141
- });
3289
+ log(`New task workspace: ${taskDir}`);
3290
+ log(`Logs: ${logPath}`);
3142
3291
 
3143
- client
3144
- .sendJson({
3145
- type: "task_status_update",
3146
- payload: {
3147
- task_id: taskId,
3148
- project_id: projectId,
3149
- status: "RUNNING",
3150
- },
3151
- })
3152
- .catch((err) => {
3153
- logError(`Failed to report task status (RUNNING) for ${taskId}: ${err?.message || err}`);
3292
+ activeTaskProcesses.set(taskId, {
3293
+ child,
3294
+ projectId,
3295
+ logPath,
3296
+ stopForceKillTimer: null,
3154
3297
  });
3155
3298
 
3156
- if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
3157
- child.stdout.pipe(logStream, { end: false });
3158
- } else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
3159
- child.stdout.on("data", (chunk) => logStream.write(chunk));
3160
- }
3161
- if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
3162
- child.stderr.pipe(logStream, { end: false });
3163
- } else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
3164
- child.stderr.on("data", (chunk) => logStream.write(chunk));
3165
- }
3299
+ client
3300
+ .sendJson({
3301
+ type: "task_status_update",
3302
+ payload: {
3303
+ task_id: taskId,
3304
+ project_id: projectId,
3305
+ status: "RUNNING",
3306
+ },
3307
+ })
3308
+ .catch((err) => {
3309
+ logError(`Failed to report task status (RUNNING) for ${taskId}: ${err?.message || err}`);
3310
+ });
3166
3311
 
3167
- child.on("error", (err) => {
3168
- logError(`Failed to spawn CLI: ${err.message}`);
3169
- if (logStream) {
3170
- const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3171
- logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
3312
+ if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
3313
+ child.stdout.pipe(logStream, { end: false });
3314
+ } else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
3315
+ child.stdout.on("data", (chunk) => logStream.write(chunk));
3172
3316
  }
3173
- });
3174
-
3175
- child.on("exit", (code, signal) => {
3176
- const active = activeTaskProcesses.get(taskId);
3177
- if (active?.stopForceKillTimer) {
3178
- clearTimeout(active.stopForceKillTimer);
3317
+ if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
3318
+ child.stderr.pipe(logStream, { end: false });
3319
+ } else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
3320
+ child.stderr.on("data", (chunk) => logStream.write(chunk));
3179
3321
  }
3180
- activeTaskProcesses.delete(taskId);
3181
- const suppressExitStatusReport = suppressedExitStatusReports.has(taskId);
3182
- suppressedExitStatusReports.delete(taskId);
3183
- if (logStream) {
3184
- const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3322
+
3323
+ child.on("error", (err) => {
3324
+ logError(`Failed to spawn CLI: ${err.message}`);
3325
+ if (logStream) {
3326
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3327
+ logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
3328
+ }
3329
+ });
3330
+
3331
+ child.on("exit", (code, signal) => {
3332
+ const active = activeTaskProcesses.get(taskId);
3333
+ if (active?.stopForceKillTimer) {
3334
+ clearTimeout(active.stopForceKillTimer);
3335
+ }
3336
+ activeTaskProcesses.delete(taskId);
3337
+ const suppressExitStatusReport = suppressedExitStatusReports.has(taskId);
3338
+ suppressedExitStatusReports.delete(taskId);
3339
+ if (logStream) {
3340
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3341
+ if (signal) {
3342
+ logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3343
+ } else {
3344
+ logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3345
+ }
3346
+ logStream.end();
3347
+ }
3185
3348
  if (signal) {
3186
- logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3349
+ log(`Task ${taskId} killed by signal ${signal}`);
3187
3350
  } else {
3188
- logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3351
+ log(`Task ${taskId} finished with code ${code}`);
3189
3352
  }
3190
- logStream.end();
3191
- }
3192
- if (signal) {
3193
- log(`Task ${taskId} killed by signal ${signal}`);
3194
- } else {
3195
- log(`Task ${taskId} finished with code ${code}`);
3196
- }
3197
- log(`Logs: ${logPath}`);
3198
-
3199
- const isKilledBySignal = Boolean(signal);
3200
- const isKilledByExitCode = code === 130 || code === 143;
3201
- const isKilled = isKilledBySignal || isKilledByExitCode;
3202
-
3203
- const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
3204
- const summary = isKilled
3205
- ? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
3206
- : code === 0
3207
- ? "completed"
3208
- : `exited with code ${code}`;
3209
-
3210
- if (!suppressExitStatusReport) {
3211
- client
3212
- .sendJson({
3213
- type: "task_status_update",
3214
- payload: {
3215
- task_id: taskId,
3216
- project_id: projectId,
3217
- status,
3218
- summary,
3219
- },
3220
- })
3221
- .catch((err) => {
3222
- logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
3223
- });
3224
- }
3225
- });
3353
+ log(`Logs: ${logPath}`);
3354
+
3355
+ const isKilledBySignal = Boolean(signal);
3356
+ const isKilledByExitCode = code === 130 || code === 143;
3357
+ const isKilled = isKilledBySignal || isKilledByExitCode;
3358
+
3359
+ const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
3360
+ const summary = isKilled
3361
+ ? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
3362
+ : code === 0
3363
+ ? "completed"
3364
+ : `exited with code ${code}`;
3365
+
3366
+ if (!suppressExitStatusReport) {
3367
+ client
3368
+ .sendJson({
3369
+ type: "task_status_update",
3370
+ payload: {
3371
+ task_id: taskId,
3372
+ project_id: projectId,
3373
+ status,
3374
+ summary,
3375
+ },
3376
+ })
3377
+ .catch((err) => {
3378
+ logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
3379
+ });
3380
+ }
3381
+ });
3382
+ } catch (error) {
3383
+ reportCreateTaskFailure({
3384
+ taskId,
3385
+ projectId,
3386
+ requestId,
3387
+ error,
3388
+ sendAck: !acceptedAckSent,
3389
+ });
3390
+ } finally {
3391
+ pendingTaskStarts.delete(taskId);
3392
+ }
3226
3393
  }
3227
3394
 
3228
3395
  async function handleRestartTask(payload) {
@@ -3299,8 +3466,10 @@ export function startDaemon(config = {}, deps = {}) {
3299
3466
  return;
3300
3467
  }
3301
3468
 
3302
- const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
3303
- if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
3469
+ const effectiveBackend = await normalizeRuntimeBackendAlias(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0], {
3470
+ configFilePath: config.CONFIG_FILE,
3471
+ });
3472
+ if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
3304
3473
  reportRestartFailure({
3305
3474
  taskId: normalizedTargetTaskId,
3306
3475
  projectId: normalizedProjectId,
@@ -3409,13 +3578,13 @@ export function startDaemon(config = {}, deps = {}) {
3409
3578
  return;
3410
3579
  }
3411
3580
 
3412
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3581
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3413
3582
 
3414
3583
  log("");
3415
3584
  log(
3416
3585
  `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
3417
3586
  );
3418
- log(`CLI command: ${cliCommand}`);
3587
+ log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3419
3588
 
3420
3589
  if (normalizedMode !== "resume_inplace") {
3421
3590
  client
@@ -3472,7 +3641,8 @@ export function startDaemon(config = {}, deps = {}) {
3472
3641
  ...process.env,
3473
3642
  CONDUCTOR_PROJECT_ID: normalizedProjectId,
3474
3643
  CONDUCTOR_TASK_ID: normalizedTargetTaskId,
3475
- CONDUCTOR_CLI_COMMAND: cliCommand,
3644
+ CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
3645
+ ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
3476
3646
  CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
3477
3647
  };
3478
3648
  if (config.CONFIG_FILE) {