@llblab/pi-telegram 0.55.0 → 0.56.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 (50) hide show
  1. package/AGENTS.md +3 -2
  2. package/BACKLOG.md +4 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +27 -18
  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 +5 -1
  9. package/dist/lib/bus-leader.js +9 -1
  10. package/dist/lib/bus.d.ts +5 -0
  11. package/dist/lib/bus.js +17 -6
  12. package/dist/lib/extension.js +14 -2
  13. package/dist/lib/lifecycle.d.ts +2 -1
  14. package/dist/lib/lifecycle.js +28 -20
  15. package/dist/lib/menu-queue.d.ts +1 -0
  16. package/dist/lib/menu-queue.js +102 -99
  17. package/dist/lib/pi.d.ts +3 -2
  18. package/dist/lib/pi.js +3 -0
  19. package/dist/lib/polling.js +3 -1
  20. package/dist/lib/queue.d.ts +57 -5
  21. package/dist/lib/queue.js +313 -25
  22. package/dist/lib/routing.d.ts +1 -1
  23. package/dist/lib/routing.js +261 -29
  24. package/dist/lib/status.d.ts +1 -2
  25. package/dist/lib/status.js +6 -7
  26. package/dist/lib/turns.d.ts +4 -0
  27. package/dist/lib/turns.js +6 -0
  28. package/dist/lib/updates.d.ts +10 -6
  29. package/dist/lib/updates.js +20 -4
  30. package/dist/package.json +1 -1
  31. package/docs/README.md +1 -1
  32. package/docs/architecture.md +17 -14
  33. package/docs/callback-namespaces.md +4 -0
  34. package/docs/multi-instance-bus.md +4 -2
  35. package/docs/ui-style.md +14 -10
  36. package/lib/bindings.ts +52 -3
  37. package/lib/bus-follower.ts +5 -0
  38. package/lib/bus-leader.ts +9 -0
  39. package/lib/bus.ts +20 -3
  40. package/lib/extension.ts +14 -1
  41. package/lib/lifecycle.ts +34 -28
  42. package/lib/menu-queue.ts +116 -162
  43. package/lib/pi.ts +8 -0
  44. package/lib/polling.ts +3 -1
  45. package/lib/queue.ts +328 -33
  46. package/lib/routing.ts +299 -29
  47. package/lib/status.ts +7 -7
  48. package/lib/turns.ts +7 -0
  49. package/lib/updates.ts +24 -10
  50. package/package.json +1 -1
package/lib/bus-leader.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
24
24
  TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
25
25
  TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
26
+ hasTelegramBusSteerQueueHandoffCapability,
26
27
  TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
27
28
  TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
28
29
  TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
@@ -3273,6 +3274,10 @@ export function createTelegramBusLeaderEnvelopeHandler(
3273
3274
  "Telegram queue handoff capability was not negotiated.",
3274
3275
  );
3275
3276
  }
3277
+ if (envelope.payload.queueLane === "steer" &&
3278
+ ![deps.protocolIdentity, donor.protocol, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
3279
+ return rejectTelegramBusRequest(envelope.requestId, "Steer queue handoff capability was not negotiated.");
3280
+ }
3276
3281
  if (
3277
3282
  !recipient?.registrationGeneration ||
3278
3283
  envelope.recipientRegistrationGeneration !==
@@ -4477,6 +4482,10 @@ export function createTelegramBusLeaderRuntime<TContext>(
4477
4482
  "Telegram queue handoff capability was not negotiated.",
4478
4483
  );
4479
4484
  }
4485
+ if (input.payload.queueLane === "steer" &&
4486
+ ![deps.protocolIdentity, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
4487
+ return rejectTelegramBusRequest(input.requestId, "Steer queue handoff capability was not negotiated.");
4488
+ }
4480
4489
  if (
4481
4490
  !recipient?.registrationGeneration ||
4482
4491
  input.recipientRegistrationGeneration !== recipient.registrationGeneration
package/lib/bus.ts CHANGED
@@ -142,6 +142,8 @@ export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION =
142
142
  "durable-follower-admission-v1" as const;
143
143
  export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
144
144
  "queue-handoff-v1" as const;
145
+ /** Unspent Steer lane codec; independent from baseline receipt handoff support. */
146
+ export const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER = "queue-steer-v1" as const;
145
147
  export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
146
148
  "input-custody-reference-v1" as const;
147
149
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
@@ -461,6 +463,11 @@ export function hasTelegramBusCapability(
461
463
  return identity?.capabilities.includes(capability) ?? false;
462
464
  }
463
465
 
466
+ export function hasTelegramBusSteerQueueHandoffCapability(identity: TelegramBusProtocolIdentity | undefined): boolean {
467
+ return hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) &&
468
+ hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_STEER);
469
+ }
470
+
464
471
  export function getTelegramInputCustodyPeerReadiness(
465
472
  followers: readonly Pick<
466
473
  TelegramBusFollowerView,
@@ -1926,6 +1933,8 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1926
1933
  message: TMessage;
1927
1934
  ownership: TelegramBusForwardOwnership;
1928
1935
  ctx: TContext;
1936
+ /** Process-local source/recipient lifetime; never part of the wire envelope. */
1937
+ assertAuthority?: () => void;
1929
1938
  }) => Promise<TelegramBusForeignUpdateSettlement>;
