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
@@ -12,11 +12,30 @@ function deferred() {
12
12
  return { promise, resolve };
13
13
  }
14
14
 
15
+ const safeMemoryPressure = async () => ({
16
+ availableBytes: 512 * 1024 * 1024,
17
+ totalBytes: 4 * 1024 * 1024 * 1024,
18
+ workerRssBytes: 100 * 1024 * 1024,
19
+ swapTotalBytes: 1024,
20
+ swapUsedPercent: 50
21
+ });
22
+
23
+ const settle = () => new Promise((resolve) => setImmediate(resolve));
24
+
15
25
  test("normalizes manifest weights and configurable class capacities", () => {
16
26
  assert.deepEqual(normalizeToolExecution({ resourceClass: "browser", weight: 2 }), {
17
27
  resourceClass: "browser",
18
- weight: 2
28
+ weight: 2,
29
+ deduplicateConcurrent: false,
30
+ maxHeapMb: 4096,
31
+ maxMemoryMb: 16_384,
32
+ maxOutputBytes: 1_048_576
19
33
  });
34
+ assert.equal(normalizeToolExecution({
35
+ resourceClass: "orchestrator",
36
+ weight: 1,
37
+ deduplicateConcurrent: true
38
+ }).deduplicateConcurrent, true);
20
39
  assert.equal(normalizeToolExecution(undefined), null);
21
40
  assert.throws(() => normalizeToolExecution({ resourceClass: "Browser!", weight: 1 }), /Invalid tool execution resource class/);
22
41
  assert.throws(() => normalizeToolExecution({ resourceClass: "browser", weight: 0 }), /positive integer/);
@@ -27,6 +46,16 @@ test("normalizes manifest weights and configurable class capacities", () => {
27
46
  }), {
28
47
  defaultCapacity: 3,
29
48
  maxQueuedPerClass: 12,
49
+ maxWorkerRssMb: 384,
50
+ maxSwapUsedPercent: 95,
51
+ initialToolMemoryMb: 384,
52
+ minimumToolMemoryMb: 128,
53
+ maximumToolMemoryMb: 4096,
54
+ systemReserveMb: 128,
55
+ coreReserveMb: 384,
56
+ toolHeapPercent: 65,
57
+ toolMemoryHighPercent: 85,
58
+ toolSwapMaxMb: 128,
30
59
  capacities: { browser: 4 }
31
60
  });
32
61
  });
