@love-moon/conductor-cli 0.2.29 → 0.2.31

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,
@@ -180,14 +186,46 @@ const DEFAULT_CLI_LIST = {
180
186
  opencode: "opencode",
181
187
  };
182
188
 
189
+ const LEGACY_RUNTIME_BACKEND_ALIASES = new Set(["code", "claude-code", "open-code", "open_code", "kimi-cli", "kimi-code"]);
190
+
191
+ function filterConfiguredAllowCliList(allowCliList) {
192
+ if (!allowCliList || typeof allowCliList !== "object") {
193
+ return {};
194
+ }
195
+ const builtInBackends = new Set(RUNTIME_SUPPORTED_BACKENDS);
196
+ const filtered = {};
197
+ for (const [backend, command] of Object.entries(allowCliList)) {
198
+ const normalizedBackend = normalizeRuntimeBackendName(backend);
199
+ if (
200
+ !normalizedBackend ||
201
+ LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend) ||
202
+ !builtInBackends.has(normalizedBackend)
203
+ ) {
204
+ continue;
205
+ }
206
+ if (typeof command !== "string" || !command.trim()) {
207
+ continue;
208
+ }
209
+ if (filtered[normalizedBackend] !== undefined) {
210
+ continue;
211
+ }
212
+ filtered[normalizedBackend] = command.trim();
213
+ }
214
+ return filtered;
215
+ }
216
+
183
217
  function getAllowCliList(userConfig) {
184
218
  // If user has configured allow_cli_list, use it; otherwise use defaults
185
219
  if (userConfig.allow_cli_list && typeof userConfig.allow_cli_list === "object") {
186
- return filterRuntimeSupportedAllowCliList(userConfig.allow_cli_list);
220
+ return filterConfiguredAllowCliList(userConfig.allow_cli_list);
187
221
  }
188
222
  return DEFAULT_CLI_LIST;
189
223
  }
190
224
 
