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.
Files changed (37) hide show
  1. package/AGENTS.md +0 -2
  2. package/package.json +1 -1
  3. package/src/core/agent/agent-manager.js +39 -489
  4. package/src/core/agent/agent-session-lifecycle.js +181 -0
  5. package/src/core/agent/pi-capability-tools.js +183 -0
  6. package/src/core/artifacts/artifact-store.js +73 -17
  7. package/src/core/capabilities/capability-service.js +340 -0
  8. package/src/core/tasks/task-routing.js +7 -0
  9. package/src/core/tasks/task-runner.js +53 -0
  10. package/src/core/tasks/task-store.js +316 -92
  11. package/src/core/tools/tool-output-materializer.js +5 -5
  12. package/src/official-tools.lock.json +7 -4
  13. package/src/runtime/arisa-capabilities.js +51 -242
  14. package/src/runtime/create-app.js +10 -1
  15. package/src/runtime/create-headless-app.js +5 -2
  16. package/src/transport/telegram/bot.js +112 -368
  17. package/src/transport/telegram/chat-queue.js +72 -6
  18. package/src/transport/telegram/prompt-builders.js +9 -0
  19. package/src/transport/telegram/task-dispatcher.js +73 -36
  20. package/src/transport/telegram/telegram-auth-controller.js +180 -0
  21. package/src/transport/telegram/telegram-session-bridge.js +170 -0
  22. package/src/transport/telegram/telegram-tools-command.js +28 -0
  23. package/src/transport/telegram/telegram-workspace-controller.js +66 -0
  24. package/test/agent-session-lifecycle.test.js +58 -0
  25. package/test/artifact-store.test.js +38 -2
  26. package/test/capabilities-security.test.js +58 -0
  27. package/test/context-and-task-bounds.test.js +76 -1
  28. package/test/device-code-message.test.js +9 -0
  29. package/test/media-caption.test.js +1 -1
  30. package/test/pi-capability-tools.test.js +65 -0
  31. package/test/session-start-operational-notes.test.js +1 -1
  32. package/test/task-idempotency.test.js +40 -0
  33. package/test/task-routing.test.js +62 -0
  34. package/test/task-store.test.js +178 -6
  35. package/test/telegram-task-dispatcher.test.js +99 -23
  36. package/test/telegram-text-artifact.test.js +13 -2
  37. package/test/telegram-tools-command.test.js +47 -0
@@ -1,15 +1,12 @@
1
- import { Bot, InputFile } from "grammy";
2
- import path from "node:path";
1
+ import { Bot } from "grammy";
3
2
  import { authorizeChat } from "./auth.js";
4
3
  import { captureIncomingArtifact } from "./media.js";
5
- import { buildDeviceCodeTelegramMessage } from "./device-code-message.js";
6
4
  import { renderTelegramHtml } from "./text-format.js";
7
- import { buildPiAuthRecoveryBlockedMessage, buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue, getPiAuthStatus } from "../../core/agent/auth-flow.js";
8
- import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
5
+ import { buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue } from "../../core/agent/auth-flow.js";
6
+ import { createTelegramAuthController } from "./telegram-auth-controller.js";
9
7
  import { resolveChatSpeed } from "../../core/agent/model-selection.js";
10
8
  import { SessionSeedStore } from "../../core/conversation/session-seed-store.js";
11
9
  import { formatDoctorReport } from "../../runtime/doctor.js";
12
- import { formatToolUsageReport } from "../../runtime/tool-usage-report.js";
13
10
  import { ToolResourceNoteStore } from "../../core/tools/tool-resource-note-store.js";
14
11
  import { formatUpdateReport } from "../../runtime/update-manager.js";
15
12
  import { cancelRestartReceipt, deliverRestartReceipt, prepareRestartReceipt } from "../../runtime/restart-receipt.js";
@@ -17,7 +14,10 @@ import { buildUpdatePicker, createTelegramUpdateCallbackHandler } from "./update
17
14
  import { createTelegramModelControls } from "./model-controls.js";
18
15
  import { createTelegramModelCallbackHandler } from "./model-callback.js";
19
16
  import { createTelegramTaskDispatcher } from "./task-dispatcher.js";
