arisa 5.1.68 → 5.2.7

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 (77) hide show
  1. package/README.md +7 -4
  2. package/package.json +1 -1
  3. package/src/core/agent/agent-manager.js +46 -6
  4. package/src/core/agent/agent-session-lifecycle.js +80 -3
  5. package/src/core/agent/core-tools.js +1 -1
  6. package/src/core/agent/pi-auth-login.js +1 -1
  7. package/src/core/agent/pi-runtime.js +1 -1
  8. package/src/core/agent/runtime-context.js +1 -1
  9. package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
  10. package/src/core/artifacts/artifact-store.js +1 -1
  11. package/src/core/capabilities/capability-service.js +1 -1
  12. package/src/core/config/config-defaults.js +30 -1
  13. package/src/core/config/config-store.js +1 -1
  14. package/src/core/conversation/session-seed-store.js +1 -1
  15. package/src/core/tasks/task-store.js +1 -1
  16. package/src/core/tools/daemon-client.js +180 -0
  17. package/src/core/tools/daemon-processes.js +19 -3
  18. package/src/core/tools/daemon-protocol.js +72 -0
  19. package/src/core/tools/daemon-runtime.js +13 -490
  20. package/src/core/tools/daemon-worker.js +310 -0
  21. package/src/core/tools/ipc-client.js +2 -2
  22. package/src/core/tools/memory-pressure.js +56 -0
  23. package/src/core/tools/official-tool-installer.js +1 -1
  24. package/src/core/tools/tool-config.js +1 -1
  25. package/src/core/tools/tool-process-output.js +100 -0
  26. package/src/core/tools/tool-process-runner.js +175 -0
  27. package/src/core/tools/tool-registry.js +99 -187
  28. package/src/core/tools/tool-resource-note-store.js +1 -1
  29. package/src/core/tools/tool-usage-store.js +1 -1
  30. package/src/core/tools/weighted-resource-governor.js +188 -38
  31. package/src/index.js +14 -2
  32. package/src/official-tools.lock.json +424 -50
  33. package/src/platform/paths.js +152 -0
  34. package/src/runtime/bootstrap-cli.js +121 -0
  35. package/src/runtime/bootstrap-config.js +97 -0
  36. package/src/runtime/bootstrap-telegram.js +325 -0
  37. package/src/runtime/bootstrap.js +6 -543
  38. package/src/runtime/doctor.js +6 -3
  39. package/src/runtime/flush.js +1 -1
  40. package/src/runtime/ipc/ipc-server.js +1 -1
  41. package/src/runtime/log-viewer.js +1 -1
  42. package/src/runtime/oom-protection.js +20 -0
  43. package/src/runtime/paths.js +3 -151
  44. package/src/runtime/restart-receipt.js +1 -1
  45. package/src/runtime/service-manager.js +1 -1
  46. package/src/runtime/service-supervisor.js +14 -0
  47. package/src/runtime/slave-cli.js +1 -1
  48. package/src/runtime/tool-process-supervisor.js +1 -1
  49. package/src/runtime/tui.js +200 -0
  50. package/src/runtime/update-manager.js +1 -1
  51. package/src/runtime/worker-recovery-report.js +142 -0
  52. package/src/transport/telegram/bot.js +42 -320
  53. package/src/transport/telegram/prompt-builders.js +8 -3
  54. package/src/transport/telegram/telegram-prompt-controller.js +346 -0
  55. package/src/transport/telegram/workspace-topic-store.js +1 -1
  56. package/test/agent-session-lifecycle.test.js +92 -0
  57. package/test/architecture-boundaries.test.js +29 -0
  58. package/test/bootstrap.test.js +65 -0
  59. package/test/daemon-process-invocation.test.js +27 -0
  60. package/test/daemon-runtime.test.js +36 -1
  61. package/test/doctor.test.js +22 -0
  62. package/test/memory-pressure.test.js +36 -0
  63. package/test/model-selection.test.js +11 -1
  64. package/test/official-tool-dependencies.test.js +1 -1
  65. package/test/official-tool-installer.test.js +18 -1
  66. package/test/oom-protection.test.js +32 -0
  67. package/test/paths.test.js +7 -0
  68. package/test/pi-compaction.test.js +9 -0
  69. package/test/service-manager.test.js +6 -1
  70. package/test/telegram-prompt-controller.test.js +81 -0
  71. package/test/telegram-text-artifact.test.js +30 -0
  72. package/test/tool-registry-run.test.js +108 -4
  73. package/test/tui.test.js +41 -0
  74. package/test/weighted-resource-governor.test.js +97 -5
  75. package/test/worker-heap-circuit-breaker.test.js +79 -0
  76. package/test/worker-recovery-report.test.js +69 -0
  77. package/test-fixtures/fake-daemon.js +5 -0
