@ponythewhite/base-context 1.0.0 → 1.0.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 (285) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/NOTICE +0 -5
  3. package/README.md +44 -27
  4. package/dist/NOTICE +0 -5
  5. package/dist/base-context-runtime/pyproject.toml +1 -1
  6. package/dist/base-context-runtime/src/rlm/__init__.py +7 -5
  7. package/dist/base-context-runtime/test/test_subagent_registry.py +28 -1
  8. package/dist/base-context-runtime/uv.lock +1 -1
  9. package/dist/build-info.json +1 -1
  10. package/dist/bundle/amazon-bedrock.js +21 -0
  11. package/dist/bundle/{anthropic-YY4S534V.js → anthropic-UV5GZDGU.js} +5 -5
  12. package/dist/bundle/{azure-openai-responses-JRP6MIM3.js → azure-openai-responses-WSNPXLYC.js} +6 -6
  13. package/dist/bundle/{bundled-modules-P5HARVZA.js → bundled-modules-PWOGKUQQ.js} +71 -16
  14. package/dist/bundle/chunk-27QCJ2AZ.js +2578 -0
  15. package/dist/bundle/{chunk-BYVP7LWV.js → chunk-44PEHPOG.js} +1916 -4689
  16. package/dist/bundle/{chunk-MZHTNNVA.js → chunk-4GXPNXOJ.js} +8 -0
  17. package/dist/bundle/{chunk-SC5KG5R4.js → chunk-DGNX7HQ3.js} +1 -7
  18. package/dist/bundle/{chunk-A53O363M.js → chunk-GRJFXLBM.js} +0 -1
  19. package/dist/bundle/chunk-HKINIBP4.js +29 -0
  20. package/dist/bundle/{chunk-73DCPU3D.js → chunk-IADCPVEI.js} +1019 -3427
  21. package/dist/bundle/{chunk-UDSW6FNG.js → chunk-KTNETTHI.js} +1 -1
  22. package/dist/bundle/{chunk-U7AUVG3B.js → chunk-VSIYFWYI.js} +24 -6
  23. package/dist/bundle/{chunk-IW77OXFS.js → chunk-W46VZFI6.js} +0 -2099
  24. package/dist/bundle/{cli-main-O4X7GFGY.js → cli-main-4DS7Y3PI.js} +8 -6
  25. package/dist/bundle/cli.js +2 -2
  26. package/dist/bundle/{google-AUTVGDGZ.js → google-T32FMD4G.js} +6 -6
  27. package/dist/bundle/{google-vertex-Q75D7RKS.js → google-vertex-56KP6I3B.js} +3 -3
  28. package/dist/bundle/{main-XKOW2BV6.js → main-SV7JLVEB.js} +7 -5
  29. package/dist/bundle/{mistral-7ESB7QUD.js → mistral-WGBMYIVU.js} +5 -5
  30. package/dist/bundle/{openai-codex-responses-LV3QRPN4.js → openai-codex-responses-PIV2RQ3G.js} +6 -6
  31. package/dist/bundle/{openai-completions-OJECLPXJ.js → openai-completions-64C3JPR4.js} +11 -22
  32. package/dist/bundle/{openai-responses-FN6HZMBC.js → openai-responses-UI7RSTDV.js} +6 -6
  33. package/dist/cli/args.d.ts.map +1 -1
  34. package/dist/cli/args.js +108 -59
  35. package/dist/cli/args.js.map +1 -1
  36. package/dist/cli/daemon-ps.d.ts +4 -3
  37. package/dist/cli/daemon-ps.d.ts.map +1 -1
  38. package/dist/cli/daemon-ps.js +6 -1
  39. package/dist/cli/daemon-ps.js.map +1 -1
  40. package/dist/cli/product-doctor.d.ts +0 -1
  41. package/dist/cli/product-doctor.d.ts.map +1 -1
  42. package/dist/cli/product-doctor.js +6 -3
  43. package/dist/cli/product-doctor.js.map +1 -1
  44. package/dist/config.d.ts +0 -3
  45. package/dist/config.d.ts.map +1 -1
  46. package/dist/config.js +0 -8
  47. package/dist/config.js.map +1 -1
  48. package/dist/core/agent-session-config.d.ts +1 -2
  49. package/dist/core/agent-session-config.d.ts.map +1 -1
  50. package/dist/core/agent-session-config.js +0 -3
  51. package/dist/core/agent-session-config.js.map +1 -1
  52. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  53. package/dist/core/agent-session-runtime.js +5 -4
  54. package/dist/core/agent-session-runtime.js.map +1 -1
  55. package/dist/core/agent-session-services.d.ts +5 -3
  56. package/dist/core/agent-session-services.d.ts.map +1 -1
  57. package/dist/core/agent-session-services.js +2 -27
  58. package/dist/core/agent-session-services.js.map +1 -1
  59. package/dist/core/agent-session.d.ts +10 -6
  60. package/dist/core/agent-session.d.ts.map +1 -1
  61. package/dist/core/agent-session.js +107 -107
  62. package/dist/core/agent-session.js.map +1 -1
  63. package/dist/core/auth-storage.d.ts +8 -35
  64. package/dist/core/auth-storage.d.ts.map +1 -1
  65. package/dist/core/auth-storage.js +33 -243
  66. package/dist/core/auth-storage.js.map +1 -1
  67. package/dist/core/cron-jobs.d.ts.map +1 -1
  68. package/dist/core/cron-jobs.js +7 -1
  69. package/dist/core/cron-jobs.js.map +1 -1
  70. package/dist/core/inference-coordinator.d.ts +6 -1
  71. package/dist/core/inference-coordinator.d.ts.map +1 -1
  72. package/dist/core/inference-coordinator.js +18 -2
  73. package/dist/core/inference-coordinator.js.map +1 -1
  74. package/dist/core/kernel/bootstrap.d.ts.map +1 -1
  75. package/dist/core/kernel/bootstrap.js +2 -2
  76. package/dist/core/kernel/bootstrap.js.map +1 -1
  77. package/dist/core/kernel/repl-manager.d.ts +2 -0
  78. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  79. package/dist/core/kernel/repl-manager.js +44 -1
  80. package/dist/core/kernel/repl-manager.js.map +1 -1
  81. package/dist/core/logging.d.ts +4 -3
  82. package/dist/core/logging.d.ts.map +1 -1
  83. package/dist/core/logging.js +6 -6
  84. package/dist/core/logging.js.map +1 -1
  85. package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
  86. package/dist/core/mcp/mcp-manager.js +3 -3
  87. package/dist/core/mcp/mcp-manager.js.map +1 -1
  88. package/dist/core/model-registry.d.ts +1 -16
  89. package/dist/core/model-registry.d.ts.map +1 -1
  90. package/dist/core/model-registry.js +88 -196
  91. package/dist/core/model-registry.js.map +1 -1
  92. package/dist/core/model-resolver.d.ts +2 -19
  93. package/dist/core/model-resolver.d.ts.map +1 -1
  94. package/dist/core/model-resolver.js +36 -266
  95. package/dist/core/model-resolver.js.map +1 -1
  96. package/dist/core/provider-contracts.d.ts +5 -6
  97. package/dist/core/provider-contracts.d.ts.map +1 -1
  98. package/dist/core/provider-contracts.js +25 -36
  99. package/dist/core/provider-contracts.js.map +1 -1
  100. package/dist/core/provider-display-names.d.ts.map +1 -1
  101. package/dist/core/provider-display-names.js +0 -2
  102. package/dist/core/provider-display-names.js.map +1 -1
  103. package/dist/core/rlm-max-subagents.d.ts +32 -0
  104. package/dist/core/rlm-max-subagents.d.ts.map +1 -0
  105. package/dist/core/rlm-max-subagents.js +50 -0
  106. package/dist/core/rlm-max-subagents.js.map +1 -0
  107. package/dist/core/rlm-runtime.d.ts +3 -3
  108. package/dist/core/rlm-runtime.d.ts.map +1 -1
  109. package/dist/core/rlm-runtime.js.map +1 -1
  110. package/dist/core/sdk.d.ts +4 -3
  111. package/dist/core/sdk.d.ts.map +1 -1
  112. package/dist/core/sdk.js +27 -14
  113. package/dist/core/sdk.js.map +1 -1
  114. package/dist/core/settings-manager.d.ts +3 -16
  115. package/dist/core/settings-manager.d.ts.map +1 -1
  116. package/dist/core/settings-manager.js +12 -44
  117. package/dist/core/settings-manager.js.map +1 -1
  118. package/dist/core/slash-commands.d.ts.map +1 -1
  119. package/dist/core/slash-commands.js +6 -6
  120. package/dist/core/slash-commands.js.map +1 -1
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js.map +1 -1
  124. package/dist/installer.mjs +2 -3
  125. package/dist/main.d.ts.map +1 -1
  126. package/dist/main.js +39 -34
  127. package/dist/main.js.map +1 -1
  128. package/dist/modes/acp/acp-mode.d.ts.map +1 -1
  129. package/dist/modes/acp/acp-mode.js +1 -1
  130. package/dist/modes/acp/acp-mode.js.map +1 -1
  131. package/dist/modes/agent-connection/daemon-agent-connection.d.ts +6 -2
  132. package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
  133. package/dist/modes/agent-connection/daemon-agent-connection.js +13 -2
  134. package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
  135. package/dist/modes/agent-connection/in-process-agent-connection.d.ts +2 -0
  136. package/dist/modes/agent-connection/in-process-agent-connection.d.ts.map +1 -1
  137. package/dist/modes/agent-connection/in-process-agent-connection.js +6 -0
  138. package/dist/modes/agent-connection/in-process-agent-connection.js.map +1 -1
  139. package/dist/modes/agent-connection/types.d.ts +3 -0
  140. package/dist/modes/agent-connection/types.d.ts.map +1 -1
  141. package/dist/modes/agent-connection/types.js.map +1 -1
  142. package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
  143. package/dist/modes/agents-view/agents-view-mode.js +2 -19
  144. package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
  145. package/dist/modes/daemon/daemon-mode.d.ts +5 -0
  146. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  147. package/dist/modes/daemon/daemon-mode.js +107 -12
  148. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  149. package/dist/modes/daemon/daemon-protocol.d.ts +194 -165
  150. package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
  151. package/dist/modes/daemon/daemon-protocol.js +16 -11
  152. package/dist/modes/daemon/daemon-protocol.js.map +1 -1
  153. package/dist/modes/daemon/daemon-rlm-capacity-coordinator.d.ts +19 -0
  154. package/dist/modes/daemon/daemon-rlm-capacity-coordinator.d.ts.map +1 -0
  155. package/dist/modes/daemon/daemon-rlm-capacity-coordinator.js +74 -0
  156. package/dist/modes/daemon/daemon-rlm-capacity-coordinator.js.map +1 -0
  157. package/dist/modes/daemon/daemon-rlm-capacity.d.ts +19 -0
  158. package/dist/modes/daemon/daemon-rlm-capacity.d.ts.map +1 -0
  159. package/dist/modes/daemon/daemon-rlm-capacity.js +85 -0
  160. package/dist/modes/daemon/daemon-rlm-capacity.js.map +1 -0
  161. package/dist/modes/daemon/daemon-session-summarizer.d.ts +2 -3
  162. package/dist/modes/daemon/daemon-session-summarizer.d.ts.map +1 -1
  163. package/dist/modes/daemon/daemon-session-summarizer.js +32 -23
  164. package/dist/modes/daemon/daemon-session-summarizer.js.map +1 -1
  165. package/dist/modes/daemon/daemon-state-root.d.ts +15 -0
  166. package/dist/modes/daemon/daemon-state-root.d.ts.map +1 -0
  167. package/dist/modes/daemon/daemon-state-root.js +40 -0
  168. package/dist/modes/daemon/daemon-state-root.js.map +1 -0
  169. package/dist/modes/daemon/daemon-supervisor-ownership.d.ts +2 -0
  170. package/dist/modes/daemon/daemon-supervisor-ownership.d.ts.map +1 -1
  171. package/dist/modes/daemon/daemon-supervisor-ownership.js +27 -0
  172. package/dist/modes/daemon/daemon-supervisor-ownership.js.map +1 -1
  173. package/dist/modes/daemon/daemon-supervisor.d.ts +10 -1
  174. package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
  175. package/dist/modes/daemon/daemon-supervisor.js +213 -20
  176. package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
  177. package/dist/modes/daemon/daemon-worker-protocol.d.ts +28 -1
  178. package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
  179. package/dist/modes/daemon/daemon-worker-protocol.js +16 -2
  180. package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
  181. package/dist/modes/daemon/saved-session-catalog.d.ts.map +1 -1
  182. package/dist/modes/daemon/saved-session-catalog.js +2 -0
  183. package/dist/modes/daemon/saved-session-catalog.js.map +1 -1
  184. package/dist/modes/interactive/auth-flows.d.ts +0 -6
  185. package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
  186. package/dist/modes/interactive/auth-flows.js +6 -126
  187. package/dist/modes/interactive/auth-flows.js.map +1 -1
  188. package/dist/modes/interactive/components/configuration-menu.d.ts +3 -0
  189. package/dist/modes/interactive/components/configuration-menu.d.ts.map +1 -1
  190. package/dist/modes/interactive/components/configuration-menu.js +13 -2
  191. package/dist/modes/interactive/components/configuration-menu.js.map +1 -1
  192. package/dist/modes/interactive/components/footer.d.ts +1 -1
  193. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  194. package/dist/modes/interactive/components/footer.js +2 -2
  195. package/dist/modes/interactive/components/footer.js.map +1 -1
  196. package/dist/modes/interactive/components/index.d.ts +0 -1
  197. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  198. package/dist/modes/interactive/components/index.js +0 -1
  199. package/dist/modes/interactive/components/index.js.map +1 -1
  200. package/dist/modes/interactive/components/login-dialog.d.ts +0 -1
  201. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/login-dialog.js +3 -40
  203. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  204. package/dist/modes/interactive/components/model-selector.d.ts +2 -0
  205. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  206. package/dist/modes/interactive/components/model-selector.js +13 -2
  207. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  208. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  209. package/dist/modes/interactive/components/oauth-selector.js +2 -11
  210. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  211. package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
  212. package/dist/modes/interactive/feature-hints.js +0 -4
  213. package/dist/modes/interactive/feature-hints.js.map +1 -1
  214. package/dist/modes/interactive/interactive-mode.d.ts +3 -12
  215. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  216. package/dist/modes/interactive/interactive-mode.js +81 -507
  217. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  218. package/dist/modes/interactive/onboarding.d.ts +0 -3
  219. package/dist/modes/interactive/onboarding.d.ts.map +1 -1
  220. package/dist/modes/interactive/onboarding.js +0 -17
  221. package/dist/modes/interactive/onboarding.js.map +1 -1
  222. package/dist/modes/rpc/rpc-client.d.ts +4 -1
  223. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  224. package/dist/modes/rpc/rpc-client.js +82 -42
  225. package/dist/modes/rpc/rpc-client.js.map +1 -1
  226. package/dist/node/amazon-bedrock.d.ts +2 -0
  227. package/dist/node/amazon-bedrock.d.ts.map +1 -0
  228. package/dist/node/amazon-bedrock.js +6 -0
  229. package/dist/node/amazon-bedrock.js.map +1 -0
  230. package/dist/product-identity.d.ts +0 -1
  231. package/dist/product-identity.d.ts.map +1 -1
  232. package/dist/product-identity.js +0 -1
  233. package/dist/product-identity.js.map +1 -1
  234. package/docs/development.md +1 -1
  235. package/docs/fork-philosophy.md +4 -8
  236. package/docs/installation.md +83 -62
  237. package/docs/providers.md +23 -25
  238. package/docs/quickstart.md +19 -10
  239. package/docs/rlm-runtime.md +13 -5
  240. package/docs/rpc.md +4 -4
  241. package/docs/sdk.md +20 -11
  242. package/docs/sessions.md +0 -1
  243. package/docs/settings.md +9 -42
  244. package/docs/skills.md +0 -1
  245. package/docs/upstream-0.9.5.md +171 -0
  246. package/docs/usage.md +14 -10
  247. package/package.json +4 -4
  248. package/dist/core/agent-traces.d.ts +0 -149
  249. package/dist/core/agent-traces.d.ts.map +0 -1
  250. package/dist/core/agent-traces.js +0 -904
  251. package/dist/core/agent-traces.js.map +0 -1
  252. package/dist/core/prime-inference-auth.d.ts +0 -70
  253. package/dist/core/prime-inference-auth.d.ts.map +0 -1
  254. package/dist/core/prime-inference-auth.js +0 -352
  255. package/dist/core/prime-inference-auth.js.map +0 -1
  256. package/dist/core/prime-inference-model-selection.d.ts +0 -16
  257. package/dist/core/prime-inference-model-selection.d.ts.map +0 -1
  258. package/dist/core/prime-inference-model-selection.js +0 -16
  259. package/dist/core/prime-inference-model-selection.js.map +0 -1
  260. package/dist/core/prime-inference-models.d.ts +0 -6
  261. package/dist/core/prime-inference-models.d.ts.map +0 -1
  262. package/dist/core/prime-inference-models.js +0 -62
  263. package/dist/core/prime-inference-models.js.map +0 -1
  264. package/dist/core/telemetry.d.ts +0 -95
  265. package/dist/core/telemetry.d.ts.map +0 -1
  266. package/dist/core/telemetry.js +0 -637
  267. package/dist/core/telemetry.js.map +0 -1
  268. package/dist/modes/interactive/components/prime-onboarding-splash.d.ts +0 -39
  269. package/dist/modes/interactive/components/prime-onboarding-splash.d.ts.map +0 -1
  270. package/dist/modes/interactive/components/prime-onboarding-splash.js +0 -286
  271. package/dist/modes/interactive/components/prime-onboarding-splash.js.map +0 -1
  272. package/dist/modes/interactive/components/prime-team-selector.d.ts +0 -30
  273. package/dist/modes/interactive/components/prime-team-selector.d.ts.map +0 -1
  274. package/dist/modes/interactive/components/prime-team-selector.js +0 -166
  275. package/dist/modes/interactive/components/prime-team-selector.js.map +0 -1
  276. package/dist/skills/prime-intellect/SKILL.md +0 -87
  277. package/dist/skills/prime-intellect/references/compute.md +0 -45
  278. package/dist/skills/prime-intellect/references/environments.md +0 -73
  279. package/dist/skills/prime-intellect/references/inference.md +0 -47
  280. package/dist/skills/prime-intellect/references/sandboxes.md +0 -75
  281. package/skills/prime-intellect/SKILL.md +0 -87
  282. package/skills/prime-intellect/references/compute.md +0 -45
  283. package/skills/prime-intellect/references/environments.md +0 -73
  284. package/skills/prime-intellect/references/inference.md +0 -47
  285. package/skills/prime-intellect/references/sandboxes.md +0 -75
