@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,7 +1,15 @@
1
1
  import crypto from "node:crypto";
2
+ import { parseAbsoluteTimeMs } from "../parse.js";
2
3
  import { computeNextRunAtMs } from "../schedule.js";
3
4
  import { normalizeOptionalAgentId, normalizeOptionalText, normalizePayloadToSystemText, normalizeRequiredName, } from "./normalize.js";
4
5
  const STUCK_RUN_MS = 2 * 60 * 60 * 1000;
6
+ function resolveEveryAnchorMs(params) {
7
+ const raw = params.schedule.anchorMs;
8
+ if (typeof raw === "number" && Number.isFinite(raw)) {
9
+ return Math.max(0, Math.floor(raw));
10
+ }
11
+ return Math.max(0, Math.floor(params.fallbackAnchorMs));
12
+ }
5
13
  export function assertSupportedJobSpec(job) {
6
14
  if (job.sessionTarget === "main" && job.payload.kind !== "systemEvent") {
7
15
  throw new Error('main cron jobs require payload.kind="systemEvent"');
@@ -10,100 +18,189 @@ export function assertSupportedJobSpec(job) {
10
18
  throw new Error('isolated cron jobs require payload.kind="agentTurn"');
11
19
  }
12
20
  }
21
+ function assertDeliverySupport(job) {
22
+ if (job.delivery && job.sessionTarget !== "isolated") {
23
+ throw new Error('cron delivery config is only supported for sessionTarget="isolated"');
24
+ }
25
+ }
13
26
  export function findJobOrThrow(state, id) {
14
27
  const job = state.store?.jobs.find((j) => j.id === id);
15
- if (!job)
28
+ if (!job) {
16
29
  throw new Error(`unknown cron job id: ${id}`);
30
+ }
17
31
  return job;
18
32
  }
19
33
  export function computeJobNextRunAtMs(job, nowMs) {
20
- if (!job.enabled)
34
+ if (!job.enabled) {
21
35
  return undefined;
36
+ }
37
+ if (job.schedule.kind === "every") {
38
+ const anchorMs = resolveEveryAnchorMs({
39
+ schedule: job.schedule,
40
+ fallbackAnchorMs: job.createdAtMs,
41
+ });
42
+ return computeNextRunAtMs({ ...job.schedule, anchorMs }, nowMs);
43
+ }
22
44
  if (job.schedule.kind === "at") {
23
45
  // One-shot jobs stay due until they successfully finish.
24
- if (job.state.lastStatus === "ok" && job.state.lastRunAtMs)
46
+ if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) {
25
47
  return undefined;
26
- return job.schedule.atMs;
48
+ }
49
+ // Handle both canonical `at` (string) and legacy `atMs` (number) fields.
50
+ // The store migration should convert atMs→at, but be defensive in case
51
+ // the migration hasn't run yet or was bypassed.
52
+ const schedule = job.schedule;
53
+ const atMs = typeof schedule.atMs === "number" && Number.isFinite(schedule.atMs) && schedule.atMs > 0
54
+ ? schedule.atMs
55
+ : typeof schedule.atMs === "string"
56
+ ? parseAbsoluteTimeMs(schedule.atMs)
57
+ : typeof schedule.at === "string"
58
+ ? parseAbsoluteTimeMs(schedule.at)
59
+ : null;
60
+ return atMs !== null ? atMs : undefined;
27
61
  }
28
62
  return computeNextRunAtMs(job.schedule, nowMs);
29
63
  }
