@llblab/pi-telegram 0.45.11 → 0.47.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 (204) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +6 -6
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +17 -3
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +11 -3
  183. package/lib/bus-leader.ts +57 -20
  184. package/lib/bus.ts +12 -2
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +26 -1
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/pi.ts +4 -0
  193. package/lib/sync.ts +3 -0
  194. package/lib/telegram-api.ts +12 -1
  195. package/lib/thread-cleanup-manager.ts +36 -6
  196. package/lib/thread-display.ts +96 -18
  197. package/lib/threads.ts +150 -30
  198. package/package.json +56 -13
  199. package/scripts/build-dist.mjs +44 -0
  200. package/scripts/measure-bus.mjs +8 -1
  201. package/scripts/measure-workspace.mjs +8 -1
  202. package/skills/generative-apps/SKILL.md +1 -1
  203. package/skills/show-me/SKILL.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
package/lib/bindings.ts CHANGED
@@ -10,6 +10,7 @@ import * as ChannelPosts from "./channel-posts.ts";
10
10
  import * as CommandTemplates from "./command-templates.ts";
11
11
  import * as Commands from "./commands.ts";
12
12
  import * as Config from "./config.ts";
13
+ import * as Delivery from "./delivery.ts";
13
14
  import * as Keyboard from "./keyboard.ts";
14
15
  import * as Lifecycle from "./lifecycle.ts";
15
16
  import * as Locks from "./locks.ts";
@@ -176,14 +177,47 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
176
177
  };
177
178
  }
178
179
 
180
+ export interface TelegramGenerativeAppLiveSurfaceBinding {
181
+ get: () =>
182
+ | GenerativeApps.GenerativeAppLiveSurfaceRuntime<GenerativeApps.TelegramBindLiveHandle>
183
+ | undefined;
184
+ set: (
185
+ runtime: GenerativeApps.GenerativeAppLiveSurfaceRuntime<GenerativeApps.TelegramBindLiveHandle>
186
+ | undefined,
187
+ ) => void;
188
+ shutdown: () => void;
189
+ }
190
+
191
+ export function createTelegramGenerativeAppLiveSurfaceBinding():
192
+ TelegramGenerativeAppLiveSurfaceBinding {
193
+ let current:
194
+ | GenerativeApps.GenerativeAppLiveSurfaceRuntime<GenerativeApps.TelegramBindLiveHandle>
195
+ | undefined;
196
+ return {
197
+ get() { return current; },
198
+ set(runtime) {
199
+ current?.shutdown();
200
+ current = runtime;
201
+ },
202
+ shutdown() {
203
+ current?.shutdown();
204
+ current = undefined;
205
+ },
206
+ };
207
+ }
208
+
179
209
  export function createTelegramGenerativeAppBoundButtonActionInvoker<
180
210
  TQuery extends {
181
- message?: { chat?: { id?: number }; message_id?: number };
211
+ message?: { chat?: { id?: number }; message_id?: number; message_thread_id?: number };
182
212
  },
