@love-moon/conductor-cli 0.2.28 → 0.2.30
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-diagnose.js +1 -0
- package/bin/conductor-fire.js +40 -7
- package/package.json +8 -7
- package/src/daemon.js +520 -11
- package/src/native-deps.js +9 -7
|
@@ -366,6 +366,7 @@ function normalizeRole(role) {
|
|
|
366
366
|
function normalizeTaskStatus(status) {
|
|
367
367
|
const normalized = cleanText(status).toLowerCase();
|
|
368
368
|
if (normalized === "completed") return "completed";
|
|
369
|
+
if (normalized === "init") return "init";
|
|
369
370
|
if (normalized === "running") return "running";
|
|
370
371
|
if (normalized === "killed" || normalized === "failed" || normalized === "cancelled") return "killed";
|
|
371
372
|
return normalized || "unknown";
|
package/bin/conductor-fire.js
CHANGED
|
@@ -427,13 +427,12 @@ async function main() {
|
|
|
427
427
|
|
|
428
428
|
let resumeContext = null;
|
|
429
429
|
if (cliArgs.resumeSessionId) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
runtimeProjectPath =
|
|
436
|
-
log(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
430
|
+
const bootstrap = await bootstrapResumeContextForFire({
|
|
431
|
+
backend: cliArgs.backend,
|
|
432
|
+
resumeSessionId: cliArgs.resumeSessionId,
|
|
433
|
+
});
|
|
434
|
+
resumeContext = bootstrap.resumeContext;
|
|
435
|
+
runtimeProjectPath = bootstrap.runtimeProjectPath;
|
|
437
436
|
}
|
|
438
437
|
|
|
439
438
|
const env = buildEnv();
|
|
@@ -1224,6 +1223,40 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
|
1224
1223
|
return resolveCliResumeContext(backend, sessionId, options);
|
|
1225
1224
|
}
|
|
1226
1225
|
|
|
1226
|
+
export async function bootstrapResumeContextForFire({
|
|
1227
|
+
backend,
|
|
1228
|
+
resumeSessionId,
|
|
1229
|
+
env = process.env,
|
|
1230
|
+
resolveResumeContextFn = resolveResumeContext,
|
|
1231
|
+
applyWorkingDirectoryFn = applyWorkingDirectory,
|
|
1232
|
+
logger = log,
|
|
1233
|
+
}) {
|
|
1234
|
+
let runtimeProjectPath = process.cwd();
|
|
1235
|
+
let resumeContext = null;
|
|
1236
|
+
|
|
1237
|
+
if (!resumeSessionId) {
|
|
1238
|
+
return { resumeContext, runtimeProjectPath };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const overrideResumeCwd =
|
|
1242
|
+
typeof env?.CONDUCTOR_RESUME_CWD === "string" ? env.CONDUCTOR_RESUME_CWD.trim() : "";
|
|
1243
|
+
if (overrideResumeCwd) {
|
|
1244
|
+
logger(`Using CONDUCTOR_RESUME_CWD override for --resume ${resumeSessionId}: ${overrideResumeCwd}`);
|
|
1245
|
+
runtimeProjectPath = await applyWorkingDirectoryFn(overrideResumeCwd);
|
|
1246
|
+
logger(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
1247
|
+
return { resumeContext, runtimeProjectPath };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
resumeContext = await resolveResumeContextFn(backend, resumeSessionId);
|
|
1251
|
+
logger(
|
|
1252
|
+
`Validated --resume ${resumeContext.sessionId} (${resumeContext.provider}) at ${resumeContext.sessionPath}`,
|
|
1253
|
+
);
|
|
1254
|
+
logger(`Resume will run backend from ${resumeContext.cwd}`);
|
|
1255
|
+
runtimeProjectPath = await applyWorkingDirectoryFn(resumeContext.cwd);
|
|
1256
|
+
logger(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
1257
|
+
return { resumeContext, runtimeProjectPath };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1227
1260
|
export async function applyWorkingDirectory(targetPath) {
|
|
1228
1261
|
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
1229
1262
|
if (!normalizedPath) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.2.30",
|
|
4
|
+
"gitCommitId": "e6a71ad",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -17,16 +17,17 @@
|
|
|
17
17
|
"test": "node --test test/*.test.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@love-moon/ai-
|
|
21
|
-
"@love-moon/
|
|
20
|
+
"@love-moon/ai-bridge": "0.1.4",
|
|
21
|
+
"@love-moon/ai-sdk": "0.2.30",
|
|
22
|
+
"@love-moon/conductor-sdk": "0.2.30",
|
|
23
|
+
"chrome-launcher": "^1.2.1",
|
|
24
|
+
"chrome-remote-interface": "^0.33.0",
|
|
22
25
|
"dotenv": "^16.4.5",
|
|
23
26
|
"enquirer": "^2.4.1",
|
|
24
27
|
"js-yaml": "^4.1.1",
|
|
25
28
|
"node-pty": "^1.0.0",
|
|
26
29
|
"ws": "^8.18.0",
|
|
27
|
-
"yargs": "^17.7.2"
|
|
28
|
-
"chrome-launcher": "^1.2.1",
|
|
29
|
-
"chrome-remote-interface": "^0.33.0"
|
|
30
|
+
"yargs": "^17.7.2"
|
|
30
31
|
},
|
|
31
32
|
"optionalDependencies": {
|
|
32
33
|
"@roamhq/wrtc": "^0.10.0"
|
package/src/daemon.js
CHANGED
|
@@ -3,13 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
import dotenv from "dotenv";
|
|
9
9
|
import yaml from "js-yaml";
|
|
10
10
|
|
|
11
11
|
import { ConductorWebSocketClient, ConductorConfig, loadConfig, ConfigFileNotFound } from "@love-moon/conductor-sdk";
|
|
12
12
|
import { DaemonLogCollector } from "./log-collector.js";
|
|
13
|
+
import { resolveResumeContext } from "./fire/resume.js";
|
|
13
14
|
import { filterRuntimeSupportedAllowCliList, normalizeRuntimeBackendName } from "./runtime-backends.js";
|
|
14
15
|
import {
|
|
15
16
|
PACKAGE_NAME,
|
|
@@ -30,6 +31,7 @@ dotenv.config();
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
32
|
const __dirname = path.dirname(__filename);
|
|
32
33
|
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
34
|
+
const DEFAULT_AI_BRIDGE_API_SPECIFIER = "@love-moon/ai-bridge/dist/api.js";
|
|
33
35
|
const moduleRequire = createRequire(import.meta.url);
|
|
34
36
|
const CLI_PATH = path.resolve(PACKAGE_ROOT, "bin", "conductor-fire.js");
|
|
35
37
|
const DAEMON_LOG_DIR = path.join(os.homedir(), ".conductor", "logs");
|
|
@@ -283,6 +285,24 @@ function normalizeOptionalString(value) {
|
|
|
283
285
|
return normalized || null;
|
|
284
286
|
}
|
|
285
287
|
|
|
288
|
+
function resolveImportTarget(specifierOrPath) {
|
|
289
|
+
const normalized = normalizeOptionalString(specifierOrPath);
|
|
290
|
+
if (!normalized) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
normalized.startsWith("file:") ||
|
|
295
|
+
normalized.startsWith("node:") ||
|
|
296
|
+
normalized.startsWith("data:")
|
|
297
|
+
) {
|
|
298
|
+
return normalized;
|
|
299
|
+
}
|
|
300
|
+
if (path.isAbsolute(normalized) || normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
301
|
+
return pathToFileURL(path.resolve(normalized)).href;
|
|
302
|
+
}
|
|
303
|
+
return normalized;
|
|
304
|
+
}
|
|
305
|
+
|
|
286
306
|
function normalizeTerminalResumeStrategy(value) {
|
|
287
307
|
const normalized = normalizeOptionalString(value);
|
|
288
308
|
if (!normalized) {
|
|
@@ -1154,13 +1174,18 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1154
1174
|
});
|
|
1155
1175
|
}
|
|
1156
1176
|
|
|
1157
|
-
function runCommand(command, args,
|
|
1177
|
+
function runCommand(command, args, options = 120_000) {
|
|
1158
1178
|
return new Promise((resolve) => {
|
|
1159
1179
|
let stdout = "";
|
|
1160
1180
|
let stderr = "";
|
|
1181
|
+
const normalizedOptions =
|
|
1182
|
+
typeof options === "number"
|
|
1183
|
+
? { timeoutMs: options }
|
|
1184
|
+
: (options || {});
|
|
1161
1185
|
const child = spawnFn(command, args, {
|
|
1162
1186
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1163
|
-
env: { ...process.env },
|
|
1187
|
+
env: normalizedOptions.env || { ...process.env },
|
|
1188
|
+
cwd: normalizedOptions.cwd || process.cwd(),
|
|
1164
1189
|
});
|
|
1165
1190
|
const timer = setTimeout(() => {
|
|
1166
1191
|
try {
|
|
@@ -1168,7 +1193,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1168
1193
|
} catch {
|
|
1169
1194
|
/* ignore */
|
|
1170
1195
|
}
|
|
1171
|
-
}, timeoutMs);
|
|
1196
|
+
}, normalizedOptions.timeoutMs ?? 120_000);
|
|
1172
1197
|
child.stdout?.on("data", (chunk) => {
|
|
1173
1198
|
if (stdout.length < 4000) stdout += chunk.toString().slice(0, 2000);
|
|
1174
1199
|
});
|
|
@@ -1187,11 +1212,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1187
1212
|
}
|
|
1188
1213
|
|
|
1189
1214
|
function runBufferedCommand(command, args, options = {}) {
|
|
1190
|
-
return runCommand(
|
|
1191
|
-
command,
|
|
1192
|
-
args,
|
|
1193
|
-
typeof options === "number" ? options : options?.timeoutMs ?? 120_000,
|
|
1194
|
-
);
|
|
1215
|
+
return runCommand(command, args, options);
|
|
1195
1216
|
}
|
|
1196
1217
|
|
|
1197
1218
|
async function readInstalledCliVersion() {
|
|
@@ -2518,6 +2539,16 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2518
2539
|
rejectCreateTaskDuringShutdown(event.payload);
|
|
2519
2540
|
return;
|
|
2520
2541
|
}
|
|
2542
|
+
if (event.type === "restart_task") {
|
|
2543
|
+
reportRestartFailure({
|
|
2544
|
+
taskId: event?.payload?.target_task_id ? String(event.payload.target_task_id) : "",
|
|
2545
|
+
projectId: event?.payload?.project_id ? String(event.payload.project_id) : "",
|
|
2546
|
+
requestId: event?.payload?.request_id ? String(event.payload.request_id) : "",
|
|
2547
|
+
mode: event?.payload?.mode ? String(event.payload.mode) : "",
|
|
2548
|
+
error: new Error("daemon shutting down"),
|
|
2549
|
+
});
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2521
2552
|
if (event.type === "create_pty_task") {
|
|
2522
2553
|
rejectCreatePtyTaskDuringShutdown(event.payload);
|
|
2523
2554
|
return;
|
|
@@ -2528,6 +2559,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2528
2559
|
handleCreateTask(event.payload);
|
|
2529
2560
|
return;
|
|
2530
2561
|
}
|
|
2562
|
+
if (event.type === "restart_task") {
|
|
2563
|
+
void handleRestartTask(event.payload);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2531
2566
|
if (event.type === "create_pty_task") {
|
|
2532
2567
|
void handleCreatePtyTask(event.payload);
|
|
2533
2568
|
return;
|
|
@@ -2792,6 +2827,111 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2792
2827
|
}
|
|
2793
2828
|
}
|
|
2794
2829
|
|
|
2830
|
+
let bridgeSessionHelperPromise = null;
|
|
2831
|
+
async function getBridgeSessionHelper() {
|
|
2832
|
+
if (typeof deps.bridgeSessionBetweenBackends === "function") {
|
|
2833
|
+
return deps.bridgeSessionBetweenBackends;
|
|
2834
|
+
}
|
|
2835
|
+
if (!bridgeSessionHelperPromise) {
|
|
2836
|
+
bridgeSessionHelperPromise = (async () => {
|
|
2837
|
+
try {
|
|
2838
|
+
const bridgeImportTarget =
|
|
2839
|
+
resolveImportTarget(process.env.CONDUCTOR_AI_BRIDGE_API_PATH) ||
|
|
2840
|
+
DEFAULT_AI_BRIDGE_API_SPECIFIER;
|
|
2841
|
+
const bridgeModule = await importOptionalModule(bridgeImportTarget);
|
|
2842
|
+
if (typeof bridgeModule.bridgeSessionBetweenBackends !== "function") {
|
|
2843
|
+
throw new Error("bridgeSessionBetweenBackends is not available");
|
|
2844
|
+
}
|
|
2845
|
+
return bridgeModule.bridgeSessionBetweenBackends;
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
bridgeSessionHelperPromise = null;
|
|
2848
|
+
throw error;
|
|
2849
|
+
}
|
|
2850
|
+
})();
|
|
2851
|
+
}
|
|
2852
|
+
return bridgeSessionHelperPromise;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function reportRestartFailure({ taskId, projectId, requestId, mode, error, sendAck = true }) {
|
|
2856
|
+
const prefix =
|
|
2857
|
+
mode === "bridge_to_new_task" || mode === "fork_to_new_task"
|
|
2858
|
+
? "new task failed"
|
|
2859
|
+
: "restart failed";
|
|
2860
|
+
const summary = `${prefix}: ${error?.message || error}`;
|
|
2861
|
+
if (sendAck) {
|
|
2862
|
+
sendAgentCommandAck({
|
|
2863
|
+
requestId,
|
|
2864
|
+
taskId,
|
|
2865
|
+
eventType: "restart_task",
|
|
2866
|
+
accepted: false,
|
|
2867
|
+
}).catch(() => {});
|
|
2868
|
+
}
|
|
2869
|
+
client
|
|
2870
|
+
.sendJson({
|
|
2871
|
+
type: "task_status_update",
|
|
2872
|
+
payload: {
|
|
2873
|
+
task_id: taskId,
|
|
2874
|
+
project_id: projectId,
|
|
2875
|
+
status: "KILLED",
|
|
2876
|
+
summary,
|
|
2877
|
+
},
|
|
2878
|
+
})
|
|
2879
|
+
.catch((err) => {
|
|
2880
|
+
logError(`Failed to report restart_task failure for ${taskId}: ${err?.message || err}`);
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
async function resolveRestartCwd({
|
|
2885
|
+
projectId,
|
|
2886
|
+
preferredCwd = "",
|
|
2887
|
+
backendType,
|
|
2888
|
+
sessionId,
|
|
2889
|
+
sourceSessionFilePath = "",
|
|
2890
|
+
}) {
|
|
2891
|
+
const normalizedPreferredCwd = typeof preferredCwd === "string" ? preferredCwd.trim() : "";
|
|
2892
|
+
if (normalizedPreferredCwd) {
|
|
2893
|
+
return normalizedPreferredCwd;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
const boundPath = await getProjectLocalPath(projectId);
|
|
2897
|
+
if (boundPath) {
|
|
2898
|
+
return boundPath;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
const normalizedBackend = normalizeRuntimeBackendName(backendType);
|
|
2902
|
+
const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
2903
|
+
if (normalizedSessionId && normalizedBackend && normalizedBackend !== "opencode") {
|
|
2904
|
+
try {
|
|
2905
|
+
const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
|
|
2906
|
+
normalizedBackend,
|
|
2907
|
+
normalizedSessionId,
|
|
2908
|
+
{ cwd: process.cwd() },
|
|
2909
|
+
);
|
|
2910
|
+
if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
|
|
2911
|
+
return resumeContext.cwd.trim();
|
|
2912
|
+
}
|
|
2913
|
+
} catch {
|
|
2914
|
+
// ignore provider-specific fallback failure here; we'll try the remaining fallbacks
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
const normalizedSessionPath =
|
|
2919
|
+
typeof sourceSessionFilePath === "string" ? sourceSessionFilePath.trim() : "";
|
|
2920
|
+
if (normalizedSessionPath) {
|
|
2921
|
+
try {
|
|
2922
|
+
const stats = fs.statSync(normalizedSessionPath);
|
|
2923
|
+
if (stats.isDirectory()) {
|
|
2924
|
+
return normalizedSessionPath;
|
|
2925
|
+
}
|
|
2926
|
+
return path.dirname(normalizedSessionPath);
|
|
2927
|
+
} catch {
|
|
2928
|
+
// ignore missing local path
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
return "";
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2795
2935
|
async function handleCreateTask(payload) {
|
|
2796
2936
|
const {
|
|
2797
2937
|
task_id: taskId,
|
|
@@ -2888,11 +3028,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2888
3028
|
payload: {
|
|
2889
3029
|
task_id: taskId,
|
|
2890
3030
|
project_id: projectId,
|
|
2891
|
-
status: "
|
|
3031
|
+
status: "INIT",
|
|
2892
3032
|
},
|
|
2893
3033
|
})
|
|
2894
3034
|
.catch((err) => {
|
|
2895
|
-
logError(`Failed to report task status (
|
|
3035
|
+
logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
|
|
2896
3036
|
});
|
|
2897
3037
|
|
|
2898
3038
|
// Check if project has a bound local path for this daemon
|
|
@@ -3085,6 +3225,375 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3085
3225
|
});
|
|
3086
3226
|
}
|
|
3087
3227
|
|
|
3228
|
+
async function handleRestartTask(payload) {
|
|
3229
|
+
const {
|
|
3230
|
+
mode,
|
|
3231
|
+
source_task_id: sourceTaskId,
|
|
3232
|
+
target_task_id: targetTaskId,
|
|
3233
|
+
project_id: projectId,
|
|
3234
|
+
title,
|
|
3235
|
+
source_backend_type: sourceBackendType,
|
|
3236
|
+
source_session_id: sourceSessionId,
|
|
3237
|
+
source_session_file_path: sourceSessionFilePath,
|
|
3238
|
+
target_backend_type: targetBackendType,
|
|
3239
|
+
request_id: requestIdRaw,
|
|
3240
|
+
} = payload || {};
|
|
3241
|
+
|
|
3242
|
+
const requestId = requestIdRaw ? String(requestIdRaw) : "";
|
|
3243
|
+
const normalizedMode = typeof mode === "string" ? mode.trim() : "";
|
|
3244
|
+
const normalizedSourceTaskId = sourceTaskId ? String(sourceTaskId) : "";
|
|
3245
|
+
const normalizedTargetTaskId = targetTaskId ? String(targetTaskId) : "";
|
|
3246
|
+
const normalizedProjectId = projectId ? String(projectId) : "";
|
|
3247
|
+
const normalizedSourceSessionId = sourceSessionId ? String(sourceSessionId).trim() : "";
|
|
3248
|
+
|
|
3249
|
+
if (
|
|
3250
|
+
!normalizedMode ||
|
|
3251
|
+
!normalizedSourceTaskId ||
|
|
3252
|
+
!normalizedTargetTaskId ||
|
|
3253
|
+
!normalizedProjectId ||
|
|
3254
|
+
!normalizedSourceSessionId
|
|
3255
|
+
) {
|
|
3256
|
+
logError(`Invalid restart_task payload: ${JSON.stringify(payload)}`);
|
|
3257
|
+
sendAgentCommandAck({
|
|
3258
|
+
requestId,
|
|
3259
|
+
taskId: normalizedTargetTaskId || normalizedSourceTaskId,
|
|
3260
|
+
eventType: "restart_task",
|
|
3261
|
+
accepted: false,
|
|
3262
|
+
}).catch(() => {});
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
if (requestId && !markRequestSeen(requestId)) {
|
|
3267
|
+
log(
|
|
3268
|
+
`Duplicate restart_task ignored for ${normalizedTargetTaskId} (request_id=${requestId})`,
|
|
3269
|
+
);
|
|
3270
|
+
sendAgentCommandAck({
|
|
3271
|
+
requestId,
|
|
3272
|
+
taskId: normalizedTargetTaskId,
|
|
3273
|
+
eventType: "restart_task",
|
|
3274
|
+
accepted: true,
|
|
3275
|
+
}).catch(() => {});
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
if (daemonShuttingDown) {
|
|
3280
|
+
reportRestartFailure({
|
|
3281
|
+
taskId: normalizedTargetTaskId,
|
|
3282
|
+
projectId: normalizedProjectId,
|
|
3283
|
+
requestId,
|
|
3284
|
+
mode: normalizedMode,
|
|
3285
|
+
error: new Error("daemon shutting down"),
|
|
3286
|
+
});
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
const activeTarget = activeTaskProcesses.get(normalizedTargetTaskId);
|
|
3291
|
+
if (activeTarget?.child) {
|
|
3292
|
+
reportRestartFailure({
|
|
3293
|
+
taskId: normalizedTargetTaskId,
|
|
3294
|
+
projectId: normalizedProjectId,
|
|
3295
|
+
requestId,
|
|
3296
|
+
mode: normalizedMode,
|
|
3297
|
+
error: new Error(`task already active (pid=${activeTarget.child.pid ?? "unknown"})`),
|
|
3298
|
+
});
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
|
|
3303
|
+
if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
|
|
3304
|
+
reportRestartFailure({
|
|
3305
|
+
taskId: normalizedTargetTaskId,
|
|
3306
|
+
projectId: normalizedProjectId,
|
|
3307
|
+
requestId,
|
|
3308
|
+
mode: normalizedMode,
|
|
3309
|
+
error: new Error(`Unsupported backend: ${effectiveBackend}`),
|
|
3310
|
+
});
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
if (normalizedMode === "resume_inplace") {
|
|
3315
|
+
if (normalizedTargetTaskId !== normalizedSourceTaskId) {
|
|
3316
|
+
reportRestartFailure({
|
|
3317
|
+
taskId: normalizedTargetTaskId,
|
|
3318
|
+
projectId: normalizedProjectId,
|
|
3319
|
+
requestId,
|
|
3320
|
+
mode: normalizedMode,
|
|
3321
|
+
error: new Error("In-place restart must reuse the same task"),
|
|
3322
|
+
});
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
if (effectiveBackend !== sourceBackendType) {
|
|
3326
|
+
reportRestartFailure({
|
|
3327
|
+
taskId: normalizedTargetTaskId,
|
|
3328
|
+
projectId: normalizedProjectId,
|
|
3329
|
+
requestId,
|
|
3330
|
+
mode: normalizedMode,
|
|
3331
|
+
error: new Error("In-place restart must reuse the same backend"),
|
|
3332
|
+
});
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
sendAgentCommandAck({
|
|
3338
|
+
requestId,
|
|
3339
|
+
taskId: normalizedTargetTaskId,
|
|
3340
|
+
eventType: "restart_task",
|
|
3341
|
+
accepted: true,
|
|
3342
|
+
}).catch((err) => {
|
|
3343
|
+
logError(`Failed to report agent_command_ack(restart_task) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
let resolvedResumeSessionId = normalizedSourceSessionId;
|
|
3347
|
+
let resolvedResumeCwd = "";
|
|
3348
|
+
try {
|
|
3349
|
+
if (normalizedMode === "bridge_to_new_task" || normalizedMode === "fork_to_new_task") {
|
|
3350
|
+
const sourceResumeCwd = await resolveRestartCwd({
|
|
3351
|
+
projectId: normalizedProjectId,
|
|
3352
|
+
backendType: sourceBackendType,
|
|
3353
|
+
sessionId: normalizedSourceSessionId,
|
|
3354
|
+
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
3355
|
+
});
|
|
3356
|
+
const bridgeSession = await getBridgeSessionHelper();
|
|
3357
|
+
const bridgeResult = await bridgeSession({
|
|
3358
|
+
sourceTool: sourceBackendType,
|
|
3359
|
+
sourceSessionId: normalizedSourceSessionId,
|
|
3360
|
+
sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
|
|
3361
|
+
sourceSessionInfo: {
|
|
3362
|
+
tool: sourceBackendType,
|
|
3363
|
+
sessionId: normalizedSourceSessionId,
|
|
3364
|
+
path: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
|
|
3365
|
+
cwd: sourceResumeCwd || undefined,
|
|
3366
|
+
},
|
|
3367
|
+
targetTool: effectiveBackend,
|
|
3368
|
+
targetCwdFallback: sourceResumeCwd || undefined,
|
|
3369
|
+
});
|
|
3370
|
+
resolvedResumeSessionId = bridgeResult.sessionId;
|
|
3371
|
+
resolvedResumeCwd = await resolveRestartCwd({
|
|
3372
|
+
projectId: normalizedProjectId,
|
|
3373
|
+
preferredCwd: bridgeResult.cwd,
|
|
3374
|
+
backendType: effectiveBackend,
|
|
3375
|
+
sessionId: bridgeResult.sessionId,
|
|
3376
|
+
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
3377
|
+
});
|
|
3378
|
+
} else if (normalizedMode === "resume_inplace") {
|
|
3379
|
+
resolvedResumeCwd = await resolveRestartCwd({
|
|
3380
|
+
projectId: normalizedProjectId,
|
|
3381
|
+
backendType: effectiveBackend,
|
|
3382
|
+
sessionId: normalizedSourceSessionId,
|
|
3383
|
+
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
3384
|
+
});
|
|
3385
|
+
} else {
|
|
3386
|
+
throw new Error(`Unsupported restart mode: ${normalizedMode}`);
|
|
3387
|
+
}
|
|
3388
|
+
} catch (error) {
|
|
3389
|
+
reportRestartFailure({
|
|
3390
|
+
taskId: normalizedTargetTaskId,
|
|
3391
|
+
projectId: normalizedProjectId,
|
|
3392
|
+
requestId,
|
|
3393
|
+
mode: normalizedMode,
|
|
3394
|
+
error,
|
|
3395
|
+
sendAck: false,
|
|
3396
|
+
});
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
if (!resolvedResumeCwd) {
|
|
3401
|
+
reportRestartFailure({
|
|
3402
|
+
taskId: normalizedTargetTaskId,
|
|
3403
|
+
projectId: normalizedProjectId,
|
|
3404
|
+
requestId,
|
|
3405
|
+
mode: normalizedMode,
|
|
3406
|
+
error: new Error("Could not resolve resume cwd"),
|
|
3407
|
+
sendAck: false,
|
|
3408
|
+
});
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
|
|
3413
|
+
|
|
3414
|
+
log("");
|
|
3415
|
+
log(
|
|
3416
|
+
`Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
|
|
3417
|
+
);
|
|
3418
|
+
log(`CLI command: ${cliCommand}`);
|
|
3419
|
+
|
|
3420
|
+
if (normalizedMode !== "resume_inplace") {
|
|
3421
|
+
client
|
|
3422
|
+
.sendJson({
|
|
3423
|
+
type: "task_status_update",
|
|
3424
|
+
payload: {
|
|
3425
|
+
task_id: normalizedTargetTaskId,
|
|
3426
|
+
project_id: normalizedProjectId,
|
|
3427
|
+
status: "INIT",
|
|
3428
|
+
},
|
|
3429
|
+
})
|
|
3430
|
+
.catch((err) => {
|
|
3431
|
+
logError(`Failed to report task status (INIT) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
if (daemonShuttingDown) {
|
|
3436
|
+
reportRestartFailure({
|
|
3437
|
+
taskId: normalizedTargetTaskId,
|
|
3438
|
+
projectId: normalizedProjectId,
|
|
3439
|
+
requestId,
|
|
3440
|
+
mode: normalizedMode,
|
|
3441
|
+
error: new Error("daemon shutting down"),
|
|
3442
|
+
sendAck: false,
|
|
3443
|
+
});
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
let taskDir = resolvedResumeCwd;
|
|
3448
|
+
let logPath = path.join(taskDir, "conductor.log");
|
|
3449
|
+
|
|
3450
|
+
try {
|
|
3451
|
+
mkdirSyncFn(taskDir, { recursive: true });
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
reportRestartFailure({
|
|
3454
|
+
taskId: normalizedTargetTaskId,
|
|
3455
|
+
projectId: normalizedProjectId,
|
|
3456
|
+
requestId,
|
|
3457
|
+
mode: normalizedMode,
|
|
3458
|
+
error: new Error(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`),
|
|
3459
|
+
sendAck: false,
|
|
3460
|
+
});
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
const args = [];
|
|
3465
|
+
if (effectiveBackend) {
|
|
3466
|
+
args.push("--backend", effectiveBackend);
|
|
3467
|
+
}
|
|
3468
|
+
args.push("--resume", resolvedResumeSessionId);
|
|
3469
|
+
args.push("--");
|
|
3470
|
+
|
|
3471
|
+
const env = {
|
|
3472
|
+
...process.env,
|
|
3473
|
+
CONDUCTOR_PROJECT_ID: normalizedProjectId,
|
|
3474
|
+
CONDUCTOR_TASK_ID: normalizedTargetTaskId,
|
|
3475
|
+
CONDUCTOR_CLI_COMMAND: cliCommand,
|
|
3476
|
+
CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
|
|
3477
|
+
};
|
|
3478
|
+
if (config.CONFIG_FILE) {
|
|
3479
|
+
env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
|
|
3480
|
+
}
|
|
3481
|
+
if (AGENT_TOKEN) {
|
|
3482
|
+
env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
|
|
3483
|
+
}
|
|
3484
|
+
if (BACKEND_HTTP) {
|
|
3485
|
+
env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
|
|
3489
|
+
cwd: taskDir,
|
|
3490
|
+
env,
|
|
3491
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
3492
|
+
});
|
|
3493
|
+
|
|
3494
|
+
let logStream;
|
|
3495
|
+
try {
|
|
3496
|
+
logStream = createWriteStreamFn(logPath, { flags: "a" });
|
|
3497
|
+
if (logStream && typeof logStream.on === "function") {
|
|
3498
|
+
const logPathSnapshot = logPath;
|
|
3499
|
+
logStream.on("error", (err) => {
|
|
3500
|
+
logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
} catch (err) {
|
|
3504
|
+
logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
log(`Task title: ${title || normalizedTargetTaskId}`);
|
|
3508
|
+
log(`Resume session: ${resolvedResumeSessionId}`);
|
|
3509
|
+
log(`Resume cwd: ${resolvedResumeCwd}`);
|
|
3510
|
+
log(`Logs: ${logPath}`);
|
|
3511
|
+
|
|
3512
|
+
activeTaskProcesses.set(normalizedTargetTaskId, {
|
|
3513
|
+
child,
|
|
3514
|
+
projectId: normalizedProjectId,
|
|
3515
|
+
logPath,
|
|
3516
|
+
stopForceKillTimer: null,
|
|
3517
|
+
});
|
|
3518
|
+
|
|
3519
|
+
client
|
|
3520
|
+
.sendJson({
|
|
3521
|
+
type: "task_status_update",
|
|
3522
|
+
payload: {
|
|
3523
|
+
task_id: normalizedTargetTaskId,
|
|
3524
|
+
project_id: normalizedProjectId,
|
|
3525
|
+
status: "RUNNING",
|
|
3526
|
+
},
|
|
3527
|
+
})
|
|
3528
|
+
.catch((err) => {
|
|
3529
|
+
logError(`Failed to report task status (RUNNING) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3530
|
+
});
|
|
3531
|
+
|
|
3532
|
+
if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
|
|
3533
|
+
child.stdout.pipe(logStream, { end: false });
|
|
3534
|
+
} else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
|
|
3535
|
+
child.stdout.on("data", (chunk) => logStream.write(chunk));
|
|
3536
|
+
}
|
|
3537
|
+
if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
|
|
3538
|
+
child.stderr.pipe(logStream, { end: false });
|
|
3539
|
+
} else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
|
|
3540
|
+
child.stderr.on("data", (chunk) => logStream.write(chunk));
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
child.on("error", (err) => {
|
|
3544
|
+
logError(`Failed to spawn restart CLI for ${normalizedTargetTaskId}: ${err.message}`);
|
|
3545
|
+
if (logStream) {
|
|
3546
|
+
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
3547
|
+
logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
child.on("exit", (code, signal) => {
|
|
3552
|
+
const active = activeTaskProcesses.get(normalizedTargetTaskId);
|
|
3553
|
+
if (active?.stopForceKillTimer) {
|
|
3554
|
+
clearTimeout(active.stopForceKillTimer);
|
|
3555
|
+
}
|
|
3556
|
+
activeTaskProcesses.delete(normalizedTargetTaskId);
|
|
3557
|
+
const suppressExitStatusReport = suppressedExitStatusReports.has(normalizedTargetTaskId);
|
|
3558
|
+
suppressedExitStatusReports.delete(normalizedTargetTaskId);
|
|
3559
|
+
if (logStream) {
|
|
3560
|
+
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
3561
|
+
if (signal) {
|
|
3562
|
+
logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
|
|
3563
|
+
} else {
|
|
3564
|
+
logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
|
|
3565
|
+
}
|
|
3566
|
+
logStream.end();
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
const isKilledBySignal = Boolean(signal);
|
|
3570
|
+
const isKilledByExitCode = code === 130 || code === 143;
|
|
3571
|
+
const isKilled = isKilledBySignal || isKilledByExitCode;
|
|
3572
|
+
const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
|
|
3573
|
+
const summary = isKilled
|
|
3574
|
+
? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
|
|
3575
|
+
: code === 0
|
|
3576
|
+
? "completed"
|
|
3577
|
+
: `exited with code ${code}`;
|
|
3578
|
+
|
|
3579
|
+
if (!suppressExitStatusReport) {
|
|
3580
|
+
client
|
|
3581
|
+
.sendJson({
|
|
3582
|
+
type: "task_status_update",
|
|
3583
|
+
payload: {
|
|
3584
|
+
task_id: normalizedTargetTaskId,
|
|
3585
|
+
project_id: normalizedProjectId,
|
|
3586
|
+
status,
|
|
3587
|
+
summary,
|
|
3588
|
+
},
|
|
3589
|
+
})
|
|
3590
|
+
.catch((err) => {
|
|
3591
|
+
logError(`Failed to report task status (${status}) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3088
3597
|
let closePromise = null;
|
|
3089
3598
|
async function shutdownDaemon(reason = "manual close") {
|
|
3090
3599
|
if (closePromise) {
|
package/src/native-deps.js
CHANGED
|
@@ -315,8 +315,16 @@ export async function repairAndVerifyGlobalNodePty({
|
|
|
315
315
|
await ensurePnpmOnlyBuiltDependencies({ runCommand, dependencies, global: true });
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
const packageDirectory = await resolveGlobalPackageDirectory({
|
|
319
|
+
packageManager,
|
|
320
|
+
packageName,
|
|
321
|
+
runCommand,
|
|
322
|
+
});
|
|
323
|
+
|
|
318
324
|
if (packageManager === "pnpm") {
|
|
319
|
-
const rebuildResult = await runCommand("pnpm", ["rebuild",
|
|
325
|
+
const rebuildResult = await runCommand("pnpm", ["rebuild", ...dependencies], {
|
|
326
|
+
cwd: packageDirectory,
|
|
327
|
+
});
|
|
320
328
|
if (!rebuildResult.success) {
|
|
321
329
|
throw new Error(
|
|
322
330
|
`pnpm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
|
|
@@ -336,12 +344,6 @@ export async function repairAndVerifyGlobalNodePty({
|
|
|
336
344
|
);
|
|
337
345
|
}
|
|
338
346
|
}
|
|
339
|
-
|
|
340
|
-
const packageDirectory = await resolveGlobalPackageDirectory({
|
|
341
|
-
packageManager,
|
|
342
|
-
packageName,
|
|
343
|
-
runCommand,
|
|
344
|
-
});
|
|
345
347
|
await verifyNodePtyForPackageDirectory({
|
|
346
348
|
packageDirectory,
|
|
347
349
|
runCommand,
|