@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/bin/conductor-config.js +2 -1
- package/bin/conductor-fire.js +85 -34
- package/package.json +4 -4
- package/src/daemon.js +359 -249
- package/src/fire/resume.js +101 -1
- package/src/native-deps.js +9 -7
- package/src/runtime-backends.js +219 -9
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
907
|
-
|
|
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
|
-
|
|
911
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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,
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
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:
|
|
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: "
|
|
3003
|
-
summary: `Unsupported backend: ${effectiveBackend}`,
|
|
3131
|
+
status: "INIT",
|
|
3004
3132
|
},
|
|
3005
3133
|
})
|
|
3006
|
-
.catch(() => {
|
|
3007
|
-
|
|
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
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
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
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
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
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
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
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
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
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
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
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
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
|
-
|
|
3133
|
-
|
|
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
|
-
|
|
3136
|
-
|
|
3137
|
-
projectId,
|
|
3138
|
-
logPath,
|
|
3139
|
-
stopForceKillTimer: null,
|
|
3140
|
-
});
|
|
3228
|
+
log(`New task workspace: ${taskDir}`);
|
|
3229
|
+
log(`Logs: ${logPath}`);
|
|
3141
3230
|
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
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
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
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
|
-
|
|
3167
|
-
|
|
3168
|
-
if (logStream) {
|
|
3169
|
-
|
|
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
|
-
|
|
3175
|
-
|
|
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
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
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
|
-
|
|
3288
|
+
log(`Task ${taskId} killed by signal ${signal}`);
|
|
3186
3289
|
} else {
|
|
3187
|
-
|
|
3290
|
+
log(`Task ${taskId} finished with code ${code}`);
|
|
3188
3291
|
}
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
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 =
|
|
3302
|
-
|
|
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
|
-
|
|
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) {
|