@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
@@ -8,7 +8,7 @@ import { unlink } from "node:fs/promises";
8
8
  import { basename, extname } from "node:path";
9
9
 
10
10
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
11
- import { buildTelegramMultipartReplyParameters } from "./replies.ts";
11
+ import { withTelegramReplyParameters } from "./replies.ts";
12
12
  import {
13
13
  getTelegramTargetThreadParams,
14
14
  type TelegramTarget,
@@ -80,17 +80,6 @@ export interface TelegramVoiceReplySenderPorts<THandler = unknown> {
80
80
  getProgrammaticVoiceHandlers?: () => TelegramOutboundProgrammaticVoiceHandler[];
81
81
  }
82
82
 
83
- function buildVoiceReplyParameters(
84
- chatId: number,
85
- replyToPrompt: boolean | undefined,
86
- replyToMessageId: number | undefined,
87
- target?: TelegramTarget,
88
- ): string | undefined {
89
- if (replyToPrompt === false || replyToMessageId === undefined)
90
- return undefined;
91
- return buildTelegramMultipartReplyParameters(chatId, replyToMessageId, target);
92
- }
93
-
94
83
  async function ensureTelegramVoiceFileFormat(
95
84
  filePath: string,
96
85
  ): Promise<string> {
@@ -131,36 +120,33 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
131
120
  if (deps.isDeliveryActive?.() === false) return;
132
121
  await sendVoiceChatAction(deps, turn.chatId);
133
122
  if (deps.isDeliveryActive?.() === false) return;
134
- const replyParameters = buildVoiceReplyParameters(
135
- turn.chatId,
136
- options?.replyToPrompt,
137
- turn.replyToMessageId,
138
- turn.target,
139
- );
140
- await deps.sendMultipart(
141
- "sendVoice",
142
- {
143
- chat_id: String(turn.chatId),
144
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
145
- ...(turn.target
146
- ? Object.fromEntries(
147
- Object.entries(getTelegramTargetThreadParams(turn.target)).map(
148
- ([key, value]) => [key, String(value)],
149
- ),
150
- )
151
- : {}),
152
- ...(options?.replyMarkup !== undefined && options.replyMarkup !== null
153
- ? {
154
- reply_markup:
155
- typeof options.replyMarkup === "string"
156
- ? options.replyMarkup
157
- : JSON.stringify(options.replyMarkup),
158
- }
159
- : {}),
160
- },
161
- "voice",
162
- voiceFilePath,
163
- basename(voiceFilePath),
123
+ await withTelegramReplyParameters(
124
+ turn.chatId, options?.replyToPrompt === false ? undefined : turn.replyToMessageId, turn.target,
125
+ (replyParameters) => deps.sendMultipart(
126
+ "sendVoice",
127
+ {
128
+ chat_id: String(turn.chatId),
129
+ ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
130
+ ...(turn.target
131
+ ? Object.fromEntries(
132
+ Object.entries(getTelegramTargetThreadParams(turn.target)).map(
133
+ ([key, value]) => [key, String(value)],
134
+ ),
135
+ )
136
+ : {}),
137
+ ...(options?.replyMarkup !== undefined && options.replyMarkup !== null
138
+ ? {
139
+ reply_markup:
140
+ typeof options.replyMarkup === "string"
141
+ ? options.replyMarkup
142
+ : JSON.stringify(options.replyMarkup),
143
+ }
144
+ : {}),
145
+ },
146
+ "voice",
147
+ voiceFilePath,
148
+ basename(voiceFilePath),
149
+ ),
164
150
  );
165
151
  };
166
152
 
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import type { TelegramAssistantSegmentEvent } from "./activity.ts";
12
12
  import { resolveTelegramTempDir } from "./paths.ts";
13
13
  import * as Replies from "./replies.ts";
14
+ import type { TelegramPreparedPreviewDelivery } from "./preview.ts";
14
15
  import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
15
16
  import type {
16
17
  TelegramEditMessageTextBody,
@@ -249,12 +250,8 @@ export interface TelegramOutboundTextPreviewRuntimeDeps<
249
250
  > {
250
251
  execCommand: TelegramVoiceReplySenderDeps["execCommand"];
251
252
  getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
252
- finalizeMarkdownPreview: (
253
- chatId: number,
254
- markdown: string,
255
- replyToMessageId: number,
256
- options?: { replyMarkup?: TReplyMarkup },
257
- ) => Promise<boolean>;
253
+ finalizeMarkdownPreview: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"];
254
+ preparePreviewDelivery?: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
258
255
  cwd?: string;
259
256
  recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
260
257
  }
@@ -729,12 +726,13 @@ export function createTelegramOutboundTextPreviewRuntime<
729
726
  TReplyMarkup = unknown,
730
727
  >(
731
728
  deps: TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
732
- ): Pick<
733
- TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
734
- "finalizeMarkdownPreview"
735
- > {
736
- return {
737
- finalizeMarkdownPreview: async (
729
+ ): {
730
+ finalizeMarkdownPreview: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"];
731
+ preparePreviewDelivery: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup> | undefined;
732
+ } {
733
+ const wrap = (
734
+ finalize: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"],
735
+ ): TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"] => async (
738
736
  chatId,
739
737
  markdown,
740
738
  replyToMessageId,
@@ -747,7 +745,7 @@ export function createTelegramOutboundTextPreviewRuntime<
747
745
  recordRuntimeEvent: deps.recordRuntimeEvent,
748
746
  replyMarkup: options?.replyMarkup,
749
747
  });
750
- return deps.finalizeMarkdownPreview(
748
+ return finalize(
751
749
  chatId,
752
750
  transformed.text,
753
751
  replyToMessageId,
@@ -758,6 +756,12 @@ export function createTelegramOutboundTextPreviewRuntime<
758
756
  : {}),
759
757
  },
760
758
  );
759
+ };
760
+ return {
761
+ finalizeMarkdownPreview: wrap(deps.finalizeMarkdownPreview),
762
+ preparePreviewDelivery(isDeliveryActive) {
763
+ const prepared = deps.preparePreviewDelivery?.(isDeliveryActive);
764
+ return prepared ? { ...prepared, finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview) } : undefined;
761
765
  },
762
766
  };
763
767
  }
