@love-moon/conductor-cli 0.2.27 → 0.2.29
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 +511 -3
- package/src/native-deps.js +34 -0
|
@@ -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.29",
|
|
4
|
+
"gitCommitId": "56ce873",
|
|
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.29",
|
|
22
|
+
"@love-moon/conductor-sdk": "0.2.29",
|
|
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) {
|
|
@@ -2518,6 +2538,16 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2518
2538
|
rejectCreateTaskDuringShutdown(event.payload);
|
|
2519
2539
|
return;
|
|
2520
2540
|
}
|
|
2541
|
+
if (event.type === "restart_task") {
|
|
2542
|
+
reportRestartFailure({
|
|
2543
|
+
taskId: event?.payload?.target_task_id ? String(event.payload.target_task_id) : "",
|
|
2544
|
+
projectId: event?.payload?.project_id ? String(event.payload.project_id) : "",
|
|
2545
|
+
requestId: event?.payload?.request_id ? String(event.payload.request_id) : "",
|
|
2546
|
+
mode: event?.payload?.mode ? String(event.payload.mode) : "",
|
|
2547
|
+
error: new Error("daemon shutting down"),
|
|
2548
|
+
});
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2521
2551
|
if (event.type === "create_pty_task") {
|
|
2522
2552
|
rejectCreatePtyTaskDuringShutdown(event.payload);
|
|
2523
2553
|
return;
|
|
@@ -2528,6 +2558,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2528
2558
|
handleCreateTask(event.payload);
|
|
2529
2559
|
return;
|
|
2530
2560
|
}
|
|
2561
|
+
if (event.type === "restart_task") {
|
|
2562
|
+
void handleRestartTask(event.payload);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2531
2565
|
if (event.type === "create_pty_task") {
|
|
2532
2566
|
void handleCreatePtyTask(event.payload);
|
|
2533
2567
|
return;
|
|
@@ -2792,6 +2826,111 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2792
2826
|
}
|
|
2793
2827
|
}
|
|
2794
2828
|
|
|
2829
|
+
let bridgeSessionHelperPromise = null;
|
|
2830
|
+
async function getBridgeSessionHelper() {
|
|
2831
|
+
if (typeof deps.bridgeSessionBetweenBackends === "function") {
|
|
2832
|
+
return deps.bridgeSessionBetweenBackends;
|
|
2833
|
+
}
|
|
2834
|
+
if (!bridgeSessionHelperPromise) {
|
|
2835
|
+
bridgeSessionHelperPromise = (async () => {
|
|
2836
|
+
try {
|
|
2837
|
+
const bridgeImportTarget =
|
|
2838
|
+
resolveImportTarget(process.env.CONDUCTOR_AI_BRIDGE_API_PATH) ||
|
|
2839
|
+
DEFAULT_AI_BRIDGE_API_SPECIFIER;
|
|
2840
|
+
const bridgeModule = await importOptionalModule(bridgeImportTarget);
|
|
2841
|
+
if (typeof bridgeModule.bridgeSessionBetweenBackends !== "function") {
|
|
2842
|
+
throw new Error("bridgeSessionBetweenBackends is not available");
|
|
2843
|
+
}
|
|
2844
|
+
return bridgeModule.bridgeSessionBetweenBackends;
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
bridgeSessionHelperPromise = null;
|
|
2847
|
+
throw error;
|
|
2848
|
+
}
|
|
2849
|
+
})();
|
|
2850
|
+
}
|
|
2851
|
+
return bridgeSessionHelperPromise;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function reportRestartFailure({ taskId, projectId, requestId, mode, error, sendAck = true }) {
|
|
2855
|
+
const prefix =
|
|
2856
|
+
mode === "bridge_to_new_task" || mode === "fork_to_new_task"
|
|
2857
|
+
? "new task failed"
|
|
2858
|
+
: "restart failed";
|
|
2859
|
+
const summary = `${prefix}: ${error?.message || error}`;
|
|
2860
|
+
if (sendAck) {
|
|
2861
|
+
sendAgentCommandAck({
|
|
2862
|
+
requestId,
|
|
2863
|
+
taskId,
|
|
2864
|
+
eventType: "restart_task",
|
|
2865
|
+
accepted: false,
|
|
2866
|
+
}).catch(() => {});
|
|
2867
|
+
}
|
|
2868
|
+
client
|
|
2869
|
+
.sendJson({
|
|
2870
|
+
type: "task_status_update",
|
|
2871
|
+
payload: {
|
|
2872
|
+
task_id: taskId,
|
|
2873
|
+
project_id: projectId,
|
|
2874
|
+
status: "KILLED",
|
|
2875
|
+
summary,
|
|
2876
|
+
},
|
|
2877
|
+
})
|
|
2878
|
+
.catch((err) => {
|
|
2879
|
+
logError(`Failed to report restart_task failure for ${taskId}: ${err?.message || err}`);
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
async function resolveRestartCwd({
|
|
2884
|
+
projectId,
|
|
2885
|
+
preferredCwd = "",
|
|
2886
|
+
backendType,
|
|
2887
|
+
sessionId,
|
|
2888
|
+
sourceSessionFilePath = "",
|
|
2889
|
+
}) {
|
|
2890
|
+
const normalizedPreferredCwd = typeof preferredCwd === "string" ? preferredCwd.trim() : "";
|
|
2891
|
+
if (normalizedPreferredCwd) {
|
|
2892
|
+
return normalizedPreferredCwd;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
const boundPath = await getProjectLocalPath(projectId);
|
|
2896
|
+
if (boundPath) {
|
|
2897
|
+
return boundPath;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
const normalizedBackend = normalizeRuntimeBackendName(backendType);
|
|
2901
|
+
const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
2902
|
+
if (normalizedSessionId && normalizedBackend && normalizedBackend !== "opencode") {
|
|
2903
|
+
try {
|
|
2904
|
+
const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
|
|
2905
|
+
normalizedBackend,
|
|
2906
|
+
normalizedSessionId,
|
|
2907
|
+
{ cwd: process.cwd() },
|
|
2908
|
+
);
|
|
2909
|
+
if (typeof resumeContext?.cwd === "string" && resumeContext.cwd.trim()) {
|
|
2910
|
+
return resumeContext.cwd.trim();
|
|
2911
|
+
}
|
|
2912
|
+
} catch {
|
|
2913
|
+
// ignore provider-specific fallback failure here; we'll try the remaining fallbacks
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
const normalizedSessionPath =
|
|
2918
|
+
typeof sourceSessionFilePath === "string" ? sourceSessionFilePath.trim() : "";
|
|
2919
|
+
if (normalizedSessionPath) {
|
|
2920
|
+
try {
|
|
2921
|
+
const stats = fs.statSync(normalizedSessionPath);
|
|
2922
|
+
if (stats.isDirectory()) {
|
|
2923
|
+
return normalizedSessionPath;
|
|
2924
|
+
}
|
|
2925
|
+
return path.dirname(normalizedSessionPath);
|
|
2926
|
+
} catch {
|
|
2927
|
+
// ignore missing local path
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
return "";
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2795
2934
|
async function handleCreateTask(payload) {
|
|
2796
2935
|
const {
|
|
2797
2936
|
task_id: taskId,
|
|
@@ -2888,11 +3027,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2888
3027
|
payload: {
|
|
2889
3028
|
task_id: taskId,
|
|
2890
3029
|
project_id: projectId,
|
|
2891
|
-
status: "
|
|
3030
|
+
status: "INIT",
|
|
2892
3031
|
},
|
|
2893
3032
|
})
|
|
2894
3033
|
.catch((err) => {
|
|
2895
|
-
logError(`Failed to report task status (
|
|
3034
|
+
logError(`Failed to report task status (INIT) for ${taskId}: ${err?.message || err}`);
|
|
2896
3035
|
});
|
|
2897
3036
|
|
|
2898
3037
|
// Check if project has a bound local path for this daemon
|
|
@@ -3085,6 +3224,375 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3085
3224
|
});
|
|
3086
3225
|
}
|
|
3087
3226
|
|
|
3227
|
+
async function handleRestartTask(payload) {
|
|
3228
|
+
const {
|
|
3229
|
+
mode,
|
|
3230
|
+
source_task_id: sourceTaskId,
|
|
3231
|
+
target_task_id: targetTaskId,
|
|
3232
|
+
project_id: projectId,
|
|
3233
|
+
title,
|
|
3234
|
+
source_backend_type: sourceBackendType,
|
|
3235
|
+
source_session_id: sourceSessionId,
|
|
3236
|
+
source_session_file_path: sourceSessionFilePath,
|
|
3237
|
+
target_backend_type: targetBackendType,
|
|
3238
|
+
request_id: requestIdRaw,
|
|
3239
|
+
} = payload || {};
|
|
3240
|
+
|
|
3241
|
+
const requestId = requestIdRaw ? String(requestIdRaw) : "";
|
|
3242
|
+
const normalizedMode = typeof mode === "string" ? mode.trim() : "";
|
|
3243
|
+
const normalizedSourceTaskId = sourceTaskId ? String(sourceTaskId) : "";
|
|
3244
|
+
const normalizedTargetTaskId = targetTaskId ? String(targetTaskId) : "";
|
|
3245
|
+
const normalizedProjectId = projectId ? String(projectId) : "";
|
|
3246
|
+
const normalizedSourceSessionId = sourceSessionId ? String(sourceSessionId).trim() : "";
|
|
3247
|
+
|
|
3248
|
+
if (
|
|
3249
|
+
!normalizedMode ||
|
|
3250
|
+
!normalizedSourceTaskId ||
|
|
3251
|
+
!normalizedTargetTaskId ||
|
|
3252
|
+
!normalizedProjectId ||
|
|
3253
|
+
!normalizedSourceSessionId
|
|
3254
|
+
) {
|
|
3255
|
+
logError(`Invalid restart_task payload: ${JSON.stringify(payload)}`);
|
|
3256
|
+
sendAgentCommandAck({
|
|
3257
|
+
requestId,
|
|
3258
|
+
taskId: normalizedTargetTaskId || normalizedSourceTaskId,
|
|
3259
|
+
eventType: "restart_task",
|
|
3260
|
+
accepted: false,
|
|
3261
|
+
}).catch(() => {});
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
if (requestId && !markRequestSeen(requestId)) {
|
|
3266
|
+
log(
|
|
3267
|
+
`Duplicate restart_task ignored for ${normalizedTargetTaskId} (request_id=${requestId})`,
|
|
3268
|
+
);
|
|
3269
|
+
sendAgentCommandAck({
|
|
3270
|
+
requestId,
|
|
3271
|
+
taskId: normalizedTargetTaskId,
|
|
3272
|
+
eventType: "restart_task",
|
|
3273
|
+
accepted: true,
|
|
3274
|
+
}).catch(() => {});
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
if (daemonShuttingDown) {
|
|
3279
|
+
reportRestartFailure({
|
|
3280
|
+
taskId: normalizedTargetTaskId,
|
|
3281
|
+
projectId: normalizedProjectId,
|
|
3282
|
+
requestId,
|
|
3283
|
+
mode: normalizedMode,
|
|
3284
|
+
error: new Error("daemon shutting down"),
|
|
3285
|
+
});
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
const activeTarget = activeTaskProcesses.get(normalizedTargetTaskId);
|
|
3290
|
+
if (activeTarget?.child) {
|
|
3291
|
+
reportRestartFailure({
|
|
3292
|
+
taskId: normalizedTargetTaskId,
|
|
3293
|
+
projectId: normalizedProjectId,
|
|
3294
|
+
requestId,
|
|
3295
|
+
mode: normalizedMode,
|
|
3296
|
+
error: new Error(`task already active (pid=${activeTarget.child.pid ?? "unknown"})`),
|
|
3297
|
+
});
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
const effectiveBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
|
|
3302
|
+
if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
|
|
3303
|
+
reportRestartFailure({
|
|
3304
|
+
taskId: normalizedTargetTaskId,
|
|
3305
|
+
projectId: normalizedProjectId,
|
|
3306
|
+
requestId,
|
|
3307
|
+
mode: normalizedMode,
|
|
3308
|
+
error: new Error(`Unsupported backend: ${effectiveBackend}`),
|
|
3309
|
+
});
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
if (normalizedMode === "resume_inplace") {
|
|
3314
|
+
if (normalizedTargetTaskId !== normalizedSourceTaskId) {
|
|
3315
|
+
reportRestartFailure({
|
|
3316
|
+
taskId: normalizedTargetTaskId,
|
|
3317
|
+
projectId: normalizedProjectId,
|
|
3318
|
+
requestId,
|
|
3319
|
+
mode: normalizedMode,
|
|
3320
|
+
error: new Error("In-place restart must reuse the same task"),
|
|
3321
|
+
});
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (effectiveBackend !== sourceBackendType) {
|
|
3325
|
+
reportRestartFailure({
|
|
3326
|
+
taskId: normalizedTargetTaskId,
|
|
3327
|
+
projectId: normalizedProjectId,
|
|
3328
|
+
requestId,
|
|
3329
|
+
mode: normalizedMode,
|
|
3330
|
+
error: new Error("In-place restart must reuse the same backend"),
|
|
3331
|
+
});
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
sendAgentCommandAck({
|
|
3337
|
+
requestId,
|
|
3338
|
+
taskId: normalizedTargetTaskId,
|
|
3339
|
+
eventType: "restart_task",
|
|
3340
|
+
accepted: true,
|
|
3341
|
+
}).catch((err) => {
|
|
3342
|
+
logError(`Failed to report agent_command_ack(restart_task) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
let resolvedResumeSessionId = normalizedSourceSessionId;
|
|
3346
|
+
let resolvedResumeCwd = "";
|
|
3347
|
+
try {
|
|
3348
|
+
if (normalizedMode === "bridge_to_new_task" || normalizedMode === "fork_to_new_task") {
|
|
3349
|
+
const sourceResumeCwd = await resolveRestartCwd({
|
|
3350
|
+
projectId: normalizedProjectId,
|
|
3351
|
+
backendType: sourceBackendType,
|
|
3352
|
+
sessionId: normalizedSourceSessionId,
|
|
3353
|
+
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
3354
|
+
});
|
|
3355
|
+
const bridgeSession = await getBridgeSessionHelper();
|
|
3356
|
+
const bridgeResult = await bridgeSession({
|
|
3357
|
+
sourceTool: sourceBackendType,
|
|
3358
|
+
sourceSessionId: normalizedSourceSessionId,
|
|
3359
|
+
sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
|
|
3360
|
+
sourceSessionInfo: {
|
|
3361
|
+
tool: sourceBackendType,
|
|
3362
|
+
sessionId: normalizedSourceSessionId,
|
|
3363
|
+
path: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
|
|
3364
|
+
cwd: sourceResumeCwd || undefined,
|
|
3365
|
+
},
|
|
3366
|
+
targetTool: effectiveBackend,
|
|
3367
|
+
targetCwdFallback: sourceResumeCwd || undefined,
|
|
3368
|
+
});
|
|
3369
|
+
resolvedResumeSessionId = bridgeResult.sessionId;
|
|
3370
|
+
resolvedResumeCwd = await resolveRestartCwd({
|
|
3371
|
+
projectId: normalizedProjectId,
|
|
3372
|
+
preferredCwd: bridgeResult.cwd,
|
|
3373
|
+
backendType: effectiveBackend,
|
|
3374
|
+
sessionId: bridgeResult.sessionId,
|
|
3375
|
+
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
3376
|
+
});
|
|
3377
|
+
} else if (normalizedMode === "resume_inplace") {
|
|
3378
|
+
resolvedResumeCwd = await resolveRestartCwd({
|
|
3379
|
+
projectId: normalizedProjectId,
|
|
3380
|
+
backendType: effectiveBackend,
|
|
3381
|
+
sessionId: normalizedSourceSessionId,
|
|
3382
|
+
sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
|
|
3383
|
+
});
|
|
3384
|
+
} else {
|
|
3385
|
+
throw new Error(`Unsupported restart mode: ${normalizedMode}`);
|
|
3386
|
+
}
|
|
3387
|
+
} catch (error) {
|
|
3388
|
+
reportRestartFailure({
|
|
3389
|
+
taskId: normalizedTargetTaskId,
|
|
3390
|
+
projectId: normalizedProjectId,
|
|
3391
|
+
requestId,
|
|
3392
|
+
mode: normalizedMode,
|
|
3393
|
+
error,
|
|
3394
|
+
sendAck: false,
|
|
3395
|
+
});
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
if (!resolvedResumeCwd) {
|
|
3400
|
+
reportRestartFailure({
|
|
3401
|
+
taskId: normalizedTargetTaskId,
|
|
3402
|
+
projectId: normalizedProjectId,
|
|
3403
|
+
requestId,
|
|
3404
|
+
mode: normalizedMode,
|
|
3405
|
+
error: new Error("Could not resolve resume cwd"),
|
|
3406
|
+
sendAck: false,
|
|
3407
|
+
});
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
|
|
3412
|
+
|
|
3413
|
+
log("");
|
|
3414
|
+
log(
|
|
3415
|
+
`Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
|
|
3416
|
+
);
|
|
3417
|
+
log(`CLI command: ${cliCommand}`);
|
|
3418
|
+
|
|
3419
|
+
if (normalizedMode !== "resume_inplace") {
|
|
3420
|
+
client
|
|
3421
|
+
.sendJson({
|
|
3422
|
+
type: "task_status_update",
|
|
3423
|
+
payload: {
|
|
3424
|
+
task_id: normalizedTargetTaskId,
|
|
3425
|
+
project_id: normalizedProjectId,
|
|
3426
|
+
status: "INIT",
|
|
3427
|
+
},
|
|
3428
|
+
})
|
|
3429
|
+
.catch((err) => {
|
|
3430
|
+
logError(`Failed to report task status (INIT) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
if (daemonShuttingDown) {
|
|
3435
|
+
reportRestartFailure({
|
|
3436
|
+
taskId: normalizedTargetTaskId,
|
|
3437
|
+
projectId: normalizedProjectId,
|
|
3438
|
+
requestId,
|
|
3439
|
+
mode: normalizedMode,
|
|
3440
|
+
error: new Error("daemon shutting down"),
|
|
3441
|
+
sendAck: false,
|
|
3442
|
+
});
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
let taskDir = resolvedResumeCwd;
|
|
3447
|
+
let logPath = path.join(taskDir, "conductor.log");
|
|
3448
|
+
|
|
3449
|
+
try {
|
|
3450
|
+
mkdirSyncFn(taskDir, { recursive: true });
|
|
3451
|
+
} catch (err) {
|
|
3452
|
+
reportRestartFailure({
|
|
3453
|
+
taskId: normalizedTargetTaskId,
|
|
3454
|
+
projectId: normalizedProjectId,
|
|
3455
|
+
requestId,
|
|
3456
|
+
mode: normalizedMode,
|
|
3457
|
+
error: new Error(`Failed to ensure task workspace ${taskDir}: ${err?.message || err}`),
|
|
3458
|
+
sendAck: false,
|
|
3459
|
+
});
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
const args = [];
|
|
3464
|
+
if (effectiveBackend) {
|
|
3465
|
+
args.push("--backend", effectiveBackend);
|
|
3466
|
+
}
|
|
3467
|
+
args.push("--resume", resolvedResumeSessionId);
|
|
3468
|
+
args.push("--");
|
|
3469
|
+
|
|
3470
|
+
const env = {
|
|
3471
|
+
...process.env,
|
|
3472
|
+
CONDUCTOR_PROJECT_ID: normalizedProjectId,
|
|
3473
|
+
CONDUCTOR_TASK_ID: normalizedTargetTaskId,
|
|
3474
|
+
CONDUCTOR_CLI_COMMAND: cliCommand,
|
|
3475
|
+
CONDUCTOR_RESUME_CWD: resolvedResumeCwd,
|
|
3476
|
+
};
|
|
3477
|
+
if (config.CONFIG_FILE) {
|
|
3478
|
+
env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
|
|
3479
|
+
}
|
|
3480
|
+
if (AGENT_TOKEN) {
|
|
3481
|
+
env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
|
|
3482
|
+
}
|
|
3483
|
+
if (BACKEND_HTTP) {
|
|
3484
|
+
env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
const child = spawnFn(process.execPath, [CLI_PATH_VAL, ...args], {
|
|
3488
|
+
cwd: taskDir,
|
|
3489
|
+
env,
|
|
3490
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3493
|
+
let logStream;
|
|
3494
|
+
try {
|
|
3495
|
+
logStream = createWriteStreamFn(logPath, { flags: "a" });
|
|
3496
|
+
if (logStream && typeof logStream.on === "function") {
|
|
3497
|
+
const logPathSnapshot = logPath;
|
|
3498
|
+
logStream.on("error", (err) => {
|
|
3499
|
+
logError(`Log stream error (${logPathSnapshot}): ${err?.message || err}`);
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
logError(`Failed to open log file ${logPath}: ${err?.message || err}`);
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
log(`Task title: ${title || normalizedTargetTaskId}`);
|
|
3507
|
+
log(`Resume session: ${resolvedResumeSessionId}`);
|
|
3508
|
+
log(`Resume cwd: ${resolvedResumeCwd}`);
|
|
3509
|
+
log(`Logs: ${logPath}`);
|
|
3510
|
+
|
|
3511
|
+
activeTaskProcesses.set(normalizedTargetTaskId, {
|
|
3512
|
+
child,
|
|
3513
|
+
projectId: normalizedProjectId,
|
|
3514
|
+
logPath,
|
|
3515
|
+
stopForceKillTimer: null,
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
client
|
|
3519
|
+
.sendJson({
|
|
3520
|
+
type: "task_status_update",
|
|
3521
|
+
payload: {
|
|
3522
|
+
task_id: normalizedTargetTaskId,
|
|
3523
|
+
project_id: normalizedProjectId,
|
|
3524
|
+
status: "RUNNING",
|
|
3525
|
+
},
|
|
3526
|
+
})
|
|
3527
|
+
.catch((err) => {
|
|
3528
|
+
logError(`Failed to report task status (RUNNING) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3529
|
+
});
|
|
3530
|
+
|
|
3531
|
+
if (child.stdout && typeof child.stdout.pipe === "function" && logStream) {
|
|
3532
|
+
child.stdout.pipe(logStream, { end: false });
|
|
3533
|
+
} else if (child.stdout && typeof child.stdout.on === "function" && logStream) {
|
|
3534
|
+
child.stdout.on("data", (chunk) => logStream.write(chunk));
|
|
3535
|
+
}
|
|
3536
|
+
if (child.stderr && typeof child.stderr.pipe === "function" && logStream) {
|
|
3537
|
+
child.stderr.pipe(logStream, { end: false });
|
|
3538
|
+
} else if (child.stderr && typeof child.stderr.on === "function" && logStream) {
|
|
3539
|
+
child.stderr.on("data", (chunk) => logStream.write(chunk));
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
child.on("error", (err) => {
|
|
3543
|
+
logError(`Failed to spawn restart CLI for ${normalizedTargetTaskId}: ${err.message}`);
|
|
3544
|
+
if (logStream) {
|
|
3545
|
+
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
3546
|
+
logStream.write(`[daemon ${ts}] spawn error: ${err.message}\n`);
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3549
|
+
|
|
3550
|
+
child.on("exit", (code, signal) => {
|
|
3551
|
+
const active = activeTaskProcesses.get(normalizedTargetTaskId);
|
|
3552
|
+
if (active?.stopForceKillTimer) {
|
|
3553
|
+
clearTimeout(active.stopForceKillTimer);
|
|
3554
|
+
}
|
|
3555
|
+
activeTaskProcesses.delete(normalizedTargetTaskId);
|
|
3556
|
+
const suppressExitStatusReport = suppressedExitStatusReports.has(normalizedTargetTaskId);
|
|
3557
|
+
suppressedExitStatusReports.delete(normalizedTargetTaskId);
|
|
3558
|
+
if (logStream) {
|
|
3559
|
+
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
3560
|
+
if (signal) {
|
|
3561
|
+
logStream.write(`[daemon ${ts}] process killed by signal ${signal}\n`);
|
|
3562
|
+
} else {
|
|
3563
|
+
logStream.write(`[daemon ${ts}] process exited with code ${code}\n`);
|
|
3564
|
+
}
|
|
3565
|
+
logStream.end();
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
const isKilledBySignal = Boolean(signal);
|
|
3569
|
+
const isKilledByExitCode = code === 130 || code === 143;
|
|
3570
|
+
const isKilled = isKilledBySignal || isKilledByExitCode;
|
|
3571
|
+
const status = isKilled ? "KILLED" : code === 0 ? "COMPLETED" : "KILLED";
|
|
3572
|
+
const summary = isKilled
|
|
3573
|
+
? (signal ? `killed by signal ${signal}` : `terminated (exit code ${code})`)
|
|
3574
|
+
: code === 0
|
|
3575
|
+
? "completed"
|
|
3576
|
+
: `exited with code ${code}`;
|
|
3577
|
+
|
|
3578
|
+
if (!suppressExitStatusReport) {
|
|
3579
|
+
client
|
|
3580
|
+
.sendJson({
|
|
3581
|
+
type: "task_status_update",
|
|
3582
|
+
payload: {
|
|
3583
|
+
task_id: normalizedTargetTaskId,
|
|
3584
|
+
project_id: normalizedProjectId,
|
|
3585
|
+
status,
|
|
3586
|
+
summary,
|
|
3587
|
+
},
|
|
3588
|
+
})
|
|
3589
|
+
.catch((err) => {
|
|
3590
|
+
logError(`Failed to report task status (${status}) for ${normalizedTargetTaskId}: ${err?.message || err}`);
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3088
3596
|
let closePromise = null;
|
|
3089
3597
|
async function shutdownDaemon(reason = "manual close") {
|
|
3090
3598
|
if (closePromise) {
|
package/src/native-deps.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { spawn as spawnProcess } from "node:child_process";
|
|
@@ -166,6 +167,38 @@ export async function resolveGlobalPackageDirectory({
|
|
|
166
167
|
return path.join(normalizeRoot(rawRoot), packageName);
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
export function ensureNodePtySpawnHelperExecutableForPackageDirectory({
|
|
171
|
+
packageDirectory,
|
|
172
|
+
platform = process.platform,
|
|
173
|
+
arch = process.arch,
|
|
174
|
+
existsSync = fs.existsSync,
|
|
175
|
+
statSync = fs.statSync,
|
|
176
|
+
chmodSync = fs.chmodSync,
|
|
177
|
+
} = {}) {
|
|
178
|
+
if (!packageDirectory || platform === "win32") {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const helperCandidates = [
|
|
183
|
+
path.join(packageDirectory, "node_modules", "node-pty", "build", "Release", "spawn-helper"),
|
|
184
|
+
path.join(packageDirectory, "node_modules", "node-pty", "build", "Debug", "spawn-helper"),
|
|
185
|
+
path.join(packageDirectory, "node_modules", "node-pty", "prebuilds", `${platform}-${arch}`, "spawn-helper"),
|
|
186
|
+
];
|
|
187
|
+
const helperPath = helperCandidates.find((candidate) => existsSync(candidate));
|
|
188
|
+
if (!helperPath) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const currentMode = statSync(helperPath).mode & 0o777;
|
|
193
|
+
if ((currentMode & 0o111) !== 0) {
|
|
194
|
+
return { helperPath, updated: false };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const nextMode = currentMode | 0o111;
|
|
198
|
+
chmodSync(helperPath, nextMode);
|
|
199
|
+
return { helperPath, updated: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
169
202
|
export function buildNodePtyVerificationScript() {
|
|
170
203
|
return String.raw`
|
|
171
204
|
const fs = require('node:fs');
|
|
@@ -252,6 +285,7 @@ export async function verifyNodePtyForPackageDirectory({
|
|
|
252
285
|
if (!packageDirectory) {
|
|
253
286
|
throw new Error("packageDirectory is required");
|
|
254
287
|
}
|
|
288
|
+
ensureNodePtySpawnHelperExecutableForPackageDirectory({ packageDirectory });
|
|
255
289
|
const result = await runCommand(nodeExecutable, ["-e", buildNodePtyVerificationScript(), packageDirectory], {
|
|
256
290
|
timeoutMs: 15_000,
|
|
257
291
|
});
|