@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/dist/lib/bus.js CHANGED
@@ -4,95 +4,21 @@
4
4
  * Owns serializable bus envelopes, socket/auth helpers, local IPC client/server primitives,
5
5
  * cross-instance forwarding helpers, and the live follower registry model.
6
6
  */
7
- import { execFileSync } from "node:child_process";
8
7
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
9
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, symlinkSync, unlinkSync, } from "node:fs";
10
- import { createConnection, createServer, } from "node:net";
8
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readlinkSync, renameSync, symlinkSync, unlinkSync, } from "node:fs";
11
9
  import { createRequire } from "node:module";
10
+ import { createConnection, createServer, } from "node:net";
12
11
  import { platform as getPlatform, tmpdir } from "node:os";
13
12
  import { basename, dirname, join } from "node:path";
14
- import { classifyTelegramBusTransportError, createTelegramBusTransportTimeoutError, delayTelegramBusTransportRetry, getTelegramBusEndpointDiagnostics, getTelegramBusFollowerEndpoint, getTelegramBusLeaderEndpoint, getTelegramBusPipePath, getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES, isTelegramBusPipePath, isRetryableTelegramBusTransportError, probeTelegramBusEndpoint, } from "./bus-transport.js";
15
- import { parseTelegramQueueHandoffPayload, TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES, } from "./queue.js";
16
- import { isProcessAlive } from "./locks.js";
17
- import { isWireRecord as isRecord } from "./wire.js";
13
+ import { isDeepStrictEqual } from "node:util";
14
+ import { classifyTelegramBusTransportError, createTelegramBusTransportTimeoutError, delayTelegramBusTransportRetry, getTelegramBusEndpointDiagnostics, getTelegramBusFollowerEndpoint, getTelegramBusLeaderEndpoint, getTelegramBusPipePath, getTelegramBusTransportRetryPolicy, isRetryableTelegramBusTransportError, isTelegramBusPipePath, probeTelegramBusEndpoint, TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES, } from "./bus-transport.js";
15
+ import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS } from "./locks.js";
18
16
  import { resolveAgentDir } from "./paths.js";
17
+ import { getTelegramProcessBirthIdentity } from "./process-identity.js";
18
+ import { parseTelegramQueueHandoffPayload, TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES, } from "./queue.js";
19
+ import { areTelegramTargetsEqual, parseTelegramTarget as parseTarget, } from "./target.js";
19
20
  import { normalizeTelegramSessionReplacementIntent, } from "./threads.js";