@@ -1,15 +1,14 @@
1
1
  import { Bot } from "grammy";
2
2
  import { authorizeChat } from "./auth.js";
3
- import { captureIncomingArtifact } from "./media.js";
4
3
  import { renderTelegramHtml } from "./text-format.js";
5
4
  import { buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue } from "../../core/agent/auth-flow.js";
6
5
  import { createTelegramAuthController } from "./telegram-auth-controller.js";
7
- import { resolveChatSpeed } from "../../core/agent/model-selection.js";
8
6
  import { SessionSeedStore } from "../../core/conversation/session-seed-store.js";
9
7
  import { formatDoctorReport } from "../../runtime/doctor.js";
10
8
  import { ToolResourceNoteStore } from "../../core/tools/tool-resource-note-store.js";
11
9
  import { formatUpdateReport } from "../../runtime/update-manager.js";
12
10
  import { cancelRestartReceipt, deliverRestartReceipt, prepareRestartReceipt } from "../../runtime/restart-receipt.js";
11
+ import { consumeWorkerRecoveryReport, loadWorkerRecoveryReport } from "../../runtime/worker-recovery-report.js";
13
12
  import { buildUpdatePicker, createTelegramUpdateCallbackHandler } from "./update-command.js";
14
13
  import { createTelegramModelControls } from "./model-controls.js";
15
14
  import { createTelegramModelCallbackHandler } from "./model-callback.js";
@@ -18,36 +17,10 @@ import { createTelegramSessionBridgeController } from "./telegram-session-bridge
18
17
  import { createTelegramToolsCommandHandler } from "./telegram-tools-command.js";
19
18
  import { createTelegramWorkspaceController } from "./telegram-workspace-controller.js";
20
19
  import { resolveTelegramWorkspaceRoute } from "./workspace-group.js";
21
- import {
22
- appendGeneralReplyRoutingInstruction,
23
- isGeneralWorkspaceRoute,
24
- routeGeneralWorkspaceReply
25
- } from "./reply-topic-routing.js";
26
20
  import { migrateLegacyReplyTopics, WorkspaceTopicStore } from "./workspace-topic-store.js";
27
- import {
28
- buildNewSessionPrompt,
29
- buildPrompt,
30
- buildReactionPrompt,
31
- buildSessionHandoffPrompt,
32
- buildStartupMessage,
33
- collectText,
34
- getIncomingMessageText,
35
- isScheduledTaskPrompt,
36
- isSilentReply,
37
- normalizeIncomingArtifact,
38
- sanitizeSessionHandoff,
39
- scheduledPromptSpeedOptions,
40
- shouldIncludeArtifactReference,
41
- withPromptSpeed
42
- } from "./prompt-builders.js";
43
- import {
44
- createChatStateStore,
45
- createPromptExecutionReceipt,
46
- drainChatPromptQueue,
47
- queueChatPrompt,
48
- resolveTelegramBusyMessageMode,
49
- routeBusyPrompt
50
- } from "./chat-queue.js";
21
+ import { buildReactionPrompt, buildStartupMessage } from "./prompt-builders.js";
22
+ import { createChatStateStore, resolveTelegramBusyMessageMode } from "./chat-queue.js";
23
+ import { createTelegramPromptController } from "./telegram-prompt-controller.js";
51
24
  export {
52
25
  createChatStateStore,
53
26
  drainChatPromptQueue,
@@ -268,30 +241,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
268
241
  logger?.log("telegram", `inherited model ${inherited.model} for workspace topic session ${route.sessionId}`);
269
242
  }
