@love-moon/conductor-cli 0.2.30 → 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;
@@ -2556,7 +2613,9 @@ export function startDaemon(config = {}, deps = {}) {
2556
2613
  }
2557
2614
 
2558
2615
  if (event.type === "create_task") {
2559
- handleCreateTask(event.payload);
2616
+ void handleCreateTask(event.payload).catch((error) => {
2617
+ logError(`Unhandled create_task failure: ${error?.message || error}`);
2618
+ });
2560
2619
  return;
2561
2620
  }
2562
2621
  if (event.type === "restart_task") {
@@ -2881,6 +2940,39 @@ export function startDaemon(config = {}, deps = {}) {
2881
2940
  });
2882
2941
  }
2883
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
+
2884
2976
  async function resolveRestartCwd({
2885
2977
  projectId,
2886
2978
  preferredCwd = "",
@@ -2905,7 +2997,10 @@ export function startDaemon(config = {}, deps = {}) {
2905
2997
  const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
2906
2998
  normalizedBackend,
2907
2999
  normalizedSessionId,
2908
- { cwd: process.cwd() },
3000
+ {
3001
+ cwd: process.cwd(),
3002
+ configFilePath: config.CONFIG_FILE,
3003
+ },
2909
3004
  );
2910
3005
  if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
2911
3006
  return resumeContext.cwd.trim();
@@ -2971,9 +3066,9 @@ export function startDaemon(config = {}, deps = {}) {
2971
3066
  }
2972
3067
 
2973
3068
  const existingTaskRecord = activeTaskProcesses.get(taskId);
2974
- if (existingTaskRecord?.child) {
3069
+ if (existingTaskRecord?.child || pendingTaskStarts.has(taskId)) {
2975
3070
  log(
2976
- `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"})`,
2977
3072
  );
2978
3073
  sendAgentCommandAck({
2979
3074
  requestId,
@@ -2984,245 +3079,256 @@ export function startDaemon(config = {}, deps = {}) {
2984
3079
  return;
2985
3080
  }
2986
3081
 
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(", ")}`);
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
+
2991
3110
  sendAgentCommandAck({
2992
3111
  requestId,
2993
3112
  taskId,
2994
3113
  eventType: "create_task",
2995
- accepted: false,
2996
- }).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)}`);
2997
3125
  client
2998
3126
  .sendJson({
2999
3127
  type: "task_status_update",
3000
3128
  payload: {
3001
3129
  task_id: taskId,
3002
3130
  project_id: projectId,
3003
- status: "KILLED",
3004
- summary: `Unsupported backend: ${effectiveBackend}`,
3131
+ status: "INIT",
3005
3132
  },
3006
3133
  })
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;
3134
+ .catch((err) => {
3135
+ logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
3136
+ });
3047
3137
 
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
- }
3138
+ const boundPath = await getProjectLocalPath(projectId);
3139
+ if (daemonShuttingDown) {
3140
+ rejectCreateTaskDuringShutdown(payload, { sendAck: false });
3141
+ return;
3142
+ }
3066
3143
 
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("--");
3144
+ let taskDir;
3145
+ let logPath;
3146
+ let runTimestampPart = null;
3076
3147
 
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
- }
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
+ }
3092
3187
 
3093
- const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3094
- cwd: taskDir,
3095
- env,
3096
- stdio: ["inherit", "pipe", "pipe"],
3097
- });
3188
+ const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
3189
+ cwd: taskDir,
3190
+ env,
3191
+ stdio: ["inherit", "pipe", "pipe"],
3192
+ });
3098
3193
 
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
- );
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
+ }
3110
3206
  }
3111
3207
  }
3112
- }
3113
3208
 
3114
- try {
3115
- mkdirSyncFn(taskDir, { recursive: true });
3116
- } catch (err) {
3117
- logError(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`);
3118
- }
3119
-
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
- });
3209
+ try {
3210
+ mkdirSyncFn(taskDir, { recursive: true });
3211
+ } catch (err) {
3212
+ logError(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`);
3128
3213
  }
3129
- } catch (err) {
3130
- logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
3131
- }
3132
3214
 
3133
- log(`New task workspace: ${taskDir}`);
3134
- 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
+ }
3135
3227
 
3136
- activeTaskProcesses.set(taskId, {
3137
- child,
3138
- projectId,
3139
- logPath,
3140
- stopForceKillTimer: null,
3141
- });
3228
+ log(`New task workspace: ${taskDir}`);
3229
+ log(`Logs: ${logPath}`);
3142
3230
 
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}`);
3231
+ activeTaskProcesses.set(taskId, {
3232
+ child,
3233
+ projectId,
3234
+ logPath,
3235
+ stopForceKillTimer: null,
3154
3236
  });
3155
3237
 
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
- }
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
+ });
3166
3250
 
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`);
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));
3172
3255
  }
3173
- });
3174
-
3175
- child.on("exit", (code, signal) => {
3176
- const active = activeTaskProcesses.get(taskId);
3177
- if (active?.stopForceKillTimer) {
3178
- 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));
3179
3260
  }
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");
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
+ }
3185
3287
  if (signal) {
3186
- logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
3288
+ log(`Task ${taskId} killed by signal ${signal}`);
3187
3289
  } else {
3188
- logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
3290
+ log(`Task ${taskId} finished with code ${code}`);
3189
3291
  }
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
- });
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
+ }
3226
3332
  }
3227
3333
 
3228
3334
  async function handleRestartTask(payload) {
@@ -3299,8 +3405,10 @@ export function startDaemon(config = {}, deps = {}) {
3299
3405
  return;
3300
3406
  }
3301
3407
 
3302
- const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
3303
- 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 }))) {
3304
3412
  reportRestartFailure({
3305
3413
  taskId: normalizedTargetTaskId,
3306
3414
  projectId: normalizedProjectId,
@@ -3409,13 +3517,13 @@ export function startDaemon(config = {}, deps = {}) {
3409
3517
  return;
3410
3518
  }
3411
3519
 
3412
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
3520
+ const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3413
3521
 
3414
3522
  log("");
3415
3523
  log(
3416
3524
  `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
3417
3525
  );
3418
- log(`CLI command: ${cliCommand}`);
3526
+ log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3419
3527
 
3420
3528
  if (normalizedMode !== "resume_inplace") {
3421
3529
  client
@@ -3472,7 +3580,8 @@ export function startDaemon(config = {}, deps = {}) {
3472
3580
  ...process.env,
3473
3581
  CONDUCTOR_PROJECT_ID: normalizedProjectId,
3474
3582
  CONDUCTOR_TASK_ID: normalizedTargetTaskId,
3475
- CONDUCTOR_CLI_COMMAND: cliCommand,
3583
+ CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
3584
+ ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
3476
3585
  CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
3477
3586
  };
3478
3587
  if (config.CONFIG_FILE) {