arisa 5.1.68 → 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 (77) 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-processes.js +19 -3
  18. package/src/core/tools/daemon-protocol.js +72 -0
  19. package/src/core/tools/daemon-runtime.js +13 -490
  20. package/src/core/tools/daemon-worker.js +310 -0
  21. package/src/core/tools/ipc-client.js +2 -2
  22. package/src/core/tools/memory-pressure.js +56 -0
  23. package/src/core/tools/official-tool-installer.js +1 -1
  24. package/src/core/tools/tool-config.js +1 -1
  25. package/src/core/tools/tool-process-output.js +100 -0
  26. package/src/core/tools/tool-process-runner.js +175 -0
  27. package/src/core/tools/tool-registry.js +99 -187
  28. package/src/core/tools/tool-resource-note-store.js +1 -1
  29. package/src/core/tools/tool-usage-store.js +1 -1
  30. package/src/core/tools/weighted-resource-governor.js +188 -38
  31. package/src/index.js +14 -2
  32. package/src/official-tools.lock.json +424 -50
  33. package/src/platform/paths.js +152 -0
  34. package/src/runtime/bootstrap-cli.js +121 -0
  35. package/src/runtime/bootstrap-config.js +97 -0
  36. package/src/runtime/bootstrap-telegram.js +325 -0
  37. package/src/runtime/bootstrap.js +6 -543
  38. package/src/runtime/doctor.js +6 -3
  39. package/src/runtime/flush.js +1 -1
  40. package/src/runtime/ipc/ipc-server.js +1 -1
  41. package/src/runtime/log-viewer.js +1 -1
  42. package/src/runtime/oom-protection.js +20 -0
  43. package/src/runtime/paths.js +3 -151
  44. package/src/runtime/restart-receipt.js +1 -1
  45. package/src/runtime/service-manager.js +1 -1
  46. package/src/runtime/service-supervisor.js +14 -0
  47. package/src/runtime/slave-cli.js +1 -1
  48. package/src/runtime/tool-process-supervisor.js +1 -1
  49. package/src/runtime/tui.js +200 -0
  50. package/src/runtime/update-manager.js +1 -1
  51. package/src/runtime/worker-recovery-report.js +142 -0
  52. package/src/transport/telegram/bot.js +42 -320
  53. package/src/transport/telegram/prompt-builders.js +8 -3
  54. package/src/transport/telegram/telegram-prompt-controller.js +346 -0
  55. package/src/transport/telegram/workspace-topic-store.js +1 -1
  56. package/test/agent-session-lifecycle.test.js +92 -0
  57. package/test/architecture-boundaries.test.js +29 -0
  58. package/test/bootstrap.test.js +65 -0
  59. package/test/daemon-process-invocation.test.js +27 -0
  60. package/test/daemon-runtime.test.js +36 -1
  61. package/test/doctor.test.js +22 -0
  62. package/test/memory-pressure.test.js +36 -0
  63. package/test/model-selection.test.js +11 -1
  64. package/test/official-tool-dependencies.test.js +1 -1
  65. package/test/official-tool-installer.test.js +18 -1
  66. package/test/oom-protection.test.js +32 -0
  67. package/test/paths.test.js +7 -0
  68. package/test/pi-compaction.test.js +9 -0
  69. package/test/service-manager.test.js +6 -1
  70. package/test/telegram-prompt-controller.test.js +81 -0
  71. package/test/telegram-text-artifact.test.js +30 -0
  72. package/test/tool-registry-run.test.js +108 -4
  73. package/test/tui.test.js +41 -0
  74. package/test/weighted-resource-governor.test.js +97 -5
  75. package/test/worker-heap-circuit-breaker.test.js +79 -0
  76. package/test/worker-recovery-report.test.js +69 -0
  77. 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,43 +42,15 @@ 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
- });
68
- }
69
-
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 };
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
+ };
78
54
  }
79
55
 
80
56
  function requirementNames(requirements) {
@@ -85,123 +61,6 @@ function requirementNames(requirements) {
85
61
  return [];
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
-
146
- 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
- }
171
- };
172
- }
173
-
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;
200
- }
201
- const [parsed, stderr] = await Promise.all([stdoutTask, stderrTask]);
202
- return { code, parsed, stderr };
203
- }
204
-
205
64
  function normalizeCategory(category) {
206
65
  if (typeof category !== "string") return null;
207
66
  const trimmed = category.trim();
@@ -407,29 +266,44 @@ export class ToolRegistry {
407
266
  async help(name) {
408
267
  const tool = this.get(name);
409
268
  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}`);
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;
431
306
  }
432
- return `${sections.filter(Boolean).join("\n\n")}\n`;
433
307
  }
434
308
 
435
309
  async resolveSkills(name) {
@@ -522,6 +396,7 @@ export class ToolRegistry {
522
396
  const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
523
397
  const requestFile = path.join(tmpDir, `.request-${Date.now()}-${randomUUID()}.json`);
524
398
  let lease = null;
399
+ let leaseOutcome = {};
525
400
  let result;
526
401
  try {
527
402
  lease = await this.executionGovernor.acquire(tool.execution, name);
@@ -543,11 +418,25 @@ export class ToolRegistry {
543
418
  result = await runtime.submit(enrichedRequest, { onEvent });
544
419
  } else {
545
420
  await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
546
- 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, {
547
434
  cwd: tool.dir,
548
- env: toolEnv(),
435
+ env: toolProcessEnv(),
549
436
  onEvent,
437
+ parserName: name,
550
438
  maxFrameBytes: daemonConfigDefaults.ipcFrameBytes,
439
+ maxOutputBytes: tool.execution?.maxOutputBytes || daemonConfigDefaults.ipcFrameBytes,
551
440
  timeoutMs: this.runTimeoutMs,
552
441
  killGraceMs: this.killGraceMs,
553
442
  label: `Tool run for ${name}`
@@ -555,11 +444,21 @@ export class ToolRegistry {
555
444
  if (processResult.stderr.trim()) {
556
445
  this.logger?.log("tools", `${name} stderr: ${processResult.stderr.trim()}`);
557
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
+ }
558
456
  result = processResult.parsed.mode === "ndjson"
559
457
  ? processResult.parsed.result
560
458
  : JSON.parse(processResult.parsed.output);
561
459
  }
562
460
  const normalized = normalizeToolResult(name, result);
461
+ leaseOutcome = { success: normalized.ok !== false };
563
462
  if (normalized.ok === false) {
564
463
  this.logger?.log("tools", `${name} -> ${normalized.status || "error"}: ${normalized.error || "unknown error"}`);
565
464
  } else {
@@ -567,7 +466,20 @@ export class ToolRegistry {
567
466
  }
568
467
  return normalized;
569
468
  } catch (error) {
570
- 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)) {
571
483
  return normalizeToolResult(name, {
572
484
  ok: false,
573
485
  status: "outcome_uncertain",
@@ -575,7 +487,7 @@ export class ToolRegistry {
575
487
  resolution: {
576
488
  type: "status_check_required",
577
489
  retry: false,
578
- 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."
579
491
  }
580
492
  });
581
493
  }
@@ -584,7 +496,7 @@ export class ToolRegistry {
584
496
  error: error?.message || `Invalid tool response for ${name}`
585
497
  });
586
498
  } finally {
587
- lease?.release();
499
+ lease?.release(leaseOutcome);
588
500
  await unlink(requestFile).catch(() => {});
589
501
  await rmdir(tmpDir).catch(() => {});
590
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: {} };