arisa 5.1.66 → 5.2.7

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 (79) hide show
  1. package/README.md +7 -4
  2. package/package.json +1 -1
  3. package/src/core/agent/agent-manager.js +46 -6
  4. package/src/core/agent/agent-session-lifecycle.js +80 -3
  5. package/src/core/agent/core-tools.js +1 -1
  6. package/src/core/agent/pi-auth-login.js +1 -1
  7. package/src/core/agent/pi-runtime.js +1 -1
  8. package/src/core/agent/runtime-context.js +1 -1
  9. package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
  10. package/src/core/artifacts/artifact-store.js +1 -1
  11. package/src/core/capabilities/capability-service.js +1 -1
  12. package/src/core/config/config-defaults.js +30 -1
  13. package/src/core/config/config-store.js +1 -1
  14. package/src/core/conversation/session-seed-store.js +1 -1
  15. package/src/core/tasks/task-store.js +1 -1
  16. package/src/core/tools/daemon-client.js +180 -0
  17. package/src/core/tools/daemon-health.js +5 -2
  18. package/src/core/tools/daemon-processes.js +19 -3
  19. package/src/core/tools/daemon-protocol.js +72 -0
  20. package/src/core/tools/daemon-runtime.js +13 -490
  21. package/src/core/tools/daemon-worker.js +310 -0
  22. package/src/core/tools/ipc-client.js +2 -2
  23. package/src/core/tools/memory-pressure.js +56 -0
  24. package/src/core/tools/official-tool-installer.js +1 -1
  25. package/src/core/tools/tool-config.js +1 -1
  26. package/src/core/tools/tool-process-output.js +100 -0
  27. package/src/core/tools/tool-process-runner.js +175 -0
  28. package/src/core/tools/tool-registry.js +130 -186
  29. package/src/core/tools/tool-resource-note-store.js +1 -1
  30. package/src/core/tools/tool-usage-store.js +1 -1
  31. package/src/core/tools/weighted-resource-governor.js +192 -38
  32. package/src/index.js +14 -2
  33. package/src/official-tools.lock.json +430 -60
  34. package/src/platform/paths.js +152 -0
  35. package/src/runtime/bootstrap-cli.js +121 -0
  36. package/src/runtime/bootstrap-config.js +97 -0
  37. package/src/runtime/bootstrap-telegram.js +325 -0
  38. package/src/runtime/bootstrap.js +6 -543
  39. package/src/runtime/doctor.js +6 -3
  40. package/src/runtime/flush.js +1 -1
  41. package/src/runtime/ipc/ipc-server.js +1 -1
  42. package/src/runtime/log-viewer.js +1 -1
  43. package/src/runtime/oom-protection.js +20 -0
  44. package/src/runtime/paths.js +3 -151
  45. package/src/runtime/restart-receipt.js +1 -1
  46. package/src/runtime/service-manager.js +1 -1
  47. package/src/runtime/service-supervisor.js +14 -0
  48. package/src/runtime/slave-cli.js +2 -1
  49. package/src/runtime/tool-process-supervisor.js +1 -1
  50. package/src/runtime/tui.js +200 -0
  51. package/src/runtime/update-manager.js +1 -1
  52. package/src/runtime/worker-recovery-report.js +142 -0
  53. package/src/transport/telegram/bot.js +42 -320
  54. package/src/transport/telegram/prompt-builders.js +8 -3
  55. package/src/transport/telegram/telegram-prompt-controller.js +346 -0
  56. package/src/transport/telegram/workspace-topic-store.js +1 -1
  57. package/test/agent-session-lifecycle.test.js +92 -0
  58. package/test/architecture-boundaries.test.js +29 -0
  59. package/test/bootstrap.test.js +65 -0
  60. package/test/daemon-process-invocation.test.js +27 -0
  61. package/test/daemon-runtime.test.js +56 -1
  62. package/test/doctor.test.js +22 -0
  63. package/test/memory-pressure.test.js +36 -0
  64. package/test/model-selection.test.js +11 -1
  65. package/test/official-tool-dependencies.test.js +1 -1
  66. package/test/official-tool-installer.test.js +18 -1
  67. package/test/oom-protection.test.js +32 -0
  68. package/test/paths.test.js +7 -0
  69. package/test/pi-compaction.test.js +9 -0
  70. package/test/service-manager.test.js +6 -1
  71. package/test/slave-cli.test.js +20 -1
  72. package/test/telegram-prompt-controller.test.js +81 -0
  73. package/test/telegram-text-artifact.test.js +30 -0
  74. package/test/tool-registry-run.test.js +147 -4
  75. package/test/tui.test.js +41 -0
  76. package/test/weighted-resource-governor.test.js +103 -5
  77. package/test/worker-heap-circuit-breaker.test.js +79 -0
  78. package/test/worker-recovery-report.test.js +69 -0
  79. 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
