@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,9 +1,10 @@
1
1
  import { run } from "@grammyjs/runner";
2
- import { loadConfig } from "../config/config.js";
3
2
  import { resolveAgentMaxConcurrent } from "../config/agent-limits.js";
3
+ import { loadConfig } from "../config/config.js";
4
4
  import { computeBackoff, sleepWithAbort } from "../infra/backoff.js";
5
5
  import { formatErrorMessage } from "../infra/errors.js";
6
- import { formatDurationMs } from "../infra/format-duration.js";
6
+ import { formatDurationPrecise } from "../infra/format-time/format-duration.js";
7
+ import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
7
8
  import { resolveTelegramAccount } from "./accounts.js";
8
9
  import { resolveTelegramAllowedUpdates } from "./allowed-updates.js";
9
10
  import { createTelegramBot } from "./bot.js";
@@ -50,101 +51,125 @@ const isGetUpdatesConflict = (err) => {
50
51
  .toLowerCase();
51
52
  return haystack.includes("getupdates");
52
53
  };
53
- export async function monitorTelegramProvider(opts = {}) {
54
- const cfg = opts.config ?? loadConfig();
55
- const account = resolveTelegramAccount({
56
- cfg,
57
- accountId: opts.accountId,
58
- });
59
- const token = opts.token?.trim() || account.token;
60
- if (!token) {
61
- throw new Error(`Telegram bot token missing for account "${account.accountId}" (set channels.telegram.accounts.${account.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
54
+ /** Check if error is a Grammy HttpError (used to scope unhandled rejection handling) */
55
+ const isGrammyHttpError = (err) => {
56
+ if (!err || typeof err !== "object") {
57
+ return false;
62
58
  }
63
- const proxyFetch = opts.proxyFetch ??
64
- (account.config.proxy ? makeProxyFetch(account.config.proxy) : undefined);
65
- let lastUpdateId = await readTelegramUpdateOffset({
66
- accountId: account.accountId,
67
- });
68
- const persistUpdateId = async (updateId) => {
69
- if (lastUpdateId !== null && updateId <= lastUpdateId)
70
- return;
71
- lastUpdateId = updateId;
72
- try {
73
- await writeTelegramUpdateOffset({
74
- accountId: account.accountId,
75
- updateId,
76
- });
77
- }
78
- catch (err) {
79
- (opts.runtime?.error ?? console.error)(`telegram: failed to persist update offset: ${String(err)}`);
59
+ return err.name === "HttpError";
60
+ };
61
+ export async function monitorTelegramProvider(opts = {}) {
62
+ const log = opts.runtime?.error ?? console.error;
63
+ // Register handler for Grammy HttpError unhandled rejections.
64
+ // This catches network errors that escape the polling loop's try-catch
65
+ // (e.g., from setMyCommands during bot setup).
66
+ // We gate on isGrammyHttpError to avoid suppressing non-Telegram errors.
67
+ const unregisterHandler = registerUnhandledRejectionHandler((err) => {
68
+ if (isGrammyHttpError(err) && isRecoverableTelegramNetworkError(err, { context: "polling" })) {
69
+ log(`[telegram] Suppressed network error: ${formatErrorMessage(err)}`);
70
+ return true; // handled - don't crash
80
71
  }
81
- };
82
- const bot = createTelegramBot({
83
- token,
84
- runtime: opts.runtime,
85
- proxyFetch,
86
- config: cfg,
87
- accountId: account.accountId,
88
- updateOffset: {
89
- lastUpdateId,
90
- onUpdateId: persistUpdateId,
91
- },
72
+ return false;
92
73
  });
93
- if (opts.useWebhook) {
94
- await startTelegramWebhook({
95
- token,
74
+ try {
75
+ const cfg = opts.config ?? loadConfig();
76
+ const account = resolveTelegramAccount({
77
+ cfg,
78
+ accountId: opts.accountId,
79
+ });
80
+ const token = opts.token?.trim() || account.token;
81
+ if (!token) {
82
+ throw new Error(`Telegram bot token missing for account "${account.accountId}" (set channels.telegram.accounts.${account.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
83
+ }
84
+ const proxyFetch = opts.proxyFetch ??
85
+ (account.config.proxy ? makeProxyFetch(account.config.proxy) : undefined);
86
+ let lastUpdateId = await readTelegramUpdateOffset({
96
87
  accountId: account.accountId,
97
- config: cfg,
98
- path: opts.webhookPath,
99
- port: opts.webhookPort,
100
- secret: opts.webhookSecret,
101
- runtime: opts.runtime,
102
- fetch: proxyFetch,
103
- abortSignal: opts.abortSignal,
104
- publicUrl: opts.webhookUrl,
105
88
  });
106
- return;
107
- }
108
- // Use grammyjs/runner for concurrent update processing
109
- let restartAttempts = 0;
110
- while (!opts.abortSignal?.aborted) {
111
- const runner = run(bot, createTelegramRunnerOptions(cfg));
112
- const stopOnAbort = () => {
113
- if (opts.abortSignal?.aborted) {
114
- void runner.stop();
89
+ const persistUpdateId = async (updateId) => {
90
+ if (lastUpdateId !== null && updateId <= lastUpdateId)
91
+ return;
92
+ lastUpdateId = updateId;
93
+ try {
94
+ await writeTelegramUpdateOffset({
95
+ accountId: account.accountId,
96
+ updateId,
97
+ });
98
+ }
99
+ catch (err) {
100
+ (opts.runtime?.error ?? console.error)(`telegram: failed to persist update offset: ${String(err)}`);
115
101
  }
116
102
  };
117
- opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true });
118
- try {
119
- // runner.task() returns a promise that resolves when the runner stops
120
- await runner.task();
103
+ const bot = createTelegramBot({
104
+ token,
105
+ runtime: opts.runtime,
106
+ proxyFetch,
107
+ config: cfg,
108
+ accountId: account.accountId,
109
+ updateOffset: {
110
+ lastUpdateId,
111
+ onUpdateId: persistUpdateId,
112
+ },
113
+ });
114
+ if (opts.useWebhook) {
115
+ await startTelegramWebhook({
116
+ token,
117
+ accountId: account.accountId,
118
+ config: cfg,
119
+ path: opts.webhookPath,
120
+ port: opts.webhookPort,
121
+ secret: opts.webhookSecret,
122
+ runtime: opts.runtime,
123
+ fetch: proxyFetch,
124
+ abortSignal: opts.abortSignal,
125
+ publicUrl: opts.webhookUrl,
126
+ });
121
127
  return;
122
128
  }
123
- catch (err) {
124
- if (opts.abortSignal?.aborted) {
125
- throw err;
126
- }
127
- const isConflict = isGetUpdatesConflict(err);
128
- const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
129
- if (!isConflict && !isRecoverable) {
130
- throw err;
131
- }
132
- restartAttempts += 1;
133
- const delayMs = computeBackoff(TELEGRAM_POLL_RESTART_POLICY, restartAttempts);
134
- const reason = isConflict ? "getUpdates conflict" : "network error";
135
- const errMsg = formatErrorMessage(err);
136
- (opts.runtime?.error ?? console.error)(`Telegram ${reason}: ${errMsg}; retrying in ${formatDurationMs(delayMs)}.`);
129
+ // Use grammyjs/runner for concurrent update processing
130
+ let restartAttempts = 0;
131
+ while (!opts.abortSignal?.aborted) {
132
+ const runner = run(bot, createTelegramRunnerOptions(cfg));
133
+ const stopOnAbort = () => {
134
+ if (opts.abortSignal?.aborted) {
135
+ void runner.stop();
136
+ }
137
+ };
138
+ opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true });
137
139
  try {
138
- await sleepWithAbort(delayMs, opts.abortSignal);
140
+ // runner.task() returns a promise that resolves when the runner stops
141
+ await runner.task();
142
+ return;
139
143
  }
140
- catch (sleepErr) {
141
- if (opts.abortSignal?.aborted)
142
- return;
143
- throw sleepErr;
144
+ catch (err) {
145
+ if (opts.abortSignal?.aborted) {
146
+ throw err;
147
+ }
148
+ const isConflict = isGetUpdatesConflict(err);
149
+ const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
150
+ if (!isConflict && !isRecoverable) {
151
+ throw err;
152
+ }
153
+ restartAttempts += 1;
154
+ const delayMs = computeBackoff(TELEGRAM_POLL_RESTART_POLICY, restartAttempts);
155
+ const reason = isConflict ? "getUpdates conflict" : "network error";
156
+ const errMsg = formatErrorMessage(err);
157
+ (opts.runtime?.error ?? console.error)(`Telegram ${reason}: ${errMsg}; retrying in ${formatDurationPrecise(delayMs)}.`);
158
+ try {
159
+ await sleepWithAbort(delayMs, opts.abortSignal);
160
+ }
161
+ catch (sleepErr) {
162
+ if (opts.abortSignal?.aborted)
163
+ return;
164
+ throw sleepErr;
165
+ }
166
+ }
167
+ finally {
168
+ opts.abortSignal?.removeEventListener("abort", stopOnAbort);
144
169
  }
145
170
  }
146
- finally {
147
- opts.abortSignal?.removeEventListener("abort", stopOnAbort);
148
- }
171
+ }
172
+ finally {
173
+ unregisterHandler();
149
174
  }
150
175
  }
@@ -23,6 +23,7 @@ import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js
23
23
  import { resolveTelegramVoiceSend } from "./voice.js";
24
24
  import { buildTelegramThreadParams } from "./bot/helpers.js";
25
25
  const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
26
+ const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
26
27
  const diagLogger = createSubsystemLogger("telegram/diagnostic");
27
28
  function createTelegramHttpLogger(cfg) {
28
29
  const enabled = isDiagnosticFlagEnabled("telegram.http", cfg);
@@ -101,6 +102,26 @@ function normalizeMessageId(raw) {
101
102
  }
102
103
  throw new Error("Message id is required for Telegram actions");
103
104
  }
105
+ function isTelegramThreadNotFoundError(err) {
106
+ return THREAD_NOT_FOUND_RE.test(formatErrorMessage(err));
107
+ }
108
+ function hasMessageThreadIdParam(params) {
109
+ if (!params)
110
+ return false;
111
+ const value = params.message_thread_id;
112
+ if (typeof value === "number")
113
+ return Number.isFinite(value);
114
+ if (typeof value === "string")
115
+ return value.trim().length > 0;
116
+ return false;
117
+ }
118
+ function removeMessageThreadIdParam(params) {
119
+ if (!params || !hasMessageThreadIdParam(params))
120
+ return params;
121
+ const next = { ...params };
122
+ delete next.message_thread_id;
123
+ return Object.keys(next).length > 0 ? next : undefined;
124
+ }
104
125
  export function buildInlineKeyboard(buttons) {
105
126
  if (!buttons?.length)
106
127
  return undefined;
@@ -136,8 +157,17 @@ export async function sendMessageTelegram(to, text, opts = {}) {
136
157
  const messageThreadId = opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
137
158
  const threadIdParams = buildTelegramThreadParams(messageThreadId);
138
159
  const threadParams = threadIdParams ? { ...threadIdParams } : {};
160
+ const quoteText = opts.quoteText?.trim();
139
161
  if (opts.replyToMessageId != null) {
140
- threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
162
+ if (quoteText) {
163
+ threadParams.reply_parameters = {
164
+ message_id: Math.trunc(opts.replyToMessageId),
165
+ quote: quoteText,
166
+ };
167
+ }
168
+ else {
169
+ threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
170
+ }
141
171
  }
142
172
  const hasThreadParams = Object.keys(threadParams).length > 0;
143
173
  const request = createTelegramRetryRunner({
@@ -163,6 +193,21 @@ export async function sendMessageTelegram(to, text, opts = {}) {
163
193
  `Input was: ${JSON.stringify(to)}.`,
164
194
  ].join(" "));
165
195
  };
196
+ const sendWithThreadFallback = async (params, label, attempt) => {
197
+ try {
198
+ return await attempt(params, label);
199
+ }
200
+ catch (err) {
201
+ if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) {
202
+ throw err;
203
+ }
204
+ if (opts.verbose) {
205
+ console.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
206
+ }
207
+ const retriedParams = removeMessageThreadIdParam(params);
208
+ return await attempt(retriedParams, `${label}-threadless`);
209
+ }
210
+ };
166
211
  const textMode = opts.textMode ?? "markdown";
167
212
  const tableMode = resolveMarkdownTableMode({
168
213
  cfg,
@@ -174,36 +219,40 @@ export async function sendMessageTelegram(to, text, opts = {}) {
174
219
  const linkPreviewEnabled = account.config.linkPreview ?? true;
175
220
  const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
176
221
  const sendTelegramText = async (rawText, params, fallbackText) => {
177
- const htmlText = renderHtmlText(rawText);
178
- const baseParams = params ? { ...params } : {};
179
- if (linkPreviewOptions) {
180
- baseParams.link_preview_options = linkPreviewOptions;
181
- }
182
- const hasBaseParams = Object.keys(baseParams).length > 0;
183
- const sendParams = {
184
- parse_mode: "HTML",
185
- ...baseParams,
186
- ...(opts.silent === true ? { disable_notification: true } : {}),
187
- };
188
- const res = await requestWithDiag(() => api.sendMessage(chatId, htmlText, sendParams), "message").catch(async (err) => {
189
- // Telegram rejects malformed HTML (e.g., unsupported tags or entities).
190
- // When that happens, fall back to plain text so the message still delivers.
191
- const errText = formatErrorMessage(err);
192
- if (PARSE_ERR_RE.test(errText)) {
193
- if (opts.verbose) {
194
- console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
195
- }
196
- const fallback = fallbackText ?? rawText;
197
- const plainParams = hasBaseParams ? baseParams : undefined;
198
- return await requestWithDiag(() => plainParams
199
- ? api.sendMessage(chatId, fallback, plainParams)
200
- : api.sendMessage(chatId, fallback), "message-plain").catch((err2) => {
201
- throw wrapChatNotFound(err2);
202
- });
222
+ return await sendWithThreadFallback(params, "message", async (effectiveParams, label) => {
223
+ const htmlText = renderHtmlText(rawText);
224
+ const baseParams = effectiveParams ? { ...effectiveParams } : {};
225
+ if (linkPreviewOptions) {
226
+ baseParams.link_preview_options = linkPreviewOptions;
203
227
  }
204
- throw wrapChatNotFound(err);
228
+ const hasBaseParams = Object.keys(baseParams).length > 0;
229
+ const sendParams = {
230
+ parse_mode: "HTML",
231
+ ...baseParams,
232
+ ...(opts.silent === true ? { disable_notification: true } : {}),
233
+ };
234
+ const res = await requestWithDiag(() => api.sendMessage(chatId, htmlText, sendParams), label).catch(async (err) => {
235
+ // Telegram rejects malformed HTML (e.g., unsupported tags or entities).
236
+ // When that happens, fall back to plain text so the message still delivers.
237
+ const errText = formatErrorMessage(err);
238
+ if (PARSE_ERR_RE.test(errText)) {
239
+ if (opts.verbose) {
240
+ console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
241
+ }
242
+ const fallback = fallbackText ?? rawText;
243
+ const plainParams = hasBaseParams
244
+ ? baseParams
245
+ : undefined;
246
+ return await requestWithDiag(() => plainParams
247
+ ? api.sendMessage(chatId, fallback, plainParams)
248
+ : api.sendMessage(chatId, fallback), `${label}-plain`).catch((err2) => {
249
+ throw wrapChatNotFound(err2);
250
+ });
251
+ }
252
+ throw wrapChatNotFound(err);
253
+ });
254
+ return res;
205
255
  });
206
- return res;
207
256
  };
208
257
  if (mediaUrl) {
209
258
  const media = await loadWebMedia(mediaUrl, opts.maxBytes);
@@ -212,9 +261,21 @@ export async function sendMessageTelegram(to, text, opts = {}) {
212
261
  contentType: media.contentType,
213
262
  fileName: media.fileName,
214
263
  });
264
+ const isVideoNote = kind === "video" && opts.asVideoNote === true;
215
265
  const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
216
266
  const file = new InputFile(media.buffer, fileName);
217
- const { caption, followUpText } = splitTelegramCaption(text);
267
+ let caption;
268
+ let followUpText;
269
+ if (isVideoNote) {
270
+ // Video notes don't support captions; send any text as follow-up.
271
+ caption = undefined;
272
+ followUpText = text.trim() ? text : undefined;
273
+ }
274
+ else {
275
+ const split = splitTelegramCaption(text);
276
+ caption = split.caption;
277
+ followUpText = split.followUpText;
278
+ }
218
279
  const htmlCaption = caption ? renderHtmlText(caption) : undefined;
219
280
  // If text exceeds Telegram's caption limit, send media without caption
220
281
  // then send text as a separate follow-up message.
@@ -226,26 +287,32 @@ export async function sendMessageTelegram(to, text, opts = {}) {
226
287
  ...(!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}),
227
288
  };
228
289
  const mediaParams = {
229
- caption: htmlCaption,
230
- ...(htmlCaption ? { parse_mode: "HTML" } : {}),
290
+ ...(htmlCaption ? { caption: htmlCaption, parse_mode: "HTML" } : {}),
231
291
  ...baseMediaParams,
232
292
  ...(opts.silent === true ? { disable_notification: true } : {}),
233
293
  };
234
294
  let result;
235
295
  if (isGif) {
236
- result = await requestWithDiag(() => api.sendAnimation(chatId, file, mediaParams), "animation").catch((err) => {
296
+ result = await sendWithThreadFallback(mediaParams, "animation", async (effectiveParams, label) => requestWithDiag(() => api.sendAnimation(chatId, file, effectiveParams), label).catch((err) => {
237
297
  throw wrapChatNotFound(err);
238
- });
298
+ }));
239
299
  }
240
300
  else if (kind === "image") {
241
- result = await requestWithDiag(() => api.sendPhoto(chatId, file, mediaParams), "photo").catch((err) => {
301
+ result = await sendWithThreadFallback(mediaParams, "photo", async (effectiveParams, label) => requestWithDiag(() => api.sendPhoto(chatId, file, effectiveParams), label).catch((err) => {
242
302
  throw wrapChatNotFound(err);
243
- });
303
+ }));
244
304
  }
245
305
  else if (kind === "video") {
246
- result = await requestWithDiag(() => api.sendVideo(chatId, file, mediaParams), "video").catch((err) => {
247
- throw wrapChatNotFound(err);
248
- });
306
+ if (isVideoNote) {
307
+ result = await sendWithThreadFallback(mediaParams, "video_note", async (effectiveParams, label) => requestWithDiag(() => api.sendVideoNote(chatId, file, effectiveParams), label).catch((err) => {
308
+ throw wrapChatNotFound(err);
309
+ }));
310
+ }
311
+ else {
312
+ result = await sendWithThreadFallback(mediaParams, "video", async (effectiveParams, label) => requestWithDiag(() => api.sendVideo(chatId, file, effectiveParams), label).catch((err) => {
313
+ throw wrapChatNotFound(err);
314
+ }));
315
+ }
249
316
  }
250
317
  else if (kind === "audio") {
251
318
  const { useVoice } = resolveTelegramVoiceSend({
@@ -255,20 +322,20 @@ export async function sendMessageTelegram(to, text, opts = {}) {
255
322
  logFallback: logVerbose,
256
323
  });
257
324
  if (useVoice) {
258
- result = await requestWithDiag(() => api.sendVoice(chatId, file, mediaParams), "voice").catch((err) => {
325
+ result = await sendWithThreadFallback(mediaParams, "voice", async (effectiveParams, label) => requestWithDiag(() => api.sendVoice(chatId, file, effectiveParams), label).catch((err) => {
259
326
  throw wrapChatNotFound(err);
260
- });
327
+ }));
261
328
  }
262
329
  else {
263
- result = await requestWithDiag(() => api.sendAudio(chatId, file, mediaParams), "audio").catch((err) => {
330
+ result = await sendWithThreadFallback(mediaParams, "audio", async (effectiveParams, label) => requestWithDiag(() => api.sendAudio(chatId, file, effectiveParams), label).catch((err) => {
264
331
  throw wrapChatNotFound(err);
265
- });
332
+ }));
266
333
  }
267
334
  }
268
335
  else {
269
- result = await requestWithDiag(() => api.sendDocument(chatId, file, mediaParams), "document").catch((err) => {
336
+ result = await sendWithThreadFallback(mediaParams, "document", async (effectiveParams, label) => requestWithDiag(() => api.sendDocument(chatId, file, effectiveParams), label).catch((err) => {
270
337
  throw wrapChatNotFound(err);
271
- });
338
+ }));
272
339
  }
273
340
  const mediaMessageId = String(result?.message_id ?? "unknown");
274
341
  const resolvedChatId = String(result?.chat?.id ?? chatId);
@@ -508,10 +575,25 @@ export async function sendStickerTelegram(to, fileId, opts = {}) {
508
575
  `Input was: ${JSON.stringify(to)}.`,
509
576
  ].join(" "));
510
577
  };
578
+ const sendWithStickerThreadFallback = async (params, label, attempt) => {
579
+ try {
580
+ return await attempt(params, label);
581
+ }
582
+ catch (err) {
583
+ if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) {
584
+ throw err;
585
+ }
586
+ if (opts.verbose) {
587
+ console.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
588
+ }
589
+ const retriedParams = removeMessageThreadIdParam(params);
590
+ return await attempt(retriedParams, `${label}-threadless`);
591
+ }
592
+ };
511
593
  const stickerParams = hasThreadParams ? threadParams : undefined;
512
- const result = await requestWithDiag(() => api.sendSticker(chatId, fileId.trim(), stickerParams), "sticker").catch((err) => {
594
+ const result = await sendWithStickerThreadFallback(stickerParams, "sticker", async (effectiveParams, label) => requestWithDiag(() => api.sendSticker(chatId, fileId.trim(), effectiveParams), label).catch((err) => {
513
595
  throw wrapChatNotFound(err);
514
- });
596
+ }));
515
597
  const messageId = String(result?.message_id ?? "unknown");
516
598
  const resolvedChatId = String(result?.chat?.id ?? chatId);
517
599
  if (result?.message_id) {
@@ -0,0 +1,45 @@
1
+ import { clearActiveProgressLine } from "./progress-line.js";
2
+ const RESET_SEQUENCE = "\x1b[0m\x1b[?25h\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l";
3
+ function reportRestoreFailure(scope, err, reason) {
4
+ const suffix = reason ? ` (${reason})` : "";
5
+ const message = `[terminal] restore ${scope} failed${suffix}: ${String(err)}`;
6
+ try {
7
+ process.stderr.write(`${message}\n`);
8
+ }
9
+ catch (writeErr) {
10
+ console.error(`[terminal] restore reporting failed${suffix}: ${String(writeErr)}`);
11
+ }
12
+ }
13
+ export function restoreTerminalState(reason) {
14
+ try {
15
+ clearActiveProgressLine();
16
+ }
17
+ catch (err) {
18
+ reportRestoreFailure("progress line", err, reason);
19
+ }
20
+ const stdin = process.stdin;
21
+ if (stdin.isTTY && typeof stdin.setRawMode === "function") {
22
+ try {
23
+ stdin.setRawMode(false);
24
+ }
25
+ catch (err) {
26
+ reportRestoreFailure("raw mode", err, reason);
27
+ }
28
+ if (typeof stdin.isPaused === "function" && stdin.isPaused()) {
29
+ try {
30
+ stdin.resume();
31
+ }
32
+ catch (err) {
33
+ reportRestoreFailure("stdin resume", err, reason);
34
+ }
35
+ }
36
+ }
37
+ if (process.stdout.isTTY) {
38
+ try {
39
+ process.stdout.write(RESET_SEQUENCE);
40
+ }
41
+ catch (err) {
42
+ reportRestoreFailure("stdout reset", err, reason);
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,16 @@
1
+ export function snapshotStateDirEnv() {
2
+ return {
3
+ clawdbotStateDir: process.env.CLAWDBOT_STATE_DIR,
4
+ };
5
+ }
6
+ export function restoreStateDirEnv(snapshot) {
7
+ if (snapshot.clawdbotStateDir === undefined) {
8
+ delete process.env.CLAWDBOT_STATE_DIR;
9
+ }
10
+ else {
11
+ process.env.CLAWDBOT_STATE_DIR = snapshot.clawdbotStateDir;
12
+ }
13
+ }
14
+ export function setStateDirEnv(stateDir) {
15
+ process.env.CLAWDBOT_STATE_DIR = stateDir;
16
+ }
package/dist/tts/tts.js CHANGED
@@ -579,9 +579,15 @@ export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"];
579
579
  * Custom OpenAI-compatible TTS endpoint.
580
580
  * When set, model/voice validation is relaxed to allow non-OpenAI models.
581
581
  * Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1
582
+ *
583
+ * Note: Read at runtime (not module load) to support config.env loading.
582
584
  */
583
- const OPENAI_TTS_BASE_URL = (process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1").replace(/\/+$/, "");
584
- const isCustomOpenAIEndpoint = OPENAI_TTS_BASE_URL !== "https://api.openai.com/v1";
585
+ function getOpenAITtsBaseUrl() {
586
+ return (process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1").replace(/\/+$/, "");
587
+ }
588
+ function isCustomOpenAIEndpoint() {
589
+ return getOpenAITtsBaseUrl() !== "https://api.openai.com/v1";
590
+ }
585
591
  export const OPENAI_TTS_VOICES = [
586
592
  "alloy",
587
593
  "ash",
@@ -595,13 +601,13 @@ export const OPENAI_TTS_VOICES = [
595
601
  ];
596
602
  function isValidOpenAIModel(model) {
597
603
  // Allow any model when using custom endpoint (e.g., Kokoro, LocalAI)
598
- if (isCustomOpenAIEndpoint)
604
+ if (isCustomOpenAIEndpoint())
599
605
  return true;
600
606
  return OPENAI_TTS_MODELS.includes(model);
601
607
  }
602
608
  function isValidOpenAIVoice(voice) {
603
609
  // Allow any voice when using custom endpoint (e.g., Kokoro Chinese voices)
604
- if (isCustomOpenAIEndpoint)
610
+ if (isCustomOpenAIEndpoint())
605
611
  return true;
606
612
  return OPENAI_TTS_VOICES.includes(voice);
607
613
  }
@@ -679,7 +685,7 @@ async function summarizeText(params) {
679
685
  catch (err) {
680
686
  const error = err;
681
687
  if (error.name === "AbortError") {
682
- throw new Error("Summarization timed out");
688
+ throw new Error("Summarization timed out", { cause: err });
683
689
  }
684
690
  throw err;
685
691
  }
@@ -754,7 +760,7 @@ async function openaiTTS(params) {
754
760
  const controller = new AbortController();
755
761
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
756
762
  try {
757
- const response = await fetch(`${OPENAI_TTS_BASE_URL}/audio/speech`, {
763
+ const response = await fetch(`${getOpenAITtsBaseUrl()}/audio/speech`, {
758
764
  method: "POST",
759
765
  headers: {
760
766
  Authorization: `Bearer ${apiKey}`,