arisa 5.1.49 → 5.1.60
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 +0 -2
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +39 -489
- package/src/core/agent/agent-session-lifecycle.js +181 -0
- package/src/core/agent/pi-capability-tools.js +183 -0
- package/src/core/artifacts/artifact-store.js +73 -17
- package/src/core/capabilities/capability-service.js +340 -0
- package/src/core/tasks/task-routing.js +7 -0
- package/src/core/tasks/task-runner.js +53 -0
- package/src/core/tasks/task-store.js +316 -92
- package/src/core/tools/tool-output-materializer.js +5 -5
- package/src/official-tools.lock.json +7 -4
- package/src/runtime/arisa-capabilities.js +51 -242
- package/src/runtime/create-app.js +10 -1
- package/src/runtime/create-headless-app.js +5 -2
- package/src/transport/telegram/bot.js +112 -368
- package/src/transport/telegram/chat-queue.js +72 -6
- package/src/transport/telegram/prompt-builders.js +9 -0
- package/src/transport/telegram/task-dispatcher.js +73 -36
- package/src/transport/telegram/telegram-auth-controller.js +180 -0
- package/src/transport/telegram/telegram-session-bridge.js +170 -0
- package/src/transport/telegram/telegram-tools-command.js +28 -0
- package/src/transport/telegram/telegram-workspace-controller.js +66 -0
- package/test/agent-session-lifecycle.test.js +58 -0
- package/test/artifact-store.test.js +38 -2
- package/test/capabilities-security.test.js +58 -0
- package/test/context-and-task-bounds.test.js +76 -1
- package/test/device-code-message.test.js +9 -0
- package/test/media-caption.test.js +1 -1
- package/test/pi-capability-tools.test.js +65 -0
- package/test/session-start-operational-notes.test.js +1 -1
- package/test/task-idempotency.test.js +40 -0
- package/test/task-routing.test.js +62 -0
- package/test/task-store.test.js +178 -6
- package/test/telegram-task-dispatcher.test.js +99 -23
- package/test/telegram-text-artifact.test.js +13 -2
- package/test/telegram-tools-command.test.js +47 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getErrorMessage } from "../../core/agent/auth-flow.js";
|
|
2
|
+
import { formatToolUsageReport } from "../../runtime/tool-usage-report.js";
|
|
3
|
+
import { renderTelegramHtml } from "./text-format.js";
|
|
4
|
+
|
|
5
|
+
export function createTelegramToolsCommandHandler({
|
|
6
|
+
authorize,
|
|
7
|
+
contextRoute,
|
|
8
|
+
toolRegistry,
|
|
9
|
+
withTyping,
|
|
10
|
+
logger
|
|
11
|
+
}) {
|
|
12
|
+
return async (ctx) => {
|
|
13
|
+
logger?.log("telegram", `/tools command received in chat ${ctx.chat.id}`);
|
|
14
|
+
return withTyping(ctx, async () => {
|
|
15
|
+
const auth = await authorize(ctx);
|
|
16
|
+
if (!auth.ok) return;
|
|
17
|
+
try {
|
|
18
|
+
const report = await toolRegistry.usage(contextRoute(ctx).scopeChatId);
|
|
19
|
+
await ctx.reply(renderTelegramHtml(formatToolUsageReport(report)), { parse_mode: "HTML" });
|
|
20
|
+
logger?.log("telegram", `/tools command completed in chat ${ctx.chat.id}`);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const message = getErrorMessage(error);
|
|
23
|
+
logger?.error("telegram", `/tools command failed in chat ${ctx.chat.id}: ${message}`);
|
|
24
|
+
await ctx.reply(`Tool usage report failed: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { authorizeChat } from "./auth.js";
|
|
2
|
+
import { resolveTelegramWorkspaceRoute } from "./workspace-group.js";
|
|
3
|
+
|
|
4
|
+
function incomingChatMeta(ctx) {
|
|
5
|
+
return {
|
|
6
|
+
languageCode: ctx.from?.language_code || "",
|
|
7
|
+
username: ctx.from?.username || "",
|
|
8
|
+
firstName: ctx.from?.first_name || "",
|
|
9
|
+
lastName: ctx.from?.last_name || ""
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createTelegramWorkspaceController({ config, api, saveConfig }) {
|
|
14
|
+
const routes = new WeakMap();
|
|
15
|
+
const gateStates = new Map();
|
|
16
|
+
|
|
17
|
+
async function authorizeContext(ctx) {
|
|
18
|
+
const route = await resolveTelegramWorkspaceRoute({ config, api: ctx.api || api, ctx });
|
|
19
|
+
if (!route.workspace) {
|
|
20
|
+
const authorization = await authorizeChat({
|
|
21
|
+
config,
|
|
22
|
+
chatId: ctx.chat.id,
|
|
23
|
+
saveConfig,
|
|
24
|
+
chatMeta: incomingChatMeta(ctx)
|
|
25
|
+
});
|
|
26
|
+
if (authorization.ok) routes.set(ctx, route);
|
|
27
|
+
return authorization;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const gateKey = String(ctx.chat.id);
|
|
31
|
+
const previous = gateStates.get(gateKey);
|
|
32
|
+
if (!route.ok) {
|
|
33
|
+
gateStates.set(gateKey, route.reason || "locked");
|
|
34
|
+
if (previous !== (route.reason || "locked")) {
|
|
35
|
+
await ctx.reply("Private workspace access is paused because this forum is no longer owner-only.").catch(() => {});
|
|
36
|
+
}
|
|
37
|
+
return { ok: false, reason: route.reason || "workspace-locked" };
|
|
38
|
+
}
|
|
39
|
+
if (!(config.telegram.authorizedChatIds || []).includes(route.ownerChatId)) {
|
|
40
|
+
return { ok: false, reason: "owner-not-authorized" };
|
|
41
|
+
}
|
|
42
|
+
routes.set(ctx, route);
|
|
43
|
+
gateStates.set(gateKey, "ready");
|
|
44
|
+
if (previous && previous !== "ready") {
|
|
45
|
+
await ctx.reply("Private workspace access restored.").catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, firstTime: false, workspace: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function contextRoute(ctx) {
|
|
51
|
+
return routes.get(ctx) || {
|
|
52
|
+
ok: true,
|
|
53
|
+
workspace: false,
|
|
54
|
+
sessionId: String(ctx.chat.id),
|
|
55
|
+
scopeChatId: ctx.chat.id,
|
|
56
|
+
transportChatId: ctx.chat.id,
|
|
57
|
+
threadId: null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
authorizeContext,
|
|
63
|
+
contextRoute,
|
|
64
|
+
registerRoute: (ctx, route) => routes.set(ctx, route)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { AgentSessionLifecycle } from "../src/core/agent/agent-session-lifecycle.js";
|
|
4
|
+
|
|
5
|
+
test("session lifecycle owns cache closure and pending handoffs", async () => {
|
|
6
|
+
let closes = 0;
|
|
7
|
+
const lifecycle = new AgentSessionLifecycle({
|
|
8
|
+
logger: null,
|
|
9
|
+
summarizeContext: () => ({ messages: 0, estimatedTokens: 0 })
|
|
10
|
+
});
|
|
11
|
+
lifecycle.sessions.set("chat", {
|
|
12
|
+
session: {
|
|
13
|
+
async close() { closes += 1; }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
lifecycle.resetSession("chat", { handoff: "continue here", parentSession: "parent.jsonl" });
|
|
18
|
+
await lifecycle.waitForClose("chat");
|
|
19
|
+
|
|
20
|
+
assert.equal(closes, 1);
|
|
21
|
+
assert.equal(lifecycle.sessions.has("chat"), false);
|
|
22
|
+
assert.equal(lifecycle.pendingNewSessions.has("chat"), true);
|
|
23
|
+
assert.deepEqual(lifecycle.pendingSessionHandoffs.get("chat"), {
|
|
24
|
+
text: "continue here",
|
|
25
|
+
parentSession: "parent.jsonl"
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
lifecycle.completeNewSession("chat");
|
|
29
|
+
assert.equal(lifecycle.pendingNewSessions.has("chat"), false);
|
|
30
|
+
assert.equal(lifecycle.pendingSessionHandoffs.has("chat"), false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("session lifecycle diagnostics remain available after extraction", async () => {
|
|
34
|
+
const lifecycle = new AgentSessionLifecycle({
|
|
35
|
+
logger: null,
|
|
36
|
+
summarizeContext: () => ({ messages: 2, estimatedTokens: 42 })
|
|
37
|
+
});
|
|
38
|
+
lifecycle.sessions.set("123", {
|
|
39
|
+
session: {
|
|
40
|
+
messages: [{ role: "user" }, { role: "assistant" }],
|
|
41
|
+
getSessionStats: () => ({ contextUsage: { tokens: 42, contextWindow: 1000, percent: 4.2 } })
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.deepEqual(await lifecycle.getDiagnostic(), {
|
|
46
|
+
harness: "pi",
|
|
47
|
+
sessions: 1,
|
|
48
|
+
closingSessions: 0,
|
|
49
|
+
contexts: [{
|
|
50
|
+
chatId: "123",
|
|
51
|
+
messages: 2,
|
|
52
|
+
estimatedTokens: 42,
|
|
53
|
+
tokens: 42,
|
|
54
|
+
contextWindow: 1000,
|
|
55
|
+
percent: 4.2
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
@@ -9,7 +9,7 @@ process.env.HOME = homeDir;
|
|
|
9
9
|
process.env.USERPROFILE = homeDir;
|
|
10
10
|
|
|
11
11
|
const { ArtifactStore } = await import("../src/core/artifacts/artifact-store.js");
|
|
12
|
-
const { arisaHomeDir } = await import("../src/runtime/paths.js");
|
|
12
|
+
const { arisaHomeDir, getChatArtifactsIndexFile } = await import("../src/runtime/paths.js");
|
|
13
13
|
|
|
14
14
|
async function resetHome() {
|
|
15
15
|
await rm(arisaHomeDir, { recursive: true, force: true });
|
|
@@ -46,6 +46,42 @@ test("creates, persists, reads, and lists text artifacts by recency", async () =
|
|
|
46
46
|
assert.deepEqual(await reloadedStore.get(first.id), first);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test("serializes 100 concurrent artifact writes across store instances", async () => {
|
|
50
|
+
await resetHome();
|
|
51
|
+
const chatId = "concurrent-chat";
|
|
52
|
+
const stores = Array.from({ length: 5 }, () => new ArtifactStore().forChat(chatId));
|
|
53
|
+
const artifacts = await Promise.all(Array.from({ length: 100 }, (_, index) => (
|
|
54
|
+
stores[index % stores.length].createText({
|
|
55
|
+
text: `message ${index}`,
|
|
56
|
+
source: { type: "test", index }
|
|
57
|
+
})
|
|
58
|
+
)));
|
|
59
|
+
|
|
60
|
+
const persisted = JSON.parse(await readFile(getChatArtifactsIndexFile(chatId), "utf8"));
|
|
61
|
+
assert.equal(persisted.length, 100);
|
|
62
|
+
assert.equal(new Set(persisted.map((artifact) => artifact.id)).size, 100);
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
new Set(persisted.map((artifact) => artifact.id)),
|
|
65
|
+
new Set(artifacts.map((artifact) => artifact.id))
|
|
66
|
+
);
|
|
67
|
+
const stateFiles = await readdir(path.dirname(getChatArtifactsIndexFile(chatId)));
|
|
68
|
+
assert.equal(stateFiles.some((name) => name.endsWith(".tmp")), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("refuses to overwrite a corrupt artifact index", async () => {
|
|
72
|
+
await resetHome();
|
|
73
|
+
const chatId = "corrupt-chat";
|
|
74
|
+
const indexFile = getChatArtifactsIndexFile(chatId);
|
|
75
|
+
await new ArtifactStore().forChat(chatId).createText({ text: "safe", source: { type: "test" } });
|
|
76
|
+
await writeFile(indexFile, "{truncated", "utf8");
|
|
77
|
+
|
|
78
|
+
await assert.rejects(
|
|
79
|
+
new ArtifactStore().forChat(chatId).createText({ text: "must not replace", source: { type: "test" } }),
|
|
80
|
+
/Artifact index is unreadable/
|
|
81
|
+
);
|
|
82
|
+
assert.equal(await readFile(indexFile, "utf8"), "{truncated");
|
|
83
|
+
});
|
|
84
|
+
|
|
49
85
|
test("copies file artifacts into the chat artifact directory", async () => {
|
|
50
86
|
await resetHome();
|
|
51
87
|
const originalDir = await mkdtemp(path.join(os.tmpdir(), "arisa-source-file-"));
|
|
@@ -142,6 +142,64 @@ test("requires chatId for chat-scoped IPC methods", async () => {
|
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
test("tool-emitted tasks cannot override trusted chat routing", async () => {
|
|
146
|
+
const capabilities = createCapabilities();
|
|
147
|
+
const created = await capabilities.dispatch({
|
|
148
|
+
method: "tasks.add",
|
|
149
|
+
toolName: "poller",
|
|
150
|
+
chatId: "trusted-chat",
|
|
151
|
+
params: {
|
|
152
|
+
task: {
|
|
153
|
+
kind: "agent_task",
|
|
154
|
+
route: { transport: "telegram", destination: { chatId: "attacker-chat" } },
|
|
155
|
+
payload: {
|
|
156
|
+
chatId: "attacker-chat",
|
|
157
|
+
telegramContext: { transportChatId: "attacker-chat" },
|
|
158
|
+
prompt: "Safe payload"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.equal(created.route, undefined);
|
|
165
|
+
assert.equal(created.payload.chatId, "trusted-chat");
|
|
166
|
+
assert.equal(created.payload.telegramContext, undefined);
|
|
167
|
+
assert.equal(created.payload.prompt, "Safe payload");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("IPC resource notes remain scoped to the calling tool", async () => {
|
|
171
|
+
const calls = [];
|
|
172
|
+
const capabilities = createArisaCapabilities({
|
|
173
|
+
artifactStore: createFakeArtifactStore(),
|
|
174
|
+
taskStore: createFakeTaskStore(),
|
|
175
|
+
resourceNotes: {
|
|
176
|
+
set: async (...args) => {
|
|
177
|
+
calls.push(args);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await capabilities.dispatch({
|
|
184
|
+
method: "tools.setResourceNote",
|
|
185
|
+
toolName: "caller-tool",
|
|
186
|
+
chatId: "chat-1",
|
|
187
|
+
params: { name: "other-tool", resourceId: "resource-1", note: "watch" }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
assert.deepEqual(calls, [["chat-1", "caller-tool", "resource-1", "watch"]]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("IPC does not expose Telegram-only capability methods", async () => {
|
|
194
|
+
const capabilities = createCapabilities();
|
|
195
|
+
await assert.rejects(() => capabilities.dispatch({
|
|
196
|
+
method: "telegram.createTopic",
|
|
197
|
+
toolName: "caller-tool",
|
|
198
|
+
chatId: "chat-1",
|
|
199
|
+
params: { name: "Unsafe", context: "No" }
|
|
200
|
+
}), /unknown IPC method: telegram\.createTopic/);
|
|
201
|
+
});
|
|
202
|
+
|
|
145
203
|
test("agent events preserve a bounded immediate acknowledgement", async () => {
|
|
146
204
|
const capabilities = createCapabilities();
|
|
147
205
|
const created = await capabilities.dispatch({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
collectText,
|
|
5
5
|
ensureQueuedTelegramTyping,
|
|
6
6
|
isSilentReply,
|
|
7
|
+
resolveIncomingBusyMessageMode,
|
|
7
8
|
stopQueuedTelegramTyping
|
|
8
9
|
} from "../src/transport/telegram/bot.js";
|
|
9
10
|
import {
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
resolveTelegramBusyMessageMode,
|
|
14
15
|
routeBusyPrompt
|
|
15
16
|
} from "../src/transport/telegram/chat-queue.js";
|
|
16
|
-
import { selectScheduledTasks } from "../src/core/
|
|
17
|
+
import { selectScheduledTasks } from "../src/core/capabilities/capability-service.js";
|
|
17
18
|
|
|
18
19
|
test("queued Telegram prompts start typing immediately and share one indicator", async () => {
|
|
19
20
|
let actions = 0;
|
|
@@ -158,6 +159,8 @@ test("chat state uses one queue for numeric and string chat IDs", () => {
|
|
|
158
159
|
processing: false,
|
|
159
160
|
pendingPrompts: [],
|
|
160
161
|
pendingPromptContexts: [],
|
|
162
|
+
pendingPromptReceipts: [],
|
|
163
|
+
pendingPromptCoalescible: [],
|
|
161
164
|
continueAfterClose: false,
|
|
162
165
|
historyRevision: 0,
|
|
163
166
|
beforeNextPrompt: null,
|
|
@@ -183,6 +186,32 @@ test("queued prompts retain their message boundaries and order", async () => {
|
|
|
183
186
|
assert.deepEqual(processed, ["first request", "second request", "third request"]);
|
|
184
187
|
});
|
|
185
188
|
|
|
189
|
+
test("queued execution receipts resolve only after their prompt runs", async () => {
|
|
190
|
+
const chatState = createChatStateStore().get("chat");
|
|
191
|
+
chatState.processing = true;
|
|
192
|
+
let releaseFirst;
|
|
193
|
+
const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
|
|
194
|
+
const { createPromptExecutionReceipt } = await import("../src/transport/telegram/chat-queue.js");
|
|
195
|
+
const receipt = createPromptExecutionReceipt();
|
|
196
|
+
queueChatPrompt(chatState, "scheduled request", { receipt });
|
|
197
|
+
let confirmed = false;
|
|
198
|
+
receipt.promise.then(() => { confirmed = true; });
|
|
199
|
+
|
|
200
|
+
const draining = drainChatPromptQueue({
|
|
201
|
+
chatState,
|
|
202
|
+
initialPrompt: "active request",
|
|
203
|
+
processPrompt: async ({ prompt }) => {
|
|
204
|
+
if (prompt === "active request") await firstGate;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
208
|
+
assert.equal(confirmed, false);
|
|
209
|
+
releaseFirst();
|
|
210
|
+
await receipt.promise;
|
|
211
|
+
assert.equal(confirmed, true);
|
|
212
|
+
await draining;
|
|
213
|
+
});
|
|
214
|
+
|
|
186
215
|
test("a queued /new continues after the active session closes", async () => {
|
|
187
216
|
const chatState = createChatStateStore().get("chat");
|
|
188
217
|
chatState.processing = true;
|
|
@@ -265,6 +294,20 @@ test("busy message mode supports global defaults and per-chat overrides", () =>
|
|
|
265
294
|
assert.equal(resolveTelegramBusyMessageMode({ telegram: { busyMessageMode: "invalid" } }, 123), "queue");
|
|
266
295
|
});
|
|
267
296
|
|
|
297
|
+
test("forum topics queue while direct Arisa messages retain steer mode", () => {
|
|
298
|
+
const config = { telegram: { busyMessageMode: "steer", chatMeta: {} } };
|
|
299
|
+
assert.equal(resolveIncomingBusyMessageMode({
|
|
300
|
+
config,
|
|
301
|
+
route: { workspace: true, sessionId: "owner--topic-87" },
|
|
302
|
+
message: { text: "topic follow-up" }
|
|
303
|
+
}), "queue");
|
|
304
|
+
assert.equal(resolveIncomingBusyMessageMode({
|
|
305
|
+
config,
|
|
306
|
+
route: { workspace: false, sessionId: "879964957" },
|
|
307
|
+
message: { text: "direct follow-up" }
|
|
308
|
+
}), "steer");
|
|
309
|
+
});
|
|
310
|
+
|
|
268
311
|
test("steer mode sends text to the active Pi session", async () => {
|
|
269
312
|
const received = [];
|
|
270
313
|
const chatState = createChatStateStore().get("chat");
|
|
@@ -297,6 +340,38 @@ test("failed or unavailable steering falls back to the ordered queue", async ()
|
|
|
297
340
|
assert.deepEqual(chatState.pendingPrompts, ["keep this", "and this"]);
|
|
298
341
|
});
|
|
299
342
|
|
|
343
|
+
test("direct steer fallback coalesces consecutive text without reordering it", async () => {
|
|
344
|
+
const steered = [];
|
|
345
|
+
const chatState = createChatStateStore().get("chat");
|
|
346
|
+
|
|
347
|
+
const unavailable = await routeBusyPrompt({
|
|
348
|
+
chatState,
|
|
349
|
+
prompt: "first direct message",
|
|
350
|
+
mode: "steer",
|
|
351
|
+
coalesceQueued: true,
|
|
352
|
+
ctx: { message: { message_id: 1 } }
|
|
353
|
+
});
|
|
354
|
+
chatState.activeSession = {
|
|
355
|
+
isStreaming: true,
|
|
356
|
+
async steer(prompt) { steered.push(prompt); }
|
|
357
|
+
};
|
|
358
|
+
const next = await routeBusyPrompt({
|
|
359
|
+
chatState,
|
|
360
|
+
prompt: "second direct message",
|
|
361
|
+
mode: "steer",
|
|
362
|
+
coalesceQueued: true,
|
|
363
|
+
ctx: { message: { message_id: 2 } }
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
assert.equal(unavailable.disposition, "queued");
|
|
367
|
+
assert.equal(next.disposition, "coalesced");
|
|
368
|
+
assert.deepEqual(steered, []);
|
|
369
|
+
assert.deepEqual(chatState.pendingPrompts, [
|
|
370
|
+
"first direct message\n\n--- next direct message ---\n\nsecond direct message"
|
|
371
|
+
]);
|
|
372
|
+
assert.equal(chatState.pendingPromptContexts[0].message.message_id, 2);
|
|
373
|
+
});
|
|
374
|
+
|
|
300
375
|
test("a pending /new forces later text into the replacement queue", async () => {
|
|
301
376
|
const steered = [];
|
|
302
377
|
const chatState = createChatStateStore().get("chat");
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { buildDeviceCodeTelegramMessage } from "../src/transport/telegram/device-code-message.js";
|
|
4
|
+
import { selectTelegramLoginOption } from "../src/transport/telegram/telegram-auth-controller.js";
|
|
5
|
+
|
|
6
|
+
test("Telegram auth prefers device login and falls back deterministically", () => {
|
|
7
|
+
const browser = { id: "oauth", label: "Browser login" };
|
|
8
|
+
const device = { id: "device-code", label: "Device code" };
|
|
9
|
+
assert.strictEqual(selectTelegramLoginOption([browser, device]), device);
|
|
10
|
+
assert.strictEqual(selectTelegramLoginOption([browser]), browser);
|
|
11
|
+
assert.equal(selectTelegramLoginOption([]), null);
|
|
12
|
+
});
|
|
4
13
|
|
|
5
14
|
test("builds a copyable device-code Telegram message with HTML and inline buttons", () => {
|
|
6
15
|
const payload = buildDeviceCodeTelegramMessage({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { resolveMediaCaption } from "../src/core/
|
|
3
|
+
import { resolveMediaCaption } from "../src/core/capabilities/capability-service.js";
|
|
4
4
|
|
|
5
5
|
test("does not turn a domain-like filename into a caption", () => {
|
|
6
6
|
assert.equal(resolveMediaCaption(undefined), undefined);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createPiCapabilityTools } from "../src/core/agent/pi-capability-tools.js";
|
|
4
|
+
|
|
5
|
+
function createHarness() {
|
|
6
|
+
const calls = [];
|
|
7
|
+
let taskContext = { transportChatId: "chat-1", messageThreadId: 10 };
|
|
8
|
+
const capabilityService = {
|
|
9
|
+
async execute(request) {
|
|
10
|
+
calls.push(request);
|
|
11
|
+
if (request.method === "tools.list") return { tools: [] };
|
|
12
|
+
if (request.method === "tools.run") return { ok: true, output: {} };
|
|
13
|
+
return { ok: true };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const telegram = {
|
|
17
|
+
getTaskContext: () => taskContext,
|
|
18
|
+
sendMedia: async () => {}
|
|
19
|
+
};
|
|
20
|
+
const tools = createPiCapabilityTools({
|
|
21
|
+
capabilityService,
|
|
22
|
+
telegram,
|
|
23
|
+
chatId: "session-1",
|
|
24
|
+
policy: {
|
|
25
|
+
workspaceDir: "/workspace",
|
|
26
|
+
tools: ["read"],
|
|
27
|
+
excludeTools: [],
|
|
28
|
+
shell: {}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
calls,
|
|
33
|
+
tools,
|
|
34
|
+
setTaskContext(value) {
|
|
35
|
+
taskContext = value;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("Pi tools delegate capability policy to CapabilityService", async () => {
|
|
41
|
+
const harness = createHarness();
|
|
42
|
+
const listTools = harness.tools.find((tool) => tool.name === "list_tools");
|
|
43
|
+
|
|
44
|
+
await listTools.execute("call-1", { query: "audio" });
|
|
45
|
+
|
|
46
|
+
assert.equal(harness.calls.length, 1);
|
|
47
|
+
assert.equal(harness.calls[0].method, "tools.list");
|
|
48
|
+
assert.equal(harness.calls[0].actorToolName, "list_tools");
|
|
49
|
+
assert.equal(harness.calls[0].chatId, "session-1");
|
|
50
|
+
assert.equal(harness.calls[0].context.workspaceDir, "/workspace");
|
|
51
|
+
assert.equal(harness.calls[0].context.coreTools[0].name, "read");
|
|
52
|
+
assert.equal(harness.calls[0].context.nativeTools[0].name, "system_shell");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("Pi tool execution resolves the current Telegram task context per call", async () => {
|
|
56
|
+
const harness = createHarness();
|
|
57
|
+
const runTool = harness.tools.find((tool) => tool.name === "run_tool");
|
|
58
|
+
|
|
59
|
+
await runTool.execute("call-1", { name: "worker", args: {} });
|
|
60
|
+
harness.setTaskContext({ transportChatId: "chat-1", messageThreadId: 20 });
|
|
61
|
+
await runTool.execute("call-2", { name: "worker", args: {} });
|
|
62
|
+
|
|
63
|
+
assert.equal(harness.calls[0].context.taskContext.messageThreadId, 10);
|
|
64
|
+
assert.equal(harness.calls[1].context.taskContext.messageThreadId, 20);
|
|
65
|
+
});
|
|
@@ -16,7 +16,7 @@ async function loadNotesWithHome(homeDir, notesPayload) {
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
const script = `
|
|
19
|
-
const mod = await import(${JSON.stringify(new URL("../src/core/agent/agent-
|
|
19
|
+
const mod = await import(${JSON.stringify(new URL("../src/core/agent/agent-session-lifecycle.js", import.meta.url).href)});
|
|
20
20
|
process.stdout.write(JSON.stringify(mod.loadSessionStartOperationalNotes()));
|
|
21
21
|
`;
|
|
22
22
|
const { stdout } = await execFileAsync(process.execPath, ["--input-type=module", "--eval", script], {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), "arisa-task-idempotency-home-"));
|
|
8
|
+
process.env.HOME = homeDir;
|
|
9
|
+
process.env.USERPROFILE = homeDir;
|
|
10
|
+
|
|
11
|
+
const { TaskStore } = await import("../src/core/tasks/task-store.js");
|
|
12
|
+
const { createTaskRunner } = await import("../src/core/tasks/task-runner.js");
|
|
13
|
+
const { arisaHomeDir } = await import("../src/runtime/paths.js");
|
|
14
|
+
|
|
15
|
+
test("two concurrent dispatchers execute one task id exactly once", async () => {
|
|
16
|
+
await rm(arisaHomeDir, { recursive: true, force: true });
|
|
17
|
+
const seedStore = new TaskStore();
|
|
18
|
+
await seedStore.add({
|
|
19
|
+
id: "single-execution",
|
|
20
|
+
kind: "agent_task",
|
|
21
|
+
runAt: new Date(Date.now() - 1_000).toISOString()
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const executions = [];
|
|
25
|
+
const dispatch = async (task) => {
|
|
26
|
+
executions.push(task.id);
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
28
|
+
};
|
|
29
|
+
const first = createTaskRunner({ taskStore: new TaskStore(), dispatch });
|
|
30
|
+
const second = createTaskRunner({ taskStore: new TaskStore(), dispatch });
|
|
31
|
+
|
|
32
|
+
const results = (await Promise.all([
|
|
33
|
+
first.dispatchDueTasks(),
|
|
34
|
+
second.dispatchDueTasks()
|
|
35
|
+
])).flat();
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(executions, ["single-execution"]);
|
|
38
|
+
assert.deepEqual(results, [{ taskId: "single-execution", status: "completed" }]);
|
|
39
|
+
assert.equal((await new TaskStore().get("single-execution")).status, "done");
|
|
40
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { taskWithoutCallerRouting } from "../src/core/tasks/task-routing.js";
|
|
4
|
+
import { materializeToolOutput } from "../src/core/tools/tool-output-materializer.js";
|
|
5
|
+
|
|
6
|
+
test("external tasks cannot override owner scope or transport routing", () => {
|
|
7
|
+
assert.deepEqual(taskWithoutCallerRouting({
|
|
8
|
+
kind: "agent_task",
|
|
9
|
+
route: { transport: "telegram", destination: { chatId: -999, threadId: 1 } },
|
|
10
|
+
payload: {
|
|
11
|
+
chatId: "other-owner",
|
|
12
|
+
telegramContext: { transportChatId: -999, messageThreadId: 1 },
|
|
13
|
+
prompt: "safe"
|
|
14
|
+
}
|
|
15
|
+
}), {
|
|
16
|
+
kind: "agent_task",
|
|
17
|
+
payload: { prompt: "safe" }
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("tool output materialization stores routing outside task payloads", async () => {
|
|
22
|
+
let capturedTasks;
|
|
23
|
+
let capturedDefaults;
|
|
24
|
+
const taskStore = {
|
|
25
|
+
async addMany(tasks, defaults) {
|
|
26
|
+
capturedTasks = tasks;
|
|
27
|
+
capturedDefaults = defaults;
|
|
28
|
+
return tasks;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const artifactStore = {
|
|
32
|
+
forChat() {
|
|
33
|
+
return {
|
|
34
|
+
async createText() { throw new Error("unexpected text artifact"); },
|
|
35
|
+
async createFromFile() { throw new Error("unexpected file artifact"); }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const route = { transport: "telegram", destination: { chatId: -1001, threadId: 87 } };
|
|
40
|
+
|
|
41
|
+
await materializeToolOutput({
|
|
42
|
+
result: {
|
|
43
|
+
asyncTask: {
|
|
44
|
+
kind: "agent_task",
|
|
45
|
+
route: { transport: "telegram", destination: { chatId: -999, threadId: 1 } },
|
|
46
|
+
payload: { chatId: "other", prompt: "run" }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
name: "scheduler",
|
|
50
|
+
chatId: "owner",
|
|
51
|
+
artifactStore,
|
|
52
|
+
taskStore,
|
|
53
|
+
taskContext: route
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(capturedTasks, [{ kind: "agent_task", payload: { prompt: "run" } }]);
|
|
57
|
+
assert.deepEqual(capturedDefaults, {
|
|
58
|
+
payload: { chatId: "owner" },
|
|
59
|
+
route,
|
|
60
|
+
source: { type: "tool", toolName: "scheduler", chatId: "owner" }
|
|
61
|
+
});
|
|
62
|
+
});
|