- import { randomUUID } from "node:crypto";
5
- import { arisaIpcSocketFile, arisaPackageDir, getToolConfigPath, getToolStateDir, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
3
+ import { createHash, randomUUID } from "node:crypto";
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;
@@ -25,43 +29,28 @@ function positiveDuration(value, fallback) {
25
29
  return Number.isFinite(value) && value > 0 ? value : fallback;
26
30
  }
27
31
 
28
- function waitForToolProcess(child, { timeoutMs, killGraceMs, label }) {
29
- return new Promise((resolve, reject) => {
30
- let timedOut = false;
31
- let forceTimer = null;
32
- const timeout = setTimeout(() => {
33
- timedOut = true;
34
- child.kill("SIGTERM");
35
- forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
36
- }, timeoutMs);
37
-
38
- const finish = (callback, value) => {
39
- clearTimeout(timeout);
40
- clearTimeout(forceTimer);
41
- callback(value);
42
- };
32
+ function canonicalRequestValue(value) {
33
+ if (Array.isArray(value)) return value.map(canonicalRequestValue);
34
+ if (value && typeof value === "object") {
35
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalRequestValue(value[key])]));
36
+ }
37
+ return value === undefined ? null : value;
38
+ }
43
39
 
44
- child.once("error", (error) => finish(reject, error));
45
- child.once("close", (code) => {
46
- if (!timedOut) {
47
- finish(resolve, code);
48
- return;
49
- }
50
- const error = new Error(`${label} timed out after ${timeoutMs}ms`);
51
- error.code = "TOOL_PROCESS_TIMEOUT";
52
- finish(reject, error);
53
- });
54
- });
40
+ function concurrentExecutionKey(name, chatId, request) {
41
+ const serialized = JSON.stringify(canonicalRequestValue({ name, chatId: chatId == null ? null : String(chatId), request }));
42
+ return createHash("sha256").update(serialized).digest("hex");
55
43
  }
56
44
 
