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.
- package/README.md +7 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +46 -6
- package/src/core/agent/agent-session-lifecycle.js +80 -3
- package/src/core/agent/core-tools.js +1 -1
- package/src/core/agent/pi-auth-login.js +1 -1
- package/src/core/agent/pi-runtime.js +1 -1
- package/src/core/agent/runtime-context.js +1 -1
- package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
- package/src/core/artifacts/artifact-store.js +1 -1
- package/src/core/capabilities/capability-service.js +1 -1
- package/src/core/config/config-defaults.js +30 -1
- package/src/core/config/config-store.js +1 -1
- package/src/core/conversation/session-seed-store.js +1 -1
- package/src/core/tasks/task-store.js +1 -1
- package/src/core/tools/daemon-client.js +180 -0
- package/src/core/tools/daemon-processes.js +19 -3
- package/src/core/tools/daemon-protocol.js +72 -0
- package/src/core/tools/daemon-runtime.js +13 -490
- package/src/core/tools/daemon-worker.js +310 -0
- package/src/core/tools/ipc-client.js +2 -2
- package/src/core/tools/memory-pressure.js +56 -0
- package/src/core/tools/official-tool-installer.js +1 -1
- package/src/core/tools/tool-config.js +1 -1
- package/src/core/tools/tool-process-output.js +100 -0
- package/src/core/tools/tool-process-runner.js +175 -0
- package/src/core/tools/tool-registry.js +99 -187
- package/src/core/tools/tool-resource-note-store.js +1 -1
- package/src/core/tools/tool-usage-store.js +1 -1
- package/src/core/tools/weighted-resource-governor.js +188 -38
- package/src/index.js +14 -2
- package/src/official-tools.lock.json +424 -50
- package/src/platform/paths.js +152 -0
- package/src/runtime/bootstrap-cli.js +121 -0
- package/src/runtime/bootstrap-config.js +97 -0
- package/src/runtime/bootstrap-telegram.js +325 -0
- package/src/runtime/bootstrap.js +6 -543
- package/src/runtime/doctor.js +6 -3
- package/src/runtime/flush.js +1 -1
- package/src/runtime/ipc/ipc-server.js +1 -1
- package/src/runtime/log-viewer.js +1 -1
- package/src/runtime/oom-protection.js +20 -0
- package/src/runtime/paths.js +3 -151
- package/src/runtime/restart-receipt.js +1 -1
- package/src/runtime/service-manager.js +1 -1
- package/src/runtime/service-supervisor.js +14 -0
- package/src/runtime/slave-cli.js +1 -1
- package/src/runtime/tool-process-supervisor.js +1 -1
- package/src/runtime/tui.js +200 -0
- package/src/runtime/update-manager.js +1 -1
- package/src/runtime/worker-recovery-report.js +142 -0
- package/src/transport/telegram/bot.js +42 -320
- package/src/transport/telegram/prompt-builders.js +8 -3
- package/src/transport/telegram/telegram-prompt-controller.js +346 -0
- package/src/transport/telegram/workspace-topic-store.js +1 -1
- package/test/agent-session-lifecycle.test.js +92 -0
- package/test/architecture-boundaries.test.js +29 -0
- package/test/bootstrap.test.js +65 -0
- package/test/daemon-process-invocation.test.js +27 -0
- package/test/daemon-runtime.test.js +36 -1
- package/test/doctor.test.js +22 -0
- package/test/memory-pressure.test.js +36 -0
- package/test/model-selection.test.js +11 -1
- package/test/official-tool-dependencies.test.js +1 -1
- package/test/official-tool-installer.test.js +18 -1
- package/test/oom-protection.test.js +32 -0
- package/test/paths.test.js +7 -0
- package/test/pi-compaction.test.js +9 -0
- package/test/service-manager.test.js +6 -1
- package/test/telegram-prompt-controller.test.js +81 -0
- package/test/telegram-text-artifact.test.js +30 -0
- package/test/tool-registry-run.test.js +108 -4
- package/test/tui.test.js +41 -0
- package/test/weighted-resource-governor.test.js +97 -5
- package/test/worker-heap-circuit-breaker.test.js +79 -0
- package/test/worker-recovery-report.test.js +69 -0
- 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 "../../
|
|
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
|
-
|
|
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
|
+
}
|