20
- import { resolveTelegramWorkspaceRoute, topicSessionId } from "./workspace-group.js";
17
+ import { createTelegramSessionBridgeController } from "./telegram-session-bridge.js";
18
+ import { createTelegramToolsCommandHandler } from "./telegram-tools-command.js";
19
+ import { createTelegramWorkspaceController } from "./telegram-workspace-controller.js";
20
+ import { resolveTelegramWorkspaceRoute } from "./workspace-group.js";
21
21
  import {
22
22
  buildNewSessionPrompt,
23
23
  buildPrompt,
@@ -30,17 +30,18 @@ import {
30
30
  isSilentReply,
31
31
  normalizeIncomingArtifact,
32
32
  sanitizeSessionHandoff,
33
+ scheduledPromptSpeedOptions,
33
34
  shouldIncludeArtifactReference,
34
35
  withPromptSpeed
35
36
  } from "./prompt-builders.js";
36
37
  import {
37
38
  createChatStateStore,
39
+ createPromptExecutionReceipt,
38
40
  drainChatPromptQueue,
39
41
  queueChatPrompt,
40
42
  resolveTelegramBusyMessageMode,
41
43
  routeBusyPrompt
42
44
  } from "./chat-queue.js";
43
-
44
45
  export {
45
46
  createChatStateStore,
46
47
  drainChatPromptQueue,
@@ -56,6 +57,7 @@ export {
56
57
  collectText,
57
58
  isScheduledTaskPrompt,
58
59
  isSilentReply,
60
+ scheduledPromptSpeedOptions,
59
61
  shouldIncludeArtifactReference,
60
62
  withPromptSpeed
61
63
  } from "./prompt-builders.js";
@@ -73,6 +75,12 @@ export function isProcessableTelegramMessage(message = {}) {
73
75
  );
74
76
  }
75
77
 
78
+ export function resolveIncomingBusyMessageMode({ config, route, message }) {
79
+ if (route?.workspace) return "queue";
80
+ if (typeof message?.text !== "string") return "queue";
81
+ return resolveTelegramBusyMessageMode(config, route?.sessionId);
82
+ }
83
+
76
84
  export function buildTopicInitializationHandoff({ name, context }) {
77
85
  return [
78
86
  `Telegram topic: ${String(name || "").trim()}`,
@@ -167,11 +175,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
167
175
  const perChatState = createChatStateStore();
168
176
  const sessionSeeds = new SessionSeedStore();
169
177
  const notifiedPromptErrors = new WeakSet();
170
- const authRenewals = new Map();
171
- const workspaceRoutes = new WeakMap();
172
- const workspaceGateStates = new Map();
173
- let piAuthIssue = null;
174
178
  let taskTimer = null;
179
+ const { authorizeContext, contextRoute, registerRoute } = createTelegramWorkspaceController({
180
+ config,
181
+ api: bot.api,
182
+ saveConfig
183
+ });
175
184
 
176
185
  const requestRestartWithReceipt = async (ctx, reason = "Telegram restart") => {
177
186
  const route = contextRoute(ctx);
@@ -211,152 +220,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
211
220
  if (error instanceof Error) notifiedPromptErrors.add(error);
212
221
  }
213
222
 
214
- function rememberPiAuthIssue(error) {
215
- const issue = getPiAuthIssue(error);
216
- if (issue) piAuthIssue = issue;
217
- return issue;
218
- }
219
-
220
- async function notifyPiAuthIssueIfNeeded(chatId, error) {
221
- const issue = rememberPiAuthIssue(error);
222
- if (!issue) return false;
223
-
224
- try {
225
- await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue }));
226
- markPromptErrorNotified(error);
227
- return true;
228
- } catch (notifyError) {
229
- logger?.error("telegram", `auth issue notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
230
- return false;
231
- }
232
- }
233
-
234
- function selectTelegramLoginOption(options = []) {
235
- return options.find((option) => /device/i.test(`${option.id} ${option.label}`))
236
- || options.find((option) => /browser|oauth|web/i.test(`${option.id} ${option.label}`))
237
- || options[0]
238
- || null;
239
- }
240
-
241
- async function finishAuthRenewal(chatId, renewal) {
242
- try {
243
- await renewal.promise;
244
- await agentManager.validateAgent();
245
- agentManager.clearSessionCache(chatId);
246
- piAuthIssue = null;
247
- logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
248
- await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, verified: true }));
249
- } catch (error) {
250
- const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
251
- piAuthIssue = issue;
252
- logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
253
- await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue })).catch((notifyError) => {
254
- logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
255
- });
256
- } finally {
257
- authRenewals.delete(chatKey(chatId));
258
- }
259
- }
260
-
261
- async function startAuthRenewal(chatId) {
262
- const key = chatKey(chatId);
263
- const existing = authRenewals.get(key);
264
- if (existing) {
265
- return { started: false, renewal: existing };
266
- }
267
-
268
- const renewal = createPiOAuthLogin({
269
- provider: config.pi.provider,
270
- onSelect: async ({ message, options }) => {
271
- const selected = selectTelegramLoginOption(options);
272
- if (!selected) return undefined;
273
- logger?.log("telegram", `Pi auth option for chat ${chatId}: ${selected.id}`);
274
- await bot.api.sendMessage(chatId, `${message}\nUsing: ${selected.label || selected.id}`);
275
- return selected.id;
276
- },
277
- onAuth: async ({ url, instructions }) => {
278
- await bot.api.sendMessage(chatId, [
279
- instructions || "Open this URL to continue Pi authentication:",
280
- url,
281
- "After login, paste the full redirect URL back here."
282
- ].join("\n"));
283
- },
284
- onDeviceCode: async ({ userCode, verificationUri, expiresInSeconds }) => {
285
- const payload = buildDeviceCodeTelegramMessage({ userCode, verificationUri, expiresInSeconds });
286
- const { text, ...options } = payload;
287
- await bot.api.sendMessage(chatId, text, options);
288
- },
289
- onPrompt: async ({ message, controller }) => {
290
- await bot.api.sendMessage(chatId, `${message}\nReply here with the value.`);
291
- return controller.waitForManualCode();
292
- },
293
- onProgress: (message) => {
294
- if (message) logger?.log("telegram", `Pi auth progress for chat ${chatId}: ${message}`);
295
- }
296
- });
297
-
298
- authRenewals.set(key, renewal);
299
- finishAuthRenewal(chatId, renewal);
300
- return { started: true, renewal };
301
- }
302
-
303
- async function submitAuthRenewalInput(ctx) {
304
- const renewal = authRenewals.get(chatKey(ctx.chat.id));
305
- const text = getIncomingMessageText(ctx.message).trim();
306
- if (!renewal || !renewal.manualInputRequested || !text) return false;
307
-
308
- if (!renewal.submitManualCode(text)) return false;
309
- await ctx.reply("Got it. Finishing Pi login now...");
310
- return true;
311
- }
312
-
313
- function getIncomingChatMeta(ctx) {
314
- return {
315
- languageCode: ctx.from?.language_code || "",
316
- username: ctx.from?.username || "",
317
- firstName: ctx.from?.first_name || "",
318
- lastName: ctx.from?.last_name || ""
319
- };
320
- }
321
-
322
- async function authorizeContext(ctx) {
323
- const route = await resolveTelegramWorkspaceRoute({ config, api: ctx.api, ctx });
324
- if (!route.workspace) {
325
- const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
326
- if (auth.ok) workspaceRoutes.set(ctx, route);
327
- return auth;
328
- }
329
-
330
- const gateKey = String(ctx.chat.id);
331
- const previous = workspaceGateStates.get(gateKey);
332
- if (!route.ok) {
333
- workspaceGateStates.set(gateKey, route.reason || "locked");
334
- if (previous !== (route.reason || "locked")) {
335
- await ctx.reply("Private workspace access is paused because this forum is no longer owner-only.").catch(() => {});
336
- }
337
- return { ok: false, reason: route.reason || "workspace-locked" };
338
- }
339
- if (!(config.telegram.authorizedChatIds || []).includes(route.ownerChatId)) {
340
- return { ok: false, reason: "owner-not-authorized" };
341
- }
342
- workspaceRoutes.set(ctx, route);
343
- workspaceGateStates.set(gateKey, "ready");
344
- if (previous && previous !== "ready") {
345
- await ctx.reply("Private workspace access restored.").catch(() => {});
346
- }
347
- return { ok: true, firstTime: false, workspace: true };
348
- }
349
-
350
- function contextRoute(ctx) {
351
- return workspaceRoutes.get(ctx) || {
352
- ok: true,
353
- workspace: false,
354
- sessionId: String(ctx.chat.id),
355
- scopeChatId: ctx.chat.id,
356
- transportChatId: ctx.chat.id,
357
- threadId: null
358
- };
359
- }
223
+ const authController = createTelegramAuthController({
224
+ config,
225
+ api: bot.api,
226
+ agentManager,
227
+ logger,
228
+ markPromptErrorNotified
229
+ });
360
230
 
361
231
  function getChatState(chatId) {
362
232
  return perChatState.get(chatId);
@@ -409,142 +279,24 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
409
279
  return buildPrompt({ ctx, artifact, transcript, toolResult });
410
280
  }
411
281
 
412
- async function sendTextReply({ sendText, sendDocument, chatId, artifactChatId = chatId, text }) {
413
- const maxInlineReplyLength = 3500;
414
-
415
- if (isSilentReply(text)) {
416
- logger?.log("telegram", `suppressing silent reply for chat ${chatId}`);
417
- return;
418
- }
419
-
420
- if (text.length > maxInlineReplyLength) {
421
- logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
422
- const chatArtifactStore = artifactStore.forChat(artifactChatId);
423
- const artifact = await chatArtifactStore.createGeneratedFile({
424
- fileName: `reply-${Date.now()}.md`,
425
- content: text,
426
- kind: "document",
427
- mimeType: "text/markdown",
428
- source: { type: "assistant", chatId },
429
- metadata: { delivery: "telegram-document" }
430
- });
431
- await sendDocument(new InputFile(artifact.path, path.basename(artifact.path)), {
432
- caption: "Response attached as Markdown."
433
- });
434
- return;
435
- }
436
-
437
- logger?.log("telegram", `sending text reply for chat ${chatId}`);
438
- const sent = await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
439
- if (sent?.message_id) {
440
- const messages = getChatState(chatId).assistantMessages;
441
- messages.set(sent.message_id, text);
442
- while (messages.size > 50) messages.delete(messages.keys().next().value);
443
- }
444
- }
445
-
446
- function createWorkspaceAccessGuard(route) {
447
- return async () => {
448
- if (!route.workspace) return;
449
- const current = await resolveTelegramWorkspaceRoute({
450
- config,
451
- api: bot.api,
452
- ctx: {
453
- chat: { id: route.transportChatId, type: "supergroup", is_forum: true },
454
- from: { id: route.ownerChatId },
455
- message: { message_thread_id: route.threadId }
456
- }
457
- });
458
- if (!current.ok) throw new Error("Owner-only workspace access is paused.");
459
- };
460
- }
461
-
462
- function createTelegramSessionBridge(route) {
463
- const messageOptions = (extra = {}) => route.workspace && route.threadId
464
- ? { ...extra, message_thread_id: route.threadId }
465
- : extra;
466
- const initializeForumTopic = async ({ messageThreadId, name, context }) => {
467
- if (!route.workspace) throw new Error("Telegram topic initialization is only available from the owner workspace forum.");
468
- await createWorkspaceAccessGuard(route)();
469
- const initializedSessionId = topicSessionId({
470
- ownerChatId: route.ownerChatId,
471
- groupChatId: route.transportChatId,
472
- threadId: messageThreadId,
473
- generalTopicId: route.generalTopicId
474
- });
475
- if (initializedSessionId === String(route.ownerChatId)) {
476
- throw new Error("The General topic already uses the owner's private session and cannot be reinitialized here.");
477
- }
478
- const handoff = buildTopicInitializationHandoff({ name, context });
479
- await sessionSeeds.set(initializedSessionId, handoff);
480
- agentManager.resetSession(initializedSessionId, { handoff });
481
- await agentManager.waitForSessionClose(initializedSessionId);
482
- return {
483
- ok: true,
484
- chatId: route.transportChatId,
485
- messageThreadId,
486
- sessionId: initializedSessionId,
487
- name,
488
- initialized: true
489
- };
490
- };
491
- return {
492
- sendMedia: async (filePath, { method = "audio", caption, filename } = {}) => {
493
- logger?.log("telegram", `sending ${method} reply for chat ${route.transportChatId}`);
494
- const input = new InputFile(filePath, filename || undefined);
495
- const options = messageOptions({ caption });
496
- if (method === "voice") return bot.api.sendVoice(route.transportChatId, input, options);
497
- if (method === "document") return bot.api.sendDocument(route.transportChatId, input, options);
498
- if (method === "photo" || method === "image") return bot.api.sendPhoto(route.transportChatId, input, options);
499
- if (method === "video") return bot.api.sendVideo(route.transportChatId, input, options);
500
- return bot.api.sendAudio(route.transportChatId, input, options);
501
- },
502
- createForumTopic: async (name, context) => {
503
- if (!route.workspace) throw new Error("Telegram topic creation is only available from the owner workspace forum.");
504
- await createWorkspaceAccessGuard(route)();
505
- const topic = await bot.api.createForumTopic(route.transportChatId, name);
506
- return initializeForumTopic({
507
- messageThreadId: topic.message_thread_id,
508
- name: topic.name,
509
- context
510
- });
511
- },
512
- initializeForumTopic,
513
- prepareRestartReceipt: (summary) => prepareRestartReceipt({
514
- transportChatId: route.transportChatId,
515
- threadId: route.threadId
516
- }, { reason: String(summary || "Agent-requested restart").trim() }),
517
- cancelRestartReceipt,
518
- getTaskContext: () => route.workspace ? {
519
- transportChatId: route.transportChatId,
520
- messageThreadId: route.topicThreadId
521
- } : null
522
- };
523
- }
524
-
525
- agentManager.setArtifactDeliveryHandler?.(async ({ chatId, artifact, caption, method }) => {
526
- const resolvedMethod = method
527
- || artifact.metadata?.delivery?.method
528
- || (artifact.kind === "audio" || artifact.mimeType?.startsWith("audio/") ? "audio"
529
- : artifact.kind === "image" || artifact.mimeType?.startsWith("image/") ? "photo"
530
- : artifact.kind === "video" || artifact.mimeType?.startsWith("video/") ? "video"
531
- : "document");
532
- const safeCaption = caption && !/(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(caption) ? caption : undefined;
533
- await createTelegramSessionBridge({
534
- workspace: false,
535
- sessionId: String(chatId),
536
- scopeChatId: chatId,
537
- transportChatId: chatId,
538
- threadId: null
539
- }).sendMedia(artifact.path, {
540
- method: resolvedMethod,
541
- caption: safeCaption,
542
- filename: path.basename(artifact.path)
543
- });
544
- return { ok: true, artifactId: artifact.id, method: resolvedMethod };
282
+ const {
283
+ createSessionBridge: createTelegramSessionBridge,
284
+ createWorkspaceAccessGuard,
285
+ installArtifactDeliveryHandler,
286
+ sendTextReply
287
+ } = createTelegramSessionBridgeController({
288
+ config,
289
+ api: bot.api,
290
+ agentManager,
291
+ artifactStore,
292
+ sessionSeeds,
293
+ getChatState,
294
+ buildTopicInitializationHandoff,
295
+ logger
545
296
  });
297
+ installArtifactDeliveryHandler();
546
298
 
547
- async function processPromptForChat({ chatId, prompt, ctx = null }) {
299
+ async function processPromptForChat({ chatId, prompt, ctx = null, executionReceipt = null }) {
548
300
  const route = ctx ? contextRoute(ctx) : {
549
301
  workspace: false,
550
302
  sessionId: String(chatId),
@@ -575,11 +327,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
575
327
  chatState.activeSession = session;
576
328
  chatState.activeRoute = route;
577
329
  try {
578
- text = await withPromptSpeed({
330
+ text = await withPromptSpeed(scheduledPromptSpeedOptions({
331
+ prompt,
332
+ session,
579
333
  speedController,
580
- speed: isScheduledTaskPrompt(prompt) ? 1 : undefined,
581
- restoreSpeed: () => clampModelSpeed(session.model, resolveChatSpeed(config, sessionId))
582
- }, () => collectText(session, prompt, {
334
+ configuredSpeed: resolveChatSpeed(config, sessionId)
335
+ }), () => collectText(session, prompt, {
583
336
  logger,
584
337
  chatId: sessionId,
585
338
  onSlowPrompt: () => bot.api.sendMessage(
@@ -590,11 +343,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
590
343
  }));
591
344
  } catch (error) {
592
345
  agentManager.resetSession(sessionId);
346
+ if (error && typeof error === "object") {
347
+ error.retryable = false;
348
+ error.outcomeUncertain = true;
349
+ }
593
350
  throw error;
594
351
  } finally {
595
352
  if (chatState.activeSession === session) chatState.activeSession = null;
596
353
  chatState.activeRoute = null;
597
354
  }
355
+ executionReceipt?.resolve({ status: "executed" });
598
356
  if (text) {
599
357
  await createWorkspaceAccessGuard(route)();
600
358
  await sendTextReply({
@@ -611,8 +369,18 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
611
369
  return work();
612
370
  }
613
371
 
614
- async function enqueuePrompt({ chatId, prompt, label, ctx = null, replaceQueued = false, busyMessageMode = "queue" }) {
372
+ async function enqueuePrompt({
373
+ chatId,
374
+ prompt,
375
+ label,
376
+ ctx = null,
377
+ replaceQueued = false,
378
+ busyMessageMode = "queue",
379
+ waitForExecution = false,
380
+ coalesceQueued = false
381
+ }) {
615
382
  const chatState = getChatState(chatId);
383
+ const receipt = waitForExecution ? createPromptExecutionReceipt() : null;
616
384
 
617
385
  if (chatState.processing) {
618
386
  const incomingRoute = ctx ? contextRoute(ctx) : null;
@@ -626,10 +394,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
626
394
  prompt,
627
395
  mode: sameDelivery ? busyMessageMode : "queue",
628
396
  replaceQueued,
629
- ctx
397
+ ctx,
398
+ receipt,
399
+ coalesceQueued
630
400
  });
631
401
  if (routed.disposition === "steered") {
632
402
  logger?.log("telegram", `chat ${chatId} busy, steering ${label}`);
403
+ } else if (routed.disposition === "coalesced") {
404
+ logger?.log("telegram", `chat ${chatId} busy, coalescing ${label} into pending direct turn`);
633
405
  } else {
634
406
  logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
635
407
  if (routed.steerError) {
@@ -637,24 +409,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
637
409
  }
638
410
  }
639
411
  if (replaceQueued) chatState.continueAfterClose = true;
640
- return;
412
+ return receipt ? receipt.promise : undefined;
641
413
  }
642
414
 
643
415
  chatState.processing = true;
644
416
  logger?.log("telegram", `processing ${label} in chat ${chatId}`);
645
- return processChatPromptQueue({ chatId, prompt, label, ctx });
417
+ const draining = processChatPromptQueue({ chatId, prompt, label, ctx, initialReceipt: receipt });
418
+ if (!receipt) return draining;
419
+ draining.catch(() => {});
420
+ return receipt.promise;
646
421
  }
647
422
 
648
- function processChatPromptQueue({ chatId, prompt, label, ctx = null, beforeInitialPrompt }) {
423
+ function processChatPromptQueue({ chatId, prompt, label, ctx = null, beforeInitialPrompt, initialReceipt = null }) {
649
424
  const chatState = getChatState(chatId);
650
425
  return drainChatPromptQueue({
651
426
  chatState,
652
427
  initialPrompt: prompt,
653
428
  initialCtx: ctx,
429
+ initialReceipt,
654
430
  beforeInitialPrompt,
655
- processPrompt: ({ prompt: currentPrompt, ctx: currentCtx }) => {
431
+ processPrompt: ({ prompt: currentPrompt, ctx: currentCtx, receipt }) => {
656
432
  logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
657
- return processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
433
+ return processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx, executionReceipt: receipt });
658
434
  },
659
435
  onPromptInterrupted: (error) => {
660
436
  logger?.log("telegram", `${label} interrupted by queued /new for chat ${chatId}: ${getErrorMessage(error)}`);
@@ -662,7 +438,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
662
438
  onPromptFailure: async (error) => {
663
439
  const message = getErrorMessage(error);
664
440
  logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
665
- await notifyPiAuthIssueIfNeeded(chatId, error);
441
+ await authController.notifyIssueIfNeeded(chatId, error);
666
442
  }
667
443
  });
668
444
  }
@@ -674,14 +450,17 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
674
450
  if (chatState.processing) {
675
451
  await ensureQueuedTelegramTyping(chatState, ctx);
676
452
  const incomingPrompt = await buildIncomingPrompt(ctx, route);
677
- const busyMessageMode = typeof ctx.message?.text === "string"
678
- ? resolveTelegramBusyMessageMode(config, route.sessionId)
679
- : "queue";
453
+ const busyMessageMode = resolveIncomingBusyMessageMode({
454
+ config,
455
+ route,
456
+ message: ctx.message
457
+ });
680
458
  return enqueuePrompt({
681
459
  chatId: route.sessionId,
682
460
  prompt: incomingPrompt,
683
461
  label: `message ${ctx.msg.message_id}`,
684
462
  busyMessageMode,
463
+ coalesceQueued: busyMessageMode === "steer" && typeof ctx.message?.text === "string",
685
464
  ctx
686
465
  });
687
466
  }
@@ -726,28 +505,29 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
726
505
  timer.unref?.();
727
506
  }
728
507
 
729
- async function enqueueAsyncPrompt({ chatId, prompt, label, telegramContext }) {
508
+ async function enqueueAsyncPrompt({ chatId, prompt, label, route: taskRoute }) {
730
509
  let ctx = { chat: { id: chatId }, api: bot.api };
731
- if (telegramContext?.transportChatId && telegramContext?.messageThreadId) {
510
+ const destination = taskRoute?.transport === "telegram" ? taskRoute.destination : null;
511
+ if (destination?.chatId && destination?.threadId) {
732
512
  ctx = {
733
- chat: { id: telegramContext.transportChatId, type: "supergroup", is_forum: true },
513
+ chat: { id: destination.chatId, type: "supergroup", is_forum: true },
734
514
  from: { id: chatId },
735
- message: { message_thread_id: telegramContext.messageThreadId },
515
+ message: { message_thread_id: destination.threadId },
736
516
  api: bot.api
737
517
  };
738
518
  const route = await resolveTelegramWorkspaceRoute({ config, api: bot.api, ctx });
739
519
  if (!route.ok) throw new Error("Scheduled owner-workspace destination is unavailable.");
740
- workspaceRoutes.set(ctx, route);
520
+ registerRoute(ctx, route);
741
521
  }
742
522
  const route = contextRoute(ctx);
743
523
  const chatState = getChatState(route.sessionId);
744
524
  if (chatState.processing) await ensureQueuedTelegramTyping(chatState, ctx);
745
- return enqueuePrompt({ chatId: route.sessionId, prompt, label, ctx });
525
+ return enqueuePrompt({ chatId: route.sessionId, prompt, label, ctx, waitForExecution: true });
746
526
  }
747
527
 
748
528
  const { dispatchDueTasks } = createTelegramTaskDispatcher({
749
529
  taskStore,
750
- sendMessage: (chatId, text) => bot.api.sendMessage(chatId, text),
530
+ sendMessage: (chatId, text, options) => bot.api.sendMessage(chatId, text, options),
751
531
  enqueueAsyncPrompt,
752
532
  artifactStore,
753
533
  toolRegistry,
@@ -840,13 +620,8 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
840
620
  bot.command("new", async (ctx) => {
841
621
  const auth = await authorizeContext(ctx);
842
622
  if (!auth.ok) return;
843
- if (piAuthIssue) {
844
- await ctx.reply(buildPiAuthRecoveryBlockedMessage({
845
- config,
846
- chatId: ctx.chat.id,
847
- issue: piAuthIssue,
848
- renewalActive: authRenewals.has(chatKey(ctx.chat.id))
849
- }));
623
+ if (authController.getIssue()) {
624
+ await ctx.reply(authController.buildBlockedMessage(ctx.chat.id));
850
625
  return;
851
626
  }
852
627
  await handleNewCommand(ctx);
@@ -890,11 +665,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
890
665
  }
891
666
  });
892
667
 
893
- bot.command("tools", async (ctx) => {
894
- const auth = await authorizeContext(ctx);
895
- if (!auth.ok) return;
896
- await ctx.reply(renderTelegramHtml(formatToolUsageReport(await toolRegistry.usage(contextRoute(ctx).scopeChatId))), { parse_mode: "HTML" });
897
- });
668
+ bot.command("tools", createTelegramToolsCommandHandler({
669
+ authorize: authorizeContext,
670
+ contextRoute,
671
+ toolRegistry,
672
+ withTyping,
673
+ logger
674
+ }));
898
675
 
899
676
  bot.command("model", async (ctx) => {
900
677
  const auth = await authorizeContext(ctx);
@@ -914,38 +691,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
914
691
  await showSpeedPicker(ctx);
915
692
  });
916
693
 
917
- bot.command("auth", async (ctx) => {
918
- const auth = await authorizeContext(ctx);
919
- if (!auth.ok) return;
920
-
921
- const status = getPiAuthStatus(config, ctx.chat.id);
922
- if (status.hasApiKey || !status.supportsOAuth) {
923
- await withTyping(ctx, async () => {
924
- try {
925
- await agentManager.validateAgent();
926
- agentManager.clearSessionCache(ctx.chat.id);
927
- piAuthIssue = null;
928
- await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
929
- } catch (error) {
930
- const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
931
- piAuthIssue = issue;
932
- await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
933
- }
934
- });
935
- return;
936
- }
937
-
938
- try {
939
- const { started } = await startAuthRenewal(ctx.chat.id);
940
- await ctx.reply(started
941
- ? "Starting Pi login from Telegram..."
942
- : "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
943
- } catch (error) {
944
- const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
945
- piAuthIssue = issue;
946
- await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
947
- }
948
- });
694
+ bot.command("auth", (ctx) => authController.handleCommand(ctx, {
695
+ authorize: authorizeContext,
696
+ withTyping
697
+ }));
949
698
 
950
699
  const handleModelCallback = createTelegramModelCallbackHandler({
951
700
  config,
@@ -970,7 +719,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
970
719
  const reaction = ctx.messageReaction;
971
720
  const chatId = reaction.chat.id;
972
721
  const auth = await authorizeChat({ config, chatId, saveConfig });
973
- if (!auth.ok || piAuthIssue) return;
722
+ if (!auth.ok || authController.getIssue()) return;
974
723
 
975
724
  const reactedMessageText = getChatState(chatId).assistantMessages.get(reaction.message_id) || "";
976
725
  const prompt = buildReactionPrompt({ reaction, reactedMessageText });
@@ -993,15 +742,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
993
742
  const command = getTelegramCommand(ctx);
994
743
  if (command) return;
995
744
 
996
- if (await submitAuthRenewalInput(ctx)) return;
745
+ if (await authController.submitRenewalInput(ctx)) return;
997
746
 
998
- if (piAuthIssue) {
999
- await ctx.reply(buildPiAuthRecoveryBlockedMessage({
1000
- config,
1001
- chatId: ctx.chat.id,
1002
- issue: piAuthIssue,
1003
- renewalActive: authRenewals.has(chatKey(ctx.chat.id))
1004
- }));
747
+ if (authController.getIssue()) {
748
+ await ctx.reply(authController.buildBlockedMessage(ctx.chat.id));
1005
749
  return;
1006
750
  }
1007
751
 
@@ -1047,7 +791,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
1047
791
  async notifyPiAuthIssue(error) {
1048
792
  let notified = false;
1049
793
  for (const chatId of config.telegram.authorizedChatIds || []) {
1050
- notified = await notifyPiAuthIssueIfNeeded(chatId, error) || notified;
794
+ notified = await authController.notifyIssueIfNeeded(chatId, error) || notified;
1051
795
  }
1052
796
  return notified;
1053
797
  }