225
+ function formatBackendLaunchCommand(cliCommand) {
226
+ return typeof cliCommand === "string" && cliCommand.trim() ? cliCommand.trim() : "ai-sdk-managed";
227
+ }
228
+
191
229
  async function defaultCreatePty(command, args, options) {
192
230
  if (!nodePtySpawnPromise) {
193
231
  const spawnHelperInfo = ensureNodePtySpawnHelperExecutable();
@@ -458,7 +496,7 @@ export function startDaemon(config = {}, deps = {}) {
458
496
 
459
497
  // Get allow_cli_list from config
460
498
  const ALLOW_CLI_LIST = getAllowCliList(userConfig);
461
- const SUPPORTED_BACKENDS = Object.keys(ALLOW_CLI_LIST);
499
+ let SUPPORTED_BACKENDS = Object.keys(ALLOW_CLI_LIST);
462
500
  const fetchLatestVersionFn = deps.fetchLatestVersion || fetchLatestVersion;
463
501
  const isNewerVersionFn = deps.isNewerVersion || isNewerVersion;
464
502
  const detectPackageManagerFn = deps.detectPackageManager || detectPackageManager;
@@ -770,13 +808,6 @@ export function startDaemon(config = {}, deps = {}) {
770
808
  return { close: detachProcessHandlers };
771
809
  }
772
810
 
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
811
  const sdkConfig = new ConductorConfig({
781
812
  agentToken: AGENT_TOKEN,
782
813
  backendUrl: BACKEND_HTTP,
@@ -787,6 +818,7 @@ export function startDaemon(config = {}, deps = {}) {
787
818
  let didRecoverStaleTasks = false;
788
819
  let daemonShuttingDown = false;
789
820
  const activeTaskProcesses = new Map();
821
+ const pendingTaskStarts = new Set();
790
822
  const activePtySessions = new Map();
791
823
  const activePtyRtcTransports = new Map();
792
824
  const suppressedExitStatusReports = new Set();
@@ -903,23 +935,48 @@ export function startDaemon(config = {}, deps = {}) {
903
935
  handleEvent(payload);
904
936
  });
905
937
 
906
- client.connect().catch((err) => {
907
- logError(`Failed to connect: ${err}`);
908
- });
938
+ void (async () => {
939
+ try {
940
+ const runtimeBackends = await listRuntimeSupportedBackends({ configFilePath: config.CONFIG_FILE });
941
+ const externalBackends = runtimeBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
942
+ SUPPORTED_BACKENDS = [...new Set([...Object.keys(ALLOW_CLI_LIST), ...externalBackends])];
943
+ } catch (error) {
944
+ logError(`Failed to discover external backends: ${error?.message || error}`);
945
+ }
909
946
 
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
- }
947
+ extraHeaders["x-conductor-backends"] = SUPPORTED_BACKENDS.join(",");
948
+ if (typeof client?.setExtraHeaders === "function") {
949
+ client.setExtraHeaders(extraHeaders);
950
+ }
951
+ if (daemonShuttingDown) {
952
+ return;
953
+ }
913
954
 
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
- }
955
+ log("Daemon starting...");
956
+ log(`Backend: ${BACKEND_URL}`);
957
+ log(`Workspace: ${WORKSPACE_ROOT}`);
958
+ log(`CLI Path: ${CLI_PATH_VAL}`);
959
+ log(`Daemon Name: ${AGENT_NAME}`);
960
+ log(`Supported Backends: ${SUPPORTED_BACKENDS.join(", ")}`);
961
+
962
+ client.connect().catch((err) => {
963
+ logError(`Failed to connect: ${err}`);
964
+ });
965
+
966
+ if (!AUTO_UPDATE_ENABLED && autoUpdateSupportedInstall === false) {
967
+ log("[auto-update] Disabled for local/dev install; set CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL=true to override");
968
+ }
969
+
970
+ watchdogTimer = setInterval(() => {
971
+ void runDaemonWatchdog();
972
+ // Auto-update checks (internally throttled)
973
+ void checkForUpdate().catch(() => {});
974
+ void tryAutoUpdate().catch(() => {});
975
+ }, DAEMON_WATCHDOG_INTERVAL_MS);
976
+ if (typeof watchdogTimer?.unref === "function") {
977
+ watchdogTimer.unref();
978
+ }
979
+ })();
923
980
 
924
981
  function markBackendHttpSuccess(at = Date.now()) {
925
982
  lastSuccessfulHttpAt = at;
@@ -1174,13 +1231,18 @@ export function startDaemon(config = {}, deps = {}) {
1174
1231
  });
1175
1232
  }
1176
1233
 
1177
- function runCommand(command, args, timeoutMs = 120_000) {
1234
+ function runCommand(command, args, options = 120_000) {
1178
1235
  return new Promise((resolve) => {
1179
1236
  let stdout = "";
1180
1237
  let stderr = "";
1238
+ const normalizedOptions =
1239
+ typeof options === "number"
1240
+ ? { timeoutMs: options }
1241
+ : (options || {});
1181
1242
  const child = spawnFn(command, args, {
1182
1243
  stdio: ["ignore", "pipe", "pipe"],
1183
- env: { ...process.env },
1244
+ env: normalizedOptions.env || { ...process.env },
1245
+ cwd: normalizedOptions.cwd || process.cwd(),
1184
1246
  });
1185
1247
  const timer = setTimeout(() => {
1186
1248
  try {
@@ -1188,7 +1250,7 @@ export function startDaemon(config = {}, deps = {}) {
1188
1250
  } catch {
1189
1251
  /* ignore */
1190
1252
  }
1191
- }, timeoutMs);
1253
+ }, normalizedOptions.timeoutMs ?? 120_000);
1192
1254
  child.stdout?.on("data", (chunk) => {
1193
1255
  if (stdout.length < 4000) stdout += chunk.toString().slice(0, 2000);
1194
1256
  });
@@ -1207,11 +1269,7 @@ export function startDaemon(config = {}, deps = {}) {
1207
1269
  }
1208
1270
 
1209
1271
  function runBufferedCommand(command, args, options = {}) {
1210
- return runCommand(
1211
- command,
1212
- args,
1213
- typeof options === "number" ? options : options?.timeoutMs ?? 120_000,
1214
- );
1272
+ return runCommand(command, args, options);
1215
1273
  }
1216
1274
 
1217
1275
  async function readInstalledCliVersion() {
@@ -2555,7 +2613,9 @@ export function startDaemon(config = {}, deps = {}) {
2555
2613
  }
2556
2614
 
2557
2615
  if (event.type === "create_task") {
2558
- handleCreateTask(event.payload);
2616
+ void handleCreateTask(event.payload).catch((error) => {
2617
+ logError(`Unhandled create_task failure: ${error?.message || error}`);
2618
+ });
2559
2619
  return;
2560
2620
  }
2561
2621
  if (event.type === "restart_task") {
@@ -2880,6 +2940,39 @@ export function startDaemon(config = {}, deps = {}) {
2880
2940
  });
2881
2941
  }
2882
2942
 
2943
+ function reportCreateTaskFailure({ taskId, projectId, requestId, error, sendAck = true }) {
2944
+ const normalizedTaskId = taskId ? String(taskId) : "";
2945
+ const normalizedProjectId = projectId ? String(projectId) : "";
2946
+ const message = error instanceof Error ? error.message : String(error);
2947
+ logError(`Failed to create task ${normalizedTaskId || "unknown"}: ${message}`);
2948
+ if (sendAck) {
2949
+ sendAgentCommandAck({
2950
+ requestId,
2951
+ taskId: normalizedTaskId,
2952
+ eventType: "create_task",
2953
+ accepted: false,
2954
+ }).catch((err) => {
2955
+ logError(`Failed to report agent_command_ack(create_task) for ${normalizedTaskId}: ${err?.message || err}`);
2956
+ });
2957
+ }
2958
+ if (!normalizedTaskId || !normalizedProjectId) {
2959
+ return;
2960
+ }
2961
+ client
2962
+ .sendJson({
2963
+ type: "task_status_update",
2964
+ payload: {
2965
+ task_id: normalizedTaskId,
2966
+ project_id: normalizedProjectId,
2967
+ status: "KILLED",
2968
+ summary: message,
2969
+ },
2970
+ })
2971
+ .catch((err) => {
2972
+ logError(`Failed to report task status (KILLED) for ${normalizedTaskId}: ${err?.message || err}`);
2973
+ });
2974
+ }
2975
+
2883
2976
  async function resolveRestartCwd({
2884
2977
  projectId,
2885
2978
  preferredCwd = "",
@@ -2904,7 +2997,10 @@ export function startDaemon(config = {}, deps = {}) {
2904
2997
  const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
2905
2998
  normalizedBackend,
2906
2999
  normalizedSessionId,
2907
- { cwd: process.cwd() },
3000
+ {
3001
+ cwd: process.cwd(),
3002
+ configFilePath: config.CONFIG_FILE,
3003
+ },
2908
3004
  );
2909
3005
  if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
2910
3006
  return resumeContext.cwd.trim();
@@ -2970,9 +3066,9 @@ export function startDaemon(config = {}, deps = {}) {
2970
3066
  }
2971
3067
 
2972
3068
  const existingTaskRecord = activeTaskProcesses.get(taskId);
2973
- if (existingTaskRecord?.child) {
3069
+ if (existingTaskRecord?.child || pendingTaskStarts.has(taskId)) {
2974
3070
  log(
2975
- `Duplicate create_task ignored for ${taskId}: task already active (pid=${existingTaskRecord.child.pid ?? "unknown"})`,
3071
+ `Duplicate create_task ignored for ${taskId}: task already active (pid=${existingTaskRecord?.child?.pid ?? "unknown"})`,
2976
3072
  );
2977
3073
  sendAgentCommandAck({
2978
3074
  requestId,
@@ -2983,245 +3079,256 @@ export function startDaemon(config = {}, deps = {}) {
2983
3079
  return;
2984
3080
  }
2985
3081
 
2986
- // Validate and get CLI command for the backend
2987
- const effectiveBackend = normalizeRuntimeBackendName(backendType || SUPPORTED_BACKENDS[0]);
2988
- if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
2989
- logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
3082
+ pendingTaskStarts.add(taskId);
3083
+ let acceptedAckSent = false;
3084
+ try {
3085
+ const effectiveBackend = await normalizeRuntimeBackendAlias(backendType || SUPPORTED_BACKENDS[0], {
3086
+ configFilePath: config.CONFIG_FILE,
3087
+ });
3088
+ if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
3089
+ logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
3090
+ sendAgentCommandAck({
3091
+ requestId,
3092
+ taskId,
3093
+ eventType: "create_task",
3094
+ accepted: false,
3095
+ }).catch(() => {});
3096
+ client
3097
+ .sendJson({
3098
+ type: "task_status_update",
3099
+ payload: {
3100
+ task_id: taskId,
3101
+ project_id: projectId,
3102
+ status: "KILLED",
3103
+ summary: `Unsupported backend: ${effectiveBackend}`,
3104
+ },
3105
+ })
3106
+ .catch(() => {});
3107
+ return;
3108
+ }
3109
+
2990
3110
  sendAgentCommandAck({
2991
3111
  requestId,
2992
3112
  taskId,
2993
3113
  eventType: "create_task",
2994
- accepted: false,
2995
- }).catch(() => {});
3114
+ accepted: true,
3115
+ }).catch((err) => {
3116
+ logError(`Failed to report agent_command_ack(create_task) for ${taskId}: ${err?.message || err}`);
3117
+ });
3118
+ acceptedAckSent = true;
3119
+
3120
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3121
+
3122
+ log("");
3123
+ log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
3124
+ log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
2996
3125
  client
2997
3126
  .sendJson({
2998
3127
  type: "task_status_update",
2999
3128
  payload: {
3000
3129
  task_id: taskId,
3001
3130
  project_id: projectId,
3002
- status: "KILLED",
3003
- summary: `Unsupported backend: ${effectiveBackend}`,
3131
+ status: "INIT",
3004
3132
  },
3005
3133
  })
3006
- .catch(() => {});
3007
- return;
3008
- }
3009
-
3010
- sendAgentCommandAck({
3011
- requestId,
3012
- taskId,
3013
- eventType: "create_task",
3014
- accepted: true,
3015
- }).catch((err) => {
3016
- logError(`Failed to report agent_command_ack(create_task) for ${taskId}: ${err?.message || err}`);
3017
- });
3018
-
3019
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3020
-
3021
- log("");
3022
- log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
3023
- log(`CLI command: ${cliCommand}`);
3024
- client
3025
- .sendJson({
3026
- type: "task_status_update",
3027
- payload: {
3028
- task_id: taskId,
3029
- project_id: projectId,
3030
- status: "INIT",
3031
- },
3032
- })
3033
- .catch((err) => {
3034
- logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
3035
- });
3036
-
3037
- // Check if project has a bound local path for this daemon
3038
- const boundPath = await getProjectLocalPath(projectId);
3039
- if (daemonShuttingDown) {
3040
- rejectCreateTaskDuringShutdown(payload, { sendAck: false });
3041
- return;
3042
- }
3043
- let taskDir;
3044
- let logPath;
3045
- let runTimestampPart = null;
3134
+ .catch((err) => {
3135
+ logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
3136
+ });
3046
3137
 
3047
- if (boundPath) {
3048
- // Use the bound path directly (don't create subdirectory)
3049
- taskDir = boundPath;
3050
- log(`Using project bound path: ${taskDir}`);
3051
- // Create log file in the bound path
3052
- logPath = path.join(taskDir, "conductor.log");
3053
- } else {
3054
- // Use Beijing timestamp + process id workspace structure:
3055
- // YYYY-MM-DD/HH-mm-ss_pid_<fire-pid>/
3056
- // Child pid is only known after spawn; start with daemon pid and rename after spawn.
3057
- const now = new Date();
3058
- const dayDir = path.join(WORKSPACE_ROOT, formatWorkspaceDate(now));
3059
- runTimestampPart = formatWorkspaceRunTimestamp(now);
3060
- const pendingRunDir = `${runTimestampPart}_pid_${process.pid}`;
3061
- taskDir = path.join(dayDir, pendingRunDir);
3062
- mkdirSyncFn(taskDir, { recursive: true });
3063
- logPath = path.join(taskDir, "conductor.log");
3064
- }
3138
+ const boundPath = await getProjectLocalPath(projectId);
3139
+ if (daemonShuttingDown) {
3140
+ rejectCreateTaskDuringShutdown(payload, { sendAck: false });
3141
+ return;
3142
+ }
3065
3143
 
3066
- const args = [];
3067
- if (effectiveBackend) {
3068
- args.push("--backend", effectiveBackend);
3069
- }
3070
- if (initialContent) {
3071
- args.push("--prefill", initialContent);
3072
- }
3073
- // Explicitly separate conductor flags from backend args so they don't leak into messages
3074
- args.push("--");
3144
+ let taskDir;
3145
+ let logPath;
3146
+ let runTimestampPart = null;
3075
3147
 
3076
- const env = {
3077
- ...process.env,
3078
- CONDUCTOR_PROJECT_ID: projectId,
3079
- CONDUCTOR_TASK_ID: taskId,
3080
- CONDUCTOR_CLI_COMMAND: cliCommand,
3081
- };
3082
- if (config.CONFIG_FILE) {
3083
- env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
3084
- }
3085
- if (AGENT_TOKEN) {
3086
- env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
3087
- }
3088
- if (BACKEND_HTTP) {
3089
- env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
3090
- }
3148
+ if (boundPath) {
3149
+ taskDir = boundPath;
3150
+ log(`Using project bound path: ${taskDir}`);
3151
+ logPath = path.join(taskDir, "conductor.log");
3152
+ } else {
3153
+ const now = new Date();
3154
+ const dayDir = path.join(WORKSPACE_ROOT, formatWorkspaceDate(now));
3155
+ runTimestampPart = formatWorkspaceRunTimestamp(now);
3156
+ const pendingRunDir = `${runTimestampPart}_pid_${process.pid}`;
3157
+ taskDir = path.join(dayDir, pendingRunDir);
3158
+ mkdirSyncFn(taskDir, { recursive: true });
3159
+ logPath = path.join(taskDir, "conductor.log");
3160
+ }
3161
+
3162
+ const args = [];
3163
+ if (effectiveBackend) {
3164
+ args.push("--backend", effectiveBackend);
3165
+ }
3166
+ if (initialContent) {
3167
+ args.push("--prefill", initialContent);
3168
+ }
3169
+ args.push("--");
3170
+
3171
+ const env = {
3172
+ ...process.env,
3173
+ CONDUCTOR_PROJECT_ID: projectId,
3174
+ CONDUCTOR_TASK_ID: taskId,
3175
+ CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
3176
+ ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
3177
+ };
3178
+ if (config.CONFIG_FILE) {
3179
+ env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
3180
+ }
3181
+ if (AGENT_TOKEN) {
3182
+ env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
3183
+ }
3184
+ if (BACKEND_HTTP) {
3185
+ env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
3186
+ }
3091
3187
 
3092
- const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3093
- cwd: taskDir,
3094
- env,
3095
- stdio: ["inherit", "pipe", "pipe"],
3096
- });
3188
+ const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3189
+ cwd: taskDir,
3190
+ env,
3191
+ stdio: ["inherit", "pipe", "pipe"],
3192
+ });
3097
3193
 
3098
- if (!boundPath && runTimestampPart && Number.isInteger(child?.pid) && child.pid > 0) {
3099
- const desiredTaskDir = path.join(path.dirname(taskDir), `${runTimestampPart}_pid_${child.pid}`);
3100
- if (desiredTaskDir !== taskDir) {
3101
- try {
3102
- renameSyncFn(taskDir, desiredTaskDir);
3103
- taskDir = desiredTaskDir;
3104
- logPath = path.join(taskDir, "conductor.log");
3105
- } catch (err) {
3106
- logError(
3107
- `Failed to rename workspace dir from ${taskDir} to ${desiredTaskDir}: ${err?.message || err}`,
3108
- );
3194
+ if (!boundPath && runTimestampPart && Number.isInteger(child?.pid) && child.pid > 0) {
3195
+ const desiredTaskDir = path.join(path.dirname(taskDir), `${runTimestampPart}_pid_${child.pid}`);
3196
+ if (desiredTaskDir !== taskDir) {
3197
+ try {
3198
+ renameSyncFn(taskDir, desiredTaskDir);
3199
+ taskDir = desiredTaskDir;
3200
+ logPath = path.join(taskDir, "conductor.log");
3201
+ } catch (err) {
3202
+ logError(
3203
+ `Failed to rename workspace dir from ${taskDir} to ${desiredTaskDir}: ${err?.message || err}`,
3204
+ );
3205
+ }
3109
3206
  }
3110
3207
  }
3111
- }
3112
3208
 
3113
- try {
3114
- mkdirSyncFn(taskDir, { recursive: true });
3115
- } catch (err) {
3116
- logError(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`);
3117
- }
3118
-
3119
- let logStream;
3120
- try {
3121
- logStream = createWriteStreamFn(logPath, { flags: "a" });
3122
- if (logStream && typeof logStream.on === "function") {
3123
- const logPathSnapshot = logPath;
3124
- logStream.on("error", (err) => {
3125
- logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
3126
- });
3209
+ try {
3210
+ mkdirSyncFn(taskDir, { recursive: true });
3211
+ } catch (err) {
3212
+ logError(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`);
3127
3213
  }
3128
- } catch (err) {
3129
- logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3130
- }
3131
3214
 
3132
- log(`New task workspace: ${taskDir}`);
3133
- log(`Logs: ${logPath}`);
3215
+ let logStream;
3216
+ try {
3217
+ logStream = createWriteStreamFn(logPath, { flags: "a" });
3218
+ if (logStream && typeof logStream.on === "function") {
3219
+ const logPathSnapshot = logPath;
3220
+ logStream.on("error", (err) => {
3221
+ logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
3222
+ });
3223
+ }
3224
+ } catch (err) {
3225
+ logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3226
+ }
3134
3227
 
3135
- activeTaskProcesses.set(taskId, {
3136
- child,
3137
- projectId,
3138
- logPath,
3139
- stopForceKillTimer: null,
3140
- });
3228
+ log(`New task workspace: ${taskDir}`);
3229
+ log(`Logs: ${logPath}`);
3141
3230
 
3142
- client
3143
- .sendJson({
3144
- type: "task_status_update",
3145
- payload: {
3146
- task_id: taskId,
3147
- project_id: projectId,
3148
- status: "RUNNING",
3149
- },
3150
- })
3151
- .catch((err) => {
3152
- logError(`Failed to report task status (RUNNING) for ${taskId}: ${err?.message || err}`);
3231
+ activeTaskProcesses.set(taskId, {
3232
+ child,
3233
+ projectId,
3234
+ logPath,
3235
+ stopForceKillTimer: null,
3153
3236
  });
3154
3237
 
3155
- if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
3156
- child.stdout.pipe(logStream, { end: false });
3157
- } else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
3158
- child.stdout.on("data", (chunk) => logStream.write(chunk));
3159
- }
3160
- if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
3161
- child.stderr.pipe(logStream, { end: false });
3162
- } else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
3163
- child.stderr.on("data", (chunk) => logStream.write(chunk));
3164
- }
3238
+ client
3239
+ .sendJson({
3240
+ type: "task_status_update",
3241
+ payload: {
3242
+ task_id: taskId,
3243
+ project_id: projectId,
3244
+ status: "RUNNING",
3245
+ },
3246
+ })
3247
+ .catch((err) => {
3248
+ logError(`Failed to report task status (RUNNING) for ${taskId}: ${err?.message || err}`);
3249
+ });
3165
3250
 
3166
- child.on("error", (err) => {
3167
- logError(`Failed to spawn CLI: ${err.message}`);
3168
- if (logStream) {
3169
- const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3170
- logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
3251
+ if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
3252
+ child.stdout.pipe(logStream, { end: false });
3253
+ } else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
3254
+ child.stdout.on("data", (chunk) => logStream.write(chunk));
3171
3255
  }
3172
- });
3173
-
3174
- child.on("exit", (code, signal) => {
3175
- const active = activeTaskProcesses.get(taskId);
3176
- if (active?.stopForceKillTimer) {
3177
- clearTimeout(active.stopForceKillTimer);
3256
+ if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
3257
+ child.stderr.pipe(logStream, { end: false });
3258
+ } else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
3259
+ child.stderr.on("data", (chunk) => logStream.write(chunk));
3178
3260
  }
3179
- activeTaskProcesses.delete(taskId);
3180
- const suppressExitStatusReport = suppressedExitStatusReports.has(taskId);
3181
- suppressedExitStatusReports.delete(taskId);
3182
- if (logStream) {
3183
- const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3261
+
3262
+ child.on("error", (err) => {
3263
+ logError(`Failed to spawn CLI: ${err.message}`);
3264
+ if (logStream) {
3265
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3266
+ logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
3267
+ }
3268
+ });
3269
+
3270
+ child.on("exit", (code, signal) => {
3271
+ const active = activeTaskProcesses.get(taskId);
3272
+ if (active?.stopForceKillTimer) {
3273
+ clearTimeout(active.stopForceKillTimer);
3274
+ }
3275
+ activeTaskProcesses.delete(taskId);
3276
+ const suppressExitStatusReport = suppressedExitStatusReports.has(taskId);
3277
+ suppressedExitStatusReports.delete(taskId);
3278
+ if (logStream) {
3279
+ const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
3280
+ if (signal) {
3281
+ logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3282
+ } else {
3283
+ logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3284
+ }
3285
+ logStream.end();
3286
+ }
3184
3287
  if (signal) {
3185
- logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3288
+ log(`Task ${taskId} killed by signal ${signal}`);
3186
3289
  } else {
3187
- logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3290
+ log(`Task ${taskId} finished with code ${code}`);
3188
3291
  }
3189
- logStream.end();
3190
- }
3191
- if (signal) {
3192
- log(`Task ${taskId} killed by signal ${signal}`);
3193
- } else {
3194
- log(`Task ${taskId} finished with code ${code}`);
3195
- }
3196
- log(`Logs: ${logPath}`);
3197
-
3198
- const isKilledBySignal = Boolean(signal);
3199
- const isKilledByExitCode = code === 130 || code === 143;
3200
- const isKilled = isKilledBySignal || isKilledByExitCode;
3201
-
3202
- const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
3203
- const summary = isKilled
3204
- ? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
3205
- : code === 0
3206
- ? "completed"
3207
- : `exited with code ${code}`;
3208
-
3209
- if (!suppressExitStatusReport) {
3210
- client
3211
- .sendJson({
3212
- type: "task_status_update",
3213
- payload: {
3214
- task_id: taskId,
3215
- project_id: projectId,
3216
- status,
3217
- summary,
3218
- },
3219
- })
3220
- .catch((err) => {
3221
- logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
3222
- });
3223
- }
3224
- });
3292
+ log(`Logs: ${logPath}`);
3293
+
3294
+ const isKilledBySignal = Boolean(signal);
3295
+ const isKilledByExitCode = code === 130 || code === 143;
3296
+ const isKilled = isKilledBySignal || isKilledByExitCode;
3297
+
3298
+ const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
3299
+ const summary = isKilled
3300
+ ? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
3301
+ : code === 0
3302
+ ? "completed"
3303
+ : `exited with code ${code}`;
3304
+
3305
+ if (!suppressExitStatusReport) {
3306
+ client
3307
+ .sendJson({
3308
+ type: "task_status_update",
3309
+ payload: {
3310
+ task_id: taskId,
3311
+ project_id: projectId,
3312
+ status,
3313
+ summary,
3314
+ },
3315
+ })
3316
+ .catch((err) => {
3317
+ logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
3318
+ });
3319
+ }
3320
+ });
3321
+ } catch (error) {
3322
+ reportCreateTaskFailure({
3323
+ taskId,
3324
+ projectId,
3325
+ requestId,
3326
+ error,
3327
+ sendAck: !acceptedAckSent,
3328
+ });
3329
+ } finally {
3330
+ pendingTaskStarts.delete(taskId);
3331
+ }
3225
3332
  }
