@llblab/pi-telegram 0.27.11 → 0.28.0

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/lib/turns.ts CHANGED
@@ -22,28 +22,23 @@ import {
22
22
  type DownloadTelegramMessageFilesDeps,
23
23
  type TelegramMediaMessage,
24
24
  } from "./media.ts";
25
- import type {
26
- PendingTelegramTurn,
27
- TelegramPromptContent,
28
- TelegramQueueItem,
29
- TelegramQueueStore,
25
+ import {
26
+ createTelegramQueueAdmissionReceipt,
27
+ truncateTelegramQueueSummary,
28
+ type PendingTelegramTurn,
29
+ type TelegramPromptContent,
30
+ type TelegramQueueAdmissionReceipt,
31
+ type TelegramQueueItem,
32
+ type TelegramQueueStore,
30
33
  } from "./queue.ts";
31
34
 
32
35
  import {
33
36
  computeVoicePromptContribution,
34
37
  computeVoiceTurnFlags,
35
38
  getTelegramVoiceReplyMode,
36
- TELEGRAM_VOICE_REPLY_MODES,
37
39
  type TelegramVoiceReplyMode,
38
40
  } from "./voice.ts";
39
41
 
40
- // Re-export for backward compatibility with existing namespace imports (e.g. Turns.getTelegramVoiceReplyMode in routing.ts)
41
- export {
42
- getTelegramVoiceReplyMode,
43
- TELEGRAM_VOICE_REPLY_MODES,
44
- type TelegramVoiceReplyMode,
45
- };
46
-
47
42
  export const TELEGRAM_PREFIX = "[telegram]";
48
43
 
49
44
  export interface TelegramTurnTarget {
@@ -55,6 +50,7 @@ export interface TelegramTurnMessage {
55
50
  message_id: number;
56
51
  message_thread_id?: number;
57
52
  pi_telegram_agent_source_thread?: string;
53
+ pi_telegram_source_update_id?: number;
58
54
  chat: { id: number; type?: string };
59
55
  }
60
56
 
@@ -93,7 +89,6 @@ export function formatTelegramTurnPrefix(
93
89
  return basePrefix;
94
90
  }
95
91
 
96
- import { truncateTelegramQueueSummary } from "./queue.ts";
97
92
  export { truncateTelegramQueueSummary };
98
93
 
99
94
  export function formatTelegramTurnStatusSummary(
@@ -417,6 +412,8 @@ export interface BuildTelegramPromptTurnOptions {
417
412
  inferImageMimeType: (path: string) => string | undefined;
418
413
  voiceReplyMode?: TelegramVoiceReplyMode;
419
414
  voicePromptContribution?: string;
415
+ admissionScope?: string;
416
+ admissionJournalBinding?: string;
420
417
  }
421
418
 
422
419
  export type BuildTelegramPromptTurnRuntimeOptions = Omit<
@@ -445,6 +442,9 @@ export interface TelegramPromptTurnRuntimeBuilderDeps<
445
442
  message_thread_id?: number;
446
443
  }) => string | undefined;
447
444
  getAllowedUserId?: () => number | undefined;
445
+ getAdmissionScope?: () => string | undefined;
446
+ getAdmissionJournalBinding?: () => string | undefined;
447
+ assertExecutionCurrent?: (message: TelegramTurnMessage) => void;
448
448
  }
449
449
 
450
450
  export function createTelegramPromptTurnRuntimeBuilder<
@@ -460,12 +460,14 @@ export function createTelegramPromptTurnRuntimeBuilder<
460
460
  return async (messages, historyTurns = [], ctx) => {
461
461
  const rawText = extractTelegramMessagesText(messages);
462
462
  const firstMessage = messages[0];
463
+ if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
463
464
  const replyFiles = firstMessage?.reply_to_message
464
465
  ? await downloadTelegramMessageFiles(
465
466
  [firstMessage.reply_to_message as typeof firstMessage],
466
467
  { downloadFile: deps.downloadFile },
467
468
  )
468
469
  : [];
470
+ if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
469
471
  const replyContext = firstMessage
470
472
  ? buildTelegramReplyContextBlock(firstMessage, replyFiles)
471
473
  : "";
@@ -490,9 +492,11 @@ export function createTelegramPromptTurnRuntimeBuilder<
490
492
  const files = await downloadTelegramMessageFiles(messages, {
491
493
  downloadFile: deps.downloadFile,
492
494
  });
495
+ if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
493
496
  const processed = deps.processAttachments
494
497
  ? await deps.processAttachments(files, rawText, ctx as TContext)
495
498
  : { rawText, promptFiles: files };
499
+ if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
496
500
  const sourceBlocks: string[] = [];
497
501
  let promptRawText = processed.rawText;
498
502
  const forwardedFilePaths = new Set<string>();
@@ -579,6 +583,8 @@ export function createTelegramPromptTurnRuntimeBuilder<
579
583
  files,
580
584
  rawText,
581
585
  ),
