arisa 5.1.68 → 5.2.17

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.
Files changed (97) hide show
  1. package/README.md +28 -6
  2. package/package.json +1 -1
  3. package/src/core/agent/agent-manager.js +123 -8
  4. package/src/core/agent/agent-session-lifecycle.js +108 -8
  5. package/src/core/agent/agent-turn-coordinator.js +143 -0
  6. package/src/core/agent/core-tools.js +1 -1
  7. package/src/core/agent/pi-auth-login.js +1 -1
  8. package/src/core/agent/pi-capability-tools.js +7 -4
  9. package/src/core/agent/pi-runtime.js +1 -1
  10. package/src/core/agent/runtime-context.js +1 -1
  11. package/src/core/agent/session-history-reader.js +168 -0
  12. package/src/core/agent/session-preload-migration.js +162 -0
  13. package/src/core/agent/session-rotation.js +29 -0
  14. package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
  15. package/src/core/agent/worker-tool-fanout.js +117 -0
  16. package/src/core/artifacts/artifact-store.js +1 -1
  17. package/src/core/capabilities/capability-service.js +4 -3
  18. package/src/core/config/config-defaults.js +59 -1
  19. package/src/core/config/config-store.js +1 -1
  20. package/src/core/conversation/session-seed-store.js +1 -1
  21. package/src/core/tasks/task-runner.js +8 -4
  22. package/src/core/tasks/task-store.js +48 -5
  23. package/src/core/tools/daemon-client.js +180 -0
  24. package/src/core/tools/daemon-processes.js +26 -3
  25. package/src/core/tools/daemon-protocol.js +72 -0
  26. package/src/core/tools/daemon-runtime.js +13 -490
  27. package/src/core/tools/daemon-worker.js +310 -0
  28. package/src/core/tools/ipc-client.js +2 -2
  29. package/src/core/tools/memory-pressure.js +56 -0
  30. package/src/core/tools/official-tool-installer.js +1 -1
  31. package/src/core/tools/tool-config.js +1 -1
  32. package/src/core/tools/tool-process-output.js +100 -0
  33. package/src/core/tools/tool-process-runner.js +175 -0
  34. package/src/core/tools/tool-registry.js +131 -191
  35. package/src/core/tools/tool-resource-note-store.js +1 -1
  36. package/src/core/tools/tool-usage-store.js +1 -1
  37. package/src/core/tools/weighted-resource-governor.js +189 -38
  38. package/src/index.js +14 -2
  39. package/src/official-tools.lock.json +526 -78
  40. package/src/platform/paths.js +152 -0
  41. package/src/runtime/bootstrap-cli.js +121 -0
  42. package/src/runtime/bootstrap-config.js +97 -0
  43. package/src/runtime/bootstrap-telegram.js +325 -0
  44. package/src/runtime/bootstrap.js +6 -543
  45. package/src/runtime/doctor.js +26 -76
  46. package/src/runtime/flush.js +1 -1
  47. package/src/runtime/ipc/ipc-server.js +1 -1
  48. package/src/runtime/log-viewer.js +1 -1
  49. package/src/runtime/obsolete-daemon-reaper.js +43 -0
  50. package/src/runtime/oom-protection.js +20 -0
  51. package/src/runtime/paths.js +3 -151
  52. package/src/runtime/process-inspection.js +78 -0
  53. package/src/runtime/restart-receipt.js +1 -1
  54. package/src/runtime/service-manager.js +1 -1
  55. package/src/runtime/service-supervisor.js +14 -0
  56. package/src/runtime/slave-cli.js +1 -1
  57. package/src/runtime/tool-process-supervisor.js +36 -9
  58. package/src/runtime/tui.js +200 -0
  59. package/src/runtime/update-manager.js +1 -1
  60. package/src/runtime/worker-recovery-report.js +142 -0
  61. package/src/transport/telegram/bot.js +45 -321
  62. package/src/transport/telegram/chat-queue.js +6 -2
  63. package/src/transport/telegram/prompt-builders.js +8 -3
  64. package/src/transport/telegram/task-dispatcher.js +36 -11
  65. package/src/transport/telegram/telegram-prompt-controller.js +359 -0
  66. package/src/transport/telegram/workspace-topic-store.js +1 -1
  67. package/test/agent-session-lifecycle.test.js +92 -0
  68. package/test/agent-turn-coordinator.test.js +45 -0
  69. package/test/architecture-boundaries.test.js +29 -0
  70. package/test/bootstrap.test.js +65 -0
  71. package/test/context-and-task-bounds.test.js +2 -1
  72. package/test/daemon-process-invocation.test.js +27 -0
  73. package/test/daemon-runtime.test.js +38 -5
  74. package/test/doctor.test.js +41 -0
  75. package/test/memory-pressure.test.js +41 -0
  76. package/test/model-selection.test.js +11 -1
  77. package/test/obsolete-daemon-reaper.test.js +61 -0
  78. package/test/official-tool-dependencies.test.js +7 -2
  79. package/test/official-tool-installer.test.js +18 -1
  80. package/test/oom-protection.test.js +32 -0
  81. package/test/paths.test.js +7 -0
  82. package/test/pi-compaction.test.js +30 -0
  83. package/test/service-manager.test.js +6 -1
  84. package/test/session-history-reader.test.js +84 -0
  85. package/test/session-preload-migration.test.js +120 -0
  86. package/test/session-rotation.test.js +110 -0
  87. package/test/task-store.test.js +32 -0
  88. package/test/telegram-prompt-controller.test.js +82 -0
  89. package/test/telegram-task-dispatcher.test.js +66 -5
  90. package/test/telegram-text-artifact.test.js +30 -0
  91. package/test/tool-registry-run.test.js +117 -4
  92. package/test/tui.test.js +41 -0
  93. package/test/weighted-resource-governor.test.js +125 -5
  94. package/test/worker-heap-circuit-breaker.test.js +79 -0
  95. package/test/worker-recovery-report.test.js +69 -0
  96. package/test/worker-tool-fanout.test.js +79 -0
  97. package/test-fixtures/fake-daemon.js +5 -0
@@ -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 active owner-scoped Arisa tasks, including authentication-blocked 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 "./paths.js";
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
+ }