3226
3333
 
3227
3334
  async function handleRestartTask(payload) {
@@ -3298,8 +3405,10 @@ export function startDaemon(config = {}, deps = {}) {
3298
3405
  return;
3299
3406
  }
3300
3407
 
3301
- const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
3302
- if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
3408
+ const effectiveBackend = await normalizeRuntimeBackendAlias(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0], {
3409
+ configFilePath: config.CONFIG_FILE,
3410
+ });
3411
+ if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
3303
3412
  reportRestartFailure({
3304
3413
  taskId: normalizedTargetTaskId,
3305
3414
  projectId: normalizedProjectId,
@@ -3408,13 +3517,13 @@ export function startDaemon(config = {}, deps = {}) {
3408
3517
  return;
3409
3518
  }
3410
3519
 
3411
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3520
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3412
3521
 
3413
3522
  log("");
3414
3523
  log(
3415
3524
  `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
3416
3525
  );
3417
- log(`CLI command: ${cliCommand}`);
3526
+ log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3418
3527
 
3419
3528
  if (normalizedMode !== "resume_inplace") {
3420
3529
  client
@@ -3471,7 +3580,8 @@ export function startDaemon(config = {}, deps = {}) {
3471
3580
  ...process.env,
3472
3581
  CONDUCTOR_PROJECT_ID: normalizedProjectId,
3473
3582
  CONDUCTOR_TASK_ID: normalizedTargetTaskId,
3474
- CONDUCTOR_CLI_COMMAND: cliCommand,
3583
+ CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
3584
+ ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
3475
3585
  CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
3476
3586
  };
3477
3587
  if (config.CONFIG_FILE) {