arisa 5.1.68 → 5.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +46 -6
- package/src/core/agent/agent-session-lifecycle.js +80 -3
- package/src/core/agent/core-tools.js +1 -1
- package/src/core/agent/pi-auth-login.js +1 -1
- package/src/core/agent/pi-runtime.js +1 -1
- package/src/core/agent/runtime-context.js +1 -1
- package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
- package/src/core/artifacts/artifact-store.js +1 -1
- package/src/core/capabilities/capability-service.js +1 -1
- package/src/core/config/config-defaults.js +30 -1
- package/src/core/config/config-store.js +1 -1
- package/src/core/conversation/session-seed-store.js +1 -1
- package/src/core/tasks/task-store.js +1 -1
- package/src/core/tools/daemon-client.js +180 -0
- package/src/core/tools/daemon-processes.js +19 -3
- package/src/core/tools/daemon-protocol.js +72 -0
- package/src/core/tools/daemon-runtime.js +13 -490
- package/src/core/tools/daemon-worker.js +310 -0
- package/src/core/tools/ipc-client.js +2 -2
- package/src/core/tools/memory-pressure.js +56 -0
- package/src/core/tools/official-tool-installer.js +1 -1
- package/src/core/tools/tool-config.js +1 -1
- package/src/core/tools/tool-process-output.js +100 -0
- package/src/core/tools/tool-process-runner.js +175 -0
- package/src/core/tools/tool-registry.js +99 -187
- package/src/core/tools/tool-resource-note-store.js +1 -1
- package/src/core/tools/tool-usage-store.js +1 -1
- package/src/core/tools/weighted-resource-governor.js +188 -38
- package/src/index.js +14 -2
- package/src/official-tools.lock.json +424 -50
- package/src/platform/paths.js +152 -0
- package/src/runtime/bootstrap-cli.js +121 -0
- package/src/runtime/bootstrap-config.js +97 -0
- package/src/runtime/bootstrap-telegram.js +325 -0
- package/src/runtime/bootstrap.js +6 -543
- package/src/runtime/doctor.js +6 -3
- package/src/runtime/flush.js +1 -1
- package/src/runtime/ipc/ipc-server.js +1 -1
- package/src/runtime/log-viewer.js +1 -1
- package/src/runtime/oom-protection.js +20 -0
- package/src/runtime/paths.js +3 -151
- package/src/runtime/restart-receipt.js +1 -1
- package/src/runtime/service-manager.js +1 -1
- package/src/runtime/service-supervisor.js +14 -0
- package/src/runtime/slave-cli.js +1 -1
- package/src/runtime/tool-process-supervisor.js +1 -1
- package/src/runtime/tui.js +200 -0
- package/src/runtime/update-manager.js +1 -1
- package/src/runtime/worker-recovery-report.js +142 -0
- package/src/transport/telegram/bot.js +42 -320
- package/src/transport/telegram/prompt-builders.js +8 -3
- package/src/transport/telegram/telegram-prompt-controller.js +346 -0
- package/src/transport/telegram/workspace-topic-store.js +1 -1
- package/test/agent-session-lifecycle.test.js +92 -0
- package/test/architecture-boundaries.test.js +29 -0
- package/test/bootstrap.test.js +65 -0
- package/test/daemon-process-invocation.test.js +27 -0
- package/test/daemon-runtime.test.js +36 -1
- package/test/doctor.test.js +22 -0
- package/test/memory-pressure.test.js +36 -0
- package/test/model-selection.test.js +11 -1
- package/test/official-tool-dependencies.test.js +1 -1
- package/test/official-tool-installer.test.js +18 -1
- package/test/oom-protection.test.js +32 -0
- package/test/paths.test.js +7 -0
- package/test/pi-compaction.test.js +9 -0
- package/test/service-manager.test.js +6 -1
- package/test/telegram-prompt-controller.test.js +81 -0
- package/test/telegram-text-artifact.test.js +30 -0
- package/test/tool-registry-run.test.js +108 -4
- package/test/tui.test.js +41 -0
- package/test/weighted-resource-governor.test.js +97 -5
- package/test/worker-heap-circuit-breaker.test.js +79 -0
- package/test/worker-recovery-report.test.js +69 -0
- package/test-fixtures/fake-daemon.js +5 -0
package/src/runtime/paths.js
CHANGED
|
@@ -1,151 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
export const arisaPackageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
-
export const arisaHomeDir = process.env.ARISA_HOME
|
|
9
|
-
? path.resolve(process.env.ARISA_HOME)
|
|
10
|
-
: path.join(os.homedir(), ".arisa");
|
|
11
|
-
export const stateDir = path.join(arisaHomeDir, "state");
|
|
12
|
-
export const configFile = path.join(stateDir, "config.json");
|
|
13
|
-
export const piAuthFile = path.join(stateDir, "pi-auth.json");
|
|
14
|
-
export const servicePidFile = path.join(stateDir, "arisa.pid");
|
|
15
|
-
export const serviceLogFile = path.join(stateDir, "arisa.log");
|
|
16
|
-
export const restartReceiptFile = path.join(stateDir, "restart-receipt.json");
|
|
17
|
-
export const sessionStartOperationalNotesFile = path.join(stateDir, "session-start-operational-notes.json");
|
|
18
|
-
export function createIpcSocketPath({ homeDir = arisaHomeDir, platform = process.platform } = {}) {
|
|
19
|
-
if (platform === "win32") {
|
|
20
|
-
const suffix = crypto.createHash("sha256").update(homeDir).digest("hex").slice(0, 16);
|
|
21
|
-
return `\\\\.\\pipe\\arisa-${suffix}`;
|
|
22
|
-
}
|
|
23
|
-
return path.join(homeDir, "state", "arisa.sock");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const arisaIpcSocketFile = createIpcSocketPath();
|
|
27
|
-
export const tasksFile = path.join(stateDir, "tasks.json");
|
|
28
|
-
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
29
|
-
export const chatsDir = path.join(arisaHomeDir, "chats");
|
|
30
|
-
export const toolStateDir = path.join(stateDir, "tools");
|
|
31
|
-
|
|
32
|
-
export function requireToolName(toolName) {
|
|
33
|
-
const name = String(toolName ?? "");
|
|
34
|
-
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
|
35
|
-
throw new Error(`Invalid tool name: ${name || "empty"}`);
|
|
36
|
-
}
|
|
37
|
-
return name;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getChatDir(chatId) {
|
|
41
|
-
return path.join(chatsDir, String(chatId));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function getChatArtifactsDir(chatId) {
|
|
45
|
-
return path.join(getChatDir(chatId), "artifacts");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function getChatArtifactsIndexFile(chatId) {
|
|
49
|
-
return path.join(getChatDir(chatId), "state", "artifacts.json");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function getChatSessionSeedFile(chatId) {
|
|
53
|
-
return path.join(getChatDir(chatId), "state", "session-seed.jsonl");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getChatLegacyConversationHistoryFile(chatId) {
|
|
57
|
-
return path.join(getChatDir(chatId), "state", "conversation.jsonl");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function getChatToolUsageFile(chatId) {
|
|
61
|
-
return path.join(getChatDir(chatId), "state", "tool-usage.json");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getChatToolResourceNotesFile(chatId) {
|
|
65
|
-
return path.join(getChatDir(chatId), "state", "tool-resource-notes.json");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function getChatTelegramWorkspacesFile(chatId) {
|
|
69
|
-
return path.join(getChatDir(chatId), "state", "telegram-workspaces.json");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function getChatToolStateDir(chatId, toolName) {
|
|
73
|
-
return path.join(getChatDir(chatId), "state", "tools", requireToolName(toolName));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function normalizeDaemonScope(scope = { type: "global" }) {
|
|
77
|
-
if (!scope || scope === "global" || scope.type === "global") {
|
|
78
|
-
return { type: "global" };
|
|
79
|
-
}
|
|
80
|
-
if (scope === "chat") {
|
|
81
|
-
throw new Error("chat daemon scope requires chatId");
|
|
82
|
-
}
|
|
83
|
-
if (scope.type !== "chat") {
|
|
84
|
-
throw new Error(`Unsupported daemon scope: ${scope.type || scope}`);
|
|
85
|
-
}
|
|
86
|
-
const chatId = String(scope.chatId ?? "").trim();
|
|
87
|
-
if (!/^-?\d+$/.test(chatId)) {
|
|
88
|
-
throw new Error(`Invalid chat daemon scope: ${chatId || "missing chatId"}`);
|
|
89
|
-
}
|
|
90
|
-
return { type: "chat", chatId };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function getDaemonInstanceId(scope = { type: "global" }) {
|
|
94
|
-
const normalized = normalizeDaemonScope(scope);
|
|
95
|
-
return normalized.type === "global" ? "global" : `chat:${normalized.chatId}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getDaemonInstanceDir(toolName, scope = { type: "global" }) {
|
|
99
|
-
const normalized = normalizeDaemonScope(scope);
|
|
100
|
-
return normalized.type === "global"
|
|
101
|
-
? getToolStateDir(toolName)
|
|
102
|
-
: path.join(getChatToolStateDir(normalized.chatId, toolName), "daemon");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function getChatPiSessionsDir(chatId, sessionRevision = 0) {
|
|
106
|
-
if (!Number.isSafeInteger(sessionRevision) || sessionRevision < 0) {
|
|
107
|
-
throw new Error(`Invalid Pi session revision: ${sessionRevision}`);
|
|
108
|
-
}
|
|
109
|
-
const sessionsDir = path.join(getChatDir(chatId), "state", "pi-sessions");
|
|
110
|
-
return sessionRevision === 0
|
|
111
|
-
? sessionsDir
|
|
112
|
-
: path.join(sessionsDir, String(sessionRevision));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function getToolDir(toolName) {
|
|
116
|
-
return path.join(toolsDir, requireToolName(toolName));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getToolConfigPath(toolName) {
|
|
120
|
-
return path.join(getToolDir(toolName), "config.js");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function getChatConfigDir(chatId) {
|
|
124
|
-
return path.join(getChatDir(chatId), "config");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function getChatTmpDir(chatId) {
|
|
128
|
-
return path.join(getChatDir(chatId), "tmp");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function getChatToolConfigPath(chatId, toolName) {
|
|
132
|
-
return path.join(getChatConfigDir(chatId), "tools", requireToolName(toolName), "config.js");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function getToolStateDir(toolName) {
|
|
136
|
-
return path.join(toolStateDir, requireToolName(toolName));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function getToolTmpDir(toolName) {
|
|
140
|
-
return path.join(getToolStateDir(toolName), "tmp");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function getChatToolTmpDir(chatId, toolName) {
|
|
144
|
-
return path.join(getChatTmpDir(chatId), "tools", toolName);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function ensureArisaHome() {
|
|
148
|
-
await mkdir(stateDir, { recursive: true });
|
|
149
|
-
await mkdir(toolsDir, { recursive: true });
|
|
150
|
-
await mkdir(chatsDir, { recursive: true });
|
|
151
|
-
}
|
|
1
|
+
// Public compatibility facade for tools and integrations.
|
|
2
|
+
// Core and runtime internals import the neutral platform implementation directly.
|
|
3
|
+
export * from "../platform/paths.js";
|
|
@@ -3,7 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { arisaPackageDir, restartReceiptFile } from "
|
|
6
|
+
import { arisaPackageDir, restartReceiptFile } from "../platform/paths.js";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
@@ -2,7 +2,7 @@ import { open, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { ensureArisaHome, serviceLogFile, servicePidFile } from "
|
|
5
|
+
import { ensureArisaHome, serviceLogFile, servicePidFile } from "../platform/paths.js";
|
|
6
6
|
|
|
7
7
|
export const serviceEntryFile = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
8
8
|
|
|
@@ -22,6 +22,7 @@ export function createServiceSupervisor({
|
|
|
22
22
|
restartBackoffMaxMs = 60_000,
|
|
23
23
|
stableRuntimeMs = 60_000,
|
|
24
24
|
logger,
|
|
25
|
+
onUnexpectedExit = null,
|
|
25
26
|
spawnProcess = spawn,
|
|
26
27
|
wait = sleep
|
|
27
28
|
}) {
|
|
@@ -65,6 +66,19 @@ export function createServiceSupervisor({
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const delay = Math.min(maximumBackoffMs, initialBackoffMs * (2 ** (consecutiveFailures - 1)));
|
|
69
|
+
try {
|
|
70
|
+
await onUnexpectedExit?.({
|
|
71
|
+
occurredAt: new Date().toISOString(),
|
|
72
|
+
runtimeMs,
|
|
73
|
+
restartDelayMs: delay,
|
|
74
|
+
consecutiveFailures,
|
|
75
|
+
code: outcome.code ?? null,
|
|
76
|
+
signal: outcome.signal || null,
|
|
77
|
+
detail
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger?.error("service", `worker recovery report failed: ${errorMessage(error)}`);
|
|
81
|
+
}
|
|
68
82
|
logger?.log("service", `restarting worker in ${delay}ms`);
|
|
69
83
|
await Promise.race([
|
|
70
84
|
wait(delay),
|
package/src/runtime/slave-cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { applyConfigDefaults } from "../core/config/config-defaults.js";
|
|
5
5
|
import { installLockedOfficialTool } from "../core/tools/official-tool-installer.js";
|
|
6
6
|
import { createHeadlessApp } from "./create-headless-app.js";
|
|
7
|
-
import { arisaPackageDir } from "
|
|
7
|
+
import { arisaPackageDir } from "../platform/paths.js";
|
|
8
8
|
import { readPackageVersion, readRecentLogLines, followLogFile } from "./log-viewer.js";
|
|
9
9
|
import { parseSlaveBootstrapUrl } from "./slave-bootstrap-url.js";
|
|
10
10
|
import { withSecureRequestFile } from "./secure-request-file.js";
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
writeDaemonStatus
|
|
9
9
|
} from "../core/tools/daemon-processes.js";
|
|
10
10
|
import { loadDaemonPolicy } from "../core/tools/daemon-policy.js";
|
|
11
|
-
import { ensureArisaHome } from "
|
|
11
|
+
import { ensureArisaHome } from "../platform/paths.js";
|
|
12
12
|
|
|
13
13
|
async function fileExists(file) {
|
|
14
14
|
try {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
createAgentSessionFromServices,
|
|
4
|
+
createAgentSessionRuntime,
|
|
5
|
+
createAgentSessionServices,
|
|
6
|
+
defineTool,
|
|
7
|
+
InteractiveMode,
|
|
8
|
+
SessionManager
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import { loadConfig } from "../core/config/config-store.js";
|
|
12
|
+
import { buildPiToolPolicy } from "../core/agent/core-tools.js";
|
|
13
|
+
import { createModelSpeedController } from "../core/agent/model-speed.js";
|
|
14
|
+
import { clampModelThinkingLevel, createPiRuntime, hasProviderAuth } from "../core/agent/pi-runtime.js";
|
|
15
|
+
import { appendArisaAgentsFile, arisaAgentsFile } from "../core/agent/runtime-context.js";
|
|
16
|
+
import { createPiSettingsManager } from "../core/agent/agent-manager.js";
|
|
17
|
+
import { createArisaClient } from "../core/tools/ipc-client.js";
|
|
18
|
+
import { arisaHomeDir, tuiSessionsDir } from "../platform/paths.js";
|
|
19
|
+
|
|
20
|
+
const tuiToolNames = Object.freeze([
|
|
21
|
+
"list_tools",
|
|
22
|
+
"tool_help",
|
|
23
|
+
"tool_skills",
|
|
24
|
+
"set_tool_config",
|
|
25
|
+
"run_tool",
|
|
26
|
+
"list_scheduled_tasks",
|
|
27
|
+
"cancel_scheduled_task",
|
|
28
|
+
"cancel_all_scheduled_tasks"
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function jsonResult(result) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
34
|
+
details: result
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveTuiChatId(config) {
|
|
39
|
+
const chatId = config?.telegram?.authorizedChatIds?.[0];
|
|
40
|
+
if (chatId == null || !String(chatId).trim()) {
|
|
41
|
+
throw new Error("arisa tui requires at least one authorized chat for scoped tools and artifacts");
|
|
42
|
+
}
|
|
43
|
+
return chatId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createTuiCapabilityTools(client) {
|
|
47
|
+
return [
|
|
48
|
+
defineTool({
|
|
49
|
+
name: "list_tools",
|
|
50
|
+
label: "List tools",
|
|
51
|
+
description: "List or search Arisa modular tools by capability.",
|
|
52
|
+
parameters: Type.Object({ query: Type.Optional(Type.String()) }),
|
|
53
|
+
execute: async (_id, params) => jsonResult(await client.tools.list(params))
|
|
54
|
+
}),
|
|
55
|
+
defineTool({
|
|
56
|
+
name: "tool_help",
|
|
57
|
+
label: "Tool help",
|
|
58
|
+
description: "Show the CLI help for an Arisa modular tool.",
|
|
59
|
+
parameters: Type.Object({ name: Type.String() }),
|
|
60
|
+
execute: async (_id, params) => jsonResult(await client.tools.help(params))
|
|
61
|
+
}),
|
|
62
|
+
defineTool({
|
|
63
|
+
name: "tool_skills",
|
|
64
|
+
label: "Tool skills",
|
|
65
|
+
description: "Show skill hints assigned to an Arisa modular tool.",
|
|
66
|
+
parameters: Type.Object({ name: Type.String() }),
|
|
67
|
+
execute: async (_id, params) => jsonResult(await client.tools.skills(params))
|
|
68
|
+
}),
|
|
69
|
+
defineTool({
|
|
70
|
+
name: "set_tool_config",
|
|
71
|
+
label: "Set tool config",
|
|
72
|
+
description: "Write an Arisa tool config value in the owner chat scope.",
|
|
73
|
+
parameters: Type.Object({ name: Type.String(), field: Type.String(), value: Type.String() }),
|
|
74
|
+
execute: async (_id, params) => jsonResult(await client.tools.setConfig(params))
|
|
75
|
+
}),
|
|
76
|
+
defineTool({
|
|
77
|
+
name: "run_tool",
|
|
78
|
+
label: "Run tool",
|
|
79
|
+
description: "Run an Arisa modular tool. Generated files remain owner-scoped artifacts; Telegram delivery is unavailable from TUI mode.",
|
|
80
|
+
parameters: Type.Object({
|
|
81
|
+
name: Type.String(),
|
|
82
|
+
artifactId: Type.Optional(Type.String()),
|
|
83
|
+
text: Type.Optional(Type.String()),
|
|
84
|
+
resourceId: Type.Optional(Type.String()),
|
|
85
|
+
args: Type.Optional(Type.Record(Type.String(), Type.String()))
|
|
86
|
+
}),
|
|
87
|
+
execute: async (_id, params) => jsonResult(await client.tools.run(params, { timeoutMs: 900_000 }))
|
|
88
|
+
}),
|
|
89
|
+
defineTool({
|
|
90
|
+
name: "list_scheduled_tasks",
|
|
91
|
+
label: "List scheduled tasks",
|
|
92
|
+
description: "List owner-scoped Arisa tasks.",
|
|
93
|
+
parameters: Type.Object({
|
|
94
|
+
status: Type.Optional(Type.String()),
|
|
95
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 }))
|
|
96
|
+
}),
|
|
97
|
+
execute: async (_id, params) => jsonResult(await client.tasks.list(params))
|
|
98
|
+
}),
|
|
99
|
+
defineTool({
|
|
100
|
+
name: "cancel_scheduled_task",
|
|
101
|
+
label: "Cancel scheduled task",
|
|
102
|
+
description: "Cancel one owner-scoped Arisa task.",
|
|
103
|
+
parameters: Type.Object({ id: Type.String() }),
|
|
104
|
+
execute: async (_id, params) => jsonResult(await client.tasks.cancel(params))
|
|
105
|
+
}),
|
|
106
|
+
defineTool({
|
|
107
|
+
name: "cancel_all_scheduled_tasks",
|
|
108
|
+
label: "Cancel all scheduled tasks",
|
|
109
|
+
description: "Cancel all pending or running owner-scoped Arisa tasks.",
|
|
110
|
+
parameters: Type.Object({}),
|
|
111
|
+
execute: async () => jsonResult(await client.tasks.cancelAll())
|
|
112
|
+
})
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function requiresProviderAuth(model) {
|
|
117
|
+
if (typeof model?.baseUrl !== "string") return true;
|
|
118
|
+
try {
|
|
119
|
+
const hostname = new URL(model.baseUrl).hostname;
|
|
120
|
+
return hostname !== "127.0.0.1" && hostname !== "localhost";
|
|
121
|
+
} catch {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function createArisaTuiRuntime({ config, client, logger } = {}) {
|
|
127
|
+
const chatId = resolveTuiChatId(config);
|
|
128
|
+
const policy = buildPiToolPolicy({ config, customToolNames: tuiToolNames });
|
|
129
|
+
const piRuntime = createPiRuntime({ provider: config.pi.provider, apiKey: config.pi.apiKey });
|
|
130
|
+
const model = piRuntime.modelRegistry.find(config.pi.provider, config.pi.model);
|
|
131
|
+
if (!model) throw new Error(`Model not found: ${config.pi.provider}/${config.pi.model}`);
|
|
132
|
+
if (requiresProviderAuth(model) && !config.pi.apiKey && !hasProviderAuth(config.pi.provider, piRuntime)) {
|
|
133
|
+
throw new Error(`No auth found for ${config.pi.provider}. Complete Arisa bootstrap first.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await mkdir(tuiSessionsDir, { recursive: true });
|
|
137
|
+
const arisaAgentsContent = await readFile(arisaAgentsFile, "utf8");
|
|
138
|
+
const customTools = createTuiCapabilityTools(client);
|
|
139
|
+
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
140
|
+
const settingsManager = createPiSettingsManager(config);
|
|
141
|
+
const services = await createAgentSessionServices({
|
|
142
|
+
cwd,
|
|
143
|
+
agentDir: arisaHomeDir,
|
|
144
|
+
authStorage: piRuntime.authStorage,
|
|
145
|
+
modelRegistry: piRuntime.modelRegistry,
|
|
146
|
+
settingsManager,
|
|
147
|
+
resourceLoaderOptions: {
|
|
148
|
+
agentsFilesOverride: (current) => appendArisaAgentsFile(current, arisaAgentsContent)
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const created = await createAgentSessionFromServices({
|
|
152
|
+
services,
|
|
153
|
+
sessionManager,
|
|
154
|
+
sessionStartEvent,
|
|
155
|
+
model,
|
|
156
|
+
thinkingLevel: clampModelThinkingLevel(model, config.pi.thinkingLevel),
|
|
157
|
+
tools: policy.tools,
|
|
158
|
+
excludeTools: policy.excludeTools,
|
|
159
|
+
customTools
|
|
160
|
+
});
|
|
161
|
+
const speedController = createModelSpeedController(created.session.agent.streamFn, config.pi.speed);
|
|
162
|
+
created.session.agent.streamFn = speedController.streamFn;
|
|
163
|
+
logger?.log("tui", `opened ${created.session.sessionFile || "in-memory session"} for owner scope ${chatId}`);
|
|
164
|
+
return { ...created, services, diagnostics: services.diagnostics };
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return createAgentSessionRuntime(createRuntime, {
|
|
168
|
+
cwd: policy.workspaceDir,
|
|
169
|
+
agentDir: arisaHomeDir,
|
|
170
|
+
sessionManager: SessionManager.create(policy.workspaceDir, tuiSessionsDir)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function runTui({ logger } = {}) {
|
|
175
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
176
|
+
throw new Error("arisa tui requires an interactive terminal");
|
|
177
|
+
}
|
|
178
|
+
const config = await loadConfig();
|
|
179
|
+
const chatId = resolveTuiChatId(config);
|
|
180
|
+
const client = createArisaClient({ toolName: "arisa-tui", chatId });
|
|
181
|
+
try {
|
|
182
|
+
await client.tools.list();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new Error(`arisa tui requires the Arisa background service: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const runtime = await createArisaTuiRuntime({ config, client, logger });
|
|
188
|
+
const mode = new InteractiveMode(runtime, {
|
|
189
|
+
migratedProviders: [],
|
|
190
|
+
modelFallbackMessage: runtime.modelFallbackMessage,
|
|
191
|
+
initialImages: [],
|
|
192
|
+
initialMessages: [],
|
|
193
|
+
verbose: false
|
|
194
|
+
});
|
|
195
|
+
try {
|
|
196
|
+
await mode.run();
|
|
197
|
+
} finally {
|
|
198
|
+
await runtime.dispose();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { access, cp, mkdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { arisaPackageDir, getToolDir } from "
|
|
5
|
+
import { arisaPackageDir, getToolDir } from "../platform/paths.js";
|
|
6
6
|
import { renderTextReport, reportRow, wrapReportText } from "./report-format.js";
|
|
7
7
|
|
|
8
8
|
const defaultRepoUrl = "https://github.com/clasen/Arisa.git";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readRecentLogLines } from "./log-viewer.js";
|
|
5
|
+
import { arisaPackageDir, serviceLogFile, stateDir, tasksFile } from "../platform/paths.js";
|
|
6
|
+
|
|
7
|
+
export const workerRecoveryReportFile = path.join(stateDir, "worker-recovery-report.json");
|
|
8
|
+
|
|
9
|
+
function boundedText(value, maximum = 300) {
|
|
10
|
+
return String(value || "").trim().slice(0, maximum);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function recordUnexpectedWorkerExit(input, { reportFile = workerRecoveryReportFile } = {}) {
|
|
14
|
+
const report = {
|
|
15
|
+
id: crypto.randomUUID(),
|
|
16
|
+
occurredAt: input.occurredAt || new Date().toISOString(),
|
|
17
|
+
runtimeMs: Math.max(0, Number(input.runtimeMs || 0)),
|
|
18
|
+
restartDelayMs: Math.max(0, Number(input.restartDelayMs || 0)),
|
|
19
|
+
consecutiveFailures: Math.max(1, Number(input.consecutiveFailures || 1)),
|
|
20
|
+
code: input.code == null ? null : boundedText(input.code, 40),
|
|
21
|
+
signal: input.signal == null ? null : boundedText(input.signal, 40),
|
|
22
|
+
detail: boundedText(input.detail)
|
|
23
|
+
};
|
|
24
|
+
await mkdir(path.dirname(reportFile), { recursive: true, mode: 0o700 });
|
|
25
|
+
const temporary = `${reportFile}.${process.pid}.tmp`;
|
|
26
|
+
await writeFile(temporary, `${JSON.stringify(report, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
27
|
+
await rename(temporary, reportFile);
|
|
28
|
+
return report;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function logTimestamp(line) {
|
|
32
|
+
const match = String(line).match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
|
|
33
|
+
if (!match) return null;
|
|
34
|
+
const value = new Date(match[1].replace(" ", "T")).getTime();
|
|
35
|
+
return Number.isFinite(value) ? value : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function relevantLogLines(lines, report) {
|
|
39
|
+
const occurredAt = new Date(report.occurredAt).getTime();
|
|
40
|
+
if (!Number.isFinite(occurredAt)) return lines.slice(-300);
|
|
41
|
+
const earliest = occurredAt - 2 * 60_000;
|
|
42
|
+
const latest = occurredAt + 5_000;
|
|
43
|
+
let start = -1;
|
|
44
|
+
let end = -1;
|
|
45
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
46
|
+
const timestamp = logTimestamp(lines[index]);
|
|
47
|
+
if (timestamp == null) continue;
|
|
48
|
+
if (start === -1 && timestamp >= earliest) start = index;
|
|
49
|
+
if (timestamp <= latest) end = index;
|
|
50
|
+
}
|
|
51
|
+
if (start === -1 || end < start) return lines.slice(-300);
|
|
52
|
+
return lines.slice(start, end + 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function summarizeRecoveryEvidence(lines, report) {
|
|
56
|
+
const relevant = relevantLogLines(lines, report);
|
|
57
|
+
const outOfMemory = relevant.some((line) => /heap limit|heap out of memory|allocation failed.*memory/i.test(line));
|
|
58
|
+
const tools = new Map();
|
|
59
|
+
for (const line of relevant) {
|
|
60
|
+
const match = line.match(/\[agent\] run_tool ([a-z0-9-]+)/i);
|
|
61
|
+
if (match) tools.set(match[1], (tools.get(match[1]) || 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
cause: outOfMemory
|
|
65
|
+
? "JavaScript heap out of memory"
|
|
66
|
+
: report.signal
|
|
67
|
+
? `worker terminated by ${report.signal}`
|
|
68
|
+
: `worker exited with code ${report.code ?? "unknown"}`,
|
|
69
|
+
tools: [...tools.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function interruptedTaskCount(report, file = tasksFile) {
|
|
74
|
+
try {
|
|
75
|
+
const document = JSON.parse(await readFile(file, "utf8"));
|
|
76
|
+
const tasks = Array.isArray(document) ? document : document.tasks || [];
|
|
77
|
+
const occurredAt = new Date(report.occurredAt).getTime();
|
|
78
|
+
return tasks.filter((task) => {
|
|
79
|
+
const updatedAt = new Date(task.updatedAt || 0).getTime();
|
|
80
|
+
return task.lastOutcome === "outcome_uncertain"
|
|
81
|
+
&& /interrupted before confirmation/i.test(task.lastError || "")
|
|
82
|
+
&& Number.isFinite(occurredAt)
|
|
83
|
+
&& Math.abs(updatedAt - occurredAt) <= 2 * 60_000;
|
|
84
|
+
}).length;
|
|
85
|
+
} catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runtimeVersion() {
|
|
91
|
+
try {
|
|
92
|
+
return String(JSON.parse(await readFile(path.join(arisaPackageDir, "package.json"), "utf8")).version || "unknown");
|
|
93
|
+
} catch {
|
|
94
|
+
return "unknown";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function loadWorkerRecoveryReport({
|
|
99
|
+
reportFile = workerRecoveryReportFile,
|
|
100
|
+
logFile = serviceLogFile,
|
|
101
|
+
taskFile = tasksFile,
|
|
102
|
+
readLines = readRecentLogLines,
|
|
103
|
+
getVersion = runtimeVersion
|
|
104
|
+
} = {}) {
|
|
105
|
+
let report;
|
|
106
|
+
try {
|
|
107
|
+
report = JSON.parse(await readFile(reportFile, "utf8"));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error?.code === "ENOENT") return null;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
const snapshot = await readLines(logFile, 1_000).catch(() => []);
|
|
113
|
+
const lines = Array.isArray(snapshot) ? snapshot : String(snapshot?.text || "").split(/\r?\n/);
|
|
114
|
+
const evidence = summarizeRecoveryEvidence(lines, report);
|
|
115
|
+
const uncertain = await interruptedTaskCount(report, taskFile);
|
|
116
|
+
const version = await getVersion();
|
|
117
|
+
const context = evidence.tools.length
|
|
118
|
+
? `Recent tool activity: ${evidence.tools.map(([name, count]) => `${name} ×${count}`).join(", ")}.`
|
|
119
|
+
: null;
|
|
120
|
+
return {
|
|
121
|
+
report,
|
|
122
|
+
text: [
|
|
123
|
+
"Arisa recovered automatically after an unexpected worker exit.",
|
|
124
|
+
`Cause: ${evidence.cause}.`,
|
|
125
|
+
context,
|
|
126
|
+
uncertain ? `${uncertain} scheduled execution${uncertain === 1 ? " was" : "s were"} marked outcome-uncertain and not replayed.` : null,
|
|
127
|
+
`Recovery: worker restarted after ${Math.round(report.restartDelayMs / 100) / 10}s; Arisa ${version} is running.`
|
|
128
|
+
].filter(Boolean).join("\n")
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function consumeWorkerRecoveryReport(reportId, { reportFile = workerRecoveryReportFile } = {}) {
|
|
133
|
+
try {
|
|
134
|
+
const report = JSON.parse(await readFile(reportFile, "utf8"));
|
|
135
|
+
if (report.id !== reportId) return false;
|
|
136
|
+
await rm(reportFile, { force: true });
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error?.code === "ENOENT") return false;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|