@llblab/pi-telegram 0.54.4 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/AGENTS.md +4 -2
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +5 -3
  5. package/dist/lib/activity.d.ts +5 -1
  6. package/dist/lib/activity.js +31 -2
  7. package/dist/lib/bindings.d.ts +3 -1
  8. package/dist/lib/bindings.js +8 -2
  9. package/dist/lib/bus-api.d.ts +2 -2
  10. package/dist/lib/bus-api.js +7 -2
  11. package/dist/lib/bus-follower.d.ts +49 -3
  12. package/dist/lib/bus-follower.js +313 -146
  13. package/dist/lib/bus-leader.d.ts +3 -0
  14. package/dist/lib/bus-leader.js +99 -18
  15. package/dist/lib/bus.d.ts +5 -3
  16. package/dist/lib/bus.js +13 -10
  17. package/dist/lib/delivery.d.ts +4 -0
  18. package/dist/lib/delivery.js +48 -15
  19. package/dist/lib/extension.js +48 -6
  20. package/dist/lib/lifecycle.d.ts +68 -0
  21. package/dist/lib/lifecycle.js +178 -58
  22. package/dist/lib/machine.d.ts +23 -0
  23. package/dist/lib/machine.js +37 -0
  24. package/dist/lib/media.d.ts +114 -28
  25. package/dist/lib/media.js +292 -119
  26. package/dist/lib/model.d.ts +54 -15
  27. package/dist/lib/model.js +98 -53
  28. package/dist/lib/polling.js +2 -0
  29. package/dist/lib/preview.d.ts +128 -113
  30. package/dist/lib/preview.js +448 -366
  31. package/dist/lib/queue.d.ts +2 -2
  32. package/dist/lib/queue.js +3 -3
  33. package/dist/lib/routing.d.ts +6 -1
  34. package/dist/lib/routing.js +334 -210
  35. package/dist/lib/runtime.d.ts +77 -11
  36. package/dist/lib/runtime.js +174 -92
  37. package/dist/lib/sections.js +0 -1
  38. package/dist/lib/telegram-api.d.ts +11 -9
  39. package/dist/lib/telegram-api.js +53 -4
  40. package/dist/lib/text-groups.d.ts +0 -26
  41. package/dist/lib/text-groups.js +51 -98
  42. package/dist/lib/thread-display.js +14 -6
  43. package/dist/lib/threads.d.ts +18 -5
  44. package/dist/lib/threads.js +107 -30
  45. package/dist/lib/turns.d.ts +2 -0
  46. package/dist/lib/turns.js +6 -1
  47. package/dist/lib/updates.d.ts +11 -4
  48. package/dist/lib/updates.js +54 -19
  49. package/dist/package.json +1 -1
  50. package/docs/activity.md +1 -1
  51. package/docs/architecture.md +12 -11
  52. package/docs/delivery.md +7 -5
  53. package/docs/multi-instance-bus.md +18 -8
  54. package/docs/public-api.md +2 -2
  55. package/docs/ui-style.md +2 -2
  56. package/lib/activity.ts +29 -5
  57. package/lib/bindings.ts +9 -1
  58. package/lib/bus-api.ts +11 -3
  59. package/lib/bus-follower.ts +417 -203
  60. package/lib/bus-leader.ts +88 -14
  61. package/lib/bus.ts +17 -15
  62. package/lib/delivery.ts +47 -19
  63. package/lib/extension.ts +51 -9
  64. package/lib/lifecycle.ts +229 -55
  65. package/lib/machine.ts +60 -0
  66. package/lib/media.ts +438 -172
  67. package/lib/model.ts +169 -74
  68. package/lib/polling.ts +2 -0
  69. package/lib/preview.ts +715 -566
  70. package/lib/queue.ts +5 -5
  71. package/lib/routing.ts +325 -238
  72. package/lib/runtime.ts +260 -104
  73. package/lib/sections.ts +0 -2
  74. package/lib/telegram-api.ts +61 -8
  75. package/lib/text-groups.ts +75 -136
  76. package/lib/thread-display.ts +15 -4
  77. package/lib/threads.ts +120 -36
  78. package/lib/turns.ts +8 -1
  79. package/lib/updates.ts +58 -21
  80. package/package.json +1 -1
@@ -63,6 +63,8 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
63
63
  }
