@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
@@ -3,11 +3,44 @@
3
3
  * Zones: telegram controls, pi agent commands, queue controls
4
4
  * Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
5
5
  */
6
- import { randomUUID } from "node:crypto";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { addAbortListener } from "node:events";
8
+ import { isDeepStrictEqual } from "node:util";
7
9
  import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
8
- import { escapeHtml } from "./rendering.js";
9
- import { formatTelegramConnectionFailure } from "./status.js";
10
+ import { isPiStaleContextError } from "./pi.js";
10
11
  import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
12
+ import { escapeHtml } from "./rendering.js";
13
+ import { formatTelegramConnectionFailure, } from "./status.js";
14
+ import { TelegramApiAuthorityError, } from "./telegram-api.js";
15
+ import { acquireTelegramUpdateRouting, carryTelegramUpdateExecutionFence, getTelegramUpdateExecutionFence, inspectTelegramDeferredSource, prepareTelegramDeferredQueueAdmission, reportTelegramQueueAdmission, } from "./updates.js";
16
+ /** Exact singleton operator-private text command original eligible for a held follower plan. */
17
+ export function isTelegramSelectedHeldOriginal(update, target, operator, name) {
18
+ if (!update ||
19
+ typeof update !== "object" ||
20
+ Array.isArray(update) ||
21
+ Object.keys(update).some((key) => !["update_id", "message"].includes(key)))
22
+ return false;
23
+ const value = Reflect.get(update, "message");
24
+ if (!value || typeof value !== "object" || Array.isArray(value))
25
+ return false;
26
+ const text = Reflect.get(value, "text"), chat = Reflect.get(value, "chat"), from = Reflect.get(value, "from");
27
+ return (typeof text === "string" &&
28
+ parseTelegramCommand(text)?.name === name &&
29
+ value.caption === undefined &&
30
+ value.media_group_id === undefined &&
31
+ chat?.type === "private" &&
32
+ from?.is_bot === false &&
33
+ from.id === operator &&
34
+ chat.id === operator &&
35
+ Number.isSafeInteger(value.message_id) &&
36
+ value.message_id > 0 &&
37
+ typeof operator === "number" &&
38
+ Number.isSafeInteger(operator) &&
39
+ operator > 0 &&
40
+ target.chatId === operator &&
41
+ Number.isSafeInteger(target.threadId) &&
42
+ target.threadId > 0);
43
+ }
11
44
  const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__";
12
45
  const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