20
- function readDarwinProcessStart(pid) {
21
- return execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
22
- encoding: "utf8",
23
- stdio: ["ignore", "pipe", "ignore"],
24
- }).trim();
25
- }
26
- function getTelegramProcessBirthProof(pid, options = {}) {
27
- if (pid <= 0)
28
- return { status: "unverifiable" };
29
- const platform = options.platform ?? getPlatform();
30
- if (platform === "linux") {
31
- try {
32
- const stat = (options.readProcStat ?? ((targetPid) => readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
33
- const closeParen = stat.lastIndexOf(")");
34
- const fields = stat
35
- .slice(closeParen + 2)
36
- .trim()
37
- .split(/\s+/u);
38
- const startTicks = fields[19];
39
- if (startTicks) {
40
- return { status: "proven", identity: `${pid}:start:${startTicks}` };
41
- }
42
- }
43
- catch {
44
- /* inaccessible process metadata */
45
- }
46
- }
47
- else if (platform === "darwin") {
48
- try {
49
- const startedAt = (options.readDarwinProcessStart ?? readDarwinProcessStart)(pid);
50
- if (startedAt) {
51
- const fingerprint = createHash("sha256")
52
- .update(startedAt)
53
- .digest("hex")
54
- .slice(0, 16);
55
- return { status: "proven", identity: `${pid}:start:${fingerprint}` };
56
- }
57
- }
58
- catch {
59
- /* inaccessible process metadata */
60
- }
61
- }
62
- return { status: "unverifiable" };
63
- }
64
- export function getTelegramProcessBirthIdentity(pid, fallbackGeneration, options = {}) {
65
- const proof = getTelegramProcessBirthProof(pid, options);
66
- return proof.status === "proven"
67
- ? proof.identity
68
- : `${pid}:generation:${fallbackGeneration}`;
69
- }
70
- export function getTelegramProcessLiveness(owner, options = {}) {
71
- const processAlive = options.isProcessAlive ?? isProcessAlive;
72
- if (!processAlive(owner.processId))
73
- return "dead";
74
- const proof = getTelegramProcessBirthProof(owner.processId, options);
75
- if (proof.status === "unverifiable")
76
- return "unverifiable";
77
- return proof.identity === owner.processBirthId ? "alive" : "dead";
78
- }
79
- export function getTelegramProcessBirthIdentityLiveness(processBirthId, options = {}) {
80
- const match = /^(\d+):(start|generation):(.+)$/u.exec(processBirthId);
81
- if (!match)
82
- return "unverifiable";
83
- const processId = Number(match[1]);
84
- if (!Number.isSafeInteger(processId) || processId <= 0)
85
- return "unverifiable";
86
- const processAlive = options.isProcessAlive ?? isProcessAlive;
87
- if (!processAlive(processId))
88
- return "dead";
89
- if (match[2] === "generation")
90
- return "unverifiable";
91
- const proof = getTelegramProcessBirthProof(processId, options);
92
- if (proof.status === "unverifiable")
93
- return "unverifiable";
94
- return proof.identity === processBirthId ? "alive" : "dead";
95
- }
21
+ import { hasOnlyWireKeys, isWireRecord as isRecord } from "./wire.js";
96
22
  export function createCurrentTelegramBusProcessRuntime(input) {
97
23
  return createTelegramBusProcessRuntime({
98
24
  getActiveProfileName: input.getActiveProfileName,
@@ -130,6 +56,90 @@ export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT = "directory-displ
130
56
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT = "workspace-follower-auto-connect-v1";
131
57
  export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT = "session-replacement-intent-v1";
132
58
  export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE = "workspace-restore-v1";
59
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE = "live-thread-rebind-save-v1";
60
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY = "live-thread-rebind-apply-v1";
61
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE = "live-thread-rebind-settle-v1";
62
+ /** Restricted recipient text/menu delivery; negotiated identity and captured effect authority remain required. */
63
+ export const TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY = "selected-menu-delivery-v1";
64
+ /** Versioned held-command set; syntax is shared, while the captured recipient Commands registry owns supported plans. */
65
+ export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET = "live-thread-rebind-command-set-v1";
66
+ /** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
67
+ export const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES = [
68
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
69
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
70
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
71
+ ];
72
+ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
73
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
74
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
75
+ ];
76
+ function parseSelectedCommandInput(value) {
77
+ if (!isRecord(value) ||
78
+ !hasOnlyWireKeys(value, ["name", "target"]) ||
79
+ typeof value.name !== "string" ||
80
+ !/^[a-z0-9_]{1,32}$/u.test(value.name) ||
81
+ !isRecord(value.target) ||
82
+ !hasOnlyWireKeys(value.target, ["chatId", "threadId"]))
83
+ return undefined;
84
+ const target = parseThreadTarget(value.target);
85
+ return target &&
86
+ Number.isSafeInteger(target.chatId) &&
87
+ target.chatId > 0 &&
88
+ Number.isSafeInteger(target.threadId) &&
89
+ target.threadId > 0
90
+ ? { name: value.name, target }
91
+ : undefined;
92
+ }
93
+ /** Pure live-rebind recipient/operation identity shared by prepare/apply/settle envelopes. */
94
+ function parseLiveRebindRecipient(value) {
95
+ const { recipientInstanceId, recipientRegistrationGeneration, recipientSessionId, recipientBindingKey, operationId, } = value;
96
+ return typeof recipientInstanceId === "string" &&
97
+ recipientInstanceId &&
98
+ typeof recipientRegistrationGeneration === "string" &&
99
+ recipientRegistrationGeneration &&
100
+ typeof recipientSessionId === "string" &&
101
+ recipientSessionId &&
102
+ typeof recipientBindingKey === "string" &&
103
+ recipientBindingKey &&
104
+ typeof operationId === "string" &&
105
+ operationId.length > 0 &&
106
+ operationId.length <= 128
107
+ ? {
108
+ recipientInstanceId,
109
+ recipientRegistrationGeneration,
110
+ recipientSessionId,
111
+ recipientBindingKey,
112
+ operationId,
113
+ }
114
+ : undefined;
115
+ }
116
+ /** Non-empty, duplicate-free, non-negative safe-integer Telegram update ids. */
117
+ function isUniqueSourceUpdateIds(value) {
118
+ return (Array.isArray(value) &&
119
+ value.length > 0 &&
120
+ value.every((id) => Number.isSafeInteger(id) && id >= 0) &&
121
+ new Set(value).size === value.length);
122
+ }
123
+ function parsePreparedCommandSource(value) {
124
+ if (!isRecord(value) ||
125
+ !hasOnlyWireKeys(value, [
126
+ "journalBindingKey",
127
+ "updateId",
128
+ "sourceSha256",
129
+ ]) ||
130
+ typeof value.journalBindingKey !== "string" ||
131
+ !value.journalBindingKey ||
132
+ !Number.isSafeInteger(value.updateId) ||
133
+ value.updateId < 0 ||
134
+ typeof value.sourceSha256 !== "string" ||
135
+ !/^[a-f0-9]{64}$/u.test(value.sourceSha256))
136
+ return undefined;
137
+ return {
138
+ journalBindingKey: value.journalBindingKey,
139
+ updateId: value.updateId,
140
+ sourceSha256: value.sourceSha256,
141
+ };
142
+ }
133
143
  export function createTelegramBusProtocolIdentity(input) {
134
144
  const runtimeBuild = input.runtimeBuild.trim();
135
145
  if (!runtimeBuild || runtimeBuild.length > 128) {
@@ -137,8 +147,7 @@ export function createTelegramBusProtocolIdentity(input) {
137
147
  }
138
148
  const capabilities = [...new Set(input.capabilities ?? [])].sort();
139
149
  if (capabilities.length > 32 ||
140
- capabilities.some((capability) => capability.length > 128 ||
141
- !/^[a-z0-9][a-z0-9._-]*$/u.test(capability))) {
150
+ capabilities.some((capability) => capability.length > 128 || !/^[a-z0-9][a-z0-9._-]*$/u.test(capability))) {
142
151
  throw new Error("Telegram bus capabilities must be canonical identifiers.");
143
152
  }
144
153
  return {
@@ -161,13 +170,20 @@ export function hasTelegramBusCapability(identity, capability) {
161
170
  return identity?.capabilities.includes(capability) ?? false;
162
171
  }
163
172
  export function getTelegramInputCustodyPeerReadiness(followers) {
164
- return followers.map(follower => {
173
+ return followers.map((follower) => {
165
174
  if (!follower.registrationGeneration || !follower.protocol)
166
175
  return "unknown";
167
176
  return hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) &&
168
- hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) ? "ready" : "legacy";
177
+ hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE)
178
+ ? "ready"
179
+ : "legacy";
169
180
  });
170
181
  }
182
+ /** Both peers advertise every required capability over a compatible protocol; reads only the two identities. */
183
+ export function hasTelegramBusSharedCapabilities(local, remote, required) {
184
+ return (required.every((capability) => hasTelegramBusCapability(local, capability) &&
185
+ hasTelegramBusCapability(remote, capability)) && getTelegramBusProtocolCompatibility({ local, remote }).compatible);
186
+ }
171
187
  export function getTelegramBusProtocolCompatibility(input) {
172
188
  if (!input.remote) {
173
189
  return {
@@ -196,7 +212,12 @@ export function getTelegramBusProtocolCompatibility(input) {
196
212
  : { compatible: true, missingCapabilities: [] };
197
213
  }
198
214
  export function getTelegramBusSocketPath(agentDir = resolveAgentDir(), platform = getPlatform(), profileName, layout) {
199
- return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName, layout });
215
+ return getTelegramBusLeaderEndpoint({
216
+ agentDir,
217
+ platform,
218
+ profileName,
219
+ layout,
220
+ });
200
221
  }
201
222
  export function getTelegramBusFollowerSocketPath(instanceId, agentDir = resolveAgentDir(), platform = getPlatform(), profileName, layout) {
202
223
  return getTelegramBusFollowerEndpoint({
@@ -207,6 +228,20 @@ export function getTelegramBusFollowerSocketPath(instanceId, agentDir = resolveA
207
228
  layout,
208
229
  });
209
230
  }
231
+ /** Whether a live follower is still exactly the captured registration: identity, process, session, endpoint and negotiated protocol. */
232
+ export function isSameTelegramBusFollowerRegistration(live, captured) {
233
+ return (live.instanceId === captured.instanceId &&
234
+ live.profileKey === captured.profileKey &&
235
+ live.sessionId === captured.sessionId &&
236
+ live.registrationGeneration === captured.registrationGeneration &&
237
+ live.pid === captured.pid &&
238
+ live.processBirthId === captured.processBirthId &&
239
+ live.sessionGeneration === captured.sessionGeneration &&
240
+ live.cwd === captured.cwd &&
241
+ live.slot === captured.slot &&
242
+ live.busSocketPath === captured.busSocketPath &&
243
+ isDeepStrictEqual(live.protocol, captured.protocol));
244
+ }
210
245
  export function getTelegramFollowerTargetOwnership(input) {
211
246
  const liveFollower = input.followers.find((follower) => {
212
247
  return (follower.target?.chatId === input.target.chatId &&
@@ -228,30 +263,17 @@ export function getTelegramFollowerTargetOwnership(input) {
228
263
  }
229
264
  const TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD = "__piTelegramAggregateDelivery";
230
265
  const TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD = "__piTelegramCrossTargetDelivery";
231
- export function markTelegramBusAggregateDelivery(body) {
232
- return {
233
- ...body,
234
- [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true,
235
- };
236
- }
237
- function isTelegramBusAggregateDelivery(body) {
266
+ function hasTelegramBusDeliveryMarker(body, marker) {
238
267
  return Boolean(body &&
239
268
  typeof body === "object" &&
240
269
  !Array.isArray(body) &&
241
- body[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD] ===
242
- true);
270
+ body[marker] === true);
243
271
  }
244
- export function markTelegramBusCrossTargetDelivery(body) {
245
- return {
246
- ...body,
247
- [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true,
248
- };
272
+ export function markTelegramBusAggregateDelivery(body) {
273
+ return { ...body, [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true };
249
274
  }
250
- function isTelegramBusCrossTargetDelivery(body) {
251
- return Boolean(body &&
252
- typeof body === "object" &&
253
- !Array.isArray(body) &&
254
- body[TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD] === true);
275
+ export function markTelegramBusCrossTargetDelivery(body) {
276
+ return { ...body, [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true };
255
277
  }
256
278
  export function stripTelegramBusApiMetadata(body) {
257
279
  if (!(TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD in body) &&
@@ -317,8 +339,7 @@ export function isTelegramFollowerApiCallAllowed(input) {
317
339
  return target.threadId !== undefined;
318
340
  const parsedThreadId = typeof threadId === "number" ? threadId : Number(threadId);
319
341
  return (Number.isInteger(parsedThreadId) &&
320
- (target.threadId === undefined ||
321
- !matchesId(threadId, target.threadId)));
342
+ (target.threadId === undefined || !matchesId(threadId, target.threadId)));
322
343
  };
323
344
  const isTargetMessageScoped = (body) => {
324
345
  if (!isTargetChatScoped(body))
@@ -355,12 +376,12 @@ export function isTelegramFollowerApiCallAllowed(input) {
355
376
  if (apiMethod === "sendChatAction")
356
377
  return isTargetChatScoped(input.args[1]);
357
378
  if (apiMethod === "sendMessage" &&
358
- isTelegramBusAggregateDelivery(input.args[1])) {
379
+ hasTelegramBusDeliveryMarker(input.args[1], TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD)) {
359
380
  const body = input.args[1];
360
381
  return body.message_thread_id === undefined && isTargetChatScoped(body);
361
382
  }
362
383
  if ((apiMethod === "sendMessage" || apiMethod === "sendRichMessage") &&
363
- isTelegramBusCrossTargetDelivery(input.args[1])) {
384
+ hasTelegramBusDeliveryMarker(input.args[1], TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD)) {
364
385
  return isDifferentTargetScoped(input.args[1]);
365
386
  }
366
387
  if (apiMethod === "deleteMessage" ||
@@ -401,8 +422,10 @@ export function createTelegramBusFollowerDeliveryIdentity(input) {
401
422
  !Number.isSafeInteger(input.sourceUpdateId) ||
402
423
  input.sourceUpdateId < 0 ||
403
424
  (input.sourceClaim !== undefined &&
404
- (!input.sourceClaim.acquisitionId || input.sourceClaim.acquisitionId.length > 256 ||
405
- !input.sourceClaim.handoffId || input.sourceClaim.handoffId.length > 256))) {
425
+ (!input.sourceClaim.acquisitionId ||
426
+ input.sourceClaim.acquisitionId.length > 256 ||
427
+ !input.sourceClaim.handoffId ||
428
+ input.sourceClaim.handoffId.length > 256))) {
406
429
  throw new Error("Telegram follower delivery identity is incomplete.");
407
430
  }
408
431
  const deliveryId = createHash("sha256")
@@ -417,23 +440,29 @@ export function createTelegramBusFollowerDeliveryIdentity(input) {
417
440
  deliveryId: `telegram-follower-v1-${deliveryId}`,
418
441
  sourceUpdateId: input.sourceUpdateId,
419
442
  recipientBindingKey: input.recipientBindingKey,
420
- ...(input.sourceRecoveryKey ? { sourceRecoveryKey: input.sourceRecoveryKey } : {}),
443
+ ...(input.sourceRecoveryKey
444
+ ? { sourceRecoveryKey: input.sourceRecoveryKey }
445
+ : {}),
421
446
  ...(input.sourceClaim ? { sourceClaim: { ...input.sourceClaim } } : {}),
422
447
  };
423
448
  }
424
449
  export function canUseTelegramBusInputCustodyReference(input) {
425
- return hasTelegramBusCapability(input.local, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) &&
426
- hasTelegramBusCapability(input.remote, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE);
450
+ return (hasTelegramBusCapability(input.local, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) &&
451
+ hasTelegramBusCapability(input.remote, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE));
427
452
  }
428
453
  export function createTelegramBusFollowerSourceReferenceDeliveryIdentity(input) {
429
454
  if (!input.sourceRecoveryKey || !input.source.owner.handoffId)
430
455
  throw new Error("Telegram follower source-reference delivery requires an accepted handoff.");
431
- return createTelegramBusFollowerDeliveryIdentity({ kind: input.kind,
456
+ return createTelegramBusFollowerDeliveryIdentity({
457
+ kind: input.kind,
432
458
  recipientBindingKey: input.recipientBindingKey,
433
459
  sourceUpdateId: input.source.updateId,
434
460
  sourceRecoveryKey: input.sourceRecoveryKey,
435
- sourceClaim: { acquisitionId: input.source.owner.acquisitionId,
436
- handoffId: input.source.owner.handoffId } });
461
+ sourceClaim: {
462
+ acquisitionId: input.source.owner.acquisitionId,
463
+ handoffId: input.source.owner.handoffId,
464
+ },
465
+ });
437
466
  }
438
467
  export function getTelegramBusEnvelopeTrafficClass(envelope) {
439
468
  if (envelope.kind === "follower.register" ||
@@ -478,18 +507,23 @@ export function parseTelegramBusEnvelope(line) {
478
507
  envelope = parseRegisterEnvelope(value, requestId, kind);
479
508
  break;
480
509
  case "follower.heartbeat":
481
- envelope = parseHeartbeatEnvelope(value, requestId);
482
- break;
483
510
  case "follower.disconnect":
484
- envelope = parseDisconnectEnvelope(value, requestId);
511
+ envelope = parseFollowerPresenceEnvelope(value, requestId, kind);
485
512
  break;
486
513
  case "follower.setThreadDisplayMode":
487
514
  if (typeof value.instanceId === "string" &&
488
515
  typeof value.registrationGeneration === "string" &&
489
- (value.mode === "letters" || value.mode === "names" ||
490
- value.mode === "directory-snake" || value.mode === "directory-title")) {
491
- envelope = { kind, requestId, instanceId: value.instanceId,
492
- registrationGeneration: value.registrationGeneration, mode: value.mode };
516
+ (value.mode === "letters" ||
517
+ value.mode === "names" ||
518
+ value.mode === "directory-snake" ||
519
+ value.mode === "directory-title")) {
520
+ envelope = {
521
+ kind,
522
+ requestId,
523
+ instanceId: value.instanceId,
524
+ registrationGeneration: value.registrationGeneration,
525
+ mode: value.mode,
526
+ };
493
527
  }
494
528
  break;
495
529
  case "follower.renameThread":
@@ -517,7 +551,8 @@ export function parseTelegramBusEnvelope(line) {
517
551
  const intent = normalizeTelegramSessionReplacementIntent(value.intent);
518
552
  if (typeof value.instanceId === "string" &&
519
553
  typeof value.registrationGeneration === "string" &&
520
- typeof value.sentAtMs === "number" && intent) {
554
+ typeof value.sentAtMs === "number" &&
555
+ intent) {
521
556
  envelope = {
522
557
  kind,
523
558
  requestId,
@@ -547,15 +582,122 @@ export function parseTelegramBusEnvelope(line) {
547
582
  case "leader.forwardEditedMessage":
548
583
  envelope = parseForwardMessageEnvelope(value, requestId, "leader.forwardEditedMessage");
549
584
  break;
585
+ case "leader.prepareLiveRebind": {
586
+ const selectedCommand = parseSelectedCommandInput(value.selectedCommand);
587
+ if (!hasOnlyWireKeys(value, [
588
+ "kind",
589
+ "requestId",
590
+ "auth",
591
+ "recipientInstanceId",
592
+ "recipientRegistrationGeneration",
593
+ "recipientSessionId",
594
+ "recipientBindingKey",
595
+ "operationId",
596
+ "updates",
597
+ "selectedCommand",
598
+ "sentAtMs",
599
+ ]) ||
600
+ (Object.hasOwn(value, "selectedCommand") &&
601
+ (!selectedCommand ||
602
+ !Array.isArray(value.updates) ||
603
+ value.updates.length !== 1)))
604
+ break;
605
+ const recipient = parseLiveRebindRecipient(value);
606
+ if (recipient &&
607
+ Array.isArray(value.updates) &&
608
+ value.updates.every(isRecord) &&
609
+ isUniqueSourceUpdateIds(value.updates.map((update) => update.update_id)) &&
610
+ typeof value.sentAtMs === "number" &&
611
+ Number.isFinite(value.sentAtMs)) {
612
+ envelope = {
613
+ kind: "leader.prepareLiveRebind",
614
+ requestId,
615
+ ...recipient,
616
+ updates: value.updates,
617
+ sentAtMs: value.sentAtMs,
618
+ ...(selectedCommand ? { selectedCommand } : {}),
619
+ };
620
+ }
621
+ break;
622
+ }
623
+ case "leader.applyLiveRebind":
624
+ case "leader.settleLiveRebind": {
625
+ const selectedCommand = parseSelectedCommandInput(value.selectedCommand), preparedSource = parsePreparedCommandSource(value.preparedSource);
626
+ if (!hasOnlyWireKeys(value, [
627
+ "kind",
628
+ "requestId",
629
+ "auth",
630
+ "recipientInstanceId",
631
+ "recipientRegistrationGeneration",
632
+ "recipientSessionId",
633
+ "recipientBindingKey",
634
+ "operationId",
635
+ "sourceUpdateIds",
636
+ "mode",
637
+ "oldTarget",
638
+ "selectedCommand",
639
+ "preparedSource",
640
+ "sentAtMs",
641
+ ]) ||
642
+ (Object.hasOwn(value, "preparedSource") && !selectedCommand) ||
643
+ (value.mode === "observe-command" &&
644
+ (!selectedCommand || Object.hasOwn(value, "oldTarget"))))
645
+ break;
646
+ if (Object.hasOwn(value, "selectedCommand") &&
647
+ (!selectedCommand ||
648
+ !Array.isArray(value.sourceUpdateIds) ||
649
+ value.sourceUpdateIds.length !== 1 ||
650
+ !preparedSource ||
651
+ preparedSource.updateId !== value.sourceUpdateIds[0] ||
652
+ preparedSource.journalBindingKey !== value.recipientBindingKey))
653
+ break;
654
+ const recipient = parseLiveRebindRecipient(value);
655
+ if (recipient &&
656
+ (value.kind === "leader.applyLiveRebind"
657
+ ? value.mode === "apply" || value.mode === "inspect"
658
+ : value.mode === "release" ||
659
+ value.mode === "discard" ||
660
+ value.mode === "observe-command" ||
661
+ (value.mode === "observe" &&
662
+ !!parseThreadTarget(value.oldTarget))) &&
663
+ isUniqueSourceUpdateIds(value.sourceUpdateIds) &&
664
+ typeof value.sentAtMs === "number" &&
665
+ Number.isFinite(value.sentAtMs)) {
666
+ envelope = {
667
+ kind: value.kind,
668
+ requestId,
669
+ ...recipient,
670
+ sourceUpdateIds: value.sourceUpdateIds,
671
+ mode: value.mode,
672
+ sentAtMs: value.sentAtMs,
673
+ ...(value.mode === "observe"
674
+ ? { oldTarget: parseThreadTarget(value.oldTarget) }
675
+ : {}),
676
+ ...(selectedCommand ? { selectedCommand, preparedSource } : {}),
677
+ };
678
+ }
679
+ break;
680
+ }
550
681
  case "leader.workspaceRestore":
551
- if (typeof value.recipientInstanceId === "string" && value.recipientInstanceId &&
552
- typeof value.recipientRegistrationGeneration === "string" && value.recipientRegistrationGeneration &&
553
- typeof value.operationId === "string" && value.operationId.length > 0 && value.operationId.length <= 128 &&
682
+ if (typeof value.recipientInstanceId === "string" &&
683
+ value.recipientInstanceId &&
684
+ typeof value.recipientRegistrationGeneration === "string" &&
685
+ value.recipientRegistrationGeneration &&
686
+ typeof value.operationId === "string" &&
687
+ value.operationId.length > 0 &&
688
+ value.operationId.length <= 128 &&
554
689
  (value.mode === "apply" || value.mode === "inspect") &&
555
- typeof value.sentAtMs === "number" && Number.isFinite(value.sentAtMs)) {
556
- envelope = { kind: "leader.workspaceRestore", requestId, recipientInstanceId: value.recipientInstanceId,
557
- recipientRegistrationGeneration: value.recipientRegistrationGeneration, operationId: value.operationId,
558
- mode: value.mode, sentAtMs: value.sentAtMs };
690
+ typeof value.sentAtMs === "number" &&
691
+ Number.isFinite(value.sentAtMs)) {
692
+ envelope = {
693
+ kind: "leader.workspaceRestore",
694
+ requestId,
695
+ recipientInstanceId: value.recipientInstanceId,
696
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
697
+ operationId: value.operationId,
698
+ mode: value.mode,
699
+ sentAtMs: value.sentAtMs,
700
+ };
559
701
  }
560
702
  break;
561
703
  case "leader.offerQueueHandoff":
@@ -570,6 +712,9 @@ export function parseTelegramBusEnvelope(line) {
570
712
  case "follower.routeAgentMessage":
571
713
  envelope = parseRouteAgentMessageEnvelope(value, requestId);
572
714
  break;
715
+ case "follower.deliverSelectedMenu":
716
+ envelope = parseSelectedMenuDeliveryEnvelope(value, requestId);
717
+ break;
573
718
  case "follower.callApi":
574
719
  envelope = parseCallApiEnvelope(value, requestId);
575
720
  break;
@@ -614,11 +759,24 @@ export function resolveTelegramBusSocketPath(source, platform = getPlatform()) {
614
759
  .slice(0, 16);
615
760
  return join(fallbackDir, `${digest}.sock`);
616
761
  }
762
+ /** Local IPC issuance proof only: this callback/error never enters the envelope or certifies a remote API effect. */
763
+ export class TelegramBusLocalAuthorityError extends Error {
764
+ requestIssued;
765
+ constructor(requestIssued, reason) {
766
+ super(reason
767
+ ? `Telegram bus local authority is unavailable: ${reason}`
768
+ : "Telegram bus local authority is unavailable.");
769
+ this.name = "TelegramBusLocalAuthorityError";
770
+ this.requestIssued = requestIssued;
771
+ }
772
+ }
617
773
  export function isTelegramBusForwardOwnershipCurrent(expected, current) {
618
- return Boolean(current && current.instanceId === expected.instanceId &&
774
+ return Boolean(current &&
775
+ current.instanceId === expected.instanceId &&
619
776
  current.ownerGeneration === expected.ownerGeneration &&
620
777
  current.recipientBindingKey === expected.recipientBindingKey &&
621
- JSON.stringify(current.protocolIdentity) === JSON.stringify(expected.protocolIdentity));
778
+ JSON.stringify(current.protocolIdentity) ===
779
+ JSON.stringify(expected.protocolIdentity));
622
780
  }
623
781
  function getTelegramBusForwardSourceUpdateId(value) {
624
782
  if (!isRecord(value))
@@ -635,6 +793,11 @@ function createTelegramBusForwardDelivery(kind, sourceUpdateId, recipientBinding
635
793
  sourceUpdateId,
636
794
  });
637
795
  }
796
+ /**
797
+ * Leader-to-follower forwards and control replies share the stale-liveness window: a follower doing journal work or stalled
798
+ * by an ordinary multi-second Pi/TUI turn, or a slower Windows named pipe, is not a lost recipient.
799
+ */
800
+ const TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
638
801
  export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
639
802
  const getNowMs = deps.getNowMs ?? Date.now;
640
803
  const reject = (input) => {
@@ -684,21 +847,32 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
684
847
  };
685
848
  }
686
849
  const sourceReference = canUseTelegramBusInputCustodyReference({
687
- local: deps.localProtocolIdentity, remote: ownership.protocolIdentity
850
+ local: deps.localProtocolIdentity,
851
+ remote: ownership.protocolIdentity,
688
852
  });
689
- const reference = sourceReference ? deps.resolveInputCustodyReference?.({
690
- sourceUpdateId, recipientBindingKey: ownership.recipientBindingKey
691
- }) : undefined;
853
+ const reference = sourceReference
854
+ ? deps.resolveInputCustodyReference?.({
855
+ sourceUpdateId,
856
+ recipientBindingKey: ownership.recipientBindingKey,
857
+ })
858
+ : undefined;
692
859
  if (sourceReference && !reference)
693
- return { settlement: reject({
694
- status: "retryable", failureClass: "source-reference-missing",
860
+ return {
861
+ settlement: reject({
862
+ status: "retryable",
863
+ failureClass: "source-reference-missing",
695
864
  message: "Forwarded Telegram update has no exact custody reference.",
696
- envelopeKind: kind, ownership, sourceUpdateId
697
- }) };
865
+ envelopeKind: kind,
866
+ ownership,
867
+ sourceUpdateId,
868
+ }),
869
+ };
698
870
  const delivery = reference
699
871
  ? createTelegramBusFollowerSourceReferenceDeliveryIdentity({
700
- kind: "leader.wakeInputCustody", recipientBindingKey: ownership.recipientBindingKey,
701
- sourceRecoveryKey: reference.sourceRecoveryKey, source: reference.source
872
+ kind: "leader.wakeInputCustody",
873
+ recipientBindingKey: ownership.recipientBindingKey,
874
+ sourceRecoveryKey: reference.sourceRecoveryKey,
875
+ source: reference.source,
702
876
  })
703
877
  : createTelegramBusForwardDelivery(kind, sourceUpdateId, ownership.recipientBindingKey);
704
878
  if (!ownership.ownerGeneration) {
@@ -720,11 +894,15 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
720
894
  };
721
895
  };
722
896
  const send = async (envelope, ownership) => {
723
- if (deps.validateForwardOwnership && !deps.validateForwardOwnership(ownership))
897
+ if (deps.validateForwardOwnership &&
898
+ !deps.validateForwardOwnership(ownership))
724
899
  return reject({
725
- status: "retryable", failureClass: "recipient-ownership-stale",
900
+ status: "retryable",
901
+ failureClass: "recipient-ownership-stale",
726
902
  message: "Telegram follower ownership changed before forwarding.",
727
- envelopeKind: envelope.kind, ownership, delivery: envelope.delivery
903
+ envelopeKind: envelope.kind,
904
+ ownership,
905
+ delivery: envelope.delivery,
728
906
  });
729
907
  if (deps.getAuthSecret)
730
908
  envelope.auth = deps.getAuthSecret();
@@ -734,7 +912,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
734
912
  response = await sendTelegramBusLocalEnvelope({
735
913
  socketPath,
736
914
  envelope,
737
- timeoutMs: deps.timeoutMs,
915
+ timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
738
916
  retry: getTelegramBusTransportRetryPolicy({
739
917
  endpoint: socketPath,
740
918
  operation: "operation",
@@ -809,88 +987,52 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
809
987
  }
810
988
  return { status: "accepted", delivery: envelope.delivery };
811
989
  };
990
+ // A recipient already holding the source reference is only woken; it never receives a second payload copy.
991
+ const forward = (kind, value, ownership, build) => {
992
+ const prepared = prepare(kind, value, ownership);
993
+ if ("settlement" in prepared)
994
+ return Promise.resolve(prepared.settlement);
995
+ const base = {
996
+ requestId: deps.createRequestId(),
997
+ recipientInstanceId: ownership.instanceId,
998
+ recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
999
+ delivery: prepared.delivery,
1000
+ };
1001
+ return send(prepared.sourceReference
1002
+ ? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
1003
+ : build(base, getNowMs()), ownership);
1004
+ };
812
1005
  return {
813
- forwardCallback: ({ query, ownership }) => {
814
- const prepared = prepare("leader.forwardCallback", query, ownership);
815
- if ("settlement" in prepared)
816
- return Promise.resolve(prepared.settlement);
817
- if (prepared.sourceReference)
818
- return send({ kind: "leader.wakeInputCustody",
819
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
820
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
821
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
822
- return send({
823
- kind: "leader.forwardCallback",
824
- requestId: deps.createRequestId(),
825
- recipientInstanceId: ownership.instanceId,
826
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
827
- delivery: prepared.delivery,
828
- query,
829
- sentAtMs: getNowMs(),
830
- }, ownership);
831
- },
832
- forwardReaction: ({ reactionUpdate, ownership }) => {
833
- const prepared = prepare("leader.forwardReaction", reactionUpdate, ownership);
834
- if ("settlement" in prepared)
835
- return Promise.resolve(prepared.settlement);
836
- if (prepared.sourceReference)
837
- return send({ kind: "leader.wakeInputCustody",
838
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
839
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
840
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
841
- return send({
842
- kind: "leader.forwardReaction",
843
- requestId: deps.createRequestId(),
844
- recipientInstanceId: ownership.instanceId,
845
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
846
- delivery: prepared.delivery,
847
- reactionUpdate,
848
- sentAtMs: getNowMs(),
849
- }, ownership);
850
- },
851
- forwardMessage: ({ message, ownership }) => {
852
- const prepared = prepare("leader.forwardMessage", message, ownership);
853
- if ("settlement" in prepared)
854
- return Promise.resolve(prepared.settlement);
855
- if (prepared.sourceReference)
856
- return send({ kind: "leader.wakeInputCustody",
857
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
858
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
859
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
860
- return send({
1006
+ forwardCallback: ({ query, ownership }) => forward("leader.forwardCallback", query, ownership, (base, sentAtMs) => ({
1007
+ kind: "leader.forwardCallback",
1008
+ ...base,
1009
+ query,
1010
+ sentAtMs,
1011
+ })),
1012
+ forwardReaction: ({ reactionUpdate, ownership }) => forward("leader.forwardReaction", reactionUpdate, ownership, (base, sentAtMs) => ({
1013
+ kind: "leader.forwardReaction",
1014
+ ...base,
1015
+ reactionUpdate,
1016
+ sentAtMs,
1017
+ })),
1018
+ forwardMessage: ({ message, ownership }) => forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
1019
+ const forwardCommentBatchPosition = deps.getForwardCommentBatchPosition?.(message);
1020
+ return {
861
1021
  kind: "leader.forwardMessage",
862
- requestId: deps.createRequestId(),
863
- recipientInstanceId: ownership.instanceId,
864
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
865
- delivery: prepared.delivery,
1022
+ ...base,
866
1023
  message,
867
- ...(deps.getForwardCommentBatchPosition?.(message) !== undefined
868
- ? {
869
- forwardCommentBatchPosition: deps.getForwardCommentBatchPosition(message),
870
- }
1024
+ ...(forwardCommentBatchPosition !== undefined
1025
+ ? { forwardCommentBatchPosition }
871
1026
  : {}),
872
- sentAtMs: getNowMs(),
873
- }, ownership);
874
- },
875
- forwardEditedMessage: ({ message, ownership }) => {
876
- const prepared = prepare("leader.forwardEditedMessage", message, ownership);
877
- if ("settlement" in prepared)
878
- return Promise.resolve(prepared.settlement);
879
- if (prepared.sourceReference)
880
- return send({ kind: "leader.wakeInputCustody",
881
- requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
882
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
883
- delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
884
- return send({
885
- kind: "leader.forwardEditedMessage",
886
- requestId: deps.createRequestId(),
887
- recipientInstanceId: ownership.instanceId,
888
- recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
889
- delivery: prepared.delivery,
890
- message,
891
- sentAtMs: getNowMs(),
892
- }, ownership);
893
- },
1027
+ sentAtMs,
1028
+ };
1029
+ }),
1030
+ forwardEditedMessage: ({ message, ownership }) => forward("leader.forwardEditedMessage", message, ownership, (base, sentAtMs) => ({
1031
+ kind: "leader.forwardEditedMessage",
1032
+ ...base,
1033
+ message,
1034
+ sentAtMs,
1035
+ })),
894
1036
  };
895
1037
  }
896
1038
  export function listTelegramBusLiveThreadTargets(input) {
@@ -912,48 +1054,441 @@ export function createTelegramBusWorkspaceRestoreController(deps) {
912
1054
  return undefined;
913
1055
  const follower = deps.getFollower(input.instanceId);
914
1056
  const secret = deps.getAuthSecret();
915
- if (!secret || !follower?.registrationGeneration || !follower.busSocketPath ||
916
- follower.sessionId !== input.sessionId || follower.slot !== input.slot ||
917
- !hasTelegramBusCapability(deps.localProtocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
918
- !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
919
- !getTelegramBusProtocolCompatibility({ local: deps.localProtocolIdentity, remote: follower.protocol }).compatible)
1057
+ if (!secret ||
1058
+ !follower?.registrationGeneration ||
1059
+ !follower.busSocketPath ||
1060
+ follower.sessionId !== input.sessionId ||
1061
+ follower.slot !== input.slot ||
1062
+ !hasTelegramBusSharedCapabilities(deps.localProtocolIdentity, follower.protocol, [TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE]))
920
1063
  return undefined;
921
1064
  const captured = structuredClone(follower);
922
- const expected = { operationId: input.operationId, sessionId: input.sessionId, slot: input.slot,
923
- target: { ...input.target }, oldTarget: { ...input.oldTarget }, mode: input.mode };
1065
+ const expected = {
1066
+ operationId: input.operationId,
1067
+ sessionId: input.sessionId,
1068
+ slot: input.slot,
1069
+ target: { ...input.target },
1070
+ oldTarget: { ...input.oldTarget },
1071
+ mode: input.mode,
1072
+ };
924
1073
  const current = () => {
925
- if (!isCurrent() || deps.getAuthSecret() !== secret ||
1074
+ if (!isCurrent() ||
1075
+ deps.getAuthSecret() !== secret ||
926
1076
  !hasTelegramBusCapability(deps.localProtocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
927
1077
  return false;
928
1078
  const live = deps.getFollower(captured.instanceId);
929
- return !!live && live.registrationGeneration === captured.registrationGeneration &&
930
- live.sessionId === captured.sessionId && live.cwd === captured.cwd && live.slot === captured.slot &&
931
- live.busSocketPath === captured.busSocketPath && live.target?.chatId === captured.target?.chatId &&
1079
+ return (!!live &&
1080
+ live.registrationGeneration === captured.registrationGeneration &&
1081
+ live.sessionId === captured.sessionId &&
1082
+ live.cwd === captured.cwd &&
1083
+ live.slot === captured.slot &&
1084
+ live.busSocketPath === captured.busSocketPath &&
1085
+ live.target?.chatId === captured.target?.chatId &&
932
1086
  live.target?.threadId === captured.target?.threadId &&
933
1087
  hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
934
- getTelegramBusProtocolCompatibility({ local: deps.localProtocolIdentity, remote: live.protocol }).compatible;
1088
+ getTelegramBusProtocolCompatibility({
1089
+ local: deps.localProtocolIdentity,
1090
+ remote: live.protocol,
1091
+ }).compatible);
935
1092
  };
936
1093
  if (!current())
937
1094
  return undefined;
938
1095
  const requestId = deps.createRequestId();
939
- const response = await sendTelegramBusLocalEnvelope({ socketPath: captured.busSocketPath, timeoutMs: deps.timeoutMs,
940
- retry: { attempts: 1, delayMs: 0 }, envelope: { kind: "leader.workspaceRestore", requestId,
941
- recipientInstanceId: captured.instanceId, recipientRegistrationGeneration: captured.registrationGeneration,
942
- operationId: expected.operationId, mode: expected.mode, auth: secret, sentAtMs: Date.now() } });
943
- if (!current() || response?.kind !== "bus.ack" || response.requestId !== requestId || response.ok !== true ||
944
- !response.result || typeof response.result !== "object")
1096
+ const response = await sendTelegramBusLocalEnvelope({
1097
+ socketPath: captured.busSocketPath,
1098
+ timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
1099
+ retry: { attempts: 1, delayMs: 0 },
1100
+ envelope: {
1101
+ kind: "leader.workspaceRestore",
1102
+ requestId,
1103
+ recipientInstanceId: captured.instanceId,
1104
+ recipientRegistrationGeneration: captured.registrationGeneration,
1105
+ operationId: expected.operationId,
1106
+ mode: expected.mode,
1107
+ auth: secret,
1108
+ sentAtMs: Date.now(),
1109
+ },
1110
+ });
1111
+ if (!current() ||
1112
+ response?.kind !== "bus.ack" ||
1113
+ response.requestId !== requestId ||
1114
+ response.ok !== true ||
1115
+ !response.result ||
1116
+ typeof response.result !== "object")
945
1117
  return undefined;
946
1118
  const result = response.result;
947
1119
  const recipient = result.recipient;
948
1120
  const target = parseThreadTarget(result.target);
949
1121
  const wanted = result.ready === true ? expected.target : expected.oldTarget;
950
- if (result.operationId !== expected.operationId || typeof result.ready !== "boolean" || result.slot !== expected.slot ||
951
- !target || target.chatId !== wanted.chatId || target.threadId !== wanted.threadId ||
952
- recipient?.kind !== "follower" || recipient.instanceId !== captured.instanceId ||
953
- recipient.sessionId !== expected.sessionId || recipient.generation !== captured.registrationGeneration)
1122
+ if (result.operationId !== expected.operationId ||
1123
+ typeof result.ready !== "boolean" ||
1124
+ result.slot !== expected.slot ||
1125
+ !target ||
1126
+ target.chatId !== wanted.chatId ||
1127
+ target.threadId !== wanted.threadId ||
1128
+ recipient?.kind !== "follower" ||
1129
+ recipient.instanceId !== captured.instanceId ||
1130
+ recipient.sessionId !== expected.sessionId ||
1131
+ recipient.generation !== captured.registrationGeneration)
1132
+ return undefined;
1133
+ return {
1134
+ operationId: expected.operationId,
1135
+ recipient: {
1136
+ kind: "follower",
1137
+ instanceId: captured.instanceId,
1138
+ sessionId: expected.sessionId,
1139
+ generation: captured.registrationGeneration,
1140
+ },
1141
+ target,
1142
+ slot: expected.slot,
1143
+ ready: result.ready,
1144
+ };
1145
+ };
1146
+ }
1147
+ /** Each capability has its own effect boundary; transport retries never replay save/dispatch/disposal. */
1148
+ export function createTelegramBusLiveRebindController(deps) {
1149
+ return async (input) => {
1150
+ const isCurrent = input.isCurrent.bind(input), localProtocol = structuredClone(deps.localProtocolIdentity);
1151
+ const selectedCommand = parseSelectedCommandInput(input.selectedCommand), held = selectedCommand, preparedSource = parsePreparedCommandSource(input.preparedSource);
1152
+ if (!hasOnlyWireKeys(input, [
1153
+ "operationId",
1154
+ "instanceId",
1155
+ "sessionId",
1156
+ "recipientBindingKey",
1157
+ "isCurrent",
1158
+ "selectedCommand",
1159
+ "preparedSource",
1160
+ "updates",
1161
+ "mode",
1162
+ "sourceUpdateIds",
1163
+ "slot",
1164
+ "target",
1165
+ "oldTarget",
1166
+ ]) ||
1167
+ ("mode" in input &&
1168
+ (![
1169
+ "apply",
1170
+ "inspect",
1171
+ "release",
1172
+ "discard",
1173
+ "observe",
1174
+ "observe-command",
1175
+ ].includes(input.mode) ||
1176
+ (input.mode === "observe-command" && !selectedCommand))))
1177
+ return undefined;
1178
+ if ((input.preparedSource !== undefined &&
1179
+ (!held ||
1180
+ !preparedSource ||
1181
+ preparedSource.journalBindingKey !== input.recipientBindingKey ||
1182
+ preparedSource.updateId !==
1183
+ ("mode" in input
1184
+ ? input.sourceUpdateIds[0]
1185
+ : input.updates[0]?.update_id))) ||
1186
+ (held && "mode" in input && !preparedSource))
1187
+ return undefined;
1188
+ if (input.selectedCommand !== undefined &&
1189
+ (!held ||
1190
+ ("mode" in input
1191
+ ? input.sourceUpdateIds.length !== 1 ||
1192
+ !areTelegramTargetsEqual(input.target, held.target)
1193
+ : input.updates.length !== 1)))
1194
+ return undefined;
1195
+ const marker = selectedCommand ? { selectedCommand } : {};
1196
+ const expected = structuredClone({
1197
+ operationId: input.operationId,
1198
+ instanceId: input.instanceId,
1199
+ sessionId: input.sessionId,
1200
+ recipientBindingKey: input.recipientBindingKey,
1201
+ ...(held
1202
+ ? { ...marker, ...(preparedSource ? { preparedSource } : {}) }
1203
+ : {}),
1204
+ ...("mode" in input
1205
+ ? {
1206
+ mode: input.mode,
1207
+ sourceUpdateIds: input.sourceUpdateIds,
1208
+ slot: input.slot,
1209
+ target: input.target,
1210
+ oldTarget: input.oldTarget,
1211
+ }
1212
+ : { updates: input.updates }),
1213
+ });
1214
+ const observing = "mode" in expected && expected.mode === "observe";
1215
+ const commandObserving = "mode" in expected && expected.mode === "observe-command";
1216
+ const settling = "mode" in expected &&
1217
+ (expected.mode === "release" ||
1218
+ expected.mode === "discard" ||
1219
+ observing ||
1220
+ commandObserving);
1221
+ const required = [
1222
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
1223
+ ...("mode" in expected || held
1224
+ ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY]
1225
+ : []),
1226
+ ...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
1227
+ ...(held
1228
+ ? [
1229
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
1230
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
1231
+ ]
1232
+ : []),
1233
+ ];
1234
+ const follower = deps.getFollower(expected.instanceId), secret = deps.getAuthSecret();
1235
+ if (!isCurrent() ||
1236
+ !secret ||
1237
+ !follower?.registrationGeneration ||
1238
+ !follower.busSocketPath ||
1239
+ follower.sessionId !== expected.sessionId ||
1240
+ !hasTelegramBusSharedCapabilities(deps.localProtocolIdentity, follower.protocol, required))
1241
+ return undefined;
1242
+ const captured = structuredClone(follower);
1243
+ /** The first failed authority condition, by fixed label; undefined while the captured recipient is current. */
1244
+ const staleReason = () => {
1245
+ if (!isCurrent())
1246
+ return "caller authority";
1247
+ if (deps.getAuthSecret() !== secret)
1248
+ return "auth secret";
1249
+ if (!isDeepStrictEqual(deps.localProtocolIdentity, localProtocol) ||
1250
+ !required.every((cap) => hasTelegramBusCapability(deps.localProtocolIdentity, cap)))
1251
+ return "local protocol";
1252
+ const live = deps.getFollower(expected.instanceId);
1253
+ if (!live)
1254
+ return "follower registration";
1255
+ if (live.sessionId !== captured.sessionId ||
1256
+ live.registrationGeneration !== captured.registrationGeneration ||
1257
+ live.sessionGeneration !== captured.sessionGeneration)
1258
+ return "follower generation";
1259
+ if (live.pid !== captured.pid ||
1260
+ live.processBirthId !== captured.processBirthId)
1261
+ return "follower process";
1262
+ if (live.busSocketPath !== captured.busSocketPath ||
1263
+ live.slot !== captured.slot ||
1264
+ live.cwd !== captured.cwd)
1265
+ return "follower endpoint";
1266
+ if ("mode" in expected
1267
+ ? live.slot !== expected.slot ||
1268
+ !live.target ||
1269
+ (!areTelegramTargetsEqual(live.target, expected.target) &&
1270
+ !areTelegramTargetsEqual(live.target, expected.oldTarget))
1271
+ : !isDeepStrictEqual(live.target, captured.target))
1272
+ return "follower target";
1273
+ if (!required.every((cap) => hasTelegramBusCapability(live.protocol, cap)) ||
1274
+ !isDeepStrictEqual(live.protocol, captured.protocol) ||
1275
+ !getTelegramBusProtocolCompatibility({
1276
+ local: deps.localProtocolIdentity,
1277
+ remote: live.protocol,
1278
+ }).compatible)
1279
+ return "follower protocol";
1280
+ return undefined;
1281
+ };
1282
+ const current = () => staleReason() === undefined;
1283
+ if (!current())
1284
+ return undefined;
1285
+ const requestId = deps.createRequestId();
1286
+ const response = await sendTelegramBusLocalEnvelope({
1287
+ socketPath: captured.busSocketPath,
1288
+ timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
1289
+ retry: { attempts: 1, delayMs: 0 },
1290
+ ...(held
1291
+ ? {
1292
+ assertAuthority: () => {
1293
+ const reason = staleReason();
1294
+ if (reason)
1295
+ throw new Error(`Selected held input sender authority changed (${reason}).`);
1296
+ },
1297
+ }
1298
+ : {}),
1299
+ envelope: {
1300
+ requestId,
1301
+ auth: secret,
1302
+ ...(held
1303
+ ? {
1304
+ ...marker,
1305
+ ...("mode" in expected
1306
+ ? { preparedSource: expected.preparedSource }
1307
+ : {}),
1308
+ }
1309
+ : {}),
1310
+ recipientInstanceId: expected.instanceId,
1311
+ recipientRegistrationGeneration: captured.registrationGeneration,
1312
+ recipientSessionId: expected.sessionId,
1313
+ recipientBindingKey: expected.recipientBindingKey,
1314
+ operationId: expected.operationId,
1315
+ sentAtMs: Date.now(),
1316
+ ...("mode" in expected
1317
+ ? settling
1318
+ ? {
1319
+ kind: "leader.settleLiveRebind",
1320
+ mode: expected.mode,
1321
+ sourceUpdateIds: expected.sourceUpdateIds,
1322
+ ...(observing ? { oldTarget: expected.oldTarget } : {}),
1323
+ }
1324
+ : {
1325
+ kind: "leader.applyLiveRebind",
1326
+ mode: expected.mode,
1327
+ sourceUpdateIds: expected.sourceUpdateIds,
1328
+ }
1329
+ : {
1330
+ kind: "leader.prepareLiveRebind",
1331
+ updates: expected.updates,
1332
+ }),
1333
+ },
1334
+ });
1335
+ if (!current() ||
1336
+ response?.kind !== "bus.ack" ||
1337
+ response.requestId !== requestId ||
1338
+ response.ok !== true ||
1339
+ !isRecord(response.result))
1340
+ return undefined;
1341
+ const result = response.result, recipient = result.recipient;
1342
+ if (!hasOnlyWireKeys(result, [
1343
+ "operationId",
1344
+ "recipient",
1345
+ "sourceUpdateIds",
1346
+ "selectedCommand",
1347
+ "preparedSource",
1348
+ "status",
1349
+ "command",
1350
+ "sourceAck",
1351
+ "target",
1352
+ "slot",
1353
+ "action",
1354
+ "oldTarget",
1355
+ "work",
1356
+ ]))
1357
+ return undefined;
1358
+ if (selectedCommand
1359
+ ? !isDeepStrictEqual(parseSelectedCommandInput(result.selectedCommand), selectedCommand)
1360
+ : Object.hasOwn(result, "selectedCommand"))
1361
+ return undefined;
1362
+ const ids = ("mode" in expected
1363
+ ? [...expected.sourceUpdateIds]
1364
+ : expected.updates.map((update) => update.update_id)).sort((a, b) => a - b);
1365
+ const observedSource = parsePreparedCommandSource(result.preparedSource);
1366
+ if (held
1367
+ ? !observedSource ||
1368
+ observedSource.updateId !== ids[0] ||
1369
+ observedSource.journalBindingKey !== expected.recipientBindingKey ||
1370
+ (expected.preparedSource &&
1371
+ !isDeepStrictEqual(observedSource, expected.preparedSource))
1372
+ : Object.hasOwn(result, "preparedSource"))
1373
+ return undefined;
1374
+ if (result.operationId !== expected.operationId ||
1375
+ !(commandObserving
1376
+ ? result.status === "command-observed"
1377
+ : observing
1378
+ ? result.status === "observed"
1379
+ : settling
1380
+ ? result.status === "protected" ||
1381
+ result.status === "unknown" ||
1382
+ result.status ===
1383
+ (expected.mode === "release" ? "released" : "discarded")
1384
+ : "mode" in expected
1385
+ ? result.status === "saved" || result.status === "applied"
1386
+ : result.status === "saved") ||
1387
+ !isRecord(recipient) ||
1388
+ recipient.instanceId !== expected.instanceId ||
1389
+ recipient.sessionId !== expected.sessionId ||
1390
+ recipient.generation !== captured.registrationGeneration ||
1391
+ recipient.bindingKey !== expected.recipientBindingKey ||
1392
+ !Array.isArray(result.sourceUpdateIds) ||
1393
+ result.sourceUpdateIds.length !== ids.length ||
1394
+ result.sourceUpdateIds.some((id, index) => id !== ids[index]))
1395
+ return undefined;
1396
+ const observation = {
1397
+ operationId: expected.operationId,
1398
+ recipient: {
1399
+ instanceId: expected.instanceId,
1400
+ sessionId: expected.sessionId,
1401
+ generation: captured.registrationGeneration,
1402
+ bindingKey: expected.recipientBindingKey,
1403
+ },
1404
+ sourceUpdateIds: ids,
1405
+ ...(held
1406
+ ? { ...structuredClone(marker), preparedSource: { ...observedSource } }
1407
+ : {}),
1408
+ status: "saved",
1409
+ };
1410
+ if (!("mode" in expected))
1411
+ return observation;
1412
+ if (commandObserving) {
1413
+ const sourceAck = parsePreparedCommandSource(result.sourceAck);
1414
+ if (!hasOnlyWireKeys(result, [
1415
+ "operationId",
1416
+ "recipient",
1417
+ "sourceUpdateIds",
1418
+ "selectedCommand",
1419
+ "preparedSource",
1420
+ "status",
1421
+ "command",
1422
+ "sourceAck",
1423
+ ]) ||
1424
+ !hasOnlyWireKeys(recipient, [
1425
+ "instanceId",
1426
+ "sessionId",
1427
+ "generation",
1428
+ "bindingKey",
1429
+ ]) ||
1430
+ !["completed", "unknown", "not-issued"].includes(result.command) ||
1431
+ (Object.hasOwn(result, "sourceAck") &&
1432
+ (!sourceAck ||
1433
+ !isDeepStrictEqual(sourceAck, expected.preparedSource) ||
1434
+ result.command === "not-issued")))
1435
+ return undefined;
1436
+ const settled = {
1437
+ command: result.command,
1438
+ ...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
1439
+ };
1440
+ return {
1441
+ ...observation,
1442
+ status: "command-observed",
1443
+ selectedCommand: structuredClone(selectedCommand),
1444
+ preparedSource: { ...observedSource },
1445
+ ...settled,
1446
+ };
1447
+ }
1448
+ if (observing) {
1449
+ const work = result.work, oldTarget = parseThreadTarget(result.oldTarget);
1450
+ if (!oldTarget ||
1451
+ !isDeepStrictEqual(oldTarget, expected.oldTarget) ||
1452
+ !isRecord(work) ||
1453
+ [
1454
+ work.sessionBusy,
1455
+ work.targetWork,
1456
+ work.deliveryPending,
1457
+ work.unknown,
1458
+ ].some((flag) => typeof flag !== "boolean"))
1459
+ return undefined;
1460
+ return {
1461
+ ...observation,
1462
+ status: "observed",
1463
+ oldTarget,
1464
+ work: {
1465
+ sessionBusy: work.sessionBusy,
1466
+ targetWork: work.targetWork,
1467
+ deliveryPending: work.deliveryPending,
1468
+ unknown: work.unknown,
1469
+ },
1470
+ };
1471
+ }
1472
+ if (settling) {
1473
+ if (result.action !== expected.mode)
1474
+ return undefined;
1475
+ return {
1476
+ ...observation,
1477
+ action: expected.mode,
1478
+ status: result.status,
1479
+ };
1480
+ }
1481
+ const target = parseThreadTarget(result.target), wanted = result.status === "applied" ? expected.target : expected.oldTarget;
1482
+ if (!target ||
1483
+ !isDeepStrictEqual(target, wanted) ||
1484
+ result.slot !== expected.slot)
954
1485
  return undefined;
955
- return { operationId: expected.operationId, recipient: { kind: "follower", instanceId: captured.instanceId,
956
- sessionId: expected.sessionId, generation: captured.registrationGeneration }, target, slot: expected.slot, ready: result.ready };
1486
+ return {
1487
+ ...observation,
1488
+ status: result.status,
1489
+ target,
1490
+ slot: expected.slot,
1491
+ };
957
1492
  };
958
1493
  }
959
1494
  export function isTelegramBusEnvelopeAuthorized(envelope, secret) {
@@ -965,13 +1500,12 @@ export function isTelegramBusEnvelopeAuthorized(envelope, secret) {
965
1500
  const expected = Buffer.from(secret);
966
1501
  return auth.length === expected.length && timingSafeEqual(auth, expected);
967
1502
  }
1503
+ /** Negative acknowledgement carrying only a diagnostic message. */
1504
+ export function rejectTelegramBusRequest(requestId, message) {
1505
+ return { kind: "bus.ack", requestId, ok: false, message };
1506
+ }
968
1507
  export function createUnauthorizedBusAck(requestId) {
969
- return {
970
- kind: "bus.ack",
971
- requestId,
972
- ok: false,
973
- message: "Unauthorized Telegram bus envelope.",
974
- };
1508
+ return rejectTelegramBusRequest(requestId, "Unauthorized Telegram bus envelope.");
975
1509
  }
976
1510
  export function createTelegramBusLocalServer(deps) {
977
1511
  const requestLedger = new Map();
@@ -1022,7 +1556,9 @@ export function createTelegramBusLocalServer(deps) {
1022
1556
  result: Promise.resolve().then(() => deps.handleEnvelope(envelope)),
1023
1557
  };
1024
1558
  requestLedger.set(key, entry);
1025
- const settled = () => { entry.settled = true; };
1559
+ const settled = () => {
1560
+ entry.settled = true;
1561
+ };
1026
1562
  void entry.result.then(settled, settled);
1027
1563
  return entry.result;
1028
1564
  };
@@ -1246,6 +1782,24 @@ function getTelegramBusEnvelopeDiagnostics(envelope) {
1246
1782
  function sendTelegramBusLocalEnvelopeOnce(options) {
1247
1783
  const timeoutMs = options.timeoutMs ?? 1000;
1248
1784
  return new Promise((resolve, reject) => {
1785
+ const assertAuthority = options.assertAuthority;
1786
+ let issued = false;
1787
+ const assertCurrent = () => {
1788
+ try {
1789
+ assertAuthority?.();
1790
+ }
1791
+ catch (error) {
1792
+ // The caller's reason is a fixed condition label, never request or credential content.
1793
+ throw new TelegramBusLocalAuthorityError(issued, error instanceof Error ? error.message : undefined);
1794
+ }
1795
+ };
1796
+ try {
1797
+ assertCurrent();
1798
+ }
1799
+ catch (error) {
1800
+ reject(error);
1801
+ return;
1802
+ }
1249
1803
  const socket = createConnection(options.socketPath);
1250
1804
  let settled = false;
1251
1805
  let buffer = "";
@@ -1274,7 +1828,16 @@ function sendTelegramBusLocalEnvelopeOnce(options) {
1274
1828
  }, timeoutMs);
1275
1829
  socket.setEncoding("utf8");
1276
1830
  socket.once("connect", () => {
1277
- socket.write(encodeTelegramBusEnvelope(options.envelope));
1831
+ try {
1832
+ assertCurrent();
1833
+ const encoded = encodeTelegramBusEnvelope(options.envelope);
1834
+ assertCurrent();
1835
+ issued = true;
1836
+ socket.write(encoded);
1837
+ }
1838
+ catch (error) {
1839
+ settle(() => reject(error));
1840
+ }
1278
1841
  });
1279
1842
  socket.on("data", (chunk) => {
1280
1843
  buffer += chunk;
@@ -1282,7 +1845,14 @@ function sendTelegramBusLocalEnvelopeOnce(options) {
1282
1845
  if (newlineIndex < 0)
1283
1846
  return;
1284
1847
  const line = buffer.slice(0, newlineIndex);
1285
- settle(() => resolve(parseTelegramBusEnvelope(line)));
1848
+ try {
1849
+ const response = parseTelegramBusEnvelope(line);
1850
+ assertCurrent();
1851
+ settle(() => resolve(response));
1852
+ }
1853
+ catch (error) {
1854
+ settle(() => reject(error));
1855
+ }
1286
1856
  });
1287
1857
  socket.once("error", (error) => settle(() => reject(error)));
1288
1858
  socket.once("end", () => settle(() => resolve(undefined)));
@@ -1308,7 +1878,9 @@ export async function sendTelegramBusLocalEnvelope(options) {
1308
1878
  attempts,
1309
1879
  ...info,
1310
1880
  });
1311
- if (attempt >= attempts || !isRetryableTelegramBusTransportError(error)) {
1881
+ if (error instanceof TelegramBusLocalAuthorityError ||
1882
+ attempt >= attempts ||
1883
+ !isRetryableTelegramBusTransportError(error)) {
1312
1884
  throw error;
1313
1885
  }
1314
1886
  resolvedOptions.recordTransportEvent?.("client-retry", {
@@ -1324,21 +1896,27 @@ export async function sendTelegramBusLocalEnvelope(options) {
1324
1896
  }
1325
1897
  }
1326
1898
  export function createTelegramBusForwardOwnershipValidator(registry) {
1327
- return ownership => {
1899
+ return (ownership) => {
1328
1900
  const follower = registry.get(ownership.instanceId);
1329
- return isTelegramBusForwardOwnershipCurrent(ownership, follower?.registrationGeneration && follower.profileKey && follower.protocol ? {
1330
- instanceId: follower.instanceId,
1331
- ownerGeneration: follower.registrationGeneration,
1332
- recipientBindingKey: follower.profileKey,
1333
- protocolIdentity: follower.protocol,
1334
- } : undefined);
1901
+ return isTelegramBusForwardOwnershipCurrent(ownership, follower?.registrationGeneration &&
1902
+ follower.profileKey &&
1903
+ follower.protocol
1904
+ ? {
1905
+ instanceId: follower.instanceId,
1906
+ ownerGeneration: follower.registrationGeneration,
1907
+ recipientBindingKey: follower.profileKey,
1908
+ protocolIdentity: follower.protocol,
1909
+ }
1910
+ : undefined);
1335
1911
  };
1336
1912
  }
1337
1913
  function hasTelegramBusFollowerIdentityOverlap(first, second) {
1338
- return first.instanceId === second.instanceId ||
1339
- (first.profileKey !== undefined && first.profileKey === second.profileKey) ||
1340
- (first.target !== undefined && first.target.chatId === second.target?.chatId &&
1341
- first.target.threadId === second.target.threadId);
1914
+ return (first.instanceId === second.instanceId ||
1915
+ (first.profileKey !== undefined &&
1916
+ first.profileKey === second.profileKey) ||
1917
+ (first.target !== undefined &&
1918
+ first.target.chatId === second.target?.chatId &&
1919
+ first.target.threadId === second.target.threadId));
1342
1920
  }
1343
1921
  export function createTelegramBusFollowerRegistry() {
1344
1922
  const followers = new Map();
@@ -1417,8 +1995,10 @@ export function createTelegramBusFollowerRegistry() {
1417
1995
  },
1418
1996
  observeUnregistered: (follower) => {
1419
1997
  // This watches replacement, not process death or delivery authority.
1420
- const observation = { follower: clone(follower), current: ![...followers.values()]
1421
- .some((current) => hasTelegramBusFollowerIdentityOverlap(current, follower)) };
1998
+ const observation = {
1999
+ follower: clone(follower),
2000
+ current: ![...followers.values()].some((current) => hasTelegramBusFollowerIdentityOverlap(current, follower)),
2001
+ };
1422
2002
  if (observation.current)
1423
2003
  observations.add(observation);
1424
2004
  return {
@@ -1468,37 +2048,18 @@ async function handleTelegramBusSocketLine(line, socket, handleEnvelope, recordT
1468
2048
  ...getTelegramBusEnvelopeDiagnostics(envelope),
1469
2049
  ...classifyTelegramBusTransportError(error),
1470
2050
  });
1471
- socket.write(encodeTelegramBusEnvelope({
1472
- kind: "bus.ack",
1473
- requestId: envelope.requestId,
1474
- ok: false,
1475
- message: "Telegram bus handler failed.",
1476
- }));
2051
+ socket.write(encodeTelegramBusEnvelope(rejectTelegramBusRequest(envelope.requestId, "Telegram bus handler failed.")));
1477
2052
  }
1478
2053
  }
1479
2054
  function parseRegisterEnvelope(value, requestId, kind) {
1480
2055
  const registration = parseRegistration(value.registration);
1481
2056
  return registration ? { kind, requestId, registration } : undefined;
1482
2057
  }
1483
- function parseHeartbeatEnvelope(value, requestId) {
2058
+ function parseFollowerPresenceEnvelope(value, requestId, kind) {
1484
2059
  return typeof value.instanceId === "string" &&
1485
2060
  typeof value.sentAtMs === "number"
1486
2061
  ? {
1487
- kind: "follower.heartbeat",
1488
- requestId,
1489
- instanceId: value.instanceId,
1490
- ...(typeof value.registrationGeneration === "string"
1491
- ? { registrationGeneration: value.registrationGeneration }
1492
- : {}),
1493
- sentAtMs: value.sentAtMs,
1494
- }
1495
- : undefined;
1496
- }
1497
- function parseDisconnectEnvelope(value, requestId) {
1498
- return typeof value.instanceId === "string" &&
1499
- typeof value.sentAtMs === "number"
1500
- ? {
1501
- kind: "follower.disconnect",
2062
+ kind,
1502
2063
  requestId,
1503
2064
  instanceId: value.instanceId,
1504
2065
  ...(typeof value.registrationGeneration === "string"
@@ -1536,14 +2097,17 @@ function parseTelegramBusFollowerDeliveryIdentity(value) {
1536
2097
  typeof value.recipientBindingKey !== "string" ||
1537
2098
  !value.recipientBindingKey ||
1538
2099
  (value.sourceRecoveryKey !== undefined &&
1539
- (typeof value.sourceRecoveryKey !== "string" || !value.sourceRecoveryKey ||
2100
+ (typeof value.sourceRecoveryKey !== "string" ||
2101
+ !value.sourceRecoveryKey ||
1540
2102
  value.sourceRecoveryKey.length > 1_024)) ||
1541
2103
  (value.sourceClaim !== undefined &&
1542
2104
  (!isRecord(value.sourceClaim) ||
1543
2105
  typeof value.sourceClaim.acquisitionId !== "string" ||
1544
- !value.sourceClaim.acquisitionId || value.sourceClaim.acquisitionId.length > 256 ||
2106
+ !value.sourceClaim.acquisitionId ||
2107
+ value.sourceClaim.acquisitionId.length > 256 ||
1545
2108
  typeof value.sourceClaim.handoffId !== "string" ||
1546
- !value.sourceClaim.handoffId || value.sourceClaim.handoffId.length > 256))) {
2109
+ !value.sourceClaim.handoffId ||
2110
+ value.sourceClaim.handoffId.length > 256))) {
1547
2111
  return undefined;
1548
2112
  }
1549
2113
  return {
@@ -1551,10 +2115,16 @@ function parseTelegramBusFollowerDeliveryIdentity(value) {
1551
2115
  sourceUpdateId: value.sourceUpdateId,
1552
2116
  recipientBindingKey: value.recipientBindingKey,
1553
2117
  ...(typeof value.sourceRecoveryKey === "string"
1554
- ? { sourceRecoveryKey: value.sourceRecoveryKey } : {}),
2118
+ ? { sourceRecoveryKey: value.sourceRecoveryKey }
2119
+ : {}),
1555
2120
  ...(isRecord(value.sourceClaim)
1556
- ? { sourceClaim: { acquisitionId: value.sourceClaim.acquisitionId,
1557
- handoffId: value.sourceClaim.handoffId } } : {}),
2121
+ ? {
2122
+ sourceClaim: {
2123
+ acquisitionId: value.sourceClaim.acquisitionId,
2124
+ handoffId: value.sourceClaim.handoffId,
2125
+ },
2126
+ }
2127
+ : {}),
1558
2128
  };
1559
2129
  }
1560
2130
  function parseForwardCallbackEnvelope(value, requestId) {
@@ -1617,37 +2187,58 @@ function parseForwardMessageEnvelope(value, requestId, kind) {
1617
2187
  }
1618
2188
  function parseOfferInputCustodyHandoffEnvelope(value, requestId) {
1619
2189
  const source = value.source;
1620
- if (!isRecord(source) || typeof value.recipientInstanceId !== "string" ||
2190
+ if (!isRecord(source) ||
2191
+ typeof value.recipientInstanceId !== "string" ||
1621
2192
  typeof value.recipientRegistrationGeneration !== "string" ||
1622
- typeof value.recipientBindingKey !== "string" || !value.recipientBindingKey ||
1623
- typeof value.sourceRecoveryKey !== "string" || !value.sourceRecoveryKey ||
2193
+ typeof value.recipientBindingKey !== "string" ||
2194
+ !value.recipientBindingKey ||
2195
+ typeof value.sourceRecoveryKey !== "string" ||
2196
+ !value.sourceRecoveryKey ||
1624
2197
  value.sourceRecoveryKey.length > 1_024 ||
1625
- typeof source.journalBindingKey !== "string" || !source.journalBindingKey ||
2198
+ typeof source.journalBindingKey !== "string" ||
2199
+ !source.journalBindingKey ||
1626
2200
  source.journalBindingKey !== value.sourceRecoveryKey ||
1627
- typeof source.tokenSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
1628
- !Number.isSafeInteger(source.updateId) || source.updateId < 0 ||
1629
- typeof value.handoffId !== "string" || !value.handoffId || value.handoffId.length > 256 ||
2201
+ typeof source.tokenSha256 !== "string" ||
2202
+ !/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
2203
+ !Number.isSafeInteger(source.updateId) ||
2204
+ source.updateId < 0 ||
2205
+ typeof value.handoffId !== "string" ||
2206
+ !value.handoffId ||
2207
+ value.handoffId.length > 256 ||
1630
2208
  typeof value.sentAtMs !== "number")
1631
2209
  return undefined;
1632
- return { kind: "leader.offerInputCustodyHandoff", requestId,
2210
+ return {
2211
+ kind: "leader.offerInputCustodyHandoff",
2212
+ requestId,
1633
2213
  recipientInstanceId: value.recipientInstanceId,
1634
2214
  recipientRegistrationGeneration: value.recipientRegistrationGeneration,
1635
2215
  recipientBindingKey: value.recipientBindingKey,
1636
2216
  sourceRecoveryKey: value.sourceRecoveryKey,
1637
- source: { journalBindingKey: source.journalBindingKey,
1638
- tokenSha256: source.tokenSha256, updateId: source.updateId },
1639
- handoffId: value.handoffId, sentAtMs: value.sentAtMs };
2217
+ source: {
2218
+ journalBindingKey: source.journalBindingKey,
2219
+ tokenSha256: source.tokenSha256,
2220
+ updateId: source.updateId,
2221
+ },
2222
+ handoffId: value.handoffId,
2223
+ sentAtMs: value.sentAtMs,
2224
+ };
1640
2225
  }
1641
2226
  function parseWakeInputCustodyEnvelope(value, requestId) {
1642
2227
  const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
1643
- return delivery && delivery.sourceRecoveryKey && delivery.sourceClaim &&
2228
+ return delivery &&
2229
+ delivery.sourceRecoveryKey &&
2230
+ delivery.sourceClaim &&
1644
2231
  typeof value.recipientInstanceId === "string" &&
1645
2232
  typeof value.recipientRegistrationGeneration === "string" &&
1646
2233
  typeof value.sentAtMs === "number"
1647
- ? { kind: "leader.wakeInputCustody", requestId,
2234
+ ? {
2235
+ kind: "leader.wakeInputCustody",
2236
+ requestId,
1648
2237
  recipientInstanceId: value.recipientInstanceId,
1649
2238
  recipientRegistrationGeneration: value.recipientRegistrationGeneration,
1650
- delivery, sentAtMs: value.sentAtMs }
2239
+ delivery,
2240
+ sentAtMs: value.sentAtMs,
2241
+ }
1651
2242
  : undefined;
1652
2243
  }
1653
2244
  function parseQueueHandoffEnvelope(value, requestId, kind) {
@@ -1777,6 +2368,144 @@ function parseRouteAgentMessageEnvelope(value, requestId) {
1777
2368
  sentAtMs: value.sentAtMs,
1778
2369
  };
1779
2370
  }
2371
+ const nonempty = (input, limit = 128) => typeof input === "string" && !!input.trim() && input.length <= limit;
2372
+ const positive = (input) => Number.isSafeInteger(input) && input > 0;
2373
+ const nonnegative = (input) => Number.isSafeInteger(input) && input >= 0;
2374
+ /** Shared closed text-effect codec; parsing alone supplies no recipient or issuance authority. */
2375
+ export function parseTelegramBusSelectedMenuTextEffect(effect) {
2376
+ if (!isRecord(effect) ||
2377
+ (effect.kind !== "send-text" && effect.kind !== "edit-text") ||
2378
+ !hasOnlyWireKeys(effect, [
2379
+ "kind",
2380
+ "text",
2381
+ "parseMode",
2382
+ "replyMarkup",
2383
+ ...(effect.kind === "send-text" ? ["replyToMessageId"] : ["messageId"]),
2384
+ ]) ||
2385
+ !nonempty(effect.text, 4096) ||
2386
+ (effect.parseMode !== undefined && effect.parseMode !== "HTML") ||
2387
+ (effect.kind === "send-text"
2388
+ ? effect.replyToMessageId !== undefined &&
2389
+ !positive(effect.replyToMessageId)
2390
+ : !positive(effect.messageId)))
2391
+ return undefined;
2392
+ let replyMarkup;
2393
+ if (effect.replyMarkup !== undefined) {
2394
+ const markup = effect.replyMarkup;
2395
+ if (!isRecord(markup) ||
2396
+ !hasOnlyWireKeys(markup, ["inline_keyboard"]) ||
2397
+ !Array.isArray(markup.inline_keyboard) ||
2398
+ markup.inline_keyboard.length > 32 ||
2399
+ markup.inline_keyboard.some((row) => !Array.isArray(row) ||
2400
+ !row.length ||
2401
+ row.length > 8 ||
2402
+ row.some((button) => !isRecord(button) ||
2403
+ !hasOnlyWireKeys(button, ["text", "callback_data"]) ||
2404
+ !nonempty(button.text) ||
2405
+ !nonempty(button.callback_data, 64) ||
2406
+ Buffer.byteLength(button.callback_data, "utf8") > 64)))
2407
+ return undefined;
2408
+ replyMarkup = {
2409
+ inline_keyboard: markup.inline_keyboard.map((row) => row.map((button) => ({
2410
+ text: button.text,
2411
+ callback_data: button.callback_data,
2412
+ }))),
2413
+ };
2414
+ }
2415
+ const common = {
2416
+ text: effect.text,
2417
+ ...(effect.parseMode === "HTML" ? { parseMode: "HTML" } : {}),
2418
+ ...(replyMarkup ? { replyMarkup } : {}),
2419
+ };
2420
+ return effect.kind === "send-text"
2421
+ ? {
2422
+ ...common,
2423
+ kind: "send-text",
2424
+ ...(effect.replyToMessageId !== undefined
2425
+ ? { replyToMessageId: effect.replyToMessageId }
2426
+ : {}),
2427
+ }
2428
+ : { ...common, kind: "edit-text", messageId: effect.messageId };
2429
+ }
2430
+ function parseSelectedMenuDeliveryEnvelope(value, requestId) {
2431
+ const recipient = value.recipient, executor = value.executor, effect = parseTelegramBusSelectedMenuTextEffect(value.effect);
2432
+ if (!hasOnlyWireKeys(value, [
2433
+ "kind",
2434
+ "requestId",
2435
+ "auth",
2436
+ "instanceId",
2437
+ "registrationGeneration",
2438
+ "operationId",
2439
+ "recipient",
2440
+ "executor",
2441
+ "operatorUserId",
2442
+ "effect",
2443
+ "sentAtMs",
2444
+ ]) ||
2445
+ !nonempty(requestId) ||
2446
+ !nonempty(value.instanceId) ||
2447
+ !nonempty(value.registrationGeneration) ||
2448
+ !nonempty(value.operationId) ||
2449
+ !isRecord(recipient) ||
2450
+ !hasOnlyWireKeys(recipient, [
2451
+ "sessionId",
2452
+ "sessionGeneration",
2453
+ "processId",
2454
+ "processBirthId",
2455
+ "profileKey",
2456
+ "journalBindingKey",
2457
+ "target",
2458
+ ]) ||
2459
+ !nonempty(recipient.sessionId) ||
2460
+ !nonnegative(recipient.sessionGeneration) ||
2461
+ !positive(recipient.processId) ||
2462
+ !nonempty(recipient.processBirthId) ||
2463
+ !nonempty(recipient.profileKey, 1024) ||
2464
+ !nonempty(recipient.journalBindingKey, 8192) ||
2465
+ recipient.profileKey === recipient.journalBindingKey ||
2466
+ !isRecord(recipient.target) ||
2467
+ !hasOnlyWireKeys(recipient.target, ["chatId", "threadId"]) ||
2468
+ !positive(recipient.target.chatId) ||
2469
+ !positive(recipient.target.threadId) ||
2470
+ !positive(value.operatorUserId) ||
2471
+ value.operatorUserId !== recipient.target.chatId ||
2472
+ !isRecord(executor) ||
2473
+ !hasOnlyWireKeys(executor, ["instanceId", "leaderEpoch"]) ||
2474
+ !nonempty(executor.instanceId) ||
2475
+ !nonempty(executor.leaderEpoch) ||
2476
+ !effect ||
2477
+ typeof value.sentAtMs !== "number" ||
2478
+ !Number.isFinite(value.sentAtMs) ||
2479
+ value.sentAtMs < 0 ||
2480
+ (value.auth !== undefined && typeof value.auth !== "string"))
2481
+ return undefined;
2482
+ return {
2483
+ kind: "follower.deliverSelectedMenu",
2484
+ requestId,
2485
+ instanceId: value.instanceId,
2486
+ registrationGeneration: value.registrationGeneration,
2487
+ operationId: value.operationId,
2488
+ recipient: {
2489
+ sessionId: recipient.sessionId,
2490
+ sessionGeneration: recipient.sessionGeneration,
2491
+ processId: recipient.processId,
2492
+ processBirthId: recipient.processBirthId,
2493
+ profileKey: recipient.profileKey,
2494
+ journalBindingKey: recipient.journalBindingKey,
2495
+ target: {
2496
+ chatId: recipient.target.chatId,
2497
+ threadId: recipient.target.threadId,
2498
+ },
2499
+ },
2500
+ executor: {
2501
+ instanceId: executor.instanceId,
2502
+ leaderEpoch: executor.leaderEpoch,
2503
+ },
2504
+ operatorUserId: value.operatorUserId,
2505
+ effect,
2506
+ sentAtMs: value.sentAtMs,
2507
+ };
2508
+ }
1780
2509
  function parseCallApiEnvelope(value, requestId) {
1781
2510
  return typeof value.instanceId === "string" &&
1782
2511
  typeof value.method === "string" &&
@@ -1888,7 +2617,8 @@ function parseRegistration(value) {
1888
2617
  if (typeof value.cwd === "string")
1889
2618
  registration.cwd = value.cwd;
1890
2619
  if (value.sessionId !== undefined) {
1891
- if (typeof value.sessionId !== "string" || !value.sessionId ||
2620
+ if (typeof value.sessionId !== "string" ||
2621
+ !value.sessionId ||
1892
2622
  value.sessionId !== value.sessionId.trim() ||
1893
2623
  Buffer.byteLength(value.sessionId, "utf8") > 256)
1894
2624
  return undefined;
@@ -1916,15 +2646,6 @@ function parseRegistration(value) {
1916
2646
  registration.target = target;
1917
2647
  return registration;
1918
2648
  }
1919
- function parseTarget(value) {
1920
- if (value === undefined)
1921
- return undefined;
1922
- if (!isRecord(value) || typeof value.chatId !== "number")
1923
- return undefined;
1924
- return typeof value.threadId === "number"
1925
- ? { chatId: value.chatId, threadId: value.threadId }
1926
- : { chatId: value.chatId };
1927
- }
1928
2649
  function parseThreadTarget(value) {
1929
2650
  const target = parseTarget(value);
1930
2651
  return target && typeof target.threadId === "number"