@llblab/pi-kit 0.33.1 → 0.34.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 (64) 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 +6 -5
  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 +28 -17
  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 +1 -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 +8 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +21 -7
  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 +100 -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 +4 -0
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +62 -1
  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/updates.d.ts +13 -32
  39. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -15
  40. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  41. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  42. package/node_modules/@llblab/pi-telegram/docs/architecture.md +25 -14
  43. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
  44. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -6
  45. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +17 -17
  46. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
  47. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
  48. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
  49. package/node_modules/@llblab/pi-telegram/lib/bus.ts +23 -7
  50. package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
  51. package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
  52. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
  53. package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
  54. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +114 -162
  55. package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
  56. package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
  57. package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
  58. package/node_modules/@llblab/pi-telegram/lib/routing.ts +51 -1
  59. package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
  60. package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
  61. package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
  62. package/node_modules/@llblab/pi-telegram/lib/updates.ts +44 -21
  63. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  64. package/package.json +2 -2
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { isDeepStrictEqual } from "node:util";
8
8
  import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
9
- import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
9
+ import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY, hasTelegramBusSteerQueueHandoffCapability, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
10
10
  import { escapeHtml } from "./rendering.js";
11
11
  import * as Sync from "./sync.js";
12
12
  import { parseTelegramIntegerId as asInteger, } from "./target.js";
@@ -881,6 +881,16 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
881
881
  if (typeof chatId !== "number")
882
882
  return registration.target;
883
883
  await deps.topicTargetStore.load();
884
+ if (options?.recoverConnection && !options.existingWorkspaceBindingOnly) {
885
+ const epoch = deps.getCurrentLeaderEpoch?.();
886
+ await Threads.resetTelegramConnectionProvisioning({
887
+ store: deps.topicTargetStore, owner: "manual-follower", instanceId: registration.instanceId,
888
+ profileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
889
+ isCurrent: () => !deps.getCurrentLeaderEpoch ||
890
+ (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch),
891
+ recordEvent: deps.recordRuntimeEvent,
892
+ });
893
+ }
884
894
  let restoredPredecessor;
885
895
  if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
886
896
  !registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
@@ -917,6 +927,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
917
927
  ? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId ?? restoredPredecessor?.instanceId, {
918
928
  existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
919
929
  sessionId: registration.sessionId,
930
+ provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
920
931
  onCapacityUnavailable() {
921
932
  capacityUnavailable = true;
922
933
  },
@@ -1129,11 +1140,6 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1129
1140
  };
1130
1141
  }
1131
1142
  const probeRequiredRecord = reconnectRecord?.status === "probe-required";
1132
- const exactSessionHandoff = crossSessionReuse &&
1133
- !!requestedTarget &&
1134
- registration.previousInstanceId === reconnectRecord?.instanceId &&
1135
- requestedTarget.chatId === reconnectRecord.target.chatId &&
1136
- requestedTarget.threadId === reconnectRecord.target.threadId;
1137
1143
  const requiresVisibilityProbe = crossSessionReuse ||
1138
1144
  probeRequiredRecord ||
1139
1145
  recoverableTarget !== undefined;
