@llblab/pi-telegram 0.56.0 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/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
  },
package/lib/threads.ts CHANGED
@@ -783,6 +783,7 @@ export interface TelegramTopicTargetStore {
783
783
  ) => { slot?: string; threadName?: string } | undefined;
784
784
  listReservations: () => TelegramThreadReservation[];
785
785
  listPendingProvisions: () => TelegramThreadPendingProvision[];
786
+ resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
786
787
  listPendingCleanups: () => TelegramThreadCleanupIntent[];
787
788
  listSyncObservations: () => TelegramTopicSyncObservation[];
788
789
  reserveThread: (reservation: TelegramThreadReservation) => void;
@@ -902,6 +903,7 @@ export interface TelegramTopicTargetStore {
902
903
  options?: {
903
904
  existingBindingOnly?: boolean;
904
905
  sessionId?: string;
906
+ provisionProfileKey?: string;
905
907
  onCapacityUnavailable?: () => void;
906
908
  },
907
909
  ) => TelegramWorkspaceBindingIdentity | undefined;
@@ -4168,6 +4170,7 @@ export function createTelegramTopicTargetStore(
4168
4170
  }
4169
4171
  ),
4170
4172
  isCurrentPublication?: () => boolean,
4173
+ resetProvisions?: readonly TelegramThreadPendingProvision[],
4171
4174
  ): Promise<boolean> => {
4172
4175
  const submittedPath = getPath(),
4173
4176
  submittedProfile = activeProfile();
@@ -4367,6 +4370,13 @@ export function createTelegramTopicTargetStore(
4367
4370
  !!expectedRestore,
4368
4371
  ),
4369
4372
  );
4373
+ if (resetProvisions) {
4374
+ const recoveries = readProvisionRecoveries(path, true);
4375
+ if (mutationRevision !== persistedRevision || resetProvisions.some(expected =>
4376
+ expected.target || Object.hasOwn(recoveries, expected.id) ||
4377
+ !isDeepStrictEqual(disk.pendingProvisions?.find(value => value.id === expected.id), expected)))
4378
+ throw new Error("Telegram connection recovery provisioning evidence changed.");
4379
+ }
4370
4380
  if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
4371
4381
  throw new Error(
4372
4382
  "Workspace Restore evidence changed before publication.",
@@ -4429,7 +4439,7 @@ export function createTelegramTopicTargetStore(
4429
4439
  }
4430
4440
  : {}),
4431
4441
  reservations: reservations.map((reservation) => ({ ...reservation })),
