@love-moon/conductor-cli 0.2.5 → 0.2.7
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-fire.js +58 -204
- package/package.json +3 -4
- package/src/daemon.js +90 -22
package/bin/conductor-fire.js
CHANGED
|
@@ -4,24 +4,21 @@
|
|
|
4
4
|
* conductor-fire - Conductor-aware AI coding agent runner.
|
|
5
5
|
*
|
|
6
6
|
* Supports configurable backends via allow_cli_list in config file.
|
|
7
|
-
* This CLI bridges various AI coding agents with Conductor via
|
|
7
|
+
* This CLI bridges various AI coding agents with Conductor via the Conductor SDK.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
|
-
import { createRequire } from "node:module";
|
|
12
11
|
import os from "node:os";
|
|
13
12
|
import path from "node:path";
|
|
14
13
|
import process from "node:process";
|
|
15
14
|
import { setTimeout as delay } from "node:timers/promises";
|
|
16
15
|
import { fileURLToPath } from "node:url";
|
|
17
16
|
|
|
18
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
19
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
20
17
|
import yargs from "yargs/yargs";
|
|
21
18
|
import { hideBin } from "yargs/helpers";
|
|
22
19
|
import yaml from "js-yaml";
|
|
23
|
-
import { TuiDriver, claudeCodeProfile, codexProfile } from "@love-moon/tui-driver";
|
|
24
|
-
import { loadConfig } from "@love-moon/conductor-sdk";
|
|
20
|
+
import { TuiDriver, claudeCodeProfile, codexProfile, copilotProfile } from "@love-moon/tui-driver";
|
|
21
|
+
import { ConductorClient, loadConfig } from "@love-moon/conductor-sdk";
|
|
25
22
|
import {
|
|
26
23
|
loadHistoryFromSpec,
|
|
27
24
|
parseFromSpec,
|
|
@@ -31,21 +28,8 @@ import {
|
|
|
31
28
|
|
|
32
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
30
|
const __dirname = path.dirname(__filename);
|
|
34
|
-
const require = createRequire(import.meta.url);
|
|
35
31
|
const PKG_ROOT = path.join(__dirname, "..");
|
|
36
32
|
const CLI_PROJECT_PATH = process.cwd();
|
|
37
|
-
const REPO_ROOT_FALLBACK = path.resolve(__dirname, "..", "..");
|
|
38
|
-
const SDK_FALLBACK_PATH = path.join(REPO_ROOT_FALLBACK, "modules", "sdk");
|
|
39
|
-
const SDK_ROOT =
|
|
40
|
-
process.env.CONDUCTOR_SDK_PATH ||
|
|
41
|
-
(() => {
|
|
42
|
-
try {
|
|
43
|
-
return path.dirname(require.resolve("@love-moon/conductor-sdk/package.json"));
|
|
44
|
-
} catch {
|
|
45
|
-
return SDK_FALLBACK_PATH;
|
|
46
|
-
}
|
|
47
|
-
})();
|
|
48
|
-
const MCP_SERVER_SCRIPT = path.join(SDK_ROOT, "dist", "bin", "mcp-server.js");
|
|
49
33
|
|
|
50
34
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
51
35
|
const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1] || "conductor-fire")).replace(
|
|
@@ -122,12 +106,6 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
|
122
106
|
10,
|
|
123
107
|
);
|
|
124
108
|
|
|
125
|
-
const MCP_SERVER_LAUNCH = {
|
|
126
|
-
command: "node",
|
|
127
|
-
args: [MCP_SERVER_SCRIPT],
|
|
128
|
-
cwd: SDK_ROOT,
|
|
129
|
-
};
|
|
130
|
-
|
|
131
109
|
async function main() {
|
|
132
110
|
const cliArgs = parseCliArgs();
|
|
133
111
|
|
|
@@ -145,7 +123,7 @@ async function main() {
|
|
|
145
123
|
|
|
146
124
|
if (cliArgs.listBackends) {
|
|
147
125
|
if (supportedBackends.length === 0) {
|
|
148
|
-
process.stdout.write(`No backends configured.\n\nAdd allow_cli_list to your config file (~/.conductor/config.yaml):\n allow_cli_list:\n codex: codex --dangerously-bypass-approvals-and-sandbox\n claude: claude --dangerously-skip-permissions\n kimi: kimi\n`);
|
|
126
|
+
process.stdout.write(`No backends configured.\n\nAdd allow_cli_list to your config file (~/.conductor/config.yaml):\n allow_cli_list:\n codex: codex --dangerously-bypass-approvals-and-sandbox\n claude: claude --dangerously-skip-permissions\n copilot: copilot --allow-all-paths --allow-all-tools\n kimi: kimi\n`);
|
|
149
127
|
} else {
|
|
150
128
|
process.stdout.write(`Supported backends (from config):\n`);
|
|
151
129
|
for (const [name, command] of Object.entries(allowCliList)) {
|
|
@@ -223,10 +201,9 @@ async function main() {
|
|
|
223
201
|
}
|
|
224
202
|
|
|
225
203
|
const conductor = await ConductorClient.connect({
|
|
226
|
-
launcher: MCP_SERVER_LAUNCH,
|
|
227
|
-
workingDirectory: CLI_PROJECT_PATH,
|
|
228
204
|
projectPath: CLI_PROJECT_PATH,
|
|
229
205
|
extraEnv: env,
|
|
206
|
+
configFile: cliArgs.configFile,
|
|
230
207
|
});
|
|
231
208
|
|
|
232
209
|
const taskContext = await ensureTaskContext(conductor, {
|
|
@@ -268,19 +245,44 @@ async function main() {
|
|
|
268
245
|
process.on("SIGINT", onSigint);
|
|
269
246
|
process.on("SIGTERM", onSigterm);
|
|
270
247
|
|
|
248
|
+
if (!launchedByDaemon) {
|
|
249
|
+
try {
|
|
250
|
+
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
251
|
+
status: "RUNNING",
|
|
252
|
+
});
|
|
253
|
+
} catch (error) {
|
|
254
|
+
log(`Failed to report task status (RUNNING): ${error?.message || error}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let runnerError = null;
|
|
271
259
|
try {
|
|
272
260
|
await runner.start(signals.signal);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
runnerError = error;
|
|
263
|
+
throw error;
|
|
273
264
|
} finally {
|
|
274
265
|
process.off("SIGINT", onSigint);
|
|
275
266
|
process.off("SIGTERM", onSigterm);
|
|
276
|
-
if (
|
|
267
|
+
if (!launchedByDaemon) {
|
|
268
|
+
const finalStatus = shutdownSignal
|
|
269
|
+
? {
|
|
270
|
+
status: "KILLED",
|
|
271
|
+
summary: `terminated by ${shutdownSignal}`,
|
|
272
|
+
}
|
|
273
|
+
: runnerError
|
|
274
|
+
? {
|
|
275
|
+
status: "KILLED",
|
|
276
|
+
summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
|
|
277
|
+
}
|
|
278
|
+
: {
|
|
279
|
+
status: "COMPLETED",
|
|
280
|
+
summary: "conductor fire exited",
|
|
281
|
+
};
|
|
277
282
|
try {
|
|
278
|
-
await conductor.sendTaskStatus(taskContext.taskId,
|
|
279
|
-
status: "KILLED",
|
|
280
|
-
summary: `terminated by ${shutdownSignal}`,
|
|
281
|
-
});
|
|
283
|
+
await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
282
284
|
} catch (error) {
|
|
283
|
-
log(`Failed to report task status (
|
|
285
|
+
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
284
286
|
}
|
|
285
287
|
}
|
|
286
288
|
if (typeof backendSession.close === "function") {
|
|
@@ -403,10 +405,12 @@ Config file format (~/.conductor/config.yaml):
|
|
|
403
405
|
allow_cli_list:
|
|
404
406
|
codex: codex --dangerously-bypass-approvals-and-sandbox
|
|
405
407
|
claude: claude --dangerously-skip-permissions
|
|
408
|
+
copilot: copilot --allow-all-paths --allow-all-tools
|
|
406
409
|
|
|
407
410
|
Examples:
|
|
408
411
|
${CLI_NAME} -- "fix the bug" # Use default backend
|
|
409
412
|
${CLI_NAME} --backend claude -- "fix the bug" # Use Claude CLI backend
|
|
413
|
+
${CLI_NAME} --backend copilot -- "fix the bug" # Use GitHub Copilot CLI backend
|
|
410
414
|
${CLI_NAME} --list-backends # Show configured backends
|
|
411
415
|
${CLI_NAME} --config-file ~/.conductor/config.yaml -- "fix the bug"
|
|
412
416
|
|
|
@@ -535,7 +539,19 @@ async function resolveProjectId(conductor, explicit) {
|
|
|
535
539
|
try {
|
|
536
540
|
const record = await conductor.getLocalProjectRecord();
|
|
537
541
|
if (record?.project_id) {
|
|
538
|
-
|
|
542
|
+
try {
|
|
543
|
+
const listing = await conductor.listProjects();
|
|
544
|
+
const exists = Array.isArray(listing?.projects)
|
|
545
|
+
? listing.projects.some((project) => String(project?.id || "") === String(record.project_id))
|
|
546
|
+
: false;
|
|
547
|
+
if (exists) {
|
|
548
|
+
return record.project_id;
|
|
549
|
+
}
|
|
550
|
+
log(`Local session project ${record.project_id} no longer exists; falling back to server project list`);
|
|
551
|
+
} catch (verifyError) {
|
|
552
|
+
log(`Unable to verify local project record; using cached project id: ${verifyError.message}`);
|
|
553
|
+
return record.project_id;
|
|
554
|
+
}
|
|
539
555
|
}
|
|
540
556
|
} catch (error) {
|
|
541
557
|
log(`Unable to resolve project via local session: ${error.message}`);
|
|
@@ -577,6 +593,7 @@ const BACKEND_PROFILE_MAP = {
|
|
|
577
593
|
codex: "codex",
|
|
578
594
|
claude: "claude-code",
|
|
579
595
|
"claude-code": "claude-code",
|
|
596
|
+
copilot: "copilot",
|
|
580
597
|
};
|
|
581
598
|
|
|
582
599
|
function profileNameForBackend(backend) {
|
|
@@ -650,7 +667,12 @@ class TuiDriverSession {
|
|
|
650
667
|
throw new Error(`Backend "${backend}" is not supported by tui-driver`);
|
|
651
668
|
}
|
|
652
669
|
|
|
653
|
-
const
|
|
670
|
+
const profileMap = {
|
|
671
|
+
codex: codexProfile,
|
|
672
|
+
"claude-code": claudeCodeProfile,
|
|
673
|
+
copilot: copilotProfile,
|
|
674
|
+
};
|
|
675
|
+
const baseProfile = profileMap[profileName];
|
|
654
676
|
const envConfig = loadEnvConfig(options.configFile);
|
|
655
677
|
const proxyEnv = proxyToEnv(envConfig);
|
|
656
678
|
const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
@@ -965,174 +987,6 @@ class TuiDriverSession {
|
|
|
965
987
|
}
|
|
966
988
|
}
|
|
967
989
|
|
|
968
|
-
class ConductorClient {
|
|
969
|
-
constructor(client, transport, options = {}) {
|
|
970
|
-
this.client = client;
|
|
971
|
-
this.transport = transport;
|
|
972
|
-
this.closed = false;
|
|
973
|
-
this.projectPath = options.projectPath || process.cwd();
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
static async connect({ launcher, workingDirectory, extraEnv, projectPath }) {
|
|
977
|
-
if (!fs.existsSync(launcher.args[0])) {
|
|
978
|
-
throw new Error(`Conductor MCP server not found at ${launcher.args[0]}`);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
const transport = new StdioClientTransport({
|
|
982
|
-
command: launcher.command,
|
|
983
|
-
args: launcher.args,
|
|
984
|
-
env: extraEnv,
|
|
985
|
-
cwd: launcher.cwd || workingDirectory,
|
|
986
|
-
stderr: "pipe",
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
const client = new Client({
|
|
990
|
-
name: CLI_NAME,
|
|
991
|
-
version: pkgJson.version,
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
client.onerror = (err) => {
|
|
995
|
-
log(`MCP client error: ${err.message}`);
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
const stderr = transport.stderr;
|
|
999
|
-
if (stderr) {
|
|
1000
|
-
stderr.setEncoding("utf-8");
|
|
1001
|
-
stderr.on("data", (chunk) => {
|
|
1002
|
-
const text = chunk.toString();
|
|
1003
|
-
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
1004
|
-
text
|
|
1005
|
-
.split(/\r?\n/)
|
|
1006
|
-
.filter(Boolean)
|
|
1007
|
-
.forEach((line) => process.stderr.write(`[conductor ${ts}] ${line}\n`));
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
await client.connect(transport);
|
|
1012
|
-
return new ConductorClient(client, transport, { projectPath });
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
async close() {
|
|
1016
|
-
if (this.closed) {
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
this.closed = true;
|
|
1020
|
-
await this.client.close();
|
|
1021
|
-
await this.transport.close();
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
injectProjectPath(payload = {}) {
|
|
1025
|
-
if (!this.projectPath || payload.project_path) {
|
|
1026
|
-
return payload;
|
|
1027
|
-
}
|
|
1028
|
-
return { ...payload, project_path: this.projectPath };
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async createTaskSession(payload) {
|
|
1032
|
-
const args = payload ? { ...payload } : {};
|
|
1033
|
-
return this.callTool("create_task_session", this.injectProjectPath(args));
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
async sendMessage(taskId, content, metadata) {
|
|
1037
|
-
return this.callTool("send_message", {
|
|
1038
|
-
task_id: taskId,
|
|
1039
|
-
content,
|
|
1040
|
-
metadata,
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
async sendTaskStatus(taskId, payload) {
|
|
1045
|
-
return this.callTool("send_task_status", {
|
|
1046
|
-
task_id: taskId,
|
|
1047
|
-
...(payload || {}),
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
async sendRuntimeStatus(taskId, payload) {
|
|
1052
|
-
return this.callTool("send_runtime_status", {
|
|
1053
|
-
task_id: taskId,
|
|
1054
|
-
...(payload || {}),
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
async receiveMessages(taskId, limit = 20) {
|
|
1059
|
-
return this.callTool("receive_messages", {
|
|
1060
|
-
task_id: taskId,
|
|
1061
|
-
limit,
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
async ackMessages(taskId, ackToken) {
|
|
1066
|
-
if (!ackToken) {
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
await this.callTool("ack_messages", { task_id: taskId, ack_token: ackToken });
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
async listProjects() {
|
|
1073
|
-
return this.callTool("list_projects", {});
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
async createProject(name, description, metadata) {
|
|
1077
|
-
return this.callTool("create_project", {
|
|
1078
|
-
name,
|
|
1079
|
-
description,
|
|
1080
|
-
metadata,
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
async getLocalProjectRecord() {
|
|
1085
|
-
return this.callTool("get_local_project_id", this.injectProjectPath({}));
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
async matchProjectByPath() {
|
|
1089
|
-
return this.callTool("match_project_by_path", this.injectProjectPath({}));
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
async bindProjectPath(projectId) {
|
|
1093
|
-
return this.callTool("bind_project_path", this.injectProjectPath({ project_id: projectId }));
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
async callTool(name, args) {
|
|
1097
|
-
const result = await this.client.callTool({
|
|
1098
|
-
name,
|
|
1099
|
-
arguments: args || {},
|
|
1100
|
-
});
|
|
1101
|
-
if (result?.isError) {
|
|
1102
|
-
const message = extractFirstText(result?.content) || `tool ${name} failed`;
|
|
1103
|
-
throw new Error(message);
|
|
1104
|
-
}
|
|
1105
|
-
const structured = result?.structuredContent;
|
|
1106
|
-
if (structured && Object.keys(structured).length > 0) {
|
|
1107
|
-
return structured;
|
|
1108
|
-
}
|
|
1109
|
-
const textPayload = extractFirstText(result?.content);
|
|
1110
|
-
if (!textPayload) {
|
|
1111
|
-
return undefined;
|
|
1112
|
-
}
|
|
1113
|
-
try {
|
|
1114
|
-
return JSON.parse(textPayload);
|
|
1115
|
-
} catch {
|
|
1116
|
-
return textPayload;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
function extractFirstText(blocks) {
|
|
1122
|
-
if (!Array.isArray(blocks)) {
|
|
1123
|
-
return "";
|
|
1124
|
-
}
|
|
1125
|
-
for (const block of blocks) {
|
|
1126
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
1127
|
-
const trimmed = block.text.trim();
|
|
1128
|
-
if (trimmed) {
|
|
1129
|
-
return trimmed;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
return "";
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
990
|
class BridgeRunner {
|
|
1137
991
|
constructor({
|
|
1138
992
|
backendSession,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"conductor": "bin/conductor.js"
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@love-moon/tui-driver": "0.2.
|
|
20
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
21
|
-
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
19
|
+
"@love-moon/tui-driver": "0.2.7",
|
|
20
|
+
"@love-moon/conductor-sdk": "0.2.7",
|
|
22
21
|
"dotenv": "^16.4.5",
|
|
23
22
|
"enquirer": "^2.4.1",
|
|
24
23
|
"js-yaml": "^4.1.1",
|
package/src/daemon.js
CHANGED
|
@@ -59,6 +59,8 @@ function getAllowCliList(userConfig) {
|
|
|
59
59
|
export function startDaemon(config = {}, deps = {}) {
|
|
60
60
|
const exitFn = deps.exit || process.exit;
|
|
61
61
|
const killFn = deps.kill || process.kill;
|
|
62
|
+
let requestShutdown = async () => {};
|
|
63
|
+
let shutdownSignalHandled = false;
|
|
62
64
|
|
|
63
65
|
const exitAndReturn = (code) => {
|
|
64
66
|
exitFn(code);
|
|
@@ -214,18 +216,31 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
214
216
|
};
|
|
215
217
|
|
|
216
218
|
process.on("exit", cleanupLock);
|
|
219
|
+
const handleSignal = (signal) => {
|
|
220
|
+
if (shutdownSignalHandled) return;
|
|
221
|
+
shutdownSignalHandled = true;
|
|
222
|
+
void (async () => {
|
|
223
|
+
try {
|
|
224
|
+
log(`Received ${signal}, shutting down...`);
|
|
225
|
+
await requestShutdown(`signal ${signal}`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logError(`Graceful shutdown failed on ${signal}: ${err?.message || err}`);
|
|
228
|
+
} finally {
|
|
229
|
+
cleanupLock();
|
|
230
|
+
exitFn(0);
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
};
|
|
217
234
|
process.on("SIGINT", () => {
|
|
218
|
-
|
|
219
|
-
process.exit();
|
|
235
|
+
handleSignal("SIGINT");
|
|
220
236
|
});
|
|
221
237
|
process.on("SIGTERM", () => {
|
|
222
|
-
|
|
223
|
-
process.exit();
|
|
238
|
+
handleSignal("SIGTERM");
|
|
224
239
|
});
|
|
225
240
|
process.on("uncaughtException", (err) => {
|
|
226
241
|
logError(`Uncaught exception: ${err}`);
|
|
227
242
|
cleanupLock();
|
|
228
|
-
|
|
243
|
+
exitFn(1);
|
|
229
244
|
});
|
|
230
245
|
|
|
231
246
|
if (config.CLEAN_ALL) {
|
|
@@ -256,6 +271,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
256
271
|
let disconnectedSinceLastConnectedLog = false;
|
|
257
272
|
let didRecoverStaleTasks = false;
|
|
258
273
|
const activeTaskProcesses = new Map();
|
|
274
|
+
const suppressedExitStatusReports = new Set();
|
|
259
275
|
const client = createWebSocketClient(sdkConfig, {
|
|
260
276
|
extraHeaders: {
|
|
261
277
|
"x-conductor-host": AGENT_NAME,
|
|
@@ -652,6 +668,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
652
668
|
clearTimeout(active.stopForceKillTimer);
|
|
653
669
|
}
|
|
654
670
|
activeTaskProcesses.delete(taskId);
|
|
671
|
+
const suppressExitStatusReport = suppressedExitStatusReports.has(taskId);
|
|
672
|
+
suppressedExitStatusReports.delete(taskId);
|
|
655
673
|
if (logStream) {
|
|
656
674
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
657
675
|
if (signal) {
|
|
@@ -679,25 +697,59 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
679
697
|
? "completed"
|
|
680
698
|
: `exited with code ${code}`;
|
|
681
699
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
if (!suppressExitStatusReport) {
|
|
701
|
+
client
|
|
702
|
+
.sendJson({
|
|
703
|
+
type: "task_status_update",
|
|
704
|
+
payload: {
|
|
705
|
+
task_id: taskId,
|
|
706
|
+
project_id: projectId,
|
|
707
|
+
status,
|
|
708
|
+
summary,
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
.catch((err) => {
|
|
712
|
+
logError(`Failed to report task status (${status}) for ${taskId}: ${err?.message || err}`);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
695
715
|
});
|
|
696
716
|
}
|
|
697
717
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
718
|
+
let closePromise = null;
|
|
719
|
+
async function shutdownDaemon(reason = "manual close") {
|
|
720
|
+
if (closePromise) {
|
|
721
|
+
return closePromise;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
closePromise = (async () => {
|
|
725
|
+
const activeEntries = [...activeTaskProcesses.entries()];
|
|
726
|
+
if (activeEntries.length > 0) {
|
|
727
|
+
log(`Shutdown requested (${reason}); stopping ${activeEntries.length} active task(s)`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
await Promise.allSettled(
|
|
731
|
+
activeEntries.map(async ([taskId, record]) => {
|
|
732
|
+
suppressedExitStatusReports.add(taskId);
|
|
733
|
+
try {
|
|
734
|
+
await client.sendJson({
|
|
735
|
+
type: "task_status_update",
|
|
736
|
+
payload: {
|
|
737
|
+
task_id: taskId,
|
|
738
|
+
project_id: record.projectId,
|
|
739
|
+
status: "KILLED",
|
|
740
|
+
summary: `daemon shutdown (${reason})`,
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
} catch (err) {
|
|
744
|
+
logError(`Failed to report shutdown status (KILLED) for ${taskId}: ${err?.message || err}`);
|
|
745
|
+
}
|
|
746
|
+
}),
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
for (const [taskId, record] of activeEntries) {
|
|
750
|
+
if (record?.stopForceKillTimer) {
|
|
751
|
+
clearTimeout(record.stopForceKillTimer);
|
|
752
|
+
}
|
|
701
753
|
try {
|
|
702
754
|
if (typeof record.child?.kill === "function") {
|
|
703
755
|
record.child.kill("SIGTERM");
|
|
@@ -706,8 +758,24 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
706
758
|
logError(`Failed to stop task ${taskId} on daemon close: ${error?.message || error}`);
|
|
707
759
|
}
|
|
708
760
|
}
|
|
761
|
+
|
|
709
762
|
activeTaskProcesses.clear();
|
|
710
|
-
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
await Promise.resolve(client.disconnect());
|
|
766
|
+
} catch (error) {
|
|
767
|
+
logError(`Failed to disconnect client on daemon close: ${error?.message || error}`);
|
|
768
|
+
}
|
|
769
|
+
})();
|
|
770
|
+
|
|
771
|
+
return closePromise;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
requestShutdown = shutdownDaemon;
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
close: () => {
|
|
778
|
+
void shutdownDaemon();
|
|
711
779
|
},
|
|
712
780
|
};
|
|
713
781
|
}
|