@lazyingart/agintiflow 0.20.119 → 0.20.121
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/package.json +1 -1
- package/scripts/smoke-cli-chat.js +90 -3
- package/scripts/smoke-coding-tools.js +12 -0
- package/src/agent-runner.js +4 -4
- package/src/auxiliary-tools.js +49 -9
- package/src/interactive-cli.js +116 -13
- package/src/model-client.js +1 -0
- package/src/permission-advice.js +1 -1
- package/src/tmux-tools.js +13 -9
- package/src/tool-wrappers.js +91 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.121",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -110,6 +110,88 @@ function runCli(args, inputText) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function shellQuote(value = "") {
|
|
114
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function tmux(args, options = {}) {
|
|
118
|
+
return execFileSync("tmux", args, {
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
timeout: options.timeout || 12000,
|
|
121
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tmuxCapture(session) {
|
|
126
|
+
try {
|
|
127
|
+
return tmux(["capture-pane", "-t", session, "-p", "-S", "-200"], { timeout: 5000 });
|
|
128
|
+
} catch {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function waitForTmuxText(session, pattern, timeoutMs = 12000) {
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
while (Date.now() - started < timeoutMs) {
|
|
136
|
+
const pane = tmuxCapture(session);
|
|
137
|
+
if (pattern.test(pane)) return pane;
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
139
|
+
}
|
|
140
|
+
return tmuxCapture(session);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function runTmuxInterruptSmoke({ key, expected }) {
|
|
144
|
+
const session = `aginti-interrupt-${process.pid}-${key.toLowerCase().replace(/[^a-z0-9]+/g, "")}`;
|
|
145
|
+
const command = [
|
|
146
|
+
"env",
|
|
147
|
+
`AGINTIFLOW_HOME=${shellQuote(agintiflowHome)}`,
|
|
148
|
+
"AGINTIFLOW_RUNTIME_DIR=",
|
|
149
|
+
"AGINTI_LANGUAGE=en",
|
|
150
|
+
"AGINTI_INTERRUPT_FORCE_EXIT_MS=2500",
|
|
151
|
+
shellQuote(process.execPath),
|
|
152
|
+
shellQuote(binPath),
|
|
153
|
+
"chat",
|
|
154
|
+
"--provider",
|
|
155
|
+
"mock",
|
|
156
|
+
"--routing",
|
|
157
|
+
"manual",
|
|
158
|
+
"--profile",
|
|
159
|
+
"code",
|
|
160
|
+
"--allow-shell",
|
|
161
|
+
"-s",
|
|
162
|
+
"normal",
|
|
163
|
+
].join(" ");
|
|
164
|
+
const shellCommand = `cd ${shellQuote(tempRoot)} && ${command}`;
|
|
165
|
+
try {
|
|
166
|
+
tmux(["kill-session", "-t", session], { timeout: 2000 });
|
|
167
|
+
} catch {
|
|
168
|
+
// Session does not exist.
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
tmux(["new-session", "-d", "-s", session, "bash", "-lc", `${shellCommand}; printf '\\nEXIT:%s\\n' "$?"; sleep 3`]);
|
|
172
|
+
let pane = await waitForTmuxText(session, /user>/, 10000);
|
|
173
|
+
if (!/user>/.test(pane)) throw new Error(`interrupt smoke did not reach prompt before ${key}\n${pane}`);
|
|
174
|
+
tmux(["send-keys", "-t", session, "-l", "sleep interruption smoke"]);
|
|
175
|
+
tmux(["send-keys", "-t", session, "Enter"]);
|
|
176
|
+
pane = await waitForTmuxText(session, /run_command|model_wait|Docker:/, 12000);
|
|
177
|
+
if (!/run_command|model_wait|Docker:/.test(pane)) throw new Error(`interrupt smoke did not start a run before ${key}\n${pane}`);
|
|
178
|
+
tmux(["send-keys", "-t", session, key]);
|
|
179
|
+
pane = await waitForTmuxText(session, expected, 10000);
|
|
180
|
+
if (!expected.test(pane)) throw new Error(`interrupt smoke did not observe ${expected} after ${key}\n${pane}`);
|
|
181
|
+
if (key === "Escape" && !/EXIT:/.test(pane)) {
|
|
182
|
+
tmux(["send-keys", "-t", session, "-l", "/exit"]);
|
|
183
|
+
tmux(["send-keys", "-t", session, "Enter"]);
|
|
184
|
+
await waitForTmuxText(session, /EXIT:0/, 8000);
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
try {
|
|
188
|
+
tmux(["kill-session", "-t", session], { timeout: 2000 });
|
|
189
|
+
} catch {
|
|
190
|
+
// Session may already have exited.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
113
195
|
try {
|
|
114
196
|
const translatedHelpKeys = [
|
|
115
197
|
"helpHelp",
|
|
@@ -363,8 +445,8 @@ try {
|
|
|
363
445
|
if (classifyEscapeAction({ active: false }) !== "noop") {
|
|
364
446
|
throw new Error("idle Esc should not redraw or clear the prompt");
|
|
365
447
|
}
|
|
366
|
-
if (classifyEscapeAction({ active: true, pendingAsap: [{ content: "apply now" }] }) !== "
|
|
367
|
-
throw new Error("active Esc should
|
|
448
|
+
if (classifyEscapeAction({ active: true, pendingAsap: [{ content: "apply now" }] }) !== "abort") {
|
|
449
|
+
throw new Error("active Esc should always abort, even when ASAP pipe messages are pending");
|
|
368
450
|
}
|
|
369
451
|
if (classifyEscapeAction({ active: true, pendingAsap: [] }) !== "abort") {
|
|
370
452
|
throw new Error("active Esc should abort when no ASAP pipe messages are pending");
|
|
@@ -664,6 +746,9 @@ try {
|
|
|
664
746
|
throw new Error("resume history should render full saved messages instead of compact previews");
|
|
665
747
|
}
|
|
666
748
|
|
|
749
|
+
await runTmuxInterruptSmoke({ key: "Escape", expected: /Active run stopped|Session saved/ });
|
|
750
|
+
await runTmuxInterruptSmoke({ key: "C-c", expected: /EXIT:130|Session stopped by ctrl-c|Interrupted\. Session saved/ });
|
|
751
|
+
|
|
667
752
|
console.log(
|
|
668
753
|
JSON.stringify(
|
|
669
754
|
{
|
|
@@ -702,6 +787,8 @@ try {
|
|
|
702
787
|
"interactive-chat",
|
|
703
788
|
"mock-file-write",
|
|
704
789
|
"run-status",
|
|
790
|
+
"tmux-escape-active-run-stop",
|
|
791
|
+
"tmux-ctrl-c-session-stop",
|
|
705
792
|
"resume-latest",
|
|
706
793
|
"resume-history-metadata",
|
|
707
794
|
"resume-history-prompt-labels",
|
|
@@ -568,6 +568,18 @@ try {
|
|
|
568
568
|
assert(failedOutsidePathAdvice?.failureKind === "workspace-path", "outside host path failure advice was not generated");
|
|
569
569
|
assert(failedOutsidePathAdvice.suggestedCommand.includes("--sandbox-mode host"), "outside path advice did not suggest host mode");
|
|
570
570
|
assert(!failedOutsidePathAdvice.suggestedCommand.includes("aginti run --sandbox host"), "outside path advice used legacy sandbox syntax");
|
|
571
|
+
const missingReadonlyHostPathAdvice = buildFailedCommandAdvice({
|
|
572
|
+
args: { command: "ls /home/lachlan/ProjectsLFS/ProteinStructure" },
|
|
573
|
+
commandPolicy: evaluateCommandPolicy("ls /home/lachlan/ProjectsLFS/ProteinStructure", dockerWorkspacePolicy),
|
|
574
|
+
commandResult: {
|
|
575
|
+
ok: false,
|
|
576
|
+
stdout: "",
|
|
577
|
+
stderr: "ls: cannot access '/home/lachlan/ProjectsLFS/ProteinStructure': No such file or directory",
|
|
578
|
+
},
|
|
579
|
+
config: dockerWorkspacePolicy,
|
|
580
|
+
state: { sessionId: "coding-readonly-missing-host-path-smoke" },
|
|
581
|
+
});
|
|
582
|
+
assert(missingReadonlyHostPathAdvice === null, "missing read-only host path should not trigger danger-mode permission advice");
|
|
571
583
|
assert(
|
|
572
584
|
shouldRunParallelScouts(
|
|
573
585
|
{
|
package/src/agent-runner.js
CHANGED
|
@@ -1661,9 +1661,9 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
1661
1661
|
break;
|
|
1662
1662
|
case "wait":
|
|
1663
1663
|
if (browserState.page) {
|
|
1664
|
-
await browserState.page.waitForTimeout(Number.isFinite(args.ms) ? Number(args.ms) : 1000);
|
|
1664
|
+
await abortable(browserState.page.waitForTimeout(Number.isFinite(args.ms) ? Number(args.ms) : 1000), config.abortSignal);
|
|
1665
1665
|
} else {
|
|
1666
|
-
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000));
|
|
1666
|
+
await abortable(new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000)), config.abortSignal);
|
|
1667
1667
|
}
|
|
1668
1668
|
break;
|
|
1669
1669
|
case "inspect_project":
|
|
@@ -1749,14 +1749,14 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
1749
1749
|
return result;
|
|
1750
1750
|
}
|
|
1751
1751
|
case "tmux_list_sessions": {
|
|
1752
|
-
const result = await listTmuxSessions(args);
|
|
1752
|
+
const result = await listTmuxSessions(args, config);
|
|
1753
1753
|
const eventResult = sanitizeToolResult(result);
|
|
1754
1754
|
await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
|
1755
1755
|
observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
|
1756
1756
|
return result;
|
|
1757
1757
|
}
|
|
1758
1758
|
case "tmux_capture_pane": {
|
|
1759
|
-
const result = await captureTmuxPane(args);
|
|
1759
|
+
const result = await captureTmuxPane(args, config);
|
|
1760
1760
|
const eventResult = sanitizeToolResult(result);
|
|
1761
1761
|
await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
|
1762
1762
|
observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
package/src/auxiliary-tools.js
CHANGED
|
@@ -164,11 +164,43 @@ async function writeJson(filePath, payload) {
|
|
|
164
164
|
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
function throwIfAborted(signal) {
|
|
168
|
+
if (!signal?.aborted) return;
|
|
169
|
+
const error = signal.reason instanceof Error ? signal.reason : new Error("Operation interrupted by user.");
|
|
170
|
+
error.name = error.name || "AbortError";
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sleepAbortable(ms, signal) {
|
|
175
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
176
|
+
throwIfAborted(signal);
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const onAbort = () => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Operation interrupted by user."));
|
|
181
|
+
};
|
|
182
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
cleanup();
|
|
185
|
+
resolve();
|
|
186
|
+
}, ms);
|
|
187
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function requestJson(url, payload, apiKey, { timeoutMs = 300000, retries = 2, signal = null } = {}) {
|
|
168
192
|
let lastError = null;
|
|
169
193
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
194
|
+
throwIfAborted(signal);
|
|
170
195
|
const controller = new AbortController();
|
|
171
|
-
const
|
|
196
|
+
const onAbort = () => controller.abort(signal.reason || new Error("Operation interrupted by user."));
|
|
197
|
+
let timedOut = false;
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
timedOut = true;
|
|
200
|
+
controller.abort();
|
|
201
|
+
}, timeoutMs);
|
|
202
|
+
if (signal?.aborted) onAbort();
|
|
203
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
172
204
|
try {
|
|
173
205
|
const response = await fetch(url, {
|
|
174
206
|
method: "POST",
|
|
@@ -187,25 +219,29 @@ async function requestJson(url, payload, apiKey, { timeoutMs = 300000, retries =
|
|
|
187
219
|
}
|
|
188
220
|
return parseJsonResponse(text);
|
|
189
221
|
} catch (error) {
|
|
222
|
+
if (signal?.aborted || error?.code === "ABORT_ERR" || (error?.name === "AbortError" && !timedOut)) throw error;
|
|
190
223
|
lastError = error;
|
|
191
224
|
const retryable = TRANSIENT_HTTP_CODES.has(Number(error?.status)) || /aborted|timeout|fetch failed/i.test(String(error?.message || ""));
|
|
192
225
|
if (!retryable || attempt >= retries) throw error;
|
|
193
|
-
await
|
|
226
|
+
await sleepAbortable(2000 + attempt * 2000, signal);
|
|
194
227
|
} finally {
|
|
195
228
|
clearTimeout(timer);
|
|
229
|
+
signal?.removeEventListener("abort", onAbort);
|
|
196
230
|
}
|
|
197
231
|
}
|
|
198
232
|
throw lastError || new Error("Image API request failed.");
|
|
199
233
|
}
|
|
200
234
|
|
|
201
|
-
async function pollResult({ host, taskId, apiKey, outputDir, intervalMs = 5000, timeoutMs = 900000, requestTimeoutMs = 300000 }) {
|
|
235
|
+
async function pollResult({ host, taskId, apiKey, outputDir, intervalMs = 5000, timeoutMs = 900000, requestTimeoutMs = 300000, signal = null }) {
|
|
202
236
|
const started = Date.now();
|
|
203
237
|
const pollUrl = `${host.replace(/\/+$/, "")}/v1/draw/result`;
|
|
204
238
|
let attempt = 0;
|
|
205
239
|
while (true) {
|
|
240
|
+
throwIfAborted(signal);
|
|
206
241
|
const result = await requestJson(pollUrl, { id: taskId }, apiKey, {
|
|
207
242
|
timeoutMs: requestTimeoutMs,
|
|
208
243
|
retries: 4,
|
|
244
|
+
signal,
|
|
209
245
|
});
|
|
210
246
|
await writeJson(path.join(outputDir, "result_response.json"), result);
|
|
211
247
|
|
|
@@ -214,15 +250,16 @@ async function pollResult({ host, taskId, apiKey, outputDir, intervalMs = 5000,
|
|
|
214
250
|
if (status === "succeeded" || status === "failed") return result;
|
|
215
251
|
if (Date.now() - started > timeoutMs) throw new Error(`Image generation polling timed out for task ${taskId}.`);
|
|
216
252
|
attempt += 1;
|
|
217
|
-
await
|
|
253
|
+
await sleepAbortable(Math.min(intervalMs + attempt * 250, 12000), signal);
|
|
218
254
|
}
|
|
219
255
|
}
|
|
220
256
|
|
|
221
|
-
async function downloadImage(url, destination) {
|
|
257
|
+
async function downloadImage(url, destination, signal = null) {
|
|
222
258
|
const response = await fetch(url, {
|
|
223
259
|
headers: {
|
|
224
260
|
"User-Agent": "AgInTiFlow/1.0",
|
|
225
261
|
},
|
|
262
|
+
signal,
|
|
226
263
|
});
|
|
227
264
|
if (!response.ok) throw new Error(`Image download failed with HTTP ${response.status}.`);
|
|
228
265
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -268,7 +305,7 @@ function veniceBaseUrl(value = "") {
|
|
|
268
305
|
.replace(/\/+$/, "");
|
|
269
306
|
}
|
|
270
307
|
|
|
271
|
-
async function generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath }) {
|
|
308
|
+
async function generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, signal = null }) {
|
|
272
309
|
const apiKey = veniceKey();
|
|
273
310
|
if (!apiKey) {
|
|
274
311
|
throw new Error("Missing VENICE_API_KEY. Run `aginti login venice` or `aginti keys set venice --stdin`.");
|
|
@@ -295,6 +332,7 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
|
|
|
295
332
|
const resultPayload = await requestJson(`${base}/image/generate`, payload, apiKey, {
|
|
296
333
|
timeoutMs: Number(args.requestTimeoutMs) || 300000,
|
|
297
334
|
retries: 2,
|
|
335
|
+
signal,
|
|
298
336
|
});
|
|
299
337
|
await writeJson(path.join(target.absolutePath, "venice_result_response.json"), {
|
|
300
338
|
id: resultPayload.id || "",
|
|
@@ -419,7 +457,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
419
457
|
}
|
|
420
458
|
|
|
421
459
|
if (provider === "venice") {
|
|
422
|
-
return generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath });
|
|
460
|
+
return generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, signal: config.abortSignal });
|
|
423
461
|
}
|
|
424
462
|
|
|
425
463
|
const apiKey = grsaiKey();
|
|
@@ -431,6 +469,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
431
469
|
const submitPayload = await requestJson(submitUrl, payload, apiKey, {
|
|
432
470
|
timeoutMs: Number(args.requestTimeoutMs) || 300000,
|
|
433
471
|
retries: 2,
|
|
472
|
+
signal: config.abortSignal,
|
|
434
473
|
});
|
|
435
474
|
await writeJson(path.join(target.absolutePath, "submit_response.json"), submitPayload);
|
|
436
475
|
const taskId = submitPayload?.data?.id || submitPayload?.id;
|
|
@@ -449,6 +488,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
449
488
|
intervalMs: Number(args.pollIntervalMs) || 5000,
|
|
450
489
|
timeoutMs: Number(args.pollTimeoutMs) || 900000,
|
|
451
490
|
requestTimeoutMs: Number(args.requestTimeoutMs) || 300000,
|
|
491
|
+
signal: config.abortSignal,
|
|
452
492
|
});
|
|
453
493
|
|
|
454
494
|
const status = resultPayload.status || resultPayload?.data?.status;
|
|
@@ -468,7 +508,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
468
508
|
const suffix = path.extname(parsed.pathname) || ".png";
|
|
469
509
|
const filename = urls.length === 1 ? `${outputStem}${suffix}` : `${outputStem}_${String(index + 1).padStart(2, "0")}${suffix}`;
|
|
470
510
|
const absolutePath = path.join(target.absolutePath, filename);
|
|
471
|
-
const info = await downloadImage(urls[index], absolutePath);
|
|
511
|
+
const info = await downloadImage(urls[index], absolutePath, config.abortSignal);
|
|
472
512
|
const relativePath = path.posix.join(target.relativePath, filename);
|
|
473
513
|
imagePaths.push(relativePath);
|
|
474
514
|
downloads.push({ path: relativePath, ...info });
|
package/src/interactive-cli.js
CHANGED
|
@@ -210,6 +210,10 @@ export class ComposerHistory {
|
|
|
210
210
|
|
|
211
211
|
const promptHistory = new ComposerHistory();
|
|
212
212
|
let activeRunInput = null;
|
|
213
|
+
let activeRunController = null;
|
|
214
|
+
let activeRunExitAfterStop = false;
|
|
215
|
+
let activeRunAbortSource = "";
|
|
216
|
+
let activeRunForceExitTimer = null;
|
|
213
217
|
let cliLanguage = resolveLanguage();
|
|
214
218
|
|
|
215
219
|
function setCliLanguage(language = "") {
|
|
@@ -230,6 +234,24 @@ function sleep(ms) {
|
|
|
230
234
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
231
235
|
}
|
|
232
236
|
|
|
237
|
+
function interruptForceExitMs() {
|
|
238
|
+
const value = Number(process.env.AGINTI_INTERRUPT_FORCE_EXIT_MS || 8000);
|
|
239
|
+
return Number.isFinite(value) && value > 0 ? value : 8000;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function restoreTerminalForExit() {
|
|
243
|
+
try {
|
|
244
|
+
if (typeof input.setRawMode === "function" && input.isRaw) input.setRawMode(false);
|
|
245
|
+
} catch {
|
|
246
|
+
// Terminal may already be closed.
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
output.write(ansi.cursorShow);
|
|
250
|
+
} catch {
|
|
251
|
+
// Ignore closed stdout.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
const ROLE_LABEL_WIDTH = "aginti".length;
|
|
234
256
|
const PROMPT_LABEL_WIDTH = "aginti>".length;
|
|
235
257
|
|
|
@@ -1173,16 +1195,83 @@ function removeAt(buffer, cursor) {
|
|
|
1173
1195
|
};
|
|
1174
1196
|
}
|
|
1175
1197
|
|
|
1176
|
-
function createAbortError(message = "Aborted with Ctrl+C") {
|
|
1198
|
+
function createAbortError(message = "Aborted with Ctrl+C", options = {}) {
|
|
1177
1199
|
const error = new Error(message);
|
|
1178
1200
|
error.code = "ABORT_ERR";
|
|
1179
1201
|
error.name = "AbortError";
|
|
1202
|
+
error.source = options.source || "";
|
|
1203
|
+
error.exitSession = Boolean(options.exitSession);
|
|
1180
1204
|
return error;
|
|
1181
1205
|
}
|
|
1182
1206
|
|
|
1183
1207
|
export function classifyEscapeAction({ active = false, pendingAsap = [] } = {}) {
|
|
1184
1208
|
if (!active) return "noop";
|
|
1185
|
-
return
|
|
1209
|
+
return "abort";
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function scheduleRunForceExit(reason = "interrupt", forceExitMs = interruptForceExitMs()) {
|
|
1213
|
+
if (activeRunForceExitTimer || !Number.isFinite(forceExitMs) || forceExitMs <= 0) return;
|
|
1214
|
+
activeRunForceExitTimer = setTimeout(() => {
|
|
1215
|
+
restoreTerminalForExit();
|
|
1216
|
+
printSystemLine(`status=force_exit reason=${reason}`);
|
|
1217
|
+
process.exit(130);
|
|
1218
|
+
}, forceExitMs);
|
|
1219
|
+
activeRunForceExitTimer.unref?.();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function clearRunForceExit() {
|
|
1223
|
+
if (!activeRunForceExitTimer) return;
|
|
1224
|
+
clearTimeout(activeRunForceExitTimer);
|
|
1225
|
+
activeRunForceExitTimer = null;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function requestActiveRunStop({ source = "escape", exitSession = false, forceExitMs = interruptForceExitMs() } = {}) {
|
|
1229
|
+
if (!activeRunController) return false;
|
|
1230
|
+
activeRunExitAfterStop = activeRunExitAfterStop || Boolean(exitSession);
|
|
1231
|
+
activeRunAbortSource = activeRunAbortSource || source;
|
|
1232
|
+
const reason = source === "ctrl-c" ? "ctrl-c" : source === "sigterm" ? "sigterm" : "escape";
|
|
1233
|
+
const message =
|
|
1234
|
+
exitSession || source === "ctrl-c"
|
|
1235
|
+
? "Interrupted by ctrl-c; stopping the session."
|
|
1236
|
+
: `Interrupted by ${reason}; stopping the active run.`;
|
|
1237
|
+
if (!activeRunController.signal.aborted) {
|
|
1238
|
+
printStatusEvent({ status: "stopping" }, "interrupt", `${reason}; hard stop in ${Math.ceil(forceExitMs / 1000)}s`);
|
|
1239
|
+
activeRunController.abort(createAbortError(message, { source, exitSession }));
|
|
1240
|
+
} else if (exitSession) {
|
|
1241
|
+
scheduleRunForceExit("second-interrupt", 100);
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
scheduleRunForceExit(reason, forceExitMs);
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function clearActiveRunControl(controller) {
|
|
1249
|
+
if (activeRunController !== controller) return;
|
|
1250
|
+
activeRunController = null;
|
|
1251
|
+
activeRunAbortSource = "";
|
|
1252
|
+
activeRunExitAfterStop = false;
|
|
1253
|
+
clearRunForceExit();
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function installProcessInterruptHandlers() {
|
|
1257
|
+
const onSigint = () => {
|
|
1258
|
+
if (requestActiveRunStop({ source: "ctrl-c", exitSession: true, forceExitMs: interruptForceExitMs() })) return;
|
|
1259
|
+
restoreTerminalForExit();
|
|
1260
|
+
printSystemLine("status=exit reason=ctrl-c");
|
|
1261
|
+
process.exit(130);
|
|
1262
|
+
};
|
|
1263
|
+
const onSigterm = () => {
|
|
1264
|
+
if (requestActiveRunStop({ source: "sigterm", exitSession: true, forceExitMs: 1000 })) return;
|
|
1265
|
+
restoreTerminalForExit();
|
|
1266
|
+
printSystemLine("status=exit reason=sigterm");
|
|
1267
|
+
process.exit(143);
|
|
1268
|
+
};
|
|
1269
|
+
process.on("SIGINT", onSigint);
|
|
1270
|
+
process.on("SIGTERM", onSigterm);
|
|
1271
|
+
return () => {
|
|
1272
|
+
process.off("SIGINT", onSigint);
|
|
1273
|
+
process.off("SIGTERM", onSigterm);
|
|
1274
|
+
};
|
|
1186
1275
|
}
|
|
1187
1276
|
|
|
1188
1277
|
export function activeRunSlashCommandAction(value = "") {
|
|
@@ -1307,7 +1396,7 @@ function readTtyPrompt(options = {}) {
|
|
|
1307
1396
|
if (key.ctrl && key.name === "c") {
|
|
1308
1397
|
clearPromptPanel();
|
|
1309
1398
|
cleanup();
|
|
1310
|
-
reject(createAbortError());
|
|
1399
|
+
reject(createAbortError("Interrupted by ctrl-c.", { source: "ctrl-c", exitSession: true }));
|
|
1311
1400
|
return;
|
|
1312
1401
|
}
|
|
1313
1402
|
if ((key.ctrl && key.name === "j") || key.sequence === "\n") {
|
|
@@ -1679,19 +1768,13 @@ class LiveRunInput {
|
|
|
1679
1768
|
handleKey(str = "", key = {}) {
|
|
1680
1769
|
if (key.ctrl && key.name === "c") {
|
|
1681
1770
|
this.setStatus("stopping · ctrl-c");
|
|
1682
|
-
|
|
1771
|
+
requestActiveRunStop({ source: "ctrl-c", exitSession: true });
|
|
1683
1772
|
return;
|
|
1684
1773
|
}
|
|
1685
1774
|
if (key.name === "escape") {
|
|
1686
1775
|
const action = classifyEscapeAction({ active: true, pendingAsap: this.pendingAsap });
|
|
1687
|
-
if (action === "wait-for-asap") {
|
|
1688
|
-
const count = this.pendingAsap.length;
|
|
1689
|
-
this.setStatus(`running · waiting to apply ${count} asap pipe message${count === 1 ? "" : "s"}`);
|
|
1690
|
-
this.redraw();
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
1776
|
this.setStatus("stopping · escape");
|
|
1694
|
-
|
|
1777
|
+
if (action === "abort") requestActiveRunStop({ source: "escape", exitSession: false });
|
|
1695
1778
|
return;
|
|
1696
1779
|
}
|
|
1697
1780
|
if (key.meta && key.name === "up") {
|
|
@@ -2003,7 +2086,7 @@ function attachRunInterrupts(controller) {
|
|
|
2003
2086
|
if (controller.signal.aborted) return;
|
|
2004
2087
|
const reason = isEscape ? "escape" : "ctrl-c";
|
|
2005
2088
|
printSystemLine(`status=stopping reason=${reason}`);
|
|
2006
|
-
|
|
2089
|
+
requestActiveRunStop({ source: reason, exitSession: isCtrlC });
|
|
2007
2090
|
};
|
|
2008
2091
|
input.on("keypress", handler);
|
|
2009
2092
|
return () => {
|
|
@@ -3599,6 +3682,10 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3599
3682
|
projectSessionsDir: config.projectSessionsDir,
|
|
3600
3683
|
});
|
|
3601
3684
|
const liveInput = new LiveRunInput({ state, store, controller });
|
|
3685
|
+
activeRunController = controller;
|
|
3686
|
+
activeRunExitAfterStop = false;
|
|
3687
|
+
activeRunAbortSource = "";
|
|
3688
|
+
clearRunForceExit();
|
|
3602
3689
|
const liveStarted = liveInput.start();
|
|
3603
3690
|
const detachInterrupts = liveStarted ? () => {} : attachRunInterrupts(controller);
|
|
3604
3691
|
if (liveStarted) {
|
|
@@ -3685,11 +3772,15 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3685
3772
|
} finally {
|
|
3686
3773
|
detachInterrupts();
|
|
3687
3774
|
queuedAfterFinish = await liveInput.stop();
|
|
3775
|
+
clearRunForceExit();
|
|
3688
3776
|
}
|
|
3689
3777
|
if (runError) {
|
|
3690
3778
|
state.status = isAbortError(runError) ? "stopped" : "failed";
|
|
3691
3779
|
state.activeGoal = "";
|
|
3692
3780
|
printSystemLine(`status=${state.status} session=${state.sessionId}`);
|
|
3781
|
+
const exitSession = Boolean(runError.exitSession || activeRunExitAfterStop);
|
|
3782
|
+
clearActiveRunControl(controller);
|
|
3783
|
+
if (isAbortError(runError) && !exitSession) return queuedAfterFinish;
|
|
3693
3784
|
throw runError;
|
|
3694
3785
|
}
|
|
3695
3786
|
state.sessionId = result.sessionId || state.sessionId;
|
|
@@ -3697,9 +3788,16 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3697
3788
|
state.activeGoal = "";
|
|
3698
3789
|
printSystemLine(`status=${state.status} session=${state.sessionId}`);
|
|
3699
3790
|
if (result.stopped && result.reason === "user_interrupt") {
|
|
3700
|
-
|
|
3791
|
+
const exitSession = activeRunExitAfterStop;
|
|
3792
|
+
const abortSource = activeRunAbortSource || "ctrl-c";
|
|
3793
|
+
clearActiveRunControl(controller);
|
|
3794
|
+
if (exitSession) {
|
|
3795
|
+
throw createAbortError("Session stopped by ctrl-c.", { source: abortSource, exitSession: true });
|
|
3796
|
+
}
|
|
3797
|
+
printAgentMessage("Active run stopped. Session saved; enter a new prompt or use /status.");
|
|
3701
3798
|
return [];
|
|
3702
3799
|
}
|
|
3800
|
+
clearActiveRunControl(controller);
|
|
3703
3801
|
if (!result.stopped && latestPermissionAdvice) {
|
|
3704
3802
|
const approvalQueued = await maybeContinueAfterPermissionApproval(latestPermissionAdvice, state, packageDir, {
|
|
3705
3803
|
approvalDepth,
|
|
@@ -3712,6 +3810,7 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3712
3810
|
|
|
3713
3811
|
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
3714
3812
|
const state = createState(args);
|
|
3813
|
+
const detachProcessInterrupts = installProcessInterruptHandlers();
|
|
3715
3814
|
setCliLanguage(state.language);
|
|
3716
3815
|
const rl =
|
|
3717
3816
|
input.isTTY && output.isTTY
|
|
@@ -3738,6 +3837,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
3738
3837
|
} catch (error) {
|
|
3739
3838
|
if (error?.code === "ERR_USE_AFTER_CLOSE") break;
|
|
3740
3839
|
if (isAbortError(error)) {
|
|
3840
|
+
if (error.exitSession) process.exitCode = 130;
|
|
3741
3841
|
await printResumeHint(state);
|
|
3742
3842
|
break;
|
|
3743
3843
|
}
|
|
@@ -3767,6 +3867,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
3767
3867
|
}
|
|
3768
3868
|
} catch (error) {
|
|
3769
3869
|
if (isAbortError(error)) {
|
|
3870
|
+
if (error.exitSession) process.exitCode = 130;
|
|
3770
3871
|
await printResumeHint(state);
|
|
3771
3872
|
break;
|
|
3772
3873
|
}
|
|
@@ -3774,6 +3875,8 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
3774
3875
|
}
|
|
3775
3876
|
}
|
|
3776
3877
|
} finally {
|
|
3878
|
+
detachProcessInterrupts();
|
|
3777
3879
|
rl?.close();
|
|
3880
|
+
restoreTerminalForExit();
|
|
3778
3881
|
}
|
|
3779
3882
|
}
|
package/src/model-client.js
CHANGED
|
@@ -405,6 +405,7 @@ export function normalizeTextToolCallResponse(response) {
|
|
|
405
405
|
|
|
406
406
|
function mockCommandForGoal(goal = "") {
|
|
407
407
|
const text = String(goal).toLowerCase();
|
|
408
|
+
if (/\bsleep\b|\binterrupt\b|\bstall\b|\blong[- ]running\b/.test(text)) return "python3 -c 'import time; time.sleep(20)'";
|
|
408
409
|
if (/\blist\b|folder contents|directory contents|files?/.test(text)) return "ls -la";
|
|
409
410
|
return "pwd";
|
|
410
411
|
}
|
package/src/permission-advice.js
CHANGED
|
@@ -249,7 +249,7 @@ export function looksLikeDockerWorkspacePathFailure(result = {}, config = {}) {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
export function buildFailedCommandAdvice({ args = {}, commandPolicy = {}, commandResult = {}, config = {}, state = {} } = {}) {
|
|
252
|
-
if (looksLikeDockerWorkspacePathFailure(commandResult, config)) {
|
|
252
|
+
if (looksLikeDockerWorkspacePathFailure(commandResult, config) && commandPolicy.category !== "read-only") {
|
|
253
253
|
return {
|
|
254
254
|
...adviceForCategory("workspace-path", {
|
|
255
255
|
toolName: "run_command",
|
package/src/tmux-tools.js
CHANGED
|
@@ -50,6 +50,7 @@ async function runTmux(args, options = {}) {
|
|
|
50
50
|
timeout: options.timeout ?? 12000,
|
|
51
51
|
maxBuffer: options.maxBuffer ?? 220 * 1024,
|
|
52
52
|
env: safeEnv(),
|
|
53
|
+
signal: options.signal,
|
|
53
54
|
});
|
|
54
55
|
return {
|
|
55
56
|
ok: true,
|
|
@@ -57,6 +58,7 @@ async function runTmux(args, options = {}) {
|
|
|
57
58
|
stderr: redactSensitiveText(result.stderr || ""),
|
|
58
59
|
};
|
|
59
60
|
} catch (error) {
|
|
61
|
+
if (error?.name === "AbortError" || error?.code === "ABORT_ERR") throw error;
|
|
60
62
|
const message = redactSensitiveText(error instanceof Error ? error.message : String(error));
|
|
61
63
|
return {
|
|
62
64
|
ok: false,
|
|
@@ -161,10 +163,11 @@ function isShellPaneCommand(command = "") {
|
|
|
161
163
|
return /^(?:ba|z|fi|da|k)?sh$|^fish$/.test(String(command || "").trim());
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
async function getPaneCurrentCommand(target) {
|
|
166
|
+
async function getPaneCurrentCommand(target, config = {}) {
|
|
165
167
|
const result = await runTmux(["display-message", "-p", "-t", target, "#{pane_current_command}"], {
|
|
166
168
|
timeout: 4000,
|
|
167
169
|
maxBuffer: 16 * 1024,
|
|
170
|
+
signal: config.abortSignal,
|
|
168
171
|
});
|
|
169
172
|
if (!result.ok) return "";
|
|
170
173
|
return String(result.stdout || "").trim();
|
|
@@ -215,12 +218,12 @@ function parsePanes(stdout = "") {
|
|
|
215
218
|
});
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
export async function listTmuxSessions(args = {}) {
|
|
221
|
+
export async function listTmuxSessions(args = {}, config = {}) {
|
|
219
222
|
const sessionsResult = await runTmux([
|
|
220
223
|
"list-sessions",
|
|
221
224
|
"-F",
|
|
222
225
|
"#{session_name}|#{session_windows}|#{session_attached}|#{session_created}|#{session_activity}",
|
|
223
|
-
]);
|
|
226
|
+
], { signal: config.abortSignal });
|
|
224
227
|
if (!sessionsResult.ok && /no server running|failed to connect/i.test(`${sessionsResult.stderr} ${sessionsResult.error}`)) {
|
|
225
228
|
return { ok: true, toolName: "tmux_list_sessions", sessions: [], panes: [], summary: "No tmux server is running." };
|
|
226
229
|
}
|
|
@@ -233,7 +236,7 @@ export async function listTmuxSessions(args = {}) {
|
|
|
233
236
|
"-a",
|
|
234
237
|
"-F",
|
|
235
238
|
"#{session_name}:#{window_index}.#{pane_index}|#{pane_current_path}|#{pane_current_command}|#{pane_active}|#{pane_title}",
|
|
236
|
-
]);
|
|
239
|
+
], { signal: config.abortSignal });
|
|
237
240
|
if (panesResult.ok) panes = parsePanes(panesResult.stdout);
|
|
238
241
|
}
|
|
239
242
|
const sessions = parseSessions(sessionsResult.stdout);
|
|
@@ -246,13 +249,14 @@ export async function listTmuxSessions(args = {}) {
|
|
|
246
249
|
};
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
export async function captureTmuxPane(args = {}) {
|
|
252
|
+
export async function captureTmuxPane(args = {}, config = {}) {
|
|
250
253
|
const target = validateTarget(args.target);
|
|
251
254
|
if (!target.ok) return { ok: false, toolName: "tmux_capture_pane", blocked: true, reason: target.reason };
|
|
252
255
|
const lines = clampInteger(args.lines, 80, 1, MAX_CAPTURE_LINES);
|
|
253
256
|
const result = await runTmux(["capture-pane", "-t", target.target, "-p", "-S", `-${lines}`], {
|
|
254
257
|
timeout: 8000,
|
|
255
258
|
maxBuffer: 260 * 1024,
|
|
259
|
+
signal: config.abortSignal,
|
|
256
260
|
});
|
|
257
261
|
if (!result.ok) return { ok: false, toolName: "tmux_capture_pane", target: target.target, error: result.stderr || result.error };
|
|
258
262
|
const content = redactSensitiveText(result.stdout || "").replace(/\s+$/g, "");
|
|
@@ -299,7 +303,7 @@ export async function sendTmuxKeys(args = {}, config = {}) {
|
|
|
299
303
|
const keyArgsForPolicy = [...keys];
|
|
300
304
|
if (enter) keyArgsForPolicy.push("Enter");
|
|
301
305
|
if (text && keyArgsForPolicy.includes("Enter") && shouldApplyDockerShellPolicy(config)) {
|
|
302
|
-
|
|
306
|
+
const paneCommand = await getPaneCurrentCommand(target.target, config);
|
|
303
307
|
if (isShellPaneCommand(paneCommand)) {
|
|
304
308
|
const dockerPolicy = checkDockerShellPolicyForTmuxText(text, config, "tmux text");
|
|
305
309
|
if (!dockerPolicy.ok) {
|
|
@@ -323,14 +327,14 @@ export async function sendTmuxKeys(args = {}, config = {}) {
|
|
|
323
327
|
|
|
324
328
|
const steps = [];
|
|
325
329
|
if (text) {
|
|
326
|
-
const sent = await runTmux(["send-keys", "-t", target.target, "-l", text], { timeout: 8000 });
|
|
330
|
+
const sent = await runTmux(["send-keys", "-t", target.target, "-l", text], { timeout: 8000, signal: config.abortSignal });
|
|
327
331
|
if (!sent.ok) return { ok: false, toolName: "tmux_send_keys", target: target.target, error: sent.stderr || sent.error };
|
|
328
332
|
steps.push("literal-text");
|
|
329
333
|
}
|
|
330
334
|
const keyArgs = [...keys];
|
|
331
335
|
if (enter) keyArgs.push("Enter");
|
|
332
336
|
if (keyArgs.length > 0) {
|
|
333
|
-
const sent = await runTmux(["send-keys", "-t", target.target, ...keyArgs], { timeout: 8000 });
|
|
337
|
+
const sent = await runTmux(["send-keys", "-t", target.target, ...keyArgs], { timeout: 8000, signal: config.abortSignal });
|
|
334
338
|
if (!sent.ok) return { ok: false, toolName: "tmux_send_keys", target: target.target, error: sent.stderr || sent.error };
|
|
335
339
|
steps.push(...keyArgs);
|
|
336
340
|
}
|
|
@@ -395,7 +399,7 @@ export async function startTmuxSession(args = {}, config = {}) {
|
|
|
395
399
|
|
|
396
400
|
const tmuxArgs = ["new-session", "-d", "-s", name.name, "-c", cwd.cwd];
|
|
397
401
|
if (command) tmuxArgs.push(command);
|
|
398
|
-
const result = await runTmux(tmuxArgs, { timeout: 10000 });
|
|
402
|
+
const result = await runTmux(tmuxArgs, { timeout: 10000, signal: config.abortSignal });
|
|
399
403
|
if (!result.ok) return { ok: false, toolName: "tmux_start_session", session: name.name, error: result.stderr || result.error };
|
|
400
404
|
return {
|
|
401
405
|
ok: true,
|
package/src/tool-wrappers.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
2
|
import { getModelPresets } from "./model-routing.js";
|
|
4
3
|
import { redactSensitiveText } from "./redaction.js";
|
|
5
4
|
|
|
6
|
-
const execFile = promisify(execFileCallback);
|
|
7
|
-
|
|
8
5
|
export const WRAPPER_NAMES = ["codex", "claude", "gemini", "copilot", "qwen"];
|
|
9
6
|
export const DEFAULT_WRAPPER_NAME = "codex";
|
|
10
7
|
|
|
@@ -31,6 +28,93 @@ function cleanOutput(value, limit) {
|
|
|
31
28
|
return redactSensitiveText(value).trim().slice(0, limit);
|
|
32
29
|
}
|
|
33
30
|
|
|
31
|
+
function killChildTree(child, signal = "SIGTERM") {
|
|
32
|
+
if (!child || child.killed) return;
|
|
33
|
+
try {
|
|
34
|
+
if (process.platform === "win32") child.kill(signal);
|
|
35
|
+
else process.kill(-child.pid, signal);
|
|
36
|
+
} catch {
|
|
37
|
+
try {
|
|
38
|
+
child.kill(signal);
|
|
39
|
+
} catch {
|
|
40
|
+
// Process may already have exited.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runWrapperProcess(spec, config) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const child = spawn(spec.command, spec.args, {
|
|
48
|
+
cwd: config.commandCwd,
|
|
49
|
+
env: process.env,
|
|
50
|
+
detached: process.platform !== "win32",
|
|
51
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
52
|
+
});
|
|
53
|
+
const timeoutMs = Number(config.wrapperTimeoutMs) || 120000;
|
|
54
|
+
const maxStdout = 512 * 1024;
|
|
55
|
+
const maxStderr = 128 * 1024;
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let stderr = "";
|
|
58
|
+
let settled = false;
|
|
59
|
+
let timedOut = false;
|
|
60
|
+
|
|
61
|
+
const settle = (callback) => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
if (timer) clearTimeout(timer);
|
|
65
|
+
if (config.abortSignal && onAbort) config.abortSignal.removeEventListener("abort", onAbort);
|
|
66
|
+
callback();
|
|
67
|
+
};
|
|
68
|
+
const makeError = (message, code = 1) => {
|
|
69
|
+
const error = new Error(message);
|
|
70
|
+
error.code = code;
|
|
71
|
+
error.stdout = stdout;
|
|
72
|
+
error.stderr = stderr;
|
|
73
|
+
return error;
|
|
74
|
+
};
|
|
75
|
+
const onAbort = () => {
|
|
76
|
+
killChildTree(child, "SIGTERM");
|
|
77
|
+
setTimeout(() => killChildTree(child, "SIGKILL"), 1200).unref?.();
|
|
78
|
+
const error = makeError("Wrapper interrupted by user.", "ABORT_ERR");
|
|
79
|
+
error.name = "AbortError";
|
|
80
|
+
settle(() => reject(error));
|
|
81
|
+
};
|
|
82
|
+
const timer =
|
|
83
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
84
|
+
? setTimeout(() => {
|
|
85
|
+
timedOut = true;
|
|
86
|
+
killChildTree(child, "SIGTERM");
|
|
87
|
+
setTimeout(() => killChildTree(child, "SIGKILL"), 1200).unref?.();
|
|
88
|
+
}, timeoutMs)
|
|
89
|
+
: null;
|
|
90
|
+
timer?.unref?.();
|
|
91
|
+
|
|
92
|
+
if (config.abortSignal?.aborted) return onAbort();
|
|
93
|
+
if (config.abortSignal) config.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
|
|
95
|
+
child.stdout?.on("data", (chunk) => {
|
|
96
|
+
if (stdout.length < maxStdout) stdout += chunk.toString();
|
|
97
|
+
});
|
|
98
|
+
child.stderr?.on("data", (chunk) => {
|
|
99
|
+
if (stderr.length < maxStderr) stderr += chunk.toString();
|
|
100
|
+
});
|
|
101
|
+
child.on("error", (error) => {
|
|
102
|
+
settle(() => reject(makeError(error instanceof Error ? error.message : String(error), 1)));
|
|
103
|
+
});
|
|
104
|
+
child.on("close", (code, signal) => {
|
|
105
|
+
if (timedOut) {
|
|
106
|
+
settle(() => reject(makeError(`Wrapper timed out after ${timeoutMs}ms.`, 124)));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (Number(code || 0) === 0) {
|
|
110
|
+
settle(() => resolve({ stdout, stderr }));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
settle(() => reject(makeError(`Wrapper exited with ${signal || code}.`, Number.isInteger(code) ? code : 1)));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
34
118
|
function buildPrompt(prompt) {
|
|
35
119
|
return `${BASE_ADVISORY_PROMPT}\n\nTask:\n${prompt}`;
|
|
36
120
|
}
|
|
@@ -157,13 +241,7 @@ export async function runAgentWrapper({ wrapper, prompt }, config) {
|
|
|
157
241
|
return { ok: false, wrapper, error: `Wrapper command is not available: ${wrapper}` };
|
|
158
242
|
}
|
|
159
243
|
|
|
160
|
-
const runOnce = async (spec) =>
|
|
161
|
-
execFile(spec.command, spec.args, {
|
|
162
|
-
cwd: config.commandCwd,
|
|
163
|
-
timeout: Number(config.wrapperTimeoutMs) || 120000,
|
|
164
|
-
maxBuffer: 512 * 1024,
|
|
165
|
-
env: process.env,
|
|
166
|
-
});
|
|
244
|
+
const runOnce = async (spec) => runWrapperProcess(spec, config);
|
|
167
245
|
|
|
168
246
|
try {
|
|
169
247
|
const result = await runOnce(commandSpec);
|
|
@@ -174,6 +252,7 @@ export async function runAgentWrapper({ wrapper, prompt }, config) {
|
|
|
174
252
|
stderr: cleanOutput(result.stderr, 4000),
|
|
175
253
|
};
|
|
176
254
|
} catch (error) {
|
|
255
|
+
if (error?.name === "AbortError" || error?.code === "ABORT_ERR") throw error;
|
|
177
256
|
if (wrapper === "codex") {
|
|
178
257
|
const fallbackSpec = wrapperCommand(wrapper, prompt, config, { fallback: true });
|
|
179
258
|
try {
|
|
@@ -186,6 +265,7 @@ export async function runAgentWrapper({ wrapper, prompt }, config) {
|
|
|
186
265
|
stderr: cleanOutput(fallback.stderr, 4000),
|
|
187
266
|
};
|
|
188
267
|
} catch (fallbackError) {
|
|
268
|
+
if (fallbackError?.name === "AbortError" || fallbackError?.code === "ABORT_ERR") throw fallbackError;
|
|
189
269
|
return {
|
|
190
270
|
ok: false,
|
|
191
271
|
wrapper,
|