@poolzin/pool-bot 2026.2.0 → 2026.2.2

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -1,22 +1,67 @@
1
1
  import { formatLocationText } from "../../channels/location.js";
2
2
  const TELEGRAM_GENERAL_TOPIC_ID = 1;
3
+ /**
4
+ * Resolve the thread ID for Telegram forum topics.
5
+ * For non-forum groups, returns undefined even if messageThreadId is present
6
+ * (reply threads in regular groups should not create separate sessions).
7
+ * For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
8
+ */
3
9
  export function resolveTelegramForumThreadId(params) {
4
- if (params.isForum && params.messageThreadId == null) {
10
+ // Non-forum groups: ignore message_thread_id (reply threads are not real topics)
11
+ if (!params.isForum) {
12
+ return undefined;
13
+ }
14
+ // Forum groups: use the topic ID, defaulting to General topic
15
+ if (params.messageThreadId == null) {
5
16
  return TELEGRAM_GENERAL_TOPIC_ID;
6
17
  }
7
- return params.messageThreadId ?? undefined;
18
+ return params.messageThreadId;
19
+ }
20
+ export function resolveTelegramThreadSpec(params) {
21
+ if (params.isGroup) {
22
+ const id = resolveTelegramForumThreadId({
23
+ isForum: params.isForum,
24
+ messageThreadId: params.messageThreadId,
25
+ });
26
+ return {
27
+ id,
28
+ scope: params.isForum ? "forum" : "none",
29
+ };
30
+ }
31
+ if (params.messageThreadId == null) {
32
+ return { scope: "dm" };
33
+ }
34
+ return {
35
+ id: params.messageThreadId,
36
+ scope: "dm",
37
+ };
8
38
  }
9
39
  /**
10
40
  * Build thread params for Telegram API calls (messages, media).
11
41
  * General forum topic (id=1) must be treated like a regular supergroup send:
12
42
  * Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
43
+ *
44
+ * Accepts either a TelegramThreadSpec (preferred) or a raw messageThreadId number
45
+ * for backward compatibility with callers that haven't migrated yet.
13
46
  */
14
- export function buildTelegramThreadParams(messageThreadId) {
15
- if (messageThreadId == null) {
47
+ export function buildTelegramThreadParams(threadOrId) {
48
+ if (threadOrId == null) {
49
+ return undefined;
50
+ }
51
+ // Legacy call: raw number
52
+ if (typeof threadOrId === "number") {
53
+ const normalized = Math.trunc(threadOrId);
54
+ if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
55
+ return undefined;
56
+ }
57
+ return { message_thread_id: normalized };
58
+ }
59
+ // New call: TelegramThreadSpec
60
+ if (!threadOrId.id) {
16
61
  return undefined;
17
62
  }
18
- const normalized = Math.trunc(messageThreadId);
19
- if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
63
+ const normalized = Math.trunc(threadOrId.id);
64
+ if (normalized === TELEGRAM_GENERAL_TOPIC_ID && threadOrId.scope === "forum") {
20
65
  return undefined;
21
66
  }
22
67
  return { message_thread_id: normalized };
@@ -43,6 +88,19 @@ export function buildTelegramGroupPeerId(chatId, messageThreadId) {
43
88
  export function buildTelegramGroupFrom(chatId, messageThreadId) {
44
89
  return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
45
90
  }
91
+ /**
92
+ * Build parentPeer for forum topic binding inheritance.
93
+ * When a message comes from a forum topic, the peer ID includes the topic suffix
94
+ * (e.g., `-1001234567890:topic:99`). To allow bindings configured for the base
95
+ * group ID to match, we provide the parent group as `parentPeer` so the routing
96
+ * layer can fall back to it when the exact peer doesn't match.
97
+ */
98
+ export function buildTelegramParentPeer(params) {
99
+ if (!params.isGroup || params.resolvedThreadId == null) {
100
+ return undefined;
101
+ }
102
+ return { kind: "group", id: String(params.chatId) };
103
+ }
46
104
  export function buildSenderName(msg) {
47
105
  const name = [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() ||
48
106
  msg.from?.username;
@@ -93,7 +151,7 @@ export function expandTextLinks(text, entities) {
93
151
  return text;
94
152
  const textLinks = entities
95
153
  .filter((entity) => entity.type === "text_link" && Boolean(entity.url))
96
- .sort((a, b) => b.offset - a.offset);
154
+ .toSorted((a, b) => b.offset - a.offset);
97
155
  if (textLinks.length === 0)
98
156
  return text;
99
157
  let result = text;
@@ -115,33 +173,53 @@ export function resolveTelegramReplyId(raw) {
115
173
  }
116
174
  export function describeReplyTarget(msg) {
117
175
  const reply = msg.reply_to_message;
118
- if (!reply)
119
- return null;
120
- const replyBody = (reply.text ?? reply.caption ?? "").trim();
121
- let body = replyBody;
122
- if (!body) {
123
- if (reply.photo)
124
- body = "<media:image>";
125
- else if (reply.video)
126
- body = "<media:video>";
127
- else if (reply.audio || reply.voice)
128
- body = "<media:audio>";
129
- else if (reply.document)
130
- body = "<media:document>";
131
- else {
132
- const locationData = extractTelegramLocation(reply);
133
- if (locationData)
134
- body = formatLocationText(locationData);
176
+ const externalReply = msg
177
+ .external_reply;
178
+ const quoteText = msg.quote?.text ??
179
+ externalReply?.quote?.text;
180
+ let body = "";
181
+ let kind = "reply";
182
+ if (typeof quoteText === "string") {
183
+ body = quoteText.trim();
184
+ if (body) {
185
+ kind = "quote";
135
186
  }
136
187
  }
137
- if (!body)
188
+ const replyLike = reply ?? externalReply;
189
+ if (!body && replyLike) {
190
+ const replyBody = (replyLike.text ?? replyLike.caption ?? "").trim();
191
+ body = replyBody;
192
+ if (!body) {
193
+ if (replyLike.photo) {
194
+ body = "<media:image>";
195
+ }
196
+ else if (replyLike.video) {
197
+ body = "<media:video>";
198
+ }
199
+ else if (replyLike.audio || replyLike.voice) {
200
+ body = "<media:audio>";
201
+ }
202
+ else if (replyLike.document) {
203
+ body = "<media:document>";
204
+ }
205
+ else {
206
+ const locationData = extractTelegramLocation(replyLike);
207
+ if (locationData) {
208
+ body = formatLocationText(locationData);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ if (!body) {
138
214
  return null;
139
- const sender = buildSenderName(reply);
140
- const senderLabel = sender ? `${sender}` : "unknown sender";
215
+ }
216
+ const sender = replyLike ? buildSenderName(replyLike) : undefined;
217
+ const senderLabel = sender ?? "unknown sender";
141
218
  return {
142
- id: reply.message_id ? String(reply.message_id) : undefined,
219
+ id: replyLike?.message_id ? String(replyLike.message_id) : undefined,
143
220
  sender: senderLabel,
144
221
  body,
222
+ kind,
145
223
  };
146
224
  }
147
225
  function normalizeForwardedUserLabel(user) {
@@ -191,6 +269,7 @@ function buildForwardedContextFromChat(params) {
191
269
  return null;
192
270
  const signature = params.signature?.trim() || undefined;
193
271
  const from = signature ? `${display} (${signature})` : display;
272
+ const chatType = (params.chat.type?.trim() || undefined);
194
273
  return {
195
274
  from,
196
275
  date: params.date,
@@ -199,6 +278,8 @@ function buildForwardedContextFromChat(params) {
199
278
  fromUsername: username,
200
279
  fromTitle: title,
201
280
  fromSignature: signature,
281
+ fromChatType: chatType,
282
+ fromMessageId: params.messageId,
202
283
  };
203
284
  }
204
285
  function resolveForwardOrigin(origin, signature) {
@@ -2,21 +2,27 @@
2
2
  import { hasControlCommand } from "../auto-reply/command-detection.js";
3
3
  import { createInboundDebouncer, resolveInboundDebounceMs, } from "../auto-reply/inbound-debounce.js";
4
4
  import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js";
5
+ import { buildModelsProviderData } from "../auto-reply/reply/commands-models.js";
6
+ import { resolveStoredModelOverride } from "../auto-reply/reply/model-selection.js";
5
7
  import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
6
8
  import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
7
9
  import { resolveDefaultAgentId } from "../agents/agent-scope.js";
8
10
  import { loadConfig } from "../config/config.js";
11
+ import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
12
+ import { resolveAgentRoute } from "../routing/resolve-route.js";
13
+ import { resolveThreadSessionKeys } from "../routing/session-key.js";
9
14
  import { writeConfigFile } from "../config/io.js";
10
15
  import { danger, logVerbose, warn } from "../globals.js";
11
16
  import { resolveMedia } from "./bot/delivery.js";
12
17
  import { withTelegramApiErrorLogging } from "./api-logging.js";
13
- import { resolveTelegramForumThreadId } from "./bot/helpers.js";
18
+ import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, } from "./bot/helpers.js";
14
19
  import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
15
20
  import { MEDIA_GROUP_TIMEOUT_MS } from "./bot-updates.js";
16
21
  import { migrateTelegramGroupConfig } from "./group-migration.js";
17
22
  import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
18
23
  import { readTelegramAllowFromStore } from "./pairing-store.js";
19
24
  import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
25
+ import { buildModelsKeyboard, buildProviderKeyboard, calculateTotalPages, getModelsPageSize, parseModelCallbackData, } from "./model-buttons.js";
20
26
  import { buildInlineKeyboard } from "./send.js";
21
27
  export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, mediaMaxBytes, telegramCfg, groupAllowFrom, resolveGroupPolicy, resolveTelegramGroupConfig, shouldSkipUpdate, processMessage, logger, }) => {
22
28
  const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000;
@@ -72,6 +78,57 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
72
78
  runtime.error?.(danger(`telegram debounce flush failed: ${String(err)}`));
73
79
  },
74
80
  });
81
+ const resolveTelegramSessionModel = (params) => {
82
+ const resolvedThreadId = params.resolvedThreadId ??
83
+ resolveTelegramForumThreadId({
84
+ isForum: params.isForum,
85
+ messageThreadId: params.messageThreadId,
86
+ });
87
+ const peerId = params.isGroup
88
+ ? buildTelegramGroupPeerId(params.chatId, resolvedThreadId)
89
+ : String(params.chatId);
90
+ const parentPeer = buildTelegramParentPeer({
91
+ isGroup: params.isGroup,
92
+ resolvedThreadId,
93
+ chatId: params.chatId,
94
+ });
95
+ const route = resolveAgentRoute({
96
+ cfg,
97
+ channel: "telegram",
98
+ accountId,
99
+ peer: {
100
+ kind: params.isGroup ? "group" : "dm",
101
+ id: peerId,
102
+ },
103
+ parentPeer,
104
+ });
105
+ const baseSessionKey = route.sessionKey;
106
+ const dmThreadId = !params.isGroup ? params.messageThreadId : undefined;
107
+ const threadKeys = dmThreadId != null
108
+ ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
109
+ : null;
110
+ const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
111
+ const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });
112
+ const store = loadSessionStore(storePath);
113
+ const entry = store[sessionKey];
114
+ const storedOverride = resolveStoredModelOverride({
115
+ sessionEntry: entry,
116
+ sessionStore: store,
117
+ sessionKey,
118
+ });
119
+ if (storedOverride) {
120
+ return storedOverride.provider
121
+ ? `${storedOverride.provider}/${storedOverride.model}`
122
+ : storedOverride.model;
123
+ }
124
+ const provider = entry?.modelProvider?.trim();
125
+ const model = entry?.model?.trim();
126
+ if (provider && model) {
127
+ return `${provider}/${model}`;
128
+ }
129
+ const modelCfg = cfg.agents?.defaults?.model;
130
+ return typeof modelCfg === "string" ? modelCfg : modelCfg?.primary;
131
+ };
75
132
  const processMediaGroup = async (entry) => {
76
133
  try {
77
134
  entry.messages.sort((a, b) => a.msg.message_id - b.msg.message_id);
@@ -205,7 +262,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
205
262
  }
206
263
  }
207
264
  const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
208
- const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
265
+ const groupPolicy = firstDefined(topicConfig?.groupPolicy, groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
209
266
  if (groupPolicy === "disabled") {
210
267
  logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
211
268
  return;
@@ -293,6 +350,90 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
293
350
  }
294
351
  return;
295
352
  }
353
+ // Model selection callback handler (mdl_prov, mdl_list_*, mdl_sel_*, mdl_back)
354
+ const modelCallback = parseModelCallbackData(data);
355
+ if (modelCallback) {
356
+ const modelData = await buildModelsProviderData(cfg);
357
+ const { byProvider, providers } = modelData;
358
+ const editMessageWithButtons = async (text, buttons) => {
359
+ const keyboard = buildInlineKeyboard(buttons);
360
+ try {
361
+ await bot.api.editMessageText(callbackMessage.chat.id, callbackMessage.message_id, text, keyboard ? { reply_markup: keyboard } : undefined);
362
+ }
363
+ catch (editErr) {
364
+ const errStr = String(editErr);
365
+ if (!errStr.includes("message is not modified")) {
366
+ throw editErr;
367
+ }
368
+ }
369
+ };
370
+ if (modelCallback.type === "providers" || modelCallback.type === "back") {
371
+ if (providers.length === 0) {
372
+ await editMessageWithButtons("No providers available.", []);
373
+ return;
374
+ }
375
+ const providerInfos = providers.map((p) => ({
376
+ id: p,
377
+ count: byProvider.get(p)?.size ?? 0,
378
+ }));
379
+ const buttons = buildProviderKeyboard(providerInfos);
380
+ await editMessageWithButtons("Select a provider:", buttons);
381
+ return;
382
+ }
383
+ if (modelCallback.type === "list") {
384
+ const { provider, page } = modelCallback;
385
+ const modelSet = byProvider.get(provider);
386
+ if (!modelSet || modelSet.size === 0) {
387
+ const providerInfos = providers.map((p) => ({
388
+ id: p,
389
+ count: byProvider.get(p)?.size ?? 0,
390
+ }));
391
+ const buttons = buildProviderKeyboard(providerInfos);
392
+ await editMessageWithButtons(`Unknown provider: ${provider}\n\nSelect a provider:`, buttons);
393
+ return;
394
+ }
395
+ const models = [...modelSet].toSorted();
396
+ const pageSize = getModelsPageSize();
397
+ const totalPages = calculateTotalPages(models.length, pageSize);
398
+ const safePage = Math.max(1, Math.min(page, totalPages));
399
+ const currentModel = resolveTelegramSessionModel({
400
+ chatId,
401
+ isGroup,
402
+ isForum,
403
+ messageThreadId,
404
+ resolvedThreadId,
405
+ });
406
+ const buttons = buildModelsKeyboard({
407
+ provider,
408
+ models,
409
+ currentModel,
410
+ currentPage: safePage,
411
+ totalPages,
412
+ pageSize,
413
+ });
414
+ const text = `Models (${provider}) — ${models.length} available`;
415
+ await editMessageWithButtons(text, buttons);
416
+ return;
417
+ }
418
+ if (modelCallback.type === "select") {
419
+ const { provider, model } = modelCallback;
420
+ const syntheticMessage = {
421
+ ...callbackMessage,
422
+ from: callback.from,
423
+ text: `/model ${provider}/${model}`,
424
+ caption: undefined,
425
+ caption_entities: undefined,
426
+ entities: undefined,
427
+ };
428
+ const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
429
+ await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
430
+ forceWasMentioned: true,
431
+ messageIdOverride: callback.id,
432
+ });
433
+ return;
434
+ }
435
+ return;
436
+ }
296
437
  const syntheticMessage = {
297
438
  ...callbackMessage,
298
439
  from: callback.from,
@@ -403,7 +544,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
403
544
  // - "disabled": block all group messages entirely
404
545
  // - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
405
546
  const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
406
- const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
547
+ const groupPolicy = firstDefined(topicConfig?.groupPolicy, groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
407
548
  if (groupPolicy === "disabled") {
408
549
  logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
409
550
  return;
@@ -11,6 +11,7 @@ import { formatLocationText, toLocationContext } from "../channels/location.js";
11
11
  import { recordInboundSession } from "../channels/session.js";
12
12
  import { formatCliCommand } from "../cli/command-format.js";
13
13
  import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js";
14
+ import { loadConfig } from "../config/config.js";
14
15
  import { logVerbose, shouldLogVerbose } from "../globals.js";
15
16
  import { recordChannelActivity } from "../infra/channel-activity.js";
16
17
  import { resolveAgentRoute } from "../routing/resolve-route.js";
@@ -20,7 +21,7 @@ import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
20
21
  import { resolveControlCommandGate } from "../channels/command-gating.js";
21
22
  import { logInboundDrop } from "../channels/logging.js";
22
23
  import { withTelegramApiErrorLogging } from "./api-logging.js";
23
- import { buildGroupLabel, buildSenderLabel, buildSenderName, buildTelegramGroupFrom, buildTelegramGroupPeerId, buildTypingThreadParams, expandTextLinks, normalizeForwardedContext, describeReplyTarget, extractTelegramLocation, hasBotMention, resolveTelegramForumThreadId, } from "./bot/helpers.js";
24
+ import { buildGroupLabel, buildSenderLabel, buildSenderName, buildTelegramGroupFrom, buildTelegramGroupPeerId, buildTelegramParentPeer, buildTypingThreadParams, expandTextLinks, normalizeForwardedContext, describeReplyTarget, extractTelegramLocation, hasBotMention, resolveTelegramThreadSpec, } from "./bot/helpers.js";
24
25
  import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore, resolveSenderAllowMatch, } from "./bot-access.js";
25
26
  import { upsertTelegramPairingRequest } from "./pairing-store.js";
26
27
  async function resolveStickerVisionSupport(params) {
@@ -50,23 +51,28 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
50
51
  const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
51
52
  const messageThreadId = msg.message_thread_id;
52
53
  const isForum = msg.chat.is_forum === true;
53
- const resolvedThreadId = resolveTelegramForumThreadId({
54
+ const threadSpec = resolveTelegramThreadSpec({
55
+ isGroup,
54
56
  isForum,
55
57
  messageThreadId,
56
58
  });
59
+ const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : undefined;
60
+ const replyThreadId = threadSpec.id;
57
61
  const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
58
62
  const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
63
+ const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
59
64
  const route = resolveAgentRoute({
60
- cfg,
65
+ cfg: loadConfig(),
61
66
  channel: "telegram",
62
67
  accountId: account.accountId,
63
68
  peer: {
64
69
  kind: isGroup ? "group" : "dm",
65
70
  id: peerId,
66
71
  },
72
+ parentPeer,
67
73
  });
68
74
  const baseSessionKey = route.sessionKey;
69
- const dmThreadId = !isGroup ? resolvedThreadId : undefined;
75
+ const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
70
76
  const threadKeys = dmThreadId != null
71
77
  ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
72
78
  : null;
@@ -90,14 +96,14 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
90
96
  const sendTyping = async () => {
91
97
  await withTelegramApiErrorLogging({
92
98
  operation: "sendChatAction",
93
- fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId)),
99
+ fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(replyThreadId)),
94
100
  });
95
101
  };
96
102
  const sendRecordVoice = async () => {
97
103
  try {
98
104
  await withTelegramApiErrorLogging({
99
105
  operation: "sendChatAction",
100
- fn: () => bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(resolvedThreadId)),
106
+ fn: () => bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(replyThreadId)),
101
107
  });
102
108
  }
103
109
  catch (err) {
@@ -109,7 +115,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
109
115
  if (dmPolicy === "disabled")
110
116
  return null;
111
117
  if (dmPolicy !== "open") {
112
- const candidate = String(chatId);
118
+ const senderUserId = msg.from?.id != null ? String(msg.from.id) : null;
119
+ const candidate = senderUserId ?? String(chatId);
113
120
  const senderUsername = msg.from?.username ?? "";
114
121
  const allowMatch = resolveSenderAllowMatch({
115
122
  allow: effectiveDmAllow,
@@ -131,7 +138,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
131
138
  });
132
139
  if (created) {
133
140
  logger.info({
134
- chatId: candidate,
141
+ chatId: String(chatId),
142
+ senderUserId: senderUserId ?? undefined,
135
143
  username: from?.username,
136
144
  firstName: from?.first_name,
137
145
  lastName: from?.last_name,
@@ -201,6 +209,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
201
209
  placeholder = "<media:image>";
202
210
  else if (msg.video)
203
211
  placeholder = "<media:video>";
212
+ else if (msg.video_note)
213
+ placeholder = "<media:video>";
204
214
  else if (msg.audio || msg.voice)
205
215
  placeholder = "<media:audio>";
206
216
  else if (msg.document)
@@ -326,7 +336,9 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
326
336
  const replyTarget = describeReplyTarget(msg);
327
337
  const forwardOrigin = normalizeForwardedContext(msg);
328
338
  const replySuffix = replyTarget
329
- ? `\n\n[Replying to ${replyTarget.sender}${replyTarget.id ? ` id:${replyTarget.id}` : ""}]\n${replyTarget.body}\n[/Replying]`
339
+ ? replyTarget.kind === "quote"
340
+ ? `\n\n[Quoting ${replyTarget.sender}${replyTarget.id ? ` id:${replyTarget.id}` : ""}]\n"${replyTarget.body}"\n[/Quoting]`
341
+ : `\n\n[Replying to ${replyTarget.sender}${replyTarget.id ? ` id:${replyTarget.id}` : ""}]\n${replyTarget.body}\n[/Replying]`
330
342
  : "";
331
343
  const forwardPrefix = forwardOrigin
332
344
  ? `[Forwarded from ${forwardOrigin.from}${forwardOrigin.date ? ` at ${new Date(forwardOrigin.date * 1000).toISOString()}` : ""}]\n`
@@ -383,9 +395,17 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
383
395
  ].filter((entry) => Boolean(entry));
384
396
  const groupSystemPrompt = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
385
397
  const commandBody = normalizeCommandBody(rawBody, { botUsername });
398
+ const inboundHistory = isGroup && historyKey && historyLimit > 0
399
+ ? (groupHistories.get(historyKey) ?? []).map((entry) => ({
400
+ sender: entry.sender,
401
+ body: entry.body,
402
+ timestamp: entry.timestamp,
403
+ }))
404
+ : undefined;
386
405
  const ctxPayload = finalizeInboundContext({
387
406
  Body: combinedBody,
388
407
  RawBody: rawBody,
408
+ BodyForAgent: bodyText,
389
409
  CommandBody: commandBody,
390
410
  From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
391
411
  To: `telegram:${chatId}`,
@@ -404,12 +424,15 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
404
424
  ReplyToId: replyTarget?.id,
405
425
  ReplyToBody: replyTarget?.body,
406
426
  ReplyToSender: replyTarget?.sender,
427
+ ReplyToIsQuote: replyTarget?.kind === "quote" ? true : undefined,
407
428
  ForwardedFrom: forwardOrigin?.from,
408
429
  ForwardedFromType: forwardOrigin?.fromType,
409
430
  ForwardedFromId: forwardOrigin?.fromId,
410
431
  ForwardedFromUsername: forwardOrigin?.fromUsername,
411
432
  ForwardedFromTitle: forwardOrigin?.fromTitle,
412
433
  ForwardedFromSignature: forwardOrigin?.fromSignature,
434
+ ForwardedFromChatType: forwardOrigin?.fromChatType,
435
+ ForwardedFromMessageId: forwardOrigin?.fromMessageId,
413
436
  ForwardedDate: forwardOrigin?.date ? forwardOrigin.date * 1000 : undefined,
414
437
  Timestamp: msg.date ? msg.date * 1000 : undefined,
415
438
  WasMentioned: isGroup ? effectiveWasMentioned : undefined,
@@ -435,7 +458,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
435
458
  Sticker: allMedia[0]?.stickerMetadata,
436
459
  ...(locationData ? toLocationContext(locationData) : undefined),
437
460
  CommandAuthorized: commandAuthorized,
438
- MessageThreadId: resolvedThreadId,
461
+ InboundHistory: inboundHistory,
462
+ MessageThreadId: threadSpec.id,
439
463
  IsForum: isForum,
440
464
  // Originating channel for reply routing.
441
465
  OriginatingChannel: "telegram",
@@ -451,6 +475,7 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
451
475
  channel: "telegram",
452
476
  to: String(chatId),
453
477
  accountId: route.accountId,
478
+ threadId: dmThreadId != null ? String(dmThreadId) : undefined,
454
479
  }
455
480
  : undefined,
456
481
  onRecordError: (err) => {
@@ -477,6 +502,8 @@ export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, storeA
477
502
  chatId,
478
503
  isGroup,
479
504
  resolvedThreadId,
505
+ threadSpec,
506
+ replyThreadId,
480
507
  isForum,
481
508
  historyKey,
482
509
  historyLimit,
@@ -1,7 +1,7 @@
1
- // @ts-nocheck
2
- import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
1
+ import { resolveAgentDir } from "../agents/agent-scope.js";
3
2
  import { findModelInCatalog, loadModelCatalog, modelSupportsVision, } from "../agents/model-catalog.js";
4
3
  import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
4
+ import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
5
5
  import { resolveChunkMode } from "../auto-reply/chunk.js";
6
6
  import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
7
7
  import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
@@ -9,13 +9,13 @@ import { removeAckReactionAfterReply } from "../channels/ack-reactions.js";
9
9
  import { logAckFailure, logTypingFailure } from "../channels/logging.js";
10
10
  import { createReplyPrefixContext } from "../channels/reply-prefix.js";
11
11
  import { createTypingCallbacks } from "../channels/typing.js";
12
- import { danger, logVerbose } from "../globals.js";
13
12
  import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
13
+ import { danger, logVerbose } from "../globals.js";
14
14
  import { deliverReplies } from "./bot/delivery.js";
15
15
  import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
16
16
  import { createTelegramDraftStream } from "./draft-stream.js";
17
17
  import { cacheSticker, describeStickerImage } from "./sticker-cache.js";
18
- import { resolveAgentDir } from "../agents/agent-scope.js";
18
+ const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
19
19
  async function resolveStickerVisionSupport(cfg, agentId) {
20
20
  try {
21
21
  const catalog = await loadModelCatalog({ config: cfg });
@@ -118,7 +118,7 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
118
118
  // Handle uncached stickers: get a dedicated vision description before dispatch
119
119
  // This ensures we cache a raw description rather than a conversational response
120
120
  const sticker = ctxPayload.Sticker;
121
- if (sticker?.fileUniqueId && ctxPayload.MediaPath) {
121
+ if (sticker?.fileId && sticker.fileUniqueId && ctxPayload.MediaPath) {
122
122
  const agentDir = resolveAgentDir(cfg, route.agentId);
123
123
  const stickerSupportsVision = await resolveStickerVisionSupport(cfg, route.agentId);
124
124
  let description = sticker.cachedDescription ?? null;
@@ -150,19 +150,32 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
150
150
  ctxPayload.MediaTypes = undefined;
151
151
  }
152
152
  // Cache the description for future encounters
153
- cacheSticker({
154
- fileId: sticker.fileId,
155
- fileUniqueId: sticker.fileUniqueId,
156
- emoji: sticker.emoji,
157
- setName: sticker.setName,
158
- description,
159
- cachedAt: new Date().toISOString(),
160
- receivedFrom: ctxPayload.From,
161
- });
162
- logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`);
153
+ if (sticker.fileId) {
154
+ cacheSticker({
155
+ fileId: sticker.fileId,
156
+ fileUniqueId: sticker.fileUniqueId,
157
+ emoji: sticker.emoji,
158
+ setName: sticker.setName,
159
+ description,
160
+ cachedAt: new Date().toISOString(),
161
+ receivedFrom: ctxPayload.From,
162
+ });
163
+ logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`);
164
+ }
165
+ else {
166
+ logVerbose(`telegram: skipped sticker cache (missing fileId)`);
167
+ }
163
168
  }
164
169
  }
165
- const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
170
+ // TODO: pass replyQuoteText to deliverReplies once delivery.ts supports it
171
+ const _replyQuoteText = ctxPayload.ReplyToIsQuote && ctxPayload.ReplyToBody
172
+ ? ctxPayload.ReplyToBody.trim() || undefined
173
+ : undefined;
174
+ void _replyQuoteText;
175
+ const deliveryState = {
176
+ delivered: false,
177
+ };
178
+ const { queuedFinal, counts } = await dispatchReplyWithBufferedBlockDispatcher({
166
179
  ctx: ctxPayload,
167
180
  cfg,
168
181
  dispatcherOptions: {
@@ -187,6 +200,7 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
187
200
  onVoiceRecording: sendRecordVoice,
188
201
  linkPreview: telegramCfg.linkPreview,
189
202
  });
203
+ deliveryState.delivered = true;
190
204
  },
191
205
  onError: (err, info) => {
192
206
  runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`));
@@ -219,7 +233,30 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
219
233
  },
220
234
  });
221
235
  draftStream?.stop();
222
- if (!queuedFinal) {
236
+ let sentFallback = false;
237
+ if (!deliveryState.delivered && (counts.tool + counts.block + counts.final) > 0) {
238
+ try {
239
+ await deliverReplies({
240
+ replies: [{ text: EMPTY_RESPONSE_FALLBACK }],
241
+ chatId: String(chatId),
242
+ token: opts.token,
243
+ runtime,
244
+ bot,
245
+ replyToMode,
246
+ textLimit,
247
+ messageThreadId: resolvedThreadId,
248
+ tableMode,
249
+ chunkMode,
250
+ linkPreview: telegramCfg.linkPreview,
251
+ });
252
+ sentFallback = true;
253
+ }
254
+ catch {
255
+ // Fallback delivery failed; proceed without it
256
+ }
257
+ }
258
+ const hasFinalResponse = queuedFinal || sentFallback;
259
+ if (!hasFinalResponse) {
223
260
  if (isGroup && historyKey) {
224
261
  clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
225
262
  }