30
64
  export function recomputeNextRuns(state) {
31
- if (!state.store)
32
- return;
65
+ if (!state.store) {
66
+ return false;
67
+ }
68
+ let changed = false;
33
69
  const now = state.deps.nowMs();
34
70
  for (const job of state.store.jobs) {
35
- if (!job.state)
71
+ if (!job.state) {
36
72
  job.state = {};
73
+ changed = true;
74
+ }
37
75
  if (!job.enabled) {
38
- job.state.nextRunAtMs = undefined;
39
- job.state.runningAtMs = undefined;
76
+ if (job.state.nextRunAtMs !== undefined) {
77
+ job.state.nextRunAtMs = undefined;
78
+ changed = true;
79
+ }
80
+ if (job.state.runningAtMs !== undefined) {
81
+ job.state.runningAtMs = undefined;
82
+ changed = true;
83
+ }
40
84
  continue;
41
85
  }
42
86
  const runningAt = job.state.runningAtMs;
43
87
  if (typeof runningAt === "number" && now - runningAt > STUCK_RUN_MS) {
44
88
  state.deps.log.warn({ jobId: job.id, runningAtMs: runningAt }, "cron: clearing stuck running marker");
45
89
  job.state.runningAtMs = undefined;
90
+ changed = true;
91
+ }
92
+ // Only recompute if nextRunAtMs is missing or already past-due.
93
+ // Preserving a still-future nextRunAtMs avoids accidentally advancing
94
+ // a job that hasn't fired yet (e.g. during restart recovery).
95
+ const nextRun = job.state.nextRunAtMs;
96
+ const isDueOrMissing = nextRun === undefined || now >= nextRun;
97
+ if (isDueOrMissing) {
98
+ const newNext = computeJobNextRunAtMs(job, now);
99
+ if (job.state.nextRunAtMs !== newNext) {
100
+ job.state.nextRunAtMs = newNext;
101
+ changed = true;
102
+ }
46
103
  }
47
- job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
48
104
  }
105
+ return changed;
49
106
  }
50
107
  export function nextWakeAtMs(state) {
51
108
  const jobs = state.store?.jobs ?? [];
52
109
  const enabled = jobs.filter((j) => j.enabled && typeof j.state.nextRunAtMs === "number");
53
- if (enabled.length === 0)
110
+ if (enabled.length === 0) {
54
111
  return undefined;
112
+ }
55
113
  return enabled.reduce((min, j) => Math.min(min, j.state.nextRunAtMs), enabled[0].state.nextRunAtMs);
56
114
  }
57
115
  export function createJob(state, input) {
58
116
  const now = state.deps.nowMs();
59
117
  const id = crypto.randomUUID();
118
+ const schedule = input.schedule.kind === "every"
119
+ ? {
120
+ ...input.schedule,
121
+ anchorMs: resolveEveryAnchorMs({
122
+ schedule: input.schedule,
123
+ fallbackAnchorMs: now,
124
+ }),
125
+ }
126
+ : input.schedule;
127
+ const deleteAfterRun = typeof input.deleteAfterRun === "boolean"
128
+ ? input.deleteAfterRun
129
+ : schedule.kind === "at"
130
+ ? true
131
+ : undefined;
132
+ const enabled = typeof input.enabled === "boolean" ? input.enabled : true;
60
133
  const job = {
61
134
  id,
62
135
  agentId: normalizeOptionalAgentId(input.agentId),
63
136
  name: normalizeRequiredName(input.name),
64
137
  description: normalizeOptionalText(input.description),
65
- enabled: input.enabled !== false,
66
- deleteAfterRun: input.deleteAfterRun,
138
+ enabled,
139
+ deleteAfterRun,
67
140
  createdAtMs: now,
68
141
  updatedAtMs: now,
69
- schedule: input.schedule,
142
+ schedule,
70
143
  sessionTarget: input.sessionTarget,
71
144
  wakeMode: input.wakeMode,
72
145
  payload: input.payload,
73
- isolation: input.isolation,
146
+ delivery: input.delivery,
74
147
  state: {
75
148
  ...input.state,
76
149
  },
77
150
  };
78
151
  assertSupportedJobSpec(job);
152
+ assertDeliverySupport(job);
79
153
  job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
80
154
  return job;
81
155
  }
