@poolzin/pool-bot 2026.2.21 → 2026.2.22

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 (369) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/diagnostics-otel/package.json +1 -1
  326. package/extensions/discord/package.json +1 -1
  327. package/extensions/feishu/package.json +1 -1
  328. package/extensions/google-antigravity-auth/package.json +1 -1
  329. package/extensions/google-gemini-cli-auth/package.json +1 -1
  330. package/extensions/googlechat/package.json +1 -1
  331. package/extensions/imessage/package.json +1 -1
  332. package/extensions/irc/package.json +1 -1
  333. package/extensions/line/package.json +1 -1
  334. package/extensions/llm-task/package.json +1 -1
  335. package/extensions/lobster/package.json +1 -1
  336. package/extensions/matrix/CHANGELOG.md +5 -0
  337. package/extensions/matrix/package.json +1 -1
  338. package/extensions/mattermost/package.json +1 -1
  339. package/extensions/memory-core/package.json +1 -1
  340. package/extensions/memory-lancedb/package.json +1 -1
  341. package/extensions/minimax-portal-auth/package.json +1 -1
  342. package/extensions/msteams/CHANGELOG.md +5 -0
  343. package/extensions/msteams/package.json +1 -1
  344. package/extensions/nextcloud-talk/package.json +1 -1
  345. package/extensions/nostr/CHANGELOG.md +5 -0
  346. package/extensions/nostr/package.json +1 -1
  347. package/extensions/open-prose/package.json +1 -1
  348. package/extensions/openai-codex-auth/package.json +1 -1
  349. package/extensions/signal/package.json +1 -1
  350. package/extensions/slack/package.json +1 -1
  351. package/extensions/telegram/package.json +1 -1
  352. package/extensions/tlon/package.json +1 -1
  353. package/extensions/twitch/CHANGELOG.md +5 -0
  354. package/extensions/twitch/package.json +1 -1
  355. package/extensions/voice-call/CHANGELOG.md +5 -0
  356. package/extensions/voice-call/package.json +1 -1
  357. package/extensions/whatsapp/package.json +1 -1
  358. package/extensions/zalo/CHANGELOG.md +5 -0
  359. package/extensions/zalo/package.json +1 -1
  360. package/extensions/zalouser/CHANGELOG.md +5 -0
  361. package/extensions/zalouser/package.json +1 -1
  362. package/package.json +1 -1
  363. package/skills/apple-reminders/SKILL.md +100 -49
  364. package/skills/coding-agent/SKILL.md +34 -28
  365. package/skills/github/SKILL.md +131 -16
  366. package/skills/imsg/SKILL.md +112 -15
  367. package/skills/openhue/SKILL.md +101 -19
  368. package/skills/tmux/SKILL.md +111 -79
  369. package/skills/weather/SKILL.md +88 -25
@@ -1,7 +1,7 @@
1
1
  import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
2
2
  import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
3
3
  import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
4
- import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
4
+ import { createPoolbotCodingTools } from "../../agents/pi-tools.js";
5
5
  import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
6
6
  import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
7
7
  import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
@@ -37,7 +37,7 @@ export async function resolveCommandsSystemPromptBundle(params) {
37
37
  });
