@poolzin/pool-bot 2026.2.21 → 2026.2.23

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 (378) hide show
  1. package/CHANGELOG.md +25 -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/device-pair/index.ts +2 -2
  326. package/extensions/diagnostics-otel/package.json +1 -1
  327. package/extensions/discord/package.json +1 -1
  328. package/extensions/feishu/package.json +1 -1
  329. package/extensions/google-antigravity-auth/package.json +1 -1
  330. package/extensions/google-gemini-cli-auth/package.json +1 -1
  331. package/extensions/googlechat/package.json +1 -1
  332. package/extensions/imessage/package.json +1 -1
  333. package/extensions/irc/package.json +1 -1
  334. package/extensions/irc/src/accounts.ts +1 -1
  335. package/extensions/irc/src/onboarding.ts +4 -4
  336. package/extensions/line/package.json +1 -1
  337. package/extensions/llm-task/package.json +1 -1
  338. package/extensions/lobster/package.json +1 -1
  339. package/extensions/matrix/CHANGELOG.md +10 -0
  340. package/extensions/matrix/package.json +1 -1
  341. package/extensions/mattermost/package.json +1 -1
  342. package/extensions/memory-core/package.json +1 -1
  343. package/extensions/memory-lancedb/package.json +1 -1
  344. package/extensions/minimax-portal-auth/package.json +1 -1
  345. package/extensions/msteams/CHANGELOG.md +10 -0
  346. package/extensions/msteams/package.json +1 -1
  347. package/extensions/nextcloud-talk/package.json +1 -1
  348. package/extensions/nostr/CHANGELOG.md +10 -0
  349. package/extensions/nostr/package.json +1 -1
  350. package/extensions/open-prose/package.json +1 -1
  351. package/extensions/openai-codex-auth/package.json +1 -1
  352. package/extensions/signal/package.json +1 -1
  353. package/extensions/slack/package.json +1 -1
  354. package/extensions/telegram/package.json +1 -1
  355. package/extensions/tlon/package.json +1 -1
  356. package/extensions/twitch/CHANGELOG.md +10 -0
  357. package/extensions/twitch/package.json +1 -1
  358. package/extensions/voice-call/CHANGELOG.md +10 -0
  359. package/extensions/voice-call/package.json +1 -1
  360. package/extensions/whatsapp/package.json +1 -1
  361. package/extensions/zalo/CHANGELOG.md +10 -0
  362. package/extensions/zalo/package.json +1 -1
  363. package/extensions/zalouser/CHANGELOG.md +10 -0
  364. package/extensions/zalouser/package.json +1 -1
  365. package/package.json +1 -1
  366. package/skills/apple-reminders/SKILL.md +100 -49
  367. package/skills/coding-agent/SKILL.md +34 -28
  368. package/skills/github/SKILL.md +131 -16
  369. package/skills/imsg/SKILL.md +112 -15
  370. package/skills/openhue/SKILL.md +101 -19
  371. package/skills/tmux/SKILL.md +111 -79
  372. package/skills/weather/SKILL.md +88 -25
  373. package/dist/agents/openclaw-tools.js +0 -151
  374. package/dist/agents/tool-security.js +0 -96
  375. package/dist/gateway/url-validation.js +0 -94
  376. package/dist/infra/openclaw-root.js +0 -109
  377. package/dist/infra/tmp-openclaw-dir.js +0 -81
  378. package/dist/media/path-sanitization.js +0 -78
@@ -1,10 +1,18 @@
1
1
  import { DEFAULT_AGENT_ID } from "../../routing/session-key.js";
2
2
  import { resolveCronDeliveryPlan } from "../delivery.js";
3
3
  import { sweepCronRunSessions } from "../session-reaper.js";
4
- import { computeJobNextRunAtMs, nextWakeAtMs, recomputeNextRuns, resolveJobPayloadTextForMain, } from "./jobs.js";
4
+ import { computeJobNextRunAtMs, nextWakeAtMs, recomputeNextRunsForMaintenance, resolveJobPayloadTextForMain, } from "./jobs.js";
5
5
  import { locked } from "./locked.js";
6
6
  import { ensureLoaded, persist } from "./store.js";
7
7
  const MAX_TIMER_DELAY_MS = 60_000;
