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
@@ -0,0 +1,180 @@
1
+ import crypto from "node:crypto";
2
+ import net from "node:net";
3
+ import { mkdir, readFile } from "node:fs/promises";
4
+ import { daemonPaths, readJson, writeJson } from "./daemon-processes.js";
5
+ import { loadDaemonPolicy } from "./daemon-policy.js";
6
+ import {
7
+ DAEMON_CONTROL_FIELD,
8
+ DAEMON_PROTOCOL_VERSION,
9
+ daemonJobPaths,
10
+ daemonTerminalResult,
11
+ validateDaemonEvent
12
+ } from "./daemon-protocol.js";
13
+
14
+ function sleep(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+
18
+ async function readCapability(paths) {
19
+ const token = (await readFile(paths.capabilityFile, "utf8")).trim();
20
+ if (!token) throw new Error(`Invalid daemon capability for ${paths.toolName}`);
21
+ return token;
22
+ }
23
+
24
+ export async function connectDaemon(paths, request, { timeoutMs, onEvent, maxFrameBytes }) {
25
+ const startedAt = Date.now();
26
+ const token = await readCapability(paths);
27
+ let lastError;
28
+ while (Date.now() - startedAt < timeoutMs) {
29
+ try {
30
+ return await new Promise((resolve, reject) => {
31
+ const socket = net.createConnection(paths.socketFile);
32
+ let buffer = "";
33
+ let sequence = 0;
34
+ let terminalSeen = false;
35
+ let settled = false;
36
+ let submitted = false;
37
+ let timeoutTriggered = false;
38
+ let observerChain = Promise.resolve();
39
+ const remainingMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
40
+ let cancelTimer;
41
+ const timeoutError = Object.assign(
42
+ new Error(`${paths.toolName} daemon job timed out after ${timeoutMs}ms`),
43
+ { code: "DAEMON_JOB_TIMEOUT" }
44
+ );
45
+ const timer = setTimeout(() => {
46
+ timeoutTriggered = true;
47
+ if (submitted && !socket.destroyed && socket.writable) {
48
+ const frame = `${JSON.stringify({
49
+ version: DAEMON_PROTOCOL_VERSION,
50
+ type: "cancel",
51
+ jobId: request.jobId,
52
+ capabilityToken: token
53
+ })}\n`;
54
+ socket.end(frame, () => finish(reject, timeoutError));
55
+ cancelTimer = setTimeout(() => finish(reject, timeoutError), 50);
56
+ cancelTimer.unref?.();
57
+ return;
58
+ }
59
+ finish(reject, timeoutError);
60
+ }, remainingMs);
61
+ timer.unref?.();
62
+
63
+ function finish(fn, value) {
64
+ if (settled) return;
65
+ settled = true;
66
+ clearTimeout(timer);
67
+ clearTimeout(cancelTimer);
68
+ socket.destroy();
69
+ fn(value);
70
+ }
71
+
72
+ socket.setEncoding("utf8");
73
+ socket.once("connect", () => {
74
+ if (timeoutTriggered) return;
75
+ submitted = true;
76
+ socket.write(`${JSON.stringify({
77
+ version: DAEMON_PROTOCOL_VERSION,
78
+ type: "submit",
79
+ jobId: request.jobId,
80
+ capabilityToken: token
81
+ })}\n`);
82
+ });
83
+ socket.on("data", (chunk) => {
84
+ buffer += chunk;
85
+ if (Buffer.byteLength(buffer, "utf8") > maxFrameBytes && !buffer.includes("\n")) {
86
+ finish(reject, new Error(`Daemon IPC frame exceeds ${maxFrameBytes} bytes`));
87
+ return;
88
+ }
89
+ let newlineIndex = buffer.indexOf("\n");
90
+ while (newlineIndex !== -1) {
91
+ const line = buffer.slice(0, newlineIndex).trim();
92
+ buffer = buffer.slice(newlineIndex + 1);
93
+ newlineIndex = buffer.indexOf("\n");
94
+ if (!line) continue;
95
+ let frame;
96
+ try {
97
+ frame = JSON.parse(line);
98
+ const validated = validateDaemonEvent(frame, {
99
+ jobId: request.jobId,
100
+ previousSequence: sequence,
101
+ terminalSeen
102
+ });
103
+ sequence = validated.sequence;
104
+ terminalSeen = validated.terminal;
105
+ } catch (error) {
106
+ finish(reject, error);
107
+ return;
108
+ }
109
+ observerChain = observerChain.then(() => onEvent?.(frame));
110
+ if (terminalSeen) {
111
+ observerChain.then(() => {
112
+ if (timeoutTriggered) {
113
+ finish(reject, timeoutError);
114
+ return;
115
+ }
116
+ try {
117
+ finish(resolve, daemonTerminalResult(frame));
118
+ } catch (error) {
119
+ finish(reject, error);
120
+ }
121
+ }, (error) => finish(reject, timeoutTriggered ? timeoutError : error));
122
+ }
123
+ }
124
+ });
125
+ socket.once("error", (error) => finish(reject, error));
126
+ socket.once("close", () => {
127
+ if (!settled) finish(reject, timeoutTriggered
128
+ ? timeoutError
129
+ : new Error("Daemon IPC connection closed before terminal result"));
130
+ });
131
+ });
132
+ } catch (error) {
133
+ lastError = error;
134
+ if (!["ENOENT", "ECONNREFUSED"].includes(error?.code)) throw error;
135
+ await sleep(Math.min(25, Math.max(1, timeoutMs - (Date.now() - startedAt))));
136
+ }
137
+ }
138
+ const error = new Error(`${paths.toolName} daemon IPC was unavailable after ${timeoutMs}ms`);
139
+ error.code = lastError?.code || "DAEMON_IPC_UNAVAILABLE";
140
+ throw error;
141
+ }
142
+
143
+ export async function enqueueDaemonJob(paths, payload, {
144
+ control = false,
145
+ timeoutMs,
146
+ onEvent,
147
+ jobId,
148
+ maxFrameBytes
149
+ } = {}) {
150
+ await mkdir(paths.commandsDir, { recursive: true });
151
+ const id = jobId || `${control ? "control" : "job"}-${crypto.randomUUID()}`;
152
+ const files = daemonJobPaths(paths, id);
153
+ const existingResult = await readJson(files.result, null);
154
+ if (existingResult?.terminal) {
155
+ await onEvent?.(existingResult.terminal);
156
+ return daemonTerminalResult(existingResult.terminal);
157
+ }
158
+ const existingRequest = await readJson(files.request, null);
159
+ const existingAccepted = await readJson(files.processing, null);
160
+ if (!existingRequest && !existingAccepted) {
161
+ await writeJson(files.request, {
162
+ id,
163
+ status: "queued",
164
+ queuedAt: new Date().toISOString(),
165
+ payload
166
+ });
167
+ }
168
+ return connectDaemon(paths, { jobId: id }, { timeoutMs, onEvent, maxFrameBytes });
169
+ }
170
+
171
+ export async function submitDaemonControl(record, operation, { timeoutMs, onEvent } = {}) {
172
+ const paths = daemonPaths({ toolName: record.toolName, scope: record.scope });
173
+ const policy = await loadDaemonPolicy();
174
+ return enqueueDaemonJob(paths, { [DAEMON_CONTROL_FIELD]: { operation } }, {
175
+ control: true,
176
+ timeoutMs: timeoutMs ?? policy.healthTimeoutMs,
177
+ onEvent,
178
+ maxFrameBytes: policy.ipcFrameBytes || 1_048_576
179
+ });
180
+ }
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import crypto from "node:crypto";
3
- import { closeSync, openSync } from "node:fs";
3
+ import { closeSync, existsSync, openSync } from "node:fs";
4
4
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import {
@@ -9,7 +9,7 @@ import {
9
9
  getDaemonInstanceId,
10
10
  normalizeDaemonScope,
11
11
  toolStateDir
12
- } from "../../runtime/paths.js";
12
+ } from "../../platform/paths.js";
13
13
  import { loadDaemonPolicy } from "./daemon-policy.js";
14
14
 
15
15
  export const DAEMON_STATES = Object.freeze([
@@ -22,6 +22,21 @@ export const DAEMON_STATES = Object.freeze([
22
22
  "failed"
23
23
  ]);
24
24
 
25
+ export function daemonProcessInvocation(entryPath, {
26
+ platform = process.platform,
27
+ nodePath = process.execPath,
28
+ oomAdjustAvailable = existsSync("/usr/bin/choom")
29
+ } = {}) {
30
+ if (platform !== "linux" || !oomAdjustAvailable) {
31
+ return { command: nodePath, args: [entryPath, "daemon"], oomProtected: false };
32
+ }
33
+ return {
34
+ command: "/usr/bin/choom",
35
+ args: ["-n", "500", "--", nodePath, entryPath, "daemon"],
36
+ oomProtected: true
37
+ };
38
+ }
39
+
25
40
  function daemonIdentity(toolNameOrOptions, scope) {
26
41
  if (typeof toolNameOrOptions === "string") {
27
42
  return { toolName: toolNameOrOptions, scope: normalizeDaemonScope(scope) };
@@ -367,7 +382,8 @@ export async function startManagedDaemon({
367
382
  const out = openSync(paths.logFile, "a");
368
383
  let child;
369
384
  try {
370
- child = spawn(process.execPath, [entryPath, "daemon"], {
385
+ const invocation = daemonProcessInvocation(entryPath);
386
+ child = spawn(invocation.command, invocation.args, {
371
387
  detached: false,
372
388
  stdio: ["ignore", out, out],
373
389
  env: {
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+
3
+ export const DAEMON_PROTOCOL_VERSION = 1;
4
+ export const DAEMON_EVENT_TYPES = Object.freeze(["accepted", "progress", "chunk", "completed", "failed"]);
5
+ export const DAEMON_CONTROL_FIELD = "__daemon";
6
+
7
+ const TERMINAL_EVENT_TYPES = new Set(["completed", "failed"]);
8
+
9
+ export function daemonJobPaths(paths, id) {
10
+ return {
11
+ request: path.join(paths.commandsDir, `${id}.request.json`),
12
+ processing: path.join(paths.commandsDir, `${id}.processing.json`),
13
+ result: path.join(paths.commandsDir, `${id}.result.json`)
14
+ };
15
+ }
16
+
17
+ export function daemonFrame(jobId, type, sequence, payload = {}) {
18
+ return { version: DAEMON_PROTOCOL_VERSION, jobId, type, sequence, payload };
19
+ }
20
+
21
+ export function daemonTerminalResult(frame) {
22
+ if (frame.type === "failed") {
23
+ const error = new Error(frame.payload?.error || "Daemon job failed");
24
+ if (frame.payload?.code) error.code = frame.payload.code;
25
+ throw error;
26
+ }
27
+ return frame.payload?.output || {};
28
+ }
29
+
30
+ export function validateDaemonEvent(frame, { jobId, previousSequence = 0, terminalSeen = false } = {}) {
31
+ if (!frame || frame.version !== DAEMON_PROTOCOL_VERSION || frame.jobId !== jobId) {
32
+ throw new Error("Invalid daemon event identity or protocol version");
33
+ }
34
+ if (!DAEMON_EVENT_TYPES.includes(frame.type)) throw new Error(`Invalid daemon event type: ${frame.type}`);
35
+ if (!Number.isSafeInteger(frame.sequence) || frame.sequence <= previousSequence) {
36
+ throw new Error(`Invalid daemon event sequence for ${jobId}: ${frame.sequence}`);
37
+ }
38
+ if (terminalSeen) throw new Error(`Daemon job ${jobId} emitted more than one terminal event`);
39
+ return { sequence: frame.sequence, terminal: TERMINAL_EVENT_TYPES.has(frame.type) };
40
+ }
41
+
42
+ export async function writeDaemonSocketFrame(socket, frame, {
43
+ maxFrameBytes = 1_048_576,
44
+ streamBufferBytes = 1_048_576
45
+ } = {}) {
46
+ if (socket.destroyed || !socket.writable) return false;
47
+ const encoded = `${JSON.stringify(frame)}\n`;
48
+ if (Buffer.byteLength(encoded, "utf8") > maxFrameBytes) {
49
+ throw new Error(`Daemon IPC frame exceeds ${maxFrameBytes} bytes`);
50
+ }
51
+ if (socket.writableLength >= streamBufferBytes) {
52
+ await waitForWritableSocket(socket);
53
+ if (socket.destroyed || !socket.writable) return false;
54
+ }
55
+ if (socket.write(encoded)) return true;
56
+ await waitForWritableSocket(socket);
57
+ return !socket.destroyed;
58
+ }
59
+
60
+ function waitForWritableSocket(socket) {
61
+ return new Promise((resolve) => {
62
+ const finish = () => {
63
+ socket.off("drain", finish);
64
+ socket.off("close", finish);
65
+ socket.off("error", finish);
66
+ resolve();
67
+ };
68
+ socket.once("drain", finish);
69
+ socket.once("close", finish);
70
+ socket.once("error", finish);
71
+ });
72
+ }