@poolzin/pool-bot 2026.2.0 → 2026.2.1

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 (230) hide show
  1. package/dist/agents/bash-tools.exec.js +76 -25
  2. package/dist/agents/cli-runner/helpers.js +9 -11
  3. package/dist/agents/identity.js +47 -7
  4. package/dist/agents/memory-search.js +25 -8
  5. package/dist/agents/model-selection.js +21 -0
  6. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  7. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  8. package/dist/agents/pi-embedded-helpers.js +1 -1
  9. package/dist/agents/pi-embedded-runner/compact.js +1 -0
  10. package/dist/agents/pi-embedded-runner/model.js +61 -2
  11. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  12. package/dist/agents/pi-embedded-runner/run.js +199 -46
  13. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  14. package/dist/agents/pi-embedded-subscribe.js +118 -29
  15. package/dist/agents/pi-tools.js +10 -5
  16. package/dist/agents/poolbot-tools.js +15 -10
  17. package/dist/agents/sandbox-paths.js +31 -0
  18. package/dist/agents/session-tool-result-guard.js +94 -15
  19. package/dist/agents/shell-utils.js +51 -0
  20. package/dist/agents/skills/bundled-context.js +23 -0
  21. package/dist/agents/skills/bundled-dir.js +41 -7
  22. package/dist/agents/skills-install.js +60 -23
  23. package/dist/agents/subagent-announce.js +79 -34
  24. package/dist/agents/tool-policy.conformance.js +14 -0
  25. package/dist/agents/tool-policy.js +24 -0
  26. package/dist/agents/tools/cron-tool.js +166 -19
  27. package/dist/agents/tools/discord-actions-presence.js +78 -0
  28. package/dist/agents/tools/message-tool.js +56 -2
  29. package/dist/agents/tools/sessions-history-tool.js +69 -1
  30. package/dist/agents/tools/web-search.js +211 -42
  31. package/dist/agents/usage.js +23 -1
  32. package/dist/agents/workspace-run.js +67 -0
  33. package/dist/agents/workspace-templates.js +44 -0
  34. package/dist/auto-reply/command-auth.js +121 -6
  35. package/dist/auto-reply/envelope.js +50 -72
  36. package/dist/auto-reply/reply/commands-compact.js +1 -0
  37. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  38. package/dist/auto-reply/reply/commands-context.js +1 -0
  39. package/dist/auto-reply/reply/commands-models.js +107 -60
  40. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  41. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  42. package/dist/auto-reply/reply/inbound-context.js +5 -1
  43. package/dist/auto-reply/reply/model-selection.js +3 -3
  44. package/dist/auto-reply/thinking.js +88 -43
  45. package/dist/browser/bridge-server.js +13 -0
  46. package/dist/browser/cdp.helpers.js +38 -24
  47. package/dist/browser/client-fetch.js +50 -7
  48. package/dist/browser/config.js +1 -10
  49. package/dist/browser/extension-relay.js +101 -40
  50. package/dist/browser/pw-ai.js +1 -1
  51. package/dist/browser/pw-session.js +143 -8
  52. package/dist/browser/pw-tools-core.interactions.js +125 -27
  53. package/dist/browser/pw-tools-core.responses.js +1 -1
  54. package/dist/browser/pw-tools-core.state.js +1 -1
  55. package/dist/browser/routes/agent.act.js +86 -41
  56. package/dist/browser/routes/dispatcher.js +4 -4
  57. package/dist/browser/screenshot.js +1 -1
  58. package/dist/browser/server.js +13 -0
  59. package/dist/build-info.json +3 -3
  60. package/dist/channels/reply-prefix.js +8 -1
  61. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  62. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  63. package/dist/cli/cron-cli/shared.js +56 -41
  64. package/dist/cli/dns-cli.js +26 -14
  65. package/dist/cli/gateway-cli/register.js +37 -19
  66. package/dist/cli/memory-cli.js +5 -5
  67. package/dist/cli/parse-bytes.js +37 -0
  68. package/dist/cli/update-cli.js +173 -52
  69. package/dist/commands/agent.js +1 -0
  70. package/dist/commands/doctor-config-flow.js +61 -5
  71. package/dist/commands/doctor-state-migrations.js +1 -1
  72. package/dist/commands/health.js +1 -1
  73. package/dist/commands/model-allowlist.js +29 -0
  74. package/dist/commands/model-picker.js +2 -1
  75. package/dist/commands/models/list.status-command.js +43 -23
  76. package/dist/commands/models/shared.js +15 -0
  77. package/dist/commands/onboard-custom.js +384 -0
  78. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  79. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  80. package/dist/commands/onboard-skills.js +63 -38
  81. package/dist/commands/openai-model-default.js +41 -0
  82. package/dist/config/defaults.js +3 -2
  83. package/dist/config/paths.js +136 -35
  84. package/dist/config/plugin-auto-enable.js +21 -5
  85. package/dist/config/redact-snapshot.js +153 -0
  86. package/dist/config/schema.field-metadata.js +590 -0
  87. package/dist/config/schema.js +2 -2
  88. package/dist/config/sessions/store.js +291 -23
  89. package/dist/config/zod-schema.agent-defaults.js +3 -0
  90. package/dist/config/zod-schema.agent-runtime.js +13 -2
  91. package/dist/config/zod-schema.providers-core.js +142 -0
  92. package/dist/config/zod-schema.session.js +3 -0
  93. package/dist/cron/delivery.js +57 -0
  94. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  95. package/dist/cron/isolated-agent/helpers.js +22 -5
  96. package/dist/cron/isolated-agent/run.js +171 -63
  97. package/dist/cron/isolated-agent/session.js +2 -0
  98. package/dist/cron/normalize.js +356 -28
  99. package/dist/cron/parse.js +10 -5
  100. package/dist/cron/run-log.js +35 -10
  101. package/dist/cron/schedule.js +41 -6
  102. package/dist/cron/service/jobs.js +208 -35
  103. package/dist/cron/service/ops.js +72 -16
  104. package/dist/cron/service/state.js +2 -0
  105. package/dist/cron/service/store.js +386 -14
  106. package/dist/cron/service/timer.js +390 -147
  107. package/dist/cron/session-reaper.js +86 -0
  108. package/dist/cron/store.js +23 -8
  109. package/dist/cron/validate-timestamp.js +43 -0
  110. package/dist/discord/monitor/agent-components.js +438 -0
  111. package/dist/discord/monitor/allow-list.js +28 -5
  112. package/dist/discord/monitor/gateway-registry.js +29 -0
  113. package/dist/discord/monitor/native-command.js +44 -23
  114. package/dist/discord/monitor/sender-identity.js +45 -0
  115. package/dist/discord/pluralkit.js +27 -0
  116. package/dist/discord/send.outbound.js +92 -5
  117. package/dist/discord/send.shared.js +60 -23
  118. package/dist/discord/targets.js +84 -1
  119. package/dist/entry.js +15 -9
  120. package/dist/extensionAPI.js +8 -0
  121. package/dist/gateway/control-ui.js +8 -1
  122. package/dist/gateway/hooks-mapping.js +3 -0
  123. package/dist/gateway/hooks.js +65 -0
  124. package/dist/gateway/net.js +96 -31
  125. package/dist/gateway/node-command-policy.js +50 -15
  126. package/dist/gateway/origin-check.js +56 -0
  127. package/dist/gateway/protocol/client-info.js +9 -0
  128. package/dist/gateway/protocol/index.js +9 -2
  129. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  130. package/dist/gateway/protocol/schema/cron.js +22 -10
  131. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  132. package/dist/gateway/protocol/schema/sessions.js +12 -0
  133. package/dist/gateway/server/hooks.js +1 -1
  134. package/dist/gateway/server-broadcast.js +26 -9
  135. package/dist/gateway/server-chat.js +112 -23
  136. package/dist/gateway/server-discovery-runtime.js +10 -2
  137. package/dist/gateway/server-http.js +109 -11
  138. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  139. package/dist/gateway/server-methods/agents.js +321 -2
  140. package/dist/gateway/server-methods/usage.js +559 -16
  141. package/dist/gateway/server-runtime-state.js +22 -8
  142. package/dist/gateway/server-startup-memory.js +16 -0
  143. package/dist/gateway/server.impl.js +5 -1
  144. package/dist/gateway/session-utils.fs.js +23 -25
  145. package/dist/gateway/session-utils.js +20 -10
  146. package/dist/gateway/sessions-patch.js +7 -22
  147. package/dist/gateway/test-helpers.server.js +35 -2
  148. package/dist/imessage/constants.js +2 -0
  149. package/dist/imessage/monitor/deliver.js +4 -1
  150. package/dist/imessage/monitor/monitor-provider.js +51 -1
  151. package/dist/infra/bonjour-discovery.js +131 -70
  152. package/dist/infra/control-ui-assets.js +134 -12
  153. package/dist/infra/errors.js +12 -0
  154. package/dist/infra/exec-approvals.js +266 -57
  155. package/dist/infra/format-time/format-datetime.js +79 -0
  156. package/dist/infra/format-time/format-duration.js +81 -0
  157. package/dist/infra/format-time/format-relative.js +80 -0
  158. package/dist/infra/heartbeat-runner.js +140 -49
  159. package/dist/infra/home-dir.js +54 -0
  160. package/dist/infra/net/fetch-guard.js +122 -0
  161. package/dist/infra/net/ssrf.js +65 -29
  162. package/dist/infra/outbound/abort.js +14 -0
  163. package/dist/infra/outbound/message-action-runner.js +77 -13
  164. package/dist/infra/outbound/outbound-session.js +143 -37
  165. package/dist/infra/poolbot-root.js +43 -1
  166. package/dist/infra/session-cost-usage.js +631 -41
  167. package/dist/infra/state-migrations.js +317 -47
  168. package/dist/infra/update-global.js +35 -0
  169. package/dist/infra/update-runner.js +149 -43
  170. package/dist/infra/warning-filter.js +65 -0
  171. package/dist/infra/widearea-dns.js +30 -9
  172. package/dist/logging/redact-identifier.js +12 -0
  173. package/dist/media/fetch.js +81 -58
  174. package/dist/media-understanding/apply.js +403 -3
  175. package/dist/media-understanding/attachments.js +38 -27
  176. package/dist/media-understanding/defaults.js +16 -0
  177. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  178. package/dist/media-understanding/providers/google/audio.js +24 -17
  179. package/dist/media-understanding/providers/google/video.js +24 -17
  180. package/dist/media-understanding/providers/image.js +2 -2
  181. package/dist/media-understanding/providers/index.js +4 -1
  182. package/dist/media-understanding/providers/openai/audio.js +22 -14
  183. package/dist/media-understanding/providers/shared.js +16 -11
  184. package/dist/media-understanding/providers/zai/index.js +6 -0
  185. package/dist/media-understanding/runner.js +158 -90
  186. package/dist/memory/batch-voyage.js +277 -0
  187. package/dist/memory/embeddings-voyage.js +75 -0
  188. package/dist/memory/embeddings.js +28 -16
  189. package/dist/memory/internal.js +101 -18
  190. package/dist/memory/manager.js +154 -48
  191. package/dist/memory/search-manager.js +173 -0
  192. package/dist/memory/session-files.js +9 -3
  193. package/dist/node-host/runner.js +34 -24
  194. package/dist/node-host/with-timeout.js +27 -0
  195. package/dist/plugins/commands.js +5 -1
  196. package/dist/plugins/config-state.js +86 -7
  197. package/dist/plugins/source-display.js +51 -0
  198. package/dist/process/exec.js +20 -2
  199. package/dist/routing/resolve-route.js +12 -0
  200. package/dist/routing/session-key.js +15 -0
  201. package/dist/runtime.js +2 -0
  202. package/dist/security/audit-extra.async.js +601 -0
  203. package/dist/security/audit-extra.js +2 -830
  204. package/dist/security/audit-extra.sync.js +505 -0
  205. package/dist/security/channel-metadata.js +34 -0
  206. package/dist/security/external-content.js +88 -6
  207. package/dist/security/skill-scanner.js +330 -0
  208. package/dist/sessions/session-key-utils.js +7 -0
  209. package/dist/signal/monitor/event-handler.js +80 -1
  210. package/dist/slack/monitor/media.js +85 -15
  211. package/dist/tailscale/detect.js +1 -2
  212. package/dist/telegram/bot/helpers.js +109 -28
  213. package/dist/telegram/bot-handlers.js +144 -3
  214. package/dist/telegram/bot-message-context.js +37 -10
  215. package/dist/telegram/bot-message-dispatch.js +48 -15
  216. package/dist/telegram/bot-native-commands.js +86 -29
  217. package/dist/telegram/bot.js +30 -29
  218. package/dist/telegram/model-buttons.js +163 -0
  219. package/dist/telegram/monitor.js +110 -85
  220. package/dist/telegram/send.js +129 -47
  221. package/dist/terminal/restore.js +45 -0
  222. package/dist/test-helpers/state-dir-env.js +16 -0
  223. package/dist/tts/tts.js +12 -6
  224. package/dist/tui/tui-session-actions.js +166 -54
  225. package/dist/utils/fetch-timeout.js +20 -0
  226. package/dist/utils/normalize-secret-input.js +19 -0
  227. package/dist/utils/transcript-tools.js +58 -0
  228. package/dist/utils.js +45 -14
  229. package/dist/version.js +42 -5
  230. package/package.json +1 -1
