@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/bin/conductor-config.js +2 -1
- package/bin/conductor-fire.js +85 -34
- package/package.json +4 -4
- package/src/daemon.js +350 -241
- package/src/fire/resume.js +101 -1
- 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;
|
|
@@ -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
|
-
{
|
|
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
|
|
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
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
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:
|
|
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: "
|
|
3004
|
-
summary: `Unsupported backend: ${effectiveBackend}`,
|
|
3131
|
+
status: "INIT",
|
|
3005
3132
|
},
|
|
3006
3133
|
})
|
|
3007
|
-
.catch(() => {
|
|
3008
|
-
|
|
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
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
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
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
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
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
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
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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
|
-
|
|
3134
|
-
|
|
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
|
-
|
|
3137
|
-
|
|
3138
|
-
projectId,
|
|
3139
|
-
logPath,
|
|
3140
|
-
stopForceKillTimer: null,
|
|
3141
|
-
});
|
|
3228
|
+
log(`New task workspace: ${taskDir}`);
|
|
3229
|
+
log(`Logs: ${logPath}`);
|
|
3142
3230
|
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
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
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
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
|
-
|
|
3168
|
-
|
|
3169
|
-
if (logStream) {
|
|
3170
|
-
|
|
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
|
-
|
|
3176
|
-
|
|
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
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
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
|
-
|
|
3288
|
+
log(`Task ${taskId} killed by signal ${signal}`);
|
|
3187
3289
|
} else {
|
|
3188
|
-
|
|
3290
|
+
log(`Task ${taskId} finished with code ${code}`);
|
|
3189
3291
|
}
|
|
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
|
-
|
|
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 =
|
|
3303
|
-
|
|
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
|
-
|
|
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) {
|