arisa 5.1.68 → 5.2.17
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/README.md +28 -6
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +123 -8
- package/src/core/agent/agent-session-lifecycle.js +108 -8
- package/src/core/agent/agent-turn-coordinator.js +143 -0
- package/src/core/agent/core-tools.js +1 -1
- package/src/core/agent/pi-auth-login.js +1 -1
- package/src/core/agent/pi-capability-tools.js +7 -4
- package/src/core/agent/pi-runtime.js +1 -1
- package/src/core/agent/runtime-context.js +1 -1
- package/src/core/agent/session-history-reader.js +168 -0
- package/src/core/agent/session-preload-migration.js +162 -0
- package/src/core/agent/session-rotation.js +29 -0
- package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
- package/src/core/agent/worker-tool-fanout.js +117 -0
- package/src/core/artifacts/artifact-store.js +1 -1
- package/src/core/capabilities/capability-service.js +4 -3
- package/src/core/config/config-defaults.js +59 -1
- package/src/core/config/config-store.js +1 -1
- package/src/core/conversation/session-seed-store.js +1 -1
- package/src/core/tasks/task-runner.js +8 -4
- package/src/core/tasks/task-store.js +48 -5
- package/src/core/tools/daemon-client.js +180 -0
- package/src/core/tools/daemon-processes.js +26 -3
- package/src/core/tools/daemon-protocol.js +72 -0
- package/src/core/tools/daemon-runtime.js +13 -490
- package/src/core/tools/daemon-worker.js +310 -0
- package/src/core/tools/ipc-client.js +2 -2
- package/src/core/tools/memory-pressure.js +56 -0
- package/src/core/tools/official-tool-installer.js +1 -1
- package/src/core/tools/tool-config.js +1 -1
- package/src/core/tools/tool-process-output.js +100 -0
- package/src/core/tools/tool-process-runner.js +175 -0
- package/src/core/tools/tool-registry.js +131 -191
- package/src/core/tools/tool-resource-note-store.js +1 -1
- package/src/core/tools/tool-usage-store.js +1 -1
- package/src/core/tools/weighted-resource-governor.js +189 -38
- package/src/index.js +14 -2
- package/src/official-tools.lock.json +526 -78
- package/src/platform/paths.js +152 -0
- package/src/runtime/bootstrap-cli.js +121 -0
- package/src/runtime/bootstrap-config.js +97 -0
- package/src/runtime/bootstrap-telegram.js +325 -0
- package/src/runtime/bootstrap.js +6 -543
- package/src/runtime/doctor.js +26 -76
- package/src/runtime/flush.js +1 -1
- package/src/runtime/ipc/ipc-server.js +1 -1
- package/src/runtime/log-viewer.js +1 -1
- package/src/runtime/obsolete-daemon-reaper.js +43 -0
- package/src/runtime/oom-protection.js +20 -0
- package/src/runtime/paths.js +3 -151
- package/src/runtime/process-inspection.js +78 -0
- package/src/runtime/restart-receipt.js +1 -1
- package/src/runtime/service-manager.js +1 -1
- package/src/runtime/service-supervisor.js +14 -0
- package/src/runtime/slave-cli.js +1 -1
- package/src/runtime/tool-process-supervisor.js +36 -9
- package/src/runtime/tui.js +200 -0
- package/src/runtime/update-manager.js +1 -1
- package/src/runtime/worker-recovery-report.js +142 -0
- package/src/transport/telegram/bot.js +45 -321
- package/src/transport/telegram/chat-queue.js +6 -2
- package/src/transport/telegram/prompt-builders.js +8 -3
- package/src/transport/telegram/task-dispatcher.js +36 -11
- package/src/transport/telegram/telegram-prompt-controller.js +359 -0
- package/src/transport/telegram/workspace-topic-store.js +1 -1
- package/test/agent-session-lifecycle.test.js +92 -0
- package/test/agent-turn-coordinator.test.js +45 -0
- package/test/architecture-boundaries.test.js +29 -0
- package/test/bootstrap.test.js +65 -0
- package/test/context-and-task-bounds.test.js +2 -1
- package/test/daemon-process-invocation.test.js +27 -0
- package/test/daemon-runtime.test.js +38 -5
- package/test/doctor.test.js +41 -0
- package/test/memory-pressure.test.js +41 -0
- package/test/model-selection.test.js +11 -1
- package/test/obsolete-daemon-reaper.test.js +61 -0
- package/test/official-tool-dependencies.test.js +7 -2
- package/test/official-tool-installer.test.js +18 -1
- package/test/oom-protection.test.js +32 -0
- package/test/paths.test.js +7 -0
- package/test/pi-compaction.test.js +30 -0
- package/test/service-manager.test.js +6 -1
- package/test/session-history-reader.test.js +84 -0
- package/test/session-preload-migration.test.js +120 -0
- package/test/session-rotation.test.js +110 -0
- package/test/task-store.test.js +32 -0
- package/test/telegram-prompt-controller.test.js +82 -0
- package/test/telegram-task-dispatcher.test.js +66 -5
- package/test/telegram-text-artifact.test.js +30 -0
- package/test/tool-registry-run.test.js +117 -4
- package/test/tui.test.js +41 -0
- package/test/weighted-resource-governor.test.js +125 -5
- package/test/worker-heap-circuit-breaker.test.js +79 -0
- package/test/worker-recovery-report.test.js +69 -0
- package/test/worker-tool-fanout.test.js +79 -0
- package/test-fixtures/fake-daemon.js +5 -0
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
4
|
+
import { getToolConfigPath, getToolStateDir, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../platform/paths.js";
|
|
6
5
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
7
6
|
import { normalizeToolResult } from "./tool-result.js";
|
|
8
7
|
import { readDaemonDiagnostic } from "./daemon-processes.js";
|
|
9
|
-
import { createDaemonRuntime
|
|
8
|
+
import { createDaemonRuntime } from "./daemon-runtime.js";
|
|
10
9
|
import { daemonConfigDefaults } from "../config/config-defaults.js";
|
|
11
10
|
import { SkillRegistry } from "../skills/skill-registry.js";
|
|
12
11
|
import { ToolUsageStore } from "./tool-usage-store.js";
|
|
13
12
|
import { inspectToolDependencies, normalizeToolDependencies } from "./tool-dependencies.js";
|
|
14
13
|
import { normalizeToolExecution, WeightedResourceGovernor } from "./weighted-resource-governor.js";
|
|
14
|
+
import {
|
|
15
|
+
isolatedToolProcessInvocation,
|
|
16
|
+
runToolHelpProcess,
|
|
17
|
+
runToolProcess,
|
|
18
|
+
toolProcessEnv
|
|
19
|
+
} from "./tool-process-runner.js";
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
21
|
+
export { createToolOutputParser } from "./tool-process-output.js";
|
|
22
|
+
export { isolatedToolProcessInvocation } from "./tool-process-runner.js";
|
|
19
23
|
|
|
20
24
|
const defaultToolHelpTimeoutMs = 10_000;
|
|
21
25
|
const defaultToolRunTimeoutMs = 30 * 60_000;
|
|
@@ -38,168 +42,42 @@ function concurrentExecutionKey(name, chatId, request) {
|
|
|
38
42
|
return createHash("sha256").update(serialized).digest("hex");
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
child.kill("SIGTERM");
|
|
48
|
-
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
49
|
-
}, timeoutMs);
|
|
50
|
-
|
|
51
|
-
const finish = (callback, value) => {
|
|
52
|
-
clearTimeout(timeout);
|
|
53
|
-
clearTimeout(forceTimer);
|
|
54
|
-
callback(value);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
child.once("error", (error) => finish(reject, error));
|
|
58
|
-
child.once("close", (code) => {
|
|
59
|
-
if (!timedOut) {
|
|
60
|
-
finish(resolve, code);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const error = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
64
|
-
error.code = "TOOL_PROCESS_TIMEOUT";
|
|
65
|
-
finish(reject, error);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
45
|
+
export function readyDaemonAdmission(tool, diagnostic) {
|
|
46
|
+
const reusable = Boolean(tool?.daemon)
|
|
47
|
+
&& diagnostic?.alive === true
|
|
48
|
+
&& diagnostic?.state === "ready"
|
|
49
|
+
&& diagnostic?.restart?.requested !== true;
|
|
50
|
+
return reusable ? { ignoreWorkerRss: true } : {};
|
|
68
51
|
}
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
76
|
-
const code = await waitForToolProcess(child, { timeoutMs, killGraceMs, label });
|
|
77
|
-
return { code, stdout, stderr };
|
|
53
|
+
function declaredDaemonScope(tool, chatId) {
|
|
54
|
+
if (!tool?.daemon) return null;
|
|
55
|
+
return tool.daemon.scope === "chat"
|
|
56
|
+
? { type: "chat", chatId }
|
|
57
|
+
: { type: "global" };
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
return requirements.map((item) => typeof item === "string" ? item : item?.name).filter(Boolean);
|
|
83
|
-
}
|
|
84
|
-
if (requirements && typeof requirements === "object") return Object.keys(requirements);
|
|
85
|
-
return [];
|
|
60
|
+
function nativeDaemonScope(tool, chatId) {
|
|
61
|
+
return tool?.daemon?.protocol === "arisa-daemon-v1" ? declaredDaemonScope(tool, chatId) : null;
|
|
86
62
|
}
|
|
87
63
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let mode = "unknown";
|
|
91
|
-
let rawOutput = "";
|
|
92
|
-
let terminalResult = null;
|
|
93
|
-
let activeJobId = null;
|
|
94
|
-
let sequence = 0;
|
|
95
|
-
let terminalSeen = false;
|
|
96
|
-
|
|
97
|
-
async function parseEvent(line) {
|
|
98
|
-
let event;
|
|
99
|
-
try {
|
|
100
|
-
event = JSON.parse(line);
|
|
101
|
-
} catch {
|
|
102
|
-
throw new Error(`Invalid NDJSON from ${name}`);
|
|
103
|
-
}
|
|
104
|
-
if (event?.version !== DAEMON_PROTOCOL_VERSION || !DAEMON_EVENT_TYPES.includes(event?.type)) {
|
|
105
|
-
throw new Error(`Invalid versioned tool event from ${name}`);
|
|
106
|
-
}
|
|
107
|
-
if (typeof event.jobId !== "string" || !event.jobId) throw new Error(`Tool event from ${name} is missing jobId`);
|
|
108
|
-
if (activeJobId == null) activeJobId = event.jobId;
|
|
109
|
-
if (event.jobId !== activeJobId) throw new Error(`Tool ${name} multiplexed an unexpected jobId`);
|
|
110
|
-
if (!Number.isSafeInteger(event.sequence) || event.sequence !== sequence + 1) {
|
|
111
|
-
throw new Error(`Invalid tool event sequence from ${name}: ${event.sequence}`);
|
|
112
|
-
}
|
|
113
|
-
if (terminalSeen) throw new Error(`Tool ${name} emitted more than one terminal event`);
|
|
114
|
-
sequence = event.sequence;
|
|
115
|
-
terminalSeen = event.type === "completed" || event.type === "failed";
|
|
116
|
-
await onEvent?.(event);
|
|
117
|
-
if (terminalSeen) {
|
|
118
|
-
terminalResult = event.type === "completed"
|
|
119
|
-
? event.payload?.result ?? event.payload?.output ?? event.payload
|
|
120
|
-
: { ok: false, error: event.payload?.error || `Tool failed: ${name}`, ...(event.payload?.code ? { code: event.payload.code } : {}) };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function consumeLine(line) {
|
|
125
|
-
if (Buffer.byteLength(line, "utf8") > maxFrameBytes) throw new Error(`Tool event from ${name} exceeds ${maxFrameBytes} bytes`);
|
|
126
|
-
if (mode === "unknown") {
|
|
127
|
-
let candidate;
|
|
128
|
-
try {
|
|
129
|
-
candidate = JSON.parse(line);
|
|
130
|
-
} catch {
|
|
131
|
-
mode = "legacy";
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (candidate?.version === DAEMON_PROTOCOL_VERSION && DAEMON_EVENT_TYPES.includes(candidate?.type)) {
|
|
135
|
-
mode = "ndjson";
|
|
136
|
-
rawOutput = "";
|
|
137
|
-
return parseEvent(line);
|
|
138
|
-
}
|
|
139
|
-
mode = "legacy";
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (mode === "legacy") return;
|
|
143
|
-
return parseEvent(line);
|
|
144
|
-
}
|
|
145
|
-
|
|
64
|
+
function executionForLease(execution, lease) {
|
|
65
|
+
if (!execution) return null;
|
|
146
66
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (Buffer.byteLength(buffer, "utf8") > maxFrameBytes && !buffer.includes("\n")) {
|
|
153
|
-
throw new Error(`Tool event from ${name} exceeds ${maxFrameBytes} bytes`);
|
|
154
|
-
}
|
|
155
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
156
|
-
while (newlineIndex !== -1) {
|
|
157
|
-
const line = buffer.slice(0, newlineIndex).trim();
|
|
158
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
159
|
-
if (line) await consumeLine(line);
|
|
160
|
-
newlineIndex = buffer.indexOf("\n");
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
async finish() {
|
|
164
|
-
const tail = buffer.trim();
|
|
165
|
-
buffer = "";
|
|
166
|
-
if (tail) await consumeLine(tail);
|
|
167
|
-
if (mode !== "ndjson") return { mode: "legacy", output: rawOutput };
|
|
168
|
-
if (!terminalSeen) throw new Error(`Tool ${name} ended without a terminal event`);
|
|
169
|
-
return { mode: "ndjson", result: terminalResult };
|
|
170
|
-
}
|
|
67
|
+
...execution,
|
|
68
|
+
maxHeapMb: lease.heapLimitMb || execution.maxHeapMb,
|
|
69
|
+
maxMemoryMb: lease.memoryLimitMb || execution.maxMemoryMb,
|
|
70
|
+
memoryHighPercent: lease.memoryHighPercent,
|
|
71
|
+
swapMaxMb: lease.swapMaxMb
|
|
171
72
|
};
|
|
172
73
|
}
|
|
173
74
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const stderrChunks = [];
|
|
178
|
-
let stderrBytes = 0;
|
|
179
|
-
const stdoutTask = (async () => {
|
|
180
|
-
for await (const chunk of child.stdout) await parser.push(chunk);
|
|
181
|
-
return parser.finish();
|
|
182
|
-
})();
|
|
183
|
-
const stderrTask = (async () => {
|
|
184
|
-
for await (const chunk of child.stderr) {
|
|
185
|
-
if (stderrBytes >= maxFrameBytes) continue;
|
|
186
|
-
const accepted = chunk.subarray(0, maxFrameBytes - stderrBytes);
|
|
187
|
-
stderrChunks.push(accepted);
|
|
188
|
-
stderrBytes += accepted.length;
|
|
189
|
-
}
|
|
190
|
-
return Buffer.concat(stderrChunks).toString("utf8");
|
|
191
|
-
})();
|
|
192
|
-
child.stdout.resume();
|
|
193
|
-
child.stderr.resume();
|
|
194
|
-
let code;
|
|
195
|
-
try {
|
|
196
|
-
code = await waitForToolProcess(child, { timeoutMs, killGraceMs, label });
|
|
197
|
-
} catch (error) {
|
|
198
|
-
await Promise.allSettled([stdoutTask, stderrTask]);
|
|
199
|
-
throw error;
|
|
75
|
+
function requirementNames(requirements) {
|
|
76
|
+
if (Array.isArray(requirements)) {
|
|
77
|
+
return requirements.map((item) => typeof item === "string" ? item : item?.name).filter(Boolean);
|
|
200
78
|
}
|
|
201
|
-
|
|
202
|
-
return
|
|
79
|
+
if (requirements && typeof requirements === "object") return Object.keys(requirements);
|
|
80
|
+
return [];
|
|
203
81
|
}
|
|
204
82
|
|
|
205
83
|
function normalizeCategory(category) {
|
|
@@ -407,29 +285,44 @@ export class ToolRegistry {
|
|
|
407
285
|
async help(name) {
|
|
408
286
|
const tool = this.get(name);
|
|
409
287
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
288
|
+
const lease = await this.executionGovernor.acquire(tool.execution, `${name}:help`);
|
|
289
|
+
try {
|
|
290
|
+
const execution = executionForLease(tool.execution, lease);
|
|
291
|
+
const nodeArgs = [
|
|
292
|
+
...(execution?.maxHeapMb ? [`--max-old-space-size=${execution.maxHeapMb}`] : []),
|
|
293
|
+
tool.entry,
|
|
294
|
+
"--help"
|
|
295
|
+
];
|
|
296
|
+
const invocation = isolatedToolProcessInvocation(nodeArgs, execution);
|
|
297
|
+
const result = await runToolHelpProcess(invocation.command, invocation.args, {
|
|
298
|
+
cwd: tool.dir,
|
|
299
|
+
env: toolProcessEnv(),
|
|
300
|
+
timeoutMs: this.helpTimeoutMs,
|
|
301
|
+
killGraceMs: this.killGraceMs,
|
|
302
|
+
maxOutputBytes: execution?.maxOutputBytes || daemonConfigDefaults.ipcFrameBytes,
|
|
303
|
+
label: `Tool help for ${name}`
|
|
304
|
+
});
|
|
305
|
+
const help = result.stdout || result.stderr;
|
|
306
|
+
const skills = await this.resolveSkills(name);
|
|
307
|
+
const sections = [
|
|
308
|
+
help.trimEnd(),
|
|
309
|
+
formatSemanticMetadata(tool),
|
|
310
|
+
formatToolDependencies(tool, this.tools)
|
|
311
|
+
];
|
|
312
|
+
if (skills.length) {
|
|
313
|
+
const skillHelp = skills.map((item) => [
|
|
314
|
+
`- ${item.name}${item.when ? ` (${item.when})` : ""}`,
|
|
315
|
+
item.description ? ` ${item.description}` : null,
|
|
316
|
+
item.found ? ` path: ${item.path}` : " warning: skill not found"
|
|
317
|
+
].filter(Boolean).join("\n")).join("\n");
|
|
318
|
+
sections.push(`Assigned skills:\n${skillHelp}`);
|
|
319
|
+
}
|
|
320
|
+
lease.release({ success: result.code === 0 });
|
|
321
|
+
return `${sections.filter(Boolean).join("\n\n")}\n`;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
lease.release({ memoryLimited: error?.code === "TOOL_PROCESS_MEMORY_LIMIT" });
|
|
324
|
+
throw error;
|
|
431
325
|
}
|
|
432
|
-
return `${sections.filter(Boolean).join("\n\n")}\n`;
|
|
433
326
|
}
|
|
434
327
|
|
|
435
328
|
async resolveSkills(name) {
|
|
@@ -522,32 +415,56 @@ export class ToolRegistry {
|
|
|
522
415
|
const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
|
|
523
416
|
const requestFile = path.join(tmpDir, `.request-${Date.now()}-${randomUUID()}.json`);
|
|
524
417
|
let lease = null;
|
|
418
|
+
let leaseOutcome = {};
|
|
525
419
|
let result;
|
|
526
420
|
try {
|
|
527
|
-
|
|
421
|
+
const admissionScope = declaredDaemonScope(tool, chatId);
|
|
422
|
+
let admission = {};
|
|
423
|
+
if (admissionScope) {
|
|
424
|
+
const diagnostic = await readDaemonDiagnostic({
|
|
425
|
+
toolName: name,
|
|
426
|
+
scope: admissionScope,
|
|
427
|
+
autoStart: Boolean(tool.daemon.autoStart)
|
|
428
|
+
}).catch(() => null);
|
|
429
|
+
admission = readyDaemonAdmission(tool, diagnostic);
|
|
430
|
+
if (admission.ignoreWorkerRss) this.logger?.log("tools", `reusing ready ${name} daemon without worker RSS spawn admission`);
|
|
431
|
+
}
|
|
432
|
+
lease = await this.executionGovernor.acquire(tool.execution, name, admission);
|
|
528
433
|
this.logger?.log("tools", `running ${name}`);
|
|
529
434
|
await mkdir(tmpDir, { recursive: true });
|
|
530
435
|
const skills = await this.resolveSkills(name);
|
|
531
436
|
const enrichedRequest = { ...request, chatId, skills };
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
? { type: "chat", chatId }
|
|
535
|
-
: { type: "global" };
|
|
437
|
+
const runtimeScope = nativeDaemonScope(tool, chatId);
|
|
438
|
+
if (runtimeScope) {
|
|
536
439
|
const runtime = createDaemonRuntime({
|
|
537
440
|
toolName: name,
|
|
538
441
|
entryPath: tool.entry,
|
|
539
|
-
scope,
|
|
540
|
-
startupContext:
|
|
442
|
+
scope: runtimeScope,
|
|
443
|
+
startupContext: runtimeScope.type === "chat" ? { chatId: String(chatId) } : {},
|
|
541
444
|
autoStart: Boolean(tool.daemon.autoStart)
|
|
542
445
|
});
|
|
543
446
|
result = await runtime.submit(enrichedRequest, { onEvent });
|
|
544
447
|
} else {
|
|
545
448
|
await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
|
|
546
|
-
const
|
|
449
|
+
const execution = executionForLease(tool.execution, lease);
|
|
450
|
+
const nodeArgs = [
|
|
451
|
+
...(execution?.maxHeapMb ? [`--max-old-space-size=${execution.maxHeapMb}`] : []),
|
|
452
|
+
tool.entry,
|
|
453
|
+
"run",
|
|
454
|
+
"--request-file",
|
|
455
|
+
requestFile
|
|
456
|
+
];
|
|
457
|
+
const processInvocation = isolatedToolProcessInvocation(nodeArgs, execution);
|
|
458
|
+
if (processInvocation.isolated) {
|
|
459
|
+
this.logger?.log("tools", `${name} isolated at ${execution.maxMemoryMb} MiB total memory (${execution.maxHeapMb} MiB heap)`);
|
|
460
|
+
}
|
|
461
|
+
const processResult = await runToolProcess(processInvocation.command, processInvocation.args, {
|
|
547
462
|
cwd: tool.dir,
|
|
548
|
-
env:
|
|
463
|
+
env: toolProcessEnv(),
|
|
549
464
|
onEvent,
|
|
465
|
+
parserName: name,
|
|
550
466
|
maxFrameBytes: daemonConfigDefaults.ipcFrameBytes,
|
|
467
|
+
maxOutputBytes: tool.execution?.maxOutputBytes || daemonConfigDefaults.ipcFrameBytes,
|
|
551
468
|
timeoutMs: this.runTimeoutMs,
|
|
552
469
|
killGraceMs: this.killGraceMs,
|
|
553
470
|
label: `Tool run for ${name}`
|
|
@@ -555,11 +472,21 @@ export class ToolRegistry {
|
|
|
555
472
|
if (processResult.stderr.trim()) {
|
|
556
473
|
this.logger?.log("tools", `${name} stderr: ${processResult.stderr.trim()}`);
|
|
557
474
|
}
|
|
475
|
+
if (processResult.code !== 0) {
|
|
476
|
+
const memoryLimited = /heap limit|heap out of memory|allocation failed.*memory|memory cgroup out of memory|\bkilled\b/i.test(processResult.stderr)
|
|
477
|
+
|| (processInvocation.isolated && [9, 134, 137].includes(processResult.code));
|
|
478
|
+
const error = new Error(memoryLimited
|
|
479
|
+
? `Tool ${name} exceeded its isolated memory limit`
|
|
480
|
+
: `Tool ${name} exited with code ${processResult.code}`);
|
|
481
|
+
error.code = memoryLimited ? "TOOL_PROCESS_MEMORY_LIMIT" : "TOOL_PROCESS_EXIT";
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
558
484
|
result = processResult.parsed.mode === "ndjson"
|
|
559
485
|
? processResult.parsed.result
|
|
560
486
|
: JSON.parse(processResult.parsed.output);
|
|
561
487
|
}
|
|
562
488
|
const normalized = normalizeToolResult(name, result);
|
|
489
|
+
leaseOutcome = { success: normalized.ok !== false };
|
|
563
490
|
if (normalized.ok === false) {
|
|
564
491
|
this.logger?.log("tools", `${name} -> ${normalized.status || "error"}: ${normalized.error || "unknown error"}`);
|
|
565
492
|
} else {
|
|
@@ -567,7 +494,20 @@ export class ToolRegistry {
|
|
|
567
494
|
}
|
|
568
495
|
return normalized;
|
|
569
496
|
} catch (error) {
|
|
570
|
-
|
|
497
|
+
leaseOutcome = { memoryLimited: error?.code === "TOOL_PROCESS_MEMORY_LIMIT" };
|
|
498
|
+
if (error?.code === "TOOL_RESOURCE_PRESSURE") {
|
|
499
|
+
return normalizeToolResult(name, {
|
|
500
|
+
ok: false,
|
|
501
|
+
status: "retryable",
|
|
502
|
+
error: error.message,
|
|
503
|
+
resolution: {
|
|
504
|
+
type: "retry_later",
|
|
505
|
+
retry: true,
|
|
506
|
+
message: "The tool was not started. Retry after host memory pressure falls."
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (["TOOL_PROCESS_TIMEOUT", "TOOL_OUTPUT_LIMIT", "TOOL_PROCESS_MEMORY_LIMIT", "TOOL_PROCESS_EXIT"].includes(error?.code)) {
|
|
571
511
|
return normalizeToolResult(name, {
|
|
572
512
|
ok: false,
|
|
573
513
|
status: "outcome_uncertain",
|
|
@@ -575,7 +515,7 @@ export class ToolRegistry {
|
|
|
575
515
|
resolution: {
|
|
576
516
|
type: "status_check_required",
|
|
577
517
|
retry: false,
|
|
578
|
-
message: "The tool process
|
|
518
|
+
message: "The isolated tool process ended without a confirmed result. Check external state before retrying."
|
|
579
519
|
}
|
|
580
520
|
});
|
|
581
521
|
}
|
|
@@ -584,7 +524,7 @@ export class ToolRegistry {
|
|
|
584
524
|
error: error?.message || `Invalid tool response for ${name}`
|
|
585
525
|
});
|
|
586
526
|
} finally {
|
|
587
|
-
lease?.release();
|
|
527
|
+
lease?.release(leaseOutcome);
|
|
588
528
|
await unlink(requestFile).catch(() => {});
|
|
589
529
|
await rmdir(tmpDir).catch(() => {});
|
|
590
530
|
if (chatId != null) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getChatToolResourceNotesFile } from "../../
|
|
3
|
+
import { getChatToolResourceNotesFile } from "../../platform/paths.js";
|
|
4
4
|
|
|
5
5
|
export const maxToolResourceNoteCharacters = 200;
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getChatToolUsageFile } from "../../
|
|
3
|
+
import { getChatToolUsageFile } from "../../platform/paths.js";
|
|
4
4
|
|
|
5
5
|
function emptyUsage() {
|
|
6
6
|
return { version: 1, tools: {} };
|