586
+ admissionScope: deps.getAdmissionScope?.(),
587
+ admissionJournalBinding: deps.getAdmissionJournalBinding?.(),
582
588
  });
583
589
  };
584
590
  }
@@ -596,6 +602,58 @@ function getTelegramVoicePromptContext(
596
602
  return { delivery: "automatic voice" };
597
603
  }
598
604
 
605
+ function collectTelegramTurnAdmissionReceipts(
606
+ messages: TelegramTurnMessage[],
607
+ historyTurns: PendingTelegramTurn[],
608
+ admissionScope: string | undefined,
609
+ admissionJournalBinding: string | undefined,
610
+ ): TelegramQueueAdmissionReceipt[] {
611
+ const receipts = new Map<string, TelegramQueueAdmissionReceipt>();
612
+ const addReceipt = (receipt: TelegramQueueAdmissionReceipt): void => {
613
+ const existing = receipts.get(receipt.receiptId);
614
+ if (existing) {
615
+ if (
616
+ existing.queueKind !== receipt.queueKind ||
617
+ existing.journalBindingKey !== receipt.journalBindingKey ||
618
+ existing.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
619
+ existing.sourceUpdateIds.some(
620
+ (updateId, index) => updateId !== receipt.sourceUpdateIds[index],
621
+ )
622
+ ) {
623
+ throw new Error(
624
+ `Conflicting Telegram turn receipt: ${receipt.receiptId}`,
625
+ );
626
+ }
627
+ return;
628
+ }
629
+ receipts.set(receipt.receiptId, structuredClone(receipt));
630
+ };
631
+ for (const turn of historyTurns) {
632
+ for (const receipt of turn.admissionReceipts ?? []) addReceipt(receipt);
633
+ }
634
+ const sourceUpdateIds = messages.flatMap((message) =>
635
+ typeof message.pi_telegram_source_update_id === "number"
636
+ ? [message.pi_telegram_source_update_id]
637
+ : [],
638
+ );
639
+ if (sourceUpdateIds.length > 0) {
640
+ const currentReceipt = createTelegramQueueAdmissionReceipt({
641
+ queueKind: "prompt",
642
+ scope: admissionScope ?? "",
643
+ sourceUpdateIds,
644
+ });
645
+ if (currentReceipt) {
646
+ addReceipt({
647
+ ...currentReceipt,
648
+ ...(admissionJournalBinding
649
+ ? { journalBindingKey: admissionJournalBinding }
650
+ : {}),
651
+ });
652
+ }
653
+ }
654
+ return [...receipts.values()];
655
+ }
656
+
599
657
  export async function buildTelegramPromptTurn(
600
658
  options: BuildTelegramPromptTurnOptions,
601
659
  ): Promise<PendingTelegramTurn> {
@@ -648,6 +706,12 @@ export async function buildTelegramPromptTurn(
648
706
  textItem.text = `${textItem.text}\n\n${options.voicePromptContribution.trim()}`;
649
707
  }
650
708
  }
709
+ const admissionReceipts = collectTelegramTurnAdmissionReceipts(
710
+ options.messages,
711
+ options.historyTurns ?? [],
712
+ options.admissionScope,
713
+ options.admissionJournalBinding,
714
+ );
651
715
 
652
716
  return {
653
717
  kind: "prompt",
@@ -673,6 +737,7 @@ export async function buildTelegramPromptTurn(
673
737
  options.displayFiles ?? options.promptFiles ?? options.files,
674
738
  options.handlerOutputs,
675
739
  ),
740
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
676
741
  // Voice tagging (used for preview suppression and prompt guidance)
677
742
  ...computeVoiceTurnFlags(voiceReplyMode, hasVoiceFile),
678
743
  };