@love-moon/conductor-cli 0.2.34 → 0.2.36
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 +163 -7
- package/package.json +6 -4
- package/src/ai-manager-handlers.js +158 -0
- package/src/daemon.js +329 -110
- package/src/runtime-backends.js +34 -21
package/bin/conductor-fire.js
CHANGED
|
@@ -82,24 +82,149 @@ export function shouldRunReconnectRecovery({
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Load allow_cli_list from config file (no defaults - must be configured)
|
|
85
|
-
|
|
85
|
+
function loadFireConfigYaml(configFilePath) {
|
|
86
86
|
const home = os.homedir();
|
|
87
87
|
const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
|
|
88
|
-
let parsed = null;
|
|
89
88
|
try {
|
|
90
89
|
if (fs.existsSync(configPath)) {
|
|
91
90
|
const content = fs.readFileSync(configPath, "utf8");
|
|
92
|
-
parsed = yaml.load(content);
|
|
91
|
+
const parsed = yaml.load(content);
|
|
92
|
+
if (parsed && typeof parsed === "object") {
|
|
93
|
+
return { configPath, parsed };
|
|
94
|
+
}
|
|
93
95
|
}
|
|
94
96
|
} catch (error) {
|
|
95
97
|
// ignore error
|
|
96
98
|
}
|
|
97
|
-
|
|
99
|
+
return { configPath, parsed: null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Load allow_cli_list from config file (no defaults - must be configured)
|
|
103
|
+
async function loadAllowCliList(configFilePath) {
|
|
104
|
+
const { configPath, parsed } = loadFireConfigYaml(configFilePath);
|
|
105
|
+
if (parsed && parsed.allow_cli_list) {
|
|
98
106
|
return await filterRuntimeSupportedAllowCliList(parsed.allow_cli_list, { configFilePath: configPath });
|
|
99
107
|
}
|
|
100
108
|
return {};
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
function loadPrePromptMap(configFilePath) {
|
|
112
|
+
const { parsed } = loadFireConfigYaml(configFilePath);
|
|
113
|
+
if (parsed && parsed.pre_prompt && typeof parsed.pre_prompt === "object") {
|
|
114
|
+
return parsed.pre_prompt;
|
|
115
|
+
}
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function expandEnvVars(text, env = process.env) {
|
|
120
|
+
if (typeof text !== "string" || !text) {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, bare) => {
|
|
124
|
+
const key = braced || bare;
|
|
125
|
+
return Object.prototype.hasOwnProperty.call(env, key) && env[key] != null ? String(env[key]) : match;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resolveConfiguredPrePrompt({ configFilePath, backend, sessionBackend, env = process.env } = {}) {
|
|
130
|
+
const prePromptMap = loadPrePromptMap(configFilePath);
|
|
131
|
+
const candidates = [backend, sessionBackend]
|
|
132
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
if (typeof prePromptMap[candidate] === "string") {
|
|
136
|
+
const resolved = expandEnvVars(prePromptMap[candidate], env);
|
|
137
|
+
return resolved || undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseCommandParts(commandLine) {
|
|
144
|
+
const input = String(commandLine || "").trim();
|
|
145
|
+
if (!input) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const parts = [];
|
|
150
|
+
let current = "";
|
|
151
|
+
let quote = "";
|
|
152
|
+
let escaping = false;
|
|
153
|
+
let tokenStarted = false;
|
|
154
|
+
|
|
155
|
+
for (const char of input) {
|
|
156
|
+
if (escaping) {
|
|
157
|
+
current += char;
|
|
158
|
+
tokenStarted = true;
|
|
159
|
+
escaping = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (char === "\\") {
|
|
164
|
+
escaping = true;
|
|
165
|
+
tokenStarted = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (quote) {
|
|
170
|
+
if (char === quote) {
|
|
171
|
+
quote = "";
|
|
172
|
+
} else {
|
|
173
|
+
current += char;
|
|
174
|
+
}
|
|
175
|
+
tokenStarted = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (char === "'" || char === "\"") {
|
|
180
|
+
quote = char;
|
|
181
|
+
tokenStarted = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (/\s/.test(char)) {
|
|
186
|
+
if (tokenStarted) {
|
|
187
|
+
parts.push(current);
|
|
188
|
+
current = "";
|
|
189
|
+
tokenStarted = false;
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
current += char;
|
|
195
|
+
tokenStarted = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (tokenStarted) {
|
|
199
|
+
parts.push(current);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return parts;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function extractModelOptionFromCommandLine(commandLine) {
|
|
206
|
+
const parts = parseCommandParts(commandLine);
|
|
207
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
208
|
+
const token = String(parts[index] || "").trim();
|
|
209
|
+
if (!token) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (token === "--model") {
|
|
213
|
+
const next = String(parts[index + 1] || "").trim();
|
|
214
|
+
return next || "";
|
|
215
|
+
}
|
|
216
|
+
if (token.startsWith("--model=")) {
|
|
217
|
+
return token.slice("--model=".length).trim();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function extractAiSessionOptionsFromCommandLine(commandLine) {
|
|
224
|
+
const model = extractModelOptionFromCommandLine(commandLine);
|
|
225
|
+
return model ? { model } : {};
|
|
226
|
+
}
|
|
227
|
+
|
|
103
228
|
export function resolveAiSessionCommandLine(backend, allowCliList, env = process.env, sessionBackend = backend) {
|
|
104
229
|
const normalizedBackend = normalizeRuntimeBackendName(backend);
|
|
105
230
|
const normalizedSessionBackend = normalizeRuntimeBackendName(sessionBackend);
|
|
@@ -142,6 +267,12 @@ export function resolveAiSessionCommandLine(backend, allowCliList, env = process
|
|
|
142
267
|
return resolvedCommand;
|
|
143
268
|
}
|
|
144
269
|
|
|
270
|
+
export function resolveAiSessionOptions(backend, allowCliList, env = process.env, sessionBackend = backend) {
|
|
271
|
+
return extractAiSessionOptionsFromCommandLine(
|
|
272
|
+
resolveAiSessionCommandLine(backend, allowCliList, env, sessionBackend),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
145
276
|
const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
146
277
|
process.env.CONDUCTOR_CLI_POLL_INTERVAL_MS || process.env.CCODEX_POLL_INTERVAL_MS || "2000",
|
|
147
278
|
10,
|
|
@@ -595,6 +726,7 @@ async function main() {
|
|
|
595
726
|
envTaskTitle: process.env.CONDUCTOR_TASK_TITLE,
|
|
596
727
|
runtimeProjectPath,
|
|
597
728
|
});
|
|
729
|
+
const resolvedDaemonName = resolveDaemonHost(configuredDaemonName);
|
|
598
730
|
|
|
599
731
|
conductor = await ConductorClient.connect({
|
|
600
732
|
projectPath: runtimeProjectPath,
|
|
@@ -623,7 +755,7 @@ async function main() {
|
|
|
623
755
|
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
624
756
|
requestedTitle: requestedTaskTitle,
|
|
625
757
|
backend: cliArgs.backend,
|
|
626
|
-
daemonName:
|
|
758
|
+
daemonName: resolvedDaemonName,
|
|
627
759
|
projectPath: runtimeProjectPath,
|
|
628
760
|
});
|
|
629
761
|
injectResolvedTaskId(taskContext.taskId);
|
|
@@ -647,6 +779,7 @@ async function main() {
|
|
|
647
779
|
project_id: process.env.CONDUCTOR_PROJECT_ID,
|
|
648
780
|
project_path: runtimeProjectPath,
|
|
649
781
|
backend_type: cliArgs.backend,
|
|
782
|
+
daemon_name: resolvedDaemonName,
|
|
650
783
|
});
|
|
651
784
|
} catch {
|
|
652
785
|
// best effort only
|
|
@@ -661,14 +794,24 @@ async function main() {
|
|
|
661
794
|
process.env,
|
|
662
795
|
cliArgs.sessionBackend,
|
|
663
796
|
);
|
|
797
|
+
const resolvedPrePrompt = resolveConfiguredPrePrompt({
|
|
798
|
+
configFilePath: cliArgs.configFile,
|
|
799
|
+
backend: cliArgs.backend,
|
|
800
|
+
sessionBackend: cliArgs.sessionBackend,
|
|
801
|
+
env: process.env,
|
|
802
|
+
});
|
|
664
803
|
|
|
665
804
|
backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
|
|
666
805
|
initialImages: cliArgs.initialImages,
|
|
667
806
|
cwd: runtimeProjectPath,
|
|
668
807
|
resumeSessionId: resolvedResumeSessionId,
|
|
669
808
|
configFile: cliArgs.configFile,
|
|
809
|
+
...(cliArgs.sessionOptions || {}),
|
|
670
810
|
...(sessionCommandLine ? { commandLine: sessionCommandLine } : {}),
|
|
671
811
|
logger: { log },
|
|
812
|
+
...(resolvedPrePrompt ? { prePrompt: resolvedPrePrompt } : {}),
|
|
813
|
+
sessionStoreKey: taskContext.taskId ? `task-${taskContext.taskId}` : undefined,
|
|
814
|
+
resumePersistedSession: Boolean(!resolvedResumeSessionId && taskContext.taskId),
|
|
672
815
|
});
|
|
673
816
|
|
|
674
817
|
log(`Using backend: ${cliArgs.backend}`);
|
|
@@ -694,7 +837,7 @@ async function main() {
|
|
|
694
837
|
cliArgs: cliArgs.rawBackendArgs,
|
|
695
838
|
backendName: cliArgs.backend,
|
|
696
839
|
resumeSessionId: resolvedResumeSessionId,
|
|
697
|
-
daemonName:
|
|
840
|
+
daemonName: resolvedDaemonName,
|
|
698
841
|
});
|
|
699
842
|
reconnectRunner = runner;
|
|
700
843
|
if (pendingRemoteStopEvent) {
|
|
@@ -1038,6 +1181,12 @@ Environment:
|
|
|
1038
1181
|
const sessionBackend =
|
|
1039
1182
|
configuredBackend?.runtimeBackend ||
|
|
1040
1183
|
(backend ? await normalizeRuntimeBackendAlias(backend, { configFilePath: configFileFromArgs }) : "");
|
|
1184
|
+
const sessionOptions = resolveAiSessionOptions(
|
|
1185
|
+
backend,
|
|
1186
|
+
allowCliList,
|
|
1187
|
+
process.env,
|
|
1188
|
+
sessionBackend || backend,
|
|
1189
|
+
);
|
|
1041
1190
|
const shouldRequireBackend =
|
|
1042
1191
|
!Boolean(conductorArgs.listBackends) &&
|
|
1043
1192
|
!listBackendsWithoutSeparator &&
|
|
@@ -1086,6 +1235,7 @@ Environment:
|
|
|
1086
1235
|
hasExplicitTaskTitle: typeof conductorArgs.title === "string" && Boolean(conductorArgs.title.trim()),
|
|
1087
1236
|
configFile: conductorArgs.configFile,
|
|
1088
1237
|
sessionBackend,
|
|
1238
|
+
sessionOptions,
|
|
1089
1239
|
resumeSessionId,
|
|
1090
1240
|
showVersion: Boolean(conductorArgs.version) || versionWithoutSeparator,
|
|
1091
1241
|
listBackends: Boolean(conductorArgs.listBackends) || listBackendsWithoutSeparator,
|
|
@@ -1288,7 +1438,7 @@ export async function resolveProjectId(conductor, explicit, opts = {}) {
|
|
|
1288
1438
|
return resolveDefaultProjectId(conductor);
|
|
1289
1439
|
}
|
|
1290
1440
|
|
|
1291
|
-
function resolveDaemonHost(daemonName) {
|
|
1441
|
+
export function resolveDaemonHost(daemonName) {
|
|
1292
1442
|
if (typeof daemonName === "string" && daemonName.trim()) {
|
|
1293
1443
|
return daemonName.trim();
|
|
1294
1444
|
}
|
|
@@ -1708,6 +1858,7 @@ export class BridgeRunner {
|
|
|
1708
1858
|
session_file_path:
|
|
1709
1859
|
typeof sessionFilePath === "string" && sessionFilePath.trim() ? sessionFilePath.trim() : undefined,
|
|
1710
1860
|
backend_type: this.backendName,
|
|
1861
|
+
daemon_name: this.daemonName,
|
|
1711
1862
|
});
|
|
1712
1863
|
this.boundSessionId = normalizedSessionId;
|
|
1713
1864
|
return true;
|
|
@@ -2077,6 +2228,11 @@ export class BridgeRunner {
|
|
|
2077
2228
|
session_file_path: payload.session_file_path || runtimeContext?.session_file_path,
|
|
2078
2229
|
token_usage_percent: runtimeContext?.token_usage_percent,
|
|
2079
2230
|
context_usage_percent: runtimeContext?.context_usage_percent,
|
|
2231
|
+
tool_name: payload.tool_name ? String(payload.tool_name) : undefined,
|
|
2232
|
+
tool_id: payload.tool_id ? String(payload.tool_id) : undefined,
|
|
2233
|
+
item_id: payload.item_id ? String(payload.item_id) : undefined,
|
|
2234
|
+
turn_started_at: payload.turn_started_at ? String(payload.turn_started_at) : undefined,
|
|
2235
|
+
event_count: typeof payload.event_count === "number" ? payload.event_count : undefined,
|
|
2080
2236
|
};
|
|
2081
2237
|
}
|
|
2082
2238
|
|
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.36",
|
|
4
|
+
"gitCommitId": "54d9de4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@love-moon/ai-bridge": "0.1.4",
|
|
21
|
-
"@love-moon/ai-
|
|
22
|
-
"@love-moon/
|
|
21
|
+
"@love-moon/ai-manager": "0.2.36",
|
|
22
|
+
"@love-moon/ai-sdk": "0.2.36",
|
|
23
|
+
"@love-moon/conductor-sdk": "0.2.36",
|
|
23
24
|
"chrome-launcher": "^1.2.1",
|
|
24
25
|
"chrome-remote-interface": "^0.33.0",
|
|
25
26
|
"dotenv": "^16.4.5",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
],
|
|
40
41
|
"overrides": {
|
|
41
42
|
"@love-moon/ai-sdk": "file:../modules/ai-sdk",
|
|
43
|
+
"@love-moon/ai-manager": "file:../modules/ai-manager",
|
|
42
44
|
"@love-moon/conductor-sdk": "file:../modules/conductor-sdk"
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Daemon-side glue between the realtime WebSocket and the @love-moon/ai-manager module.
|
|
2
|
+
// The web backend sends `ai_manager_request`; we dispatch by `action`, run it, and
|
|
3
|
+
// reply with `ai_manager_response` carrying the same `request_id`.
|
|
4
|
+
|
|
5
|
+
import { AiManager } from "@love-moon/ai-manager";
|
|
6
|
+
|
|
7
|
+
const VALID_ACTIONS = new Set(["status", "quota", "list_accounts", "switch_account"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {string} [opts.configPath] Path to ~/.conductor/config.yaml. Defaults to ai-manager's default.
|
|
12
|
+
*/
|
|
13
|
+
export function createAiManagerHandlers(opts = {}) {
|
|
14
|
+
const manager = new AiManager(opts.configPath ? { configPath: opts.configPath } : undefined);
|
|
15
|
+
|
|
16
|
+
async function status() {
|
|
17
|
+
// Probe install first; only check network for tools that are actually
|
|
18
|
+
// present so we don't pay an outbound HTTP timeout for a CLI the user
|
|
19
|
+
// never installed.
|
|
20
|
+
const [install, current] = await Promise.all([
|
|
21
|
+
manager.checkInstallAll(),
|
|
22
|
+
manager.getCurrentCodexAccount().catch(() => null),
|
|
23
|
+
]);
|
|
24
|
+
const network = {};
|
|
25
|
+
const tools = ["codex", "claude", "kimi"];
|
|
26
|
+
await Promise.all(
|
|
27
|
+
tools.map(async (tool) => {
|
|
28
|
+
if (install[tool]?.installed) {
|
|
29
|
+
network[tool] = await manager.checkNetwork(tool);
|
|
30
|
+
} else {
|
|
31
|
+
network[tool] = {
|
|
32
|
+
reachable: false,
|
|
33
|
+
endpoint: "",
|
|
34
|
+
error: "not installed",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
return { install, network, currentCodexAccount: current };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function quota(args = {}) {
|
|
43
|
+
const tools = pickToolFilter(args);
|
|
44
|
+
const out = {};
|
|
45
|
+
if (tools.has("codex")) {
|
|
46
|
+
try {
|
|
47
|
+
out.codex = await manager.getCodexQuota({
|
|
48
|
+
forceRefresh: Boolean(args.forceRefresh),
|
|
49
|
+
});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
out.codex = { tool: "codex", error: errMsg(err), source: "unknown" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (tools.has("claude")) {
|
|
55
|
+
try {
|
|
56
|
+
out.claude = await manager.getClaudeQuota({
|
|
57
|
+
forceRefresh: Boolean(args.forceRefresh),
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
out.claude = { tool: "claude", error: errMsg(err), source: "unknown" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (tools.has("kimi")) {
|
|
64
|
+
try {
|
|
65
|
+
out.kimi = await manager.getKimiQuota({
|
|
66
|
+
forceRefresh: Boolean(args.forceRefresh),
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
out.kimi = { tool: "kimi", error: errMsg(err), source: "unknown" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function listAccounts() {
|
|
76
|
+
return { accounts: await manager.listCodexAccounts() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function switchAccount(args = {}) {
|
|
80
|
+
if (!args.name || typeof args.name !== "string") {
|
|
81
|
+
throw new Error("switch_account requires a `name` string");
|
|
82
|
+
}
|
|
83
|
+
return await manager.switchCodexAccount(args.name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run a single action and return a `result` object, never throwing.
|
|
88
|
+
* @param {{action:string,args?:object}} payload
|
|
89
|
+
*/
|
|
90
|
+
async function dispatch(payload) {
|
|
91
|
+
const action = payload?.action;
|
|
92
|
+
if (!VALID_ACTIONS.has(action)) {
|
|
93
|
+
return { error: `unknown action: ${action}` };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
switch (action) {
|
|
97
|
+
case "status":
|
|
98
|
+
return { result: await status() };
|
|
99
|
+
case "quota":
|
|
100
|
+
return { result: await quota(payload?.args ?? {}) };
|
|
101
|
+
case "list_accounts":
|
|
102
|
+
return { result: await listAccounts() };
|
|
103
|
+
case "switch_account":
|
|
104
|
+
return { result: await switchAccount(payload?.args ?? {}) };
|
|
105
|
+
default:
|
|
106
|
+
return { error: `unhandled action: ${action}` };
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return { error: errMsg(err) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { dispatch, manager };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wire a handler against an event payload from the web backend, sending the
|
|
118
|
+
* response back through `client.sendJson` once the action completes.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} client - conductor websocket client (must have sendJson)
|
|
121
|
+
* @param {ReturnType<typeof createAiManagerHandlers>} handlers
|
|
122
|
+
* @param {object} payload - event.payload with shape { request_id, action, args }
|
|
123
|
+
*/
|
|
124
|
+
export async function handleAiManagerRequest(client, handlers, payload) {
|
|
125
|
+
const requestId = payload?.request_id ? String(payload.request_id) : "";
|
|
126
|
+
const action = payload?.action ? String(payload.action) : "";
|
|
127
|
+
if (!requestId) {
|
|
128
|
+
// Without a request_id we cannot route the response anywhere; drop and log upstream.
|
|
129
|
+
return { error: "missing request_id" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const out = await handlers.dispatch({ action, args: payload?.args });
|
|
133
|
+
await client
|
|
134
|
+
.sendJson({
|
|
135
|
+
type: "ai_manager_response",
|
|
136
|
+
payload: {
|
|
137
|
+
request_id: requestId,
|
|
138
|
+
action,
|
|
139
|
+
result: out.result,
|
|
140
|
+
error: out.error,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {});
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pickToolFilter(args) {
|
|
148
|
+
const t = args?.tool;
|
|
149
|
+
if (t === "codex") return new Set(["codex"]);
|
|
150
|
+
if (t === "claude") return new Set(["claude"]);
|
|
151
|
+
if (t === "kimi") return new Set(["kimi"]);
|
|
152
|
+
return new Set(["codex", "claude", "kimi"]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function errMsg(err) {
|
|
156
|
+
if (err instanceof Error) return err.message;
|
|
157
|
+
return String(err);
|
|
158
|
+
}
|
package/src/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ProjectContext,
|
|
17
17
|
} from "@love-moon/conductor-sdk";
|
|
18
18
|
import { DaemonLogCollector } from "./log-collector.js";
|
|
19
|
+
import { createAiManagerHandlers, handleAiManagerRequest } from "./ai-manager-handlers.js";
|
|
19
20
|
import { resolveResumeContext } from "./fire/resume.js";
|
|
20
21
|
import {
|
|
21
22
|
filterRuntimeSupportedAllowCliList,
|
|
@@ -354,6 +355,35 @@ export function ensureNodePtySpawnHelperExecutable(deps = {}) {
|
|
|
354
355
|
return { helperPath, updated: true };
|
|
355
356
|
}
|
|
356
357
|
|
|
358
|
+
export function isSafeTaskWorktreeRoot(projectWorkspacePath, worktreeRoot) {
|
|
359
|
+
const normalizedWorkspacePath =
|
|
360
|
+
typeof projectWorkspacePath === "string" ? projectWorkspacePath.trim() : "";
|
|
361
|
+
const normalizedWorktreeRoot =
|
|
362
|
+
typeof worktreeRoot === "string" ? worktreeRoot.trim() : "";
|
|
363
|
+
if (!normalizedWorkspacePath || !normalizedWorktreeRoot) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const resolvedWorkspacePath = path.resolve(normalizedWorkspacePath);
|
|
368
|
+
const resolvedWorktreeRoot = path.resolve(normalizedWorktreeRoot);
|
|
369
|
+
if (resolvedWorkspacePath === resolvedWorktreeRoot) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const expectedParent = path.resolve(resolvedWorkspacePath, ".conductor", "worktrees");
|
|
374
|
+
const relativeToExpectedParent = path.relative(expectedParent, resolvedWorktreeRoot);
|
|
375
|
+
if (
|
|
376
|
+
!relativeToExpectedParent ||
|
|
377
|
+
relativeToExpectedParent === "." ||
|
|
378
|
+
relativeToExpectedParent.startsWith("..") ||
|
|
379
|
+
path.isAbsolute(relativeToExpectedParent)
|
|
380
|
+
) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
357
387
|
function normalizeOptionalString(value) {
|
|
358
388
|
if (typeof value !== "string") {
|
|
359
389
|
return null;
|
|
@@ -638,6 +668,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
638
668
|
const autoUpdateForceLocal = parseBooleanEnv(process.env.CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL);
|
|
639
669
|
const autoUpdateSupportedInstall =
|
|
640
670
|
autoUpdateForceLocal || isManagedInstallPathFn(installedPackageRoot);
|
|
671
|
+
const skipPidLockCheck = parseBooleanEnv(process.env.CONDUCTOR_TUI_DEBUG);
|
|
641
672
|
const lockHandoffToken =
|
|
642
673
|
normalizeOptionalString(config.LOCK_HANDOFF_TOKEN) ||
|
|
643
674
|
normalizeOptionalString(process.env.CONDUCTOR_LOCK_HANDOFF_TOKEN);
|
|
@@ -738,6 +769,30 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
738
769
|
return resolvedPath;
|
|
739
770
|
}
|
|
740
771
|
|
|
772
|
+
const PROJECT_SETTINGS_TEMPLATE = [
|
|
773
|
+
"worktree:",
|
|
774
|
+
" sync_branch: false",
|
|
775
|
+
" symlink: []",
|
|
776
|
+
" # Example: symlink paths from the parent workspace into each worktree",
|
|
777
|
+
" # symlink:",
|
|
778
|
+
" # - node_modules",
|
|
779
|
+
" # - .env",
|
|
780
|
+
"",
|
|
781
|
+
].join("\n");
|
|
782
|
+
|
|
783
|
+
function ensureProjectSettingsTemplate(projectWorkspacePath) {
|
|
784
|
+
const settingsPath = path.join(projectWorkspacePath, ".conductor", "settings.yaml");
|
|
785
|
+
if (existsSyncFn(settingsPath)) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
mkdirSyncFn(path.join(projectWorkspacePath, ".conductor"), { recursive: true });
|
|
790
|
+
writeFileSyncFn(settingsPath, PROJECT_SETTINGS_TEMPLATE, "utf8");
|
|
791
|
+
} catch (_error) {
|
|
792
|
+
// best-effort; do not block project validation if template creation fails
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
741
796
|
function readProjectWorktreeSettings(projectWorkspacePath) {
|
|
742
797
|
const settingsCandidates = [
|
|
743
798
|
path.join(projectWorkspacePath, ".conductor", "settings.yaml"),
|
|
@@ -759,6 +814,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
759
814
|
: {};
|
|
760
815
|
return {
|
|
761
816
|
symlinkPaths: normalizeConfiguredPathList(worktreeSettings.symlink, projectWorkspacePath),
|
|
817
|
+
syncBranch: worktreeSettings.sync_branch === true || worktreeSettings.syncBranch === true,
|
|
762
818
|
settingsPath,
|
|
763
819
|
};
|
|
764
820
|
} catch (error) {
|
|
@@ -768,6 +824,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
768
824
|
|
|
769
825
|
return {
|
|
770
826
|
symlinkPaths: [],
|
|
827
|
+
syncBranch: false,
|
|
771
828
|
settingsPath: null,
|
|
772
829
|
};
|
|
773
830
|
}
|
|
@@ -811,9 +868,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
811
868
|
|
|
812
869
|
async function runSpawnProcess(command, args, options = {}) {
|
|
813
870
|
let child;
|
|
871
|
+
const { timeoutMs, ...spawnOptions } = options || {};
|
|
814
872
|
try {
|
|
815
873
|
child = spawnFn(command, args, {
|
|
816
|
-
...
|
|
874
|
+
...spawnOptions,
|
|
817
875
|
stdio: ["ignore", "pipe", "pipe"],
|
|
818
876
|
});
|
|
819
877
|
} catch (error) {
|
|
@@ -824,12 +882,17 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
824
882
|
let stdout = "";
|
|
825
883
|
let stderr = "";
|
|
826
884
|
let settled = false;
|
|
885
|
+
let timeoutHandle = null;
|
|
827
886
|
|
|
828
887
|
const finishResolve = () => {
|
|
829
888
|
if (settled) {
|
|
830
889
|
return;
|
|
831
890
|
}
|
|
832
891
|
settled = true;
|
|
892
|
+
if (timeoutHandle) {
|
|
893
|
+
clearTimeout(timeoutHandle);
|
|
894
|
+
timeoutHandle = null;
|
|
895
|
+
}
|
|
833
896
|
resolve({ stdout, stderr });
|
|
834
897
|
};
|
|
835
898
|
|
|
@@ -838,9 +901,26 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
838
901
|
return;
|
|
839
902
|
}
|
|
840
903
|
settled = true;
|
|
904
|
+
if (timeoutHandle) {
|
|
905
|
+
clearTimeout(timeoutHandle);
|
|
906
|
+
timeoutHandle = null;
|
|
907
|
+
}
|
|
841
908
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
842
909
|
};
|
|
843
910
|
|
|
911
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
912
|
+
timeoutHandle = setTimeout(() => {
|
|
913
|
+
try {
|
|
914
|
+
if (child && typeof child.kill === "function") {
|
|
915
|
+
child.kill("SIGTERM");
|
|
916
|
+
}
|
|
917
|
+
} catch {
|
|
918
|
+
// ignore process kill failures; the timeout error is the useful signal
|
|
919
|
+
}
|
|
920
|
+
finishReject(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`));
|
|
921
|
+
}, timeoutMs);
|
|
922
|
+
}
|
|
923
|
+
|
|
844
924
|
if (child.stdout && typeof child.stdout.on === "function") {
|
|
845
925
|
child.stdout.on("data", (chunk) => {
|
|
846
926
|
stdout += String(chunk ?? "");
|
|
@@ -888,6 +968,48 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
888
968
|
const finalCwd = resolveTaskWorktreeCwd(worktreeRoot, worktreeConfig.projectRelativePath);
|
|
889
969
|
const gitMarkerPath = path.join(worktreeRoot, ".git");
|
|
890
970
|
if (!existsSyncFn(gitMarkerPath)) {
|
|
971
|
+
const { syncBranch } = readProjectWorktreeSettings(worktreeConfig.projectWorkspacePath);
|
|
972
|
+
if (syncBranch) {
|
|
973
|
+
try {
|
|
974
|
+
const { stdout: remoteStdout } = await runSpawnProcess(
|
|
975
|
+
"git",
|
|
976
|
+
["-C", worktreeConfig.projectRepoRoot, "remote"],
|
|
977
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
978
|
+
);
|
|
979
|
+
const hasRemote = remoteStdout.trim().length > 0;
|
|
980
|
+
if (hasRemote) {
|
|
981
|
+
await runSpawnProcess(
|
|
982
|
+
"git",
|
|
983
|
+
["-C", worktreeConfig.projectRepoRoot, "fetch"],
|
|
984
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
985
|
+
);
|
|
986
|
+
const { stdout: branchStdout } = await runSpawnProcess(
|
|
987
|
+
"git",
|
|
988
|
+
["-C", worktreeConfig.projectRepoRoot, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
989
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
990
|
+
);
|
|
991
|
+
const currentBranch = branchStdout.trim();
|
|
992
|
+
if (currentBranch && currentBranch !== "HEAD") {
|
|
993
|
+
const { stdout: trackingStdout } = await runSpawnProcess(
|
|
994
|
+
"git",
|
|
995
|
+
["-C", worktreeConfig.projectRepoRoot, "rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`],
|
|
996
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
997
|
+
).catch(() => ({ stdout: "" }));
|
|
998
|
+
const upstream = trackingStdout.trim();
|
|
999
|
+
if (upstream) {
|
|
1000
|
+
await runSpawnProcess(
|
|
1001
|
+
"git",
|
|
1002
|
+
["-C", worktreeConfig.projectRepoRoot, "merge", "--ff-only", upstream],
|
|
1003
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
1004
|
+
).catch(() => {});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} catch (_syncError) {
|
|
1009
|
+
// sync_branch is best-effort; proceed with worktree creation even if sync fails
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
891
1013
|
mkdirSyncFn(path.dirname(worktreeRoot), { recursive: true });
|
|
892
1014
|
try {
|
|
893
1015
|
await runSpawnProcess(
|
|
@@ -962,6 +1084,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
962
1084
|
process.env.CONDUCTOR_DAEMON_FORCE_KILL_WAIT_MS,
|
|
963
1085
|
2_000,
|
|
964
1086
|
);
|
|
1087
|
+
const WORKTREE_SYNC_TIMEOUT_MS = parsePositiveInt(
|
|
1088
|
+
process.env.CONDUCTOR_WORKTREE_SYNC_TIMEOUT_MS,
|
|
1089
|
+
5_000,
|
|
1090
|
+
);
|
|
965
1091
|
const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
|
|
966
1092
|
process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
|
|
967
1093
|
1000,
|
|
@@ -1073,87 +1199,94 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1073
1199
|
|
|
1074
1200
|
const LOCK_FILE = path.join(WORKSPACE_ROOT, "daemon.pid");
|
|
1075
1201
|
try {
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1202
|
+
if (skipPidLockCheck) {
|
|
1203
|
+
log("CONDUCTOR_TUI_DEBUG enabled; skipping daemon PID lock enforcement");
|
|
1204
|
+
} else {
|
|
1205
|
+
if (existsSyncFn(LOCK_FILE)) {
|
|
1206
|
+
const lockState = readLockState();
|
|
1207
|
+
const pid = lockState?.pid;
|
|
1208
|
+
if (pid) {
|
|
1209
|
+
const handoffMatched = hasMatchingLockHandoff(lockState);
|
|
1210
|
+
try {
|
|
1211
|
+
if (handoffMatched) {
|
|
1212
|
+
log(`Taking over daemon lock from PID ${pid} via handoff`);
|
|
1213
|
+
} else {
|
|
1214
|
+
const alive = isProcessAlive(pid);
|
|
1215
|
+
if (alive) {
|
|
1216
|
+
if (config.FORCE) {
|
|
1217
|
+
log(`Force enabled: stopping existing daemon PID ${pid}`);
|
|
1218
|
+
let alreadyExited = false;
|
|
1219
|
+
try {
|
|
1220
|
+
killFn(pid, "SIGTERM");
|
|
1221
|
+
} catch (killErr) {
|
|
1222
|
+
if (killErr?.code === "ESRCH") {
|
|
1223
|
+
alreadyExited = true;
|
|
1224
|
+
} else {
|
|
1225
|
+
logError(`Failed to stop existing daemon PID ${pid}: ${killErr.message}`);
|
|
1226
|
+
return exitAndReturn(1);
|
|
1227
|
+
}
|
|
1098
1228
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1229
|
+
try {
|
|
1230
|
+
let exited = alreadyExited || waitForProcessExitSync(pid, DAEMON_FORCE_STOP_GRACE_MS);
|
|
1231
|
+
if (!exited) {
|
|
1232
|
+
log(
|
|
1233
|
+
`Existing daemon PID ${pid} did not exit within ${DAEMON_FORCE_STOP_GRACE_MS}ms; sending SIGKILL`,
|
|
1234
|
+
);
|
|
1235
|
+
try {
|
|
1236
|
+
killFn(pid, "SIGKILL");
|
|
1237
|
+
} catch (killErr) {
|
|
1238
|
+
if (killErr?.code !== "ESRCH") {
|
|
1239
|
+
logError(`Failed to force kill existing daemon PID ${pid}: ${killErr.message}`);
|
|
1240
|
+
return exitAndReturn(1);
|
|
1241
|
+
}
|
|
1112
1242
|
}
|
|
1243
|
+
exited = waitForProcessExitSync(pid, DAEMON_FORCE_KILL_WAIT_MS);
|
|
1113
1244
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1245
|
+
if (!exited) {
|
|
1246
|
+
logError(`Existing daemon PID ${pid} is still running after force restart; please stop it manually.`);
|
|
1247
|
+
return exitAndReturn(1);
|
|
1248
|
+
}
|
|
1249
|
+
} catch (checkErr) {
|
|
1250
|
+
logError(`Failed to verify daemon PID ${pid}: ${checkErr.message}`);
|
|
1118
1251
|
return exitAndReturn(1);
|
|
1119
1252
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1253
|
+
log("Removing lock file after force stop");
|
|
1254
|
+
if (existsSyncFn(LOCK_FILE)) {
|
|
1255
|
+
unlinkSyncFn(LOCK_FILE);
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
logError(`Daemon already running with PID ${pid}`);
|
|
1122
1259
|
return exitAndReturn(1);
|
|
1123
1260
|
}
|
|
1124
|
-
log("Removing lock file after force stop");
|
|
1125
|
-
if (existsSyncFn(LOCK_FILE)) {
|
|
1126
|
-
unlinkSyncFn(LOCK_FILE);
|
|
1127
|
-
}
|
|
1128
1261
|
} else {
|
|
1129
|
-
|
|
1130
|
-
|
|
1262
|
+
log("Removing stale lock file");
|
|
1263
|
+
unlinkSyncFn(LOCK_FILE);
|
|
1131
1264
|
}
|
|
1265
|
+
}
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
if (handoffMatched) {
|
|
1268
|
+
log(`Taking over daemon lock from PID ${pid} via handoff`);
|
|
1132
1269
|
} else {
|
|
1133
|
-
|
|
1134
|
-
|
|
1270
|
+
logError(`Daemon already running with PID ${pid} (access denied)`);
|
|
1271
|
+
return exitAndReturn(1);
|
|
1135
1272
|
}
|
|
1136
1273
|
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
} else {
|
|
1141
|
-
logError(`Daemon already running with PID ${pid} (access denied)`);
|
|
1142
|
-
return exitAndReturn(1);
|
|
1143
|
-
}
|
|
1274
|
+
} else {
|
|
1275
|
+
log("Removing malformed lock file");
|
|
1276
|
+
unlinkSyncFn(LOCK_FILE);
|
|
1144
1277
|
}
|
|
1145
|
-
} else {
|
|
1146
|
-
log("Removing malformed lock file");
|
|
1147
|
-
unlinkSyncFn(LOCK_FILE);
|
|
1148
1278
|
}
|
|
1279
|
+
writeFileSyncFn(LOCK_FILE, process.pid.toString());
|
|
1149
1280
|
}
|
|
1150
|
-
writeFileSyncFn(LOCK_FILE, process.pid.toString());
|
|
1151
1281
|
} catch (err) {
|
|
1152
1282
|
logError("Failed to acquire lock:", err);
|
|
1153
1283
|
return exitAndReturn(1);
|
|
1154
1284
|
}
|
|
1155
1285
|
|
|
1156
1286
|
const cleanupLock = () => {
|
|
1287
|
+
if (skipPidLockCheck) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1157
1290
|
try {
|
|
1158
1291
|
if (existsSyncFn(LOCK_FILE)) {
|
|
1159
1292
|
const lockState = readLockState();
|
|
@@ -1167,6 +1300,9 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1167
1300
|
};
|
|
1168
1301
|
|
|
1169
1302
|
const writeLockHandoff = ({ handoffToken, handoffFromPid, handoffExpiresAt }) => {
|
|
1303
|
+
if (skipPidLockCheck) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1170
1306
|
writeFileSyncFn(
|
|
1171
1307
|
LOCK_FILE,
|
|
1172
1308
|
JSON.stringify({
|
|
@@ -1321,6 +1457,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1321
1457
|
if (advertisedCapabilities.length > 0) {
|
|
1322
1458
|
extraHeaders["x-conductor-capabilities"] = advertisedCapabilities.join(",");
|
|
1323
1459
|
}
|
|
1460
|
+
const aiManagerHandlers = createAiManagerHandlers({ configPath: config.CONFIG_FILE });
|
|
1461
|
+
|
|
1324
1462
|
const client = createWebSocketClient(sdkConfig, {
|
|
1325
1463
|
extraHeaders,
|
|
1326
1464
|
onConnected: ({ isReconnect, connectedAt } = { isReconnect: false, connectedAt: Date.now() }) => {
|
|
@@ -2874,6 +3012,30 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2874
3012
|
logError(`Failed to report agent_command_ack(cleanup_task_worktree) for ${taskId}: ${error?.message || error}`);
|
|
2875
3013
|
});
|
|
2876
3014
|
|
|
3015
|
+
if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
|
|
3016
|
+
if (forceCleanup) {
|
|
3017
|
+
const stopStarted = stopActiveTaskProcess(taskId, {
|
|
3018
|
+
reason: "cleanup_task_worktree",
|
|
3019
|
+
suppressExitStatusReport: true,
|
|
3020
|
+
});
|
|
3021
|
+
if (stopStarted) {
|
|
3022
|
+
const stopped = await waitForTaskToStop(taskId);
|
|
3023
|
+
if (!stopped && (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId))) {
|
|
3024
|
+
await reportTaskWorktreeCleanupResult({
|
|
3025
|
+
requestId,
|
|
3026
|
+
taskId,
|
|
3027
|
+
worktreeBranch: worktreeConfig.worktreeBranch,
|
|
3028
|
+
cleaned: false,
|
|
3029
|
+
error: "Task is still active",
|
|
3030
|
+
}).catch((error) => {
|
|
3031
|
+
logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${error?.message || error}`);
|
|
3032
|
+
});
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
2877
3039
|
if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
|
|
2878
3040
|
await reportTaskWorktreeCleanupResult({
|
|
2879
3041
|
requestId,
|
|
@@ -2891,6 +3053,19 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2891
3053
|
worktreeConfig.projectWorkspacePath,
|
|
2892
3054
|
worktreeConfig.worktreeId,
|
|
2893
3055
|
);
|
|
3056
|
+
if (!isSafeTaskWorktreeRoot(worktreeConfig.projectWorkspacePath, worktreeRoot)) {
|
|
3057
|
+
await reportTaskWorktreeCleanupResult({
|
|
3058
|
+
requestId,
|
|
3059
|
+
taskId,
|
|
3060
|
+
worktreeBranch: worktreeConfig.worktreeBranch,
|
|
3061
|
+
removedPath: worktreeRoot,
|
|
3062
|
+
cleaned: false,
|
|
3063
|
+
error: `Refusing to remove unsafe worktree path: ${worktreeRoot}`,
|
|
3064
|
+
}).catch((error) => {
|
|
3065
|
+
logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${error?.message || error}`);
|
|
3066
|
+
});
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
2894
3069
|
const worktreeCwd = resolveTaskWorktreeCwd(worktreeRoot, worktreeConfig.projectRelativePath);
|
|
2895
3070
|
const statusCwd = existsSyncFn(worktreeCwd) ? worktreeCwd : worktreeRoot;
|
|
2896
3071
|
|
|
@@ -3259,6 +3434,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3259
3434
|
if (event.type === "validate_project_path") {
|
|
3260
3435
|
void handleValidateProjectPath(event.payload);
|
|
3261
3436
|
}
|
|
3437
|
+
if (event.type === "ai_manager_request") {
|
|
3438
|
+
handleAiManagerRequest(client, aiManagerHandlers, event.payload).catch((error) => {
|
|
3439
|
+
logError(`Unhandled ai_manager_request failure: ${error?.message || error}`);
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3262
3442
|
}
|
|
3263
3443
|
|
|
3264
3444
|
function markWatchdogHealthy(signal, at = Date.now()) {
|
|
@@ -3367,12 +3547,14 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3367
3547
|
};
|
|
3368
3548
|
} else {
|
|
3369
3549
|
const snapshot = await Promise.resolve(resolveProjectSnapshotFn(resolvedPath));
|
|
3550
|
+
const effectiveWorkspace =
|
|
3551
|
+
typeof snapshot?.projectRoot === "string" && snapshot.projectRoot.trim()
|
|
3552
|
+
? snapshot.projectRoot.trim()
|
|
3553
|
+
: resolvedPath;
|
|
3554
|
+
ensureProjectSettingsTemplate(effectiveWorkspace);
|
|
3370
3555
|
result = {
|
|
3371
3556
|
...result,
|
|
3372
|
-
workspacePath:
|
|
3373
|
-
typeof snapshot?.projectRoot === "string" && snapshot.projectRoot.trim()
|
|
3374
|
-
? snapshot.projectRoot.trim()
|
|
3375
|
-
: resolvedPath,
|
|
3557
|
+
workspacePath: effectiveWorkspace,
|
|
3376
3558
|
repoRoot:
|
|
3377
3559
|
typeof snapshot?.repoRoot === "string" && snapshot.repoRoot.trim()
|
|
3378
3560
|
? snapshot.repoRoot.trim()
|
|
@@ -3420,57 +3602,25 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3420
3602
|
}
|
|
3421
3603
|
}
|
|
3422
3604
|
|
|
3423
|
-
function
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
requestId,
|
|
3431
|
-
taskId,
|
|
3432
|
-
eventType: "stop_task",
|
|
3433
|
-
accepted: true,
|
|
3434
|
-
}).catch(() => {});
|
|
3435
|
-
return;
|
|
3436
|
-
}
|
|
3437
|
-
|
|
3438
|
-
const sendStopAck = (accepted) => {
|
|
3439
|
-
if (!requestId) return;
|
|
3440
|
-
client
|
|
3441
|
-
.sendJson({
|
|
3442
|
-
type: "task_stop_ack",
|
|
3443
|
-
payload: {
|
|
3444
|
-
task_id: taskId,
|
|
3445
|
-
request_id: requestId,
|
|
3446
|
-
accepted: Boolean(accepted),
|
|
3447
|
-
},
|
|
3448
|
-
})
|
|
3449
|
-
.catch((err) => {
|
|
3450
|
-
logError(`Failed to report task_stop_ack for ${taskId}: ${err?.message || err}`);
|
|
3451
|
-
});
|
|
3452
|
-
sendAgentCommandAck({
|
|
3453
|
-
requestId,
|
|
3454
|
-
taskId,
|
|
3455
|
-
eventType: "stop_task",
|
|
3456
|
-
accepted,
|
|
3457
|
-
}).catch((err) => {
|
|
3458
|
-
logError(`Failed to report agent_command_ack(stop_task) for ${taskId}: ${err?.message || err}`);
|
|
3459
|
-
});
|
|
3460
|
-
};
|
|
3461
|
-
|
|
3605
|
+
function stopActiveTaskProcess(
|
|
3606
|
+
taskId,
|
|
3607
|
+
{
|
|
3608
|
+
reason = "",
|
|
3609
|
+
suppressExitStatusReport = false,
|
|
3610
|
+
} = {},
|
|
3611
|
+
) {
|
|
3462
3612
|
const processRecord = activeTaskProcesses.get(taskId);
|
|
3463
3613
|
const ptyRecord = activePtySessions.get(taskId);
|
|
3464
3614
|
if ((!processRecord || !processRecord.child) && !ptyRecord) {
|
|
3465
|
-
|
|
3466
|
-
sendStopAck(false);
|
|
3467
|
-
return;
|
|
3615
|
+
return false;
|
|
3468
3616
|
}
|
|
3469
3617
|
|
|
3470
|
-
|
|
3471
|
-
|
|
3618
|
+
if (suppressExitStatusReport) {
|
|
3619
|
+
suppressedExitStatusReports.add(taskId);
|
|
3620
|
+
}
|
|
3472
3621
|
|
|
3473
|
-
|
|
3622
|
+
const reasonSuffix = reason ? ` (${reason})` : "";
|
|
3623
|
+
log(`Stopping task ${taskId}${reasonSuffix}`);
|
|
3474
3624
|
|
|
3475
3625
|
const activeRecord = processRecord || ptyRecord;
|
|
3476
3626
|
if (activeRecord?.stopForceKillTimer) {
|
|
@@ -3528,6 +3678,75 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3528
3678
|
if (typeof activeRecord.stopForceKillTimer?.unref === "function") {
|
|
3529
3679
|
activeRecord.stopForceKillTimer.unref();
|
|
3530
3680
|
}
|
|
3681
|
+
|
|
3682
|
+
return true;
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
async function waitForTaskToStop(taskId, timeoutMs = DAEMON_FORCE_STOP_GRACE_MS) {
|
|
3686
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
3687
|
+
|
|
3688
|
+
while (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
|
|
3689
|
+
const remainingMs = deadline - Date.now();
|
|
3690
|
+
if (remainingMs <= 0) {
|
|
3691
|
+
return false;
|
|
3692
|
+
}
|
|
3693
|
+
await new Promise((resolve) =>
|
|
3694
|
+
setTimeout(resolve, Math.min(DAEMON_FORCE_STOP_POLL_INTERVAL_MS, remainingMs)),
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
return true;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
function handleStopTask(payload) {
|
|
3702
|
+
const taskId = payload?.task_id;
|
|
3703
|
+
if (!taskId) return;
|
|
3704
|
+
const requestId = payload?.request_id ? String(payload.request_id) : "";
|
|
3705
|
+
if (requestId && !markRequestSeen(requestId)) {
|
|
3706
|
+
log(`Duplicate stop_task ignored for ${taskId} (request_id=${requestId})`);
|
|
3707
|
+
sendAgentCommandAck({
|
|
3708
|
+
requestId,
|
|
3709
|
+
taskId,
|
|
3710
|
+
eventType: "stop_task",
|
|
3711
|
+
accepted: true,
|
|
3712
|
+
}).catch(() => {});
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
const sendStopAck = (accepted) => {
|
|
3717
|
+
if (!requestId) return;
|
|
3718
|
+
client
|
|
3719
|
+
.sendJson({
|
|
3720
|
+
type: "task_stop_ack",
|
|
3721
|
+
payload: {
|
|
3722
|
+
task_id: taskId,
|
|
3723
|
+
request_id: requestId,
|
|
3724
|
+
accepted: Boolean(accepted),
|
|
3725
|
+
},
|
|
3726
|
+
})
|
|
3727
|
+
.catch((err) => {
|
|
3728
|
+
logError(`Failed to report task_stop_ack for ${taskId}: ${err?.message || err}`);
|
|
3729
|
+
});
|
|
3730
|
+
sendAgentCommandAck({
|
|
3731
|
+
requestId,
|
|
3732
|
+
taskId,
|
|
3733
|
+
eventType: "stop_task",
|
|
3734
|
+
accepted,
|
|
3735
|
+
}).catch((err) => {
|
|
3736
|
+
logError(`Failed to report agent_command_ack(stop_task) for ${taskId}: ${err?.message || err}`);
|
|
3737
|
+
});
|
|
3738
|
+
};
|
|
3739
|
+
|
|
3740
|
+
const processRecord = activeTaskProcesses.get(taskId);
|
|
3741
|
+
const ptyRecord = activePtySessions.get(taskId);
|
|
3742
|
+
if ((!processRecord || !processRecord.child) && !ptyRecord) {
|
|
3743
|
+
log(`Stop requested for task ${taskId}, but no active process found`);
|
|
3744
|
+
sendStopAck(false);
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
sendStopAck(true);
|
|
3749
|
+
stopActiveTaskProcess(taskId, { reason: payload?.reason });
|
|
3531
3750
|
}
|
|
3532
3751
|
|
|
3533
3752
|
async function getProjectLocalPath(projectId) {
|
package/src/runtime-backends.js
CHANGED
|
@@ -17,16 +17,29 @@ const LEGACY_RUNTIME_BACKEND_ALIASES = new Set([
|
|
|
17
17
|
const externalRuntimeCatalogPromises = new Map();
|
|
18
18
|
let externalRuntimeImportNonce = 0;
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
|
|
20
|
+
function appendProviderModulePaths(parts, value) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (const entry of value) {
|
|
23
|
+
appendProviderModulePaths(parts, entry);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const raw = String(value || "").trim();
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const item of raw.split(process.platform === "win32" ? ";" : ":")) {
|
|
32
|
+
const normalized = item.trim();
|
|
33
|
+
if (normalized) {
|
|
34
|
+
parts.push(normalized);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
function listProviderModulePaths(providerPathEnv) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return [...new Set(raw.split(process.platform === "win32" ? ";" : ":").map((item) => item.trim()).filter(Boolean))];
|
|
40
|
+
const parts = [];
|
|
41
|
+
appendProviderModulePaths(parts, providerPathEnv);
|
|
42
|
+
return [...new Set(parts)];
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
function normalizeRuntimeBackendName(backend) {
|
|
@@ -221,18 +234,17 @@ function readConfigEnvValue(configFilePath, key) {
|
|
|
221
234
|
if (!parsed || typeof parsed !== "object") {
|
|
222
235
|
return "";
|
|
223
236
|
}
|
|
224
|
-
|
|
225
|
-
return typeof value === "string" ? value.trim() : "";
|
|
237
|
+
return parsed?.envs?.[key];
|
|
226
238
|
} catch {
|
|
227
|
-
return
|
|
239
|
+
return undefined;
|
|
228
240
|
}
|
|
229
241
|
}
|
|
230
242
|
|
|
231
|
-
function
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
243
|
+
function resolveProviderModulePaths(options = {}) {
|
|
244
|
+
return [
|
|
245
|
+
...listProviderModulePaths(process.env.AISDK_PROVIDER_PATH),
|
|
246
|
+
...listProviderModulePaths(readConfigEnvValue(options.configFilePath, "AISDK_PROVIDER_PATH")),
|
|
247
|
+
].filter((value, index, array) => array.indexOf(value) === index);
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
function createEmptyExternalCatalog() {
|
|
@@ -342,15 +354,16 @@ async function loadExternalRuntimeCatalog(providerPathEnv) {
|
|
|
342
354
|
}
|
|
343
355
|
|
|
344
356
|
async function getExternalRuntimeCatalog(options = {}) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
357
|
+
const modulePaths = resolveProviderModulePaths(options);
|
|
358
|
+
const cacheKey = modulePaths.join("\0");
|
|
359
|
+
if (!externalRuntimeCatalogPromises.has(cacheKey)) {
|
|
360
|
+
const loadPromise = loadExternalRuntimeCatalog(modulePaths).catch((error) => {
|
|
361
|
+
externalRuntimeCatalogPromises.delete(cacheKey);
|
|
349
362
|
throw error;
|
|
350
363
|
});
|
|
351
|
-
externalRuntimeCatalogPromises.set(
|
|
364
|
+
externalRuntimeCatalogPromises.set(cacheKey, loadPromise);
|
|
352
365
|
}
|
|
353
|
-
return externalRuntimeCatalogPromises.get(
|
|
366
|
+
return externalRuntimeCatalogPromises.get(cacheKey);
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
export async function normalizeRuntimeBackendAlias(backend, options = {}) {
|