1930
1939
  forwardEditedMessage: (input: {
1931
1940
  message: TMessage;
@@ -2057,6 +2066,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2057
2066
  const send = async (
2058
2067
  envelope: TelegramBusDurableForwardEnvelope,
2059
2068
  ownership: TelegramBusForwardOwnership,
2069
+ assertAuthority?: () => void,
2060
2070
  ): Promise<TelegramBusForeignUpdateSettlement> => {
2061
2071
  if (
2062
2072
  deps.validateForwardOwnership &&
@@ -2078,10 +2088,15 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2078
2088
  socketPath,
2079
2089
  envelope,
2080
2090
  timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
2081
- retry: getTelegramBusTransportRetryPolicy({
2091
+ retry: assertAuthority ? { attempts: 1, delayMs: 0 } : getTelegramBusTransportRetryPolicy({
2082
2092
  endpoint: socketPath,
2083
2093
  operation: "operation",
2084
2094
  }),
2095
+ ...(assertAuthority || deps.validateForwardOwnership ? { assertAuthority() {
2096
+ assertAuthority?.();
2097
+ if (deps.validateForwardOwnership && !deps.validateForwardOwnership(ownership))
2098
+ throw new Error("Telegram follower forwarding ownership changed.");
2099
+ } } : {}),
2085
2100
  });
2086
2101
  } catch (error) {
2087
2102
  return reject({
@@ -2178,6 +2193,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2178
2193
  base: ForwardBase,
2179
2194
  sentAtMs: number,
2180
2195
  ) => TelegramBusDurableForwardEnvelope,
2196
+ assertAuthority?: () => void,
2181
2197
  ): Promise<TelegramBusForeignUpdateSettlement> => {
2182
2198
  const prepared = prepare(kind, value, ownership);
2183
2199
  if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
@@ -2192,6 +2208,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2192
2208
  ? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
2193
2209
  : build(base, getNowMs()),
2194
2210
  ownership,
2211
+ assertAuthority,
2195
2212
  );
2196
2213
  };
2197
2214
  return {
@@ -2214,7 +2231,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2214
2231
  sentAtMs,
2215
2232
  }),
2216
2233
  ),
2217
- forwardMessage: ({ message, ownership }) =>
2234
+ forwardMessage: ({ message, ownership, assertAuthority }) =>
2218
2235
  forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
2219
2236
  const forwardCommentBatchPosition =
2220
2237
  deps.getForwardCommentBatchPosition?.(message);
@@ -2227,7 +2244,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2227
2244
  : {}),
2228
2245
  sentAtMs,
2229
2246
  };
2230
- }),
2247
+ }, assertAuthority),
2231
2248
  forwardEditedMessage: ({ message, ownership }) =>
