@love-moon/conductor-cli 0.2.33 → 0.2.35
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 +192 -49
- package/package.json +4 -4
- package/src/daemon.js +321 -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) {
|
|
@@ -760,7 +903,13 @@ async function main() {
|
|
|
760
903
|
process.off("SIGINT", onSigint);
|
|
761
904
|
process.off("SIGTERM", onSigterm);
|
|
762
905
|
if (!launchedByDaemon) {
|
|
906
|
+
const remoteStopReason = typeof runner.getRemoteStopReason === "function" ? runner.getRemoteStopReason() : null;
|
|
763
907
|
const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
|
|
908
|
+
// When the task was deleted by the user, the DB record is already gone —
|
|
909
|
+
// attempting to send a final status update would fail with 500 and the
|
|
910
|
+
// SDK durable outbox would retry forever, preventing the process from
|
|
911
|
+
// exiting.
|
|
912
|
+
const taskDeletedByUser = remoteStopReason === "deleted_by_user";
|
|
764
913
|
const finalStatus = shutdownSignal
|
|
765
914
|
? {
|
|
766
915
|
status: "KILLED",
|
|
@@ -780,16 +929,25 @@ async function main() {
|
|
|
780
929
|
status: "COMPLETED",
|
|
781
930
|
summary: "conductor fire exited",
|
|
782
931
|
};
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
932
|
+
if (!taskDeletedByUser) {
|
|
933
|
+
try {
|
|
934
|
+
const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
935
|
+
if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
|
|
936
|
+
await conductor.flushPendingUpstreamEvents({
|
|
937
|
+
timeoutMs: 5_000,
|
|
938
|
+
retryIntervalMs: 250,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
} catch (error) {
|
|
942
|
+
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
log(`Skipping final status report: task was deleted by user`);
|
|
946
|
+
// Also clear any pending durable outbox retries (e.g. task_stop_ack)
|
|
947
|
+
// that would keep failing against the deleted task.
|
|
948
|
+
if (typeof conductor.clearDurableOutboxTimer === "function") {
|
|
949
|
+
conductor.clearDurableOutboxTimer();
|
|
790
950
|
}
|
|
791
|
-
} catch (error) {
|
|
792
|
-
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
793
951
|
}
|
|
794
952
|
}
|
|
795
953
|
if (shutdownSignal === "SIGINT") {
|
|
@@ -1023,6 +1181,12 @@ Environment:
|
|
|
1023
1181
|
const sessionBackend =
|
|
1024
1182
|
configuredBackend?.runtimeBackend ||
|
|
1025
1183
|
(backend ? await normalizeRuntimeBackendAlias(backend, { configFilePath: configFileFromArgs }) : "");
|
|
1184
|
+
const sessionOptions = resolveAiSessionOptions(
|
|
1185
|
+
backend,
|
|
1186
|
+
allowCliList,
|
|
1187
|
+
process.env,
|
|
1188
|
+
sessionBackend || backend,
|
|
1189
|
+
);
|
|
1026
1190
|
const shouldRequireBackend =
|
|
1027
1191
|
!Boolean(conductorArgs.listBackends) &&
|
|
1028
1192
|
!listBackendsWithoutSeparator &&
|
|
@@ -1071,6 +1235,7 @@ Environment:
|
|
|
1071
1235
|
hasExplicitTaskTitle: typeof conductorArgs.title === "string" && Boolean(conductorArgs.title.trim()),
|
|
1072
1236
|
configFile: conductorArgs.configFile,
|
|
1073
1237
|
sessionBackend,
|
|
1238
|
+
sessionOptions,
|
|
1074
1239
|
resumeSessionId,
|
|
1075
1240
|
showVersion: Boolean(conductorArgs.version) || versionWithoutSeparator,
|
|
1076
1241
|
listBackends: Boolean(conductorArgs.listBackends) || listBackendsWithoutSeparator,
|
|
@@ -1269,43 +1434,11 @@ export async function resolveProjectId(conductor, explicit, opts = {}) {
|
|
|
1269
1434
|
log(`Unable to match project by path: ${error.message}`);
|
|
1270
1435
|
}
|
|
1271
1436
|
|
|
1272
|
-
|
|
1273
|
-
const created = await conductor.createProject({
|
|
1274
|
-
name: projectName,
|
|
1275
|
-
bindingConfirmed: true,
|
|
1276
|
-
daemonHost,
|
|
1277
|
-
workspacePath: snapshot.projectRoot,
|
|
1278
|
-
repoRoot: snapshot.repoRoot,
|
|
1279
|
-
worktreeBranch: snapshot.worktreeBranch,
|
|
1280
|
-
lastCommit: snapshot.lastCommit,
|
|
1281
|
-
fileCount: snapshot.fileCount,
|
|
1282
|
-
});
|
|
1283
|
-
if (created?.id) {
|
|
1284
|
-
log(`Created bound project ${created.name || created.id} for ${daemonHost}:${snapshot.projectRoot}`);
|
|
1285
|
-
return created.id;
|
|
1286
|
-
}
|
|
1287
|
-
throw new Error("create_project returned no id");
|
|
1288
|
-
} catch (error) {
|
|
1289
|
-
log(`Unable to create bound project: ${error.message}`);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
try {
|
|
1293
|
-
const retryMatch = await conductor.matchProjectByPath({
|
|
1294
|
-
daemon_host: daemonHost,
|
|
1295
|
-
project_path: snapshot.projectRoot,
|
|
1296
|
-
});
|
|
1297
|
-
if (retryMatch?.project_id) {
|
|
1298
|
-
return retryMatch.project_id;
|
|
1299
|
-
}
|
|
1300
|
-
} catch {
|
|
1301
|
-
// ignore retry match failures
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
log(`Unable to resolve bound project for ${daemonHost}:${snapshot.projectRoot}, falling back to default`);
|
|
1437
|
+
log(`No matching project found for ${daemonHost}:${snapshot.projectRoot}, falling back to default`);
|
|
1305
1438
|
return resolveDefaultProjectId(conductor);
|
|
1306
1439
|
}
|
|
1307
1440
|
|
|
1308
|
-
function resolveDaemonHost(daemonName) {
|
|
1441
|
+
export function resolveDaemonHost(daemonName) {
|
|
1309
1442
|
if (typeof daemonName === "string" && daemonName.trim()) {
|
|
1310
1443
|
return daemonName.trim();
|
|
1311
1444
|
}
|
|
@@ -1725,6 +1858,7 @@ export class BridgeRunner {
|
|
|
1725
1858
|
session_file_path:
|
|
1726
1859
|
typeof sessionFilePath === "string" && sessionFilePath.trim() ? sessionFilePath.trim() : undefined,
|
|
1727
1860
|
backend_type: this.backendName,
|
|
1861
|
+
daemon_name: this.daemonName,
|
|
1728
1862
|
});
|
|
1729
1863
|
this.boundSessionId = normalizedSessionId;
|
|
1730
1864
|
return true;
|
|
@@ -1807,6 +1941,10 @@ export class BridgeRunner {
|
|
|
1807
1941
|
return this.stopped || Boolean(this.remoteStopInfo);
|
|
1808
1942
|
}
|
|
1809
1943
|
|
|
1944
|
+
getRemoteStopReason() {
|
|
1945
|
+
return this.remoteStopInfo?.reason || null;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1810
1948
|
getRemoteStopSummary() {
|
|
1811
1949
|
if (!this.remoteStopInfo) {
|
|
1812
1950
|
return null;
|
|
@@ -2090,6 +2228,11 @@ export class BridgeRunner {
|
|
|
2090
2228
|
session_file_path: payload.session_file_path || runtimeContext?.session_file_path,
|
|
2091
2229
|
token_usage_percent: runtimeContext?.token_usage_percent,
|
|
2092
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,
|
|
2093
2236
|
};
|
|
2094
2237
|
}
|
|
2095
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.35",
|
|
4
|
+
"gitCommitId": "686ee4d",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@love-moon/ai-bridge": "0.1.4",
|
|
21
|
-
"@love-moon/ai-sdk": "0.2.
|
|
22
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
21
|
+
"@love-moon/ai-sdk": "0.2.35",
|
|
22
|
+
"@love-moon/conductor-sdk": "0.2.35",
|
|
23
23
|
"chrome-launcher": "^1.2.1",
|
|
24
24
|
"chrome-remote-interface": "^0.33.0",
|
|
25
25
|
"dotenv": "^16.4.5",
|
package/src/daemon.js
CHANGED
|
@@ -354,6 +354,35 @@ export function ensureNodePtySpawnHelperExecutable(deps = {}) {
|
|
|
354
354
|
return { helperPath, updated: true };
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
export function isSafeTaskWorktreeRoot(projectWorkspacePath, worktreeRoot) {
|
|
358
|
+
const normalizedWorkspacePath =
|
|
359
|
+
typeof projectWorkspacePath === "string" ? projectWorkspacePath.trim() : "";
|
|
360
|
+
const normalizedWorktreeRoot =
|
|
361
|
+
typeof worktreeRoot === "string" ? worktreeRoot.trim() : "";
|
|
362
|
+
if (!normalizedWorkspacePath || !normalizedWorktreeRoot) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const resolvedWorkspacePath = path.resolve(normalizedWorkspacePath);
|
|
367
|
+
const resolvedWorktreeRoot = path.resolve(normalizedWorktreeRoot);
|
|
368
|
+
if (resolvedWorkspacePath === resolvedWorktreeRoot) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const expectedParent = path.resolve(resolvedWorkspacePath, ".conductor", "worktrees");
|
|
373
|
+
const relativeToExpectedParent = path.relative(expectedParent, resolvedWorktreeRoot);
|
|
374
|
+
if (
|
|
375
|
+
!relativeToExpectedParent ||
|
|
376
|
+
relativeToExpectedParent === "." ||
|
|
377
|
+
relativeToExpectedParent.startsWith("..") ||
|
|
378
|
+
path.isAbsolute(relativeToExpectedParent)
|
|
379
|
+
) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
357
386
|
function normalizeOptionalString(value) {
|
|
358
387
|
if (typeof value !== "string") {
|
|
359
388
|
return null;
|
|
@@ -638,6 +667,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
638
667
|
const autoUpdateForceLocal = parseBooleanEnv(process.env.CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL);
|
|
639
668
|
const autoUpdateSupportedInstall =
|
|
640
669
|
autoUpdateForceLocal || isManagedInstallPathFn(installedPackageRoot);
|
|
670
|
+
const skipPidLockCheck = parseBooleanEnv(process.env.CONDUCTOR_TUI_DEBUG);
|
|
641
671
|
const lockHandoffToken =
|
|
642
672
|
normalizeOptionalString(config.LOCK_HANDOFF_TOKEN) ||
|
|
643
673
|
normalizeOptionalString(process.env.CONDUCTOR_LOCK_HANDOFF_TOKEN);
|
|
@@ -738,6 +768,30 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
738
768
|
return resolvedPath;
|
|
739
769
|
}
|
|
740
770
|
|
|
771
|
+
const PROJECT_SETTINGS_TEMPLATE = [
|
|
772
|
+
"worktree:",
|
|
773
|
+
" sync_branch: false",
|
|
774
|
+
" symlink: []",
|
|
775
|
+
" # Example: symlink paths from the parent workspace into each worktree",
|
|
776
|
+
" # symlink:",
|
|
777
|
+
" # - node_modules",
|
|
778
|
+
" # - .env",
|
|
779
|
+
"",
|
|
780
|
+
].join("\n");
|
|
781
|
+
|
|
782
|
+
function ensureProjectSettingsTemplate(projectWorkspacePath) {
|
|
783
|
+
const settingsPath = path.join(projectWorkspacePath, ".conductor", "settings.yaml");
|
|
784
|
+
if (existsSyncFn(settingsPath)) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
mkdirSyncFn(path.join(projectWorkspacePath, ".conductor"), { recursive: true });
|
|
789
|
+
writeFileSyncFn(settingsPath, PROJECT_SETTINGS_TEMPLATE, "utf8");
|
|
790
|
+
} catch (_error) {
|
|
791
|
+
// best-effort; do not block project validation if template creation fails
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
741
795
|
function readProjectWorktreeSettings(projectWorkspacePath) {
|
|
742
796
|
const settingsCandidates = [
|
|
743
797
|
path.join(projectWorkspacePath, ".conductor", "settings.yaml"),
|
|
@@ -759,6 +813,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
759
813
|
: {};
|
|
760
814
|
return {
|
|
761
815
|
symlinkPaths: normalizeConfiguredPathList(worktreeSettings.symlink, projectWorkspacePath),
|
|
816
|
+
syncBranch: worktreeSettings.sync_branch === true || worktreeSettings.syncBranch === true,
|
|
762
817
|
settingsPath,
|
|
763
818
|
};
|
|
764
819
|
} catch (error) {
|
|
@@ -768,6 +823,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
768
823
|
|
|
769
824
|
return {
|
|
770
825
|
symlinkPaths: [],
|
|
826
|
+
syncBranch: false,
|
|
771
827
|
settingsPath: null,
|
|
772
828
|
};
|
|
773
829
|
}
|
|
@@ -811,9 +867,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
811
867
|
|
|
812
868
|
async function runSpawnProcess(command, args, options = {}) {
|
|
813
869
|
let child;
|
|
870
|
+
const { timeoutMs, ...spawnOptions } = options || {};
|
|
814
871
|
try {
|
|
815
872
|
child = spawnFn(command, args, {
|
|
816
|
-
...
|
|
873
|
+
...spawnOptions,
|
|
817
874
|
stdio: ["ignore", "pipe", "pipe"],
|
|
818
875
|
});
|
|
819
876
|
} catch (error) {
|
|
@@ -824,12 +881,17 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
824
881
|
let stdout = "";
|
|
825
882
|
let stderr = "";
|
|
826
883
|
let settled = false;
|
|
884
|
+
let timeoutHandle = null;
|
|
827
885
|
|
|
828
886
|
const finishResolve = () => {
|
|
829
887
|
if (settled) {
|
|
830
888
|
return;
|
|
831
889
|
}
|
|
832
890
|
settled = true;
|
|
891
|
+
if (timeoutHandle) {
|
|
892
|
+
clearTimeout(timeoutHandle);
|
|
893
|
+
timeoutHandle = null;
|
|
894
|
+
}
|
|
833
895
|
resolve({ stdout, stderr });
|
|
834
896
|
};
|
|
835
897
|
|
|
@@ -838,9 +900,26 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
838
900
|
return;
|
|
839
901
|
}
|
|
840
902
|
settled = true;
|
|
903
|
+
if (timeoutHandle) {
|
|
904
|
+
clearTimeout(timeoutHandle);
|
|
905
|
+
timeoutHandle = null;
|
|
906
|
+
}
|
|
841
907
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
842
908
|
};
|
|
843
909
|
|
|
910
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
911
|
+
timeoutHandle = setTimeout(() => {
|
|
912
|
+
try {
|
|
913
|
+
if (child && typeof child.kill === "function") {
|
|
914
|
+
child.kill("SIGTERM");
|
|
915
|
+
}
|
|
916
|
+
} catch {
|
|
917
|
+
// ignore process kill failures; the timeout error is the useful signal
|
|
918
|
+
}
|
|
919
|
+
finishReject(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`));
|
|
920
|
+
}, timeoutMs);
|
|
921
|
+
}
|
|
922
|
+
|
|
844
923
|
if (child.stdout && typeof child.stdout.on === "function") {
|
|
845
924
|
child.stdout.on("data", (chunk) => {
|
|
846
925
|
stdout += String(chunk ?? "");
|
|
@@ -888,6 +967,48 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
888
967
|
const finalCwd = resolveTaskWorktreeCwd(worktreeRoot, worktreeConfig.projectRelativePath);
|
|
889
968
|
const gitMarkerPath = path.join(worktreeRoot, ".git");
|
|
890
969
|
if (!existsSyncFn(gitMarkerPath)) {
|
|
970
|
+
const { syncBranch } = readProjectWorktreeSettings(worktreeConfig.projectWorkspacePath);
|
|
971
|
+
if (syncBranch) {
|
|
972
|
+
try {
|
|
973
|
+
const { stdout: remoteStdout } = await runSpawnProcess(
|
|
974
|
+
"git",
|
|
975
|
+
["-C", worktreeConfig.projectRepoRoot, "remote"],
|
|
976
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
977
|
+
);
|
|
978
|
+
const hasRemote = remoteStdout.trim().length > 0;
|
|
979
|
+
if (hasRemote) {
|
|
980
|
+
await runSpawnProcess(
|
|
981
|
+
"git",
|
|
982
|
+
["-C", worktreeConfig.projectRepoRoot, "fetch"],
|
|
983
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
984
|
+
);
|
|
985
|
+
const { stdout: branchStdout } = await runSpawnProcess(
|
|
986
|
+
"git",
|
|
987
|
+
["-C", worktreeConfig.projectRepoRoot, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
988
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
989
|
+
);
|
|
990
|
+
const currentBranch = branchStdout.trim();
|
|
991
|
+
if (currentBranch && currentBranch !== "HEAD") {
|
|
992
|
+
const { stdout: trackingStdout } = await runSpawnProcess(
|
|
993
|
+
"git",
|
|
994
|
+
["-C", worktreeConfig.projectRepoRoot, "rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`],
|
|
995
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
996
|
+
).catch(() => ({ stdout: "" }));
|
|
997
|
+
const upstream = trackingStdout.trim();
|
|
998
|
+
if (upstream) {
|
|
999
|
+
await runSpawnProcess(
|
|
1000
|
+
"git",
|
|
1001
|
+
["-C", worktreeConfig.projectRepoRoot, "merge", "--ff-only", upstream],
|
|
1002
|
+
{ cwd: worktreeConfig.projectRepoRoot, timeoutMs: WORKTREE_SYNC_TIMEOUT_MS },
|
|
1003
|
+
).catch(() => {});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
} catch (_syncError) {
|
|
1008
|
+
// sync_branch is best-effort; proceed with worktree creation even if sync fails
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
891
1012
|
mkdirSyncFn(path.dirname(worktreeRoot), { recursive: true });
|
|
892
1013
|
try {
|
|
893
1014
|
await runSpawnProcess(
|
|
@@ -962,6 +1083,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
962
1083
|
process.env.CONDUCTOR_DAEMON_FORCE_KILL_WAIT_MS,
|
|
963
1084
|
2_000,
|
|
964
1085
|
);
|
|
1086
|
+
const WORKTREE_SYNC_TIMEOUT_MS = parsePositiveInt(
|
|
1087
|
+
process.env.CONDUCTOR_WORKTREE_SYNC_TIMEOUT_MS,
|
|
1088
|
+
5_000,
|
|
1089
|
+
);
|
|
965
1090
|
const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
|
|
966
1091
|
process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
|
|
967
1092
|
1000,
|
|
@@ -1073,87 +1198,94 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1073
1198
|
|
|
1074
1199
|
const LOCK_FILE = path.join(WORKSPACE_ROOT, "daemon.pid");
|
|
1075
1200
|
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
|
-
|
|
1201
|
+
if (skipPidLockCheck) {
|
|
1202
|
+
log("CONDUCTOR_TUI_DEBUG enabled; skipping daemon PID lock enforcement");
|
|
1203
|
+
} else {
|
|
1204
|
+
if (existsSyncFn(LOCK_FILE)) {
|
|
1205
|
+
const lockState = readLockState();
|
|
1206
|
+
const pid = lockState?.pid;
|
|
1207
|
+
if (pid) {
|
|
1208
|
+
const handoffMatched = hasMatchingLockHandoff(lockState);
|
|
1209
|
+
try {
|
|
1210
|
+
if (handoffMatched) {
|
|
1211
|
+
log(`Taking over daemon lock from PID ${pid} via handoff`);
|
|
1212
|
+
} else {
|
|
1213
|
+
const alive = isProcessAlive(pid);
|
|
1214
|
+
if (alive) {
|
|
1215
|
+
if (config.FORCE) {
|
|
1216
|
+
log(`Force enabled: stopping existing daemon PID ${pid}`);
|
|
1217
|
+
let alreadyExited = false;
|
|
1218
|
+
try {
|
|
1219
|
+
killFn(pid, "SIGTERM");
|
|
1220
|
+
} catch (killErr) {
|
|
1221
|
+
if (killErr?.code === "ESRCH") {
|
|
1222
|
+
alreadyExited = true;
|
|
1223
|
+
} else {
|
|
1224
|
+
logError(`Failed to stop existing daemon PID ${pid}: ${killErr.message}`);
|
|
1225
|
+
return exitAndReturn(1);
|
|
1226
|
+
}
|
|
1098
1227
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1228
|
+
try {
|
|
1229
|
+
let exited = alreadyExited || waitForProcessExitSync(pid, DAEMON_FORCE_STOP_GRACE_MS);
|
|
1230
|
+
if (!exited) {
|
|
1231
|
+
log(
|
|
1232
|
+
`Existing daemon PID ${pid} did not exit within ${DAEMON_FORCE_STOP_GRACE_MS}ms; sending SIGKILL`,
|
|
1233
|
+
);
|
|
1234
|
+
try {
|
|
1235
|
+
killFn(pid, "SIGKILL");
|
|
1236
|
+
} catch (killErr) {
|
|
1237
|
+
if (killErr?.code !== "ESRCH") {
|
|
1238
|
+
logError(`Failed to force kill existing daemon PID ${pid}: ${killErr.message}`);
|
|
1239
|
+
return exitAndReturn(1);
|
|
1240
|
+
}
|
|
1112
1241
|
}
|
|
1242
|
+
exited = waitForProcessExitSync(pid, DAEMON_FORCE_KILL_WAIT_MS);
|
|
1113
1243
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1244
|
+
if (!exited) {
|
|
1245
|
+
logError(`Existing daemon PID ${pid} is still running after force restart; please stop it manually.`);
|
|
1246
|
+
return exitAndReturn(1);
|
|
1247
|
+
}
|
|
1248
|
+
} catch (checkErr) {
|
|
1249
|
+
logError(`Failed to verify daemon PID ${pid}: ${checkErr.message}`);
|
|
1118
1250
|
return exitAndReturn(1);
|
|
1119
1251
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1252
|
+
log("Removing lock file after force stop");
|
|
1253
|
+
if (existsSyncFn(LOCK_FILE)) {
|
|
1254
|
+
unlinkSyncFn(LOCK_FILE);
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
logError(`Daemon already running with PID ${pid}`);
|
|
1122
1258
|
return exitAndReturn(1);
|
|
1123
1259
|
}
|
|
1124
|
-
log("Removing lock file after force stop");
|
|
1125
|
-
if (existsSyncFn(LOCK_FILE)) {
|
|
1126
|
-
unlinkSyncFn(LOCK_FILE);
|
|
1127
|
-
}
|
|
1128
1260
|
} else {
|
|
1129
|
-
|
|
1130
|
-
|
|
1261
|
+
log("Removing stale lock file");
|
|
1262
|
+
unlinkSyncFn(LOCK_FILE);
|
|
1131
1263
|
}
|
|
1264
|
+
}
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
if (handoffMatched) {
|
|
1267
|
+
log(`Taking over daemon lock from PID ${pid} via handoff`);
|
|
1132
1268
|
} else {
|
|
1133
|
-
|
|
1134
|
-
|
|
1269
|
+
logError(`Daemon already running with PID ${pid} (access denied)`);
|
|
1270
|
+
return exitAndReturn(1);
|
|
1135
1271
|
}
|
|
1136
1272
|
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
} else {
|
|
1141
|
-
logError(`Daemon already running with PID ${pid} (access denied)`);
|
|
1142
|
-
return exitAndReturn(1);
|
|
1143
|
-
}
|
|
1273
|
+
} else {
|
|
1274
|
+
log("Removing malformed lock file");
|
|
1275
|
+
unlinkSyncFn(LOCK_FILE);
|
|
1144
1276
|
}
|
|
1145
|
-
} else {
|
|
1146
|
-
log("Removing malformed lock file");
|
|
1147
|
-
unlinkSyncFn(LOCK_FILE);
|
|
1148
1277
|
}
|
|
1278
|
+
writeFileSyncFn(LOCK_FILE, process.pid.toString());
|
|
1149
1279
|
}
|
|
1150
|
-
writeFileSyncFn(LOCK_FILE, process.pid.toString());
|
|
1151
1280
|
} catch (err) {
|
|
1152
1281
|
logError("Failed to acquire lock:", err);
|
|
1153
1282
|
return exitAndReturn(1);
|
|
1154
1283
|
}
|
|
1155
1284
|
|
|
1156
1285
|
const cleanupLock = () => {
|
|
1286
|
+
if (skipPidLockCheck) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1157
1289
|
try {
|
|
1158
1290
|
if (existsSyncFn(LOCK_FILE)) {
|
|
1159
1291
|
const lockState = readLockState();
|
|
@@ -1167,6 +1299,9 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1167
1299
|
};
|
|
1168
1300
|
|
|
1169
1301
|
const writeLockHandoff = ({ handoffToken, handoffFromPid, handoffExpiresAt }) => {
|
|
1302
|
+
if (skipPidLockCheck) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1170
1305
|
writeFileSyncFn(
|
|
1171
1306
|
LOCK_FILE,
|
|
1172
1307
|
JSON.stringify({
|
|
@@ -2874,6 +3009,30 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2874
3009
|
logError(`Failed to report agent_command_ack(cleanup_task_worktree) for ${taskId}: ${error?.message || error}`);
|
|
2875
3010
|
});
|
|
2876
3011
|
|
|
3012
|
+
if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
|
|
3013
|
+
if (forceCleanup) {
|
|
3014
|
+
const stopStarted = stopActiveTaskProcess(taskId, {
|
|
3015
|
+
reason: "cleanup_task_worktree",
|
|
3016
|
+
suppressExitStatusReport: true,
|
|
3017
|
+
});
|
|
3018
|
+
if (stopStarted) {
|
|
3019
|
+
const stopped = await waitForTaskToStop(taskId);
|
|
3020
|
+
if (!stopped && (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId))) {
|
|
3021
|
+
await reportTaskWorktreeCleanupResult({
|
|
3022
|
+
requestId,
|
|
3023
|
+
taskId,
|
|
3024
|
+
worktreeBranch: worktreeConfig.worktreeBranch,
|
|
3025
|
+
cleaned: false,
|
|
3026
|
+
error: "Task is still active",
|
|
3027
|
+
}).catch((error) => {
|
|
3028
|
+
logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${error?.message || error}`);
|
|
3029
|
+
});
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
2877
3036
|
if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
|
|
2878
3037
|
await reportTaskWorktreeCleanupResult({
|
|
2879
3038
|
requestId,
|
|
@@ -2891,6 +3050,19 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2891
3050
|
worktreeConfig.projectWorkspacePath,
|
|
2892
3051
|
worktreeConfig.worktreeId,
|
|
2893
3052
|
);
|
|
3053
|
+
if (!isSafeTaskWorktreeRoot(worktreeConfig.projectWorkspacePath, worktreeRoot)) {
|
|
3054
|
+
await reportTaskWorktreeCleanupResult({
|
|
3055
|
+
requestId,
|
|
3056
|
+
taskId,
|
|
3057
|
+
worktreeBranch: worktreeConfig.worktreeBranch,
|
|
3058
|
+
removedPath: worktreeRoot,
|
|
3059
|
+
cleaned: false,
|
|
3060
|
+
error: `Refusing to remove unsafe worktree path: ${worktreeRoot}`,
|
|
3061
|
+
}).catch((error) => {
|
|
3062
|
+
logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${error?.message || error}`);
|
|
3063
|
+
});
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
2894
3066
|
const worktreeCwd = resolveTaskWorktreeCwd(worktreeRoot, worktreeConfig.projectRelativePath);
|
|
2895
3067
|
const statusCwd = existsSyncFn(worktreeCwd) ? worktreeCwd : worktreeRoot;
|
|
2896
3068
|
|
|
@@ -3367,12 +3539,14 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3367
3539
|
};
|
|
3368
3540
|
} else {
|
|
3369
3541
|
const snapshot = await Promise.resolve(resolveProjectSnapshotFn(resolvedPath));
|
|
3542
|
+
const effectiveWorkspace =
|
|
3543
|
+
typeof snapshot?.projectRoot === "string" && snapshot.projectRoot.trim()
|
|
3544
|
+
? snapshot.projectRoot.trim()
|
|
3545
|
+
: resolvedPath;
|
|
3546
|
+
ensureProjectSettingsTemplate(effectiveWorkspace);
|
|
3370
3547
|
result = {
|
|
3371
3548
|
...result,
|
|
3372
|
-
workspacePath:
|
|
3373
|
-
typeof snapshot?.projectRoot === "string" && snapshot.projectRoot.trim()
|
|
3374
|
-
? snapshot.projectRoot.trim()
|
|
3375
|
-
: resolvedPath,
|
|
3549
|
+
workspacePath: effectiveWorkspace,
|
|
3376
3550
|
repoRoot:
|
|
3377
3551
|
typeof snapshot?.repoRoot === "string" && snapshot.repoRoot.trim()
|
|
3378
3552
|
? snapshot.repoRoot.trim()
|
|
@@ -3420,57 +3594,25 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3420
3594
|
}
|
|
3421
3595
|
}
|
|
3422
3596
|
|
|
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
|
-
|
|
3597
|
+
function stopActiveTaskProcess(
|
|
3598
|
+
taskId,
|
|
3599
|
+
{
|
|
3600
|
+
reason = "",
|
|
3601
|
+
suppressExitStatusReport = false,
|
|
3602
|
+
} = {},
|
|
3603
|
+
) {
|
|
3462
3604
|
const processRecord = activeTaskProcesses.get(taskId);
|
|
3463
3605
|
const ptyRecord = activePtySessions.get(taskId);
|
|
3464
3606
|
if ((!processRecord || !processRecord.child) && !ptyRecord) {
|
|
3465
|
-
|
|
3466
|
-
sendStopAck(false);
|
|
3467
|
-
return;
|
|
3607
|
+
return false;
|
|
3468
3608
|
}
|
|
3469
3609
|
|
|
3470
|
-
|
|
3471
|
-
|
|
3610
|
+
if (suppressExitStatusReport) {
|
|
3611
|
+
suppressedExitStatusReports.add(taskId);
|
|
3612
|
+
}
|
|
3472
3613
|
|
|
3473
|
-
|
|
3614
|
+
const reasonSuffix = reason ? ` (${reason})` : "";
|
|
3615
|
+
log(`Stopping task ${taskId}${reasonSuffix}`);
|
|
3474
3616
|
|
|
3475
3617
|
const activeRecord = processRecord || ptyRecord;
|
|
3476
3618
|
if (activeRecord?.stopForceKillTimer) {
|
|
@@ -3528,6 +3670,75 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3528
3670
|
if (typeof activeRecord.stopForceKillTimer?.unref === "function") {
|
|
3529
3671
|
activeRecord.stopForceKillTimer.unref();
|
|
3530
3672
|
}
|
|
3673
|
+
|
|
3674
|
+
return true;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
async function waitForTaskToStop(taskId, timeoutMs = DAEMON_FORCE_STOP_GRACE_MS) {
|
|
3678
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
3679
|
+
|
|
3680
|
+
while (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
|
|
3681
|
+
const remainingMs = deadline - Date.now();
|
|
3682
|
+
if (remainingMs <= 0) {
|
|
3683
|
+
return false;
|
|
3684
|
+
}
|
|
3685
|
+
await new Promise((resolve) =>
|
|
3686
|
+
setTimeout(resolve, Math.min(DAEMON_FORCE_STOP_POLL_INTERVAL_MS, remainingMs)),
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
return true;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function handleStopTask(payload) {
|
|
3694
|
+
const taskId = payload?.task_id;
|
|
3695
|
+
if (!taskId) return;
|
|
3696
|
+
const requestId = payload?.request_id ? String(payload.request_id) : "";
|
|
3697
|
+
if (requestId && !markRequestSeen(requestId)) {
|
|
3698
|
+
log(`Duplicate stop_task ignored for ${taskId} (request_id=${requestId})`);
|
|
3699
|
+
sendAgentCommandAck({
|
|
3700
|
+
requestId,
|
|
3701
|
+
taskId,
|
|
3702
|
+
eventType: "stop_task",
|
|
3703
|
+
accepted: true,
|
|
3704
|
+
}).catch(() => {});
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
const sendStopAck = (accepted) => {
|
|
3709
|
+
if (!requestId) return;
|
|
3710
|
+
client
|
|
3711
|
+
.sendJson({
|
|
3712
|
+
type: "task_stop_ack",
|
|
3713
|
+
payload: {
|
|
3714
|
+
task_id: taskId,
|
|
3715
|
+
request_id: requestId,
|
|
3716
|
+
accepted: Boolean(accepted),
|
|
3717
|
+
},
|
|
3718
|
+
})
|
|
3719
|
+
.catch((err) => {
|
|
3720
|
+
logError(`Failed to report task_stop_ack for ${taskId}: ${err?.message || err}`);
|
|
3721
|
+
});
|
|
3722
|
+
sendAgentCommandAck({
|
|
3723
|
+
requestId,
|
|
3724
|
+
taskId,
|
|
3725
|
+
eventType: "stop_task",
|
|
3726
|
+
accepted,
|
|
3727
|
+
}).catch((err) => {
|
|
3728
|
+
logError(`Failed to report agent_command_ack(stop_task) for ${taskId}: ${err?.message || err}`);
|
|
3729
|
+
});
|
|
3730
|
+
};
|
|
3731
|
+
|
|
3732
|
+
const processRecord = activeTaskProcesses.get(taskId);
|
|
3733
|
+
const ptyRecord = activePtySessions.get(taskId);
|
|
3734
|
+
if ((!processRecord || !processRecord.child) && !ptyRecord) {
|
|
3735
|
+
log(`Stop requested for task ${taskId}, but no active process found`);
|
|
3736
|
+
sendStopAck(false);
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
sendStopAck(true);
|
|
3741
|
+
stopActiveTaskProcess(taskId, { reason: payload?.reason });
|
|
3531
3742
|
}
|
|
3532
3743
|
|
|
3533
3744
|
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 = {}) {
|