arisa 3.1.6 → 3.1.8
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/package.json +1 -1
- package/src/core/tools/daemon-processes.js +16 -17
- package/src/core/tools/daemon-runtime.js +1 -42
- package/src/core/tools/tool-registry.js +2 -3
- package/src/runtime/create-app.js +1 -1
- package/src/runtime/paths.js +0 -1
- package/src/runtime/tool-process-supervisor.js +12 -59
package/package.json
CHANGED
|
@@ -4,9 +4,6 @@ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { getToolStateDir, toolStateDir } from "../../runtime/paths.js";
|
|
6
6
|
|
|
7
|
-
export const OWNER_HEARTBEAT_INTERVAL_MS = 2000;
|
|
8
|
-
export const OWNER_HEARTBEAT_TTL_MS = 10000;
|
|
9
|
-
|
|
10
7
|
export function daemonPaths(toolName) {
|
|
11
8
|
const root = getToolStateDir(toolName);
|
|
12
9
|
return {
|
|
@@ -54,9 +51,11 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
54
51
|
export async function stopManagedDaemon(toolName, { signal = "SIGTERM", forceAfterMs = 3000 } = {}) {
|
|
55
52
|
const paths = daemonPaths(toolName);
|
|
56
53
|
const { pid } = await readJson(paths.pidFile, {});
|
|
54
|
+
let stopped = false;
|
|
57
55
|
if (isProcessAlive(pid)) {
|
|
58
56
|
try {
|
|
59
57
|
process.kill(pid, signal);
|
|
58
|
+
stopped = true;
|
|
60
59
|
} catch {}
|
|
61
60
|
if (signal !== "SIGKILL" && forceAfterMs > 0 && !(await waitForExit(pid, forceAfterMs))) {
|
|
62
61
|
try {
|
|
@@ -65,19 +64,24 @@ export async function stopManagedDaemon(toolName, { signal = "SIGTERM", forceAft
|
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
await rm(paths.pidFile, { force: true });
|
|
67
|
+
return { toolName, pid: pid || null, stopped };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export async function startManagedDaemon({ toolName, entryPath, beforeStart = null,
|
|
70
|
+
export async function startManagedDaemon({ toolName, entryPath, beforeStart = null, autoStart = true }) {
|
|
71
71
|
const paths = daemonPaths(toolName);
|
|
72
72
|
await mkdir(paths.commandsDir, { recursive: true });
|
|
73
73
|
|
|
74
74
|
const current = await readJson(paths.pidFile, {});
|
|
75
75
|
if (isProcessAlive(current.pid)) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
await writeJson(paths.metaFile, {
|
|
77
|
+
toolName,
|
|
78
|
+
entryPath,
|
|
79
|
+
autoStart,
|
|
80
|
+
lastStartedAt: current.startedAt || new Date().toISOString()
|
|
81
|
+
});
|
|
82
|
+
return current.pid;
|
|
80
83
|
}
|
|
84
|
+
await rm(paths.pidFile, { force: true });
|
|
81
85
|
|
|
82
86
|
await rm(paths.commandsDir, { recursive: true, force: true });
|
|
83
87
|
await mkdir(paths.commandsDir, { recursive: true });
|
|
@@ -88,26 +92,21 @@ export async function startManagedDaemon({ toolName, entryPath, beforeStart = nu
|
|
|
88
92
|
const child = spawn(process.execPath, [entryPath, "daemon"], {
|
|
89
93
|
detached: false,
|
|
90
94
|
stdio: ["ignore", out, out],
|
|
91
|
-
env:
|
|
95
|
+
env: process.env
|
|
92
96
|
});
|
|
93
97
|
child.unref();
|
|
94
98
|
|
|
95
99
|
const startedAt = new Date().toISOString();
|
|
96
100
|
const record = {
|
|
97
101
|
pid: child.pid,
|
|
98
|
-
startedAt
|
|
99
|
-
ownerPid: Number.parseInt(ownerEnv.ARISA_OWNER_PID || "", 10) || null,
|
|
100
|
-
ownerToken: ownerEnv.ARISA_TOOL_OWNER_TOKEN || ""
|
|
102
|
+
startedAt
|
|
101
103
|
};
|
|
102
104
|
await writeJson(paths.pidFile, record);
|
|
103
105
|
await writeJson(paths.metaFile, {
|
|
104
106
|
toolName,
|
|
105
107
|
entryPath,
|
|
106
|
-
autoStart
|
|
107
|
-
lastStartedAt: startedAt
|
|
108
|
-
ownerFile: ownerEnv.ARISA_TOOL_OWNER_FILE || "",
|
|
109
|
-
ownerPid: record.ownerPid,
|
|
110
|
-
ownerToken: record.ownerToken
|
|
108
|
+
autoStart,
|
|
109
|
+
lastStartedAt: startedAt
|
|
111
110
|
});
|
|
112
111
|
return child.pid;
|
|
113
112
|
} finally {
|
|
@@ -2,8 +2,6 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { mkdir, readdir, rename, unlink } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
OWNER_HEARTBEAT_INTERVAL_MS,
|
|
6
|
-
OWNER_HEARTBEAT_TTL_MS,
|
|
7
5
|
daemonPaths,
|
|
8
6
|
isProcessAlive,
|
|
9
7
|
readJson,
|
|
@@ -40,12 +38,7 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
|
|
|
40
38
|
return startManagedDaemon({
|
|
41
39
|
toolName,
|
|
42
40
|
entryPath,
|
|
43
|
-
beforeStart
|
|
44
|
-
ownerEnv: {
|
|
45
|
-
ARISA_OWNER_PID: process.env.ARISA_OWNER_PID,
|
|
46
|
-
ARISA_TOOL_OWNER_FILE: process.env.ARISA_TOOL_OWNER_FILE,
|
|
47
|
-
ARISA_TOOL_OWNER_TOKEN: process.env.ARISA_TOOL_OWNER_TOKEN
|
|
48
|
-
}
|
|
41
|
+
beforeStart
|
|
49
42
|
});
|
|
50
43
|
}
|
|
51
44
|
|
|
@@ -53,39 +46,6 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
|
|
|
53
46
|
await stopManagedDaemon(toolName);
|
|
54
47
|
}
|
|
55
48
|
|
|
56
|
-
function installOwnerWatch() {
|
|
57
|
-
const ownerFile = process.env.ARISA_TOOL_OWNER_FILE;
|
|
58
|
-
const ownerToken = process.env.ARISA_TOOL_OWNER_TOKEN;
|
|
59
|
-
if (!ownerFile || !ownerToken) return;
|
|
60
|
-
|
|
61
|
-
let exiting = false;
|
|
62
|
-
async function exitIfOrphaned(message) {
|
|
63
|
-
if (exiting) return;
|
|
64
|
-
exiting = true;
|
|
65
|
-
await writeStatus({ state: "stopped", message });
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const timer = setInterval(async () => {
|
|
70
|
-
const owner = await readJson(ownerFile, null);
|
|
71
|
-
if (!owner || owner.token !== ownerToken) {
|
|
72
|
-
await exitIfOrphaned("Arisa owner stopped");
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const heartbeatAt = Date.parse(owner.heartbeatAt || "");
|
|
77
|
-
if (!Number.isFinite(heartbeatAt) || Date.now() - heartbeatAt > OWNER_HEARTBEAT_TTL_MS) {
|
|
78
|
-
await exitIfOrphaned("Arisa owner heartbeat expired");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!isProcessAlive(owner.pid)) {
|
|
83
|
-
await exitIfOrphaned("Arisa owner process exited");
|
|
84
|
-
}
|
|
85
|
-
}, OWNER_HEARTBEAT_INTERVAL_MS);
|
|
86
|
-
timer.unref();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
49
|
async function waitReady({ timeoutMs = 120000, readyStates = ["ready"] } = {}) {
|
|
90
50
|
const startTime = Date.now();
|
|
91
51
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -143,7 +103,6 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
|
|
|
143
103
|
}
|
|
144
104
|
|
|
145
105
|
async function workLoop({ processJob, idleTimeoutMs = 0, intervalMs = 250 }) {
|
|
146
|
-
installOwnerWatch();
|
|
147
106
|
let lastActivity = Date.now();
|
|
148
107
|
setInterval(async () => {
|
|
149
108
|
try {
|
|
@@ -25,9 +25,8 @@ function runProcess(command, args, options = {}) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export class ToolRegistry {
|
|
28
|
-
constructor({ logger
|
|
28
|
+
constructor({ logger } = {}) {
|
|
29
29
|
this.logger = logger;
|
|
30
|
-
this.processOwnerEnv = processOwnerEnv;
|
|
31
30
|
this.tools = new Map();
|
|
32
31
|
this.skillRegistry = new SkillRegistry();
|
|
33
32
|
}
|
|
@@ -154,7 +153,7 @@ export class ToolRegistry {
|
|
|
154
153
|
await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
|
|
155
154
|
const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
|
|
156
155
|
cwd: tool.dir,
|
|
157
|
-
env:
|
|
156
|
+
env: process.env
|
|
158
157
|
});
|
|
159
158
|
await unlink(requestFile).catch(() => {});
|
|
160
159
|
await rmdir(tmpDir).catch(() => {});
|
|
@@ -53,7 +53,7 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
53
53
|
|
|
54
54
|
const artifactStore = new ArtifactStore();
|
|
55
55
|
const toolProcessSupervisor = createToolProcessSupervisor({ logger });
|
|
56
|
-
const toolRegistry = new ToolRegistry({ logger
|
|
56
|
+
const toolRegistry = new ToolRegistry({ logger });
|
|
57
57
|
const taskStore = new TaskStore();
|
|
58
58
|
await toolRegistry.load();
|
|
59
59
|
logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
|
package/src/runtime/paths.js
CHANGED
|
@@ -7,7 +7,6 @@ export const stateDir = path.join(arisaHomeDir, "state");
|
|
|
7
7
|
export const configFile = path.join(stateDir, "config.json");
|
|
8
8
|
export const servicePidFile = path.join(stateDir, "arisa.pid");
|
|
9
9
|
export const serviceLogFile = path.join(stateDir, "arisa.log");
|
|
10
|
-
export const toolDaemonOwnerFile = path.join(stateDir, "tool-daemon-owner.json");
|
|
11
10
|
export const tasksFile = path.join(stateDir, "tasks.json");
|
|
12
11
|
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
13
12
|
export const chatsDir = path.join(arisaHomeDir, "chats");
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import { access, rm } from "node:fs/promises";
|
|
3
2
|
import {
|
|
4
3
|
daemonPaths,
|
|
4
|
+
isProcessAlive,
|
|
5
5
|
listRegisteredDaemons,
|
|
6
6
|
readJson,
|
|
7
|
-
startManagedDaemon
|
|
8
|
-
stopManagedDaemon,
|
|
9
|
-
writeJson
|
|
7
|
+
startManagedDaemon
|
|
10
8
|
} from "../core/tools/daemon-processes.js";
|
|
11
|
-
import { ensureArisaHome
|
|
9
|
+
import { ensureArisaHome } from "./paths.js";
|
|
12
10
|
|
|
13
11
|
async function fileExists(file) {
|
|
14
12
|
try {
|
|
@@ -20,36 +18,10 @@ async function fileExists(file) {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export function createToolProcessSupervisor({ logger } = {}) {
|
|
23
|
-
const token = crypto.randomUUID();
|
|
24
|
-
let heartbeatTimer = null;
|
|
25
21
|
let running = false;
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
ARISA_OWNER_PID: String(process.pid),
|
|
29
|
-
ARISA_TOOL_OWNER_FILE: toolDaemonOwnerFile,
|
|
30
|
-
ARISA_TOOL_OWNER_TOKEN: token
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
async function writeHeartbeat() {
|
|
34
|
-
await writeJson(toolDaemonOwnerFile, {
|
|
35
|
-
pid: process.pid,
|
|
36
|
-
token,
|
|
37
|
-
heartbeatAt: new Date().toISOString()
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function startHeartbeat() {
|
|
23
|
+
async function reconcileDaemons() {
|
|
42
24
|
await ensureArisaHome();
|
|
43
|
-
await writeHeartbeat();
|
|
44
|
-
heartbeatTimer = setInterval(() => {
|
|
45
|
-
writeHeartbeat().catch((error) => {
|
|
46
|
-
logger?.error("tools", `tool daemon heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
-
});
|
|
48
|
-
}, 2000);
|
|
49
|
-
heartbeatTimer.unref();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function restartRegisteredDaemons() {
|
|
53
25
|
for (const record of await listRegisteredDaemons()) {
|
|
54
26
|
if (!record.autoStart) continue;
|
|
55
27
|
if (!(await fileExists(record.entryPath))) {
|
|
@@ -59,52 +31,33 @@ export function createToolProcessSupervisor({ logger } = {}) {
|
|
|
59
31
|
|
|
60
32
|
const paths = daemonPaths(record.toolName);
|
|
61
33
|
const { pid } = await readJson(paths.pidFile, {});
|
|
34
|
+
if (isProcessAlive(pid)) {
|
|
35
|
+
logger?.log("tools", `adopted managed daemon ${record.toolName} (pid ${pid})`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
62
38
|
if (pid) {
|
|
63
|
-
await
|
|
39
|
+
await rm(paths.pidFile, { force: true });
|
|
40
|
+
logger?.log("tools", `removed stale daemon pid for ${record.toolName} (${pid})`);
|
|
64
41
|
}
|
|
65
42
|
|
|
66
43
|
logger?.log("tools", `starting managed daemon ${record.toolName}`);
|
|
67
44
|
await startManagedDaemon({
|
|
68
45
|
toolName: record.toolName,
|
|
69
|
-
entryPath: record.entryPath
|
|
70
|
-
ownerEnv
|
|
46
|
+
entryPath: record.entryPath
|
|
71
47
|
});
|
|
72
48
|
}
|
|
73
49
|
}
|
|
74
50
|
|
|
75
|
-
async function stopRegisteredDaemons() {
|
|
76
|
-
for (const record of await listRegisteredDaemons()) {
|
|
77
|
-
logger?.log("tools", `stopping managed daemon ${record.toolName}`);
|
|
78
|
-
await stopManagedDaemon(record.toolName);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function removeOwnerFile() {
|
|
83
|
-
const current = await readJson(toolDaemonOwnerFile, null);
|
|
84
|
-
if (current?.token === token) {
|
|
85
|
-
await rm(toolDaemonOwnerFile, { force: true });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
51
|
return {
|
|
90
|
-
env() {
|
|
91
|
-
return ownerEnv;
|
|
92
|
-
},
|
|
93
|
-
|
|
94
52
|
async start() {
|
|
95
53
|
if (running) return;
|
|
96
54
|
running = true;
|
|
97
|
-
await
|
|
98
|
-
await restartRegisteredDaemons();
|
|
55
|
+
await reconcileDaemons();
|
|
99
56
|
},
|
|
100
57
|
|
|
101
58
|
async stop() {
|
|
102
59
|
if (!running) return;
|
|
103
60
|
running = false;
|
|
104
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
105
|
-
heartbeatTimer = null;
|
|
106
|
-
await stopRegisteredDaemons();
|
|
107
|
-
await removeOwnerFile();
|
|
108
61
|
}
|
|
109
62
|
};
|
|
110
63
|
}
|