64
64
  export interface TelegramBusFollowerTargetProvisionerDeps {
65
65
  getAllowedUserId: () => number | undefined;
66
+ getLiveFollowers?: () => readonly TelegramBusFollowerView[];
67
+ isFollowerProcessAlive?: (pid: number) => boolean;
66
68
  topicTargetStore: Threads.TelegramTopicTargetStore;
67
69
  callApi: <TResponse>(method: string, body: Record<string, unknown>) => Promise<TResponse>;
68
70
  getCurrentLeaderEpoch?: () => number | string | undefined;
@@ -276,6 +278,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
276
278
  recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
277
279
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
278
280
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
281
+ requiresPreDispatchRebindReader?: () => boolean;
279
282
  getCurrentLeaderEpoch?: () => number | string | undefined;
280
283
  getTelegramProfile?: () => string | undefined;
281
284
  getAllowedUserId?: () => number | undefined;
@@ -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_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_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";
@@ -223,12 +223,17 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
223
223
  });
224
224
  });
225
225
  };
226
+ /** Returns the stabilization this startup owns; a stop or a newer startup meanwhile makes it inert. */
226
227
  const beginStartupDisplayStabilization = () => {
227
228
  cancelStartupDisplayStabilization();
228
229
  startupDisplaySettled = false;
230
+ return startupDisplayGeneration;
229
231
  };
230
- const armStartupDisplayReconciliation = () => {
231
- const generation = startupDisplayGeneration;
232
+ const armStartupDisplayReconciliation = (generation) => {
233
+ if (startupDisplayGeneration !== generation)
234
+ return;
235
+ if (startupDisplayTimer)
236
+ clearTimeout(startupDisplayTimer);
232
237
  const delayMs = deps.runtime.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
233
238
  startupDisplayTimer = setTimeout(() => {
234
239
  if (startupDisplayGeneration !== generation)
@@ -299,6 +304,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
299
304
  });
300
305
  const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
301
306
  ...provisionerPorts,
307
+ getLiveFollowers: deps.runtime.followerRegistry.list,
308
+ isFollowerProcessAlive: deps.runtime.isFollowerProcessAlive,
302
309
  });
