arisa 4.3.4 → 5.0.2
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 +18 -17
- package/README.md +30 -9
- package/package.json +6 -2
- package/pnpm-workspace.yaml +1 -0
- package/src/core/agent/agent-manager.js +288 -29
- package/src/core/agent/auth-flow.js +12 -8
- package/src/core/agent/model-selection.js +54 -14
- package/src/core/agent/model-speed.js +59 -0
- package/src/core/config/config-defaults.js +56 -4
- package/src/core/config/config-store.js +5 -1
- package/src/core/conversation/conversation-history-store.js +142 -0
- package/src/core/tasks/task-store.js +16 -0
- package/src/core/tools/daemon-health.js +11 -2
- package/src/core/tools/daemon-processes.js +92 -2
- package/src/core/tools/daemon-runtime.js +4 -2
- package/src/core/tools/ipc-client.js +15 -3
- package/src/core/tools/tool-registry.js +27 -0
- package/src/index.js +61 -6
- package/src/runtime/arisa-capabilities.js +45 -1
- package/src/runtime/bootstrap.js +3 -2
- package/src/runtime/create-app.js +47 -11
- package/src/runtime/doctor.js +307 -0
- package/src/runtime/log-viewer.js +165 -0
- package/src/runtime/paths.js +4 -1
- package/src/runtime/service-manager.js +106 -8
- package/src/runtime/tool-process-supervisor.js +107 -10
- package/src/transport/telegram/bot.js +533 -99
- package/src/transport/telegram/model-picker.js +28 -2
- package/test/agent-tool-policy.test.js +26 -1
- package/test/auth-flow.test.js +28 -2
- package/test/capabilities-security.test.js +37 -0
- package/test/context-and-task-bounds.test.js +279 -0
- package/test/daemon-runtime.test.js +130 -2
- package/test/dependency-warnings.test.js +17 -0
- package/test/doctor.test.js +90 -0
- package/test/log-viewer.test.js +90 -0
- package/test/model-selection.test.js +125 -2
- package/test/paths.test.js +8 -0
- package/test/pi-compaction.test.js +43 -0
- package/test/service-manager.test.js +234 -0
- package/test/task-store.test.js +31 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const MODEL_SPEEDS = Object.freeze([1, 1.5]);
|
|
2
|
+
|
|
3
|
+
export function normalizeModelSpeed(speed) {
|
|
4
|
+
const value = Number(speed);
|
|
5
|
+
if (!MODEL_SPEEDS.includes(value)) {
|
|
6
|
+
throw new Error(`Invalid model speed: ${speed}`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function modelSupportsSpeed(model) {
|
|
12
|
+
return model?.provider === "openai-codex"
|
|
13
|
+
&& model.api === "openai-codex-responses"
|
|
14
|
+
&& typeof model.id === "string"
|
|
15
|
+
&& (
|
|
16
|
+
model.id === "gpt-5.4"
|
|
17
|
+
|| model.id === "gpt-5.5"
|
|
18
|
+
|| model.id === "gpt-5.6"
|
|
19
|
+
|| model.id.startsWith("gpt-5.6-")
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clampModelSpeed(model, speed) {
|
|
24
|
+
const normalized = normalizeModelSpeed(speed);
|
|
25
|
+
return normalized === 1.5 && !modelSupportsSpeed(model) ? 1 : normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function speedToServiceTier(speed) {
|
|
29
|
+
return normalizeModelSpeed(speed) === 1.5 ? "priority" : "default";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createModelSpeedController(streamFn, initialSpeed) {
|
|
33
|
+
if (typeof streamFn !== "function") throw new Error("Pi stream function is unavailable");
|
|
34
|
+
let speed = normalizeModelSpeed(initialSpeed);
|
|
35
|
+
return {
|
|
36
|
+
get speed() {
|
|
37
|
+
return speed;
|
|
38
|
+
},
|
|
39
|
+
setSpeed(nextSpeed) {
|
|
40
|
+
speed = normalizeModelSpeed(nextSpeed);
|
|
41
|
+
},
|
|
42
|
+
streamFn(model, context, options) {
|
|
43
|
+
const serviceTier = speedToServiceTier(speed);
|
|
44
|
+
const onPayload = options?.onPayload;
|
|
45
|
+
return streamFn(model, context, {
|
|
46
|
+
...options,
|
|
47
|
+
serviceTier,
|
|
48
|
+
async onPayload(payload, requestModel) {
|
|
49
|
+
const replacement = await onPayload?.(payload, requestModel);
|
|
50
|
+
const effectivePayload = replacement === undefined ? payload : replacement;
|
|
51
|
+
if (!effectivePayload || typeof effectivePayload !== "object" || Array.isArray(effectivePayload)) {
|
|
52
|
+
throw new Error("Pi provider payload is not an object");
|
|
53
|
+
}
|
|
54
|
+
return { ...effectivePayload, service_tier: serviceTier };
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -15,23 +15,75 @@ export const daemonConfigDefaults = Object.freeze({
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export const telegramConfigDefaults = Object.freeze({
|
|
18
|
-
modelPickerPageSize: 8
|
|
18
|
+
modelPickerPageSize: 8,
|
|
19
|
+
busyMessageMode: "steer"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const doctorConfigDefaults = Object.freeze({
|
|
23
|
+
contextInspectionTimeoutMs: 5_000,
|
|
24
|
+
contextWarningPercent: 70,
|
|
25
|
+
contextCriticalPercent: 90,
|
|
26
|
+
contextInefficientMinTokens: 32_000,
|
|
27
|
+
contextToolResultWarningPercent: 60,
|
|
28
|
+
contextSingleMessageWarningPercent: 50
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const cliLogConfig = Object.freeze({
|
|
32
|
+
recentLines: 100,
|
|
33
|
+
followPollIntervalMs: 250
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const serviceConfigDefaults = Object.freeze({
|
|
37
|
+
shutdownTimeoutMs: 15_000,
|
|
38
|
+
shutdownPollIntervalMs: 100
|
|
19
39
|
});
|
|
20
40
|
|
|
21
41
|
export const piConfigDefaults = Object.freeze({
|
|
22
|
-
thinkingLevel: "medium"
|
|
42
|
+
thinkingLevel: "medium",
|
|
43
|
+
speed: 1,
|
|
44
|
+
compaction: Object.freeze({
|
|
45
|
+
enabled: true,
|
|
46
|
+
reserveTokens: 16_384,
|
|
47
|
+
keepRecentTokens: 20_000
|
|
48
|
+
})
|
|
23
49
|
});
|
|
24
50
|
|
|
51
|
+
function cloneChatModels(chatModels) {
|
|
52
|
+
if (!chatModels || typeof chatModels !== "object") return chatModels;
|
|
53
|
+
return Object.fromEntries(Object.entries(chatModels).map(([chatId, selection]) => [
|
|
54
|
+
chatId,
|
|
55
|
+
selection && typeof selection === "object" ? { ...selection } : selection
|
|
56
|
+
]));
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
export function applyConfigDefaults(config) {
|
|
60
|
+
const normalized = { ...config };
|
|
61
|
+
delete normalized.agent;
|
|
62
|
+
delete normalized.prime;
|
|
63
|
+
const configuredPi = normalized.pi || {};
|
|
64
|
+
|
|
26
65
|
return {
|
|
27
|
-
...
|
|
66
|
+
...normalized,
|
|
28
67
|
telegram: {
|
|
29
68
|
...telegramConfigDefaults,
|
|
30
69
|
...(config.telegram || {})
|
|
31
70
|
},
|
|
71
|
+
doctor: {
|
|
72
|
+
...doctorConfigDefaults,
|
|
73
|
+
...(config.doctor || {})
|
|
74
|
+
},
|
|
75
|
+
service: {
|
|
76
|
+
...serviceConfigDefaults,
|
|
77
|
+
...(config.service || {})
|
|
78
|
+
},
|
|
32
79
|
pi: {
|
|
33
80
|
...piConfigDefaults,
|
|
34
|
-
...
|
|
81
|
+
...configuredPi,
|
|
82
|
+
compaction: {
|
|
83
|
+
...piConfigDefaults.compaction,
|
|
84
|
+
...(configuredPi.compaction || {})
|
|
85
|
+
},
|
|
86
|
+
chatModels: cloneChatModels(configuredPi.chatModels)
|
|
35
87
|
},
|
|
36
88
|
daemons: {
|
|
37
89
|
...daemonConfigDefaults,
|
|
@@ -3,6 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import { configFile } from "../../runtime/paths.js";
|
|
4
4
|
import { applyConfigDefaults } from "./config-defaults.js";
|
|
5
5
|
|
|
6
|
+
export function prepareConfigForSave(config) {
|
|
7
|
+
return applyConfigDefaults(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export async function loadConfig() {
|
|
7
11
|
const raw = await readFile(configFile, "utf8");
|
|
8
12
|
return applyConfigDefaults(JSON.parse(raw));
|
|
@@ -10,7 +14,7 @@ export async function loadConfig() {
|
|
|
10
14
|
|
|
11
15
|
export async function saveConfig(config) {
|
|
12
16
|
await mkdir(path.dirname(configFile), { recursive: true });
|
|
13
|
-
await writeFile(configFile, `${JSON.stringify(
|
|
17
|
+
await writeFile(configFile, `${JSON.stringify(prepareConfigForSave(config), null, 2)}\n`, "utf8");
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export async function updateConfig(mutator) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getChatConversationHistoryFile } from "../../runtime/paths.js";
|
|
5
|
+
|
|
6
|
+
const utf8Bom = "\uFEFF";
|
|
7
|
+
|
|
8
|
+
function normalizeText(value) {
|
|
9
|
+
return String(value || "").trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseHistory(contents) {
|
|
13
|
+
return String(contents || "")
|
|
14
|
+
.replace(/^\uFEFF/, "")
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((line) => JSON.parse(line));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function serializeRecord(record) {
|
|
21
|
+
return `${JSON.stringify(record)}\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatPortableConversation(records) {
|
|
25
|
+
if (!records.length) return "";
|
|
26
|
+
const sections = [
|
|
27
|
+
"Portable Arisa conversation history.",
|
|
28
|
+
"This portable history belongs to the same Telegram chat and is independent of the active agent harness.",
|
|
29
|
+
"Use it as prior conversation context. Do not repeat it unless the user asks."
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
if (record.kind === "seed") {
|
|
34
|
+
sections.push(`Imported earlier conversation:\n${record.history}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (record.prompt) parts.push(`User or system request:\n${record.prompt}`);
|
|
39
|
+
if (record.response) parts.push(`Assistant response:\n${record.response}`);
|
|
40
|
+
if (parts.length) sections.push(parts.join("\n\n"));
|
|
41
|
+
}
|
|
42
|
+
return sections.join("\n\n---\n\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ConversationHistoryStore {
|
|
46
|
+
constructor({ historyFile = getChatConversationHistoryFile } = {}) {
|
|
47
|
+
this.locks = new Map();
|
|
48
|
+
this.historyFile = historyFile;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async withChatLock(chatId, work) {
|
|
52
|
+
const key = String(chatId);
|
|
53
|
+
const previous = this.locks.get(key) || Promise.resolve();
|
|
54
|
+
const current = previous.catch(() => {}).then(work);
|
|
55
|
+
this.locks.set(key, current);
|
|
56
|
+
try {
|
|
57
|
+
return await current;
|
|
58
|
+
} finally {
|
|
59
|
+
if (this.locks.get(key) === current) this.locks.delete(key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async read(chatId) {
|
|
64
|
+
try {
|
|
65
|
+
return parseHistory(await readFile(this.historyFile(chatId), "utf8"));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error?.code === "ENOENT") return [];
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async hasEntries(chatId) {
|
|
73
|
+
return (await this.read(chatId)).length > 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async appendRecord(chatId, record) {
|
|
77
|
+
const file = this.historyFile(chatId);
|
|
78
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
79
|
+
const handle = await open(file, "a+", 0o600);
|
|
80
|
+
try {
|
|
81
|
+
const stats = await handle.stat();
|
|
82
|
+
if (stats.size === 0) await handle.write(utf8Bom);
|
|
83
|
+
await handle.write(serializeRecord(record));
|
|
84
|
+
} finally {
|
|
85
|
+
await handle.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async ensureSeed(chatId, { runtime, history }) {
|
|
90
|
+
const normalizedHistory = normalizeText(history);
|
|
91
|
+
if (!normalizedHistory) return false;
|
|
92
|
+
return this.withChatLock(chatId, async () => {
|
|
93
|
+
if ((await this.read(chatId)).length) return false;
|
|
94
|
+
await this.appendRecord(chatId, {
|
|
95
|
+
id: crypto.randomUUID(),
|
|
96
|
+
kind: "seed",
|
|
97
|
+
runtime,
|
|
98
|
+
history: normalizedHistory,
|
|
99
|
+
createdAt: new Date().toISOString()
|
|
100
|
+
});
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async appendTurn(chatId, { runtime, prompt, response }) {
|
|
106
|
+
const normalizedPrompt = normalizeText(prompt);
|
|
107
|
+
const normalizedResponse = normalizeText(response);
|
|
108
|
+
if (!normalizedPrompt && !normalizedResponse) return null;
|
|
109
|
+
const record = {
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
kind: "turn",
|
|
112
|
+
runtime,
|
|
113
|
+
prompt: normalizedPrompt,
|
|
114
|
+
response: normalizedResponse,
|
|
115
|
+
createdAt: new Date().toISOString()
|
|
116
|
+
};
|
|
117
|
+
await this.withChatLock(chatId, () => this.appendRecord(chatId, record));
|
|
118
|
+
return record;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async reset(chatId, { runtime, history = "" } = {}) {
|
|
122
|
+
return this.withChatLock(chatId, async () => {
|
|
123
|
+
const file = this.historyFile(chatId);
|
|
124
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
125
|
+
const normalizedHistory = normalizeText(history);
|
|
126
|
+
const seed = normalizedHistory
|
|
127
|
+
? serializeRecord({
|
|
128
|
+
id: crypto.randomUUID(),
|
|
129
|
+
kind: "seed",
|
|
130
|
+
runtime,
|
|
131
|
+
history: normalizedHistory,
|
|
132
|
+
createdAt: new Date().toISOString()
|
|
133
|
+
})
|
|
134
|
+
: "";
|
|
135
|
+
await writeFile(file, `${utf8Bom}${seed}`, { encoding: "utf8", mode: 0o600 });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async buildHandoff(chatId) {
|
|
140
|
+
return formatPortableConversation(await this.read(chatId));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -99,6 +99,22 @@ export class TaskStore {
|
|
|
99
99
|
return due;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
async recoverInterrupted() {
|
|
103
|
+
await this.reload();
|
|
104
|
+
const recovered = [];
|
|
105
|
+
const updatedAt = new Date().toISOString();
|
|
106
|
+
|
|
107
|
+
for (const task of this.tasks) {
|
|
108
|
+
if (task.status !== "running") continue;
|
|
109
|
+
task.status = "pending";
|
|
110
|
+
task.updatedAt = updatedAt;
|
|
111
|
+
recovered.push({ ...task });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (recovered.length) await this.save();
|
|
115
|
+
return recovered;
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
async complete(taskId) {
|
|
103
119
|
await this.init();
|
|
104
120
|
const task = this.tasks.find((item) => item.id === taskId);
|
|
@@ -17,7 +17,8 @@ function errorRecord(phase, error) {
|
|
|
17
17
|
return {
|
|
18
18
|
at: new Date().toISOString(),
|
|
19
19
|
phase,
|
|
20
|
-
message: error?.message || String(error)
|
|
20
|
+
message: error?.message || String(error),
|
|
21
|
+
...(error?.code ? { code: error.code } : {})
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -25,6 +26,13 @@ function isTimeout(error) {
|
|
|
25
26
|
return ["DAEMON_JOB_TIMEOUT", "DAEMON_OPERATION_TIMEOUT"].includes(error?.code);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function recordedError(status, fallback) {
|
|
30
|
+
if (!status.lastError?.message) return fallback;
|
|
31
|
+
const error = new Error(status.lastError.message);
|
|
32
|
+
if (status.lastError.code) error.code = status.lastError.code;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
function healthDue(status, policy, now = Date.now()) {
|
|
29
37
|
const heartbeatAt = new Date(status.heartbeatAt || 0).getTime();
|
|
30
38
|
const healthAt = new Date(status.lastHealthCheckAt || 0).getTime();
|
|
@@ -156,6 +164,7 @@ export async function superviseDaemon(record, policy) {
|
|
|
156
164
|
const alive = isProcessAlive(pid);
|
|
157
165
|
|
|
158
166
|
if (!alive) {
|
|
167
|
+
if (status.state === "failed") return "failed";
|
|
159
168
|
if (!record.autoStart && !status.restartRequested && ["stopped", "unhealthy", "failed"].includes(status.state)) {
|
|
160
169
|
return status.state;
|
|
161
170
|
}
|
|
@@ -165,7 +174,7 @@ export async function superviseDaemon(record, policy) {
|
|
|
165
174
|
if (status.state === "restarting") {
|
|
166
175
|
return restartIfDue(record, paths, status, policy);
|
|
167
176
|
}
|
|
168
|
-
return scheduleRestart(record, paths, status, policy, new Error("Daemon process exited"));
|
|
177
|
+
return scheduleRestart(record, paths, status, policy, recordedError(status, new Error("Daemon process exited")));
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
if (!healthDue(status, policy)) return "healthy";
|
|
@@ -97,6 +97,59 @@ export async function writeDaemonStatus(pathsOrIdentity, patch) {
|
|
|
97
97
|
return next;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function daemonDisposition({ state, alive, autoStart, restartRequested }) {
|
|
101
|
+
if (state === "failed") return "requires-attention";
|
|
102
|
+
if (state === "restarting" || restartRequested) return "automatic-retry";
|
|
103
|
+
if (["degraded", "unhealthy"].includes(state)) return autoStart ? "automatic-recovery" : "requires-attention";
|
|
104
|
+
if (state === "stopped") return autoStart ? "automatic-restart" : "leave-stopped";
|
|
105
|
+
if (state === "ready" && !alive) return autoStart ? "automatic-restart" : "start-on-demand";
|
|
106
|
+
if (state === "ready") return "available";
|
|
107
|
+
if (state === "starting") return "starting";
|
|
108
|
+
return autoStart ? "automatic-start" : "start-on-demand";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function redactDiagnosticText(value) {
|
|
112
|
+
if (!value) return null;
|
|
113
|
+
return String(value)
|
|
114
|
+
.replace(/\b(Bearer)\s+[^\s,;]+/gi, "$1 [redacted]")
|
|
115
|
+
.replace(/\b(token|secret|password|api[_-]?key)(\s*[=:]\s*)[^\s,;&]+/gi, "$1$2[redacted]")
|
|
116
|
+
.replace(/([?&](?:token|secret|password|api[_-]?key)=)[^\s&#]+/gi, "$1[redacted]")
|
|
117
|
+
.replace(/(https?:\/\/[^\s:/]+:)[^\s@/]+@/gi, "$1[redacted]@");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function diagnosticError(lastError) {
|
|
121
|
+
if (!lastError) return null;
|
|
122
|
+
return {
|
|
123
|
+
...lastError,
|
|
124
|
+
message: redactDiagnosticText(lastError.message)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function readDaemonDiagnostic({ toolName, scope, autoStart = false }) {
|
|
129
|
+
const paths = daemonPaths({ toolName, scope });
|
|
130
|
+
const status = await readJson(paths.statusFile, {});
|
|
131
|
+
const { pid } = await readJson(paths.pidFile, {});
|
|
132
|
+
const state = status.state || "not-started";
|
|
133
|
+
const alive = isProcessAlive(pid);
|
|
134
|
+
const restartRequested = Boolean(status.restartRequested);
|
|
135
|
+
const hasActiveError = ["degraded", "unhealthy", "restarting", "failed"].includes(state);
|
|
136
|
+
return {
|
|
137
|
+
state,
|
|
138
|
+
alive,
|
|
139
|
+
pid: alive ? pid : null,
|
|
140
|
+
message: redactDiagnosticText(status.message),
|
|
141
|
+
lastError: hasActiveError ? diagnosticError(status.lastError) : null,
|
|
142
|
+
restart: {
|
|
143
|
+
attempts: Number(status.restartAttempts || 0),
|
|
144
|
+
requested: restartRequested,
|
|
145
|
+
nextAt: status.nextRestartAt || null
|
|
146
|
+
},
|
|
147
|
+
disposition: daemonDisposition({ state, alive, autoStart: Boolean(autoStart), restartRequested }),
|
|
148
|
+
updatedAt: status.updatedAt || null,
|
|
149
|
+
logFile: paths.logFile
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
100
153
|
function registrationRecord({ paths, entryPath, autoStart, startupContext, current = {}, startedAt = null }) {
|
|
101
154
|
return {
|
|
102
155
|
toolName: paths.toolName,
|
|
@@ -168,6 +221,22 @@ async function acquireStartLock(paths, policy) {
|
|
|
168
221
|
throw new Error(`Timed out acquiring daemon start lock for ${paths.toolName} (${paths.instanceId})`);
|
|
169
222
|
}
|
|
170
223
|
|
|
224
|
+
export async function unregisterManagedDaemon(toolNameOrOptions, { scope } = {}) {
|
|
225
|
+
const paths = daemonPaths(toolNameOrOptions, scope);
|
|
226
|
+
const { pid } = await readJson(paths.pidFile, {});
|
|
227
|
+
if (isProcessAlive(pid)) {
|
|
228
|
+
throw new Error(`Refusing to unregister a live daemon: ${paths.toolName} (${paths.instanceId})`);
|
|
229
|
+
}
|
|
230
|
+
await Promise.all([
|
|
231
|
+
rm(paths.metaFile, { force: true }),
|
|
232
|
+
rm(paths.pidFile, { force: true }),
|
|
233
|
+
rm(paths.statusFile, { force: true }),
|
|
234
|
+
rm(paths.startLockFile, { force: true }),
|
|
235
|
+
rm(paths.commandsDir, { recursive: true, force: true })
|
|
236
|
+
]);
|
|
237
|
+
return { toolName: paths.toolName, scope: paths.scope, instanceId: paths.instanceId };
|
|
238
|
+
}
|
|
239
|
+
|
|
171
240
|
export async function stopManagedDaemon(toolNameOrOptions, {
|
|
172
241
|
scope,
|
|
173
242
|
signal = "SIGTERM",
|
|
@@ -285,9 +354,29 @@ export async function startManagedDaemon({
|
|
|
285
354
|
} finally {
|
|
286
355
|
closeSync(out);
|
|
287
356
|
}
|
|
357
|
+
const pidRegistration = writeJson(paths.pidFile, { pid: child.pid, startedAt });
|
|
358
|
+
child.once("exit", async (exitCode, signal) => {
|
|
359
|
+
await pidRegistration.catch(() => {});
|
|
360
|
+
const currentPid = (await readJson(paths.pidFile, {})).pid;
|
|
361
|
+
const currentStatus = await readJson(paths.statusFile, {});
|
|
362
|
+
if (currentPid !== child.pid || ["stopped", "failed"].includes(currentStatus.state)) return;
|
|
363
|
+
const exitReason = signal ? `signal ${signal}` : `exit code ${exitCode}`;
|
|
364
|
+
await writeDaemonStatus(paths, {
|
|
365
|
+
state: "degraded",
|
|
366
|
+
pid: null,
|
|
367
|
+
heartbeatAt: null,
|
|
368
|
+
lastError: {
|
|
369
|
+
at: new Date().toISOString(),
|
|
370
|
+
phase: "process-exit",
|
|
371
|
+
message: `Daemon process exited with ${exitReason}`,
|
|
372
|
+
code: signal || `EXIT_${exitCode}`
|
|
373
|
+
},
|
|
374
|
+
message: `Daemon process exited with ${exitReason}`
|
|
375
|
+
}).catch(() => {});
|
|
376
|
+
});
|
|
288
377
|
child.unref();
|
|
289
378
|
|
|
290
|
-
await
|
|
379
|
+
await pidRegistration;
|
|
291
380
|
await writeDaemonStatus(paths, {
|
|
292
381
|
state: "starting",
|
|
293
382
|
pid: child.pid,
|
|
@@ -301,7 +390,8 @@ export async function startManagedDaemon({
|
|
|
301
390
|
lastError: {
|
|
302
391
|
at: new Date().toISOString(),
|
|
303
392
|
phase: "start",
|
|
304
|
-
message: error?.message || String(error)
|
|
393
|
+
message: error?.message || String(error),
|
|
394
|
+
...(error?.code ? { code: error.code } : {})
|
|
305
395
|
},
|
|
306
396
|
message: error?.message || String(error)
|
|
307
397
|
}).catch(() => {});
|
|
@@ -289,7 +289,8 @@ export function createDaemonRuntime({
|
|
|
289
289
|
lastError: {
|
|
290
290
|
at: new Date().toISOString(),
|
|
291
291
|
phase: operation || "job",
|
|
292
|
-
message: error?.message || String(error)
|
|
292
|
+
message: error?.message || String(error),
|
|
293
|
+
...(error?.code ? { code: error.code } : {})
|
|
293
294
|
},
|
|
294
295
|
message: error?.message || String(error)
|
|
295
296
|
});
|
|
@@ -315,7 +316,8 @@ export function createDaemonRuntime({
|
|
|
315
316
|
lastError: {
|
|
316
317
|
at: new Date().toISOString(),
|
|
317
318
|
phase: "work-loop",
|
|
318
|
-
message: error?.message || String(error)
|
|
319
|
+
message: error?.message || String(error),
|
|
320
|
+
...(error?.code ? { code: error.code } : {})
|
|
319
321
|
},
|
|
320
322
|
message: error?.message || String(error)
|
|
321
323
|
});
|
|
@@ -53,7 +53,12 @@ function requestIpc({ socketPath, request, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function createArisaClient({
|
|
56
|
+
export function createArisaClient({
|
|
57
|
+
toolName,
|
|
58
|
+
chatId = null,
|
|
59
|
+
capabilityToken = process.env.ARISA_IPC_TOKEN || "",
|
|
60
|
+
socketPath = process.env.ARISA_IPC_SOCKET || arisaIpcSocketFile
|
|
61
|
+
} = {}) {
|
|
57
62
|
if (typeof toolName !== "string" || !toolName.trim()) {
|
|
58
63
|
throw new Error("toolName is required");
|
|
59
64
|
}
|
|
@@ -66,6 +71,7 @@ export function createArisaClient({ toolName, chatId = null, socketPath = proces
|
|
|
66
71
|
method,
|
|
67
72
|
toolName,
|
|
68
73
|
chatId,
|
|
74
|
+
capabilityToken,
|
|
69
75
|
params
|
|
70
76
|
}
|
|
71
77
|
});
|
|
@@ -74,17 +80,23 @@ export function createArisaClient({ toolName, chatId = null, socketPath = proces
|
|
|
74
80
|
artifacts: {
|
|
75
81
|
createText: (params) => call("artifacts.createText", params),
|
|
76
82
|
listRecent: (params) => call("artifacts.listRecent", params),
|
|
77
|
-
get: (params) => call("artifacts.get", params)
|
|
83
|
+
get: (params) => call("artifacts.get", params),
|
|
84
|
+
deliver: (params) => call("artifacts.deliver", params, { timeoutMs: 120_000 })
|
|
78
85
|
},
|
|
79
86
|
tasks: {
|
|
80
87
|
add: (params) => call("tasks.add", params),
|
|
81
88
|
list: (params) => call("tasks.list", params),
|
|
82
|
-
cancel: (params) => call("tasks.cancel", params)
|
|
89
|
+
cancel: (params) => call("tasks.cancel", params),
|
|
90
|
+
cancelAll: () => call("tasks.cancelAll")
|
|
83
91
|
},
|
|
84
92
|
agent: {
|
|
85
93
|
enqueueEvent: (params) => call("agent.enqueueEvent", params)
|
|
86
94
|
},
|
|
87
95
|
tools: {
|
|
96
|
+
list: () => call("tools.list"),
|
|
97
|
+
help: (params) => call("tools.help", params),
|
|
98
|
+
skills: (params) => call("tools.skills", params),
|
|
99
|
+
setConfig: (params) => call("tools.setConfig", params),
|
|
88
100
|
run: (params, options) => call("tools.run", params, options)
|
|
89
101
|
},
|
|
90
102
|
paths: {
|
|
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { arisaIpcSocketFile, arisaPackageDir, getToolConfigPath, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
5
5
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
6
6
|
import { normalizeToolResult } from "./tool-result.js";
|
|
7
|
+
import { readDaemonDiagnostic } from "./daemon-processes.js";
|
|
7
8
|
import { SkillRegistry } from "../skills/skill-registry.js";
|
|
8
9
|
|
|
9
10
|
function toolEnv() {
|
|
@@ -105,6 +106,32 @@ export class ToolRegistry {
|
|
|
105
106
|
}));
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
async listWithRuntime(chatId = null) {
|
|
110
|
+
return Promise.all(this.list().map(async (listedTool) => {
|
|
111
|
+
const daemon = this.get(listedTool.name)?.daemon;
|
|
112
|
+
if (!daemon) return listedTool;
|
|
113
|
+
if (daemon.scope === "chat" && (chatId == null || chatId === "")) {
|
|
114
|
+
throw new Error(`Daemon status for ${listedTool.name} requires chatId`);
|
|
115
|
+
}
|
|
116
|
+
const scope = daemon.scope === "chat"
|
|
117
|
+
? { type: "chat", chatId }
|
|
118
|
+
: { type: "global" };
|
|
119
|
+
return {
|
|
120
|
+
...listedTool,
|
|
121
|
+
daemon: {
|
|
122
|
+
scope: daemon.scope,
|
|
123
|
+
autoStart: Boolean(daemon.autoStart),
|
|
124
|
+
health: daemon.health || null,
|
|
125
|
+
runtime: await readDaemonDiagnostic({
|
|
126
|
+
toolName: listedTool.name,
|
|
127
|
+
scope,
|
|
128
|
+
autoStart: daemon.autoStart
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
get(name) {
|
|
109
136
|
return this.tools.get(name) || null;
|
|
110
137
|
}
|