@llblab/pi-kit 0.5.2 → 0.7.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -37,6 +37,7 @@ function getTelegramApiTempDir(): string {
37
37
  return resolveTelegramTempDir();
38
38
  }
39
39
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
40
+ const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
40
41
  const TELEGRAM_TEMP_SCRATCH_FILE_PATTERN =
41
42
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/u;
42
43
  const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
@@ -465,6 +466,263 @@ export interface TelegramApiClient {
465
466
  ) => Promise<void>;
466
467
  }
467
468
 
469
+ export interface TelegramApiTargetActivityRuntime {
470
+ begin: (
471
+ method: string,
472
+ body: Record<string, unknown>,
473
+ ) => () => void;
474
+ hasPendingTarget: (target: { chatId: number; threadId?: number }) => boolean;
475
+ listPendingTargets: () => { chatId: number; threadId: number }[];
476
+ listPendingChats: () => number[];
477
+ }
478
+
479
+ function parseTelegramApiTargetInteger(value: unknown): number | undefined {
480
+ const parsed = typeof value === "number" ? value :
481
+ typeof value === "string" && /^-?\d+$/u.test(value) ? Number(value) : undefined;
482
+ return parsed !== undefined && Number.isSafeInteger(parsed) ? parsed : undefined;
483
+ }
484
+
485
+ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActivityRuntime {
486
+ const pending = new Map<string, {
487
+ target: { chatId: number; threadId: number };
488
+ count: number;
489
+ }>();
490
+ const pendingChats = new Map<number, number>();
491
+ const messageScopedMethods = new Set([
492
+ "deleteMessage",
493
+ "editMessageCaption",
494
+ "editMessageReplyMarkup",
495
+ "editMessageText",
496
+ ]);
497
+ return {
498
+ begin(method, body) {
499
+ const chatId = parseTelegramApiTargetInteger(body.chat_id);
500
+ const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
501
+ const messageId = parseTelegramApiTargetInteger(body.message_id);
502
+ const chatScoped = chatId !== undefined && threadId === undefined &&
503
+ messageId !== undefined && messageScopedMethods.has(method);
504
+ if (chatId === undefined || (threadId === undefined && !chatScoped)) {
505
+ return () => undefined;
506
+ }
507
+ const key = threadId === undefined ? undefined : `${chatId}:${threadId}`;
508
+ if (key) {
509
+ const existing = pending.get(key);
510
+ if (existing) existing.count += 1;
511
+ else pending.set(key, { target: { chatId, threadId: threadId! }, count: 1 });
512
+ } else {
513
+ pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
514
+ }
515
+ let completed = false;
516
+ return () => {
517
+ if (completed) return;
518
+ completed = true;
519
+ if (!key) {
520
+ const count = pendingChats.get(chatId);
521
+ if (!count || count <= 1) pendingChats.delete(chatId);
522
+ else pendingChats.set(chatId, count - 1);
523
+ return;
524
+ }
525
+ const current = pending.get(key);
526
+ if (!current || current.count <= 1) pending.delete(key);
527
+ else current.count -= 1;
528
+ };
529
+ },
530
+ hasPendingTarget(target) {
531
+ return pendingChats.has(target.chatId) ||
532
+ (target.threadId !== undefined && pending.has(`${target.chatId}:${target.threadId}`));
533
+ },
534
+ listPendingTargets() {
535
+ return Array.from(pending.values(), ({ target }) => ({ ...target }));
536
+ },
537
+ listPendingChats() {
538
+ return Array.from(pendingChats.keys());
539
+ },
540
+ };
541
+ }
542
+
543
+ export function createTelegramApiTargetTrackingClient(
544
+ client: TelegramApiClient,
545
+ activity: TelegramApiTargetActivityRuntime,
546
+ ): TelegramApiClient {
547
+ const track = async <T>(
548
+ method: string,
549
+ body: Record<string, unknown>,
550
+ operation: () => Promise<T>,
551
+ ): Promise<T> => {
552
+ const end = activity.begin(method, body);
553
+ try {
554
+ return await operation();
555
+ } finally {
556
+ end();
557
+ }
558
+ };
559
+ return {
560
+ call: (method, body, options) => track(
561
+ method,
562
+ body,
563
+ () => client.call(method, body, options),
564
+ ),
565
+ callMultipart: (method, fields, fileField, filePath, fileName, options) =>
566
+ track(method, fields, () => client.callMultipart(
567
+ method, fields, fileField, filePath, fileName, options,
568
+ )),
569
+ downloadFile: client.downloadFile,
570
+ answerCallbackQuery: client.answerCallbackQuery,
571
+ ...(client.answerGuestQuery
572
+ ? { answerGuestQuery: client.answerGuestQuery }
573
+ : {}),
574
+ };
575
+ }
576
+
577
+ export type TelegramApiWorkspaceAdmissionScopeLike =
578
+ | { kind: "target"; target: { chatId: number; threadId: number } }
579
+ | { kind: "chat"; chatId: number }
580
+ | { kind: "profile" };
581
+
582
+ export interface TelegramApiWorkspaceAdmissionLeaseLike {
583
+ operationId: string;
584
+ operationKind: string;
585
+ profileKey: string;
586
+ scope: TelegramApiWorkspaceAdmissionScopeLike;
587
+ owner: { processId: number; processBirthId: string };
588
+ acquiredAtMs: number;
589
+ }
590
+
591
+ export interface TelegramApiWorkspaceAdmissionPort {
592
+ acquireAdmission: (input: {
593
+ operationId: string;
594
+ operationKind: string;
595
+ scope: TelegramApiWorkspaceAdmissionScopeLike;
596
+ }) =>
597
+ | {
598
+ kind: "acquired";
599
+ lease: TelegramApiWorkspaceAdmissionLeaseLike;
600
+ resumed: boolean;
601
+ }
602
+ | { kind: "blocked"; reason: "retirement-fenced" };
603
+ releaseAdmission: (expected: TelegramApiWorkspaceAdmissionLeaseLike) => boolean;
604
+ }
605
+
606
+ export class TelegramApiWorkspaceAdmissionError extends Error {
607
+ readonly code: "blocked" | "unavailable" | "release-lost" | "duplicate-operation";
608
+
609
+ constructor(code: TelegramApiWorkspaceAdmissionError["code"], message: string) {
610
+ super(message);
611
+ this.name = "TelegramApiWorkspaceAdmissionError";
612
+ this.code = code;
613
+ }
614
+ }
615
+
616
+ export function getTelegramApiWorkspaceAdmissionScope(
617
+ body: Record<string, unknown>,
618
+ ): TelegramApiWorkspaceAdmissionScopeLike | undefined {
619
+ if (!("chat_id" in body)) return undefined;
620
+ const chatId = parseTelegramApiTargetInteger(body.chat_id);
621
+ if (chatId === undefined || chatId === 0) return { kind: "profile" };
622
+ if (!("message_thread_id" in body) || body.message_thread_id === undefined) {
623
+ return { kind: "chat", chatId };
624
+ }
625
+ const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
626
+ if (threadId === undefined || threadId <= 0) return { kind: "profile" };
627
+ return { kind: "target", target: { chatId, threadId } };
628
+ }
629
+
630
+ export function createTelegramApiWorkspaceAdmissionClient(
631
+ client: TelegramApiClient,
632
+ admission:
633
+ | TelegramApiWorkspaceAdmissionPort
634
+ | (() => TelegramApiWorkspaceAdmissionPort | undefined),
635
+ options: {
636
+ onReleaseError?: (error: unknown, method: string) => void;
637
+ createOperationId?: () => string;
638
+ } = {},
639
+ ): TelegramApiClient {
640
+ const admit = async <T>(
641
+ method: string,
642
+ body: Record<string, unknown>,
643
+ operation: () => Promise<T>,
644
+ ): Promise<T> => {
645
+ const scope = getTelegramApiWorkspaceAdmissionScope(body);
646
+ if (!scope) return operation();
647
+ const currentAdmission =
648
+ typeof admission === "function" ? admission() : admission;
649
+ if (!currentAdmission) {
650
+ throw new TelegramApiWorkspaceAdmissionError(
651
+ "unavailable",
652
+ "Telegram API Workspace admission authority is unavailable.",
653
+ );
654
+ }
655
+ const operationId = `api:${(options.createOperationId ?? randomUUID)()}`;
656
+ if (activeTelegramApiWorkspaceAdmissionOperationIds.has(operationId)) {
657
+ throw new TelegramApiWorkspaceAdmissionError(
658
+ "duplicate-operation",
659
+ "Telegram API Workspace admission operation is already active.",
660
+ );
661
+ }
662
+ activeTelegramApiWorkspaceAdmissionOperationIds.add(operationId);
663
+ try {
664
+ const acquired = currentAdmission.acquireAdmission({
665
+ operationId,
666
+ operationKind: `api.${method}`,
667
+ scope,
668
+ });
669
+ if (acquired.kind === "blocked") {
670
+ throw new TelegramApiWorkspaceAdmissionError(
671
+ "blocked",
672
+ "Telegram API target is temporarily unavailable during Workspace retirement.",
673
+ );
674
+ }
675
+ try {
676
+ return await operation();
677
+ } finally {
678
+ try {
679
+ if (!currentAdmission.releaseAdmission(acquired.lease)) {
680
+ throw new TelegramApiWorkspaceAdmissionError(
681
+ "release-lost",
682
+ "Telegram API Workspace admission lease disappeared before release.",
683
+ );
684
+ }
685
+ } catch (error) {
686
+ try {
687
+ options.onReleaseError?.(error, method);
688
+ } catch {
689
+ // Diagnostics cannot convert an already-settled API request into replay.
690
+ }
691
+ }
692
+ }
693
+ } finally {
694
+ activeTelegramApiWorkspaceAdmissionOperationIds.delete(operationId);
695
+ }
696
+ };
697
+ return {
698
+ call: (method, body, callOptions) =>
699
+ admit(method, body, () => client.call(method, body, callOptions)),
700
+ callMultipart: (
701
+ method,
702
+ fields,
703
+ fileField,
704
+ filePath,
705
+ fileName,
706
+ callOptions,
707
+ ) =>
708
+ admit(method, fields, () =>
709
+ client.callMultipart(
710
+ method,
711
+ fields,
712
+ fileField,
713
+ filePath,
714
+ fileName,
715
+ callOptions,
716
+ ),
717
+ ),
718
+ downloadFile: client.downloadFile,
719
+ answerCallbackQuery: client.answerCallbackQuery,
720
+ ...(client.answerGuestQuery
721
+ ? { answerGuestQuery: client.answerGuestQuery }
722
+ : {}),
723
+ };
724
+ }
725
+
468
726
  export interface TelegramBridgeApiRuntimeDeps {
469
727
  captureRequestErrorHandler?: (body: Record<string, unknown>) =>
470
728
  ((error: unknown) => Promise<void>) | undefined;
@@ -1411,7 +1669,7 @@ export function createTelegramNativeMarkdownDraftSender(deps: {
1411
1669
  return deps.sendRichMessageDraft({
1412
1670
  chat_id: chatId,
1413
1671
  draft_id: draftId,
1414
- rich_message: { markdown: text, skip_entity_detection: true },
1672
+ rich_message: { markdown: text },
1415
1673
  ...(options?.message_thread_id !== undefined
1416
1674
  ? { message_thread_id: options.message_thread_id }
1417
1675
  : {}),
@@ -1446,11 +1704,28 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1446
1704
  getBotToken: () => string | undefined;
1447
1705
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
1448
1706
  captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
1707
+ targetActivity?: TelegramApiTargetActivityRuntime;
1708
+ workspaceAdmission?:
1709
+ | TelegramApiWorkspaceAdmissionPort
1710
+ | (() => TelegramApiWorkspaceAdmissionPort | undefined);
1449
1711
  }): TelegramBridgeApiRuntime {
1712
+ const client = createTelegramApiClient(deps.getBotToken, {
1713
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1714
+ });
1715
+ const admittedClient = deps.workspaceAdmission
1716
+ ? createTelegramApiWorkspaceAdmissionClient(client, deps.workspaceAdmission, {
1717
+ onReleaseError(error, method) {
1718
+ deps.recordRuntimeEvent("api", error, {
1719
+ phase: "workspace-admission-release",
1720
+ method,
1721
+ });
1722
+ },
1723
+ })
1724
+ : client;
1450
1725
  return createTelegramBridgeApiRuntime({
1451
- client: createTelegramApiClient(deps.getBotToken, {
1452
- recordRuntimeEvent: deps.recordRuntimeEvent,
1453
- }),
1726
+ client: deps.targetActivity
1727
+ ? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
1728
+ : admittedClient,
1454
1729
  tempDir: getTelegramApiTempDir(),
1455
1730
  maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
1456
1731
  tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
@@ -1783,11 +2058,44 @@ export function createTelegramBridgeApiRuntime(
1783
2058
  */
1784
2059
  export function createTelegramApiClient(
1785
2060
  getBotToken: () => string | undefined,
1786
- options: TelegramAnswerCallbackQueryOptions = {},
2061
+ options: TelegramAnswerCallbackQueryOptions & { now?: () => number } = {},
1787
2062
  ): TelegramApiClient {
2063
+ const now = options.now ?? Date.now;
2064
+ const draftRetryNotBeforeByTarget = new Map<string, number>();
1788
2065
  return {
1789
- call: async (method, body, options) => {
1790
- return callTelegram(getBotToken(), method, body, options);
2066
+ call: async <TResponse>(
2067
+ method: string,
2068
+ body: Record<string, unknown>,
2069
+ options?: TelegramApiCallOptions,
2070
+ ): Promise<TResponse> => {
2071
+ const token = getBotToken();
2072
+ // Cooldown keys retain only the public bot-id prefix, not the credential.
2073
+ const botId = token?.match(/^(\d+):/)?.[1];
2074
+ const isDraft = method === "sendMessageDraft" || method === "sendRichMessageDraft";
2075
+ const draftKey = isDraft && botId
2076
+ ? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
2077
+ : undefined;
2078
+ if (draftKey) {
2079
+ const nowMs = now();
2080
+ for (const [key, deadline] of draftRetryNotBeforeByTarget) {
2081
+ if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
2082
+ }
2083
+ if (draftRetryNotBeforeByTarget.has(draftKey)) return false as TResponse;
2084
+ }
2085
+ try {
2086
+ // A draft is a replaceable snapshot, not a body to replay after backoff.
2087
+ return await callTelegram<TResponse>(
2088
+ token, method, body, isDraft ? { ...options, maxAttempts: 1 } : options,
2089
+ );
2090
+ } catch (error) {
2091
+ if (draftKey && isRetryableTelegramApiError(error)) {
2092
+ draftRetryNotBeforeByTarget.set(draftKey, Math.max(
2093
+ draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
2094
+ now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500),
2095
+ ));
2096
+ }
2097
+ throw error;
2098
+ }
1791
2099
  },
1792
2100
  callMultipart: async (
1793
2101
  method,