303
310
  const renameLeaderThreadAdmitted = async (threadName, expectedTarget, assertRecipientAuthority) => {
304
311
  const store = deps.topicTargetStore, callApi = deps.callApi;
@@ -802,17 +809,19 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
802
809
  resetThreadNameAdmitted,
803
810
  captureWorkspaceExternalProtection: deps.captureWorkspaceExternalProtection,
804
811
  async startPolling(ctx) {
805
- if (display)
806
- beginStartupDisplayStabilization();
812
+ const stabilization = display
813
+ ? beginStartupDisplayStabilization()
814
+ : undefined;
807
815
  try {
808
816
  await runtime.startPolling(ctx);
809
817
  }
810
818
  catch (error) {
811
- cancelStartupDisplayStabilization();
819
+ if (stabilization === startupDisplayGeneration)
820
+ cancelStartupDisplayStabilization();
812
821
  throw error;
813
822
  }
814
- if (display)
815
- armStartupDisplayReconciliation();
823
+ if (stabilization !== undefined)
824
+ armStartupDisplayReconciliation(stabilization);
816
825
  },
817
826
  async stopPolling() {
818
827
  cancelStartupDisplayStabilization();
@@ -872,9 +881,40 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
872
881
  if (typeof chatId !== "number")
873
882
  return registration.target;
874
883
  await deps.topicTargetStore.load();
884
+ let restoredPredecessor;
885
+ if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
886
+ !registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
887
+ const identity = Threads.createTelegramWorkspaceBindingIdentity(registration.cwd, 0, registration.sessionId);
888
+ const records = deps.topicTargetStore.list(), liveFollowers = deps.getLiveFollowers();
889
+ const candidates = deps.topicTargetStore.listWorkspaceBindings()
890
+ .filter(binding => identity && binding.cwd === identity.cwd && binding.sessionId === identity.sessionId &&
891
+ (!registration.target || (binding.target.chatId === registration.target.chatId && binding.target.threadId === registration.target.threadId)))
892
+ .flatMap(binding => {
893
+ const matches = records.filter(record => record.status === "active" && record.owner?.kind === "manual-follower" &&
894
+ record.target.chatId === binding.target.chatId && record.target.threadId === binding.target.threadId);
895
+ return matches.length === 1 ? matches : [];
896
+ })
897
+ .filter(record => {
898
+ if (liveFollowers.some(peer => peer.instanceId === record.instanceId ||
899
+ (peer.target?.chatId === record.target.chatId && peer.target.threadId === record.target.threadId)))
900
+ return false;
901
+ // A retained status is not current execution proof. Only an absent actual runtime PID grants this claim handoff.
902
+ const match = /^([1-9][0-9]*):[0-9]+$/.exec(record.instanceId ?? ""), pid = match ? Number(match[1]) : undefined;
903
+ if (pid === undefined || !Number.isSafeInteger(pid))
904
+ return false;
905
+ try {
906
+ return deps.isFollowerProcessAlive(pid) === false;
907
+ }
908
+ catch {
909
+ return false;
910
+ }
911
+ });
912
+ if (candidates.length === 1)
913
+ restoredPredecessor = candidates[0];
914
+ }
875
915
  let capacityUnavailable = false;
876
916
  const workspaceIdentity = registration.cwd
877
- ? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId, {
917
+ ? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId ?? restoredPredecessor?.instanceId, {
878
918
  existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
879
919
  sessionId: registration.sessionId,
880
920
  onCapacityUnavailable() {
@@ -925,6 +965,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
925
965
  return (record.owner?.kind === "manual-follower" &&
926
966
  ((matchesCurrentIdentity && matchesRequestedTarget) ||
927
967
  matchesSessionHandoff ||
968
+ (record.instanceId === restoredPredecessor?.instanceId && matchesRequestedTarget) ||
928
969
  matchesWorkspaceRecovery));
929
970
  });
930
971
  const followerOwner = Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
@@ -1096,14 +1137,12 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1096
1137
  const requiresVisibilityProbe = crossSessionReuse ||
1097
1138
  probeRequiredRecord ||
1098
1139
  recoverableTarget !== undefined;
1140
+ const retainedDisplayBinding = deps.topicTargetStore.getWorkspaceBindingByTarget(result.target, registration.sessionId);
1099
1141
  let connectedAnnouncement = !result.reused || requiresVisibilityProbe
1100
1142
  ? createTelegramBusInstanceLifecycleAnnouncement({
1101
1143
  target: result.target,
1102
1144
  threadName: result.displayTitle ??
1103
- (workspaceBinding?.target.chatId === result.target.chatId &&
1104
- workspaceBinding.target.threadId === result.target.threadId
1105
- ? workspaceBinding.displayTitle
1106
- : undefined) ??
1145
+ (retainedDisplayBinding?.cwd === workspaceIdentity?.cwd ? retainedDisplayBinding?.displayTitle : undefined) ??
1107
1146
  result.record.threadName,
1108
1147
  slot: result.record.slot,
1109
1148
  state: "connected",
@@ -1242,6 +1281,16 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1242
1281
  updatedAtMs: getNowMs(),
1243
1282
  },
1244
1283
  });
1284
+ // Same-target reuse can retain a different slot than the initial claim.
1285
+ if (result.record.slot !== workspaceCommit.slot) {
1286
+ result = {
1287
+ ...result,
1288
+ record: deps.topicTargetStore.upsert({
1289
+ ...result.record,
1290
+ slot: workspaceCommit.slot,
1291
+ }),
1292
+ };
1293
+ }
1245
1294
  if (connectedAnnouncement && workspaceCommit.displayTitle) {
1246
1295
  connectedAnnouncement =
1247
1296
  createTelegramBusInstanceLifecycleAnnouncement({
@@ -1712,8 +1761,7 @@ export function createTelegramBusSelectedMenuDeliveryHandler(deps) {
1712
1761
  .listLiveRebindings()
1713
1762
  .find((value) => value.request.operationId === expected.operationId);
1714
1763
  if (!retained ||
1715
- retained.phase !== "released" ||
1716
- retained.cleanup ||
1764
+ !Threads.isTelegramLiveRebindInputAuthorized(retained) ||
1717
1765
  retained.request.source.updateIds.length !== 1 ||
1718
1766
  retained.recipient.kind !== "follower" ||
1719
1767
  !isDeepStrictEqual(retained.executor, executor) ||
@@ -1792,8 +1840,13 @@ export function createTelegramBusSelectedMenuDeliveryHandler(deps) {
1792
1840
  live.target.threadId !== recipient.target.threadId ||
1793
1841
  resolveJournal.call(workspace, live) !== recipient.journalBindingKey)
1794
1842
  throw new Error("Selected-menu authority changed.");
1843
+ const latest = store.listLiveRebindings().find(value => value.request.operationId === intent.request.operationId);
1844
+ if (!latest || !Threads.isTelegramLiveRebindInputAuthorized(latest) ||
1845
+ !isDeepStrictEqual({ ...latest, ...(intent.inputRelease ? { inputRelease: intent.inputRelease } : {}), revision: intent.revision, updatedAtMs: intent.updatedAtMs }, intent) ||
1846
+ (latest.inputRelease !== intent.inputRelease && !(intent.inputRelease === "issued" && latest.inputRelease === "accepted")))
1847
+ throw new Error("Selected-menu input grant changed.");
1795
1848
  let confirmed = false;
1796
- snapshot.call(threads, intent, (view) => {
1849
+ snapshot.call(threads, latest, (view) => {
1797
1850
  const bindings = (view.workspaceBindings ?? []).filter((value) => value.bindingKey === intent.request.binding.bindingKey);
1798
1851
  const owners = view.threads.filter((value) => value.status === "active" &&
1799
1852
  (value.slot === intent.request.binding.slot ||
@@ -2198,6 +2251,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2198
2251
  local: deps.protocolIdentity,
2199
2252
  remote: envelope.registration.protocol,
2200
2253
  });
2254
+ const readerCompatible = () => deps.requiresPreDispatchRebindReader?.() !== true ||
2255
+ hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND);
2201
2256
  const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" ||
2202
2257
  (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
2203
2258
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
@@ -2206,6 +2261,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2206
2261
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
2207
2262
  if (!compatibility.compatible ||
2208
2263
  !displayCompatible() ||
2264
+ !readerCompatible() ||
2209
2265
  (envelope.registration.cwd !== undefined &&
2210
2266
  envelope.registration.sessionId === undefined) ||
2211
2267
  (restoringWorkspace && !supportsWorkspaceAutoConnect)) {
@@ -2236,8 +2292,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2236
2292
  const leaderEpoch = deps.getCurrentLeaderEpoch?.();
2237
2293
  const telegramProfile = deps.getTelegramProfile?.();
2238
2294
  const operatorUserId = deps.getAllowedUserId?.();
2239
- const registrationCurrent = () => (!deps.getCurrentLeaderEpoch ||
2240
- deps.getCurrentLeaderEpoch() === leaderEpoch) &&
2295
+ const registrationCurrent = () => readerCompatible() &&
2296
+ (!deps.getCurrentLeaderEpoch ||
2297
+ deps.getCurrentLeaderEpoch() === leaderEpoch) &&
2241
2298
  (!deps.getTelegramProfile ||
2242
2299
  deps.getTelegramProfile() === telegramProfile) &&
2243
2300
  (!deps.getAllowedUserId ||
@@ -2955,6 +3012,7 @@ export function createTelegramBusLeaderRuntime(deps) {
2955
3012
  recordRuntimeEvent: deps.recordRuntimeEvent,
2956
3013
  applyThreadDisplayMode: deps.applyThreadDisplayMode,
2957
3014
  getThreadDisplayMode: deps.getThreadDisplayMode,
3015
+ requiresPreDispatchRebindReader: deps.requiresPreDispatchRebindReader,
2958
3016
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
2959
3017
  getTelegramProfile: deps.getTelegramProfile,
2960
3018
  getAllowedUserId: deps.getAllowedUserId,
@@ -3027,24 +3085,47 @@ export function createTelegramBusLeaderRuntime(deps) {
3027
3085
  },
3028
3086
  handleEnvelope,
3029
3087
  });
3088
+ // The start that owns the endpoint and pruning. A stop clears it; a start that loses it mid-await undoes only what
3089
+ // nobody newer owns, so a stop during startup never leaves a stopped leader's endpoint listening.
3090
+ let activeStart;
3091
+ const releaseSupersededStart = async () => {
3092
+ if (activeStart !== undefined)
3093
+ return;
3094
+ stopPruning();
3095
+ await localServer
3096
+ .stop()
3097
+ .catch((error) => deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }));
3098
+ };
3030
3099
  return {
3031
3100
  routeQueueHandoff: (envelope) => routeQueueHandoffEnvelope(envelope),
3032
3101
  startPolling: async (ctx) => {
3102
+ const start = {};
3103
+ activeStart = start;
3033
3104
  // Replay durable cleanup before publishing the follower endpoint so a
3034
3105
  // replacement registration cannot reclaim a target while it is deleted.
3035
3106
  await deps.provisionLeaderTarget?.(ctx);
3107
+ if (activeStart !== start)
3108
+ return;
3036
3109
  await localServer.start();
3110
+ if (activeStart !== start)
3111
+ return releaseSupersededStart();
3037
3112
  startPruning();
3038
3113
  try {
3039
3114
  await deps.startPolling(ctx);
3040
3115
  }
3041
3116
  catch (error) {
3117
+ if (activeStart !== start)
3118
+ return releaseSupersededStart();
3119
+ activeStart = undefined;
3042
3120
  stopPruning();
3043
3121
  await localServer.stop();
3044
3122
  throw error;
3045
3123
  }
3124
+ if (activeStart !== start)
3125
+ return releaseSupersededStart();
3046
3126
  },
3047
3127
  stopPolling: async () => {
3128
+ activeStart = undefined;
3048
3129
  stopPruning();
3049
3130
  try {
3050
3131
  await deps.stopPolling();
package/dist/lib/bus.d.ts CHANGED
@@ -43,6 +43,9 @@ export declare const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT: "director
43
43
  export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT: "workspace-follower-auto-connect-v1";
44
44
  export declare const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT: "session-replacement-intent-v1";
45
45
  export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE: "workspace-restore-v1";
46
+ /** Shared Workspace readers understand independent pre-dispatch input/cleanup state. */
47
+ export declare const TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND: "live-rebind-pre-dispatch-reader-v2";
48
+ export declare const TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION: "live-rebind-pre-dispatch-v1";
46
49
  export declare const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE: "live-thread-rebind-save-v1";
47
50
  export declare const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY: "live-thread-rebind-apply-v1";
48
51
  export declare const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE: "live-thread-rebind-settle-v1";
@@ -239,7 +242,6 @@ export declare function getTelegramFollowerTargetOwnership(input: {
239
242
  recipientBindingKey: string;
240
243
  protocolIdentity: TelegramBusProtocolIdentity;
241
244
  } | undefined;
242
- export declare function markTelegramBusAggregateDelivery<T extends Record<string, unknown>>(body: T): T;
243
245
  export declare function markTelegramBusCrossTargetDelivery<T extends Record<string, unknown>>(body: T): T;
244
246
  export declare function stripTelegramBusApiMetadata<T extends Record<string, unknown>>(body: T): T;
245
247
  export declare function isTelegramFollowerApiCallAllowed(input: {
@@ -460,7 +462,7 @@ export type TelegramBusEnvelope = ({
460
462
  recipientBindingKey: string;
461
463
  operationId: string;
462
464
  sourceUpdateIds: number[];
463
- mode: "release" | "discard" | "observe" | "observe-command";
465
+ mode: "release" | "discard" | "observe" | "observe-held" | "observe-command";
464
466
  oldTarget?: TelegramTarget & {
465
467
  threadId: number;
466
468
  };
@@ -720,7 +722,7 @@ export declare function createTelegramBusLiveRebindController(deps: {
720
722
  update_id: number;
721
723
  } & Record<string, unknown>)[];
722
724
  } | {
723
- mode: "apply" | "inspect" | "release" | "discard" | "observe" | "observe-command";
725
+ mode: "apply" | "inspect" | "release" | "discard" | "observe" | "observe-held" | "observe-command";
724
726
  sourceUpdateIds: number[];
725
727
  slot: string;
726
728
  target: TelegramTarget & {
package/dist/lib/bus.js CHANGED
@@ -57,6 +57,9 @@ export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT = "directory-displ
57
57
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT = "workspace-follower-auto-connect-v1";
58
58
  export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT = "session-replacement-intent-v1";
59
59
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE = "workspace-restore-v1";
60
+ /** Shared Workspace readers understand independent pre-dispatch input/cleanup state. */
61
+ export const TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND = "live-rebind-pre-dispatch-reader-v2";
62
+ export const TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION = "live-rebind-pre-dispatch-v1";
60
63
  export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE = "live-thread-rebind-save-v1";
61
64
  export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY = "live-thread-rebind-apply-v1";
62
65
  export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE = "live-thread-rebind-settle-v1";
@@ -270,9 +273,6 @@ function hasTelegramBusDeliveryMarker(body, marker) {
270
273
  !Array.isArray(body) &&
271
274
  body[marker] === true);
272
275
  }
273
- export function markTelegramBusAggregateDelivery(body) {
274
- return { ...body, [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true };
275
- }
276
276
  export function markTelegramBusCrossTargetDelivery(body) {
277
277
  return { ...body, [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true };
278
278
  }
@@ -375,11 +375,10 @@ export function isTelegramFollowerApiCallAllowed(input) {
375
375
  if (apiMethod === "setMyCommands")
376
376
  return isBotCommandRegistration(input.args[1]);
377
377
  if (apiMethod === "sendChatAction")
378
- return isTargetChatScoped(input.args[1]);
378
+ return isTargetScoped(input.args[1]);
379
379
  if (apiMethod === "sendMessage" &&
380
380
  hasTelegramBusDeliveryMarker(input.args[1], TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD)) {
381
- const body = input.args[1];
382
- return body.message_thread_id === undefined && isTargetChatScoped(body);
381
+ return false;
383
382
  }
384
383
  if ((apiMethod === "sendMessage" || apiMethod === "sendRichMessage") &&
385
384
  hasTelegramBusDeliveryMarker(input.args[1], TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD)) {
@@ -643,7 +642,8 @@ export function parseTelegramBusEnvelope(line) {
643
642
  ]) ||
644
643
  (Object.hasOwn(value, "preparedSource") && !selectedCommand) ||
645
644
  (value.mode === "observe-command" &&
646
- (!selectedCommand || Object.hasOwn(value, "oldTarget"))))
645
+ (!selectedCommand || Object.hasOwn(value, "oldTarget"))) ||
646
+ (value.mode === "observe-held" && (parseThreadTarget(value.oldTarget)?.threadId ?? 0) <= 0))
647
647
  break;
648
648
  if (Object.hasOwn(value, "selectedCommand") &&
649
649
  (!selectedCommand ||
@@ -660,7 +660,7 @@ export function parseTelegramBusEnvelope(line) {
660
660
  : value.mode === "release" ||
661
661
  value.mode === "discard" ||
662
662
  value.mode === "observe-command" ||
663
- (value.mode === "observe" &&
663
+ ((value.mode === "observe" || value.mode === "observe-held") &&
664
664
  !!parseThreadTarget(value.oldTarget))) &&
665
665
  isUniqueSourceUpdateIds(value.sourceUpdateIds) &&
666
666
  typeof value.sentAtMs === "number" &&
@@ -672,7 +672,7 @@ export function parseTelegramBusEnvelope(line) {
672
672
  sourceUpdateIds: value.sourceUpdateIds,
673
673
  mode: value.mode,
674
674
  sentAtMs: value.sentAtMs,
675
- ...(value.mode === "observe"
675
+ ...((value.mode === "observe" || value.mode === "observe-held")
676
676
  ? { oldTarget: parseThreadTarget(value.oldTarget) }
677
677
  : {}),
678
678
  ...(selectedCommand ? { selectedCommand, preparedSource } : {}),
@@ -1249,6 +1249,7 @@ export function createTelegramBusLiveRebindController(deps) {
1249
1249
  "release",
1250
1250
  "discard",
1251
1251
  "observe",
1252
+ "observe-held",
1252
1253
  "observe-command",
1253
1254
  ].includes(input.mode) ||
1254
1255
  (input.mode === "observe-command" && !selectedCommand))))
@@ -1289,7 +1290,8 @@ export function createTelegramBusLiveRebindController(deps) {
1289
1290
  }
1290
1291
  : { updates: input.updates }),
1291
1292
  });
1292
- const observing = "mode" in expected && expected.mode === "observe";
1293
+ const observing = "mode" in expected && (expected.mode === "observe" || expected.mode === "observe-held");
1294
+ const observingHeld = "mode" in expected && expected.mode === "observe-held";
1293
1295
  const commandObserving = "mode" in expected && expected.mode === "observe-command";
1294
1296
  const settling = "mode" in expected &&
1295
1297
  (expected.mode === "release" ||
@@ -1302,6 +1304,7 @@ export function createTelegramBusLiveRebindController(deps) {
1302
1304
  ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY]
1303
1305
  : []),
1304
1306
  ...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
1307
+ ...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
1305
1308
  ...(held
1306
1309
  ? [
1307
1310
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
@@ -76,6 +76,7 @@ export interface TelegramDeliveryTransportOptions {
76
76
  /** @internal */
77
77
  export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResolverDeps {
78
78
  generation: string;
79
+ captureDrain?: () => (() => void) | undefined;
79
80
  renderView: (view: TelegramDeliveryView) => readonly TelegramDeliveryRenderedChunk[];
80
81
  sendChunk: (target: TelegramDeliveryTarget, chunk: TelegramDeliveryRenderedChunk, options: TelegramDeliveryTransportOptions) => Promise<number>;
81
82
  editChunk: (target: TelegramDeliveryTarget, messageId: number, chunk: TelegramDeliveryRenderedChunk, options: TelegramDeliveryTransportOptions) => Promise<void>;
@@ -86,6 +87,7 @@ export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResol
86
87
  /** @internal */
87
88
  export interface TelegramBridgeDeliveryRuntimeDeps {
88
89
  generation: string;
90
+ captureDrain?: () => (() => void) | undefined;
89
91
  getTargetPolicyView: () => TelegramDeliveryTargetPolicyView;
90
92
  getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
91
93
  isTransportActive?: () => boolean;
@@ -114,6 +116,7 @@ export declare function createTelegramBridgeDeliveryLifecycleHooks<TTransportSta
114
116
  export interface TelegramDeliveryTargetPolicyView {
115
117
  canDeliver: boolean;
116
118
  ownsDirect: boolean;
119
+ threaded?: boolean;
117
120
  allowedChatId?: number;
118
121
  followerTarget?: TelegramDeliveryTarget;
119
122
  leaderTarget?: TelegramDeliveryTarget;
@@ -127,6 +130,7 @@ export interface TelegramDeliveryTargetPolicyRuntime {
127
130
  /** @internal */
128
131
  export declare function createTelegramDeliveryTargetPolicyRuntime(deps: {
129
132
  ownsDirect(): boolean;
133
+ isThreadedMode?(): boolean;
130
134
  isFollowerRegistered(): boolean;
131
135
  getAllowedChatId(): number | undefined;
132
136
  getFollowerTarget(): TelegramDeliveryTarget | undefined;
@@ -3,7 +3,6 @@
3
3
  * Zones: telegram delivery, extension API, runtime binding
4
4
  * Owns the public extension delivery contract, authorized scope resolution, operational rendering adapter, per-target serialization, chunk reconciliation, generation-fenced logical handles, and process-local runtime membrane; composes the established reply renderer with bus-aware Telegram API ports and excludes bot clients, Pi contexts, and consumer-extension policy
5
5
  */
6
- import { markTelegramBusAggregateDelivery } from "./bus.js";
7
6
  import { assertTelegramInlineKeyboardCallbackData, } from "./keyboard.js";
8
7
  import { renderTelegramMessage, withTelegramReplyParameters, } from "./replies.js";
9
8
  import { areTelegramTargetsEqual as areDeliveryTargetsEqual, getTelegramTargetThreadParams, } from "./target.js";
@@ -90,6 +89,7 @@ export function createTelegramDeliveryTargetPolicyRuntime(deps) {
90
89
  return {
91
90
  canDeliver: ownsDirect || deps.isFollowerRegistered(),
92
91
  ownsDirect,
92
+ threaded: deps.isThreadedMode?.() === true,
93
93
  allowedChatId: deps.getAllowedChatId(),
94
94
  followerTarget: deps.getFollowerTarget(),
95
95
  leaderTarget: deps.getLeaderTarget(),
@@ -103,19 +103,20 @@ export function createTelegramDeliveryTargetPolicyRuntime(deps) {
103
103
  },
104
104
  };
105
105
  }
106
+ function isThreadedDelivery(view) {
107
+ return view.threaded === true || view.followerTarget?.threadId !== undefined || view.leaderTarget?.threadId !== undefined;
108
+ }
106
109
  /** @internal */
107
110
  export function resolveTelegramDeliveryInstanceTarget(view) {
108
111
  if (!view.canDeliver)
109
112
  return undefined;
110
- return (view.followerTarget ??
111
- view.leaderTarget ??
112
- (view.allowedChatId === undefined
113
- ? undefined
114
- : { chatId: view.allowedChatId }));
113
+ const target = view.followerTarget ?? view.leaderTarget ??
114
+ (view.allowedChatId === undefined ? undefined : { chatId: view.allowedChatId });
115
+ return target && (!isThreadedDelivery(view) || target.threadId !== undefined) ? target : undefined;
115
116
  }
116
117
  /** @internal */
117
118
  export function resolveTelegramDeliveryAggregateTarget(view) {
118
- return !view.canDeliver || view.allowedChatId === undefined
119
+ return !view.canDeliver || view.allowedChatId === undefined || isThreadedDelivery(view)
119
120
  ? undefined
120
121
  : { chatId: view.allowedChatId };
121
122
  }
@@ -127,7 +128,7 @@ export function isTelegramDeliveryExplicitTargetAuthorized(candidate, view) {
127
128
  return false;
128
129
  }
129
130
  if (candidate.threadId === undefined)
130
- return true;
131
+ return !isThreadedDelivery(view);
131
132
  if (view.followerTarget) {
132
133
  return areDeliveryTargetsEqual(candidate, view.followerTarget);
133
134
  }
@@ -166,10 +167,28 @@ function resolveTelegramDeliveryTarget(scope, deps) {
166
167
  }
167
168
  return { ok: true, value: cloneTarget(target) };
168
169
  }
169
- function createTelegramDeliveryTargetQueue() {
170
+ /** Observation cannot turn failed delivery into success or borrow a newer queue lifetime. */
171
+ function captureDeliveryDrainObserver(capture) {
172
+ try {
173
+ const notify = capture?.();
174
+ return () => {
175
+ try {
176
+ const emitted = notify?.();
177
+ if (emitted)
178
+ void Promise.resolve(emitted).catch(() => undefined);
179
+ }
180
+ catch { /* Observer failure is inert. */ }
181
+ };
182
+ }
183
+ catch {
184
+ return () => { };
185
+ }
186
+ }
187
+ function createTelegramDeliveryTargetQueue(captureDrain) {
170
188
  const queues = new Map();
171
189
  async function run(target, operation) {
172
190
  const key = targetKey(target);
191
+ const notifyDrained = captureDeliveryDrainObserver(captureDrain);
173
192
  const previous = queues.get(key) ?? Promise.resolve();
174
193
  const current = previous.then(operation, operation);
175
194
  const settled = current.then(() => { }, () => { });
@@ -178,8 +197,10 @@ function createTelegramDeliveryTargetQueue() {
178
197
  return await current;
179
198
  }
180
199
  finally {
181
- if (queues.get(key) === settled)
200
+ if (queues.get(key) === settled) {
182
201
  queues.delete(key);
202
+ notifyDrained();
203
+ }
183
204
  }
184
205
  }
185
206
  return {
@@ -203,7 +224,10 @@ function getChunkTransportOptions(view, index, chunkCount, replyToMessageId, edi
203
224
  export function createTelegramDeliveryRuntime(deps) {
204
225
  let active = true;
205
226
  const handleBindings = new WeakMap();
206
- const targetQueue = createTelegramDeliveryTargetQueue();
227
+ const targetQueue = createTelegramDeliveryTargetQueue(() => {
228
+ const notify = deps.captureDrain?.();
229
+ return () => active ? notify?.() : undefined;
230
+ });
207
231
  const runForTarget = targetQueue.run;
208
232
  const render = (view) => {
209
233
  const chunks = deps.renderView(view);
@@ -398,9 +422,17 @@ export function createTelegramBridgeDeliveryRuntime(deps) {
398
422
  throw new TelegramDeliveryTransportGenerationError();
399
423
  }
400
424
  };
425
+ const assertOutputTarget = (target) => {
426
+ if (isThreadedDelivery(getPolicyView()) && target.threadId === undefined)
427
+ throw new Error("Telegram All is input-only in Threaded Mode.");
428
+ };
401
429
  return createTelegramDeliveryRuntime({
402
430
  generation: deps.generation,
403
- getActiveTurnTarget: deps.getActiveTurnTarget,
431
+ captureDrain: deps.captureDrain,
432
+ getActiveTurnTarget() {
433
+ const target = deps.getActiveTurnTarget();
434
+ return target && (!isThreadedDelivery(getPolicyView()) || target.threadId !== undefined) ? target : undefined;
435
+ },
404
436
  getInstanceTarget() {
405
437
  return resolveTelegramDeliveryInstanceTarget(getPolicyView());
406
438
  },
@@ -423,6 +455,7 @@ export function createTelegramBridgeDeliveryRuntime(deps) {
423
455
  },
424
456
  async sendChunk(target, chunk, options) {
425
457
  assertTransportActive();
458
+ assertOutputTarget(target);
426
459
  const sent = await withTelegramReplyParameters(target.chatId, options.replyToMessageId, target, (replyParameters) => {
427
460
  const body = {
428
461
  chat_id: target.chatId,
@@ -436,9 +469,7 @@ export function createTelegramBridgeDeliveryRuntime(deps) {
436
469
  ? { reply_markup: options.replyMarkup }
437
470
  : {}),
438
471
  };
439
- return deps.api.sendMessage(target.threadId === undefined
440
- ? markTelegramBusAggregateDelivery(body)
441
- : body);
472
+ return deps.api.sendMessage(body);
442
473
  });
443
474
  assertTransportActive();
444
475
  deps.recordOwnership({
@@ -450,6 +481,7 @@ export function createTelegramBridgeDeliveryRuntime(deps) {
450
481
  },
451
482
  async editChunk(target, messageId, chunk, options) {
452
483
  assertTransportActive();
484
+ assertOutputTarget(target);
453
485
  await deps.api.editMessageText({
454
486
  chat_id: target.chatId,
455
487
  message_id: messageId,
@@ -466,6 +498,7 @@ export function createTelegramBridgeDeliveryRuntime(deps) {
466
498
  },
467
499
  async sendChatAction(target, action) {
468
500
  assertTransportActive();
501
+ assertOutputTarget(target);
469
502
  await deps.api.sendChatAction(target.chatId, action, {
470
503
  message_thread_id: target.threadId,
471
504
  });