@@ -36,13 +65,15 @@ test("queues weighted work fairly within one resource class", async () => {
36
65
  const governor = new WeightedResourceGovernor({
37
66
  policy: { defaultCapacity: 2 },
38
67
  now: () => time,
39
- memoryUsage: () => ({ rss: 100 * 1024 * 1024 })
68
+ memoryUsage: () => ({ rss: 100 * 1024 * 1024 }),
69
+ memoryPressure: safeMemoryPressure
40
70
  });
41
71
  const execution = { resourceClass: "browser", weight: 1 };
42
72
  const first = await governor.acquire(execution, "first");
43
73
  const second = await governor.acquire(execution, "second");
44
74
  const thirdLease = governor.acquire(execution, "third");
45
75
  const fourthLease = governor.acquire(execution, "fourth");
76
+ await settle();
46
77
 
47
78
  assert.deepEqual(governor.snapshot().resources.browser, {
48
79
  capacity: 2,
@@ -66,26 +97,52 @@ test("queues weighted work fairly within one resource class", async () => {
66
97
  assert.equal(governor.snapshot().resources.browser.activeWeight, 0);
67
98
  });
68
99
 
69
- test("undeclared lightweight work bypasses constrained resource queues", async () => {
70
- const governor = new WeightedResourceGovernor({ policy: { defaultCapacity: 1 } });
100
+ test("undeclared work bypasses the optional resource governor", async () => {
101
+ const governor = new WeightedResourceGovernor({ policy: { defaultCapacity: 1 }, memoryPressure: safeMemoryPressure });
71
102
  const heavy = await governor.acquire({ resourceClass: "browser", weight: 1 }, "heavy");
72
103
  const queued = governor.acquire({ resourceClass: "browser", weight: 1 }, "queued");
104
+ await settle();
73
105
  const light = await governor.acquire(null, "light");
74
106
  assert.equal(light.waitedMs, 0);
107
+ assert.equal(light.memoryLimitMb, undefined);
108
+ assert.equal(light.heapLimitMb, undefined);
75
109
  assert.equal(governor.snapshot().resources.browser.queued, 1);
76
110
  light.release();
77
111
  heavy.release();
78
112
  (await queued).release();
79
113
  });
80
114
 
115
+ test("rejects declared heavy tools before spawn when memory pressure is unsafe", async () => {
116
+ const logs = [];
117
+ const governor = new WeightedResourceGovernor({
118
+ policy: { maxWorkerRssMb: 384, maxSwapUsedPercent: 95 },
119
+ memoryPressure: async () => ({
120
+ availableBytes: 80 * 1024 * 1024,
121
+ workerRssBytes: 400 * 1024 * 1024,
122
+ swapTotalBytes: 1024,
123
+ swapUsedPercent: 50
124
+ }),
125
+ logger: { log: (...parts) => logs.push(parts.join(" ")) }
126
+ });
127
+
128
+ await assert.rejects(
129
+ () => governor.acquire({ resourceClass: "browser", weight: 1 }, "web-browser"),
130
+ (error) => error.code === "TOOL_RESOURCE_PRESSURE" && /was not started/.test(error.message)
131
+ );
132
+ assert.match(logs.join("\n"), /web-browser rejected for browser/);
133
+ assert.equal(governor.snapshot().resources.browser.activeWeight, 0);
134
+ });
135
+
81
136
  test("larger weights consume shared capacity and worker RSS peaks are retained", async () => {
82
137
  let rss = 120 * 1024 * 1024;
83
138
  const governor = new WeightedResourceGovernor({
84
139
  policy: { defaultCapacity: 3 },
85
- memoryUsage: () => ({ rss })
140
+ memoryUsage: () => ({ rss }),
141
+ memoryPressure: safeMemoryPressure
86
142
  });
87
143
  const large = await governor.acquire({ resourceClass: "browser", weight: 2 }, "large");
88
144
  const waiting = governor.acquire({ resourceClass: "browser", weight: 2 }, "waiting");
145
+ await settle();
89
146
  assert.equal(governor.snapshot().resources.browser.queued, 1);
90
147
  rss = 180 * 1024 * 1024;
91
148
  large.release();
@@ -93,3 +150,44 @@ test("larger weights consume shared capacity and worker RSS peaks are retained",
93
150
  assert.equal(governor.snapshot().peakRssBytes, rss);
94
151
  next.release();
95
152
  });
153
+
154
+ test("shares one host memory budget across independent resource classes", async () => {
155
+ const pressure = async () => ({
156
+ availableBytes: 512 * 1024 * 1024,
157
+ totalBytes: 1024 * 1024 * 1024,
158
+ workerRssBytes: 100 * 1024 * 1024,
159
+ swapTotalBytes: 0,
160
+ swapUsedPercent: 0
161
+ });
162
+ const governor = new WeightedResourceGovernor({
163
+ policy: { systemReserveMb: 128, coreReserveMb: 384, initialToolMemoryMb: 384 },
164
+ memoryPressure: pressure
165
+ });
166
+ const browser = await governor.acquire({ resourceClass: "browser" }, "browser");
167
+ const queued = governor.acquire({ resourceClass: "orchestrator" }, "orchestrator");
168
+ await settle();
169
+
170
+ assert.equal(governor.snapshot().memory.budgetMb, 512);
171
+ assert.equal(governor.snapshot().memory.activeMb, 384);
172
+ assert.equal(governor.snapshot().resources.orchestrator.queued, 1);
173
+
174
+ browser.release({ success: true });
175
+ const orchestrator = await queued;
176
+ assert.equal(orchestrator.memoryLimitMb, 384);
177
+ orchestrator.release({ success: true });
178
+ });
179
+
180
+ test("raises a tool memory recommendation after an isolated limit failure", async () => {
181
+ const governor = new WeightedResourceGovernor({
182
+ policy: { initialToolMemoryMb: 256 },
183
+ memoryPressure: safeMemoryPressure
184
+ });
185
+ const first = await governor.acquire({}, "growing-tool");
186
+ assert.equal(first.memoryLimitMb, 256);
187
+ first.release({ memoryLimited: true });
188
+
189
+ const second = await governor.acquire({}, "growing-tool");
190
+ assert.equal(second.memoryLimitMb, 384);
191
+ assert.equal(governor.snapshot().memory.profiles["growing-tool"].memoryLimitFailures, 1);
192
+ second.release({ success: true });
193
+ });
@@ -0,0 +1,79 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ readWorkerHeapPressure,
5
+ WorkerHeapCircuitBreaker,
6
+ WorkerHeapPressureError
7
+ } from "../src/core/agent/worker-heap-circuit-breaker.js";
8
+
9
+ function pressure(percent) {
10
+ return { heapUsed: percent, heapLimit: 100, percent };
11
+ }
12
+
13
+ test("reports bounded worker heap pressure", () => {
14
+ assert.deepEqual(readWorkerHeapPressure({
15
+ memoryUsage: () => ({ heapUsed: 40 }),
16
+ heapStatistics: () => ({ heap_size_limit: 100 })
17
+ }), pressure(40));
18
+ });
19
+
20
+ test("evicts inactive sessions under soft pressure and admits recovered work", async () => {
21
+ const samples = [pressure(75), pressure(60)];
22
+ let evictions = 0;
23
+ const breaker = new WorkerHeapCircuitBreaker({
24
+ lifecycle: { async evictInactive() { evictions += 1; return [{ sessionKey: "idle" }]; } },
25
+ measure: () => samples.shift() || pressure(60)
26
+ });
27
+
28
+ assert.deepEqual(await breaker.admit(), pressure(60));
29
+ assert.equal(evictions, 1);
30
+ assert.deepEqual(breaker.getDiagnostic(), {
31
+ enabled: true,
32
+ softPercent: 70,
33
+ criticalPercent: 82,
34
+ waitMs: 15_000,
35
+ pollMs: 500,
36
+ heapUsed: 60,
37
+ heapLimit: 100,
38
+ currentHeapPercent: 60,
39
+ pressureEvents: 1,
40
+ evictedSessions: 1,
41
+ delayedAdmissions: 0,
42
+ rejectedAdmissions: 0,
43
+ peakHeapPercent: 75
44
+ });
45
+ });
46
+
47
+ test("delays critical admissions until active work releases memory", async () => {
48
+ const samples = [pressure(90), pressure(89), pressure(78)];
49
+ let now = 0;
50
+ const breaker = new WorkerHeapCircuitBreaker({
51
+ lifecycle: { async evictInactive() { return []; } },
52
+ config: { waitMs: 1_000, pollMs: 100 },
53
+ measure: () => samples.shift() || pressure(78),
54
+ now: () => now,
55
+ sleep: async (ms) => { now += ms; }
56
+ });
57
+
58
+ assert.deepEqual(await breaker.admit(), pressure(78));
59
+ assert.equal(breaker.getDiagnostic().delayedAdmissions, 1);
60
+ });
61
+
62
+ test("rejects persistent critical pressure with a retryable error", async () => {
63
+ let now = 0;
64
+ const breaker = new WorkerHeapCircuitBreaker({
65
+ lifecycle: { async evictInactive() { return []; } },
66
+ config: { waitMs: 200, pollMs: 100 },
67
+ measure: () => pressure(90),
68
+ now: () => now,
69
+ sleep: async (ms) => { now += ms; }
70
+ });
71
+
72
+ await assert.rejects(
73
+ breaker.admit(),
74
+ (error) => error instanceof WorkerHeapPressureError
75
+ && error.code === "WORKER_HEAP_PRESSURE"
76
+ && error.retryable === true
77
+ );
78
+ assert.equal(breaker.getDiagnostic().rejectedAdmissions, 1);
79
+ });
@@ -0,0 +1,69 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ consumeWorkerRecoveryReport,
8
+ loadWorkerRecoveryReport,
9
+ recordUnexpectedWorkerExit,
10
+ summarizeRecoveryEvidence
11
+ } from "../src/runtime/worker-recovery-report.js";
12
+
13
+ function localLogTimestamp(date) {
14
+ const two = (value) => String(value).padStart(2, "0");
15
+ return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}`;
16
+ }
17
+
18
+ test("summarizes only bounded crash evidence", () => {
19
+ const evidence = summarizeRecoveryEvidence([
20
+ "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory",
21
+ "[agent] run_tool web-browser",
22
+ "[agent] run_tool web-browser",
23
+ "[agent] run_tool campaign-draft-runner"
24
+ ], { occurredAt: "invalid", signal: "SIGABRT" });
25
+
26
+ assert.equal(evidence.cause, "JavaScript heap out of memory");
27
+ assert.deepEqual(evidence.tools, [["web-browser", 2], ["campaign-draft-runner", 1]]);
28
+ });
29
+
30
+ test("persists, formats, and consumes one automatic recovery report", async () => {
31
+ const directory = await mkdtemp(path.join(os.tmpdir(), "arisa-worker-recovery-"));
32
+ const reportFile = path.join(directory, "report.json");
33
+ const taskFile = path.join(directory, "tasks.json");
34
+ const occurredAt = new Date();
35
+ const report = await recordUnexpectedWorkerExit({
36
+ occurredAt: occurredAt.toISOString(),
37
+ runtimeMs: 10_000,
38
+ restartDelayMs: 2_000,
39
+ consecutiveFailures: 1,
40
+ code: null,
41
+ signal: "SIGABRT",
42
+ detail: "signal=SIGABRT"
43
+ }, { reportFile });
44
+ await writeFile(taskFile, `${JSON.stringify({ tasks: [{
45
+ updatedAt: occurredAt.toISOString(),
46
+ lastOutcome: "outcome_uncertain",
47
+ lastError: "execution interrupted before confirmation"
48
+ }] })}\n`);
49
+ const timestamp = localLogTimestamp(occurredAt);
50
+ const recovery = await loadWorkerRecoveryReport({
51
+ reportFile,
52
+ taskFile,
53
+ readLines: async () => ({ text: [
54
+ `[${timestamp}] [agent] run_tool web-browser`,
55
+ `[${timestamp}] [agent] run_tool web-browser`,
56
+ "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory",
57
+ `[${timestamp}] [service] worker exited unexpectedly (signal=SIGABRT)`
58
+ ].join("\n") }),
59
+ getVersion: async () => "5.1.70"
60
+ });
61
+
62
+ assert.match(recovery.text, /Cause: JavaScript heap out of memory/);
63
+ assert.match(recovery.text, /web-browser ×2/);
64
+ assert.match(recovery.text, /1 scheduled execution was marked outcome-uncertain and not replayed/);
65
+ assert.match(recovery.text, /restarted after 2s; Arisa 5\.1\.70 is running/);
66
+ assert.equal(await consumeWorkerRecoveryReport(report.id, { reportFile }), true);
67
+ await assert.rejects(() => readFile(reportFile), /ENOENT/);
68
+ await rm(directory, { recursive: true, force: true });
69
+ });
@@ -43,6 +43,11 @@ await runtime.workLoop({
43
43
  recover,
44
44
  processJob: async (payload, execution) => {
45
45
  if (payload.action === "fail") throw new Error("synthetic job failure");
46
+ if (payload.action === "hang-until-cancelled") {
47
+ await new Promise((_, reject) => execution.signal.addEventListener("abort", () => {
48
+ reject(Object.assign(new Error("synthetic job cancelled"), { code: "DAEMON_JOB_CANCELLED" }));
49
+ }, { once: true }));
50
+ }
46
51
  if (payload.action === "stream") {
47
52
  await execution.emit("progress", { percent: 50 });
48
53
  await execution.emit("chunk", { text: "partial" });