@llblab/pi-telegram 0.56.0 → 0.56.2

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.
package/docs/ui-style.md CHANGED
@@ -56,7 +56,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
56
56
  | `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
57
57
  | `⛔️` | Cancel source routing | `⛔️ Cancel routing` chooser action and the `Routing cancelled` toast | Not an abort of active Pi work. Private retention remains mandatory; disposable-tab removal stays bound to exact eligible target authority and follows only once every input in the tab is resolved. |
58
58
  | `❌` | No / cancel / terminal failure | Confirmation cancel buttons and terminal failure notices | Do not use for a recoverable operation failure that leaves session state intact. |
59
- | `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
59
+ | `🗑` | Delete | Destructive confirmations | Not a queue reaction shortcut; queue menus use reversible Keep/Skip selectors. |
60
60
 
61
61
  ### Informational Feedback
62
62
 
@@ -96,14 +96,10 @@ The menu may clear internal Skip but cannot remove a reaction created by the use
96
96
  | --- | --- | --- | --- |
97
97
  | `👍` | Promote to priority | Queue reaction shortcut | Normalized from variants like `👍️`. |
98
98
  | `⚡` | Producer-owned Control marker | Control queue rows | Not a positive-reaction alias or command classifier; old stored Priority badges project `👍` without changing lane authority. |
99
- | `❤` / `❤️` | Promote to priority | Queue reaction shortcut | Normalize display consistently where code normalizes reactions. |
100
- | `🕊` / `🕊️` | Promote to priority | Queue reaction shortcut | Soft/peaceful promotion gesture. |
101
- | `🔥` | Select Steer | Queue reaction shortcut and Steer badge | Dominates `👍`, heart and dove; not immediate issuance or model-use proof. |
99
+ | `🔥` | Select Steer | Queue reaction shortcut and Steer badge | Dominates `👍`; not immediate issuance or model-use proof. |
102
100
  | `👎` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible until the marked turn reaches dispatch; not negative feedback to the agent. |
103
- | `👻` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Disappear/remove metaphor. |
104
- | `💔` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible cancel metaphor. |
105
- | `💩` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible reject metaphor. |
106
- | `🗑` | Defer removal | Queue reaction shortcut and queue emoji marker | Like Skip, the reaction remains reversible until dispatch reaches the marked turn. |
101
+
102
+ Only `👍`, `👎`, and `🔥` are queue reaction shortcuts. Every other reaction is decorative, including the retired aliases. Retained queue state uses canonical badges without changing lane, disposition or receipt authority.
107
103
 
108
104
  ### Decorative Or Local-Example Emoji
109
105
 
@@ -42,7 +42,6 @@ import {
42
42
  sendTelegramBusLocalEnvelope,
43
43
  TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
44
44
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
45
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
46
45
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
47
46
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
48
47
  TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
@@ -52,6 +51,7 @@ import {
52
51
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
53
52
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
54
53
  TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
54
+ getTelegramBusHeldCommandCapabilities,
55
55
  TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
56
56
  TelegramBusLocalAuthorityError,
57
57
  type TelegramBusAgentMessage,
@@ -178,6 +178,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
178
178
  target?: TelegramTarget;
179
179
  previousInstanceId?: string;
180
180
  restoreWorkspace?: boolean;
181
+ recoverConnection?: boolean;
181
182
  },
182
183
  ) => Promise<boolean>;
183
184
  setContext: (ctx: TContext) => void | Promise<void>;
@@ -2930,7 +2931,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
2930
2931
  deps.setActiveAuthSecret?.(undefined);
2931
2932
  await deps.stopReceiving?.();
2932
2933
  };
2933
- const pendingHandoff = options
2934
+ const pendingHandoff = options && !options.recoverConnection
2934
2935
  ? undefined
2935
2936
  : getTelegramFollowerSessionHandoff();
