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.
Files changed (97) hide show
  1. package/README.md +28 -6
  2. package/package.json +1 -1
  3. package/src/core/agent/agent-manager.js +123 -8
  4. package/src/core/agent/agent-session-lifecycle.js +108 -8
  5. package/src/core/agent/agent-turn-coordinator.js +143 -0
  6. package/src/core/agent/core-tools.js +1 -1
  7. package/src/core/agent/pi-auth-login.js +1 -1
  8. package/src/core/agent/pi-capability-tools.js +7 -4
  9. package/src/core/agent/pi-runtime.js +1 -1
  10. package/src/core/agent/runtime-context.js +1 -1
  11. package/src/core/agent/session-history-reader.js +168 -0
  12. package/src/core/agent/session-preload-migration.js +162 -0
  13. package/src/core/agent/session-rotation.js +29 -0
  14. package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
  15. package/src/core/agent/worker-tool-fanout.js +117 -0
  16. package/src/core/artifacts/artifact-store.js +1 -1
  17. package/src/core/capabilities/capability-service.js +4 -3
  18. package/src/core/config/config-defaults.js +59 -1
  19. package/src/core/config/config-store.js +1 -1
  20. package/src/core/conversation/session-seed-store.js +1 -1
  21. package/src/core/tasks/task-runner.js +8 -4
  22. package/src/core/tasks/task-store.js +48 -5
  23. package/src/core/tools/daemon-client.js +180 -0
  24. package/src/core/tools/daemon-processes.js +26 -3
  25. package/src/core/tools/daemon-protocol.js +72 -0
  26. package/src/core/tools/daemon-runtime.js +13 -490
  27. package/src/core/tools/daemon-worker.js +310 -0
  28. package/src/core/tools/ipc-client.js +2 -2
  29. package/src/core/tools/memory-pressure.js +56 -0
  30. package/src/core/tools/official-tool-installer.js +1 -1
  31. package/src/core/tools/tool-config.js +1 -1
  32. package/src/core/tools/tool-process-output.js +100 -0
  33. package/src/core/tools/tool-process-runner.js +175 -0
  34. package/src/core/tools/tool-registry.js +131 -191
  35. package/src/core/tools/tool-resource-note-store.js +1 -1
  36. package/src/core/tools/tool-usage-store.js +1 -1
  37. package/src/core/tools/weighted-resource-governor.js +189 -38
  38. package/src/index.js +14 -2
  39. package/src/official-tools.lock.json +526 -78
  40. package/src/platform/paths.js +152 -0
  41. package/src/runtime/bootstrap-cli.js +121 -0
  42. package/src/runtime/bootstrap-config.js +97 -0
  43. package/src/runtime/bootstrap-telegram.js +325 -0
  44. package/src/runtime/bootstrap.js +6 -543
  45. package/src/runtime/doctor.js +26 -76
  46. package/src/runtime/flush.js +1 -1
  47. package/src/runtime/ipc/ipc-server.js +1 -1
  48. package/src/runtime/log-viewer.js +1 -1
  49. package/src/runtime/obsolete-daemon-reaper.js +43 -0
  50. package/src/runtime/oom-protection.js +20 -0
  51. package/src/runtime/paths.js +3 -151
  52. package/src/runtime/process-inspection.js +78 -0
  53. package/src/runtime/restart-receipt.js +1 -1
  54. package/src/runtime/service-manager.js +1 -1
  55. package/src/runtime/service-supervisor.js +14 -0
  56. package/src/runtime/slave-cli.js +1 -1
  57. package/src/runtime/tool-process-supervisor.js +36 -9
  58. package/src/runtime/tui.js +200 -0
  59. package/src/runtime/update-manager.js +1 -1
  60. package/src/runtime/worker-recovery-report.js +142 -0
  61. package/src/transport/telegram/bot.js +45 -321
  62. package/src/transport/telegram/chat-queue.js +6 -2
  63. package/src/transport/telegram/prompt-builders.js +8 -3
  64. package/src/transport/telegram/task-dispatcher.js +36 -11
  65. package/src/transport/telegram/telegram-prompt-controller.js +359 -0
  66. package/src/transport/telegram/workspace-topic-store.js +1 -1
  67. package/test/agent-session-lifecycle.test.js +92 -0
  68. package/test/agent-turn-coordinator.test.js +45 -0
  69. package/test/architecture-boundaries.test.js +29 -0
  70. package/test/bootstrap.test.js +65 -0
  71. package/test/context-and-task-bounds.test.js +2 -1
  72. package/test/daemon-process-invocation.test.js +27 -0
  73. package/test/daemon-runtime.test.js +38 -5
  74. package/test/doctor.test.js +41 -0
  75. package/test/memory-pressure.test.js +41 -0
  76. package/test/model-selection.test.js +11 -1
  77. package/test/obsolete-daemon-reaper.test.js +61 -0
  78. package/test/official-tool-dependencies.test.js +7 -2
  79. package/test/official-tool-installer.test.js +18 -1
  80. package/test/oom-protection.test.js +32 -0
  81. package/test/paths.test.js +7 -0
  82. package/test/pi-compaction.test.js +30 -0
  83. package/test/service-manager.test.js +6 -1
  84. package/test/session-history-reader.test.js +84 -0
  85. package/test/session-preload-migration.test.js +120 -0
  86. package/test/session-rotation.test.js +110 -0
  87. package/test/task-store.test.js +32 -0
  88. package/test/telegram-prompt-controller.test.js +82 -0
  89. package/test/telegram-task-dispatcher.test.js +66 -5
  90. package/test/telegram-text-artifact.test.js +30 -0
  91. package/test/tool-registry-run.test.js +117 -4
  92. package/test/tui.test.js +41 -0
  93. package/test/weighted-resource-governor.test.js +125 -5
  94. package/test/worker-heap-circuit-breaker.test.js +79 -0
  95. package/test/worker-recovery-report.test.js +69 -0
  96. package/test/worker-tool-fanout.test.js +79 -0
  97. 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 { arisaIpcSocketFile, arisaPackageDir, getToolConfigPath, getToolStateDir, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
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, DAEMON_EVENT_TYPES, DAEMON_PROTOCOL_VERSION } from "./daemon-runtime.js";
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
- function toolEnv() {
17
- return { ...process.env, ARISA_PACKAGE_DIR: arisaPackageDir, ARISA_IPC_SOCKET: arisaIpcSocketFile };
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 waitForToolProcess(child, { timeoutMs, killGraceMs, label }) {
42
- return new Promise((resolve, reject) => {
43
- let timedOut = false;
44
- let forceTimer = null;
45
- const timeout = setTimeout(() => {
46
- timedOut = true;
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
- async function runProcess(command, args, { timeoutMs, killGraceMs, label, ...options } = {}) {
71
- const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
72
- let stdout = "";
73
- let stderr = "";
74
- child.stdout.on("data", (d) => { stdout += d.toString(); });
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 requirementNames(requirements) {
81
- if (Array.isArray(requirements)) {
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
- export function createToolOutputParser(name, { onEvent, maxFrameBytes = 1_048_576 } = {}) {
89
- let buffer = "";
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
- async push(chunk) {
148
- const text = chunk.toString("utf8");
149
- if (mode !== "ndjson") rawOutput += text;
150
- if (mode === "legacy") return;
151
- buffer += text;
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
- async function runToolProcess(command, args, { onEvent, maxFrameBytes, timeoutMs, killGraceMs, label, ...options } = {}) {
175
- const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
176
- const parser = createToolOutputParser(path.basename(args[0] || command), { onEvent, maxFrameBytes });
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
- const [parsed, stderr] = await Promise.all([stdoutTask, stderrTask]);
202
- return { code, parsed, stderr };
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 result = await runProcess("node", [tool.entry, "--help"], {
411
- cwd: tool.dir,
412
- env: toolEnv(),
413
- timeoutMs: this.helpTimeoutMs,
414
- killGraceMs: this.killGraceMs,
415
- label: `Tool help for ${name}`
416
- });
417
- const help = result.stdout || result.stderr;
418
- const skills = await this.resolveSkills(name);
419
- const sections = [
420
- help.trimEnd(),
421
- formatSemanticMetadata(tool),
422
- formatToolDependencies(tool, this.tools)
423
- ];
424
- if (skills.length) {
425
- const skillHelp = skills.map((item) => [
426
- `- ${item.name}${item.when ? ` (${item.when})` : ""}`,
427
- item.description ? ` ${item.description}` : null,
428
- item.found ? ` path: ${item.path}` : " warning: skill not found"
429
- ].filter(Boolean).join("\n")).join("\n");
430
- sections.push(`Assigned skills:\n${skillHelp}`);
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
- lease = await this.executionGovernor.acquire(tool.execution, name);
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
- if (tool.daemon?.protocol === "arisa-daemon-v1") {
533
- const scope = tool.daemon.scope === "chat"
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: scope.type === "chat" ? { chatId: String(chatId) } : {},
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 processResult = await runToolProcess("node", [tool.entry, "run", "--request-file", requestFile], {
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: toolEnv(),
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
- if (error?.code === "TOOL_PROCESS_TIMEOUT") {
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 was terminated after timing out. Check external state before retrying."
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 "../../runtime/paths.js";
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 "../../runtime/paths.js";
3
+ import { getChatToolUsageFile } from "../../platform/paths.js";
4
4
 
5
5
  function emptyUsage() {
6
6
  return { version: 1, tools: {} };