arisa 5.1.68 → 5.2.17
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 +28 -6
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +123 -8
- package/src/core/agent/agent-session-lifecycle.js +108 -8
- package/src/core/agent/agent-turn-coordinator.js +143 -0
- package/src/core/agent/core-tools.js +1 -1
- package/src/core/agent/pi-auth-login.js +1 -1
- package/src/core/agent/pi-capability-tools.js +7 -4
- package/src/core/agent/pi-runtime.js +1 -1
- package/src/core/agent/runtime-context.js +1 -1
- package/src/core/agent/session-history-reader.js +168 -0
- package/src/core/agent/session-preload-migration.js +162 -0
- package/src/core/agent/session-rotation.js +29 -0
- package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
- package/src/core/agent/worker-tool-fanout.js +117 -0
- package/src/core/artifacts/artifact-store.js +1 -1
- package/src/core/capabilities/capability-service.js +4 -3
- package/src/core/config/config-defaults.js +59 -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-runner.js +8 -4
- package/src/core/tasks/task-store.js +48 -5
- package/src/core/tools/daemon-client.js +180 -0
- package/src/core/tools/daemon-processes.js +26 -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 +131 -191
- 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 +189 -38
- package/src/index.js +14 -2
- package/src/official-tools.lock.json +526 -78
- 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 +26 -76
- 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/obsolete-daemon-reaper.js +43 -0
- package/src/runtime/oom-protection.js +20 -0
- package/src/runtime/paths.js +3 -151
- package/src/runtime/process-inspection.js +78 -0
- 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 +36 -9
- 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 +45 -321
- package/src/transport/telegram/chat-queue.js +6 -2
- package/src/transport/telegram/prompt-builders.js +8 -3
- package/src/transport/telegram/task-dispatcher.js +36 -11
- package/src/transport/telegram/telegram-prompt-controller.js +359 -0
- package/src/transport/telegram/workspace-topic-store.js +1 -1
- package/test/agent-session-lifecycle.test.js +92 -0
- package/test/agent-turn-coordinator.test.js +45 -0
- package/test/architecture-boundaries.test.js +29 -0
- package/test/bootstrap.test.js +65 -0
- package/test/context-and-task-bounds.test.js +2 -1
- package/test/daemon-process-invocation.test.js +27 -0
- package/test/daemon-runtime.test.js +38 -5
- package/test/doctor.test.js +41 -0
- package/test/memory-pressure.test.js +41 -0
- package/test/model-selection.test.js +11 -1
- package/test/obsolete-daemon-reaper.test.js +61 -0
- package/test/official-tool-dependencies.test.js +7 -2
- 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 +30 -0
- package/test/service-manager.test.js +6 -1
- package/test/session-history-reader.test.js +84 -0
- package/test/session-preload-migration.test.js +120 -0
- package/test/session-rotation.test.js +110 -0
- package/test/task-store.test.js +32 -0
- package/test/telegram-prompt-controller.test.js +82 -0
- package/test/telegram-task-dispatcher.test.js +66 -5
- package/test/telegram-text-artifact.test.js +30 -0
- package/test/tool-registry-run.test.js +117 -4
- package/test/tui.test.js +41 -0
- package/test/weighted-resource-governor.test.js +125 -5
- package/test/worker-heap-circuit-breaker.test.js +79 -0
- package/test/worker-recovery-report.test.js +69 -0
- package/test/worker-tool-fanout.test.js +79 -0
- package/test-fixtures/fake-daemon.js +5 -0
package/src/runtime/doctor.js
CHANGED
|
@@ -1,86 +1,21 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { statfs } from "node:fs/promises";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import process from "node:process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
4
|
import { stopManagedDaemon, unregisterManagedDaemon } from "../core/tools/daemon-processes.js";
|
|
7
5
|
import { getServiceStatus, serviceEntryFile } from "./service-manager.js";
|
|
8
|
-
import { arisaHomeDir } from "
|
|
6
|
+
import { arisaHomeDir } from "../platform/paths.js";
|
|
9
7
|
import { renderTextReport, reportRow, wrapReportText } from "./report-format.js";
|
|
8
|
+
import {
|
|
9
|
+
isArisaServiceProcess as matchesArisaServiceProcess,
|
|
10
|
+
isDaemonProcess,
|
|
11
|
+
listSystemProcesses,
|
|
12
|
+
terminateProcess
|
|
13
|
+
} from "./process-inspection.js";
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function parsePosixProcesses(output) {
|
|
14
|
-
return String(output || "")
|
|
15
|
-
.split("\n")
|
|
16
|
-
.map((line) => /^\s*(\d+)\s+(.+)$/.exec(line))
|
|
17
|
-
.filter(Boolean)
|
|
18
|
-
.map((match) => ({ pid: Number(match[1]), command: match[2] }));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseWindowsProcesses(output) {
|
|
22
|
-
const parsed = JSON.parse(output || "[]");
|
|
23
|
-
const records = Array.isArray(parsed) ? parsed : [parsed];
|
|
24
|
-
return records
|
|
25
|
-
.filter((record) => record?.ProcessId && record?.CommandLine)
|
|
26
|
-
.map((record) => ({ pid: Number(record.ProcessId), command: String(record.CommandLine) }));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function listSystemProcesses({ timeoutMs, platform = process.platform } = {}) {
|
|
30
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
31
|
-
throw new Error("Doctor process inspection requires a positive timeoutMs");
|
|
32
|
-
}
|
|
33
|
-
if (platform === "win32") {
|
|
34
|
-
const { stdout } = await execFileAsync("powershell.exe", [
|
|
35
|
-
"-NoProfile",
|
|
36
|
-
"-Command",
|
|
37
|
-
"Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress"
|
|
38
|
-
], { timeout: timeoutMs, windowsHide: true });
|
|
39
|
-
return parseWindowsProcesses(stdout);
|
|
40
|
-
}
|
|
41
|
-
const { stdout } = await execFileAsync("ps", ["-ww", "-axo", "pid=,command="], { timeout: timeoutMs });
|
|
42
|
-
return parsePosixProcesses(stdout);
|
|
43
|
-
}
|
|
15
|
+
export { listSystemProcesses, terminateProcess } from "./process-inspection.js";
|
|
44
16
|
|
|
45
17
|
function isArisaServiceProcess(record) {
|
|
46
|
-
return record
|
|
47
|
-
&& /(?:^|\s)--service-runner(?:\s|$)/.test(record.command);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isDaemonProcess(record, daemon) {
|
|
51
|
-
return record.command.includes(daemon.entryPath)
|
|
52
|
-
&& /(?:^|\s)daemon(?:\s|$)/.test(record.command);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function sleep(ms) {
|
|
56
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isAlive(pid) {
|
|
60
|
-
try {
|
|
61
|
-
process.kill(pid, 0);
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function terminateProcess(pid, { forceAfterMs } = {}) {
|
|
69
|
-
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
|
|
70
|
-
throw new Error(`Refusing to terminate invalid doctor target: ${pid}`);
|
|
71
|
-
}
|
|
72
|
-
if (!Number.isFinite(forceAfterMs) || forceAfterMs <= 0) {
|
|
73
|
-
throw new Error("Doctor process cleanup requires a positive forceAfterMs");
|
|
74
|
-
}
|
|
75
|
-
if (!isAlive(pid)) return false;
|
|
76
|
-
process.kill(pid, "SIGTERM");
|
|
77
|
-
const startedAt = Date.now();
|
|
78
|
-
while (Date.now() - startedAt < forceAfterMs) {
|
|
79
|
-
if (!isAlive(pid)) return true;
|
|
80
|
-
await sleep(Math.min(100, forceAfterMs));
|
|
81
|
-
}
|
|
82
|
-
if (isAlive(pid)) process.kill(pid, "SIGKILL");
|
|
83
|
-
return true;
|
|
18
|
+
return matchesArisaServiceProcess(record, serviceEntryFile);
|
|
84
19
|
}
|
|
85
20
|
|
|
86
21
|
function daemonLabel(record) {
|
|
@@ -314,7 +249,8 @@ export async function runDoctor({
|
|
|
314
249
|
unregisterDaemon = unregisterManagedDaemon,
|
|
315
250
|
inspectResources = inspectSystemResources,
|
|
316
251
|
inspectInfrastructure = null,
|
|
317
|
-
inspectToolDependencies = null
|
|
252
|
+
inspectToolDependencies = null,
|
|
253
|
+
supervisorPid = Number.parseInt(process.env.ARISA_SUPERVISOR_PID || "", 10)
|
|
318
254
|
}) {
|
|
319
255
|
assertDoctorPolicy(doctorPolicy);
|
|
320
256
|
const runtime = await agentManager.getRuntimeDiagnostic();
|
|
@@ -362,7 +298,9 @@ export async function runDoctor({
|
|
|
362
298
|
|
|
363
299
|
const processByPid = new Map(processes.map((record) => [record.pid, record]));
|
|
364
300
|
const currentService = await serviceStatus();
|
|
365
|
-
|
|
301
|
+
const belongsToCurrentService = currentService.pid === process.pid
|
|
302
|
+
|| (Number.isSafeInteger(supervisorPid) && currentService.pid === supervisorPid);
|
|
303
|
+
if (currentService.running && !belongsToCurrentService) {
|
|
366
304
|
const registered = processByPid.get(currentService.pid);
|
|
367
305
|
if (registered && isArisaServiceProcess(registered)) {
|
|
368
306
|
try {
|
|
@@ -383,6 +321,18 @@ export async function runDoctor({
|
|
|
383
321
|
}
|
|
384
322
|
for (const result of report.daemons) {
|
|
385
323
|
const label = daemonLabel(result.record);
|
|
324
|
+
if (result.outcome === "obsolete-removed") {
|
|
325
|
+
report.repairs.push(`Removed obsolete daemon ${label}: ${result.reason}.`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (result.outcome === "obsolete-unverified") {
|
|
329
|
+
report.attention.push(`${label} is obsolete, but its live process identity could not be verified.`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (result.outcome === "obsolete-cleanup-error") {
|
|
333
|
+
report.attention.push(`${label} obsolete daemon cleanup failed: ${result.error || "unknown error"}.`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
386
336
|
if (result.outcome === "stale-registration") {
|
|
387
337
|
const pid = result.diagnostic?.pid;
|
|
388
338
|
const registered = pid ? processByPid.get(pid) : null;
|
package/src/runtime/flush.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { chmod, mkdir, unlink } from "node:fs/promises";
|
|
4
|
-
import { arisaIpcSocketFile } from "
|
|
4
|
+
import { arisaIpcSocketFile } from "../../platform/paths.js";
|
|
5
5
|
|
|
6
6
|
function writeResponse(socket, response) {
|
|
7
7
|
socket.write(`${JSON.stringify(response)}\n`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { open, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { cliLogConfig } from "../core/config/config-defaults.js";
|
|
3
|
-
import { ensureArisaHome, serviceLogFile } from "
|
|
3
|
+
import { ensureArisaHome, serviceLogFile } from "../platform/paths.js";
|
|
4
4
|
|
|
5
5
|
const readChunkSize = 64 * 1024;
|
|
6
6
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { purgeManagedDaemon } from "../core/tools/daemon-processes.js";
|
|
2
|
+
import { isDaemonProcess, listSystemProcesses, terminateProcess } from "./process-inspection.js";
|
|
3
|
+
|
|
4
|
+
export async function reapObsoleteDaemon({
|
|
5
|
+
record,
|
|
6
|
+
diagnostic,
|
|
7
|
+
reason,
|
|
8
|
+
timeoutMs,
|
|
9
|
+
stopTimeoutMs,
|
|
10
|
+
inspectProcesses = listSystemProcesses,
|
|
11
|
+
stopProcess = terminateProcess,
|
|
12
|
+
purgeDaemon = purgeManagedDaemon
|
|
13
|
+
} = {}) {
|
|
14
|
+
const pid = diagnostic?.pid;
|
|
15
|
+
if (pid) {
|
|
16
|
+
let processes;
|
|
17
|
+
try {
|
|
18
|
+
processes = await inspectProcesses({ timeoutMs });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return {
|
|
21
|
+
record,
|
|
22
|
+
outcome: "obsolete-unverified",
|
|
23
|
+
reason,
|
|
24
|
+
diagnostic,
|
|
25
|
+
error: `process inspection failed: ${error instanceof Error ? error.message : String(error)}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const processRecord = processes.find((item) => item.pid === pid);
|
|
29
|
+
if (!processRecord || !isDaemonProcess(processRecord, record)) {
|
|
30
|
+
return { record, outcome: "obsolete-unverified", reason, diagnostic };
|
|
31
|
+
}
|
|
32
|
+
await stopProcess(pid, { forceAfterMs: stopTimeoutMs });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await purgeDaemon({ toolName: record.toolName, scope: record.scope });
|
|
36
|
+
return {
|
|
37
|
+
record,
|
|
38
|
+
outcome: "obsolete-removed",
|
|
39
|
+
reason,
|
|
40
|
+
diagnostic,
|
|
41
|
+
stoppedPid: pid || null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const defaultCoreOomScoreAdjust = -900;
|
|
4
|
+
|
|
5
|
+
export async function protectCoreFromOom({
|
|
6
|
+
platform = process.platform,
|
|
7
|
+
score = defaultCoreOomScoreAdjust,
|
|
8
|
+
writeScore = (value) => writeFile("/proc/self/oom_score_adj", `${value}\n`, "utf8"),
|
|
9
|
+
logger
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (platform !== "linux") return false;
|
|
12
|
+
try {
|
|
13
|
+
await writeScore(score);
|
|
14
|
+
logger?.log("service", `core OOM priority protected at ${score}`);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
logger?.log("service", `core OOM priority could not be lowered: ${error?.code || error?.message || error}`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/runtime/paths.js
CHANGED
|
@@ -1,151 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
export const arisaPackageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
-
export const arisaHomeDir = process.env.ARISA_HOME
|
|
9
|
-
? path.resolve(process.env.ARISA_HOME)
|
|
10
|
-
: path.join(os.homedir(), ".arisa");
|
|
11
|
-
export const stateDir = path.join(arisaHomeDir, "state");
|
|
12
|
-
export const configFile = path.join(stateDir, "config.json");
|
|
13
|
-
export const piAuthFile = path.join(stateDir, "pi-auth.json");
|
|
14
|
-
export const servicePidFile = path.join(stateDir, "arisa.pid");
|
|
15
|
-
export const serviceLogFile = path.join(stateDir, "arisa.log");
|
|
16
|
-
export const restartReceiptFile = path.join(stateDir, "restart-receipt.json");
|
|
17
|
-
export const sessionStartOperationalNotesFile = path.join(stateDir, "session-start-operational-notes.json");
|
|
18
|
-
export function createIpcSocketPath({ homeDir = arisaHomeDir, platform = process.platform } = {}) {
|
|
19
|
-
if (platform === "win32") {
|
|
20
|
-
const suffix = crypto.createHash("sha256").update(homeDir).digest("hex").slice(0, 16);
|
|
21
|
-
return `\\\\.\\pipe\\arisa-${suffix}`;
|
|
22
|
-
}
|
|
23
|
-
return path.join(homeDir, "state", "arisa.sock");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const arisaIpcSocketFile = createIpcSocketPath();
|
|
27
|
-
export const tasksFile = path.join(stateDir, "tasks.json");
|
|
28
|
-
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
29
|
-
export const chatsDir = path.join(arisaHomeDir, "chats");
|
|
30
|
-
export const toolStateDir = path.join(stateDir, "tools");
|
|
31
|
-
|
|
32
|
-
export function requireToolName(toolName) {
|
|
33
|
-
const name = String(toolName ?? "");
|
|
34
|
-
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
|
35
|
-
throw new Error(`Invalid tool name: ${name || "empty"}`);
|
|
36
|
-
}
|
|
37
|
-
return name;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getChatDir(chatId) {
|
|
41
|
-
return path.join(chatsDir, String(chatId));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function getChatArtifactsDir(chatId) {
|
|
45
|
-
return path.join(getChatDir(chatId), "artifacts");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function getChatArtifactsIndexFile(chatId) {
|
|
49
|
-
return path.join(getChatDir(chatId), "state", "artifacts.json");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function getChatSessionSeedFile(chatId) {
|
|
53
|
-
return path.join(getChatDir(chatId), "state", "session-seed.jsonl");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getChatLegacyConversationHistoryFile(chatId) {
|
|
57
|
-
return path.join(getChatDir(chatId), "state", "conversation.jsonl");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function getChatToolUsageFile(chatId) {
|
|
61
|
-
return path.join(getChatDir(chatId), "state", "tool-usage.json");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getChatToolResourceNotesFile(chatId) {
|
|
65
|
-
return path.join(getChatDir(chatId), "state", "tool-resource-notes.json");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function getChatTelegramWorkspacesFile(chatId) {
|
|
69
|
-
return path.join(getChatDir(chatId), "state", "telegram-workspaces.json");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function getChatToolStateDir(chatId, toolName) {
|
|
73
|
-
return path.join(getChatDir(chatId), "state", "tools", requireToolName(toolName));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function normalizeDaemonScope(scope = { type: "global" }) {
|
|
77
|
-
if (!scope || scope === "global" || scope.type === "global") {
|
|
78
|
-
return { type: "global" };
|
|
79
|
-
}
|
|
80
|
-
if (scope === "chat") {
|
|
81
|
-
throw new Error("chat daemon scope requires chatId");
|
|
82
|
-
}
|
|
83
|
-
if (scope.type !== "chat") {
|
|
84
|
-
throw new Error(`Unsupported daemon scope: ${scope.type || scope}`);
|
|
85
|
-
}
|
|
86
|
-
const chatId = String(scope.chatId ?? "").trim();
|
|
87
|
-
if (!/^-?\d+$/.test(chatId)) {
|
|
88
|
-
throw new Error(`Invalid chat daemon scope: ${chatId || "missing chatId"}`);
|
|
89
|
-
}
|
|
90
|
-
return { type: "chat", chatId };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function getDaemonInstanceId(scope = { type: "global" }) {
|
|
94
|
-
const normalized = normalizeDaemonScope(scope);
|
|
95
|
-
return normalized.type === "global" ? "global" : `chat:${normalized.chatId}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getDaemonInstanceDir(toolName, scope = { type: "global" }) {
|
|
99
|
-
const normalized = normalizeDaemonScope(scope);
|
|
100
|
-
return normalized.type === "global"
|
|
101
|
-
? getToolStateDir(toolName)
|
|
102
|
-
: path.join(getChatToolStateDir(normalized.chatId, toolName), "daemon");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function getChatPiSessionsDir(chatId, sessionRevision = 0) {
|
|
106
|
-
if (!Number.isSafeInteger(sessionRevision) || sessionRevision < 0) {
|
|
107
|
-
throw new Error(`Invalid Pi session revision: ${sessionRevision}`);
|
|
108
|
-
}
|
|
109
|
-
const sessionsDir = path.join(getChatDir(chatId), "state", "pi-sessions");
|
|
110
|
-
return sessionRevision === 0
|
|
111
|
-
? sessionsDir
|
|
112
|
-
: path.join(sessionsDir, String(sessionRevision));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function getToolDir(toolName) {
|
|
116
|
-
return path.join(toolsDir, requireToolName(toolName));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getToolConfigPath(toolName) {
|
|
120
|
-
return path.join(getToolDir(toolName), "config.js");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function getChatConfigDir(chatId) {
|
|
124
|
-
return path.join(getChatDir(chatId), "config");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function getChatTmpDir(chatId) {
|
|
128
|
-
return path.join(getChatDir(chatId), "tmp");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function getChatToolConfigPath(chatId, toolName) {
|
|
132
|
-
return path.join(getChatConfigDir(chatId), "tools", requireToolName(toolName), "config.js");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function getToolStateDir(toolName) {
|
|
136
|
-
return path.join(toolStateDir, requireToolName(toolName));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function getToolTmpDir(toolName) {
|
|
140
|
-
return path.join(getToolStateDir(toolName), "tmp");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function getChatToolTmpDir(chatId, toolName) {
|
|
144
|
-
return path.join(getChatTmpDir(chatId), "tools", toolName);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function ensureArisaHome() {
|
|
148
|
-
await mkdir(stateDir, { recursive: true });
|
|
149
|
-
await mkdir(toolsDir, { recursive: true });
|
|
150
|
-
await mkdir(chatsDir, { recursive: true });
|
|
151
|
-
}
|
|
1
|
+
// Public compatibility facade for tools and integrations.
|
|
2
|
+
// Core and runtime internals import the neutral platform implementation directly.
|
|
3
|
+
export * from "../platform/paths.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
function parsePosixProcesses(output) {
|
|
8
|
+
return String(output || "")
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((line) => /^\s*(\d+)\s+(.+)$/.exec(line))
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((match) => ({ pid: Number(match[1]), command: match[2] }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseWindowsProcesses(output) {
|
|
16
|
+
const parsed = JSON.parse(output || "[]");
|
|
17
|
+
const records = Array.isArray(parsed) ? parsed : [parsed];
|
|
18
|
+
return records
|
|
19
|
+
.filter((record) => record?.ProcessId && record?.CommandLine)
|
|
20
|
+
.map((record) => ({ pid: Number(record.ProcessId), command: String(record.CommandLine) }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function listSystemProcesses({ timeoutMs, platform = process.platform } = {}) {
|
|
24
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
25
|
+
throw new Error("Process inspection requires a positive timeoutMs");
|
|
26
|
+
}
|
|
27
|
+
if (platform === "win32") {
|
|
28
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
29
|
+
"-NoProfile",
|
|
30
|
+
"-Command",
|
|
31
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress"
|
|
32
|
+
], { timeout: timeoutMs, windowsHide: true });
|
|
33
|
+
return parseWindowsProcesses(stdout);
|
|
34
|
+
}
|
|
35
|
+
const { stdout } = await execFileAsync("ps", ["-ww", "-axo", "pid=,command="], { timeout: timeoutMs });
|
|
36
|
+
return parsePosixProcesses(stdout);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isArisaServiceProcess(record, entryFile) {
|
|
40
|
+
return record.command.includes(entryFile)
|
|
41
|
+
&& /(?:^|\s)--service-runner(?:\s|$)/.test(record.command);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isDaemonProcess(record, daemon) {
|
|
45
|
+
return record.command.includes(daemon.entryPath)
|
|
46
|
+
&& /(?:^|\s)daemon(?:\s|$)/.test(record.command);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sleep(ms) {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isAlive(pid) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function terminateProcess(pid, { forceAfterMs } = {}) {
|
|
63
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
|
|
64
|
+
throw new Error(`Refusing to terminate invalid process target: ${pid}`);
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isFinite(forceAfterMs) || forceAfterMs <= 0) {
|
|
67
|
+
throw new Error("Process cleanup requires a positive forceAfterMs");
|
|
68
|
+
}
|
|
69
|
+
if (!isAlive(pid)) return false;
|
|
70
|
+
process.kill(pid, "SIGTERM");
|
|
71
|
+
const startedAt = Date.now();
|
|
72
|
+
while (Date.now() - startedAt < forceAfterMs) {
|
|
73
|
+
if (!isAlive(pid)) return true;
|
|
74
|
+
await sleep(Math.min(100, forceAfterMs));
|
|
75
|
+
}
|
|
76
|
+
if (isAlive(pid)) process.kill(pid, "SIGKILL");
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
@@ -3,7 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { arisaPackageDir, restartReceiptFile } from "
|
|
6
|
+
import { arisaPackageDir, restartReceiptFile } from "../platform/paths.js";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
@@ -2,7 +2,7 @@ import { open, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { ensureArisaHome, serviceLogFile, servicePidFile } from "
|
|
5
|
+
import { ensureArisaHome, serviceLogFile, servicePidFile } from "../platform/paths.js";
|
|
6
6
|
|
|
7
7
|
export const serviceEntryFile = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
8
8
|
|
|
@@ -22,6 +22,7 @@ export function createServiceSupervisor({
|
|
|
22
22
|
restartBackoffMaxMs = 60_000,
|
|
23
23
|
stableRuntimeMs = 60_000,
|
|
24
24
|
logger,
|
|
25
|
+
onUnexpectedExit = null,
|
|
25
26
|
spawnProcess = spawn,
|
|
26
27
|
wait = sleep
|
|
27
28
|
}) {
|
|
@@ -65,6 +66,19 @@ export function createServiceSupervisor({
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const delay = Math.min(maximumBackoffMs, initialBackoffMs * (2 ** (consecutiveFailures - 1)));
|
|
69
|
+
try {
|
|
70
|
+
await onUnexpectedExit?.({
|
|
71
|
+
occurredAt: new Date().toISOString(),
|
|
72
|
+
runtimeMs,
|
|
73
|
+
restartDelayMs: delay,
|
|
74
|
+
consecutiveFailures,
|
|
75
|
+
code: outcome.code ?? null,
|
|
76
|
+
signal: outcome.signal || null,
|
|
77
|
+
detail
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger?.error("service", `worker recovery report failed: ${errorMessage(error)}`);
|
|
81
|
+
}
|
|
68
82
|
logger?.log("service", `restarting worker in ${delay}ms`);
|
|
69
83
|
await Promise.race([
|
|
70
84
|
wait(delay),
|
package/src/runtime/slave-cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { applyConfigDefaults } from "../core/config/config-defaults.js";
|
|
5
5
|
import { installLockedOfficialTool } from "../core/tools/official-tool-installer.js";
|
|
6
6
|
import { createHeadlessApp } from "./create-headless-app.js";
|
|
7
|
-
import { arisaPackageDir } from "
|
|
7
|
+
import { arisaPackageDir } from "../platform/paths.js";
|
|
8
8
|
import { readPackageVersion, readRecentLogLines, followLogFile } from "./log-viewer.js";
|
|
9
9
|
import { parseSlaveBootstrapUrl } from "./slave-bootstrap-url.js";
|
|
10
10
|
import { withSecureRequestFile } from "./secure-request-file.js";
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
writeDaemonStatus
|
|
9
9
|
} from "../core/tools/daemon-processes.js";
|
|
10
10
|
import { loadDaemonPolicy } from "../core/tools/daemon-policy.js";
|
|
11
|
-
import { ensureArisaHome } from "
|
|
11
|
+
import { ensureArisaHome } from "../platform/paths.js";
|
|
12
|
+
import { reapObsoleteDaemon } from "./obsolete-daemon-reaper.js";
|
|
12
13
|
|
|
13
14
|
async function fileExists(file) {
|
|
14
15
|
try {
|
|
@@ -58,7 +59,12 @@ function validateRegistration(record, toolRegistry) {
|
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export function createToolProcessSupervisor({
|
|
62
|
+
export function createToolProcessSupervisor({
|
|
63
|
+
logger,
|
|
64
|
+
policy,
|
|
65
|
+
toolRegistry,
|
|
66
|
+
obsoleteDaemonReaper = reapObsoleteDaemon
|
|
67
|
+
} = {}) {
|
|
62
68
|
let running = false;
|
|
63
69
|
let timer = null;
|
|
64
70
|
let reconciliation = null;
|
|
@@ -75,13 +81,34 @@ export function createToolProcessSupervisor({ logger, policy, toolRegistry } = {
|
|
|
75
81
|
for (const registeredRecord of await listRegisteredDaemons()) {
|
|
76
82
|
const validation = validateRegistration(registeredRecord, toolRegistry);
|
|
77
83
|
if (!validation.valid) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const diagnostic = await readDaemonDiagnostic(registeredRecord);
|
|
85
|
+
try {
|
|
86
|
+
const result = await obsoleteDaemonReaper({
|
|
87
|
+
record: registeredRecord,
|
|
88
|
+
diagnostic,
|
|
89
|
+
reason: validation.reason,
|
|
90
|
+
timeoutMs: daemonPolicy.healthTimeoutMs,
|
|
91
|
+
stopTimeoutMs: daemonPolicy.stopTimeoutMs
|
|
92
|
+
});
|
|
93
|
+
results.push(result);
|
|
94
|
+
const key = `${registeredRecord.toolName}:${registeredRecord.instanceId || "global"}`;
|
|
95
|
+
const message = result.outcome === "obsolete-removed"
|
|
96
|
+
? `removed obsolete daemon ${registeredRecord.toolName} (${registeredRecord.instanceId || "global"}): ${validation.reason}`
|
|
97
|
+
: `left obsolete daemon ${registeredRecord.toolName} (${registeredRecord.instanceId || "global"}) untouched because process identity could not be verified`;
|
|
98
|
+
if (reportedDiagnostics.get(key) !== message) {
|
|
99
|
+
reportedDiagnostics.set(key, message);
|
|
100
|
+
logger?.log("tools", message);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger?.error?.("tools", `obsolete daemon cleanup failed for ${registeredRecord.toolName}: ${error?.message || error}`);
|
|
104
|
+
results.push({
|
|
105
|
+
record: registeredRecord,
|
|
106
|
+
outcome: "obsolete-cleanup-error",
|
|
107
|
+
reason: validation.reason,
|
|
108
|
+
diagnostic,
|
|
109
|
+
error: error?.message || String(error)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
85
112
|
continue;
|
|
86
113
|
}
|
|
87
114
|
const record = validation.record;
|