@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
package/lib/bus.ts CHANGED
@@ -5,28 +5,27 @@
5
5
  * cross-instance forwarding helpers, and the live follower registry model.
6
6
  */
7
7
 
8
- import { execFileSync } from "node:child_process";
9
8
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
10
9
  import {
11
10
  chmodSync,
12
11
  existsSync,
13
12
  lstatSync,
14
13
  mkdirSync,
15
- readFileSync,
16
14
  readlinkSync,
17
15
  renameSync,
18
16
  symlinkSync,
19
17
  unlinkSync,
20
18
  } from "node:fs";
19
+ import { createRequire } from "node:module";
21
20
  import {
22
21
  createConnection,
23
22
  createServer,
24
23
  type Server,
25
24
  type Socket,
26
25
  } from "node:net";
27
- import { createRequire } from "node:module";
28
26
  import { platform as getPlatform, tmpdir } from "node:os";
29
27
  import { basename, dirname, join } from "node:path";
28
+ import { isDeepStrictEqual } from "node:util";
30
29
 
31
30
  import {
32
31
  classifyTelegramBusTransportError,
@@ -37,28 +36,33 @@ import {
37
36
  getTelegramBusLeaderEndpoint,
38
37
  getTelegramBusPipePath,
39
38
  getTelegramBusTransportRetryPolicy,
40
- type TelegramBusEndpointLayout,
41
- TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES,
42
- isTelegramBusPipePath,
43
39
  isRetryableTelegramBusTransportError,
40
+ isTelegramBusPipePath,
44
41
  probeTelegramBusEndpoint,
42
+ TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES,
43
+ type TelegramBusEndpointLayout,
45
44
  type TelegramBusTransportEventRecorder,
46
45
  type TelegramBusTransportRetryPolicy,
47
46
  } from "./bus-transport.ts";
47
+ import type { TelegramThreadDisplayMode } from "./config.ts";
48
+ import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS } from "./locks.ts";
49
+ import { resolveAgentDir } from "./paths.ts";
50
+ import { getTelegramProcessBirthIdentity } from "./process-identity.ts";
48
51
  import {
49
52
  parseTelegramQueueHandoffPayload,
50
53
  TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES,
51
54
  type TelegramQueueHandoffPayload,
52
55
  } from "./queue.ts";
53
- import type { TelegramTarget } from "./target.ts";
54
- import type { TelegramThreadDisplayMode } from "./config.ts";
55
- import { isProcessAlive } from "./locks.ts";
56
- import { isWireRecord as isRecord } from "./wire.ts";
57
- import { resolveAgentDir } from "./paths.ts";
56
+ import {
57
+ areTelegramTargetsEqual,
58
+ parseTelegramTarget as parseTarget,
59
+ type TelegramTarget,
60
+ } from "./target.ts";
58
61
  import {
59
62
  normalizeTelegramSessionReplacementIntent,
60
63
  type TelegramSessionReplacementIntent,
61
64
  } from "./threads.ts";
65
+ import { hasOnlyWireKeys, isWireRecord as isRecord } from "./wire.ts";
62
66
 
