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,11 +1,32 @@
1
+ import { memoryPressureReason, readMemoryPressure } from "./memory-pressure.js";
2
+
1
3
  const defaultCapacity = 2;
2
4
  const defaultMaxQueuedPerClass = 100;
5
+ const defaultMaxWorkerRssMb = 384;
6
+ const defaultMaxSwapUsedPercent = 95;
7
+ const defaultInitialToolMemoryMb = 384;
8
+ const defaultMinimumToolMemoryMb = 128;
9
+ const defaultMaximumToolMemoryMb = 4096;
10
+ const defaultSystemReserveMb = 128;
11
+ const defaultCoreReserveMb = 384;
12
+ const defaultToolHeapPercent = 65;
13
+ const defaultToolMemoryHighPercent = 85;
14
+ const defaultToolSwapMaxMb = 128;
15
+ const defaultMaxToolHeapMb = 4096;
16
+ const defaultMaxToolMemoryMb = 16_384;
17
+ const defaultMaxToolOutputBytes = 1_048_576;
18
+ const mebibyte = 1024 * 1024;
3
19
 
4
20
  function positiveInteger(value, fallback) {
5
21
  const parsed = Number(value);
6
22
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
7
23
  }
8
24
 
25
+ function boundedInteger(value, fallback, minimum, maximum) {
26
+ const parsed = Number(value);
27
+ return Number.isSafeInteger(parsed) && parsed >= minimum ? Math.min(parsed, maximum) : fallback;
28
+ }
29
+
9
30
  function resourceClassName(value) {
10
31
  const name = String(value || "").trim();
11
32
  if (!name) return "";
@@ -20,14 +41,17 @@ export function normalizeToolExecution(execution) {
20
41
  if (!execution || typeof execution !== "object" || Array.isArray(execution)) {
21
42
  throw new Error("Tool execution policy must be an object");
22
43
  }
23
- const resourceClass = resourceClassName(execution.resourceClass);
24
- if (!resourceClass) throw new Error("Tool execution resourceClass is required");
25
- const weight = positiveInteger(execution.weight, 0);
44
+ const configured = execution;
45
+ const resourceClass = resourceClassName(configured.resourceClass || "default");
46
+ const weight = configured.weight == null ? 1 : positiveInteger(configured.weight, 0);
26
47
  if (!weight) throw new Error("Tool execution weight must be a positive integer");
27
48
  return {
28
49
  resourceClass,
29
50
  weight,
30
- deduplicateConcurrent: execution.deduplicateConcurrent === true
51
+ deduplicateConcurrent: configured.deduplicateConcurrent === true,
52
+ maxHeapMb: boundedInteger(configured.maxHeapMb, defaultMaxToolHeapMb, 64, 4096),
53
+ maxMemoryMb: boundedInteger(configured.maxMemoryMb, defaultMaxToolMemoryMb, 128, 16_384),
54
+ maxOutputBytes: boundedInteger(configured.maxOutputBytes, defaultMaxToolOutputBytes, 65_536, 16_777_216)
31
55
  };
32
56
  }
33
57
 
@@ -42,6 +66,16 @@ export function normalizeToolExecutionPolicy(policy = {}) {
42
66
  return {
43
67
  defaultCapacity: positiveInteger(policy?.defaultCapacity, defaultCapacity),
44
68
  maxQueuedPerClass: positiveInteger(policy?.maxQueuedPerClass, defaultMaxQueuedPerClass),
69
+ maxWorkerRssMb: boundedInteger(policy?.maxWorkerRssMb, defaultMaxWorkerRssMb, 64, 65_536),
70
+ maxSwapUsedPercent: boundedInteger(policy?.maxSwapUsedPercent, defaultMaxSwapUsedPercent, 1, 100),
71
+ initialToolMemoryMb: boundedInteger(policy?.initialToolMemoryMb, defaultInitialToolMemoryMb, 128, 16_384),
72
+ minimumToolMemoryMb: boundedInteger(policy?.minimumToolMemoryMb, defaultMinimumToolMemoryMb, 64, 4096),
73
+ maximumToolMemoryMb: boundedInteger(policy?.maximumToolMemoryMb, defaultMaximumToolMemoryMb, 128, 16_384),
74
+ systemReserveMb: boundedInteger(policy?.systemReserveMb, defaultSystemReserveMb, 32, 65_536),
75
+ coreReserveMb: boundedInteger(policy?.coreReserveMb, defaultCoreReserveMb, 64, 65_536),
76
+ toolHeapPercent: boundedInteger(policy?.toolHeapPercent, defaultToolHeapPercent, 25, 90),
77
+ toolMemoryHighPercent: boundedInteger(policy?.toolMemoryHighPercent, defaultToolMemoryHighPercent, 50, 95),
78
+ toolSwapMaxMb: boundedInteger(policy?.toolSwapMaxMb, defaultToolSwapMaxMb, 0, 4096),
45
79
  capacities
46
80
  };
47
81
  }
@@ -51,14 +85,26 @@ function noopLease() {
51
85
  }
52
86
 
53
87
  export class WeightedResourceGovernor {
54
- constructor({ policy, logger, now = () => Date.now(), memoryUsage = () => process.memoryUsage() } = {}) {
88
+ constructor({
89
+ policy,
90
+ logger,
91
+ now = () => Date.now(),
92
+ memoryUsage = () => process.memoryUsage(),
93
+ memoryPressure = readMemoryPressure
94
+ } = {}) {
55
95
  this.policy = normalizeToolExecutionPolicy(policy);
56
96
  this.logger = logger;
57
97
  this.now = now;
58
98
  this.memoryUsage = memoryUsage;
99
+ this.memoryPressure = memoryPressure;
59
100
  this.resources = new Map();
101
+ this.memoryProfiles = new Map();
102
+ this.activeMemoryMb = 0;
103
+ this.lastMemoryBudgetMb = 0;
60
104
  this.peakRssBytes = 0;
61
105
  this.lastLoggedRssBytes = 0;
106
+ this.lastPressure = null;
107
+ this.draining = false;
62
108
  }
63
109
 
64
110
  capacityFor(resourceClass) {
@@ -78,75 +124,179 @@ export class WeightedResourceGovernor {
78
124
  const rss = Number(this.memoryUsage()?.rss) || 0;
79
125
  if (rss <= this.peakRssBytes) return;
80
126
  this.peakRssBytes = rss;
81
- const logStepBytes = 8 * 1024 * 1024;
127
+ const logStepBytes = 8 * mebibyte;
82
128
  if (this.lastLoggedRssBytes && rss - this.lastLoggedRssBytes < logStepBytes) return;
83
129
  this.lastLoggedRssBytes = rss;
84
- this.logger?.log("tools", `worker RSS peak ${Math.ceil(rss / 1024 / 1024)} MiB while using ${resourceClass}`);
130
+ this.logger?.log("tools", `worker RSS peak ${Math.ceil(rss / mebibyte)} MiB while using ${resourceClass}`);
131
+ }
132
+
133
+ memoryBudgetMb(snapshot) {
134
+ const totalMb = Math.floor((Number(snapshot?.totalBytes) || 0) / mebibyte);
135
+ const configuredMaximum = this.policy.maximumToolMemoryMb;
136
+ if (!totalMb) return configuredMaximum;
137
+ return Math.max(0, Math.min(
138
+ configuredMaximum,
139
+ totalMb - this.policy.systemReserveMb - this.policy.coreReserveMb
140
+ ));
141
+ }
142
+
143
+ requestedMemoryMb(execution, label, budgetMb) {
144
+ const learned = this.memoryProfiles.get(label)?.recommendedMemoryMb;
145
+ const requested = learned || this.policy.initialToolMemoryMb;
146
+ return Math.min(requested, execution.maxMemoryMb, this.policy.maximumToolMemoryMb, budgetMb);
147
+ }
148
+
149
+ heapLimitMb(execution, memoryLimitMb) {
150
+ const proportional = Math.max(64, Math.floor(memoryLimitMb * this.policy.toolHeapPercent / 100));
151
+ return Math.min(execution.maxHeapMb, proportional);
152
+ }
153
+
154
+ recordOutcome(label, memoryLimitMb, outcome = {}) {
155
+ const current = this.memoryProfiles.get(label) || {
156
+ recommendedMemoryMb: this.policy.initialToolMemoryMb,
157
+ memoryLimitFailures: 0,
158
+ successes: 0
159
+ };
160
+ if (outcome.memoryLimited) {
161
+ current.memoryLimitFailures += 1;
162
+ current.recommendedMemoryMb = Math.min(
163
+ this.policy.maximumToolMemoryMb,
164
+ Math.max(current.recommendedMemoryMb, memoryLimitMb + 128, Math.ceil(memoryLimitMb * 1.5))
165
+ );
166
+ this.logger?.log("tools", `${label} memory recommendation raised to ${current.recommendedMemoryMb} MiB after isolated limit failure`);
167
+ } else if (outcome.success) {
168
+ current.successes += 1;
169
+ }
170
+ this.memoryProfiles.set(label, current);
85
171
  }
86
172
 
87
- createLease(resourceClass, weight, waitedMs) {
173
+ createLease(request, waitedMs) {
88
174
  let released = false;
89
175
  return {
90
176
  waitedMs,
91
- release: () => {
177
+ memoryLimitMb: request.memoryMb,
178
+ heapLimitMb: this.heapLimitMb(request.execution, request.memoryMb),
179
+ memoryHighPercent: this.policy.toolMemoryHighPercent,
180
+ swapMaxMb: this.policy.toolSwapMaxMb,
181
+ release: (outcome = {}) => {
92
182
  if (released) return;
93
183
  released = true;
94
- const state = this.stateFor(resourceClass);
95
- state.activeWeight = Math.max(0, state.activeWeight - weight);
96
- this.observeMemory(resourceClass);
97
- this.drain(resourceClass);
184
+ const state = this.stateFor(request.resourceClass);
185
+ state.activeWeight = Math.max(0, state.activeWeight - request.weight);
186
+ this.activeMemoryMb = Math.max(0, this.activeMemoryMb - request.memoryMb);
187
+ this.recordOutcome(request.label, request.memoryMb, outcome);
188
+ this.observeMemory(request.resourceClass);
189
+ this.drainAll().catch((error) => {
190
+ this.logger?.error("tools", `could not drain tool execution queues: ${error?.message || String(error)}`);
191
+ });
98
192
  }
99
193
  };
100
194
  }
101
195
 
102
- grant(resourceClass, request) {
103
- const state = this.stateFor(resourceClass);
196
+ grant(request) {
197
+ const state = this.stateFor(request.resourceClass);
104
198
  state.activeWeight += request.weight;
199
+ this.activeMemoryMb += request.memoryMb;
105
200
  const waitedMs = Math.max(0, this.now() - request.queuedAt);
106
- this.observeMemory(resourceClass);
201
+ this.observeMemory(request.resourceClass);
107
202
  if (waitedMs > 0) {
108
- this.logger?.log("tools", `${request.label} acquired ${resourceClass} capacity after ${waitedMs}ms`);
203
+ this.logger?.log("tools", `${request.label} acquired ${request.resourceClass} capacity and ${request.memoryMb} MiB after ${waitedMs}ms`);
109
204
  }
110
- request.resolve(this.createLease(resourceClass, request.weight, waitedMs));
205
+ request.resolve(this.createLease(request, waitedMs));
111
206
  }
112
207
 
113
- drain(resourceClass) {
114
- const state = this.stateFor(resourceClass);
115
- const capacity = this.capacityFor(resourceClass);
116
- while (state.queue.length) {
117
- const next = state.queue[0];
118
- if (state.activeWeight + next.weight > capacity) break;
119
- state.queue.shift();
120
- this.grant(resourceClass, next);
208
+ async pressureSnapshot(label, resourceClass) {
209
+ const pressure = await this.memoryPressure();
210
+ this.lastPressure = pressure;
211
+ this.lastMemoryBudgetMb = this.memoryBudgetMb(pressure);
212
+ const reason = memoryPressureReason(pressure, this.policy);
213
+ if (!reason) return pressure;
214
+ const error = new Error(`Tool ${label} was not started because ${reason}`);
215
+ error.code = "TOOL_RESOURCE_PRESSURE";
216
+ this.logger?.log("tools", `${label} rejected for ${resourceClass}: ${reason}`);
217
+ throw error;
218
+ }
219
+
220
+ canGrant(request) {
221
+ const state = this.stateFor(request.resourceClass);
222
+ return state.activeWeight + request.weight <= this.capacityFor(request.resourceClass)
223
+ && this.activeMemoryMb + request.memoryMb <= this.lastMemoryBudgetMb;
224
+ }
225
+
226
+ async drainAll() {
227
+ if (this.draining) return;
228
+ this.draining = true;
229
+ try {
230
+ let progressed = true;
231
+ while (progressed) {
232
+ progressed = false;
233
+ for (const [resourceClass, state] of this.resources) {
234
+ const next = state.queue[0];
235
+ if (!next) continue;
236
+ try {
237
+ const pressure = await this.pressureSnapshot(next.label, resourceClass);
238
+ const budgetMb = this.memoryBudgetMb(pressure);
239
+ next.memoryMb = this.requestedMemoryMb(next.execution, next.label, budgetMb);
240
+ if (next.memoryMb < this.policy.minimumToolMemoryMb || !this.canGrant(next)) continue;
241
+ state.queue.shift();
242
+ this.grant(next);
243
+ progressed = true;
244
+ } catch (error) {
245
+ state.queue.shift();
246
+ next.reject(error);
247
+ progressed = true;
248
+ }
249
+ }
250
+ }
251
+ } finally {
252
+ this.draining = false;
121
253
  }
122
254
  }
123
255
 
124
- acquire(execution, label = "tool") {
125
- if (!execution) return Promise.resolve(noopLease());
256
+ async acquire(rawExecution, label = "tool") {
257
+ const execution = normalizeToolExecution(rawExecution);
258
+ if (!execution) return noopLease();
126
259
  const resourceClass = execution.resourceClass;
127
260
  const capacity = this.capacityFor(resourceClass);
128
261
  const weight = Math.min(execution.weight, capacity);
129
262
  const state = this.stateFor(resourceClass);
130
- if (!state.queue.length && state.activeWeight + weight <= capacity) {
131
- return new Promise((resolve) => this.grant(resourceClass, {
132
- weight,
133
- label,
134
- queuedAt: this.now(),
135
- resolve
136
- }));
263
+ const pressure = await this.pressureSnapshot(label, resourceClass);
264
+ const budgetMb = this.memoryBudgetMb(pressure);
265
+ const memoryMb = this.requestedMemoryMb(execution, label, budgetMb);
266
+ if (budgetMb < this.policy.minimumToolMemoryMb || memoryMb < this.policy.minimumToolMemoryMb) {
267
+ const error = new Error(`Tool ${label} was not started because the global tool memory pool is ${budgetMb} MiB`);
268
+ error.code = "TOOL_RESOURCE_PRESSURE";
269
+ throw error;
270
+ }
271
+ const request = {
272
+ execution,
273
+ resourceClass,
274
+ weight,
275
+ memoryMb,
276
+ label,
277
+ queuedAt: this.now()
278
+ };
279
+ if (!state.queue.length && this.canGrant(request)) {
280
+ return new Promise((resolve) => this.grant({ ...request, resolve }));
137
281
  }
138
282
  if (state.queue.length >= this.policy.maxQueuedPerClass) {
139
283
  throw new Error(`Tool execution queue is full for resource class ${resourceClass}`);
140
284
  }
141
- this.logger?.log("tools", `${label} queued for ${resourceClass} capacity (${state.activeWeight}/${capacity})`);
142
- return new Promise((resolve) => {
143
- state.queue.push({ weight, label, queuedAt: this.now(), resolve });
285
+ this.logger?.log("tools", `${label} queued for ${resourceClass} capacity and global memory (${this.activeMemoryMb}/${budgetMb} MiB)`);
286
+ return new Promise((resolve, reject) => {
287
+ state.queue.push({ ...request, resolve, reject });
144
288
  });
145
289
  }
146
290
 
147
291
  snapshot() {
148
292
  return {
149
293
  peakRssBytes: this.peakRssBytes,
294
+ memoryPressure: this.lastPressure,
295
+ memory: {
296
+ budgetMb: this.lastMemoryBudgetMb,
297
+ activeMb: this.activeMemoryMb,
298
+ profiles: Object.fromEntries([...this.memoryProfiles.entries()].map(([name, profile]) => [name, { ...profile }]))
299
+ },
150
300
  resources: Object.fromEntries([...this.resources.entries()].map(([name, state]) => [name, {
151
301
  capacity: this.capacityFor(name),
152
302
  activeWeight: state.activeWeight,
package/src/index.js CHANGED
@@ -6,11 +6,14 @@ import { loadConfig } from "./core/config/config-store.js";
6
6
  import { createLogger } from "./runtime/logger.js";
7
7
  import { getServiceStatus, handoffServiceRestart, registerServiceProcess, restartService, serviceEntryFile, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
8
8
  import { createServiceSupervisor } from "./runtime/service-supervisor.js";
9
+ import { recordUnexpectedWorkerExit } from "./runtime/worker-recovery-report.js";
9
10
  import { flushArisaHome } from "./runtime/flush.js";
10
11
  import { readPackageVersion, showServiceLogs } from "./runtime/log-viewer.js";
11
- import { arisaPackageDir } from "./runtime/paths.js";
12
+ import { arisaPackageDir } from "./platform/paths.js";
12
13
  import { runSlaveCli } from "./runtime/slave-cli.js";
13
14
  import { unregisterSlaveServiceProcess } from "./runtime/slave-service.js";
15
+ import { protectCoreFromOom } from "./runtime/oom-protection.js";
16
+ import { runTui } from "./runtime/tui.js";
14
17
 
15
18
  process.env.ARISA_PACKAGE_DIR = arisaPackageDir;
16
19
 
@@ -25,6 +28,9 @@ const slaveCommand = command === "slave";
25
28
  const slaveServiceRunner = slaveCommand && serviceRunner;
26
29
  const runtimeOverrides = toNestedOverrides(cli.nestedFlags);
27
30
  const logger = createLogger({ verbose });
31
+ if (command === "run" || command === "tui" || serviceRunner || serviceWorker || slaveServiceRunner) {
32
+ await protectCoreFromOom({ logger });
33
+ }
28
34
  let activeApp = null;
29
35
  let shuttingDown = false;
30
36
 
@@ -245,7 +251,8 @@ async function main() {
245
251
  restartBackoffMs: persistedConfig.service.workerRestartBackoffMs,
246
252
  restartBackoffMaxMs: persistedConfig.service.workerRestartBackoffMaxMs,
247
253
  stableRuntimeMs: persistedConfig.service.workerStableRuntimeMs,
248
- logger
254
+ logger,
255
+ onUnexpectedExit: recordUnexpectedWorkerExit
249
256
  });
250
257
  activeApp = supervisor;
251
258
  await supervisor.start();
@@ -257,6 +264,11 @@ async function main() {
257
264
  return;
258
265
  }
259
266
 
267
+ if (command === "tui") {
268
+ await runTui({ logger });
269
+ return;
270
+ }
271
+
260
272
  if (command === "start") {
261
273
  const bootstrapResult = await bootstrapIfNeeded({ force: forceBootstrap });
262
274
  if (bootstrapResult.configCreated && bootstrapResult.viaTelegram && !bootstrapResult.startInBackground) {