@@ -1,23 +1,23 @@
1
- import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
2
1
  import { resolveChunkMode } from "../auto-reply/chunk.js";
3
2
  import { buildCommandTextFromArgs, findCommandByNativeName, listNativeCommandSpecs, listNativeCommandSpecsForConfig, parseCommandArgs, resolveCommandArgMenu, } from "../auto-reply/commands-registry.js";
4
- import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
5
- import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
6
- import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
7
3
  import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
8
- import { danger, logVerbose } from "../globals.js";
4
+ import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
5
+ import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
6
+ import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
7
+ import { createReplyPrefixContext } from "../channels/reply-prefix.js";
9
8
  import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
10
- import { withTelegramApiErrorLogging } from "./api-logging.js";
9
+ import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
11
10
  import { normalizeTelegramCommandName, TELEGRAM_COMMAND_NAME_PATTERN, } from "../config/telegram-custom-commands.js";
11
+ import { danger, logVerbose } from "../globals.js";
12
+ import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
13
+ import { executePluginCommand, getPluginCommandSpecs, matchPluginCommand, } from "../plugins/commands.js";
12
14
  import { resolveAgentRoute } from "../routing/resolve-route.js";
13
15
  import { resolveThreadSessionKeys } from "../routing/session-key.js";
14
- import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
15
- import { executePluginCommand, getPluginCommandSpecs, matchPluginCommand, } from "../plugins/commands.js";
16
+ import { withTelegramApiErrorLogging } from "./api-logging.js";
17
+ import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
16
18
  import { deliverReplies } from "./bot/delivery.js";
