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