183
213
  >(deps: {
184
214
  agentDir: string;
185
215
  assertExecutionCurrent: (query: TQuery) => void;
186
216
  getExecutionFence: (query: TQuery) => GenerativeApps.GenerativeAppExecutionFence | undefined;
217
+ getActiveProfileName?: () => string | undefined;
218
+ getLiveSurfaceRuntime?: () =>
219
+ | GenerativeApps.GenerativeAppLiveSurfaceRuntime<GenerativeApps.TelegramBindLiveHandle>
220
+ | undefined;
187
221
  planOutput: ReturnType<typeof OutboundHandlers.createTelegramOutboundReplyPlanner>;
188
222
  sendMarkdownReply: (
189
223
  chatId: number,
@@ -205,10 +239,29 @@ export function createTelegramGenerativeAppBoundButtonActionInvoker<
205
239
  ) => Promise<false | "new" | "edit"> {
206
240
  return async (action, query) => {
207
241
  let boundAction: GenerativeApps.GenerativeAppBoundAction | undefined;
242
+ let handedOff:
243
+ | GenerativeApps.GenerativeAppLiveSurface<GenerativeApps.TelegramBindLiveHandle>
244
+ | undefined;
208
245
  try {
209
246
  boundAction = GenerativeApps.parseGenerativeAppBoundAction(action.prompt);
210
247
  if (!boundAction) return false;
211
248
  deps.assertExecutionCurrent(query);
249
+ const chatId = query.message?.chat?.id;
250
+ const messageId = query.message?.message_id;
251
+ if (typeof chatId !== "number" || typeof messageId !== "number") {
252
+ throw new Error("Generative App callback target is unavailable.");
253
+ }
254
+ const liveSurfaces = deps.getLiveSurfaceRuntime?.();
255
+ handedOff = liveSurfaces?.take(GenerativeApps.getTelegramBindLiveSurfaceKey(
256
+ boundAction.app,
257
+ deps.getActiveProfileName?.() ?? "default",
258
+ {
259
+ chatId,
260
+ ...(query.message?.message_thread_id !== undefined
261
+ ? { threadId: query.message.message_thread_id }
262
+ : {}),
263
+ },
264
+ ));
212
265
  const result = await GenerativeApps.invokeGenerativeApp({
213
266
  agentDir: deps.agentDir,
214
267
  ...(deps.getExecutionFence(query)
@@ -227,11 +280,6 @@ export function createTelegramGenerativeAppBoundButtonActionInvoker<
227
280
  app: boundAction.app,
228
281
  });
229
282
  deps.assertExecutionCurrent(query);
230
- const chatId = query.message?.chat?.id;
231
- const messageId = query.message?.message_id;
232
- if (typeof chatId !== "number" || typeof messageId !== "number") {
233
- throw new Error("Generative App callback target is unavailable.");
234
- }
235
283
  const reply = deps.planOutput(result.output, {
236
284
  binding: {
237
285
  generation: result.generation,
@@ -239,6 +287,19 @@ export function createTelegramGenerativeAppBoundButtonActionInvoker<
239
287
  revision: result.revision,
240
288
  },
241
289
  });
290
+ if (handedOff && liveSurfaces) {
291
+ try {
292
+ await liveSurfaces.resume(handedOff, result);
293
+ deps.assertExecutionCurrent(query);
294
+ return "edit";
295
+ } catch (error) {
296
+ deps.recordRuntimeEvent("generative-app", error, {
297
+ phase: "bound-action-live-edit-fallback",
298
+ app: boundAction.app,
299
+ method: boundAction.method,
300
+ });
301
+ }
302
+ }
242
303
  if (result.viewMode === "edit" && deps.editInteractiveMessage) {
243
304
  let editFailed = false;
244
305
  try {
@@ -267,6 +328,7 @@ export function createTelegramGenerativeAppBoundButtonActionInvoker<
267
328
  deps.assertExecutionCurrent(query);
268
329
  return "new";
269
330
  } catch (error) {
331
+ if (handedOff) deps.getLiveSurfaceRuntime?.()?.open(handedOff);
270
332
  deps.recordRuntimeEvent("generative-app", error, {
271
333
  phase: "bound-action",
272
334
  ...(boundAction
@@ -534,6 +596,10 @@ interface TelegramCommandsAndToolsBindingDeps {
534
596
  resolveAgentTarget?: OutboundAttachments.TelegramOutboundMessageToolRegistrationDeps["resolveAgentTarget"];
535
597
  routeAgentMessage?: OutboundAttachments.TelegramOutboundMessageToolRegistrationDeps["routeAgentMessage"];
536
598
  canSendDirect: () => boolean;
599
+ setGenerativeAppLiveSurfaceRuntime?: (
600
+ runtime: GenerativeApps.GenerativeAppLiveSurfaceRuntime<GenerativeApps.TelegramBindLiveHandle>
601
+ | undefined,
602
+ ) => void;
537
603
  updateStatus: TelegramBridgeStatusUpdater;
538
604
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
539
605
  }
@@ -565,17 +631,31 @@ export function registerTelegramCommandsAndTools({
565
631
  resolveAgentTarget,
566
632
  routeAgentMessage,
567
633
  canSendDirect,
634
+ setGenerativeAppLiveSurfaceRuntime,
568
635
  recordRuntimeEvent,
569
636
  updateStatus,
570
637
  }: TelegramCommandsAndToolsBindingDeps): void {
571
638
  GenerativeApps.registerTelegramBindTool(pi, {
572
639
  agentDir,
640
+ getActiveProfileName: configStore.getActiveProfileName,
573
641
  getActiveTurn: activeTurnRuntime.get,
642
+ ...(setGenerativeAppLiveSurfaceRuntime
643
+ ? { setLiveSurfaceRuntime: setGenerativeAppLiveSurfaceRuntime }
644
+ : {}),
645
+ isDeliveryHandleCurrent: Delivery.isTelegramDeliveryHandleCurrent,
646
+ editView: (handle, view) => Delivery.editTelegramView(
647
+ handle as Delivery.TelegramDeliveryHandle,
648
+ view as Delivery.TelegramDeliveryView,
649
+ ),
574
650
  planOutput: OutboundHandlers.createTelegramOutboundReplyPlanner(
575
651
  buttonActionStore,
576
652
  Config.createTelegramConfigControls(configStore).getAssistantRenderingMode,
577
653
  ),
578
654
  sendMarkdownReply,
655
+ sendView: (view, options) => Delivery.sendTelegramView(
656
+ view as Delivery.TelegramDeliveryView,
657
+ options,
658
+ ),
579
659
  recordRuntimeEvent,
580
660
  });
581
661
  ChannelPosts.registerTelegramChannelPostMutationTool(pi, { mutate: mutateChannelPost });
@@ -782,6 +862,7 @@ interface TelegramLifecycleBindingDeps {
782
862
  deferredQueueDispatchRuntime: Queue.TelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>;
783
863
  modelContextAvailabilityRuntime: Prompts.TelegramModelContextAvailabilityRuntime;
784
864
  disconnectOnQuit?: () => Promise<unknown>;
865
+ shutdownGenerativeAppLiveSurfaces?: () => void;
785
866
  resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
786
867
  buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
787
868
  callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
@@ -871,6 +952,7 @@ export function registerTelegramLifecycleRuntimeHooks({
871
952
  deferredQueueDispatchRuntime,
872
953
  modelContextAvailabilityRuntime,
873
954
  disconnectOnQuit,
955
+ shutdownGenerativeAppLiveSurfaces,
874
956
  resolveAutomaticThreadCleanupEnabled,
875
957
  buttonActionStore,
876
958
  callMultipart,
@@ -1194,6 +1276,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1194
1276
  },
1195
1277
  async onSessionShutdown(event, ctx) {
1196
1278
  if (!isSessionContextActive(ctx)) return;
1279
+ shutdownGenerativeAppLiveSurfaces?.();
1197
1280
  agentLifecycleHooks.clearRetainedAgentEnd();
1198
1281
  activityRuntime.onSessionShutdown();
1199
1282
  activityVerbosityRuntime?.reset();
@@ -43,6 +43,7 @@ import {
43
43
  type TelegramBusSocketPathSource,
44
44
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
45
45
  TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
46
+ TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
46
47
  } from "./bus.ts";
47
48
  import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
48
49
  import {
@@ -293,6 +294,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
293
294
  getNowMs?: () => number;
294
295
  getPid?: () => number;
295
296
  getProcessBirthId?: () => string;
297
+ getSessionId?: (ctx: TContext) => string | undefined;
296
298
  getSessionGeneration?: () => number;
297
299
  timeoutMs?: number;
298
300
  registrationTimeoutMs?: number;
@@ -374,6 +376,7 @@ export function createTelegramBusFollowerPromotionHandler<
374
376
  topicTargetStore: Threads.TelegramTopicTargetStore;
375
377
  instanceId: string;
376
378
  getActiveProfileName: () => string | undefined;
379
+ getSessionId?: (ctx: TContext) => string | undefined;
377
380
  startLeader: (
378
381
  ctx: TContext,
379
382
  election: TelegramBusFollowerElection,
@@ -400,6 +403,7 @@ export function createTelegramBusFollowerPromotionHandler<
400
403
  store: input.topicTargetStore,
401
404
  instanceId: input.instanceId,
402
405
  cwd: ctx.cwd,
406
+ sessionId: input.getSessionId?.(ctx),
403
407
  telegramProfile: input.getActiveProfileName(),
404
408
  target: binding.target,
405
409
  slot: binding.slot,
@@ -1771,6 +1775,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1771
1775
  ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
1772
1776
  : {}),
1773
1777
  cwd: ctx.cwd,
1778
+ sessionId: deps.getSessionId?.(ctx),
1774
1779
  pid: getPid(),
1775
1780
  processBirthId: deps.getProcessBirthId?.(),
1776
1781
  sessionGeneration: deps.getSessionGeneration?.(),
@@ -1910,9 +1915,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
1910
1915
  if (!socketPath || !generation || !deps.registrationState?.isRegistered()) {
1911
1916
  throw new Error("Telegram follower is not registered with the leader.");
1912
1917
  }
1913
- if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) ||
1914
- !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)) {
1915
- throw new Error("The Telegram peers do not support Thread display settings. Update or restart both instances.");
1918
+ const requiredCapability = mode === "directory-snake" || mode === "directory-title"
1919
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
1920
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
1921
+ if (!hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
1922
+ !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), requiredCapability)) {
1923
+ throw new Error("The Telegram peers do not support this Thread display setting. Update or restart both instances.");
1916
1924
  }
1917
1925
  const requestId = deps.createRequestId();
1918
1926
  const response = await sendTelegramBusLocalEnvelope({
package/lib/bus-leader.ts CHANGED
@@ -9,6 +9,7 @@ import * as Sync from "./sync.ts";
9
9
  import {
10
10
  createTelegramThreadDisplayReconciler,
11
11
  resolveTelegramInitialWorkspaceDisplayName,
12
+ resolveTelegramLiveWorkspaceBindingKeys,
12
13
  } from "./thread-display.ts";
13
14
  import type { TelegramThreadDisplayMode } from "./config.ts";
14
15
  import * as ThreadReconciler from "./thread-reconciler.ts";
@@ -40,6 +41,7 @@ import {
40
41
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
41
42
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
42
43
  TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
44
+ TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
43
45
  } from "./bus.ts";
44
46
  import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
45
47
  import type { TelegramQueueHandoffPayload } from "./queue.ts";
@@ -113,6 +115,7 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
113
115
  getAllowedUserId: () => number | undefined;
114
116
  instanceId: string;
115
117
  getCwd?: (ctx: TContext) => string | undefined;
118
+ getSessionId?: (ctx: TContext) => string | undefined;
116
119
  getTelegramProfile?: () => string | undefined;
117
120
  shouldForceFreshUnnamed?: () => boolean;
118
121
  getRequestedThreadName?: () => string | undefined;
@@ -234,6 +237,7 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
234
237
  getAllowedUserId: () => number | undefined;
235
238
  instanceId: string;
236
239
  getCwd?: (ctx: TContext) => string | undefined;
240
+ getSessionId?: (ctx: TContext) => string | undefined;
237
241
  getTelegramProfile?: () => string | undefined;
238
242
  shouldForceFreshUnnamed?: () => boolean;
239
243
  getRequestedThreadName?: () => string | undefined;
@@ -307,6 +311,13 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
307
311
  });
308
312
  },
309
313
  }).run;
314
+ const captureLiveBindingKeys = (
315
+ bindings: readonly Threads.TelegramWorkspaceThreadBinding[],
316
+ ): ReadonlySet<string> => resolveTelegramLiveWorkspaceBindingKeys(
317
+ bindings,
318
+ deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target,
319
+ deps.runtime.followerRegistry.list(),
320
+ );
310
321
  const provisionerPorts = {
311
322
  getAllowedUserId: deps.getAllowedUserId,
312
323
  topicTargetStore: deps.topicTargetStore,
@@ -325,6 +336,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
325
336
  bindings: deps.topicTargetStore.listWorkspaceBindings(),
326
337
  binding,
327
338
  mode: deps.getThreadDisplayMode!(),
339
+ liveBindingKeys: captureLiveBindingKeys(
340
+ deps.topicTargetStore.listWorkspaceBindings(),
341
+ ),
328
342
  });
329
343
  },
330
344
  }
@@ -337,6 +351,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
337
351
  getMode: deps.getThreadDisplayMode,
338
352
  getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
339
353
  getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
354
+ captureLiveBindingKeys,
340
355
  captureBindingAuthority(binding) {
341
356
  const matches = (target: TelegramTarget | undefined) =>
342
357
  target?.chatId === binding.target.chatId && target?.threadId === binding.target.threadId;
@@ -390,8 +405,11 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
390
405
  throw new Error("Telegram Thread display setting requires current leader authority.");
391
406
  }
392
407
  const assertDisplayPeers = () => {
408
+ const requiredCapability = mode === "directory-snake" || mode === "directory-title"
409
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
410
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
393
411
  if (mode !== "names" && deps.runtime.followerRegistry.list().some((follower) =>
394
- !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE),
412
+ !hasTelegramBusCapability(follower.protocol, requiredCapability),
395
413
  )) throw new Error("Update or restart all connected followers before changing Thread display mode.");
396
414
  };
397
415
  assertDisplayPeers();
@@ -413,6 +431,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
413
431
  ...provisionerPorts,
414
432
  instanceId: deps.instanceId,
415
433
  getCwd: deps.getCwd,
434
+ getSessionId: deps.getSessionId,
416
435
  getTelegramProfile: deps.getTelegramProfile,
417
436
  shouldForceFreshUnnamed: deps.shouldForceFreshUnnamed,
418
437
  getRequestedThreadName: deps.getRequestedThreadName,
@@ -594,14 +613,17 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
594
613
  );
595
614
  return binding?.displayTitle ?? binding?.threadName;
596
615
  },
597
- onFollowerDisconnected: (follower) => runWorkspaceOperation(
598
- {
599
- operationId: createTelegramWorkspaceAdmissionOperationId(),
600
- operationKind: "workspace.disconnect-follower",
601
- scopes: [{ kind: "profile" }],
602
- },
603
- () => disconnectFollower(follower),
604
- ),
616
+ onFollowerDisconnected: async (follower) => {
617
+ await runWorkspaceOperation(
618
+ {
619
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
620
+ operationKind: "workspace.disconnect-follower",
621
+ scopes: [{ kind: "profile" }],
622
+ },
623
+ () => disconnectFollower(follower),
624
+ );
625
+ scheduleDisplay();
626
+ },
605
627
  async renameFollowerThread(follower, threadName) {
606
628
  return runWorkspaceOperation({
607
629
  operationId: createTelegramWorkspaceAdmissionOperationId(),
@@ -689,14 +711,17 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
689
711
  });
690
712
  },
691
713
  ),
692
- onFollowerConfirmedDead: (follower) => runWorkspaceOperation(
693
- {
694
- operationId: createTelegramWorkspaceAdmissionOperationId(),
695
- operationKind: "workspace.cleanup-dead-follower",
696
- scopes: [{ kind: "profile" }],
697
- },
698
- () => cleanupConfirmedDeadFollower(follower),
699
- ),
714
+ onFollowerConfirmedDead: async (follower) => {
715
+ await runWorkspaceOperation(
716
+ {
717
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
718
+ operationKind: "workspace.cleanup-dead-follower",
719
+ scopes: [{ kind: "profile" }],
720
+ },
721
+ () => cleanupConfirmedDeadFollower(follower),
722
+ );
723
+ scheduleDisplay();
724
+ },
700
725
  provisionFollowerTarget: (registration, options) => runWorkspaceOperation(
701
726
  {
702
727
  operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
@@ -908,6 +933,7 @@ export function createTelegramBusFollowerTargetProvisioner(
908
933
  {
909
934
  existingBindingOnly:
910
935
  options?.existingWorkspaceBindingOnly === true,
936
+ sessionId: registration.sessionId,
911
937
  onCapacityUnavailable() {
912
938
  capacityUnavailable = true;
913
939
  },
@@ -925,6 +951,7 @@ export function createTelegramBusFollowerTargetProvisioner(
925
951
  ? deps.topicTargetStore.getWorkspaceBinding(
926
952
  workspaceIdentity.cwd,
927
953
  workspaceIdentity.instanceSlot,
954
+ workspaceIdentity.sessionId,
928
955
  )
929
956
  : undefined;
930
957
  const provision = Threads.createTelegramTopicTargetProvisioner({
@@ -1567,6 +1594,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
1567
1594
  getAllowedUserId: deps.getAllowedUserId,
1568
1595
  instanceId: deps.instanceId,
1569
1596
  cwd: normalizedCwd,
1597
+ sessionId: deps.getSessionId?.(ctx),
1570
1598
  telegramProfile: deps.getTelegramProfile?.(),
1571
1599
  forceFreshUnnamed: deps.shouldForceFreshUnnamed?.(),
1572
1600
  requestedThreadName: deps.getRequestedThreadName?.(),
@@ -2101,6 +2129,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2101
2129
  );
2102
2130
  if (
2103
2131
  !compatibility.compatible || !displayCompatible() ||
2132
+ (envelope.registration.cwd !== undefined &&
2133
+ envelope.registration.sessionId === undefined) ||
2104
2134
  (restoringWorkspace && !supportsWorkspaceAutoConnect)
2105
2135
  ) {
2106
2136
  return {
@@ -2110,7 +2140,11 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2110
2140
  protocol: deps.protocolIdentity,
2111
2141
  error: { code: "incompatible-protocol" as const },
2112
2142
  message: `Incompatible Telegram bus protocol: ${
2113
- compatibility.reason ?? "missing-capability"
2143
+ compatibility.reason ??
2144
+ (envelope.registration.cwd !== undefined &&
2145
+ envelope.registration.sessionId === undefined
2146
+ ? "missing-session-identity"
2147
+ : "missing-capability")
2114
2148
  }.`,
2115
2149
  };
2116
2150
  }
@@ -2231,9 +2265,12 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2231
2265
  const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
2232
2266
  deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration === envelope.registrationGeneration;
2233
2267
  const follower = deps.followerRegistry.get(envelope.instanceId);
2268
+ const requiredCapability = envelope.mode === "directory-snake" || envelope.mode === "directory-title"
2269
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
2270
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
2234
2271
  if (!current() || !follower?.registrationGeneration || !deps.applyThreadDisplayMode ||
2235
- !hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) ||
2236
- !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)) {
2272
+ !hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
2273
+ !hasTelegramBusCapability(follower.protocol, requiredCapability)) {
2237
2274
  return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
2238
2275
  message: "Thread display settings require current registration and compatible leader authority." };
2239
2276
  }
package/lib/bus.ts CHANGED
@@ -220,7 +220,7 @@ export function createTelegramBusAuthSecret(): string {
220
220
  return randomBytes(32).toString("base64url");
221
221
  }
222
222
 
223
- export const TELEGRAM_BUS_PROTOCOL_VERSION = 1 as const;
223
+ export const TELEGRAM_BUS_PROTOCOL_VERSION = 2 as const;
224
224
  export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION =
225
225
  "durable-follower-admission-v1" as const;
226
226
  export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
@@ -230,6 +230,8 @@ export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
230
230
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
231
231
  "workspace-thread-rename-v1" as const;
232
232
  export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE = "thread-display-mode-v1" as const;
233
+ export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT =
234
+ "directory-display-format-v1" as const;
233
235
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT =
234
236
  "workspace-follower-auto-connect-v1" as const;
235
237
 
@@ -369,6 +371,7 @@ export interface TelegramBusInstanceRegistration {
369
371
  threadName?: string;
370
372
  slot?: string;
371
373
  cwd?: string;
374
+ sessionId?: string;
372
375
  pid?: number;
373
376
  target?: TelegramTarget;
374
377
  busSocketPath?: string;
@@ -1026,7 +1029,8 @@ export function parseTelegramBusEnvelope(
1026
1029
  case "follower.setThreadDisplayMode":
1027
1030
  if (typeof value.instanceId === "string" &&
1028
1031
  typeof value.registrationGeneration === "string" &&
1029
- (value.mode === "letters" || value.mode === "names" || value.mode === "directories")) {
1032
+ (value.mode === "letters" || value.mode === "names" || value.mode === "directories" ||
1033
+ value.mode === "directory-snake" || value.mode === "directory-title")) {
1030
1034
  envelope = { kind, requestId, instanceId: value.instanceId,
1031
1035
  registrationGeneration: value.registrationGeneration, mode: value.mode };
1032
1036
  }
@@ -2991,6 +2995,12 @@ function parseRegistration(
2991
2995
  registration.slot = value.slot;
2992
2996
  }
2993
2997
  if (typeof value.cwd === "string") registration.cwd = value.cwd;
2998
+ if (value.sessionId !== undefined) {
2999
+ if (typeof value.sessionId !== "string" || !value.sessionId ||
3000
+ value.sessionId !== value.sessionId.trim() ||
3001
+ Buffer.byteLength(value.sessionId, "utf8") > 256) return undefined;
3002
+ registration.sessionId = value.sessionId;
3003
+ }
2994
3004
  if (typeof value.pid === "number") registration.pid = value.pid;
2995
3005
  if (typeof value.busSocketPath === "string") {
2996
3006
  registration.busSocketPath = value.busSocketPath;
package/lib/commands.ts CHANGED
@@ -1205,7 +1205,7 @@ export const TELEGRAM_APP_MENU_INTRO_HTML = [
1205
1205
  "<b>Pi Telegram</b>",
1206
1206
  "",
1207
1207
  `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
1208
- `${formatTelegramCommandEmojiPrefix("name")}/name Name — Rename this thread`,
1208
+ `${formatTelegramCommandEmojiPrefix("name")}/name — Rename this thread`,
1209
1209
  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
1210
1210
  `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
1211
1211
  `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
@@ -1246,7 +1246,7 @@ function buildTelegramAppMenuIntroHtml(): string {
1246
1246
  "<b>Pi Telegram</b>",
1247
1247
  "",
1248
1248
  `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
1249
- `${formatTelegramCommandEmojiPrefix("name")}/name Name — Rename this thread`,
1249
+ `${formatTelegramCommandEmojiPrefix("name")}/name — Rename this thread`,
1250
1250
  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
1251
1251
  ...extensionLines,
1252
1252
  `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
package/lib/config.ts CHANGED
@@ -136,16 +136,23 @@ export interface ResolvedTelegramTimeConfig {
136
136
  timezone: string;
137
137
  }
138
138
 
139
- export type TelegramThreadDisplayMode = "letters" | "names" | "directories";
139
+ export type TelegramThreadDisplayMode =
140
+ | "letters"
141
+ | "names"
142
+ | "directories"
143
+ | "directory-snake"
144
+ | "directory-title";
145
+
146
+ const TELEGRAM_THREAD_DISPLAY_MODES: readonly TelegramThreadDisplayMode[] = [
147
+ "letters", "names", "directories", "directory-snake", "directory-title",
148
+ ];
140
149
 
141
150
  export function resolveTelegramThreadDisplayMode(
142
151
  config: Pick<TelegramConfig, "threadDisplayMode">,
143
152
  ): TelegramThreadDisplayMode {
144
- return config.threadDisplayMode === "names"
145
- ? "names"
146
- : config.threadDisplayMode === "directories"
147
- ? "directories"
148
- : "letters";
153
+ return TELEGRAM_THREAD_DISPLAY_MODES.includes(config.threadDisplayMode as TelegramThreadDisplayMode)
154
+ ? config.threadDisplayMode as TelegramThreadDisplayMode
155
+ : "letters";
149
156
  }
150
157
 
151
158
  export async function setTelegramThreadDisplayMode(
@@ -153,7 +160,7 @@ export async function setTelegramThreadDisplayMode(
153
160
  mode: TelegramThreadDisplayMode,
154
161
  isCurrent: () => boolean,
155
162
  ): Promise<void> {
156
- if (!["letters", "names", "directories"].includes(mode)) {
163
+ if (!TELEGRAM_THREAD_DISPLAY_MODES.includes(mode)) {
157
164
  throw new Error("Invalid Telegram Thread display mode.");
158
165
  }
159
166
  const profile = store.getActiveProfileName();
package/lib/delivery.ts CHANGED
@@ -18,7 +18,10 @@ import {
18
18
  type TelegramTarget,
19
19
  } from "./target.ts";
20
20
  import {
21
+ getTelegramApiRetryAfterMs,
22
+ isRetryableTelegramApiError,
21
23
  isTelegramApiCommitUnknownError,
24
+ isTelegramMessageUnavailableError,
22
25
  type TelegramBridgeApiRuntime,
23
26
  } from "./telegram-api.ts";
24
27
 
@@ -60,6 +63,9 @@ export type TelegramDeliveryFailureReason =
60
63
  | "stale-handle"
61
64
  | "invalid-view"
62
65
  | "commit-unknown"
66
+ | "message-unavailable"
67
+ | "rate-limited"
68
+ | "transport-retryable"
63
69
  | "transport-failed";
64
70
 
65
71
  export type TelegramDeliveryResult<T> =
@@ -68,6 +74,8 @@ export type TelegramDeliveryResult<T> =
68
74
  ok: false;
69
75
  reason: TelegramDeliveryFailureReason;
70
76
  message: string;
77
+ /** Exact Telegram flood-control delay when supplied by the API. */
78
+ retryAfterMs?: number;
71
79
  /** Successfully materialized state that callers may edit or delete to recover. */
72
80
  partial?: T;
73
81
  };
@@ -246,10 +254,26 @@ function failure<T>(
246
254
  reason: TelegramDeliveryFailureReason,
247
255
  message: string,
248
256
  partial?: T,
257
+ retryAfterMs?: number,
249
258
  ): TelegramDeliveryResult<T> {
259
+ const retry = retryAfterMs === undefined ? {} : { retryAfterMs };
250
260
  return partial === undefined
251
- ? { ok: false, reason, message }
252
- : { ok: false, reason, message, partial };
261
+ ? { ok: false, reason, message, ...retry }
262
+ : { ok: false, reason, message, partial, ...retry };
263
+ }
264
+
265
+ export function classifyTelegramDeliveryTransportError(error: unknown): {
266
+ reason: Extract<TelegramDeliveryFailureReason,
267
+ "commit-unknown" | "message-unavailable" | "rate-limited" |
268
+ "transport-retryable" | "transport-failed">;
269
+ retryAfterMs?: number;
270
+ } {
271
+ if (isTelegramApiCommitUnknownError(error)) return { reason: "commit-unknown" };
272
+ if (isTelegramMessageUnavailableError(error)) return { reason: "message-unavailable" };
273
+ const retryAfterMs = getTelegramApiRetryAfterMs(error);
274
+ if (retryAfterMs !== undefined) return { reason: "rate-limited", retryAfterMs };
275
+ if (isRetryableTelegramApiError(error)) return { reason: "transport-retryable" };
276
+ return { reason: "transport-failed" };
253
277
  }
254
278
 
255
279
  /** @internal */
@@ -479,14 +503,14 @@ export function createTelegramDeliveryRuntime(
479
503
  if (error instanceof TelegramDeliveryTransportGenerationError) {
480
504
  return inactive();
481
505
  }
506
+ const classified = classifyTelegramDeliveryTransportError(error);
482
507
  return failure(
483
- isTelegramApiCommitUnknownError(error)
484
- ? "commit-unknown"
485
- : "transport-failed",
486
- isTelegramApiCommitUnknownError(error)
508
+ classified.reason,
509
+ classified.reason === "commit-unknown"
487
510
  ? `Telegram delivery ${operation} may have committed before transport failed.`
488
511
  : `Telegram delivery ${operation} failed.`,
489
512
  partial,
513
+ classified.retryAfterMs,
490
514
  );
491
515
  };
492
516
  const createHandle = (
@@ -837,6 +861,14 @@ export function clearTelegramDeliveryRuntime(): void {
837
861
  registry.runtime = undefined;
838
862
  }
839
863
 
864
+ /** @internal */
865
+ export function isTelegramDeliveryHandleCurrent(
866
+ handle: TelegramDeliveryHandle,
867
+ ): boolean {
868
+ const runtime = getTelegramDeliveryRuntimeRegistry().runtime;
869
+ return runtime !== undefined && runtime.generation === handle.generation;
870
+ }
871
+
840
872
  export async function sendTelegramView(
841
873
  view: TelegramDeliveryView,
842
874
  options: SendTelegramViewOptions,