arisa 5.1.49 → 5.1.64

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 (56) hide show
  1. package/AGENTS.md +0 -2
  2. package/README.md +9 -0
  3. package/package.json +1 -1
  4. package/src/core/agent/agent-manager.js +49 -489
  5. package/src/core/agent/agent-session-lifecycle.js +181 -0
  6. package/src/core/agent/pi-capability-tools.js +183 -0
  7. package/src/core/artifacts/artifact-store.js +73 -17
  8. package/src/core/capabilities/capability-service.js +340 -0
  9. package/src/core/config/config-defaults.js +28 -1
  10. package/src/core/tasks/task-routing.js +7 -0
  11. package/src/core/tasks/task-runner.js +68 -0
  12. package/src/core/tasks/task-store.js +382 -92
  13. package/src/core/tools/tool-output-materializer.js +5 -5
  14. package/src/core/tools/tool-registry.js +20 -5
  15. package/src/core/tools/weighted-resource-governor.js +153 -0
  16. package/src/index.js +20 -0
  17. package/src/official-tools.lock.json +62 -45
  18. package/src/runtime/arisa-capabilities.js +51 -242
  19. package/src/runtime/create-app.js +11 -2
  20. package/src/runtime/create-headless-app.js +7 -4
  21. package/src/runtime/paths.js +4 -0
  22. package/src/runtime/service-manager.js +3 -1
  23. package/src/runtime/service-supervisor.js +98 -0
  24. package/src/transport/telegram/bot.js +186 -374
  25. package/src/transport/telegram/chat-queue.js +83 -6
  26. package/src/transport/telegram/prompt-builders.js +9 -0
  27. package/src/transport/telegram/reply-topic-routing.js +111 -0
  28. package/src/transport/telegram/task-dispatcher.js +96 -36
  29. package/src/transport/telegram/telegram-auth-controller.js +180 -0
  30. package/src/transport/telegram/telegram-session-bridge.js +177 -0
  31. package/src/transport/telegram/telegram-tools-command.js +28 -0
  32. package/src/transport/telegram/telegram-workspace-controller.js +66 -0
  33. package/src/transport/telegram/workspace-topic-store.js +228 -0
  34. package/test/agent-session-lifecycle.test.js +58 -0
  35. package/test/artifact-store.test.js +38 -2
  36. package/test/capabilities-security.test.js +58 -0
  37. package/test/chat-queue.test.js +32 -0
  38. package/test/context-and-task-bounds.test.js +76 -1
  39. package/test/device-code-message.test.js +9 -0
  40. package/test/media-caption.test.js +1 -1
  41. package/test/model-selection.test.js +9 -1
  42. package/test/official-tool-dependencies.test.js +1 -1
  43. package/test/paths.test.js +8 -0
  44. package/test/pi-capability-tools.test.js +65 -0
  45. package/test/service-manager.test.js +48 -0
  46. package/test/session-start-operational-notes.test.js +1 -1
  47. package/test/task-idempotency.test.js +40 -0
  48. package/test/task-routing.test.js +62 -0
  49. package/test/task-store.test.js +231 -7
  50. package/test/telegram-reply-topic-routing.test.js +94 -0
  51. package/test/telegram-task-dispatcher.test.js +150 -23
  52. package/test/telegram-text-artifact.test.js +13 -2
  53. package/test/telegram-tools-command.test.js +47 -0
  54. package/test/telegram-workspace-topic-store.test.js +124 -0
  55. package/test/tool-registry-run.test.js +41 -0
  56. package/test/weighted-resource-governor.test.js +95 -0
