@llblab/pi-telegram 0.55.1 → 0.56.1

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 (60) hide show
  1. package/AGENTS.md +5 -4
  2. package/BACKLOG.md +6 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +28 -17
  5. package/dist/lib/bindings.d.ts +8 -4
  6. package/dist/lib/bindings.js +41 -1
  7. package/dist/lib/bus-follower.d.ts +1 -0
  8. package/dist/lib/bus-follower.js +10 -5
  9. package/dist/lib/bus-leader.d.ts +2 -0
  10. package/dist/lib/bus-leader.js +34 -20
  11. package/dist/lib/bus.d.ts +8 -0
  12. package/dist/lib/bus.js +21 -7
  13. package/dist/lib/commands.d.ts +1 -1
  14. package/dist/lib/commands.js +33 -28
  15. package/dist/lib/extension.js +18 -3
  16. package/dist/lib/lifecycle.d.ts +2 -1
  17. package/dist/lib/lifecycle.js +28 -20
  18. package/dist/lib/locks.d.ts +6 -2
  19. package/dist/lib/locks.js +3 -3
  20. package/dist/lib/menu-queue.d.ts +1 -0
  21. package/dist/lib/menu-queue.js +102 -99
  22. package/dist/lib/pi.d.ts +3 -2
  23. package/dist/lib/pi.js +3 -0
  24. package/dist/lib/polling.d.ts +6 -2
  25. package/dist/lib/polling.js +5 -3
  26. package/dist/lib/queue.d.ts +57 -5
  27. package/dist/lib/queue.js +313 -25
  28. package/dist/lib/routing.d.ts +4 -0
  29. package/dist/lib/routing.js +62 -1
  30. package/dist/lib/status.d.ts +1 -2
  31. package/dist/lib/status.js +6 -7
  32. package/dist/lib/sync.js +11 -1
  33. package/dist/lib/threads.d.ts +11 -0
  34. package/dist/lib/threads.js +57 -6
  35. package/dist/lib/updates.d.ts +12 -7
  36. package/dist/lib/updates.js +44 -9
  37. package/dist/package.json +1 -1
  38. package/docs/README.md +1 -1
  39. package/docs/architecture.md +24 -13
  40. package/docs/callback-namespaces.md +4 -0
  41. package/docs/multi-instance-bus.md +6 -4
  42. package/docs/ui-style.md +14 -10
  43. package/lib/bindings.ts +52 -3
  44. package/lib/bus-follower.ts +13 -5
  45. package/lib/bus-leader.ts +37 -23
  46. package/lib/bus.ts +23 -7
  47. package/lib/commands.ts +52 -47
  48. package/lib/extension.ts +18 -2
  49. package/lib/lifecycle.ts +34 -28
  50. package/lib/locks.ts +5 -2
  51. package/lib/menu-queue.ts +116 -162
  52. package/lib/pi.ts +8 -0
  53. package/lib/polling.ts +7 -2
  54. package/lib/queue.ts +328 -33
  55. package/lib/routing.ts +51 -1
  56. package/lib/status.ts +7 -7
  57. package/lib/sync.ts +11 -0
  58. package/lib/threads.ts +57 -5
  59. package/lib/updates.ts +44 -15
  60. package/package.json +1 -1
package/dist/lib/bus.js CHANGED
@@ -50,6 +50,9 @@ export function createTelegramBusAuthSecret() {
50
50
  const TELEGRAM_BUS_PROTOCOL_VERSION = 3;
51
51
  export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION = "durable-follower-admission-v1";
52
52
  export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF = "queue-handoff-v1";
53
+ /** Unspent Steer lane codec; independent from baseline receipt handoff support. */
54
+ export const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER = "queue-steer-v1";
55
+ export const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY = "explicit-connect-recovery-v1";
53
56
  export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE = "input-custody-reference-v1";
54
57
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME = "workspace-thread-rename-v1";
55
58
  export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE = "thread-display-mode-v1";
@@ -77,6 +80,13 @@ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
77
80
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
78
81
  TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
79
82
  ];
