arisa 5.1.12 → 5.1.24
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 +4 -0
- package/ARISA-MASTER-SLAVE-SPEC.md +7 -4
- package/README.md +2 -0
- package/package.json +7 -8
- package/src/core/tools/official-tool-installer.js +78 -6
- package/src/core/tools/tool-dependencies.js +99 -0
- package/src/core/tools/tool-registry.js +50 -6
- package/src/official-tools.lock.json +160 -8
- package/src/runtime/arisa-capabilities.js +12 -1
- package/src/runtime/create-app.js +1 -0
- package/src/runtime/doctor.js +71 -19
- package/src/runtime/tool-usage-report.js +25 -10
- package/src/transport/telegram/bot.js +51 -15
- package/test/capabilities-security.test.js +21 -0
- package/test/context-and-task-bounds.test.js +66 -2
- package/test/doctor.test.js +55 -0
- package/test/official-tool-dependencies.test.js +23 -0
- package/test/official-tool-installer.test.js +41 -2
- package/test/tool-dependencies.test.js +53 -0
- package/test/tool-registry-run.test.js +19 -1
- package/test/tool-usage.test.js +26 -4
|
@@ -43,6 +43,13 @@ function normalizeLimit(limit) {
|
|
|
43
43
|
return Math.min(value, 100);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function normalizeAcknowledgement(value) {
|
|
47
|
+
if (value == null || value === "") return "";
|
|
48
|
+
const acknowledgement = requireString(value, "acknowledgement").trim();
|
|
49
|
+
if (acknowledgement.length > 500) throw new Error("acknowledgement must be at most 500 characters");
|
|
50
|
+
return acknowledgement;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
export function createArisaCapabilities({
|
|
47
54
|
artifactStore,
|
|
48
55
|
taskStore,
|
|
@@ -206,7 +213,11 @@ export function createArisaCapabilities({
|
|
|
206
213
|
const resourceId = String(params.resourceId || "").trim();
|
|
207
214
|
return taskStore.add({
|
|
208
215
|
kind: "agent_event",
|
|
209
|
-
payload: {
|
|
216
|
+
payload: {
|
|
217
|
+
prompt: requireString(params.prompt, "prompt"),
|
|
218
|
+
resourceId,
|
|
219
|
+
acknowledgement: normalizeAcknowledgement(params.acknowledgement)
|
|
220
|
+
}
|
|
210
221
|
}, {
|
|
211
222
|
payload: { chatId: scopedChatId },
|
|
212
223
|
source: { type: "tool", toolName: scopedToolName, chatId: scopedChatId, resourceId }
|
|
@@ -135,6 +135,7 @@ export async function createApp({ logger, runtimeOverrides, requestRestart } = {
|
|
|
135
135
|
toolProcessSupervisor,
|
|
136
136
|
daemonPolicy: config.daemons,
|
|
137
137
|
doctorPolicy: config.doctor,
|
|
138
|
+
inspectToolDependencies: async () => toolRegistry.dependencyIssues(),
|
|
138
139
|
inspectInfrastructure: async () => {
|
|
139
140
|
const tool = toolRegistry.get("master-slave");
|
|
140
141
|
if (!tool) return null;
|
package/src/runtime/doctor.js
CHANGED
|
@@ -87,16 +87,43 @@ function daemonLabel(record) {
|
|
|
87
87
|
return `${record.toolName} (${record.instanceId || "global"})`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function
|
|
91
|
-
|
|
90
|
+
function daemonState(result) {
|
|
91
|
+
return result.diagnostic?.state || result.outcome;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function daemonStateLabel(state) {
|
|
95
|
+
return String(state || "unknown")
|
|
96
|
+
.split("-")
|
|
97
|
+
.map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : part)
|
|
98
|
+
.join(" ");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function daemonReportLines(results) {
|
|
102
|
+
const priority = ["ready", "starting", "degraded", "unhealthy", "restarting", "stopped", "failed"];
|
|
103
|
+
const groups = new Map();
|
|
92
104
|
for (const result of results) {
|
|
93
|
-
const state = result
|
|
94
|
-
|
|
105
|
+
const state = daemonState(result);
|
|
106
|
+
if (!groups.has(state)) groups.set(state, []);
|
|
107
|
+
groups.get(state).push(result);
|
|
95
108
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
const states = [...groups.keys()].sort((left, right) => {
|
|
110
|
+
const leftIndex = priority.indexOf(left);
|
|
111
|
+
const rightIndex = priority.indexOf(right);
|
|
112
|
+
if (leftIndex === -1 && rightIndex === -1) return left.localeCompare(right);
|
|
113
|
+
if (leftIndex === -1) return 1;
|
|
114
|
+
if (rightIndex === -1) return -1;
|
|
115
|
+
return leftIndex - rightIndex;
|
|
116
|
+
});
|
|
117
|
+
const lines = [];
|
|
118
|
+
for (const state of states) {
|
|
119
|
+
const group = groups.get(state);
|
|
120
|
+
lines.push(` ${daemonStateLabel(state)} (${group.length})`);
|
|
121
|
+
for (const result of group.sort((left, right) => daemonLabel(left.record).localeCompare(daemonLabel(right.record)))) {
|
|
122
|
+
const scope = result.record.scope?.type || (result.record.instanceId === "global" ? "global" : "chat");
|
|
123
|
+
lines.push(...wrapReportText(`${result.record.toolName} [${scope}]`, { firstPrefix: " - ", nextPrefix: " " }));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return lines;
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
function formatTokenCount(tokens) {
|
|
@@ -123,6 +150,24 @@ function formatUptime(seconds) {
|
|
|
123
150
|
return [days ? `${days}d` : "", hours ? `${hours}h` : "", `${minutes}m`].filter(Boolean).join(" ");
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
function masterSlaveMode(infrastructure) {
|
|
154
|
+
const parts = [infrastructure.role || "unknown", infrastructure.daemon?.state || "unknown"];
|
|
155
|
+
if (infrastructure.paired != null) parts.push(infrastructure.paired ? "paired" : "unpaired");
|
|
156
|
+
return parts.join(" · ");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function masterSlaveActivity(jobs) {
|
|
160
|
+
const active = jobs?.active;
|
|
161
|
+
const queued = jobs?.queued;
|
|
162
|
+
const failed = jobs?.failed;
|
|
163
|
+
if ([active, queued, failed].every((value) => value === 0)) return "idle";
|
|
164
|
+
return `${active ?? "?"} active · ${queued ?? "?"} queued · ${failed ?? "?"} failed`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compactEndpoint(endpoint) {
|
|
168
|
+
return String(endpoint || "not configured").replace(/^tcp:\/\//, "");
|
|
169
|
+
}
|
|
170
|
+
|
|
126
171
|
export async function inspectSystemResources({ diskPath = arisaHomeDir } = {}) {
|
|
127
172
|
const [load1, load5, load15] = os.loadavg();
|
|
128
173
|
const memoryTotal = os.totalmem();
|
|
@@ -221,22 +266,18 @@ export function formatDoctorReport(report) {
|
|
|
221
266
|
lines.push(...reportRow("Large", large));
|
|
222
267
|
lines.push(...reportRow("Ineff.", inefficient));
|
|
223
268
|
if (measured.length) lines.push(...reportRow("Max", `${Math.max(...measured.map((context) => context.percent)).toFixed(1)}%`));
|
|
224
|
-
lines.push("",
|
|
225
|
-
lines.push(...
|
|
226
|
-
if (report.daemons.length) lines.push(...reportRow("Status", daemonResultSummary(report.daemons)));
|
|
269
|
+
lines.push("", `Daemons (${report.daemons.length})`);
|
|
270
|
+
if (report.daemons.length) lines.push(...daemonReportLines(report.daemons));
|
|
227
271
|
if (report.infrastructure) {
|
|
228
272
|
lines.push("", "Master/Slave");
|
|
229
273
|
if (report.infrastructure.error) {
|
|
230
274
|
lines.push(...reportRow("Status", `unavailable: ${report.infrastructure.error}`));
|
|
231
275
|
} else {
|
|
232
|
-
lines.push(...reportRow("
|
|
233
|
-
lines.push(...reportRow("
|
|
234
|
-
lines.push(...reportRow("
|
|
235
|
-
lines.push(...reportRow("Identity", report.infrastructure.identityFingerprint || "not configured"));
|
|
236
|
-
lines.push(...reportRow("Paired", report.infrastructure.paired == null ? "n/a" : report.infrastructure.paired ? "yes" : "no"));
|
|
276
|
+
lines.push(...reportRow("Mode", masterSlaveMode(report.infrastructure)));
|
|
277
|
+
lines.push(...reportRow("Endpoint", compactEndpoint(report.infrastructure.endpoint)));
|
|
278
|
+
lines.push(...reportRow("Activity", masterSlaveActivity(report.infrastructure.jobs)));
|
|
237
279
|
lines.push(...reportRow("Tools", report.infrastructure.toolCount ?? "unknown"));
|
|
238
|
-
lines.push(...reportRow("
|
|
239
|
-
lines.push(...reportRow("Pending secrets", report.infrastructure.pendingSecrets ?? "unknown"));
|
|
280
|
+
lines.push(...reportRow("Secrets", `${report.infrastructure.pendingSecrets ?? "unknown"} pending`));
|
|
240
281
|
}
|
|
241
282
|
}
|
|
242
283
|
if (report.system) {
|
|
@@ -273,7 +314,8 @@ export async function runDoctor({
|
|
|
273
314
|
stopDaemon = stopManagedDaemon,
|
|
274
315
|
unregisterDaemon = unregisterManagedDaemon,
|
|
275
316
|
inspectResources = inspectSystemResources,
|
|
276
|
-
inspectInfrastructure = null
|
|
317
|
+
inspectInfrastructure = null,
|
|
318
|
+
inspectToolDependencies = null
|
|
277
319
|
}) {
|
|
278
320
|
assertDoctorPolicy(doctorPolicy);
|
|
279
321
|
const runtime = await agentManager.getRuntimeDiagnostic({
|
|
@@ -290,6 +332,16 @@ export async function runDoctor({
|
|
|
290
332
|
infrastructure: null
|
|
291
333
|
};
|
|
292
334
|
addContextAttention(report);
|
|
335
|
+
if (inspectToolDependencies) {
|
|
336
|
+
try {
|
|
337
|
+
for (const issue of await inspectToolDependencies()) {
|
|
338
|
+
const installed = issue.installedVersion ? `; installed ${issue.installedVersion}` : "";
|
|
339
|
+
report.attention.push(`Tool dependency ${issue.type}: ${issue.tool} requires ${issue.dependency}@${issue.range || "valid"}${installed}.`);
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
report.attention.push(`Tool dependency inspection failed: ${error?.message || error}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
293
345
|
if (inspectInfrastructure) {
|
|
294
346
|
try {
|
|
295
347
|
report.infrastructure = await inspectInfrastructure();
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { renderTextReport } from "./report-format.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!tools.length) lines.push(" (none installed)");
|
|
6
|
-
|
|
7
|
-
const sortedTools = [...tools].sort((left, right) =>
|
|
3
|
+
function sortedTools(tools) {
|
|
4
|
+
return [...tools].sort((left, right) =>
|
|
8
5
|
Number(right.count) - Number(left.count) || String(left.name).localeCompare(String(right.name))
|
|
9
6
|
);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function usageRows(tools, nameWidth, countWidth) {
|
|
10
|
+
if (!tools.length) return [" (none)"];
|
|
11
|
+
return sortedTools(tools).map((tool) => {
|
|
13
12
|
const name = String(tool.name).padEnd(nameWidth);
|
|
14
13
|
const count = String(tool.count).padStart(countWidth);
|
|
15
|
-
|
|
16
|
-
}
|
|
14
|
+
return `- ${name} ${count}`;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatToolUsageReport(tools) {
|
|
19
|
+
const official = tools.filter((tool) => tool.official);
|
|
20
|
+
const local = tools.filter((tool) => !tool.official);
|
|
21
|
+
const nameWidth = Math.max(0, ...tools.map((tool) => String(tool.name).length));
|
|
22
|
+
const countWidth = Math.max(1, ...tools.map((tool) => String(tool.count).length));
|
|
23
|
+
const lines = [
|
|
24
|
+
"Arisa tools",
|
|
25
|
+
"===========",
|
|
26
|
+
"Official",
|
|
27
|
+
...usageRows(official, nameWidth, countWidth),
|
|
28
|
+
"",
|
|
29
|
+
"Local",
|
|
30
|
+
...usageRows(local, nameWidth, countWidth)
|
|
31
|
+
];
|
|
17
32
|
return renderTextReport(lines);
|
|
18
33
|
}
|
|
@@ -354,21 +354,23 @@ function buildStartupMessage(chatMeta = {}) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
export async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
357
|
-
|
|
357
|
+
const assistantMessages = [];
|
|
358
|
+
let assistantMessage = "";
|
|
358
359
|
let assistantErrorMessage = "";
|
|
359
|
-
let shouldSeparateAssistantMessage = false;
|
|
360
360
|
let slowPromptTimer = null;
|
|
361
|
+
const finishAssistantMessage = () => {
|
|
362
|
+
if (assistantMessage && !isSilentReply(assistantMessage)) {
|
|
363
|
+
assistantMessages.push(assistantMessage);
|
|
364
|
+
}
|
|
365
|
+
assistantMessage = "";
|
|
366
|
+
};
|
|
361
367
|
const unsubscribe = session.subscribe((event) => {
|
|
362
368
|
if (event.arisaPromptScoped === false) return;
|
|
363
369
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
364
|
-
|
|
370
|
+
finishAssistantMessage();
|
|
365
371
|
}
|
|
366
372
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
367
|
-
|
|
368
|
-
text += "\n\n";
|
|
369
|
-
shouldSeparateAssistantMessage = false;
|
|
370
|
-
}
|
|
371
|
-
text += event.assistantMessageEvent.delta;
|
|
373
|
+
assistantMessage += event.assistantMessageEvent.delta;
|
|
372
374
|
}
|
|
373
375
|
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
374
376
|
if (event.message.stopReason === "error") {
|
|
@@ -377,6 +379,7 @@ export async function collectText(session, prompt, { logger, chatId, onSlowPromp
|
|
|
377
379
|
// Auto-compaction and retry can emit a transient error before a successful continuation.
|
|
378
380
|
assistantErrorMessage = "";
|
|
379
381
|
}
|
|
382
|
+
finishAssistantMessage();
|
|
380
383
|
}
|
|
381
384
|
const logMessage = sessionEventLogMessage(event);
|
|
382
385
|
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
@@ -402,7 +405,8 @@ export async function collectText(session, prompt, { logger, chatId, onSlowPromp
|
|
|
402
405
|
throw new Error(assistantErrorMessage);
|
|
403
406
|
}
|
|
404
407
|
|
|
405
|
-
|
|
408
|
+
finishAssistantMessage();
|
|
409
|
+
return assistantMessages.join("\n\n").trim();
|
|
406
410
|
}
|
|
407
411
|
|
|
408
412
|
export function isSilentReply(text) {
|
|
@@ -431,16 +435,30 @@ function sanitizeSessionHandoff(text) {
|
|
|
431
435
|
return `${sanitized.slice(0, 3997).trim()}...`;
|
|
432
436
|
}
|
|
433
437
|
|
|
434
|
-
async function
|
|
435
|
-
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
438
|
+
export async function startTelegramTyping(ctx) {
|
|
439
|
+
await ctx.api.sendChatAction(ctx.chat.id, "typing").catch(() => {});
|
|
436
440
|
const timer = setInterval(() => {
|
|
437
441
|
ctx.api.sendChatAction(ctx.chat.id, "typing").catch(() => {});
|
|
438
442
|
}, 4000);
|
|
443
|
+
return () => clearInterval(timer);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function ensureQueuedTelegramTyping(chatState, ctx) {
|
|
447
|
+
if (chatState.stopQueuedTyping) return;
|
|
448
|
+
chatState.stopQueuedTyping = await startTelegramTyping(ctx);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function stopQueuedTelegramTyping(chatState) {
|
|
452
|
+
chatState.stopQueuedTyping?.();
|
|
453
|
+
chatState.stopQueuedTyping = null;
|
|
454
|
+
}
|
|
439
455
|
|
|
456
|
+
async function withTyping(ctx, work) {
|
|
457
|
+
const stopTyping = await startTelegramTyping(ctx);
|
|
440
458
|
try {
|
|
441
459
|
return await work();
|
|
442
460
|
} finally {
|
|
443
|
-
|
|
461
|
+
stopTyping();
|
|
444
462
|
}
|
|
445
463
|
}
|
|
446
464
|
|
|
@@ -456,7 +474,8 @@ export function createChatStateStore() {
|
|
|
456
474
|
beforeNextPrompt: null,
|
|
457
475
|
activeSession: null,
|
|
458
476
|
activeSteers: [],
|
|
459
|
-
assistantMessages: new Map()
|
|
477
|
+
assistantMessages: new Map(),
|
|
478
|
+
stopQueuedTyping: null
|
|
460
479
|
};
|
|
461
480
|
states.set(String(chatId), state);
|
|
462
481
|
return state;
|
|
@@ -555,6 +574,7 @@ export async function drainChatPromptQueue({
|
|
|
555
574
|
chatState.continueAfterClose = false;
|
|
556
575
|
}
|
|
557
576
|
} finally {
|
|
577
|
+
stopQueuedTelegramTyping(chatState);
|
|
558
578
|
chatState.processing = false;
|
|
559
579
|
chatState.activeSession = null;
|
|
560
580
|
chatState.activeSteers = [];
|
|
@@ -1065,6 +1085,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1065
1085
|
const chatState = getChatState(ctx.chat.id);
|
|
1066
1086
|
|
|
1067
1087
|
if (chatState.processing) {
|
|
1088
|
+
await ensureQueuedTelegramTyping(chatState, ctx);
|
|
1068
1089
|
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
1069
1090
|
const busyMessageMode = typeof ctx.message?.text === "string"
|
|
1070
1091
|
? resolveTelegramBusyMessageMode(config, ctx.chat.id)
|
|
@@ -1111,6 +1132,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1111
1132
|
timer.unref?.();
|
|
1112
1133
|
}
|
|
1113
1134
|
|
|
1135
|
+
async function enqueueAsyncPrompt({ chatId, prompt, label }) {
|
|
1136
|
+
const ctx = { chat: { id: chatId }, api: bot.api };
|
|
1137
|
+
const chatState = getChatState(chatId);
|
|
1138
|
+
if (chatState.processing) await ensureQueuedTelegramTyping(chatState, ctx);
|
|
1139
|
+
return enqueuePrompt({ chatId, prompt, label, ctx });
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1114
1142
|
async function dispatchTask(task) {
|
|
1115
1143
|
const chatId = task.payload?.chatId;
|
|
1116
1144
|
if (!chatId) {
|
|
@@ -1124,7 +1152,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1124
1152
|
return;
|
|
1125
1153
|
}
|
|
1126
1154
|
logger?.log("tasks", `running task ${task.id} for chat ${chatId}`);
|
|
1127
|
-
await
|
|
1155
|
+
await enqueueAsyncPrompt({
|
|
1128
1156
|
chatId,
|
|
1129
1157
|
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, resourceNotes, logger }),
|
|
1130
1158
|
label: `scheduled task ${task.id}`
|
|
@@ -1135,7 +1163,15 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1135
1163
|
|
|
1136
1164
|
if (task.kind === "agent_event") {
|
|
1137
1165
|
logger?.log("tasks", `agent event ${task.id} for chat ${chatId}`);
|
|
1138
|
-
|
|
1166
|
+
const acknowledgement = String(task.payload?.acknowledgement || "").trim();
|
|
1167
|
+
if (acknowledgement) {
|
|
1168
|
+
try {
|
|
1169
|
+
await bot.api.sendMessage(chatId, acknowledgement);
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
logger?.log("telegram", `agent event acknowledgement failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
await enqueueAsyncPrompt({
|
|
1139
1175
|
chatId,
|
|
1140
1176
|
prompt: await buildAsyncEventPrompt(task, resourceNotes),
|
|
1141
1177
|
label: `agent event ${task.id}`
|
|
@@ -142,6 +142,27 @@ test("requires chatId for chat-scoped IPC methods", async () => {
|
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
test("agent events preserve a bounded immediate acknowledgement", async () => {
|
|
146
|
+
const capabilities = createCapabilities();
|
|
147
|
+
const created = await capabilities.dispatch({
|
|
148
|
+
method: "agent.enqueueEvent",
|
|
149
|
+
toolName: "browser-session-bridge",
|
|
150
|
+
chatId: "chat-a",
|
|
151
|
+
params: {
|
|
152
|
+
prompt: "Continue the pending authorization flow",
|
|
153
|
+
acknowledgement: "Authorization received. Continuing now."
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
assert.equal(created.payload.acknowledgement, "Authorization received. Continuing now.");
|
|
158
|
+
await assert.rejects(() => capabilities.dispatch({
|
|
159
|
+
method: "agent.enqueueEvent",
|
|
160
|
+
toolName: "browser-session-bridge",
|
|
161
|
+
chatId: "chat-a",
|
|
162
|
+
params: { prompt: "Continue", acknowledgement: "x".repeat(501) }
|
|
163
|
+
}), /at most 500 characters/);
|
|
164
|
+
});
|
|
165
|
+
|
|
145
166
|
test("delivers only artifacts resolved from the requesting chat", async () => {
|
|
146
167
|
const artifact = { id: "artifact-1", chatId: "chat-a", path: "/safe/chat-a/file.txt" };
|
|
147
168
|
const deliveries = [];
|
|
@@ -4,13 +4,38 @@ import {
|
|
|
4
4
|
collectText,
|
|
5
5
|
createChatStateStore,
|
|
6
6
|
drainChatPromptQueue,
|
|
7
|
+
ensureQueuedTelegramTyping,
|
|
7
8
|
isSilentReply,
|
|
8
9
|
queueChatPrompt,
|
|
9
10
|
resolveTelegramBusyMessageMode,
|
|
10
|
-
routeBusyPrompt
|
|
11
|
+
routeBusyPrompt,
|
|
12
|
+
stopQueuedTelegramTyping
|
|
11
13
|
} from "../src/transport/telegram/bot.js";
|
|
12
14
|
import { selectScheduledTasks } from "../src/core/agent/agent-manager.js";
|
|
13
15
|
|
|
16
|
+
test("queued Telegram prompts start typing immediately and share one indicator", async () => {
|
|
17
|
+
let actions = 0;
|
|
18
|
+
const chatState = { stopQueuedTyping: null };
|
|
19
|
+
const ctx = {
|
|
20
|
+
chat: { id: 879964957 },
|
|
21
|
+
api: {
|
|
22
|
+
async sendChatAction(chatId, action) {
|
|
23
|
+
assert.equal(chatId, 879964957);
|
|
24
|
+
assert.equal(action, "typing");
|
|
25
|
+
actions += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await ensureQueuedTelegramTyping(chatState, ctx);
|
|
31
|
+
await ensureQueuedTelegramTyping(chatState, ctx);
|
|
32
|
+
assert.equal(actions, 1);
|
|
33
|
+
assert.equal(typeof chatState.stopQueuedTyping, "function");
|
|
34
|
+
|
|
35
|
+
stopQueuedTelegramTyping(chatState);
|
|
36
|
+
assert.equal(chatState.stopQueuedTyping, null);
|
|
37
|
+
});
|
|
38
|
+
|
|
14
39
|
function createSession(events) {
|
|
15
40
|
const listeners = new Set();
|
|
16
41
|
return {
|
|
@@ -59,6 +84,44 @@ test("collectText preserves the final assistant error", async () => {
|
|
|
59
84
|
await assert.rejects(() => collectText(session, "hello"), /terminal failure/);
|
|
60
85
|
});
|
|
61
86
|
|
|
87
|
+
test("collectText drops a standalone silent event reply before a steered response", async () => {
|
|
88
|
+
const session = createSession([
|
|
89
|
+
{ type: "message_start", message: { role: "assistant" } },
|
|
90
|
+
{
|
|
91
|
+
type: "message_update",
|
|
92
|
+
message: { role: "assistant" },
|
|
93
|
+
assistantMessageEvent: { type: "text_delta", delta: "NO_REPLY" }
|
|
94
|
+
},
|
|
95
|
+
{ type: "message_end", message: { role: "assistant", stopReason: "stop" } },
|
|
96
|
+
{ type: "message_start", message: { role: "assistant" } },
|
|
97
|
+
{
|
|
98
|
+
type: "message_update",
|
|
99
|
+
message: { role: "assistant" },
|
|
100
|
+
assistantMessageEvent: { type: "text_delta", delta: "The useful Telegram response." }
|
|
101
|
+
},
|
|
102
|
+
{ type: "message_end", message: { role: "assistant", stopReason: "stop" } }
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
assert.equal(await collectText(session, "event followed by a Telegram steer"), "The useful Telegram response.");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("collectText preserves useful text in the same message as a silent marker", async () => {
|
|
109
|
+
const session = createSession([
|
|
110
|
+
{ type: "message_start", message: { role: "assistant" } },
|
|
111
|
+
{
|
|
112
|
+
type: "message_update",
|
|
113
|
+
message: { role: "assistant" },
|
|
114
|
+
assistantMessageEvent: { type: "text_delta", delta: "NO_REPLY\n\nThis text belongs to the same response." }
|
|
115
|
+
},
|
|
116
|
+
{ type: "message_end", message: { role: "assistant", stopReason: "stop" } }
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
assert.equal(
|
|
120
|
+
await collectText(session, "one response that mentions the marker"),
|
|
121
|
+
"NO_REPLY\n\nThis text belongs to the same response."
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
62
125
|
test("recognizes standalone silent reply markers", () => {
|
|
63
126
|
assert.equal(isSilentReply("NO_REPLY"), true);
|
|
64
127
|
assert.equal(isSilentReply("No reply needed."), true);
|
|
@@ -96,7 +159,8 @@ test("chat state uses one queue for numeric and string chat IDs", () => {
|
|
|
96
159
|
beforeNextPrompt: null,
|
|
97
160
|
activeSession: null,
|
|
98
161
|
activeSteers: [],
|
|
99
|
-
assistantMessages: new Map()
|
|
162
|
+
assistantMessages: new Map(),
|
|
163
|
+
stopQueuedTyping: null
|
|
100
164
|
});
|
|
101
165
|
});
|
|
102
166
|
|
package/test/doctor.test.js
CHANGED
|
@@ -87,6 +87,61 @@ test("reports Pi context size and retained-content inefficiency", async () => {
|
|
|
87
87
|
assert.ok(formatted.split("\n").every((line) => [...line].length <= 35));
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("lists each checked daemon with its scope and state", async () => {
|
|
91
|
+
const { report } = await run({
|
|
92
|
+
repairs: [
|
|
93
|
+
{
|
|
94
|
+
record: { toolName: "master-slave", instanceId: "global", scope: { type: "global" } },
|
|
95
|
+
diagnostic: { state: "ready" },
|
|
96
|
+
outcome: "healthy"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
record: { toolName: "context-vault", instanceId: "chat-1", scope: { type: "chat" } },
|
|
100
|
+
diagnostic: { state: "stopped" },
|
|
101
|
+
outcome: "stopped"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
report.infrastructure = {
|
|
107
|
+
role: "master",
|
|
108
|
+
daemon: { state: "ready" },
|
|
109
|
+
endpoint: "tcp://198.74.61.48:4719",
|
|
110
|
+
paired: null,
|
|
111
|
+
identityFingerprint: "unnecessarily-long-fingerprint",
|
|
112
|
+
toolCount: 0,
|
|
113
|
+
jobs: { active: 0, queued: 0, failed: 0 },
|
|
114
|
+
pendingSecrets: 0
|
|
115
|
+
};
|
|
116
|
+
const formatted = formatDoctorReport(report);
|
|
117
|
+
assert.match(formatted, /Daemons \(2\)\n Ready \(1\)\n - master-slave \[global\]/);
|
|
118
|
+
assert.match(formatted, / Stopped \(1\)\n - context-vault \[chat\]/);
|
|
119
|
+
assert.match(formatted, /Master\/Slave\n Mode master · ready/);
|
|
120
|
+
assert.match(formatted, /Endpoint 198\.74\.61\.48:4719/);
|
|
121
|
+
assert.match(formatted, /Activity idle/);
|
|
122
|
+
assert.doesNotMatch(formatted, /Identity|fingerprint|Paired/);
|
|
123
|
+
assert.ok(formatted.split("\n").every((line) => [...line].length <= 35));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("reports missing tool dependencies as attention items", async () => {
|
|
127
|
+
const report = await runDoctor({
|
|
128
|
+
agentManager: { getRuntimeDiagnostic: async () => runtime() },
|
|
129
|
+
toolProcessSupervisor: { repair: async () => [] },
|
|
130
|
+
daemonPolicy,
|
|
131
|
+
doctorPolicy,
|
|
132
|
+
listProcesses: async () => [],
|
|
133
|
+
serviceStatus: async () => ({ running: false }),
|
|
134
|
+
inspectResources: async () => system,
|
|
135
|
+
inspectToolDependencies: async () => [{
|
|
136
|
+
tool: "magnific-mcp",
|
|
137
|
+
type: "missing",
|
|
138
|
+
dependency: "mcp-client",
|
|
139
|
+
range: "^0.1.0"
|
|
140
|
+
}]
|
|
141
|
+
});
|
|
142
|
+
assert.match(report.attention.join("\n"), /magnific-mcp requires mcp-client@\^0\.1\.0/);
|
|
143
|
+
});
|
|
144
|
+
|
|
90
145
|
test("stops only a registered duplicate Arisa service with verified identity", async () => {
|
|
91
146
|
const duplicatePid = 321;
|
|
92
147
|
const { report, stopped } = await run({
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
async function manifest(name) {
|
|
6
|
+
return JSON.parse(await readFile(new URL(`../../tools/${name}/tool.manifest.json`, import.meta.url), "utf8"));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test("official orchestrators declare their hard tool dependencies", async () => {
|
|
10
|
+
assert.deepEqual((await manifest("magnific-mcp")).toolDependencies, { "mcp-client": "^0.1.0" });
|
|
11
|
+
assert.deepEqual((await manifest("campaign-draft-runner")).toolDependencies, {
|
|
12
|
+
"pr-campaign": "^0.1.0",
|
|
13
|
+
"gmail-workspace": "^0.1.0"
|
|
14
|
+
});
|
|
15
|
+
assert.deepEqual((await manifest("x-campaign-runner")).toolDependencies, { "x-dm": "^0.2.0" });
|
|
16
|
+
assert.deepEqual((await manifest("official-tool-sync")).toolDependencies, { trash: "^1.0.0" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("optional tool integrations do not become hard dependencies", async () => {
|
|
20
|
+
assert.deepEqual((await manifest("whatsapp-web")).toolDependencies, undefined);
|
|
21
|
+
assert.deepEqual((await manifest("pr-campaign")).toolDependencies, undefined);
|
|
22
|
+
assert.deepEqual((await manifest("master-slave")).toolDependencies, undefined);
|
|
23
|
+
});
|
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import test from "node:test";
|
|
7
8
|
import {
|
|
8
9
|
installBundledOfficialTool,
|
|
@@ -59,6 +60,18 @@ test("verifies the exact file set and digests", async (t) => {
|
|
|
59
60
|
await assert.rejects(() => verifyOfficialToolTree(source, files), /unexpected=extra.js/);
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
test("every bundled official tool lock matches the catalog source", async () => {
|
|
64
|
+
const lock = JSON.parse(await readFile(new URL("../src/official-tools.lock.json", import.meta.url), "utf8"));
|
|
65
|
+
for (const [name, entry] of Object.entries(lock.tools)) {
|
|
66
|
+
const source = fileURLToPath(new URL(`../../tools/${name}/`, import.meta.url));
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
await verifyOfficialToolTree(source, entry.files),
|
|
69
|
+
{ files: Object.keys(entry.files).length },
|
|
70
|
+
name
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
62
75
|
test("rejects symbolic links before deployment", async (t) => {
|
|
63
76
|
const { source, files } = await fixture(t);
|
|
64
77
|
await symlink(path.join(source, "index.js"), path.join(source, "link.js"));
|
|
@@ -72,15 +85,18 @@ test("installs a verified staged tree without overwriting an existing tool", asy
|
|
|
72
85
|
await mkdir(path.join(checkoutDir, "tools"), { recursive: true });
|
|
73
86
|
await cp(source, path.join(checkoutDir, "tools", "master-slave"), { recursive: true });
|
|
74
87
|
};
|
|
88
|
+
const lifecycle = [];
|
|
75
89
|
const result = await installLockedOfficialTool({
|
|
76
90
|
toolName: "master-slave",
|
|
77
91
|
lock: lock(files),
|
|
78
92
|
destination,
|
|
79
93
|
scratchRoot: root,
|
|
80
94
|
checkout,
|
|
81
|
-
|
|
95
|
+
installDependencies: async () => { lifecycle.push("dependencies"); },
|
|
96
|
+
validate: async () => { lifecycle.push("validate"); }
|
|
82
97
|
});
|
|
83
98
|
assert.equal(result.commit, "a".repeat(40));
|
|
99
|
+
assert.deepEqual(lifecycle, ["dependencies", "validate"]);
|
|
84
100
|
assert.equal(await readFile(path.join(destination, "index.js"), "utf8"), "process.stdout.write('ok');\n");
|
|
85
101
|
await assert.rejects(
|
|
86
102
|
() => installLockedOfficialTool({ toolName: "master-slave", lock: lock(files), destination, scratchRoot: root, checkout }),
|
|
@@ -100,8 +116,31 @@ test("loads the bundled lock before selecting the canonical tool destination", a
|
|
|
100
116
|
return { installed: true };
|
|
101
117
|
}
|
|
102
118
|
});
|
|
103
|
-
assert.deepEqual(result, { installed: true });
|
|
119
|
+
assert.deepEqual(result, { installed: true, dependencies: [] });
|
|
104
120
|
assert.equal(calls[0].toolName, "master-slave");
|
|
105
121
|
assert.deepEqual(calls[0].lock, lock(files));
|
|
106
122
|
assert.match(calls[0].destination, /tools\/master-slave$/);
|
|
107
123
|
});
|
|
124
|
+
|
|
125
|
+
test("installs locked tool dependencies before the requested tool", async (t) => {
|
|
126
|
+
const { root, files } = await fixture(t);
|
|
127
|
+
const dependencyLock = lock(files);
|
|
128
|
+
dependencyLock.tools = {
|
|
129
|
+
"mcp-client": { version: "0.1.0", toolDependencies: {}, files },
|
|
130
|
+
"magnific-mcp": { version: "0.1.0", toolDependencies: { "mcp-client": "^0.1.0" }, files }
|
|
131
|
+
};
|
|
132
|
+
const lockFile = path.join(root, "dependency-lock.json");
|
|
133
|
+
await writeFile(lockFile, `${JSON.stringify(dependencyLock)}\n`);
|
|
134
|
+
const calls = [];
|
|
135
|
+
const result = await installBundledOfficialTool("magnific-mcp", {
|
|
136
|
+
lockFile,
|
|
137
|
+
resolveInstalledVersion: async () => undefined,
|
|
138
|
+
install: async ({ toolName }) => {
|
|
139
|
+
calls.push(toolName);
|
|
140
|
+
return { toolName, installed: true };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
assert.deepEqual(calls, ["mcp-client", "magnific-mcp"]);
|
|
144
|
+
assert.equal(result.toolName, "magnific-mcp");
|
|
145
|
+
assert.deepEqual(result.dependencies, [{ name: "mcp-client", version: "0.1.0", status: "installed" }]);
|
|
146
|
+
});
|