@llblab/pi-telegram 0.45.11 → 0.47.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/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +6 -6
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +17 -3
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +11 -3
  183. package/lib/bus-leader.ts +57 -20
  184. package/lib/bus.ts +12 -2
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +26 -1
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/pi.ts +4 -0
  193. package/lib/sync.ts +3 -0
  194. package/lib/telegram-api.ts +12 -1
  195. package/lib/thread-cleanup-manager.ts +36 -6
  196. package/lib/thread-display.ts +96 -18
  197. package/lib/threads.ts +150 -30
  198. package/package.json +56 -13
  199. package/scripts/build-dist.mjs +44 -0
  200. package/scripts/measure-bus.mjs +8 -1
  201. package/scripts/measure-workspace.mjs +8 -1
  202. package/skills/generative-apps/SKILL.md +1 -1
  203. package/skills/show-me/SKILL.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -0,0 +1,1155 @@
1
+ /**
2
+ * Telegram command routing helpers
3
+ * Zones: telegram controls, pi agent commands, queue controls
4
+ * Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports
5
+ */
6
+ import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
7
+ import { escapeHtml } from "./rendering.js";
8
+ import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
9
+ const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__";
10
+ const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
11
+ function getOrCreateTelegramCommandRegistry() {
12
+ const existing = globalThis[TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY];
13
+ if (existing &&
14
+ typeof existing === "object" &&
15
+ existing !== null &&
16
+ "commands" in existing &&
17
+ existing.commands instanceof Map) {
18
+ return existing;
19
+ }
20
+ const registry = { commands: new Map() };
21
+ globalThis[TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY] = registry;
22
+ return registry;
23
+ }
24
+ export function normalizeTelegramExtensionCommandName(name) {
25
+ return name.trim().replace(/^\/+/, "").toLowerCase();
26
+ }
27
+ export function isTelegramExtensionCommandName(name) {
28
+ return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
29
+ }
30
+ function normalizeTelegramExtensionCommandEmoji(emoji) {
31
+ const normalized = emoji?.trim();
32
+ return normalized ? normalized : undefined;
33
+ }
34
+ export function registerTelegramCommand(registration) {
35
+ const name = normalizeTelegramExtensionCommandName(registration.name);
36
+ const showInMenu = registration.showInMenu ?? false;
37
+ const emoji = normalizeTelegramExtensionCommandEmoji(registration.emoji);
38
+ if (!isTelegramExtensionCommandName(name)) {
39
+ throw new Error(`Invalid Telegram command name: ${registration.name}`);
40
+ }
41
+ if (showInMenu && !emoji) {
42
+ throw new Error(`Visible Telegram command requires emoji: ${name}`);
43
+ }
44
+ if (emoji && emoji.length > 8) {
45
+ throw new Error(`Telegram command emoji is too long: ${name}`);
46
+ }
47
+ if (isTelegramReservedCommandName(name)) {
48
+ throw new Error(`Telegram command conflicts with built-in command: ${name}`);
49
+ }
50
+ const registry = getOrCreateTelegramCommandRegistry();
51
+ if (registry.commands.has(name)) {
52
+ throw new Error(`Telegram command is already registered: ${name}`);
53
+ }
54
+ const command = {
55
+ name,
56
+ description: registration.description,
57
+ order: registration.order ?? 0,
58
+ showInMenu,
59
+ emoji,
60
+ handler: registration.handler,
61
+ };
62
+ registry.commands.set(name, command);
63
+ return () => {
64
+ if (registry.commands.get(name) === command)
65
+ registry.commands.delete(name);
66
+ };
67
+ }
68
+ export function getTelegramExtensionCommands() {
69
+ return Array.from(getOrCreateTelegramCommandRegistry().commands.values()).sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
70
+ }
71
+ export function findTelegramExtensionCommand(name) {
72
+ if (!name)
73
+ return undefined;
74
+ return getOrCreateTelegramCommandRegistry().commands.get(normalizeTelegramExtensionCommandName(name));
75
+ }
76
+ export function clearTelegramExtensionCommands() {
77
+ getOrCreateTelegramCommandRegistry().commands.clear();
78
+ }
79
+ export const TELEGRAM_COMMAND_EMOJI = {
80
+ start: "🟢",
81
+ status: "📊",
82
+ model: "🤖",
83
+ thinking: "🧠",
84
+ compact: "🗜",
85
+ queue: "🔢",
86
+ thread: "🧵",
87
+ next: "⏩",
88
+ continue: "▶️",
89
+ abort: "⏹️",
90
+ stop: "🟥",
91
+ name: "🏷️",
92
+ };
93
+ export function getTelegramCommandEmoji(command) {
94
+ return TELEGRAM_COMMAND_EMOJI[command];
95
+ }
96
+ export function formatTelegramCommandEmojiPrefix(command) {
97
+ return `${getTelegramCommandEmoji(command)} `;
98
+ }
99
+ export function formatTelegramPiCommandHtml(command) {
100
+ return `<code>${escapeHtml(command)}</code>`;
101
+ }
102
+ export function formatTelegramInformationHeading(emoji, text) {
103
+ return `<b>${escapeHtml(emoji)} ${escapeHtml(text)}</b>`;
104
+ }
105
+ export function formatTelegramInvalidInstanceName(validationError) {
106
+ const details = validationError.replace(/^Invalid Telegram (?:instance name|Thread display name):\s*/, "");
107
+ const items = details
108
+ .split(/;\s+|(?<=\.)\s+(?=[A-Z])/)
109
+ .map((item) => item.trim())
110
+ .filter(Boolean)
111
+ .map((item) => /[.!?]$/.test(item) ? item : `${item}.`)
112
+ .map((item) => item[0].toUpperCase() + item.slice(1));
113
+ return [
114
+ "<b>⚠️ Invalid Thread Display Name:</b>\n",
115
+ ...items.map((item) => `• ${escapeHtml(item)}`),
116
+ ].join("\n");
117
+ }
118
+ export function formatTelegramThreadDisplayNameSavedHeading(name) {
119
+ return `<b>✅ Thread display name saved as <i>${escapeHtml(name)}</i>.</b>`;
120
+ }
121
+ export function formatTelegramAutomaticThreadDisplayNameRestoredHeading(name) {
122
+ return `<b>✅ Automatic Thread display name restored as <i>${escapeHtml(name)}</i>.</b>`;
123
+ }
124
+ export const TELEGRAM_COMPACTION_STARTED_TEXT = formatTelegramInformationHeading(getTelegramCommandEmoji("compact"), "Compaction started.");
125
+ export const TELEGRAM_COMPACTION_COMPLETED_TEXT = formatTelegramInformationHeading("✅", "Compaction completed.");
126
+ export const TELEGRAM_COMPACTION_STARTED_MARKDOWN = `**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`;
127
+ export const TELEGRAM_COMPACTION_COMPLETED_MARKDOWN = "**✅ Compaction completed.**";
128
+ function formatTelegramBotCommandDescription(command, description) {
129
+ return `${formatTelegramCommandEmojiPrefix(command)}${description}`;
130
+ }
131
+ export const TELEGRAM_BUILTIN_BOT_COMMANDS = [
132
+ {
133
+ command: "start",
134
+ description: formatTelegramBotCommandDescription("start", "Open menu / Pair bridge"),
135
+ },
136
+ {
137
+ command: "name",
138
+ description: formatTelegramBotCommandDescription("name", "Rename this thread"),
139
+ },
140
+ {
141
+ command: "compact",
142
+ description: formatTelegramBotCommandDescription("compact", "Compact current session"),
143
+ },
144
+ {
145
+ command: "next",
146
+ description: formatTelegramBotCommandDescription("next", "Force next turn"),
147
+ },
148
+ {
149
+ command: "continue",
150
+ description: formatTelegramBotCommandDescription("continue", "Queue continue prompt"),
151
+ },
152
+ {
153
+ command: "abort",
154
+ description: formatTelegramBotCommandDescription("abort", "Abort Pi"),
155
+ },
156
+ {
157
+ command: "stop",
158
+ description: formatTelegramBotCommandDescription("stop", "Abort Pi & Clear queue"),
159
+ },
160
+ ];
161
+ export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;
162
+ function getVisibleTelegramExtensionBotCommands() {
163
+ return getTelegramExtensionCommands()
164
+ .filter((command) => command.showInMenu && command.description)
165
+ .map((command) => ({
166
+ command: command.name,
167
+ description: `${command.emoji} ${command.description ?? command.name}`,
168
+ }));
169
+ }
170
+ export function getTelegramReservedCommandNames() {
171
+ return [
172
+ ...TELEGRAM_RESERVED_COMMAND_NAMES,
173
+ ...getTelegramExtensionCommands().map((command) => command.name),
174
+ ];
175
+ }
176
+ export async function registerTelegramBotCommands(deps) {
177
+ const extensionCommands = getVisibleTelegramExtensionBotCommands();
178
+ if (extensionCommands.length === 0) {
179
+ await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
180
+ return;
181
+ }
182
+ const compactCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex((command) => command.command === "compact");
183
+ if (compactCommandIndex === -1) {
184
+ await deps.setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
185
+ return;
186
+ }
187
+ await deps.setMyCommands([
188
+ ...TELEGRAM_BOT_COMMANDS.slice(0, compactCommandIndex + 1),
189
+ ...extensionCommands,
190
+ ...TELEGRAM_BOT_COMMANDS.slice(compactCommandIndex + 1),
191
+ ]);
192
+ }
193
+ export function createTelegramBotCommandRegistrar(deps) {
194
+ let pending;
195
+ return () => {
196
+ if (pending)
197
+ return pending;
198
+ let request;
199
+ request = registerTelegramBotCommands(deps).finally(() => {
200
+ if (pending === request)
201
+ pending = undefined;
202
+ });
203
+ pending = request;
204
+ return request;
205
+ };
206
+ }
207
+ export function createTelegramThreadDisplayNameResetBinding() {
208
+ let current;
209
+ return {
210
+ bind(reset) { current = reset; },
211
+ async reset(target) {
212
+ return current
213
+ ? current(target)
214
+ : { ok: false, message: "Thread display name reset is unavailable." };
215
+ },
216
+ };
217
+ }
218
+ export function createTelegramThreadDisplayNameRenameBinding() {
219
+ let current;
220
+ return {
221
+ bind(rename) {
222
+ current = rename;
223
+ },
224
+ async rename(target, threadName) {
225
+ if (!current) {
226
+ return {
227
+ ok: false,
228
+ message: "Thread display naming is unavailable.",
229
+ };
230
+ }
231
+ return current(target, threadName);
232
+ },
233
+ };
234
+ }
235
+ function parseTelegramProfileArg(args) {
236
+ const word = args.trim().split(/\s+/)[0];
237
+ if (!word || word.length === 0)
238
+ return undefined;
239
+ if (word.startsWith("-") || /^as=/i.test(word))
240
+ return undefined;
241
+ return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
242
+ }
243
+ export function parseTelegramRequestedThreadName(args) {
244
+ const token = args
245
+ .trim()
246
+ .split(/\s+/)
247
+ .find((word) => /^as=/i.test(word));
248
+ const value = token?.slice(3).trim();
249
+ return value || undefined;
250
+ }
251
+ function formatTelegramTakeoverTitle(ctx) {
252
+ return ctx.ui.theme.fg("accent", "pi-telegram");
253
+ }
254
+ function formatTelegramTakeoverPrompt(ctx, owner) {
255
+ const theme = ctx.ui.theme;
256
+ const action = theme.fg("warning", "move singleton lock here?");
257
+ const from = theme.fg("muted", "from:");
258
+ const to = theme.fg("muted", "to:");
259
+ const source = owner ?? "another Pi instance";
260
+ return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
261
+ }
262
+ export function registerTelegramBridgeCommands(pi, deps) {
263
+ pi.registerCommand("telegram-setup", {
264
+ description: "Configure Telegram bot token. Use /telegram-setup <name> for named profiles.",
265
+ handler: async (args, ctx) => {
266
+ await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
267
+ },
268
+ });
269
+ pi.registerCommand("telegram-status", {
270
+ description: "Show Telegram bridge status",
271
+ handler: async (args, ctx) => {
272
+ const verbose = /(^|\s)(--debug|debug|--verbose|verbose)(\s|$)/i.test(args);
273
+ ctx.ui.notify(deps.getStatusLines({ verbose }).join("\n"), "info");
274
+ },
275
+ });
276
+ pi.registerCommand("telegram-connect", {
277
+ description: "Start the Telegram bridge. Use /telegram-connect <profile> and optional as=Name for a fresh Workspace Thread.",
278
+ handler: async (args, ctx) => {
279
+ const profileName = parseTelegramProfileArg(args);
280
+ const requestedNameTokens = args
281
+ .trim()
282
+ .split(/\s+/)
283
+ .filter((word) => /^as=/i.test(word));
284
+ const requestedThreadName = parseTelegramRequestedThreadName(args);
285
+ const requestedNameError = requestedNameTokens.length > 1
286
+ ? "Specify at most one as=Name Workspace Thread name."
287
+ : requestedNameTokens.length === 1 && !requestedThreadName
288
+ ? "Usage: /telegram-connect [profile] as=Flightprice"
289
+ : requestedThreadName
290
+ ? deps.validateThreadName?.(requestedThreadName)
291
+ : undefined;
292
+ if (requestedNameError) {
293
+ ctx.ui.notify(requestedNameError, "warning");
294
+ deps.updateStatus(ctx);
295
+ return;
296
+ }
297
+ if (profileName && deps.activateProfileConfig) {
298
+ const ok = await deps.activateProfileConfig(ctx, profileName);
299
+ if (!ok) {
300
+ ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
301
+ deps.updateStatus(ctx);
302
+ return;
303
+ }
304
+ ctx.ui.notify(`Activated profile "${profileName}".`, "info");
305
+ }
306
+ else {
307
+ await (deps.activateDefaultProfileConfig?.(ctx) ?? deps.reloadConfig());
308
+ }
309
+ if (!deps.hasBotToken()) {
310
+ const botTokenDiagnostic = deps.getBotTokenDiagnostic?.();
311
+ if (botTokenDiagnostic)
312
+ ctx.ui.notify(botTokenDiagnostic, "error");
313
+ const profileNames = deps.getProfileNames?.() ?? [];
314
+ if (!profileName && profileNames.length > 0) {
315
+ ctx.ui.notify(`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`, "info");
316
+ deps.updateStatus(ctx);
317
+ return;
318
+ }
319
+ await deps.promptForConfig(ctx, profileName);
320
+ return;
321
+ }
322
+ let recoveryUsed = false;
323
+ const startWithRecovery = async (options) => {
324
+ try {
325
+ return await deps.startPolling(ctx, options);
326
+ }
327
+ catch (error) {
328
+ if (!deps.recoverPollingStart || recoveryUsed)
329
+ throw error;
330
+ const recovery = await deps.recoverPollingStart(error);
331
+ if (recovery.kind === "unhandled")
332
+ throw error;
333
+ if (recovery.kind === "blocked") {
334
+ return { ok: false, message: recovery.message };
335
+ }
336
+ recoveryUsed = true;
337
+ try {
338
+ const retry = await deps.startPolling(ctx, options);
339
+ if (!retry) {
340
+ return { ok: true, message: recovery.message };
341
+ }
342
+ return {
343
+ ...retry,
344
+ message: retry.ok
345
+ ? `${recovery.message} ${retry.message ?? "Telegram bridge connected."}`
346
+ : retry.message,
347
+ };
348
+ }
349
+ catch {
350
+ return {
351
+ ok: false,
352
+ message: "Telegram temporary state was recovered, but the bridge could not restart. Restart this Pi instance and run /telegram-connect again.",
353
+ };
354
+ }
355
+ }
356
+ };
357
+ let result = await startWithRecovery({
358
+ forceFreshLeaderThread: true,
359
+ ...(requestedThreadName ? { requestedThreadName } : {}),
360
+ });
361
+ if (result && !result.ok && result.canTakeover) {
362
+ const confirmed = await ctx.ui.confirm(formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner));
363
+ if (!confirmed) {
364
+ ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
365
+ deps.updateStatus(ctx);
366
+ return;
367
+ }
368
+ result = await startWithRecovery({
369
+ force: true,
370
+ forceFreshLeaderThread: true,
371
+ ...(requestedThreadName ? { requestedThreadName } : {}),
372
+ });
373
+ }
374
+ if (result?.message) {
375
+ ctx.ui.notify(result.message, result.ok ? "info" : "warning");
376
+ }
377
+ if (!result || result.ok) {
378
+ deps.queueAgentConnectionContext?.(true);
379
+ }
380
+ deps.updateStatus(ctx);
381
+ },
382
+ });
383
+ pi.registerCommand("telegram-disconnect", {
384
+ description: "Stop Telegram; in Threaded Mode, delete this instance's current thread",
385
+ handler: async (_args, ctx) => {
386
+ const threadName = deps.getDisconnectThreadName?.();
387
+ if (threadName) {
388
+ const confirmed = await ctx.ui.confirm(ctx.ui.theme.fg("accent", "pi-telegram"), `Delete Telegram thread ${ctx.ui.theme.fg("warning", threadName)} and disconnect this Pi session?`);
389
+ if (!confirmed) {
390
+ ctx.ui.notify("Telegram disconnect cancelled.", "info");
391
+ deps.updateStatus(ctx);
392
+ return;
393
+ }
394
+ }
395
+ try {
396
+ const message = await deps.stopPolling();
397
+ if (message)
398
+ ctx.ui.notify(message, "info");
399
+ deps.queueAgentConnectionContext?.(false);
400
+ }
401
+ catch (error) {
402
+ const detail = error instanceof Error ? error.message : String(error);
403
+ ctx.ui.notify(`Telegram disconnect did not complete: ${detail} Keep this Pi session open, restore leader connectivity, inspect /telegram-status --debug, and retry /telegram-disconnect.`, "warning");
404
+ throw error;
405
+ }
406
+ finally {
407
+ deps.updateStatus(ctx);
408
+ }
409
+ },
410
+ });
411
+ }
412
+ export const TELEGRAM_RESERVED_COMMAND_NAMES = [
413
+ "stop",
414
+ "name",
415
+ "abort",
416
+ "next",
417
+ "continue",
418
+ "status",
419
+ "queue",
420
+ "compact",
421
+ "model",
422
+ "thinking",
423
+ "settings",
424
+ "help",
425
+ "start",
426
+ ];
427
+ const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set(TELEGRAM_RESERVED_COMMAND_NAMES);
428
+ export function isTelegramReservedCommandName(commandName) {
429
+ return (commandName !== undefined &&
430
+ TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName));
431
+ }
432
+ function canPairTelegramUserFromCommandMessage(message) {
433
+ return message.chat.type === undefined || message.chat.type === "private";
434
+ }
435
+ export function getTelegramCommandMessageTarget(message) {
436
+ return {
437
+ chatId: message.chat.id,
438
+ threadId: typeof message.message_thread_id === "number"
439
+ ? message.message_thread_id
440
+ : undefined,
441
+ replyToMessageId: message.message_id,
442
+ };
443
+ }
444
+ export function createTelegramCommandControlQueueRuntime(deps) {
445
+ const controlQueueController = createTelegramControlQueueController({
446
+ appendControlItem: deps.appendControlItem,
447
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
448
+ });
449
+ return createTelegramCommandControlEnqueueAdapter({
450
+ createControlItem: deps.createControlItem,
451
+ enqueueControlItem: controlQueueController.enqueue,
452
+ });
453
+ }
454
+ export function createTelegramCommandControlEnqueueAdapter(deps) {
455
+ return (target, ctx, controlType, statusSummary, execute, admissionReceipts, onQueued) => {
456
+ deps.enqueueControlItem(deps.createControlItem({
457
+ ...target,
458
+ controlType,
459
+ statusSummary,
460
+ ...(admissionReceipts?.length ? { admissionReceipts } : {}),
461
+ execute,
462
+ }), ctx, onQueued);
463
+ };
464
+ }
465
+ export function createTelegramCommandTargetQueueRuntime(deps) {
466
+ return createTelegramCommandTargetRuntime({
467
+ enqueueControlItem: createTelegramCommandControlQueueRuntime({
468
+ createControlItem: deps.createControlItem,
469
+ appendControlItem: deps.appendControlItem,
470
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
471
+ }),
472
+ getAdmissionScope: deps.getAdmissionScope,
473
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
474
+ onControlQueued: deps.onControlQueued,
475
+ showStatus: deps.showStatus,
476
+ openModelMenu: deps.openModelMenu,
477
+ openSettingsMenu: deps.openSettingsMenu,
478
+ sendTextReply: deps.sendTextReply,
479
+ });
480
+ }
481
+ export function createTelegramCommandTargetRuntime(deps) {
482
+ return {
483
+ enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
484
+ const sourceUpdateId = message.pi_telegram_source_update_id;
485
+ const baseReceipt = typeof sourceUpdateId === "number"
486
+ ? createTelegramQueueAdmissionReceipt({
487
+ queueKind: "control",
488
+ scope: deps.getAdmissionScope?.() ?? "",
489
+ sourceUpdateIds: [sourceUpdateId],
490
+ })
491
+ : undefined;
492
+ const journalBindingKey = deps.getAdmissionJournalBinding?.();
493
+ const receipt = baseReceipt
494
+ ? {
495
+ ...baseReceipt,
496
+ ...(journalBindingKey ? { journalBindingKey } : {}),
497
+ }
498
+ : undefined;
499
+ deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt
500
+ ? () => deps.onControlQueued?.(message, receipt)
501
+ : undefined);
502
+ },
503
+ showStatus: (message, ctx) => {
504
+ const target = getTelegramCommandMessageTarget(message);
505
+ return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId);
506
+ },
507
+ openModelMenu: (message, ctx) => {
508
+ const target = getTelegramCommandMessageTarget(message);
509
+ return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId);
510
+ },
511
+ openSettingsMenu: async (message, ctx) => {
512
+ const target = getTelegramCommandMessageTarget(message);
513
+ if (!deps.openSettingsMenu) {
514
+ await deps.sendTextReply(target.chatId, target.replyToMessageId, formatTelegramInformationHeading("🚫", "Settings menu is unavailable."), { target, parseMode: "HTML" });
515
+ return;
516
+ }
517
+ await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId);
518
+ },
519
+ sendTextReply: async (message, text, options) => {
520
+ const target = getTelegramCommandMessageTarget(message);
521
+ await deps.sendTextReply(target.chatId, target.replyToMessageId, text, {
522
+ ...options,
523
+ target,
524
+ });
525
+ },
526
+ };
527
+ }
528
+ export const TELEGRAM_APP_MENU_INTRO_HTML = [
529
+ "<b>Pi Telegram</b>",
530
+ "",
531
+ `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
532
+ `${formatTelegramCommandEmojiPrefix("name")}/name — Rename this thread`,
533
+ `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
534
+ `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
535
+ `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
536
+ `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
537
+ `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
538
+ ].join("\n");
539
+ function escapeTelegramCommandMenuHtml(text) {
540
+ return text
541
+ .replace(/&/g, "&amp;")
542
+ .replace(/</g, "&lt;")
543
+ .replace(/>/g, "&gt;");
544
+ }
545
+ function buildTelegramPromptTemplateMenuHtml(promptTemplates = []) {
546
+ if (promptTemplates.length === 0)
547
+ return "";
548
+ return promptTemplates
549
+ .map((template) => `🧩 /${escapeTelegramCommandMenuHtml(template.command)}`)
550
+ .join("\n");
551
+ }
552
+ function buildTelegramExtensionCommandMenuLines() {
553
+ return getTelegramExtensionCommands()
554
+ .filter((command) => command.showInMenu)
555
+ .map((command) => {
556
+ const prefix = `${escapeTelegramCommandMenuHtml(command.emoji ?? "")} /${escapeTelegramCommandMenuHtml(command.name)}`;
557
+ if (!command.description)
558
+ return prefix;
559
+ return `${prefix} — ${escapeTelegramCommandMenuHtml(command.description)}`;
560
+ });
561
+ }
562
+ function buildTelegramAppMenuIntroHtml() {
563
+ const extensionLines = buildTelegramExtensionCommandMenuLines();
564
+ if (extensionLines.length === 0)
565
+ return TELEGRAM_APP_MENU_INTRO_HTML;
566
+ return [
567
+ "<b>Pi Telegram</b>",
568
+ "",
569
+ `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
570
+ `${formatTelegramCommandEmojiPrefix("name")}/name — Rename this thread`,
571
+ `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
572
+ ...extensionLines,
573
+ `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
574
+ `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
575
+ `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
576
+ `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
577
+ ].join("\n");
578
+ }
579
+ export function buildTelegramAppMenuHtml(statusHtml, promptTemplates = []) {
580
+ const introHtml = buildTelegramAppMenuIntroHtml();
581
+ const promptTemplateHtml = buildTelegramPromptTemplateMenuHtml(promptTemplates);
582
+ if (!promptTemplateHtml)
583
+ return `${introHtml}\n\n${statusHtml}`;
584
+ return `${introHtml}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
585
+ }
586
+ export function createTelegramAppMenuHtmlBuilder(deps) {
587
+ return (ctx) => {
588
+ return buildTelegramAppMenuHtml(deps.buildStatusHtml(ctx), deps.getPromptTemplateCommands?.());
589
+ };
590
+ }
591
+ function getTelegramCommandErrorMessage(error) {
592
+ return error instanceof Error ? error.message : String(error);
593
+ }
594
+ function formatTelegramCompactionFailure(error) {
595
+ let message = getTelegramCommandErrorMessage(error).trim();
596
+ const redundantPrefixes = [
597
+ "Compaction failed: ",
598
+ "Turn prefix summarization failed: ",
599
+ ];
600
+ let stripped = true;
601
+ while (stripped) {
602
+ stripped = false;
603
+ for (const prefix of redundantPrefixes) {
604
+ if (!message.startsWith(prefix))
605
+ continue;
606
+ message = message.slice(prefix.length).trim();
607
+ stripped = true;
608
+ }
609
+ }
610
+ const sentence = /[.!?]$/u.test(message) ? message : `${message}.`;
611
+ return `Compaction failed! ${sentence}`;
612
+ }
613
+ export function parseTelegramCommand(text) {
614
+ const trimmed = text.trim();
615
+ if (!trimmed.startsWith("/"))
616
+ return undefined;
617
+ const [head, ...tail] = trimmed.split(/\s+/);
618
+ const name = head.slice(1).split("@")[0]?.toLowerCase();
619
+ if (!name)
620
+ return undefined;
621
+ return { name, args: tail.join(" ").trim() };
622
+ }
623
+ export const TELEGRAM_COMMAND_ACTIONS = {
624
+ stop: { kind: "stop", executionMode: "immediate" },
625
+ name: { kind: "name", executionMode: "immediate" },
626
+ abort: { kind: "abort", executionMode: "immediate" },
627
+ next: { kind: "next", executionMode: "immediate" },
628
+ continue: { kind: "continue", executionMode: "immediate" },
629
+ status: { kind: "status", executionMode: "immediate" },
630
+ queue: { kind: "queue", executionMode: "immediate" },
631
+ compact: { kind: "compact", executionMode: "immediate" },
632
+ model: { kind: "model", executionMode: "immediate" },
633
+ thinking: { kind: "thinking", executionMode: "immediate" },
634
+ settings: { kind: "settings", executionMode: "immediate" },
635
+ help: { kind: "help", commandName: "help", executionMode: "immediate" },
636
+ start: { kind: "help", commandName: "start", executionMode: "immediate" },
637
+ };
638
+ export function buildTelegramCommandAction(commandName) {
639
+ if (!isTelegramReservedCommandName(commandName)) {
640
+ return { kind: "ignore", executionMode: "ignored" };
641
+ }
642
+ return TELEGRAM_COMMAND_ACTIONS[commandName];
643
+ }
644
+ export function getTelegramCommandExecutionMode(action) {
645
+ return action.executionMode;
646
+ }
647
+ function formatTelegramQueuedTurnCount(count) {
648
+ return count === 1 ? "1 queued turn" : `${count} queued turns`;
649
+ }
650
+ export async function handleTelegramStopCommand(deps) {
651
+ deps.clearPendingModelSwitch();
652
+ const clearedCount = deps.clearQueuedTelegramItems();
653
+ deps.setFoldQueuedPromptsIntoHistory(false);
654
+ if (!deps.hasAbortHandler()) {
655
+ const clearedSuffix = clearedCount > 0
656
+ ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
657
+ : "";
658
+ if (clearedCount > 0)
659
+ deps.updateStatus();
660
+ await deps.sendTextReply(formatTelegramInformationHeading("💤", `No active turn.${clearedSuffix}`), { parseMode: "HTML" });
661
+ return;
662
+ }
663
+ deps.abortCurrentTurn();
664
+ deps.updateStatus();
665
+ const clearedSuffix = clearedCount > 0
666
+ ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
667
+ : "";
668
+ await deps.sendTextReply(formatTelegramInformationHeading("⏹️", `Aborted current turn.${clearedSuffix}`), { parseMode: "HTML" });
669
+ }
670
+ export async function handleTelegramAbortCommand(deps) {
671
+ deps.clearPendingModelSwitch();
672
+ if (!deps.hasAbortHandler()) {
673
+ await deps.sendTextReply(formatTelegramInformationHeading("💤", "No active turn."), { parseMode: "HTML" });
674
+ return;
675
+ }
676
+ deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn());
677
+ deps.abortCurrentTurn();
678
+ deps.updateStatus();
679
+ await deps.sendTextReply(formatTelegramInformationHeading("⏹️", "Aborted current turn."), { parseMode: "HTML" });
680
+ }
681
+ export async function handleTelegramNextCommand(deps) {
682
+ deps.clearPendingModelSwitch();
683
+ if (!deps.hasQueuedItems()) {
684
+ await deps.sendTextReply(formatTelegramInformationHeading("⌛", "Queue is empty"), { parseMode: "HTML" });
685
+ return;
686
+ }
687
+ if (!deps.isIdle() && deps.hasAbortHandler()) {
688
+ deps.clearFoldForDispatch();
689
+ deps.requestNextDispatchAnnouncement?.();
690
+ deps.abortCurrentTurn();
691
+ deps.updateStatus();
692
+ return;
693
+ }
694
+ if (!deps.isIdle()) {
695
+ await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Pi is busy. Send /abort or /stop first."), { parseMode: "HTML" });
696
+ return;
697
+ }
698
+ deps.requestNextDispatchAnnouncement?.();
699
+ deps.dispatchNextQueuedTurn();
700
+ deps.updateStatus();
701
+ }
702
+ export async function handleTelegramContinueCommand(message, ctx, deps) {
703
+ await deps.enqueueContinueTurn(message, ctx);
704
+ }
705
+ function dispatchNextQueuedTelegramTurnAfterCompact(deps) {
706
+ if (deps.requestDeferredDispatchNextQueuedTelegramTurn) {
707
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(deps.dispatchNextQueuedTelegramTurn);
708
+ return;
709
+ }
710
+ deps.dispatchNextQueuedTelegramTurn();
711
+ }
712
+ export function buildTelegramCompactConfirmationReplyMarkup() {
713
+ return {
714
+ inline_keyboard: [
715
+ [
716
+ { text: "🗜 Yes, compact", callback_data: "compact:confirm" },
717
+ { text: "❌ No", callback_data: "compact:cancel" },
718
+ ],
719
+ ],
720
+ };
721
+ }
722
+ export function getTelegramCompactConfirmationHtml() {
723
+ return "<b>Compact session?</b>";
724
+ }
725
+ export async function openTelegramCompactConfirmation(target, deps) {
726
+ await deps.sendInteractiveMessage(target.chatId, getTelegramCompactConfirmationHtml(), "html", buildTelegramCompactConfirmationReplyMarkup(), target.threadId !== undefined
727
+ ? { target: { chatId: target.chatId, threadId: target.threadId } }
728
+ : undefined);
729
+ }
730
+ export async function handleTelegramCompactConfirmationCallback(query, deps) {
731
+ if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
732
+ return false;
733
+ }
734
+ const callbackMessage = query.message;
735
+ const chatId = callbackMessage?.chat?.id;
736
+ const messageId = callbackMessage?.message_id;
737
+ if (typeof chatId !== "number" || typeof messageId !== "number") {
738
+ await deps.answerCallbackQuery(query.id, "⌛ Interactive message expired.");
739
+ return true;
740
+ }
741
+ if (query.data === "compact:cancel") {
742
+ await deps.editInteractiveMessage(chatId, messageId, "<b>🚫 Compaction cancelled.</b>", "html", { inline_keyboard: [] });
743
+ await deps.answerCallbackQuery(query.id);
744
+ return true;
745
+ }
746
+ await deps.editInteractiveMessage(chatId, messageId, TELEGRAM_COMPACTION_STARTED_TEXT, "html", { inline_keyboard: [] });
747
+ await deps.answerCallbackQuery(query.id);
748
+ const threadId = callbackMessage?.message_thread_id;
749
+ await deps.runCompact(deps.ctx, chatId, messageId, typeof threadId === "number" ? { chatId, threadId } : { chatId });
750
+ return true;
751
+ }
752
+ export async function handleTelegramCompactCommand(deps) {
753
+ if (!deps.isIdle() ||
754
+ deps.hasPendingMessages() ||
755
+ deps.hasActiveTelegramTurn() ||
756
+ deps.hasDispatchPending() ||
757
+ deps.hasQueuedTelegramItems() ||
758
+ deps.isCompactionInProgress()) {
759
+ 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" });
760
+ return;
761
+ }
762
+ deps.setCompactionInProgress(true);
763
+ deps.updateStatus();
764
+ deps.startTypingLoop?.();
765
+ try {
766
+ deps.compact({
767
+ onComplete: () => {
768
+ deps.stopTypingLoop?.();
769
+ deps.setCompactionInProgress(false);
770
+ deps.updateStatus();
771
+ dispatchNextQueuedTelegramTurnAfterCompact(deps);
772
+ void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT, {
773
+ parseMode: "HTML",
774
+ });
775
+ },
776
+ onError: (error) => {
777
+ deps.stopTypingLoop?.();
778
+ deps.setCompactionInProgress(false);
779
+ deps.updateStatus();
780
+ dispatchNextQueuedTelegramTurnAfterCompact(deps);
781
+ deps.recordRuntimeEvent?.("compact", error);
782
+ void deps.sendTextReply(formatTelegramInformationHeading("⚠️", formatTelegramCompactionFailure(error)), { parseMode: "HTML" });
783
+ },
784
+ });
785
+ }
786
+ catch (error) {
787
+ deps.stopTypingLoop?.();
788
+ deps.setCompactionInProgress(false);
789
+ deps.updateStatus();
790
+ deps.recordRuntimeEvent?.("compact", error);
791
+ await deps.sendTextReply(formatTelegramInformationHeading("⚠️", formatTelegramCompactionFailure(error)), { parseMode: "HTML" });
792
+ return;
793
+ }
794
+ if (!deps.suppressStartNotice) {
795
+ await deps.sendTextReply(TELEGRAM_COMPACTION_STARTED_TEXT, {
796
+ parseMode: "HTML",
797
+ });
798
+ }
799
+ }
800
+ function isTelegramStaleContextError(error) {
801
+ return (error instanceof Error &&
802
+ (error.message.includes("stale after session") ||
803
+ error.message.includes("stale ctx")));
804
+ }
805
+ export async function handleTelegramStatusCommand(deps) {
806
+ try {
807
+ await deps.showStatus(deps.ctx);
808
+ }
809
+ catch (error) {
810
+ if (!isTelegramStaleContextError(error))
811
+ throw error;
812
+ }
813
+ }
814
+ export async function handleTelegramModelCommand(deps) {
815
+ try {
816
+ await deps.openModelMenu(deps.ctx);
817
+ }
818
+ catch (error) {
819
+ if (!isTelegramStaleContextError(error))
820
+ throw error;
821
+ }
822
+ }
823
+ export async function executeTelegramCommandAction(action, message, ctx, deps, commandArgs = "") {
824
+ switch (action.kind) {
825
+ case "ignore":
826
+ return false;
827
+ case "stop":
828
+ await deps.handleStop(message, ctx);
829
+ return true;
830
+ case "name":
831
+ await deps.handleName(message, ctx, commandArgs);
832
+ return true;
833
+ case "abort":
834
+ await deps.handleAbort(message, ctx);
835
+ return true;
836
+ case "next":
837
+ await deps.handleNext(message, ctx);
838
+ return true;
839
+ case "continue":
840
+ await deps.handleContinue(message, ctx);
841
+ return true;
842
+ case "queue":
843
+ await deps.handleQueue(message, ctx);
844
+ return true;
845
+ case "compact":
846
+ await deps.handleCompact(message, ctx);
847
+ return true;
848
+ case "status":
849
+ await deps.handleStatus(message, ctx);
850
+ return true;
851
+ case "model":
852
+ await deps.handleModel(message, ctx);
853
+ return true;
854
+ case "thinking":
855
+ await deps.handleThinking(message, ctx);
856
+ return true;
857
+ case "settings":
858
+ if (!deps.handleSettings)
859
+ return false;
860
+ await deps.handleSettings(message, ctx);
861
+ return true;
862
+ case "help":
863
+ await deps.handleHelp(message, action.commandName, ctx);
864
+ return true;
865
+ }
866
+ }
867
+ export function createTelegramCommandHandlerTargetRuntime(deps) {
868
+ const commandTargetRuntime = createTelegramCommandTargetQueueRuntime({
869
+ createControlItem: createTelegramControlItemBuilder({
870
+ allocateItemOrder: deps.allocateItemOrder,
871
+ allocateControlOrder: deps.allocateControlOrder,
872
+ }),
873
+ appendControlItem: deps.appendControlItem,
874
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
875
+ getAdmissionScope: deps.getAdmissionScope,
876
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
877
+ onControlQueued: deps.onControlQueued,
878
+ showStatus: deps.showStatus,
879
+ openModelMenu: deps.openModelMenu,
880
+ openSettingsMenu: deps.openSettingsMenu,
881
+ sendTextReply: deps.sendTextReply,
882
+ });
883
+ return createTelegramCommandHandler({
884
+ hasAbortHandler: deps.hasAbortHandler,
885
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
886
+ hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
887
+ clearQueuedTelegramItems: deps.clearQueuedTelegramItems,
888
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
889
+ abortCurrentTurn: deps.abortCurrentTurn,
890
+ isIdle: deps.isIdle,
891
+ hasPendingMessages: deps.hasPendingMessages,
892
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
893
+ hasDispatchPending: deps.hasDispatchPending,
894
+ isCompactionInProgress: deps.isCompactionInProgress,
895
+ setCompactionInProgress: deps.setCompactionInProgress,
896
+ updateStatus: deps.updateStatus,
897
+ isContextActive: deps.isContextActive,
898
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
899
+ startTypingLoop: deps.startTypingLoop,
900
+ stopTypingLoop: deps.stopTypingLoop,
901
+ enqueueContinueTurn: deps.enqueueContinueTurn,
902
+ compact: deps.compact,
903
+ sendInteractiveMessage: deps.sendInteractiveMessage,
904
+ enqueueControlItem: commandTargetRuntime.enqueueControlItem,
905
+ showStatus: commandTargetRuntime.showStatus,
906
+ openModelMenu: commandTargetRuntime.openModelMenu,
907
+ openThinkingMenu: deps.openThinkingMenu,
908
+ openQueueMenu: deps.openQueueMenu,
909
+ openSettingsMenu: commandTargetRuntime.openSettingsMenu,
910
+ handleForumBootstrap: deps.handleForumBootstrap,
911
+ getAllowedUserId: deps.getAllowedUserId,
912
+ persistAllowedUserId: deps.persistAllowedUserId,
913
+ registerBotCommands: createTelegramBotCommandRegistrar({
914
+ setMyCommands: deps.setMyCommands,
915
+ }),
916
+ validateThreadName: deps.validateThreadName,
917
+ renameCurrentThread: deps.renameCurrentThread,
918
+ resetCurrentThreadName: deps.resetCurrentThreadName,
919
+ openThreadNameDialog: deps.openThreadNameDialog,
920
+ sendTextReply: commandTargetRuntime.sendTextReply,
921
+ recordRuntimeEvent: deps.recordRuntimeEvent,
922
+ });
923
+ }
924
+ export function createTelegramCommandHandler(deps) {
925
+ return async (commandName, message, ctx, commandArgs) => {
926
+ return handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs);
927
+ };
928
+ }
929
+ export function createTelegramCommandOrPromptRuntime(deps) {
930
+ return {
931
+ dispatchMessages: async (messages, ctx) => {
932
+ const firstMessage = messages[0];
933
+ if (!firstMessage)
934
+ return;
935
+ if (deps.shouldIgnoreMessages?.(messages))
936
+ return;
937
+ deps.assertExecutionCurrent?.(firstMessage);
938
+ if (await deps.consumeThreadNameInput?.(messages, ctx)) {
939
+ deps.assertExecutionCurrent?.(firstMessage);
940
+ return;
941
+ }
942
+ const command = parseTelegramCommand(deps.extractRawText(messages));
943
+ const handled = await deps.handleCommand(command?.name, firstMessage, ctx, command?.args);
944
+ deps.assertExecutionCurrent?.(firstMessage);
945
+ if (handled)
946
+ return;
947
+ if (command && deps.executeExtensionCommand) {
948
+ const handledByExtension = await deps.executeExtensionCommand(command, messages[0], ctx);
949
+ deps.assertExecutionCurrent?.(firstMessage);
950
+ if (handledByExtension)
951
+ return;
952
+ }
953
+ if (command?.name && deps.expandPromptTemplateCommand) {
954
+ const expanded = deps.expandPromptTemplateCommand(command.name, command.args);
955
+ if (expanded !== undefined) {
956
+ deps.assertExecutionCurrent?.(firstMessage);
957
+ await deps.enqueueTurn([
958
+ deps.replaceMessageText(firstMessage, expanded),
959
+ ...messages.slice(1),
960
+ ], ctx);
961
+ return;
962
+ }
963
+ }
964
+ deps.assertExecutionCurrent?.(firstMessage);
965
+ await deps.enqueueTurn(messages, ctx);
966
+ },
967
+ };
968
+ }
969
+ function scheduleTelegramCommandEffect(ctx, command, phase, deps, effect, assertExecutionCurrent) {
970
+ void Promise.resolve()
971
+ .then(async () => {
972
+ if (deps.isContextActive?.(ctx) === false)
973
+ return;
974
+ assertExecutionCurrent?.();
975
+ await effect();
976
+ assertExecutionCurrent?.();
977
+ })
978
+ .catch((error) => {
979
+ try {
980
+ deps.recordRuntimeEvent?.("telegram-command", error, {
981
+ command,
982
+ phase,
983
+ });
984
+ }
985
+ catch {
986
+ // Effect diagnostics cannot create an unhandled detached Promise.
987
+ }
988
+ });
989
+ }
990
+ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs = "") {
991
+ const assertExecutionCurrentFor = (nextMessage) => () => deps.assertExecutionCurrent?.(nextMessage);
992
+ const sendReplyFor = (nextMessage) => async (text, options) => {
993
+ deps.assertExecutionCurrent?.(nextMessage);
994
+ await deps.sendTextReply(nextMessage, text, options);
995
+ deps.assertExecutionCurrent?.(nextMessage);
996
+ };
997
+ const updateStatusFor = (commandCtx) => () => deps.updateStatus(commandCtx);
998
+ return executeTelegramCommandAction(buildTelegramCommandAction(commandName), message, ctx, {
999
+ handleStop: async (nextMessage, commandCtx) => {
1000
+ await handleTelegramStopCommand({
1001
+ hasAbortHandler: deps.hasAbortHandler,
1002
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
1003
+ clearQueuedTelegramItems: () => deps.clearQueuedTelegramItems(commandCtx),
1004
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1005
+ abortCurrentTurn: deps.abortCurrentTurn,
1006
+ updateStatus: updateStatusFor(commandCtx),
1007
+ sendTextReply: sendReplyFor(nextMessage),
1008
+ });
1009
+ },
1010
+ handleName: async (nextMessage, _commandCtx, requestedName) => {
1011
+ const threadName = requestedName.trim();
1012
+ if (!threadName) {
1013
+ if (deps.openThreadNameDialog) {
1014
+ await deps.openThreadNameDialog(nextMessage, _commandCtx);
1015
+ }
1016
+ else {
1017
+ await sendReplyFor(nextMessage)(formatTelegramInformationHeading("🏷️", "Usage: /name Navigator"), { parseMode: "HTML" });
1018
+ }
1019
+ return;
1020
+ }
1021
+ if (/^[A-Z]$/.test(threadName) && deps.resetCurrentThreadName) {
1022
+ const result = await deps.resetCurrentThreadName(getTelegramCommandMessageTarget(nextMessage));
1023
+ await sendReplyFor(nextMessage)(result.ok && !result.message
1024
+ ? formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? threadName)
1025
+ : formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."), { parseMode: "HTML" });
1026
+ return;
1027
+ }
1028
+ const validationError = deps.validateThreadName?.(threadName);
1029
+ if (validationError) {
1030
+ await sendReplyFor(nextMessage)(formatTelegramInvalidInstanceName(validationError), { parseMode: "HTML" });
1031
+ return;
1032
+ }
1033
+ if (!deps.renameCurrentThread) {
1034
+ await sendReplyFor(nextMessage)(formatTelegramInformationHeading("🚫", "Thread display naming is unavailable."), { parseMode: "HTML" });
1035
+ return;
1036
+ }
1037
+ deps.assertExecutionCurrent?.(nextMessage);
1038
+ const result = await deps.renameCurrentThread(getTelegramCommandMessageTarget(nextMessage), threadName);
1039
+ deps.assertExecutionCurrent?.(nextMessage);
1040
+ await sendReplyFor(nextMessage)(result.ok && !result.message
1041
+ ? formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? threadName)
1042
+ : formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name update failed."), { parseMode: "HTML" });
1043
+ },
1044
+ handleAbort: async (nextMessage, commandCtx) => {
1045
+ await handleTelegramAbortCommand({
1046
+ hasAbortHandler: deps.hasAbortHandler,
1047
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
1048
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
1049
+ abortCurrentTurn: deps.abortCurrentTurn,
1050
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1051
+ updateStatus: updateStatusFor(commandCtx),
1052
+ sendTextReply: sendReplyFor(nextMessage),
1053
+ });
1054
+ },
1055
+ handleNext: async (nextMessage, commandCtx) => {
1056
+ await handleTelegramNextCommand({
1057
+ hasAbortHandler: deps.hasAbortHandler,
1058
+ isIdle: () => deps.isIdle(commandCtx),
1059
+ hasQueuedItems: deps.hasQueuedTelegramItems,
1060
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
1061
+ abortCurrentTurn: deps.abortCurrentTurn,
1062
+ dispatchNextQueuedTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx),
1063
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
1064
+ clearFoldForDispatch: () => deps.setFoldQueuedPromptsIntoHistory(false),
1065
+ updateStatus: updateStatusFor(commandCtx),
1066
+ sendTextReply: sendReplyFor(nextMessage),
1067
+ getActiveTurnReply: deps.getActiveTurnReply,
1068
+ });
1069
+ },
1070
+ handleContinue: async (nextMessage, commandCtx) => {
1071
+ await handleTelegramContinueCommand(nextMessage, commandCtx, {
1072
+ enqueueContinueTurn: deps.enqueueContinueTurn,
1073
+ });
1074
+ },
1075
+ handleQueue: async (nextMessage, commandCtx) => {
1076
+ scheduleTelegramCommandEffect(commandCtx, "queue", "menu-render", deps, () => deps.openQueueMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
1077
+ },
1078
+ handleCompact: async (nextMessage, commandCtx) => {
1079
+ if (deps.sendInteractiveMessage) {
1080
+ await openTelegramCompactConfirmation(getTelegramCommandMessageTarget(nextMessage), { sendInteractiveMessage: deps.sendInteractiveMessage });
1081
+ return;
1082
+ }
1083
+ await handleTelegramCompactCommand({
1084
+ isIdle: () => deps.isIdle(commandCtx),
1085
+ hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
1086
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
1087
+ hasDispatchPending: deps.hasDispatchPending,
1088
+ hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
1089
+ isCompactionInProgress: deps.isCompactionInProgress,
1090
+ setCompactionInProgress: deps.setCompactionInProgress,
1091
+ updateStatus: updateStatusFor(commandCtx),
1092
+ dispatchNextQueuedTelegramTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx),
1093
+ requestDeferredDispatchNextQueuedTelegramTurn: deps.requestDeferredDispatchNextQueuedTelegramTurn
1094
+ ? (dispatch) => deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() => dispatch())
1095
+ : undefined,
1096
+ compact: (callbacks) => deps.compact(commandCtx, callbacks),
1097
+ startTypingLoop: deps.startTypingLoop
1098
+ ? () => deps.startTypingLoop?.(commandCtx, nextMessage.chat.id, {
1099
+ target: getTelegramCommandMessageTarget(nextMessage),
1100
+ })
1101
+ : undefined,
1102
+ stopTypingLoop: deps.stopTypingLoop,
1103
+ sendTextReply: sendReplyFor(nextMessage),
1104
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1105
+ });
1106
+ },
1107
+ handleStatus: async (nextMessage, commandCtx) => {
1108
+ scheduleTelegramCommandEffect(commandCtx, "status", "menu-render", deps, () => deps.showStatus(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
1109
+ },
1110
+ handleModel: async (nextMessage, commandCtx) => {
1111
+ scheduleTelegramCommandEffect(commandCtx, "model", "menu-render", deps, () => handleTelegramModelCommand({
1112
+ ctx: commandCtx,
1113
+ openModelMenu: (controlCtx) => deps.openModelMenu(nextMessage, controlCtx),
1114
+ }), assertExecutionCurrentFor(nextMessage));
1115
+ },
1116
+ handleThinking: async (nextMessage, commandCtx) => {
1117
+ scheduleTelegramCommandEffect(commandCtx, "thinking", "menu-render", deps, () => deps.openThinkingMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
1118
+ },
1119
+ handleSettings: deps.openSettingsMenu
1120
+ ? async (nextMessage, commandCtx) => {
1121
+ scheduleTelegramCommandEffect(commandCtx, "settings", "menu-render", deps, () => deps.openSettingsMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
1122
+ }
1123
+ : undefined,
1124
+ handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
1125
+ if (nextMessage.from?.id !== undefined &&
1126
+ canPairTelegramUserFromCommandMessage(nextMessage)) {
1127
+ const allowed = await pairTelegramUserIfNeeded(nextMessage.from.id, {
1128
+ allowedUserId: deps.getAllowedUserId(),
1129
+ ctx: undefined,
1130
+ persistAllowedUserId: deps.persistAllowedUserId,
1131
+ updateStatus: updateStatusFor(commandCtx),
1132
+ assertExecutionCurrent: assertExecutionCurrentFor(nextMessage),
1133
+ });
1134
+ if (!allowed)
1135
+ return;
1136
+ }
1137
+ const isContextActive = () => deps.isContextActive?.(commandCtx) !== false;
1138
+ scheduleTelegramCommandEffect(commandCtx, nextCommandName, "menu-render", deps, async () => {
1139
+ let forumBootstrapMessage;
1140
+ if (nextCommandName === "start" && deps.handleForumBootstrap) {
1141
+ forumBootstrapMessage = await deps.handleForumBootstrap(nextMessage, commandCtx);
1142
+ }
1143
+ if (!isContextActive())
1144
+ return;
1145
+ if (forumBootstrapMessage) {
1146
+ await deps.sendTextReply(nextMessage, forumBootstrapMessage);
1147
+ }
1148
+ if (!isContextActive())
1149
+ return;
1150
+ await deps.showStatus(nextMessage, commandCtx);
1151
+ }, assertExecutionCurrentFor(nextMessage));
1152
+ scheduleTelegramCommandEffect(commandCtx, nextCommandName, "bot-command-sync", deps, deps.registerBotCommands, assertExecutionCurrentFor(nextMessage));
1153
+ },
1154
+ }, commandArgs);
1155
+ }