13
46
  function getOrCreateTelegramCommandRegistry() {
@@ -60,6 +93,9 @@ export function registerTelegramCommand(registration) {
60
93
  showInMenu,
61
94
  emoji,
62
95
  handler: registration.handler,
96
+ selected: registration.selected
97
+ ? Object.freeze({ prepare: registration.selected.prepare })
98
+ : undefined,
63
99
  };
64
100
  registry.commands.set(name, command);
65
101
  return () => {
@@ -75,6 +111,64 @@ export function findTelegramExtensionCommand(name) {
75
111
  return undefined;
76
112
  return getOrCreateTelegramCommandRegistry().commands.get(normalizeTelegramExtensionCommandName(name));
77
113
  }
114
+ /** Preparation only: no execution, admission, source freeze or binding effects. Routing owns those later. */
115
+ export async function prepareTelegramSelectedExtensionCommand(command, authority) {
116
+ const registration = findTelegramExtensionCommand(command.name), selected = registration?.selected;
117
+ const prepare = selected?.prepare;
118
+ if (!registration || !selected || typeof prepare !== "function")
119
+ return undefined;
120
+ const input = Object.freeze({ name: registration.name, args: command.args });
121
+ const { assertSourceCurrent, assertRecipientCurrent } = authority;
122
+ const assertRegistrationCurrent = () => {
123
+ if (findTelegramExtensionCommand(input.name) !== registration ||
124
+ registration.selected !== selected ||
125
+ selected.prepare !== prepare)
126
+ throw new Error("Selected extension command registration changed.");
127
+ };
128
+ const assertCurrent = () => {
129
+ assertSourceCurrent();
130
+ assertRecipientCurrent();
131
+ assertRegistrationCurrent();
132
+ };
133
+ try {
134
+ assertCurrent();
135
+ const prepared = await prepare(input);
136
+ assertCurrent();
137
+ if (!prepared || typeof prepared !== "object" || Array.isArray(prepared))
138
+ return undefined;
139
+ const keys = Reflect.ownKeys(prepared), kind = prepared.kind;
140
+ let plan;
141
+ if (kind === "command-only" &&
142
+ keys.length === 2 &&
143
+ keys.includes("kind") &&
144
+ keys.includes("execute")) {
145
+ const execute = prepared.execute;
146
+ if (typeof execute !== "function")
147
+ return undefined;
148
+ plan = { kind, execute };
149
+ }
150
+ else if (kind === "generated-prompt" &&
151
+ keys.length === 2 &&
152
+ keys.includes("kind") &&
153
+ keys.includes("prompt")) {
154
+ const prompt = prepared.prompt;
155
+ if (typeof prompt !== "string" || !prompt.trim())
156
+ return undefined;
157
+ plan = { kind, prompt };
158
+ }
159
+ else
160
+ return undefined;
161
+ // Producer accessors can invalidate authority while the plan is being copied.
162
+ assertCurrent();
163
+ return Object.freeze({
164
+ plan: Object.freeze(plan),
165
+ assertRegistrationCurrent,
166
+ });
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
78
172
  export function clearTelegramExtensionCommands() {
79
173
  getOrCreateTelegramCommandRegistry().commands.clear();
80
174
  }
@@ -111,11 +205,11 @@ export function formatTelegramInvalidInstanceName(validationError) {
111
205
  .split(/;\s+|(?<=\.)\s+(?=[A-Z])/)
112
206
  .map((item) => item.trim())
113
207
  .filter(Boolean)
114
- .map((item) => /[.!?]$/.test(item) ? item : `${item}.`)
208
+ .map((item) => (/[.!?]$/.test(item) ? item : `${item}.`))
115
209
  .map((item) => item[0].toUpperCase() + item.slice(1));
116
210
  return [
117
211
  "<b>⚠️ Invalid Thread Display Name:</b>\n",
118
- ...items.map((item) => `• ${escapeHtml(item)}`),
212
+ ...items.map((item) => `<code>-</code> ${escapeHtml(item)}`),
119
213
  ].join("\n");
120
214
  }
121
215
  export function formatTelegramThreadDisplayNameSavedHeading(name) {
@@ -176,62 +270,118 @@ export function getTelegramReservedCommandNames() {
176
270
  ...getTelegramExtensionCommands().map((command) => command.name),
177
271
  ];
178
272
  }
179
- export async function registerTelegramBotCommands(deps) {
273
+ export async function registerTelegramBotCommands(deps, options) {
274
+ const assertAuthority = options?.assertAuthority, send = deps.setMyCommands;
275
+ const capturedOptions = assertAuthority ? { assertAuthority } : undefined;
276
+ const setMyCommands = async (commands) => {
277
+ assertAuthority?.();
278
+ await send(commands, capturedOptions);
279
+ assertAuthority?.();
280
+ };
281
+ assertAuthority?.();
180
282
  const extensionCommands = getVisibleTelegramExtensionBotCommands();
181
283
  if (extensionCommands.length === 0) {
182
- await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
284
+ await setMyCommands(TELEGRAM_BOT_COMMANDS);
183
285
  return;
184
286
  }
185
287
  const nextCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex((command) => command.command === "next");
186
288
  if (nextCommandIndex === -1) {
187
- await deps.setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
289
+ await setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
188
290
  return;
189
291
  }
190
- await deps.setMyCommands([
292
+ await setMyCommands([
191
293
  ...TELEGRAM_BOT_COMMANDS.slice(0, nextCommandIndex + 1),
192
294
  ...extensionCommands,
193
295
  ...TELEGRAM_BOT_COMMANDS.slice(nextCommandIndex + 1),
194
296
  ]);
195
297
  }
196
298
  export function createTelegramBotCommandRegistrar(deps) {
197
- let pending;
198
- return () => {
199
- if (pending)
200
- return pending;
299
+ // Only an identical local lifetime may share an in-flight sync; unguarded calls remain separate.
300
+ const pending = new Map();
301
+ return (options) => {
302
+ const assertAuthority = options?.assertAuthority;
303
+ try {
304
+ assertAuthority?.();
305
+ }
306
+ catch (error) {
307
+ return Promise.reject(error);
308
+ }
309
+ const joined = pending.get(assertAuthority);
310
+ if (joined)
311
+ return joined;
201
312
  let request;
202
- request = registerTelegramBotCommands(deps).finally(() => {
203
- if (pending === request)
204
- pending = undefined;
313
+ request = registerTelegramBotCommands(deps, assertAuthority ? { assertAuthority } : undefined).finally(() => {
314
+ if (pending.get(assertAuthority) === request)
315
+ pending.delete(assertAuthority);
205
316
  });
206
- pending = request;
317
+ pending.set(assertAuthority, request);
207
318
  return request;
208
319
  };
209
320
  }
210
321
  export function createTelegramThreadDisplayNameResetBinding() {
211
- let current;
322
+ let current, generation = 0;
212
323
  return {
213
- bind(reset) { current = reset; },
214
- async reset(target) {
215
- return current
216
- ? current(target)
217
- : { ok: false, message: "Thread display name reset is unavailable." };
324
+ bind(reset) {
325
+ current = reset;
326
+ generation++;
327
+ },
328
+ async reset(target, options) {
329
+ const reset = current, assertAuthority = options?.assertAuthority, capturedGeneration = generation;
330
+ const capturedTarget = {
331
+ chatId: target.chatId,
332
+ ...(target.threadId === undefined ? {} : { threadId: target.threadId }),
333
+ };
334
+ if (!reset)
335
+ return {
336
+ ok: false,
337
+ message: "Thread display name reset is unavailable.",
338
+ };
339
+ const assertCurrent = () => {
340
+ assertAuthority?.();
341
+ if (assertAuthority && generation !== capturedGeneration)
342
+ throw new Error("Thread display name reset binding changed.");
343
+ };
344
+ assertCurrent();
345
+ const result = assertAuthority
346
+ ? await reset(capturedTarget, { assertAuthority: assertCurrent })
347
+ : await reset(capturedTarget);
348
+ assertCurrent();
349
+ return result;
218
350
  },
219
351
  };
220
352
  }
221
353
  export function createTelegramThreadDisplayNameRenameBinding() {
222
- let current;
354
+ let current, generation = 0;
223
355
  return {
224
356
  bind(rename) {
225
357
  current = rename;
358
+ generation++;
226
359
  },
227
- async rename(target, threadName) {
228
- if (!current) {
360
+ async rename(target, threadName, options) {
361
+ const rename = current, assertAuthority = options?.assertAuthority, capturedGeneration = generation;
362
+ const capturedTarget = {
363
+ chatId: target.chatId,
364
+ ...(target.threadId === undefined ? {} : { threadId: target.threadId }),
365
+ };
366
+ if (!rename) {
229
367
  return {
230
368
  ok: false,
231
369
  message: "Thread display naming is unavailable.",
232
370
  };
233
371
  }
234
- return current(target, threadName);
372
+ const assertCurrent = () => {
373
+ assertAuthority?.();
374
+ if (assertAuthority && generation !== capturedGeneration)
375
+ throw new Error("Thread display naming binding changed.");
376
+ };
377
+ assertCurrent();
378
+ const result = assertAuthority
379
+ ? await rename(capturedTarget, threadName, {
380
+ assertAuthority: assertCurrent,
381
+ })
382
+ : await rename(capturedTarget, threadName);
383
+ assertCurrent();
384
+ return result;
235
385
  },
236
386
  };
237
387
  }
@@ -275,11 +425,15 @@ export function registerTelegramBridgeCommands(pi, deps) {
275
425
  let intentId;
276
426
  // Pi context getters throw after replacement; check plain intent/generation first.
277
427
  const isCurrent = () => (!intentId || deps.connectionIntent?.isActive(intentId) !== false) &&
278
- (sessionGeneration === undefined || deps.getSessionGeneration?.() === sessionGeneration) &&
428
+ (sessionGeneration === undefined ||
429
+ deps.getSessionGeneration?.() === sessionGeneration) &&
279
430
  deps.isContextCurrent?.(ctx) !== false;
280
431
  if (!isCurrent())
281
432
  return;
282
- if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
433
+ if (args
434
+ .trim()
435
+ .split(/\s+/)
436
+ .some((word) => /^as=/i.test(word))) {
283
437
  ctx.ui.notify("Thread names are configured from Telegram, not from Pi commands.", "warning");
284
438
  deps.updateStatus(ctx);
285
439
  return;
@@ -299,7 +453,8 @@ export function registerTelegramBridgeCommands(pi, deps) {
299
453
  ctx.ui.notify(`Activated profile "${profileName}".`, "info");
300
454
  }
301
455
  else {
302
- await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ?? deps.reloadConfig());
456
+ await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ??
457
+ deps.reloadConfig());
303
458
  if (!isCurrent())
304
459
  return;
305
460
  }
@@ -316,53 +471,17 @@ export function registerTelegramBridgeCommands(pi, deps) {
316
471
  await deps.promptForConfig(ctx, profileName);
317
472
  return;
318
473
  }
319
- let recoveryUsed = false;
320
- const startWithRecovery = async (options) => {
474
+ const startPolling = async (options) => {
321
475
  try {
322
476
  return await deps.startPolling(ctx, options);
323
477
  }
324
478
  catch (error) {
325
479
  if (!isCurrent())
326
480
  return;
327
- if (!deps.recoverPollingStart || recoveryUsed)
328
- throw error;
329
- deps.recordConnectionEvent?.(error, "polling-start");
330
- const recovery = await deps.recoverPollingStart(error);
331
- if (!isCurrent())
332
- return;
333
- if (recovery.kind === "unhandled")
334
- throw error;
335
- if (recovery.kind === "blocked") {
336
- return { ok: false, message: recovery.message,
337
- notice: "Telegram recovery blocked. Check /telegram-status --debug." };
338
- }
339
- recoveryUsed = true;
340
- deps.recordConnectionEvent?.(recovery.message, "recovery");
341
- try {
342
- const retry = await deps.startPolling(ctx, options);
343
- if (!isCurrent())
344
- return;
345
- if (!retry)
346
- return { ok: true, message: "Telegram bridge connected; temporary state recovered." };
347
- return {
348
- ...retry,
349
- message: retry.ok
350
- ? "Telegram bridge connected; temporary state recovered."
351
- : retry.message,
352
- };
353
- }
354
- catch (error) {
355
- if (!isCurrent())
356
- return;
357
- deps.recordConnectionEvent?.(error, "recovery-retry");
358
- return {
359
- ok: false,
360
- notice: "Telegram recovery failed. Restart this Pi instance.",
361
- };
362
- }
481
+ throw error;
363
482
  }
364
483
  };
365
- let result = await startWithRecovery({ forceFreshLeaderThread: true });
484
+ let result = await startPolling({ forceFreshLeaderThread: true });
366
485
  if (!isCurrent())
367
486
  return;
368
487
  if (result && !result.ok && result.canTakeover) {
@@ -374,14 +493,17 @@ export function registerTelegramBridgeCommands(pi, deps) {
374
493
  deps.updateStatus(ctx);
375
494
  return;
376
495
  }
377
- result = await startWithRecovery({ force: true, forceFreshLeaderThread: true });
496
+ result = await startPolling({
497
+ force: true,
498
+ forceFreshLeaderThread: true,
499
+ });
378
500
  if (!isCurrent())
379
501
  return;
380
502
  }
381
503
  if (result && !result.ok) {
382
504
  if (result.message)
383
505
  deps.recordConnectionEvent?.(result.message, "connect-refused");
384
- ctx.ui.notify(result.notice ?? formatTelegramConnectionFailure(result.message), "warning");
506
+ ctx.ui.notify(formatTelegramConnectionFailure(result.message), "warning");
385
507
  }
386
508
  else if (result?.message) {
387
509
  ctx.ui.notify(result.message, "info");
@@ -407,7 +529,8 @@ export function registerTelegramBridgeCommands(pi, deps) {
407
529
  description: "Stop Telegram and delete current thread in Threaded Mode",
408
530
  handler: async (_args, ctx) => {
409
531
  const generation = deps.getSessionGeneration?.();
410
- const isCurrent = () => (generation === undefined || deps.getSessionGeneration?.() === generation) &&
532
+ const isCurrent = () => (generation === undefined ||
533
+ deps.getSessionGeneration?.() === generation) &&
411
534
  deps.isContextCurrent?.(ctx) !== false;
412
535
  if (!isCurrent())
413
536
  return;
@@ -464,6 +587,14 @@ export function isTelegramReservedCommandName(commandName) {
464
587
  return (commandName !== undefined &&
465
588
  TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName));
466
589
  }
590
+ function isTelegramSessionBusy(ports) {
591
+ return (!ports.isIdle() ||
592
+ ports.hasPendingMessages() ||
593
+ ports.hasActiveTelegramTurn() ||
594
+ ports.hasDispatchPending() ||
595
+ ports.hasQueuedTelegramItems() ||
596
+ ports.isCompactionInProgress());
597
+ }
467
598
  function canPairTelegramUserFromCommandMessage(message) {
468
599
  return message.chat.type === undefined || message.chat.type === "private";
469
600
  }
@@ -476,16 +607,6 @@ export function getTelegramCommandMessageTarget(message) {
476
607
  replyToMessageId: message.message_id,
477
608
  };
478
609
  }
479
- export function createTelegramCommandControlQueueRuntime(deps) {
480
- const controlQueueController = createTelegramControlQueueController({
481
- appendControlItem: deps.appendControlItem,
482
- dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
483
- });
484
- return createTelegramCommandControlEnqueueAdapter({
485
- createControlItem: deps.createControlItem,
486
- enqueueControlItem: controlQueueController.enqueue,
487
- });
488
- }
489
610
  export function createTelegramCommandControlEnqueueAdapter(deps) {
490
611
  return (target, ctx, controlType, statusSummary, execute, admissionReceipts, onQueued) => {
491
612
  deps.enqueueControlItem(deps.createControlItem({
@@ -498,11 +619,15 @@ export function createTelegramCommandControlEnqueueAdapter(deps) {
498
619
  };
499
620
  }
500
621
  export function createTelegramCommandTargetQueueRuntime(deps) {
622
+ const { createControlItem, appendControlItem, dispatchNextQueuedTelegramTurn, } = deps;
623
+ const controlQueueController = createTelegramControlQueueController({
624
+ appendControlItem,
625
+ dispatchNextQueuedTelegramTurn,
626
+ });
501
627
  return createTelegramCommandTargetRuntime({
502
- enqueueControlItem: createTelegramCommandControlQueueRuntime({
503
- createControlItem: deps.createControlItem,
504
- appendControlItem: deps.appendControlItem,
505
- dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
628
+ enqueueControlItem: createTelegramCommandControlEnqueueAdapter({
629
+ createControlItem,
630
+ enqueueControlItem: controlQueueController.enqueue,
506
631
  }),
507
632
  getAdmissionScope: deps.getAdmissionScope,
508
633
  getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
@@ -531,85 +656,70 @@ export function createTelegramCommandTargetRuntime(deps) {
531
656
  ...(journalBindingKey ? { journalBindingKey } : {}),
532
657
  }
533
658
  : undefined;
534
- deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt
535
- ? () => deps.onControlQueued?.(message, receipt)
536
- : undefined);
659
+ deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt ? () => deps.onControlQueued?.(message, receipt) : undefined);
537
660
  },
538
- showStatus: (message, ctx) => {
661
+ showStatus: (message, ctx, options) => {
539
662
  const target = getTelegramCommandMessageTarget(message);
540
- return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId);
663
+ return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
541
664
  },
542
- openModelMenu: (message, ctx) => {
665
+ openModelMenu: (message, ctx, options) => {
543
666
  const target = getTelegramCommandMessageTarget(message);
544
- return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId);
667
+ return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
545
668
  },
546
- openSettingsMenu: async (message, ctx) => {
669
+ openSettingsMenu: async (message, ctx, options) => {
547
670
  const target = getTelegramCommandMessageTarget(message);
548
671
  if (!deps.openSettingsMenu) {
549
672
  await deps.sendTextReply(target.chatId, target.replyToMessageId, formatTelegramInformationHeading("🚫", "Settings menu is unavailable."), { target, parseMode: "HTML" });
550
673
  return;
551
674
  }
552
- await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId);
675
+ await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
553
676
  },
554
677
  sendTextReply: async (message, text, options) => {
555
678
  const target = getTelegramCommandMessageTarget(message);
556
- await deps.sendTextReply(target.chatId, target.replyToMessageId, text, {
557
- ...options,
558
- target,
559
- });
679
+ const assertAuthority = options?.assertAuthority, send = deps.sendTextReply;
680
+ const capturedOptions = { ...options, target };
681
+ assertAuthority?.();
682
+ await send(target.chatId, target.replyToMessageId, text, capturedOptions);
683
+ assertAuthority?.();
560
684
  },
561
685
  };
562
686
  }
563
- export const TELEGRAM_APP_MENU_INTRO_HTML = [
564
- "<b>Pi Telegram</b>",
565
- "",
566
- `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
567
- `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
568
- `${formatTelegramCommandEmojiPrefix("new")}/new — Start a new session`,
569
- `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
570
- `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
571
- `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
572
- `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
573
- ].join("\n");
574
- function escapeTelegramCommandMenuHtml(text) {
575
- return text
576
- .replace(/&/g, "&amp;")
577
- .replace(/</g, "&lt;")
578
- .replace(/>/g, "&gt;");
687
+ /** Pure intro: built-in controls with registered extension lines before the abort controls. */
688
+ function formatTelegramAppMenuIntroHtml(extensionLines) {
689
+ const line = (name, label) => `${formatTelegramCommandEmojiPrefix(name)}/${name} — ${label}`;
690
+ return [
691
+ "<b>Pi Telegram</b>",
692
+ "",
693
+ line("start", "Open menu / Pair bridge"),
694
+ line("compact", "Compact current session"),
695
+ line("new", "Start a new session"),
696
+ line("continue", "Queue continue prompt"),
697
+ line("next", "Force next turn"),
698
+ ...extensionLines,
699
+ line("abort", "Abort Pi"),
700
+ line("stop", "Abort Pi & Clear queue"),
701
+ ].join("\n");
579
702
  }
703
+ export const TELEGRAM_APP_MENU_INTRO_HTML = formatTelegramAppMenuIntroHtml([]);
580
704
  function buildTelegramPromptTemplateMenuHtml(promptTemplates = []) {
581
705
  if (promptTemplates.length === 0)
582
706
  return "";
583
707
  return promptTemplates
584
- .map((template) => `🧩 /${escapeTelegramCommandMenuHtml(template.command)}`)
708
+ .map((template) => `🧩 /${escapeHtml(template.command)}`)
585
709
  .join("\n");
586
710
  }
587
711
  function buildTelegramExtensionCommandMenuLines() {
588
712
  return getTelegramExtensionCommands()
589
713
  .filter((command) => command.showInMenu)
590
714
  .map((command) => {
591
- const prefix = `${escapeTelegramCommandMenuHtml(command.emoji ?? "")} /${escapeTelegramCommandMenuHtml(command.name)}`;
715
+ const prefix = `${escapeHtml(command.emoji ?? "")} /${escapeHtml(command.name)}`;
592
716
  if (!command.description)
593
717
  return prefix;
594
- return `${prefix} — ${escapeTelegramCommandMenuHtml(command.description)}`;
718
+ return `${prefix} — ${escapeHtml(command.description)}`;
595
719
  });
596
720
  }
597
721
  function buildTelegramAppMenuIntroHtml() {
598
- const extensionLines = buildTelegramExtensionCommandMenuLines();
599
- if (extensionLines.length === 0)
600
- return TELEGRAM_APP_MENU_INTRO_HTML;
601
- return [
602
- "<b>Pi Telegram</b>",
603
- "",
604
- `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
605
- `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
606
- `${formatTelegramCommandEmojiPrefix("new")}/new — Start a new session`,
607
- `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
608
- `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
609
- ...extensionLines,
610
- `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
611
- `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
612
- ].join("\n");
722
+ return formatTelegramAppMenuIntroHtml(buildTelegramExtensionCommandMenuLines());
613
723
  }
614
724
  export function buildTelegramAppMenuHtml(statusHtml, promptTemplates = []) {
615
725
  const introHtml = buildTelegramAppMenuIntroHtml();
@@ -677,9 +787,6 @@ export function buildTelegramCommandAction(commandName) {
677
787
  }
678
788
  return TELEGRAM_COMMAND_ACTIONS[commandName];
679
789
  }
680
- export function getTelegramCommandExecutionMode(action) {
681
- return action.executionMode;
682
- }
683
790
  function formatTelegramQueuedTurnCount(count) {
684
791
  return count === 1 ? "1 queued turn" : `${count} queued turns`;
685
792
  }
@@ -758,13 +865,26 @@ function buildTelegramNewConfirmationReplyMarkup() {
758
865
  ],
759
866
  };
760
867
  }
868
+ /** Send one confirmation to an exact copied recipient, with authority checks around the issued request. */
869
+ async function sendTelegramConfirmation(target, deps, html, markup, assertAuthority) {
870
+ const recipientTarget = {
871
+ chatId: target.chatId,
872
+ ...(target.threadId !== undefined ? { threadId: target.threadId } : {}),
873
+ };
874
+ const options = assertAuthority
875
+ ? { target: recipientTarget, assertAuthority }
876
+ : target.threadId !== undefined
877
+ ? { target: recipientTarget }
878
+ : undefined;
879
+ assertAuthority?.();
880
+ await deps.sendInteractiveMessage(recipientTarget.chatId, html, "html", markup, options);
881
+ assertAuthority?.();
882
+ }
761
883
  function getTelegramNewConfirmationHtml() {
762
884
  return "<b>Start a new session?</b>";
763
885
  }
764
- export async function openTelegramNewConfirmation(target, deps) {
765
- await deps.sendInteractiveMessage(target.chatId, getTelegramNewConfirmationHtml(), "html", buildTelegramNewConfirmationReplyMarkup(), target.threadId !== undefined
766
- ? { target: { chatId: target.chatId, threadId: target.threadId } }
767
- : undefined);
886
+ export async function openTelegramNewConfirmation(target, deps, assertAuthority) {
887
+ await sendTelegramConfirmation(target, deps, getTelegramNewConfirmationHtml(), buildTelegramNewConfirmationReplyMarkup(), assertAuthority);
768
888
  }
769
889
  export async function handleTelegramNewConfirmationCallback(query, deps) {
770
890
  if (query.data !== "new:confirm" && query.data !== "new:cancel")
@@ -772,7 +892,7 @@ export async function handleTelegramNewConfirmationCallback(query, deps) {
772
892
  const chatId = query.message?.chat?.id;
773
893
  const messageId = query.message?.message_id;
774
894
  if (typeof chatId !== "number" || typeof messageId !== "number") {
775
- await deps.answerCallbackQuery(query.id, "⌛ Interactive message expired.");
895
+ await deps.answerCallbackQuery(query.id, "Interactive message expired");
776
896
  return true;
777
897
  }
778
898
  if (query.data === "new:cancel") {
@@ -798,10 +918,8 @@ function buildTelegramCompactConfirmationReplyMarkup() {
798
918
  function getTelegramCompactConfirmationHtml() {
799
919
  return "<b>Compact session?</b>";
800
920
  }
801
- async function openTelegramCompactConfirmation(target, deps) {
802
- await deps.sendInteractiveMessage(target.chatId, getTelegramCompactConfirmationHtml(), "html", buildTelegramCompactConfirmationReplyMarkup(), target.threadId !== undefined
803
- ? { target: { chatId: target.chatId, threadId: target.threadId } }
804
- : undefined);
921
+ async function openTelegramCompactConfirmation(target, deps, assertAuthority) {
922
+ await sendTelegramConfirmation(target, deps, getTelegramCompactConfirmationHtml(), buildTelegramCompactConfirmationReplyMarkup(), assertAuthority);
805
923
  }
806
924
  export async function handleTelegramCompactConfirmationCallback(query, deps) {
807
925
  if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
@@ -811,7 +929,7 @@ export async function handleTelegramCompactConfirmationCallback(query, deps) {
811
929
  const chatId = callbackMessage?.chat?.id;
812
930
  const messageId = callbackMessage?.message_id;
813
931
  if (typeof chatId !== "number" || typeof messageId !== "number") {
814
- await deps.answerCallbackQuery(query.id, "⌛ Interactive message expired.");
932
+ await deps.answerCallbackQuery(query.id, "Interactive message expired");
815
933
  return true;
816
934
  }
817
935
  if (query.data === "compact:cancel") {
@@ -826,12 +944,7 @@ export async function handleTelegramCompactConfirmationCallback(query, deps) {
826
944
  return true;
827
945
  }
828
946
  export async function handleTelegramNewCommand(deps) {
829
- if (!deps.isIdle() ||
830
- deps.hasPendingMessages() ||
831
- deps.hasActiveTelegramTurn() ||
832
- deps.hasDispatchPending() ||
833
- deps.hasQueuedTelegramItems() ||
834
- deps.isCompactionInProgress()) {
947
+ if (isTelegramSessionBusy(deps)) {
835
948
  await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot start a new session while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
836
949
  return;
837
950
  }
@@ -842,12 +955,7 @@ export async function handleTelegramNewCommand(deps) {
842
955
  deps.requestNewSession();
843
956
  }
844
957
  export async function handleTelegramCompactCommand(deps) {
845
- if (!deps.isIdle() ||
846
- deps.hasPendingMessages() ||
847
- deps.hasActiveTelegramTurn() ||
848
- deps.hasDispatchPending() ||
849
- deps.hasQueuedTelegramItems() ||
850
- deps.isCompactionInProgress()) {
958
+ if (isTelegramSessionBusy(deps)) {
851
959
  await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
852
960
  return;
853
961
  }
@@ -889,26 +997,12 @@ export async function handleTelegramCompactCommand(deps) {
889
997
  });
890
998
  }
891
999
  }
892
- function isTelegramStaleContextError(error) {
893
- return (error instanceof Error &&
894
- (error.message.includes("stale after session") ||
895
- error.message.includes("stale ctx")));
896
- }
897
- export async function handleTelegramStatusCommand(deps) {
898
- try {
899
- await deps.showStatus(deps.ctx);
900
- }
901
- catch (error) {
902
- if (!isTelegramStaleContextError(error))
903
- throw error;
904
- }
905
- }
906
1000
  export async function handleTelegramModelCommand(deps) {
907
1001
  try {
908
1002
  await deps.openModelMenu(deps.ctx);
909
1003
  }
910
1004
  catch (error) {
911
- if (!isTelegramStaleContextError(error))
1005
+ if (!isPiStaleContextError(error))
912
1006
  throw error;
913
1007
  }
914
1008
  }
@@ -976,6 +1070,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
976
1070
  sendTextReply: deps.sendTextReply,
977
1071
  });
978
1072
  return createTelegramCommandHandler({
1073
+ assertExecutionCurrent: deps.assertExecutionCurrent,
979
1074
  hasAbortHandler: deps.hasAbortHandler,
980
1075
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
981
1076
  hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
@@ -990,6 +1085,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
990
1085
  setCompactionInProgress: deps.setCompactionInProgress,
991
1086
  updateStatus: deps.updateStatus,
992
1087
  isContextActive: deps.isContextActive,
1088
+ beginCommandEffectWork: deps.beginCommandEffectWork,
993
1089
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
994
1090
  requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
995
1091
  markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
@@ -997,6 +1093,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
997
1093
  startTypingLoop: deps.startTypingLoop,
998
1094
  stopTypingLoop: deps.stopTypingLoop,
999
1095
  enqueueContinueTurn: deps.enqueueContinueTurn,
1096
+ heldTurn: deps.heldTurn,
1000
1097
  compact: deps.compact,
1001
1098
  requestNewSession: deps.requestNewSession,
1002
1099
  sendInteractiveMessage: deps.sendInteractiveMessage,
@@ -1020,61 +1117,1009 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
1020
1117
  recordRuntimeEvent: deps.recordRuntimeEvent,
1021
1118
  });
1022
1119
  }
1023
- export function createTelegramCommandHandler(deps) {
1024
- return async (commandName, message, ctx, commandArgs) => {
1025
- return handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs);
1120
+ /**
1121
+ * Captures admission ports now and checks source → execution → recipient; `strict` rechecks the source after the
1122
+ * recipient, for owners whose effects outlive one synchronous step.
1123
+ */
1124
+ function createSelectedSemanticGuard(admission, assertExecution, strict) {
1125
+ const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
1126
+ return () => {
1127
+ assertSourceCurrent();
1128
+ assertExecution();
1129
+ assertRecipientCurrent();
1130
+ if (!strict)
1131
+ return;
1132
+ assertSourceCurrent();
1133
+ assertExecution();
1134
+ };
1135
+ }
1136
+ /** Wrap one synchronous command effect between semantic checks. */
1137
+ function guardSelectedEffect(assertCurrent) {
1138
+ return (effect) => (...args) => {
1139
+ assertCurrent();
1140
+ const result = effect(...args);
1141
+ assertCurrent();
1142
+ return result;
1143
+ };
1144
+ }
1145
+ /** One issuance only: later calls return false without assertions or effects, never replay. */
1146
+ function issueSelectedOnce(assertCurrent, run) {
1147
+ let issued = false;
1148
+ return async () => {
1149
+ if (issued)
1150
+ return false;
1151
+ assertCurrent();
1152
+ issued = true;
1153
+ return run();
1026
1154
  };
1027
1155
  }
1028
- export function createTelegramCommandOrPromptRuntime(deps) {
1156
+ /** Body-free reply anchor: chat, message and Thread only, never the original's admission symbols. */
1157
+ function copySelectedReplyAnchor(original) {
1029
1158
  return {
1030
- dispatchMessages: async (messages, ctx) => {
1031
- const firstMessage = messages[0];
1032
- if (!firstMessage)
1033
- return;
1034
- if (deps.shouldIgnoreMessages?.(messages))
1035
- return;
1036
- deps.assertExecutionCurrent?.(firstMessage);
1037
- if (await deps.consumeThreadNameInput?.(messages, ctx)) {
1038
- deps.assertExecutionCurrent?.(firstMessage);
1039
- return;
1159
+ chat: { ...original.chat },
1160
+ message_id: original.message_id,
1161
+ message_thread_id: original.message_thread_id,
1162
+ };
1163
+ }
1164
+ function copySelectedMenuCarrier(original) {
1165
+ const message = Object.fromEntries(Object.entries(original).filter(([key]) => key !== "pi_telegram_source_update_id"));
1166
+ message.chat = { ...original.chat };
1167
+ return message;
1168
+ }
1169
+ export function createTelegramCommandHandler(deps) {
1170
+ const handle = async (commandName, message, ctx, commandArgs) => {
1171
+ return handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs);
1172
+ };
1173
+ const heldQueueReceipts = new Map();
1174
+ const heldQueueOwners = new WeakSet();
1175
+ const watchHeldQueueOwner = (signal) => {
1176
+ if (heldQueueOwners.has(signal))
1177
+ return;
1178
+ addAbortListener(signal, () => {
1179
+ // Native owner end is irreversible; keep refusal identity, not its context/carrier closures or a fallback grant.
1180
+ for (const [id, record] of heldQueueReceipts)
1181
+ if (record?.signal === signal)
1182
+ heldQueueReceipts.set(id, null);
1183
+ });
1184
+ heldQueueOwners.add(signal);
1185
+ };
1186
+ const prepareHeldSelectedCommand = (name, readiness, admission, captured, portsCurrent, completion, run, inspectQueuedCompletion) => {
1187
+ const snapshot = readiness.snapshot, raw = snapshot.update.message, target = { ...admission.target };
1188
+ const bind = readiness.bindCarrier, isCurrent = readiness.isCurrent, assertSource = admission.assertSourceCurrent, assertRecipient = admission.assertRecipientCurrent;
1189
+ if (typeof bind !== "function" ||
1190
+ typeof isCurrent !== "function" ||
1191
+ typeof assertSource !== "function" ||
1192
+ typeof assertRecipient !== "function" ||
1193
+ !isCurrent.call(readiness) ||
1194
+ !raw ||
1195
+ typeof raw !== "object" ||
1196
+ Array.isArray(raw) ||
1197
+ Object.keys(snapshot.update).some((key) => !["update_id", "message"].includes(key)))
1198
+ return undefined;
1199
+ const value = raw, chat = value.chat, from = value.from;
1200
+ if (typeof captured.getAllowedUserId !== "function")
1201
+ return undefined;
1202
+ const command = typeof value.text === "string"
1203
+ ? parseTelegramCommand(value.text)
1204
+ : undefined, operator = captured.getAllowedUserId();
1205
+ if (!command ||
1206
+ !chat ||
1207
+ !from ||
1208
+ !isTelegramSelectedHeldOriginal(snapshot.update, target, operator, name))
1209
+ return undefined;
1210
+ const source = { ...snapshot.source }, preparedCommand = { ...command };
1211
+ const message = {
1212
+ ...structuredClone(value),
1213
+ chat: { ...chat },
1214
+ message_thread_id: target.threadId,
1215
+ };
1216
+ delete message.pi_telegram_source_update_id;
1217
+ let bound, issued = false;
1218
+ const ownerCurrent = () => portsCurrent() &&
1219
+ readiness.bindCarrier === bind &&
1220
+ readiness.isCurrent === isCurrent &&
1221
+ isCurrent.call(readiness) &&
1222
+ captured.getAllowedUserId() === operator;
1223
+ const completionCurrent = () => !!bound &&
1224
+ Object.entries(bound.ports).every(([key, port]) => Reflect.get(bound.completion, key) === port) &&
1225
+ bound.ports.isCurrent.call(bound.completion);
1226
+ const assertSourceCurrent = () => {
1227
+ assertSource.call(admission);
1228
+ if (!ownerCurrent() || !completionCurrent() || !bound)
1229
+ throw new Error(`Held ${name} source owner is unavailable.`);
1230
+ captured.assertExecutionCurrent?.(bound.carrier);
1231
+ const current = inspectTelegramDeferredSource(bound.carrier);
1232
+ if (!current ||
1233
+ current.updateId !== source.updateId ||
1234
+ current.journalBindingKey !== source.journalBindingKey ||
1235
+ current.sourceSha256 !== source.sourceSha256 ||
1236
+ !ownerCurrent() ||
1237
+ !completionCurrent())
1238
+ throw new Error(`Held ${name} original changed.`);
1239
+ };
1240
+ return {
1241
+ get source() {
1242
+ return { ...source };
1243
+ },
1244
+ get command() {
1245
+ return { ...preparedCommand };
1246
+ },
1247
+ bindCarrier(value) {
1248
+ if (!ownerCurrent())
1249
+ return false;
1250
+ if (bound)
1251
+ return value === bound.carrier && completionCurrent();
1252
+ const completion = bind.call(readiness, value);
1253
+ if (!completion ||
1254
+ completion.source.updateId !== source.updateId ||
1255
+ completion.source.journalBindingKey !== source.journalBindingKey ||
1256
+ completion.source.sourceSha256 !== source.sourceSha256 ||
1257
+ !ownerCurrent())
1258
+ return false;
1259
+ const ports = {
1260
+ isCurrent: completion.isCurrent,
1261
+ reportCompleted: completion.reportCompleted,
1262
+ inspectCompletion: completion.inspectCompletion,
1263
+ };
1264
+ if (Object.values(ports).some((port) => typeof port !== "function"))
1265
+ return false;
1266
+ bound = { carrier: value, completion, ports };
1267
+ return completionCurrent();
1268
+ },
1269
+ async execute() {
1270
+ if (issued)
1271
+ return false;
1272
+ assertSourceCurrent();
1273
+ assertRecipient.call(admission);
1274
+ assertSourceCurrent();
1275
+ issued = true;
1276
+ if (completion === "before-effect" &&
1277
+ !bound.ports.reportCompleted.call(bound.completion))
1278
+ return false;
1279
+ return run({
1280
+ message,
1281
+ carrier: bound.carrier,
1282
+ source: { ...source },
1283
+ command: { ...preparedCommand },
1284
+ assertRecipient,
1285
+ reportCompleted: () => bound.ports.reportCompleted.call(bound.completion),
1286
+ assertSemantic() {
1287
+ assertSourceCurrent();
1288
+ assertRecipient.call(admission);
1289
+ },
1290
+ });
1291
+ },
1292
+ inspectCompletion() {
1293
+ return ownerCurrent() && completionCurrent()
1294
+ ? completion === "queue-receipt"
1295
+ ? inspectQueuedCompletion?.()
1296
+ : bound.ports.inspectCompletion.call(bound.completion)
1297
+ : undefined;
1298
+ },
1299
+ };
1300
+ };
1301
+ const prepareHeldReplyPlan = (ctx, effects, run, replyDelivery = "detached") => {
1302
+ const send = effects.sendTextReply, captured = { ...deps };
1303
+ return {
1304
+ captured,
1305
+ portsCurrent: () => effects.sendTextReply === send,
1306
+ async run(input) {
1307
+ const semantic = guardSelectedEffect(input.assertSemantic);
1308
+ const complete = () => {
1309
+ input.assertSemantic();
1310
+ if (!input.reportCompleted())
1311
+ throw new Error(`Held ${input.command.name} source completion refused.`);
1312
+ input.assertRecipient();
1313
+ };
1314
+ await run(captured, semantic, complete, async (text, options) => {
1315
+ complete();
1316
+ const deliver = async () => {
1317
+ input.assertRecipient();
1318
+ await send.call(effects, input.message.chat.id, input.message.message_id, text, {
1319
+ ...options,
1320
+ target: {
1321
+ chatId: input.message.chat.id,
1322
+ threadId: input.message.message_thread_id,
1323
+ },
1324
+ assertAuthority: input.assertRecipient,
1325
+ });
1326
+ input.assertRecipient();
1327
+ };
1328
+ // Transport waiting never changes the already reported semantic completion or licenses another effect.
1329
+ if (replyDelivery === "inline")
1330
+ await deliver();
1331
+ else
1332
+ scheduleTelegramCommandEffect(ctx, input.command.name, "selected-reply", captured, deliver, input.assertRecipient);
1333
+ });
1334
+ return true;
1335
+ },
1336
+ };
1337
+ };
1338
+ const prepareHeldQueuePlan = (ctx, template) => {
1339
+ const captured = { ...deps }, owner = captured.heldTurn, enqueue = owner.enqueue;
1340
+ let retained;
1341
+ const portsCurrent = () => deps.heldTurn === owner &&
1342
+ owner.enqueue === enqueue &&
1343
+ (!template || template.isCurrent()) &&
1344
+ deps.heldTurn === owner &&
1345
+ owner.enqueue === enqueue;
1346
+ return {
1347
+ captured,
1348
+ portsCurrent,
1349
+ async run(input) {
1350
+ input.assertSemantic();
1351
+ if (template && !isDeepStrictEqual(input.command, template.command))
1352
+ return false;
1353
+ const { carrier, source } = input;
1354
+ if (!carrier || !source)
1355
+ return false;
1356
+ const signal = getTelegramUpdateExecutionFence(carrier)?.signal;
1357
+ if (!signal || signal.aborted)
1358
+ return false;
1359
+ const queue = input.queue ?? prepareTelegramDeferredQueueAdmission(carrier);
1360
+ if (!queue || !queue.isCurrent())
1361
+ return false;
1362
+ const ports = {
1363
+ isCurrent: queue.isCurrent,
1364
+ prepareCompletionScope: queue.prepareCompletionScope,
1365
+ inspectCompletion: queue.inspectCompletion,
1366
+ };
1367
+ if (Object.values(ports).some((port) => typeof port !== "function"))
1368
+ return false;
1369
+ const current = () => {
1370
+ if (!portsCurrent() ||
1371
+ !Object.entries(ports).every(([key, value]) => Reflect.get(queue, key) === value) ||
1372
+ !ports.isCurrent.call(queue))
1373
+ return false;
1374
+ try {
1375
+ input.assertRecipient();
1376
+ return portsCurrent() && ports.isCurrent.call(queue);
1377
+ }
1378
+ catch {
1379
+ return false;
1380
+ }
1381
+ };
1382
+ const sameTarget = carrier.message_thread_id === input.message.message_thread_id;
1383
+ const message = carryTelegramUpdateExecutionFence(carrier, {
1384
+ ...carrier,
1385
+ ...input.message,
1386
+ pi_telegram_source_update_id: source.updateId,
1387
+ ...(template ? { text: template.expanded, caption: undefined } : {}),
1388
+ ...(sameTarget ? {} : { message_id: 0, reply_to_message: undefined }),
1389
+ });
1390
+ const completionSha256 = createHash("sha256")
1391
+ .update(JSON.stringify([
1392
+ "held-command-v1",
1393
+ randomUUID(),
1394
+ source,
1395
+ input.command,
1396
+ getTelegramCommandMessageTarget(message),
1397
+ ]))
1398
+ .digest("hex");
1399
+ const admission = {
1400
+ assertCurrent: input.assertSemantic,
1401
+ report(receipts) {
1402
+ input.assertSemantic();
1403
+ const receipt = receipts[0];
1404
+ if (retained ||
1405
+ receipts.length !== 1 ||
1406
+ !receipt ||
1407
+ receipt.queueKind !== "prompt" ||
1408
+ receipt.journalBindingKey !== source.journalBindingKey ||
1409
+ receipt.sourceUpdateIds.length !== 1 ||
1410
+ receipt.sourceUpdateIds[0] !== source.updateId ||
1411
+ heldQueueReceipts.has(receipt.receiptId) ||
1412
+ !current())
1413
+ throw new Error("Held command needs one exact original queue receipt.");
1414
+ const expected = structuredClone(receipt);
1415
+ retained = {
1416
+ ctx,
1417
+ signal,
1418
+ receipt,
1419
+ queue,
1420
+ completionSha256,
1421
+ current: () => current() && isDeepStrictEqual(receipt, expected),
1422
+ };
1423
+ watchHeldQueueOwner(signal);
1424
+ heldQueueReceipts.set(receipt.receiptId, signal.aborted ? null : retained);
1425
+ if (!reportTelegramQueueAdmission([carrier], [receipt]))
1426
+ throw new Error("Held command queue admission refused.");
1427
+ },
1428
+ };
1429
+ if (template)
1430
+ await enqueue.call(owner, message, ctx, "prompt", admission);
1431
+ else
1432
+ await handleTelegramContinueCommand(message, ctx, {
1433
+ enqueueContinueTurn: (value, context) => enqueue.call(owner, value, context, "continue", admission),
1434
+ });
1435
+ return !!retained && current();
1436
+ },
1437
+ inspectQueuedCompletion() {
1438
+ if (!retained || !retained.current())
1439
+ return undefined;
1440
+ const proof = retained.queue.inspectCompletion(retained.receipt);
1441
+ if (proof && retained.current()) {
1442
+ heldQueueReceipts.delete(retained.receipt.receiptId);
1443
+ return { ...proof };
1444
+ }
1445
+ return undefined;
1446
+ },
1447
+ };
1448
+ };
1449
+ const heldPlans = {
1450
+ status: {
1451
+ completion: "before-effect",
1452
+ effect: "showStatus",
1453
+ prepare(ctx, effects) {
1454
+ const scopedStatus = effects?.showStatus;
1455
+ const captured = {
1456
+ ...deps,
1457
+ showStatus: scopedStatus
1458
+ ? (message, context, options) => {
1459
+ const address = getTelegramCommandMessageTarget(message);
1460
+ return scopedStatus.call(effects, address.chatId, address.replyToMessageId, context, address.threadId, options);
1461
+ }
1462
+ : deps.showStatus,
1463
+ };
1464
+ return {
1465
+ captured,
1466
+ portsCurrent: () => !effects || effects.showStatus === scopedStatus,
1467
+ async run({ message, command, assertRecipient }) {
1468
+ const recipientOptions = { assertAuthority: assertRecipient };
1469
+ return handleTelegramCommandRuntime(command.name, message, ctx, {
1470
+ ...captured,
1471
+ assertExecutionCurrent: assertRecipient,
1472
+ showStatus: (value, context) => captured.showStatus(value, context, recipientOptions),
1473
+ }, command.args);
1474
+ },
1475
+ };
1476
+ },
1477
+ },
1478
+ abort: {
1479
+ completion: "after-semantics",
1480
+ effect: "sendTextReply",
1481
+ prepare(ctx, effects, _command, delivery) {
1482
+ return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, _complete, reply) => {
1483
+ await handleTelegramAbortCommand({
1484
+ hasAbortHandler: semantic(captured.hasAbortHandler),
1485
+ hasActiveTelegramTurn: semantic(captured.hasActiveTelegramTurn),
1486
+ clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
1487
+ cancelNextTransitionAnnouncements: captured.cancelNextTransitionAnnouncements &&
1488
+ semantic(captured.cancelNextTransitionAnnouncements),
1489
+ abortCurrentTurn: semantic(captured.abortCurrentTurn),
1490
+ setFoldQueuedPromptsIntoHistory: semantic(captured.setFoldQueuedPromptsIntoHistory),
1491
+ updateStatus: semantic(() => captured.updateStatus(ctx)),
1492
+ sendTextReply: reply,
1493
+ });
1494
+ }, delivery);
1495
+ },
1496
+ },
1497
+ stop: {
1498
+ completion: "after-semantics",
1499
+ effect: "sendTextReply",
1500
+ available: () => typeof deps.cancelNextTransitionAnnouncements === "function" &&
1501
+ typeof deps.clearQueuedTelegramItems === "function",
1502
+ prepare(ctx, effects, _command, delivery) {
1503
+ return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, _complete, reply) => {
1504
+ await handleTelegramStopCommand({
1505
+ hasAbortHandler: semantic(captured.hasAbortHandler),
1506
+ clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
1507
+ cancelNextTransitionAnnouncements: semantic(captured.cancelNextTransitionAnnouncements),
1508
+ clearQueuedTelegramItems: semantic(() => captured.clearQueuedTelegramItems(ctx)),
1509
+ setFoldQueuedPromptsIntoHistory: semantic(captured.setFoldQueuedPromptsIntoHistory),
1510
+ abortCurrentTurn: semantic(captured.abortCurrentTurn),
1511
+ updateStatus: semantic(() => captured.updateStatus(ctx)),
1512
+ sendTextReply: reply,
1513
+ });
1514
+ }, delivery);
1515
+ },
1516
+ },
1517
+ next: {
1518
+ completion: "after-semantics",
1519
+ effect: "sendTextReply",
1520
+ available: () => typeof deps.requestNextDispatchAnnouncement === "function" &&
1521
+ typeof deps.markActiveTurnNextAbortAnnouncement === "function",
1522
+ prepare(ctx, effects, _command, delivery) {
1523
+ return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, complete, reply) => {
1524
+ await handleTelegramNextCommand({
1525
+ hasAbortHandler: semantic(captured.hasAbortHandler),
1526
+ isIdle: semantic(() => captured.isIdle(ctx)),
1527
+ hasQueuedItems: semantic(captured.hasQueuedTelegramItems),
1528
+ clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
1529
+ abortCurrentTurn: semantic(captured.abortCurrentTurn),
1530
+ dispatchNextQueuedTurn: semantic(() => captured.dispatchNextQueuedTelegramTurn(ctx)),
1531
+ requestNextDispatchAnnouncement: semantic(captured.requestNextDispatchAnnouncement),
1532
+ markActiveTurnNextAbortAnnouncement: semantic(captured.markActiveTurnNextAbortAnnouncement),
1533
+ clearFoldForDispatch: semantic(() => captured.setFoldQueuedPromptsIntoHistory(false)),
1534
+ updateStatus() {
1535
+ semantic(() => captured.updateStatus(ctx))();
1536
+ complete();
1537
+ },
1538
+ sendTextReply: reply,
1539
+ });
1540
+ }, delivery);
1541
+ },
1542
+ },
1543
+ continue: {
1544
+ completion: "queue-receipt",
1545
+ available: () => typeof deps.heldTurn?.enqueue === "function",
1546
+ prepare(ctx) {
1547
+ return prepareHeldQueuePlan(ctx);
1548
+ },
1549
+ },
1550
+ };
1551
+ const getHeldTemplatePlan = (name) => {
1552
+ const owner = deps.heldTurn, templates = owner?.templates, get = templates?.getCommands, expand = templates?.expand;
1553
+ if (!owner ||
1554
+ !templates ||
1555
+ typeof owner.enqueue !== "function" ||
1556
+ typeof get !== "function" ||
1557
+ typeof expand !== "function" ||
1558
+ getTelegramReservedCommandNames().includes(name) ||
1559
+ findTelegramExtensionCommand(name))
1560
+ return undefined;
1561
+ const getIdentity = () => {
1562
+ const matching = get
1563
+ .call(templates)
1564
+ .filter((value) => value.command === name);
1565
+ const selected = matching[0];
1566
+ return matching.length === 1 &&
1567
+ selected &&
1568
+ typeof selected.path === "string" &&
1569
+ selected.path.trim()
1570
+ ? { command: selected.command, path: selected.path }
1571
+ : undefined;
1572
+ };
1573
+ let identity;
1574
+ try {
1575
+ identity = getIdentity();
1576
+ }
1577
+ catch {
1578
+ return undefined;
1579
+ }
1580
+ if (!identity)
1581
+ return undefined;
1582
+ const current = () => {
1583
+ if (deps.heldTurn !== owner ||
1584
+ owner.templates !== templates ||
1585
+ templates.getCommands !== get ||
1586
+ templates.expand !== expand ||
1587
+ findTelegramExtensionCommand(name))
1588
+ return false;
1589
+ try {
1590
+ return (isDeepStrictEqual(getIdentity(), identity) &&
1591
+ deps.heldTurn === owner &&
1592
+ owner.templates === templates &&
1593
+ templates.getCommands === get &&
1594
+ templates.expand === expand &&
1595
+ !findTelegramExtensionCommand(name));
1040
1596
  }
1041
- const command = parseTelegramCommand(deps.extractRawText(messages));
1042
- const handled = await deps.handleCommand(command?.name, firstMessage, ctx, command?.args);
1043
- deps.assertExecutionCurrent?.(firstMessage);
1044
- if (handled)
1045
- return;
1046
- if (command && deps.executeExtensionCommand) {
1047
- const handledByExtension = await deps.executeExtensionCommand(command, messages[0], ctx);
1048
- deps.assertExecutionCurrent?.(firstMessage);
1049
- if (handledByExtension)
1050
- return;
1597
+ catch {
1598
+ return false;
1051
1599
  }
1052
- if (command?.name && deps.expandPromptTemplateCommand) {
1053
- const expanded = deps.expandPromptTemplateCommand(command.name, command.args);
1054
- if (expanded !== undefined) {
1055
- deps.assertExecutionCurrent?.(firstMessage);
1056
- await deps.enqueueTurn([
1057
- deps.replaceMessageText(firstMessage, expanded),
1058
- ...messages.slice(1),
1059
- ], ctx);
1060
- return;
1600
+ };
1601
+ if (!current())
1602
+ return undefined;
1603
+ return {
1604
+ completion: "queue-receipt",
1605
+ template: true,
1606
+ prepare(ctx, _effects, command) {
1607
+ if (!command || command.name !== name || !current())
1608
+ return undefined;
1609
+ const captured = { ...command };
1610
+ let expanded;
1611
+ try {
1612
+ expanded = expand.call(templates, captured.name, captured.args);
1061
1613
  }
1614
+ catch {
1615
+ return undefined;
1616
+ }
1617
+ if (typeof expanded !== "string" || !current())
1618
+ return undefined;
1619
+ return prepareHeldQueuePlan(ctx, {
1620
+ command: captured,
1621
+ expanded,
1622
+ isCurrent() {
1623
+ if (!current())
1624
+ return false;
1625
+ try {
1626
+ return (expand.call(templates, captured.name, captured.args) ===
1627
+ expanded && current());
1628
+ }
1629
+ catch {
1630
+ return false;
1631
+ }
1632
+ },
1633
+ });
1634
+ },
1635
+ };
1636
+ };
1637
+ const getHeldPlan = (name, effects) => {
1638
+ if (!Object.hasOwn(heldPlans, name))
1639
+ return getHeldTemplatePlan(name);
1640
+ const plan = heldPlans[name];
1641
+ if (plan.available?.() === false)
1642
+ return undefined;
1643
+ if ((plan.effect &&
1644
+ effects !== undefined &&
1645
+ (!effects || typeof effects[plan.effect] !== "function")) ||
1646
+ (plan.effect === "sendTextReply" && !effects) ||
1647
+ (plan.effect === "showStatus" &&
1648
+ effects === undefined &&
1649
+ typeof deps.showStatus !== "function"))
1650
+ return undefined;
1651
+ return plan;
1652
+ };
1653
+ const prepareSelectedPlan = (command, messages, ctx, admission) => {
1654
+ const original = messages[0];
1655
+ if (!["status", "abort", "stop", "next"].includes(command.name) ||
1656
+ messages.length !== 1 ||
1657
+ !original ||
1658
+ typeof admission.assertSourceCurrent !== "function" ||
1659
+ typeof admission.assertRecipientCurrent !== "function" ||
1660
+ typeof admission.reportCompleted !== "function")
1661
+ return undefined;
1662
+ const captured = { ...deps }, parsed = { ...command }, message = parsed.name === "status"
1663
+ ? copySelectedMenuCarrier(original)
1664
+ : copySelectedReplyAnchor(original);
1665
+ const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted;
1666
+ const effects = parsed.name === "status"
1667
+ ? undefined
1668
+ : {
1669
+ async sendTextReply(_chat, _anchor, text, options) {
1670
+ await captured.sendTextReply(message, text, {
1671
+ parseMode: options.parseMode,
1672
+ assertAuthority: options.assertAuthority,
1673
+ });
1674
+ },
1675
+ };
1676
+ const plan = getHeldPlan(parsed.name, effects);
1677
+ if (!plan ||
1678
+ plan.completion === "queue-receipt" ||
1679
+ (parsed.name !== "status" && typeof captured.sendTextReply !== "function"))
1680
+ return undefined;
1681
+ const prepared = plan.prepare(ctx, effects, parsed, "inline");
1682
+ if (!prepared)
1683
+ return undefined;
1684
+ const semantic = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), plan.completion === "before-effect");
1685
+ const assertCurrent = () => {
1686
+ semantic();
1687
+ if (!prepared.portsCurrent())
1688
+ throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
1689
+ };
1690
+ return issueSelectedOnce(assertCurrent, async () => {
1691
+ if (plan.completion === "before-effect" && !report.call(admission))
1692
+ return false;
1693
+ return prepared.run({
1694
+ message,
1695
+ command: parsed,
1696
+ assertSemantic: assertCurrent,
1697
+ assertRecipient: assertRecipientCurrent,
1698
+ reportCompleted: () => report.call(admission),
1699
+ });
1700
+ });
1701
+ };
1702
+ const prepareSelectedHelp = (name) => (command, messages, ctx, admission) => {
1703
+ const original = messages[0];
1704
+ if (command.name !== name ||
1705
+ messages.length !== 1 ||
1706
+ !original ||
1707
+ !canPairTelegramUserFromCommandMessage(original) ||
1708
+ original.from?.id === undefined ||
1709
+ original.from.is_bot)
1710
+ return undefined;
1711
+ const captured = { ...deps }, userId = original.from.id;
1712
+ if (captured.getAllowedUserId() !== userId)
1713
+ return undefined;
1714
+ const message = {
1715
+ ...copySelectedReplyAnchor(original),
1716
+ from: { ...original.from },
1717
+ };
1718
+ const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
1719
+ const assertAdmissionCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), false);
1720
+ const assertSemanticCurrent = () => {
1721
+ assertAdmissionCurrent();
1722
+ if (captured.getAllowedUserId() !== userId)
1723
+ throw new Error(`Selected ${name} operator authority is unavailable.`);
1724
+ };
1725
+ return issueSelectedOnce(assertSemanticCurrent, () => handleTelegramHelpCommand(name, message, ctx, {
1726
+ ...captured,
1727
+ persistAllowedUserId: async () => {
1728
+ throw new Error(`Selected ${name} cannot acquire pairing authority.`);
1729
+ },
1730
+ }, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted }));
1731
+ };
1732
+ const prepareSelectedConfirmation = (name) => (command, messages, ctx, admission) => {
1733
+ const original = messages[0], captured = { ...deps };
1734
+ if (command.name !== name ||
1735
+ messages.length !== 1 ||
1736
+ !original ||
1737
+ !captured.sendInteractiveMessage)
1738
+ return undefined;
1739
+ const target = getTelegramCommandMessageTarget(original);
1740
+ const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
1741
+ const openConfirmation = name === "new"
1742
+ ? openTelegramNewConfirmation
1743
+ : openTelegramCompactConfirmation;
1744
+ const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
1745
+ return issueSelectedOnce(assertSemanticCurrent, async () => {
1746
+ if (!reportCompleted())
1747
+ return false;
1748
+ assertRecipientCurrent();
1749
+ scheduleTelegramCommandEffect(ctx, name, "confirmation-render", captured, () => openConfirmation(target, { sendInteractiveMessage: captured.sendInteractiveMessage }, assertRecipientCurrent), assertRecipientCurrent);
1750
+ return true;
1751
+ });
1752
+ };
1753
+ return Object.assign(handle, {
1754
+ /** Command-only producer execution; Routing must supply the captured plan and released exact-source authority. */
1755
+ prepareSelectedExtensionCommand(prepared, messages, ctx, admission) {
1756
+ const original = messages[0], captured = { ...deps }, plan = prepared.plan;
1757
+ if (messages.length !== 1 ||
1758
+ !original ||
1759
+ plan.kind !== "command-only" ||
1760
+ typeof captured.sendTextReply !== "function" ||
1761
+ typeof admission.reportCompleted !== "function")
1762
+ return undefined;
1763
+ const execute = plan.execute, assertRegistrationCurrent = prepared.assertRegistrationCurrent;
1764
+ const assertSourceCurrent = admission.assertSourceCurrent, recipientCurrent = admission.assertRecipientCurrent;
1765
+ const reportCompleted = admission.reportCompleted;
1766
+ // Only a body-free fixed recipient/anchor enters detached delivery, never the original's admission symbols.
1767
+ const message = copySelectedReplyAnchor(original);
1768
+ const assertRecipientCurrent = () => {
1769
+ if (captured.isContextActive?.(ctx) === false)
1770
+ throw new Error("Selected extension context changed.");
1771
+ assertRegistrationCurrent();
1772
+ recipientCurrent();
1773
+ assertRegistrationCurrent();
1774
+ if (captured.isContextActive?.(ctx) === false)
1775
+ throw new Error("Selected extension context changed.");
1776
+ };
1777
+ const assertCurrent = () => {
1778
+ assertSourceCurrent();
1779
+ captured.assertExecutionCurrent?.(original);
1780
+ assertRecipientCurrent();
1781
+ assertSourceCurrent();
1782
+ captured.assertExecutionCurrent?.(original);
1783
+ };
1784
+ try {
1785
+ assertCurrent();
1062
1786
  }
1063
- deps.assertExecutionCurrent?.(firstMessage);
1064
- await deps.enqueueTurn(messages, ctx);
1787
+ catch {
1788
+ return undefined;
1789
+ }
1790
+ let issued = false, reportAttempted = false;
1791
+ const execution = Object.freeze({
1792
+ assertCurrent,
1793
+ reportCompleted() {
1794
+ if (!issued || reportAttempted)
1795
+ return false;
1796
+ // Lost/refused reports never authorize another completion attempt or producer execution.
1797
+ reportAttempted = true;
1798
+ assertCurrent();
1799
+ return reportCompleted();
1800
+ },
1801
+ reply(text) {
1802
+ assertRecipientCurrent();
1803
+ if (typeof text !== "string")
1804
+ throw new Error("Selected extension reply requires text.");
1805
+ scheduleTelegramCommandEffect(ctx, "extension", "selected-reply", captured, () => captured.sendTextReply(message, text, {
1806
+ assertAuthority: assertRecipientCurrent,
1807
+ }), assertRecipientCurrent);
1808
+ },
1809
+ });
1810
+ return async () => {
1811
+ if (issued)
1812
+ return;
1813
+ issued = true;
1814
+ assertCurrent();
1815
+ await execute(execution);
1816
+ // Return, rejection and reply delivery are deliberately not semantic completion reports.
1817
+ };
1818
+ },
1819
+ /** Selected help/start require the existing authenticated owner; cold pairing remains ordinary first contact. */
1820
+ prepareSelectedStartCommand: prepareSelectedHelp("start"),
1821
+ prepareSelectedHelpCommand: prepareSelectedHelp("help"),
1822
+ /** Selected new handles only confirmation; the later authenticated callback owns replacement admission. */
1823
+ prepareSelectedNewCommand: prepareSelectedConfirmation("new"),
1824
+ /** Selected compaction handles only confirmation; the later authenticated callback owns actual compaction. */
1825
+ prepareSelectedCompactCommand: prepareSelectedConfirmation("compact"),
1826
+ /** Bare naming completes only after the exact dialog owner confirms current publication, not delivery alone. */
1827
+ prepareSelectedNameDialogCommand(command, messages, ctx, admission) {
1828
+ const original = messages[0], captured = { ...deps };
1829
+ if (command.name !== "name" ||
1830
+ command.args.trim() ||
1831
+ messages.length !== 1 ||
1832
+ !original ||
1833
+ !captured.openThreadNameDialog)
1834
+ return undefined;
1835
+ const message = copySelectedReplyAnchor(original);
1836
+ const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
1837
+ const reportCompleted = admission.reportCompleted;
1838
+ const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
1839
+ return issueSelectedOnce(assertSemanticCurrent, async () => {
1840
+ const publication = await captured.openThreadNameDialog(message, ctx, {
1841
+ assertSemanticCurrent,
1842
+ assertRecipientCurrent,
1843
+ });
1844
+ assertSemanticCurrent();
1845
+ const assertPublished = publication?.assertPublished;
1846
+ if (typeof assertPublished !== "function")
1847
+ throw new Error("Selected name dialog publication is unconfirmed.");
1848
+ assertPublished();
1849
+ assertSemanticCurrent();
1850
+ assertPublished();
1851
+ assertSourceCurrent();
1852
+ captured.assertExecutionCurrent?.(original);
1853
+ if (!reportCompleted())
1854
+ throw new Error("Selected name source completion refused.");
1855
+ return true;
1856
+ });
1857
+ },
1858
+ /** Explicit naming retains source authority until its owner result; bare dialogs use their own publication leaf. */
1859
+ prepareSelectedNameCommand(command, messages, ctx, admission) {
1860
+ const original = messages[0], threadName = command.args.trim(), captured = { ...deps };
1861
+ if (command.name !== "name" ||
1862
+ messages.length !== 1 ||
1863
+ !original ||
1864
+ !threadName ||
1865
+ (/^[A-Z]$/.test(threadName)
1866
+ ? !captured.resetCurrentThreadName
1867
+ : !captured.renameCurrentThread))
1868
+ return undefined;
1869
+ const message = copySelectedReplyAnchor(original);
1870
+ const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
1871
+ const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
1872
+ return issueSelectedOnce(assertSemanticCurrent, async () => {
1873
+ await handleTelegramNameCommand(message, ctx, threadName, captured, {
1874
+ assertSemanticCurrent,
1875
+ assertRecipientCurrent,
1876
+ reportCompleted,
1877
+ });
1878
+ return true;
1879
+ });
1065
1880
  },
1881
+ /** Genuine leader originals capture their warm queue owner before release; no hold copy or semantic-completion report. */
1882
+ prepareSelectedQueueCommand(command, messages, ctx, admission) {
1883
+ const original = messages[0], parsed = { ...command };
1884
+ if (messages.length !== 1 ||
1885
+ !original ||
1886
+ typeof admission.assertSourceCurrent !== "function" ||
1887
+ typeof admission.assertRecipientCurrent !== "function")
1888
+ return undefined;
1889
+ const raw = Reflect.get(original, "text") ?? Reflect.get(original, "caption"), operator = deps.getAllowedUserId();
1890
+ const actual = typeof raw === "string" ? parseTelegramCommand(raw) : undefined;
1891
+ if (!isDeepStrictEqual(actual, parsed) ||
1892
+ typeof operator !== "number" ||
1893
+ original.chat.type !== "private" ||
1894
+ original.chat.id !== operator ||
1895
+ original.from?.id !== operator ||
1896
+ original.from.is_bot ||
1897
+ admission.target.chatId !== operator ||
1898
+ !Number.isSafeInteger(admission.target.threadId) ||
1899
+ admission.target.threadId < 1)
1900
+ return undefined;
1901
+ const plan = getHeldPlan(parsed.name);
1902
+ if (!plan || plan.completion !== "queue-receipt")
1903
+ return undefined;
1904
+ const source = inspectTelegramDeferredSource(original), queue = prepareTelegramDeferredQueueAdmission(original);
1905
+ if (!source || source.completionSha256 || !queue?.isCurrent())
1906
+ return undefined;
1907
+ const prepared = plan.prepare(ctx, undefined, parsed);
1908
+ if (!prepared)
1909
+ return undefined;
1910
+ const target = { ...admission.target }, originalProjection = structuredClone(copySelectedMenuCarrier(original));
1911
+ const message = structuredClone(originalProjection);
1912
+ message.message_thread_id = target.threadId;
1913
+ const assertSource = admission.assertSourceCurrent, assertRecipient = admission.assertRecipientCurrent;
1914
+ const assertCurrent = () => {
1915
+ assertSource.call(admission);
1916
+ prepared.captured.assertExecutionCurrent?.(original);
1917
+ assertRecipient.call(admission);
1918
+ if (deps.getAllowedUserId() !== operator ||
1919
+ !queue.isCurrent() ||
1920
+ !prepared.portsCurrent() ||
1921
+ !isDeepStrictEqual(copySelectedMenuCarrier(original), originalProjection) ||
1922
+ !isDeepStrictEqual(inspectTelegramDeferredSource(original), source))
1923
+ throw new Error("Selected queue command source/owner changed.");
1924
+ assertSource.call(admission);
1925
+ prepared.captured.assertExecutionCurrent?.(original);
1926
+ };
1927
+ const execute = issueSelectedOnce(assertCurrent, async () => {
1928
+ const release = acquireTelegramUpdateRouting(original);
1929
+ try {
1930
+ assertCurrent();
1931
+ return await prepared.run({
1932
+ message,
1933
+ carrier: original,
1934
+ source: { ...source },
1935
+ queue,
1936
+ command: { ...parsed },
1937
+ assertSemantic: assertCurrent,
1938
+ assertRecipient,
1939
+ reportCompleted() {
1940
+ throw new Error("Queue admission cannot report semantic source removal.");
1941
+ },
1942
+ });
1943
+ }
1944
+ finally {
1945
+ release();
1946
+ }
1947
+ });
1948
+ return {
1949
+ get source() {
1950
+ return { ...source };
1951
+ },
1952
+ get command() {
1953
+ return { ...parsed };
1954
+ },
1955
+ execute,
1956
+ inspectCompletion() {
1957
+ try {
1958
+ assertRecipient.call(admission);
1959
+ if (deps.getAllowedUserId() !== operator ||
1960
+ !queue.isCurrent() ||
1961
+ !prepared.portsCurrent())
1962
+ return undefined;
1963
+ const proof = prepared.inspectQueuedCompletion?.();
1964
+ assertRecipient.call(admission);
1965
+ return deps.getAllowedUserId() === operator &&
1966
+ queue.isCurrent() &&
1967
+ prepared.portsCurrent()
1968
+ ? proof
1969
+ : undefined;
1970
+ }
1971
+ catch {
1972
+ return undefined;
1973
+ }
1974
+ },
1975
+ };
1976
+ },
1977
+ /** Selected leaders reuse registry semantics; native source/recipient admission and inline transport remain role-owned. */
1978
+ prepareSelectedCommand: prepareSelectedPlan,
1979
+ /** Existing native publication boundary; unrelated/legacy receipts keep their ordinary owner. */
1980
+ prepareHeldQueueReceipt: ((receipt, queueOwner, context, isWorkerCurrent) => {
1981
+ const record = heldQueueReceipts.get(receipt.receiptId);
1982
+ if (record === null)
1983
+ throw new Error("Held queue receipt publication authority changed.");
1984
+ if (!record)
1985
+ return undefined;
1986
+ if (context !== record.ctx ||
1987
+ !isWorkerCurrent() ||
1988
+ !record.current() ||
1989
+ !isDeepStrictEqual(record.receipt, receipt))
1990
+ throw new Error("Held queue receipt publication authority changed.");
1991
+ const scope = record.queue.prepareCompletionScope(record.receipt, queueOwner, record.completionSha256);
1992
+ if (!scope || !isWorkerCurrent() || !record.current())
1993
+ throw new Error("Held queue receipt original scope is unavailable.");
1994
+ return [scope];
1995
+ }),
1996
+ /** Pure registry/effect availability; never reads a source or activates future recipient authority. */
1997
+ canPrepareHeldCommand(name, effects) {
1998
+ return getHeldPlan(name, effects) !== undefined;
1999
+ },
2000
+ /** Compile one registry plan from a saved original; no admission, recipient activation or update-handler replay. */
2001
+ prepareHeldCommand(name, readiness, ctx, admission, effects) {
2002
+ const plan = getHeldPlan(name, effects);
2003
+ if (!plan)
2004
+ return undefined;
2005
+ const value = plan.template
2006
+ ? readiness.snapshot.update.message
2007
+ : undefined;
2008
+ const command = value &&
2009
+ typeof value === "object" &&
2010
+ typeof Reflect.get(value, "text") === "string"
2011
+ ? parseTelegramCommand(Reflect.get(value, "text"))
2012
+ : undefined;
2013
+ const prepared = plan.prepare(ctx, effects, command);
2014
+ if (!prepared)
2015
+ return undefined;
2016
+ return prepareHeldSelectedCommand(name, readiness, admission, prepared.captured, prepared.portsCurrent, plan.completion, prepared.run, prepared.inspectQueuedCompletion);
2017
+ },
2018
+ /** Warm menu-only issuance; completion reporting is not a receipt or a durable removal ACK. */
2019
+ prepareSelectedMenuCommand(command, messages, ctx, admission) {
2020
+ const action = buildTelegramCommandAction(command.name);
2021
+ if (messages.length !== 1 ||
2022
+ !messages[0] ||
2023
+ !["status", "model", "thinking", "queue", "settings"].includes(action.kind) ||
2024
+ (action.kind === "settings" && !deps.openSettingsMenu))
2025
+ return undefined;
2026
+ if (action.kind === "status")
2027
+ return prepareSelectedPlan(command, messages, ctx, admission);
2028
+ const original = messages[0], name = command.name, args = command.args;
2029
+ if (typeof admission.assertSourceCurrent !== "function" ||
2030
+ typeof admission.assertRecipientCurrent !== "function" ||
2031
+ typeof admission.reportCompleted !== "function")
2032
+ return undefined;
2033
+ const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
2034
+ const reportCompleted = admission.reportCompleted, assertExecutionCurrent = deps.assertExecutionCurrent;
2035
+ const assertIssuanceCurrent = () => {
2036
+ assertSourceCurrent.call(admission);
2037
+ assertExecutionCurrent?.call(deps, original);
2038
+ assertRecipientCurrent();
2039
+ // Recipient observations can revoke the source too; never complete from an earlier source sample.
2040
+ assertSourceCurrent.call(admission);
2041
+ assertExecutionCurrent?.call(deps, original);
2042
+ };
2043
+ // Preparation proves branch availability, not the future released recipient's active authority.
2044
+ // Detached rendering must not inherit the original admission/fence or a mutable target carrier.
2045
+ const message = copySelectedMenuCarrier(original);
2046
+ const menuDeps = { ...deps };
2047
+ const recipientOptions = { assertAuthority: assertRecipientCurrent };
2048
+ const recipientDeps = {
2049
+ ...deps,
2050
+ assertExecutionCurrent: assertRecipientCurrent,
2051
+ showStatus: (value, context) => menuDeps.showStatus(value, context, recipientOptions),
2052
+ openModelMenu: (value, context) => menuDeps.openModelMenu(value, context, recipientOptions),
2053
+ openThinkingMenu: (value, context) => menuDeps.openThinkingMenu(value, context, recipientOptions),
2054
+ openQueueMenu: (value, context) => menuDeps.openQueueMenu(value, context, recipientOptions),
2055
+ openSettingsMenu: menuDeps.openSettingsMenu &&
2056
+ ((value, context) => menuDeps.openSettingsMenu(value, context, recipientOptions)),
2057
+ };
2058
+ return issueSelectedOnce(assertIssuanceCurrent, async () => reportCompleted.call(admission) &&
2059
+ handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args));
2060
+ },
2061
+ });
2062
+ }
2063
+ export function createTelegramCommandOrPromptDispatcher(deps) {
2064
+ const dispatchPromptTemplate = async (command, messages, ctx) => {
2065
+ const first = messages[0];
2066
+ if (!first)
2067
+ return false;
2068
+ deps.assertExecutionCurrent?.(first);
2069
+ const expanded = command && deps.expandPromptTemplateCommand?.(command.name, command.args);
2070
+ if (expanded === undefined)
2071
+ return false;
2072
+ deps.assertExecutionCurrent?.(first);
2073
+ const replaced = deps.replaceMessageText(first, expanded);
2074
+ deps.assertExecutionCurrent?.(first);
2075
+ await deps.enqueueTurn([replaced, ...messages.slice(1)], ctx);
2076
+ return true;
2077
+ };
2078
+ return async (messages, ctx) => {
2079
+ const firstMessage = messages[0];
2080
+ if (!firstMessage)
2081
+ return;
2082
+ if (deps.shouldIgnoreMessages?.(messages))
2083
+ return;
2084
+ deps.assertExecutionCurrent?.(firstMessage);
2085
+ if (await deps.consumeThreadNameInput?.(messages, ctx)) {
2086
+ deps.assertExecutionCurrent?.(firstMessage);
2087
+ return;
2088
+ }
2089
+ const command = parseTelegramCommand(deps.extractRawText(messages));
2090
+ const handled = await deps.handleCommand(command?.name, firstMessage, ctx, command?.args);
2091
+ deps.assertExecutionCurrent?.(firstMessage);
2092
+ if (handled)
2093
+ return;
2094
+ if (command && deps.executeExtensionCommand) {
2095
+ const handledByExtension = await deps.executeExtensionCommand(command, messages[0], ctx);
2096
+ deps.assertExecutionCurrent?.(firstMessage);
2097
+ if (handledByExtension)
2098
+ return;
2099
+ }
2100
+ if (await dispatchPromptTemplate(command, messages, ctx))
2101
+ return;
2102
+ deps.assertExecutionCurrent?.(firstMessage);
2103
+ await deps.enqueueTurn(messages, ctx);
1066
2104
  };
1067
2105
  }
1068
2106
  function scheduleTelegramCommandEffect(ctx, command, phase, deps, effect, assertExecutionCurrent) {
2107
+ const work = deps.beginCommandEffectWork?.();
2108
+ let started = false, outcome = "settled";
1069
2109
  void Promise.resolve()
1070
2110
  .then(async () => {
1071
2111
  if (deps.isContextActive?.(ctx) === false)
1072
2112
  return;
1073
2113
  assertExecutionCurrent?.();
2114
+ started = true;
1074
2115
  await effect();
1075
2116
  assertExecutionCurrent?.();
1076
2117
  })
1077
2118
  .catch((error) => {
2119
+ // A failed started effect cannot prove non-issuance; never turn its uncertainty into known idle.
2120
+ if (started &&
2121
+ !(error instanceof TelegramApiAuthorityError && !error.requestIssued))
2122
+ outcome = "unconfirmed";
1078
2123
  try {
1079
2124
  deps.recordRuntimeEvent?.("telegram-command", error, {
1080
2125
  command,
@@ -1084,9 +2129,129 @@ function scheduleTelegramCommandEffect(ctx, command, phase, deps, effect, assert
1084
2129
  catch {
1085
2130
  // Effect diagnostics cannot create an unhandled detached Promise.
1086
2131
  }
1087
- });
2132
+ })
2133
+ .finally(() => work?.settle(outcome));
2134
+ }
2135
+ async function handleTelegramHelpCommand(commandName, message, ctx, deps, admission) {
2136
+ const assertSemanticCurrent = admission?.assertSemanticCurrent ??
2137
+ (() => deps.assertExecutionCurrent?.(message));
2138
+ admission?.assertSemanticCurrent();
2139
+ if (message.from?.id !== undefined &&
2140
+ canPairTelegramUserFromCommandMessage(message)) {
2141
+ const allowed = await pairTelegramUserIfNeeded(message.from.id, {
2142
+ allowedUserId: deps.getAllowedUserId(),
2143
+ ctx: undefined,
2144
+ persistAllowedUserId: deps.persistAllowedUserId,
2145
+ updateStatus: () => deps.updateStatus(ctx),
2146
+ assertExecutionCurrent: assertSemanticCurrent,
2147
+ });
2148
+ if (!allowed)
2149
+ return false;
2150
+ }
2151
+ if (admission) {
2152
+ assertSemanticCurrent();
2153
+ if (!admission.reportCompleted())
2154
+ return false;
2155
+ admission.assertRecipientCurrent();
2156
+ }
2157
+ const assertEffectCurrent = admission?.assertRecipientCurrent ??
2158
+ (() => deps.assertExecutionCurrent?.(message));
2159
+ const options = admission && {
2160
+ assertAuthority: admission.assertRecipientCurrent,
2161
+ };
2162
+ const isContextActive = () => deps.isContextActive?.(ctx) !== false;
2163
+ scheduleTelegramCommandEffect(ctx, commandName, "menu-render", deps, async () => {
2164
+ let forumBootstrapMessage;
2165
+ if (commandName === "start" && deps.handleForumBootstrap) {
2166
+ forumBootstrapMessage = await deps.handleForumBootstrap(message, ctx, options);
2167
+ }
2168
+ if (!isContextActive())
2169
+ return;
2170
+ assertEffectCurrent();
2171
+ if (forumBootstrapMessage) {
2172
+ await deps.sendTextReply(message, forumBootstrapMessage, options);
2173
+ assertEffectCurrent();
2174
+ }
2175
+ if (!isContextActive())
2176
+ return;
2177
+ assertEffectCurrent();
2178
+ await deps.showStatus(message, ctx, options);
2179
+ }, assertEffectCurrent);
2180
+ scheduleTelegramCommandEffect(ctx, commandName, "bot-command-sync", deps, () => deps.registerBotCommands(options), assertEffectCurrent);
2181
+ return true;
2182
+ }
2183
+ async function handleTelegramNameCommand(message, ctx, requestedName, deps, admission) {
2184
+ const threadName = requestedName.trim();
2185
+ const reply = async (text) => {
2186
+ if (admission) {
2187
+ admission.assertSemanticCurrent();
2188
+ if (!admission.reportCompleted())
2189
+ throw new Error("Selected name source completion refused.");
2190
+ admission.assertRecipientCurrent();
2191
+ await deps.sendTextReply(message, text, {
2192
+ parseMode: "HTML",
2193
+ assertAuthority: admission.assertRecipientCurrent,
2194
+ });
2195
+ admission.assertRecipientCurrent();
2196
+ }
2197
+ else {
2198
+ deps.assertExecutionCurrent?.(message);
2199
+ await deps.sendTextReply(message, text, { parseMode: "HTML" });
2200
+ deps.assertExecutionCurrent?.(message);
2201
+ }
2202
+ };
2203
+ if (!threadName) {
2204
+ if (deps.openThreadNameDialog)
2205
+ await deps.openThreadNameDialog(message, ctx);
2206
+ else
2207
+ await reply(formatTelegramInformationHeading("🏷️", "Usage: /name Navigator"));
2208
+ return;
2209
+ }
2210
+ if (/^[A-Z]$/.test(threadName) && deps.resetCurrentThreadName) {
2211
+ admission?.assertSemanticCurrent();
2212
+ const target = getTelegramCommandMessageTarget(message);
2213
+ const result = admission
2214
+ ? await deps.resetCurrentThreadName(target, {
2215
+ assertAuthority: admission.assertSemanticCurrent,
2216
+ })
2217
+ : await deps.resetCurrentThreadName(target);
2218
+ admission?.assertSemanticCurrent();
2219
+ await reply(result.ok && !result.message
2220
+ ? formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? threadName)
2221
+ : formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."));
2222
+ return;
2223
+ }
2224
+ admission?.assertSemanticCurrent();
2225
+ const validationError = deps.validateThreadName?.(threadName);
2226
+ admission?.assertSemanticCurrent();
2227
+ if (validationError) {
2228
+ await reply(formatTelegramInvalidInstanceName(validationError));
2229
+ return;
2230
+ }
2231
+ if (!deps.renameCurrentThread) {
2232
+ await reply(formatTelegramInformationHeading("🚫", "Thread display naming is unavailable."));
2233
+ return;
2234
+ }
2235
+ if (admission)
2236
+ admission.assertSemanticCurrent();
2237
+ else
2238
+ deps.assertExecutionCurrent?.(message);
2239
+ const target = getTelegramCommandMessageTarget(message);
2240
+ const result = admission
2241
+ ? await deps.renameCurrentThread(target, threadName, {
2242
+ assertAuthority: admission.assertSemanticCurrent,
2243
+ })
2244
+ : await deps.renameCurrentThread(target, threadName);
2245
+ if (admission)
2246
+ admission.assertSemanticCurrent();
2247
+ else
2248
+ deps.assertExecutionCurrent?.(message);
2249
+ await reply(result.ok && !result.message
2250
+ ? formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? threadName)
2251
+ : formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name update failed."));
1088
2252
  }
1089
2253
  async function handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs = "") {
2254
+ deps.assertExecutionCurrent?.(message);
1090
2255
  const assertExecutionCurrentFor = (nextMessage) => () => deps.assertExecutionCurrent?.(nextMessage);
1091
2256
  const sendReplyFor = (nextMessage) => async (text, options) => {
1092
2257
  deps.assertExecutionCurrent?.(nextMessage);
@@ -1107,40 +2272,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1107
2272
  sendTextReply: sendReplyFor(nextMessage),
1108
2273
  });
1109
2274
  },
1110
- handleName: async (nextMessage, _commandCtx, requestedName) => {
1111
- const threadName = requestedName.trim();
1112
- if (!threadName) {
1113
- if (deps.openThreadNameDialog) {
1114
- await deps.openThreadNameDialog(nextMessage, _commandCtx);
1115
- }
1116
- else {
1117
- await sendReplyFor(nextMessage)(formatTelegramInformationHeading("🏷️", "Usage: /name Navigator"), { parseMode: "HTML" });
1118
- }
1119
- return;
1120
- }
1121
- if (/^[A-Z]$/.test(threadName) && deps.resetCurrentThreadName) {
1122
- const result = await deps.resetCurrentThreadName(getTelegramCommandMessageTarget(nextMessage));
1123
- await sendReplyFor(nextMessage)(result.ok && !result.message
1124
- ? formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? threadName)
1125
- : formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."), { parseMode: "HTML" });
1126
- return;
1127
- }
1128
- const validationError = deps.validateThreadName?.(threadName);
1129
- if (validationError) {
1130
- await sendReplyFor(nextMessage)(formatTelegramInvalidInstanceName(validationError), { parseMode: "HTML" });
1131
- return;
1132
- }
1133
- if (!deps.renameCurrentThread) {
1134
- await sendReplyFor(nextMessage)(formatTelegramInformationHeading("🚫", "Thread display naming is unavailable."), { parseMode: "HTML" });
1135
- return;
1136
- }
1137
- deps.assertExecutionCurrent?.(nextMessage);
1138
- const result = await deps.renameCurrentThread(getTelegramCommandMessageTarget(nextMessage), threadName);
1139
- deps.assertExecutionCurrent?.(nextMessage);
1140
- await sendReplyFor(nextMessage)(result.ok && !result.message
1141
- ? formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? threadName)
1142
- : formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name update failed."), { parseMode: "HTML" });
1143
- },
2275
+ handleName: (nextMessage, commandCtx, requestedName) => handleTelegramNameCommand(nextMessage, commandCtx, requestedName, deps),
1144
2276
  handleAbort: async (nextMessage, commandCtx) => {
1145
2277
  await handleTelegramAbortCommand({
1146
2278
  hasAbortHandler: deps.hasAbortHandler,
@@ -1243,34 +2375,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1243
2375
  }
1244
2376
  : undefined,
1245
2377
  handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
1246
- if (nextMessage.from?.id !== undefined &&
1247
- canPairTelegramUserFromCommandMessage(nextMessage)) {
1248
- const allowed = await pairTelegramUserIfNeeded(nextMessage.from.id, {
1249
- allowedUserId: deps.getAllowedUserId(),
1250
- ctx: undefined,
1251
- persistAllowedUserId: deps.persistAllowedUserId,
1252
- updateStatus: updateStatusFor(commandCtx),
1253
- assertExecutionCurrent: assertExecutionCurrentFor(nextMessage),
1254
- });
1255
- if (!allowed)
1256
- return;
1257
- }
1258
- const isContextActive = () => deps.isContextActive?.(commandCtx) !== false;
1259
- scheduleTelegramCommandEffect(commandCtx, nextCommandName, "menu-render", deps, async () => {
1260
- let forumBootstrapMessage;
1261
- if (nextCommandName === "start" && deps.handleForumBootstrap) {
1262
- forumBootstrapMessage = await deps.handleForumBootstrap(nextMessage, commandCtx);
1263
- }
1264
- if (!isContextActive())
1265
- return;
1266
- if (forumBootstrapMessage) {
1267
- await deps.sendTextReply(nextMessage, forumBootstrapMessage);
1268
- }
1269
- if (!isContextActive())
1270
- return;
1271
- await deps.showStatus(nextMessage, commandCtx);
1272
- }, assertExecutionCurrentFor(nextMessage));
1273
- scheduleTelegramCommandEffect(commandCtx, nextCommandName, "bot-command-sync", deps, deps.registerBotCommands, assertExecutionCurrentFor(nextMessage));
2378
+ await handleTelegramHelpCommand(nextCommandName, nextMessage, commandCtx, deps);
1274
2379
  },
1275
2380
  }, commandArgs);
1276
2381
  }
@@ -1282,7 +2387,8 @@ function delayTelegramSessionAction(delayMs) {
1282
2387
  }
1283
2388
  export async function settleTelegramSessionReplacement(deps) {
1284
2389
  const now = deps.now ?? Date.now;
1285
- const sleep = deps.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
2390
+ const sleep = deps.sleep ??
2391
+ ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
1286
2392
  while (deps.isCurrent?.() !== false) {
1287
2393
  const intent = await deps.getIntent();
1288
2394
  if (!intent || intent.sourceSessionId === deps.sessionId)
@@ -1295,7 +2401,7 @@ export async function settleTelegramSessionReplacement(deps) {
1295
2401
  await sleep(100);
1296
2402
  continue;
1297
2403
  }
1298
- if (!await deps.clearIntent(intent))
2404
+ if (!(await deps.clearIntent(intent)))
1299
2405
  return "failed";
1300
2406
  do {
1301
2407
  const delivered = await deps.editSuccess(intent);
@@ -1319,8 +2425,7 @@ function createTelegramSessionReplacementSettlementRuntime(deps) {
1319
2425
  return;
1320
2426
  void settleTelegramSessionReplacement({
1321
2427
  ...resolved,
1322
- isCurrent: () => currentGeneration === generation &&
1323
- resolved.isCurrent?.() !== false,
2428
+ isCurrent: () => currentGeneration === generation && resolved.isCurrent?.() !== false,
1324
2429
  }).then(deps.onResult, deps.onError);
1325
2430
  },
1326
2431
  };
@@ -1329,8 +2434,10 @@ export function createTelegramSessionActionAssembly(deps) {
1329
2434
  const now = deps.now ?? Date.now;
1330
2435
  const report = (error) => deps.recordRuntimeEvent?.("new-session", error);
1331
2436
  const sendTerminalResult = async (target, result) => {
1332
- const text = result === "success" ? "<b>🆕 New session started.</b>"
1333
- : result === "cancelled" ? "<b>🚫 New session cancelled.</b>"
2437
+ const text = result === "success"
2438
+ ? "<b>🆕 New session started.</b>"
2439
+ : result === "cancelled"
2440
+ ? "<b>🚫 New session cancelled.</b>"
1334
2441
  : "<b>⚠️ New session failed.</b>";
1335
2442
  const deadline = now() + 10_000;
1336
2443
  do {
@@ -1346,9 +2453,12 @@ export function createTelegramSessionActionAssembly(deps) {
1346
2453
  const action = createTelegramSessionActionRuntime({
1347
2454
  registerCommand: deps.registerCommand,
1348
2455
  sendUserMessage: deps.sendUserMessage,
1349
- notifyResult(target, result) { return sendTerminalResult(target, result); },
2456
+ notifyResult(target, result) {
2457
+ return sendTerminalResult(target, result);
2458
+ },
1350
2459
  async prepareReplacement(ctx, updateId, target) {
1351
- const follower = !deps.ownsPersistence() && typeof target.threadId === "number" &&
2460
+ const follower = !deps.ownsPersistence() &&
2461
+ typeof target.threadId === "number" &&
1352
2462
  deps.follower?.isRegisteredFor(target)
1353
2463
  ? deps.follower
1354
2464
  : undefined;
@@ -1375,15 +2485,16 @@ export function createTelegramSessionActionAssembly(deps) {
1375
2485
  target: binding ? { ...binding.target } : { chatId: target.chatId },
1376
2486
  messageId: target.messageId,
1377
2487
  ...(binding?.slot ? { slot: binding.slot } : {}),
1378
- ...(binding?.manualThreadName ?? binding?.threadName
1379
- ? { threadName: binding.manualThreadName ?? binding.threadName } : {}),
2488
+ ...((binding?.manualThreadName ?? binding?.threadName)
2489
+ ? { threadName: binding.manualThreadName ?? binding.threadName }
2490
+ : {}),
1380
2491
  createdAtMs,
1381
2492
  expiresAtMs: createdAtMs + deps.handoffTtlMs,
1382
2493
  ...(follower ? { sourceInstanceId: follower.instanceId } : {}),
1383
2494
  };
1384
- if (!await (follower
2495
+ if (!(await (follower
1385
2496
  ? follower.requestSessionReplacement("publish", intent)
1386
- : deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence))) {
2497
+ : deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence)))) {
1387
2498
  throw new Error("Telegram session replacement intent was not persisted.");
1388
2499
  }
1389
2500
  },
@@ -1395,23 +2506,30 @@ export function createTelegramSessionActionAssembly(deps) {
1395
2506
  if (!sessionId)
1396
2507
  return undefined;
1397
2508
  return {
1398
- async getIntent() { await deps.store.refresh?.(); return deps.store.getSessionReplacementIntent(); },
2509
+ async getIntent() {
2510
+ await deps.store.refresh?.();
2511
+ return deps.store.getSessionReplacementIntent();
2512
+ },
1399
2513
  hasSuccessorContinuity(intent) {
1400
2514
  if (intent.continuity === "classic-chat")
1401
2515
  return true;
1402
- if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
1403
- intent.cwd)
2516
+ if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
2517
+ ?.cwd !== intent.cwd)
1404
2518
  return false;
1405
2519
  // A follower successor claims only after its own re-registration is live.
1406
- return intent.sourceInstanceId === undefined || deps.ownsPersistence() ||
1407
- deps.follower?.isRegisteredFor(intent.target) === true;
2520
+ return (intent.sourceInstanceId === undefined ||
2521
+ deps.ownsPersistence() ||
2522
+ deps.follower?.isRegisteredFor(intent.target) === true);
2523
+ },
2524
+ editSuccess(intent) {
2525
+ return deps.sendResult(intent.target, "<b>🆕 New session started.</b>");
1408
2526
  },
1409
- editSuccess(intent) { return deps.sendResult(intent.target, "<b>🆕 New session started.</b>"); },
1410
2527
  async clearIntent(intent) {
1411
- if (intent.sourceInstanceId === undefined || deps.ownsPersistence()) {
2528
+ if (intent.sourceInstanceId === undefined ||
2529
+ deps.ownsPersistence()) {
1412
2530
  return deps.store.removeSessionReplacementIntent(intent, deps.ownsPersistence);
1413
2531
  }
1414
- return await deps.follower?.requestSessionReplacement("settle", intent) ?? false;
2532
+ return ((await deps.follower?.requestSessionReplacement("settle", intent)) ?? false);
1415
2533
  },
1416
2534
  profileName: deps.getProfileName() ?? "default",
1417
2535
  cwd: ctx.cwd,