@@ -1018,7 +1022,7 @@ export function createTelegramAssistantOutputSender<
1018
1022
  };
1019
1023
  await outboundRuntime.sendMarkdownReply(
1020
1024
  target.chatId,
1021
- undefined,
1025
+ event.source === "telegram" ? event.replyToMessageId : undefined,
1022
1026
  buttonReply.markdown,
1023
1027
  {
1024
1028
  target,
@@ -87,6 +87,35 @@ export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
87
87
  };
88
88
  }
89
89
 
90
+ /** Durable Workspace admission ledger (<agentDir>/tmp/telegram/workspace-admission[.<profile>].json). */
91
+ export function resolveTelegramWorkspaceAdmissionPath(
92
+ agentDir = resolveAgentDir(),
93
+ profileName?: string,
94
+ ): string {
95
+ return resolveTelegramProfileTempFilePath(
96
+ "workspace-admission",
97
+ "json",
98
+ agentDir,
99
+ profileName,
100
+ );
101
+ }
102
+
103
+ /** Durable inactive Thread cleanup work-set journal. */
104
+ export function resolveTelegramThreadCleanupWorkPath(
105
+ agentDir = resolveAgentDir(),
106
+ profileName?: string,
107
+ ): string {
108
+ return resolveTelegramProfileTempFilePath("thread-cleanup", "json", agentDir, profileName);
109
+ }
110
+
111
+ /** Durable agent-authored channel post journal. */
112
+ export function resolveTelegramChannelPostJournalPath(
113
+ agentDir = resolveAgentDir(),
114
+ profileName?: string,
115
+ ): string {
116
+ return resolveTelegramProfileTempFilePath("channel-posts", "json", agentDir, profileName);
117
+ }
118
+
90
119
  /** Durable inbound update journal (<agentDir>/tmp/telegram/inbox[.<profile>].json). */
