@llblab/pi-kit 0.33.0 → 0.34.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 (67) hide show
  1. package/BACKLOG.md +2 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +3 -3
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +5 -4
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
  7. package/node_modules/@llblab/pi-telegram/README.md +29 -18
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +8 -4
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +41 -1
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +2 -0
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +10 -5
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +2 -0
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +34 -20
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +10 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +32 -13
  16. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -1
  17. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +33 -28
  18. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +18 -3
  19. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
  20. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +28 -20
  21. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +6 -2
  22. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +3 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
  24. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +102 -99
  25. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +3 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +3 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +6 -2
  28. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -3
  29. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +57 -5
  30. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +313 -25
  31. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +5 -1
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +323 -30
  33. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +1 -2
  34. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +6 -7
  35. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +11 -1
  36. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +11 -0
  37. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +57 -6
  38. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +4 -0
  39. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +6 -0
  40. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +14 -7
  41. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -9
  42. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  43. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  44. package/node_modules/@llblab/pi-telegram/docs/architecture.md +27 -14
  45. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
  46. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -4
  47. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +14 -10
  48. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
  49. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
  50. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
  51. package/node_modules/@llblab/pi-telegram/lib/bus.ts +36 -10
  52. package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
  53. package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
  54. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
  55. package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
  56. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +116 -162
  57. package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
  58. package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
  59. package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
  60. package/node_modules/@llblab/pi-telegram/lib/routing.ts +350 -30
  61. package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
  62. package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
  63. package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
  64. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  65. package/node_modules/@llblab/pi-telegram/lib/updates.ts +46 -15
  66. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  67. package/package.json +2 -2
@@ -35,13 +35,13 @@ import {
35
35
  getTelegramBusProtocolCompatibility,
36
36
  getTelegramBusSocketPath,
37
37
  hasTelegramBusCapability,
38
+ hasTelegramBusSteerQueueHandoffCapability,
38
39
  isTelegramBusEnvelopeAuthorized,
39
40
  parseTelegramBusEnvelope,
40
41
  resolveTelegramBusSocketPath,
41
42
  sendTelegramBusLocalEnvelope,
42
43
  TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
43
44
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
44
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
45
45
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
46
46
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
47
47
  TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
@@ -51,6 +51,7 @@ import {
51
51
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
52
52
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
53
53
  TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
54
+ getTelegramBusHeldCommandCapabilities,
54
55
  TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
55
56
  TelegramBusLocalAuthorityError,
56
57
  type TelegramBusAgentMessage,
@@ -177,6 +178,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
177
178
  target?: TelegramTarget;
178
179
  previousInstanceId?: string;
179
180
  restoreWorkspace?: boolean;
181
+ recoverConnection?: boolean;
180
182
  },
181
183
  ) => Promise<boolean>;
182
184
  setContext: (ctx: TContext) => void | Promise<void>;
@@ -2929,7 +2931,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
2929
2931
  deps.setActiveAuthSecret?.(undefined);
2930
2932
  await deps.stopReceiving?.();
2931
2933
  };
2932
- const pendingHandoff = options
2934
+ const pendingHandoff = options && !options.recoverConnection
2933
2935
  ? undefined
2934
2936
  : getTelegramFollowerSessionHandoff();
2935
2937
  const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(
@@ -2945,8 +2947,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
2945
2947
  target?: TelegramTarget;
2946
2948
  previousInstanceId?: string;
2947
2949
  restoreWorkspace?: boolean;
2950
+ recoverConnection?: boolean;
2948
2951
  }
2949
- | undefined = options ?? pendingHandoffOptions;
2952
+ | undefined = options ? { ...pendingHandoffOptions, ...options } : pendingHandoffOptions;
2950
2953
  const leaderSocketPath =
2951
2954
  leader.busSocketPath ??
2952
2955
  deps.getLeaderSocketPath?.() ??
@@ -2967,6 +2970,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
2967
2970
  : "follower.register",
2968
2971
  requestId: registrationGeneration,