83
+ /** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
84
+ export const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES = "held-menu-aliases-v1";
85
+ export function getTelegramBusHeldCommandCapabilities(name) {
86
+ return name === "start" || name === "help"
87
+ ? [...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES]
88
+ : TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES;
89
+ }
80
90
  function parseSelectedCommandInput(value) {
81
91
  if (!isRecord(value) ||
82
92
  !hasOnlyWireKeys(value, ["name", "target"]) ||
@@ -173,6 +183,10 @@ export function createTelegramCurrentBusProtocolIdentity(capabilities = []) {
173
183
  export function hasTelegramBusCapability(identity, capability) {
174
184
  return identity?.capabilities.includes(capability) ?? false;
175
185
  }
186
+ export function hasTelegramBusSteerQueueHandoffCapability(identity) {
187
+ return hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) &&
188
+ hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_STEER);
189
+ }
176
190
  export function getTelegramInputCustodyPeerReadiness(followers) {
177
191
  return followers.map((follower) => {
178
192
  if (!follower.registrationGeneration || !follower.protocol)
@@ -1310,12 +1324,7 @@ export function createTelegramBusLiveRebindController(deps) {
1310
1324
  : []),
1311
1325
  ...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
1312
1326
  ...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
1313
- ...(held
1314
- ? [
1315
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
1316
- TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
1317
- ]
1318
- : []),
1327
+ ...(held ? getTelegramBusHeldCommandCapabilities(held.name) : []),
1319
1328
  ];
1320
1329
  const follower = deps.getFollower(expected.instanceId), secret = deps.getAuthSecret();
1321
1330
  if (!isCurrent() ||
@@ -2138,8 +2147,13 @@ async function handleTelegramBusSocketLine(line, socket, handleEnvelope, recordT
2138
2147
  }
2139
2148
  }
2140
2149
  function parseRegisterEnvelope(value, requestId, kind) {
2150
+ if (value.recoverConnection !== undefined &&
2151
+ (kind !== "follower.register" || value.recoverConnection !== true))
2152
+ return undefined;
2141
2153
  const registration = parseRegistration(value.registration);
2142
- return registration ? { kind, requestId, registration } : undefined;
2154
+ return registration ? { kind, requestId, registration,
2155
+ ...(kind === "follower.register" && value.recoverConnection === true ? { recoverConnection: true } : {}),
2156
+ } : undefined;
2143
2157
  }
2144
2158
  function parseFollowerPresenceEnvelope(value, requestId, kind) {
2145
2159
  return typeof value.instanceId === "string" &&
@@ -13,7 +13,7 @@ import { type TelegramBridgeStatusLineOptions } from "./status.ts";
13
13
  import { type TelegramApiCallOptions } from "./telegram-api.ts";
14
14
  import type { TelegramSessionReplacementIntent } from "./threads.ts";
15
15
  import { type TelegramDeferredSourceEvidence, type TelegramLiveSourceCompletionReadiness, type TelegramQueueAdmissionReceiptLike } from "./updates.ts";
16
- export type TelegramHeldCommandName = "status" | "abort" | "stop" | "next" | "continue";
16
+ export type TelegramHeldCommandName = "status" | "start" | "help" | "abort" | "stop" | "next" | "continue";
17
17
  /** Commands-owned no-fold admission over the ordinary recipient queue; source reporting stays separate from removal. */
18
18
  export interface TelegramHeldTurnAdmission {
19
19
  assertCurrent(): void;
@@ -1446,35 +1446,40 @@ export function createTelegramCommandHandler(deps) {
1446
1446
  },
1447
1447
  };
1448
1448
  };