2936
2937
  const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(
@@ -2946,8 +2947,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
2946
2947
  target?: TelegramTarget;
2947
2948
  previousInstanceId?: string;
2948
2949
  restoreWorkspace?: boolean;
2950
+ recoverConnection?: boolean;
2949
2951
  }
2950
- | undefined = options ?? pendingHandoffOptions;
2952
+ | undefined = options ? { ...pendingHandoffOptions, ...options } : pendingHandoffOptions;
2951
2953
  const leaderSocketPath =
2952
2954
  leader.busSocketPath ??
2953
2955
  deps.getLeaderSocketPath?.() ??
@@ -2968,6 +2970,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
2968
2970
  : "follower.register",
2969
2971
  requestId: registrationGeneration,
2970
2972
  auth: activeAuthSecret,
2973
+ ...(registrationOptions?.recoverConnection && !registrationOptions.restoreWorkspace
2974
+ ? { recoverConnection: true as const } : {}),
2971
2975
  registration: {
2972
2976
  instanceId: deps.instanceId,
2973
2977
  ...(registrationOptions?.previousInstanceId
@@ -4546,8 +4550,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
4546
4550
  ? [
4547
4551
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
4548
4552
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
4549
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
4550
- TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
4553
+ ...getTelegramBusHeldCommandCapabilities(commandInput.name),
4551
4554
  ]
4552
4555
  : []),
4553
4556
  ];
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
+ TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
26
27
  hasTelegramBusSteerQueueHandoffCapability,
27
28
  TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
28
29
  TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
@@ -1529,7 +1530,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1529
1530
  deps: TelegramBusFollowerTargetProvisionerDeps,
1530
1531
  ): (
1531
1532
  registration: TelegramBusInstanceRegistration,
1532
- options?: { existingWorkspaceBindingOnly?: boolean },
1533
+ options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
1533
1534
  ) => Promise<
1534
1535
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
1535
1536
  > {
@@ -1548,6 +1549,16 @@ export function createTelegramBusFollowerTargetProvisioner(
1548
1549
  const chatId = deps.getAllowedUserId();
1549
1550
  if (typeof chatId !== "number") return registration.target;
1550
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
+ }
1551
1562
  let restoredPredecessor: Threads.TelegramTopicTargetRecord | undefined;
1552
1563
  if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
1553
1564
  !registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
@@ -1580,6 +1591,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1580
1591
  {
1581
1592
  existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
1582
1593
  sessionId: registration.sessionId,
1594
+ provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
1583
1595
  onCapacityUnavailable() {
1584
1596
  capacityUnavailable = true;
1585
1597
  },
@@ -1842,12 +1854,6 @@ export function createTelegramBusFollowerTargetProvisioner(
1842
1854
  };
1843
1855
  }
1844
1856
  const probeRequiredRecord = reconnectRecord?.status === "probe-required";
1845
- const exactSessionHandoff =
1846
- crossSessionReuse &&
1847
- !!requestedTarget &&
1848
- registration.previousInstanceId === reconnectRecord?.instanceId &&
1849
- requestedTarget.chatId === reconnectRecord.target.chatId &&
1850
- requestedTarget.threadId === reconnectRecord.target.threadId;
1851
1857
  const requiresVisibilityProbe =
1852
1858
  crossSessionReuse ||
1853
1859
  probeRequiredRecord ||
@@ -1869,19 +1875,14 @@ export function createTelegramBusFollowerTargetProvisioner(
1869
1875
  try {
1870
1876
  assertRestoreRegistration();
1871
1877
  await deps.callApi(
1872
- exactSessionHandoff ? "sendChatAction" : "sendMessage",
1873
- exactSessionHandoff
1874
- ? {
1875
- chat_id: connectedAnnouncement.target.chatId,
1876
- message_thread_id: connectedAnnouncement.target.threadId,
1877
- action: "typing",
1878
- }
1879
- : {
1880
- chat_id: connectedAnnouncement.target.chatId,
1881
- message_thread_id: connectedAnnouncement.target.threadId,
1882
- text: connectedAnnouncement.text,
1883
- parse_mode: connectedAnnouncement.parseMode,
1884
- },
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
+ },
1885
1886
  );
1886
1887
  assertRestoreRegistration();
1887
1888
  if (recoverableTarget || probeRequiredRecord) {
@@ -2944,7 +2945,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
2944
2945
  ) => Promise<unknown> | unknown;
2945
2946
  provisionFollowerTarget?: (
2946
2947
  registration: TelegramBusInstanceRegistration,
2947
- options?: { existingWorkspaceBindingOnly?: boolean },
2948
+ options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
2948
2949
  ) =>
2949
2950
  | Promise<
2950
2951
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
@@ -3453,6 +3454,9 @@ export function createTelegramBusLeaderEnvelopeHandler(
3453
3454
  ));
3454
3455
  const restoringWorkspace =
3455
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));
3456
3460
  const supportsWorkspaceAutoConnect =