@@ -1 +1 @@
1
- {"version":3,"file":"cron-jobs.d.ts","sourceRoot":"","sources":["../../src/core/cron-jobs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { lockSync } from \"proper-lockfile\";\nimport { getSessionArtifactPathForFile } from \"./session-manager.js\";\n\nexport type AgentCronJobStatus = \"active\" | \"paused\" | \"completed\" | \"cancelled\";\nexport type AgentCronScheduleKind = \"once\" | \"cron\" | \"interval\";\nexport type AgentCronJobSource = \"cron\" | \"heartbeat\" | \"rlm_heartbeat\";\nexport type AgentCronJobRuntimeKind = \"top-level\" | \"subagent\";\nexport type AgentHeartbeatUpdateAction = \"pause\" | \"resume\" | \"clear\";\nexport type AgentHeartbeatManagementAction = \"pause\" | \"resume\" | \"stop\";\nexport type AgentRlmHeartbeatStatusUpdate = \"pause\" | \"resume\";\n/**\n * How a scheduled heartbeat prompt is delivered when the target session is busy:\n * \"steer\" interrupts the current turn, \"follow_up\" waits for it to finish.\n */\nexport type AgentHeartbeatDeliveryMode = \"steer\" | \"follow_up\";\n\nexport interface AgentCronSchedule {\n\tkind: AgentCronScheduleKind;\n\texpression: string;\n\tintervalMs?: number;\n}\n\nexport interface AgentCronJob {\n\tid: string;\n\tstatus: AgentCronJobStatus;\n\tsource?: AgentCronJobSource;\n\truntimeKind?: AgentCronJobRuntimeKind;\n\t/** Delivery mode for heartbeat/rlm_heartbeat jobs when the session is busy. Defaults to \"steer\". */\n\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\tactiveSessionId: string;\n\tsessionId: string;\n\tsessionFile: string;\n\tcwd: string;\n\tlabel?: string;\n\tprompt: string;\n\tschedule: AgentCronSchedule;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tnextRunAt?: string;\n\tlastRunAt?: string;\n\tlastSkippedAt?: string;\n\tlastError?: string;\n\trunCount: number;\n}\n\nexport interface CreateAgentCronJobInput {\n\tactiveSessionId: string;\n\tsessionId: string;\n\tsessionFile: string;\n\tcwd: string;\n\tlabel?: string;\n\tprompt: string;\n\tscheduleText: string;\n\tsource?: AgentCronJobSource;\n\truntimeKind?: AgentCronJobRuntimeKind;\n\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\tnow?: Date;\n}\n\nexport type AgentCronJobRunResult = \"ran\" | \"skipped\";\n\nexport interface AgentCronDispatch {\n\tid: string;\n\tjob: AgentCronJob;\n}\n\nexport interface AgentCronSchedulerHooks {\n\trunJob: (job: AgentCronJob) => Promise<AgentCronJobRunResult | undefined>;\n\tbeginDispatch?: (dispatch: AgentCronDispatch) => (() => void) | undefined;\n\tnow?: () => Date;\n\tonError?: (job: AgentCronJob, error: unknown) => void;\n}\n\nexport interface HeartbeatCronSessionActivity {\n\tisStreaming: boolean;\n\tisCompacting?: boolean;\n\tisRetrying?: boolean;\n\tisBashRunning: boolean;\n\thasPendingSessionWork: boolean;\n\tunfinishedActionCount: number;\n}\n\ninterface CronJobsFile {\n\tjobs?: unknown;\n\tdispatches?: unknown;\n}\n\ninterface AgentCronDispatchRecord {\n\tid: string;\n\tjobId: string;\n\tclaimedAt: string;\n\tscheduledFor: string;\n}\n\ninterface CronJobsState {\n\tjobs: AgentCronJob[];\n\tdispatches: AgentCronDispatchRecord[];\n}\n\nexport const SESSION_SCHEDULED_JOBS_FILENAME = \"scheduled-jobs.json\";\n\nconst MAX_TIMEOUT_MS = 2_147_483_647;\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60_000;\nexport const DEFAULT_HEARTBEAT_SCHEDULE = \"every 5m\";\nexport const DEFAULT_HEARTBEAT_DELIVERY_MODE: AgentHeartbeatDeliveryMode = \"steer\";\n\nexport type ParsedHeartbeatCommand =\n\t| { type: \"status\" }\n\t| { type: \"pause\" }\n\t| { type: \"resume\" }\n\t| { type: \"clear\" }\n\t| { type: \"set\"; schedule: string; instruction: string; deliveryMode?: AgentHeartbeatDeliveryMode };\n\nexport interface AgentRlmHeartbeatController {\n\tlistRlmHeartbeats(options?: { includeInactive?: boolean }): AgentCronJob[];\n\tcreateRlmHeartbeat(input: {\n\t\tinstruction: string;\n\t\tinterval?: string;\n\t\tlabel?: string;\n\t\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\t}): AgentCronJob;\n\tupdateRlmHeartbeat(input: {\n\t\tid: string;\n\t\tinstruction?: string;\n\t\tinterval?: string;\n\t\tlabel?: string;\n\t\tstatus?: AgentRlmHeartbeatStatusUpdate;\n\t\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\t}): AgentCronJob | undefined;\n\tdeleteRlmHeartbeat(id: string): AgentCronJob | undefined;\n}\n\nfunction heartbeatCatalogSignature(jobs: readonly AgentCronJob[]): string {\n\treturn JSON.stringify(\n\t\tjobs\n\t\t\t.filter((job) => isHeartbeatCronJob(job) && (job.status === \"active\" || job.status === \"paused\"))\n\t\t\t.sort((left, right) => left.id.localeCompare(right.id))\n\t\t\t.map((job) => ({\n\t\t\t\tid: job.id,\n\t\t\t\tstatus: job.status,\n\t\t\t\tsource: job.source,\n\t\t\t\truntimeKind: job.runtimeKind,\n\t\t\t\tdeliveryMode: job.deliveryMode,\n\t\t\t\tactiveSessionId: job.activeSessionId,\n\t\t\t\tsessionId: job.sessionId,\n\t\t\t\tsessionFile: job.sessionFile,\n\t\t\t\tcwd: job.cwd,\n\t\t\t\tlabel: job.label,\n\t\t\t\tprompt: job.prompt,\n\t\t\t\tschedule: job.schedule,\n\t\t\t\tcreatedAt: job.createdAt,\n\t\t\t})),\n\t);\n}\n\nexport class AgentCronJobStore {\n\tprivate readonly sessionArtifactFiles = new Map<string, string>();\n\tprivate readonly heartbeatChangeListeners = new Set<() => void>();\n\n\tconstructor(\n\t\tprivate readonly filePath?: string,\n\t\tprivate readonly sessionArtifactMode = false,\n\t) {\n\t\tif (!filePath && !sessionArtifactMode) {\n\t\t\tthrow new Error(\"Cron job store requires a file path\");\n\t\t}\n\t}\n\n\tstatic forSessionArtifacts(): AgentCronJobStore {\n\t\treturn new AgentCronJobStore(undefined, true);\n\t}\n\n\tonHeartbeatChange(listener: () => void): () => void {\n\t\tthis.heartbeatChangeListeners.add(listener);\n\t\treturn () => this.heartbeatChangeListeners.delete(listener);\n\t}\n\n\tregisterSessionArtifact(sessionId: string, artifactDir: string): boolean {\n\t\tif (!this.sessionArtifactMode) {\n\t\t\treturn false;\n\t\t}\n\t\tconst path = join(artifactDir, SESSION_SCHEDULED_JOBS_FILENAME);\n\t\tif (this.sessionArtifactFiles.get(sessionId) === path) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.sessionArtifactFiles.set(sessionId, path);\n\t\treturn true;\n\t}\n\n\trecoverSessionArtifact(sessionId: string, now = new Date()): AgentCronJob[] {\n\t\tconst path = this.sessionArtifactFiles.get(sessionId);\n\t\tif (!path) {\n\t\t\treturn [];\n\t\t}\n\t\treturn withCronJobsStateLocks([path], () => {\n\t\t\tconst state = readJobsState(path);\n\t\t\tconst recovered: AgentCronJob[] = [];\n\t\t\tif (state.dispatches.length > 0) {\n\t\t\t\trecoverInterruptedInState(state, now, recovered);\n\t\t\t\twriteJobsState(path, state);\n\t\t\t}\n\t\t\treturn recovered;\n\t\t});\n\t}\n\n\tlist(): AgentCronJob[] {\n\t\treturn this.readJobs().sort((a, b) => compareOptionalIso(a.nextRunAt, b.nextRunAt));\n\t}\n\n\t/** Import supported declarations directly as paused data; never import dispatch ownership. */\n\timportPaused(\n\t\tjobs: readonly AgentCronJob[],\n\t\ttarget: { sessionId: string; sessionFile: string; cwd: string },\n\t): AgentCronJob[] {\n\t\tconst now = new Date().toISOString();\n\t\tconst imported = jobs.map((job): AgentCronJob => {\n\t\t\tif (\n\t\t\t\t!isAgentCronJob(job) ||\n\t\t\t\t(job.status !== \"active\" && job.status !== \"paused\") ||\n\t\t\t\t(job.source === \"rlm_heartbeat\" && job.schedule.kind === \"once\") ||\n\t\t\t\tjob.runtimeKind === \"subagent\"\n\t\t\t)\n\t\t\t\tthrow new Error(\"Unsupported paused schedule owner\");\n\t\t\treturn {\n\t\t\t\tid: randomUUID(),\n\t\t\t\tstatus: \"paused\",\n\t\t\t\tsource: job.source ?? \"cron\",\n\t\t\t\truntimeKind: \"top-level\",\n\t\t\t\tactiveSessionId: target.sessionId,\n\t\t\t\tsessionId: target.sessionId,\n\t\t\t\tsessionFile: target.sessionFile,\n\t\t\t\tcwd: target.cwd,\n\t\t\t\tprompt: job.prompt,\n\t\t\t\tschedule: {\n\t\t\t\t\tkind: job.schedule.kind,\n\t\t\t\t\texpression: job.schedule.expression,\n\t\t\t\t\t...(job.schedule.kind === \"interval\" ? { intervalMs: job.schedule.intervalMs } : {}),\n\t\t\t\t},\n\t\t\t\t...(job.label === undefined ? {} : { label: job.label }),\n\t\t\t\t...(job.deliveryMode === undefined ? {} : { deliveryMode: job.deliveryMode }),\n\t\t\t\tcreatedAt: now,\n\t\t\t\tupdatedAt: now,\n\t\t\t\trunCount: 0,\n\t\t\t};\n\t\t});\n\t\tif (imported.length > 0) this.writeJobs([...this.readJobs(), ...imported]);\n\t\treturn imported;\n\t}\n\n\tcreate(input: CreateAgentCronJobInput): AgentCronJob {\n\t\tconst now = input.now ?? new Date();\n\t\tconst prompt = input.prompt.trim();\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"Cron job prompt cannot be empty\");\n\t\t}\n\t\tconst parsed = parseAgentCronSchedule(input.scheduleText, now);\n\t\tconst nowIso = now.toISOString();\n\t\tconst job: AgentCronJob = {\n\t\t\tid: randomUUID(),\n\t\t\tstatus: \"active\",\n\t\t\tsource: input.source ?? \"cron\",\n\t\t\truntimeKind: input.runtimeKind,\n\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\tsessionId: input.sessionId,\n\t\t\tsessionFile: input.sessionFile,\n\t\t\tcwd: input.cwd,\n\t\t\tlabel: normalizeOptionalLabel(input.label),\n\t\t\tprompt,\n\t\t\tschedule: parsed.schedule,\n\t\t\tcreatedAt: nowIso,\n\t\t\tupdatedAt: nowIso,\n\t\t\tnextRunAt: parsed.nextRunAt.toISOString(),\n\t\t\trunCount: 0,\n\t\t};\n\t\tthis.writeJobs([...this.readJobs(), job]);\n\t\treturn job;\n\t}\n\n\t/**\n\t * Active session ids are daemon-local. When a persisted session is restored,\n\t * bind jobs stored for its stable session file to the new live session id.\n\t * When a live session switches to another persisted file, move jobs stored for\n\t * its stable active session id to the new file so future restores target the\n\t * current session instead of the previous one.\n\t */\n\trebindSessionJobs(input: {\n\t\tactiveSessionId: string;\n\t\tsessionId: string;\n\t\tsessionFile: string;\n\t\tcwd: string;\n\t}): AgentCronJob[] {\n\t\tconst targetSessionFile = resolve(input.sessionFile);\n\t\tconst reboundJobs: AgentCronJob[] = [];\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.activeSessionId !== input.activeSessionId && resolve(job.sessionFile) !== targetSessionFile) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tjob.activeSessionId === input.activeSessionId &&\n\t\t\t\tjob.sessionId === input.sessionId &&\n\t\t\t\tresolve(job.sessionFile) === targetSessionFile &&\n\t\t\t\tjob.cwd === input.cwd\n\t\t\t) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst rebound = {\n\t\t\t\t...job,\n\t\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tsessionFile: input.sessionFile,\n\t\t\t\tcwd: input.cwd,\n\t\t\t};\n\t\t\treboundJobs.push(rebound);\n\t\t\treturn rebound;\n\t\t});\n\t\tif (reboundJobs.length > 0) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn reboundJobs;\n\t}\n\n\tgetHeartbeat(activeSessionId: string): AgentCronJob | undefined {\n\t\treturn this.readJobs()\n\t\t\t.filter((job) => {\n\t\t\t\treturn (\n\t\t\t\t\tjob.activeSessionId === activeSessionId &&\n\t\t\t\t\tjob.source === \"heartbeat\" &&\n\t\t\t\t\t(job.status === \"active\" || job.status === \"paused\")\n\t\t\t\t);\n\t\t\t})\n\t\t\t.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];\n\t}\n\n\tgetLatestHeartbeat(activeSessionId: string): AgentCronJob | undefined {\n\t\treturn this.readJobs()\n\t\t\t.filter((job) => job.activeSessionId === activeSessionId && job.source === \"heartbeat\")\n\t\t\t.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];\n\t}\n\n\tcreateHeartbeat(input: CreateAgentCronJobInput): AgentCronJob {\n\t\tconst now = input.now ?? new Date();\n\t\tconst parsed = parseAgentCronSchedule(input.scheduleText, now);\n\t\tif (parsed.schedule.kind === \"once\") {\n\t\t\tthrow new Error(\"Heartbeat schedule must be recurring\");\n\t\t}\n\t\tconst existing = this.readJobs().map((job) => {\n\t\t\tif (\n\t\t\t\tjob.activeSessionId === input.activeSessionId &&\n\t\t\t\tjob.source === \"heartbeat\" &&\n\t\t\t\t(job.status === \"active\" || job.status === \"paused\")\n\t\t\t) {\n\t\t\t\treturn { ...job, status: \"cancelled\" as const, nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\t}\n\t\t\treturn job;\n\t\t});\n\t\tconst prompt = input.prompt.trim();\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"Heartbeat instruction cannot be empty\");\n\t\t}\n\t\tconst nowIso = now.toISOString();\n\t\tconst job: AgentCronJob = {\n\t\t\tid: randomUUID(),\n\t\t\tstatus: \"active\",\n\t\t\tsource: \"heartbeat\",\n\t\t\truntimeKind: input.runtimeKind,\n\t\t\tdeliveryMode: input.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE,\n\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\tsessionId: input.sessionId,\n\t\t\tsessionFile: input.sessionFile,\n\t\t\tcwd: input.cwd,\n\t\t\tlabel: normalizeOptionalLabel(input.label),\n\t\t\tprompt,\n\t\t\tschedule: parsed.schedule,\n\t\t\tcreatedAt: nowIso,\n\t\t\tupdatedAt: nowIso,\n\t\t\tnextRunAt: parsed.nextRunAt.toISOString(),\n\t\t\trunCount: 0,\n\t\t};\n\t\tthis.writeJobs([...existing, job]);\n\t\treturn job;\n\t}\n\n\tlistRlmHeartbeats(activeSessionId: string, options: { includeInactive?: boolean } = {}): AgentCronJob[] {\n\t\treturn this.readJobs()\n\t\t\t.filter((job) => {\n\t\t\t\tif (job.activeSessionId !== activeSessionId || job.source !== \"rlm_heartbeat\") {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (options.includeInactive) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn job.status === \"active\" || job.status === \"paused\";\n\t\t\t})\n\t\t\t.sort((a, b) => compareOptionalIso(a.nextRunAt, b.nextRunAt));\n\t}\n\n\tcreateRlmHeartbeat(input: CreateAgentCronJobInput): AgentCronJob {\n\t\tconst now = input.now ?? new Date();\n\t\tconst parsed = parseAgentCronSchedule(input.scheduleText, now);\n\t\tif (parsed.schedule.kind === \"once\") {\n\t\t\tthrow new Error(\"RLM heartbeat schedule must be recurring\");\n\t\t}\n\t\tconst prompt = input.prompt.trim();\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"RLM heartbeat instruction cannot be empty\");\n\t\t}\n\t\tconst nowIso = now.toISOString();\n\t\tconst job: AgentCronJob = {\n\t\t\tid: randomUUID(),\n\t\t\tstatus: \"active\",\n\t\t\tsource: \"rlm_heartbeat\",\n\t\t\truntimeKind: input.runtimeKind,\n\t\t\tdeliveryMode: input.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE,\n\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\tsessionId: input.sessionId,\n\t\t\tsessionFile: input.sessionFile,\n\t\t\tcwd: input.cwd,\n\t\t\tlabel: normalizeOptionalLabel(input.label),\n\t\t\tprompt,\n\t\t\tschedule: parsed.schedule,\n\t\t\tcreatedAt: nowIso,\n\t\t\tupdatedAt: nowIso,\n\t\t\tnextRunAt: parsed.nextRunAt.toISOString(),\n\t\t\trunCount: 0,\n\t\t};\n\t\tthis.writeJobs([...this.readJobs(), job]);\n\t\treturn job;\n\t}\n\n\tupdateRlmHeartbeat(\n\t\tactiveSessionId: string,\n\t\tid: string,\n\t\tupdate: {\n\t\t\tlabel?: string;\n\t\t\tprompt?: string;\n\t\t\tscheduleText?: string;\n\t\t\tstatus?: AgentRlmHeartbeatStatusUpdate;\n\t\t\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\t\t\tnow?: Date;\n\t\t},\n\t): AgentCronJob | undefined {\n\t\tconst now = update.now ?? new Date();\n\t\tlet updated: AgentCronJob | undefined;\n\t\tlet matchedRlmHeartbeat = false;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.activeSessionId !== activeSessionId || job.source !== \"rlm_heartbeat\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tmatchedRlmHeartbeat = true;\n\t\t\tif (job.status === \"cancelled\" || job.status === \"completed\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tlet nextJob: AgentCronJob = { ...job };\n\t\t\tif (update.label !== undefined) {\n\t\t\t\tnextJob = { ...nextJob, label: normalizeOptionalLabel(update.label) };\n\t\t\t}\n\t\t\tif (update.deliveryMode !== undefined) {\n\t\t\t\tnextJob = { ...nextJob, deliveryMode: update.deliveryMode };\n\t\t\t}\n\t\t\tif (update.prompt !== undefined) {\n\t\t\t\tconst prompt = update.prompt.trim();\n\t\t\t\tif (!prompt) {\n\t\t\t\t\tthrow new Error(\"RLM heartbeat instruction cannot be empty\");\n\t\t\t\t}\n\t\t\t\tnextJob = { ...nextJob, prompt };\n\t\t\t}\n\t\t\tif (update.scheduleText !== undefined) {\n\t\t\t\tconst parsed = parseAgentCronSchedule(update.scheduleText, now);\n\t\t\t\tif (parsed.schedule.kind === \"once\") {\n\t\t\t\t\tthrow new Error(\"RLM heartbeat schedule must be recurring\");\n\t\t\t\t}\n\t\t\t\tnextJob =\n\t\t\t\t\tnextJob.status === \"paused\"\n\t\t\t\t\t\t? withoutNextRunAt({ ...nextJob, schedule: parsed.schedule })\n\t\t\t\t\t\t: { ...nextJob, schedule: parsed.schedule, nextRunAt: parsed.nextRunAt.toISOString() };\n\t\t\t}\n\t\t\tif (update.status === \"pause\") {\n\t\t\t\tnextJob = withoutNextRunAt({ ...nextJob, status: \"paused\" });\n\t\t\t} else if (update.status === \"resume\") {\n\t\t\t\tconst nextRunAt = nextRunAtForSchedule(nextJob.schedule, now);\n\t\t\t\tif (!nextRunAt) {\n\t\t\t\t\tthrow new Error(\"RLM heartbeat schedule must be recurring\");\n\t\t\t\t}\n\t\t\t\tnextJob = { ...nextJob, status: \"active\", nextRunAt: nextRunAt.toISOString() };\n\t\t\t}\n\t\t\tupdated = { ...nextJob, updatedAt: now.toISOString() };\n\t\t\treturn updated;\n\t\t});\n\t\tif (matchedRlmHeartbeat && updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\tdeleteRlmHeartbeat(activeSessionId: string, id: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet deleted: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.activeSessionId !== activeSessionId || job.source !== \"rlm_heartbeat\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tdeleted = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\treturn deleted;\n\t\t});\n\t\tif (deleted) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn deleted;\n\t}\n\n\tcancelRlmHeartbeatsForSession(activeSessionId: string, now = new Date()): AgentCronJob[] {\n\t\tconst cancelled: AgentCronJob[] = [];\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (\n\t\t\t\tjob.activeSessionId !== activeSessionId ||\n\t\t\t\tjob.source !== \"rlm_heartbeat\" ||\n\t\t\t\t(job.status !== \"active\" && job.status !== \"paused\")\n\t\t\t) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst cancelledJob = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\tcancelled.push(cancelledJob);\n\t\t\treturn cancelledJob;\n\t\t});\n\t\tif (cancelled.length > 0) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn cancelled;\n\t}\n\n\tcancelJobsForSession(\n\t\tinput: { activeSessionId?: string; sessionId?: string; sessionFile?: string },\n\t\tnow = new Date(),\n\t): AgentCronJob[] {\n\t\tconst targetSessionFile = input.sessionFile ? resolve(input.sessionFile) : undefined;\n\t\tconst cancelled: AgentCronJob[] = [];\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tconst matches =\n\t\t\t\t(input.activeSessionId !== undefined && job.activeSessionId === input.activeSessionId) ||\n\t\t\t\t(input.sessionId !== undefined && job.sessionId === input.sessionId) ||\n\t\t\t\t(targetSessionFile !== undefined && resolve(job.sessionFile) === targetSessionFile);\n\t\t\tif (!matches || (job.status !== \"active\" && job.status !== \"paused\")) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst cancelledJob = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\tcancelled.push(cancelledJob);\n\t\t\treturn cancelledJob;\n\t\t});\n\t\tif (cancelled.length > 0) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn cancelled;\n\t}\n\n\tpauseHeartbeat(activeSessionId: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet paused: AgentCronJob | undefined;\n\t\tconst current = this.getHeartbeat(activeSessionId);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== current.id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tpaused = { ...job, status: \"paused\", nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\treturn paused;\n\t\t});\n\t\tthis.writeJobs(jobs);\n\t\treturn paused;\n\t}\n\n\tresumeHeartbeat(activeSessionId: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet resumed: AgentCronJob | undefined;\n\t\tconst current = this.getHeartbeat(activeSessionId);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst nextRunAt = nextRunAtForSchedule(current.schedule, now);\n\t\tif (!nextRunAt) {\n\t\t\tthrow new Error(\"Heartbeat schedule must be recurring\");\n\t\t}\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== current.id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tresumed = { ...job, status: \"active\", nextRunAt: nextRunAt.toISOString(), updatedAt: now.toISOString() };\n\t\t\treturn resumed;\n\t\t});\n\t\tthis.writeJobs(jobs);\n\t\treturn resumed;\n\t}\n\n\tclearHeartbeat(activeSessionId: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet cleared: AgentCronJob | undefined;\n\t\tconst current = this.getHeartbeat(activeSessionId);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== current.id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tcleared = { ...job, status: \"cancelled\", nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\treturn cleared;\n\t\t});\n\t\tthis.writeJobs(jobs);\n\t\treturn cleared;\n\t}\n\n\tmanageHeartbeat(\n\t\tactiveSessionId: string,\n\t\tid: string,\n\t\taction: AgentHeartbeatManagementAction,\n\t\tnow = new Date(),\n\t): AgentCronJob | undefined {\n\t\tlet updated: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.activeSessionId !== activeSessionId || !isHeartbeatCronJob(job)) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (job.status === \"cancelled\" || job.status === \"completed\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (action === \"pause\") {\n\t\t\t\tupdated = withoutNextRunAt({ ...job, status: \"paused\", updatedAt: now.toISOString() });\n\t\t\t\treturn updated;\n\t\t\t}\n\t\t\tif (action === \"stop\") {\n\t\t\t\tupdated = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\t\treturn updated;\n\t\t\t}\n\t\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, now);\n\t\t\tif (!nextRunAt) {\n\t\t\t\tthrow new Error(\"Heartbeat schedule must be recurring\");\n\t\t\t}\n\t\t\tupdated = {\n\t\t\t\t...job,\n\t\t\t\tstatus: \"active\",\n\t\t\t\tnextRunAt: nextRunAt.toISOString(),\n\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t};\n\t\t\treturn updated;\n\t\t});\n\t\tif (updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\tresumeCronJob(\n\t\tid: string,\n\t\towner: { activeSessionId: string; sessionId: string; sessionFile: string },\n\t\tnow = new Date(),\n\t): AgentCronJob | undefined {\n\t\tconst jobs = this.readJobs();\n\t\tconst current = jobs.find((job) => job.id === id);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (\n\t\t\tcurrent.activeSessionId !== owner.activeSessionId ||\n\t\t\tcurrent.sessionId !== owner.sessionId ||\n\t\t\tresolve(current.sessionFile) !== resolve(owner.sessionFile)\n\t\t) {\n\t\t\tthrow new Error(\"Cron job does not belong to the bound session\");\n\t\t}\n\t\tif (current.status !== \"paused\" || current.source !== \"cron\" || current.runtimeKind !== \"top-level\") {\n\t\t\tthrow new Error(\"Only paused top-level cron jobs can be resumed\");\n\t\t}\n\t\tconst nextRunAt = nextRunAtForSchedule(current.schedule, now);\n\t\tif (!nextRunAt) {\n\t\t\tthrow new Error(\"Cron resume requires a recurring schedule; reschedule one-shot jobs explicitly\");\n\t\t}\n\t\tconst resumed: AgentCronJob = {\n\t\t\t...current,\n\t\t\tstatus: \"active\",\n\t\t\tnextRunAt: nextRunAt.toISOString(),\n\t\t\tupdatedAt: now.toISOString(),\n\t\t};\n\t\tthis.writeJobs(jobs.map((job) => (job.id === id ? resumed : job)));\n\t\treturn resumed;\n\t}\n\n\tcancel(id: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet cancelled: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.status === \"cancelled\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tcancelled = { ...job, status: \"cancelled\", nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\treturn cancelled;\n\t\t});\n\t\tif (cancelled) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn cancelled;\n\t}\n\n\trecordRunResult(id: string, result: { now?: Date; error?: unknown }): AgentCronJob | undefined {\n\t\tconst now = result.now ?? new Date();\n\t\tlet updated: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (job.status !== \"active\") {\n\t\t\t\tupdated = job;\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst lastError = result.error === undefined ? undefined : errorMessage(result.error);\n\t\t\tconst nextRunAt =\n\t\t\t\tjob.schedule.kind === \"cron\"\n\t\t\t\t\t? nextRunAtForSchedule(job.schedule, new Date(now.getTime() + 1))\n\t\t\t\t\t: job.schedule.kind === \"interval\"\n\t\t\t\t\t\t? nextRunAtForSchedule(job.schedule, now)\n\t\t\t\t\t\t: undefined;\n\t\t\tupdated = {\n\t\t\t\t...job,\n\t\t\t\tstatus: job.schedule.kind === \"once\" ? \"completed\" : \"active\",\n\t\t\t\tnextRunAt: nextRunAt?.toISOString(),\n\t\t\t\tlastRunAt: now.toISOString(),\n\t\t\t\tlastError,\n\t\t\t\trunCount: job.runCount + 1,\n\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t};\n\t\t\treturn updated;\n\t\t});\n\t\tif (updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\trecordSkipResult(id: string, result: { now?: Date }): AgentCronJob | undefined {\n\t\tconst now = result.now ?? new Date();\n\t\tlet updated: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (job.status !== \"active\") {\n\t\t\t\tupdated = job;\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, now);\n\t\t\tupdated = {\n\t\t\t\t...job,\n\t\t\t\tnextRunAt: nextRunAt?.toISOString(),\n\t\t\t\tlastSkippedAt: now.toISOString(),\n\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t};\n\t\t\treturn updated;\n\t\t});\n\t\tif (updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\tdue(now = new Date()): AgentCronJob[] {\n\t\treturn this.readJobs().filter((job) => isDueJob(job, now));\n\t}\n\n\tclaimDue(dueAt = new Date(), claimedAt = dueAt): AgentCronDispatch[] {\n\t\treturn this.mutateStates((state) => claimDueInState(state, dueAt, claimedAt));\n\t}\n\n\tgetClaimedJob(id: string): AgentCronJob | undefined {\n\t\tfor (const state of this.readStates()) {\n\t\t\tif (!state.dispatches.some((dispatch) => dispatch.jobId === id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn state.jobs.find((job) => job.id === id && job.status === \"active\");\n\t\t}\n\t\treturn undefined;\n\t}\n\n\trecordDispatchResult(\n\t\tdispatchId: string,\n\t\tresult: { now?: Date; outcome: AgentCronJobRunResult; error?: unknown },\n\t): AgentCronJob | undefined {\n\t\tlet updated: AgentCronJob | undefined;\n\t\tthis.mutateStates((state) => {\n\t\t\tconst dispatch = state.dispatches.find((candidate) => candidate.id === dispatchId);\n\t\t\tif (!dispatch) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst now = result.now ?? new Date();\n\t\t\tstate.dispatches = state.dispatches.filter((candidate) => candidate.id !== dispatchId);\n\t\t\tstate.jobs = state.jobs.map((job) => {\n\t\t\t\tif (job.id !== dispatch.jobId || job.status !== \"active\") {\n\t\t\t\t\treturn job;\n\t\t\t\t}\n\t\t\t\tif (result.outcome === \"skipped\" && result.error === undefined) {\n\t\t\t\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, now);\n\t\t\t\t\tupdated = {\n\t\t\t\t\t\t...job,\n\t\t\t\t\t\tstatus: job.schedule.kind === \"once\" ? \"completed\" : job.status,\n\t\t\t\t\t\tnextRunAt: nextRunAt?.toISOString(),\n\t\t\t\t\t\tlastSkippedAt: now.toISOString(),\n\t\t\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t\t\t};\n\t\t\t\t\treturn updated;\n\t\t\t\t}\n\t\t\t\tupdated = {\n\t\t\t\t\t...job,\n\t\t\t\t\tstatus: job.schedule.kind === \"once\" ? \"completed\" : job.status,\n\t\t\t\t\tlastRunAt: now.toISOString(),\n\t\t\t\t\tlastError: result.error === undefined ? undefined : errorMessage(result.error),\n\t\t\t\t\trunCount: job.runCount + 1,\n\t\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t\t};\n\t\t\t\treturn updated;\n\t\t\t});\n\t\t\treturn [];\n\t\t});\n\t\treturn updated;\n\t}\n\n\trecoverInterruptedDispatches(now = new Date()): AgentCronJob[] {\n\t\tconst recovered: AgentCronJob[] = [];\n\t\tthis.mutateStates((state) => {\n\t\t\trecoverInterruptedInState(state, now, recovered);\n\t\t\treturn [];\n\t\t});\n\t\treturn recovered;\n\t}\n\n\trecoverInterruptedDispatchesById(dispatchIds: readonly string[], now = new Date()): AgentCronJob[] {\n\t\tconst recovered: AgentCronJob[] = [];\n\t\tconst interruptedDispatchIds = new Set(dispatchIds);\n\t\tthis.mutateStates((state) => {\n\t\t\trecoverInterruptedInState(state, now, recovered, interruptedDispatchIds);\n\t\t\treturn [];\n\t\t});\n\t\treturn recovered;\n\t}\n\n\tgetDueJob(id: string, now = new Date()): AgentCronJob | undefined {\n\t\treturn this.readJobs().find((job) => job.id === id && isDueJob(job, now));\n\t}\n\n\tnextActiveRunAt(): Date | undefined {\n\t\tconst times = this.readJobs()\n\t\t\t.filter((job) => job.status === \"active\" && job.nextRunAt !== undefined)\n\t\t\t.map((job) => new Date(job.nextRunAt!))\n\t\t\t.filter((date) => Number.isFinite(date.getTime()))\n\t\t\t.sort((a, b) => a.getTime() - b.getTime());\n\t\treturn times[0];\n\t}\n\n\tprivate readJobs(): AgentCronJob[] {\n\t\treturn this.readStates().flatMap((state) => state.jobs);\n\t}\n\n\tprivate readStates(): CronJobsState[] {\n\t\tif (this.sessionArtifactMode) {\n\t\t\treturn [...this.sessionArtifactFiles.values()].map((path) => readJobsState(path));\n\t\t}\n\t\treturn [readJobsState(this.requireFilePath())];\n\t}\n\n\tprivate mutateStates(mutator: (state: CronJobsState) => AgentCronDispatch[]): AgentCronDispatch[] {\n\t\tconst paths = this.sessionArtifactMode ? [...this.sessionArtifactFiles.values()] : [this.requireFilePath()];\n\t\tconst previousHeartbeats = heartbeatCatalogSignature(this.readJobs());\n\t\tlet changed = false;\n\t\tconst dispatches = withCronJobsStateLocks(paths, () => {\n\t\t\tconst dispatches: AgentCronDispatch[] = [];\n\t\t\tfor (const path of paths) {\n\t\t\t\tconst state = readJobsState(path);\n\t\t\t\tconst before = JSON.stringify(state);\n\t\t\t\tdispatches.push(...mutator(state));\n\t\t\t\tif (JSON.stringify(state) !== before) {\n\t\t\t\t\twriteJobsState(path, state);\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn dispatches;\n\t\t});\n\t\tif (changed && heartbeatCatalogSignature(this.readJobs()) !== previousHeartbeats) {\n\t\t\tthis.notifyHeartbeatChange();\n\t\t}\n\t\treturn dispatches;\n\t}\n\n\tprivate writeJobs(jobs: readonly AgentCronJob[]): void {\n\t\tconst previousHeartbeats = heartbeatCatalogSignature(this.readJobs());\n\t\tif (this.sessionArtifactMode) {\n\t\t\tconst registeredSessionIds = new Set(this.sessionArtifactFiles.keys());\n\t\t\tconst unregistered = jobs.find((job) => !registeredSessionIds.has(job.sessionId));\n\t\t\tif (unregistered) {\n\t\t\t\tthrow new Error(`Cron job ${unregistered.id} targets an unregistered session artifact`);\n\t\t\t}\n\t\t\tconst paths = [...this.sessionArtifactFiles.values()];\n\t\t\twithCronJobsStateLocks(paths, () => {\n\t\t\t\tconst currentBySessionId = new Map(\n\t\t\t\t\t[...this.sessionArtifactFiles].map(([sessionId, path]) => [sessionId, readJobsState(path)]),\n\t\t\t\t);\n\t\t\t\tconst incomingById = new Map(jobs.map((job) => [job.id, job]));\n\t\t\t\tconst mergedJobsBySessionId = new Map<string, AgentCronJob[]>();\n\t\t\t\tfor (const [sessionId, current] of currentBySessionId) {\n\t\t\t\t\tconst retained = current.jobs.filter((job) => {\n\t\t\t\t\t\tconst incoming = incomingById.get(job.id);\n\t\t\t\t\t\treturn incoming === undefined || incoming.sessionId === sessionId;\n\t\t\t\t\t});\n\t\t\t\t\tmergedJobsBySessionId.set(\n\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\tmergeFreshJobs(\n\t\t\t\t\t\t\tretained,\n\t\t\t\t\t\t\tjobs.filter((job) => job.sessionId === sessionId),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst sessionIdByJobId = new Map(\n\t\t\t\t\t[...mergedJobsBySessionId].flatMap(([sessionId, sessionJobs]) =>\n\t\t\t\t\t\tsessionJobs.map((job) => [job.id, sessionId] as const),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tconst dispatches = [...currentBySessionId.values()].flatMap((state) => state.dispatches);\n\t\t\t\tfor (const [sessionId, path] of this.sessionArtifactFiles) {\n\t\t\t\t\tconst current = currentBySessionId.get(sessionId) ?? { jobs: [], dispatches: [] };\n\t\t\t\t\tconst nextState = {\n\t\t\t\t\t\tjobs: mergedJobsBySessionId.get(sessionId) ?? [],\n\t\t\t\t\t\tdispatches: dispatches.filter((dispatch) => sessionIdByJobId.get(dispatch.jobId) === sessionId),\n\t\t\t\t\t};\n\t\t\t\t\tif (JSON.stringify(current) !== JSON.stringify(nextState)) {\n\t\t\t\t\t\twriteJobsState(path, nextState);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (heartbeatCatalogSignature(this.readJobs()) !== previousHeartbeats) {\n\t\t\t\tthis.notifyHeartbeatChange();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst path = this.requireFilePath();\n\t\twithCronJobsStateLocks([path], () => writeJobsFile(path, jobs, true));\n\t\tif (heartbeatCatalogSignature(this.readJobs()) !== previousHeartbeats) {\n\t\t\tthis.notifyHeartbeatChange();\n\t\t}\n\t}\n\n\tprivate notifyHeartbeatChange(): void {\n\t\tfor (const listener of this.heartbeatChangeListeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n\n\tprivate requireFilePath(): string {\n\t\tif (!this.filePath) {\n\t\t\tthrow new Error(\"Cron job store does not have a legacy file path\");\n\t\t}\n\t\treturn this.filePath;\n\t}\n}\n\nexport function migrateLegacyCronJobsToSessionArtifacts(\n\tfilePath: string,\n\toptions: { isSessionOwned?: (job: AgentCronJob) => boolean; now?: Date } = {},\n): number {\n\tconst now = options.now ?? new Date();\n\tconst legacyState = readJobsState(filePath);\n\trecoverInterruptedInState(legacyState, now, []);\n\tconst jobs = legacyState.jobs.map((job) => {\n\t\tif (\n\t\t\t!options.isSessionOwned ||\n\t\t\toptions.isSessionOwned(job) ||\n\t\t\t(job.status !== \"active\" && job.status !== \"paused\")\n\t\t) {\n\t\t\treturn job;\n\t\t}\n\t\treturn withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t});\n\tif (jobs.length === 0) {\n\t\treturn 0;\n\t}\n\tconst jobsByArtifact = new Map<string, AgentCronJob[]>();\n\tfor (const job of jobs) {\n\t\tconst artifactPath = join(\n\t\t\tgetSessionArtifactPathForFile(resolve(job.sessionFile), job.sessionId),\n\t\t\tSESSION_SCHEDULED_JOBS_FILENAME,\n\t\t);\n\t\tconst grouped = jobsByArtifact.get(artifactPath) ?? [];\n\t\tgrouped.push(job);\n\t\tjobsByArtifact.set(artifactPath, grouped);\n\t}\n\tfor (const [artifactPath, artifactJobs] of jobsByArtifact) {\n\t\twriteJobsFile(artifactPath, artifactJobs, true);\n\t}\n\trenameSync(filePath, `${filePath}.migrated-${Date.now()}`);\n\treturn jobs.length;\n}\n\nexport class AgentCronScheduler {\n\tprivate timer: ReturnType<typeof setTimeout> | undefined;\n\tprivate running = false;\n\tprivate stopped = true;\n\tprivate hasStarted = false;\n\tprivate readonly dispatchLanes = new Map<string, Promise<void>>();\n\n\tconstructor(\n\t\tprivate readonly store: AgentCronJobStore,\n\t\tprivate readonly hooks: AgentCronSchedulerHooks,\n\t) {}\n\n\tstart(): void {\n\t\tthis.stopped = false;\n\t\tif (!this.hasStarted) {\n\t\t\tthis.hasStarted = true;\n\t\t\tthis.store.recoverInterruptedDispatches(this.now());\n\t\t}\n\t\tthis.scheduleNext();\n\t}\n\n\tstop(): void {\n\t\tthis.stopped = true;\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\twake(): void {\n\t\tif (this.stopped) {\n\t\t\treturn;\n\t\t}\n\t\tthis.scheduleNext(0);\n\t}\n\n\tasync runDue(now = this.now()): Promise<number> {\n\t\tif (this.running || (this.stopped && this.hasStarted)) {\n\t\t\treturn 0;\n\t\t}\n\t\tthis.running = true;\n\t\tconst dispatches: Array<{ dispatch: AgentCronDispatch; endDispatch?: () => void }> = [];\n\t\tlet claimedDispatches: AgentCronDispatch[] | undefined;\n\t\ttry {\n\t\t\tclaimedDispatches = this.store.claimDue(now, this.now());\n\t\t\tfor (const dispatch of claimedDispatches) {\n\t\t\t\tdispatches.push({ dispatch, endDispatch: this.hooks.beginDispatch?.(dispatch) });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tfor (const claimed of dispatches) claimed.endDispatch?.();\n\t\t\tif (claimedDispatches) {\n\t\t\t\tthis.store.recoverInterruptedDispatchesById(\n\t\t\t\t\tclaimedDispatches.map((dispatch) => dispatch.id),\n\t\t\t\t\tthis.now(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tthis.running = false;\n\t\t\tif (!this.stopped) {\n\t\t\t\tthis.scheduleNext();\n\t\t\t}\n\t\t}\n\t\tconst results = await Promise.all(\n\t\t\tdispatches.map(({ dispatch, endDispatch }) => this.queueDispatch(dispatch, endDispatch)),\n\t\t);\n\t\treturn results.filter((result) => result !== \"skipped\").length;\n\t}\n\n\tprivate queueDispatch(\n\t\tdispatch: AgentCronDispatch,\n\t\tendDispatch?: () => void,\n\t): Promise<AgentCronJobRunResult | undefined> {\n\t\tconst laneKey = dispatch.job.activeSessionId;\n\t\tconst previous = this.dispatchLanes.get(laneKey) ?? Promise.resolve();\n\t\tconst task = previous\n\t\t\t.catch(() => undefined)\n\t\t\t.then(async (): Promise<AgentCronJobRunResult | undefined> => {\n\t\t\t\ttry {\n\t\t\t\t\tconst job = this.store.getClaimedJob(dispatch.job.id);\n\t\t\t\t\tif (!job) {\n\t\t\t\t\t\tthis.store.recordDispatchResult(dispatch.id, { now: this.now(), outcome: \"skipped\" });\n\t\t\t\t\t\treturn \"skipped\";\n\t\t\t\t\t}\n\t\t\t\t\tlet runResult: AgentCronJobRunResult | undefined;\n\t\t\t\t\tlet error: unknown;\n\t\t\t\t\ttry {\n\t\t\t\t\t\trunResult = await this.hooks.runJob(job);\n\t\t\t\t\t} catch (runError) {\n\t\t\t\t\t\terror = runError;\n\t\t\t\t\t\tthis.hooks.onError?.(job, runError);\n\t\t\t\t\t}\n\t\t\t\t\tthis.store.recordDispatchResult(dispatch.id, {\n\t\t\t\t\t\tnow: this.now(),\n\t\t\t\t\t\toutcome: runResult === \"skipped\" && error === undefined ? \"skipped\" : \"ran\",\n\t\t\t\t\t\terror,\n\t\t\t\t\t});\n\t\t\t\t\treturn runResult;\n\t\t\t\t} finally {\n\t\t\t\t\tendDispatch?.();\n\t\t\t\t}\n\t\t\t});\n\t\tconst lane = task.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\tthis.dispatchLanes.set(laneKey, lane);\n\t\tvoid lane.finally(() => {\n\t\t\tif (this.dispatchLanes.get(laneKey) === lane) {\n\t\t\t\tthis.dispatchLanes.delete(laneKey);\n\t\t\t}\n\t\t});\n\t\treturn task;\n\t}\n\n\tprivate scheduleNext(delayMs?: number): void {\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t\tconst now = this.now();\n\t\tconst nextDelay =\n\t\t\tdelayMs ??\n\t\t\t(() => {\n\t\t\t\tconst next = this.store.nextActiveRunAt();\n\t\t\t\tif (!next) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn Math.max(0, next.getTime() - now.getTime());\n\t\t\t})();\n\t\tif (nextDelay === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthis.timer = setTimeout(\n\t\t\t() => {\n\t\t\t\tvoid this.runDue();\n\t\t\t},\n\t\t\tMath.min(nextDelay, MAX_TIMEOUT_MS),\n\t\t);\n\t}\n\n\tprivate now(): Date {\n\t\treturn this.hooks.now?.() ?? new Date();\n\t}\n}\n\nexport function parseAgentCronSchedule(\n\tinput: string,\n\tnow = new Date(),\n): { schedule: AgentCronSchedule; nextRunAt: Date } {\n\tconst text = stripMatchingQuotes(input.trim());\n\tif (!text) {\n\t\tthrow new Error(\"Cron schedule cannot be empty\");\n\t}\n\n\tconst inMatch = /^in\\s+(\\d+)\\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i.exec(text);\n\tif (inMatch) {\n\t\tconst amount = Number.parseInt(inMatch[1]!, 10);\n\t\tconst unit = inMatch[2]!.toLowerCase();\n\t\tconst multiplier = unit.startsWith(\"m\")\n\t\t\t? ONE_MINUTE_MS\n\t\t\t: unit.startsWith(\"h\")\n\t\t\t\t? 60 * ONE_MINUTE_MS\n\t\t\t\t: 24 * 60 * ONE_MINUTE_MS;\n\t\treturn {\n\t\t\tschedule: { kind: \"once\", expression: text },\n\t\t\tnextRunAt: new Date(now.getTime() + amount * multiplier),\n\t\t};\n\t}\n\n\tconst everyMatch =\n\t\t/^(?:every|each)\\s+(\\d+)\\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/i.exec(\n\t\t\ttext,\n\t\t);\n\tif (everyMatch) {\n\t\tconst amount = Number.parseInt(everyMatch[1]!, 10);\n\t\tconst unit = everyMatch[2]!.toLowerCase();\n\t\tconst multiplier = unit.startsWith(\"s\")\n\t\t\t? ONE_SECOND_MS\n\t\t\t: unit.startsWith(\"m\")\n\t\t\t\t? ONE_MINUTE_MS\n\t\t\t\t: 60 * ONE_MINUTE_MS;\n\t\tconst intervalMs = amount * multiplier;\n\t\tif (intervalMs < 10 * ONE_SECOND_MS) {\n\t\t\tthrow new Error(\"Recurring interval must be at least 10 seconds\");\n\t\t}\n\t\treturn {\n\t\t\tschedule: { kind: \"interval\", expression: text, intervalMs },\n\t\t\tnextRunAt: new Date(now.getTime() + intervalMs),\n\t\t};\n\t}\n\n\tif (text.toLowerCase().startsWith(\"at \")) {\n\t\tconst when = new Date(text.slice(3).trim());\n\t\tif (!Number.isFinite(when.getTime())) {\n\t\t\tthrow new Error(\"Invalid one-shot schedule. Use: at <ISO date>\");\n\t\t}\n\t\tif (when.getTime() <= now.getTime()) {\n\t\t\tthrow new Error(\"One-shot schedule must be in the future\");\n\t\t}\n\t\treturn { schedule: { kind: \"once\", expression: text }, nextRunAt: when };\n\t}\n\n\tconst expression = normalizeCronAlias(text);\n\tconst nextRunAt = nextCronRunAfter(expression, now);\n\treturn { schedule: { kind: \"cron\", expression }, nextRunAt };\n}\n\nexport function normalizeHeartbeatSchedule(input: string | undefined): string {\n\tconst text = input?.trim();\n\tif (!text) {\n\t\treturn DEFAULT_HEARTBEAT_SCHEDULE;\n\t}\n\tif (/^\\d+\\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/i.test(text)) {\n\t\treturn `every ${text}`;\n\t}\n\treturn text;\n}\n\nexport function normalizeHeartbeatDeliveryMode(value: unknown): AgentHeartbeatDeliveryMode | undefined {\n\tif (value === undefined || value === null) {\n\t\treturn undefined;\n\t}\n\tif (value === \"steer\" || value === \"follow_up\") {\n\t\treturn value;\n\t}\n\tthrow new Error('Heartbeat delivery mode must be \"steer\" or \"follow_up\"');\n}\n\nexport function resolveHeartbeatStreamingBehavior(\n\tdeliveryMode: AgentHeartbeatDeliveryMode | undefined,\n): \"steer\" | \"followUp\" {\n\treturn (deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE) === \"follow_up\" ? \"followUp\" : \"steer\";\n}\n\nexport function parseHeartbeatCommand(input: string): ParsedHeartbeatCommand {\n\tconst text = input.replace(/^\\/heartbeat\\b/, \"\").trim();\n\tif (!text || text === \"status\") {\n\t\treturn { type: \"status\" };\n\t}\n\tif (text === \"pause\") {\n\t\treturn { type: \"pause\" };\n\t}\n\tif (text === \"resume\") {\n\t\treturn { type: \"resume\" };\n\t}\n\tif (text === \"clear\" || text === \"stop\") {\n\t\treturn { type: \"clear\" };\n\t}\n\n\tconst leadingDelivery = consumeDeliveryOption(text);\n\tlet deliveryMode = leadingDelivery.deliveryMode;\n\tlet remaining = leadingDelivery.rest;\n\n\tconst option = consumeEveryOption(remaining);\n\tif (option) {\n\t\tconst trailingDelivery = consumeDeliveryOption(option.rest);\n\t\tdeliveryMode = trailingDelivery.deliveryMode ?? deliveryMode;\n\t\tif (!trailingDelivery.rest) {\n\t\t\tthrow new Error(\"Usage: /heartbeat [--every <interval>] [--steer|--follow-up] <instruction>\");\n\t\t}\n\t\treturn {\n\t\t\ttype: \"set\",\n\t\t\tschedule: normalizeHeartbeatSchedule(option.interval),\n\t\t\tinstruction: trailingDelivery.rest,\n\t\t\t...(deliveryMode ? { deliveryMode } : {}),\n\t\t};\n\t}\n\n\tconst leadingSchedule = consumeLeadingEverySchedule(remaining);\n\tif (leadingSchedule) {\n\t\tconst trailingDelivery = consumeDeliveryOption(leadingSchedule.rest);\n\t\tdeliveryMode = trailingDelivery.deliveryMode ?? deliveryMode;\n\t\tif (!trailingDelivery.rest) {\n\t\t\tthrow new Error(\"Usage: /heartbeat [--every <interval>] [--steer|--follow-up] <instruction>\");\n\t\t}\n\t\treturn {\n\t\t\ttype: \"set\",\n\t\t\tschedule: normalizeHeartbeatSchedule(leadingSchedule.interval),\n\t\t\tinstruction: trailingDelivery.rest,\n\t\t\t...(deliveryMode ? { deliveryMode } : {}),\n\t\t};\n\t}\n\n\tremaining = remaining.trim();\n\tif (!remaining) {\n\t\tthrow new Error(\"Usage: /heartbeat [--every <interval>] [--steer|--follow-up] <instruction>\");\n\t}\n\treturn {\n\t\ttype: \"set\",\n\t\tschedule: DEFAULT_HEARTBEAT_SCHEDULE,\n\t\tinstruction: remaining,\n\t\t...(deliveryMode ? { deliveryMode } : {}),\n\t};\n}\n\nexport function nextRunAtForSchedule(schedule: AgentCronSchedule, after: Date): Date | undefined {\n\tif (schedule.kind === \"once\") {\n\t\treturn undefined;\n\t}\n\tif (schedule.kind === \"interval\") {\n\t\tif (!schedule.intervalMs || schedule.intervalMs <= 0) {\n\t\t\tthrow new Error(`Invalid interval schedule: ${schedule.expression}`);\n\t\t}\n\t\treturn new Date(after.getTime() + schedule.intervalMs);\n\t}\n\treturn nextCronRunAfter(schedule.expression, after);\n}\n\nexport function formatAgentCronJob(job: AgentCronJob): string {\n\tconst next = job.nextRunAt ? new Date(job.nextRunAt).toLocaleString() : \"-\";\n\tconst last = job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : \"-\";\n\tconst preview = job.prompt.replace(/\\s+/g, \" \").slice(0, 80);\n\tconst error = job.lastError ? ` error=${job.lastError}` : \"\";\n\tconst label = job.label ? ` label=\"${job.label}\"` : \"\";\n\tconst skipped = job.lastSkippedAt ? ` skipped=${new Date(job.lastSkippedAt).toLocaleString()}` : \"\";\n\treturn `${job.id} ${job.status}${label} next=${next} last=${last}${skipped} runs=${job.runCount} schedule=\"${job.schedule.expression}\" prompt=\"${preview}\"${error}`;\n}\n\nfunction consumeDeliveryOption(text: string): { deliveryMode: AgentHeartbeatDeliveryMode | undefined; rest: string } {\n\tlet rest = text.trim();\n\tif (/(?:^|\\s)--deliver=?$/i.test(rest)) {\n\t\tthrow new Error('Heartbeat delivery mode must be \"steer\" or \"follow_up\"');\n\t}\n\tlet deliveryMode: AgentHeartbeatDeliveryMode | undefined;\n\tlet leading = consumeLeadingDeliveryFlag(rest);\n\twhile (leading) {\n\t\tdeliveryMode = leading.deliveryMode;\n\t\trest = leading.rest.trim();\n\t\tleading = consumeLeadingDeliveryFlag(rest);\n\t}\n\n\tlet trailing = consumeTrailingDeliveryFlag(rest);\n\tlet trailingDeliveryMode: AgentHeartbeatDeliveryMode | undefined;\n\twhile (trailing) {\n\t\t// Consume all trailing flags, but keep the rightmost flag's mode because it\n\t\t// is textually latest and should win over earlier flags.\n\t\ttrailingDeliveryMode ??= trailing.deliveryMode;\n\t\trest = trailing.rest.trim();\n\t\ttrailing = consumeTrailingDeliveryFlag(rest);\n\t}\n\treturn { deliveryMode: trailingDeliveryMode ?? deliveryMode, rest };\n}\n\nfunction consumeLeadingDeliveryFlag(\n\ttext: string,\n): { deliveryMode: AgentHeartbeatDeliveryMode; rest: string } | undefined {\n\tconst match = /^--(?:deliver(?:=|\\s+)(\\S+)|(steer)|(follow[-_]up))(?:\\s+|$)([\\s\\S]*)$/i.exec(text);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tdeliveryMode: parseDeliveryModeToken(match[1] ?? match[2] ?? match[3] ?? \"\"),\n\t\trest: match[4]?.trim() ?? \"\",\n\t};\n}\n\nfunction consumeTrailingDeliveryFlag(\n\ttext: string,\n): { deliveryMode: AgentHeartbeatDeliveryMode; rest: string } | undefined {\n\tconst deliverWithSpace = /^([\\s\\S]*?)\\s+--deliver\\s+(\\S+)$/i.exec(text);\n\tif (deliverWithSpace) {\n\t\treturn { deliveryMode: parseDeliveryModeToken(deliverWithSpace[2] ?? \"\"), rest: deliverWithSpace[1] ?? \"\" };\n\t}\n\tconst deliverWithEquals = /^([\\s\\S]*?)\\s+--deliver=(\\S+)$/i.exec(text);\n\tif (deliverWithEquals) {\n\t\treturn { deliveryMode: parseDeliveryModeToken(deliverWithEquals[2] ?? \"\"), rest: deliverWithEquals[1] ?? \"\" };\n\t}\n\tconst shorthand = /^([\\s\\S]*?)\\s+--(steer|follow[-_]up)$/i.exec(text);\n\tif (shorthand) {\n\t\treturn { deliveryMode: parseDeliveryModeToken(shorthand[2] ?? \"\"), rest: shorthand[1] ?? \"\" };\n\t}\n\treturn undefined;\n}\n\nfunction parseDeliveryModeToken(token: string): AgentHeartbeatDeliveryMode {\n\tconst normalized = token.toLowerCase().replace(\"-\", \"_\");\n\tif (normalized === \"steer\" || normalized === \"follow_up\") {\n\t\treturn normalized;\n\t}\n\tthrow new Error('Heartbeat delivery mode must be \"steer\" or \"follow_up\"');\n}\n\nfunction consumeEveryOption(text: string): { interval: string; rest: string } | undefined {\n\tconst match =\n\t\t/^--every(?:=|\\s+)(?:\"([^\"]+)\"|'([^']+)'|(\\d+\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours))|(\\S+))(?:\\s+|$)([\\s\\S]*)$/i.exec(\n\t\t\ttext,\n\t\t);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tinterval: match[1] ?? match[2] ?? match[3] ?? match[4] ?? \"\",\n\t\trest: match[5]?.trim() ?? \"\",\n\t};\n}\n\nfunction consumeLeadingEverySchedule(text: string): { interval: string; rest: string } | undefined {\n\tconst match =\n\t\t/^(every|each)\\s+\\d+\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\\b/i.exec(text);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tinterval: match[0],\n\t\trest: text\n\t\t\t.slice(match[0].length)\n\t\t\t.trim()\n\t\t\t// Strip only a standalone \"--\" separator, never a flag like \"--follow-up\".\n\t\t\t.replace(/^--(?=\\s|$)/, \"\")\n\t\t\t.trim(),\n\t};\n}\n\nexport function isHeartbeatCronJob(job: AgentCronJob): boolean {\n\treturn job.source === \"heartbeat\" || job.source === \"rlm_heartbeat\";\n}\n\nexport function shouldDeferHeartbeatCronJob(job: AgentCronJob, activity: HeartbeatCronSessionActivity): boolean {\n\tif (!isHeartbeatCronJob(job)) {\n\t\treturn false;\n\t}\n\t// States where delivering a heartbeat is unsafe or would stack redundant work,\n\t// regardless of delivery mode.\n\tconst busyBesidesStreaming =\n\t\tactivity.isCompacting === true ||\n\t\tactivity.isRetrying === true ||\n\t\tactivity.isBashRunning ||\n\t\tactivity.hasPendingSessionWork ||\n\t\t(!activity.isStreaming && activity.unfinishedActionCount > 0);\n\tif (busyBesidesStreaming) {\n\t\treturn true;\n\t}\n\t// \"steer\" heartbeats interrupt the current turn, so a plain streaming turn must\n\t// not defer them; \"follow_up\" heartbeats wait, so streaming still defers.\n\tif (resolveHeartbeatStreamingBehavior(job.deliveryMode) === \"steer\") {\n\t\treturn false;\n\t}\n\treturn activity.isStreaming;\n}\n\nfunction nextCronRunAfter(expression: string, after: Date): Date {\n\tconst fields = parseCronExpression(expression);\n\tconst candidate = new Date(after.getTime());\n\tcandidate.setSeconds(0, 0);\n\tcandidate.setMinutes(candidate.getMinutes() + 1);\n\n\tconst deadline = candidate.getTime() + 366 * 24 * 60 * ONE_MINUTE_MS;\n\twhile (candidate.getTime() <= deadline) {\n\t\tif (matchesCronFields(candidate, fields)) {\n\t\t\treturn candidate;\n\t\t}\n\t\tcandidate.setMinutes(candidate.getMinutes() + 1);\n\t}\n\tthrow new Error(`Cron schedule did not match within one year: ${expression}`);\n}\n\nfunction parseCronExpression(expression: string): CronFields {\n\tconst parts = expression.trim().split(/\\s+/);\n\tif (parts.length !== 5) {\n\t\tthrow new Error(\n\t\t\t\"Unsupported cron schedule. Use 'in 10m', 'at <ISO date>', @hourly, or five fields: minute hour day month weekday\",\n\t\t);\n\t}\n\treturn {\n\t\tminute: parseCronField(parts[0]!, 0, 59),\n\t\thour: parseCronField(parts[1]!, 0, 23),\n\t\tdayOfMonth: parseCronField(parts[2]!, 1, 31),\n\t\tmonth: parseCronField(parts[3]!, 1, 12),\n\t\tdayOfWeek: parseCronField(parts[4]!, 0, 7),\n\t};\n}\n\ninterface CronFields {\n\tminute: Set<number>;\n\thour: Set<number>;\n\tdayOfMonth: Set<number>;\n\tmonth: Set<number>;\n\tdayOfWeek: Set<number>;\n}\n\nfunction parseCronField(field: string, min: number, max: number): Set<number> {\n\tconst values = new Set<number>();\n\tfor (const part of field.split(\",\")) {\n\t\tif (!part) {\n\t\t\tthrow new Error(`Invalid cron field: ${field}`);\n\t\t}\n\t\tconst [rangeText, stepText] = part.split(\"/\");\n\t\tconst step = stepText === undefined ? 1 : parseCronNumber(stepText, 1, max);\n\t\tlet start: number;\n\t\tlet end: number;\n\t\tif (rangeText === \"*\") {\n\t\t\tstart = min;\n\t\t\tend = max;\n\t\t} else if (rangeText?.includes(\"-\")) {\n\t\t\tconst [startText, endText] = rangeText.split(\"-\");\n\t\t\tstart = parseCronNumber(startText, min, max);\n\t\t\tend = parseCronNumber(endText, min, max);\n\t\t\tif (start > end) {\n\t\t\t\tthrow new Error(`Invalid cron range: ${rangeText}`);\n\t\t\t}\n\t\t} else {\n\t\t\tstart = parseCronNumber(rangeText, min, max);\n\t\t\tend = start;\n\t\t}\n\t\tfor (let value = start; value <= end; value += step) {\n\t\t\tvalues.add(value);\n\t\t}\n\t}\n\treturn values;\n}\n\nfunction parseCronNumber(value: string | undefined, min: number, max: number): number {\n\tif (!value || !/^\\d+$/.test(value)) {\n\t\tthrow new Error(`Invalid cron number: ${value ?? \"\"}`);\n\t}\n\tconst parsed = Number.parseInt(value, 10);\n\tif (parsed < min || parsed > max) {\n\t\tthrow new Error(`Cron number out of range: ${value}`);\n\t}\n\treturn parsed;\n}\n\nfunction matchesCronFields(date: Date, fields: CronFields): boolean {\n\tconst day = date.getDay();\n\tconst dayMatches = fields.dayOfWeek.has(day) || (day === 0 && fields.dayOfWeek.has(7));\n\treturn (\n\t\tfields.minute.has(date.getMinutes()) &&\n\t\tfields.hour.has(date.getHours()) &&\n\t\tfields.dayOfMonth.has(date.getDate()) &&\n\t\tfields.month.has(date.getMonth() + 1) &&\n\t\tdayMatches\n\t);\n}\n\nfunction normalizeCronAlias(text: string): string {\n\tswitch (text) {\n\t\tcase \"@hourly\":\n\t\t\treturn \"0 * * * *\";\n\t\tcase \"@daily\":\n\t\t\treturn \"0 0 * * *\";\n\t\tcase \"@weekly\":\n\t\t\treturn \"0 0 * * 0\";\n\t\tcase \"@monthly\":\n\t\t\treturn \"0 0 1 * *\";\n\t\tdefault:\n\t\t\treturn text;\n\t}\n}\n\nfunction stripMatchingQuotes(value: string): string {\n\tif (\n\t\tvalue.length >= 2 &&\n\t\t((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\")))\n\t) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction isDueJob(job: AgentCronJob, now: Date): boolean {\n\treturn job.status === \"active\" && job.nextRunAt !== undefined && Date.parse(job.nextRunAt) <= now.getTime();\n}\n\nfunction withCronJobsStateLocks<T>(paths: readonly string[], action: () => T): T {\n\tconst releases: Array<() => void> = [];\n\ttry {\n\t\tfor (const path of [...new Set(paths)].sort()) {\n\t\t\tmkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n\t\t\tlet release: (() => void) | undefined;\n\t\t\tlet lockCompromised = false;\n\t\t\tfor (let attempt = 0; attempt < 100; attempt++) {\n\t\t\t\ttry {\n\t\t\t\t\trelease = lockSync(path, {\n\t\t\t\t\t\trealpath: false,\n\t\t\t\t\t\tlockfilePath: `${path}.lock`,\n\t\t\t\t\t\tstale: 30_000,\n\t\t\t\t\t\tonCompromised: () => {\n\t\t\t\t\t\t\tlockCompromised = true;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tif (lockCompromised) {\n\t\t\t\t\t\trelease();\n\t\t\t\t\t\tthrow new Error(`Cron jobs lock compromised: ${path}`);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (lockCompromised) {\n\t\t\t\t\t\tthrow new Error(`Cron jobs lock compromised: ${path}`);\n\t\t\t\t\t}\n\t\t\t\t\tif ((error as NodeJS.ErrnoException).code !== \"ELOCKED\" || attempt === 99) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!release) {\n\t\t\t\tthrow new Error(`Could not coordinate scheduled jobs: ${path}`);\n\t\t\t}\n\t\t\treleases.push(release);\n\t\t}\n\t\treturn action();\n\t} finally {\n\t\tfor (const release of releases.reverse()) {\n\t\t\trelease();\n\t\t}\n\t}\n}\n\nfunction readJobsState(path: string): CronJobsState {\n\tif (!existsSync(path)) {\n\t\treturn { jobs: [], dispatches: [] };\n\t}\n\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as CronJobsFile;\n\treturn {\n\t\tjobs: Array.isArray(parsed.jobs) ? parsed.jobs.filter(isAgentCronJob) : [],\n\t\tdispatches: Array.isArray(parsed.dispatches) ? parsed.dispatches.filter(isAgentCronDispatchRecord) : [],\n\t};\n}\n\nfunction writeJobsFile(path: string, jobs: readonly AgentCronJob[], mergeCurrent: boolean): void {\n\tconst current = readJobsState(path);\n\twriteJobsState(path, {\n\t\tjobs: mergeCurrent ? mergeFreshJobs(current.jobs, jobs) : [...jobs],\n\t\tdispatches: current.dispatches,\n\t});\n}\n\nfunction writeJobsState(path: string, state: CronJobsState): void {\n\tconst directory = dirname(path);\n\tmkdirSync(directory, { recursive: true, mode: 0o700 });\n\tconst tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;\n\tconst descriptor = openSync(tempPath, \"w\", 0o600);\n\ttry {\n\t\twriteFileSync(descriptor, `${JSON.stringify(state, null, 2)}\\n`, \"utf-8\");\n\t\tfsyncSync(descriptor);\n\t} finally {\n\t\tcloseSync(descriptor);\n\t}\n\trenameSync(tempPath, path);\n\ttry {\n\t\tconst directoryDescriptor = openSync(directory, \"r\");\n\t\ttry {\n\t\t\tfsyncSync(directoryDescriptor);\n\t\t} finally {\n\t\t\tcloseSync(directoryDescriptor);\n\t\t}\n\t} catch {\n\t\t// Directory fsync is unavailable on some platforms; the atomic rename still protects readers.\n\t}\n}\n\nfunction claimDueInState(state: CronJobsState, dueAt: Date, claimedAt: Date): AgentCronDispatch[] {\n\tconst dispatches: AgentCronDispatch[] = [];\n\tconst claimedJobIds = new Set(state.dispatches.map((dispatch) => dispatch.jobId));\n\tstate.jobs = state.jobs.map((job) => {\n\t\tif (!isDueJob(job, dueAt)) {\n\t\t\treturn job;\n\t\t}\n\t\tconst scheduledFor = job.nextRunAt!;\n\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, claimedAt)?.toISOString();\n\t\tconst advanced = nextRunAt\n\t\t\t? { ...job, nextRunAt, updatedAt: claimedAt.toISOString() }\n\t\t\t: withoutNextRunAt({ ...job, updatedAt: claimedAt.toISOString() });\n\t\tif (claimedJobIds.has(job.id)) {\n\t\t\treturn { ...advanced, lastSkippedAt: claimedAt.toISOString() };\n\t\t}\n\t\tconst dispatch: AgentCronDispatchRecord = {\n\t\t\tid: randomUUID(),\n\t\t\tjobId: job.id,\n\t\t\tclaimedAt: claimedAt.toISOString(),\n\t\t\tscheduledFor,\n\t\t};\n\t\tstate.dispatches.push(dispatch);\n\t\tdispatches.push({ id: dispatch.id, job: advanced });\n\t\treturn advanced;\n\t});\n\treturn dispatches;\n}\n\nfunction recoverInterruptedInState(\n\tstate: CronJobsState,\n\tnow: Date,\n\trecovered: AgentCronJob[],\n\tdispatchIds?: ReadonlySet<string>,\n): void {\n\tconst interrupted = dispatchIds\n\t\t? state.dispatches.filter((dispatch) => dispatchIds.has(dispatch.id))\n\t\t: state.dispatches;\n\tif (interrupted.length === 0) {\n\t\treturn;\n\t}\n\tconst interruptedIds = new Set(interrupted.map((dispatch) => dispatch.jobId));\n\tstate.dispatches = dispatchIds ? state.dispatches.filter((dispatch) => !dispatchIds.has(dispatch.id)) : [];\n\tstate.jobs = state.jobs.map((job) => {\n\t\tif (!interruptedIds.has(job.id) || job.status !== \"active\") {\n\t\t\treturn job;\n\t\t}\n\t\tconst next = {\n\t\t\t...job,\n\t\t\tstatus: job.schedule.kind === \"once\" ? (\"completed\" as const) : job.status,\n\t\t\tlastError: \"Interrupted before scheduled operation completion\",\n\t\t\tupdatedAt: now.toISOString(),\n\t\t};\n\t\trecovered.push(next);\n\t\treturn next;\n\t});\n}\n\nfunction mergeFreshJobs(currentJobs: readonly AgentCronJob[], nextJobs: readonly AgentCronJob[]): AgentCronJob[] {\n\tconst merged = new Map<string, AgentCronJob>();\n\tfor (const job of currentJobs) {\n\t\tmerged.set(job.id, job);\n\t}\n\tfor (const job of nextJobs) {\n\t\tconst current = merged.get(job.id);\n\t\tif (!current || isAtLeastAsFresh(job, current)) {\n\t\t\tmerged.set(job.id, job);\n\t\t}\n\t}\n\treturn [...merged.values()];\n}\n\nfunction isAtLeastAsFresh(candidate: AgentCronJob, current: AgentCronJob): boolean {\n\tconst candidateTime = Date.parse(candidate.updatedAt);\n\tconst currentTime = Date.parse(current.updatedAt);\n\tif (!Number.isFinite(currentTime)) {\n\t\treturn true;\n\t}\n\tif (!Number.isFinite(candidateTime)) {\n\t\treturn false;\n\t}\n\treturn candidateTime >= currentTime;\n}\n\nfunction compareOptionalIso(left: string | undefined, right: string | undefined): number {\n\tif (left === right) {\n\t\treturn 0;\n\t}\n\tif (left === undefined) {\n\t\treturn 1;\n\t}\n\tif (right === undefined) {\n\t\treturn -1;\n\t}\n\treturn Date.parse(left) - Date.parse(right);\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction normalizeOptionalLabel(label: string | undefined): string | undefined {\n\tconst trimmed = label?.trim();\n\treturn trimmed ? trimmed : undefined;\n}\n\nfunction withoutNextRunAt(job: AgentCronJob): AgentCronJob {\n\tconst { nextRunAt: _nextRunAt, ...rest } = job;\n\treturn rest;\n}\n\nexport function isAgentCronJob(value: unknown): value is AgentCronJob {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn false;\n\t}\n\tconst candidate = value as Partial<AgentCronJob>;\n\treturn (\n\t\ttypeof candidate.id === \"string\" &&\n\t\t(candidate.status === \"active\" ||\n\t\t\tcandidate.status === \"paused\" ||\n\t\t\tcandidate.status === \"completed\" ||\n\t\t\tcandidate.status === \"cancelled\") &&\n\t\t(candidate.source === undefined ||\n\t\t\tcandidate.source === \"cron\" ||\n\t\t\tcandidate.source === \"heartbeat\" ||\n\t\t\tcandidate.source === \"rlm_heartbeat\") &&\n\t\t(candidate.runtimeKind === undefined ||\n\t\t\tcandidate.runtimeKind === \"top-level\" ||\n\t\t\tcandidate.runtimeKind === \"subagent\") &&\n\t\t(candidate.deliveryMode === undefined ||\n\t\t\tcandidate.deliveryMode === \"steer\" ||\n\t\t\tcandidate.deliveryMode === \"follow_up\") &&\n\t\ttypeof candidate.activeSessionId === \"string\" &&\n\t\ttypeof candidate.sessionId === \"string\" &&\n\t\ttypeof candidate.sessionFile === \"string\" &&\n\t\ttypeof candidate.cwd === \"string\" &&\n\t\t(candidate.label === undefined || typeof candidate.label === \"string\") &&\n\t\ttypeof candidate.prompt === \"string\" &&\n\t\ttypeof candidate.schedule === \"object\" &&\n\t\tcandidate.schedule !== null &&\n\t\t(candidate.schedule.kind === \"once\" ||\n\t\t\tcandidate.schedule.kind === \"cron\" ||\n\t\t\t(candidate.schedule.kind === \"interval\" &&\n\t\t\t\ttypeof candidate.schedule.intervalMs === \"number\" &&\n\t\t\t\tcandidate.schedule.intervalMs > 0)) &&\n\t\ttypeof candidate.schedule.expression === \"string\" &&\n\t\ttypeof candidate.createdAt === \"string\" &&\n\t\ttypeof candidate.updatedAt === \"string\" &&\n\t\ttypeof candidate.runCount === \"number\"\n\t);\n}\n\nfunction isAgentCronDispatchRecord(value: unknown): value is AgentCronDispatchRecord {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn false;\n\t}\n\tconst candidate = value as Partial<AgentCronDispatchRecord>;\n\treturn (\n\t\ttypeof candidate.id === \"string\" &&\n\t\ttypeof candidate.jobId === \"string\" &&\n\t\ttypeof candidate.claimedAt === \"string\" &&\n\t\ttypeof candidate.scheduledFor === \"string\"\n\t);\n}\n"]}
1
+ {"version":3,"file":"cron-jobs.d.ts","sourceRoot":"","sources":["../../src/core/cron-jobs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { getLogger } from \"@ponythewhite/base-context-ai\";\nimport { lockSync } from \"proper-lockfile\";\nimport { getSessionArtifactPathForFile } from \"./session-manager.js\";\n\nexport type AgentCronJobStatus = \"active\" | \"paused\" | \"completed\" | \"cancelled\";\nexport type AgentCronScheduleKind = \"once\" | \"cron\" | \"interval\";\nexport type AgentCronJobSource = \"cron\" | \"heartbeat\" | \"rlm_heartbeat\";\nexport type AgentCronJobRuntimeKind = \"top-level\" | \"subagent\";\nexport type AgentHeartbeatUpdateAction = \"pause\" | \"resume\" | \"clear\";\nexport type AgentHeartbeatManagementAction = \"pause\" | \"resume\" | \"stop\";\nexport type AgentRlmHeartbeatStatusUpdate = \"pause\" | \"resume\";\n/**\n * How a scheduled heartbeat prompt is delivered when the target session is busy:\n * \"steer\" interrupts the current turn, \"follow_up\" waits for it to finish.\n */\nexport type AgentHeartbeatDeliveryMode = \"steer\" | \"follow_up\";\n\nexport interface AgentCronSchedule {\n\tkind: AgentCronScheduleKind;\n\texpression: string;\n\tintervalMs?: number;\n}\n\nexport interface AgentCronJob {\n\tid: string;\n\tstatus: AgentCronJobStatus;\n\tsource?: AgentCronJobSource;\n\truntimeKind?: AgentCronJobRuntimeKind;\n\t/** Delivery mode for heartbeat/rlm_heartbeat jobs when the session is busy. Defaults to \"steer\". */\n\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\tactiveSessionId: string;\n\tsessionId: string;\n\tsessionFile: string;\n\tcwd: string;\n\tlabel?: string;\n\tprompt: string;\n\tschedule: AgentCronSchedule;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tnextRunAt?: string;\n\tlastRunAt?: string;\n\tlastSkippedAt?: string;\n\tlastError?: string;\n\trunCount: number;\n}\n\nexport interface CreateAgentCronJobInput {\n\tactiveSessionId: string;\n\tsessionId: string;\n\tsessionFile: string;\n\tcwd: string;\n\tlabel?: string;\n\tprompt: string;\n\tscheduleText: string;\n\tsource?: AgentCronJobSource;\n\truntimeKind?: AgentCronJobRuntimeKind;\n\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\tnow?: Date;\n}\n\nexport type AgentCronJobRunResult = \"ran\" | \"skipped\";\n\nexport interface AgentCronDispatch {\n\tid: string;\n\tjob: AgentCronJob;\n}\n\nexport interface AgentCronSchedulerHooks {\n\trunJob: (job: AgentCronJob) => Promise<AgentCronJobRunResult | undefined>;\n\tbeginDispatch?: (dispatch: AgentCronDispatch) => (() => void) | undefined;\n\tnow?: () => Date;\n\tonError?: (job: AgentCronJob, error: unknown) => void;\n}\n\nexport interface HeartbeatCronSessionActivity {\n\tisStreaming: boolean;\n\tisCompacting?: boolean;\n\tisRetrying?: boolean;\n\tisBashRunning: boolean;\n\thasPendingSessionWork: boolean;\n\tunfinishedActionCount: number;\n}\n\ninterface CronJobsFile {\n\tjobs?: unknown;\n\tdispatches?: unknown;\n}\n\ninterface AgentCronDispatchRecord {\n\tid: string;\n\tjobId: string;\n\tclaimedAt: string;\n\tscheduledFor: string;\n}\n\ninterface CronJobsState {\n\tjobs: AgentCronJob[];\n\tdispatches: AgentCronDispatchRecord[];\n}\n\nexport const SESSION_SCHEDULED_JOBS_FILENAME = \"scheduled-jobs.json\";\n\nconst log = getLogger(\"coding-agent.cron\");\nconst MAX_TIMEOUT_MS = 2_147_483_647;\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60_000;\nexport const DEFAULT_HEARTBEAT_SCHEDULE = \"every 5m\";\nexport const DEFAULT_HEARTBEAT_DELIVERY_MODE: AgentHeartbeatDeliveryMode = \"steer\";\n\nexport type ParsedHeartbeatCommand =\n\t| { type: \"status\" }\n\t| { type: \"pause\" }\n\t| { type: \"resume\" }\n\t| { type: \"clear\" }\n\t| { type: \"set\"; schedule: string; instruction: string; deliveryMode?: AgentHeartbeatDeliveryMode };\n\nexport interface AgentRlmHeartbeatController {\n\tlistRlmHeartbeats(options?: { includeInactive?: boolean }): AgentCronJob[];\n\tcreateRlmHeartbeat(input: {\n\t\tinstruction: string;\n\t\tinterval?: string;\n\t\tlabel?: string;\n\t\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\t}): AgentCronJob;\n\tupdateRlmHeartbeat(input: {\n\t\tid: string;\n\t\tinstruction?: string;\n\t\tinterval?: string;\n\t\tlabel?: string;\n\t\tstatus?: AgentRlmHeartbeatStatusUpdate;\n\t\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\t}): AgentCronJob | undefined;\n\tdeleteRlmHeartbeat(id: string): AgentCronJob | undefined;\n}\n\nfunction heartbeatCatalogSignature(jobs: readonly AgentCronJob[]): string {\n\treturn JSON.stringify(\n\t\tjobs\n\t\t\t.filter((job) => isHeartbeatCronJob(job) && (job.status === \"active\" || job.status === \"paused\"))\n\t\t\t.sort((left, right) => left.id.localeCompare(right.id))\n\t\t\t.map((job) => ({\n\t\t\t\tid: job.id,\n\t\t\t\tstatus: job.status,\n\t\t\t\tsource: job.source,\n\t\t\t\truntimeKind: job.runtimeKind,\n\t\t\t\tdeliveryMode: job.deliveryMode,\n\t\t\t\tactiveSessionId: job.activeSessionId,\n\t\t\t\tsessionId: job.sessionId,\n\t\t\t\tsessionFile: job.sessionFile,\n\t\t\t\tcwd: job.cwd,\n\t\t\t\tlabel: job.label,\n\t\t\t\tprompt: job.prompt,\n\t\t\t\tschedule: job.schedule,\n\t\t\t\tcreatedAt: job.createdAt,\n\t\t\t})),\n\t);\n}\n\nexport class AgentCronJobStore {\n\tprivate readonly sessionArtifactFiles = new Map<string, string>();\n\tprivate readonly heartbeatChangeListeners = new Set<() => void>();\n\n\tconstructor(\n\t\tprivate readonly filePath?: string,\n\t\tprivate readonly sessionArtifactMode = false,\n\t) {\n\t\tif (!filePath && !sessionArtifactMode) {\n\t\t\tthrow new Error(\"Cron job store requires a file path\");\n\t\t}\n\t}\n\n\tstatic forSessionArtifacts(): AgentCronJobStore {\n\t\treturn new AgentCronJobStore(undefined, true);\n\t}\n\n\tonHeartbeatChange(listener: () => void): () => void {\n\t\tthis.heartbeatChangeListeners.add(listener);\n\t\treturn () => this.heartbeatChangeListeners.delete(listener);\n\t}\n\n\tregisterSessionArtifact(sessionId: string, artifactDir: string): boolean {\n\t\tif (!this.sessionArtifactMode) {\n\t\t\treturn false;\n\t\t}\n\t\tconst path = join(artifactDir, SESSION_SCHEDULED_JOBS_FILENAME);\n\t\tif (this.sessionArtifactFiles.get(sessionId) === path) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.sessionArtifactFiles.set(sessionId, path);\n\t\treturn true;\n\t}\n\n\trecoverSessionArtifact(sessionId: string, now = new Date()): AgentCronJob[] {\n\t\tconst path = this.sessionArtifactFiles.get(sessionId);\n\t\tif (!path) {\n\t\t\treturn [];\n\t\t}\n\t\treturn withCronJobsStateLocks([path], () => {\n\t\t\tconst state = readJobsState(path);\n\t\t\tconst recovered: AgentCronJob[] = [];\n\t\t\tif (state.dispatches.length > 0) {\n\t\t\t\trecoverInterruptedInState(state, now, recovered);\n\t\t\t\twriteJobsState(path, state);\n\t\t\t}\n\t\t\treturn recovered;\n\t\t});\n\t}\n\n\tlist(): AgentCronJob[] {\n\t\treturn this.readJobs().sort((a, b) => compareOptionalIso(a.nextRunAt, b.nextRunAt));\n\t}\n\n\t/** Import supported declarations directly as paused data; never import dispatch ownership. */\n\timportPaused(\n\t\tjobs: readonly AgentCronJob[],\n\t\ttarget: { sessionId: string; sessionFile: string; cwd: string },\n\t): AgentCronJob[] {\n\t\tconst now = new Date().toISOString();\n\t\tconst imported = jobs.map((job): AgentCronJob => {\n\t\t\tif (\n\t\t\t\t!isAgentCronJob(job) ||\n\t\t\t\t(job.status !== \"active\" && job.status !== \"paused\") ||\n\t\t\t\t(job.source === \"rlm_heartbeat\" && job.schedule.kind === \"once\") ||\n\t\t\t\tjob.runtimeKind === \"subagent\"\n\t\t\t)\n\t\t\t\tthrow new Error(\"Unsupported paused schedule owner\");\n\t\t\treturn {\n\t\t\t\tid: randomUUID(),\n\t\t\t\tstatus: \"paused\",\n\t\t\t\tsource: job.source ?? \"cron\",\n\t\t\t\truntimeKind: \"top-level\",\n\t\t\t\tactiveSessionId: target.sessionId,\n\t\t\t\tsessionId: target.sessionId,\n\t\t\t\tsessionFile: target.sessionFile,\n\t\t\t\tcwd: target.cwd,\n\t\t\t\tprompt: job.prompt,\n\t\t\t\tschedule: {\n\t\t\t\t\tkind: job.schedule.kind,\n\t\t\t\t\texpression: job.schedule.expression,\n\t\t\t\t\t...(job.schedule.kind === \"interval\" ? { intervalMs: job.schedule.intervalMs } : {}),\n\t\t\t\t},\n\t\t\t\t...(job.label === undefined ? {} : { label: job.label }),\n\t\t\t\t...(job.deliveryMode === undefined ? {} : { deliveryMode: job.deliveryMode }),\n\t\t\t\tcreatedAt: now,\n\t\t\t\tupdatedAt: now,\n\t\t\t\trunCount: 0,\n\t\t\t};\n\t\t});\n\t\tif (imported.length > 0) this.writeJobs([...this.readJobs(), ...imported]);\n\t\treturn imported;\n\t}\n\n\tcreate(input: CreateAgentCronJobInput): AgentCronJob {\n\t\tconst now = input.now ?? new Date();\n\t\tconst prompt = input.prompt.trim();\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"Cron job prompt cannot be empty\");\n\t\t}\n\t\tconst parsed = parseAgentCronSchedule(input.scheduleText, now);\n\t\tconst nowIso = now.toISOString();\n\t\tconst job: AgentCronJob = {\n\t\t\tid: randomUUID(),\n\t\t\tstatus: \"active\",\n\t\t\tsource: input.source ?? \"cron\",\n\t\t\truntimeKind: input.runtimeKind,\n\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\tsessionId: input.sessionId,\n\t\t\tsessionFile: input.sessionFile,\n\t\t\tcwd: input.cwd,\n\t\t\tlabel: normalizeOptionalLabel(input.label),\n\t\t\tprompt,\n\t\t\tschedule: parsed.schedule,\n\t\t\tcreatedAt: nowIso,\n\t\t\tupdatedAt: nowIso,\n\t\t\tnextRunAt: parsed.nextRunAt.toISOString(),\n\t\t\trunCount: 0,\n\t\t};\n\t\tthis.writeJobs([...this.readJobs(), job]);\n\t\treturn job;\n\t}\n\n\t/**\n\t * Active session ids are daemon-local. When a persisted session is restored,\n\t * bind jobs stored for its stable session file to the new live session id.\n\t * When a live session switches to another persisted file, move jobs stored for\n\t * its stable active session id to the new file so future restores target the\n\t * current session instead of the previous one.\n\t */\n\trebindSessionJobs(input: {\n\t\tactiveSessionId: string;\n\t\tsessionId: string;\n\t\tsessionFile: string;\n\t\tcwd: string;\n\t}): AgentCronJob[] {\n\t\tconst targetSessionFile = resolve(input.sessionFile);\n\t\tconst reboundJobs: AgentCronJob[] = [];\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.activeSessionId !== input.activeSessionId && resolve(job.sessionFile) !== targetSessionFile) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tjob.activeSessionId === input.activeSessionId &&\n\t\t\t\tjob.sessionId === input.sessionId &&\n\t\t\t\tresolve(job.sessionFile) === targetSessionFile &&\n\t\t\t\tjob.cwd === input.cwd\n\t\t\t) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst rebound = {\n\t\t\t\t...job,\n\t\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tsessionFile: input.sessionFile,\n\t\t\t\tcwd: input.cwd,\n\t\t\t};\n\t\t\treboundJobs.push(rebound);\n\t\t\treturn rebound;\n\t\t});\n\t\tif (reboundJobs.length > 0) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn reboundJobs;\n\t}\n\n\tgetHeartbeat(activeSessionId: string): AgentCronJob | undefined {\n\t\treturn this.readJobs()\n\t\t\t.filter((job) => {\n\t\t\t\treturn (\n\t\t\t\t\tjob.activeSessionId === activeSessionId &&\n\t\t\t\t\tjob.source === \"heartbeat\" &&\n\t\t\t\t\t(job.status === \"active\" || job.status === \"paused\")\n\t\t\t\t);\n\t\t\t})\n\t\t\t.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];\n\t}\n\n\tgetLatestHeartbeat(activeSessionId: string): AgentCronJob | undefined {\n\t\treturn this.readJobs()\n\t\t\t.filter((job) => job.activeSessionId === activeSessionId && job.source === \"heartbeat\")\n\t\t\t.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];\n\t}\n\n\tcreateHeartbeat(input: CreateAgentCronJobInput): AgentCronJob {\n\t\tconst now = input.now ?? new Date();\n\t\tconst parsed = parseAgentCronSchedule(input.scheduleText, now);\n\t\tif (parsed.schedule.kind === \"once\") {\n\t\t\tthrow new Error(\"Heartbeat schedule must be recurring\");\n\t\t}\n\t\tconst existing = this.readJobs().map((job) => {\n\t\t\tif (\n\t\t\t\tjob.activeSessionId === input.activeSessionId &&\n\t\t\t\tjob.source === \"heartbeat\" &&\n\t\t\t\t(job.status === \"active\" || job.status === \"paused\")\n\t\t\t) {\n\t\t\t\treturn { ...job, status: \"cancelled\" as const, nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\t}\n\t\t\treturn job;\n\t\t});\n\t\tconst prompt = input.prompt.trim();\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"Heartbeat instruction cannot be empty\");\n\t\t}\n\t\tconst nowIso = now.toISOString();\n\t\tconst job: AgentCronJob = {\n\t\t\tid: randomUUID(),\n\t\t\tstatus: \"active\",\n\t\t\tsource: \"heartbeat\",\n\t\t\truntimeKind: input.runtimeKind,\n\t\t\tdeliveryMode: input.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE,\n\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\tsessionId: input.sessionId,\n\t\t\tsessionFile: input.sessionFile,\n\t\t\tcwd: input.cwd,\n\t\t\tlabel: normalizeOptionalLabel(input.label),\n\t\t\tprompt,\n\t\t\tschedule: parsed.schedule,\n\t\t\tcreatedAt: nowIso,\n\t\t\tupdatedAt: nowIso,\n\t\t\tnextRunAt: parsed.nextRunAt.toISOString(),\n\t\t\trunCount: 0,\n\t\t};\n\t\tthis.writeJobs([...existing, job]);\n\t\treturn job;\n\t}\n\n\tlistRlmHeartbeats(activeSessionId: string, options: { includeInactive?: boolean } = {}): AgentCronJob[] {\n\t\treturn this.readJobs()\n\t\t\t.filter((job) => {\n\t\t\t\tif (job.activeSessionId !== activeSessionId || job.source !== \"rlm_heartbeat\") {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (options.includeInactive) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn job.status === \"active\" || job.status === \"paused\";\n\t\t\t})\n\t\t\t.sort((a, b) => compareOptionalIso(a.nextRunAt, b.nextRunAt));\n\t}\n\n\tcreateRlmHeartbeat(input: CreateAgentCronJobInput): AgentCronJob {\n\t\tconst now = input.now ?? new Date();\n\t\tconst parsed = parseAgentCronSchedule(input.scheduleText, now);\n\t\tif (parsed.schedule.kind === \"once\") {\n\t\t\tthrow new Error(\"RLM heartbeat schedule must be recurring\");\n\t\t}\n\t\tconst prompt = input.prompt.trim();\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"RLM heartbeat instruction cannot be empty\");\n\t\t}\n\t\tconst nowIso = now.toISOString();\n\t\tconst job: AgentCronJob = {\n\t\t\tid: randomUUID(),\n\t\t\tstatus: \"active\",\n\t\t\tsource: \"rlm_heartbeat\",\n\t\t\truntimeKind: input.runtimeKind,\n\t\t\tdeliveryMode: input.deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE,\n\t\t\tactiveSessionId: input.activeSessionId,\n\t\t\tsessionId: input.sessionId,\n\t\t\tsessionFile: input.sessionFile,\n\t\t\tcwd: input.cwd,\n\t\t\tlabel: normalizeOptionalLabel(input.label),\n\t\t\tprompt,\n\t\t\tschedule: parsed.schedule,\n\t\t\tcreatedAt: nowIso,\n\t\t\tupdatedAt: nowIso,\n\t\t\tnextRunAt: parsed.nextRunAt.toISOString(),\n\t\t\trunCount: 0,\n\t\t};\n\t\tthis.writeJobs([...this.readJobs(), job]);\n\t\treturn job;\n\t}\n\n\tupdateRlmHeartbeat(\n\t\tactiveSessionId: string,\n\t\tid: string,\n\t\tupdate: {\n\t\t\tlabel?: string;\n\t\t\tprompt?: string;\n\t\t\tscheduleText?: string;\n\t\t\tstatus?: AgentRlmHeartbeatStatusUpdate;\n\t\t\tdeliveryMode?: AgentHeartbeatDeliveryMode;\n\t\t\tnow?: Date;\n\t\t},\n\t): AgentCronJob | undefined {\n\t\tconst now = update.now ?? new Date();\n\t\tlet updated: AgentCronJob | undefined;\n\t\tlet matchedRlmHeartbeat = false;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.activeSessionId !== activeSessionId || job.source !== \"rlm_heartbeat\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tmatchedRlmHeartbeat = true;\n\t\t\tif (job.status === \"cancelled\" || job.status === \"completed\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tlet nextJob: AgentCronJob = { ...job };\n\t\t\tif (update.label !== undefined) {\n\t\t\t\tnextJob = { ...nextJob, label: normalizeOptionalLabel(update.label) };\n\t\t\t}\n\t\t\tif (update.deliveryMode !== undefined) {\n\t\t\t\tnextJob = { ...nextJob, deliveryMode: update.deliveryMode };\n\t\t\t}\n\t\t\tif (update.prompt !== undefined) {\n\t\t\t\tconst prompt = update.prompt.trim();\n\t\t\t\tif (!prompt) {\n\t\t\t\t\tthrow new Error(\"RLM heartbeat instruction cannot be empty\");\n\t\t\t\t}\n\t\t\t\tnextJob = { ...nextJob, prompt };\n\t\t\t}\n\t\t\tif (update.scheduleText !== undefined) {\n\t\t\t\tconst parsed = parseAgentCronSchedule(update.scheduleText, now);\n\t\t\t\tif (parsed.schedule.kind === \"once\") {\n\t\t\t\t\tthrow new Error(\"RLM heartbeat schedule must be recurring\");\n\t\t\t\t}\n\t\t\t\tnextJob =\n\t\t\t\t\tnextJob.status === \"paused\"\n\t\t\t\t\t\t? withoutNextRunAt({ ...nextJob, schedule: parsed.schedule })\n\t\t\t\t\t\t: { ...nextJob, schedule: parsed.schedule, nextRunAt: parsed.nextRunAt.toISOString() };\n\t\t\t}\n\t\t\tif (update.status === \"pause\") {\n\t\t\t\tnextJob = withoutNextRunAt({ ...nextJob, status: \"paused\" });\n\t\t\t} else if (update.status === \"resume\") {\n\t\t\t\tconst nextRunAt = nextRunAtForSchedule(nextJob.schedule, now);\n\t\t\t\tif (!nextRunAt) {\n\t\t\t\t\tthrow new Error(\"RLM heartbeat schedule must be recurring\");\n\t\t\t\t}\n\t\t\t\tnextJob = { ...nextJob, status: \"active\", nextRunAt: nextRunAt.toISOString() };\n\t\t\t}\n\t\t\tupdated = { ...nextJob, updatedAt: now.toISOString() };\n\t\t\treturn updated;\n\t\t});\n\t\tif (matchedRlmHeartbeat && updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\tdeleteRlmHeartbeat(activeSessionId: string, id: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet deleted: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.activeSessionId !== activeSessionId || job.source !== \"rlm_heartbeat\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tdeleted = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\treturn deleted;\n\t\t});\n\t\tif (deleted) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn deleted;\n\t}\n\n\tcancelRlmHeartbeatsForSession(activeSessionId: string, now = new Date()): AgentCronJob[] {\n\t\tconst cancelled: AgentCronJob[] = [];\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (\n\t\t\t\tjob.activeSessionId !== activeSessionId ||\n\t\t\t\tjob.source !== \"rlm_heartbeat\" ||\n\t\t\t\t(job.status !== \"active\" && job.status !== \"paused\")\n\t\t\t) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst cancelledJob = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\tcancelled.push(cancelledJob);\n\t\t\treturn cancelledJob;\n\t\t});\n\t\tif (cancelled.length > 0) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn cancelled;\n\t}\n\n\tcancelJobsForSession(\n\t\tinput: { activeSessionId?: string; sessionId?: string; sessionFile?: string },\n\t\tnow = new Date(),\n\t): AgentCronJob[] {\n\t\tconst targetSessionFile = input.sessionFile ? resolve(input.sessionFile) : undefined;\n\t\tconst cancelled: AgentCronJob[] = [];\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tconst matches =\n\t\t\t\t(input.activeSessionId !== undefined && job.activeSessionId === input.activeSessionId) ||\n\t\t\t\t(input.sessionId !== undefined && job.sessionId === input.sessionId) ||\n\t\t\t\t(targetSessionFile !== undefined && resolve(job.sessionFile) === targetSessionFile);\n\t\t\tif (!matches || (job.status !== \"active\" && job.status !== \"paused\")) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst cancelledJob = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\tcancelled.push(cancelledJob);\n\t\t\treturn cancelledJob;\n\t\t});\n\t\tif (cancelled.length > 0) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn cancelled;\n\t}\n\n\tpauseHeartbeat(activeSessionId: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet paused: AgentCronJob | undefined;\n\t\tconst current = this.getHeartbeat(activeSessionId);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== current.id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tpaused = { ...job, status: \"paused\", nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\treturn paused;\n\t\t});\n\t\tthis.writeJobs(jobs);\n\t\treturn paused;\n\t}\n\n\tresumeHeartbeat(activeSessionId: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet resumed: AgentCronJob | undefined;\n\t\tconst current = this.getHeartbeat(activeSessionId);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst nextRunAt = nextRunAtForSchedule(current.schedule, now);\n\t\tif (!nextRunAt) {\n\t\t\tthrow new Error(\"Heartbeat schedule must be recurring\");\n\t\t}\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== current.id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tresumed = { ...job, status: \"active\", nextRunAt: nextRunAt.toISOString(), updatedAt: now.toISOString() };\n\t\t\treturn resumed;\n\t\t});\n\t\tthis.writeJobs(jobs);\n\t\treturn resumed;\n\t}\n\n\tclearHeartbeat(activeSessionId: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet cleared: AgentCronJob | undefined;\n\t\tconst current = this.getHeartbeat(activeSessionId);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== current.id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tcleared = { ...job, status: \"cancelled\", nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\treturn cleared;\n\t\t});\n\t\tthis.writeJobs(jobs);\n\t\treturn cleared;\n\t}\n\n\tmanageHeartbeat(\n\t\tactiveSessionId: string,\n\t\tid: string,\n\t\taction: AgentHeartbeatManagementAction,\n\t\tnow = new Date(),\n\t): AgentCronJob | undefined {\n\t\tlet updated: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.activeSessionId !== activeSessionId || !isHeartbeatCronJob(job)) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (job.status === \"cancelled\" || job.status === \"completed\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (action === \"pause\") {\n\t\t\t\tupdated = withoutNextRunAt({ ...job, status: \"paused\", updatedAt: now.toISOString() });\n\t\t\t\treturn updated;\n\t\t\t}\n\t\t\tif (action === \"stop\") {\n\t\t\t\tupdated = withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t\t\t\treturn updated;\n\t\t\t}\n\t\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, now);\n\t\t\tif (!nextRunAt) {\n\t\t\t\tthrow new Error(\"Heartbeat schedule must be recurring\");\n\t\t\t}\n\t\t\tupdated = {\n\t\t\t\t...job,\n\t\t\t\tstatus: \"active\",\n\t\t\t\tnextRunAt: nextRunAt.toISOString(),\n\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t};\n\t\t\treturn updated;\n\t\t});\n\t\tif (updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\tresumeCronJob(\n\t\tid: string,\n\t\towner: { activeSessionId: string; sessionId: string; sessionFile: string },\n\t\tnow = new Date(),\n\t): AgentCronJob | undefined {\n\t\tconst jobs = this.readJobs();\n\t\tconst current = jobs.find((job) => job.id === id);\n\t\tif (!current) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (\n\t\t\tcurrent.activeSessionId !== owner.activeSessionId ||\n\t\t\tcurrent.sessionId !== owner.sessionId ||\n\t\t\tresolve(current.sessionFile) !== resolve(owner.sessionFile)\n\t\t) {\n\t\t\tthrow new Error(\"Cron job does not belong to the bound session\");\n\t\t}\n\t\tif (current.status !== \"paused\" || current.source !== \"cron\" || current.runtimeKind !== \"top-level\") {\n\t\t\tthrow new Error(\"Only paused top-level cron jobs can be resumed\");\n\t\t}\n\t\tconst nextRunAt = nextRunAtForSchedule(current.schedule, now);\n\t\tif (!nextRunAt) {\n\t\t\tthrow new Error(\"Cron resume requires a recurring schedule; reschedule one-shot jobs explicitly\");\n\t\t}\n\t\tconst resumed: AgentCronJob = {\n\t\t\t...current,\n\t\t\tstatus: \"active\",\n\t\t\tnextRunAt: nextRunAt.toISOString(),\n\t\t\tupdatedAt: now.toISOString(),\n\t\t};\n\t\tthis.writeJobs(jobs.map((job) => (job.id === id ? resumed : job)));\n\t\treturn resumed;\n\t}\n\n\tcancel(id: string, now = new Date()): AgentCronJob | undefined {\n\t\tlet cancelled: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id || job.status === \"cancelled\") {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tcancelled = { ...job, status: \"cancelled\", nextRunAt: undefined, updatedAt: now.toISOString() };\n\t\t\treturn cancelled;\n\t\t});\n\t\tif (cancelled) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn cancelled;\n\t}\n\n\trecordRunResult(id: string, result: { now?: Date; error?: unknown }): AgentCronJob | undefined {\n\t\tconst now = result.now ?? new Date();\n\t\tlet updated: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (job.status !== \"active\") {\n\t\t\t\tupdated = job;\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst lastError = result.error === undefined ? undefined : errorMessage(result.error);\n\t\t\tconst nextRunAt =\n\t\t\t\tjob.schedule.kind === \"cron\"\n\t\t\t\t\t? nextRunAtForSchedule(job.schedule, new Date(now.getTime() + 1))\n\t\t\t\t\t: job.schedule.kind === \"interval\"\n\t\t\t\t\t\t? nextRunAtForSchedule(job.schedule, now)\n\t\t\t\t\t\t: undefined;\n\t\t\tupdated = {\n\t\t\t\t...job,\n\t\t\t\tstatus: job.schedule.kind === \"once\" ? \"completed\" : \"active\",\n\t\t\t\tnextRunAt: nextRunAt?.toISOString(),\n\t\t\t\tlastRunAt: now.toISOString(),\n\t\t\t\tlastError,\n\t\t\t\trunCount: job.runCount + 1,\n\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t};\n\t\t\treturn updated;\n\t\t});\n\t\tif (updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\trecordSkipResult(id: string, result: { now?: Date }): AgentCronJob | undefined {\n\t\tconst now = result.now ?? new Date();\n\t\tlet updated: AgentCronJob | undefined;\n\t\tconst jobs = this.readJobs().map((job) => {\n\t\t\tif (job.id !== id) {\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tif (job.status !== \"active\") {\n\t\t\t\tupdated = job;\n\t\t\t\treturn job;\n\t\t\t}\n\t\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, now);\n\t\t\tupdated = {\n\t\t\t\t...job,\n\t\t\t\tnextRunAt: nextRunAt?.toISOString(),\n\t\t\t\tlastSkippedAt: now.toISOString(),\n\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t};\n\t\t\treturn updated;\n\t\t});\n\t\tif (updated) {\n\t\t\tthis.writeJobs(jobs);\n\t\t}\n\t\treturn updated;\n\t}\n\n\tdue(now = new Date()): AgentCronJob[] {\n\t\treturn this.readJobs().filter((job) => isDueJob(job, now));\n\t}\n\n\tclaimDue(dueAt = new Date(), claimedAt = dueAt): AgentCronDispatch[] {\n\t\treturn this.mutateStates((state) => claimDueInState(state, dueAt, claimedAt));\n\t}\n\n\tgetClaimedJob(id: string): AgentCronJob | undefined {\n\t\tfor (const state of this.readStates()) {\n\t\t\tif (!state.dispatches.some((dispatch) => dispatch.jobId === id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn state.jobs.find((job) => job.id === id && job.status === \"active\");\n\t\t}\n\t\treturn undefined;\n\t}\n\n\trecordDispatchResult(\n\t\tdispatchId: string,\n\t\tresult: { now?: Date; outcome: AgentCronJobRunResult; error?: unknown },\n\t): AgentCronJob | undefined {\n\t\tlet updated: AgentCronJob | undefined;\n\t\tthis.mutateStates((state) => {\n\t\t\tconst dispatch = state.dispatches.find((candidate) => candidate.id === dispatchId);\n\t\t\tif (!dispatch) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst now = result.now ?? new Date();\n\t\t\tstate.dispatches = state.dispatches.filter((candidate) => candidate.id !== dispatchId);\n\t\t\tstate.jobs = state.jobs.map((job) => {\n\t\t\t\tif (job.id !== dispatch.jobId || job.status !== \"active\") {\n\t\t\t\t\treturn job;\n\t\t\t\t}\n\t\t\t\tif (result.outcome === \"skipped\" && result.error === undefined) {\n\t\t\t\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, now);\n\t\t\t\t\tupdated = {\n\t\t\t\t\t\t...job,\n\t\t\t\t\t\tstatus: job.schedule.kind === \"once\" ? \"completed\" : job.status,\n\t\t\t\t\t\tnextRunAt: nextRunAt?.toISOString(),\n\t\t\t\t\t\tlastSkippedAt: now.toISOString(),\n\t\t\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t\t\t};\n\t\t\t\t\treturn updated;\n\t\t\t\t}\n\t\t\t\tupdated = {\n\t\t\t\t\t...job,\n\t\t\t\t\tstatus: job.schedule.kind === \"once\" ? \"completed\" : job.status,\n\t\t\t\t\tlastRunAt: now.toISOString(),\n\t\t\t\t\tlastError: result.error === undefined ? undefined : errorMessage(result.error),\n\t\t\t\t\trunCount: job.runCount + 1,\n\t\t\t\t\tupdatedAt: now.toISOString(),\n\t\t\t\t};\n\t\t\t\treturn updated;\n\t\t\t});\n\t\t\treturn [];\n\t\t});\n\t\treturn updated;\n\t}\n\n\trecoverInterruptedDispatches(now = new Date()): AgentCronJob[] {\n\t\tconst recovered: AgentCronJob[] = [];\n\t\tthis.mutateStates((state) => {\n\t\t\trecoverInterruptedInState(state, now, recovered);\n\t\t\treturn [];\n\t\t});\n\t\treturn recovered;\n\t}\n\n\trecoverInterruptedDispatchesById(dispatchIds: readonly string[], now = new Date()): AgentCronJob[] {\n\t\tconst recovered: AgentCronJob[] = [];\n\t\tconst interruptedDispatchIds = new Set(dispatchIds);\n\t\tthis.mutateStates((state) => {\n\t\t\trecoverInterruptedInState(state, now, recovered, interruptedDispatchIds);\n\t\t\treturn [];\n\t\t});\n\t\treturn recovered;\n\t}\n\n\tgetDueJob(id: string, now = new Date()): AgentCronJob | undefined {\n\t\treturn this.readJobs().find((job) => job.id === id && isDueJob(job, now));\n\t}\n\n\tnextActiveRunAt(): Date | undefined {\n\t\tconst times = this.readJobs()\n\t\t\t.filter((job) => job.status === \"active\" && job.nextRunAt !== undefined)\n\t\t\t.map((job) => new Date(job.nextRunAt!))\n\t\t\t.filter((date) => Number.isFinite(date.getTime()))\n\t\t\t.sort((a, b) => a.getTime() - b.getTime());\n\t\treturn times[0];\n\t}\n\n\tprivate readJobs(): AgentCronJob[] {\n\t\treturn this.readStates().flatMap((state) => state.jobs);\n\t}\n\n\tprivate readStates(): CronJobsState[] {\n\t\tif (this.sessionArtifactMode) {\n\t\t\treturn [...this.sessionArtifactFiles.values()].map((path) => readJobsState(path));\n\t\t}\n\t\treturn [readJobsState(this.requireFilePath())];\n\t}\n\n\tprivate mutateStates(mutator: (state: CronJobsState) => AgentCronDispatch[]): AgentCronDispatch[] {\n\t\tconst paths = this.sessionArtifactMode ? [...this.sessionArtifactFiles.values()] : [this.requireFilePath()];\n\t\tconst previousHeartbeats = heartbeatCatalogSignature(this.readJobs());\n\t\tlet changed = false;\n\t\tconst dispatches = withCronJobsStateLocks(paths, () => {\n\t\t\tconst dispatches: AgentCronDispatch[] = [];\n\t\t\tfor (const path of paths) {\n\t\t\t\tconst state = readJobsState(path);\n\t\t\t\tconst before = JSON.stringify(state);\n\t\t\t\tdispatches.push(...mutator(state));\n\t\t\t\tif (JSON.stringify(state) !== before) {\n\t\t\t\t\twriteJobsState(path, state);\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn dispatches;\n\t\t});\n\t\tif (changed && heartbeatCatalogSignature(this.readJobs()) !== previousHeartbeats) {\n\t\t\tthis.notifyHeartbeatChange();\n\t\t}\n\t\treturn dispatches;\n\t}\n\n\tprivate writeJobs(jobs: readonly AgentCronJob[]): void {\n\t\tconst previousHeartbeats = heartbeatCatalogSignature(this.readJobs());\n\t\tif (this.sessionArtifactMode) {\n\t\t\tconst registeredSessionIds = new Set(this.sessionArtifactFiles.keys());\n\t\t\tconst unregistered = jobs.find((job) => !registeredSessionIds.has(job.sessionId));\n\t\t\tif (unregistered) {\n\t\t\t\tthrow new Error(`Cron job ${unregistered.id} targets an unregistered session artifact`);\n\t\t\t}\n\t\t\tconst paths = [...this.sessionArtifactFiles.values()];\n\t\t\twithCronJobsStateLocks(paths, () => {\n\t\t\t\tconst currentBySessionId = new Map(\n\t\t\t\t\t[...this.sessionArtifactFiles].map(([sessionId, path]) => [sessionId, readJobsState(path)]),\n\t\t\t\t);\n\t\t\t\tconst incomingById = new Map(jobs.map((job) => [job.id, job]));\n\t\t\t\tconst mergedJobsBySessionId = new Map<string, AgentCronJob[]>();\n\t\t\t\tfor (const [sessionId, current] of currentBySessionId) {\n\t\t\t\t\tconst retained = current.jobs.filter((job) => {\n\t\t\t\t\t\tconst incoming = incomingById.get(job.id);\n\t\t\t\t\t\treturn incoming === undefined || incoming.sessionId === sessionId;\n\t\t\t\t\t});\n\t\t\t\t\tmergedJobsBySessionId.set(\n\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\tmergeFreshJobs(\n\t\t\t\t\t\t\tretained,\n\t\t\t\t\t\t\tjobs.filter((job) => job.sessionId === sessionId),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst sessionIdByJobId = new Map(\n\t\t\t\t\t[...mergedJobsBySessionId].flatMap(([sessionId, sessionJobs]) =>\n\t\t\t\t\t\tsessionJobs.map((job) => [job.id, sessionId] as const),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tconst dispatches = [...currentBySessionId.values()].flatMap((state) => state.dispatches);\n\t\t\t\tfor (const [sessionId, path] of this.sessionArtifactFiles) {\n\t\t\t\t\tconst current = currentBySessionId.get(sessionId) ?? { jobs: [], dispatches: [] };\n\t\t\t\t\tconst nextState = {\n\t\t\t\t\t\tjobs: mergedJobsBySessionId.get(sessionId) ?? [],\n\t\t\t\t\t\tdispatches: dispatches.filter((dispatch) => sessionIdByJobId.get(dispatch.jobId) === sessionId),\n\t\t\t\t\t};\n\t\t\t\t\tif (JSON.stringify(current) !== JSON.stringify(nextState)) {\n\t\t\t\t\t\twriteJobsState(path, nextState);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (heartbeatCatalogSignature(this.readJobs()) !== previousHeartbeats) {\n\t\t\t\tthis.notifyHeartbeatChange();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst path = this.requireFilePath();\n\t\twithCronJobsStateLocks([path], () => writeJobsFile(path, jobs, true));\n\t\tif (heartbeatCatalogSignature(this.readJobs()) !== previousHeartbeats) {\n\t\t\tthis.notifyHeartbeatChange();\n\t\t}\n\t}\n\n\tprivate notifyHeartbeatChange(): void {\n\t\tfor (const listener of this.heartbeatChangeListeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n\n\tprivate requireFilePath(): string {\n\t\tif (!this.filePath) {\n\t\t\tthrow new Error(\"Cron job store does not have a legacy file path\");\n\t\t}\n\t\treturn this.filePath;\n\t}\n}\n\nexport function migrateLegacyCronJobsToSessionArtifacts(\n\tfilePath: string,\n\toptions: { isSessionOwned?: (job: AgentCronJob) => boolean; now?: Date } = {},\n): number {\n\tconst now = options.now ?? new Date();\n\tconst legacyState = readJobsState(filePath);\n\trecoverInterruptedInState(legacyState, now, []);\n\tconst jobs = legacyState.jobs.map((job) => {\n\t\tif (\n\t\t\t!options.isSessionOwned ||\n\t\t\toptions.isSessionOwned(job) ||\n\t\t\t(job.status !== \"active\" && job.status !== \"paused\")\n\t\t) {\n\t\t\treturn job;\n\t\t}\n\t\treturn withoutNextRunAt({ ...job, status: \"cancelled\", updatedAt: now.toISOString() });\n\t});\n\tif (jobs.length === 0) {\n\t\treturn 0;\n\t}\n\tconst jobsByArtifact = new Map<string, AgentCronJob[]>();\n\tfor (const job of jobs) {\n\t\tconst artifactPath = join(\n\t\t\tgetSessionArtifactPathForFile(resolve(job.sessionFile), job.sessionId),\n\t\t\tSESSION_SCHEDULED_JOBS_FILENAME,\n\t\t);\n\t\tconst grouped = jobsByArtifact.get(artifactPath) ?? [];\n\t\tgrouped.push(job);\n\t\tjobsByArtifact.set(artifactPath, grouped);\n\t}\n\tfor (const [artifactPath, artifactJobs] of jobsByArtifact) {\n\t\twriteJobsFile(artifactPath, artifactJobs, true);\n\t}\n\trenameSync(filePath, `${filePath}.migrated-${Date.now()}`);\n\treturn jobs.length;\n}\n\nexport class AgentCronScheduler {\n\tprivate timer: ReturnType<typeof setTimeout> | undefined;\n\tprivate running = false;\n\tprivate stopped = true;\n\tprivate hasStarted = false;\n\tprivate readonly dispatchLanes = new Map<string, Promise<void>>();\n\n\tconstructor(\n\t\tprivate readonly store: AgentCronJobStore,\n\t\tprivate readonly hooks: AgentCronSchedulerHooks,\n\t) {}\n\n\tstart(): void {\n\t\tthis.stopped = false;\n\t\tif (!this.hasStarted) {\n\t\t\tthis.hasStarted = true;\n\t\t\tthis.store.recoverInterruptedDispatches(this.now());\n\t\t}\n\t\tthis.scheduleNext();\n\t}\n\n\tstop(): void {\n\t\tthis.stopped = true;\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\twake(): void {\n\t\tif (this.stopped) {\n\t\t\treturn;\n\t\t}\n\t\tthis.scheduleNext(0);\n\t}\n\n\tasync runDue(now = this.now()): Promise<number> {\n\t\tif (this.running || (this.stopped && this.hasStarted)) {\n\t\t\treturn 0;\n\t\t}\n\t\tthis.running = true;\n\t\tconst dispatches: Array<{ dispatch: AgentCronDispatch; endDispatch?: () => void }> = [];\n\t\tlet claimedDispatches: AgentCronDispatch[] | undefined;\n\t\ttry {\n\t\t\tclaimedDispatches = this.store.claimDue(now, this.now());\n\t\t\tfor (const dispatch of claimedDispatches) {\n\t\t\t\tdispatches.push({ dispatch, endDispatch: this.hooks.beginDispatch?.(dispatch) });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tfor (const claimed of dispatches) claimed.endDispatch?.();\n\t\t\tif (claimedDispatches) {\n\t\t\t\tthis.store.recoverInterruptedDispatchesById(\n\t\t\t\t\tclaimedDispatches.map((dispatch) => dispatch.id),\n\t\t\t\t\tthis.now(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tthis.running = false;\n\t\t\tif (!this.stopped) {\n\t\t\t\tthis.scheduleNext();\n\t\t\t}\n\t\t}\n\t\tconst results = await Promise.all(\n\t\t\tdispatches.map(({ dispatch, endDispatch }) => this.queueDispatch(dispatch, endDispatch)),\n\t\t);\n\t\treturn results.filter((result) => result !== \"skipped\").length;\n\t}\n\n\tprivate queueDispatch(\n\t\tdispatch: AgentCronDispatch,\n\t\tendDispatch?: () => void,\n\t): Promise<AgentCronJobRunResult | undefined> {\n\t\tconst laneKey = dispatch.job.activeSessionId;\n\t\tconst previous = this.dispatchLanes.get(laneKey) ?? Promise.resolve();\n\t\tconst task = previous\n\t\t\t.catch(() => undefined)\n\t\t\t.then(async (): Promise<AgentCronJobRunResult | undefined> => {\n\t\t\t\ttry {\n\t\t\t\t\tconst job = this.store.getClaimedJob(dispatch.job.id);\n\t\t\t\t\tif (!job) {\n\t\t\t\t\t\tthis.store.recordDispatchResult(dispatch.id, { now: this.now(), outcome: \"skipped\" });\n\t\t\t\t\t\treturn \"skipped\";\n\t\t\t\t\t}\n\t\t\t\t\tlet runResult: AgentCronJobRunResult | undefined;\n\t\t\t\t\tlet error: unknown;\n\t\t\t\t\ttry {\n\t\t\t\t\t\trunResult = await this.hooks.runJob(job);\n\t\t\t\t\t} catch (runError) {\n\t\t\t\t\t\terror = runError;\n\t\t\t\t\t\tthis.hooks.onError?.(job, runError);\n\t\t\t\t\t}\n\t\t\t\t\tthis.store.recordDispatchResult(dispatch.id, {\n\t\t\t\t\t\tnow: this.now(),\n\t\t\t\t\t\toutcome: runResult === \"skipped\" && error === undefined ? \"skipped\" : \"ran\",\n\t\t\t\t\t\terror,\n\t\t\t\t\t});\n\t\t\t\t\treturn runResult;\n\t\t\t\t} finally {\n\t\t\t\t\tendDispatch?.();\n\t\t\t\t}\n\t\t\t});\n\t\tconst lane = task.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\tthis.dispatchLanes.set(laneKey, lane);\n\t\tvoid lane.finally(() => {\n\t\t\tif (this.dispatchLanes.get(laneKey) === lane) {\n\t\t\t\tthis.dispatchLanes.delete(laneKey);\n\t\t\t}\n\t\t});\n\t\treturn task;\n\t}\n\n\tprivate scheduleNext(delayMs?: number): void {\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t\tconst now = this.now();\n\t\tconst nextDelay =\n\t\t\tdelayMs ??\n\t\t\t(() => {\n\t\t\t\tconst next = this.store.nextActiveRunAt();\n\t\t\t\tif (!next) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn Math.max(0, next.getTime() - now.getTime());\n\t\t\t})();\n\t\tif (nextDelay === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthis.timer = setTimeout(\n\t\t\t() => {\n\t\t\t\tvoid this.runDue().catch((error) => {\n\t\t\t\t\tlog.error(\"Scheduled job dispatch failed\", {\n\t\t\t\t\t\terror: error instanceof Error ? (error.stack ?? error.message) : String(error),\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t},\n\t\t\tMath.min(nextDelay, MAX_TIMEOUT_MS),\n\t\t);\n\t}\n\n\tprivate now(): Date {\n\t\treturn this.hooks.now?.() ?? new Date();\n\t}\n}\n\nexport function parseAgentCronSchedule(\n\tinput: string,\n\tnow = new Date(),\n): { schedule: AgentCronSchedule; nextRunAt: Date } {\n\tconst text = stripMatchingQuotes(input.trim());\n\tif (!text) {\n\t\tthrow new Error(\"Cron schedule cannot be empty\");\n\t}\n\n\tconst inMatch = /^in\\s+(\\d+)\\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i.exec(text);\n\tif (inMatch) {\n\t\tconst amount = Number.parseInt(inMatch[1]!, 10);\n\t\tconst unit = inMatch[2]!.toLowerCase();\n\t\tconst multiplier = unit.startsWith(\"m\")\n\t\t\t? ONE_MINUTE_MS\n\t\t\t: unit.startsWith(\"h\")\n\t\t\t\t? 60 * ONE_MINUTE_MS\n\t\t\t\t: 24 * 60 * ONE_MINUTE_MS;\n\t\treturn {\n\t\t\tschedule: { kind: \"once\", expression: text },\n\t\t\tnextRunAt: new Date(now.getTime() + amount * multiplier),\n\t\t};\n\t}\n\n\tconst everyMatch =\n\t\t/^(?:every|each)\\s+(\\d+)\\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/i.exec(\n\t\t\ttext,\n\t\t);\n\tif (everyMatch) {\n\t\tconst amount = Number.parseInt(everyMatch[1]!, 10);\n\t\tconst unit = everyMatch[2]!.toLowerCase();\n\t\tconst multiplier = unit.startsWith(\"s\")\n\t\t\t? ONE_SECOND_MS\n\t\t\t: unit.startsWith(\"m\")\n\t\t\t\t? ONE_MINUTE_MS\n\t\t\t\t: 60 * ONE_MINUTE_MS;\n\t\tconst intervalMs = amount * multiplier;\n\t\tif (intervalMs < 10 * ONE_SECOND_MS) {\n\t\t\tthrow new Error(\"Recurring interval must be at least 10 seconds\");\n\t\t}\n\t\treturn {\n\t\t\tschedule: { kind: \"interval\", expression: text, intervalMs },\n\t\t\tnextRunAt: new Date(now.getTime() + intervalMs),\n\t\t};\n\t}\n\n\tif (text.toLowerCase().startsWith(\"at \")) {\n\t\tconst when = new Date(text.slice(3).trim());\n\t\tif (!Number.isFinite(when.getTime())) {\n\t\t\tthrow new Error(\"Invalid one-shot schedule. Use: at <ISO date>\");\n\t\t}\n\t\tif (when.getTime() <= now.getTime()) {\n\t\t\tthrow new Error(\"One-shot schedule must be in the future\");\n\t\t}\n\t\treturn { schedule: { kind: \"once\", expression: text }, nextRunAt: when };\n\t}\n\n\tconst expression = normalizeCronAlias(text);\n\tconst nextRunAt = nextCronRunAfter(expression, now);\n\treturn { schedule: { kind: \"cron\", expression }, nextRunAt };\n}\n\nexport function normalizeHeartbeatSchedule(input: string | undefined): string {\n\tconst text = input?.trim();\n\tif (!text) {\n\t\treturn DEFAULT_HEARTBEAT_SCHEDULE;\n\t}\n\tif (/^\\d+\\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/i.test(text)) {\n\t\treturn `every ${text}`;\n\t}\n\treturn text;\n}\n\nexport function normalizeHeartbeatDeliveryMode(value: unknown): AgentHeartbeatDeliveryMode | undefined {\n\tif (value === undefined || value === null) {\n\t\treturn undefined;\n\t}\n\tif (value === \"steer\" || value === \"follow_up\") {\n\t\treturn value;\n\t}\n\tthrow new Error('Heartbeat delivery mode must be \"steer\" or \"follow_up\"');\n}\n\nexport function resolveHeartbeatStreamingBehavior(\n\tdeliveryMode: AgentHeartbeatDeliveryMode | undefined,\n): \"steer\" | \"followUp\" {\n\treturn (deliveryMode ?? DEFAULT_HEARTBEAT_DELIVERY_MODE) === \"follow_up\" ? \"followUp\" : \"steer\";\n}\n\nexport function parseHeartbeatCommand(input: string): ParsedHeartbeatCommand {\n\tconst text = input.replace(/^\\/heartbeat\\b/, \"\").trim();\n\tif (!text || text === \"status\") {\n\t\treturn { type: \"status\" };\n\t}\n\tif (text === \"pause\") {\n\t\treturn { type: \"pause\" };\n\t}\n\tif (text === \"resume\") {\n\t\treturn { type: \"resume\" };\n\t}\n\tif (text === \"clear\" || text === \"stop\") {\n\t\treturn { type: \"clear\" };\n\t}\n\n\tconst leadingDelivery = consumeDeliveryOption(text);\n\tlet deliveryMode = leadingDelivery.deliveryMode;\n\tlet remaining = leadingDelivery.rest;\n\n\tconst option = consumeEveryOption(remaining);\n\tif (option) {\n\t\tconst trailingDelivery = consumeDeliveryOption(option.rest);\n\t\tdeliveryMode = trailingDelivery.deliveryMode ?? deliveryMode;\n\t\tif (!trailingDelivery.rest) {\n\t\t\tthrow new Error(\"Usage: /heartbeat [--every <interval>] [--steer|--follow-up] <instruction>\");\n\t\t}\n\t\treturn {\n\t\t\ttype: \"set\",\n\t\t\tschedule: normalizeHeartbeatSchedule(option.interval),\n\t\t\tinstruction: trailingDelivery.rest,\n\t\t\t...(deliveryMode ? { deliveryMode } : {}),\n\t\t};\n\t}\n\n\tconst leadingSchedule = consumeLeadingEverySchedule(remaining);\n\tif (leadingSchedule) {\n\t\tconst trailingDelivery = consumeDeliveryOption(leadingSchedule.rest);\n\t\tdeliveryMode = trailingDelivery.deliveryMode ?? deliveryMode;\n\t\tif (!trailingDelivery.rest) {\n\t\t\tthrow new Error(\"Usage: /heartbeat [--every <interval>] [--steer|--follow-up] <instruction>\");\n\t\t}\n\t\treturn {\n\t\t\ttype: \"set\",\n\t\t\tschedule: normalizeHeartbeatSchedule(leadingSchedule.interval),\n\t\t\tinstruction: trailingDelivery.rest,\n\t\t\t...(deliveryMode ? { deliveryMode } : {}),\n\t\t};\n\t}\n\n\tremaining = remaining.trim();\n\tif (!remaining) {\n\t\tthrow new Error(\"Usage: /heartbeat [--every <interval>] [--steer|--follow-up] <instruction>\");\n\t}\n\treturn {\n\t\ttype: \"set\",\n\t\tschedule: DEFAULT_HEARTBEAT_SCHEDULE,\n\t\tinstruction: remaining,\n\t\t...(deliveryMode ? { deliveryMode } : {}),\n\t};\n}\n\nexport function nextRunAtForSchedule(schedule: AgentCronSchedule, after: Date): Date | undefined {\n\tif (schedule.kind === \"once\") {\n\t\treturn undefined;\n\t}\n\tif (schedule.kind === \"interval\") {\n\t\tif (!schedule.intervalMs || schedule.intervalMs <= 0) {\n\t\t\tthrow new Error(`Invalid interval schedule: ${schedule.expression}`);\n\t\t}\n\t\treturn new Date(after.getTime() + schedule.intervalMs);\n\t}\n\treturn nextCronRunAfter(schedule.expression, after);\n}\n\nexport function formatAgentCronJob(job: AgentCronJob): string {\n\tconst next = job.nextRunAt ? new Date(job.nextRunAt).toLocaleString() : \"-\";\n\tconst last = job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : \"-\";\n\tconst preview = job.prompt.replace(/\\s+/g, \" \").slice(0, 80);\n\tconst error = job.lastError ? ` error=${job.lastError}` : \"\";\n\tconst label = job.label ? ` label=\"${job.label}\"` : \"\";\n\tconst skipped = job.lastSkippedAt ? ` skipped=${new Date(job.lastSkippedAt).toLocaleString()}` : \"\";\n\treturn `${job.id} ${job.status}${label} next=${next} last=${last}${skipped} runs=${job.runCount} schedule=\"${job.schedule.expression}\" prompt=\"${preview}\"${error}`;\n}\n\nfunction consumeDeliveryOption(text: string): { deliveryMode: AgentHeartbeatDeliveryMode | undefined; rest: string } {\n\tlet rest = text.trim();\n\tif (/(?:^|\\s)--deliver=?$/i.test(rest)) {\n\t\tthrow new Error('Heartbeat delivery mode must be \"steer\" or \"follow_up\"');\n\t}\n\tlet deliveryMode: AgentHeartbeatDeliveryMode | undefined;\n\tlet leading = consumeLeadingDeliveryFlag(rest);\n\twhile (leading) {\n\t\tdeliveryMode = leading.deliveryMode;\n\t\trest = leading.rest.trim();\n\t\tleading = consumeLeadingDeliveryFlag(rest);\n\t}\n\n\tlet trailing = consumeTrailingDeliveryFlag(rest);\n\tlet trailingDeliveryMode: AgentHeartbeatDeliveryMode | undefined;\n\twhile (trailing) {\n\t\t// Consume all trailing flags, but keep the rightmost flag's mode because it\n\t\t// is textually latest and should win over earlier flags.\n\t\ttrailingDeliveryMode ??= trailing.deliveryMode;\n\t\trest = trailing.rest.trim();\n\t\ttrailing = consumeTrailingDeliveryFlag(rest);\n\t}\n\treturn { deliveryMode: trailingDeliveryMode ?? deliveryMode, rest };\n}\n\nfunction consumeLeadingDeliveryFlag(\n\ttext: string,\n): { deliveryMode: AgentHeartbeatDeliveryMode; rest: string } | undefined {\n\tconst match = /^--(?:deliver(?:=|\\s+)(\\S+)|(steer)|(follow[-_]up))(?:\\s+|$)([\\s\\S]*)$/i.exec(text);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tdeliveryMode: parseDeliveryModeToken(match[1] ?? match[2] ?? match[3] ?? \"\"),\n\t\trest: match[4]?.trim() ?? \"\",\n\t};\n}\n\nfunction consumeTrailingDeliveryFlag(\n\ttext: string,\n): { deliveryMode: AgentHeartbeatDeliveryMode; rest: string } | undefined {\n\tconst deliverWithSpace = /^([\\s\\S]*?)\\s+--deliver\\s+(\\S+)$/i.exec(text);\n\tif (deliverWithSpace) {\n\t\treturn { deliveryMode: parseDeliveryModeToken(deliverWithSpace[2] ?? \"\"), rest: deliverWithSpace[1] ?? \"\" };\n\t}\n\tconst deliverWithEquals = /^([\\s\\S]*?)\\s+--deliver=(\\S+)$/i.exec(text);\n\tif (deliverWithEquals) {\n\t\treturn { deliveryMode: parseDeliveryModeToken(deliverWithEquals[2] ?? \"\"), rest: deliverWithEquals[1] ?? \"\" };\n\t}\n\tconst shorthand = /^([\\s\\S]*?)\\s+--(steer|follow[-_]up)$/i.exec(text);\n\tif (shorthand) {\n\t\treturn { deliveryMode: parseDeliveryModeToken(shorthand[2] ?? \"\"), rest: shorthand[1] ?? \"\" };\n\t}\n\treturn undefined;\n}\n\nfunction parseDeliveryModeToken(token: string): AgentHeartbeatDeliveryMode {\n\tconst normalized = token.toLowerCase().replace(\"-\", \"_\");\n\tif (normalized === \"steer\" || normalized === \"follow_up\") {\n\t\treturn normalized;\n\t}\n\tthrow new Error('Heartbeat delivery mode must be \"steer\" or \"follow_up\"');\n}\n\nfunction consumeEveryOption(text: string): { interval: string; rest: string } | undefined {\n\tconst match =\n\t\t/^--every(?:=|\\s+)(?:\"([^\"]+)\"|'([^']+)'|(\\d+\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours))|(\\S+))(?:\\s+|$)([\\s\\S]*)$/i.exec(\n\t\t\ttext,\n\t\t);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tinterval: match[1] ?? match[2] ?? match[3] ?? match[4] ?? \"\",\n\t\trest: match[5]?.trim() ?? \"\",\n\t};\n}\n\nfunction consumeLeadingEverySchedule(text: string): { interval: string; rest: string } | undefined {\n\tconst match =\n\t\t/^(every|each)\\s+\\d+\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\\b/i.exec(text);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tinterval: match[0],\n\t\trest: text\n\t\t\t.slice(match[0].length)\n\t\t\t.trim()\n\t\t\t// Strip only a standalone \"--\" separator, never a flag like \"--follow-up\".\n\t\t\t.replace(/^--(?=\\s|$)/, \"\")\n\t\t\t.trim(),\n\t};\n}\n\nexport function isHeartbeatCronJob(job: AgentCronJob): boolean {\n\treturn job.source === \"heartbeat\" || job.source === \"rlm_heartbeat\";\n}\n\nexport function shouldDeferHeartbeatCronJob(job: AgentCronJob, activity: HeartbeatCronSessionActivity): boolean {\n\tif (!isHeartbeatCronJob(job)) {\n\t\treturn false;\n\t}\n\t// States where delivering a heartbeat is unsafe or would stack redundant work,\n\t// regardless of delivery mode.\n\tconst busyBesidesStreaming =\n\t\tactivity.isCompacting === true ||\n\t\tactivity.isRetrying === true ||\n\t\tactivity.isBashRunning ||\n\t\tactivity.hasPendingSessionWork ||\n\t\t(!activity.isStreaming && activity.unfinishedActionCount > 0);\n\tif (busyBesidesStreaming) {\n\t\treturn true;\n\t}\n\t// \"steer\" heartbeats interrupt the current turn, so a plain streaming turn must\n\t// not defer them; \"follow_up\" heartbeats wait, so streaming still defers.\n\tif (resolveHeartbeatStreamingBehavior(job.deliveryMode) === \"steer\") {\n\t\treturn false;\n\t}\n\treturn activity.isStreaming;\n}\n\nfunction nextCronRunAfter(expression: string, after: Date): Date {\n\tconst fields = parseCronExpression(expression);\n\tconst candidate = new Date(after.getTime());\n\tcandidate.setSeconds(0, 0);\n\tcandidate.setMinutes(candidate.getMinutes() + 1);\n\n\tconst deadline = candidate.getTime() + 366 * 24 * 60 * ONE_MINUTE_MS;\n\twhile (candidate.getTime() <= deadline) {\n\t\tif (matchesCronFields(candidate, fields)) {\n\t\t\treturn candidate;\n\t\t}\n\t\tcandidate.setMinutes(candidate.getMinutes() + 1);\n\t}\n\tthrow new Error(`Cron schedule did not match within one year: ${expression}`);\n}\n\nfunction parseCronExpression(expression: string): CronFields {\n\tconst parts = expression.trim().split(/\\s+/);\n\tif (parts.length !== 5) {\n\t\tthrow new Error(\n\t\t\t\"Unsupported cron schedule. Use 'in 10m', 'at <ISO date>', @hourly, or five fields: minute hour day month weekday\",\n\t\t);\n\t}\n\treturn {\n\t\tminute: parseCronField(parts[0]!, 0, 59),\n\t\thour: parseCronField(parts[1]!, 0, 23),\n\t\tdayOfMonth: parseCronField(parts[2]!, 1, 31),\n\t\tmonth: parseCronField(parts[3]!, 1, 12),\n\t\tdayOfWeek: parseCronField(parts[4]!, 0, 7),\n\t};\n}\n\ninterface CronFields {\n\tminute: Set<number>;\n\thour: Set<number>;\n\tdayOfMonth: Set<number>;\n\tmonth: Set<number>;\n\tdayOfWeek: Set<number>;\n}\n\nfunction parseCronField(field: string, min: number, max: number): Set<number> {\n\tconst values = new Set<number>();\n\tfor (const part of field.split(\",\")) {\n\t\tif (!part) {\n\t\t\tthrow new Error(`Invalid cron field: ${field}`);\n\t\t}\n\t\tconst [rangeText, stepText] = part.split(\"/\");\n\t\tconst step = stepText === undefined ? 1 : parseCronNumber(stepText, 1, max);\n\t\tlet start: number;\n\t\tlet end: number;\n\t\tif (rangeText === \"*\") {\n\t\t\tstart = min;\n\t\t\tend = max;\n\t\t} else if (rangeText?.includes(\"-\")) {\n\t\t\tconst [startText, endText] = rangeText.split(\"-\");\n\t\t\tstart = parseCronNumber(startText, min, max);\n\t\t\tend = parseCronNumber(endText, min, max);\n\t\t\tif (start > end) {\n\t\t\t\tthrow new Error(`Invalid cron range: ${rangeText}`);\n\t\t\t}\n\t\t} else {\n\t\t\tstart = parseCronNumber(rangeText, min, max);\n\t\t\tend = start;\n\t\t}\n\t\tfor (let value = start; value <= end; value += step) {\n\t\t\tvalues.add(value);\n\t\t}\n\t}\n\treturn values;\n}\n\nfunction parseCronNumber(value: string | undefined, min: number, max: number): number {\n\tif (!value || !/^\\d+$/.test(value)) {\n\t\tthrow new Error(`Invalid cron number: ${value ?? \"\"}`);\n\t}\n\tconst parsed = Number.parseInt(value, 10);\n\tif (parsed < min || parsed > max) {\n\t\tthrow new Error(`Cron number out of range: ${value}`);\n\t}\n\treturn parsed;\n}\n\nfunction matchesCronFields(date: Date, fields: CronFields): boolean {\n\tconst day = date.getDay();\n\tconst dayMatches = fields.dayOfWeek.has(day) || (day === 0 && fields.dayOfWeek.has(7));\n\treturn (\n\t\tfields.minute.has(date.getMinutes()) &&\n\t\tfields.hour.has(date.getHours()) &&\n\t\tfields.dayOfMonth.has(date.getDate()) &&\n\t\tfields.month.has(date.getMonth() + 1) &&\n\t\tdayMatches\n\t);\n}\n\nfunction normalizeCronAlias(text: string): string {\n\tswitch (text) {\n\t\tcase \"@hourly\":\n\t\t\treturn \"0 * * * *\";\n\t\tcase \"@daily\":\n\t\t\treturn \"0 0 * * *\";\n\t\tcase \"@weekly\":\n\t\t\treturn \"0 0 * * 0\";\n\t\tcase \"@monthly\":\n\t\t\treturn \"0 0 1 * *\";\n\t\tdefault:\n\t\t\treturn text;\n\t}\n}\n\nfunction stripMatchingQuotes(value: string): string {\n\tif (\n\t\tvalue.length >= 2 &&\n\t\t((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\")))\n\t) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction isDueJob(job: AgentCronJob, now: Date): boolean {\n\treturn job.status === \"active\" && job.nextRunAt !== undefined && Date.parse(job.nextRunAt) <= now.getTime();\n}\n\nfunction withCronJobsStateLocks<T>(paths: readonly string[], action: () => T): T {\n\tconst releases: Array<() => void> = [];\n\ttry {\n\t\tfor (const path of [...new Set(paths)].sort()) {\n\t\t\tmkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n\t\t\tlet release: (() => void) | undefined;\n\t\t\tlet lockCompromised = false;\n\t\t\tfor (let attempt = 0; attempt < 100; attempt++) {\n\t\t\t\ttry {\n\t\t\t\t\trelease = lockSync(path, {\n\t\t\t\t\t\trealpath: false,\n\t\t\t\t\t\tlockfilePath: `${path}.lock`,\n\t\t\t\t\t\tstale: 30_000,\n\t\t\t\t\t\tonCompromised: () => {\n\t\t\t\t\t\t\tlockCompromised = true;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tif (lockCompromised) {\n\t\t\t\t\t\trelease();\n\t\t\t\t\t\tthrow new Error(`Cron jobs lock compromised: ${path}`);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (lockCompromised) {\n\t\t\t\t\t\tthrow new Error(`Cron jobs lock compromised: ${path}`);\n\t\t\t\t\t}\n\t\t\t\t\tif ((error as NodeJS.ErrnoException).code !== \"ELOCKED\" || attempt === 99) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!release) {\n\t\t\t\tthrow new Error(`Could not coordinate scheduled jobs: ${path}`);\n\t\t\t}\n\t\t\treleases.push(release);\n\t\t}\n\t\treturn action();\n\t} finally {\n\t\tfor (const release of releases.reverse()) {\n\t\t\trelease();\n\t\t}\n\t}\n}\n\nfunction readJobsState(path: string): CronJobsState {\n\tif (!existsSync(path)) {\n\t\treturn { jobs: [], dispatches: [] };\n\t}\n\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as CronJobsFile;\n\treturn {\n\t\tjobs: Array.isArray(parsed.jobs) ? parsed.jobs.filter(isAgentCronJob) : [],\n\t\tdispatches: Array.isArray(parsed.dispatches) ? parsed.dispatches.filter(isAgentCronDispatchRecord) : [],\n\t};\n}\n\nfunction writeJobsFile(path: string, jobs: readonly AgentCronJob[], mergeCurrent: boolean): void {\n\tconst current = readJobsState(path);\n\twriteJobsState(path, {\n\t\tjobs: mergeCurrent ? mergeFreshJobs(current.jobs, jobs) : [...jobs],\n\t\tdispatches: current.dispatches,\n\t});\n}\n\nfunction writeJobsState(path: string, state: CronJobsState): void {\n\tconst directory = dirname(path);\n\tmkdirSync(directory, { recursive: true, mode: 0o700 });\n\tconst tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;\n\tconst descriptor = openSync(tempPath, \"w\", 0o600);\n\ttry {\n\t\twriteFileSync(descriptor, `${JSON.stringify(state, null, 2)}\\n`, \"utf-8\");\n\t\tfsyncSync(descriptor);\n\t} finally {\n\t\tcloseSync(descriptor);\n\t}\n\trenameSync(tempPath, path);\n\ttry {\n\t\tconst directoryDescriptor = openSync(directory, \"r\");\n\t\ttry {\n\t\t\tfsyncSync(directoryDescriptor);\n\t\t} finally {\n\t\t\tcloseSync(directoryDescriptor);\n\t\t}\n\t} catch {\n\t\t// Directory fsync is unavailable on some platforms; the atomic rename still protects readers.\n\t}\n}\n\nfunction claimDueInState(state: CronJobsState, dueAt: Date, claimedAt: Date): AgentCronDispatch[] {\n\tconst dispatches: AgentCronDispatch[] = [];\n\tconst claimedJobIds = new Set(state.dispatches.map((dispatch) => dispatch.jobId));\n\tstate.jobs = state.jobs.map((job) => {\n\t\tif (!isDueJob(job, dueAt)) {\n\t\t\treturn job;\n\t\t}\n\t\tconst scheduledFor = job.nextRunAt!;\n\t\tconst nextRunAt = nextRunAtForSchedule(job.schedule, claimedAt)?.toISOString();\n\t\tconst advanced = nextRunAt\n\t\t\t? { ...job, nextRunAt, updatedAt: claimedAt.toISOString() }\n\t\t\t: withoutNextRunAt({ ...job, updatedAt: claimedAt.toISOString() });\n\t\tif (claimedJobIds.has(job.id)) {\n\t\t\treturn { ...advanced, lastSkippedAt: claimedAt.toISOString() };\n\t\t}\n\t\tconst dispatch: AgentCronDispatchRecord = {\n\t\t\tid: randomUUID(),\n\t\t\tjobId: job.id,\n\t\t\tclaimedAt: claimedAt.toISOString(),\n\t\t\tscheduledFor,\n\t\t};\n\t\tstate.dispatches.push(dispatch);\n\t\tdispatches.push({ id: dispatch.id, job: advanced });\n\t\treturn advanced;\n\t});\n\treturn dispatches;\n}\n\nfunction recoverInterruptedInState(\n\tstate: CronJobsState,\n\tnow: Date,\n\trecovered: AgentCronJob[],\n\tdispatchIds?: ReadonlySet<string>,\n): void {\n\tconst interrupted = dispatchIds\n\t\t? state.dispatches.filter((dispatch) => dispatchIds.has(dispatch.id))\n\t\t: state.dispatches;\n\tif (interrupted.length === 0) {\n\t\treturn;\n\t}\n\tconst interruptedIds = new Set(interrupted.map((dispatch) => dispatch.jobId));\n\tstate.dispatches = dispatchIds ? state.dispatches.filter((dispatch) => !dispatchIds.has(dispatch.id)) : [];\n\tstate.jobs = state.jobs.map((job) => {\n\t\tif (!interruptedIds.has(job.id) || job.status !== \"active\") {\n\t\t\treturn job;\n\t\t}\n\t\tconst next = {\n\t\t\t...job,\n\t\t\tstatus: job.schedule.kind === \"once\" ? (\"completed\" as const) : job.status,\n\t\t\tlastError: \"Interrupted before scheduled operation completion\",\n\t\t\tupdatedAt: now.toISOString(),\n\t\t};\n\t\trecovered.push(next);\n\t\treturn next;\n\t});\n}\n\nfunction mergeFreshJobs(currentJobs: readonly AgentCronJob[], nextJobs: readonly AgentCronJob[]): AgentCronJob[] {\n\tconst merged = new Map<string, AgentCronJob>();\n\tfor (const job of currentJobs) {\n\t\tmerged.set(job.id, job);\n\t}\n\tfor (const job of nextJobs) {\n\t\tconst current = merged.get(job.id);\n\t\tif (!current || isAtLeastAsFresh(job, current)) {\n\t\t\tmerged.set(job.id, job);\n\t\t}\n\t}\n\treturn [...merged.values()];\n}\n\nfunction isAtLeastAsFresh(candidate: AgentCronJob, current: AgentCronJob): boolean {\n\tconst candidateTime = Date.parse(candidate.updatedAt);\n\tconst currentTime = Date.parse(current.updatedAt);\n\tif (!Number.isFinite(currentTime)) {\n\t\treturn true;\n\t}\n\tif (!Number.isFinite(candidateTime)) {\n\t\treturn false;\n\t}\n\treturn candidateTime >= currentTime;\n}\n\nfunction compareOptionalIso(left: string | undefined, right: string | undefined): number {\n\tif (left === right) {\n\t\treturn 0;\n\t}\n\tif (left === undefined) {\n\t\treturn 1;\n\t}\n\tif (right === undefined) {\n\t\treturn -1;\n\t}\n\treturn Date.parse(left) - Date.parse(right);\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction normalizeOptionalLabel(label: string | undefined): string | undefined {\n\tconst trimmed = label?.trim();\n\treturn trimmed ? trimmed : undefined;\n}\n\nfunction withoutNextRunAt(job: AgentCronJob): AgentCronJob {\n\tconst { nextRunAt: _nextRunAt, ...rest } = job;\n\treturn rest;\n}\n\nexport function isAgentCronJob(value: unknown): value is AgentCronJob {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn false;\n\t}\n\tconst candidate = value as Partial<AgentCronJob>;\n\treturn (\n\t\ttypeof candidate.id === \"string\" &&\n\t\t(candidate.status === \"active\" ||\n\t\t\tcandidate.status === \"paused\" ||\n\t\t\tcandidate.status === \"completed\" ||\n\t\t\tcandidate.status === \"cancelled\") &&\n\t\t(candidate.source === undefined ||\n\t\t\tcandidate.source === \"cron\" ||\n\t\t\tcandidate.source === \"heartbeat\" ||\n\t\t\tcandidate.source === \"rlm_heartbeat\") &&\n\t\t(candidate.runtimeKind === undefined ||\n\t\t\tcandidate.runtimeKind === \"top-level\" ||\n\t\t\tcandidate.runtimeKind === \"subagent\") &&\n\t\t(candidate.deliveryMode === undefined ||\n\t\t\tcandidate.deliveryMode === \"steer\" ||\n\t\t\tcandidate.deliveryMode === \"follow_up\") &&\n\t\ttypeof candidate.activeSessionId === \"string\" &&\n\t\ttypeof candidate.sessionId === \"string\" &&\n\t\ttypeof candidate.sessionFile === \"string\" &&\n\t\ttypeof candidate.cwd === \"string\" &&\n\t\t(candidate.label === undefined || typeof candidate.label === \"string\") &&\n\t\ttypeof candidate.prompt === \"string\" &&\n\t\ttypeof candidate.schedule === \"object\" &&\n\t\tcandidate.schedule !== null &&\n\t\t(candidate.schedule.kind === \"once\" ||\n\t\t\tcandidate.schedule.kind === \"cron\" ||\n\t\t\t(candidate.schedule.kind === \"interval\" &&\n\t\t\t\ttypeof candidate.schedule.intervalMs === \"number\" &&\n\t\t\t\tcandidate.schedule.intervalMs > 0)) &&\n\t\ttypeof candidate.schedule.expression === \"string\" &&\n\t\ttypeof candidate.createdAt === \"string\" &&\n\t\ttypeof candidate.updatedAt === \"string\" &&\n\t\ttypeof candidate.runCount === \"number\"\n\t);\n}\n\nfunction isAgentCronDispatchRecord(value: unknown): value is AgentCronDispatchRecord {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn false;\n\t}\n\tconst candidate = value as Partial<AgentCronDispatchRecord>;\n\treturn (\n\t\ttypeof candidate.id === \"string\" &&\n\t\ttypeof candidate.jobId === \"string\" &&\n\t\ttypeof candidate.claimedAt === \"string\" &&\n\t\ttypeof candidate.scheduledFor === \"string\"\n\t);\n}\n"]}
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { getLogger } from "@ponythewhite/base-context-ai";
4
5
  import { lockSync } from "proper-lockfile";
5
6
  import { getSessionArtifactPathForFile } from "./session-manager.js";
6
7
  export const SESSION_SCHEDULED_JOBS_FILENAME = "scheduled-jobs.json";
8
+ const log = getLogger("coding-agent.cron");
7
9
  const MAX_TIMEOUT_MS = 2_147_483_647;
8
10
  const ONE_SECOND_MS = 1000;
9
11
  const ONE_MINUTE_MS = 60_000;
@@ -881,7 +883,11 @@ export class AgentCronScheduler {
881
883
  return;
882
884
  }
883
885
  this.timer = setTimeout(() => {
884
- void this.runDue();
886
+ void this.runDue().catch((error) => {
887
+ log.error("Scheduled job dispatch failed", {
888
+ error: error instanceof Error ? (error.stack ?? error.message) : String(error),
889
+ });
890
+ });
885
891
  }, Math.min(nextDelay, MAX_TIMEOUT_MS));
886
892
  }
887
893
  now() {