1449
- const heldPlans = {
1450
- status: {
1451
- completion: "before-effect",
1452
- effect: "showStatus",
1453
- prepare(ctx, effects) {
1454
- const scopedStatus = effects?.showStatus;
1455
- const captured = {
1456
- ...deps,
1457
- showStatus: scopedStatus
1458
- ? (message, context, options) => {
1459
- const address = getTelegramCommandMessageTarget(message);
1460
- return scopedStatus.call(effects, address.chatId, address.replyToMessageId, context, address.threadId, options);
1461
- }
1462
- : deps.showStatus,
1463
- };
1464
- return {
1465
- captured,
1466
- portsCurrent: () => !effects || effects.showStatus === scopedStatus,
1467
- async run({ message, command, assertRecipient }) {
1468
- const recipientOptions = { assertAuthority: assertRecipient };
1469
- return handleTelegramCommandRuntime(command.name, message, ctx, {
1470
- ...captured,
1471
- assertExecutionCurrent: assertRecipient,
1472
- showStatus: (value, context) => captured.showStatus(value, context, recipientOptions),
1473
- }, command.args);
1474
- },
1475
- };
1476
- },
1449
+ // Held followers are already paired and bound. Start/help open the same restricted menu as status;
1450
+ // they cannot pair an operator, bootstrap another Thread or invoke unrestricted Bot API effects.
1451
+ const heldMenuPlan = {
1452
+ completion: "before-effect",
1453
+ effect: "showStatus",
1454
+ prepare(ctx, effects) {
1455
+ const scopedStatus = effects?.showStatus;
1456
+ const captured = {
1457
+ ...deps,
1458
+ showStatus: scopedStatus
1459
+ ? (message, context, options) => {
1460
+ const address = getTelegramCommandMessageTarget(message);
1461
+ return scopedStatus.call(effects, address.chatId, address.replyToMessageId, context, address.threadId, options);
1462
+ }
1463
+ : deps.showStatus,
1464
+ };
1465
+ return {
1466
+ captured,
1467
+ portsCurrent: () => !effects || effects.showStatus === scopedStatus,
1468
+ async run({ message, command, assertRecipient }) {
1469
+ const recipientOptions = { assertAuthority: assertRecipient };
1470
+ return handleTelegramCommandRuntime("status", message, ctx, {
1471
+ ...captured,
1472
+ assertExecutionCurrent: assertRecipient,
1473
+ showStatus: (value, context) => captured.showStatus(value, context, recipientOptions),
1474
+ }, command.args);
1475
+ },
1476
+ };
1477
1477
  },
1478
+ };
1479
+ const heldPlans = {
1480
+ status: heldMenuPlan,
1481
+ start: heldMenuPlan,
1482
+ help: heldMenuPlan,
1478
1483
  abort: {
1479
1484
  completion: "after-semantics",
1480
1485
  effect: "sendTextReply",
@@ -59,6 +59,9 @@ import * as WorkspaceRetirement from "./workspace-retirement.js";
59
59
  const telegramBusProtocolIdentity = Bus.createTelegramCurrentBusProtocolIdentity([
60
60
  Bus.TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
61
61
  Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
62
+ Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_STEER,
63
+ Bus.TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
64
+ Bus.TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES,
62
65
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
63
66
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
64
67
  Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
@@ -557,7 +560,6 @@ export default function (pi) {
557
560
  },
558
561
  getQueuedItems: telegramQueueStore.getQueuedItems,
559
562
  getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
560
- formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
561
563
  getRecentRuntimeEvents: runtimeEvents.getEvents,
562
564
  getRuntimeLockState: lockRuntime.getStatusLabel,
563
565
  ...threadStatusProjectionRuntime,
@@ -796,7 +798,7 @@ export default function (pi) {
796
798
  editMessageText: editTelegramMessageText,
797
799
  },
798
800
  });
799
- const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, cancelNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
801
+ const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, cancelNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, onTurnEnd: onSteerTurnEnd, } = Bindings.createTelegramQueueBindingRuntime({
800
802
  store: telegramQueueStore,
801
803
  queue,
802
804
  lifecycle,
@@ -807,6 +809,8 @@ export default function (pi) {
807
809
  promptDispatch: promptDispatchRuntime,
808
810
  isIdle,
809
811
  hasPendingMessages,
812
+ isContextCurrent: telegramSessionContextStore.isCurrent,
813
+ isContextAborted: Pi.isExtensionContextAbortRequested,
810
814
  updateStatus,
811
815
  sendTextReply,
812
816
  sendUserMessage,
@@ -881,6 +885,7 @@ export default function (pi) {
881
885
  });
882
886
  // --- Queue And Settings Menus ---
883
887
  const queueMenuRuntime = MenuQueue.createTelegramQueueMenuRuntime({
888
+ hasPendingDispatch: lifecycle.hasDispatchPending,
884
889
  telegramQueueStore,
885
890
  queueMutationRuntime,
886
891
  sendInteractiveMessage,
@@ -973,7 +978,7 @@ export default function (pi) {
973
978
  isSelectedCommandAvailable(follower, name) {
974
979
  return (Bus.hasTelegramBusSharedCapabilities(telegramBusProtocolIdentity, follower.protocol, [
975
980
  ...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
976
- ...Bus.TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
981
+ ...Bus.getTelegramBusHeldCommandCapabilities(name),
977
982
  ]) &&
978
983
  typeof selectedMenuDelivery === "function" &&
979
984
  typeof inboundRouteRuntime.prepareHeldCommand === "function" &&
@@ -1222,6 +1227,7 @@ export default function (pi) {
1222
1227
  threadStore,
1223
1228
  runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
1224
1229
  liveRebindCleanupSchedule: Routing.TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE,
1230
+ liveRebindCommandCompletionTimeoutMs: 8_000,
1225
1231
  updateStatus,
1226
1232
  isContextActive: telegramSessionContextStore.isCurrent,
1227
1233
  dispatchNextQueuedTelegramTurn,
@@ -1858,6 +1864,14 @@ export default function (pi) {
1858
1864
  recordRuntimeEvent,
1859
1865
  });
1860
1866
  queueHandoffReconciliationBinding.set(Updates.createTelegramQueueHandoffReconciliationRuntimeAssembly({
1867
+ hasPendingDispatch: lifecycle.hasDispatchPending,
1868
+ canHandoffSteerWithRecipient(recipient) {
1869
+ const live = telegramBusFollowerRegistry.get(recipient.instanceId);
1870
+ return live?.registrationGeneration === recipient.registrationGeneration &&
1871
+ [telegramBusProtocolIdentity, live?.protocol,
1872
+ ...(telegramBusFollowerRegistrationState.isRegistered() ? [telegramBusFollowerRegistrationState.getLeaderProtocol()] : [])]
1873
+ .every(Bus.hasTelegramBusSteerQueueHandoffCapability);
1874
+ },
1861
1875
  ownsDirect: lockRuntime.owns,
1862
1876
  isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
1863
1877
  isBusEnabled: isTelegramBusRuntimeEnabled,
@@ -2461,6 +2475,7 @@ export default function (pi) {
2461
2475
  sendMarkdownReply,
2462
2476
  sendTextReply,
2463
2477
  dispatchNextQueuedTelegramTurn,
2478
+ onSteerTurnEnd,
2464
2479
  onPromptHandedOff(turn, ctx) {
2465
2480
  updateAdmissionRuntimeBinding
2466
2481
  .getSettlement()
@@ -5,7 +5,7 @@
5
5
  * Transport authority, durable bindings, and queue custody remain with their owners.
6
6
  */
7
7
  import * as BusFollower from "./bus-follower.ts";
8
- import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
8
+ import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
9
9
  import * as Machine from "./machine.ts";
10
10
  import * as Queue from "./queue.ts";
11
11
  export declare function createAgentStartDedupHook(inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>, resetReplyDedup: () => void, schedulePublication?: (task: () => Promise<void>) => void): (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>;
@@ -30,6 +30,7 @@ export interface TelegramLifecycleRegistrationDeps {
30
30
  onModelSelect: (event: {
31
31
  model: TelegramLifecycleModel;
32
32
  }, ctx: ExtensionContext) => Promise<void> | void;
33
+ onTurnEnd?: (event: TurnEndEvent, ctx: ExtensionContext) => TurnEndEventResult | undefined;
33
34
  onAgentStart: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>;
34
35
  onToolExecutionStart: (event: ToolExecutionStartEvent, ctx: ExtensionContext) => Promise<void> | void;
35
36
  onToolExecutionUpdate?: (event: ToolExecutionUpdateEvent, ctx: ExtensionContext) => Promise<void> | void;
@@ -265,36 +265,47 @@ export function createTelegramBridgeSessionLifecycleDeps(ports) {
265
265
  };
266
266
  }
267
267
  export function createTelegramBridgeSessionLifecycleAssembly(deps) {
268
- const isSessionActive = deps.contextStore.isCurrent;
269
268
  const suspendForReplacement = BusFollower.createTelegramBusFollowerSessionReplacementSuspender({
270
269
  registrationState: deps.follower.registrationState,
271
270
  instanceId: deps.follower.instanceId,
272
271
  suspendPolling: deps.follower.suspendPolling,
273
272
  recordRuntimeEvent: deps.follower.recordRuntimeEvent,
274
273
  });
275
- let preserveTarget = true;
276
- const queueLifecycle = Queue.createTelegramSessionLifecycleRuntime({
274
+ // A reused context can be current for a successor; inner awaits need this invocation's generation too.
275
+ const queueLifecycle = (ctx, generation, preserveTarget = true) => Queue.createTelegramSessionLifecycleRuntime({
277
276
  ...deps.queue,
278
- isSessionActive,
277
+ isSessionActive: () => deps.contextStore.isCurrent(ctx, generation),
279
278
  stopPolling: () => suspendForReplacement(preserveTarget),
280
279
  clearPendingMediaGroups: deps.services.suspendGroupedInput,
281
280
  });
282
281
  const servicesLifecycle = {
283
282
  async onSessionStart(event, ctx) {
283
+ const generation = deps.contextStore.getGeneration();
284
+ const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
284
285
  const resumeConnection = deps.services.connection?.prepare(event, ctx);
285
- await queueLifecycle.onSessionStart(event, ctx);
286
- if (!isSessionActive(ctx))
286
+ await queueLifecycle(ctx, generation).onSessionStart(event, ctx);
287
+ if (!isCurrent())
287
288
  return;
288
289
  deps.services.resumeGroupedInput(ctx);
289
290
  await deps.services.delivery.onSessionStart();
290
- if (!isSessionActive(ctx))
291
+ if (!isCurrent())
291
292
  return;
292
293
  if (resumeConnection)
293
294
  resumeConnection();
294
295
  else
295
296
  await deps.services.polling.onSessionStart(event, ctx);
297
+ if (!isCurrent())
298
+ return;
296
299
  deps.services.capabilityMonitor.start(ctx);
297
300
  deps.services.queueWatchdog.start(ctx);
301
+ await BusFollower.createTelegramBusFollowerSessionRefreshHook({
302
+ registrationState: deps.follower.registrationState,
303
+ registrationRuntime: deps.follower.registrationRuntime,
304
+ getLeaderState: deps.follower.getLeaderState,
305
+ isSessionActive: isCurrent,
306
+ updateStatus: deps.follower.updateStatus,
307
+ recordRuntimeEvent: deps.follower.recordRuntimeEvent,
308
+ })(event, ctx);
298
309
  },
299
310
  async onSessionShutdown(event, ctx) {
300
311
  const generation = deps.contextStore.getGeneration();
@@ -302,7 +313,7 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
302
313
  if (!isCurrent())
303
314
  return;
304
315
  deps.services.connection?.onSessionShutdown(event, ctx);
305
- preserveTarget = event.reason !== "resume";
316
+ const preserveTarget = event.reason !== "resume";
306
317
  let preserveThread;
307
318
  if (event.reason === "quit") {
308
319
  try {
@@ -321,7 +332,7 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
321
332
  return;
322
333
  deps.services.queueWatchdog.stop();
323
334
  deps.services.capabilityMonitor.stop();
324
- await queueLifecycle.onSessionShutdown(event, ctx);
335
+ await queueLifecycle(ctx, generation, preserveTarget).onSessionShutdown(event, ctx);
325
336
  if (!isCurrent())
326
337
  return;
327
338
  await deps.services.inboundWorker.onSessionShutdown();
@@ -337,17 +348,7 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
337
348
  }
338
349
  },
339
350
  };
340
- const followerLifecycle = appendTelegramLifecycleHooks(servicesLifecycle, {
341
- onSessionStart: BusFollower.createTelegramBusFollowerSessionRefreshHook({
342
- registrationState: deps.follower.registrationState,
343
- registrationRuntime: deps.follower.registrationRuntime,
344
- getLeaderState: deps.follower.getLeaderState,
345
- isSessionActive,
346
- updateStatus: deps.follower.updateStatus,
347
- recordRuntimeEvent: deps.follower.recordRuntimeEvent,
348
- }),
349
- }, isSessionActive);
350
- return createTelegramSessionGenerationFence(deps.contextStore, followerLifecycle);
351
+ return createTelegramSessionGenerationFence(deps.contextStore, servicesLifecycle);
351
352
  }
352
353
  function unrefTelegramLifecycleTimer(timer) {
353
354
  if (!timer || typeof timer !== "object")
@@ -605,6 +606,13 @@ export function registerTelegramLifecycleHooks(pi, deps) {
605
606
  return;
606
607
  await deps.onAgentStart(event, ctx);
607
608
  });
609
+ if (deps.onTurnEnd) {
610
+ pi.on("turn_end", (event, ctx) => {
611
+ if (!isActive(ctx))
612
+ return;
613
+ return deps.onTurnEnd?.(event, ctx);
614
+ });
615
+ }
608
616
  pi.on("tool_execution_start", async (event, ctx) => {
609
617
  if (!isActive(ctx))
610
618
  return;
@@ -259,7 +259,9 @@ export interface TelegramLockedPollingRuntime<TContext extends TelegramLockConte
259
259
  captureTransportAuthority: (ctx: TContext) => (() => boolean) | undefined;
260
260
  onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
261
261
  onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
262
- registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
262
+ registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry, options?: {
263
+ recoverConnection?: boolean;
264
+ }) => boolean | undefined | Promise<boolean | undefined>;
263
265
  restoreFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
264
266
  stopFollowerRegistration?: () => void;
265
267
  }
@@ -274,7 +276,9 @@ interface TelegramLockedPollingRuntimeDeps<TContext extends TelegramLockContext>
274
276
  formatStartBlockedMessage?: (ctx: TContext) => string;
275
277
  startPolling: (ctx: TContext, options?: TelegramLockedPollingStartOptions) => void | Promise<void>;
276
278
  stopPolling: () => Promise<void>;
277
- registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
279
+ registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry, options?: {
280
+ recoverConnection?: boolean;
281
+ }) => boolean | undefined | Promise<boolean | undefined>;
278
282
  restoreFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
279
283
  stopFollowerRegistration?: () => void;
280
284
  /** Threaded Mode takeover evidence after failed follower registration; replaces the retired file heartbeat. */
package/dist/lib/locks.js CHANGED
@@ -1625,7 +1625,7 @@ export function createTelegramLockedPollingRuntime(deps) {
1625
1625
  if (deps.registerFollowerWithOwner) {
1626
1626
  let failureMessage;
1627
1627
  try {
1628
- const registered = await deps.registerFollowerWithOwner(ctx, acquired.lock);
1628
+ const registered = await deps.registerFollowerWithOwner(ctx, acquired.lock, options.forceFreshLeaderThread ? { recoverConnection: true } : undefined);
1629
1629
  if (!isCurrent())
1630
1630
  return cancelled;
1631
1631
  if (registered) {
@@ -1838,8 +1838,8 @@ export function createTelegramLockedPollingRuntime(deps) {
1838
1838
  sessionAutoStartRun = run;
1839
1839
  },
1840
1840
  registerFollowerWithOwner: deps.registerFollowerWithOwner
1841
- ? async (ctx, owner) => {
1842
- const registered = await deps.registerFollowerWithOwner?.(ctx, owner);
1841
+ ? async (ctx, owner, options) => {
1842
+ const registered = await deps.registerFollowerWithOwner?.(ctx, owner, options);
1843
1843
  if (registered)
1844
1844
  deps.updateStatus(ctx);
1845
1845
  return registered === true;
@@ -24,6 +24,7 @@ interface TelegramQueueMenuRuntime<Context> {
24
24
  handleCallbackQuery: (query: TelegramQueueMenuCallbackQuery, ctx: Context) => Promise<boolean>;
25
25
  }
26
26
  export declare function createTelegramQueueMenuRuntime<Context, TModel extends MenuModel = MenuModel>(deps: {
27
+ hasPendingDispatch?: () => boolean;
27
28
  telegramQueueStore: Queue.TelegramQueueStateStore<Context>;
28
29
  queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
29
30
  sendInteractiveMessage: TelegramMenuMessageRuntimeDeps["sendInteractiveMessage"];