38
38
  const tools = (() => {
39
39
  try {
40
- return createOpenClawCodingTools({
40
+ return createPoolbotCodingTools({
41
41
  config: params.cfg,
42
42
  workspaceDir,
43
43
  sessionKey: params.sessionKey,
@@ -1,14 +1,15 @@
1
1
  import { resolveSessionAgentId } from "../../agents/agent-scope.js";
2
2
  import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
3
3
  import { logVerbose } from "../../globals.js";
4
+ import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
4
5
  import { isDiagnosticsEnabled } from "../../infra/diagnostic-events.js";
5
6
  import { logMessageProcessed, logMessageQueued, logSessionStateChange, } from "../../logging/diagnostic.js";
6
7
  import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
8
+ import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
7
9
  import { getReplyFromConfig } from "../reply.js";
8
10
  import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
9
11
  import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
10
12
  import { isRoutableChannel, routeReply } from "./route-reply.js";
11
- import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
12
13
  const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
13
14
  const AUDIO_HEADER_RE = /^\[Audio\b/i;
14
15
  const normalizeMediaType = (value) => value.split(";")[0]?.trim().toLowerCase();
@@ -18,8 +19,9 @@ const isInboundAudioContext = (ctx) => {
18
19
  ...(Array.isArray(ctx.MediaTypes) ? ctx.MediaTypes : []),
19
20
  ].filter(Boolean);
20
21
  const types = rawTypes.map((type) => normalizeMediaType(type));
21
- if (types.some((type) => type === "audio" || type.startsWith("audio/")))
22
+ if (types.some((type) => type === "audio" || type.startsWith("audio/"))) {
22
23
  return true;
24
+ }
23
25
  const body = typeof ctx.BodyForCommands === "string"
24
26
  ? ctx.BodyForCommands
25
27
  : typeof ctx.CommandBody === "string"
@@ -30,17 +32,20 @@ const isInboundAudioContext = (ctx) => {
30
32
  ? ctx.Body
31
33
  : "";
32
34
  const trimmed = body.trim();
33
- if (!trimmed)
35
+ if (!trimmed) {
34
36
  return false;
35
- if (AUDIO_PLACEHOLDER_RE.test(trimmed))
37
+ }
38
+ if (AUDIO_PLACEHOLDER_RE.test(trimmed)) {
36
39
  return true;
40
+ }
37
41
  return AUDIO_HEADER_RE.test(trimmed);
38
42
  };
39
43
  const resolveSessionTtsAuto = (ctx, cfg) => {
40
44
  const targetSessionKey = ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined;
41
45
  const sessionKey = (targetSessionKey ?? ctx.SessionKey)?.trim();
42
- if (!sessionKey)
46
+ if (!sessionKey) {
43
47
  return undefined;
48
+ }
44
49
  const agentId = resolveSessionAgentId({ sessionKey, config: cfg });
45
50
  const storePath = resolveStorePath(cfg.session?.store, { agentId });
46
51
  try {
@@ -62,8 +67,9 @@ export async function dispatchReplyFromConfig(params) {
62
67
  const startTime = diagnosticsEnabled ? Date.now() : 0;
63
68
  const canTrackSession = diagnosticsEnabled && Boolean(sessionKey);
64
69
  const recordProcessed = (outcome, opts) => {
65
- if (!diagnosticsEnabled)
70
+ if (!diagnosticsEnabled) {
66
71
  return;
72
+ }
67
73
  logMessageProcessed({
68
74
  channel,
69
75
  chatId,
@@ -76,8 +82,9 @@ export async function dispatchReplyFromConfig(params) {
76
82
  });
77
83
  };
78
84
  const markProcessing = () => {
79
- if (!canTrackSession || !sessionKey)
85
+ if (!canTrackSession || !sessionKey) {
80
86
  return;
87
+ }
81
88
  logMessageQueued({ sessionKey, channel, source: "dispatch" });
82
89
  logSessionStateChange({
83
90
  sessionKey,
@@ -86,8 +93,9 @@ export async function dispatchReplyFromConfig(params) {
86
93
  });
87
94
  };
88
95
  const markIdle = (reason) => {
89
- if (!canTrackSession || !sessionKey)
96
+ if (!canTrackSession || !sessionKey) {
90
97
  return;
98
+ }
91
99
  logSessionStateChange({
92
100
  sessionKey,
93
101
  state: "idle",
@@ -101,20 +109,20 @@ export async function dispatchReplyFromConfig(params) {
101
109
  const inboundAudio = isInboundAudioContext(ctx);
102
110
  const sessionTtsAuto = resolveSessionTtsAuto(ctx, cfg);
103
111
  const hookRunner = getGlobalHookRunner();
112
+ // Extract message context for hooks (plugin and internal)
113
+ const timestamp = typeof ctx.Timestamp === "number" && Number.isFinite(ctx.Timestamp) ? ctx.Timestamp : undefined;
114
+ const messageIdForHook = ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast;
115
+ const content = typeof ctx.BodyForCommands === "string"
116
+ ? ctx.BodyForCommands
117
+ : typeof ctx.RawBody === "string"
118
+ ? ctx.RawBody
119
+ : typeof ctx.Body === "string"
120
+ ? ctx.Body
121
+ : "";
122
+ const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
123
+ const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
124
+ // Trigger plugin hooks (fire-and-forget)
104
125
  if (hookRunner?.hasHooks("message_received")) {
105
- const timestamp = typeof ctx.Timestamp === "number" && Number.isFinite(ctx.Timestamp)
106
- ? ctx.Timestamp
107
- : undefined;
108
- const messageIdForHook = ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast;
109
- const content = typeof ctx.BodyForCommands === "string"
110
- ? ctx.BodyForCommands
111
- : typeof ctx.RawBody === "string"
112
- ? ctx.RawBody
113
- : typeof ctx.Body === "string"
114
- ? ctx.Body
115
- : "";
116
- const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
117
- const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
118
126
  void hookRunner
119
127
  .runMessageReceived({
120
128
  from: ctx.From ?? "",
@@ -139,7 +147,31 @@ export async function dispatchReplyFromConfig(params) {
139
147
  conversationId,
140
148
  })
141
149
  .catch((err) => {
142
- logVerbose(`dispatch-from-config: message_received hook failed: ${String(err)}`);
150
+ logVerbose(`dispatch-from-config: message_received plugin hook failed: ${String(err)}`);
151
+ });
152
+ }
153
+ // Bridge to internal hooks (HOOK.md discovery system) - refs #8807
154
+ if (sessionKey) {
155
+ void triggerInternalHook(createInternalHookEvent("message", "received", sessionKey, {
156
+ from: ctx.From ?? "",
157
+ content,
158
+ timestamp,
159
+ channelId,
160
+ accountId: ctx.AccountId,
161
+ conversationId,
162
+ messageId: messageIdForHook,
163
+ metadata: {
164
+ to: ctx.To,
165
+ provider: ctx.Provider,
166
+ surface: ctx.Surface,
167
+ threadId: ctx.MessageThreadId,
168
+ senderId: ctx.SenderId,
169
+ senderName: ctx.SenderName,
170
+ senderUsername: ctx.SenderUsername,
171
+ senderE164: ctx.SenderE164,
172
+ },
173
+ })).catch((err) => {
174
+ logVerbose(`dispatch-from-config: message_received internal hook failed: ${String(err)}`);
143
175
  });
144
176
  }
145
177
  // Check if we should route replies to originating channel instead of dispatcher.
@@ -163,10 +195,12 @@ export async function dispatchReplyFromConfig(params) {
163
195
  const sendPayloadAsync = async (payload, abortSignal, mirror) => {
164
196
  // TypeScript doesn't narrow these from the shouldRouteToOriginating check,
165
197
  // but they're guaranteed non-null when this function is called.
166
- if (!originatingChannel || !originatingTo)
198
+ if (!originatingChannel || !originatingTo) {
167
199
  return;
168
- if (abortSignal?.aborted)
200
+ }
201
+ if (abortSignal?.aborted) {
169
202
  return;
203
+ }
170
204
  const result = await routeReply({
171
205
  payload,
172
206
  channel: originatingChannel,
@@ -202,8 +236,9 @@ export async function dispatchReplyFromConfig(params) {
202
236
  cfg,
203
237
  });
204
238
  queuedFinal = result.ok;
205
- if (result.ok)
239
+ if (result.ok) {
206
240
  routedFinalCount += 1;
241
+ }
207
242
  if (!result.ok) {
208
243
  logVerbose(`dispatch-from-config: route-reply (abort) failed: ${result.error ?? "unknown error"}`);
209
244
  }
@@ -211,7 +246,6 @@ export async function dispatchReplyFromConfig(params) {
211
246
  else {
212
247
  queuedFinal = dispatcher.sendFinalReply(payload);
213
248
  }
214
- await dispatcher.waitForIdle();
215
249
  const counts = dispatcher.getQueuedCounts();
216
250
  counts.final += routedFinalCount;
217
251
  recordProcessed("completed", { reason: "fast_abort" });
@@ -223,8 +257,44 @@ export async function dispatchReplyFromConfig(params) {
223
257
  // TTS audio separately from the accumulated block content.
224
258
  let accumulatedBlockText = "";
225
259
  let blockCount = 0;
260
+ const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native";
261
+ const resolveToolDeliveryPayload = (payload) => {
262
+ if (shouldSendToolSummaries) {
263
+ return payload;
264
+ }
265
+ // Group/native flows intentionally suppress tool summary text, but media-only
266
+ // tool results (for example TTS audio) must still be delivered.
267
+ const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
268
+ if (!hasMedia) {
269
+ return null;
270
+ }
271
+ return { ...payload, text: undefined };
272
+ };
226
273
  const replyResult = await (params.replyResolver ?? getReplyFromConfig)(ctx, {
227
274
  ...params.replyOptions,
275
+ onToolResult: (payload) => {
276
+ const run = async () => {
277
+ const ttsPayload = await maybeApplyTtsToPayload({
278
+ payload,
279
+ cfg,
280
+ channel: ttsChannel,
281
+ kind: "tool",
282
+ inboundAudio,
283
+ ttsAuto: sessionTtsAuto,
284
+ });
285
+ const deliveryPayload = resolveToolDeliveryPayload(ttsPayload);
286
+ if (!deliveryPayload) {
287
+ return;
288
+ }
289
+ if (shouldRouteToOriginating) {
290
+ await sendPayloadAsync(deliveryPayload, undefined, false);
291
+ }
292
+ else {
293
+ dispatcher.sendToolResult(deliveryPayload);
294
+ }
295
+ };
296
+ return run();
297
+ },
228
298
  onBlockReply: (payload, context) => {
229
299
  const run = async () => {
230
300
  // Accumulate block text for TTS generation after streaming
@@ -280,8 +350,9 @@ export async function dispatchReplyFromConfig(params) {
280
350
  logVerbose(`dispatch-from-config: route-reply (final) failed: ${result.error ?? "unknown error"}`);
281
351
  }
282
352
  queuedFinal = result.ok || queuedFinal;
283
- if (result.ok)
353
+ if (result.ok) {
284
354
  routedFinalCount += 1;
355
+ }
285
356
  }
286
357
  else {
287
358
  queuedFinal = dispatcher.sendFinalReply(ttsReply) || queuedFinal;
@@ -322,8 +393,9 @@ export async function dispatchReplyFromConfig(params) {
322
393
  cfg,
323
394
  });
324
395
  queuedFinal = result.ok || queuedFinal;
325
- if (result.ok)
396
+ if (result.ok) {
326
397
  routedFinalCount += 1;
398
+ }
327
399
  if (!result.ok) {
328
400
  logVerbose(`dispatch-from-config: route-reply (tts-only) failed: ${result.error ?? "unknown error"}`);
329
401
  }
@@ -338,7 +410,6 @@ export async function dispatchReplyFromConfig(params) {
338
410
  logVerbose(`dispatch-from-config: accumulated block TTS failed: ${err instanceof Error ? err.message : String(err)}`);
339
411
  }
340
412
  }
341
- await dispatcher.waitForIdle();
342
413
  const counts = dispatcher.getQueuedCounts();
343
414
  counts.final += routedFinalCount;
344
415
  recordProcessed("completed");
@@ -14,7 +14,7 @@ export function formatElevatedUnavailableMessage(params) {
14
14
  lines.push("- agents.list[].tools.elevated.enabled");
15
15
  lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
16
16
  if (params.sessionKey) {
17
- lines.push(`See: ${formatCliCommand(`openclaw sandbox explain --session ${params.sessionKey}`)}`);
17
+ lines.push(`See: ${formatCliCommand(`poolbot sandbox explain --session ${params.sessionKey}`)}`);
18
18
  }
19
19
  return lines.join("\n");
20
20
  }
@@ -10,10 +10,19 @@ function safeTrim(value) {
10
10
  export function buildInboundMetaSystemPrompt(ctx) {
11
11
  const chatType = normalizeChatType(ctx.ChatType);
12
12
  const isDirect = !chatType || chatType === "direct";
13
+ const messageId = safeTrim(ctx.MessageSid);
14
+ const messageIdFull = safeTrim(ctx.MessageSidFull);
15
+ const replyToId = safeTrim(ctx.ReplyToId);
16
+ const chatId = safeTrim(ctx.OriginatingTo);
13
17
  // Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.).
14
18
  // Those belong in the user-role "untrusted context" blocks.
15
19
  const payload = {
16
20
  schema: "poolbot.inbound_meta.v1",
21
+ message_id: messageId,
22
+ message_id_full: messageIdFull && messageIdFull !== messageId ? messageIdFull : undefined,
23
+ sender_id: safeTrim(ctx.SenderId),
24
+ chat_id: chatId,
25
+ reply_to_id: replyToId,
17
26
  channel: safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface) ?? safeTrim(ctx.Provider),
18
27
  provider: safeTrim(ctx.Provider),
19
28
  surface: safeTrim(ctx.Surface),
@@ -45,7 +54,9 @@ export function buildInboundUserContextPrefix(ctx) {
45
54
  const chatType = normalizeChatType(ctx.ChatType);
46
55
  const isDirect = !chatType || chatType === "direct";
47
56
  const conversationInfo = {
48
- conversation_label: safeTrim(ctx.ConversationLabel),
57
+ message_id: safeTrim(ctx.MessageSid),
58
+ conversation_label: isDirect ? undefined : safeTrim(ctx.ConversationLabel),
59
+ sender: safeTrim(ctx.SenderE164) ?? safeTrim(ctx.SenderId) ?? safeTrim(ctx.SenderUsername),
49
60
  group_subject: safeTrim(ctx.GroupSubject),
50
61
  group_channel: safeTrim(ctx.GroupChannel),
51
62
  group_space: safeTrim(ctx.GroupSpace),
@@ -1,9 +1,7 @@
1
1
  import { resolveAgentConfig } from "../../agents/agent-scope.js";
2
2
  import { getChannelDock } from "../../channels/dock.js";
3
3
  import { normalizeChannelId } from "../../channels/plugins/index.js";
4
- function escapeRegExp(text) {
5
- return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
- }
4
+ import { escapeRegExp } from "../../utils.js";
7
5
  function deriveMentionPatterns(identity) {
8
6
  const patterns = [];
9
7
  const name = identity?.name?.trim();
@@ -21,16 +19,18 @@ function deriveMentionPatterns(identity) {
21
19
  const BACKSPACE_CHAR = "\u0008";
22
20
  export const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
23
21
  function normalizeMentionPattern(pattern) {
24
- if (!pattern.includes(BACKSPACE_CHAR))
22
+ if (!pattern.includes(BACKSPACE_CHAR)) {
25
23
  return pattern;
24
+ }
26
25
  return pattern.split(BACKSPACE_CHAR).join("\\b");
27
26
  }
28
27
  function normalizeMentionPatterns(patterns) {
29
28
  return patterns.map(normalizeMentionPattern);
30
29
  }
31
30
  function resolveMentionPatterns(cfg, agentId) {
32
- if (!cfg)
31
+ if (!cfg) {
33
32
  return [];
33
+ }
34
34
  const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
35
35
  const agentGroupChat = agentConfig?.groupChat;
36
36
  if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
@@ -60,11 +60,13 @@ export function normalizeMentionText(text) {
60
60
  return (text ?? "").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "").toLowerCase();
61
61
  }
62
62
  export function matchesMentionPatterns(text, mentionRegexes) {
63
- if (mentionRegexes.length === 0)
63
+ if (mentionRegexes.length === 0) {
64
64
  return false;
65
+ }
65
66
  const cleaned = normalizeMentionText(text ?? "");
66
- if (!cleaned)
67
+ if (!cleaned) {
67
68
  return false;
69
+ }
68
70
  return mentionRegexes.some((re) => re.test(cleaned));
69
71
  }
70
72
  export function matchesMentionWithExplicit(params) {
@@ -72,11 +74,16 @@ export function matchesMentionWithExplicit(params) {
72
74
  const explicit = params.explicit?.isExplicitlyMentioned === true;
73
75
  const explicitAvailable = params.explicit?.canResolveExplicit === true;
74
76
  const hasAnyMention = params.explicit?.hasAnyMention === true;
75
- if (hasAnyMention && explicitAvailable)
76
- return explicit || params.mentionRegexes.some((re) => re.test(cleaned));
77
- if (!cleaned)
77
+ // Check transcript if text is empty and transcript is provided
78
+ const transcriptCleaned = params.transcript ? normalizeMentionText(params.transcript) : "";
79
+ const textToCheck = cleaned || transcriptCleaned;
80
+ if (hasAnyMention && explicitAvailable) {
81
+ return explicit || params.mentionRegexes.some((re) => re.test(textToCheck));
82
+ }
83
+ if (!textToCheck) {
78
84
  return explicit;
79
- return explicit || params.mentionRegexes.some((re) => re.test(cleaned));
85
+ }
86
+ return explicit || params.mentionRegexes.some((re) => re.test(textToCheck));
80
87
  }
81
88
  export function stripStructuralPrefixes(text) {
82
89
  // Ignore wrapper labels, timestamps, and sender prefixes so directive-only
@@ -1,19 +1,23 @@
1
+ import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers.js";
1
2
  import { stripHeartbeatToken } from "../heartbeat.js";
2
3
  import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
3
- import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers.js";
4
- import { resolveResponsePrefixTemplate, } from "./response-prefix-template.js";
5
4
  import { hasLineDirectives, parseLineDirectives } from "./line-directives.js";
5
+ import { resolveResponsePrefixTemplate, } from "./response-prefix-template.js";
6
6
  export function normalizeReplyPayload(payload, opts = {}) {
7
7
  const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
8
8
  const hasChannelData = Boolean(payload.channelData && Object.keys(payload.channelData).length > 0);
9
9
  const trimmed = payload.text?.trim() ?? "";
10
- if (!trimmed && !hasMedia && !hasChannelData)
10
+ if (!trimmed && !hasMedia && !hasChannelData) {
11
+ opts.onSkip?.("empty");
11
12
  return null;
13
+ }
12
14
  const silentToken = opts.silentToken ?? SILENT_REPLY_TOKEN;
13
15
  let text = payload.text ?? undefined;
14
16
  if (text && isSilentReplyText(text, silentToken)) {
15
- if (!hasMedia && !hasChannelData)
17
+ if (!hasMedia && !hasChannelData) {
18
+ opts.onSkip?.("silent");
16
19
  return null;
20
+ }
17
21
  text = "";
18
22
  }
19
23
  if (text && !trimmed) {
@@ -23,17 +27,22 @@ export function normalizeReplyPayload(payload, opts = {}) {
23
27
  const shouldStripHeartbeat = opts.stripHeartbeat ?? true;
24
28
  if (shouldStripHeartbeat && text?.includes(HEARTBEAT_TOKEN)) {
25
29
  const stripped = stripHeartbeatToken(text, { mode: "message" });
26
- if (stripped.didStrip)
30
+ if (stripped.didStrip) {
27
31
  opts.onHeartbeatStrip?.();
28
- if (stripped.shouldSkip && !hasMedia && !hasChannelData)
32
+ }
33
+ if (stripped.shouldSkip && !hasMedia && !hasChannelData) {
34
+ opts.onSkip?.("heartbeat");
29
35
  return null;
36
+ }
30
37
  text = stripped.text;
31
38
  }
32
39
  if (text) {
33
- text = sanitizeUserFacingText(text);
40
+ text = sanitizeUserFacingText(text, { errorContext: Boolean(payload.isError) });
34
41
  }
35
- if (!text?.trim() && !hasMedia && !hasChannelData)
42
+ if (!text?.trim() && !hasMedia && !hasChannelData) {
43
+ opts.onSkip?.("empty");
36
44
  return null;
45
+ }
37
46
  // Parse LINE-specific directives from text (quick_replies, location, confirm, buttons)
38
47
  let enrichedPayload = { ...payload, text };
39
48
  if (text && hasLineDirectives(text)) {
@@ -1,19 +1,21 @@
1
+ import { sleep } from "../../utils.js";
2
+ import { registerDispatcher } from "./dispatcher-registry.js";
1
3
  import { normalizeReplyPayload } from "./normalize-reply.js";
2
4
  const DEFAULT_HUMAN_DELAY_MIN_MS = 800;
3
5
  const DEFAULT_HUMAN_DELAY_MAX_MS = 2500;
4
6
  /** Generate a random delay within the configured range. */
5
7
  function getHumanDelay(config) {
6
8
  const mode = config?.mode ?? "off";
7
- if (mode === "off")
9
+ if (mode === "off") {
8
10
  return 0;
11
+ }
9
12
  const min = mode === "custom" ? (config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS) : DEFAULT_HUMAN_DELAY_MIN_MS;
10
13
  const max = mode === "custom" ? (config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS) : DEFAULT_HUMAN_DELAY_MAX_MS;
11
- if (max <= min)
14
+ if (max <= min) {
12
15
  return min;
16
+ }
13
17
  return Math.floor(Math.random() * (max - min + 1)) + min;
14
18
  }
15
- /** Sleep for a given number of milliseconds. */
16
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
19
  function normalizeReplyPayloadInternal(payload, opts) {
18
20
  // Prefer dynamic context provider over static context
19
21
  const prefixContext = opts.responsePrefixContextProvider?.() ?? opts.responsePrefixContext;
@@ -21,12 +23,16 @@ function normalizeReplyPayloadInternal(payload, opts) {
21
23
  responsePrefix: opts.responsePrefix,
22
24
  responsePrefixContext: prefixContext,
23
25
  onHeartbeatStrip: opts.onHeartbeatStrip,
26
+ onSkip: opts.onSkip,
24
27
  });
25
28
  }
26
29
  export function createReplyDispatcher(options) {
27
30
  let sendChain = Promise.resolve();
28
31
  // Track in-flight deliveries so we can emit a reliable "idle" signal.
29
- let pending = 0;
32
+ // Start with pending=1 as a "reservation" to prevent premature gateway restart.
33
+ // This is decremented when markComplete() is called to signal no more replies will come.
34
+ let pending = 1;
35
+ let completeCalled = false;
30
36
  // Track whether we've sent a block reply (for human delay - skip delay on first block).
31
37
  let sentFirstBlock = false;
32
38
  // Serialize outbound replies to preserve tool/block/final order.
@@ -35,24 +41,40 @@ export function createReplyDispatcher(options) {
35
41
  block: 0,
36
42
  final: 0,
37
43
  };
44
+ // Register this dispatcher globally for gateway restart coordination.
45
+ const { unregister } = registerDispatcher({
46
+ pending: () => pending,
47
+ waitForIdle: () => sendChain,
48
+ });
38
49
  const enqueue = (kind, payload) => {
39
- const normalized = normalizeReplyPayloadInternal(payload, options);
40
- if (!normalized)
50
+ const normalized = normalizeReplyPayloadInternal(payload, {
51
+ responsePrefix: options.responsePrefix,
52
+ responsePrefixContext: options.responsePrefixContext,
53
+ responsePrefixContextProvider: options.responsePrefixContextProvider,
54
+ onHeartbeatStrip: options.onHeartbeatStrip,
55
+ onSkip: (reason) => options.onSkip?.(payload, { kind, reason }),
56
+ });
57
+ if (!normalized) {
41
58
  return false;
59
+ }
42
60
  queuedCounts[kind] += 1;
43
61
  pending += 1;
44
62
  // Determine if we should add human-like delay (only for block replies after the first).
45
63
  const shouldDelay = kind === "block" && sentFirstBlock;
46
- if (kind === "block")
64
+ if (kind === "block") {
47
65
  sentFirstBlock = true;
66
+ }
48
67
  sendChain = sendChain
49
68
  .then(async () => {
50
69
  // Add human-like delay between block replies for natural rhythm.
51
70
  if (shouldDelay) {
52
71
  const delayMs = getHumanDelay(options.humanDelay);
53
- if (delayMs > 0)
72
+ if (delayMs > 0) {
54
73
  await sleep(delayMs);
74
+ }
55
75
  }
76
+ // Safe: deliver is called inside an async .then() callback, so even a synchronous
77
+ // throw becomes a rejection that flows through .catch()/.finally(), ensuring cleanup.
56
78
  await options.deliver(normalized, { kind });
57
79
  })
58
80
  .catch((err) => {
@@ -60,22 +82,51 @@ export function createReplyDispatcher(options) {
60
82
  })
61
83
  .finally(() => {
62
84
  pending -= 1;
85
+ // Clear reservation if:
86
+ // 1. pending is now 1 (just the reservation left)
87
+ // 2. markComplete has been called
88
+ // 3. No more replies will be enqueued
89
+ if (pending === 1 && completeCalled) {
90
+ pending -= 1; // Clear the reservation
91
+ }
63
92
  if (pending === 0) {
93
+ // Unregister from global tracking when idle.
94
+ unregister();
64
95
  options.onIdle?.();
65
96
  }
66
97
  });
67
98
  return true;
68
99
  };
100
+ const markComplete = () => {
101
+ if (completeCalled) {
102
+ return;
103
+ }
104
+ completeCalled = true;
105
+ // If no replies were enqueued (pending is still 1 = just the reservation),
106
+ // schedule clearing the reservation after current microtasks complete.
107
+ // This gives any in-flight enqueue() calls a chance to increment pending.
108
+ void Promise.resolve().then(() => {
109
+ if (pending === 1 && completeCalled) {
110
+ // Still just the reservation, no replies were enqueued
111
+ pending -= 1;
112
+ if (pending === 0) {
113
+ unregister();
114
+ options.onIdle?.();
115
+ }
116
+ }
117
+ });
118
+ };
69
119
  return {
70
120
  sendToolResult: (payload) => enqueue("tool", payload),
71
121
  sendBlockReply: (payload) => enqueue("block", payload),
72
122
  sendFinalReply: (payload) => enqueue("final", payload),
73
123
  waitForIdle: () => sendChain,
74
124
  getQueuedCounts: () => ({ ...queuedCounts }),
125
+ markComplete,
75
126
  };
76
127
  }
77
128
  export function createReplyDispatcherWithTyping(options) {
78
- const { onReplyStart, onIdle, ...dispatcherOptions } = options;
129
+ const { onReplyStart, onIdle, onCleanup, ...dispatcherOptions } = options;
79
130
  let typingController;
80
131
  const dispatcher = createReplyDispatcher({
81
132
  ...dispatcherOptions,
@@ -88,6 +139,7 @@ export function createReplyDispatcherWithTyping(options) {
88
139
  dispatcher,
89
140
  replyOptions: {
90
141
  onReplyStart,
142
+ onTypingCleanup: onCleanup,
91
143
  onTypingController: (typing) => {
92
144
  typingController = typing;
93
145
  },