82
156
  export function applyJobPatch(job, patch) {
83
- if ("name" in patch)
157
+ if ("name" in patch) {
84
158
  job.name = normalizeRequiredName(patch.name);
85
- if ("description" in patch)
159
+ }
160
+ if ("description" in patch) {
86
161
  job.description = normalizeOptionalText(patch.description);
87
- if (typeof patch.enabled === "boolean")
162
+ }
163
+ if (typeof patch.enabled === "boolean") {
88
164
  job.enabled = patch.enabled;
89
- if (typeof patch.deleteAfterRun === "boolean")
165
+ }
166
+ if (typeof patch.deleteAfterRun === "boolean") {
90
167
  job.deleteAfterRun = patch.deleteAfterRun;
91
- if (patch.schedule)
168
+ }
169
+ if (patch.schedule) {
92
170
  job.schedule = patch.schedule;
93
- if (patch.sessionTarget)
171
+ }
172
+ if (patch.sessionTarget) {
94
173
  job.sessionTarget = patch.sessionTarget;
95
- if (patch.wakeMode)
174
+ }
175
+ if (patch.wakeMode) {
96
176
  job.wakeMode = patch.wakeMode;
97
- if (patch.payload)
177
+ }
178
+ if (patch.payload) {
98
179
  job.payload = mergeCronPayload(job.payload, patch.payload);
99
- if (patch.isolation)
100
- job.isolation = patch.isolation;
101
- if (patch.state)
180
+ }
181
+ if (!patch.delivery && patch.payload?.kind === "agentTurn") {
182
+ // Back-compat: legacy clients still update delivery via payload fields.
183
+ const legacyDeliveryPatch = buildLegacyDeliveryPatch(patch.payload);
184
+ if (legacyDeliveryPatch &&
185
+ job.sessionTarget === "isolated" &&
186
+ job.payload.kind === "agentTurn") {
187
+ job.delivery = mergeCronDelivery(job.delivery, legacyDeliveryPatch);
188
+ }
189
+ }
190
+ if (patch.delivery) {
191
+ job.delivery = mergeCronDelivery(job.delivery, patch.delivery);
192
+ }
193
+ if (job.sessionTarget === "main" && job.delivery) {
194
+ job.delivery = undefined;
195
+ }
196
+ if (patch.state) {
102
197
  job.state = { ...job.state, ...patch.state };
198
+ }
103
199
  if ("agentId" in patch) {
104
200
  job.agentId = normalizeOptionalAgentId(patch.agentId);
105
201
  }
106
202
  assertSupportedJobSpec(job);
203
+ assertDeliverySupport(job);
107
204
  }