4432
- pendingProvisions: pendingProvisions.map((provision) => ({
4442
+ pendingProvisions: pendingProvisions.filter(provision => !resetProvisions?.some(expected => expected.id === provision.id)).map((provision) => ({
4433
4443
  ...provision,
4434
4444
  ...(provision.target ? { target: { ...provision.target } } : {}),
4435
4445
  })),
@@ -4636,6 +4646,7 @@ export function createTelegramTopicTargetStore(
4636
4646
  loadedProfile = submittedProfile;
4637
4647
  loaded = true;
4638
4648
  observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
4649
+ if (resetProvisions) { pendingProvisions = file.pendingProvisions; mutationRevision += 1; }
4639
4650
  if (transition) {
4640
4651
  records = new Map(
4641
4652
  file.threads.map((record) => {
@@ -4808,6 +4819,10 @@ export function createTelegramTopicTargetStore(
4808
4819
  if (isCurrentPublication?.() === false) return false;
4809
4820
  loadedPath = path;
4810
4821
  loaded = true;
4822
+ if (resetProvisions) {
4823
+ if (mutationRevision !== persistedRevision) return false;
4824
+ pendingProvisions = file.pendingProvisions;
4825
+ }
4811
4826
  if (mutationRevision === persistedRevision) dirty = false;
4812
4827
  return true;
4813
4828
  });
@@ -6814,6 +6829,11 @@ export function createTelegramTopicTargetStore(
6814
6829
  current.status = "ambiguous";
6815
6830
  return true;
6816
6831
  },
6832
+ resetPendingProvisionForConnect(expected, isCurrent) {
6833
+ // Stage no in-memory deletion: refusal or a stale publication must keep its protective hold.
6834
+ if (expected.target || !isCurrent()) return Promise.resolve(false);
6835
+ return persistSnapshot(undefined, isCurrent, [structuredClone(expected)]);
6836
+ },
6817
6837
  removePendingProvision(id) {
6818
6838
  const before = pendingProvisions.length;
6819
6839
  pendingProvisions = pendingProvisions.filter(
@@ -7608,8 +7628,14 @@ export function createTelegramTopicTargetStore(
7608
7628
  ) {
7609
7629
  return undefined;
7610
7630
  }
7611
- const retainedTarget = binding?.target ?? legacyRecord?.target;
7612
- const retainedSlot = binding?.slot ?? legacyRecord?.slot;
7631
+ const ownedProvisions = pendingProvisions.filter(value =>
7632
+ value.workspaceBindingKey === identity.bindingKey &&
7633
+ (value.instanceId === instanceId || value.instanceId === effectivePreviousInstanceId ||
7634
+ (options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
7635
+ if (ownedProvisions.length > 1) return undefined;
7636
+ const ownProvision = ownedProvisions[0];
7637
+ const retainedTarget = ownProvision?.target ?? binding?.target ?? legacyRecord?.target;
7638
+ const retainedSlot = ownProvision?.slot ?? binding?.slot ?? legacyRecord?.slot;
7613
7639
  const otherBindings = Array.from(workspaceBindings.values())
7614
7640
  .filter(
7615
7641
  (other) => other.bindingKey !== identity.bindingKey && other.slot,
@@ -7641,7 +7667,7 @@ export function createTelegramTopicTargetStore(
7641
7667
  )
7642
7668
  .map((reservation) => reservation.slot),
7643
7669
  ...pendingProvisions
7644
- .filter((provision) =>
7670
+ .filter((provision) => provision !== ownProvision &&
7645
7671
  isPendingProvisionLiveOrTargeted(provision, nowMs),
7646
7672
  )
7647
7673
  .map((provision) => provision.slot),
@@ -9661,6 +9687,32 @@ export function createTelegramTopicTargetRenamer(
9661
9687
  };
9662
9688
  }
9663
9689
 
9690
+ /** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
9691
+ export async function resetTelegramConnectionProvisioning(input: {
9692
+ store: TelegramTopicTargetStore;
9693
+ owner: TelegramThreadPendingProvision["owner"];
9694
+ instanceId: string;
9695
+ profileKey: string;
9696
+ isCurrent: () => boolean;
9697
+ recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
9698
+ }): Promise<void> {
9699
+ if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
9700
+ const removed: TelegramThreadPendingProvision[] = [];
9701
+ for (const pending of input.store.listPendingProvisions()) {
9702
+ if (pending.owner !== input.owner || pending.target ||
9703
+ (pending.profileKey !== input.profileKey && pending.instanceId !== input.instanceId)) continue;
9704
+ if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
9705
+ if (!await input.store.resetPendingProvisionForConnect(pending, input.isCurrent))
9706
+ throw new Error("Telegram connection recovery lost publication authority.");
9707
+ if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
9708
+ removed.push(pending);
9709
+ }
9710
+ if (removed.length === 0) return;
9711
+ input.recordEvent("connection", "Explicit connect reset unfinished Thread creation; an unconfirmed Telegram Thread may remain.", {
9712
+ phase: "connect-provision-reset", count: removed.length, owner: input.owner,
9713
+ });
9714
+ }
9715
+
9664
9716
  export function createTelegramTopicTargetProvisioner(
9665
9717
  deps: TelegramTopicTargetProvisionerDeps,
9666
9718
  ): (
@@ -9923,7 +9975,7 @@ export function createTelegramTopicTargetProvisioner(
9923
9975
  slot,
9924
9976
  );
9925
9977
  }
9926
- const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
9978
+ const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}:${randomUUID()}`;
9927
9979
  const pendingOwner =
9928
9980
  request.owner?.kind === "leader" ? "leader" : "manual-follower";
9929
9981
  assertLeaderEpoch("before-pending-intent");