3457
3461
  hasTelegramBusCapability(
3458
3462
  deps.protocolIdentity,
@@ -3468,7 +3472,8 @@ export function createTelegramBusLeaderEnvelopeHandler(
3468
3472
  !readerCompatible() ||
3469
3473
  (envelope.registration.cwd !== undefined &&
3470
3474
  envelope.registration.sessionId === undefined) ||
3471
- (restoringWorkspace && !supportsWorkspaceAutoConnect)
3475
+ (restoringWorkspace && !supportsWorkspaceAutoConnect) ||
3476
+ (recoverConnection && !supportsConnectionRecovery)
3472
3477
  ) {
3473
3478
  return {
3474
3479
  kind: "bus.ack" as const,
@@ -3526,7 +3531,7 @@ export function createTelegramBusLeaderEnvelopeHandler(
3526
3531
  );
3527
3532
  const target = await deps.provisionFollowerTarget?.(
3528
3533
  envelope.registration,
3529
- { existingWorkspaceBindingOnly: restoringWorkspace },
3534
+ { existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) },
3530
3535
  );
3531
3536
  if (restoringWorkspace && !target) {
3532
3537
  return {
package/lib/bus.ts CHANGED
@@ -144,6 +144,7 @@ export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
144
144
  "queue-handoff-v1" as const;
145
145
  /** Unspent Steer lane codec; independent from baseline receipt handoff support. */
146
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;
147
148
  export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
148
149
  "input-custody-reference-v1" as const;
149
150
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
@@ -184,6 +185,14 @@ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
184
185
  TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
185
186
  ] as const;
186
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
+
187
196
  export interface TelegramBusSelectedCommandInput {
188
197
  name: string;
189
198
  target: TelegramTarget & { threadId: number };
@@ -1009,6 +1018,7 @@ export type TelegramBusEnvelope = (
1009
1018
  | {
1010
1019
  kind: "follower.register";
1011
1020
  requestId: string;
1021
+ recoverConnection?: true;
1012
1022
  registration: TelegramBusInstanceRegistration;
1013
1023
  }
1014
1024
  | {
@@ -2539,12 +2549,7 @@ export function createTelegramBusLiveRebindController(deps: {
2539
2549
  : []),
2540
2550
  ...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
2541
2551
  ...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
2542
- ...(held
2543
- ? [
2544
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
2545
- TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
2546
- ]
2547
- : []),
2552
+ ...(held ? getTelegramBusHeldCommandCapabilities(held.name) : []),
2548
2553
  ];
2549
2554
  const follower = deps.getFollower(expected.instanceId),
2550
2555
  secret = deps.getAuthSecret();
@@ -3545,8 +3550,12 @@ function parseRegisterEnvelope(
3545
3550
  requestId: string,
3546
3551
  kind: "follower.register" | "follower.restoreWorkspace",
3547
3552
  ): TelegramBusEnvelope | undefined {
3553
+ if (value.recoverConnection !== undefined &&
3554
+ (kind !== "follower.register" || value.recoverConnection !== true)) return undefined;
3548
3555
  const registration = parseRegistration(value.registration);
3549
- 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;
3550
3559
  }
3551
3560
 
3552
3561
  function parseFollowerPresenceEnvelope(
package/lib/commands.ts CHANGED
@@ -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",
package/lib/extension.ts CHANGED
@@ -65,6 +65,8 @@ const telegramBusProtocolIdentity =
65
65
  Bus.TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
66
66
  Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
67
67
  Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_STEER,
68
+ Bus.TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
69
+ Bus.TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES,
68
70
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
69
71
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
70
72
  Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
@@ -1238,7 +1240,7 @@ export default function (pi: Pi.ExtensionAPI) {
1238
1240
  follower.protocol,
1239
1241
  [
1240
1242
  ...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
1241
- ...Bus.TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
1243
+ ...Bus.getTelegramBusHeldCommandCapabilities(name),
1242
1244
  ],
1243
1245
  ) &&
1244
1246
  typeof selectedMenuDelivery === "function" &&
@@ -1559,6 +1561,7 @@ export default function (pi: Pi.ExtensionAPI) {
1559
1561
  threadStore,
1560
1562
  runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
1561
1563
  liveRebindCleanupSchedule: Routing.TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE,
1564
+ liveRebindCommandCompletionTimeoutMs: 8_000,
1562
1565
  updateStatus,
1563
1566
  isContextActive: telegramSessionContextStore.isCurrent,
1564
1567
  dispatchNextQueuedTelegramTurn,
package/lib/locks.ts CHANGED
@@ -1981,6 +1981,7 @@ export interface TelegramLockedPollingRuntime<
1981
1981
  registerFollowerWithOwner?: (
1982
1982
  ctx: TContext,
1983
1983
  owner: TelegramLockEntry,
1984
+ options?: { recoverConnection?: boolean },
1984
1985
  ) => boolean | undefined | Promise<boolean | undefined>;
1985
1986
  restoreFollowerWithOwner?: (
1986
1987
  ctx: TContext,
@@ -2008,6 +2009,7 @@ interface TelegramLockedPollingRuntimeDeps<
2008
2009
  registerFollowerWithOwner?: (
2009
2010
  ctx: TContext,
2010
2011
  owner: TelegramLockEntry,
2012
+ options?: { recoverConnection?: boolean },
2011
2013
  ) => boolean | undefined | Promise<boolean | undefined>;
2012
2014
  restoreFollowerWithOwner?: (
2013
2015
  ctx: TContext,
@@ -2294,6 +2296,7 @@ export function createTelegramLockedPollingRuntime<
2294
2296
  const registered = await deps.registerFollowerWithOwner(
2295
2297
  ctx,
2296
2298
  acquired.lock,
2299
+ options.forceFreshLeaderThread ? { recoverConnection: true } : undefined,
2297
2300
  );
2298
2301
  if (!isCurrent()) return cancelled;
2299
2302
  if (registered) {
@@ -2518,8 +2521,8 @@ export function createTelegramLockedPollingRuntime<
2518
2521
  sessionAutoStartRun = run;
2519
2522
  },
2520
2523
  registerFollowerWithOwner: deps.registerFollowerWithOwner
2521
- ? async (ctx, owner) => {
2522
- const registered = await deps.registerFollowerWithOwner?.(ctx, owner);
2524
+ ? async (ctx, owner, options) => {
2525
+ const registered = await deps.registerFollowerWithOwner?.(ctx, owner, options);
2523
2526
  if (registered) deps.updateStatus(ctx);
2524
2527
  return registered === true;
2525
2528
  }
package/lib/menu-queue.ts CHANGED
@@ -115,7 +115,7 @@ function buildTelegramQueueMenuReplyMarkup(
115
115
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
116
116
  const rows = items.map((item) => {
117
117
  const badge = getTelegramQueueMenuLaneBadge(item);
118
- const prefix = item.reactionSuppressionEmoji ? `${item.reactionSuppressionEmoji} ` :
118
+ const prefix = item.reactionSuppressionEmoji ? "👎 " :
119
119
  badge ? `${badge} ` : item.hasAttachments ? "📎 " : "";
120
120
  const position = item.reactionSuppressionEmoji
121
121
  ? formatSkippedTelegramQueuePosition(item.queuePosition)
@@ -179,16 +179,15 @@ function escapeTelegramQueueMenuHtmlPreview(text: string): string {
179
179
  return truncated ? escaped + suffix : escaped;
180
180
  }
181
181
 
182
- function getTelegramQueueMenuLaneBadge(item: Pick<TelegramQueueMenuItem, "queueLane" | "priorityEmoji">): string {
182
+ function getTelegramQueueMenuLaneBadge(item: Pick<TelegramQueueMenuItem, "queueLane">): string {
183
183
  if (item.queueLane === "control") return "⚡";
184
184
  if (item.queueLane === "steer") return "🔥";
185
185
  if (item.queueLane !== "priority") return "";
186
- const emoji = item.priorityEmoji?.replace(/\uFE0F/g, "");
187
- return emoji === "❤" || emoji === "🕊" ? emoji : "👍";
186
+ return "👍";
188
187
  }
189
188
 
190
189
  function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
191
- const marker = item.reactionSuppressionEmoji ?? getTelegramQueueMenuLaneBadge(item);
190
+ const marker = item.reactionSuppressionEmoji ? "👎" : getTelegramQueueMenuLaneBadge(item);
192
191
  const badge = marker ? ` ${marker}` : "";
193
192
  const position = item.reactionSuppressionEmoji
194
193
  ? `<s>${item.queuePosition}</s>.`
@@ -920,10 +919,9 @@ function applyQueuedTelegramPromptMenuFlags<Context>(
920
919
  const disposition: Extract<Queue.TelegramQueueReactionDisposition, { kind: "reaction-transition" }> = { kind: "reaction-transition" };
921
920
  if (flags.lane !== undefined) {
922
921
  disposition.promptLane = flags.lane;
923
- disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ?
924
- getTelegramQueueMenuLaneBadge({ queueLane: "priority", priorityEmoji: item.queueLane === "priority" ? item.priorityEmoji : undefined }) : null;
922
+ disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ? "👍" : null;
925
923
  }
926
- if (flags.skipped !== undefined) disposition.suppressionEmoji = flags.skipped ? (item.reactionSuppressionEmoji ?? "👎") : null;
924
+ if (flags.skipped !== undefined) disposition.suppressionEmoji = flags.skipped ? "👎" : null;
927
925
  return deps.queueMutationRuntime.applyReactionByMessageId(replyToMessageId, disposition, ctx) ||
928
926
  ((flags.lane === undefined || flags.lane === item.queueLane) &&
929
927
  (flags.skipped === undefined || flags.skipped === (item.reactionSuppressionEmoji !== undefined)));
package/lib/polling.ts CHANGED
@@ -757,6 +757,7 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
757
757
  registerFollowerWithOwner: (
758
758
  ctx: TContext,
759
759
  owner: TOwner,
760
+ options?: { recoverConnection?: boolean },
760
761
  ) => Promise<boolean | undefined>;
761
762
  restoreFollowerWithOwner: (
762
763
  ctx: TContext,
@@ -776,6 +777,7 @@ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
776
777
  registerFollowerWithLeader: (
777
778
  ctx: TContext,
778
779
  owner: TOwner,
780
+ options?: { recoverConnection?: boolean },
779
781
  ) => Promise<boolean | undefined>;
780
782
  restoreFollowerWithLeader?: (
781
783
  ctx: TContext,
@@ -1180,9 +1182,10 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1180
1182
  const registerFollowerWithOwner = async (
1181
1183
  ctx: TContext,
1182
1184
  owner: TOwner,
1185
+ options?: { recoverConnection?: boolean },
1183
1186
  ): Promise<boolean | undefined> => {
1184
1187
  if (!(await refreshFollowerState())) return undefined;
1185
- return deps.registerFollowerWithLeader(ctx, owner);
1188
+ return deps.registerFollowerWithLeader(ctx, owner, options);
1186
1189
  };
1187
1190
  const restoreFollowerWithOwner = async (
1188
1191
  ctx: TContext,
package/lib/routing.ts CHANGED
@@ -916,6 +916,16 @@ export function createTelegramLiveRebindCoordinator(input: {
916
916
  }
917
917
  : undefined;
918
918
  },
919
+ /** Observe only an already-issued leader command; never prepares, relocates or executes input. */
920
+ observeCommandCompletion(): "unknown" | "released" | undefined {
921
+ if (!continuationIssued || !leader?.complete) return undefined;
922
+ if (!current()) return "unknown";
923
+ try { return observeCommandCompletion(); }
924
+ catch (error) {
925
+ input.recordRuntimeEvent?.("telegram", error, { phase: "live-rebind-command-observation" });
926
+ return "unknown";
927
+ }
928
+ },
919
929
  async advance(): Promise<"protected" | "unknown" | "released"> {
920
930
  if (advancing || !current()) return "protected";
921
931
  if (released) return "released";
@@ -1561,6 +1571,8 @@ export interface TelegramInboundRouteRuntimeDeps<
1561
1571
  /** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
1562
1572
  temporaryThreadCleanupDelayMs?: number;
1563
1573
  /** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
1574
+ /** Bounded ACK observation inside the original chooser admission, independent from menu/reply delivery. */
1575
+ liveRebindCommandCompletionTimeoutMs?: number;
1564
1576
  liveRebindCleanupSchedule?: {
1565
1577
  delaysMs: readonly number[];
1566
1578
  intervalMs: number;
@@ -1937,6 +1949,7 @@ export function createTelegramInboundRouteRuntime<
1937
1949
  extensionCommand?: Commands.ParsedTelegramCommand;
1938
1950
  extensionKind?: Commands.PreparedSelectedCommand["kind"];
1939
1951
  coordinator?: ReturnType<typeof createTelegramLiveRebindCoordinator>;
1952
+ commandCompletionWaited?: true;
1940
1953
  isCurrent?: () => boolean;
1941
1954
  };
1942
1955
  /** The acknowledged tab presenting an All source; `sourceTarget` is this tab, not the original's location. */
@@ -4870,7 +4883,7 @@ export function createTelegramInboundRouteRuntime<
4870
4883
  Bus.isSameTelegramBusFollowerRegistration(live, follower!) &&
4871
4884
  [
4872
4885
  ...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
4873
- ...(heldSelected ? Bus.TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES : []),
4886
+ ...(heldSelected ? Bus.getTelegramBusHeldCommandCapabilities(heldSelected.name) : []),
4874
4887
  ].every((cap) => Bus.hasTelegramBusCapability(live.protocol, cap)) &&
4875
4888
  !!live.target &&
4876
4889
  live.target.chatId === binding.target.chatId &&
@@ -5642,6 +5655,7 @@ export function createTelegramInboundRouteRuntime<
5642
5655
  return {
5643
5656
  operationId: request.operationId,
5644
5657
  selectedCommandReference: () => coordinator?.selectedCommandReference(),
5658
+ observeCommandCompletion: () => coordinator?.observeCommandCompletion(),
5645
5659
  async advance() {
5646
5660
  if (!coordinator) {
5647
5661
  if (!current() || !getRecipient()) return "protected";
@@ -9058,6 +9072,42 @@ export function createTelegramInboundRouteRuntime<
9058
9072
  if (!(await selectPendingDestination())) return true;
9059
9073
  const operationId = selection.coordinator.operationId;
9060
9074
  if (current()) outcome = await selection.coordinator.advance();
9075
+ const completionTimeoutMs = deps.liveRebindCommandCompletionTimeoutMs ?? 0;
9076
+ if (outcome === "unknown" && !selection.commandCompletionWaited && selection.record.owner?.kind === "leader" &&
9077
+ (selection.helpCommand || selection.menuCommand || selection.abortCommand || selection.nextCommand || selection.stopCommand) &&
9078
+ Number.isFinite(completionTimeoutMs) && completionTimeoutMs > 0 &&
9079
+ selection.coordinator.observeCommandCompletion() !== undefined) {
9080
+ selection.commandCompletionWaited = true;
9081
+ const coordinator = selection.coordinator;
9082
+ const signal = Updates.getTelegramUpdateExecutionFence(query)?.signal;
9083
+ outcome = await new Promise<"unknown" | "released">((resolve) => {
9084
+ let settled = false;
9085
+ const subscriptions: (() => void)[] = [];
9086
+ const finish = (result: "unknown" | "released") => {
9087
+ if (settled) return;
9088
+ settled = true;
9089
+ clearTimeout(timer);
9090
+ for (const unsubscribe of subscriptions) unsubscribe();
9091
+ signal?.removeEventListener("abort", abort);
9092
+ resolve(result);
9093
+ };
9094
+ const abort = () => finish("unknown");
9095
+ const observe = () => {
9096
+ if (settled) return;
9097
+ if (!current()) { finish("unknown"); return; }
9098
+ if (coordinator.observeCommandCompletion() === "released") finish("released");
9099
+ };
9100
+ const timer = setTimeout(abort, completionTimeoutMs);
9101
+ timer.unref?.();
9102
+ for (const message of selection.messages) {
9103
+ const unsubscribe = Updates.subscribeTelegramDeferredSourceCompletion(message, observe);
9104
+ if (unsubscribe) subscriptions.push(unsubscribe);
9105
+ }
9106
+ signal?.addEventListener("abort", abort, { once: true });
9107
+ if (signal?.aborted || !current()) abort();
9108
+ else if (coordinator.observeCommandCompletion() === "released") finish("released");
9109
+ });
9110
+ }
9061
9111
  // Cleanup readiness follows the canonical released row, not source disposal: a detached command ACK may
9062
9112
  // still be pending while every attempt samples current work afresh.
9063
9113
  const row =
package/lib/sync.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  isSameTelegramProcessInstance,
19
19
  isTelegramTopicTargetStaleError,
20
20
  provisionOwnBusTopic,
21
+ resetTelegramConnectionProvisioning,
21
22
  type TelegramOwnTopicProvisionResult,
22
23
  type TelegramTopicTargetStore,
23
24
  type TelegramWorkspaceDisplayBinding,
@@ -826,6 +827,15 @@ export async function ensureTelegramLeaderThreadBinding(
826
827
  ...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
827
828
  };
828
829
  const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
830
+ if (deps.forceFreshUnnamed) {
831
+ await resetTelegramConnectionProvisioning({
832
+ store: deps.topicTargetStore, owner: "leader", instanceId: deps.instanceId,
833
+ profileKey: leaderProfileKey, isCurrent: () => {
834
+ assertLeaderEpoch("connect-recovery");
835
+ return true;
836
+ }, recordEvent: deps.recordEvent,
837
+ });
838
+ }
829
839
  const legacyLeaderRecord =
830
840
  deps.topicTargetStore.getByProfileKey(leaderProfileKey);
831
841
  let capacityUnavailable = false;
@@ -836,6 +846,7 @@ export async function ensureTelegramLeaderThreadBinding(
836
846
  legacyLeaderRecord?.instanceId,
837
847
  {
838
848
  sessionId: deps.sessionId,
849
+ provisionProfileKey: leaderProfileKey,
839
850
  onCapacityUnavailable() {
840
851
  capacityUnavailable = true;
841
852
  },