57
- async function runProcess(command, args, { timeoutMs, killGraceMs, label, ...options } = {}) {
58
- const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
59
- let stdout = "";
60
- let stderr = "";
61
- child.stdout.on("data", (d) => { stdout += d.toString(); });
62
- child.stderr.on("data", (d) => { stderr += d.toString(); });
63
- const code = await waitForToolProcess(child, { timeoutMs, killGraceMs, label });
64
- return { code, stdout, stderr };
45
+ function executionForLease(execution, lease) {
46
+ if (!execution) return null;
47
+ return {
48
+ ...execution,
49
+ maxHeapMb: lease.heapLimitMb || execution.maxHeapMb,
50
+ maxMemoryMb: lease.memoryLimitMb || execution.maxMemoryMb,
51
+ memoryHighPercent: lease.memoryHighPercent,
52
+ swapMaxMb: lease.swapMaxMb
53
+ };
65
54
  }
66
55
 
67
56
  function requirementNames(requirements) {
@@ -72,123 +61,6 @@ function requirementNames(requirements) {
72
61
  return [];
73
62
  }
74
63
 
75
- export function createToolOutputParser(name, { onEvent, maxFrameBytes = 1_048_576 } = {}) {
76
- let buffer = "";
77
- let mode = "unknown";
78
- let rawOutput = "";
79
- let terminalResult = null;
80
- let activeJobId = null;
81
- let sequence = 0;
82
- let terminalSeen = false;
83
-
84
- async function parseEvent(line) {
85
- let event;
86
- try {
87
- event = JSON.parse(line);
88
- } catch {
89
- throw new Error(`Invalid NDJSON from ${name}`);
90
- }
91
- if (event?.version !== DAEMON_PROTOCOL_VERSION || !DAEMON_EVENT_TYPES.includes(event?.type)) {
92
- throw new Error(`Invalid versioned tool event from ${name}`);
93
- }
94
- if (typeof event.jobId !== "string" || !event.jobId) throw new Error(`Tool event from ${name} is missing jobId`);
95
- if (activeJobId == null) activeJobId = event.jobId;
96
- if (event.jobId !== activeJobId) throw new Error(`Tool ${name} multiplexed an unexpected jobId`);
97
- if (!Number.isSafeInteger(event.sequence) || event.sequence !== sequence + 1) {
98
- throw new Error(`Invalid tool event sequence from ${name}: ${event.sequence}`);
99
- }
100
- if (terminalSeen) throw new Error(`Tool ${name} emitted more than one terminal event`);
101
- sequence = event.sequence;
102
- terminalSeen = event.type === "completed" || event.type === "failed";
103
- await onEvent?.(event);
104
- if (terminalSeen) {
105
- terminalResult = event.type === "completed"
106
- ? event.payload?.result ?? event.payload?.output ?? event.payload
107
- : { ok: false, error: event.payload?.error || `Tool failed: ${name}`, ...(event.payload?.code ? { code: event.payload.code } : {}) };
108
- }
109
- }
110
-
111
- async function consumeLine(line) {
112
- if (Buffer.byteLength(line, "utf8") > maxFrameBytes) throw new Error(`Tool event from ${name} exceeds ${maxFrameBytes} bytes`);
113
- if (mode === "unknown") {
114
- let candidate;
115
- try {
116
- candidate = JSON.parse(line);
117
- } catch {
118
- mode = "legacy";
119
- return;
120
- }
121
- if (candidate?.version === DAEMON_PROTOCOL_VERSION && DAEMON_EVENT_TYPES.includes(candidate?.type)) {
122
- mode = "ndjson";
123
- rawOutput = "";
124
- return parseEvent(line);
125
- }
126
- mode = "legacy";
127
- return;
128
- }
129
- if (mode === "legacy") return;
130
- return parseEvent(line);
131
- }
132
-
133
- return {
134
- async push(chunk) {
135
- const text = chunk.toString("utf8");
136
- if (mode !== "ndjson") rawOutput += text;
137
- if (mode === "legacy") return;
138
- buffer += text;
139
- if (Buffer.byteLength(buffer, "utf8") > maxFrameBytes && !buffer.includes("\n")) {
140
- throw new Error(`Tool event from ${name} exceeds ${maxFrameBytes} bytes`);
141
- }
142
- let newlineIndex = buffer.indexOf("\n");
143
- while (newlineIndex !== -1) {
144
- const line = buffer.slice(0, newlineIndex).trim();
145
- buffer = buffer.slice(newlineIndex + 1);
146
- if (line) await consumeLine(line);
147
- newlineIndex = buffer.indexOf("\n");
148
- }
149
- },
150
- async finish() {
151
- const tail = buffer.trim();
152
- buffer = "";
153
- if (tail) await consumeLine(tail);
154
- if (mode !== "ndjson") return { mode: "legacy", output: rawOutput };
155
- if (!terminalSeen) throw new Error(`Tool ${name} ended without a terminal event`);
156
- return { mode: "ndjson", result: terminalResult };
157
- }
158
- };
159
- }
160
-
161
- async function runToolProcess(command, args, { onEvent, maxFrameBytes, timeoutMs, killGraceMs, label, ...options } = {}) {
162
- const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
163
- const parser = createToolOutputParser(path.basename(args[0] || command), { onEvent, maxFrameBytes });
164
- const stderrChunks = [];
165
- let stderrBytes = 0;
166
- const stdoutTask = (async () => {
167
- for await (const chunk of child.stdout) await parser.push(chunk);
168
- return parser.finish();
169
- })();
170
- const stderrTask = (async () => {
171
- for await (const chunk of child.stderr) {
172
- if (stderrBytes >= maxFrameBytes) continue;
173
- const accepted = chunk.subarray(0, maxFrameBytes - stderrBytes);
174
- stderrChunks.push(accepted);
175
- stderrBytes += accepted.length;
176
- }
177
- return Buffer.concat(stderrChunks).toString("utf8");
178
- })();
179
- child.stdout.resume();
180
- child.stderr.resume();
181
- let code;
182
- try {
183
- code = await waitForToolProcess(child, { timeoutMs, killGraceMs, label });
184
- } catch (error) {
185
- await Promise.allSettled([stdoutTask, stderrTask]);
186
- throw error;
187
- }
188
- const [parsed, stderr] = await Promise.all([stdoutTask, stderrTask]);
189
- return { code, parsed, stderr };
190
- }
191
-
192
64
  function normalizeCategory(category) {
193
65
  if (typeof category !== "string") return null;
194
66
  const trimmed = category.trim();
@@ -289,6 +161,7 @@ export class ToolRegistry {
289
161
  policy: executionPolicy,
290
162
  logger
291
163
  });
164
+ this.concurrentExecutions = new Map();
292
165
  }
293
166
 
294
167
  async buildSnapshot() {
@@ -393,29 +266,44 @@ export class ToolRegistry {
393
266
  async help(name) {
394
267
  const tool = this.get(name);
395
268
  if (!tool) throw new Error(`Tool not found: ${name}`);
396
- const result = await runProcess("node", [tool.entry, "--help"], {
397
- cwd: tool.dir,
398
- env: toolEnv(),
399
- timeoutMs: this.helpTimeoutMs,
400
- killGraceMs: this.killGraceMs,
401
- label: `Tool help for ${name}`
402
- });
403
- const help = result.stdout || result.stderr;
404
- const skills = await this.resolveSkills(name);
405
- const sections = [
406
- help.trimEnd(),
407
- formatSemanticMetadata(tool),
408
- formatToolDependencies(tool, this.tools)
409
- ];
410
- if (skills.length) {
411
- const skillHelp = skills.map((item) => [
412
- `- ${item.name}${item.when ? ` (${item.when})` : ""}`,
413
- item.description ? ` ${item.description}` : null,
414
- item.found ? ` path: ${item.path}` : " warning: skill not found"
415
- ].filter(Boolean).join("\n")).join("\n");
416
- sections.push(`Assigned skills:\n${skillHelp}`);
269
+ const lease = await this.executionGovernor.acquire(tool.execution, `${name}:help`);
270
+ try {
271
+ const execution = executionForLease(tool.execution, lease);
272
+ const nodeArgs = [
273
+ ...(execution?.maxHeapMb ? [`--max-old-space-size=${execution.maxHeapMb}`] : []),
274
+ tool.entry,
275
+ "--help"
276
+ ];
277
+ const invocation = isolatedToolProcessInvocation(nodeArgs, execution);
278
+ const result = await runToolHelpProcess(invocation.command, invocation.args, {
279
+ cwd: tool.dir,
280
+ env: toolProcessEnv(),
281
+ timeoutMs: this.helpTimeoutMs,
282
+ killGraceMs: this.killGraceMs,
283
+ maxOutputBytes: execution?.maxOutputBytes || daemonConfigDefaults.ipcFrameBytes,
284
+ label: `Tool help for ${name}`
285
+ });
286
+ const help = result.stdout || result.stderr;
287
+ const skills = await this.resolveSkills(name);
288
+ const sections = [
289
+ help.trimEnd(),
290
+ formatSemanticMetadata(tool),
291
+ formatToolDependencies(tool, this.tools)
292
+ ];
293
+ if (skills.length) {
294
+ const skillHelp = skills.map((item) => [
295
+ `- ${item.name}${item.when ? ` (${item.when})` : ""}`,
296
+ item.description ? ` ${item.description}` : null,
297
+ item.found ? ` path: ${item.path}` : " warning: skill not found"
298
+ ].filter(Boolean).join("\n")).join("\n");
299
+ sections.push(`Assigned skills:\n${skillHelp}`);
300
+ }
301
+ lease.release({ success: result.code === 0 });
302
+ return `${sections.filter(Boolean).join("\n\n")}\n`;
303
+ } catch (error) {
304
+ lease.release({ memoryLimited: error?.code === "TOOL_PROCESS_MEMORY_LIMIT" });
305
+ throw error;
417
306
  }
418
- return `${sections.filter(Boolean).join("\n\n")}\n`;
419
307
  }
420
308
 
421
309
  async resolveSkills(name) {
@@ -476,7 +364,25 @@ export class ToolRegistry {
476
364
  .sort((left, right) => left.name.localeCompare(right.name));
477
365
  }
478
366
 
479
- async run({ name, request, chatId = null, onEvent = null }) {
367
+ async run(invocation) {
368
+ const tool = this.get(invocation.name);
369
+ if (!tool?.execution?.deduplicateConcurrent) return this.runOnce(invocation);
370
+ const key = concurrentExecutionKey(invocation.name, invocation.chatId, invocation.request);
371
+ const active = this.concurrentExecutions.get(key);
372
+ if (active) {
373
+ this.logger?.log("tools", `joined concurrent duplicate ${invocation.name}`);
374
+ return active;
375
+ }
376
+ const execution = this.runOnce(invocation);
377
+ this.concurrentExecutions.set(key, execution);
378
+ try {
379
+ return await execution;
380
+ } finally {
381
+ if (this.concurrentExecutions.get(key) === execution) this.concurrentExecutions.delete(key);
382
+ }
383
+ }
384
+
385
+ async runOnce({ name, request, chatId = null, onEvent = null }) {
480
386
  const tool = this.get(name);
481
387
  if (!tool) throw new Error(`Tool not found: ${name}`);
482
388
  const dependencyIssue = this.dependencyIssues(name)[0];
@@ -490,6 +396,7 @@ export class ToolRegistry {
490
396
  const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
491
397
  const requestFile = path.join(tmpDir, `.request-${Date.now()}-${randomUUID()}.json`);
492
398
  let lease = null;
399
+ let leaseOutcome = {};
493
400
  let result;
494
401
  try {
495
402
  lease = await this.executionGovernor.acquire(tool.execution, name);
@@ -511,11 +418,25 @@ export class ToolRegistry {
511
418
  result = await runtime.submit(enrichedRequest, { onEvent });
512
419
  } else {
513
420
  await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
514
- const processResult = await runToolProcess("node", [tool.entry, "run", "--request-file", requestFile], {
421
+ const execution = executionForLease(tool.execution, lease);
422
+ const nodeArgs = [
423
+ ...(execution?.maxHeapMb ? [`--max-old-space-size=${execution.maxHeapMb}`] : []),
424
+ tool.entry,
425
+ "run",
426
+ "--request-file",
427
+ requestFile
428
+ ];
429
+ const processInvocation = isolatedToolProcessInvocation(nodeArgs, execution);
430
+ if (processInvocation.isolated) {
431
+ this.logger?.log("tools", `${name} isolated at ${execution.maxMemoryMb} MiB total memory (${execution.maxHeapMb} MiB heap)`);
432
+ }
433
+ const processResult = await runToolProcess(processInvocation.command, processInvocation.args, {
515
434
  cwd: tool.dir,
516
- env: toolEnv(),
435
+ env: toolProcessEnv(),
517
436
  onEvent,
437
+ parserName: name,
518
438
  maxFrameBytes: daemonConfigDefaults.ipcFrameBytes,
439
+ maxOutputBytes: tool.execution?.maxOutputBytes || daemonConfigDefaults.ipcFrameBytes,
519
440
  timeoutMs: this.runTimeoutMs,
520
441
  killGraceMs: this.killGraceMs,
521
442
  label: `Tool run for ${name}`
@@ -523,11 +444,21 @@ export class ToolRegistry {
523
444
  if (processResult.stderr.trim()) {
524
445
  this.logger?.log("tools", `${name} stderr: ${processResult.stderr.trim()}`);
525
446
  }
447
+ if (processResult.code !== 0) {
448
+ const memoryLimited = /heap limit|heap out of memory|allocation failed.*memory|memory cgroup out of memory|\bkilled\b/i.test(processResult.stderr)
449
+ || (processInvocation.isolated && [9, 134, 137].includes(processResult.code));
450
+ const error = new Error(memoryLimited
451
+ ? `Tool ${name} exceeded its isolated memory limit`
452
+ : `Tool ${name} exited with code ${processResult.code}`);
453
+ error.code = memoryLimited ? "TOOL_PROCESS_MEMORY_LIMIT" : "TOOL_PROCESS_EXIT";
454
+ throw error;
455
+ }
526
456
  result = processResult.parsed.mode === "ndjson"
527
457
  ? processResult.parsed.result
528
458
  : JSON.parse(processResult.parsed.output);
529
459
  }
530
460
  const normalized = normalizeToolResult(name, result);
461
+ leaseOutcome = { success: normalized.ok !== false };
531
462
  if (normalized.ok === false) {
532
463
  this.logger?.log("tools", `${name} -> ${normalized.status || "error"}: ${normalized.error || "unknown error"}`);
533
464
  } else {
@@ -535,7 +466,20 @@ export class ToolRegistry {
535
466
  }
536
467
  return normalized;
537
468
  } catch (error) {
538
- if (error?.code === "TOOL_PROCESS_TIMEOUT") {
469
+ leaseOutcome = { memoryLimited: error?.code === "TOOL_PROCESS_MEMORY_LIMIT" };
470
+ if (error?.code === "TOOL_RESOURCE_PRESSURE") {
471
+ return normalizeToolResult(name, {
472
+ ok: false,
473
+ status: "retryable",
474
+ error: error.message,
475
+ resolution: {
476
+ type: "retry_later",
477
+ retry: true,
478
+ message: "The tool was not started. Retry after host memory pressure falls."
479
+ }
480
+ });
481
+ }
482
+ if (["TOOL_PROCESS_TIMEOUT", "TOOL_OUTPUT_LIMIT", "TOOL_PROCESS_MEMORY_LIMIT", "TOOL_PROCESS_EXIT"].includes(error?.code)) {
539
483
  return normalizeToolResult(name, {
540
484
  ok: false,
541
485
  status: "outcome_uncertain",
@@ -543,7 +487,7 @@ export class ToolRegistry {
543
487
  resolution: {
544
488
  type: "status_check_required",
545
489
  retry: false,
546
- message: "The tool process was terminated after timing out. Check external state before retrying."
490
+ message: "The isolated tool process ended without a confirmed result. Check external state before retrying."
547
491
  }
548
492
  });
549
493
  }
@@ -552,7 +496,7 @@ export class ToolRegistry {
552
496
  error: error?.message || `Invalid tool response for ${name}`
553
497
  });
554
498
  } finally {
555
- lease?.release();
499
+ lease?.release(leaseOutcome);
556
500
  await unlink(requestFile).catch(() => {});
557
501
  await rmdir(tmpDir).catch(() => {});
558
502
  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: {} };