arisa 5.1.24 → 5.1.49

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 (38) hide show
  1. package/package.json +2 -3
  2. package/src/core/agent/agent-manager.js +132 -70
  3. package/src/core/agent/pi-runtime.js +0 -8
  4. package/src/core/agent/system-shell-tool.js +13 -2
  5. package/src/core/artifacts/artifact-store.js +17 -18
  6. package/src/core/config/config-defaults.js +2 -2
  7. package/src/core/conversation/session-seed-store.js +85 -0
  8. package/src/core/tools/ipc-client.js +0 -2
  9. package/src/core/tools/tool-output-materializer.js +41 -0
  10. package/src/core/tools/tool-registry.js +96 -23
  11. package/src/official-tools.lock.json +145 -93
  12. package/src/runtime/doctor.js +1 -4
  13. package/src/runtime/headless-tool-executor.js +2 -32
  14. package/src/runtime/paths.js +7 -1
  15. package/src/runtime/restart-receipt.js +90 -0
  16. package/src/transport/telegram/bot.js +390 -1034
  17. package/src/transport/telegram/chat-queue.js +132 -0
  18. package/src/transport/telegram/media.js +2 -2
  19. package/src/transport/telegram/model-callback.js +211 -0
  20. package/src/transport/telegram/model-controls.js +164 -0
  21. package/src/transport/telegram/prompt-builders.js +372 -0
  22. package/src/transport/telegram/task-dispatcher.js +94 -0
  23. package/src/transport/telegram/update-command.js +1 -1
  24. package/src/transport/telegram/workspace-group.js +83 -0
  25. package/test/agent-tool-policy.test.js +7 -1
  26. package/test/context-and-task-bounds.test.js +9 -7
  27. package/test/doctor.test.js +2 -4
  28. package/test/model-selection.test.js +47 -1
  29. package/test/official-tool-dependencies.test.js +2 -0
  30. package/test/paths.test.js +4 -4
  31. package/test/restart-receipt.test.js +39 -0
  32. package/test/session-start-operational-notes.test.js +47 -0
  33. package/test/telegram-prompt-builders.test.js +33 -0
  34. package/test/telegram-task-dispatcher.test.js +102 -0
  35. package/test/telegram-workspace-group.test.js +76 -0
  36. package/test/tool-registry-run.test.js +62 -0
  37. package/test/topic-initialization.test.js +66 -0
  38. package/src/core/conversation/conversation-history-store.js +0 -142
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "5.1.24",
3
+ "version": "5.1.49",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -42,8 +42,7 @@
42
42
  "dependencies": {
43
43
  "@earendil-works/pi-coding-agent": "0.80.6",
44
44
  "@sinclair/typebox": "^0.34.41",
45
- "grammy": "^1.42.0",
46
- "typebox": "1.1.38"
45
+ "grammy": "^1.42.0"
47
46
  },
48
47
  "scripts": {
49
48
  "start": "node src/index.js",
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
- import { readFile, stat, unlink } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+ import { readFile, stat } from "node:fs/promises";
3
4
  import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, defineTool } from "@earendil-works/pi-coding-agent";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
@@ -10,9 +11,10 @@ import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
10
11
  import { createSystemShellTool } from "./system-shell-tool.js";
11
12
  import { clampModelThinkingLevel } from "./pi-runtime.js";
12
13
  import { clampModelSpeed, createModelSpeedController } from "./model-speed.js";
13
- import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
14
+ import { arisaHomeDir, getChatPiSessionsDir, sessionStartOperationalNotesFile } from "../../runtime/paths.js";
14
15
  import { searchOfficialToolCatalog } from "../tools/official-tool-catalog.js";
15
16
  import { ToolResourceNoteStore } from "../tools/tool-resource-note-store.js";
17
+ import { materializeToolOutput } from "../tools/tool-output-materializer.js";
16
18
 
17
19
  const piValidationTimeoutMs = 60_000;