2232
2249
  forward(
2233
2250
  "leader.forwardEditedMessage",
package/lib/extension.ts CHANGED
@@ -64,6 +64,7 @@ const telegramBusProtocolIdentity =
64
64
  Bus.createTelegramCurrentBusProtocolIdentity([
65
65
  Bus.TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
66
66
  Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
67
+ Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_STEER,
67
68
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
68
69
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
69
70
  Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
@@ -717,7 +718,6 @@ export default function (pi: Pi.ExtensionAPI) {
717
718
  },
718
719
  getQueuedItems: telegramQueueStore.getQueuedItems,
719
720
  getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
720
- formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
721
721
  getRecentRuntimeEvents: runtimeEvents.getEvents,
722
722
  getRuntimeLockState: lockRuntime.getStatusLabel,
723
723
  ...threadStatusProjectionRuntime,
@@ -1026,6 +1026,7 @@ export default function (pi: Pi.ExtensionAPI) {
1026
1026
  requestNextDispatchAnnouncement,
1027
1027
  cancelNextDispatchAnnouncement,
1028
1028
  watchdog: queueDispatchWatchdogRuntime,
1029
+ onTurnEnd: onSteerTurnEnd,
1029
1030
  } = Bindings.createTelegramQueueBindingRuntime({
1030
1031
  store: telegramQueueStore,
1031
1032
  queue,
@@ -1037,6 +1038,8 @@ export default function (pi: Pi.ExtensionAPI) {
1037
1038
  promptDispatch: promptDispatchRuntime,
1038
1039
  isIdle,
1039
1040
  hasPendingMessages,
1041
+ isContextCurrent: telegramSessionContextStore.isCurrent,
1042
+ isContextAborted: Pi.isExtensionContextAbortRequested,
1040
1043
  updateStatus,
1041
1044
  sendTextReply,
1042
1045
  sendUserMessage,
@@ -1123,6 +1126,7 @@ export default function (pi: Pi.ExtensionAPI) {
1123
1126
 
1124
1127
  // --- Queue And Settings Menus ---
1125
1128
  const queueMenuRuntime = MenuQueue.createTelegramQueueMenuRuntime({
1129
+ hasPendingDispatch: lifecycle.hasDispatchPending,
1126
1130
  telegramQueueStore,
1127
1131
  queueMutationRuntime,
1128
1132
  sendInteractiveMessage,
@@ -2331,6 +2335,14 @@ export default function (pi: Pi.ExtensionAPI) {
2331
2335
  });
2332
2336
  queueHandoffReconciliationBinding.set(
2333
2337
  Updates.createTelegramQueueHandoffReconciliationRuntimeAssembly({
2338
+ hasPendingDispatch: lifecycle.hasDispatchPending,
2339
+ canHandoffSteerWithRecipient(recipient) {
2340
+ const live = telegramBusFollowerRegistry.get(recipient.instanceId);
2341
+ return live?.registrationGeneration === recipient.registrationGeneration &&
2342
+ [telegramBusProtocolIdentity, live?.protocol,
2343
+ ...(telegramBusFollowerRegistrationState.isRegistered() ? [telegramBusFollowerRegistrationState.getLeaderProtocol()] : [])]
2344
+ .every(Bus.hasTelegramBusSteerQueueHandoffCapability);
2345
+ },
2334
2346
  ownsDirect: lockRuntime.owns,
2335
2347
  isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
2336
2348
  isBusEnabled: isTelegramBusRuntimeEnabled,
@@ -3046,6 +3058,7 @@ export default function (pi: Pi.ExtensionAPI) {
3046
3058
  sendMarkdownReply,
3047
3059
  sendTextReply,
3048
3060
  dispatchNextQueuedTelegramTurn,
3061
+ onSteerTurnEnd,
3049
3062
  onPromptHandedOff(turn, ctx) {
3050
3063
  updateAdmissionRuntimeBinding
3051
3064
  .getSettlement()
package/lib/lifecycle.ts CHANGED
@@ -26,6 +26,8 @@ import type {
26
26
  ToolExecutionEndEvent,
27
27
  ToolExecutionStartEvent,
28
28
  ToolExecutionUpdateEvent,
29
+ TurnEndEvent,
30
+ TurnEndEventResult,
29
31
  UIPromptEndEvent,
30
32
  UIPromptStartEvent,
31
33
  } from "./pi.ts";
@@ -100,6 +102,7 @@ export interface TelegramLifecycleRegistrationDeps {
100
102
  event: { model: TelegramLifecycleModel },
101
103
  ctx: ExtensionContext,
102
104
  ) => Promise<void> | void;
105
+ onTurnEnd?: (event: TurnEndEvent, ctx: ExtensionContext) => TurnEndEventResult | undefined;
103
106
  onAgentStart: (
104
107
  event: AgentStartEvent,
105
108
  ctx: ExtensionContext,
@@ -575,7 +578,6 @@ export function createTelegramBridgeSessionLifecycleAssembly<
575
578
  >(
576
579
  deps: TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel>,
577
580
  ): TelegramSessionLifecycleHooks {
578
- const isSessionActive = deps.contextStore.isCurrent;
579
581
  const suspendForReplacement =
580
582
  BusFollower.createTelegramBusFollowerSessionReplacementSuspender({
581
583
  registrationState: deps.follower.registrationState,
@@ -583,32 +585,44 @@ export function createTelegramBridgeSessionLifecycleAssembly<
583
585
  suspendPolling: deps.follower.suspendPolling,
584
586
  recordRuntimeEvent: deps.follower.recordRuntimeEvent,
585
587
  });
586
- let preserveTarget = true;
587
- const queueLifecycle = Queue.createTelegramSessionLifecycleRuntime({
588
- ...deps.queue,
589
- isSessionActive,
590
- stopPolling: () => suspendForReplacement(preserveTarget),
591
- clearPendingMediaGroups: deps.services.suspendGroupedInput,
592
- });
588
+ // A reused context can be current for a successor; inner awaits need this invocation's generation too.
589
+ const queueLifecycle = (ctx: ExtensionContext, generation: number, preserveTarget = true) =>
590
+ Queue.createTelegramSessionLifecycleRuntime({
591
+ ...deps.queue,
592
+ isSessionActive: () => deps.contextStore.isCurrent(ctx, generation),
593
+ stopPolling: () => suspendForReplacement(preserveTarget),
594
+ clearPendingMediaGroups: deps.services.suspendGroupedInput,
595
+ });
593
596
  const servicesLifecycle: TelegramSessionLifecycleHooks = {
594
597
  async onSessionStart(event, ctx) {
598
+ const generation = deps.contextStore.getGeneration();
599
+ const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
595
600
  const resumeConnection = deps.services.connection?.prepare(event, ctx);
596
- await queueLifecycle.onSessionStart(event, ctx);
597
- if (!isSessionActive(ctx)) return;
601
+ await queueLifecycle(ctx, generation).onSessionStart(event, ctx);
602
+ if (!isCurrent()) return;
598
603
  deps.services.resumeGroupedInput(ctx);
599
604
  await deps.services.delivery.onSessionStart();
600
- if (!isSessionActive(ctx)) return;
605
+ if (!isCurrent()) return;
601
606
  if (resumeConnection) resumeConnection();
602
607
  else await deps.services.polling.onSessionStart(event, ctx);
608
+ if (!isCurrent()) return;
603
609
  deps.services.capabilityMonitor.start(ctx);
604
610
  deps.services.queueWatchdog.start(ctx);
611
+ await BusFollower.createTelegramBusFollowerSessionRefreshHook({
612
+ registrationState: deps.follower.registrationState,
613
+ registrationRuntime: deps.follower.registrationRuntime,
614
+ getLeaderState: deps.follower.getLeaderState,
615
+ isSessionActive: isCurrent,
616
+ updateStatus: deps.follower.updateStatus,
617
+ recordRuntimeEvent: deps.follower.recordRuntimeEvent,
618
+ })(event, ctx);
605
619
  },
606
620
  async onSessionShutdown(event, ctx) {
607
621
  const generation = deps.contextStore.getGeneration();
608
622
  const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
609
623
  if (!isCurrent()) return;
610
624
  deps.services.connection?.onSessionShutdown(event, ctx);
611
- preserveTarget = event.reason !== "resume";
625
+ const preserveTarget = event.reason !== "resume";
612
626
  let preserveThread: (() => Promise<void>) | undefined;
613
627
  if (event.reason === "quit") {
614
628
  try {
@@ -625,7 +639,7 @@ export function createTelegramBridgeSessionLifecycleAssembly<
625
639
  if (!isCurrent()) return;
626
640
  deps.services.queueWatchdog.stop();
627
641
  deps.services.capabilityMonitor.stop();
628
- await queueLifecycle.onSessionShutdown(event, ctx);
642
+ await queueLifecycle(ctx, generation, preserveTarget).onSessionShutdown(event, ctx);
629
643
  if (!isCurrent()) return;
630
644
  await deps.services.inboundWorker.onSessionShutdown();
631
645
  if (!isCurrent()) return;
@@ -638,23 +652,9 @@ export function createTelegramBridgeSessionLifecycleAssembly<
638
652
  }
639
653
  },
640
654
  };
641
- const followerLifecycle = appendTelegramLifecycleHooks(
642
- servicesLifecycle,
643
- {
644
- onSessionStart: BusFollower.createTelegramBusFollowerSessionRefreshHook({
645
- registrationState: deps.follower.registrationState,
646
- registrationRuntime: deps.follower.registrationRuntime,
647
- getLeaderState: deps.follower.getLeaderState,
648
- isSessionActive,
649
- updateStatus: deps.follower.updateStatus,
650
- recordRuntimeEvent: deps.follower.recordRuntimeEvent,
651
- }),
652
- },
653
- isSessionActive,
654
- );
655
655
  return createTelegramSessionGenerationFence(
656
656
  deps.contextStore,
657
- followerLifecycle,
657
+ servicesLifecycle,
658
658
  );
659
659
  }
660
660
 
@@ -1042,6 +1042,12 @@ export function registerTelegramLifecycleHooks(
1042
1042
  if (!isActive(ctx)) return;
1043
1043
  await deps.onAgentStart(event, ctx);
1044
1044
  });
1045
+ if (deps.onTurnEnd) {
1046
+ pi.on("turn_end", (event, ctx) => {
1047
+ if (!isActive(ctx)) return;
1048
+ return deps.onTurnEnd?.(event, ctx);
1049
+ });
1050
+ }
1045
1051
  pi.on("tool_execution_start", async (event, ctx) => {
1046
1052
  if (!isActive(ctx)) return;
1047
1053
  await deps.onToolExecutionStart(event, ctx);