270
243
 
271
- async function buildIncomingPrompt(ctx, route = contextRoute(ctx)) {
272
- logger?.log("telegram", `message ${ctx.msg.message_id} in chat ${route.transportChatId} session ${route.sessionId}`);
273
- const chatArtifactStore = artifactStore.forChat(route.scopeChatId);
274
- const artifact = await captureIncomingArtifact(ctx, artifactStore, { storageChatId: route.scopeChatId });
275
- if (artifact) logger?.log("telegram", `captured artifact ${artifact.kind}${artifact.id ? ` ${artifact.id}` : ""}`);
276
- const { transcript, toolResult, normalizationRequired } = await normalizeIncomingArtifact({
277
- artifact,
278
- toolRegistry,
279
- chatArtifactStore,
280
- chatId: route.scopeChatId
281
- });
282
- if (transcript) logger?.log("telegram", `media transcribed to artifact ${transcript.id}`);
283
- if (normalizationRequired && !transcript) {
284
- logger?.log("telegram", `media normalization unavailable for chat ${route.transportChatId}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
285
- }
286
- const prompt = buildPrompt({ ctx, artifact, transcript, toolResult });
287
- if (!isGeneralWorkspaceRoute(route)) return prompt;
288
- const [topics, recentProposals] = await Promise.all([
289
- workspaceTopics.listTopics(route.ownerChatId, route.transportChatId),
290
- workspaceTopics.listRecentProposals(route.ownerChatId, route.transportChatId)
291
- ]);
292
- return appendGeneralReplyRoutingInstruction(prompt, topics, recentProposals);
293
- }
294
-
295
244
  const {
296
245
  createSessionBridge: createTelegramSessionBridge,
297
246
  createWorkspaceAccessGuard,
@@ -310,200 +259,30 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
310
259
  });
311
260
  installArtifactDeliveryHandler();
312
261
 
313
- async function processPromptForChat({ chatId, prompt, ctx = null, executionReceipt = null }) {
314
- const route = ctx ? contextRoute(ctx) : {
315
- workspace: false,
316
- sessionId: String(chatId),
317
- scopeChatId: chatId,
318
- transportChatId: chatId,
319
- threadId: null
320
- };
321
- const sessionId = route.sessionId;
322
- const bridge = createTelegramSessionBridge(route);
323
- const messageOptions = (extra = {}) => route.workspace && route.threadId
324
- ? { ...extra, message_thread_id: route.threadId }
325
- : extra;
326
- const work = async () => {
327
- await ensureWorkspaceTopicModelSelection(route);
328
- if (route.workspace && route.threadId) {
329
- const handoff = await sessionSeeds.consume(sessionId);
330
- if (handoff) {
331
- agentManager.resetSession(sessionId, { handoff });
332
- await agentManager.waitForSessionClose(sessionId);
333
- }
334
- }
335
- const { session, speedController } = await agentManager.getSessionContext(sessionId, bridge, {
336
- scopeChatId: route.scopeChatId,
337
- accessGuard: createWorkspaceAccessGuard(route)
338
- });
339
- let text = "";
340
- const chatState = getChatState(sessionId);
341
- chatState.activeSession = session;
342
- chatState.activeRoute = route;
343
- try {
344
- text = await withPromptSpeed(scheduledPromptSpeedOptions({
345
- prompt,
346
- session,
347
- speedController,
348
- configuredSpeed: resolveChatSpeed(config, sessionId)
349
- }), () => collectText(session, prompt, {
350
- logger,
351
- chatId: sessionId,
352
- onSlowPrompt: () => bot.api.sendMessage(
353
- route.transportChatId,
354
- "This is taking longer than 5 minutes, so I will keep the current session running instead of starting over. Send /new if you want to abandon it and start fresh.",
355
- messageOptions()
356
- )
357
- }));
358
- } catch (error) {
359
- agentManager.resetSession(sessionId);
360
- if (error && typeof error === "object") {
361
- error.retryable = false;
362
- error.outcomeUncertain = true;
363
- }
364
- throw error;
365
- } finally {
366
- if (chatState.activeSession === session) chatState.activeSession = null;
367
- chatState.activeRoute = null;
368
- }
369
- executionReceipt?.resolve({ status: "executed" });
370
- if (text) {
371
- const topics = route.workspace && route.ownerChatId
372
- ? await workspaceTopics.listTopics(route.ownerChatId, route.transportChatId)
373
- : [];
374
- const routedReply = routeGeneralWorkspaceReply({ route, text, topics });
375
- if (routedReply.proposal) {
376
- await workspaceTopics.recordProposal(route.ownerChatId, route.transportChatId, routedReply.proposal);
377
- logger?.log("telegram", `recorded topic proposal ${routedReply.proposal} for workspace ${route.transportChatId}`);
378
- }
379
- if (!routedReply.text) return;
380
- const deliveryRoute = routedReply.route;
381
- const deliveryOptions = (extra = {}) => deliveryRoute.workspace && deliveryRoute.threadId
382
- ? { ...extra, message_thread_id: deliveryRoute.threadId }
383
- : extra;
384
- await createWorkspaceAccessGuard(deliveryRoute)();
385
- if (routedReply.topic) {
386
- logger?.log("telegram", `routing General reply to topic ${routedReply.topic.threadId} (${routedReply.topic.name})`);
387
- }
388
- await sendTextReply({
389
- sendText: (message, extra) => bot.api.sendMessage(deliveryRoute.transportChatId, message, deliveryOptions(extra)),
390
- sendDocument: (file, extra) => bot.api.sendDocument(deliveryRoute.transportChatId, file, deliveryOptions(extra)),
391
- chatId: sessionId,
392
- artifactChatId: deliveryRoute.scopeChatId,
393
- text: routedReply.text
394
- });
395
- }
396
- };
397
-
398
- if (ctx) return withTyping(ctx, work);
399
- return work();
400
- }
401
-
402
- async function enqueuePrompt({
403
- chatId,
404
- prompt,
405
- label,
406
- ctx = null,
407
- replaceQueued = false,
408
- busyMessageMode = "queue",
409
- waitForExecution = false,
410
- onExecutionStart = null,
411
- coalesceQueued = false
412
- }) {
413
- const chatState = getChatState(chatId);
414
- const receipt = waitForExecution ? createPromptExecutionReceipt(onExecutionStart) : null;
415
-
416
- if (chatState.processing) {
417
- const incomingRoute = ctx ? contextRoute(ctx) : null;
418
- const activeRoute = chatState.activeRoute;
419
- const sameDelivery = !incomingRoute || !activeRoute || (
420
- incomingRoute.transportChatId === activeRoute.transportChatId
421
- && incomingRoute.threadId === activeRoute.threadId
422
- );
423
- const routed = await routeBusyPrompt({
424
- chatState,
425
- prompt,
426
- mode: sameDelivery ? busyMessageMode : "queue",
427
- replaceQueued,
428
- ctx,
429
- receipt,
430
- coalesceQueued
431
- });
432
- if (routed.disposition === "steered") {
433
- logger?.log("telegram", `chat ${chatId} busy, steering ${label}`);
434
- } else if (routed.disposition === "coalesced") {
435
- logger?.log("telegram", `chat ${chatId} busy, coalescing ${label} into pending direct turn`);
436
- } else {
437
- logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
438
- if (routed.steerError) {
439
- logger?.log("telegram", `steer failed for chat ${chatId}, queued instead: ${getErrorMessage(routed.steerError)}`);
440
- }
441
- }
442
- if (replaceQueued) chatState.continueAfterClose = true;
443
- return receipt ? receipt.promise : undefined;
444
- }
445
-
446
- chatState.processing = true;
447
- logger?.log("telegram", `processing ${label} in chat ${chatId}`);
448
- const draining = processChatPromptQueue({ chatId, prompt, label, ctx, initialReceipt: receipt });
449
- if (!receipt) return draining;
450
- draining.catch(() => {});
451
- return receipt.promise;
452
- }
453
-
454
- function processChatPromptQueue({ chatId, prompt, label, ctx = null, beforeInitialPrompt, initialReceipt = null }) {
455
- const chatState = getChatState(chatId);
456
- return drainChatPromptQueue({
457
- chatState,
458
- initialPrompt: prompt,
459
- initialCtx: ctx,
460
- initialReceipt,
461
- beforeInitialPrompt,
462
- processPrompt: ({ prompt: currentPrompt, ctx: currentCtx, receipt }) => {
463
- logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
464
- return processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx, executionReceipt: receipt });
465
- },
466
- onPromptInterrupted: (error) => {
467
- logger?.log("telegram", `${label} interrupted by queued /new for chat ${chatId}: ${getErrorMessage(error)}`);
468
- },
469
- onPromptFailure: async (error) => {
470
- const message = getErrorMessage(error);
471
- logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
472
- await authController.notifyIssueIfNeeded(chatId, error);
473
- }
474
- });
475
- }
476
-
477
- async function enqueueOrProcess(ctx) {
478
- const route = contextRoute(ctx);
479
- const chatState = getChatState(route.sessionId);
480
-
481
- if (chatState.processing) {
482
- await ensureQueuedTelegramTyping(chatState, ctx);
483
- const incomingPrompt = await buildIncomingPrompt(ctx, route);
484
- const busyMessageMode = resolveIncomingBusyMessageMode({
485
- config,
486
- route,
487
- message: ctx.message
488
- });
489
- return enqueuePrompt({
490
- chatId: route.sessionId,
491
- prompt: incomingPrompt,
492
- label: `message ${ctx.msg.message_id}`,
493
- busyMessageMode,
494
- coalesceQueued: busyMessageMode === "steer" && typeof ctx.message?.text === "string",
495
- ctx
496
- });
497
- }
498
-
499
- const incomingPrompt = await buildIncomingPrompt(ctx, route);
500
- return enqueuePrompt({
501
- chatId: route.sessionId,
502
- prompt: incomingPrompt,
503
- label: `message ${ctx.msg.message_id}`,
504
- ctx
505
- });
506
- }
262
+ const {
263
+ enqueueOrProcess,
264
+ enqueuePrompt,
265
+ handleNewCommand
266
+ } = createTelegramPromptController({
267
+ config,
268
+ api: bot.api,
269
+ artifactStore,
270
+ toolRegistry,
271
+ agentManager,
272
+ sessionSeeds,
273
+ workspaceTopics,
274
+ logger,
275
+ contextRoute,
276
+ getChatState,
277
+ createTelegramSessionBridge,
278
+ createWorkspaceAccessGuard,
279
+ sendTextReply,
280
+ authController,
281
+ ensureWorkspaceTopicModelSelection,
282
+ ensureQueuedTyping: ensureQueuedTelegramTyping,
283
+ withTyping,
284
+ resolveBusyMessageMode: resolveIncomingBusyMessageMode
285
+ });
507
286
 
508
287
  async function migrateConfiguredReplyTopics() {
509
288
  const migratedGroups = await migrateLegacyReplyTopics(config, workspaceTopics);
@@ -513,15 +292,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
513
292
  }
514
293
 
515
294
  async function sendStartupMessages() {
295
+ let recovery = null;
296
+ try {
297
+ recovery = await loadWorkerRecoveryReport();
298
+ } catch (error) {
299
+ logger?.log("telegram", `worker recovery report load failed: ${error instanceof Error ? error.message : String(error)}`);
300
+ }
301
+ let recoveryDelivered = false;
516
302
  for (const chatId of config.telegram.authorizedChatIds || []) {
517
303
  try {
518
304
  logger?.log("telegram", `sending startup message for chat ${chatId}`);
519
305
  const chatMeta = config.telegram.chatMeta[chatId] || {};
520
- await bot.api.sendMessage(chatId, buildStartupMessage(chatMeta));
306
+ await bot.api.sendMessage(chatId, recovery?.text || buildStartupMessage(chatMeta));
307
+ if (recovery) recoveryDelivered = true;
521
308
  } catch (error) {
522
309
  logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
523
310
  }
524
311
  }
312
+ if (recovery && recoveryDelivered) {
313
+ await consumeWorkerRecoveryReport(recovery.report.id).catch((error) => {
314
+ logger?.log("telegram", `worker recovery report cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
315
+ });
316
+ }
525
317
  try {
526
318
  const result = await deliverRestartReceipt((chatId, text, options) => bot.api.sendMessage(chatId, text, options));
527
319
  if (result) logger?.log("telegram", `delivered restart receipt ${result.receipt.id}`);
@@ -600,76 +392,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
600
392
  logger
601
393
  });
