@poolzin/pool-bot 2026.2.23 → 2026.2.25

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 (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -10,11 +10,14 @@ import { logAckFailure, logTypingFailure } from "../channels/logging.js";
10
10
  import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
11
11
  import { createTypingCallbacks } from "../channels/typing.js";
12
12
  import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
13
+ import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
13
14
  import { danger, logVerbose } from "../globals.js";
14
15
  import { getAgentScopedMediaLocalRoots } from "../media/local-roots.js";
15
16
  import { deliverReplies } from "./bot/delivery.js";
16
17
  import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
17
18
  import { createTelegramDraftStream } from "./draft-stream.js";
19
+ import { renderTelegramHtmlText } from "./format.js";
20
+ import { createTelegramReasoningStepState, splitTelegramReasoningText, } from "./reasoning-lane-coordinator.js";
18
21
  import { editMessageTelegram } from "./send.js";
19
22
  import { cacheSticker, describeStickerImage } from "./sticker-cache.js";
20
23
  const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
@@ -34,118 +37,194 @@ async function resolveStickerVisionSupport(cfg, agentId) {
34
37
  return false;
35
38
  }
36
39
  }
40
+ function resolveTelegramReasoningLevel(params) {
41
+ const { cfg, sessionKey, agentId } = params;
42
+ if (!sessionKey) {
43
+ return "off";
44
+ }
45
+ try {
46
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
47
+ const store = loadSessionStore(storePath, { skipCache: true });
48
+ const entry = store[sessionKey.toLowerCase()] ?? store[sessionKey];
49
+ const level = entry?.reasoningLevel;
50
+ if (level === "on" || level === "stream") {
51
+ return level;
52
+ }
53
+ }
54
+ catch {
55
+ // Fall through to default.
56
+ }
57
+ return "off";
58
+ }
37
59
  export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, replyToMode, streamMode, textLimit, telegramCfg, opts, }) => {
38
60
  const { ctxPayload, msg, chatId, isGroup, threadSpec, historyKey, historyLimit, groupHistories, route, skillFilter, sendTyping, sendRecordVoice, ackReactionPromise, reactionApi, removeAckAfterReply, } = context;
39
61
  const draftMaxChars = Math.min(textLimit, 4096);
62
+ const tableMode = resolveMarkdownTableMode({
63
+ cfg,
64
+ channel: "telegram",
65
+ accountId: route.accountId,
66
+ });
67
+ const renderDraftPreview = (text) => ({
68
+ text: renderTelegramHtmlText(text, { tableMode }),
69
+ parseMode: "HTML",
70
+ });
40
71
  const accountBlockStreamingEnabled = typeof telegramCfg.blockStreaming === "boolean"
41
72
  ? telegramCfg.blockStreaming
42
73
  : cfg.agents?.defaults?.blockStreamingDefault === "on";
43
- const canStreamDraft = streamMode !== "off" && !accountBlockStreamingEnabled;
74
+ const resolvedReasoningLevel = resolveTelegramReasoningLevel({
75
+ cfg,
76
+ sessionKey: ctxPayload.SessionKey,
77
+ agentId: route.agentId,
78
+ });
79
+ const forceBlockStreamingForReasoning = resolvedReasoningLevel === "on";
80
+ const streamReasoningDraft = resolvedReasoningLevel === "stream";
81
+ const canStreamAnswerDraft = streamMode !== "off" && !accountBlockStreamingEnabled && !forceBlockStreamingForReasoning;
82
+ const canStreamReasoningDraft = canStreamAnswerDraft || streamReasoningDraft;
44
83
  const draftReplyToMessageId = replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined;
45
- const draftStream = canStreamDraft
46
- ? createTelegramDraftStream({
47
- api: bot.api,
48
- chatId,
49
- maxChars: draftMaxChars,
50
- thread: threadSpec,
51
- replyToMessageId: draftReplyToMessageId,
52
- minInitialChars: DRAFT_MIN_INITIAL_CHARS,
53
- log: logVerbose,
54
- warn: logVerbose,
55
- })
56
- : undefined;
57
- const draftChunking = draftStream && streamMode === "block"
58
- ? resolveTelegramDraftStreamingChunking(cfg, route.accountId)
59
- : undefined;
60
- const shouldSplitPreviewMessages = streamMode === "block";
61
- const draftChunker = draftChunking ? new EmbeddedBlockChunker(draftChunking) : undefined;
84
+ const draftMinInitialChars = streamMode === "partial" || streamReasoningDraft ? 1 : DRAFT_MIN_INITIAL_CHARS;
62
85
  const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
63
- let lastPartialText = "";
64
- let draftText = "";
65
- let hasStreamedMessage = false;
66
- const updateDraftFromPartial = (text) => {
67
- if (!draftStream || !text) {
86
+ const createDraftLane = (enabled) => {
87
+ const stream = enabled
88
+ ? createTelegramDraftStream({
89
+ api: bot.api,
90
+ chatId,
91
+ maxChars: draftMaxChars,
92
+ thread: threadSpec,
93
+ replyToMessageId: draftReplyToMessageId,
94
+ minInitialChars: draftMinInitialChars,
95
+ renderText: renderDraftPreview,
96
+ log: logVerbose,
97
+ warn: logVerbose,
98
+ })
99
+ : undefined;
100
+ const chunker = stream && streamMode === "block"
101
+ ? new EmbeddedBlockChunker(resolveTelegramDraftStreamingChunking(cfg, route.accountId))
102
+ : undefined;
103
+ return {
104
+ stream,
105
+ lastPartialText: "",
106
+ draftText: "",
107
+ hasStreamedMessage: false,
108
+ chunker,
109
+ };
110
+ };
111
+ const lanes = {
112
+ answer: createDraftLane(canStreamAnswerDraft),
113
+ reasoning: createDraftLane(canStreamReasoningDraft),
114
+ };
115
+ const answerLane = lanes.answer;
116
+ const reasoningLane = lanes.reasoning;
117
+ let splitReasoningOnNextStream = false;
118
+ const reasoningStepState = createTelegramReasoningStepState();
119
+ const splitTextIntoLaneSegments = (text) => {
120
+ const split = splitTelegramReasoningText(text);
121
+ const segments = [];
122
+ if (split.reasoningText) {
123
+ segments.push({ lane: "reasoning", text: split.reasoningText });
124
+ }
125
+ if (split.answerText) {
126
+ segments.push({ lane: "answer", text: split.answerText });
127
+ }
128
+ return segments;
129
+ };
130
+ const resetDraftLaneState = (lane) => {
131
+ lane.lastPartialText = "";
132
+ lane.draftText = "";
133
+ lane.hasStreamedMessage = false;
134
+ lane.chunker?.reset();
135
+ };
136
+ const updateDraftFromPartial = (lane, text) => {
137
+ const laneStream = lane.stream;
138
+ if (!laneStream || !text) {
68
139
  return;
69
140
  }
70
- if (text === lastPartialText) {
141
+ if (text === lane.lastPartialText) {
71
142
  return;
72
143
  }
73
144
  // Mark that we've received streaming content (for forceNewMessage decision).
74
- hasStreamedMessage = true;
145
+ lane.hasStreamedMessage = true;
75
146
  if (streamMode === "partial") {
76
147
  // Some providers briefly emit a shorter prefix snapshot (for example
77
148
  // "Sure." -> "Sure" -> "Sure."). Keep the longer preview to avoid
78
149
  // visible punctuation flicker.
79
- if (lastPartialText &&
80
- lastPartialText.startsWith(text) &&
81
- text.length < lastPartialText.length) {
150
+ if (lane.lastPartialText &&
151
+ lane.lastPartialText.startsWith(text) &&
152
+ text.length < lane.lastPartialText.length) {
82
153
  return;
83
154
  }
84
- lastPartialText = text;
85
- draftStream.update(text);
155
+ lane.lastPartialText = text;
156
+ laneStream.update(text);
86
157
  return;
87
158
  }
88
159
  let delta = text;
89
- if (text.startsWith(lastPartialText)) {
90
- delta = text.slice(lastPartialText.length);
160
+ if (text.startsWith(lane.lastPartialText)) {
161
+ delta = text.slice(lane.lastPartialText.length);
91
162
  }
92
163
  else {
93
164
  // Streaming buffer reset (or non-monotonic stream). Start fresh.
94
- draftChunker?.reset();
95
- draftText = "";
165
+ lane.chunker?.reset();
166
+ lane.draftText = "";
96
167
  }
97
- lastPartialText = text;
168
+ lane.lastPartialText = text;
98
169
  if (!delta) {
99
170
  return;
100
171
  }
101
- if (!draftChunker) {
102
- draftText = text;
103
- draftStream.update(draftText);
172
+ if (!lane.chunker) {
173
+ lane.draftText = text;
174
+ laneStream.update(lane.draftText);
104
175
  return;
105
176
  }
106
- draftChunker.append(delta);
107
- draftChunker.drain({
177
+ lane.chunker.append(delta);
178
+ lane.chunker.drain({
108
179
  force: false,
109
180
  emit: (chunk) => {
110
- draftText += chunk;
111
- draftStream.update(draftText);
181
+ lane.draftText += chunk;
182
+ laneStream.update(lane.draftText);
112
183
  },
113
184
  });
114
185
  };
115
- const flushDraft = async () => {
116
- if (!draftStream) {
186
+ const ingestDraftLaneSegments = (text) => {
187
+ for (const segment of splitTextIntoLaneSegments(text)) {
188
+ if (segment.lane === "reasoning") {
189
+ reasoningStepState.noteReasoningHint();
190
+ reasoningStepState.noteReasoningDelivered();
191
+ }
192
+ updateDraftFromPartial(lanes[segment.lane], segment.text);
193
+ }
194
+ };
195
+ const flushDraftLane = async (lane) => {
196
+ if (!lane.stream) {
117
197
  return;
118
198
  }
119
- if (draftChunker?.hasBuffered()) {
120
- draftChunker.drain({
199
+ if (lane.chunker?.hasBuffered()) {
200
+ lane.chunker.drain({
121
201
  force: true,
122
202
  emit: (chunk) => {
123
- draftText += chunk;
203
+ lane.draftText += chunk;
124
204
  },
125
205
  });
126
- draftChunker.reset();
127
- if (draftText) {
128
- draftStream.update(draftText);
206
+ lane.chunker.reset();
207
+ if (lane.draftText) {
208
+ lane.stream.update(lane.draftText);
129
209
  }
130
210
  }
131
- await draftStream.flush();
211
+ await lane.stream.flush();
132
212
  };
133
- const disableBlockStreaming = typeof telegramCfg.blockStreaming === "boolean"
134
- ? !telegramCfg.blockStreaming
135
- : draftStream || streamMode === "off"
136
- ? true
137
- : undefined;
213
+ const disableBlockStreaming = streamMode === "off"
214
+ ? true
215
+ : forceBlockStreamingForReasoning
216
+ ? false
217
+ : typeof telegramCfg.blockStreaming === "boolean"
218
+ ? !telegramCfg.blockStreaming
219
+ : canStreamAnswerDraft
220
+ ? true
221
+ : undefined;
138
222
  const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
139
223
  cfg,
140
224
  agentId: route.agentId,
141
225
  channel: "telegram",
142
226
  accountId: route.accountId,
143
227
  });
144
- const tableMode = resolveMarkdownTableMode({
145
- cfg,
146
- channel: "telegram",
147
- accountId: route.accountId,
148
- });
149
228
  const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId);
150
229
  // Handle uncached stickers: get a dedicated vision description before dispatch
151
230
  // This ensures we cache a raw description rather than a conversational response
@@ -205,8 +284,12 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
205
284
  const deliveryState = {
206
285
  delivered: false,
207
286
  skippedNonSilent: 0,
287
+ failedNonSilent: 0,
288
+ };
289
+ const finalizedPreviewByLane = {
290
+ answer: false,
291
+ reasoning: false,
208
292
  };
209
- let finalizedViaPreviewMessage = false;
210
293
  const clearGroupHistory = () => {
211
294
  if (isGroup && historyKey) {
212
295
  clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
@@ -226,6 +309,114 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
226
309
  linkPreview: telegramCfg.linkPreview,
227
310
  replyQuoteText,
228
311
  };
312
+ const getLanePreviewText = (lane) => streamMode === "block" ? lane.draftText : lane.lastPartialText;
313
+ const tryUpdatePreviewForLane = async (params) => {
314
+ const { lane, laneName, text, previewButtons, stopBeforeEdit = false, updateLaneSnapshot = false, skipRegressive, context, } = params;
315
+ if (!lane.stream) {
316
+ return false;
317
+ }
318
+ const hadPreviewMessage = typeof lane.stream.messageId() === "number";
319
+ if (stopBeforeEdit) {
320
+ await lane.stream.stop();
321
+ }
322
+ const previewMessageId = lane.stream.messageId();
323
+ if (typeof previewMessageId !== "number") {
324
+ return false;
325
+ }
326
+ const currentPreviewText = getLanePreviewText(lane);
327
+ const shouldSkipRegressive = Boolean(currentPreviewText) &&
328
+ currentPreviewText.startsWith(text) &&
329
+ text.length < currentPreviewText.length &&
330
+ (skipRegressive === "always" || hadPreviewMessage);
331
+ if (shouldSkipRegressive) {
332
+ // Avoid regressive punctuation/wording flicker from occasional shorter finals.
333
+ deliveryState.delivered = true;
334
+ return true;
335
+ }
336
+ try {
337
+ await editMessageTelegram(chatId, previewMessageId, text, {
338
+ api: bot.api,
339
+ cfg,
340
+ accountId: route.accountId,
341
+ linkPreview: telegramCfg.linkPreview,
342
+ buttons: previewButtons,
343
+ });
344
+ if (updateLaneSnapshot) {
345
+ lane.lastPartialText = text;
346
+ lane.draftText = text;
347
+ }
348
+ deliveryState.delivered = true;
349
+ return true;
350
+ }
351
+ catch (err) {
352
+ logVerbose(`telegram: ${laneName} preview ${context} edit failed; falling back to standard send (${String(err)})`);
353
+ return false;
354
+ }
355
+ };
356
+ const applyTextToPayload = (payload, text) => {
357
+ if (payload.text === text) {
358
+ return payload;
359
+ }
360
+ return { ...payload, text };
361
+ };
362
+ const sendPayload = async (payload) => {
363
+ const result = await deliverReplies({
364
+ ...deliveryBaseOptions,
365
+ replies: [payload],
366
+ onVoiceRecording: sendRecordVoice,
367
+ });
368
+ if (result.delivered) {
369
+ deliveryState.delivered = true;
370
+ }
371
+ return result.delivered;
372
+ };
373
+ const deliverLaneText = async (params) => {
374
+ const { laneName, text, payload, infoKind, previewButtons, allowPreviewUpdateForNonFinal = false, } = params;
375
+ const lane = lanes[laneName];
376
+ const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
377
+ const canEditViaPreview = !hasMedia && text.length > 0 && text.length <= draftMaxChars && !payload.isError;
378
+ if (infoKind === "final") {
379
+ if (canEditViaPreview && !finalizedPreviewByLane[laneName]) {
380
+ await flushDraftLane(lane);
381
+ const finalized = await tryUpdatePreviewForLane({
382
+ lane,
383
+ laneName,
384
+ text,
385
+ previewButtons,
386
+ stopBeforeEdit: true,
387
+ skipRegressive: "existingOnly",
388
+ context: "final",
389
+ });
390
+ if (finalized) {
391
+ finalizedPreviewByLane[laneName] = true;
392
+ return "preview-finalized";
393
+ }
394
+ }
395
+ else if (!hasMedia && !payload.isError && text.length > draftMaxChars) {
396
+ logVerbose(`telegram: preview final too long for edit (${text.length} > ${draftMaxChars}); falling back to standard send`);
397
+ }
398
+ await lane.stream?.stop();
399
+ const delivered = await sendPayload(applyTextToPayload(payload, text));
400
+ return delivered ? "sent" : "skipped";
401
+ }
402
+ if (allowPreviewUpdateForNonFinal && canEditViaPreview) {
403
+ const updated = await tryUpdatePreviewForLane({
404
+ lane,
405
+ laneName,
406
+ text,
407
+ previewButtons,
408
+ stopBeforeEdit: false,
409
+ updateLaneSnapshot: true,
410
+ skipRegressive: "always",
411
+ context: "update",
412
+ });
413
+ if (updated) {
414
+ return "preview-updated";
415
+ }
416
+ }
417
+ const delivered = await sendPayload(applyTextToPayload(payload, text));
418
+ return delivered ? "sent" : "skipped";
419
+ };
229
420
  let queuedFinal = false;
230
421
  try {
231
422
  ({ queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
@@ -234,91 +425,74 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
234
425
  dispatcherOptions: {
235
426
  ...prefixOptions,
236
427
  deliver: async (payload, info) => {
237
- if (info.kind === "final") {
238
- await flushDraft();
239
- const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
240
- const previewMessageId = draftStream?.messageId();
241
- const finalText = payload.text;
242
- const currentPreviewText = streamMode === "block" ? draftText : lastPartialText;
243
- const previewButtons = payload.channelData?.telegram?.buttons;
244
- let draftStoppedForPreviewEdit = false;
245
- // Skip preview edit for error payloads to avoid overwriting previous content
246
- const canFinalizeViaPreviewEdit = !finalizedViaPreviewMessage &&
247
- !hasMedia &&
248
- typeof finalText === "string" &&
249
- finalText.length > 0 &&
250
- typeof previewMessageId === "number" &&
251
- finalText.length <= draftMaxChars &&
252
- !payload.isError;
253
- if (canFinalizeViaPreviewEdit) {
254
- await draftStream?.stop();
255
- draftStoppedForPreviewEdit = true;
256
- if (currentPreviewText &&
257
- currentPreviewText.startsWith(finalText) &&
258
- finalText.length < currentPreviewText.length) {
259
- // Ignore regressive final edits (e.g., "Okay." -> "Ok"), which
260
- // can appear transiently in some provider streams.
261
- return;
262
- }
263
- try {
264
- await editMessageTelegram(chatId, previewMessageId, finalText, {
265
- api: bot.api,
266
- cfg,
267
- accountId: route.accountId,
268
- linkPreview: telegramCfg.linkPreview,
269
- buttons: previewButtons,
270
- });
271
- finalizedViaPreviewMessage = true;
272
- deliveryState.delivered = true;
273
- return;
274
- }
275
- catch (err) {
276
- logVerbose(`telegram: preview final edit failed; falling back to standard send (${String(err)})`);
277
- }
428
+ const previewButtons = payload.channelData?.telegram?.buttons;
429
+ const segments = splitTextIntoLaneSegments(payload.text);
430
+ const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
431
+ const flushBufferedFinalAnswer = async () => {
432
+ const buffered = reasoningStepState.takeBufferedFinalAnswer();
433
+ if (!buffered) {
434
+ return;
278
435
  }
279
- if (!hasMedia &&
280
- !payload.isError &&
281
- typeof finalText === "string" &&
282
- finalText.length > draftMaxChars) {
283
- logVerbose(`telegram: preview final too long for edit (${finalText.length} > ${draftMaxChars}); falling back to standard send`);
436
+ const bufferedButtons = buffered.payload.channelData?.telegram?.buttons;
437
+ await deliverLaneText({
438
+ laneName: "answer",
439
+ text: buffered.text,
440
+ payload: buffered.payload,
441
+ infoKind: "final",
442
+ previewButtons: bufferedButtons,
443
+ });
444
+ reasoningStepState.resetForNextStep();
445
+ };
446
+ for (const segment of segments) {
447
+ if (segment.lane === "answer" &&
448
+ info.kind === "final" &&
449
+ reasoningStepState.shouldBufferFinalAnswer()) {
450
+ reasoningStepState.bufferFinalAnswer({ payload, text: segment.text });
451
+ continue;
284
452
  }
285
- if (!draftStoppedForPreviewEdit) {
286
- await draftStream?.stop();
453
+ if (segment.lane === "reasoning") {
454
+ reasoningStepState.noteReasoningHint();
287
455
  }
288
- // Check if stop() sent a message (debounce released on isFinal)
289
- // If so, edit that message instead of sending a new one
290
- const messageIdAfterStop = draftStream?.messageId();
291
- if (!finalizedViaPreviewMessage &&
292
- typeof messageIdAfterStop === "number" &&
293
- typeof finalText === "string" &&
294
- finalText.length > 0 &&
295
- finalText.length <= draftMaxChars &&
296
- !hasMedia &&
297
- !payload.isError) {
298
- try {
299
- await editMessageTelegram(chatId, messageIdAfterStop, finalText, {
300
- api: bot.api,
301
- cfg,
302
- accountId: route.accountId,
303
- linkPreview: telegramCfg.linkPreview,
304
- buttons: previewButtons,
305
- });
306
- finalizedViaPreviewMessage = true;
307
- deliveryState.delivered = true;
308
- return;
456
+ const result = await deliverLaneText({
457
+ laneName: segment.lane,
458
+ text: segment.text,
459
+ payload,
460
+ infoKind: info.kind,
461
+ previewButtons,
462
+ allowPreviewUpdateForNonFinal: segment.lane === "reasoning",
463
+ });
464
+ if (segment.lane === "reasoning") {
465
+ if (result !== "skipped") {
466
+ reasoningStepState.noteReasoningDelivered();
467
+ await flushBufferedFinalAnswer();
309
468
  }
310
- catch (err) {
311
- logVerbose(`telegram: post-stop preview edit failed; falling back to standard send (${String(err)})`);
469
+ continue;
470
+ }
471
+ if (info.kind === "final") {
472
+ if (reasoningLane.hasStreamedMessage) {
473
+ finalizedPreviewByLane.reasoning = true;
312
474
  }
475
+ reasoningStepState.resetForNextStep();
313
476
  }
314
477
  }
315
- const result = await deliverReplies({
316
- ...deliveryBaseOptions,
317
- replies: [payload],
318
- onVoiceRecording: sendRecordVoice,
319
- });
320
- if (result.delivered) {
321
- deliveryState.delivered = true;
478
+ if (segments.length > 0) {
479
+ return;
480
+ }
481
+ if (info.kind === "final") {
482
+ await answerLane.stream?.stop();
483
+ await reasoningLane.stream?.stop();
484
+ reasoningStepState.resetForNextStep();
485
+ }
486
+ const canSendAsIs = hasMedia || typeof payload.text !== "string" || payload.text.length > 0;
487
+ if (!canSendAsIs) {
488
+ if (info.kind === "final") {
489
+ await flushBufferedFinalAnswer();
490
+ }
491
+ return;
492
+ }
493
+ await sendPayload(payload);
494
+ if (info.kind === "final") {
495
+ await flushBufferedFinalAnswer();
322
496
  }
323
497
  },
324
498
  onSkip: (_payload, info) => {
@@ -327,6 +501,7 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
327
501
  }
328
502
  },
329
503
  onError: (err, info) => {
504
+ deliveryState.failedNonSilent += 1;
330
505
  runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`));
331
506
  },
332
507
  onReplyStart: createTypingCallbacks({
@@ -344,30 +519,36 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
344
519
  replyOptions: {
345
520
  skillFilter,
346
521
  disableBlockStreaming,
347
- onPartialReply: draftStream ? (payload) => updateDraftFromPartial(payload.text) : undefined,
348
- onAssistantMessageStart: draftStream
349
- ? () => {
350
- // Only split preview bubbles in block mode. In partial mode, keep
351
- // editing one preview message to avoid flooding the chat.
352
- logVerbose(`telegram: onAssistantMessageStart called, hasStreamedMessage=${hasStreamedMessage}`);
353
- if (shouldSplitPreviewMessages && hasStreamedMessage) {
354
- logVerbose(`telegram: calling forceNewMessage()`);
355
- draftStream.forceNewMessage();
522
+ onPartialReply: answerLane.stream || reasoningLane.stream
523
+ ? (payload) => ingestDraftLaneSegments(payload.text)
524
+ : undefined,
525
+ onReasoningStream: reasoningLane.stream
526
+ ? (payload) => {
527
+ // Split between reasoning blocks only when the next reasoning
528
+ // stream starts. Splitting at reasoning-end can orphan the active
529
+ // preview and cause duplicate reasoning sends on reasoning final.
530
+ if (splitReasoningOnNextStream) {
531
+ reasoningLane.stream?.forceNewMessage();
532
+ resetDraftLaneState(reasoningLane);
533
+ splitReasoningOnNextStream = false;
356
534
  }
357
- lastPartialText = "";
358
- draftText = "";
359
- draftChunker?.reset();
535
+ ingestDraftLaneSegments(payload.text);
360
536
  }
361
537
  : undefined,
362
- onReasoningEnd: draftStream
538
+ onAssistantMessageStart: answerLane.stream
363
539
  ? () => {
364
- // Same policy as assistant-message boundaries: split only in block mode.
365
- if (shouldSplitPreviewMessages && hasStreamedMessage) {
366
- draftStream.forceNewMessage();
540
+ reasoningStepState.resetForNextStep();
541
+ // Keep answer blocks separated in block mode; partial mode keeps one answer lane.
542
+ if (streamMode === "block" && answerLane.hasStreamedMessage) {
543
+ answerLane.stream?.forceNewMessage();
367
544
  }
368
- lastPartialText = "";
369
- draftText = "";
370
- draftChunker?.reset();
545
+ resetDraftLaneState(answerLane);
546
+ }
547
+ : undefined,
548
+ onReasoningEnd: reasoningLane.stream
549
+ ? () => {
550
+ // Split when/if a later reasoning block begins.
551
+ splitReasoningOnNextStream = reasoningLane.hasStreamedMessage;
371
552
  }
372
553
  : undefined,
373
554
  onModelSelected,
@@ -375,14 +556,35 @@ export const dispatchTelegramMessage = async ({ context, bot, cfg, runtime, repl
375
556
  }));
376
557
  }
377
558
  finally {
378
- // Must stop() first to flush debounced content before clear() wipes state
379
- await draftStream?.stop();
380
- if (!finalizedViaPreviewMessage) {
381
- await draftStream?.clear();
559
+ // Must stop() first to flush debounced content before clear() wipes state.
560
+ const streamCleanupStates = new Map();
561
+ const lanesToCleanup = [
562
+ { laneName: "answer", lane: answerLane },
563
+ { laneName: "reasoning", lane: reasoningLane },
564
+ ];
565
+ for (const laneState of lanesToCleanup) {
566
+ const stream = laneState.lane.stream;
567
+ if (!stream) {
568
+ continue;
569
+ }
570
+ const shouldClear = !finalizedPreviewByLane[laneState.laneName];
571
+ const existing = streamCleanupStates.get(stream);
572
+ if (!existing) {
573
+ streamCleanupStates.set(stream, { shouldClear });
574
+ continue;
575
+ }
576
+ existing.shouldClear = existing.shouldClear && shouldClear;
577
+ }
578
+ for (const [stream, cleanupState] of streamCleanupStates) {
579
+ await stream.stop();
580
+ if (cleanupState.shouldClear) {
581
+ await stream.clear();
582
+ }
382
583
  }
383
584
  }
384
585
  let sentFallback = false;
385
- if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {
586
+ if (!deliveryState.delivered &&
587
+ (deliveryState.skippedNonSilent > 0 || deliveryState.failedNonSilent > 0)) {
386
588
  const result = await deliverReplies({
387
589
  replies: [{ text: EMPTY_RESPONSE_FALLBACK }],
388
590
  ...deliveryBaseOptions,