8
+ /**
9
+ * Minimum gap between consecutive fires of the same cron job. This is a
10
+ * safety net that prevents spin-loops when `computeJobNextRunAtMs` returns
11
+ * a value within the same second as the just-completed run. The guard
12
+ * is intentionally generous (2 s) so it never masks a legitimate schedule
13
+ * but always breaks an infinite re-trigger cycle. (See #17821)
14
+ */
15
+ const MIN_REFIRE_GAP_MS = 2_000;
8
16
  /**
9
17
  * Maximum wall-clock time for a single job execution. Acts as a safety net
10
18
  * on top of the per-provider / per-agent timeouts to prevent one stuck job
@@ -45,7 +53,7 @@ function applyJobResult(state, job, result) {
45
53
  else {
46
54
  job.state.consecutiveErrors = 0;
47
55
  }
48
- const shouldDelete = job.schedule.kind === "at" && result.status === "ok" && job.deleteAfterRun === true;
56
+ const shouldDelete = job.schedule.kind === "at" && job.deleteAfterRun === true && result.status === "ok";
49
57
  if (!shouldDelete) {
50
58
  if (job.schedule.kind === "at") {
51
59
  // One-shot jobs are always disabled after ANY terminal status
@@ -78,7 +86,19 @@ function applyJobResult(state, job, result) {
78
86
  }, "cron: applying error backoff");
79
87
  }
80
88
  else if (job.enabled) {
81
- job.state.nextRunAtMs = computeJobNextRunAtMs(job, result.endedAt);
89
+ const naturalNext = computeJobNextRunAtMs(job, result.endedAt);
90
+ if (job.schedule.kind === "cron") {
91
+ // Safety net: ensure the next fire is at least MIN_REFIRE_GAP_MS
92
+ // after the current run ended. Prevents spin-loops when the
93
+ // schedule computation lands in the same second due to
94
+ // timezone/croner edge cases (see #17821).
95
+ const minNext = result.endedAt + MIN_REFIRE_GAP_MS;
96
+ job.state.nextRunAtMs =
97
+ naturalNext !== undefined ? Math.max(naturalNext, minNext) : minNext;
98
+ }
99
+ else {
100
+ job.state.nextRunAtMs = naturalNext;
101
+ }
82
102
  }
83
103
  else {
84
104
  job.state.nextRunAtMs = undefined;
@@ -109,18 +129,36 @@ export function armTimer(state) {
109
129
  // Wake at least once a minute to avoid schedule drift and recover quickly
110
130
  // when the process was paused or wall-clock time jumps.
111
131
  const clampedDelay = Math.min(delay, MAX_TIMER_DELAY_MS);
112
- state.timer = setTimeout(async () => {
113
- try {
114
- await onTimer(state);
115
- }
116
- catch (err) {
132
+ // Intentionally avoid an `async` timer callback:
133
+ // Vitest's fake-timer helpers can await async callbacks, which would block
134
+ // tests that simulate long-running jobs. Runtime behavior is unchanged.
135
+ state.timer = setTimeout(() => {
136
+ void onTimer(state).catch((err) => {
117
137
  state.deps.log.error({ err: String(err) }, "cron: timer tick failed");
118
- }
138
+ });
119
139
  }, clampedDelay);
120
140
  state.deps.log.debug({ nextAt, delayMs: clampedDelay, clamped: delay > MAX_TIMER_DELAY_MS }, "cron: timer armed");
121
141
  }
122
142
  export async function onTimer(state) {
123
143
  if (state.running) {
144
+ // Re-arm the timer so the scheduler keeps ticking even when a job is
145
+ // still executing. Without this, a long-running job (e.g. an agentTurn
146
+ // exceeding MAX_TIMER_DELAY_MS) causes the clamped 60 s timer to fire
147
+ // while `running` is true. The early return then leaves no timer set,
148
+ // silently killing the scheduler until the next gateway restart.
149
+ //
150
+ // We use MAX_TIMER_DELAY_MS as a fixed re-check interval to avoid a
151
+ // zero-delay hot-loop when past-due jobs are waiting for the current
152
+ // execution to finish.
153
+ // See: https://github.com/poolbot/poolbot/issues/12025
154
+ if (state.timer) {
155
+ clearTimeout(state.timer);
156
+ }
157
+ state.timer = setTimeout(() => {
158
+ void onTimer(state).catch((err) => {
159
+ state.deps.log.error({ err: String(err) }, "cron: timer tick failed");
160
+ });
161
+ }, MAX_TIMER_DELAY_MS);
124
162
  return;
125
163
  }
126
164
  state.running = true;
@@ -129,7 +167,10 @@ export async function onTimer(state) {
129
167
  await ensureLoaded(state, { forceReload: true, skipRecompute: true });
130
168
  const due = findDueJobs(state);
131
169
  if (due.length === 0) {
132
- const changed = recomputeNextRuns(state);
170
+ // Use maintenance-only recompute to avoid advancing past-due nextRunAtMs
171
+ // values without execution. This prevents jobs from being silently skipped
172
+ // when the timer wakes up but findDueJobs returns empty (see #13992).
173
+ const changed = recomputeNextRunsForMaintenance(state);
133
174
  if (changed) {
134
175
  await persist(state);
135
176
  }
@@ -151,21 +192,37 @@ export async function onTimer(state) {
151
192
  const startedAt = state.deps.nowMs();
152
193
  job.state.runningAtMs = startedAt;
153
194
  emit(state, { jobId: job.id, action: "started", runAtMs: startedAt });
154
- const jobTimeoutMs = job.payload.kind === "agentTurn" && typeof job.payload.timeoutSeconds === "number"
155
- ? job.payload.timeoutSeconds * 1_000
195
+ const configuredTimeoutMs = job.payload.kind === "agentTurn" && typeof job.payload.timeoutSeconds === "number"
196
+ ? Math.floor(job.payload.timeoutSeconds * 1_000)
197
+ : undefined;
198
+ const jobTimeoutMs = configuredTimeoutMs !== undefined
199
+ ? configuredTimeoutMs <= 0
200
+ ? undefined
201
+ : configuredTimeoutMs
156
202
  : DEFAULT_JOB_TIMEOUT_MS;
157
203
  try {
158
- let timeoutId;
159
- const result = await Promise.race([
160
- executeJobCore(state, job),
161
- new Promise((_, reject) => {
162
- timeoutId = setTimeout(() => reject(new Error("cron: job execution timed out")), jobTimeoutMs);
163
- }),
164
- ]).finally(() => clearTimeout(timeoutId));
204
+ const result = typeof jobTimeoutMs === "number"
205
+ ? await (async () => {
206
+ let timeoutId;
207
+ try {
208
+ return await Promise.race([
209
+ executeJobCore(state, job),
210
+ new Promise((_, reject) => {
211
+ timeoutId = setTimeout(() => reject(new Error("cron: job execution timed out")), jobTimeoutMs);
212
+ }),
213
+ ]);
214
+ }
215
+ finally {
216
+ if (timeoutId) {
217
+ clearTimeout(timeoutId);
218
+ }
219
+ }
220
+ })()
221
+ : await executeJobCore(state, job);
165
222
  results.push({ jobId: id, ...result, startedAt, endedAt: state.deps.nowMs() });
166
223
  }
167
224
  catch (err) {
168
- state.deps.log.warn({ jobId: id, jobName: job.name, timeoutMs: jobTimeoutMs }, `cron: job failed: ${String(err)}`);
225
+ state.deps.log.warn({ jobId: id, jobName: job.name, timeoutMs: jobTimeoutMs ?? null }, `cron: job failed: ${String(err)}`);
169
226
  results.push({
170
227
  jobId: id,
171
228
  status: "error",
@@ -189,24 +246,18 @@ export async function onTimer(state) {
189
246
  startedAt: result.startedAt,
190
247
  endedAt: result.endedAt,
191
248
  });
192
- emit(state, {
193
- jobId: job.id,
194
- action: "finished",
195
- status: result.status,
196
- error: result.error,
197
- summary: result.summary,
198
- sessionId: result.sessionId,
199
- sessionKey: result.sessionKey,
200
- runAtMs: result.startedAt,
201
- durationMs: job.state.lastDurationMs,
202
- nextRunAtMs: job.state.nextRunAtMs,
203
- });
249
+ emitJobFinished(state, job, result, result.startedAt);
204
250
  if (shouldDelete && state.store) {
205
251
  state.store.jobs = state.store.jobs.filter((j) => j.id !== job.id);
206
252
  emit(state, { jobId: job.id, action: "removed" });
207
253
  }
208
254
  }
209
- recomputeNextRuns(state);
255
+ // Use maintenance-only recompute to avoid advancing past-due
256
+ // nextRunAtMs values that became due between findDueJobs and this
257
+ // locked block. The full recomputeNextRuns would silently skip
258
+ // those jobs (advancing nextRunAtMs without execution), causing
259
+ // daily cron schedules to jump 48 h instead of 24 h (#17852).
260
+ recomputeNextRunsForMaintenance(state);
210
261
  await persist(state);
211
262
  });
212
263
  }
@@ -254,41 +305,49 @@ function findDueJobs(state) {
254
305
  return [];
255
306
  }
256
307
  const now = state.deps.nowMs();
257
- return state.store.jobs.filter((j) => {
258
- if (!j.state) {
259
- j.state = {};
260
- }
261
- if (!j.enabled) {
262
- return false;
263
- }
264
- if (typeof j.state.runningAtMs === "number") {
265
- return false;
266
- }
267
- const next = j.state.nextRunAtMs;
268
- return typeof next === "number" && now >= next;
269
- });
308
+ return collectRunnableJobs(state, now);
309
+ }
310
+ function isRunnableJob(params) {
311
+ const { job, nowMs } = params;
312
+ if (!job.state) {
313
+ job.state = {};
314
+ }
315
+ if (!job.enabled) {
316
+ return false;
317
+ }
318
+ if (params.skipJobIds?.has(job.id)) {
319
+ return false;
320
+ }
321
+ if (typeof job.state.runningAtMs === "number") {
322
+ return false;
323
+ }
324
+ if (params.skipAtIfAlreadyRan && job.schedule.kind === "at" && job.state.lastStatus) {
325
+ // Any terminal status (ok, error, skipped) means the job already ran at least once.
326
+ // Don't re-fire it on restart — applyJobResult disables one-shot jobs, but guard
327
+ // here defensively (#13845).
328
+ return false;
329
+ }
330
+ const next = job.state.nextRunAtMs;
331
+ return typeof next === "number" && nowMs >= next;
332
+ }
333
+ function collectRunnableJobs(state, nowMs, opts) {
334
+ if (!state.store) {
335
+ return [];
336
+ }
337
+ return state.store.jobs.filter((job) => isRunnableJob({
338
+ job,
339
+ nowMs,
340
+ skipJobIds: opts?.skipJobIds,
341
+ skipAtIfAlreadyRan: opts?.skipAtIfAlreadyRan,
342
+ }));
270
343
  }
271
- export async function runMissedJobs(state) {
344
+ export async function runMissedJobs(state, opts) {
272
345
  if (!state.store) {
273
346
  return;
274
347
  }
275
348
  const now = state.deps.nowMs();
276
- const missed = state.store.jobs.filter((j) => {
277
- if (!j.state) {
278
- j.state = {};
279
- }
280
- if (!j.enabled) {
281
- return false;
282
- }
283
- if (typeof j.state.runningAtMs === "number") {
284
- return false;
285
- }
286
- const next = j.state.nextRunAtMs;
287
- if (j.schedule.kind === "at" && j.state.lastStatus === "ok") {
288
- return false;
289
- }
290
- return typeof next === "number" && now >= next;
291
- });
349
+ const skipJobIds = opts?.skipJobIds;
350
+ const missed = collectRunnableJobs(state, now, { skipJobIds, skipAtIfAlreadyRan: true });
292
351
  if (missed.length > 0) {
293
352
  state.deps.log.info({ count: missed.length, jobIds: missed.map((j) => j.id) }, "cron: running missed jobs after restart");
294
353
  for (const job of missed) {
@@ -301,19 +360,7 @@ export async function runDueJobs(state) {
301
360
  return;
302
361
  }
303
362
  const now = state.deps.nowMs();
304
- const due = state.store.jobs.filter((j) => {
305
- if (!j.state) {
306
- j.state = {};
307
- }
308
- if (!j.enabled) {
309
- return false;
310
- }
311
- if (typeof j.state.runningAtMs === "number") {
312
- return false;
313
- }
314
- const next = j.state.nextRunAtMs;
315
- return typeof next === "number" && now >= next;
316
- });
363
+ const due = collectRunnableJobs(state, now);
317
364
  for (const job of due) {
318
365
  await executeJob(state, job, now, { forced: false });
319
366
  }
@@ -330,24 +377,37 @@ async function executeJobCore(state, job) {
330
377
  : 'main job requires payload.kind="systemEvent"',
331
378
  };
332
379
  }
333
- state.deps.enqueueSystemEvent(text, { agentId: job.agentId });
380
+ state.deps.enqueueSystemEvent(text, {
381
+ agentId: job.agentId,
382
+ sessionKey: job.sessionKey,
383
+ contextKey: `cron:${job.id}`,
384
+ });
334
385
  if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
335
386
  const reason = `cron:${job.id}`;
336
387
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
337
- const maxWaitMs = 2 * 60_000;
388
+ const maxWaitMs = state.deps.wakeNowHeartbeatBusyMaxWaitMs ?? 2 * 60_000;
389
+ const retryDelayMs = state.deps.wakeNowHeartbeatBusyRetryDelayMs ?? 250;
338
390
  const waitStartedAt = state.deps.nowMs();
339
391
  let heartbeatResult;
340
392
  for (;;) {
341
- heartbeatResult = await state.deps.runHeartbeatOnce({ reason });
393
+ heartbeatResult = await state.deps.runHeartbeatOnce({
394
+ reason,
395
+ agentId: job.agentId,
396
+ sessionKey: job.sessionKey,
397
+ });
342
398
  if (heartbeatResult.status !== "skipped" ||
343
399
  heartbeatResult.reason !== "requests-in-flight") {
344
400
  break;
345
401
  }
346
402
  if (state.deps.nowMs() - waitStartedAt > maxWaitMs) {
347
- state.deps.requestHeartbeatNow({ reason });
403
+ state.deps.requestHeartbeatNow({
404
+ reason,
405
+ agentId: job.agentId,
406
+ sessionKey: job.sessionKey,
407
+ });
348
408
  return { status: "ok", summary: text };
349
409
  }
350
- await delay(250);
410
+ await delay(retryDelayMs);
351
411
  }
352
412
  if (heartbeatResult.status === "ran") {
353
413
  return { status: "ok", summary: text };
@@ -360,7 +420,11 @@ async function executeJobCore(state, job) {
360
420
  }
361
421
  }
362
422
  else {
363
- state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
423
+ state.deps.requestHeartbeatNow({
424
+ reason: `cron:${job.id}`,
425
+ agentId: job.agentId,
426
+ sessionKey: job.sessionKey,
427
+ });
364
428
  return { status: "ok", summary: text };
365
429
  }
366
430
  }
@@ -371,15 +435,28 @@ async function executeJobCore(state, job) {
371
435
  job,
372
436
  message: job.payload.message,
373
437
  });
374
- // Post a short summary back to the main session.
438
+ // Post a short summary back to the main session — but only when the
439
+ // isolated run did NOT already deliver its output to the target channel.
440
+ // When `res.delivered` is true the announce flow (or direct outbound
441
+ // delivery) already sent the result, so posting the summary to main
442
+ // would wake the main agent and cause a duplicate message.
443
+ // See: https://github.com/poolbot/poolbot/issues/15692
375
444
  const summaryText = res.summary?.trim();
376
445
  const deliveryPlan = resolveCronDeliveryPlan(job);
377
- if (summaryText && deliveryPlan.requested) {
446
+ if (summaryText && deliveryPlan.requested && !res.delivered) {
378
447
  const prefix = "Cron";
379
448
  const label = res.status === "error" ? `${prefix} (error): ${summaryText}` : `${prefix}: ${summaryText}`;
380
- state.deps.enqueueSystemEvent(label, { agentId: job.agentId });
449
+ state.deps.enqueueSystemEvent(label, {
450
+ agentId: job.agentId,
451
+ sessionKey: job.sessionKey,
452
+ contextKey: `cron:${job.id}`,
453
+ });
381
454
  if (job.wakeMode === "now") {
382
- state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
455
+ state.deps.requestHeartbeatNow({
456
+ reason: `cron:${job.id}`,
457
+ agentId: job.agentId,
458
+ sessionKey: job.sessionKey,
459
+ });
383
460
  }
384
461
  }
385
462
  return {
@@ -388,6 +465,9 @@ async function executeJobCore(state, job) {
388
465
  summary: res.summary,
389
466
  sessionId: res.sessionId,
390
467
  sessionKey: res.sessionKey,
468
+ model: res.model,
469
+ provider: res.provider,
470
+ usage: res.usage,
391
471
  };
392
472
  }
393
473
  /**
@@ -416,22 +496,28 @@ export async function executeJob(state, job, _nowMs, _opts) {
416
496
  startedAt,
417
497
  endedAt,
418
498
  });
499
+ emitJobFinished(state, job, coreResult, startedAt);
500
+ if (shouldDelete && state.store) {
501
+ state.store.jobs = state.store.jobs.filter((j) => j.id !== job.id);
502
+ emit(state, { jobId: job.id, action: "removed" });
503
+ }
504
+ }
505
+ function emitJobFinished(state, job, result, runAtMs) {
419
506
  emit(state, {
420
507
  jobId: job.id,
421
508
  action: "finished",
422
- status: coreResult.status,
423
- error: coreResult.error,
424
- summary: coreResult.summary,
425
- sessionId: coreResult.sessionId,
426
- sessionKey: coreResult.sessionKey,
427
- runAtMs: startedAt,
509
+ status: result.status,
510
+ error: result.error,
511
+ summary: result.summary,
512
+ sessionId: result.sessionId,
513
+ sessionKey: result.sessionKey,
514
+ runAtMs,
428
515
  durationMs: job.state.lastDurationMs,
429
516
  nextRunAtMs: job.state.nextRunAtMs,
517
+ model: result.model,
518
+ provider: result.provider,
519
+ usage: result.usage,
430
520
  });
431
- if (shouldDelete && state.store) {
432
- state.store.jobs = state.store.jobs.filter((j) => j.id !== job.id);
433
- emit(state, { jobId: job.id, action: "removed" });
434
- }
435
521
  }
436
522
  export function wake(state, opts) {
437
523
  const text = opts.text.trim();
@@ -29,6 +29,9 @@ export class CronService {
29
29
  async run(id, mode) {
30
30
  return await ops.run(this.state, id, mode);
31
31
  }
32
+ getJob(id) {
33
+ return this.state.store?.jobs.find((job) => job.id === id);
34
+ }
32
35
  wake(opts) {
33
36
  return ops.wakeNow(this.state, opts);
34
37
  }
@@ -0,0 +1,37 @@
1
+ export const DEFAULT_TOP_OF_HOUR_STAGGER_MS = 5 * 60 * 1000;
2
+ function parseCronFields(expr) {
3
+ return expr.trim().split(/\s+/).filter(Boolean);
4
+ }
5
+ export function isRecurringTopOfHourCronExpr(expr) {
6
+ const fields = parseCronFields(expr);
7
+ if (fields.length === 5) {
8
+ const [minuteField, hourField] = fields;
9
+ return minuteField === "0" && hourField.includes("*");
10
+ }
11
+ if (fields.length === 6) {
12
+ const [secondField, minuteField, hourField] = fields;
13
+ return secondField === "0" && minuteField === "0" && hourField.includes("*");
14
+ }
15
+ return false;
16
+ }
17
+ export function normalizeCronStaggerMs(raw) {
18
+ const numeric = typeof raw === "number"
19
+ ? raw
20
+ : typeof raw === "string" && raw.trim()
21
+ ? Number(raw)
22
+ : Number.NaN;
23
+ if (!Number.isFinite(numeric)) {
24
+ return undefined;
25
+ }
26
+ return Math.max(0, Math.floor(numeric));
27
+ }
28
+ export function resolveDefaultCronStaggerMs(expr) {
29
+ return isRecurringTopOfHourCronExpr(expr) ? DEFAULT_TOP_OF_HOUR_STAGGER_MS : undefined;
30
+ }
31
+ export function resolveCronStaggerMs(schedule) {
32
+ const explicit = normalizeCronStaggerMs(schedule.staggerMs);
33
+ if (explicit !== undefined) {
34
+ return explicit;
35
+ }
36
+ return resolveDefaultCronStaggerMs(schedule.expr) ?? 0;
37
+ }