arisa 5.2.17 → 5.2.20
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/LOW-MEMORY.md +37 -0
- package/README.md +6 -3
- package/package.json +3 -3
- package/pnpm-workspace.yaml +7 -5
- package/src/core/agent/agent-manager.js +13 -14
- package/src/core/agent/auth-flow.js +6 -6
- package/src/core/agent/model-selection.js +4 -3
- package/src/core/agent/model-speed.js +13 -3
- package/src/core/agent/pi-auth-login.js +28 -28
- package/src/core/agent/pi-capability-tools.js +1 -0
- package/src/core/agent/pi-runtime.js +14 -21
- package/src/core/artifacts/artifact-index.js +107 -0
- package/src/core/artifacts/artifact-store.js +15 -84
- package/src/core/artifacts/legacy-artifact-reader.js +46 -0
- package/src/core/capabilities/capability-service.js +25 -0
- package/src/core/tasks/task-store.js +2 -1
- package/src/index.js +10 -4
- package/src/official-tools.lock.json +25 -18
- package/src/platform/paths.js +5 -0
- package/src/runtime/bootstrap-cli.js +3 -3
- package/src/runtime/bootstrap-telegram.js +7 -7
- package/src/runtime/slave-cli.js +20 -10
- package/src/runtime/slave-service.js +299 -7
- package/src/runtime/tui.js +5 -6
- package/src/transport/telegram/bot.js +9 -5
- package/src/transport/telegram/model-callback.js +3 -2
- package/src/transport/telegram/model-controls.js +4 -4
- package/src/transport/telegram/model-picker.js +1 -1
- package/src/transport/telegram/task-dispatcher.js +50 -23
- package/src/transport/telegram/telegram-auth-controller.js +7 -7
- package/src/transport/telegram/telegram-session-bridge.js +2 -1
- package/test/agent-turn-coordinator.test.js +5 -2
- package/test/artifact-index-memory.test.js +46 -0
- package/test/artifact-index-migration.test.js +88 -0
- package/test/artifact-store.test.js +3 -3
- package/test/auth-flow.test.js +2 -2
- package/test/capabilities-security.test.js +36 -0
- package/test/cli-memory.test.js +22 -0
- package/test/model-selection.test.js +14 -4
- package/test/official-tool-installer.test.js +13 -0
- package/test/paths.test.js +2 -0
- package/test/pi-auth-login.test.js +78 -0
- package/test/pi-capability-tools.test.js +3 -0
- package/test/pi-speed-integration.test.js +177 -0
- package/test/slave-cli.test.js +221 -5
- package/test/task-store.test.js +3 -1
- package/test/telegram-task-dispatcher.test.js +57 -2
|
@@ -4,6 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
export const slaveServiceName = "arisa-slave.service";
|
|
7
|
+
export const slaveLaunchdLabel = "com.arisa.slave";
|
|
8
|
+
export const slaveWindowsTaskName = "Arisa Slave";
|
|
7
9
|
|
|
8
10
|
function exists(target) {
|
|
9
11
|
return access(target).then(() => true, () => false);
|
|
@@ -15,6 +17,12 @@ function requireAccountName(value) {
|
|
|
15
17
|
return name;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function requireWindowsAccountName(value) {
|
|
21
|
+
const name = String(value || "").trim();
|
|
22
|
+
if (!name || /[\u0000-\u001f<>"|]/.test(name)) throw new Error(`Invalid Windows service account: ${name || "empty"}`);
|
|
23
|
+
return name;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
export function resolveSlaveHome({ environment = process.env, euid = process.geteuid?.(), homedir = os.homedir() } = {}) {
|
|
19
27
|
if (environment.ARISA_SLAVE_HOME) return path.resolve(environment.ARISA_SLAVE_HOME);
|
|
20
28
|
return euid === 0 ? "/var/lib/arisa-slave" : path.join(homedir, ".arisa-slave");
|
|
@@ -39,8 +47,15 @@ export function getSlavePaths(slaveHome) {
|
|
|
39
47
|
export async function selectSlaveServiceAccount({
|
|
40
48
|
euid = process.geteuid?.(),
|
|
41
49
|
currentUser = os.userInfo().username,
|
|
42
|
-
environment = process.env
|
|
50
|
+
environment = process.env,
|
|
51
|
+
platform = process.platform
|
|
43
52
|
} = {}) {
|
|
53
|
+
if (platform === "win32") {
|
|
54
|
+
const qualifiedUser = environment.USERDOMAIN && environment.USERNAME
|
|
55
|
+
? `${environment.USERDOMAIN}\\${environment.USERNAME}`
|
|
56
|
+
: currentUser;
|
|
57
|
+
return { scope: "user", user: requireWindowsAccountName(qualifiedUser), root: false, dedicated: false };
|
|
58
|
+
}
|
|
44
59
|
if (euid !== 0) {
|
|
45
60
|
return { scope: "user", user: requireAccountName(currentUser), root: false, dedicated: false };
|
|
46
61
|
}
|
|
@@ -95,6 +110,154 @@ export function buildSlaveSystemdUnit({ account, slaveHome, entryFile, nodePath
|
|
|
95
110
|
].filter((line, index, lines) => line || lines[index - 1] !== "").join("\n");
|
|
96
111
|
}
|
|
97
112
|
|
|
113
|
+
function escapeXml(value) {
|
|
114
|
+
return String(value)
|
|
115
|
+
.replaceAll("&", "&")
|
|
116
|
+
.replaceAll("<", "<")
|
|
117
|
+
.replaceAll(">", ">")
|
|
118
|
+
.replaceAll('"', """)
|
|
119
|
+
.replaceAll("'", "'");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function plistString(value) {
|
|
123
|
+
return ` <string>${escapeXml(value)}</string>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildSlaveLaunchdPlist({ account, slaveHome, entryFile, nodePath = process.execPath, environment = process.env }) {
|
|
127
|
+
if (!account?.scope || !account?.user) throw new Error("Slave launchd service requires a service account");
|
|
128
|
+
if (typeof entryFile !== "string" || !path.isAbsolute(entryFile)) throw new Error("Slave launchd service requires an absolute Arisa entry file");
|
|
129
|
+
const paths = getSlavePaths(slaveHome);
|
|
130
|
+
const serviceUser = account.scope === "system" && account.user !== "root"
|
|
131
|
+
? [" <key>UserName</key>", ` <string>${escapeXml(account.user)}</string>`]
|
|
132
|
+
: [];
|
|
133
|
+
const environmentEntries = {
|
|
134
|
+
ARISA_HOME: paths.home,
|
|
135
|
+
ARISA_SLAVE_HOME: paths.home,
|
|
136
|
+
...(environment.HOME ? { HOME: environment.HOME } : {}),
|
|
137
|
+
...(environment.PATH ? { PATH: environment.PATH } : {}),
|
|
138
|
+
...(environment.TMPDIR ? { TMPDIR: environment.TMPDIR } : {})
|
|
139
|
+
};
|
|
140
|
+
return [
|
|
141
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
142
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
143
|
+
'<plist version="1.0">',
|
|
144
|
+
'<dict>',
|
|
145
|
+
' <key>Label</key>',
|
|
146
|
+
` <string>${slaveLaunchdLabel}</string>`,
|
|
147
|
+
' <key>ProgramArguments</key>',
|
|
148
|
+
' <array>',
|
|
149
|
+
plistString(nodePath),
|
|
150
|
+
plistString(entryFile),
|
|
151
|
+
plistString("slave"),
|
|
152
|
+
plistString("--service-runner"),
|
|
153
|
+
' </array>',
|
|
154
|
+
' <key>EnvironmentVariables</key>',
|
|
155
|
+
' <dict>',
|
|
156
|
+
...Object.entries(environmentEntries).flatMap(([key, value]) => [
|
|
157
|
+
` <key>${escapeXml(key)}</key>`,
|
|
158
|
+
` <string>${escapeXml(value)}</string>`
|
|
159
|
+
]),
|
|
160
|
+
' </dict>',
|
|
161
|
+
' <key>WorkingDirectory</key>',
|
|
162
|
+
` <string>${escapeXml(paths.home)}</string>`,
|
|
163
|
+
' <key>StandardOutPath</key>',
|
|
164
|
+
` <string>${escapeXml(paths.logFile)}</string>`,
|
|
165
|
+
' <key>StandardErrorPath</key>',
|
|
166
|
+
` <string>${escapeXml(paths.logFile)}</string>`,
|
|
167
|
+
' <key>RunAtLoad</key>',
|
|
168
|
+
' <true/>',
|
|
169
|
+
' <key>KeepAlive</key>',
|
|
170
|
+
' <true/>',
|
|
171
|
+
...serviceUser,
|
|
172
|
+
'</dict>',
|
|
173
|
+
'</plist>',
|
|
174
|
+
''
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildSlaveWindowsLauncher({ slaveHome, entryFile, nodePath = process.execPath }) {
|
|
179
|
+
if (typeof entryFile !== "string" || !path.isAbsolute(entryFile)) throw new Error("Slave Windows launcher requires an absolute Arisa entry file");
|
|
180
|
+
const paths = getSlavePaths(slaveHome);
|
|
181
|
+
return [
|
|
182
|
+
'const { closeSync, openSync } = require("node:fs");',
|
|
183
|
+
'const { spawn } = require("node:child_process");',
|
|
184
|
+
`const home = ${JSON.stringify(paths.home)};`,
|
|
185
|
+
`const logFile = ${JSON.stringify(paths.logFile)};`,
|
|
186
|
+
`process.env.ARISA_HOME = ${JSON.stringify(paths.home)};`,
|
|
187
|
+
`process.env.ARISA_SLAVE_HOME = ${JSON.stringify(paths.home)};`,
|
|
188
|
+
'process.chdir(home);',
|
|
189
|
+
'const log = openSync(logFile, "a");',
|
|
190
|
+
`const child = spawn(${JSON.stringify(nodePath)}, [${JSON.stringify(entryFile)}, "slave", "--service-runner"], {`,
|
|
191
|
+
' cwd: home,',
|
|
192
|
+
' env: process.env,',
|
|
193
|
+
' stdio: ["ignore", log, log],',
|
|
194
|
+
' windowsHide: true',
|
|
195
|
+
'});',
|
|
196
|
+
'for (const signal of ["SIGINT", "SIGTERM"]) {',
|
|
197
|
+
' process.on(signal, () => child.kill(signal));',
|
|
198
|
+
'}',
|
|
199
|
+
'let finished = false;',
|
|
200
|
+
'function finish(code, error) {',
|
|
201
|
+
' if (finished) return;',
|
|
202
|
+
' finished = true;',
|
|
203
|
+
' if (error) require("node:fs").writeSync(log, `${error.stack || error.message || error}\\n`);',
|
|
204
|
+
' closeSync(log);',
|
|
205
|
+
' process.exitCode = Number.isInteger(code) ? code : 1;',
|
|
206
|
+
'}',
|
|
207
|
+
'child.once("error", (error) => finish(1, error));',
|
|
208
|
+
'child.once("close", (code) => finish(code));',
|
|
209
|
+
''
|
|
210
|
+
].join("\r\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function buildSlaveWindowsTaskXml({ account, launcherFile, nodePath = process.execPath }) {
|
|
214
|
+
if (!account?.user) throw new Error("Slave Windows task requires a service account");
|
|
215
|
+
if (typeof launcherFile !== "string" || !path.isAbsolute(launcherFile)) throw new Error("Slave Windows task requires an absolute launcher file");
|
|
216
|
+
const taskArguments = `"${launcherFile}"`;
|
|
217
|
+
return [
|
|
218
|
+
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
219
|
+
'<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
220
|
+
' <RegistrationInfo>',
|
|
221
|
+
` <Author>${escapeXml(account.user)}</Author>`,
|
|
222
|
+
' <Description>Arisa Slave headless host</Description>',
|
|
223
|
+
' </RegistrationInfo>',
|
|
224
|
+
' <Triggers>',
|
|
225
|
+
' <LogonTrigger>',
|
|
226
|
+
' <Enabled>true</Enabled>',
|
|
227
|
+
` <UserId>${escapeXml(account.user)}</UserId>`,
|
|
228
|
+
' </LogonTrigger>',
|
|
229
|
+
' </Triggers>',
|
|
230
|
+
' <Principals>',
|
|
231
|
+
' <Principal id="Author">',
|
|
232
|
+
` <UserId>${escapeXml(account.user)}</UserId>`,
|
|
233
|
+
' <LogonType>InteractiveToken</LogonType>',
|
|
234
|
+
' <RunLevel>LeastPrivilege</RunLevel>',
|
|
235
|
+
' </Principal>',
|
|
236
|
+
' </Principals>',
|
|
237
|
+
' <Settings>',
|
|
238
|
+
' <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>',
|
|
239
|
+
' <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>',
|
|
240
|
+
' <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>',
|
|
241
|
+
' <StartWhenAvailable>true</StartWhenAvailable>',
|
|
242
|
+
' <Enabled>true</Enabled>',
|
|
243
|
+
' <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>',
|
|
244
|
+
' <RestartOnFailure>',
|
|
245
|
+
' <Interval>PT1M</Interval>',
|
|
246
|
+
' <Count>999</Count>',
|
|
247
|
+
' </RestartOnFailure>',
|
|
248
|
+
' </Settings>',
|
|
249
|
+
' <Actions Context="Author">',
|
|
250
|
+
' <Exec>',
|
|
251
|
+
` <Command>${escapeXml(nodePath)}</Command>`,
|
|
252
|
+
` <Arguments>${escapeXml(taskArguments)}</Arguments>`,
|
|
253
|
+
` <WorkingDirectory>${escapeXml(path.dirname(launcherFile))}</WorkingDirectory>`,
|
|
254
|
+
' </Exec>',
|
|
255
|
+
' </Actions>',
|
|
256
|
+
'</Task>',
|
|
257
|
+
''
|
|
258
|
+
].join("\r\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
98
261
|
export function runCommand(command, args, { cwd, env = process.env } = {}) {
|
|
99
262
|
return new Promise((resolve, reject) => {
|
|
100
263
|
const child = spawn(command, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -155,6 +318,70 @@ export async function installSlaveSystemdService({
|
|
|
155
318
|
return { unitFile, serviceName: slaveServiceName, account, paths };
|
|
156
319
|
}
|
|
157
320
|
|
|
321
|
+
export async function installSlaveLaunchdService({
|
|
322
|
+
account,
|
|
323
|
+
slaveHome,
|
|
324
|
+
entryFile,
|
|
325
|
+
execute = runCommand,
|
|
326
|
+
environment = process.env,
|
|
327
|
+
platform = process.platform,
|
|
328
|
+
uid = process.getuid?.(),
|
|
329
|
+
systemUnitDir = "/Library/LaunchDaemons",
|
|
330
|
+
userUnitDir = path.join(os.homedir(), "Library", "LaunchAgents")
|
|
331
|
+
}) {
|
|
332
|
+
if (platform !== "darwin") throw new Error("Arisa Slave launchd installation requires macOS");
|
|
333
|
+
if (account.scope === "user" && !Number.isSafeInteger(uid)) throw new Error("Cannot determine the macOS user id for launchd");
|
|
334
|
+
const paths = getSlavePaths(slaveHome);
|
|
335
|
+
await mkdir(paths.state, { recursive: true, mode: 0o700 });
|
|
336
|
+
const unitDir = account.scope === "system" ? systemUnitDir : userUnitDir;
|
|
337
|
+
await mkdir(unitDir, { recursive: true });
|
|
338
|
+
const unitFile = path.join(unitDir, `${slaveLaunchdLabel}.plist`);
|
|
339
|
+
await writeFile(unitFile, buildSlaveLaunchdPlist({ account, slaveHome: paths.home, entryFile, environment }), { mode: 0o644 });
|
|
340
|
+
const domain = account.scope === "system" ? "system" : `gui/${uid}`;
|
|
341
|
+
const serviceTarget = `${domain}/${slaveLaunchdLabel}`;
|
|
342
|
+
await execute("launchctl", ["bootout", serviceTarget], { env: environment }).catch(() => {});
|
|
343
|
+
await execute("launchctl", ["bootstrap", domain, unitFile], { env: environment });
|
|
344
|
+
await execute("launchctl", ["enable", serviceTarget], { env: environment });
|
|
345
|
+
await execute("launchctl", ["kickstart", "-k", serviceTarget], { env: environment });
|
|
346
|
+
return { unitFile, serviceName: slaveLaunchdLabel, serviceManager: "launchd", serviceTarget, account, paths };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function installSlaveWindowsService({
|
|
350
|
+
account,
|
|
351
|
+
slaveHome,
|
|
352
|
+
entryFile,
|
|
353
|
+
execute = runCommand,
|
|
354
|
+
platform = process.platform
|
|
355
|
+
}) {
|
|
356
|
+
if (platform !== "win32") throw new Error("Arisa Slave Task Scheduler installation requires Windows");
|
|
357
|
+
const paths = getSlavePaths(slaveHome);
|
|
358
|
+
await mkdir(paths.state, { recursive: true, mode: 0o700 });
|
|
359
|
+
const launcherFile = path.join(paths.state, "arisa-slave-launcher.cjs");
|
|
360
|
+
const unitFile = path.join(paths.state, "arisa-slave-task.xml");
|
|
361
|
+
await writeFile(launcherFile, `\uFEFF${buildSlaveWindowsLauncher({ slaveHome: paths.home, entryFile })}`, { encoding: "utf8", mode: 0o600 });
|
|
362
|
+
await writeFile(unitFile, `\uFEFF${buildSlaveWindowsTaskXml({ account, launcherFile })}`, { encoding: "utf16le", mode: 0o600 });
|
|
363
|
+
await execute("schtasks.exe", ["/End", "/TN", slaveWindowsTaskName]).catch(() => {});
|
|
364
|
+
await execute("schtasks.exe", ["/Create", "/TN", slaveWindowsTaskName, "/XML", unitFile, "/F"]);
|
|
365
|
+
await execute("schtasks.exe", ["/Run", "/TN", slaveWindowsTaskName]);
|
|
366
|
+
return {
|
|
367
|
+
unitFile,
|
|
368
|
+
launcherFile,
|
|
369
|
+
serviceName: slaveWindowsTaskName,
|
|
370
|
+
serviceManager: "windows-task",
|
|
371
|
+
serviceTarget: slaveWindowsTaskName,
|
|
372
|
+
account,
|
|
373
|
+
paths
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function installSlaveService(options = {}) {
|
|
378
|
+
const platform = options.platform || process.platform;
|
|
379
|
+
if (platform === "linux") return installSlaveSystemdService({ ...options, platform });
|
|
380
|
+
if (platform === "darwin") return installSlaveLaunchdService({ ...options, platform });
|
|
381
|
+
if (platform === "win32") return installSlaveWindowsService({ ...options, platform });
|
|
382
|
+
throw new Error(`Arisa Slave service installation is not supported on ${platform}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
158
385
|
export async function writeSlaveServiceDescriptor(paths, descriptor) {
|
|
159
386
|
await mkdir(paths.state, { recursive: true, mode: 0o700 });
|
|
160
387
|
await writeFile(paths.descriptorFile, `${JSON.stringify(descriptor, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -169,20 +396,85 @@ export async function readSlaveServiceDescriptor(paths) {
|
|
|
169
396
|
}
|
|
170
397
|
}
|
|
171
398
|
|
|
172
|
-
|
|
173
|
-
const descriptor = await readSlaveServiceDescriptor(paths);
|
|
399
|
+
async function controlSystemdService(descriptor, operation, execute) {
|
|
174
400
|
const prefix = descriptor.account?.scope === "user" ? ["--user"] : [];
|
|
175
401
|
if (operation === "status") {
|
|
176
402
|
try {
|
|
177
403
|
const result = await execute("systemctl", [...prefix, "is-active", slaveServiceName]);
|
|
178
|
-
return { running: result.stdout.trim() === "active", status: result.stdout.trim() };
|
|
404
|
+
return { running: result.stdout.trim() === "active", status: result.stdout.trim(), serviceManager: "systemd" };
|
|
179
405
|
} catch {
|
|
180
|
-
return { running: false, status: "inactive" };
|
|
406
|
+
return { running: false, status: "inactive", serviceManager: "systemd" };
|
|
181
407
|
}
|
|
182
408
|
}
|
|
183
|
-
if (!["start", "stop", "restart"].includes(operation)) throw new Error(`Unsupported Slave service operation: ${operation}`);
|
|
184
409
|
await execute("systemctl", [...prefix, operation, slaveServiceName]);
|
|
185
|
-
return { ok: true, operation };
|
|
410
|
+
return { ok: true, operation, serviceManager: "systemd" };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function launchdIsLoaded(serviceTarget, execute) {
|
|
414
|
+
try {
|
|
415
|
+
await execute("launchctl", ["print", serviceTarget]);
|
|
416
|
+
return true;
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function controlLaunchdService(descriptor, operation, execute) {
|
|
423
|
+
const serviceTarget = descriptor.serviceTarget;
|
|
424
|
+
const unitFile = descriptor.unitFile;
|
|
425
|
+
const domain = serviceTarget?.slice(0, serviceTarget.lastIndexOf("/"));
|
|
426
|
+
if (!serviceTarget || !unitFile || !domain) throw new Error("Arisa Slave launchd descriptor is incomplete");
|
|
427
|
+
const loaded = await launchdIsLoaded(serviceTarget, execute);
|
|
428
|
+
if (operation === "status") {
|
|
429
|
+
return { running: loaded, status: loaded ? "active" : "inactive", serviceManager: "launchd" };
|
|
430
|
+
}
|
|
431
|
+
if (operation === "stop") {
|
|
432
|
+
if (loaded) await execute("launchctl", ["bootout", serviceTarget]);
|
|
433
|
+
return { ok: true, operation, serviceManager: "launchd" };
|
|
434
|
+
}
|
|
435
|
+
if (!loaded) await execute("launchctl", ["bootstrap", domain, unitFile]);
|
|
436
|
+
await execute("launchctl", ["enable", serviceTarget]);
|
|
437
|
+
await execute("launchctl", ["kickstart", ...(operation === "restart" ? ["-k"] : []), serviceTarget]);
|
|
438
|
+
return { ok: true, operation, serviceManager: "launchd" };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function registeredSlavePidIsAlive(paths) {
|
|
442
|
+
try {
|
|
443
|
+
const pid = Number.parseInt((await readFile(paths.pidFile, "utf8")).trim(), 10);
|
|
444
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
445
|
+
process.kill(pid, 0);
|
|
446
|
+
return true;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function controlWindowsTask(paths, descriptor, operation, execute) {
|
|
453
|
+
const taskName = descriptor.serviceTarget || slaveWindowsTaskName;
|
|
454
|
+
if (operation === "status") {
|
|
455
|
+
try {
|
|
456
|
+
await execute("schtasks.exe", ["/Query", "/TN", taskName]);
|
|
457
|
+
const running = await registeredSlavePidIsAlive(paths);
|
|
458
|
+
return { running, status: running ? "active" : "inactive", serviceManager: "windows-task" };
|
|
459
|
+
} catch {
|
|
460
|
+
return { running: false, status: "not-installed", serviceManager: "windows-task" };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (operation === "stop" || operation === "restart") {
|
|
464
|
+
await execute("schtasks.exe", ["/End", "/TN", taskName]).catch(() => {});
|
|
465
|
+
}
|
|
466
|
+
if (operation === "start" || operation === "restart") {
|
|
467
|
+
await execute("schtasks.exe", ["/Run", "/TN", taskName]);
|
|
468
|
+
}
|
|
469
|
+
return { ok: true, operation, serviceManager: "windows-task" };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function controlSlaveService(paths, operation, { execute = runCommand } = {}) {
|
|
473
|
+
if (!["start", "stop", "restart", "status"].includes(operation)) throw new Error(`Unsupported Slave service operation: ${operation}`);
|
|
474
|
+
const descriptor = await readSlaveServiceDescriptor(paths);
|
|
475
|
+
if (descriptor.serviceManager === "launchd") return controlLaunchdService(descriptor, operation, execute);
|
|
476
|
+
if (descriptor.serviceManager === "windows-task") return controlWindowsTask(paths, descriptor, operation, execute);
|
|
477
|
+
return controlSystemdService(descriptor, operation, execute);
|
|
186
478
|
}
|
|
187
479
|
|
|
188
480
|
export async function registerSlaveServiceProcess(paths) {
|
package/src/runtime/tui.js
CHANGED
|
@@ -126,8 +126,8 @@ function requiresProviderAuth(model) {
|
|
|
126
126
|
export async function createArisaTuiRuntime({ config, client, logger } = {}) {
|
|
127
127
|
const chatId = resolveTuiChatId(config);
|
|
128
128
|
const policy = buildPiToolPolicy({ config, customToolNames: tuiToolNames });
|
|
129
|
-
const piRuntime = createPiRuntime({ provider: config.pi.provider, apiKey: config.pi.apiKey });
|
|
130
|
-
const model = piRuntime.
|
|
129
|
+
const piRuntime = await createPiRuntime({ provider: config.pi.provider, apiKey: config.pi.apiKey });
|
|
130
|
+
const model = piRuntime.getModel(config.pi.provider, config.pi.model);
|
|
131
131
|
if (!model) throw new Error(`Model not found: ${config.pi.provider}/${config.pi.model}`);
|
|
132
132
|
if (requiresProviderAuth(model) && !config.pi.apiKey && !hasProviderAuth(config.pi.provider, piRuntime)) {
|
|
133
133
|
throw new Error(`No auth found for ${config.pi.provider}. Complete Arisa bootstrap first.`);
|
|
@@ -141,8 +141,7 @@ export async function createArisaTuiRuntime({ config, client, logger } = {}) {
|
|
|
141
141
|
const services = await createAgentSessionServices({
|
|
142
142
|
cwd,
|
|
143
143
|
agentDir: arisaHomeDir,
|
|
144
|
-
|
|
145
|
-
modelRegistry: piRuntime.modelRegistry,
|
|
144
|
+
modelRuntime: piRuntime,
|
|
146
145
|
settingsManager,
|
|
147
146
|
resourceLoaderOptions: {
|
|
148
147
|
agentsFilesOverride: (current) => appendArisaAgentsFile(current, arisaAgentsContent)
|
|
@@ -158,8 +157,8 @@ export async function createArisaTuiRuntime({ config, client, logger } = {}) {
|
|
|
158
157
|
excludeTools: policy.excludeTools,
|
|
159
158
|
customTools
|
|
160
159
|
});
|
|
161
|
-
const speedController = createModelSpeedController(created.session.agent.
|
|
162
|
-
created.session.agent.
|
|
160
|
+
const speedController = createModelSpeedController(created.session.agent.streamFunction, config.pi.speed);
|
|
161
|
+
created.session.agent.streamFunction = speedController.streamFn;
|
|
163
162
|
logger?.log("tui", `opened ${created.session.sessionFile || "in-memory session"} for owner scope ${chatId}`);
|
|
164
163
|
return { ...created, services, diagnostics: services.diagnostics };
|
|
165
164
|
};
|
|
@@ -335,7 +335,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
335
335
|
timer.unref?.();
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
async function enqueueAsyncPrompt({ chatId, prompt, label, route: taskRoute, timeoutMs, priority = "background", queueTtlMs = undefined }) {
|
|
338
|
+
async function enqueueAsyncPrompt({ chatId, prompt, label, route: taskRoute, timeoutMs, priority = "background", queueTtlMs = undefined, agentTaskExecution = null }) {
|
|
339
339
|
let ctx = { chat: { id: chatId }, api: bot.api };
|
|
340
340
|
const destination = taskRoute?.transport === "telegram" ? taskRoute.destination : null;
|
|
341
341
|
if (destination?.chatId && destination?.threadId) {
|
|
@@ -349,7 +349,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
349
349
|
if (!route.ok) throw new Error("Scheduled owner-workspace destination is unavailable.");
|
|
350
350
|
registerRoute(ctx, route);
|
|
351
351
|
}
|
|
352
|
-
const route =
|
|
352
|
+
const route = {
|
|
353
|
+
...contextRoute(ctx),
|
|
354
|
+
...(agentTaskExecution ? { agentTaskExecution } : {})
|
|
355
|
+
};
|
|
356
|
+
registerRoute(ctx, route);
|
|
353
357
|
const chatState = getChatState(route.sessionId);
|
|
354
358
|
if (chatState.processing) await ensureQueuedTelegramTyping(chatState, ctx);
|
|
355
359
|
let timer = null;
|
|
@@ -409,7 +413,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
409
413
|
const auth = await authorizeContext(ctx);
|
|
410
414
|
if (!auth.ok) return;
|
|
411
415
|
if (authController.getIssue()) {
|
|
412
|
-
await ctx.reply(authController.buildBlockedMessage(ctx.chat.id));
|
|
416
|
+
await ctx.reply(await authController.buildBlockedMessage(ctx.chat.id));
|
|
413
417
|
return;
|
|
414
418
|
}
|
|
415
419
|
await handleNewCommand(ctx);
|
|
@@ -536,7 +540,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
536
540
|
if (await authController.submitRenewalInput(ctx)) return;
|
|
537
541
|
|
|
538
542
|
if (authController.getIssue()) {
|
|
539
|
-
await ctx.reply(authController.buildBlockedMessage(ctx.chat.id));
|
|
543
|
+
await ctx.reply(await authController.buildBlockedMessage(ctx.chat.id));
|
|
540
544
|
return;
|
|
541
545
|
}
|
|
542
546
|
|
|
@@ -548,7 +552,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
548
552
|
if (wasPromptErrorNotified(error)) return;
|
|
549
553
|
const issue = getPiAuthIssue(error);
|
|
550
554
|
await ctx.reply(issue
|
|
551
|
-
? buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue })
|
|
555
|
+
? await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue })
|
|
552
556
|
: getErrorMessage(error));
|
|
553
557
|
});
|
|
554
558
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getErrorMessage } from "../../core/agent/auth-flow.js";
|
|
2
2
|
import { resolveChatModel, resolveChatSpeed, resolveChatThinkingLevel } from "../../core/agent/model-selection.js";
|
|
3
3
|
import { clampModelThinkingLevel, listModelThinkingLevels, modelSupportsThinking } from "../../core/agent/pi-runtime.js";
|
|
4
|
-
import { modelSupportsSpeed } from "../../core/agent/model-speed.js";
|
|
4
|
+
import { clampModelSpeed, modelSupportsSpeed } from "../../core/agent/model-speed.js";
|
|
5
5
|
import { parseEffortPickerAction, parseModelPickerAction, parseSpeedPickerAction } from "./model-picker.js";
|
|
6
6
|
|
|
7
7
|
export async function closeModelPicker(ctx, { messageText, callbackText }) {
|
|
@@ -181,9 +181,10 @@ export function createTelegramModelCallbackHandler({
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
if (!modelSupportsSpeed(model)) {
|
|
184
|
-
await ctx.answerCallbackQuery({ text: "This model does not support
|
|
184
|
+
await ctx.answerCallbackQuery({ text: "This model does not support fast mode.", show_alert: true });
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
|
+
action.speed = clampModelSpeed(model, action.speed);
|
|
187
188
|
const currentSpeed = resolveChatSpeed(config, modelChatId);
|
|
188
189
|
if (action.speed === currentSpeed) {
|
|
189
190
|
await closeModelPicker(ctx, {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
selectChatThinkingLevel
|
|
9
9
|
} from "../../core/agent/model-selection.js";
|
|
10
10
|
import { clampModelThinkingLevel, createPiRuntime, listModelThinkingLevels, listProviderModels, modelSupportsThinking } from "../../core/agent/pi-runtime.js";
|
|
11
|
-
import { clampModelSpeed,
|
|
11
|
+
import { clampModelSpeed, listModelSpeeds, modelSupportsSpeed } from "../../core/agent/model-speed.js";
|
|
12
12
|
import { buildEffortPicker, buildModelPicker, buildSpeedPicker, reverseModelOrder } from "./model-picker.js";
|
|
13
13
|
|
|
14
14
|
function chatKey(chatId) {
|
|
@@ -40,7 +40,7 @@ function restorePreviousSelection(agentConfig, key, hadSelections, previousSelec
|
|
|
40
40
|
|
|
41
41
|
export function createTelegramModelControls({ config, saveConfig, agentManager, contextRoute }) {
|
|
42
42
|
async function getProviderModels() {
|
|
43
|
-
const runtime = createPiRuntime({
|
|
43
|
+
const runtime = await createPiRuntime({
|
|
44
44
|
provider: config.pi.provider,
|
|
45
45
|
apiKey: config.pi.apiKey
|
|
46
46
|
});
|
|
@@ -89,12 +89,12 @@ export function createTelegramModelControls({ config, saveConfig, agentManager,
|
|
|
89
89
|
const model = models.find((item) => item.id === resolveChatModel(config, route.sessionId));
|
|
90
90
|
if (!model) throw new Error(`Model not found for provider ${agentConfig.provider}`);
|
|
91
91
|
if (!modelSupportsSpeed(model)) {
|
|
92
|
-
return editOrReplyText(ctx, `${model.provider}/${model.id} does not support
|
|
92
|
+
return editOrReplyText(ctx, `${model.provider}/${model.id} does not support fast mode.`);
|
|
93
93
|
}
|
|
94
94
|
const picker = buildSpeedPicker({
|
|
95
95
|
provider: model.provider,
|
|
96
96
|
modelId: model.id,
|
|
97
|
-
speeds:
|
|
97
|
+
speeds: listModelSpeeds(model),
|
|
98
98
|
selectedSpeed: resolveChatSpeed(config, route.sessionId)
|
|
99
99
|
});
|
|
100
100
|
return editOrReplyPicker(ctx, picker);
|
|
@@ -37,7 +37,7 @@ export function parseEffortPickerAction(data) {
|
|
|
37
37
|
|
|
38
38
|
export function parseSpeedPickerAction(data) {
|
|
39
39
|
if (data === "noop:page") return { type: "noop", value: null };
|
|
40
|
-
const speed = /^speed:(1(?:\.5)
|
|
40
|
+
const speed = /^speed:(1(?:\.5)?|2)$/.exec(String(data || ""));
|
|
41
41
|
return speed ? { type: "speed", speed: Number(speed[1]) } : null;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -41,7 +41,7 @@ function buildFailureNotice({ task, result, error }) {
|
|
|
41
41
|
if (result?.authBlockedNew === true) {
|
|
42
42
|
return [
|
|
43
43
|
"⚠️ Arisa automation paused for authentication",
|
|
44
|
-
`Tool: ${task.payload?.toolName || "unknown"}`,
|
|
44
|
+
`Tool: ${result.authBlock?.toolName || task.payload?.toolName || "unknown"}`,
|
|
45
45
|
`Reason: ${safeErrorSummary(error)}`,
|
|
46
46
|
`Next authentication check: ${result.runAt}`
|
|
47
47
|
].join("\n");
|
|
@@ -72,17 +72,62 @@ export function createTelegramTaskDispatcher({
|
|
|
72
72
|
const agentTimeoutMs = boundedTimeout(taskTimeouts.agentTimeoutMs, 15 * 60_000);
|
|
73
73
|
const eventTimeoutMs = boundedTimeout(taskTimeouts.eventTimeoutMs, 5 * 60_000);
|
|
74
74
|
|
|
75
|
+
const runBackgroundTool = (toolName, chatId, args, label) => agentManager.runTurn({
|
|
76
|
+
priority: "background",
|
|
77
|
+
label
|
|
78
|
+
}, () => agentManager.runTool({ name: toolName, request: { args }, chatId }));
|
|
79
|
+
|
|
80
|
+
function throwToolFailure(result, toolName, fallbackResolution) {
|
|
81
|
+
const error = new Error(result?.error || `${toolName} failed`);
|
|
82
|
+
if (result?.status === "blocked_auth" || fallbackResolution) {
|
|
83
|
+
error.retryable = false;
|
|
84
|
+
error.authBlocked = true;
|
|
85
|
+
error.authResolution = {
|
|
86
|
+
...(result?.resolution || fallbackResolution || {}),
|
|
87
|
+
toolName
|
|
88
|
+
};
|
|
89
|
+
} else if (result?.status === "needs_config") {
|
|
90
|
+
error.retryable = false;
|
|
91
|
+
} else if (result?.status === "outcome_uncertain") {
|
|
92
|
+
error.retryable = false;
|
|
93
|
+
error.outcomeUncertain = true;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
async function dispatchAgentTask(task, chatId) {
|
|
76
99
|
if (!task.payload.prompt) throw new NonRetryableTaskError("agent_task missing prompt");
|
|
100
|
+
if (task.authBlock?.toolName) {
|
|
101
|
+
const probe = await runBackgroundTool(
|
|
102
|
+
task.authBlock.toolName,
|
|
103
|
+
chatId,
|
|
104
|
+
task.authBlock.probeArgs || {},
|
|
105
|
+
`authentication probe ${task.authBlock.toolName}`
|
|
106
|
+
);
|
|
107
|
+
if (probe?.ok === false) throwToolFailure(probe, task.authBlock.toolName, task.authBlock);
|
|
108
|
+
logger?.log("tasks", `authentication restored for ${task.authBlock.toolName} (task ${task.id})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
logger?.log("tasks", `running task ${task.id} for chat ${chatId}`);
|
|
112
|
+
const agentTaskExecution = { blockedAuth: null };
|
|
78
113
|
await enqueueAsyncPrompt({
|
|
79
114
|
chatId,
|
|
80
115
|
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, resourceNotes, logger }),
|
|
81
116
|
label: `scheduled task ${task.id}`,
|
|
82
117
|
route: task.route,
|
|
83
118
|
timeoutMs: agentTimeoutMs,
|
|
84
|
-
priority: task.source?.toolName === "whatsapp-web" ? "interactive" : "background"
|
|
119
|
+
priority: task.source?.toolName === "whatsapp-web" ? "interactive" : "background",
|
|
120
|
+
agentTaskExecution
|
|
85
121
|
});
|
|
122
|
+
if (agentTaskExecution.blockedAuth) {
|
|
123
|
+
const blocked = agentTaskExecution.blockedAuth;
|
|
124
|
+
throwToolFailure({
|
|
125
|
+
ok: false,
|
|
126
|
+
status: "blocked_auth",
|
|
127
|
+
error: blocked.error,
|
|
128
|
+
resolution: blocked.resolution
|
|
129
|
+
}, blocked.toolName);
|
|
130
|
+
}
|
|
86
131
|
}
|
|
87
132
|
|
|
88
133
|
async function dispatchAgentEvent(task, chatId) {
|
|
@@ -111,34 +156,16 @@ export function createTelegramTaskDispatcher({
|
|
|
111
156
|
if (!toolName) throw new NonRetryableTaskError("poll_tool missing toolName");
|
|
112
157
|
logger?.log("tasks", `polling tool ${toolName} (task ${task.id}) for chat ${chatId}`);
|
|
113
158
|
|
|
114
|
-
const runTool = (args) =>
|
|
115
|
-
priority: "background",
|
|
116
|
-
label: `poll tool ${toolName}`
|
|
117
|
-
}, () => agentManager.runTool({ name: toolName, request: { args }, chatId }));
|
|
118
|
-
|
|
119
|
-
function throwFailure(result, fallbackResolution) {
|
|
120
|
-
const error = new Error(result?.error || `poll_tool ${toolName} failed`);
|
|
121
|
-
if (result?.status === "blocked_auth" || fallbackResolution) {
|
|
122
|
-
error.retryable = false;
|
|
123
|
-
error.authBlocked = true;
|
|
124
|
-
error.authResolution = result?.resolution || fallbackResolution;
|
|
125
|
-
} else if (result?.status === "needs_config") {
|
|
126
|
-
error.retryable = false;
|
|
127
|
-
} else if (result?.status === "outcome_uncertain") {
|
|
128
|
-
error.retryable = false;
|
|
129
|
-
error.outcomeUncertain = true;
|
|
130
|
-
}
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
159
|
+
const runTool = (args) => runBackgroundTool(toolName, chatId, args, `poll tool ${toolName}`);
|
|
133
160
|
|
|
134
161
|
if (task.authBlock) {
|
|
135
162
|
const probe = await runTool(task.authBlock.probeArgs || {});
|
|
136
|
-
if (probe?.ok === false)
|
|
163
|
+
if (probe?.ok === false) throwToolFailure(probe, toolName, task.authBlock);
|
|
137
164
|
logger?.log("tasks", `authentication restored for ${toolName} (task ${task.id})`);
|
|
138
165
|
}
|
|
139
166
|
|
|
140
167
|
const result = await runTool(task.payload.args || {});
|
|
141
|
-
if (result?.ok === false)
|
|
168
|
+
if (result?.ok === false) throwToolFailure(result, toolName);
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
async function dispatchTask(task) {
|
|
@@ -50,7 +50,7 @@ export function createTelegramAuthController({
|
|
|
50
50
|
if (!detected) return false;
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
|
-
await api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue: detected }));
|
|
53
|
+
await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, issue: detected }));
|
|
54
54
|
markPromptErrorNotified(error);
|
|
55
55
|
return true;
|
|
56
56
|
} catch (notifyError) {
|
|
@@ -66,11 +66,11 @@ export function createTelegramAuthController({
|
|
|
66
66
|
agentManager.clearSessionCache(chatId);
|
|
67
67
|
issue = null;
|
|
68
68
|
logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
|
|
69
|
-
await api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, verified: true }));
|
|
69
|
+
await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, verified: true }));
|
|
70
70
|
} catch (error) {
|
|
71
71
|
const detected = rememberValidationFailure(error);
|
|
72
72
|
logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
73
|
-
await api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue: detected })).catch((notifyError) => {
|
|
73
|
+
await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, issue: detected })).catch((notifyError) => {
|
|
74
74
|
logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
|
|
75
75
|
});
|
|
76
76
|
} finally {
|
|
@@ -140,17 +140,17 @@ export function createTelegramAuthController({
|
|
|
140
140
|
const authorization = await authorize(ctx);
|
|
141
141
|
if (!authorization.ok) return;
|
|
142
142
|
|
|
143
|
-
const status = getPiAuthStatus(config, ctx.chat.id);
|
|
143
|
+
const status = await getPiAuthStatus(config, ctx.chat.id);
|
|
144
144
|
if (status.hasApiKey || !status.supportsOAuth) {
|
|
145
145
|
await withTyping(ctx, async () => {
|
|
146
146
|
try {
|
|
147
147
|
await agentManager.validateAgent();
|
|
148
148
|
agentManager.clearSessionCache(ctx.chat.id);
|
|
149
149
|
issue = null;
|
|
150
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
|
|
150
|
+
await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
|
|
151
151
|
} catch (error) {
|
|
152
152
|
const detected = rememberValidationFailure(error);
|
|
153
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
153
|
+
await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
156
|
return;
|
|
@@ -163,7 +163,7 @@ export function createTelegramAuthController({
|
|
|
163
163
|
: "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
|
|
164
164
|
} catch (error) {
|
|
165
165
|
const detected = rememberValidationFailure(error);
|
|
166
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
166
|
+
await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|