108
205
  function mergeCronPayload(existing, patch) {
109
206
  if (patch.kind !== existing.kind) {
@@ -120,25 +217,69 @@ function mergeCronPayload(existing, patch) {
120
217
  return buildPayloadFromPatch(patch);
121
218
  }
122
219
  const next = { ...existing };
123
- if (typeof patch.message === "string")
220
+ if (typeof patch.message === "string") {
124
221
  next.message = patch.message;
125
- if (typeof patch.model === "string")
222
+ }
223
+ if (typeof patch.model === "string") {
126
224
  next.model = patch.model;
127
- if (typeof patch.thinking === "string")
225
+ }
226
+ if (typeof patch.thinking === "string") {
128
227
  next.thinking = patch.thinking;
129
- if (typeof patch.timeoutSeconds === "number")
228
+ }
229
+ if (typeof patch.timeoutSeconds === "number") {
130
230
  next.timeoutSeconds = patch.timeoutSeconds;
131
- if (typeof patch.deliver === "boolean")
231
+ }
232
+ if (typeof patch.allowUnsafeExternalContent === "boolean") {
233
+ next.allowUnsafeExternalContent = patch.allowUnsafeExternalContent;
234
+ }
235
+ if (typeof patch.deliver === "boolean") {
132
236
  next.deliver = patch.deliver;
133
- if (typeof patch.channel === "string")
237
+ }
238
+ if (typeof patch.channel === "string") {
134
239
  next.channel = patch.channel;
135
- if (typeof patch.to === "string")
240
+ }
241
+ if (typeof patch.to === "string") {
136
242
  next.to = patch.to;
243
+ }
137
244
  if (typeof patch.bestEffortDeliver === "boolean") {
138
245
  next.bestEffortDeliver = patch.bestEffortDeliver;
139
246
  }
140
247
  return next;
141
248
  }
249
+ function buildLegacyDeliveryPatch(payload) {
250
+ const deliver = payload.deliver;
251
+ const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
252
+ const hasLegacyHints = typeof deliver === "boolean" ||
253
+ typeof payload.bestEffortDeliver === "boolean" ||
254
+ Boolean(toRaw);
255
+ if (!hasLegacyHints) {
256
+ return null;
257
+ }
258
+ const patch = {};
259
+ let hasPatch = false;
260
+ if (deliver === false) {
261
+ patch.mode = "none";
262
+ hasPatch = true;
263
+ }
264
+ else if (deliver === true || toRaw) {
265
+ patch.mode = "announce";
266
+ hasPatch = true;
267
+ }
268
+ if (typeof payload.channel === "string") {
269
+ const channel = payload.channel.trim().toLowerCase();
270
+ patch.channel = channel ? channel : undefined;
271
+ hasPatch = true;
272
+ }
273
+ if (typeof payload.to === "string") {
274
+ patch.to = payload.to.trim();
275
+ hasPatch = true;
276
+ }
277
+ if (typeof payload.bestEffortDeliver === "boolean") {
278
+ patch.bestEffort = payload.bestEffortDeliver;
279
+ hasPatch = true;
280
+ }
281
+ return hasPatch ? patch : null;
282
+ }
142
283
  function buildPayloadFromPatch(patch) {
143
284
  if (patch.kind === "systemEvent") {
144
285
  if (typeof patch.text !== "string" || patch.text.length === 0) {
@@ -155,20 +296,52 @@ function buildPayloadFromPatch(patch) {
155
296
  model: patch.model,
156
297
  thinking: patch.thinking,
157
298
  timeoutSeconds: patch.timeoutSeconds,
299
+ allowUnsafeExternalContent: patch.allowUnsafeExternalContent,
158
300
  deliver: patch.deliver,
159
301
  channel: patch.channel,
160
302
  to: patch.to,
161
303
  bestEffortDeliver: patch.bestEffortDeliver,
162
304
  };
163
305
  }
306
+ function mergeCronDelivery(existing, patch) {
307
+ const next = {
308
+ mode: existing?.mode ?? "none",
309
+ channel: existing?.channel,
310
+ to: existing?.to,
311
+ bestEffort: existing?.bestEffort,
312
+ };
313
+ if (typeof patch.mode === "string") {
314
+ next.mode = patch.mode === "deliver" ? "announce" : patch.mode;
315
+ }
316
+ if ("channel" in patch) {
317
+ const channel = typeof patch.channel === "string" ? patch.channel.trim() : "";
318
+ next.channel = channel ? channel : undefined;
319
+ }
320
+ if ("to" in patch) {
321
+ const to = typeof patch.to === "string" ? patch.to.trim() : "";
322
+ next.to = to ? to : undefined;
323
+ }
324
+ if (typeof patch.bestEffort === "boolean") {
325
+ next.bestEffort = patch.bestEffort;
326
+ }
327
+ return next;
328
+ }
164
329
  export function isJobDue(job, nowMs, opts) {
165
- if (opts.forced)
330
+ if (!job.state) {
331
+ job.state = {};
332
+ }
333
+ if (typeof job.state.runningAtMs === "number") {
334
+ return false;
335
+ }
336
+ if (opts.forced) {
166
337
  return true;
338
+ }
167
339
  return job.enabled && typeof job.state.nextRunAtMs === "number" && nowMs >= job.state.nextRunAtMs;
168
340
  }
169
341
  export function resolveJobPayloadTextForMain(job) {
170
- if (job.payload.kind !== "systemEvent")
342
+ if (job.payload.kind !== "systemEvent") {
171
343
  return undefined;
344
+ }
172
345
  const text = normalizePayloadToSystemText(job.payload);
173
346
  return text.trim() ? text : undefined;
174
347
  }
@@ -1,14 +1,22 @@
1
1
  import { applyJobPatch, computeJobNextRunAtMs, createJob, findJobOrThrow, isJobDue, nextWakeAtMs, recomputeNextRuns, } from "./jobs.js";
2
2
  import { locked } from "./locked.js";
3
3
  import { ensureLoaded, persist, warnIfDisabled } from "./store.js";
4
- import { armTimer, emit, executeJob, stopTimer, wake } from "./timer.js";
4
+ import { armTimer, emit, executeJob, runMissedJobs, stopTimer, wake } from "./timer.js";
5
5
  export async function start(state) {
6
6
  await locked(state, async () => {
7
7
  if (!state.deps.cronEnabled) {
8
8
  state.deps.log.info({ enabled: false }, "cron: disabled");
9
9
  return;
10
10
  }
11
- await ensureLoaded(state);
11
+ await ensureLoaded(state, { skipRecompute: true });
12
+ const jobs = state.store?.jobs ?? [];
13
+ for (const job of jobs) {
14
+ if (typeof job.state.runningAtMs === "number") {
15
+ state.deps.log.warn({ jobId: job.id, runningAtMs: job.state.runningAtMs }, "cron: clearing stale running marker on startup");
16
+ job.state.runningAtMs = undefined;
17
+ }
18
+ }
19
+ await runMissedJobs(state);
12
20
  recomputeNextRuns(state);
13
21
  await persist(state);
14
22
  armTimer(state);
@@ -24,21 +32,33 @@ export function stop(state) {
24
32
  }
25
33
  export async function status(state) {
26
34
  return await locked(state, async () => {
27
- await ensureLoaded(state);
35
+ await ensureLoaded(state, { skipRecompute: true });
36
+ if (state.store) {
37
+ const changed = recomputeNextRuns(state);
38
+ if (changed) {
39
+ await persist(state);
40
+ }
41
+ }
28
42
  return {
29
43
  enabled: state.deps.cronEnabled,
30
44
  storePath: state.deps.storePath,
31
45
  jobs: state.store?.jobs.length ?? 0,
32
- nextWakeAtMs: state.deps.cronEnabled === true ? (nextWakeAtMs(state) ?? null) : null,
46
+ nextWakeAtMs: state.deps.cronEnabled ? (nextWakeAtMs(state) ?? null) : null,
33
47
  };
34
48
  });
35
49
  }
36
50
  export async function list(state, opts) {
37
51
  return await locked(state, async () => {
38
- await ensureLoaded(state);
52
+ await ensureLoaded(state, { skipRecompute: true });
53
+ if (state.store) {
54
+ const changed = recomputeNextRuns(state);
55
+ if (changed) {
56
+ await persist(state);
57
+ }
58
+ }
39
59
  const includeDisabled = opts?.includeDisabled === true;
40
60
  const jobs = (state.store?.jobs ?? []).filter((j) => includeDisabled || j.enabled);
41
- return jobs.sort((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0));
61
+ return jobs.toSorted((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0));
42
62
  });
43
63
  }
44
64
  export async function add(state, input) {
@@ -47,8 +67,18 @@ export async function add(state, input) {
47
67
  await ensureLoaded(state);
48
68
  const job = createJob(state, input);
49
69
  state.store?.jobs.push(job);
70
+ // Defensive: recompute all next-run times to ensure consistency
71
+ recomputeNextRuns(state);
50
72
  await persist(state);
51
73
  armTimer(state);
74
+ state.deps.log.info({
75
+ jobId: job.id,
76
+ jobName: job.name,
77
+ nextRunAtMs: job.state.nextRunAtMs,
78
+ schedulerNextWakeAtMs: nextWakeAtMs(state) ?? null,
79
+ timerArmed: state.timer !== null,
80
+ cronEnabled: state.deps.cronEnabled,
81
+ }, "cron: job added");
52
82
  emit(state, {
53
83
  jobId: job.id,
54
84
  action: "added",
@@ -64,13 +94,32 @@ export async function update(state, id, patch) {
64
94
  const job = findJobOrThrow(state, id);
65
95
  const now = state.deps.nowMs();
66
96
  applyJobPatch(job, patch);
67
- job.updatedAtMs = now;
68
- if (job.enabled) {
69
- job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
97
+ if (job.schedule.kind === "every") {
98
+ const anchor = job.schedule.anchorMs;
99
+ if (typeof anchor !== "number" || !Number.isFinite(anchor)) {
100
+ const patchSchedule = patch.schedule;
101
+ const fallbackAnchorMs = patchSchedule?.kind === "every"
102
+ ? now
103
+ : typeof job.createdAtMs === "number" && Number.isFinite(job.createdAtMs)
104
+ ? job.createdAtMs
105
+ : now;
106
+ job.schedule = {
107
+ ...job.schedule,
108
+ anchorMs: Math.max(0, Math.floor(fallbackAnchorMs)),
109
+ };
110
+ }
70
111
  }
71
- else {
72
- job.state.nextRunAtMs = undefined;
73
- job.state.runningAtMs = undefined;
112
+ const scheduleChanged = patch.schedule !== undefined;
113
+ const enabledChanged = patch.enabled !== undefined;
114
+ job.updatedAtMs = now;
115
+ if (scheduleChanged || enabledChanged) {
116
+ if (job.enabled) {
117
+ job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
118
+ }
119
+ else {
120
+ job.state.nextRunAtMs = undefined;
121
+ job.state.runningAtMs = undefined;
122
+ }
74
123
  }
75
124
  await persist(state);
76
125
  armTimer(state);
@@ -87,27 +136,34 @@ export async function remove(state, id) {
87
136
  warnIfDisabled(state, "remove");
88
137
  await ensureLoaded(state);
89
138
  const before = state.store?.jobs.length ?? 0;
90
- if (!state.store)
139
+ if (!state.store) {
91
140
  return { ok: false, removed: false };
141
+ }
92
142
  state.store.jobs = state.store.jobs.filter((j) => j.id !== id);
93
143
  const removed = (state.store.jobs.length ?? 0) !== before;
94
144
  await persist(state);
95
145
  armTimer(state);
96
- if (removed)
146
+ if (removed) {
97
147
  emit(state, { jobId: id, action: "removed" });
148
+ }
98
149
  return { ok: true, removed };
99
150
  });
100
151
  }
101
152
  export async function run(state, id, mode) {
102
153
  return await locked(state, async () => {
103
154
  warnIfDisabled(state, "run");
104
- await ensureLoaded(state);
155
+ await ensureLoaded(state, { skipRecompute: true });
105
156
  const job = findJobOrThrow(state, id);
157
+ if (typeof job.state.runningAtMs === "number") {
158
+ return { ok: true, ran: false, reason: "already-running" };
159
+ }
106
160
  const now = state.deps.nowMs();
107
161
  const due = isJobDue(job, now, { forced: mode === "force" });
108
- if (!due)
162
+ if (!due) {
109
163
  return { ok: true, ran: false, reason: "not-due" };
164
+ }
110
165
  await executeJob(state, job, now, { forced: mode === "force" });
166
+ recomputeNextRuns(state);
111
167
  await persist(state);
112
168
  armTimer(state);
113
169
  return { ok: true, ran: true };
@@ -6,5 +6,7 @@ export function createCronServiceState(deps) {
6
6
  running: false,
7
7
  op: Promise.resolve(),
8
8
  warnedDisabled: false,
9
+ storeLoadedAtMs: null,
10
+ storeFileMtimeMs: null,
9
11
  };
10
12
  }