63
67
  interface TelegramBusProcessRuntime {
64
68
  instanceId: string;
@@ -69,113 +73,6 @@ interface TelegramBusProcessRuntime {
69
73
  getFollowerSocketPath: () => string;
70
74
  }
71
75
 
72
- interface TelegramProcessBirthIdentityOptions {
73
- platform?: NodeJS.Platform;
74
- readProcStat?: (pid: number) => string;
75
- readDarwinProcessStart?: (pid: number) => string;
76
- }
77
-
78
- type TelegramProcessBirthProof =
79
- | { status: "proven"; identity: string }
80
- | { status: "unverifiable" };
81
-
82
- export type TelegramProcessLiveness = "alive" | "dead" | "unverifiable";
83
-
84
- interface TelegramProcessLivenessOptions
85
- extends TelegramProcessBirthIdentityOptions {
86
- isProcessAlive?: (pid: number) => boolean;
87
- }
88
-
89
- function readDarwinProcessStart(pid: number): string {
90
- return execFileSync(
91
- "/bin/ps",
92
- ["-o", "lstart=", "-p", String(pid)],
93
- {
94
- encoding: "utf8",
95
- stdio: ["ignore", "pipe", "ignore"],
96
- },
97
- ).trim();
98
- }
99
-
100
- function getTelegramProcessBirthProof(
101
- pid: number,
102
- options: TelegramProcessBirthIdentityOptions = {},
103
- ): TelegramProcessBirthProof {
104
- if (pid <= 0) return { status: "unverifiable" };
105
- const platform = options.platform ?? getPlatform();
106
- if (platform === "linux") {
107
- try {
108
- const stat = (options.readProcStat ?? ((targetPid) =>
109
- readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
110
- const closeParen = stat.lastIndexOf(")");
111
- const fields = stat
112
- .slice(closeParen + 2)
113
- .trim()
114
- .split(/\s+/u);
115
- const startTicks = fields[19];
116
- if (startTicks) {
117
- return { status: "proven", identity: `${pid}:start:${startTicks}` };
118
- }
119
- } catch {
120
- /* inaccessible process metadata */
121
- }
122
- } else if (platform === "darwin") {
123
- try {
124
- const startedAt = (
125
- options.readDarwinProcessStart ?? readDarwinProcessStart
126
- )(pid);
127
- if (startedAt) {
128
- const fingerprint = createHash("sha256")
129
- .update(startedAt)
130
- .digest("hex")
131
- .slice(0, 16);
132
- return { status: "proven", identity: `${pid}:start:${fingerprint}` };
133
- }
134
- } catch {
135
- /* inaccessible process metadata */
136
- }
137
- }
138
- return { status: "unverifiable" };
139
- }
140
-
141
- export function getTelegramProcessBirthIdentity(
142
- pid: number,
143
- fallbackGeneration: number | string,
144
- options: TelegramProcessBirthIdentityOptions = {},
145
- ): string {
146
- const proof = getTelegramProcessBirthProof(pid, options);
147
- return proof.status === "proven"
148
- ? proof.identity
149
- : `${pid}:generation:${fallbackGeneration}`;
150
- }
151
-
152
- export function getTelegramProcessLiveness(
153
- owner: { processId: number; processBirthId: string },
154
- options: TelegramProcessLivenessOptions = {},
155
- ): TelegramProcessLiveness {
156
- const processAlive = options.isProcessAlive ?? isProcessAlive;
157
- if (!processAlive(owner.processId)) return "dead";
158
- const proof = getTelegramProcessBirthProof(owner.processId, options);
159
- if (proof.status === "unverifiable") return "unverifiable";
160
- return proof.identity === owner.processBirthId ? "alive" : "dead";
161
- }
162
-
163
- export function getTelegramProcessBirthIdentityLiveness(
164
- processBirthId: string,
165
- options: TelegramProcessLivenessOptions = {},
166
- ): TelegramProcessLiveness {
167
- const match = /^(\d+):(start|generation):(.+)$/u.exec(processBirthId);
168
- if (!match) return "unverifiable";
169
- const processId = Number(match[1]);
170
- if (!Number.isSafeInteger(processId) || processId <= 0) return "unverifiable";
171
- const processAlive = options.isProcessAlive ?? isProcessAlive;
172
- if (!processAlive(processId)) return "dead";
173
- if (match[2] === "generation") return "unverifiable";
174
- const proof = getTelegramProcessBirthProof(processId, options);
175
- if (proof.status === "unverifiable") return "unverifiable";
176
- return proof.identity === processBirthId ? "alive" : "dead";
177
- }
178
-
179
76
  export function createCurrentTelegramBusProcessRuntime(input: {
180
77
  getActiveProfileName: () => string | undefined;
181
78
  endpointLayout?: TelegramBusEndpointLayout;
@@ -200,7 +97,8 @@ export function createTelegramBusProcessRuntime(input: {
200
97
  parentProcessIdentity?: string;
201
98
  createdAtMs: number;
202
99
  }): TelegramBusProcessRuntime {
203
- const getActiveProfileName = input.getActiveProfileName, endpointLayout = input.endpointLayout;
100
+ const getActiveProfileName = input.getActiveProfileName,
101
+ endpointLayout = input.endpointLayout;
204
102
  const instanceId = `${input.pid}:${input.createdAtMs}`;
205
103
  const ownerPid = input.parentPid || input.pid;
206
104
  const manualFollowerOwnerId =
@@ -242,18 +140,254 @@ export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
242
140
  "input-custody-reference-v1" as const;
243
141
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
244
142
  "workspace-thread-rename-v1" as const;
245
- export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE = "thread-display-mode-v1" as const;
143
+ export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE =
144
+ "thread-display-mode-v1" as const;
246
145
  export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT =
247
146
  "directory-display-format-v1" as const;
248
147
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT =
249
148
  "workspace-follower-auto-connect-v1" as const;
250
149
  export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT =
251
150
  "session-replacement-intent-v1" as const;
252
- export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE = "workspace-restore-v1" as const;
151
+ export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE =
152
+ "workspace-restore-v1" as const;
153
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE =
154
+ "live-thread-rebind-save-v1" as const;
155
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY =
156
+ "live-thread-rebind-apply-v1" as const;
157
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE =
158
+ "live-thread-rebind-settle-v1" as const;
159
+ /** Restricted recipient text/menu delivery; negotiated identity and captured effect authority remain required. */
160
+ export const TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY =
161
+ "selected-menu-delivery-v1" as const;
162
+ /** Versioned held-command set; syntax is shared, while the captured recipient Commands registry owns supported plans. */
163
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET =
164
+ "live-thread-rebind-command-set-v1" as const;
165
+ /** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
166
+ export const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES = [
167
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
168
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
169
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
170
+ ] as const;
171
+ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
172
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
173
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
174
+ ] as const;
175
+
176
+ export interface TelegramBusSelectedCommandInput {
177
+ name: string;
178
+ target: TelegramTarget & { threadId: number };
179
+ }
180
+
181
+ function parseSelectedCommandInput(
182
+ value: unknown,
183
+ ): TelegramBusSelectedCommandInput | undefined {
184
+ if (
185
+ !isRecord(value) ||
186
+ !hasOnlyWireKeys(value, ["name", "target"]) ||
187
+ typeof value.name !== "string" ||
188
+ !/^[a-z0-9_]{1,32}$/u.test(value.name) ||
189
+ !isRecord(value.target) ||
190
+ !hasOnlyWireKeys(value.target, ["chatId", "threadId"])
191
+ )
192
+ return undefined;
193
+ const target = parseThreadTarget(value.target);
194
+ return target &&
195
+ Number.isSafeInteger(target.chatId) &&
196
+ target.chatId > 0 &&
197
+ Number.isSafeInteger(target.threadId) &&
198
+ target.threadId > 0
199
+ ? { name: value.name, target }
200
+ : undefined;
201
+ }
202
+
203
+ export type TelegramBusSelectedMenuTextEffect = {
204
+ text: string;
205
+ parseMode?: "HTML";
206
+ replyMarkup?: {
207
+ inline_keyboard: { text: string; callback_data: string }[][];
208
+ };
209
+ } & (
210
+ | { kind: "send-text"; replyToMessageId?: number }
211
+ | { kind: "edit-text"; messageId: number }
212
+ );
213
+
214
+ /** Serializable identity/effect description, never a process-local callback or standalone delivery grant. */
215
+ export interface TelegramBusSelectedMenuDelivery {
216
+ kind: "follower.deliverSelectedMenu";
217
+ requestId: string;
218
+ instanceId: string;
219
+ registrationGeneration: string;
220
+ operationId: string;
221
+ recipient: {
222
+ sessionId: string;
223
+ sessionGeneration: number;
224
+ processId: number;
225
+ processBirthId: string;
226
+ profileKey: string;
227
+ journalBindingKey: string;
228
+ target: TelegramTarget & { threadId: number };
229
+ };
230
+ executor: { instanceId: string; leaderEpoch: string };
231
+ operatorUserId: number;
232
+ effect: TelegramBusSelectedMenuTextEffect;
233
+ sentAtMs: number;
234
+ }
235
+
236
+ export interface TelegramBusSelectedMenuDeliveryObservation {
237
+ operationId: string;
238
+ recipient: TelegramBusSelectedMenuDelivery["recipient"];
239
+ registrationGeneration: string;
240
+ effect: TelegramBusSelectedMenuTextEffect["kind"];
241
+ messageId: number;
242
+ }
243
+
244
+ /** Closed wire projection of prepared recipient source; independent of journal implementation and never a removal grant. */
245
+ export interface TelegramBusPreparedCommandSource {
246
+ journalBindingKey: string;
247
+ updateId: number;
248
+ sourceSha256: string;
249
+ }
250
+
251
+ /** Pure live-rebind recipient/operation identity shared by prepare/apply/settle envelopes. */
252
+ function parseLiveRebindRecipient(value: Record<string, unknown>):
253
+ | {
254
+ recipientInstanceId: string;
255
+ recipientRegistrationGeneration: string;
256
+ recipientSessionId: string;
257
+ recipientBindingKey: string;
258
+ operationId: string;
259
+ }
260
+ | undefined {
261
+ const {
262
+ recipientInstanceId,
263
+ recipientRegistrationGeneration,
264
+ recipientSessionId,
265
+ recipientBindingKey,
266
+ operationId,
267
+ } = value;
268
+ return typeof recipientInstanceId === "string" &&
269
+ recipientInstanceId &&
270
+ typeof recipientRegistrationGeneration === "string" &&
271
+ recipientRegistrationGeneration &&
272
+ typeof recipientSessionId === "string" &&
273
+ recipientSessionId &&
274
+ typeof recipientBindingKey === "string" &&
275
+ recipientBindingKey &&
276
+ typeof operationId === "string" &&
277
+ operationId.length > 0 &&
278
+ operationId.length <= 128
279
+ ? {
280
+ recipientInstanceId,
281
+ recipientRegistrationGeneration,
282
+ recipientSessionId,
283
+ recipientBindingKey,
284
+ operationId,
285
+ }
286
+ : undefined;
287
+ }
288
+
289
+ /** Non-empty, duplicate-free, non-negative safe-integer Telegram update ids. */
290
+ function isUniqueSourceUpdateIds(value: unknown): value is number[] {
291
+ return (
292
+ Array.isArray(value) &&
293
+ value.length > 0 &&
294
+ value.every((id) => Number.isSafeInteger(id) && (id as number) >= 0) &&
295
+ new Set(value).size === value.length
296
+ );
297
+ }
298
+
299
+ function parsePreparedCommandSource(
300
+ value: unknown,
301
+ ): TelegramBusPreparedCommandSource | undefined {
302
+ if (
303
+ !isRecord(value) ||
304
+ !hasOnlyWireKeys(value, [
305
+ "journalBindingKey",
306
+ "updateId",
307
+ "sourceSha256",
308
+ ]) ||
309
+ typeof value.journalBindingKey !== "string" ||
310
+ !value.journalBindingKey ||
311
+ !Number.isSafeInteger(value.updateId) ||
312
+ (value.updateId as number) < 0 ||
313
+ typeof value.sourceSha256 !== "string" ||
314
+ !/^[a-f0-9]{64}$/u.test(value.sourceSha256)
315
+ )
316
+ return undefined;
317
+ return {
318
+ journalBindingKey: value.journalBindingKey,
319
+ updateId: value.updateId as number,
320
+ sourceSha256: value.sourceSha256,
321
+ };
322
+ }
323
+
324
+ export interface TelegramBusLiveRebindSaveObservation {
325
+ operationId: string;
326
+ recipient: {
327
+ instanceId: string;
328
+ sessionId: string;
329
+ generation: string;
330
+ bindingKey: string;
331
+ };
332
+ sourceUpdateIds: number[];
333
+ selectedCommand?: TelegramBusSelectedCommandInput;
334
+ preparedSource?: TelegramBusPreparedCommandSource;
335
+ status: "saved";
336
+ }
337
+
338
+ /** Read-only warm observation; semantic completion and exact source ACK are distinct from detached delivery. */
339
+ export interface TelegramBusLiveRebindCommandObservation extends Omit<
340
+ TelegramBusLiveRebindSaveObservation,
341
+ "status" | "preparedSource" | "selectedCommand"
342
+ > {
343
+ status: "command-observed";
344
+ selectedCommand: TelegramBusSelectedCommandInput;
345
+ preparedSource: TelegramBusPreparedCommandSource;
346
+ command: "completed" | "unknown" | "not-issued";
347
+ sourceAck?: TelegramBusPreparedCommandSource;
348
+ }
349
+
350
+ export interface TelegramBusLiveRebindWorkState {
351
+ sessionBusy: boolean;
352
+ targetWork: boolean;
353
+ deliveryPending: boolean;
354
+ unknown: boolean;
355
+ }
356
+
357
+ export interface TelegramBusLiveRebindWorkObservation extends Omit<
358
+ TelegramBusLiveRebindSaveObservation,
359
+ "status"
360
+ > {
361
+ status: "observed";
362
+ oldTarget: TelegramTarget & { threadId: number };
363
+ work: TelegramBusLiveRebindWorkState;
364
+ }
365
+
366
+ export interface TelegramBusLiveRebindSettlementObservation extends Omit<
367
+ TelegramBusLiveRebindSaveObservation,
368
+ "status"
369
+ > {
370
+ action: "release" | "discard";
371
+ status: "released" | "discarded" | "protected" | "unknown";
372
+ }
373
+
374
+ export interface TelegramBusLiveRebindApplyObservation extends Omit<
375
+ TelegramBusLiveRebindSaveObservation,
376
+ "status"
377
+ > {
378
+ status: "saved" | "applied";
379
+ target: TelegramTarget & { threadId: number };
380
+ slot: string;
381
+ }
253
382
 
254
383
  export interface TelegramBusWorkspaceRestoreObservation {
255
384
  operationId: string;
256
- recipient: { kind: "follower"; instanceId: string; sessionId: string; generation: string };
385
+ recipient: {
386
+ kind: "follower";
387
+ instanceId: string;
388
+ sessionId: string;
389
+ generation: string;
390
+ };
257
391
  target: TelegramTarget & { threadId: number };
258
392
  slot: string;
259
393
  ready: boolean;
@@ -284,8 +418,7 @@ export function createTelegramBusProtocolIdentity(input: {
284
418
  capabilities.length > 32 ||
285
419
  capabilities.some(
286
420
  (capability) =>
287
- capability.length > 128 ||
288
- !/^[a-z0-9][a-z0-9._-]*$/u.test(capability),
421
+ capability.length > 128 || !/^[a-z0-9][a-z0-9._-]*$/u.test(capability),
289
422
  )
290
423
  ) {
291
424
  throw new Error("Telegram bus capabilities must be canonical identifiers.");
@@ -320,18 +453,42 @@ export function hasTelegramBusCapability(
320
453
  }
321
454
 
322
455
  export function getTelegramInputCustodyPeerReadiness(
323
- followers: readonly Pick<TelegramBusFollowerView,
324
- "registrationGeneration" | "protocol">[],
456
+ followers: readonly Pick<
457
+ TelegramBusFollowerView,
458
+ "registrationGeneration" | "protocol"
459
+ >[],
325
460
  ): ("ready" | "legacy" | "unknown")[] {
326
- return followers.map(follower => {
327
- if (!follower.registrationGeneration || !follower.protocol) return "unknown";
328
- return hasTelegramBusCapability(follower.protocol,
329
- TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) &&
330
- hasTelegramBusCapability(follower.protocol,
331
- TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) ? "ready" : "legacy";
461
+ return followers.map((follower) => {
462
+ if (!follower.registrationGeneration || !follower.protocol)
463
+ return "unknown";
464
+ return hasTelegramBusCapability(
465
+ follower.protocol,
466
+ TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
467
+ ) &&
468
+ hasTelegramBusCapability(
469
+ follower.protocol,
470
+ TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE,
471
+ )
472
+ ? "ready"
473
+ : "legacy";
332
474
  });
333
475
  }
334
476
 
477
+ /** Both peers advertise every required capability over a compatible protocol; reads only the two identities. */
478
+ export function hasTelegramBusSharedCapabilities(
479
+ local: TelegramBusProtocolIdentity,
480
+ remote: TelegramBusProtocolIdentity | undefined,
481
+ required: readonly string[],
482
+ ): boolean {
483
+ return (
484
+ required.every(
485
+ (capability) =>
486
+ hasTelegramBusCapability(local, capability) &&
487
+ hasTelegramBusCapability(remote, capability),
488
+ ) && getTelegramBusProtocolCompatibility({ local, remote }).compatible
489
+ );
490
+ }
491
+
335
492
  export function getTelegramBusProtocolCompatibility(input: {
336
493
  local: TelegramBusProtocolIdentity;
337
494
  remote?: TelegramBusProtocolIdentity;
@@ -372,7 +529,12 @@ export function getTelegramBusSocketPath(
372
529
  profileName?: string,
373
530
  layout?: TelegramBusEndpointLayout,
374
531
  ): string {
375
- return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName, layout });
532
+ return getTelegramBusLeaderEndpoint({
533
+ agentDir,
534
+ platform,
535
+ profileName,
536
+ layout,
537
+ });
376
538
  }
377
539
 
378
540
  export function getTelegramBusFollowerSocketPath(
@@ -413,6 +575,26 @@ export interface TelegramBusFollowerView extends TelegramBusInstanceRegistration
413
575
  lastHeartbeatMs: number;
414
576
  }
415
577
 
578
+ /** Whether a live follower is still exactly the captured registration: identity, process, session, endpoint and negotiated protocol. */
579
+ export function isSameTelegramBusFollowerRegistration(
580
+ live: TelegramBusInstanceRegistration,
581
+ captured: TelegramBusInstanceRegistration,
582
+ ): boolean {
583
+ return (
584
+ live.instanceId === captured.instanceId &&
585
+ live.profileKey === captured.profileKey &&
586
+ live.sessionId === captured.sessionId &&
587
+ live.registrationGeneration === captured.registrationGeneration &&
588
+ live.pid === captured.pid &&
589
+ live.processBirthId === captured.processBirthId &&
590
+ live.sessionGeneration === captured.sessionGeneration &&
591
+ live.cwd === captured.cwd &&
592
+ live.slot === captured.slot &&
593
+ live.busSocketPath === captured.busSocketPath &&
594
+ isDeepStrictEqual(live.protocol, captured.protocol)
595
+ );
596
+ }
597
+
416
598
  export function getTelegramFollowerTargetOwnership(input: {
417
599
  target: TelegramTarget;
418
600
  followers: readonly TelegramBusFollowerView[];
@@ -424,12 +606,14 @@ export function getTelegramFollowerTargetOwnership(input: {
424
606
  target: TelegramTarget;
425
607
  }[];
426
608
  currentInstanceId?: string;
427
- }): {
428
- instanceId: string;
429
- ownerGeneration: string;
430
- recipientBindingKey: string;
431
- protocolIdentity: TelegramBusProtocolIdentity;
432
- } | undefined {
609
+ }):
610
+ | {
611
+ instanceId: string;
612
+ ownerGeneration: string;
613
+ recipientBindingKey: string;
614
+ protocolIdentity: TelegramBusProtocolIdentity;
615
+ }
616
+ | undefined {
433
617
  const liveFollower = input.followers.find((follower) => {
434
618
  return (
435
619
  follower.target?.chatId === input.target.chatId &&
@@ -459,43 +643,32 @@ const TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD = "__piTelegramAggregateDelivery";
459
643
  const TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD =
460
644
  "__piTelegramCrossTargetDelivery";
461
645
 
462
- export function markTelegramBusAggregateDelivery<
463
- T extends Record<string, unknown>,
464
- >(body: T): T {
465
- return {
466
- ...body,
467
- [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true,
468
- };
469
- }
646
+ type TelegramBusDeliveryMarker =
647
+ | typeof TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD
648
+ | typeof TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD;
470
649
 
471
- function isTelegramBusAggregateDelivery(body: unknown): boolean {
650
+ function hasTelegramBusDeliveryMarker(
651
+ body: unknown,
652
+ marker: TelegramBusDeliveryMarker,
653
+ ): boolean {
472
654
  return Boolean(
473
655
  body &&
474
656
  typeof body === "object" &&
475
657
  !Array.isArray(body) &&
476
- (body as Record<string, unknown>)[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD] ===
477
- true,
658
+ (body as Record<string, unknown>)[marker] === true,
478
659
  );
479
660
  }
480
661
 
481
- export function markTelegramBusCrossTargetDelivery<
662
+ export function markTelegramBusAggregateDelivery<
482
663
  T extends Record<string, unknown>,
483
664
  >(body: T): T {
484
- return {
485
- ...body,
486
- [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true,
487
- };
665
+ return { ...body, [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true };
488
666
  }
489
667
 
490
- function isTelegramBusCrossTargetDelivery(body: unknown): boolean {
491
- return Boolean(
492
- body &&
493
- typeof body === "object" &&
494
- !Array.isArray(body) &&
495
- (body as Record<string, unknown>)[
496
- TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD
497
- ] === true,
498
- );
668
+ export function markTelegramBusCrossTargetDelivery<
669
+ T extends Record<string, unknown>,
670
+ >(body: T): T {
671
+ return { ...body, [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true };
499
672
  }
500
673
 
501
674
  export function stripTelegramBusApiMetadata<T extends Record<string, unknown>>(
@@ -567,8 +740,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
567
740
  typeof threadId === "number" ? threadId : Number(threadId);
568
741
  return (
569
742
  Number.isInteger(parsedThreadId) &&
570
- (target.threadId === undefined ||
571
- !matchesId(threadId, target.threadId))
743
+ (target.threadId === undefined || !matchesId(threadId, target.threadId))
572
744
  );
573
745
  };
574
746
  const isTargetMessageScoped = (body: unknown): boolean => {
@@ -612,14 +784,20 @@ export function isTelegramFollowerApiCallAllowed(input: {
612
784
  return isTargetChatScoped(input.args[1]);
613
785
  if (
614
786
  apiMethod === "sendMessage" &&
615
- isTelegramBusAggregateDelivery(input.args[1])
787
+ hasTelegramBusDeliveryMarker(
788
+ input.args[1],
789
+ TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD,
790
+ )
616
791
  ) {
617
792
  const body = input.args[1] as Record<string, unknown>;
618
793
  return body.message_thread_id === undefined && isTargetChatScoped(body);
619
794
  }
620
795
  if (
621
796
  (apiMethod === "sendMessage" || apiMethod === "sendRichMessage") &&
622
- isTelegramBusCrossTargetDelivery(input.args[1])
797
+ hasTelegramBusDeliveryMarker(
798
+ input.args[1],
799
+ TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD,
800
+ )
623
801
  ) {
624
802
  return isDifferentTargetScoped(input.args[1]);
625
803
  }
@@ -744,8 +922,10 @@ export function createTelegramBusFollowerDeliveryIdentity(input: {
744
922
  !Number.isSafeInteger(input.sourceUpdateId) ||
745
923
  input.sourceUpdateId < 0 ||
746
924
  (input.sourceClaim !== undefined &&
747
- (!input.sourceClaim.acquisitionId || input.sourceClaim.acquisitionId.length > 256 ||
748
- !input.sourceClaim.handoffId || input.sourceClaim.handoffId.length > 256))
925
+ (!input.sourceClaim.acquisitionId ||
926
+ input.sourceClaim.acquisitionId.length > 256 ||
927
+ !input.sourceClaim.handoffId ||
928
+ input.sourceClaim.handoffId.length > 256))
749
929
  ) {
750
930
  throw new Error("Telegram follower delivery identity is incomplete.");
751
931
  }
@@ -763,7 +943,9 @@ export function createTelegramBusFollowerDeliveryIdentity(input: {
763
943
  deliveryId: `telegram-follower-v1-${deliveryId}`,
764
944
  sourceUpdateId: input.sourceUpdateId,
765
945
  recipientBindingKey: input.recipientBindingKey,
766
- ...(input.sourceRecoveryKey ? { sourceRecoveryKey: input.sourceRecoveryKey } : {}),
946
+ ...(input.sourceRecoveryKey
947
+ ? { sourceRecoveryKey: input.sourceRecoveryKey }
948
+ : {}),
767
949
  ...(input.sourceClaim ? { sourceClaim: { ...input.sourceClaim } } : {}),
768
950
  };
769
951
  }
@@ -772,26 +954,46 @@ export function canUseTelegramBusInputCustodyReference(input: {
772
954
  local?: TelegramBusProtocolIdentity;
773
955
  remote?: TelegramBusProtocolIdentity;
774
956
  }): boolean {
775
- return hasTelegramBusCapability(input.local, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) &&
776
- hasTelegramBusCapability(input.remote, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE);
957
+ return (
958
+ hasTelegramBusCapability(
959
+ input.local,
960
+ TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE,
961
+ ) &&
962
+ hasTelegramBusCapability(
963
+ input.remote,
964
+ TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE,
965
+ )
966
+ );
777
967
  }
778
968
 
779
969
  export function createTelegramBusFollowerSourceReferenceDeliveryIdentity(input: {
780
- kind: "leader.forwardCallback" | "leader.forwardReaction" |
781
- "leader.forwardMessage" | "leader.forwardEditedMessage" | "leader.wakeInputCustody";
970
+ kind:
971
+ | "leader.forwardCallback"
972
+ | "leader.forwardReaction"
973
+ | "leader.forwardMessage"
974
+ | "leader.forwardEditedMessage"
975
+ | "leader.wakeInputCustody";
782
976
  recipientBindingKey: string;
783
977
  sourceRecoveryKey: string;
784
- source: { updateId: number; owner: { acquisitionId: string; handoffId?: string } };
978
+ source: {
979
+ updateId: number;
980
+ owner: { acquisitionId: string; handoffId?: string };
981
+ };
785
982
  }): TelegramBusFollowerDeliveryIdentity {
786
- if (!input.sourceRecoveryKey || !input.source.owner.handoffId) throw new Error(
787
- "Telegram follower source-reference delivery requires an accepted handoff.",
788
- );
789
- return createTelegramBusFollowerDeliveryIdentity({ kind: input.kind,
983
+ if (!input.sourceRecoveryKey || !input.source.owner.handoffId)
984
+ throw new Error(
985
+ "Telegram follower source-reference delivery requires an accepted handoff.",
986
+ );
987
+ return createTelegramBusFollowerDeliveryIdentity({
988
+ kind: input.kind,
790
989
  recipientBindingKey: input.recipientBindingKey,
791
990
  sourceUpdateId: input.source.updateId,
792
991
  sourceRecoveryKey: input.sourceRecoveryKey,
793
- sourceClaim: { acquisitionId: input.source.owner.acquisitionId,
794
- handoffId: input.source.owner.handoffId } });
992
+ sourceClaim: {
993
+ acquisitionId: input.source.owner.acquisitionId,
994
+ handoffId: input.source.owner.handoffId,
995
+ },
996
+ });
795
997
  }
796
998
 
797
999
  export type TelegramBusEnvelope = (
@@ -897,7 +1099,11 @@ export type TelegramBusEnvelope = (
897
1099
  recipientRegistrationGeneration: string;
898
1100
  recipientBindingKey: string;
899
1101
  sourceRecoveryKey: string;
900
- source: { journalBindingKey: string; tokenSha256: string; updateId: number };
1102
+ source: {
1103
+ journalBindingKey: string;
1104
+ tokenSha256: string;
1105
+ updateId: number;
1106
+ };
901
1107
  handoffId: string;
902
1108
  sentAtMs: number;
903
1109
  }
@@ -909,6 +1115,47 @@ export type TelegramBusEnvelope = (
909
1115
  delivery: TelegramBusFollowerDeliveryIdentity;
910
1116
  sentAtMs: number;
911
1117
  }
1118
+ | {
1119
+ kind: "leader.prepareLiveRebind";
1120
+ requestId: string;
1121
+ recipientInstanceId: string;
1122
+ recipientRegistrationGeneration: string;
1123
+ recipientSessionId: string;
1124
+ recipientBindingKey: string;
1125
+ operationId: string;
1126
+ updates: ({ update_id: number } & Record<string, unknown>)[];
1127
+ selectedCommand?: TelegramBusSelectedCommandInput;
1128
+ sentAtMs: number;
1129
+ }
1130
+ | {
1131
+ kind: "leader.applyLiveRebind";
1132
+ requestId: string;
1133
+ recipientInstanceId: string;
1134
+ recipientRegistrationGeneration: string;
1135
+ recipientSessionId: string;
1136
+ recipientBindingKey: string;
1137
+ operationId: string;
1138
+ sourceUpdateIds: number[];
1139
+ mode: "apply" | "inspect";
1140
+ preparedSource?: TelegramBusPreparedCommandSource;
1141
+ selectedCommand?: TelegramBusSelectedCommandInput;
1142
+ sentAtMs: number;
1143
+ }
1144
+ | {
1145
+ kind: "leader.settleLiveRebind";
1146
+ requestId: string;
1147
+ recipientInstanceId: string;
1148
+ recipientRegistrationGeneration: string;
1149
+ recipientSessionId: string;
1150
+ recipientBindingKey: string;
1151
+ operationId: string;
1152
+ sourceUpdateIds: number[];
1153
+ mode: "release" | "discard" | "observe" | "observe-command";
1154
+ oldTarget?: TelegramTarget & { threadId: number };
1155
+ preparedSource?: TelegramBusPreparedCommandSource;
1156
+ selectedCommand?: TelegramBusSelectedCommandInput;
1157
+ sentAtMs: number;
1158
+ }
912
1159
  | {
913
1160
  kind: "leader.workspaceRestore";
914
1161
  requestId: string;
@@ -965,6 +1212,7 @@ export type TelegramBusEnvelope = (
965
1212
  message: TelegramBusAgentMessage;
966
1213
  sentAtMs: number;
967
1214
  }
1215
+ | TelegramBusSelectedMenuDelivery
968
1216
  | {
969
1217
  kind: "follower.callApi";
970
1218
  requestId: string;
@@ -996,10 +1244,25 @@ export type TelegramBusEnvelope = (
996
1244
  }
997
1245
  ) & { auth?: string };
998
1246
 
1247
+ /** Follower-staged queue handoff offer before the follower adds its own request and registration framing. */
1248
+ export type TelegramBusFollowerQueueHandoffOffer = Omit<
1249
+ Extract<TelegramBusEnvelope, { kind: "follower.offerQueueHandoff" }>,
1250
+ | "kind"
1251
+ | "requestId"
1252
+ | "instanceId"
1253
+ | "registrationGeneration"
1254
+ | "sentAtMs"
1255
+ | "auth"
1256
+ >;
1257
+
1258
+ /** Leader-routed queue handoff offer as donors and leaders exchange it before envelope framing. */
1259
+ export type TelegramBusLeaderQueueHandoffOffer = Omit<
1260
+ Extract<TelegramBusEnvelope, { kind: "leader.offerQueueHandoff" }>,
1261
+ "kind"
1262
+ >;
1263
+
999
1264
  type TelegramBusEnvelopeTrafficClass =
1000
- | "bootstrap"
1001
- | "generation-fenced"
1002
- | "response";
1265
+ "bootstrap" | "generation-fenced" | "response";
1003
1266
 
1004
1267
  export function getTelegramBusEnvelopeTrafficClass(
1005
1268
  envelope: TelegramBusEnvelope,
@@ -1007,7 +1270,8 @@ export function getTelegramBusEnvelopeTrafficClass(
1007
1270
  if (
1008
1271
  envelope.kind === "follower.register" ||
1009
1272
  envelope.kind === "follower.restoreWorkspace"
1010
- ) return "bootstrap";
1273
+ )
1274
+ return "bootstrap";
1011
1275
  if (envelope.kind === "bus.ack") return "response";
1012
1276
  return "generation-fenced";
1013
1277
  }
@@ -1057,18 +1321,25 @@ export function parseTelegramBusEnvelope(
1057
1321
  envelope = parseRegisterEnvelope(value, requestId, kind);
1058
1322
  break;
1059
1323
  case "follower.heartbeat":
1060
- envelope = parseHeartbeatEnvelope(value, requestId);
1061
- break;
1062
1324
  case "follower.disconnect":
1063
- envelope = parseDisconnectEnvelope(value, requestId);
1325
+ envelope = parseFollowerPresenceEnvelope(value, requestId, kind);
1064
1326
  break;
1065
1327
  case "follower.setThreadDisplayMode":
1066
- if (typeof value.instanceId === "string" &&
1067
- typeof value.registrationGeneration === "string" &&
1068
- (value.mode === "letters" || value.mode === "names" ||
1069
- value.mode === "directory-snake" || value.mode === "directory-title")) {
1070
- envelope = { kind, requestId, instanceId: value.instanceId,
1071
- registrationGeneration: value.registrationGeneration, mode: value.mode };
1328
+ if (
1329
+ typeof value.instanceId === "string" &&
1330
+ typeof value.registrationGeneration === "string" &&
1331
+ (value.mode === "letters" ||
1332
+ value.mode === "names" ||
1333
+ value.mode === "directory-snake" ||
1334
+ value.mode === "directory-title")
1335
+ ) {
1336
+ envelope = {
1337
+ kind,
1338
+ requestId,
1339
+ instanceId: value.instanceId,
1340
+ registrationGeneration: value.registrationGeneration,
1341
+ mode: value.mode,
1342
+ };
1072
1343
  }
1073
1344
  break;
1074
1345
  case "follower.renameThread":
@@ -1076,10 +1347,12 @@ export function parseTelegramBusEnvelope(
1076
1347
  break;
1077
1348
  case "follower.resetThreadName": {
1078
1349
  const target = parseTarget(value.target);
1079
- if (typeof value.instanceId === "string" &&
1080
- typeof value.registrationGeneration === "string" &&
1081
- typeof value.sentAtMs === "number" &&
1082
- target?.threadId !== undefined) {
1350
+ if (
1351
+ typeof value.instanceId === "string" &&
1352
+ typeof value.registrationGeneration === "string" &&
1353
+ typeof value.sentAtMs === "number" &&
1354
+ target?.threadId !== undefined
1355
+ ) {
1083
1356
  envelope = {
1084
1357
  kind,
1085
1358
  requestId,
@@ -1094,9 +1367,12 @@ export function parseTelegramBusEnvelope(
1094
1367
  case "follower.publishSessionReplacement":
1095
1368
  case "follower.settleSessionReplacement": {
1096
1369
  const intent = normalizeTelegramSessionReplacementIntent(value.intent);
1097
- if (typeof value.instanceId === "string" &&
1098
- typeof value.registrationGeneration === "string" &&
1099
- typeof value.sentAtMs === "number" && intent) {
1370
+ if (
1371
+ typeof value.instanceId === "string" &&
1372
+ typeof value.registrationGeneration === "string" &&
1373
+ typeof value.sentAtMs === "number" &&
1374
+ intent
1375
+ ) {
1100
1376
  envelope = {
1101
1377
  kind,
1102
1378
  requestId,
@@ -1134,15 +1410,147 @@ export function parseTelegramBusEnvelope(
1134
1410
  "leader.forwardEditedMessage",
1135
1411
  );
1136
1412
  break;
1413
+ case "leader.prepareLiveRebind": {
1414
+ const selectedCommand = parseSelectedCommandInput(value.selectedCommand);
1415
+ if (
1416
+ !hasOnlyWireKeys(value, [
1417
+ "kind",
1418
+ "requestId",
1419
+ "auth",
1420
+ "recipientInstanceId",
1421
+ "recipientRegistrationGeneration",
1422
+ "recipientSessionId",
1423
+ "recipientBindingKey",
1424
+ "operationId",
1425
+ "updates",
1426
+ "selectedCommand",
1427
+ "sentAtMs",
1428
+ ]) ||
1429
+ (Object.hasOwn(value, "selectedCommand") &&
1430
+ (!selectedCommand ||
1431
+ !Array.isArray(value.updates) ||
1432
+ value.updates.length !== 1))
1433
+ )
1434
+ break;
1435
+ const recipient = parseLiveRebindRecipient(value);
1436
+ if (
1437
+ recipient &&
1438
+ Array.isArray(value.updates) &&
1439
+ value.updates.every(isRecord) &&
1440
+ isUniqueSourceUpdateIds(
1441
+ value.updates.map((update) => update.update_id),
1442
+ ) &&
1443
+ typeof value.sentAtMs === "number" &&
1444
+ Number.isFinite(value.sentAtMs)
1445
+ ) {
1446
+ envelope = {
1447
+ kind: "leader.prepareLiveRebind",
1448
+ requestId,
1449
+ ...recipient,
1450
+ updates: value.updates as ({ update_id: number } & Record<
1451
+ string,
1452
+ unknown
1453
+ >)[],
1454
+ sentAtMs: value.sentAtMs,
1455
+ ...(selectedCommand ? { selectedCommand } : {}),
1456
+ };
1457
+ }
1458
+ break;
1459
+ }
1460
+ case "leader.applyLiveRebind":
1461
+ case "leader.settleLiveRebind": {
1462
+ const selectedCommand = parseSelectedCommandInput(value.selectedCommand),
1463
+ preparedSource = parsePreparedCommandSource(value.preparedSource);
1464
+ if (
1465
+ !hasOnlyWireKeys(value, [
1466
+ "kind",
1467
+ "requestId",
1468
+ "auth",
1469
+ "recipientInstanceId",
1470
+ "recipientRegistrationGeneration",
1471
+ "recipientSessionId",
1472
+ "recipientBindingKey",
1473
+ "operationId",
1474
+ "sourceUpdateIds",
1475
+ "mode",
1476
+ "oldTarget",
1477
+ "selectedCommand",
1478
+ "preparedSource",
1479
+ "sentAtMs",
1480
+ ]) ||
1481
+ (Object.hasOwn(value, "preparedSource") && !selectedCommand) ||
1482
+ (value.mode === "observe-command" &&
1483
+ (!selectedCommand || Object.hasOwn(value, "oldTarget")))
1484
+ )
1485
+ break;
1486
+ if (
1487
+ Object.hasOwn(value, "selectedCommand") &&
1488
+ (!selectedCommand ||
1489
+ !Array.isArray(value.sourceUpdateIds) ||
1490
+ value.sourceUpdateIds.length !== 1 ||
1491
+ !preparedSource ||
1492
+ preparedSource.updateId !== value.sourceUpdateIds[0] ||
1493
+ preparedSource.journalBindingKey !== value.recipientBindingKey)
1494
+ )
1495
+ break;
1496
+ const recipient = parseLiveRebindRecipient(value);
1497
+ if (
1498
+ recipient &&
1499
+ (value.kind === "leader.applyLiveRebind"
1500
+ ? value.mode === "apply" || value.mode === "inspect"
1501
+ : value.mode === "release" ||
1502
+ value.mode === "discard" ||
1503
+ value.mode === "observe-command" ||
1504
+ (value.mode === "observe" &&
1505
+ !!parseThreadTarget(value.oldTarget))) &&
1506
+ isUniqueSourceUpdateIds(value.sourceUpdateIds) &&
1507
+ typeof value.sentAtMs === "number" &&
1508
+ Number.isFinite(value.sentAtMs)
1509
+ ) {
1510
+ envelope = {
1511
+ kind: value.kind,
1512
+ requestId,
1513
+ ...recipient,
1514
+ sourceUpdateIds: value.sourceUpdateIds,
1515
+ mode: value.mode as
1516
+ | "apply"
1517
+ | "inspect"
1518
+ | "release"
1519
+ | "discard"
1520
+ | "observe"
1521
+ | "observe-command",
1522
+ sentAtMs: value.sentAtMs,
1523
+ ...(value.mode === "observe"
1524
+ ? { oldTarget: parseThreadTarget(value.oldTarget)! }
1525
+ : {}),
1526
+ ...(selectedCommand ? { selectedCommand, preparedSource } : {}),
1527
+ } as TelegramBusEnvelope;
1528
+ }
1529
+ break;
1530
+ }
1137
1531
  case "leader.workspaceRestore":
1138
- if (typeof value.recipientInstanceId === "string" && value.recipientInstanceId &&
1139
- typeof value.recipientRegistrationGeneration === "string" && value.recipientRegistrationGeneration &&
1140
- typeof value.operationId === "string" && value.operationId.length > 0 && value.operationId.length <= 128 &&
1141
- (value.mode === "apply" || value.mode === "inspect") &&
1142
- typeof value.sentAtMs === "number" && Number.isFinite(value.sentAtMs)) {
1143
- envelope = { kind: "leader.workspaceRestore", requestId, recipientInstanceId: value.recipientInstanceId,
1144
- recipientRegistrationGeneration: value.recipientRegistrationGeneration, operationId: value.operationId,
1145
- mode: value.mode, sentAtMs: value.sentAtMs };
1532
+ if (
1533
+ typeof value.recipientInstanceId === "string" &&
1534
+ value.recipientInstanceId &&
1535
+ typeof value.recipientRegistrationGeneration === "string" &&
1536
+ value.recipientRegistrationGeneration &&
1537
+ typeof value.operationId === "string" &&
1538
+ value.operationId.length > 0 &&
1539
+ value.operationId.length <= 128 &&
1540
+ (value.mode === "apply" || value.mode === "inspect") &&
1541
+ typeof value.sentAtMs === "number" &&
1542
+ Number.isFinite(value.sentAtMs)
1543
+ ) {
1544
+ envelope = {
1545
+ kind: "leader.workspaceRestore",
1546
+ requestId,
1547
+ recipientInstanceId: value.recipientInstanceId,
1548
+ recipientRegistrationGeneration:
1549
+ value.recipientRegistrationGeneration,
1550
+ operationId: value.operationId,
1551
+ mode: value.mode,
1552
+ sentAtMs: value.sentAtMs,
1553
+ };
1146
1554
  }
1147
1555
  break;
1148
1556
  case "leader.offerQueueHandoff":
@@ -1165,6 +1573,9 @@ export function parseTelegramBusEnvelope(
1165
1573
  case "follower.routeAgentMessage":
1166
1574
  envelope = parseRouteAgentMessageEnvelope(value, requestId);
1167
1575
  break;
1576
+ case "follower.deliverSelectedMenu":
1577
+ envelope = parseSelectedMenuDeliveryEnvelope(value, requestId);
1578
+ break;
1168
1579
  case "follower.callApi":
1169
1580
  envelope = parseCallApiEnvelope(value, requestId);
1170
1581
  break;
@@ -1192,7 +1603,10 @@ type TelegramBusServerGlobal = typeof globalThis & {
1192
1603
  [TELEGRAM_ACTIVE_LOCAL_SERVERS]?: Map<string, TelegramBusLocalServer>;
1193
1604
  };
1194
1605
 
1195
- function getActiveTelegramBusLocalServers(): Map<string, TelegramBusLocalServer> {
1606
+ function getActiveTelegramBusLocalServers(): Map<
1607
+ string,
1608
+ TelegramBusLocalServer
1609
+ > {
1196
1610
  const root = globalThis as TelegramBusServerGlobal;
1197
1611
  return (root[TELEGRAM_ACTIVE_LOCAL_SERVERS] ??= new Map());
1198
1612
  }
@@ -1247,7 +1661,22 @@ interface TelegramBusLocalServerDeps {
1247
1661
  ) => boolean;
1248
1662
  }
1249
1663
 
1664
+ /** Local IPC issuance proof only: this callback/error never enters the envelope or certifies a remote API effect. */
1665
+ export class TelegramBusLocalAuthorityError extends Error {
1666
+ readonly requestIssued: boolean;
1667
+ constructor(requestIssued: boolean, reason?: string) {
1668
+ super(
1669
+ reason
1670
+ ? `Telegram bus local authority is unavailable: ${reason}`
1671
+ : "Telegram bus local authority is unavailable.",
1672
+ );
1673
+ this.name = "TelegramBusLocalAuthorityError";
1674
+ this.requestIssued = requestIssued;
1675
+ }
1676
+ }
1677
+
1250
1678
  interface TelegramBusLocalClientOptions {
1679
+ assertAuthority?: () => void;
1251
1680
  socketPath: string;
1252
1681
  envelope: TelegramBusEnvelope;
1253
1682
  timeoutMs?: number;
@@ -1265,13 +1694,21 @@ interface TelegramBusForeignOwnedForwarderDeps<TMessage = unknown> {
1265
1694
  message: TMessage,
1266
1695
  ) => "comment" | "forward" | undefined;
1267
1696
  localProtocolIdentity?: TelegramBusProtocolIdentity;
1268
- validateForwardOwnership?: (ownership: TelegramBusForwardOwnership) => boolean;
1697
+ validateForwardOwnership?: (
1698
+ ownership: TelegramBusForwardOwnership,
1699
+ ) => boolean;
1269
1700
  resolveInputCustodyReference?: (input: {
1270
1701
  sourceUpdateId: number;
1271
1702
  recipientBindingKey: string;
1272
- }) => { sourceRecoveryKey: string; source: {
1273
- updateId: number; owner: { acquisitionId: string; handoffId?: string };
1274
- } } | undefined;
1703
+ }) =>
1704
+ | {
1705
+ sourceRecoveryKey: string;
1706
+ source: {
1707
+ updateId: number;
1708
+ owner: { acquisitionId: string; handoffId?: string };
1709
+ };
1710
+ }
1711
+ | undefined;
1275
1712
  recordRuntimeEvent?: (
1276
1713
  category: string,
1277
1714
  error: unknown,
@@ -1290,10 +1727,14 @@ export function isTelegramBusForwardOwnershipCurrent(
1290
1727
  expected: TelegramBusForwardOwnership,
1291
1728
  current: TelegramBusForwardOwnership | undefined,
1292
1729
  ): boolean {
1293
- return Boolean(current && current.instanceId === expected.instanceId &&
1730
+ return Boolean(
1731
+ current &&
1732
+ current.instanceId === expected.instanceId &&
1294
1733
  current.ownerGeneration === expected.ownerGeneration &&
1295
1734
  current.recipientBindingKey === expected.recipientBindingKey &&
1296
- JSON.stringify(current.protocolIdentity) === JSON.stringify(expected.protocolIdentity));
1735
+ JSON.stringify(current.protocolIdentity) ===
1736
+ JSON.stringify(expected.protocolIdentity),
1737
+ );
1297
1738
  }
1298
1739
 
1299
1740
  type TelegramBusDurableForwardEnvelope = Extract<
@@ -1337,6 +1778,13 @@ function createTelegramBusForwardDelivery(
1337
1778
  });
1338
1779
  }
1339
1780
 
1781
+ /**
1782
+ * Leader-to-follower forwards and control replies share the stale-liveness window: a follower doing journal work or stalled
1783
+ * by an ordinary multi-second Pi/TUI turn, or a slower Windows named pipe, is not a lost recipient.
1784
+ */
1785
+ const TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS =
1786
+ TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
1787
+
1340
1788
  export function createTelegramBusForeignOwnedUpdateForwarder<
1341
1789
  TContext,
1342
1790
  TReactionUpdate,
@@ -1392,14 +1840,16 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1392
1840
  envelopeKind: input.envelopeKind,
1393
1841
  recipientInstanceId: input.ownership.instanceId,
1394
1842
  deliveryId: input.delivery?.deliveryId,
1395
- sourceUpdateId:
1396
- input.delivery?.sourceUpdateId ?? input.sourceUpdateId,
1843
+ sourceUpdateId: input.delivery?.sourceUpdateId ?? input.sourceUpdateId,
1397
1844
  });
1398
1845
  return settlement;
1399
1846
  };
1400
1847
  const prepare = (
1401
- kind: "leader.forwardCallback" | "leader.forwardReaction" |
1402
- "leader.forwardMessage" | "leader.forwardEditedMessage",
1848
+ kind:
1849
+ | "leader.forwardCallback"
1850
+ | "leader.forwardReaction"
1851
+ | "leader.forwardMessage"
1852
+ | "leader.forwardEditedMessage",
1403
1853
  value: unknown,
1404
1854
  ownership: TelegramBusForwardOwnership,
1405
1855
  ):
@@ -1434,27 +1884,48 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1434
1884
  };
1435
1885
  }
1436
1886
  const sourceReference = canUseTelegramBusInputCustodyReference({
1437
- local: deps.localProtocolIdentity, remote: ownership.protocolIdentity });
1438
- const reference = sourceReference ? deps.resolveInputCustodyReference?.({
1439
- sourceUpdateId, recipientBindingKey: ownership.recipientBindingKey }) : undefined;
1440
- if (sourceReference && !reference) return { settlement: reject({
1441
- status: "retryable", failureClass: "source-reference-missing",
1442
- message: "Forwarded Telegram update has no exact custody reference.",
1443
- envelopeKind: kind, ownership, sourceUpdateId }) };
1444
- const delivery = reference
1445
- ? createTelegramBusFollowerSourceReferenceDeliveryIdentity({
1446
- kind: "leader.wakeInputCustody", recipientBindingKey: ownership.recipientBindingKey,
1447
- sourceRecoveryKey: reference.sourceRecoveryKey, source: reference.source })
1448
- : createTelegramBusForwardDelivery(kind, sourceUpdateId, ownership.recipientBindingKey);
1449
- if (!ownership.ownerGeneration) {
1887
+ local: deps.localProtocolIdentity,
1888
+ remote: ownership.protocolIdentity,
1889
+ });
1890
+ const reference = sourceReference
1891
+ ? deps.resolveInputCustodyReference?.({
1892
+ sourceUpdateId,
1893
+ recipientBindingKey: ownership.recipientBindingKey,
1894
+ })
1895
+ : undefined;
1896
+ if (sourceReference && !reference)
1450
1897
  return {
1451
1898
  settlement: reject({
1452
1899
  status: "retryable",
1453
- failureClass: "recipient-generation-missing",
1454
- message: "Forwarded Telegram update has no live recipient generation.",
1900
+ failureClass: "source-reference-missing",
1901
+ message: "Forwarded Telegram update has no exact custody reference.",
1455
1902
  envelopeKind: kind,
1456
1903
  ownership,
1457
- delivery,
1904
+ sourceUpdateId,
1905
+ }),
1906
+ };
1907
+ const delivery = reference
1908
+ ? createTelegramBusFollowerSourceReferenceDeliveryIdentity({
1909
+ kind: "leader.wakeInputCustody",
1910
+ recipientBindingKey: ownership.recipientBindingKey,
1911
+ sourceRecoveryKey: reference.sourceRecoveryKey,
1912
+ source: reference.source,
1913
+ })
1914
+ : createTelegramBusForwardDelivery(
1915
+ kind,
1916
+ sourceUpdateId,
1917
+ ownership.recipientBindingKey,
1918
+ );
1919
+ if (!ownership.ownerGeneration) {
1920
+ return {
1921
+ settlement: reject({
1922
+ status: "retryable",
1923
+ failureClass: "recipient-generation-missing",
1924
+ message:
1925
+ "Forwarded Telegram update has no live recipient generation.",
1926
+ envelopeKind: kind,
1927
+ ownership,
1928
+ delivery,
1458
1929
  }),
1459
1930
  };
1460
1931
  }
@@ -1468,10 +1939,18 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1468
1939
  envelope: TelegramBusDurableForwardEnvelope,
1469
1940
  ownership: TelegramBusForwardOwnership,
1470
1941
  ): Promise<TelegramBusForeignUpdateSettlement> => {
1471
- if (deps.validateForwardOwnership && !deps.validateForwardOwnership(ownership)) return reject({
1472
- status: "retryable", failureClass: "recipient-ownership-stale",
1473
- message: "Telegram follower ownership changed before forwarding.",
1474
- envelopeKind: envelope.kind, ownership, delivery: envelope.delivery });
1942
+ if (
1943
+ deps.validateForwardOwnership &&
1944
+ !deps.validateForwardOwnership(ownership)
1945
+ )
1946
+ return reject({
1947
+ status: "retryable",
1948
+ failureClass: "recipient-ownership-stale",
1949
+ message: "Telegram follower ownership changed before forwarding.",
1950
+ envelopeKind: envelope.kind,
1951
+ ownership,
1952
+ delivery: envelope.delivery,
1953
+ });
1475
1954
  if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
1476
1955
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
1477
1956
  let response: TelegramBusEnvelope | undefined;
@@ -1479,7 +1958,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1479
1958
  response = await sendTelegramBusLocalEnvelope({
1480
1959
  socketPath,
1481
1960
  envelope,
1482
- timeoutMs: deps.timeoutMs,
1961
+ timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
1483
1962
  retry: getTelegramBusTransportRetryPolicy({
1484
1963
  endpoint: socketPath,
1485
1964
  operation: "operation",
@@ -1551,7 +2030,8 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1551
2030
  return reject({
1552
2031
  status: "terminal-rejected",
1553
2032
  failureClass: "durable-receipt-mismatched",
1554
- message: "Follower acknowledgement returned a mismatched durable receipt.",
2033
+ message:
2034
+ "Follower acknowledgement returned a mismatched durable receipt.",
1555
2035
  envelopeKind: envelope.kind,
1556
2036
  ownership,
1557
2037
  delivery: envelope.delivery,
@@ -1559,105 +2039,88 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1559
2039
  }
1560
2040
  return { status: "accepted", delivery: envelope.delivery };
1561
2041
  };
2042
+ type ForwardKind = Parameters<typeof prepare>[0];
2043
+ type ForwardBase = Pick<
2044
+ Extract<
2045
+ TelegramBusDurableForwardEnvelope,
2046
+ { kind: "leader.wakeInputCustody" }
2047
+ >,
2048
+ | "requestId"
2049
+ | "recipientInstanceId"
2050
+ | "recipientRegistrationGeneration"
2051
+ | "delivery"
2052
+ >;
2053
+ // A recipient already holding the source reference is only woken; it never receives a second payload copy.
2054
+ const forward = (
2055
+ kind: ForwardKind,
2056
+ value: unknown,
2057
+ ownership: TelegramBusForwardOwnership,
2058
+ build: (
2059
+ base: ForwardBase,
2060
+ sentAtMs: number,
2061
+ ) => TelegramBusDurableForwardEnvelope,
2062
+ ): Promise<TelegramBusForeignUpdateSettlement> => {
2063
+ const prepared = prepare(kind, value, ownership);
2064
+ if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
2065
+ const base = {
2066
+ requestId: deps.createRequestId(),
2067
+ recipientInstanceId: ownership.instanceId,
2068
+ recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
2069
+ delivery: prepared.delivery,
2070
+ };
2071
+ return send(
2072
+ prepared.sourceReference
2073
+ ? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
2074
+ : build(base, getNowMs()),
2075
+ ownership,
2076
+ );
2077
+ };
1562
2078
  return {
1563
- forwardCallback: ({ query, ownership }) => {
1564
- const prepared = prepare("leader.forwardCallback", query, ownership);
1565
- if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1566
- if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1567
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1568
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1569
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1570
- return send(
1571
- {
1572
- kind: "leader.forwardCallback",
1573
- requestId: deps.createRequestId(),
1574
- recipientInstanceId: ownership.instanceId,
1575
- recipientRegistrationGeneration:
1576
- prepared.recipientRegistrationGeneration,
1577
- delivery: prepared.delivery,
1578
- query,
1579
- sentAtMs: getNowMs(),
1580
- },
1581
- ownership,
1582
- );
1583
- },
1584
- forwardReaction: ({ reactionUpdate, ownership }) => {
1585
- const prepared = prepare(
2079
+ forwardCallback: ({ query, ownership }) =>
2080
+ forward("leader.forwardCallback", query, ownership, (base, sentAtMs) => ({
2081
+ kind: "leader.forwardCallback",
2082
+ ...base,
2083
+ query,
2084
+ sentAtMs,
2085
+ })),
2086
+ forwardReaction: ({ reactionUpdate, ownership }) =>
2087
+ forward(
1586
2088
  "leader.forwardReaction",
1587
2089
  reactionUpdate,
1588
2090
  ownership,
1589
- );
1590
- if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1591
- if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1592
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1593
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1594
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1595
- return send(
1596
- {
2091
+ (base, sentAtMs) => ({
1597
2092
  kind: "leader.forwardReaction",
1598
- requestId: deps.createRequestId(),
1599
- recipientInstanceId: ownership.instanceId,
1600
- recipientRegistrationGeneration:
1601
- prepared.recipientRegistrationGeneration,
1602
- delivery: prepared.delivery,
2093
+ ...base,
1603
2094
  reactionUpdate,
1604
- sentAtMs: getNowMs(),
1605
- },
1606
- ownership,
1607
- );
1608
- },
1609
- forwardMessage: ({ message, ownership }) => {
1610
- const prepared = prepare("leader.forwardMessage", message, ownership);
1611
- if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1612
- if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1613
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1614
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1615
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1616
- return send(
1617
- {
2095
+ sentAtMs,
2096
+ }),
2097
+ ),
2098
+ forwardMessage: ({ message, ownership }) =>
2099
+ forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
2100
+ const forwardCommentBatchPosition =
2101
+ deps.getForwardCommentBatchPosition?.(message);
2102
+ return {
1618
2103
  kind: "leader.forwardMessage",
1619
- requestId: deps.createRequestId(),
1620
- recipientInstanceId: ownership.instanceId,
1621
- recipientRegistrationGeneration:
1622
- prepared.recipientRegistrationGeneration,
1623
- delivery: prepared.delivery,
2104
+ ...base,
1624
2105
  message,
1625
- ...(deps.getForwardCommentBatchPosition?.(message) !== undefined
1626
- ? {
1627
- forwardCommentBatchPosition:
1628
- deps.getForwardCommentBatchPosition(message),
1629
- }
2106
+ ...(forwardCommentBatchPosition !== undefined
2107
+ ? { forwardCommentBatchPosition }
1630
2108
  : {}),
1631
- sentAtMs: getNowMs(),
1632
- },
1633
- ownership,
1634
- );
1635
- },
1636
- forwardEditedMessage: ({ message, ownership }) => {
1637
- const prepared = prepare(
2109
+ sentAtMs,
2110
+ };
2111
+ }),
2112
+ forwardEditedMessage: ({ message, ownership }) =>
2113
+ forward(
1638
2114
  "leader.forwardEditedMessage",
1639
2115
  message,
1640
2116
  ownership,
1641
- );
1642
- if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1643
- if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1644
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1645
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1646
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1647
- return send(
1648
- {
2117
+ (base, sentAtMs) => ({
1649
2118
  kind: "leader.forwardEditedMessage",
1650
- requestId: deps.createRequestId(),
1651
- recipientInstanceId: ownership.instanceId,
1652
- recipientRegistrationGeneration:
1653
- prepared.recipientRegistrationGeneration,
1654
- delivery: prepared.delivery,
2119
+ ...base,
1655
2120
  message,
1656
- sentAtMs: getNowMs(),
1657
- },
1658
- ownership,
1659
- );
1660
- },
2121
+ sentAtMs,
2122
+ }),
2123
+ ),
1661
2124
  };
1662
2125
  }
1663
2126
 
@@ -1683,50 +2146,573 @@ export function createTelegramBusWorkspaceRestoreController(deps: {
1683
2146
  getAuthSecret: () => string | undefined;
1684
2147
  timeoutMs?: number;
1685
2148
  }) {
1686
- return async (input: { operationId: string; instanceId: string; sessionId: string; slot: string;
1687
- target: TelegramTarget & { threadId: number }; oldTarget: TelegramTarget & { threadId: number };
1688
- mode: "apply" | "inspect"; isCurrent: () => boolean }): Promise<TelegramBusWorkspaceRestoreObservation | undefined> => {
2149
+ return async (input: {
2150
+ operationId: string;
2151
+ instanceId: string;
2152
+ sessionId: string;
2153
+ slot: string;
2154
+ target: TelegramTarget & { threadId: number };
2155
+ oldTarget: TelegramTarget & { threadId: number };
2156
+ mode: "apply" | "inspect";
2157
+ isCurrent: () => boolean;
2158
+ }): Promise<TelegramBusWorkspaceRestoreObservation | undefined> => {
1689
2159
  const isCurrent = input.isCurrent.bind(input);
1690
2160
  if (!isCurrent()) return undefined;
1691
2161
  const follower = deps.getFollower(input.instanceId);
1692
2162
  const secret = deps.getAuthSecret();
1693
- if (!secret || !follower?.registrationGeneration || !follower.busSocketPath ||
1694
- follower.sessionId !== input.sessionId || follower.slot !== input.slot ||
1695
- !hasTelegramBusCapability(deps.localProtocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
1696
- !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
1697
- !getTelegramBusProtocolCompatibility({ local: deps.localProtocolIdentity, remote: follower.protocol }).compatible) return undefined;
2163
+ if (
2164
+ !secret ||
2165
+ !follower?.registrationGeneration ||
2166
+ !follower.busSocketPath ||
2167
+ follower.sessionId !== input.sessionId ||
2168
+ follower.slot !== input.slot ||
2169
+ !hasTelegramBusSharedCapabilities(
2170
+ deps.localProtocolIdentity,
2171
+ follower.protocol,
2172
+ [TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE],
2173
+ )
2174
+ )
2175
+ return undefined;
1698
2176
  const captured = structuredClone(follower);
1699
- const expected = { operationId: input.operationId, sessionId: input.sessionId, slot: input.slot,
1700
- target: { ...input.target }, oldTarget: { ...input.oldTarget }, mode: input.mode };
2177
+ const expected = {
2178
+ operationId: input.operationId,
2179
+ sessionId: input.sessionId,
2180
+ slot: input.slot,
2181
+ target: { ...input.target },
2182
+ oldTarget: { ...input.oldTarget },
2183
+ mode: input.mode,
2184
+ };
1701
2185
  const current = (): boolean => {
1702
- if (!isCurrent() || deps.getAuthSecret() !== secret ||
1703
- !hasTelegramBusCapability(deps.localProtocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE)) return false;
2186
+ if (
2187
+ !isCurrent() ||
2188
+ deps.getAuthSecret() !== secret ||
2189
+ !hasTelegramBusCapability(
2190
+ deps.localProtocolIdentity,
2191
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
2192
+ )
2193
+ )
2194
+ return false;
1704
2195
  const live = deps.getFollower(captured.instanceId);
1705
- return !!live && live.registrationGeneration === captured.registrationGeneration &&
1706
- live.sessionId === captured.sessionId && live.cwd === captured.cwd && live.slot === captured.slot &&
1707
- live.busSocketPath === captured.busSocketPath && live.target?.chatId === captured.target?.chatId &&
2196
+ return (
2197
+ !!live &&
2198
+ live.registrationGeneration === captured.registrationGeneration &&
2199
+ live.sessionId === captured.sessionId &&
2200
+ live.cwd === captured.cwd &&
2201
+ live.slot === captured.slot &&
2202
+ live.busSocketPath === captured.busSocketPath &&
2203
+ live.target?.chatId === captured.target?.chatId &&
1708
2204
  live.target?.threadId === captured.target?.threadId &&
1709
- hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
1710
- getTelegramBusProtocolCompatibility({ local: deps.localProtocolIdentity, remote: live.protocol }).compatible;
2205
+ hasTelegramBusCapability(
2206
+ live.protocol,
2207
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
2208
+ ) &&
2209
+ getTelegramBusProtocolCompatibility({
2210
+ local: deps.localProtocolIdentity,
2211
+ remote: live.protocol,
2212
+ }).compatible
2213
+ );
1711
2214
  };
1712
2215
  if (!current()) return undefined;
1713
2216
  const requestId = deps.createRequestId();
1714
- const response = await sendTelegramBusLocalEnvelope({ socketPath: captured.busSocketPath!, timeoutMs: deps.timeoutMs,
1715
- retry: { attempts: 1, delayMs: 0 }, envelope: { kind: "leader.workspaceRestore", requestId,
1716
- recipientInstanceId: captured.instanceId, recipientRegistrationGeneration: captured.registrationGeneration!,
1717
- operationId: expected.operationId, mode: expected.mode, auth: secret, sentAtMs: Date.now() } });
1718
- if (!current() || response?.kind !== "bus.ack" || response.requestId !== requestId || response.ok !== true ||
1719
- !response.result || typeof response.result !== "object") return undefined;
2217
+ const response = await sendTelegramBusLocalEnvelope({
2218
+ socketPath: captured.busSocketPath!,
2219
+ timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
2220
+ retry: { attempts: 1, delayMs: 0 },
2221
+ envelope: {
2222
+ kind: "leader.workspaceRestore",
2223
+ requestId,
2224
+ recipientInstanceId: captured.instanceId,
2225
+ recipientRegistrationGeneration: captured.registrationGeneration!,
2226
+ operationId: expected.operationId,
2227
+ mode: expected.mode,
2228
+ auth: secret,
2229
+ sentAtMs: Date.now(),
2230
+ },
2231
+ });
2232
+ if (
2233
+ !current() ||
2234
+ response?.kind !== "bus.ack" ||
2235
+ response.requestId !== requestId ||
2236
+ response.ok !== true ||
2237
+ !response.result ||
2238
+ typeof response.result !== "object"
2239
+ )
2240
+ return undefined;
1720
2241
  const result = response.result as Record<string, unknown>;
1721
2242
  const recipient = result.recipient as Record<string, unknown> | undefined;
1722
2243
  const target = parseThreadTarget(result.target);
1723
2244
  const wanted = result.ready === true ? expected.target : expected.oldTarget;
1724
- if (result.operationId !== expected.operationId || typeof result.ready !== "boolean" || result.slot !== expected.slot ||
1725
- !target || target.chatId !== wanted.chatId || target.threadId !== wanted.threadId ||
1726
- recipient?.kind !== "follower" || recipient.instanceId !== captured.instanceId ||
1727
- recipient.sessionId !== expected.sessionId || recipient.generation !== captured.registrationGeneration) return undefined;
1728
- return { operationId: expected.operationId, recipient: { kind: "follower", instanceId: captured.instanceId,
1729
- sessionId: expected.sessionId, generation: captured.registrationGeneration! }, target, slot: expected.slot, ready: result.ready };
2245
+ if (
2246
+ result.operationId !== expected.operationId ||
2247
+ typeof result.ready !== "boolean" ||
2248
+ result.slot !== expected.slot ||
2249
+ !target ||
2250
+ target.chatId !== wanted.chatId ||
2251
+ target.threadId !== wanted.threadId ||
2252
+ recipient?.kind !== "follower" ||
2253
+ recipient.instanceId !== captured.instanceId ||
2254
+ recipient.sessionId !== expected.sessionId ||
2255
+ recipient.generation !== captured.registrationGeneration
2256
+ )
2257
+ return undefined;
2258
+ return {
2259
+ operationId: expected.operationId,
2260
+ recipient: {
2261
+ kind: "follower",
2262
+ instanceId: captured.instanceId,
2263
+ sessionId: expected.sessionId,
2264
+ generation: captured.registrationGeneration!,
2265
+ },
2266
+ target,
2267
+ slot: expected.slot,
2268
+ ready: result.ready,
2269
+ };
2270
+ };
2271
+ }
2272
+
2273
+ /** Each capability has its own effect boundary; transport retries never replay save/dispatch/disposal. */
2274
+ export function createTelegramBusLiveRebindController(deps: {
2275
+ getFollower(instanceId: string): TelegramBusFollowerView | undefined;
2276
+ localProtocolIdentity: TelegramBusProtocolIdentity;
2277
+ createRequestId(): string;
2278
+ getAuthSecret(): string | undefined;
2279
+ timeoutMs?: number;
2280
+ }) {
2281
+ return async (
2282
+ input: {
2283
+ operationId: string;
2284
+ instanceId: string;
2285
+ sessionId: string;
2286
+ recipientBindingKey: string;
2287
+ isCurrent(): boolean;
2288
+ selectedCommand?: TelegramBusSelectedCommandInput;
2289
+ preparedSource?: TelegramBusPreparedCommandSource;
2290
+ } & (
2291
+ | { updates: ({ update_id: number } & Record<string, unknown>)[] }
2292
+ | {
2293
+ mode:
2294
+ | "apply"
2295
+ | "inspect"
2296
+ | "release"
2297
+ | "discard"
2298
+ | "observe"
2299
+ | "observe-command";
2300
+ sourceUpdateIds: number[];
2301
+ slot: string;
2302
+ target: TelegramTarget & { threadId: number };
2303
+ oldTarget: TelegramTarget & { threadId: number };
2304
+ }
2305
+ ),
2306
+ ): Promise<
2307
+ | TelegramBusLiveRebindSaveObservation
2308
+ | TelegramBusLiveRebindApplyObservation
2309
+ | TelegramBusLiveRebindSettlementObservation
2310
+ | TelegramBusLiveRebindWorkObservation
2311
+ | TelegramBusLiveRebindCommandObservation
2312
+ | undefined
2313
+ > => {
2314
+ const isCurrent = input.isCurrent.bind(input),
2315
+ localProtocol = structuredClone(deps.localProtocolIdentity);
2316
+ const selectedCommand = parseSelectedCommandInput(input.selectedCommand),
2317
+ held = selectedCommand,
2318
+ preparedSource = parsePreparedCommandSource(input.preparedSource);
2319
+ if (
2320
+ !hasOnlyWireKeys(input, [
2321
+ "operationId",
2322
+ "instanceId",
2323
+ "sessionId",
2324
+ "recipientBindingKey",
2325
+ "isCurrent",
2326
+ "selectedCommand",
2327
+ "preparedSource",
2328
+ "updates",
2329
+ "mode",
2330
+ "sourceUpdateIds",
2331
+ "slot",
2332
+ "target",
2333
+ "oldTarget",
2334
+ ]) ||
2335
+ ("mode" in input &&
2336
+ (![
2337
+ "apply",
2338
+ "inspect",
2339
+ "release",
2340
+ "discard",
2341
+ "observe",
2342
+ "observe-command",
2343
+ ].includes(input.mode) ||
2344
+ (input.mode === "observe-command" && !selectedCommand)))
2345
+ )
2346
+ return undefined;
2347
+ if (
2348
+ (input.preparedSource !== undefined &&
2349
+ (!held ||
2350
+ !preparedSource ||
2351
+ preparedSource.journalBindingKey !== input.recipientBindingKey ||
2352
+ preparedSource.updateId !==
2353
+ ("mode" in input
2354
+ ? input.sourceUpdateIds[0]
2355
+ : input.updates[0]?.update_id))) ||
2356
+ (held && "mode" in input && !preparedSource)
2357
+ )
2358
+ return undefined;
2359
+ if (
2360
+ input.selectedCommand !== undefined &&
2361
+ (!held ||
2362
+ ("mode" in input
2363
+ ? input.sourceUpdateIds.length !== 1 ||
2364
+ !areTelegramTargetsEqual(input.target, held.target)
2365
+ : input.updates.length !== 1))
2366
+ )
2367
+ return undefined;
2368
+ const marker = selectedCommand ? { selectedCommand } : {};
2369
+ const expected = structuredClone({
2370
+ operationId: input.operationId,
2371
+ instanceId: input.instanceId,
2372
+ sessionId: input.sessionId,
2373
+ recipientBindingKey: input.recipientBindingKey,
2374
+ ...(held
2375
+ ? { ...marker, ...(preparedSource ? { preparedSource } : {}) }
2376
+ : {}),
2377
+ ...("mode" in input
2378
+ ? {
2379
+ mode: input.mode,
2380
+ sourceUpdateIds: input.sourceUpdateIds,
2381
+ slot: input.slot,
2382
+ target: input.target,
2383
+ oldTarget: input.oldTarget,
2384
+ }
2385
+ : { updates: input.updates }),
2386
+ });
2387
+ const observing = "mode" in expected && expected.mode === "observe";
2388
+ const commandObserving =
2389
+ "mode" in expected && expected.mode === "observe-command";
2390
+ const settling =
2391
+ "mode" in expected &&
2392
+ (expected.mode === "release" ||
2393
+ expected.mode === "discard" ||
2394
+ observing ||
2395
+ commandObserving);
2396
+ const required = [
2397
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
2398
+ ...("mode" in expected || held
2399
+ ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY]
2400
+ : []),
2401
+ ...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
2402
+ ...(held
2403
+ ? [
2404
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
2405
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
2406
+ ]
2407
+ : []),
2408
+ ];
2409
+ const follower = deps.getFollower(expected.instanceId),
2410
+ secret = deps.getAuthSecret();
2411
+ if (
2412
+ !isCurrent() ||
2413
+ !secret ||
2414
+ !follower?.registrationGeneration ||
2415
+ !follower.busSocketPath ||
2416
+ follower.sessionId !== expected.sessionId ||
2417
+ !hasTelegramBusSharedCapabilities(
2418
+ deps.localProtocolIdentity,
2419
+ follower.protocol,
2420
+ required,
2421
+ )
2422
+ )
2423
+ return undefined;
2424
+ const captured = structuredClone(follower);
2425
+ /** The first failed authority condition, by fixed label; undefined while the captured recipient is current. */
2426
+ const staleReason = (): string | undefined => {
2427
+ if (!isCurrent()) return "caller authority";
2428
+ if (deps.getAuthSecret() !== secret) return "auth secret";
2429
+ if (
2430
+ !isDeepStrictEqual(deps.localProtocolIdentity, localProtocol) ||
2431
+ !required.every((cap) =>
2432
+ hasTelegramBusCapability(deps.localProtocolIdentity, cap),
2433
+ )
2434
+ )
2435
+ return "local protocol";
2436
+ const live = deps.getFollower(expected.instanceId);
2437
+ if (!live) return "follower registration";
2438
+ if (
2439
+ live.sessionId !== captured.sessionId ||
2440
+ live.registrationGeneration !== captured.registrationGeneration ||
2441
+ live.sessionGeneration !== captured.sessionGeneration
2442
+ )
2443
+ return "follower generation";
2444
+ if (
2445
+ live.pid !== captured.pid ||
2446
+ live.processBirthId !== captured.processBirthId
2447
+ )
2448
+ return "follower process";
2449
+ if (
2450
+ live.busSocketPath !== captured.busSocketPath ||
2451
+ live.slot !== captured.slot ||
2452
+ live.cwd !== captured.cwd
2453
+ )
2454
+ return "follower endpoint";
2455
+ if (
2456
+ "mode" in expected
2457
+ ? live.slot !== expected.slot ||
2458
+ !live.target ||
2459
+ (!areTelegramTargetsEqual(live.target, expected.target!) &&
2460
+ !areTelegramTargetsEqual(live.target, expected.oldTarget!))
2461
+ : !isDeepStrictEqual(live.target, captured.target)
2462
+ )
2463
+ return "follower target";
2464
+ if (
2465
+ !required.every((cap) =>
2466
+ hasTelegramBusCapability(live.protocol, cap),
2467
+ ) ||
2468
+ !isDeepStrictEqual(live.protocol, captured.protocol) ||
2469
+ !getTelegramBusProtocolCompatibility({
2470
+ local: deps.localProtocolIdentity,
2471
+ remote: live.protocol,
2472
+ }).compatible
2473
+ )
2474
+ return "follower protocol";
2475
+ return undefined;
2476
+ };
2477
+ const current = () => staleReason() === undefined;
2478
+ if (!current()) return undefined;
2479
+ const requestId = deps.createRequestId();
2480
+ const response = await sendTelegramBusLocalEnvelope({
2481
+ socketPath: captured.busSocketPath!,
2482
+ timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
2483
+ retry: { attempts: 1, delayMs: 0 },
2484
+ ...(held
2485
+ ? {
2486
+ assertAuthority: () => {
2487
+ const reason = staleReason();
2488
+ if (reason)
2489
+ throw new Error(
2490
+ `Selected held input sender authority changed (${reason}).`,
2491
+ );
2492
+ },
2493
+ }
2494
+ : {}),
2495
+ envelope: {
2496
+ requestId,
2497
+ auth: secret,
2498
+ ...(held
2499
+ ? {
2500
+ ...marker,
2501
+ ...("mode" in expected
2502
+ ? { preparedSource: expected.preparedSource }
2503
+ : {}),
2504
+ }
2505
+ : {}),
2506
+ recipientInstanceId: expected.instanceId,
2507
+ recipientRegistrationGeneration: captured.registrationGeneration!,
2508
+ recipientSessionId: expected.sessionId,
2509
+ recipientBindingKey: expected.recipientBindingKey,
2510
+ operationId: expected.operationId,
2511
+ sentAtMs: Date.now(),
2512
+ ...("mode" in expected
2513
+ ? settling
2514
+ ? {
2515
+ kind: "leader.settleLiveRebind" as const,
2516
+ mode: expected.mode as
2517
+ "release" | "discard" | "observe" | "observe-command",
2518
+ sourceUpdateIds: expected.sourceUpdateIds!,
2519
+ ...(observing ? { oldTarget: expected.oldTarget } : {}),
2520
+ }
2521
+ : {
2522
+ kind: "leader.applyLiveRebind" as const,
2523
+ mode: expected.mode as "apply" | "inspect",
2524
+ sourceUpdateIds: expected.sourceUpdateIds!,
2525
+ }
2526
+ : {
2527
+ kind: "leader.prepareLiveRebind" as const,
2528
+ updates: expected.updates!,
2529
+ }),
2530
+ },
2531
+ });
2532
+ if (
2533
+ !current() ||
2534
+ response?.kind !== "bus.ack" ||
2535
+ response.requestId !== requestId ||
2536
+ response.ok !== true ||
2537
+ !isRecord(response.result)
2538
+ )
2539
+ return undefined;
2540
+ const result = response.result,
2541
+ recipient = result.recipient;
2542
+ if (
2543
+ !hasOnlyWireKeys(result, [
2544
+ "operationId",
2545
+ "recipient",
2546
+ "sourceUpdateIds",
2547
+ "selectedCommand",
2548
+ "preparedSource",
2549
+ "status",
2550
+ "command",
2551
+ "sourceAck",
2552
+ "target",
2553
+ "slot",
2554
+ "action",
2555
+ "oldTarget",
2556
+ "work",
2557
+ ])
2558
+ )
2559
+ return undefined;
2560
+ if (
2561
+ selectedCommand
2562
+ ? !isDeepStrictEqual(
2563
+ parseSelectedCommandInput(result.selectedCommand),
2564
+ selectedCommand,
2565
+ )
2566
+ : Object.hasOwn(result, "selectedCommand")
2567
+ )
2568
+ return undefined;
2569
+ const ids = (
2570
+ "mode" in expected
2571
+ ? [...expected.sourceUpdateIds!]
2572
+ : expected.updates!.map((update) => update.update_id)
2573
+ ).sort((a, b) => a - b);
2574
+ const observedSource = parsePreparedCommandSource(result.preparedSource);
2575
+ if (
2576
+ held
2577
+ ? !observedSource ||
2578
+ observedSource.updateId !== ids[0] ||
2579
+ observedSource.journalBindingKey !== expected.recipientBindingKey ||
2580
+ (expected.preparedSource &&
2581
+ !isDeepStrictEqual(observedSource, expected.preparedSource))
2582
+ : Object.hasOwn(result, "preparedSource")
2583
+ )
2584
+ return undefined;
2585
+ if (
2586
+ result.operationId !== expected.operationId ||
2587
+ !(commandObserving
2588
+ ? result.status === "command-observed"
2589
+ : observing
2590
+ ? result.status === "observed"
2591
+ : settling
2592
+ ? result.status === "protected" ||
2593
+ result.status === "unknown" ||
2594
+ result.status ===
2595
+ (expected.mode === "release" ? "released" : "discarded")
2596
+ : "mode" in expected
2597
+ ? result.status === "saved" || result.status === "applied"
2598
+ : result.status === "saved") ||
2599
+ !isRecord(recipient) ||
2600
+ recipient.instanceId !== expected.instanceId ||
2601
+ recipient.sessionId !== expected.sessionId ||
2602
+ recipient.generation !== captured.registrationGeneration ||
2603
+ recipient.bindingKey !== expected.recipientBindingKey ||
2604
+ !Array.isArray(result.sourceUpdateIds) ||
2605
+ result.sourceUpdateIds.length !== ids.length ||
2606
+ result.sourceUpdateIds.some((id, index) => id !== ids[index])
2607
+ )
2608
+ return undefined;
2609
+ const observation: TelegramBusLiveRebindSaveObservation = {
2610
+ operationId: expected.operationId,
2611
+ recipient: {
2612
+ instanceId: expected.instanceId,
2613
+ sessionId: expected.sessionId,
2614
+ generation: captured.registrationGeneration!,
2615
+ bindingKey: expected.recipientBindingKey,
2616
+ },
2617
+ sourceUpdateIds: ids,
2618
+ ...(held
2619
+ ? { ...structuredClone(marker), preparedSource: { ...observedSource! } }
2620
+ : {}),
2621
+ status: "saved",
2622
+ };
2623
+ if (!("mode" in expected)) return observation;
2624
+ if (commandObserving) {
2625
+ const sourceAck = parsePreparedCommandSource(result.sourceAck);
2626
+ if (
2627
+ !hasOnlyWireKeys(result, [
2628
+ "operationId",
2629
+ "recipient",
2630
+ "sourceUpdateIds",
2631
+ "selectedCommand",
2632
+ "preparedSource",
2633
+ "status",
2634
+ "command",
2635
+ "sourceAck",
2636
+ ]) ||
2637
+ !hasOnlyWireKeys(recipient, [
2638
+ "instanceId",
2639
+ "sessionId",
2640
+ "generation",
2641
+ "bindingKey",
2642
+ ]) ||
2643
+ !["completed", "unknown", "not-issued"].includes(
2644
+ result.command as string,
2645
+ ) ||
2646
+ (Object.hasOwn(result, "sourceAck") &&
2647
+ (!sourceAck ||
2648
+ !isDeepStrictEqual(sourceAck, expected.preparedSource) ||
2649
+ result.command === "not-issued"))
2650
+ )
2651
+ return undefined;
2652
+ const settled = {
2653
+ command:
2654
+ result.command as TelegramBusLiveRebindCommandObservation["command"],
2655
+ ...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
2656
+ };
2657
+ return {
2658
+ ...observation,
2659
+ status: "command-observed",
2660
+ selectedCommand: structuredClone(selectedCommand!),
2661
+ preparedSource: { ...observedSource! },
2662
+ ...settled,
2663
+ };
2664
+ }
2665
+ if (observing) {
2666
+ const work = result.work,
2667
+ oldTarget = parseThreadTarget(result.oldTarget);
2668
+ if (
2669
+ !oldTarget ||
2670
+ !isDeepStrictEqual(oldTarget, expected.oldTarget) ||
2671
+ !isRecord(work) ||
2672
+ [
2673
+ work.sessionBusy,
2674
+ work.targetWork,
2675
+ work.deliveryPending,
2676
+ work.unknown,
2677
+ ].some((flag) => typeof flag !== "boolean")
2678
+ )
2679
+ return undefined;
2680
+ return {
2681
+ ...observation,
2682
+ status: "observed",
2683
+ oldTarget,
2684
+ work: {
2685
+ sessionBusy: work.sessionBusy as boolean,
2686
+ targetWork: work.targetWork as boolean,
2687
+ deliveryPending: work.deliveryPending as boolean,
2688
+ unknown: work.unknown as boolean,
2689
+ },
2690
+ };
2691
+ }
2692
+ if (settling) {
2693
+ if (result.action !== expected.mode) return undefined;
2694
+ return {
2695
+ ...observation,
2696
+ action: expected.mode as "release" | "discard",
2697
+ status:
2698
+ result.status as TelegramBusLiveRebindSettlementObservation["status"],
2699
+ };
2700
+ }
2701
+ const target = parseThreadTarget(result.target),
2702
+ wanted =
2703
+ result.status === "applied" ? expected.target : expected.oldTarget;
2704
+ if (
2705
+ !target ||
2706
+ !isDeepStrictEqual(target, wanted) ||
2707
+ result.slot !== expected.slot
2708
+ )
2709
+ return undefined;
2710
+ return {
2711
+ ...observation,
2712
+ status: result.status as "saved" | "applied",
2713
+ target,
2714
+ slot: expected.slot!,
2715
+ };
1730
2716
  };
1731
2717
  }
1732
2718
 
@@ -1741,15 +2727,21 @@ export function isTelegramBusEnvelopeAuthorized(
1741
2727
  return auth.length === expected.length && timingSafeEqual(auth, expected);
1742
2728
  }
1743
2729
 
2730
+ /** Negative acknowledgement carrying only a diagnostic message. */
2731
+ export function rejectTelegramBusRequest(
2732
+ requestId: string,
2733
+ message: string,
2734
+ ): Extract<TelegramBusEnvelope, { kind: "bus.ack" }> {
2735
+ return { kind: "bus.ack", requestId, ok: false, message };
2736
+ }
2737
+
1744
2738
  export function createUnauthorizedBusAck(
1745
2739
  requestId: string,
1746
2740
  ): TelegramBusEnvelope {
1747
- return {
1748
- kind: "bus.ack",
2741
+ return rejectTelegramBusRequest(
1749
2742
  requestId,
1750
- ok: false,
1751
- message: "Unauthorized Telegram bus envelope.",
1752
- };
2743
+ "Unauthorized Telegram bus envelope.",
2744
+ );
1753
2745
  }
1754
2746
 
1755
2747
  export function createTelegramBusLocalServer(
@@ -1814,7 +2806,9 @@ export function createTelegramBusLocalServer(
1814
2806
  result: Promise.resolve().then(() => deps.handleEnvelope(envelope)),
1815
2807
  };
1816
2808
  requestLedger.set(key, entry);
1817
- const settled = () => { entry.settled = true; };
2809
+ const settled = () => {
2810
+ entry.settled = true;
2811
+ };
1818
2812
  void entry.result.then(settled, settled);
1819
2813
  return entry.result;
1820
2814
  };
@@ -2070,6 +3064,25 @@ function sendTelegramBusLocalEnvelopeOnce(
2070
3064
  ): Promise<TelegramBusEnvelope | undefined> {
2071
3065
  const timeoutMs = options.timeoutMs ?? 1000;
2072
3066
  return new Promise((resolve, reject) => {
3067
+ const assertAuthority = options.assertAuthority;
3068
+ let issued = false;
3069
+ const assertCurrent = () => {
3070
+ try {
3071
+ assertAuthority?.();
3072
+ } catch (error) {
3073
+ // The caller's reason is a fixed condition label, never request or credential content.
3074
+ throw new TelegramBusLocalAuthorityError(
3075
+ issued,
3076
+ error instanceof Error ? error.message : undefined,
3077
+ );
3078
+ }
3079
+ };
3080
+ try {
3081
+ assertCurrent();
3082
+ } catch (error) {
3083
+ reject(error);
3084
+ return;
3085
+ }
2073
3086
  const socket = createConnection(options.socketPath);
2074
3087
  let settled = false;
2075
3088
  let buffer = "";
@@ -2102,14 +3115,28 @@ function sendTelegramBusLocalEnvelopeOnce(
2102
3115
  }, timeoutMs);
2103
3116
  socket.setEncoding("utf8");
2104
3117
  socket.once("connect", () => {
2105
- socket.write(encodeTelegramBusEnvelope(options.envelope));
3118
+ try {
3119
+ assertCurrent();
3120
+ const encoded = encodeTelegramBusEnvelope(options.envelope);
3121
+ assertCurrent();
3122
+ issued = true;
3123
+ socket.write(encoded);
3124
+ } catch (error) {
3125
+ settle(() => reject(error));
3126
+ }
2106
3127
  });
2107
3128
  socket.on("data", (chunk) => {
2108
3129
  buffer += chunk;
2109
3130
  const newlineIndex = buffer.indexOf("\n");
2110
3131
  if (newlineIndex < 0) return;
2111
3132
  const line = buffer.slice(0, newlineIndex);
2112
- settle(() => resolve(parseTelegramBusEnvelope(line)));
3133
+ try {
3134
+ const response = parseTelegramBusEnvelope(line);
3135
+ assertCurrent();
3136
+ settle(() => resolve(response));
3137
+ } catch (error) {
3138
+ settle(() => reject(error));
3139
+ }
2113
3140
  });
2114
3141
  socket.once("error", (error) => settle(() => reject(error)));
2115
3142
  socket.once("end", () => settle(() => resolve(undefined)));
@@ -2137,7 +3164,11 @@ export async function sendTelegramBusLocalEnvelope(
2137
3164
  attempts,
2138
3165
  ...info,
2139
3166
  });
2140
- if (attempt >= attempts || !isRetryableTelegramBusTransportError(error)) {
3167
+ if (
3168
+ error instanceof TelegramBusLocalAuthorityError ||
3169
+ attempt >= attempts ||
3170
+ !isRetryableTelegramBusTransportError(error)
3171
+ ) {
2141
3172
  throw error;
2142
3173
  }
2143
3174
  resolvedOptions.recordTransportEvent?.("client-retry", {
@@ -2179,15 +3210,21 @@ export interface TelegramBusFollowerRegistry {
2179
3210
  export function createTelegramBusForwardOwnershipValidator(
2180
3211
  registry: Pick<TelegramBusFollowerRegistry, "get">,
2181
3212
  ): (ownership: TelegramBusForwardOwnership) => boolean {
2182
- return ownership => {
3213
+ return (ownership) => {
2183
3214
  const follower = registry.get(ownership.instanceId);
2184
- return isTelegramBusForwardOwnershipCurrent(ownership,
2185
- follower?.registrationGeneration && follower.profileKey && follower.protocol ? {
2186
- instanceId: follower.instanceId,
2187
- ownerGeneration: follower.registrationGeneration,
2188
- recipientBindingKey: follower.profileKey,
2189
- protocolIdentity: follower.protocol,
2190
- } : undefined);
3215
+ return isTelegramBusForwardOwnershipCurrent(
3216
+ ownership,
3217
+ follower?.registrationGeneration &&
3218
+ follower.profileKey &&
3219
+ follower.protocol
3220
+ ? {
3221
+ instanceId: follower.instanceId,
3222
+ ownerGeneration: follower.registrationGeneration,
3223
+ recipientBindingKey: follower.profileKey,
3224
+ protocolIdentity: follower.protocol,
3225
+ }
3226
+ : undefined,
3227
+ );
2191
3228
  };
2192
3229
  }
2193
3230
 
@@ -2195,15 +3232,22 @@ function hasTelegramBusFollowerIdentityOverlap(
2195
3232
  first: TelegramBusInstanceRegistration,
2196
3233
  second: TelegramBusInstanceRegistration,
2197
3234
  ): boolean {
2198
- return first.instanceId === second.instanceId ||
2199
- (first.profileKey !== undefined && first.profileKey === second.profileKey) ||
2200
- (first.target !== undefined && first.target.chatId === second.target?.chatId &&
2201
- first.target.threadId === second.target.threadId);
3235
+ return (
3236
+ first.instanceId === second.instanceId ||
3237
+ (first.profileKey !== undefined &&
3238
+ first.profileKey === second.profileKey) ||
3239
+ (first.target !== undefined &&
3240
+ first.target.chatId === second.target?.chatId &&
3241
+ first.target.threadId === second.target.threadId)
3242
+ );
2202
3243
  }
2203
3244
 
2204
3245
  export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry {
2205
3246
  const followers = new Map<string, TelegramBusFollowerView>();
2206
- const observations = new Set<{ follower: TelegramBusFollowerView; current: boolean }>();
3247
+ const observations = new Set<{
3248
+ follower: TelegramBusFollowerView;
3249
+ current: boolean;
3250
+ }>();
2207
3251
  const clone = (
2208
3252
  follower: TelegramBusFollowerView,
2209
3253
  ): TelegramBusFollowerView => ({
@@ -2223,7 +3267,8 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
2223
3267
  const existing = followers.get(registration.instanceId);
2224
3268
  for (const [instanceId, follower] of followers.entries()) {
2225
3269
  if (instanceId === registration.instanceId) continue;
2226
- if (hasTelegramBusFollowerIdentityOverlap(registration, follower)) followers.delete(instanceId);
3270
+ if (hasTelegramBusFollowerIdentityOverlap(registration, follower))
3271
+ followers.delete(instanceId);
2227
3272
  }
2228
3273
  const next: TelegramBusFollowerView = {
2229
3274
  ...registration,
@@ -2241,7 +3286,8 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
2241
3286
  };
2242
3287
  followers.set(registration.instanceId, next);
2243
3288
  for (const observation of observations) {
2244
- if (!hasTelegramBusFollowerIdentityOverlap(next, observation.follower)) continue;
3289
+ if (!hasTelegramBusFollowerIdentityOverlap(next, observation.follower))
3290
+ continue;
2245
3291
  observation.current = false;
2246
3292
  observations.delete(observation);
2247
3293
  }
@@ -2278,8 +3324,12 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
2278
3324
  },
2279
3325
  observeUnregistered: (follower) => {
2280
3326
  // This watches replacement, not process death or delivery authority.
2281
- const observation = { follower: clone(follower), current: ![...followers.values()]
2282
- .some((current) => hasTelegramBusFollowerIdentityOverlap(current, follower)) };
3327
+ const observation = {
3328
+ follower: clone(follower),
3329
+ current: ![...followers.values()].some((current) =>
3330
+ hasTelegramBusFollowerIdentityOverlap(current, follower),
3331
+ ),
3332
+ };
2283
3333
  if (observation.current) observations.add(observation);
2284
3334
  return {
2285
3335
  isCurrent: () => observation.current,
@@ -2340,12 +3390,12 @@ async function handleTelegramBusSocketLine(
2340
3390
  ...classifyTelegramBusTransportError(error),
2341
3391
  });
2342
3392
  socket.write(
2343
- encodeTelegramBusEnvelope({
2344
- kind: "bus.ack",
2345
- requestId: envelope.requestId,
2346
- ok: false,
2347
- message: "Telegram bus handler failed.",
2348
- }),
3393
+ encodeTelegramBusEnvelope(
3394
+ rejectTelegramBusRequest(
3395
+ envelope.requestId,
3396
+ "Telegram bus handler failed.",
3397
+ ),
3398
+ ),
2349
3399
  );
2350
3400
  }
2351
3401
  }
@@ -2359,32 +3409,15 @@ function parseRegisterEnvelope(
2359
3409
  return registration ? { kind, requestId, registration } : undefined;
2360
3410
  }
2361
3411
 
2362
- function parseHeartbeatEnvelope(
2363
- value: Record<string, unknown>,
2364
- requestId: string,
2365
- ): TelegramBusEnvelope | undefined {
2366
- return typeof value.instanceId === "string" &&
2367
- typeof value.sentAtMs === "number"
2368
- ? {
2369
- kind: "follower.heartbeat",
2370
- requestId,
2371
- instanceId: value.instanceId,
2372
- ...(typeof value.registrationGeneration === "string"
2373
- ? { registrationGeneration: value.registrationGeneration }
2374
- : {}),
2375
- sentAtMs: value.sentAtMs,
2376
- }
2377
- : undefined;
2378
- }
2379
-
2380
- function parseDisconnectEnvelope(
3412
+ function parseFollowerPresenceEnvelope(
2381
3413
  value: Record<string, unknown>,
2382
3414
  requestId: string,
3415
+ kind: "follower.heartbeat" | "follower.disconnect",
2383
3416
  ): TelegramBusEnvelope | undefined {
2384
3417
  return typeof value.instanceId === "string" &&
2385
3418
  typeof value.sentAtMs === "number"
2386
3419
  ? {
2387
- kind: "follower.disconnect",
3420
+ kind,
2388
3421
  requestId,
2389
3422
  instanceId: value.instanceId,
2390
3423
  ...(typeof value.registrationGeneration === "string"
@@ -2429,14 +3462,17 @@ function parseTelegramBusFollowerDeliveryIdentity(
2429
3462
  typeof value.recipientBindingKey !== "string" ||
2430
3463
  !value.recipientBindingKey ||
2431
3464
  (value.sourceRecoveryKey !== undefined &&
2432
- (typeof value.sourceRecoveryKey !== "string" || !value.sourceRecoveryKey ||
3465
+ (typeof value.sourceRecoveryKey !== "string" ||
3466
+ !value.sourceRecoveryKey ||
2433
3467
  value.sourceRecoveryKey.length > 1_024)) ||
2434
3468
  (value.sourceClaim !== undefined &&
2435
3469
  (!isRecord(value.sourceClaim) ||
2436
3470
  typeof value.sourceClaim.acquisitionId !== "string" ||
2437
- !value.sourceClaim.acquisitionId || value.sourceClaim.acquisitionId.length > 256 ||
3471
+ !value.sourceClaim.acquisitionId ||
3472
+ value.sourceClaim.acquisitionId.length > 256 ||
2438
3473
  typeof value.sourceClaim.handoffId !== "string" ||
2439
- !value.sourceClaim.handoffId || value.sourceClaim.handoffId.length > 256))
3474
+ !value.sourceClaim.handoffId ||
3475
+ value.sourceClaim.handoffId.length > 256))
2440
3476
  ) {
2441
3477
  return undefined;
2442
3478
  }
@@ -2445,10 +3481,16 @@ function parseTelegramBusFollowerDeliveryIdentity(
2445
3481
  sourceUpdateId: value.sourceUpdateId as number,
2446
3482
  recipientBindingKey: value.recipientBindingKey,
2447
3483
  ...(typeof value.sourceRecoveryKey === "string"
2448
- ? { sourceRecoveryKey: value.sourceRecoveryKey } : {}),
3484
+ ? { sourceRecoveryKey: value.sourceRecoveryKey }
3485
+ : {}),
2449
3486
  ...(isRecord(value.sourceClaim)
2450
- ? { sourceClaim: { acquisitionId: value.sourceClaim.acquisitionId as string,
2451
- handoffId: value.sourceClaim.handoffId as string } } : {}),
3487
+ ? {
3488
+ sourceClaim: {
3489
+ acquisitionId: value.sourceClaim.acquisitionId as string,
3490
+ handoffId: value.sourceClaim.handoffId as string,
3491
+ },
3492
+ }
3493
+ : {}),
2452
3494
  };
2453
3495
  }
2454
3496
 
@@ -2524,42 +3566,68 @@ function parseForwardMessageEnvelope(
2524
3566
  }
2525
3567
 
2526
3568
  function parseOfferInputCustodyHandoffEnvelope(
2527
- value: Record<string, unknown>, requestId: string,
3569
+ value: Record<string, unknown>,
3570
+ requestId: string,
2528
3571
  ): TelegramBusEnvelope | undefined {
2529
3572
  const source = value.source;
2530
- if (!isRecord(source) || typeof value.recipientInstanceId !== "string" ||
3573
+ if (
3574
+ !isRecord(source) ||
3575
+ typeof value.recipientInstanceId !== "string" ||
2531
3576
  typeof value.recipientRegistrationGeneration !== "string" ||
2532
- typeof value.recipientBindingKey !== "string" || !value.recipientBindingKey ||
2533
- typeof value.sourceRecoveryKey !== "string" || !value.sourceRecoveryKey ||
3577
+ typeof value.recipientBindingKey !== "string" ||
3578
+ !value.recipientBindingKey ||
3579
+ typeof value.sourceRecoveryKey !== "string" ||
3580
+ !value.sourceRecoveryKey ||
2534
3581
  value.sourceRecoveryKey.length > 1_024 ||
2535
- typeof source.journalBindingKey !== "string" || !source.journalBindingKey ||
3582
+ typeof source.journalBindingKey !== "string" ||
3583
+ !source.journalBindingKey ||
2536
3584
  source.journalBindingKey !== value.sourceRecoveryKey ||
2537
- typeof source.tokenSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
2538
- !Number.isSafeInteger(source.updateId) || (source.updateId as number) < 0 ||
2539
- typeof value.handoffId !== "string" || !value.handoffId || value.handoffId.length > 256 ||
2540
- typeof value.sentAtMs !== "number") return undefined;
2541
- return { kind: "leader.offerInputCustodyHandoff", requestId,
3585
+ typeof source.tokenSha256 !== "string" ||
3586
+ !/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
3587
+ !Number.isSafeInteger(source.updateId) ||
3588
+ (source.updateId as number) < 0 ||
3589
+ typeof value.handoffId !== "string" ||
3590
+ !value.handoffId ||
3591
+ value.handoffId.length > 256 ||
3592
+ typeof value.sentAtMs !== "number"
3593
+ )
3594
+ return undefined;
3595
+ return {
3596
+ kind: "leader.offerInputCustodyHandoff",
3597
+ requestId,
2542
3598
  recipientInstanceId: value.recipientInstanceId,
2543
3599
  recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2544
3600
  recipientBindingKey: value.recipientBindingKey,
2545
3601
  sourceRecoveryKey: value.sourceRecoveryKey,
2546
- source: { journalBindingKey: source.journalBindingKey,
2547
- tokenSha256: source.tokenSha256, updateId: source.updateId as number },
2548
- handoffId: value.handoffId, sentAtMs: value.sentAtMs };
3602
+ source: {
3603
+ journalBindingKey: source.journalBindingKey,
3604
+ tokenSha256: source.tokenSha256,
3605
+ updateId: source.updateId as number,
3606
+ },
3607
+ handoffId: value.handoffId,
3608
+ sentAtMs: value.sentAtMs,
3609
+ };
2549
3610
  }
2550
3611
 
2551
3612
  function parseWakeInputCustodyEnvelope(
2552
- value: Record<string, unknown>, requestId: string,
3613
+ value: Record<string, unknown>,
3614
+ requestId: string,
2553
3615
  ): TelegramBusEnvelope | undefined {
2554
3616
  const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
2555
- return delivery && delivery.sourceRecoveryKey && delivery.sourceClaim &&
3617
+ return delivery &&
3618
+ delivery.sourceRecoveryKey &&
3619
+ delivery.sourceClaim &&
2556
3620
  typeof value.recipientInstanceId === "string" &&
2557
3621
  typeof value.recipientRegistrationGeneration === "string" &&
2558
3622
  typeof value.sentAtMs === "number"
2559
- ? { kind: "leader.wakeInputCustody", requestId,
3623
+ ? {
3624
+ kind: "leader.wakeInputCustody",
3625
+ requestId,
2560
3626
  recipientInstanceId: value.recipientInstanceId,
2561
3627
  recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2562
- delivery, sentAtMs: value.sentAtMs }
3628
+ delivery,
3629
+ sentAtMs: value.sentAtMs,
3630
+ }
2563
3631
  : undefined;
2564
3632
  }
2565
3633
 
@@ -2710,6 +3778,171 @@ function parseRouteAgentMessageEnvelope(
2710
3778
  };
2711
3779
  }
2712
3780
 
3781
+ const nonempty = (input: unknown, limit = 128): input is string =>
3782
+ typeof input === "string" && !!input.trim() && input.length <= limit;
3783
+ const positive = (input: unknown): input is number =>
3784
+ Number.isSafeInteger(input) && (input as number) > 0;
3785
+ const nonnegative = (input: unknown): input is number =>
3786
+ Number.isSafeInteger(input) && (input as number) >= 0;
3787
+
3788
+ /** Shared closed text-effect codec; parsing alone supplies no recipient or issuance authority. */
3789
+ export function parseTelegramBusSelectedMenuTextEffect(
3790
+ effect: unknown,
3791
+ ): TelegramBusSelectedMenuTextEffect | undefined {
3792
+ if (
3793
+ !isRecord(effect) ||
3794
+ (effect.kind !== "send-text" && effect.kind !== "edit-text") ||
3795
+ !hasOnlyWireKeys(effect, [
3796
+ "kind",
3797
+ "text",
3798
+ "parseMode",
3799
+ "replyMarkup",
3800
+ ...(effect.kind === "send-text" ? ["replyToMessageId"] : ["messageId"]),
3801
+ ]) ||
3802
+ !nonempty(effect.text, 4096) ||
3803
+ (effect.parseMode !== undefined && effect.parseMode !== "HTML") ||
3804
+ (effect.kind === "send-text"
3805
+ ? effect.replyToMessageId !== undefined &&
3806
+ !positive(effect.replyToMessageId)
3807
+ : !positive(effect.messageId))
3808
+ )
3809
+ return undefined;
3810
+ let replyMarkup: TelegramBusSelectedMenuTextEffect["replyMarkup"];
3811
+ if (effect.replyMarkup !== undefined) {
3812
+ const markup = effect.replyMarkup;
3813
+ if (
3814
+ !isRecord(markup) ||
3815
+ !hasOnlyWireKeys(markup, ["inline_keyboard"]) ||
3816
+ !Array.isArray(markup.inline_keyboard) ||
3817
+ markup.inline_keyboard.length > 32 ||
3818
+ markup.inline_keyboard.some(
3819
+ (row) =>
3820
+ !Array.isArray(row) ||
3821
+ !row.length ||
3822
+ row.length > 8 ||
3823
+ row.some(
3824
+ (button) =>
3825
+ !isRecord(button) ||
3826
+ !hasOnlyWireKeys(button, ["text", "callback_data"]) ||
3827
+ !nonempty(button.text) ||
3828
+ !nonempty(button.callback_data, 64) ||
3829
+ Buffer.byteLength(button.callback_data, "utf8") > 64,
3830
+ ),
3831
+ )
3832
+ )
3833
+ return undefined;
3834
+ replyMarkup = {
3835
+ inline_keyboard: markup.inline_keyboard.map((row) =>
3836
+ (row as { text: string; callback_data: string }[]).map((button) => ({
3837
+ text: button.text,
3838
+ callback_data: button.callback_data,
3839
+ })),
3840
+ ),
3841
+ };
3842
+ }
3843
+ const common = {
3844
+ text: effect.text,
3845
+ ...(effect.parseMode === "HTML" ? { parseMode: "HTML" as const } : {}),
3846
+ ...(replyMarkup ? { replyMarkup } : {}),
3847
+ };
3848
+ return effect.kind === "send-text"
3849
+ ? {
3850
+ ...common,
3851
+ kind: "send-text",
3852
+ ...(effect.replyToMessageId !== undefined
3853
+ ? { replyToMessageId: effect.replyToMessageId as number }
3854
+ : {}),
3855
+ }
3856
+ : { ...common, kind: "edit-text", messageId: effect.messageId as number };
3857
+ }
3858
+
3859
+ function parseSelectedMenuDeliveryEnvelope(
3860
+ value: Record<string, unknown>,
3861
+ requestId: string,
3862
+ ): TelegramBusSelectedMenuDelivery | undefined {
3863
+ const recipient = value.recipient,
3864
+ executor = value.executor,
3865
+ effect = parseTelegramBusSelectedMenuTextEffect(value.effect);
3866
+ if (
3867
+ !hasOnlyWireKeys(value, [
3868
+ "kind",
3869
+ "requestId",
3870
+ "auth",
3871
+ "instanceId",
3872
+ "registrationGeneration",
3873
+ "operationId",
3874
+ "recipient",
3875
+ "executor",
3876
+ "operatorUserId",
3877
+ "effect",
3878
+ "sentAtMs",
3879
+ ]) ||
3880
+ !nonempty(requestId) ||
3881
+ !nonempty(value.instanceId) ||
3882
+ !nonempty(value.registrationGeneration) ||
3883
+ !nonempty(value.operationId) ||
3884
+ !isRecord(recipient) ||
3885
+ !hasOnlyWireKeys(recipient, [
3886
+ "sessionId",
3887
+ "sessionGeneration",
3888
+ "processId",
3889
+ "processBirthId",
3890
+ "profileKey",
3891
+ "journalBindingKey",
3892
+ "target",
3893
+ ]) ||
3894
+ !nonempty(recipient.sessionId) ||
3895
+ !nonnegative(recipient.sessionGeneration) ||
3896
+ !positive(recipient.processId) ||
3897
+ !nonempty(recipient.processBirthId) ||
3898
+ !nonempty(recipient.profileKey, 1024) ||
3899
+ !nonempty(recipient.journalBindingKey, 8192) ||
3900
+ recipient.profileKey === recipient.journalBindingKey ||
3901
+ !isRecord(recipient.target) ||
3902
+ !hasOnlyWireKeys(recipient.target, ["chatId", "threadId"]) ||
3903
+ !positive(recipient.target.chatId) ||
3904
+ !positive(recipient.target.threadId) ||
3905
+ !positive(value.operatorUserId) ||
3906
+ value.operatorUserId !== recipient.target.chatId ||
3907
+ !isRecord(executor) ||
3908
+ !hasOnlyWireKeys(executor, ["instanceId", "leaderEpoch"]) ||
3909
+ !nonempty(executor.instanceId) ||
3910
+ !nonempty(executor.leaderEpoch) ||
3911
+ !effect ||
3912
+ typeof value.sentAtMs !== "number" ||
3913
+ !Number.isFinite(value.sentAtMs) ||
3914
+ value.sentAtMs < 0 ||
3915
+ (value.auth !== undefined && typeof value.auth !== "string")
3916
+ )
3917
+ return undefined;
3918
+ return {
3919
+ kind: "follower.deliverSelectedMenu",
3920
+ requestId,
3921
+ instanceId: value.instanceId,
3922
+ registrationGeneration: value.registrationGeneration,
3923
+ operationId: value.operationId,
3924
+ recipient: {
3925
+ sessionId: recipient.sessionId,
3926
+ sessionGeneration: recipient.sessionGeneration,
3927
+ processId: recipient.processId,
3928
+ processBirthId: recipient.processBirthId,
3929
+ profileKey: recipient.profileKey,
3930
+ journalBindingKey: recipient.journalBindingKey,
3931
+ target: {
3932
+ chatId: recipient.target.chatId,
3933
+ threadId: recipient.target.threadId,
3934
+ },
3935
+ },
3936
+ executor: {
3937
+ instanceId: executor.instanceId,
3938
+ leaderEpoch: executor.leaderEpoch,
3939
+ },
3940
+ operatorUserId: value.operatorUserId,
3941
+ effect,
3942
+ sentAtMs: value.sentAtMs,
3943
+ };
3944
+ }
3945
+
2713
3946
  function parseCallApiEnvelope(
2714
3947
  value: Record<string, unknown>,
2715
3948
  requestId: string,
@@ -2839,9 +4072,13 @@ function parseRegistration(
2839
4072
  }
2840
4073
  if (typeof value.cwd === "string") registration.cwd = value.cwd;
2841
4074
  if (value.sessionId !== undefined) {
2842
- if (typeof value.sessionId !== "string" || !value.sessionId ||
2843
- value.sessionId !== value.sessionId.trim() ||
2844
- Buffer.byteLength(value.sessionId, "utf8") > 256) return undefined;
4075
+ if (
4076
+ typeof value.sessionId !== "string" ||
4077
+ !value.sessionId ||
4078
+ value.sessionId !== value.sessionId.trim() ||
4079
+ Buffer.byteLength(value.sessionId, "utf8") > 256
4080
+ )
4081
+ return undefined;
2845
4082
  registration.sessionId = value.sessionId;
2846
4083
  }
2847
4084
  if (typeof value.pid === "number") registration.pid = value.pid;
@@ -2866,14 +4103,6 @@ function parseRegistration(
2866
4103
  return registration;
2867
4104
  }
2868
4105
 
2869
- function parseTarget(value: unknown): TelegramTarget | undefined {
2870
- if (value === undefined) return undefined;
2871
- if (!isRecord(value) || typeof value.chatId !== "number") return undefined;
2872
- return typeof value.threadId === "number"
2873
- ? { chatId: value.chatId, threadId: value.threadId }
2874
- : { chatId: value.chatId };
2875
- }
2876
-
2877
4106
  function parseThreadTarget(
2878
4107
  value: unknown,
2879
4108
  ): (TelegramTarget & { threadId: number }) | undefined {