19
+ import { buildTelegramThreadParams, buildSenderName, buildTelegramGroupFrom, buildTelegramGroupPeerId, resolveTelegramForumThreadId, } from "./bot/helpers.js";
17
20
  import { buildInlineKeyboard } from "./send.js";
18
- import { buildSenderName, buildTelegramGroupFrom, buildTelegramGroupPeerId, resolveTelegramForumThreadId, } from "./bot/helpers.js";
19
- import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
20
- import { readTelegramAllowFromStore } from "./pairing-store.js";
21
21
  async function resolveTelegramCommandAuth(params) {
22
22
  const { msg, bot, cfg, telegramCfg, allowFrom, groupAllowFrom, useAccessGroups, resolveGroupPolicy, resolveTelegramGroupConfig, requireAuth, } = params;
23
23
  const chatId = msg.chat.id;
@@ -28,7 +28,7 @@ async function resolveTelegramCommandAuth(params) {
28
28
  isForum,
29
29
  messageThreadId,
30
30
  });
31
- const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
31
+ const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []);
32
32
  const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
33
33
  const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
34
34
  const effectiveGroupAllow = normalizeAllowFromWithStore({
@@ -69,7 +69,7 @@ async function resolveTelegramCommandAuth(params) {
69
69
  }
70
70
  if (isGroup && useAccessGroups) {
71
71
  const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
72
- const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
72
+ const groupPolicy = firstDefined(topicConfig?.groupPolicy, groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
73
73
  if (groupPolicy === "disabled") {
74
74
  await withTelegramApiErrorLogging({
75
75
  operation: "sendMessage",
@@ -134,7 +134,13 @@ async function resolveTelegramCommandAuth(params) {
134
134
  };
135
135
  }
136
136
  export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, telegramCfg, allowFrom, groupAllowFrom, replyToMode, textLimit, useAccessGroups, nativeEnabled, nativeSkillsEnabled, nativeDisabledExplicit, resolveGroupPolicy, resolveTelegramGroupConfig, shouldSkipUpdate, opts, }) => {
137
- const skillCommands = nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : [];
137
+ const boundRoute = nativeEnabled && nativeSkillsEnabled
138
+ ? resolveAgentRoute({ cfg, channel: "telegram", accountId })
139
+ : null;
140
+ const boundAgentIds = boundRoute && boundRoute.matchedBy.startsWith("binding.") ? [boundRoute.agentId] : null;
141
+ const skillCommands = nativeEnabled && nativeSkillsEnabled
142
+ ? listSkillCommandsForAgents(boundAgentIds ? { cfg, agentIds: boundAgentIds } : { cfg })
143
+ : [];
138
144
  const nativeCommands = nativeEnabled
139
145
  ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "telegram" })
140
146
  : [];
@@ -180,7 +186,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
180
186
  existingCommands.add(normalized);
181
187
  pluginCommands.push({ command: normalized, description });
182
188
  }
183
- const allCommands = [
189
+ const allCommandsFull = [
184
190
  ...nativeCommands.map((command) => ({
185
191
  command: command.name,
186
192
  description: command.description,
@@ -188,12 +194,39 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
188
194
  ...pluginCommands,
189
195
  ...customCommands,
190
196
  ];
191
- if (allCommands.length > 0) {
192
- void withTelegramApiErrorLogging({
193
- operation: "setMyCommands",
197
+ // Telegram Bot API limits commands to 100 per scope.
198
+ // Truncate with a warning rather than failing with BOT_COMMANDS_TOO_MUCH.
199
+ const TELEGRAM_MAX_COMMANDS = 100;
200
+ if (allCommandsFull.length > TELEGRAM_MAX_COMMANDS) {
201
+ runtime.log?.(`telegram: truncating ${allCommandsFull.length} commands to ${TELEGRAM_MAX_COMMANDS} (Telegram Bot API limit)`);
202
+ }
203
+ const allCommands = allCommandsFull.slice(0, TELEGRAM_MAX_COMMANDS);
204
+ // Clear stale commands before registering new ones to prevent
205
+ // leftover commands from deleted skills persisting across restarts.
206
+ // Chain delete → set so a late-resolving delete cannot wipe newly registered commands.
207
+ const registerCommands = () => {
208
+ if (allCommands.length > 0) {
209
+ withTelegramApiErrorLogging({
210
+ operation: "setMyCommands",
211
+ runtime,
212
+ fn: () => bot.api.setMyCommands(allCommands),
213
+ }).catch(() => { });
214
+ }
215
+ };
216
+ if (typeof bot.api.deleteMyCommands === "function") {
217
+ withTelegramApiErrorLogging({
218
+ operation: "deleteMyCommands",
194
219
  runtime,
195
- fn: () => bot.api.setMyCommands(allCommands),
196
- }).catch(() => { });
220
+ fn: () => bot.api.deleteMyCommands(),
221
+ })
222
+ .catch(() => { })
223
+ .then(registerCommands)
224
+ .catch(() => { });
225
+ }
226
+ else {
227
+ registerCommands();
228
+ }
229
+ if (allCommands.length > 0) {
197
230
  if (typeof bot.command !== "function") {
198
231
  logVerbose("telegram: bot.command unavailable; skipping native handlers");
199
232
  }
@@ -201,10 +234,12 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
201
234
  for (const command of nativeCommands) {
202
235
  bot.command(command.name, async (ctx) => {
203
236
  const msg = ctx.message;
204
- if (!msg)
237
+ if (!msg) {
205
238
  return;
206
- if (shouldSkipUpdate(ctx))
239
+ }
240
+ if (shouldSkipUpdate(ctx)) {
207
241
  return;
242
+ }
208
243
  const auth = await resolveTelegramCommandAuth({
209
244
  msg,
210
245
  bot,
@@ -217,9 +252,12 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
217
252
  resolveTelegramGroupConfig,
218
253
  requireAuth: true,
219
254
  });
220
- if (!auth)
255
+ if (!auth) {
221
256
  return;
257
+ }
222
258
  const { chatId, isGroup, isForum, resolvedThreadId, senderId, senderUsername, groupConfig, topicConfig, commandAuthorized, } = auth;
259
+ const messageThreadId = msg.message_thread_id;
260
+ const threadParams = buildTelegramThreadParams(resolvedThreadId) ?? {};
223
261
  const commandDefinition = findCommandByNativeName(command.name, "telegram");
224
262
  const rawText = ctx.match?.trim() ?? "";
225
263
  const commandArgs = commandDefinition
@@ -261,7 +299,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
261
299
  runtime,
262
300
  fn: () => bot.api.sendMessage(chatId, title, {
263
301
  ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
264
- ...(resolvedThreadId != null ? { message_thread_id: resolvedThreadId } : {}),
302
+ ...threadParams,
265
303
  }),
266
304
  });
267
305
  return;
@@ -276,9 +314,13 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
276
314
  },
277
315
  });
278
316
  const baseSessionKey = route.sessionKey;
279
- const dmThreadId = !isGroup ? resolvedThreadId : undefined;
317
+ // DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
318
+ const dmThreadId = !isGroup ? messageThreadId : undefined;
280
319
  const threadKeys = dmThreadId != null
281
- ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
320
+ ? resolveThreadSessionKeys({
321
+ baseSessionKey,
322
+ threadId: String(dmThreadId),
323
+ })
282
324
  : null;
283
325
  const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
284
326
  const tableMode = resolveMarkdownTableMode({
@@ -299,6 +341,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
299
341
  : (buildSenderName(msg) ?? String(senderId || chatId));
300
342
  const ctxPayload = finalizeInboundContext({
301
343
  Body: prompt,
344
+ BodyForAgent: prompt,
302
345
  RawBody: prompt,
303
346
  CommandBody: prompt,
304
347
  CommandArgs: commandArgs,
@@ -318,6 +361,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
318
361
  CommandAuthorized: commandAuthorized,
319
362
  CommandSource: "native",
320
363
  SessionKey: `telegram:slash:${senderId || chatId}`,
364
+ AccountId: route.accountId,
321
365
  CommandTargetSessionKey: sessionKey,
322
366
  MessageThreadId: resolvedThreadId,
323
367
  IsForum: isForum,
@@ -329,12 +373,16 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
329
373
  ? !telegramCfg.blockStreaming
330
374
  : undefined;
331
375
  const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId);
376
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixContext({
377
+ cfg,
378
+ agentId: route.agentId,
379
+ });
332
380
  await dispatchReplyWithBufferedBlockDispatcher({
333
381
  ctx: ctxPayload,
334
382
  cfg,
335
383
  dispatcherOptions: {
336
- responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
337
- deliver: async (payload) => {
384
+ ...prefixOptions,
385
+ deliver: async (payload, _info) => {
338
386
  await deliverReplies({
339
387
  replies: [payload],
340
388
  chatId: String(chatId),
@@ -356,6 +404,7 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
356
404
  replyOptions: {
357
405
  skillFilter,
358
406
  disableBlockStreaming,
407
+ onModelSelected,
359
408
  },
360
409
  });
361
410
  });
@@ -393,7 +442,11 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
393
442
  });
394
443
  if (!auth)
395
444
  return;
396
- const { resolvedThreadId, senderId, commandAuthorized } = auth;
445
+ const { resolvedThreadId, senderId, commandAuthorized, isGroup } = auth;
446
+ const from = isGroup
447
+ ? buildTelegramGroupFrom(chatId, resolvedThreadId)
448
+ : `telegram:${chatId}`;
449
+ const to = `telegram:${chatId}`;
397
450
  const result = await executePluginCommand({
398
451
  command: match.command,
399
452
  args: match.args,
@@ -402,6 +455,10 @@ export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, t
402
455
  isAuthorizedSender: commandAuthorized,
403
456
  commandBody,
404
457
  config: cfg,
458
+ from,
459
+ to,
460
+ accountId,
461
+ messageThreadId: resolvedThreadId,
405
462
  });
406
463
  const tableMode = resolveMarkdownTableMode({
407
464
  cfg,
@@ -17,9 +17,8 @@ import { enqueueSystemEvent } from "../infra/system-events.js";
17
17
  import { getChildLogger } from "../logging.js";
18
18
  import { withTelegramApiErrorLogging } from "./api-logging.js";
19
19
  import { resolveAgentRoute } from "../routing/resolve-route.js";
20
- import { resolveThreadSessionKeys } from "../routing/session-key.js";
21
20
  import { resolveTelegramAccount } from "./accounts.js";
22
- import { buildTelegramGroupPeerId, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js";
21
+ import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js";
23
22
  import { registerTelegramHandlers } from "./bot-handlers.js";
24
23
  import { createTelegramMessageProcessor } from "./bot-message.js";
25
24
  import { registerTelegramNativeCommands } from "./bot-native-commands.js";
@@ -45,11 +44,12 @@ export function getTelegramSequentialKey(ctx) {
45
44
  return `telegram:${chatId}:control`;
46
45
  return "telegram:control";
47
46
  }
47
+ const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
48
+ const messageThreadId = msg?.message_thread_id;
48
49
  const isForum = msg?.chat?.is_forum;
49
- const threadId = resolveTelegramForumThreadId({
50
- isForum,
51
- messageThreadId: msg?.message_thread_id,
52
- });
50
+ const threadId = isGroup
51
+ ? resolveTelegramForumThreadId({ isForum, messageThreadId })
52
+ : messageThreadId;
53
53
  if (typeof chatId === "number") {
54
54
  return threadId != null ? `telegram:${chatId}:topic:${threadId}` : `telegram:${chatId}`;
55
55
  }
@@ -73,14 +73,15 @@ export function createTelegramBot(opts) {
73
73
  network: telegramCfg.network,
74
74
  });
75
75
  const shouldProvideFetch = Boolean(fetchImpl);
76
+ // grammY's ApiClientOptions types still track `node-fetch` types; Node 22+ global fetch
77
+ // (undici) is structurally compatible at runtime but not assignable in TS.
78
+ const fetchForClient = fetchImpl;
76
79
  const timeoutSeconds = typeof telegramCfg?.timeoutSeconds === "number" && Number.isFinite(telegramCfg.timeoutSeconds)
77
80
  ? Math.max(1, Math.floor(telegramCfg.timeoutSeconds))
78
81
  : undefined;
79
82
  const client = shouldProvideFetch || timeoutSeconds
80
83
  ? {
81
- ...(shouldProvideFetch && fetchImpl
82
- ? { fetch: fetchImpl }
83
- : {}),
84
+ ...(shouldProvideFetch && fetchImpl ? { fetch: fetchForClient } : {}),
84
85
  ...(timeoutSeconds ? { timeoutSeconds } : {}),
85
86
  }
86
87
  : undefined;
@@ -193,19 +194,22 @@ export function createTelegramBot(opts) {
193
194
  const logger = getChildLogger({ module: "telegram-auto-reply" });
194
195
  let botHasTopicsEnabled;
195
196
  const resolveBotTopicsEnabled = async (ctx) => {
196
- const fromCtx = ctx?.me;
197
- if (typeof fromCtx?.has_topics_enabled === "boolean") {
198
- botHasTopicsEnabled = fromCtx.has_topics_enabled;
197
+ if (typeof ctx?.me?.has_topics_enabled === "boolean") {
198
+ botHasTopicsEnabled = ctx.me.has_topics_enabled;
199
199
  return botHasTopicsEnabled;
200
200
  }
201
201
  if (typeof botHasTopicsEnabled === "boolean")
202
202
  return botHasTopicsEnabled;
203
+ if (typeof bot.api.getMe !== "function") {
204
+ botHasTopicsEnabled = false;
205
+ return botHasTopicsEnabled;
206
+ }
203
207
  try {
204
- const me = (await withTelegramApiErrorLogging({
208
+ const me = await withTelegramApiErrorLogging({
205
209
  operation: "getMe",
206
210
  runtime,
207
211
  fn: () => bot.api.getMe(),
208
- }));
212
+ });
209
213
  botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
210
214
  }
211
215
  catch (err) {
@@ -340,28 +344,25 @@ export function createTelegramBot(opts) {
340
344
  senderLabel = `id:${user.id}`;
341
345
  }
342
346
  senderLabel = senderLabel || "unknown";
343
- // Extract forum thread info (similar to message processing)
344
- const messageThreadId = reaction.message_thread_id;
345
- const isForum = reaction.chat.is_forum === true;
346
- const resolvedThreadId = resolveTelegramForumThreadId({
347
- isForum,
348
- messageThreadId,
349
- });
350
- // Resolve agent route for session
347
+ // Reactions target a specific message_id; the Telegram Bot API does not include
348
+ // message_thread_id on MessageReactionUpdated, so we route to the chat-level
349
+ // session (forum topic routing is not available for reactions).
351
350
  const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
351
+ const isForum = reaction.chat.is_forum === true;
352
+ const resolvedThreadId = isForum
353
+ ? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined })
354
+ : undefined;
352
355
  const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
356
+ const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
357
+ // Fresh config for bindings lookup; other routing inputs are payload-derived.
353
358
  const route = resolveAgentRoute({
354
- cfg,
359
+ cfg: loadConfig(),
355
360
  channel: "telegram",
356
361
  accountId: account.accountId,
357
362
  peer: { kind: isGroup ? "group" : "dm", id: peerId },
363
+ parentPeer,
358
364
  });
359
- const baseSessionKey = route.sessionKey;
360
- const dmThreadId = !isGroup ? resolvedThreadId : undefined;
361
- const threadKeys = dmThreadId != null
362
- ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
363
- : null;
364
- const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
365
+ const sessionKey = route.sessionKey;
365
366
  // Enqueue system event for each added reaction
366
367
  for (const r of addedReactions) {
367
368
  const emoji = r.emoji;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Telegram inline button utilities for model selection.
3
+ *
4
+ * Callback data patterns (max 64 bytes for Telegram):
5
+ * - mdl_prov - show providers list
6
+ * - mdl_list_{prov}_{pg} - show models for provider (page N, 1-indexed)
7
+ * - mdl_sel_{provider/id} - select model
8
+ * - mdl_back - back to providers list
9
+ */
10
+ const MODELS_PAGE_SIZE = 8;
11
+ const MAX_CALLBACK_DATA_BYTES = 64;
12
+ /**
13
+ * Parse a model callback_data string into a structured object.
14
+ * Returns null if the data doesn't match a known pattern.
15
+ */
16
+ export function parseModelCallbackData(data) {
17
+ const trimmed = data.trim();
18
+ if (!trimmed.startsWith("mdl_")) {
19
+ return null;
20
+ }
21
+ if (trimmed === "mdl_prov" || trimmed === "mdl_back") {
22
+ return { type: trimmed === "mdl_prov" ? "providers" : "back" };
23
+ }
24
+ // mdl_list_{provider}_{page}
25
+ const listMatch = trimmed.match(/^mdl_list_([a-z0-9_-]+)_(\d+)$/i);
26
+ if (listMatch) {
27
+ const [, provider, pageStr] = listMatch;
28
+ const page = Number.parseInt(pageStr ?? "1", 10);
29
+ if (provider && Number.isFinite(page) && page >= 1) {
30
+ return { type: "list", provider, page };
31
+ }
32
+ }
33
+ // mdl_sel_{provider/model}
34
+ const selMatch = trimmed.match(/^mdl_sel_(.+)$/);
35
+ if (selMatch) {
36
+ const modelRef = selMatch[1];
37
+ if (modelRef) {
38
+ const slashIndex = modelRef.indexOf("/");
39
+ if (slashIndex > 0 && slashIndex < modelRef.length - 1) {
40
+ return {
41
+ type: "select",
42
+ provider: modelRef.slice(0, slashIndex),
43
+ model: modelRef.slice(slashIndex + 1),
44
+ };
45
+ }
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * Build provider selection keyboard with 2 providers per row.
52
+ */
53
+ export function buildProviderKeyboard(providers) {
54
+ if (providers.length === 0) {
55
+ return [];
56
+ }
57
+ const rows = [];
58
+ let currentRow = [];
59
+ for (const provider of providers) {
60
+ const button = {
61
+ text: `${provider.id} (${provider.count})`,
62
+ callback_data: `mdl_list_${provider.id}_1`,
63
+ };
64
+ currentRow.push(button);
65
+ if (currentRow.length === 2) {
66
+ rows.push(currentRow);
67
+ currentRow = [];
68
+ }
69
+ }
70
+ // Push any remaining button
71
+ if (currentRow.length > 0) {
72
+ rows.push(currentRow);
73
+ }
74
+ return rows;
75
+ }
76
+ /**
77
+ * Build model list keyboard with pagination and back button.
78
+ */
79
+ export function buildModelsKeyboard(params) {
80
+ const { provider, models, currentModel, currentPage, totalPages } = params;
81
+ const pageSize = params.pageSize ?? MODELS_PAGE_SIZE;
82
+ if (models.length === 0) {
83
+ return [[{ text: "<< Back", callback_data: "mdl_back" }]];
84
+ }
85
+ const rows = [];
86
+ // Calculate page slice
87
+ const startIndex = (currentPage - 1) * pageSize;
88
+ const endIndex = Math.min(startIndex + pageSize, models.length);
89
+ const pageModels = models.slice(startIndex, endIndex);
90
+ // Model buttons - one per row
91
+ const currentModelId = currentModel?.includes("/")
92
+ ? currentModel.split("/").slice(1).join("/")
93
+ : currentModel;
94
+ for (const model of pageModels) {
95
+ const callbackData = `mdl_sel_${provider}/${model}`;
96
+ // Skip models that would exceed Telegram's callback_data limit
97
+ if (Buffer.byteLength(callbackData, "utf8") > MAX_CALLBACK_DATA_BYTES) {
98
+ continue;
99
+ }
100
+ const isCurrentModel = model === currentModelId;
101
+ const displayText = truncateModelId(model, 38);
102
+ const text = isCurrentModel ? `${displayText} ✓` : displayText;
103
+ rows.push([
104
+ {
105
+ text,
106
+ callback_data: callbackData,
107
+ },
108
+ ]);
109
+ }
110
+ // Pagination row
111
+ if (totalPages > 1) {
112
+ const paginationRow = [];
113
+ if (currentPage > 1) {
114
+ paginationRow.push({
115
+ text: "◀ Prev",
116
+ callback_data: `mdl_list_${provider}_${currentPage - 1}`,
117
+ });
118
+ }
119
+ paginationRow.push({
120
+ text: `${currentPage}/${totalPages}`,
121
+ callback_data: `mdl_list_${provider}_${currentPage}`, // noop
122
+ });
123
+ if (currentPage < totalPages) {
124
+ paginationRow.push({
125
+ text: "Next ▶",
126
+ callback_data: `mdl_list_${provider}_${currentPage + 1}`,
127
+ });
128
+ }
129
+ rows.push(paginationRow);
130
+ }
131
+ // Back button
132
+ rows.push([{ text: "<< Back", callback_data: "mdl_back" }]);
133
+ return rows;
134
+ }
135
+ /**
136
+ * Build "Browse providers" button for /model summary.
137
+ */
138
+ export function buildBrowseProvidersButton() {
139
+ return [[{ text: "Browse providers", callback_data: "mdl_prov" }]];
140
+ }
141
+ /**
142
+ * Truncate model ID for display, preserving end if too long.
143
+ */
144
+ function truncateModelId(modelId, maxLen) {
145
+ if (modelId.length <= maxLen) {
146
+ return modelId;
147
+ }
148
+ // Show last part with ellipsis prefix
149
+ return `…${modelId.slice(-(maxLen - 1))}`;
150
+ }
151
+ /**
152
+ * Get page size for model list pagination.
153
+ */
154
+ export function getModelsPageSize() {
155
+ return MODELS_PAGE_SIZE;
156
+ }
157
+ /**
158
+ * Calculate total pages for a model list.
159
+ */
160
+ export function calculateTotalPages(totalModels, pageSize) {
161
+ const size = pageSize ?? MODELS_PAGE_SIZE;
162
+ return size > 0 ? Math.ceil(totalModels / size) : 1;
163
+ }