arisa 3.1.2 → 3.1.6
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/AGENTS.md +59 -14
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +105 -45
- package/src/core/agent/prompt-timeout.js +22 -0
- package/src/core/agent/runtime-context.js +2 -1
- package/src/core/artifacts/normalize-for-reasoning.js +5 -4
- package/src/core/skills/skill-registry.js +71 -0
- package/src/core/tasks/task-store.js +1 -1
- package/src/core/tools/daemon-processes.js +134 -0
- package/src/core/tools/daemon-runtime.js +55 -51
- package/src/core/tools/tool-registry.js +41 -6
- package/src/index.js +42 -6
- package/src/runtime/bootstrap.js +1 -1
- package/src/runtime/create-app.js +15 -2
- package/src/runtime/paths.js +18 -8
- package/src/runtime/service-manager.js +4 -14
- package/src/runtime/tool-process-supervisor.js +110 -0
- package/src/transport/telegram/bot.js +173 -44
- package/src/transport/telegram/media.js +20 -0
- package/tools/openai-transcribe/index.js +1 -1
- package/tools/openai-transcribe/tool.manifest.json +2 -2
- package/tools/openai-tts/index.js +4 -2
- package/docs/async-event-queue-flow.md +0 -68
- package/src/core/agent/project-instructions.js +0 -11
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { openSync } from "node:fs";
|
|
4
|
-
import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readdir, rename, unlink } from "node:fs/promises";
|
|
5
3
|
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function readJson(file, fallback = {}) {
|
|
20
|
-
try { return JSON.parse(await readFile(file, "utf8")); } catch { return fallback; }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function writeJson(file, value) {
|
|
24
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
25
|
-
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isProcessAlive(pid) {
|
|
29
|
-
if (!pid) return false;
|
|
30
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
31
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
OWNER_HEARTBEAT_INTERVAL_MS,
|
|
6
|
+
OWNER_HEARTBEAT_TTL_MS,
|
|
7
|
+
daemonPaths,
|
|
8
|
+
isProcessAlive,
|
|
9
|
+
readJson,
|
|
10
|
+
startManagedDaemon,
|
|
11
|
+
stopManagedDaemon,
|
|
12
|
+
writeJson
|
|
13
|
+
} from "./daemon-processes.js";
|
|
32
14
|
|
|
33
15
|
export function createDaemonRuntime({ toolName, entryPath, beforeStart = null }) {
|
|
34
16
|
const paths = daemonPaths(toolName);
|
|
@@ -54,33 +36,54 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
|
|
|
54
36
|
await writeJson(paths.statusFile, { ...current, ...patch, updatedAt: new Date().toISOString() });
|
|
55
37
|
}
|
|
56
38
|
|
|
57
|
-
async function clearJobs() {
|
|
58
|
-
await rm(paths.commandsDir, { recursive: true, force: true });
|
|
59
|
-
await mkdir(paths.commandsDir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
39
|
async function start() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
stdio: ["ignore", out, out],
|
|
73
|
-
env: process.env
|
|
40
|
+
return startManagedDaemon({
|
|
41
|
+
toolName,
|
|
42
|
+
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
|
+
}
|
|
74
49
|
});
|
|
75
|
-
child.unref();
|
|
76
|
-
await writeJson(paths.pidFile, { pid: child.pid, startedAt: new Date().toISOString() });
|
|
77
|
-
return child.pid;
|
|
78
50
|
}
|
|
79
51
|
|
|
80
52
|
async function stop() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
53
|
+
await stopManagedDaemon(toolName);
|
|
54
|
+
}
|
|
55
|
+
|
|
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();
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
async function waitReady({ timeoutMs = 120000, readyStates = ["ready"] } = {}) {
|
|
@@ -140,6 +143,7 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
async function workLoop({ processJob, idleTimeoutMs = 0, intervalMs = 250 }) {
|
|
146
|
+
installOwnerWatch();
|
|
143
147
|
let lastActivity = Date.now();
|
|
144
148
|
setInterval(async () => {
|
|
145
149
|
try {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getToolConfigPath, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
6
6
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
7
7
|
import { normalizeToolResult } from "./tool-result.js";
|
|
8
|
+
import { SkillRegistry } from "../skills/skill-registry.js";
|
|
8
9
|
|
|
9
10
|
const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
|
|
10
11
|
const toolRoots = [
|
|
@@ -24,9 +25,11 @@ function runProcess(command, args, options = {}) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export class ToolRegistry {
|
|
27
|
-
constructor({ logger } = {}) {
|
|
28
|
+
constructor({ logger, processOwnerEnv = {} } = {}) {
|
|
28
29
|
this.logger = logger;
|
|
30
|
+
this.processOwnerEnv = processOwnerEnv;
|
|
29
31
|
this.tools = new Map();
|
|
32
|
+
this.skillRegistry = new SkillRegistry();
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async load() {
|
|
@@ -52,8 +55,10 @@ export class ToolRegistry {
|
|
|
52
55
|
const configSource = await readFile(configPath, "utf8");
|
|
53
56
|
const defaults = parseConfigModule(configSource);
|
|
54
57
|
const config = await loadToolConfig(manifest.name, defaults);
|
|
58
|
+
const skillHints = this.skillRegistry.normalizeHints(manifest);
|
|
55
59
|
this.tools.set(manifest.name, {
|
|
56
60
|
...manifest,
|
|
61
|
+
skillHints,
|
|
57
62
|
dir: toolDir,
|
|
58
63
|
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
59
64
|
localConfigPath: configPath,
|
|
@@ -77,7 +82,8 @@ export class ToolRegistry {
|
|
|
77
82
|
description: tool.description,
|
|
78
83
|
input: tool.input,
|
|
79
84
|
output: tool.output,
|
|
80
|
-
configSchema: tool.configSchema || {}
|
|
85
|
+
configSchema: tool.configSchema || {},
|
|
86
|
+
skillHints: tool.skillHints || []
|
|
81
87
|
}));
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -89,7 +95,29 @@ export class ToolRegistry {
|
|
|
89
95
|
const tool = this.get(name);
|
|
90
96
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
91
97
|
const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: process.env });
|
|
92
|
-
|
|
98
|
+
const help = result.stdout || result.stderr;
|
|
99
|
+
const skills = await this.resolveSkills(name);
|
|
100
|
+
if (!skills.length) return help;
|
|
101
|
+
const skillHelp = skills.map((item) => [
|
|
102
|
+
`- ${item.name}${item.when ? ` (${item.when})` : ""}`,
|
|
103
|
+
item.description ? ` ${item.description}` : null,
|
|
104
|
+
item.found ? ` path: ${item.path}` : " warning: skill not found"
|
|
105
|
+
].filter(Boolean).join("\n")).join("\n");
|
|
106
|
+
return `${help}\n\nAssigned skills:\n${skillHelp}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async resolveSkills(name) {
|
|
110
|
+
const tool = this.get(name);
|
|
111
|
+
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
112
|
+
const hints = await this.skillRegistry.resolveHints(tool.skillHints || []);
|
|
113
|
+
return hints.map((hint) => ({
|
|
114
|
+
name: hint.name,
|
|
115
|
+
when: hint.when,
|
|
116
|
+
found: hint.found,
|
|
117
|
+
description: hint.skill?.description || "",
|
|
118
|
+
path: hint.skill?.path || "",
|
|
119
|
+
content: hint.skill?.content || ""
|
|
120
|
+
}));
|
|
93
121
|
}
|
|
94
122
|
|
|
95
123
|
async resolveConfigForChat(name, chatId) {
|
|
@@ -121,12 +149,19 @@ export class ToolRegistry {
|
|
|
121
149
|
const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
|
|
122
150
|
await mkdir(tmpDir, { recursive: true });
|
|
123
151
|
const requestFile = path.join(tmpDir, `.request-${Date.now()}.json`);
|
|
124
|
-
await
|
|
152
|
+
const skills = await this.resolveSkills(name);
|
|
153
|
+
const enrichedRequest = { ...request, chatId, skills };
|
|
154
|
+
await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
|
|
125
155
|
const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
|
|
126
156
|
cwd: tool.dir,
|
|
127
|
-
env: process.env
|
|
157
|
+
env: { ...process.env, ...this.processOwnerEnv }
|
|
128
158
|
});
|
|
129
159
|
await unlink(requestFile).catch(() => {});
|
|
160
|
+
await rmdir(tmpDir).catch(() => {});
|
|
161
|
+
if (chatId != null) {
|
|
162
|
+
await rmdir(path.dirname(tmpDir)).catch(() => {});
|
|
163
|
+
await rmdir(path.dirname(path.dirname(tmpDir))).catch(() => {});
|
|
164
|
+
}
|
|
130
165
|
try {
|
|
131
166
|
const parsed = JSON.parse(result.stdout || result.stderr);
|
|
132
167
|
const normalized = normalizeToolResult(name, parsed);
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
|
|
5
5
|
import { createApp } from "./runtime/create-app.js";
|
|
6
6
|
import { createLogger } from "./runtime/logger.js";
|
|
7
|
-
import { getServiceStatus, registerServiceProcess, startService, stopService } from "./runtime/service-manager.js";
|
|
7
|
+
import { getServiceStatus, registerServiceProcess, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
|
|
8
8
|
import { flushArisaHome } from "./runtime/flush.js";
|
|
9
9
|
import { installPiPackage, removePiPackage } from "./runtime/pi-package-manager.js";
|
|
10
10
|
|
|
@@ -17,6 +17,8 @@ const serviceRunner = Boolean(cli.flags["service-runner"]);
|
|
|
17
17
|
const bootstrapOverrides = toBootstrapOverrides(cli.nestedFlags);
|
|
18
18
|
const runtimeOverrides = toRuntimeOverrides(cli.nestedFlags);
|
|
19
19
|
const logger = createLogger({ verbose });
|
|
20
|
+
let activeApp = null;
|
|
21
|
+
let shuttingDown = false;
|
|
20
22
|
|
|
21
23
|
const httpPort = Number(process.env.PORT);
|
|
22
24
|
let httpRequestHandler = null;
|
|
@@ -97,6 +99,35 @@ const bootstrapHttpOptions = httpPort ? { httpPort, setHttpRequestHandler } : {}
|
|
|
97
99
|
const webhookUrl = bootstrapOverrides.webhook?.url || "";
|
|
98
100
|
const appHttpOptions = httpPort ? { webhookUrl, setHttpRequestHandler } : {};
|
|
99
101
|
|
|
102
|
+
async function shutdown(exitCode = 0) {
|
|
103
|
+
if (shuttingDown) return;
|
|
104
|
+
shuttingDown = true;
|
|
105
|
+
try {
|
|
106
|
+
await activeApp?.stop?.();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.error("app", `shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
exitCode = exitCode || 1;
|
|
110
|
+
}
|
|
111
|
+
if (serviceRunner) {
|
|
112
|
+
await unregisterServiceProcess();
|
|
113
|
+
}
|
|
114
|
+
process.exit(exitCode);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
process.once("SIGTERM", () => {
|
|
118
|
+
shutdown(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
process.once("SIGINT", () => {
|
|
122
|
+
shutdown(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
async function startRuntimeApp() {
|
|
126
|
+
const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
|
|
127
|
+
activeApp = app;
|
|
128
|
+
await app.start();
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
async function runForeground() {
|
|
101
132
|
const hasRuntimePiOverrides = Boolean(
|
|
102
133
|
runtimeOverrides?.pi?.model
|
|
@@ -106,11 +137,12 @@ async function runForeground() {
|
|
|
106
137
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
107
138
|
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
108
139
|
try {
|
|
109
|
-
|
|
110
|
-
await app.start();
|
|
140
|
+
await startRuntimeApp();
|
|
111
141
|
} catch (error) {
|
|
112
142
|
const message = error instanceof Error ? error.message : String(error);
|
|
113
143
|
if (message.includes("No auth found")) {
|
|
144
|
+
await activeApp?.stop?.();
|
|
145
|
+
activeApp = null;
|
|
114
146
|
console.log(`\n${message}\n`);
|
|
115
147
|
if (hasRuntimePiOverrides) {
|
|
116
148
|
console.log("Skipping automatic bootstrap because Pi runtime overrides were provided.");
|
|
@@ -119,8 +151,7 @@ async function runForeground() {
|
|
|
119
151
|
}
|
|
120
152
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
121
153
|
await bootstrapIfNeeded({ force: true, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
122
|
-
|
|
123
|
-
await app.start();
|
|
154
|
+
await startRuntimeApp();
|
|
124
155
|
return;
|
|
125
156
|
}
|
|
126
157
|
throw error;
|
|
@@ -202,4 +233,9 @@ async function main() {
|
|
|
202
233
|
await runForeground();
|
|
203
234
|
}
|
|
204
235
|
|
|
205
|
-
|
|
236
|
+
try {
|
|
237
|
+
await main();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error("app", error instanceof Error ? error.message : String(error));
|
|
240
|
+
await shutdown(1);
|
|
241
|
+
}
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ToolRegistry } from "../core/tools/tool-registry.js";
|
|
|
4
4
|
import { TaskStore } from "../core/tasks/task-store.js";
|
|
5
5
|
import { AgentManager } from "../core/agent/agent-manager.js";
|
|
6
6
|
import { createTelegramBot } from "../transport/telegram/bot.js";
|
|
7
|
+
import { createToolProcessSupervisor } from "./tool-process-supervisor.js";
|
|
7
8
|
|
|
8
9
|
function normalizeString(value) {
|
|
9
10
|
const text = String(value ?? "").trim();
|
|
@@ -51,7 +52,8 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const artifactStore = new ArtifactStore();
|
|
54
|
-
const
|
|
55
|
+
const toolProcessSupervisor = createToolProcessSupervisor({ logger });
|
|
56
|
+
const toolRegistry = new ToolRegistry({ logger, processOwnerEnv: toolProcessSupervisor.env() });
|
|
55
57
|
const taskStore = new TaskStore();
|
|
56
58
|
await toolRegistry.load();
|
|
57
59
|
logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
|
|
@@ -63,8 +65,19 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
63
65
|
async start() {
|
|
64
66
|
logger?.log("app", `validating Pi model ${config.pi.provider}/${config.pi.model}`);
|
|
65
67
|
await agentManager.validatePiAgent();
|
|
68
|
+
await toolProcessSupervisor.start();
|
|
66
69
|
logger?.log("app", "starting Telegram bot");
|
|
67
|
-
|
|
70
|
+
try {
|
|
71
|
+
await bot.start();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
await toolProcessSupervisor.stop();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async stop() {
|
|
79
|
+
await bot.stop?.();
|
|
80
|
+
await toolProcessSupervisor.stop();
|
|
68
81
|
}
|
|
69
82
|
};
|
|
70
83
|
}
|
package/src/runtime/paths.js
CHANGED
|
@@ -7,9 +7,11 @@ 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");
|
|
10
11
|
export const tasksFile = path.join(stateDir, "tasks.json");
|
|
11
12
|
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
12
13
|
export const chatsDir = path.join(arisaHomeDir, "chats");
|
|
14
|
+
export const toolStateDir = path.join(stateDir, "tools");
|
|
13
15
|
|
|
14
16
|
export function getChatDir(chatId) {
|
|
15
17
|
return path.join(chatsDir, String(chatId));
|
|
@@ -23,6 +25,10 @@ export function getChatArtifactsIndexFile(chatId) {
|
|
|
23
25
|
return path.join(getChatDir(chatId), "state", "artifacts.json");
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
export function getChatToolStateDir(chatId, toolName) {
|
|
29
|
+
return path.join(getChatDir(chatId), "state", "tools", toolName);
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export function getChatPiSessionsDir(chatId) {
|
|
27
33
|
return path.join(getChatDir(chatId), "state", "pi-sessions");
|
|
28
34
|
}
|
|
@@ -35,24 +41,28 @@ export function getToolConfigPath(toolName) {
|
|
|
35
41
|
return path.join(getToolDir(toolName), "config.js");
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
export function
|
|
39
|
-
return path.join(getChatDir(chatId), "
|
|
44
|
+
export function getChatConfigDir(chatId) {
|
|
45
|
+
return path.join(getChatDir(chatId), "config");
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
export function
|
|
43
|
-
return
|
|
48
|
+
export function getChatTmpDir(chatId) {
|
|
49
|
+
return path.join(getChatDir(chatId), "tmp");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getChatToolConfigPath(chatId, toolName) {
|
|
53
|
+
return path.join(getChatConfigDir(chatId), "tools", toolName, "config.js");
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
export function
|
|
47
|
-
return path.join(
|
|
56
|
+
export function getToolStateDir(toolName) {
|
|
57
|
+
return path.join(toolStateDir, toolName);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export function getToolTmpDir(toolName) {
|
|
51
|
-
return path.join(
|
|
61
|
+
return path.join(getToolStateDir(toolName), "tmp");
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export function getChatToolTmpDir(chatId, toolName) {
|
|
55
|
-
return path.join(
|
|
65
|
+
return path.join(getChatTmpDir(chatId), "tools", toolName);
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export async function ensureArisaHome() {
|
|
@@ -74,24 +74,14 @@ export async function stopService() {
|
|
|
74
74
|
return { ok: true, pid: status.pid };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export async function unregisterServiceProcess() {
|
|
78
|
+
await rm(servicePidFile, { force: true }).catch(() => {});
|
|
79
|
+
}
|
|
80
|
+
|
|
77
81
|
export async function registerServiceProcess() {
|
|
78
82
|
await ensureArisaHome();
|
|
79
83
|
await writeFile(servicePidFile, `${process.pid}\n`, "utf8");
|
|
80
84
|
|
|
81
|
-
const cleanup = async () => {
|
|
82
|
-
await rm(servicePidFile, { force: true }).catch(() => {});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
process.on("SIGTERM", async () => {
|
|
86
|
-
await cleanup();
|
|
87
|
-
process.exit(0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
process.on("SIGINT", async () => {
|
|
91
|
-
await cleanup();
|
|
92
|
-
process.exit(0);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
85
|
process.on("exit", () => {
|
|
96
86
|
rm(servicePidFile, { force: true }).catch(() => {});
|
|
97
87
|
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { access, rm } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
daemonPaths,
|
|
5
|
+
listRegisteredDaemons,
|
|
6
|
+
readJson,
|
|
7
|
+
startManagedDaemon,
|
|
8
|
+
stopManagedDaemon,
|
|
9
|
+
writeJson
|
|
10
|
+
} from "../core/tools/daemon-processes.js";
|
|
11
|
+
import { ensureArisaHome, toolDaemonOwnerFile } from "./paths.js";
|
|
12
|
+
|
|
13
|
+
async function fileExists(file) {
|
|
14
|
+
try {
|
|
15
|
+
await access(file);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createToolProcessSupervisor({ logger } = {}) {
|
|
23
|
+
const token = crypto.randomUUID();
|
|
24
|
+
let heartbeatTimer = null;
|
|
25
|
+
let running = false;
|
|
26
|
+
|
|
27
|
+
const ownerEnv = {
|
|
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() {
|
|
42
|
+
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
|
+
for (const record of await listRegisteredDaemons()) {
|
|
54
|
+
if (!record.autoStart) continue;
|
|
55
|
+
if (!(await fileExists(record.entryPath))) {
|
|
56
|
+
logger?.log("tools", `skipping daemon ${record.toolName}: missing entry ${record.entryPath}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const paths = daemonPaths(record.toolName);
|
|
61
|
+
const { pid } = await readJson(paths.pidFile, {});
|
|
62
|
+
if (pid) {
|
|
63
|
+
await stopManagedDaemon(record.toolName);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger?.log("tools", `starting managed daemon ${record.toolName}`);
|
|
67
|
+
await startManagedDaemon({
|
|
68
|
+
toolName: record.toolName,
|
|
69
|
+
entryPath: record.entryPath,
|
|
70
|
+
ownerEnv
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
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
|
+
return {
|
|
90
|
+
env() {
|
|
91
|
+
return ownerEnv;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async start() {
|
|
95
|
+
if (running) return;
|
|
96
|
+
running = true;
|
|
97
|
+
await startHeartbeat();
|
|
98
|
+
await restartRegisteredDaemons();
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async stop() {
|
|
102
|
+
if (!running) return;
|
|
103
|
+
running = false;
|
|
104
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
105
|
+
heartbeatTimer = null;
|
|
106
|
+
await stopRegisteredDaemons();
|
|
107
|
+
await removeOwnerFile();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|