18
20
  const arisaToolNames = [
@@ -25,33 +27,38 @@ const arisaToolNames = [
25
27
  "list_scheduled_tasks",
26
28
  "cancel_scheduled_task",
27
29
  "cancel_all_scheduled_tasks",
30
+ "create_telegram_topic",
31
+ "initialize_telegram_topic",
28
32
  "send_artifact"
29
33
  ];
30
34
 
31
- function messageText(content) {
32
- if (typeof content === "string") return content.trim();
33
- if (!Array.isArray(content)) return "";
34
- return content
35
- .filter((item) => item?.type === "text" && typeof item.text === "string")
36
- .map((item) => item.text.trim())
37
- .filter(Boolean)
38
- .join("\n");
35
+ const operationalNoteMaxChars = 220;
36
+
37
+ function normalizeOperationalNote(note) {
38
+ const text = typeof note === "string" ? note : note?.text;
39
+ const trimmed = String(text || "").replace(/\s+/g, " ").trim();
40
+ if (!trimmed) return "";
41
+ return trimmed.length <= operationalNoteMaxChars ? trimmed : `${trimmed.slice(0, operationalNoteMaxChars - 1).trim()}…`;
42
+ }
43
+
44
+ export function loadSessionStartOperationalNotes() {
45
+ try {
46
+ const raw = readFileSync(sessionStartOperationalNotesFile, "utf8");
47
+ const parsed = JSON.parse(raw);
48
+ const notes = Array.isArray(parsed) ? parsed : parsed?.notes;
49
+ if (!Array.isArray(notes)) return [];
50
+ return notes.map(normalizeOperationalNote).filter(Boolean).slice(0, 20);
51
+ } catch {
52
+ return [];
53
+ }
39
54
  }
40
55
 
41
- export function formatPortableSessionHistory(messages = []) {
42
- return messages
43
- .map((message) => {
44
- const text = messageText(message?.content);
45
- if (!text) return "";
46
- const role = message.role === "assistant"
47
- ? "Assistant"
48
- : message.role === "user"
49
- ? "User"
50
- : (message.customType ? `Session memory (${message.customType})` : "Session context");
51
- return `${role}:\n${text}`;
52
- })
53
- .filter(Boolean)
54
- .join("\n\n");
56
+ function formatSessionStartOperationalNotes(notes) {
57
+ if (!notes.length) return "";
58
+ return [
59
+ "Durable operating notes for this Arisa session:",
60
+ ...notes.map((note) => `- ${note}`)
61
+ ].join("\n");
55
62
  }
56
63
 
57
64
  const estimatedImageTokens = 1_200;
@@ -102,6 +109,16 @@ function summarizeRetainedContext(messages = []) {
102
109
  };
103
110
  }
104
111
 
112
+ function guardTools(tools, accessGuard) {
113
+ return tools.map((tool) => ({
114
+ ...tool,
115
+ execute: async (...args) => {
116
+ await accessGuard();
117
+ return tool.execute(...args);
118
+ }
119
+ }));
120
+ }
121
+
105
122
  function closeAgentSession(session) {
106
123
  if (session?.close) return session.close();
107
124
  if (session?.dispose) return session.dispose();
@@ -301,7 +318,7 @@ export class AgentManager {
301
318
  this.closeCachedSession(String(chatId));
302
319
  }
303
320
 
304
- async getRuntimeDiagnostic({ contextInspectionTimeoutMs } = {}) {
321
+ async getRuntimeDiagnostic() {
305
322
  const contexts = await Promise.all([...this.sessions.entries()].map(async ([chatId, context]) => {
306
323
  const base = { chatId };
307
324
  try {
@@ -322,7 +339,6 @@ export class AgentManager {
322
339
  harness: "pi",
323
340
  sessions: this.sessions.size,
324
341
  closingSessions: this.sessionClosePromises.size,
325
- managedProcessIds: [],
326
342
  contexts
327
343
  };
328
344
  }
@@ -338,6 +354,15 @@ export class AgentManager {
338
354
  sessionDir,
339
355
  handoff?.parentSession ? { parentSession: handoff.parentSession } : undefined
340
356
  );
357
+ const operationalNotes = formatSessionStartOperationalNotes(loadSessionStartOperationalNotes());
358
+ if (operationalNotes) {
359
+ sessionManager.appendCustomMessageEntry(
360
+ "arisa-operational-notes",
361
+ operationalNotes,
362
+ false,
363
+ { source: "session-start" }
364
+ );
365
+ }
341
366
  if (handoff?.text) {
342
367
  sessionManager.appendCustomMessageEntry(
343
368
  "arisa-session-handoff",
@@ -388,7 +413,7 @@ export class AgentManager {
388
413
  return this.validatePiAgent(config);
389
414
  }
390
415
 
391
- async getSessionContext(chatId, telegram) {
416
+ async getSessionContext(chatId, telegram, { scopeChatId = chatId, accessGuard = async () => {} } = {}) {
392
417
  const sessionKey = String(chatId);
393
418
  const modelSelection = resolveChatModelSelection(this.config, sessionKey);
394
419
  const effectiveModelId = modelSelection.model;
@@ -406,6 +431,8 @@ export class AgentManager {
406
431
  this.logger?.log("agent", `updating speed for chat ${sessionKey}: ${existing.speedController.speed}x -> ${desiredSpeed}x`);
407
432
  existing.speedController.setSpeed(desiredSpeed);
408
433
  }
434
+ existing.telegramTarget.current = telegram;
435
+ existing.accessGuardTarget.current = accessGuard;
409
436
  this.logger?.log("agent", `reusing session for chat ${sessionKey}`);
410
437
  return existing;
411
438
  }
@@ -438,10 +465,26 @@ export class AgentManager {
438
465
  );
439
466
  const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
440
467
  this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId} effort ${thinkingLevel} speed ${speed}x`);
441
- const customTools = [
442
- ...this.createTools(telegram, chatId, policy),
443
- createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
444
- ];
468
+ const telegramTarget = { current: telegram };
469
+ const accessGuardTarget = { current: accessGuard };
470
+ const telegramProxy = {
471
+ sendMedia: (...args) => telegramTarget.current.sendMedia(...args),
472
+ createForumTopic: (...args) => telegramTarget.current.createForumTopic(...args),
473
+ initializeForumTopic: (...args) => telegramTarget.current.initializeForumTopic(...args),
474
+ prepareRestartReceipt: (...args) => telegramTarget.current.prepareRestartReceipt(...args),
475
+ cancelRestartReceipt: (...args) => telegramTarget.current.cancelRestartReceipt(...args),
476
+ getTaskContext: (...args) => telegramTarget.current.getTaskContext?.(...args) || null
477
+ };
478
+ const assertAccess = () => accessGuardTarget.current();
479
+ const customTools = guardTools([
480
+ ...this.createTools(telegramProxy, scopeChatId, policy),
481
+ createSystemShellTool({
482
+ workspaceDir: policy.workspaceDir,
483
+ shell: policy.shell,
484
+ beforeRestart: (summary) => telegramProxy.prepareRestartReceipt(summary),
485
+ cancelRestart: (receiptId) => telegramProxy.cancelRestartReceipt(receiptId)
486
+ })
487
+ ], assertAccess);
445
488
  const settingsManager = createPiSettingsManager(this.config);
446
489
  const resourceLoader = await createArisaResourceLoader({
447
490
  cwd: policy.workspaceDir,
@@ -473,7 +516,14 @@ export class AgentManager {
473
516
  })}`);
474
517
  }
475
518
 
476
- const ctx = { session, modelId: effectiveModelId, modelKey: effectiveModelKey, speedController };
519
+ const ctx = {
520
+ session,
521
+ modelId: effectiveModelId,
522
+ modelKey: effectiveModelKey,
523
+ speedController,
524
+ telegramTarget,
525
+ accessGuardTarget
526
+ };
477
527
  this.sessions.set(sessionKey, ctx);
478
528
  if (isNewSession) {
479
529
  this.pendingNewSessions.delete(sessionKey);
@@ -505,10 +555,9 @@ export class AgentManager {
505
555
  ]);
506
556
  }
507
557
 
508
- async runTool({ name, request, chatId }) {
558
+ async runTool({ name, request, chatId, taskContext = null }) {
509
559
  await this.toolRegistry.load();
510
560
  this.logger?.log("agent", `run_tool ${name}`);
511
- const chatArtifactStore = this.artifactStore.forChat(chatId);
512
561
  const resourceId = String(request?.resourceId || "").trim();
513
562
  const resourceNote = resourceId
514
563
  ? await this.resourceNotes.get(chatId, name, resourceId)
@@ -516,41 +565,14 @@ export class AgentManager {
516
565
  const enrichedRequest = resourceNote ? { ...request, resourceId, resourceNote } : request;
517
566
  const result = await this.toolRegistry.run({ name, request: enrichedRequest, chatId });
518
567
 
519
- if (result.output?.text) {
520
- const outArtifact = await chatArtifactStore.createText({
521
- text: result.output.text,
522
- source: { type: "tool", toolName: name },
523
- metadata: { tool: name }
524
- });
525
- result.output.artifactId = outArtifact.id;
526
- }
527
-
528
- if (result.output?.filePath) {
529
- const generated = await chatArtifactStore.createFromFile({
530
- originalPath: result.output.filePath,
531
- fileName: result.output.fileName || path.basename(result.output.filePath),
532
- kind: result.output.kind || "file",
533
- mimeType: result.output.mimeType || "application/octet-stream",
534
- source: { type: "tool", toolName: name },
535
- metadata: { tool: name, delivery: result.output.delivery }
536
- });
537
- result.output.artifactId = generated.id;
538
- await unlink(result.output.filePath).catch(() => {});
539
- }
540
-
541
- if (result.asyncTask || result.asyncTasks?.length) {
542
- const scheduled = await this.taskStore.addMany(
543
- result.asyncTasks || [result.asyncTask],
544
- {
545
- payload: { chatId },
546
- source: { type: "tool", toolName: name, chatId }
547
- }
548
- );
549
- result.asyncTasks = scheduled;
550
- delete result.asyncTask;
551
- }
552
-
553
- return result;
568
+ return materializeToolOutput({
569
+ result,
570
+ name,
571
+ chatId,
572
+ artifactStore: this.artifactStore,
573
+ taskStore: this.taskStore,
574
+ taskContext
575
+ });
554
576
  }
555
577
 
556
578
  createTools(telegram, chatId, policy = buildPiToolPolicy({ config: this.config, customToolNames: arisaToolNames })) {
@@ -688,7 +710,8 @@ export class AgentManager {
688
710
  resourceId: params.resourceId,
689
711
  args: params.args || {}
690
712
  },
691
- chatId
713
+ chatId,
714
+ taskContext: telegram.getTaskContext()
692
715
  });
693
716
 
694
717
  if (params.deliver && result.output?.artifactId) {
@@ -757,6 +780,45 @@ export class AgentManager {
757
780
  };
758
781
  }
759
782
  }),
783
+ defineTool({
784
+ name: "create_telegram_topic",
785
+ label: "Create Telegram topic",
786
+ description: "Create and initialize a new topic in the current owner-only Telegram forum. Topic names are dynamic, and context seeds the isolated session without copying unrelated history.",
787
+ parameters: Type.Object({
788
+ name: Type.String({ minLength: 1, maxLength: 128 }),
789
+ context: Type.String({ minLength: 1, maxLength: 4000 })
790
+ }),
791
+ execute: async (_id, params) => {
792
+ if (typeof telegram.createForumTopic !== "function") {
793
+ const result = { ok: false, error: "Telegram topic creation is unavailable in this chat." };
794
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
795
+ }
796
+ const result = await telegram.createForumTopic(params.name.trim(), params.context.trim());
797
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
798
+ }
799
+ }),
800
+ defineTool({
801
+ name: "initialize_telegram_topic",
802
+ label: "Initialize Telegram topic",
803
+ description: "Seed or replace the isolated context of an existing topic in the current owner-only Telegram forum.",
804
+ parameters: Type.Object({
805
+ messageThreadId: Type.Integer({ minimum: 2 }),
806
+ name: Type.String({ minLength: 1, maxLength: 128 }),
807
+ context: Type.String({ minLength: 1, maxLength: 4000 })
808
+ }),
809
+ execute: async (_id, params) => {
810
+ if (typeof telegram.initializeForumTopic !== "function") {
811
+ const result = { ok: false, error: "Telegram topic initialization is unavailable in this chat." };
812
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
813
+ }
814
+ const result = await telegram.initializeForumTopic({
815
+ messageThreadId: params.messageThreadId,
816
+ name: params.name.trim(),
817
+ context: params.context.trim()
818
+ });
819
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
820
+ }
821
+ }),
760
822
  defineTool({
761
823
  name: "send_artifact",
762
824
  label: "Send artifact",
@@ -101,11 +101,3 @@ export function clampModelThinkingLevel(model, level) {
101
101
  export function modelSupportsThinking(model) {
102
102
  return listModelThinkingLevels(model).some((level) => level !== "off");
103
103
  }
104
-
105
- export function findPiModel({ provider, model, apiKey } = {}) {
106
- const runtime = createPiRuntime({ provider, apiKey });
107
- return {
108
- ...runtime,
109
- model: provider && model ? runtime.modelRegistry.find(provider, model) : null
110
- };
111
- }
@@ -164,25 +164,36 @@ async function runShellCommand({ command, cwd, shellPath, timeoutMs }) {
164
164
  });
165
165
  }
166
166
 
167
- export function createSystemShellTool({ workspaceDir, shell = {} }) {
167
+ export function isArisaRestartCommand(command) {
168
+ return /(?:^|[;&|]\s*)arisa\s+restart(?:\s*(?:$|[;&|]))/i.test(String(command || ""));
169
+ }
170
+
171
+ export function createSystemShellTool({ workspaceDir, shell = {}, beforeRestart, cancelRestart }) {
168
172
  return defineTool({
169
173
  name: "system_shell",
170
174
  label: "System Shell",
171
175
  description: "Run a command in the active Arisa workspace using the native system shell: PowerShell on Windows, and sh/bash-compatible shell on Unix.",
172
176
  parameters: Type.Object({
173
177
  command: Type.String({ description: "Command to execute in the active workspace." }),
174
- timeoutMs: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds for this command." }))
178
+ timeoutMs: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds for this command." })),
179
+ restartSummary: Type.Optional(Type.String({ maxLength: 500, description: "Concrete user-facing result to report after an arisa restart succeeds." }))
175
180
  }),
176
181
  execute: async (_id, params) => {
177
182
  const timeoutMs = Number.isFinite(Number(params.timeoutMs)) && Number(params.timeoutMs) > 0
178
183
  ? Math.floor(Number(params.timeoutMs))
179
184
  : (shell.timeoutMs || defaultTimeoutMs);
185
+ const restartReceipt = isArisaRestartCommand(params.command) && typeof beforeRestart === "function"
186
+ ? await beforeRestart(params.restartSummary)
187
+ : null;
180
188
  const result = await runShellCommand({
181
189
  command: params.command,
182
190
  cwd: workspaceDir,
183
191
  shellPath: shell.shellPath,
184
192
  timeoutMs
185
193
  });
194
+ if (!result.ok && restartReceipt?.id && typeof cancelRestart === "function") {
195
+ await cancelRestart(restartReceipt.id).catch(() => {});
196
+ }
186
197
  const details = {
187
198
  stdout: result.stdout,
188
199
  stderr: result.stderr,
@@ -86,14 +86,14 @@ class ChatArtifactStore {
86
86
  return artifact;
87
87
  }
88
88
 
89
- async createFromFile({ originalPath, fileName, kind, mimeType, source, metadata = {} }) {
89
+ async createFileArtifact({ fileName, kind, mimeType, source, metadata = {}, writeFileContent }) {
90
90
  await this.init();
91
91
  await this.reload();
92
92
  const artifactId = id();
93
93
  const dir = path.join(this.rootDir, artifactId);
94
94
  await mkdir(dir, { recursive: true });
95
95
  const destPath = path.join(dir, fileName);
96
- await copyArtifactFile(originalPath, destPath, mimeType);
96
+ await writeFileContent(destPath);
97
97
  const artifact = {
98
98
  id: artifactId,
99
99
  chatId: this.chatId,
@@ -109,27 +109,26 @@ class ChatArtifactStore {
109
109
  return artifact;
110
110
  }
111
111
 
112
+ async createFromFile({ originalPath, fileName, kind, mimeType, source, metadata = {} }) {
113
+ return this.createFileArtifact({
114
+ fileName,
115
+ kind,
116
+ mimeType,
117
+ source,
118
+ metadata,
119
+ writeFileContent: (destPath) => copyArtifactFile(originalPath, destPath, mimeType)
120
+ });
121
+ }
122
+
112
123
  async createGeneratedFile({ fileName, content, kind, mimeType, source, metadata = {} }) {
113
- await this.init();
114
- await this.reload();
115
- const artifactId = id();
116
- const dir = path.join(this.rootDir, artifactId);
117
- await mkdir(dir, { recursive: true });
118
- const destPath = path.join(dir, fileName);
119
- await writeArtifactFile(destPath, content);
120
- const artifact = {
121
- id: artifactId,
122
- chatId: this.chatId,
124
+ return this.createFileArtifact({
125
+ fileName,
123
126
  kind,
124
127
  mimeType,
125
- path: destPath,
126
128
  source,
127
129
  metadata,
128
- createdAt: new Date().toISOString()
129
- };
130
- this.items.push(artifact);
131
- await this.saveIndex();
132
- return artifact;
130
+ writeFileContent: (destPath) => writeArtifactFile(destPath, content)
131
+ });
133
132
  }
134
133
 
135
134
  async get(artifactId) {
@@ -18,11 +18,11 @@ export const daemonConfigDefaults = Object.freeze({
18
18
 
19
19
  export const telegramConfigDefaults = Object.freeze({
20
20
  modelPickerPageSize: 8,
21
- busyMessageMode: "steer"
21
+ busyMessageMode: "steer",
22
+ ownerWorkspaceGroups: Object.freeze({})
22
23
  });
23
24
 
24
25
  export const doctorConfigDefaults = Object.freeze({
25
- contextInspectionTimeoutMs: 5_000,
26
26
  contextWarningPercent: 70,
27
27
  contextCriticalPercent: 90,
28
28
  contextInefficientMinTokens: 32_000,
@@ -0,0 +1,85 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ getChatLegacyConversationHistoryFile,
5
+ getChatSessionSeedFile
6
+ } from "../../runtime/paths.js";
7
+
8
+ function parseRecords(contents) {
9
+ return String(contents || "")
10
+ .replace(/^\uFEFF/, "")
11
+ .split(/\r?\n/)
12
+ .filter(Boolean)
13
+ .map((line) => JSON.parse(line));
14
+ }
15
+
16
+ function seedText(record) {
17
+ return String(record?.context || record?.history || "").trim();
18
+ }
19
+
20
+ export class SessionSeedStore {
21
+ constructor({
22
+ seedFile = getChatSessionSeedFile,
23
+ legacyFile = getChatLegacyConversationHistoryFile
24
+ } = {}) {
25
+ this.seedFile = seedFile;
26
+ this.legacyFile = legacyFile;
27
+ this.locks = new Map();
28
+ }
29
+
30
+ async withChatLock(chatId, work) {
31
+ const key = String(chatId);
32
+ const previous = this.locks.get(key) || Promise.resolve();
33
+ const current = previous.catch(() => {}).then(work);
34
+ this.locks.set(key, current);
35
+ try {
36
+ return await current;
37
+ } finally {
38
+ if (this.locks.get(key) === current) this.locks.delete(key);
39
+ }
40
+ }
41
+
42
+ async write(chatId, record = null) {
43
+ const file = this.seedFile(chatId);
44
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
45
+ const body = record ? `${JSON.stringify(record)}\n` : "";
46
+ await writeFile(file, body, { encoding: "utf8", mode: 0o600 });
47
+ }
48
+
49
+ async set(chatId, context) {
50
+ const normalized = String(context || "").trim();
51
+ return this.withChatLock(chatId, async () => {
52
+ await this.write(chatId, normalized ? { kind: "seed", context: normalized } : null);
53
+ return Boolean(normalized);
54
+ });
55
+ }
56
+
57
+ async clear(chatId) {
58
+ return this.withChatLock(chatId, () => this.write(chatId));
59
+ }
60
+
61
+ async readPendingRecords(chatId) {
62
+ try {
63
+ return parseRecords(await readFile(this.seedFile(chatId), "utf8"));
64
+ } catch (error) {
65
+ if (error?.code !== "ENOENT") throw error;
66
+ }
67
+ try {
68
+ return parseRecords(await readFile(this.legacyFile(chatId), "utf8"));
69
+ } catch (error) {
70
+ if (error?.code === "ENOENT") return [];
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ async consume(chatId) {
76
+ return this.withChatLock(chatId, async () => {
77
+ const records = await this.readPendingRecords(chatId);
78
+ const pendingSeed = records.length === 1 && records[0]?.kind === "seed"
79
+ ? seedText(records[0])
80
+ : "";
81
+ await this.write(chatId);
82
+ return pendingSeed;
83
+ });
84
+ }
85
+ }
@@ -56,7 +56,6 @@ function requestIpc({ socketPath, request, timeoutMs = DEFAULT_TIMEOUT_MS }) {
56
56
  export function createArisaClient({
57
57
  toolName,
58
58
  chatId = null,
59
- capabilityToken = process.env.ARISA_IPC_TOKEN || "",
60
59
  socketPath = process.env.ARISA_IPC_SOCKET || arisaIpcSocketFile
61
60
  } = {}) {
62
61
  if (typeof toolName !== "string" || !toolName.trim()) {
@@ -71,7 +70,6 @@ export function createArisaClient({
71
70
  method,
72
71
  toolName,
73
72
  chatId,
74
- capabilityToken,
75
73
  params
76
74
  }
77
75
  });
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+ import { unlink } from "node:fs/promises";
3
+
4
+ export async function materializeToolOutput({ result, name, chatId, artifactStore, taskStore, taskContext = null }) {
5
+ const chatArtifactStore = artifactStore.forChat(chatId);
6
+
7
+ if (result.output?.text) {
8
+ const artifact = await chatArtifactStore.createText({
9
+ text: result.output.text,
10
+ source: { type: "tool", toolName: name },
11
+ metadata: { tool: name }
12
+ });
13
+ result.output.artifactId = artifact.id;
14
+ }
15
+
16
+ if (result.output?.filePath) {
17
+ const generated = await chatArtifactStore.createFromFile({
18
+ originalPath: result.output.filePath,
19
+ fileName: result.output.fileName || path.basename(result.output.filePath),
20
+ kind: result.output.kind || "file",
21
+ mimeType: result.output.mimeType || "application/octet-stream",
22
+ source: { type: "tool", toolName: name },
23
+ metadata: { tool: name, delivery: result.output.delivery }
24
+ });
25
+ result.output.artifactId = generated.id;
26
+ await unlink(result.output.filePath).catch(() => {});
27
+ }
28
+
29
+ if (result.asyncTask || result.asyncTasks?.length) {
30
+ result.asyncTasks = await taskStore.addMany(result.asyncTasks || [result.asyncTask], {
31
+ payload: {
32
+ chatId,
33
+ ...(taskContext ? { telegramContext: taskContext } : {})
34
+ },
35
+ source: { type: "tool", toolName: name, chatId }
36
+ });
37
+ delete result.asyncTask;
38
+ }
39
+
40
+ return result;
41
+ }