2969
2972
  auth: activeAuthSecret,
2973
+ ...(registrationOptions?.recoverConnection && !registrationOptions.restoreWorkspace
2974
+ ? { recoverConnection: true as const } : {}),
2970
2975
  registration: {
2971
2976
  instanceId: deps.instanceId,
2972
2977
  ...(registrationOptions?.previousInstanceId
@@ -4545,8 +4550,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
4545
4550
  ? [
4546
4551
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
4547
4552
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
4548
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
4549
- TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
4553
+ ...getTelegramBusHeldCommandCapabilities(commandInput.name),
4550
4554
  ]
4551
4555
  : []),
4552
4556
  ];
@@ -4752,6 +4756,10 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
4752
4756
  result: receipt,
4753
4757
  };
4754
4758
  }
4759
+ if (envelope.payload.queueLane === "steer" &&
4760
+ ![deps.getLocalProtocol?.(), deps.getLeaderProtocol?.()].every(hasTelegramBusSteerQueueHandoffCapability)) {
4761
+ throw new Error("Steer queue handoff capability was not negotiated.");
4762
+ }
4755
4763
  if (!deps.handleQueueHandoff) {
4756
4764
  throw new Error(
4757
4765
  "Telegram bus receiver cannot accept queue handoff payloads.",
@@ -23,6 +23,8 @@ 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
+ TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
27
+ hasTelegramBusSteerQueueHandoffCapability,
26
28
  TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
27
29
  TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
28
30
  TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
@@ -1528,7 +1530,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1528
1530
  deps: TelegramBusFollowerTargetProvisionerDeps,
1529
1531
  ): (
1530
1532
  registration: TelegramBusInstanceRegistration,
1531
- options?: { existingWorkspaceBindingOnly?: boolean },
1533
+ options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
1532
1534
  ) => Promise<
1533
1535
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
1534
1536
  > {
@@ -1547,6 +1549,16 @@ export function createTelegramBusFollowerTargetProvisioner(
1547
1549
  const chatId = deps.getAllowedUserId();
1548
1550
  if (typeof chatId !== "number") return registration.target;
1549
1551
  await deps.topicTargetStore.load();
1552
+ if (options?.recoverConnection && !options.existingWorkspaceBindingOnly) {
1553
+ const epoch = deps.getCurrentLeaderEpoch?.();
1554
+ await Threads.resetTelegramConnectionProvisioning({
1555
+ store: deps.topicTargetStore, owner: "manual-follower", instanceId: registration.instanceId,
1556
+ profileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
1557
+ isCurrent: () => !deps.getCurrentLeaderEpoch ||
1558
+ (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch),
1559
+ recordEvent: deps.recordRuntimeEvent,
1560
+ });
1561
+ }
1550
1562
  let restoredPredecessor: Threads.TelegramTopicTargetRecord | undefined;
1551
1563
  if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
1552
1564
  !registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
@@ -1579,6 +1591,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1579
1591
  {
1580
1592
  existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
1581
1593
  sessionId: registration.sessionId,
1594
+ provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
1582
1595
  onCapacityUnavailable() {
1583
1596
  capacityUnavailable = true;
1584
1597
  },
@@ -1841,12 +1854,6 @@ export function createTelegramBusFollowerTargetProvisioner(
1841
1854
  };
1842
1855
  }
1843
1856
  const probeRequiredRecord = reconnectRecord?.status === "probe-required";
1844
- const exactSessionHandoff =
1845
- crossSessionReuse &&
1846
- !!requestedTarget &&
1847
- registration.previousInstanceId === reconnectRecord?.instanceId &&
1848
- requestedTarget.chatId === reconnectRecord.target.chatId &&
1849
- requestedTarget.threadId === reconnectRecord.target.threadId;
1850
1857
  const requiresVisibilityProbe =
1851
1858
  crossSessionReuse ||
1852
1859
  probeRequiredRecord ||
@@ -1868,19 +1875,14 @@ export function createTelegramBusFollowerTargetProvisioner(
1868
1875
  try {
1869
1876
  assertRestoreRegistration();
1870
1877
  await deps.callApi(
1871
- exactSessionHandoff ? "sendChatAction" : "sendMessage",
1872
- exactSessionHandoff
1873
- ? {
1874
- chat_id: connectedAnnouncement.target.chatId,
1875
- message_thread_id: connectedAnnouncement.target.threadId,
1876
- action: "typing",
1877
- }
1878
- : {
1879
- chat_id: connectedAnnouncement.target.chatId,
1880
- message_thread_id: connectedAnnouncement.target.threadId,
1881
- text: connectedAnnouncement.text,
1882
- parse_mode: connectedAnnouncement.parseMode,
1883
- },
1878
+ // Typing is presence, not evidence that a retained Thread still accepts messages.
1879
+ "sendMessage",
1880
+ {
1881
+ chat_id: connectedAnnouncement.target.chatId,
1882
+ message_thread_id: connectedAnnouncement.target.threadId,
1883
+ text: connectedAnnouncement.text,
1884
+ parse_mode: connectedAnnouncement.parseMode,
1885
+ },
1884
1886
  );
1885
1887
  assertRestoreRegistration();
1886
1888
  if (recoverableTarget || probeRequiredRecord) {
@@ -2943,7 +2945,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
2943
2945
  ) => Promise<unknown> | unknown;
2944
2946
  provisionFollowerTarget?: (
2945
2947
  registration: TelegramBusInstanceRegistration,
2946
- options?: { existingWorkspaceBindingOnly?: boolean },
2948
+ options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
2947
2949
  ) =>
2948
2950
  | Promise<
2949
2951
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
@@ -3273,6 +3275,10 @@ export function createTelegramBusLeaderEnvelopeHandler(
3273
3275
  "Telegram queue handoff capability was not negotiated.",
3274
3276
  );
3275
3277
  }
3278
+ if (envelope.payload.queueLane === "steer" &&
3279
+ ![deps.protocolIdentity, donor.protocol, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
3280
+ return rejectTelegramBusRequest(envelope.requestId, "Steer queue handoff capability was not negotiated.");
3281
+ }
3276
3282
  if (
3277
3283
  !recipient?.registrationGeneration ||
3278
3284
  envelope.recipientRegistrationGeneration !==
@@ -3448,6 +3454,9 @@ export function createTelegramBusLeaderEnvelopeHandler(
3448
3454
  ));
3449
3455
  const restoringWorkspace =
3450
3456
  envelope.kind === "follower.restoreWorkspace";
3457
+ const recoverConnection = envelope.kind === "follower.register" && envelope.recoverConnection === true;
3458
+ const supportsConnectionRecovery = [deps.protocolIdentity, envelope.registration.protocol]
3459
+ .every(identity => hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY));
3451
3460
  const supportsWorkspaceAutoConnect =
3452
3461
  hasTelegramBusCapability(
3453
3462
  deps.protocolIdentity,
@@ -3463,7 +3472,8 @@ export function createTelegramBusLeaderEnvelopeHandler(
3463
3472
  !readerCompatible() ||
3464
3473
  (envelope.registration.cwd !== undefined &&
3465
3474
  envelope.registration.sessionId === undefined) ||
3466
- (restoringWorkspace && !supportsWorkspaceAutoConnect)
3475
+ (restoringWorkspace && !supportsWorkspaceAutoConnect) ||
3476
+ (recoverConnection && !supportsConnectionRecovery)
3467
3477
  ) {
3468
3478
  return {
3469
3479
  kind: "bus.ack" as const,
@@ -3521,7 +3531,7 @@ export function createTelegramBusLeaderEnvelopeHandler(
3521
3531
  );
3522
3532
  const target = await deps.provisionFollowerTarget?.(
3523
3533
  envelope.registration,
3524
- { existingWorkspaceBindingOnly: restoringWorkspace },
3534
+ { existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) },
3525
3535
  );
3526
3536
  if (restoringWorkspace && !target) {
3527
3537
  return {
@@ -4477,6 +4487,10 @@ export function createTelegramBusLeaderRuntime<TContext>(
4477
4487
  "Telegram queue handoff capability was not negotiated.",
4478
4488
  );
4479
4489
  }
4490
+ if (input.payload.queueLane === "steer" &&
4491
+ ![deps.protocolIdentity, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
4492
+ return rejectTelegramBusRequest(input.requestId, "Steer queue handoff capability was not negotiated.");
4493
+ }
4480
4494
  if (
4481
4495
  !recipient?.registrationGeneration ||
4482
4496
  input.recipientRegistrationGeneration !== recipient.registrationGeneration
@@ -142,6 +142,9 @@ 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;
147
+ export const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY = "explicit-connect-recovery-v1" as const;
145
148
  export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
146
149
  "input-custody-reference-v1" as const;
147
150
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
@@ -182,6 +185,14 @@ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
182
185
  TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
183
186
  ] as const;
184
187
 
188
+ /** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
189
+ export const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES = "held-menu-aliases-v1" as const;
190
+ export function getTelegramBusHeldCommandCapabilities(name: string): readonly string[] {
191
+ return name === "start" || name === "help"
192
+ ? [...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES]
193
+ : TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES;
194
+ }
195
+
185
196
  export interface TelegramBusSelectedCommandInput {
186
197
  name: string;
187
198
  target: TelegramTarget & { threadId: number };
@@ -461,6 +472,11 @@ export function hasTelegramBusCapability(
461
472
  return identity?.capabilities.includes(capability) ?? false;
462
473
  }
463
474
 
475
+ export function hasTelegramBusSteerQueueHandoffCapability(identity: TelegramBusProtocolIdentity | undefined): boolean {
476
+ return hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) &&
477
+ hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_STEER);
478
+ }
479
+
464
480
  export function getTelegramInputCustodyPeerReadiness(
465
481
  followers: readonly Pick<
466
482
  TelegramBusFollowerView,
@@ -1002,6 +1018,7 @@ export type TelegramBusEnvelope = (
1002
1018
  | {
1003
1019
  kind: "follower.register";
1004
1020
  requestId: string;
1021
+ recoverConnection?: true;
1005
1022
  registration: TelegramBusInstanceRegistration;
1006
1023
  }
1007
1024
  | {
@@ -1926,6 +1943,8 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1926
1943
  message: TMessage;
1927
1944
  ownership: TelegramBusForwardOwnership;
1928
1945
  ctx: TContext;
1946
+ /** Process-local source/recipient lifetime; never part of the wire envelope. */
1947
+ assertAuthority?: () => void;
1929
1948
  }) => Promise<TelegramBusForeignUpdateSettlement>;
1930
1949
  forwardEditedMessage: (input: {
1931
1950
  message: TMessage;
@@ -2057,6 +2076,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2057
2076
  const send = async (
2058
2077
  envelope: TelegramBusDurableForwardEnvelope,
2059
2078
  ownership: TelegramBusForwardOwnership,
2079
+ assertAuthority?: () => void,
2060
2080
  ): Promise<TelegramBusForeignUpdateSettlement> => {
2061
2081
  if (
2062
2082
  deps.validateForwardOwnership &&
@@ -2078,10 +2098,15 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2078
2098
  socketPath,
2079
2099
  envelope,
2080
2100
  timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
2081
- retry: getTelegramBusTransportRetryPolicy({
2101
+ retry: assertAuthority ? { attempts: 1, delayMs: 0 } : getTelegramBusTransportRetryPolicy({
2082
2102
  endpoint: socketPath,
2083
2103
  operation: "operation",
2084
2104
  }),
2105
+ ...(assertAuthority || deps.validateForwardOwnership ? { assertAuthority() {
2106
+ assertAuthority?.();
2107
+ if (deps.validateForwardOwnership && !deps.validateForwardOwnership(ownership))
2108
+ throw new Error("Telegram follower forwarding ownership changed.");
2109
+ } } : {}),
2085
2110
  });
2086
2111
  } catch (error) {
2087
2112
  return reject({
@@ -2178,6 +2203,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2178
2203
  base: ForwardBase,
2179
2204
  sentAtMs: number,
2180
2205
  ) => TelegramBusDurableForwardEnvelope,
2206
+ assertAuthority?: () => void,
2181
2207
  ): Promise<TelegramBusForeignUpdateSettlement> => {
2182
2208
  const prepared = prepare(kind, value, ownership);
2183
2209
  if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
@@ -2192,6 +2218,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2192
2218
  ? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
2193
2219
  : build(base, getNowMs()),
2194
2220
  ownership,
2221
+ assertAuthority,
2195
2222
  );
2196
2223
  };
2197
2224
  return {
@@ -2214,7 +2241,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2214
2241
  sentAtMs,
2215
2242
  }),
2216
2243
  ),
2217
- forwardMessage: ({ message, ownership }) =>
2244
+ forwardMessage: ({ message, ownership, assertAuthority }) =>
2218
2245
  forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
2219
2246
  const forwardCommentBatchPosition =
2220
2247
  deps.getForwardCommentBatchPosition?.(message);
@@ -2227,7 +2254,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
2227
2254
  : {}),
2228
2255
  sentAtMs,
2229
2256
  };
2230
- }),
2257
+ }, assertAuthority),
2231
2258
  forwardEditedMessage: ({ message, ownership }) =>
2232
2259
  forward(
2233
2260
  "leader.forwardEditedMessage",
@@ -2522,12 +2549,7 @@ export function createTelegramBusLiveRebindController(deps: {
2522
2549
  : []),
2523
2550
  ...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
2524
2551
  ...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
2525
- ...(held
2526
- ? [
2527
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
2528
- TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
2529
- ]
2530
- : []),
2552
+ ...(held ? getTelegramBusHeldCommandCapabilities(held.name) : []),
2531
2553
  ];
2532
2554
  const follower = deps.getFollower(expected.instanceId),
2533
2555
  secret = deps.getAuthSecret();
@@ -3528,8 +3550,12 @@ function parseRegisterEnvelope(
3528
3550
  requestId: string,
3529
3551
  kind: "follower.register" | "follower.restoreWorkspace",
3530
3552
  ): TelegramBusEnvelope | undefined {
3553
+ if (value.recoverConnection !== undefined &&
3554
+ (kind !== "follower.register" || value.recoverConnection !== true)) return undefined;
3531
3555
  const registration = parseRegistration(value.registration);
3532
- return registration ? { kind, requestId, registration } : undefined;
3556
+ return registration ? { kind, requestId, registration,
3557
+ ...(kind === "follower.register" && value.recoverConnection === true ? { recoverConnection: true as const } : {}),
3558
+ } : undefined;
3533
3559
  }
3534
3560
 
3535
3561
  function parseFollowerPresenceEnvelope(
@@ -53,7 +53,7 @@ import {
53
53
  } from "./updates.ts";
54
54
 
55
55
  export type TelegramHeldCommandName =
56
- "status" | "abort" | "stop" | "next" | "continue";
56
+ "status" | "start" | "help" | "abort" | "stop" | "next" | "continue";
57
57
 
58
58
  /** Commands-owned no-fold admission over the ordinary recipient queue; source reporting stays separate from removal. */
59
59
  export interface TelegramHeldTurnAdmission {
@@ -2773,53 +2773,58 @@ export function createTelegramCommandHandler<
2773
2773
  },
2774
2774
  };
2775
2775
  };
2776
- const heldPlans: Record<TelegramHeldCommandName, HeldPlan> = {
2777
- status: {
2778
- completion: "before-effect",
2779
- effect: "showStatus",
2780
- prepare(ctx, effects) {
2781
- const scopedStatus = effects?.showStatus;
2782
- const captured = {
2783
- ...deps,
2784
- showStatus: scopedStatus
2785
- ? (
2786
- message: TMessage,
2787
- context: TContext,
2788
- options?: Pick<TelegramApiCallOptions, "assertAuthority">,
2789
- ) => {
2790
- const address = getTelegramCommandMessageTarget(message);
2791
- return scopedStatus.call(
2792
- effects,
2793
- address.chatId,
2794
- address.replyToMessageId,
2795
- context,
2796
- address.threadId,
2797
- options,
2798
- );
2799
- }
2800
- : deps.showStatus,
2801
- };
2802
- return {
2803
- captured,
2804
- portsCurrent: () => !effects || effects.showStatus === scopedStatus,
2805
- async run({ message, command, assertRecipient }) {
2806
- const recipientOptions = { assertAuthority: assertRecipient };
2807
- return handleTelegramCommandRuntime(
2808
- command.name,
2809
- message,
2810
- ctx,
2811
- {
2812
- ...captured,
2813
- assertExecutionCurrent: assertRecipient,
2814
- showStatus: (value: TMessage, context: TContext) =>
2815
- captured.showStatus(value, context, recipientOptions),
2816
- },
2817
- command.args,
2818
- );
2819
- },
2820
- };
2821
- },
2776
+ // Held followers are already paired and bound. Start/help open the same restricted menu as status;
2777
+ // they cannot pair an operator, bootstrap another Thread or invoke unrestricted Bot API effects.
2778
+ const heldMenuPlan: HeldPlan = {
2779
+ completion: "before-effect",
2780
+ effect: "showStatus",
2781
+ prepare(ctx, effects) {
2782
+ const scopedStatus = effects?.showStatus;
2783
+ const captured = {
2784
+ ...deps,
2785
+ showStatus: scopedStatus
2786
+ ? (
2787
+ message: TMessage,
2788
+ context: TContext,
2789
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
2790
+ ) => {
2791
+ const address = getTelegramCommandMessageTarget(message);
2792
+ return scopedStatus.call(
2793
+ effects,
2794
+ address.chatId,
2795
+ address.replyToMessageId,
2796
+ context,
2797
+ address.threadId,
2798
+ options,
2799
+ );
2800
+ }
2801
+ : deps.showStatus,
2802
+ };
2803
+ return {
2804
+ captured,
2805
+ portsCurrent: () => !effects || effects.showStatus === scopedStatus,
2806
+ async run({ message, command, assertRecipient }) {
2807
+ const recipientOptions = { assertAuthority: assertRecipient };
2808
+ return handleTelegramCommandRuntime(
2809
+ "status",
2810
+ message,
2811
+ ctx,
2812
+ {
2813
+ ...captured,
2814
+ assertExecutionCurrent: assertRecipient,
2815
+ showStatus: (value: TMessage, context: TContext) =>
2816
+ captured.showStatus(value, context, recipientOptions),
2817
+ },
2818
+ command.args,
2819
+ );
2820
+ },
2821
+ };
2822
2822
  },
2823
+ };
2824
+ const heldPlans: Record<TelegramHeldCommandName, HeldPlan> = {
2825
+ status: heldMenuPlan,
2826
+ start: heldMenuPlan,
2827
+ help: heldMenuPlan,
2823
2828
  abort: {
2824
2829
  completion: "after-semantics",
2825
2830
  effect: "sendTextReply",
@@ -64,6 +64,9 @@ 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,
68
+ Bus.TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
69
+ Bus.TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES,
67
70
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
68
71
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
69
72
  Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
@@ -717,7 +720,6 @@ export default function (pi: Pi.ExtensionAPI) {
717
720
  },
718
721
  getQueuedItems: telegramQueueStore.getQueuedItems,
719
722
  getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
720
- formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
721
723
  getRecentRuntimeEvents: runtimeEvents.getEvents,
722
724
  getRuntimeLockState: lockRuntime.getStatusLabel,
723
725
  ...threadStatusProjectionRuntime,
@@ -1026,6 +1028,7 @@ export default function (pi: Pi.ExtensionAPI) {
1026
1028
  requestNextDispatchAnnouncement,
1027
1029
  cancelNextDispatchAnnouncement,
1028
1030
  watchdog: queueDispatchWatchdogRuntime,
1031
+ onTurnEnd: onSteerTurnEnd,
1029
1032
  } = Bindings.createTelegramQueueBindingRuntime({
1030
1033
  store: telegramQueueStore,
1031
1034
  queue,
@@ -1037,6 +1040,8 @@ export default function (pi: Pi.ExtensionAPI) {
1037
1040
  promptDispatch: promptDispatchRuntime,
1038
1041
  isIdle,
1039
1042
  hasPendingMessages,
1043
+ isContextCurrent: telegramSessionContextStore.isCurrent,
1044
+ isContextAborted: Pi.isExtensionContextAbortRequested,
1040
1045
  updateStatus,
1041
1046
  sendTextReply,
1042
1047
  sendUserMessage,
@@ -1123,6 +1128,7 @@ export default function (pi: Pi.ExtensionAPI) {
1123
1128
 
1124
1129
  // --- Queue And Settings Menus ---
1125
1130
  const queueMenuRuntime = MenuQueue.createTelegramQueueMenuRuntime({
1131
+ hasPendingDispatch: lifecycle.hasDispatchPending,
1126
1132
  telegramQueueStore,
1127
1133
  queueMutationRuntime,
1128
1134
  sendInteractiveMessage,
@@ -1234,7 +1240,7 @@ export default function (pi: Pi.ExtensionAPI) {
1234
1240
  follower.protocol,
1235
1241
  [
1236
1242
  ...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
1237
- ...Bus.TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
1243
+ ...Bus.getTelegramBusHeldCommandCapabilities(name),
1238
1244
  ],
1239
1245
  ) &&
1240
1246
  typeof selectedMenuDelivery === "function" &&
@@ -1555,6 +1561,7 @@ export default function (pi: Pi.ExtensionAPI) {
1555
1561
  threadStore,
1556
1562
  runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
1557
1563
  liveRebindCleanupSchedule: Routing.TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE,
1564
+ liveRebindCommandCompletionTimeoutMs: 8_000,
1558
1565
  updateStatus,
1559
1566
  isContextActive: telegramSessionContextStore.isCurrent,
1560
1567
  dispatchNextQueuedTelegramTurn,
@@ -2331,6 +2338,14 @@ export default function (pi: Pi.ExtensionAPI) {
2331
2338
  });
2332
2339
  queueHandoffReconciliationBinding.set(
2333
2340
  Updates.createTelegramQueueHandoffReconciliationRuntimeAssembly({
2341
+ hasPendingDispatch: lifecycle.hasDispatchPending,
2342
+ canHandoffSteerWithRecipient(recipient) {
2343
+ const live = telegramBusFollowerRegistry.get(recipient.instanceId);
2344
+ return live?.registrationGeneration === recipient.registrationGeneration &&
2345
+ [telegramBusProtocolIdentity, live?.protocol,
2346
+ ...(telegramBusFollowerRegistrationState.isRegistered() ? [telegramBusFollowerRegistrationState.getLeaderProtocol()] : [])]
2347
+ .every(Bus.hasTelegramBusSteerQueueHandoffCapability);
2348
+ },
2334
2349
  ownsDirect: lockRuntime.owns,
2335
2350
  isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
2336
2351
  isBusEnabled: isTelegramBusRuntimeEnabled,
@@ -3046,6 +3061,7 @@ export default function (pi: Pi.ExtensionAPI) {
3046
3061
  sendMarkdownReply,
3047
3062
  sendTextReply,
3048
3063
  dispatchNextQueuedTelegramTurn,
3064
+ onSteerTurnEnd,
3049
3065
  onPromptHandedOff(turn, ctx) {
3050
3066
  updateAdmissionRuntimeBinding
3051
3067
  .getSettlement()