91
120
  export function resolveTelegramUpdateJournalPath(
92
121
  agentDir = resolveAgentDir(),
@@ -280,11 +280,12 @@ export type TelegramDurablePollingRuntimeAssemblyDeps<
280
280
  "appendUpdateBatch" | "getJournalEntryCount" | "signalUpdateWorker"
281
281
  > & {
282
282
  canStart?: (ctx: TContext) => boolean;
283
+ prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
283
284
  journal: {
284
285
  appendBatch: (
285
286
  updates: readonly TUpdate[],
286
287
  acceptedThroughUpdateId?: number,
287
- ) => MaybePromise<unknown>;
288
+ ) => { nonExcludedUpdateIds: readonly number[] };
288
289
  getAcceptedThroughUpdateId: () => number | undefined;
289
290
  prepareCursorCutover?: () => MaybePromise<void>;
290
291
  getEntryCount: () => number;
@@ -303,7 +304,30 @@ export function createTelegramDurablePollingRuntimeAssembly<
303
304
  ): TelegramDurablePollingRuntimeAssembly<TContext> {
304
305
  const controller = createTelegramPollingControllerRuntime({
305
306
  ...deps,
306
- appendUpdateBatch: deps.journal.appendBatch,
307
+ appendUpdateBatch(updates, cursor) {
308
+ const result = deps.journal.appendBatch(updates, cursor);
309
+ if (!deps.prepareUpdateBatch) return result;
310
+ // No await: an already-draining worker must not observe the new batch before preparation.
311
+ try {
312
+ const included = new Set(result.nonExcludedUpdateIds);
313
+ let batch: TUpdate[] = [];
314
+ for (const update of updates) {
315
+ if (included.has(update.update_id)) batch.push(update);
316
+ else if (batch.length > 0) {
317
+ deps.prepareUpdateBatch(batch);
318
+ batch = [];
319
+ }
320
+ }
321
+ if (batch.length > 0) deps.prepareUpdateBatch(batch);
322
+ } catch (error) {
323
+ try {
324
+ deps.recordRuntimeEvent?.("polling", error, { phase: "batch-preparation", updateCount: updates.length });
325
+ } catch {
326
+ // Already-published input must still reach the worker if diagnostics fail.
327
+ }
328
+ }
329
+ return result;
330
+ },
307
331
  getAcceptedThroughUpdateId: deps.journal.getAcceptedThroughUpdateId,
308
332
  getJournalEntryCount: deps.journal.getEntryCount,
309
333
  signalUpdateWorker: deps.journal.signalWorker,
@@ -393,7 +417,6 @@ export function createTelegramPollingControllerRuntime<
393
417
  getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
394
418
  getJournalEntryCount: deps.getJournalEntryCount,
395
419
  signalUpdateWorker: deps.signalUpdateWorker,
396
- prepareUpdateBatch: deps.prepareUpdateBatch,
397
420
  updateStatus: deps.updateStatus,
398
421
  sleep: deps.sleep,
399
422
  onPhaseChange(phase, currentUpdateId) {
@@ -651,6 +674,7 @@ export interface TelegramThreadCapabilityRuntimeDeps<
651
674
  getPollingStartedWithTelegramBus: () => boolean;
652
675
  setPollingStartedWithTelegramBus: (started: boolean) => void;
653
676
  setTopicModeUnavailable: (unavailable: boolean) => void;
677
+ suspendLiveThreadTarget?: () => void;
654
678
  stopFollowerRegistration: () => void;
655
679
  startClassicPolling: (ctx: TContext) => MaybePromise<void>;
656
680
  stopClassicPolling: () => MaybePromise<void>;
@@ -682,6 +706,8 @@ export interface TelegramThreadCapabilityStateRuntime {
682
706
  isBusRuntimeEnabled(): boolean;
683
707
  shouldForceFreshLeaderThread(): boolean;
684
708
  setForceFreshLeaderThread(forceFresh: boolean): void;
709
+ getRequestedThreadName(): string | undefined;
710
+ setRequestedThreadName(threadName: string | undefined): void;
685
711
  }
686
712
 
687
713
  export type TelegramThreadTargetObservationHandler<TContext> = (
@@ -715,6 +741,10 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
715
741
  ctx: TContext,
716
742
  owner: TOwner,
717
743
  ) => Promise<boolean | undefined>;
744
+ restoreFollowerWithOwner: (
745
+ ctx: TContext,
746
+ owner: TOwner,
747
+ ) => Promise<boolean | undefined>;
718
748
  stopFollowerRegistration: () => void;
719
749
  }
720
750
 
@@ -738,6 +768,11 @@ export interface TelegramThreadAwarePollingDeps<
738
768
  ctx: TContext,
739
769
  owner: TOwner,
740
770
  ) => Promise<boolean | undefined>;
771
+ restoreFollowerWithLeader?: (
772
+ ctx: TContext,
773
+ owner: TOwner,
774
+ ) => Promise<boolean | undefined>;
775
+ hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
741
776
  stopFollowerRegistration: () => void;
742
777
  }
743
778
 
@@ -760,6 +795,12 @@ export interface TelegramThreadCapabilityOrchestrationDeps<
760
795
  ctx: TContext,
761
796
  owner: TOwner,
762
797
  ) => Promise<boolean | undefined>;
798
+ restoreFollowerWithLeader?: (
799
+ ctx: TContext,
800
+ owner: TOwner,
801
+ ) => Promise<boolean | undefined>;
802
+ hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
803
+ suspendLiveThreadTarget?: () => void;
763
804
  stopFollowerRegistration: () => void;
764
805
  isTopicModeUnavailableError: (error: unknown) => boolean;
765
806
  updateStatus: (ctx: TContext) => void;
@@ -780,6 +821,7 @@ export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapa
780
821
  let busPollingStarted = false;
781
822
  let topicModeUnavailable = false;
782
823
  let forceFreshLeaderThread = false;
824
+ let requestedThreadName: string | undefined;
783
825
  return {
784
826
  isBusPollingStarted: () => busPollingStarted,
785
827
  setBusPollingStarted(started) {
@@ -794,6 +836,10 @@ export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapa
794
836
  setForceFreshLeaderThread(forceFresh) {
795
837
  forceFreshLeaderThread = forceFresh;
796
838
  },
839
+ getRequestedThreadName: () => requestedThreadName,
840
+ setRequestedThreadName(threadName) {
841
+ requestedThreadName = threadName;
842
+ },
797
843
  };
798
844
  }
799
845
 
@@ -818,6 +864,7 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
818
864
  getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
819
865
  setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
820
866
  setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
867
+ suspendLiveThreadTarget: deps.suspendLiveThreadTarget,
821
868
  stopFollowerRegistration: deps.stopFollowerRegistration,
822
869
  startClassicPolling: deps.startClassicPolling,
823
870
  stopClassicPolling: deps.stopClassicPolling,
@@ -850,6 +897,8 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
850
897
  startLeaderHealth: deps.startLeaderHealth,
851
898
  stopLeaderHealth: deps.stopLeaderHealth,
852
899
  registerFollowerWithLeader: deps.registerFollowerWithLeader,
900
+ restoreFollowerWithLeader: deps.restoreFollowerWithLeader,
901
+ hasRememberedWorkspaceBinding: deps.hasRememberedWorkspaceBinding,
853
902
  stopFollowerRegistration: deps.stopFollowerRegistration,
854
903
  recordEvent: deps.recordEvent,
855
904
  setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
@@ -958,14 +1007,16 @@ export async function applyTelegramThreadCapability<TContext>(
958
1007
  if (!isCurrent()) return;
959
1008
  deps.setTopicModeUnavailable(true);
960
1009
  deps.stopFollowerRegistration();
1010
+ const hadLiveThreadTransport = deps.getPollingStartedWithTelegramBus();
961
1011
  if (
962
- deps.getPollingStartedWithTelegramBus() ||
1012
+ hadLiveThreadTransport ||
963
1013
  hasTelegramClassicRestoreFailure(previousBotState)
964
1014
  ) {
965
1015
  deps.stopLeaderHealth();
966
1016
  await deps.stopBusPolling();
967
1017
  if (!isCurrent()) return;
968
1018
  deps.setPollingStartedWithTelegramBus(false);
1019
+ if (hadLiveThreadTransport) deps.suspendLiveThreadTarget?.();
969
1020
  try {
970
1021
  await deps.startClassicPolling(ctx);
971
1022
  if (!isCurrent()) return;
@@ -1110,24 +1161,34 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1110
1161
  }
1111
1162
  await deps.stopClassicPolling();
1112
1163
  };
1113
- const registerFollowerWithOwner = async (
1114
- ctx: TContext,
1115
- owner: TOwner,
1116
- ): Promise<boolean | undefined> => {
1164
+ const refreshFollowerState = async (): Promise<boolean> => {
1117
1165
  if (deps.topicTargetStore.refresh) {
1118
1166
  await deps.topicTargetStore.refresh();
1119
1167
  } else {
1120
1168
  await deps.topicTargetStore.load();
1121
1169
  }
1122
- if (deps.topicTargetStore.getBotState().threadMode !== "enabled") {
1123
- return undefined;
1124
- }
1170
+ return deps.topicTargetStore.getBotState().threadMode === "enabled";
1171
+ };
1172
+ const registerFollowerWithOwner = async (
1173
+ ctx: TContext,
1174
+ owner: TOwner,
1175
+ ): Promise<boolean | undefined> => {
1176
+ if (!(await refreshFollowerState())) return undefined;
1125
1177
  return deps.registerFollowerWithLeader(ctx, owner);
1126
1178
  };
1179
+ const restoreFollowerWithOwner = async (
1180
+ ctx: TContext,
1181
+ owner: TOwner,
1182
+ ): Promise<boolean | undefined> => {
1183
+ if (!(await refreshFollowerState())) return undefined;
1184
+ if (!deps.hasRememberedWorkspaceBinding?.(ctx)) return undefined;
1185
+ return deps.restoreFollowerWithLeader?.(ctx, owner);
1186
+ };
1127
1187
  return {
1128
1188
  startPolling,
1129
1189
  stopPolling,
1130
1190
  registerFollowerWithOwner,
1191
+ restoreFollowerWithOwner,
1131
1192
  stopFollowerRegistration: deps.stopFollowerRegistration,
1132
1193
  };
1133
1194
  }
@@ -1179,9 +1240,9 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1179
1240
  let consecutiveDisabledProbes = 0;
1180
1241
  const stop = (): void => {
1181
1242
  generation += 1;
1182
- deps.lifecycle?.invalidate();
1183
1243
  if (interval) clearInterval(interval);
1184
1244
  interval = undefined;
1245
+ deps.lifecycle?.invalidate();
1185
1246
  };
1186
1247
  const check = (ctx: TContext): void => {
1187
1248
  if (transitionPromise || !canProbeTelegramThreadCapability(ctx, deps)) {
@@ -1276,8 +1337,19 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1276
1337
  return {
1277
1338
  start(ctx) {
1278
1339
  stop();
1340
+ const expectedGeneration = generation;
1279
1341
  interval = setInterval(() => {
1280
- check(ctx);
1342
+ if (generation !== expectedGeneration) return;
1343
+ try {
1344
+ check(ctx);
1345
+ } catch (error) {
1346
+ try { stop(); } catch { /* Timer shutdown must not escape the callback. */ }
1347
+ try {
1348
+ deps.recordEvent("bus", error, { phase: "capability-monitor" });
1349
+ } catch {
1350
+ // Monitor diagnostics cannot create an uncaught interval exception.
1351
+ }
1352
+ }
1281
1353
  }, intervalMs);
1282
1354
  interval.unref?.();
1283
1355
  },
@@ -1416,7 +1488,6 @@ export interface TelegramPollLoopDeps<
1416
1488
  getAcceptedThroughUpdateId?: () => number | undefined;
1417
1489
  getJournalEntryCount: () => number;
1418
1490
  signalUpdateWorker: () => void;
1419
- prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
1420
1491
  onErrorStatus: (message: string) => void;
1421
1492
  onStatusReset: () => void;
1422
1493
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
@@ -1446,7 +1517,6 @@ export interface TelegramPollLoopRunnerDeps<
1446
1517
  getAcceptedThroughUpdateId?: () => number | undefined;
1447
1518
  getJournalEntryCount: () => number;
1448
1519
  signalUpdateWorker: () => void;
1449
- prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
1450
1520
  updateStatus: (ctx: TContext, message?: string) => void;
1451
1521
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
1452
1522
  onPhaseChange?: (
@@ -1501,7 +1571,6 @@ export function createTelegramPollLoopRunner<
1501
1571
  getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
1502
1572
  getJournalEntryCount: deps.getJournalEntryCount,
1503
1573
  signalUpdateWorker: deps.signalUpdateWorker,
1504
- prepareUpdateBatch: deps.prepareUpdateBatch,
1505
1574
  onErrorStatus: (message) => {
1506
1575
  updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
1507
1576
  message,
@@ -1702,7 +1771,6 @@ export async function runTelegramPollLoop<
1702
1771
  reportTelegramPollingPhase(deps, "long-poll");
1703
1772
  const updates = await requestTelegramUpdatesWithinBudget(deps, request);
1704
1773
  reportTelegramPollingResponse(deps, updates.length);
1705
- deps.prepareUpdateBatch?.(updates);
1706
1774
  consecutiveGetUpdatesConflicts = 0;
1707
1775
  currentUpdateId = updates[0]?.update_id;
1708
1776
  await admitTelegramPollingUpdateBatch({