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.
- package/AGENTS.md +0 -2
- package/README.md +9 -0
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +49 -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/config/config-defaults.js +28 -1
- package/src/core/tasks/task-routing.js +7 -0
- package/src/core/tasks/task-runner.js +68 -0
- package/src/core/tasks/task-store.js +382 -92
- package/src/core/tools/tool-output-materializer.js +5 -5
- package/src/core/tools/tool-registry.js +20 -5
- package/src/core/tools/weighted-resource-governor.js +153 -0
- package/src/index.js +20 -0
- package/src/official-tools.lock.json +62 -45
- package/src/runtime/arisa-capabilities.js +51 -242
- package/src/runtime/create-app.js +11 -2
- package/src/runtime/create-headless-app.js +7 -4
- package/src/runtime/paths.js +4 -0
- package/src/runtime/service-manager.js +3 -1
- package/src/runtime/service-supervisor.js +98 -0
- package/src/transport/telegram/bot.js +186 -374
- package/src/transport/telegram/chat-queue.js +83 -6
- package/src/transport/telegram/prompt-builders.js +9 -0
- package/src/transport/telegram/reply-topic-routing.js +111 -0
- package/src/transport/telegram/task-dispatcher.js +96 -36
- package/src/transport/telegram/telegram-auth-controller.js +180 -0
- package/src/transport/telegram/telegram-session-bridge.js +177 -0
- package/src/transport/telegram/telegram-tools-command.js +28 -0
- package/src/transport/telegram/telegram-workspace-controller.js +66 -0
- package/src/transport/telegram/workspace-topic-store.js +228 -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/chat-queue.test.js +32 -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/model-selection.test.js +9 -1
- package/test/official-tool-dependencies.test.js +1 -1
- package/test/paths.test.js +8 -0
- package/test/pi-capability-tools.test.js +65 -0
- package/test/service-manager.test.js +48 -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 +231 -7
- package/test/telegram-reply-topic-routing.test.js +94 -0
- package/test/telegram-task-dispatcher.test.js +150 -23
- package/test/telegram-text-artifact.test.js +13 -2
- package/test/telegram-tools-command.test.js +47 -0
- package/test/telegram-workspace-topic-store.test.js +124 -0
- package/test/tool-registry-run.test.js +41 -0
- package/test/weighted-resource-governor.test.js +95 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createChatStateStore,
|
|
5
|
+
createPromptExecutionReceipt,
|
|
6
|
+
drainChatPromptQueue,
|
|
7
|
+
queueChatPrompt
|
|
8
|
+
} from "../src/transport/telegram/chat-queue.js";
|
|
9
|
+
|
|
10
|
+
test("execution receipts start only when their queued prompt begins", async () => {
|
|
11
|
+
const state = createChatStateStore().get("chat-1");
|
|
12
|
+
const starts = [];
|
|
13
|
+
const receipt = createPromptExecutionReceipt(() => starts.push("second"));
|
|
14
|
+
queueChatPrompt(state, "second", { receipt });
|
|
15
|
+
let releaseFirst;
|
|
16
|
+
const first = new Promise((resolve) => { releaseFirst = resolve; });
|
|
17
|
+
|
|
18
|
+
const draining = drainChatPromptQueue({
|
|
19
|
+
chatState: state,
|
|
20
|
+
initialPrompt: "first",
|
|
21
|
+
processPrompt: async ({ prompt }) => {
|
|
22
|
+
if (prompt === "first") await first;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
27
|
+
assert.deepEqual(starts, []);
|
|
28
|
+
releaseFirst();
|
|
29
|
+
await draining;
|
|
30
|
+
assert.deepEqual(starts, ["second"]);
|
|
31
|
+
await receipt.promise;
|
|
32
|
+
});
|
|
@@ -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);
|
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
selectChatSpeed,
|
|
11
11
|
selectChatThinkingLevel
|
|
12
12
|
} from "../src/core/agent/model-selection.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
applyConfigDefaults,
|
|
15
|
+
piConfigDefaults,
|
|
16
|
+
telegramConfigDefaults,
|
|
17
|
+
toolExecutionConfigDefaults
|
|
18
|
+
} from "../src/core/config/config-defaults.js";
|
|
14
19
|
import {
|
|
15
20
|
clampModelThinkingLevel,
|
|
16
21
|
listModelThinkingLevels,
|
|
@@ -295,6 +300,9 @@ test("centralizes Telegram and Pi defaults in config", () => {
|
|
|
295
300
|
|
|
296
301
|
assert.equal(config.telegram.modelPickerPageSize, telegramConfigDefaults.modelPickerPageSize);
|
|
297
302
|
assert.equal(config.telegram.busyMessageMode, "steer");
|
|
303
|
+
assert.equal(config.toolExecution.defaultCapacity, toolExecutionConfigDefaults.defaultCapacity);
|
|
304
|
+
assert.equal(config.toolExecution.maxQueuedPerClass, 100);
|
|
305
|
+
assert.deepEqual(config.toolExecution.capacities, {});
|
|
298
306
|
assert.equal(config.pi.thinkingLevel, piConfigDefaults.thinkingLevel);
|
|
299
307
|
assert.equal(config.pi.speed, piConfigDefaults.speed);
|
|
300
308
|
});
|
|
@@ -7,7 +7,7 @@ async function manifest(name) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
test("official orchestrators declare their hard tool dependencies", async () => {
|
|
10
|
-
assert.deepEqual((await manifest("magnific-mcp")).toolDependencies, { "mcp-client": "^0.
|
|
10
|
+
assert.deepEqual((await manifest("magnific-mcp")).toolDependencies, { "mcp-client": "^0.2.0" });
|
|
11
11
|
assert.deepEqual((await manifest("campaign-draft-runner")).toolDependencies, {
|
|
12
12
|
"pr-campaign": "^0.1.0",
|
|
13
13
|
"gmail-workspace": "^0.1.0"
|
package/test/paths.test.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createIpcSocketPath,
|
|
11
11
|
getChatArtifactsDir,
|
|
12
12
|
getChatSessionSeedFile,
|
|
13
|
+
getChatTelegramWorkspacesFile,
|
|
13
14
|
getChatToolConfigPath,
|
|
14
15
|
getChatToolUsageFile,
|
|
15
16
|
getChatToolStateDir,
|
|
@@ -39,6 +40,13 @@ test("keeps tool usage scoped below the chat state directory", () => {
|
|
|
39
40
|
);
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
test("keeps Telegram workspace topics scoped below the owner chat state directory", () => {
|
|
44
|
+
assert.equal(
|
|
45
|
+
getChatTelegramWorkspacesFile("chat-1"),
|
|
46
|
+
path.join(chatsDir, "chat-1", "state", "telegram-workspaces.json")
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
42
50
|
test("keeps chat tool state and config paths scoped below the chat directory for normal names", () => {
|
|
43
51
|
assert.equal(
|
|
44
52
|
getChatToolStateDir("chat-1", "strudel-agent"),
|
|
@@ -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
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
3
4
|
import { createTelegramRestartHandler, telegramCommands } from "../src/transport/telegram/bot.js";
|
|
4
5
|
import { handoffServiceRestart, restartService, serviceEntryFile, waitForServiceStop } from "../src/runtime/service-manager.js";
|
|
6
|
+
import { createServiceSupervisor } from "../src/runtime/service-supervisor.js";
|
|
5
7
|
|
|
6
8
|
test("registers maintenance as native Telegram commands", () => {
|
|
7
9
|
assert.equal(
|
|
@@ -225,6 +227,52 @@ test("restart does not start a second service when shutdown times out", async ()
|
|
|
225
227
|
assert.equal(started, false);
|
|
226
228
|
});
|
|
227
229
|
|
|
230
|
+
test("accepts restart handoff from a worker owned by the active supervisor", async () => {
|
|
231
|
+
let spawned = false;
|
|
232
|
+
await handoffServiceRestart({}, {
|
|
233
|
+
ensureHome: async () => {},
|
|
234
|
+
getStatus: async () => ({ running: true, pid: 77 }),
|
|
235
|
+
openLog: async () => ({ fd: 17, close: async () => {} }),
|
|
236
|
+
spawnProcess: () => ({ pid: 84, unref() { spawned = true; } }),
|
|
237
|
+
environment: { ARISA_SUPERVISOR_PID: "77" },
|
|
238
|
+
currentPid: 41
|
|
239
|
+
});
|
|
240
|
+
assert.equal(spawned, true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("supervisor restarts an unexpectedly exited worker and forwards shutdown", async () => {
|
|
244
|
+
const children = [];
|
|
245
|
+
const delays = [];
|
|
246
|
+
const spawnProcess = () => {
|
|
247
|
+
const child = new EventEmitter();
|
|
248
|
+
child.pid = 100 + children.length;
|
|
249
|
+
child.kill = (signal) => {
|
|
250
|
+
child.killedWith = signal;
|
|
251
|
+
queueMicrotask(() => child.emit("exit", 0, signal));
|
|
252
|
+
};
|
|
253
|
+
children.push(child);
|
|
254
|
+
return child;
|
|
255
|
+
};
|
|
256
|
+
const supervisor = createServiceSupervisor({
|
|
257
|
+
command: "node",
|
|
258
|
+
args: ["worker.js"],
|
|
259
|
+
restartLimit: 2,
|
|
260
|
+
restartBackoffMs: 5,
|
|
261
|
+
restartBackoffMaxMs: 20,
|
|
262
|
+
stableRuntimeMs: 60_000,
|
|
263
|
+
spawnProcess,
|
|
264
|
+
wait: async (ms) => { delays.push(ms); }
|
|
265
|
+
});
|
|
266
|
+
const running = supervisor.start();
|
|
267
|
+
children[0].emit("exit", 1, null);
|
|
268
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
269
|
+
assert.equal(children.length, 2);
|
|
270
|
+
assert.deepEqual(delays, [5]);
|
|
271
|
+
await supervisor.stop();
|
|
272
|
+
await running;
|
|
273
|
+
assert.equal(children[1].killedWith, "SIGTERM");
|
|
274
|
+
});
|
|
275
|
+
|
|
228
276
|
test("requires explicit positive shutdown timing policy", async () => {
|
|
229
277
|
await assert.rejects(
|
|
230
278
|
waitForServiceStop({ timeoutMs: 0, pollIntervalMs: 1 }),
|
|
@@ -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
|
+
});
|