602
394
 
603
- async function summarizeSessionBeforeReset(chatId, route = {
604
- workspace: false,
605
- sessionId: String(chatId),
606
- scopeChatId: chatId,
607
- transportChatId: chatId,
608
- threadId: null
609
- }) {
610
- try {
611
- const context = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(route), {
612
- scopeChatId: route.scopeChatId,
613
- accessGuard: createWorkspaceAccessGuard(route)
614
- });
615
- const parentSession = context.session.sessionFile || "";
616
- if (!context.session.messages.length) return { handoff: "", parentSession: "" };
617
-
618
- const summary = await collectText(context.session, buildSessionHandoffPrompt(), { logger, chatId });
619
- return { handoff: sanitizeSessionHandoff(summary), parentSession };
620
- } catch (error) {
621
- logger?.log("agent", `session handoff summary failed for chat ${chatId}: ${getErrorMessage(error)}`);
622
- return { handoff: "", parentSession: "" };
623
- }
624
- }
625
-
626
- async function handleNewCommand(ctx) {
627
- const route = contextRoute(ctx);
628
- const sessionId = route.sessionId;
629
- const chatState = getChatState(sessionId);
630
- const wasProcessing = chatState.processing;
631
- chatState.historyRevision += 1;
632
- const commandRevision = chatState.historyRevision;
633
- const prompt = buildNewSessionPrompt(ctx);
634
-
635
- if (wasProcessing) {
636
- logger?.log("telegram", `chat ${sessionId} busy, queueing new-session command`);
637
- queueChatPrompt(chatState, prompt, { replace: true, ctx });
638
- chatState.continueAfterClose = true;
639
- const reset = (async () => {
640
- await sessionSeeds.clear(sessionId);
641
- agentManager.resetSession(sessionId);
642
- })();
643
- chatState.beforeNextPrompt = reset;
644
- try {
645
- await reset;
646
- } finally {
647
- if (chatState.beforeNextPrompt === reset) chatState.beforeNextPrompt = null;
648
- }
649
- return;
650
- }
651
-
652
- chatState.processing = true;
653
- logger?.log("telegram", `processing new-session command in chat ${sessionId}`);
654
- await processChatPromptQueue({
655
- chatId: sessionId,
656
- prompt,
657
- label: "new-session command",
658
- ctx,
659
- beforeInitialPrompt: async () => {
660
- const handoff = await withTyping(ctx, () => summarizeSessionBeforeReset(sessionId, route));
661
- if (chatState.historyRevision !== commandRevision) return;
662
- if (route.workspace && route.threadId) {
663
- await sessionSeeds.set(sessionId, handoff.handoff);
664
- } else {
665
- await sessionSeeds.clear(sessionId);
666
- }
667
- if (chatState.historyRevision !== commandRevision) return;
668
- agentManager.resetSession(sessionId, handoff);
669
- }
670
- });
671
- }
672
-
673
395
  bot.catch((error) => {
674
396
  logger?.error("telegram", `bot error: ${error instanceof Error ? error.message : String(error)}`);
675
397
  console.error("Telegram bot error:", error);
@@ -4,12 +4,15 @@ import { clampModelSpeed } from "../../core/agent/model-speed.js";
4
4
 
5
5
  const slowPromptNoticeMs = 300_000;
6
6
 
7
- function quotedMessageSummary(message) {
7
+ function quotedMessageSummary(incomingMessage) {
8
+ const message = incomingMessage?.reply_to_message;
8
9
  if (!message) return [];
9
10
 
10
11
  const fromName = message.from?.username
11
12
  ? `@${message.from.username}`
12
13
  : [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ") || "unknown";
14
+ const quotedSelection = incomingMessage.quote?.text;
15
+ const quotedTopicName = message.forum_topic_created?.name;
13
16
 
14
17
  const parts = [
15
18
  `quotedMessageId: ${message.message_id}`,
@@ -18,6 +21,7 @@ function quotedMessageSummary(message) {
18
21
 
19
22
  if (message.text) parts.push(`quotedText: ${message.text}`);
20
23
  if (message.caption) parts.push(`quotedCaption: ${message.caption}`);
24
+ if (quotedSelection) parts.push(`quotedSelection: ${quotedSelection}`);
21
25
  if (message.voice) parts.push(`quotedKind: voice`);
22
26
  if (message.audio) parts.push(`quotedKind: audio`);
23
27
  if (message.photo?.length) parts.push(`quotedKind: image`);
@@ -25,8 +29,9 @@ function quotedMessageSummary(message) {
25
29
  if (message.video) parts.push(`quotedKind: video`);
26
30
  if (message.sticker) parts.push(`quotedKind: sticker`);
27
31
  if (message.location) parts.push(`quotedKind: location`, `quotedLocation: ${formatLocationText(message)}`);
32
+ if (message.forum_topic_created) parts.push(`quotedKind: forum_topic_created`, `quotedTopicName: ${quotedTopicName || "unknown"}`);
28
33
 
29
- if (!message.text && !message.caption) {
34
+ if (!message.text && !message.caption && !quotedSelection && !quotedTopicName) {
30
35
  parts.push(`Important: this message replies to a Telegram message with no textual body available in the update. Use the quoted kind and metadata as context.`);
31
36
  }
32
37
 
@@ -126,7 +131,7 @@ export function buildPrompt({ ctx, artifact, transcript, toolResult }) {
126
131
  const messageText = getIncomingMessageText(ctx.message);
127
132
  if (messageText) parts.push(`text: ${messageText}`);
128
133
  parts.push(...forwardedMessageSummary(ctx.message));
129
- parts.push(...quotedMessageSummary(ctx.message?.reply_to_message));
134
+ parts.push(...quotedMessageSummary(ctx.message));
130
135
  if (shouldIncludeArtifactReference({ artifact, messageText })) {
131
136
  if (artifact?.path) parts.push(`artifactPath: ${artifact.path}`);
132
137
  if (artifact?.id) parts.push(`artifactId: ${artifact.id}`);