@@ -0,0 +1,47 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTelegramToolsCommandHandler } from "../src/transport/telegram/telegram-tools-command.js";
4
+
5
+ test("/tools shows typing and sends the scoped usage report", async () => {
6
+ const events = [];
7
+ const handler = createTelegramToolsCommandHandler({
8
+ authorize: async () => ({ ok: true }),
9
+ contextRoute: () => ({ scopeChatId: 879964957 }),
10
+ toolRegistry: {
11
+ usage: async (chatId) => {
12
+ assert.equal(chatId, 879964957);
13
+ return [{ name: "example", count: 2, official: true }];
14
+ }
15
+ },
16
+ withTyping: async (_ctx, work) => {
17
+ events.push("typing");
18
+ return work();
19
+ },
20
+ logger: null
21
+ });
22
+ const ctx = {
23
+ chat: { id: 879964957 },
24
+ reply: async (text, options) => events.push({ text, options })
25
+ };
26
+
27
+ await handler(ctx);
28
+
29
+ assert.equal(events[0], "typing");
30
+ assert.match(events[1].text, /example/);
31
+ assert.deepEqual(events[1].options, { parse_mode: "HTML" });
32
+ });
33
+
34
+ test("/tools reports failures instead of disappearing", async () => {
35
+ const replies = [];
36
+ const handler = createTelegramToolsCommandHandler({
37
+ authorize: async () => ({ ok: true }),
38
+ contextRoute: () => ({ scopeChatId: 1 }),
39
+ toolRegistry: { usage: async () => { throw new Error("store unavailable"); } },
40
+ withTyping: async (_ctx, work) => work(),
41
+ logger: null
42
+ });
43
+
44
+ await handler({ chat: { id: 1 }, reply: async (text) => replies.push(text) });
45
+
46
+ assert.deepEqual(replies, ["Tool usage report failed: store unavailable"]);
47
+ });
@@ -0,0 +1,124 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
6
+ import {
7
+ migrateLegacyReplyTopics,
8
+ WorkspaceTopicStore
9
+ } from "../src/transport/telegram/workspace-topic-store.js";
10
+
11
+ async function fixture(now = Date.parse("2026-08-24T12:00:00.000Z")) {
12
+ const directory = await mkdtemp(path.join(os.tmpdir(), "arisa-workspace-topics-"));
13
+ const file = path.join(directory, "topics.json");
14
+ return {
15
+ file,
16
+ store: new WorkspaceTopicStore({ resolveFile: () => file, now: () => now }),
17
+ cleanup: () => rm(directory, { recursive: true, force: true })
18
+ };
19
+ }
20
+
21
+ test("workspace topics are learned, renamed, closed, and reopened dynamically", async () => {
22
+ const { store, cleanup } = await fixture();
23
+ try {
24
+ const route = {
25
+ workspace: true,
26
+ ownerChatId: 42,
27
+ transportChatId: -100123,
28
+ topicThreadId: 25,
29
+ generalTopicId: 1
30
+ };
31
+ await store.observeMessage(route, { forum_topic_created: { name: "Research" } });
32
+ assert.equal((await store.listTopics(42, -100123))[0].name, "Research");
33
+
34
+ await store.observeMessage(route, { forum_topic_edited: { name: "Research Lab" } });
35
+ assert.equal((await store.listTopics(42, -100123))[0].name, "Research Lab");
36
+
37
+ await store.observeMessage(route, { forum_topic_closed: {} });
38
+ assert.deepEqual(await store.listTopics(42, -100123), []);
39
+ assert.equal((await store.listTopics(42, -100123, { includeClosed: true }))[0].status, "closed");
40
+
41
+ await store.observeMessage(route, { forum_topic_reopened: {} });
42
+ assert.equal((await store.listTopics(42, -100123))[0].status, "open");
43
+ } finally {
44
+ await cleanup();
45
+ }
46
+ });
47
+
48
+ test("Arisa topic initialization persists semantic context", async () => {
49
+ const { store, cleanup } = await fixture();
50
+ try {
51
+ await store.upsertTopic(42, -100123, {
52
+ threadId: 114,
53
+ name: "CORE",
54
+ description: "Arisa runtime and Telegram engineering",
55
+ source: "arisa-initialized"
56
+ });
57
+ assert.deepEqual((await store.listTopics(42, -100123)).map(({ threadId, name, description, source }) => ({
58
+ threadId,
59
+ name,
60
+ description,
61
+ source
62
+ })), [{
63
+ threadId: 114,
64
+ name: "CORE",
65
+ description: "Arisa runtime and Telegram engineering",
66
+ source: "arisa-initialized"
67
+ }]);
68
+ } finally {
69
+ await cleanup();
70
+ }
71
+ });
72
+
73
+ test("concurrent topic updates preserve every workspace entry", async () => {
74
+ const { store, file, cleanup } = await fixture();
75
+ try {
76
+ await Promise.all(Array.from({ length: 30 }, (_, index) => store.upsertTopic(42, -100123, {
77
+ threadId: index + 2,
78
+ name: `Topic ${index + 2}`
79
+ })));
80
+ assert.equal((await store.listTopics(42, -100123)).length, 30);
81
+ assert.equal(JSON.parse(await readFile(file, "utf8")).version, 1);
82
+ } finally {
83
+ await cleanup();
84
+ }
85
+ });
86
+
87
+ test("legacy configured topics migrate once into dynamic state", async () => {
88
+ const { store, cleanup } = await fixture();
89
+ try {
90
+ const config = {
91
+ telegram: {
92
+ ownerWorkspaceGroups: {
93
+ "-100123": {
94
+ ownerChatId: 42,
95
+ generalTopicId: 1,
96
+ replyTopics: {
97
+ "1": { name: "General" },
98
+ "23": { name: "Stories", description: "Editorial work" }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ };
104
+ assert.equal(await migrateLegacyReplyTopics(config, store), 1);
105
+ assert.equal(config.telegram.ownerWorkspaceGroups["-100123"].replyTopics, undefined);
106
+ assert.deepEqual((await store.listTopics(42, -100123)).map((topic) => topic.name), ["Stories"]);
107
+ assert.equal(await migrateLegacyReplyTopics(config, store), 0);
108
+ } finally {
109
+ await cleanup();
110
+ }
111
+ });
112
+
113
+ test("recent proposals are deduplicated and scoped by supergroup", async () => {
114
+ const { store, cleanup } = await fixture();
115
+ try {
116
+ await store.recordProposal(42, -100123, "Research Lab");
117
+ await store.recordProposal(42, -100123, "research lab");
118
+ await store.recordProposal(42, -100999, "Another Group");
119
+ assert.deepEqual((await store.listRecentProposals(42, -100123)).map((item) => item.name), ["research lab"]);
120
+ assert.deepEqual((await store.listRecentProposals(42, -100999)).map((item) => item.name), ["Another Group"]);
121
+ } finally {
122
+ await cleanup();
123
+ }
124
+ });
@@ -157,6 +157,22 @@ test("lists optional semantic metadata with stable defaults", async () => {
157
157
 
158
158
  assert.equal(registry.list()[0].category, null);
159
159
  assert.deepEqual(registry.list()[0].keywords, []);
160
+ assert.equal(registry.get("fake-tool").execution, null);
161
+ });
162
+
163
+ test("loads weighted execution metadata from the tool manifest", async () => {
164
+ await resetHome();
165
+ await createFakeTool("heavy-tool", {
166
+ execution: { resourceClass: "browser", weight: 2 }
167
+ });
168
+
169
+ const registry = new ToolRegistry();
170
+ await registry.load();
171
+
172
+ assert.deepEqual(registry.get("heavy-tool").execution, {
173
+ resourceClass: "browser",
174
+ weight: 2
175
+ });
160
176
  });
161
177
 
162
178
  test("shows semantic metadata in tool help", async () => {
@@ -193,6 +209,31 @@ test("reports dependency status in help and blocks a tool with a missing depende
193
209
  );
194
210
  });
195
211
 
212
+ test("wraps declared tool runs in the shared execution governor", async () => {
213
+ await resetHome();
214
+ await createFakeTool("heavy-tool", {
215
+ execution: { resourceClass: "browser", weight: 1 }
216
+ });
217
+ const calls = [];
218
+ const executionGovernor = {
219
+ acquire: async (execution, label) => {
220
+ calls.push({ type: "acquire", execution, label });
221
+ return { release: () => calls.push({ type: "release", label }) };
222
+ },
223
+ snapshot: () => ({ resources: {} })
224
+ };
225
+ const registry = new ToolRegistry({ executionGovernor });
226
+ await registry.load();
227
+
228
+ const result = await registry.run({ name: "heavy-tool", request: { args: {} } });
229
+
230
+ assert.equal(result.ok, true);
231
+ assert.deepEqual(calls, [
232
+ { type: "acquire", execution: { resourceClass: "browser", weight: 1 }, label: "heavy-tool" },
233
+ { type: "release", label: "heavy-tool" }
234
+ ]);
235
+ });
236
+
196
237
  test("runs a registered tool process with an enriched request and cleans up request files", async () => {
197
238
  await resetHome();
198
239
  await createFakeTool("fake-tool");
@@ -0,0 +1,95 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ normalizeToolExecution,
5
+ normalizeToolExecutionPolicy,
6
+ WeightedResourceGovernor
7
+ } from "../src/core/tools/weighted-resource-governor.js";
8
+
9
+ function deferred() {
10
+ let resolve;
11
+ const promise = new Promise((done) => { resolve = done; });
12
+ return { promise, resolve };
13
+ }
14
+
15
+ test("normalizes manifest weights and configurable class capacities", () => {
16
+ assert.deepEqual(normalizeToolExecution({ resourceClass: "browser", weight: 2 }), {
17
+ resourceClass: "browser",
18
+ weight: 2
19
+ });
20
+ assert.equal(normalizeToolExecution(undefined), null);
21
+ assert.throws(() => normalizeToolExecution({ resourceClass: "Browser!", weight: 1 }), /Invalid tool execution resource class/);
22
+ assert.throws(() => normalizeToolExecution({ resourceClass: "browser", weight: 0 }), /positive integer/);
23
+ assert.deepEqual(normalizeToolExecutionPolicy({
24
+ defaultCapacity: 3,
25
+ maxQueuedPerClass: 12,
26
+ capacities: { browser: 4 }
27
+ }), {
28
+ defaultCapacity: 3,
29
+ maxQueuedPerClass: 12,
30
+ capacities: { browser: 4 }
31
+ });
32
+ });
33
+
34
+ test("queues weighted work fairly within one resource class", async () => {
35
+ let time = 100;
36
+ const governor = new WeightedResourceGovernor({
37
+ policy: { defaultCapacity: 2 },
38
+ now: () => time,
39
+ memoryUsage: () => ({ rss: 100 * 1024 * 1024 })
40
+ });
41
+ const execution = { resourceClass: "browser", weight: 1 };
42
+ const first = await governor.acquire(execution, "first");
43
+ const second = await governor.acquire(execution, "second");
44
+ const thirdLease = governor.acquire(execution, "third");
45
+ const fourthLease = governor.acquire(execution, "fourth");
46
+
47
+ assert.deepEqual(governor.snapshot().resources.browser, {
48
+ capacity: 2,
49
+ activeWeight: 2,
50
+ queued: 2
51
+ });
52
+
53
+ const thirdGranted = deferred();
54
+ thirdLease.then((lease) => thirdGranted.resolve(lease));
55
+ time = 140;
56
+ first.release();
57
+ const third = await thirdGranted.promise;
58
+ assert.equal(third.waitedMs, 40);
59
+ assert.equal(governor.snapshot().resources.browser.queued, 1);
60
+
61
+ second.release();
62
+ const fourth = await fourthLease;
63
+ assert.equal(fourth.waitedMs, 40);
64
+ third.release();
65
+ fourth.release();
66
+ assert.equal(governor.snapshot().resources.browser.activeWeight, 0);
67
+ });
68
+
69
+ test("undeclared lightweight work bypasses constrained resource queues", async () => {
70
+ const governor = new WeightedResourceGovernor({ policy: { defaultCapacity: 1 } });
71
+ const heavy = await governor.acquire({ resourceClass: "browser", weight: 1 }, "heavy");
72
+ const queued = governor.acquire({ resourceClass: "browser", weight: 1 }, "queued");
73
+ const light = await governor.acquire(null, "light");
74
+ assert.equal(light.waitedMs, 0);
75
+ assert.equal(governor.snapshot().resources.browser.queued, 1);
76
+ light.release();
77
+ heavy.release();
78
+ (await queued).release();
79
+ });
80
+
81
+ test("larger weights consume shared capacity and worker RSS peaks are retained", async () => {
82
+ let rss = 120 * 1024 * 1024;
83
+ const governor = new WeightedResourceGovernor({
84
+ policy: { defaultCapacity: 3 },
85
+ memoryUsage: () => ({ rss })
86
+ });
87
+ const large = await governor.acquire({ resourceClass: "browser", weight: 2 }, "large");
88
+ const waiting = governor.acquire({ resourceClass: "browser", weight: 2 }, "waiting");
89
+ assert.equal(governor.snapshot().resources.browser.queued, 1);
90
+ rss = 180 * 1024 * 1024;
91
+ large.release();
92
+ const next = await waiting;
93
+ assert.equal(governor.snapshot().peakRssBytes, rss);
94
+ next.release();
95
+ });