@@ -1151,18 +1157,14 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1151
1157
  if (requiresVisibilityProbe && connectedAnnouncement) {
1152
1158
  try {
1153
1159
  assertRestoreRegistration();
1154
- await deps.callApi(exactSessionHandoff ? "sendChatAction" : "sendMessage", exactSessionHandoff
1155
- ? {
1156
- chat_id: connectedAnnouncement.target.chatId,
1157
- message_thread_id: connectedAnnouncement.target.threadId,
1158
- action: "typing",
1159
- }
1160
- : {
1161
- chat_id: connectedAnnouncement.target.chatId,
1162
- message_thread_id: connectedAnnouncement.target.threadId,
1163
- text: connectedAnnouncement.text,
1164
- parse_mode: connectedAnnouncement.parseMode,
1165
- });
1160
+ await deps.callApi(
1161
+ // Typing is presence, not evidence that a retained Thread still accepts messages.
1162
+ "sendMessage", {
1163
+ chat_id: connectedAnnouncement.target.chatId,
1164
+ message_thread_id: connectedAnnouncement.target.threadId,
1165
+ text: connectedAnnouncement.text,
1166
+ parse_mode: connectedAnnouncement.parseMode,
1167
+ });
1166
1168
  assertRestoreRegistration();
1167
1169
  if (recoverableTarget || probeRequiredRecord) {
1168
1170
  const activatedRecord = deps.topicTargetStore.upsert({
@@ -2120,6 +2122,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2120
2122
  !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
2121
2123
  return rejectTelegramBusRequest(envelope.requestId, "Telegram queue handoff capability was not negotiated.");
2122
2124
  }
2125
+ if (envelope.payload.queueLane === "steer" &&
2126
+ ![deps.protocolIdentity, donor.protocol, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
2127
+ return rejectTelegramBusRequest(envelope.requestId, "Steer queue handoff capability was not negotiated.");
2128
+ }
2123
2129
  if (!recipient?.registrationGeneration ||
2124
2130
  envelope.recipientRegistrationGeneration !==
2125
2131
  recipient.registrationGeneration) {
@@ -2257,6 +2263,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2257
2263
  (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
2258
2264
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
2259
2265
  const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
2266
+ const recoverConnection = envelope.kind === "follower.register" && envelope.recoverConnection === true;
2267
+ const supportsConnectionRecovery = [deps.protocolIdentity, envelope.registration.protocol]
2268
+ .every(identity => hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY));
2260
2269
  const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
2261
2270
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
2262
2271
  if (!compatibility.compatible ||
@@ -2264,7 +2273,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2264
2273
  !readerCompatible() ||
2265
2274
  (envelope.registration.cwd !== undefined &&
2266
2275
  envelope.registration.sessionId === undefined) ||
2267
- (restoringWorkspace && !supportsWorkspaceAutoConnect)) {
2276
+ (restoringWorkspace && !supportsWorkspaceAutoConnect) ||
2277
+ (recoverConnection && !supportsConnectionRecovery)) {
2268
2278
  return {
2269
2279
  kind: "bus.ack",
2270
2280
  requestId: envelope.requestId,
@@ -2305,7 +2315,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2305
2315
  if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
2306
2316
  !deps.commitFollowerRegistration)
2307
2317
  throw new Error("Workspace Restore registration publication protection is unavailable.");
2308
- const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
2318
+ const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) });
2309
2319
  if (restoringWorkspace && !target) {
2310
2320
  return {
2311
2321
  kind: "bus.ack",
@@ -3026,6 +3036,10 @@ export function createTelegramBusLeaderRuntime(deps) {
3026
3036
  !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
3027
3037
  return rejectTelegramBusRequest(input.requestId, "Telegram queue handoff capability was not negotiated.");
3028
3038
  }
3039
+ if (input.payload.queueLane === "steer" &&
3040
+ ![deps.protocolIdentity, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
3041
+ return rejectTelegramBusRequest(input.requestId, "Steer queue handoff capability was not negotiated.");
3042
+ }
3029
3043
  if (!recipient?.registrationGeneration ||
3030
3044
  input.recipientRegistrationGeneration !== recipient.registrationGeneration) {
3031
3045
  return rejectTelegramBusRequest(input.requestId, "Stale Telegram queue handoff recipient registration generation.");
@@ -36,6 +36,9 @@ export declare function createTelegramBusProcessRuntime(input: {
36
36
  export declare function createTelegramBusAuthSecret(): string;
37
37
  export declare const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION: "durable-follower-admission-v1";
38
38
  export declare const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF: "queue-handoff-v1";
39
+ /** Unspent Steer lane codec; independent from baseline receipt handoff support. */
40
+ export declare const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER: "queue-steer-v1";
41
+ export declare const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY: "explicit-connect-recovery-v1";
39
42
  export declare const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE: "input-custody-reference-v1";
40
43
  export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME: "workspace-thread-rename-v1";
41
44
  export declare const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE: "thread-display-mode-v1";
@@ -56,6 +59,9 @@ export declare const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET: "live-thre
56
59
  /** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
57
60
  export declare const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES: readonly ["live-thread-rebind-save-v1", "live-thread-rebind-apply-v1", "live-thread-rebind-settle-v1"];
58
61
  export declare const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES: readonly ["live-thread-rebind-command-set-v1", "selected-menu-delivery-v1"];
62
+ /** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
63
+ export declare const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES: "held-menu-aliases-v1";
64
+ export declare function getTelegramBusHeldCommandCapabilities(name: string): readonly string[];
59
65
  export interface TelegramBusSelectedCommandInput {
60
66
  name: string;
61
67
  target: TelegramTarget & {
@@ -192,6 +198,7 @@ export declare function createTelegramBusProtocolIdentity(input: {
192
198
  }): TelegramBusProtocolIdentity;
193
199
  export declare function createTelegramCurrentBusProtocolIdentity(capabilities?: readonly string[]): TelegramBusProtocolIdentity;
194
200
  export declare function hasTelegramBusCapability(identity: TelegramBusProtocolIdentity | undefined, capability: string): boolean;
201
+ export declare function hasTelegramBusSteerQueueHandoffCapability(identity: TelegramBusProtocolIdentity | undefined): boolean;
195
202
  export declare function getTelegramInputCustodyPeerReadiness(followers: readonly Pick<TelegramBusFollowerView, "registrationGeneration" | "protocol">[]): ("ready" | "legacy" | "unknown")[];
196
203
  /** Both peers advertise every required capability over a compatible protocol; reads only the two identities. */
197
204
  export declare function hasTelegramBusSharedCapabilities(local: TelegramBusProtocolIdentity, remote: TelegramBusProtocolIdentity | undefined, required: readonly string[]): boolean;
@@ -324,6 +331,7 @@ export declare function createTelegramBusFollowerSourceReferenceDeliveryIdentity
324
331
  export type TelegramBusEnvelope = ({
325
332
  kind: "follower.register";
326
333
  requestId: string;
334
+ recoverConnection?: true;
327
335
  registration: TelegramBusInstanceRegistration;
328
336
  } | {
329
337
  kind: "follower.restoreWorkspace";
@@ -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. */
@@ -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"];