chainlesschain 0.163.5 → 0.163.7

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 (236) hide show
  1. package/completions/_cc +1 -1
  2. package/completions/cc.bash +1 -1
  3. package/completions/cc.fish +1 -1
  4. package/completions/cc.ps1 +1 -1
  5. package/package.json +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-Cv2EE1Zy.js → AIOps-axqQfwv8.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CcpxMLuW.js → ActionButton-DV-Ro0By.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DE8mJoMI.js → Analytics-Bt72WEVu.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-DKn9dk9L.js → AppLayout-BtepG55E.js} +5 -5
  10. package/src/assets/web-panel/assets/{Artifacts-Cp3mG3Py.js → Artifacts-B7KaJ7S_.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-zvsQfF2C.js → Audit-FNFaUg7A.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-Bi85SsjN.js → BackgroundAgents-DFb6J2wQ.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-Cw47Dj6s.js → Backup-CC7YBhyj.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-C5Ip2afH.js → BaseInput-RkgTNVtj.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-C0iAFkJv.js → Chat-hHEmGZy-.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BV9Wv7wO.js → ChatBubbleRenderer-CAP9YhHv.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-3KZZCiAF.js → Checkbox-z5wXOQP1.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-C8OMbBAz.js → Codegen-DVaDJnxy.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-B-MB84LJ.js → Col-CJmaRDR5.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-DpR7Dxao.js → Community-DF29_2bI.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-CyDtz0OS.js → Compact-DtkQkBzn.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-5tiRa2An.js → Compliance-xoHDxp7I.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-DnQaKZ5x.js → Cowork-BUm-R7ht.js} +4 -4
  24. package/src/assets/web-panel/assets/{Cron-BaWfoEDf.js → Cron-Cgl14ZkB.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-Bu0HoHhb.js → Crosschain-BNcbmoqh.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-31zk8upX.js → DID-Bwja0Zo9.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-BmVne-e2.js → Dashboard-Bk5XjEdJ.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-B4PkoOWd.js → Dropdown-Ci36Oitq.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-BZ6tyFtI.js → EmailListRenderer--U04ZN4q.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Osozn4Ds.js → FamilyGuardDashboard-B2gNPR2s.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-DaH8TOAP.js → Federation-C4KghYn7.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-tbU0uwis.js → FormItemContext-QL7x_Q_0.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-Df8LQBrL.js → GenericCardRenderer-F3Ryltwm.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-DsMB6-14.js → Git-2HassK5k.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BnH0dZzd.js → Governance-DoBIp7oe.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-DhnN1N_P.js → Inference--W01QhZ-.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-DnNshAgn.js → KnowledgeGraph-DmvwqGpT.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-BmBFX3_N.js → Logs-BBJ4yAel.js} +2 -2
  39. package/src/assets/web-panel/assets/{MarkdownRenderer-DCslfQU3.js → MarkdownRenderer-BLCZMzhJ.js} +1 -1
  40. package/src/assets/web-panel/assets/{Marketplace-CPEgWDc3.js → Marketplace-D5lNngSI.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-C36qjgF9.js → McpTools-CkEVOMKH.js} +5 -5
  42. package/src/assets/web-panel/assets/{Memory-tNHvrgqN.js → Memory-B1Ow1xVE.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-Xaf6cKjc.js → MobileBridge-SgYbUfb6.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-D2E5TTeO.js → MobileProjects-BWdOvQAj.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-D4AJ2dwV.js → Mtc-DcTXSSma.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-Df-gtn2_.js → MtcAudit-Dt3WG4oj.js} +4 -4
  47. package/src/assets/web-panel/assets/{Multisig-DTCHLWld.js → Multisig-B9B38UZx.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-DruZ_1Jb.js → NLProgramming-B0S4GGvG.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-Dx3pdGDs.js → Notes-DZ5U58Ng.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-Bi3L69oL.js → NotificationSettings-PjL5ZW34.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-BHY33m7C.js → OrderTableRenderer-DrAX2ESt.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-BG2ZVEV-.js → Organization-Cv1GYs-c.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-Viwtg4Mn.js → Overflow-Bbkp_PL3.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-Clm_yc1Z.js → P2P-BD8QUgoZ.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-NKmmDiOw.js → PdhVaultBrowser-BO-2PeaB.js} +5 -5
  56. package/src/assets/web-panel/assets/{Permissions-DIL8u9Nk.js → Permissions-D_OPVa02.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-B-jM4wt-.js → PersonalDataHub-mh-pEmaB.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-627I__H5.js → Pipeline-6JC2EDdb.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-2DN2qrkW.js → Privacy-CUshzUAG.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-CN4wC0sF.js → ProjectInit-Cs2Aalri.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-K35vsh5H.js → ProjectSettings-1ak49tcj.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-B_wQadhX.js → Projects-BV4oApsm.js} +1 -1
  63. package/src/assets/web-panel/assets/{Providers-MckaQUhQ.js → Providers-CXXxvafH.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BS3mtA_O.js → QrScannerModal-PTcDsF58.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BHp_nofL.js → QuickAsk-CDwucrJw.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-DlmnPuZj.js → Recommend-CJlLVRXR.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-tsyFbvpy.js → RemoteSession-DlN-ETsO.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-Ba1iWv2D.js → Reputation-DI23LR6m.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-NnlGC--f.js → Row-Cl7UQMrM.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-CjThSIRv.js → RssFeed-CexHNfh8.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-CojUyC5x.js → Search-BneBWvRN.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-c-sxeJEc.js → Security-DX7Phgrp.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-CtcKEEmF.js → Services-BWXO8Mte.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-CIBBXflN.js → Skeleton-DCyOjKPJ.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-D1XOOK9n.js → Skills-CXqERsTB.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-hRfEp5Sn.js → Sla-bhbrbz8E.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-Dexb1f_6.js → SpeechSettings-3IPU0HBZ.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-BJsSFGYy.js → SyncSettings-DBNTQLbM.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-CfSZ8A1L.js → Tasks-quP0bkN-.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-idDf47W-.js → Templates-BouWmgho.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-C-UsX6w6.js → Tenant-qv0D9s7z.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-BgJtX5n1.js → Terminal-BxvbE8BB.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-DP0BLKTl.js → TimelineRenderer-D-IeLnJ2.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-LwCzqETp.js → Tokens-CFMKKyVG.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-D_9tVch0.js → Trigger-L8as3UuJ.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-BH2UlEWh.js → Trust-MxmA4VZG.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-CIpD0nYb.js → UkeySign-C0VvJYRi.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-9ShhYxK_.js → VideoEditing-Bf5QZj8N.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-C7afGNIG.js → Wallet-BIJ9riRz.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-bUKn6i5R.js → WebAuthn-B8p9Mhkw.js} +4 -4
  91. package/src/assets/web-panel/assets/{WorkflowEditor-CxgQaGyp.js → WorkflowEditor-BVsuaPen.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-DMVkmJwx.js → chat-BfkjoKpV.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-CDzQh5nS.js → colors-DeAlsWK8.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-C51kEF4D.js → compact-item-Cs97bz5j.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BTk9bXBn.js → createContext-BqbG60LH.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-CMje33rb.js → hasIn-i_RY0b1X.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DpcrFxMI.js → index-0-agYlWL.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BW4F5YDF.js → index-79V_zv1s.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DRG-aqOs.js → index-B-3tEpTc.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BCRvG9lq.js → index-B-JVTrpr.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DgivOKHA.js → index-B1CbOH1i.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BzCkNylB.js → index-B5UkyA3c.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-x53Vf7la.js → index-BESLFt4q.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CCLj9hcD.js → index-BOVkf1K3.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CypsfcEq.js → index-BYUVuwKt.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BtlEqHLP.js → index-BcUXAB2s.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Ld_4mFmC.js → index-BkBi5TmC.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DqU9njjT.js → index-Bm7lLR4c.js} +1 -1
  110. package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +1 -0
  111. package/src/assets/web-panel/assets/{index-CwGPwjUu.js → index-BuLQobzW.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C5IWTMVl.js → index-CE2VtTlp.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DJfnCuMq.js → index-CED2PL_y.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-hg_emBsi.js → index-CG3PRyAL.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-togc_uYn.js → index-CIG8fQ2_.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C4rQCS8T.js → index-COdGA-8n.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-0Auljb10.js → index-CeWoNHau.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Cd0hmPxD.js → index-Cf3WCLmy.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BXQBPLyE.js → index-CgsqdnRV.js} +1 -1
  120. package/src/assets/web-panel/assets/index-ChVT0p9u.js +1 -0
  121. package/src/assets/web-panel/assets/{index-h31MwrME.js → index-D2B4VPyP.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C1oZReaY.js → index-D9evtvg2.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CfC-piWZ.js → index-DUUrv141.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D9rC-fGW.js → index-DiSkx4UB.js} +3 -3
  125. package/src/assets/web-panel/assets/{index-CvSeVzqe.js → index-Dk2qouAz.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CtM8OT_7.js → index-Dk7-v4hP.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BBz9LIMS.js → index-DmbfhJUP.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BAfY7m7i.js → index-DqBgBpgp.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BVePe_Rf.js → index-PT_5sBMA.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CDLkNkLj.js → index-U4f5izHz.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BTlsE8Df.js → index-YXgLng0K.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BeEaAEIp.js → index-ZxO_-E8a.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-B0-zMtKA.js → index-dIHr4B_u.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-C1oZQmAA.js → index-gyFpvL7l.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-DRLQyp2A.js → index-s1opAL-M.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-OS-vfbTA.js → index-yObj1O4n.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-vrA8K-ac.js → initDefaultProps-C9h9yKL3.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-ByEzwIAl.js → motion-BvmLsPdu.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-Ck4AWjw-.js → move-CoL4ekU1.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-6p0l9SrB.js → mtc-parser-C4pMy6xV.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-B1CUPQ6R.js → omit-DPuJfNxx.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-B1STg1iW.js → pickAttrs-D0KEeaK2.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-DTTllkBV.js → placementArrow-BpkyINxt.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-AIrrCfBb.js → responsiveObserve-DQI_wOq8.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-C_fPMjdR.js → slide-DYfdDiRg.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-D5ow2yIP.js → statusUtils-B5dtvb0D.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-Dy3hEA--.js → styleChecker-CF310mSj.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-WfzcfSh4.js → useFlexGapSupport-D1Qp4BLp.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-CKSejfas.js → useFs-C7_iyvJK.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-od3zpPBN.js → usePersonalDataHub-DZUt_fIm.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-DRD_2DFA.js → vnode-D3dsi0lZ.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-DmNrEtgf.js → zoom-WdbOlvdQ.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +5 -5
  155. package/src/command-manifest.json +1 -1
  156. package/src/commands/agenda.js +76 -4
  157. package/src/commands/agent.js +90 -1
  158. package/src/commands/automation.js +462 -3
  159. package/src/commands/background-session.js +102 -1
  160. package/src/commands/chat.js +11 -2
  161. package/src/commands/compact.js +106 -23
  162. package/src/commands/cowork.js +6 -1
  163. package/src/commands/loop.js +187 -38
  164. package/src/commands/routine.js +12 -5
  165. package/src/commands/scheduler-daemon.js +807 -0
  166. package/src/commands/session-observability.js +305 -0
  167. package/src/commands/session.js +38 -0
  168. package/src/data/changelog.json +28 -2
  169. package/src/gateways/ws/ws-agent-handler.js +439 -5
  170. package/src/harness/jsonl-session-store.js +463 -125
  171. package/src/harness/prompt-compressor.js +86 -13
  172. package/src/harness/provider-backed-compaction.js +100 -27
  173. package/src/lib/advisor-runtime.js +3 -1
  174. package/src/lib/agent-schedule-store.js +737 -47
  175. package/src/lib/agent-team/team-adjudication.js +40 -1
  176. package/src/lib/automation-center-routines.js +363 -0
  177. package/src/lib/automation-center.js +435 -0
  178. package/src/lib/automation-engine.js +568 -19
  179. package/src/lib/automation-execution-authority.js +736 -0
  180. package/src/lib/background-agent-supervisor.js +871 -215
  181. package/src/lib/background-command-argv.js +24 -0
  182. package/src/lib/background-interaction-journal.js +361 -52
  183. package/src/lib/background-interaction-resolver.js +32 -0
  184. package/src/lib/background-launch-profile.js +2 -0
  185. package/src/lib/background-needs-input-incident.js +221 -0
  186. package/src/lib/causal-observability.js +2661 -0
  187. package/src/lib/chat-session-admission.js +76 -0
  188. package/src/lib/cost-budget.js +5 -7
  189. package/src/lib/cowork/ab-comparator-cli.js +11 -0
  190. package/src/lib/cowork/debate-review-cli.js +12 -0
  191. package/src/lib/cowork-adapter.js +159 -38
  192. package/src/lib/cowork-cron.js +673 -19
  193. package/src/lib/cowork-observe.js +5 -1
  194. package/src/lib/cowork-task-runner.js +112 -17
  195. package/src/lib/delivery-coordinator.js +142 -0
  196. package/src/lib/direct-model-usage.js +121 -0
  197. package/src/lib/file-lines.js +183 -47
  198. package/src/lib/llm-pricing.js +12 -17
  199. package/src/lib/loop.js +62 -3
  200. package/src/lib/permission-engine.js +14 -7
  201. package/src/lib/public-ide-capability-manifest.js +2 -0
  202. package/src/lib/routine-store.js +546 -86
  203. package/src/lib/runtime-usage-ledger.js +182 -0
  204. package/src/lib/scheduler-kernel/agenda-adapter.js +604 -29
  205. package/src/lib/scheduler-kernel/authority-resolver.js +163 -0
  206. package/src/lib/scheduler-kernel/automation-adapter.js +994 -0
  207. package/src/lib/scheduler-kernel/automation-event-adapter.js +757 -0
  208. package/src/lib/scheduler-kernel/contract.js +11 -3
  209. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +343 -15
  210. package/src/lib/scheduler-kernel/loop-adapter.js +913 -0
  211. package/src/lib/scheduler-kernel/migration-admin.js +346 -0
  212. package/src/lib/scheduler-kernel/migration-rollback-admin.js +932 -0
  213. package/src/lib/scheduler-kernel/routine-adapter.js +691 -22
  214. package/src/lib/scheduler-kernel/runtime.js +63 -1
  215. package/src/lib/scheduler-kernel/service.js +328 -0
  216. package/src/lib/scheduler-kernel/source-locator-path.js +142 -0
  217. package/src/lib/scheduler-kernel/store.js +3439 -206
  218. package/src/lib/session-host-lease.js +171 -2
  219. package/src/lib/session-host-snapshot.js +14 -3
  220. package/src/lib/session-persistence-failure.js +1 -0
  221. package/src/lib/session-tail.js +179 -34
  222. package/src/lib/session-transcript-structure.js +96 -0
  223. package/src/lib/sub-agent-context.js +328 -22
  224. package/src/lib/ws-agent-handler.js +4 -1
  225. package/src/repl/agent-repl.js +732 -91
  226. package/src/repl/chat-repl.js +5 -1
  227. package/src/runtime/agent-core.js +1274 -208
  228. package/src/runtime/coding-agent-contract-shared.cjs +5 -0
  229. package/src/runtime/fallback-model.js +5 -0
  230. package/src/runtime/headless-runner.js +324 -82
  231. package/src/runtime/headless-stream.js +232 -26
  232. package/src/runtime/policies/agent-policy.js +4 -0
  233. package/src/workers/background-agent-worker.js +468 -98
  234. package/src/assets/web-panel/assets/devWarning-CdwPwYj8.js +0 -1
  235. package/src/assets/web-panel/assets/index-BEeOdfEe.js +0 -1
  236. package/src/assets/web-panel/assets/index-BTe0TOu6.js +0 -1
@@ -22,7 +22,7 @@
22
22
  import fs from "fs";
23
23
  import os from "os";
24
24
  import path from "path";
25
- import { randomUUID } from "crypto";
25
+ import { createHash, randomUUID } from "crypto";
26
26
  import { isEntryExpired, effectiveFireAt } from "./schedule-planner.js";
27
27
  import { SUBAGENT_PERMISSION_MODES } from "./subagent-contract.js";
28
28
  import { parseGoalCondition } from "./goal-condition-engine.js";
@@ -30,6 +30,7 @@ import { normalizeActionClass } from "./unattended-action-policy.js";
30
30
  import { withFileLock } from "./with-file-lock.js";
31
31
 
32
32
  export const SCHEDULE_KINDS = Object.freeze(["wakeup", "cron", "monitor"]);
33
+ export const AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION = 1;
33
34
 
34
35
  /** A valid absolute-epoch-ms expiry, or null (never expires). */
35
36
  function normalizeExpiresAt(v) {
@@ -168,6 +169,87 @@ function hasLiveExecutionLease(entry, now) {
168
169
  return Number(entry.executionLease?.expiresAt) > Number(now);
169
170
  }
170
171
 
172
+ function stableJsonValue(value) {
173
+ if (Array.isArray(value)) return value.map(stableJsonValue);
174
+ if (value && typeof value === "object") {
175
+ return Object.fromEntries(
176
+ Object.keys(value)
177
+ .sort()
178
+ .map((key) => [key, stableJsonValue(value[key])]),
179
+ );
180
+ }
181
+ return value;
182
+ }
183
+
184
+ function agendaMigrationSource(entry) {
185
+ const source = { ...entry };
186
+ delete source.executionLease;
187
+ delete source.schedulerExecution;
188
+ delete source.schedulerMigration;
189
+ delete source.firedAt;
190
+ delete source.lastRunAt;
191
+ delete source.runs;
192
+ delete source.nextAt;
193
+ delete source.lastCheckAt;
194
+ delete source.checks;
195
+ delete source.lastMtimeMs;
196
+ delete source.lastEventId;
197
+ delete source.lastAuthority;
198
+ delete source.status;
199
+ return source;
200
+ }
201
+
202
+ export function agendaMigrationSourceDigest(entry) {
203
+ return createHash("sha256")
204
+ .update("chainlesschain.agenda.scheduler-migration-source.v1\0", "utf8")
205
+ .update(
206
+ JSON.stringify(stableJsonValue(agendaMigrationSource(entry))),
207
+ "utf8",
208
+ )
209
+ .digest("hex");
210
+ }
211
+
212
+ export function agendaMigrationSourceSnapshot(entry) {
213
+ return stableJsonValue(agendaMigrationSource(entry));
214
+ }
215
+
216
+ function schedulerMigrationFence(entry, now) {
217
+ const migration = entry.schedulerMigration;
218
+ if (
219
+ migration?.schemaVersion !== AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
220
+ typeof migration.retirementToken !== "string" ||
221
+ migration.retirementToken.length === 0
222
+ ) {
223
+ return false;
224
+ }
225
+ return (
226
+ hasLiveExecutionLease(entry, now) &&
227
+ entry.executionLease?.owner ===
228
+ `scheduler-migration:${migration.retirementToken}`
229
+ );
230
+ }
231
+
232
+ function applySchedulerMigrationFence(entry, now) {
233
+ const token = entry.schedulerMigration?.retirementToken;
234
+ if (typeof token !== "string" || token.length === 0) return false;
235
+ entry.executionLease = {
236
+ owner: `scheduler-migration:${token}`,
237
+ claimedAt: now,
238
+ expiresAt: Number.MAX_SAFE_INTEGER,
239
+ };
240
+ return true;
241
+ }
242
+
243
+ function normalizeMigrationField(value, field) {
244
+ if (typeof value !== "string" || value.trim().length === 0) {
245
+ throw agendaStoreError(
246
+ "AGENDA_SCHEDULER_MIGRATION_INVALID",
247
+ `Agenda scheduler migration ${field} is required`,
248
+ );
249
+ }
250
+ return value.trim();
251
+ }
252
+
171
253
  function agendaStoreError(code, message, { retryable = false, retryAt } = {}) {
172
254
  const error = new Error(message);
173
255
  error.code = code;
@@ -185,7 +267,9 @@ function applyScheduledAgentSuccess(entry, atMs) {
185
267
  if (entry.kind === "cron") {
186
268
  entry.lastRunAt = atMs;
187
269
  entry.runs = (entry.runs || 0) + 1;
188
- const next = nextCronTime(entry.cron, atMs);
270
+ const next = nextCronTime(entry.cron, atMs, {
271
+ timeZone: entry.timeZone || null,
272
+ });
189
273
  if (next == null) entry.status = "exhausted";
190
274
  else entry.nextAt = next;
191
275
  return;
@@ -196,6 +280,51 @@ function applyScheduledAgentSuccess(entry, atMs) {
196
280
  );
197
281
  }
198
282
 
283
+ function applyScheduledMonitorSuccess(entry, monitorCheck, atMs) {
284
+ if (
285
+ entry.kind !== "monitor" ||
286
+ !monitorCheck ||
287
+ typeof monitorCheck !== "object" ||
288
+ Array.isArray(monitorCheck) ||
289
+ typeof monitorCheck.matched !== "boolean"
290
+ ) {
291
+ throw agendaStoreError(
292
+ "AGENDA_SCHEDULER_MONITOR_RESULT_INVALID",
293
+ `Scheduler monitor result is invalid: ${entry.id}`,
294
+ );
295
+ }
296
+ const mtimeMs =
297
+ monitorCheck.mtimeMs === null || monitorCheck.mtimeMs === undefined
298
+ ? null
299
+ : Number(monitorCheck.mtimeMs);
300
+ if (mtimeMs !== null && (!Number.isFinite(mtimeMs) || mtimeMs < 0)) {
301
+ throw agendaStoreError(
302
+ "AGENDA_SCHEDULER_MONITOR_RESULT_INVALID",
303
+ `Scheduler monitor mtime is invalid: ${entry.id}`,
304
+ );
305
+ }
306
+ entry.checks = (entry.checks || 0) + 1;
307
+ entry.lastCheckAt = atMs;
308
+ if (mtimeMs !== null && entry.lastMtimeMs == null) {
309
+ entry.lastMtimeMs = mtimeMs;
310
+ }
311
+ if (monitorCheck.matched) {
312
+ entry.status = "matched";
313
+ entry.matchedAt = atMs;
314
+ if (monitorCheck.eventId != null) {
315
+ entry.lastEventId = String(monitorCheck.eventId);
316
+ entry.lastEventAt = atMs;
317
+ }
318
+ if (monitorCheck.authority != null) {
319
+ entry.lastAuthority = String(monitorCheck.authority);
320
+ }
321
+ } else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
322
+ entry.status = "exhausted";
323
+ } else {
324
+ entry.nextAt = atMs + entry.intervalMs;
325
+ }
326
+ }
327
+
199
328
  /** Statuses an entry can never leave — safe to prune. */
200
329
  export const TERMINAL_STATUSES = Object.freeze([
201
330
  "fired",
@@ -232,6 +361,61 @@ const CRON_RANGES = [
232
361
  [0, 6], // day of week (0/7 = Sunday)
233
362
  ];
234
363
 
364
+ const CRON_TIME_ZONE_FORMATTERS = new Map();
365
+
366
+ export function normalizeTimeZone(value) {
367
+ if (value == null || value === "") return null;
368
+ if (typeof value !== "string") {
369
+ throw new Error("cron timeZone must be an IANA time zone string");
370
+ }
371
+ const requested = value.trim();
372
+ if (!requested) return null;
373
+ if (requested.length > 128 || /\p{Cc}/u.test(requested)) {
374
+ throw new Error("invalid IANA time zone");
375
+ }
376
+ try {
377
+ return new Intl.DateTimeFormat("en-US", {
378
+ timeZone: requested,
379
+ }).resolvedOptions().timeZone;
380
+ } catch {
381
+ throw new Error(`invalid IANA time zone: ${requested}`);
382
+ }
383
+ }
384
+
385
+ function timeZoneFormatter(timeZone) {
386
+ let formatter = CRON_TIME_ZONE_FORMATTERS.get(timeZone);
387
+ if (!formatter) {
388
+ formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
389
+ timeZone,
390
+ year: "numeric",
391
+ month: "2-digit",
392
+ day: "2-digit",
393
+ hour: "2-digit",
394
+ minute: "2-digit",
395
+ hourCycle: "h23",
396
+ });
397
+ CRON_TIME_ZONE_FORMATTERS.set(timeZone, formatter);
398
+ }
399
+ return formatter;
400
+ }
401
+
402
+ function timeZoneParts(epochMs, timeZone) {
403
+ const values = {};
404
+ for (const part of timeZoneFormatter(timeZone).formatToParts(epochMs)) {
405
+ if (part.type !== "literal") values[part.type] = Number(part.value);
406
+ }
407
+ return {
408
+ year: values.year,
409
+ minute: values.minute,
410
+ hour: values.hour,
411
+ day: values.day,
412
+ month: values.month,
413
+ dayOfWeek: new Date(
414
+ Date.UTC(values.year, values.month - 1, values.day),
415
+ ).getUTCDay(),
416
+ };
417
+ }
418
+
235
419
  function parseCronField(field, [min, max]) {
236
420
  const values = new Set();
237
421
  for (const part of String(field).split(",")) {
@@ -280,13 +464,175 @@ function cronMatches(sets, date) {
280
464
  );
281
465
  }
282
466
 
467
+ function timeZoneOffsetAt(epochMs, timeZone) {
468
+ const minuteEpoch = Math.floor(epochMs / 60_000) * 60_000;
469
+ const parts = timeZoneParts(minuteEpoch, timeZone);
470
+ return (
471
+ Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute) -
472
+ minuteEpoch
473
+ );
474
+ }
475
+
476
+ function localDayOffsets(year, month, day, timeZone) {
477
+ const reference = Date.UTC(year, month - 1, day, 12, 0, 0);
478
+ return [
479
+ ...new Set(
480
+ [-36, -12, 0, 12, 36].map((hours) =>
481
+ timeZoneOffsetAt(reference + hours * 3_600_000, timeZone),
482
+ ),
483
+ ),
484
+ ];
485
+ }
486
+
487
+ function epochCandidatesForCivilMinute(
488
+ { year, month, day, hour, minute },
489
+ timeZone,
490
+ offsets,
491
+ ) {
492
+ const civilEpoch = Date.UTC(year, month - 1, day, hour, minute, 0);
493
+ const matches = [];
494
+ for (const offset of offsets) {
495
+ const epoch = civilEpoch - offset;
496
+ const actual = timeZoneParts(epoch, timeZone);
497
+ if (
498
+ actual.year === year &&
499
+ actual.month === month &&
500
+ actual.day === day &&
501
+ actual.hour === hour &&
502
+ actual.minute === minute
503
+ ) {
504
+ matches.push(epoch);
505
+ }
506
+ }
507
+ return [...new Set(matches)].sort((a, b) => a - b);
508
+ }
509
+
510
+ function nextCronTimeInTimeZone(sets, fromMs, timeZone) {
511
+ const from = Number(fromMs);
512
+ const firstLocal = timeZoneParts(from, timeZone);
513
+ const firstLocalDay = Date.UTC(
514
+ firstLocal.year,
515
+ firstLocal.month - 1,
516
+ firstLocal.day,
517
+ );
518
+ const minuteLimit = 366 * 24 * 60;
519
+ const lastEpoch = Math.floor(from / 60_000) * 60_000 + minuteLimit * 60_000;
520
+ const hours = [...sets[1]].sort((a, b) => a - b);
521
+ const minutes = [...sets[0]].sort((a, b) => a - b);
522
+ let earliest = null;
523
+
524
+ // Iterate civil dates instead of formatting every real minute. At most two
525
+ // UTC offsets can map a civil minute around a DST transition; verifying each
526
+ // reconstructed candidate preserves missing and repeated wall-time behavior.
527
+ // Start one civil day earlier because a rare midnight rollback can make a
528
+ // future real instant display as the previous local date. Once a later civil
529
+ // date is more than 36 hours beyond the best epoch, no supported IANA offset
530
+ // can map that date back before the current best candidate.
531
+ for (let dayOffset = -1; dayOffset <= 367; dayOffset += 1) {
532
+ const civilDay = firstLocalDay + dayOffset * 86_400_000;
533
+ if (earliest != null && civilDay - 36 * 3_600_000 > earliest) break;
534
+ const date = new Date(civilDay);
535
+ const year = date.getUTCFullYear();
536
+ const month = date.getUTCMonth() + 1;
537
+ const day = date.getUTCDate();
538
+ const dayOfWeek = date.getUTCDay();
539
+ if (
540
+ !sets[2].has(day) ||
541
+ !sets[3].has(month) ||
542
+ (!sets[4].has(dayOfWeek) && !(sets[4].has(7) && dayOfWeek === 0))
543
+ ) {
544
+ continue;
545
+ }
546
+ const offsets = localDayOffsets(year, month, day, timeZone);
547
+ for (const hour of hours) {
548
+ for (const minute of minutes) {
549
+ for (const epoch of epochCandidatesForCivilMinute(
550
+ { year, month, day, hour, minute },
551
+ timeZone,
552
+ offsets,
553
+ )) {
554
+ if (
555
+ epoch > from &&
556
+ epoch <= lastEpoch &&
557
+ (earliest == null || epoch < earliest)
558
+ ) {
559
+ earliest = epoch;
560
+ }
561
+ }
562
+ }
563
+ }
564
+ }
565
+ return earliest;
566
+ }
567
+
568
+ function previousCronTimeInTimeZone(sets, fromMs, timeZone) {
569
+ const from = Number(fromMs);
570
+ const firstLocal = timeZoneParts(from, timeZone);
571
+ const firstLocalDay = Date.UTC(
572
+ firstLocal.year,
573
+ firstLocal.month - 1,
574
+ firstLocal.day,
575
+ );
576
+ const minuteLimit = 366 * 24 * 60;
577
+ const firstEpoch = Math.floor(from / 60_000) * 60_000 - minuteLimit * 60_000;
578
+ const hours = [...sets[1]].sort((a, b) => b - a);
579
+ const minutes = [...sets[0]].sort((a, b) => b - a);
580
+ let latest = null;
581
+
582
+ // Start one civil day ahead because a rare midnight transition can make a
583
+ // prior real instant display as the following local date.
584
+ for (let dayOffset = 1; dayOffset >= -367; dayOffset -= 1) {
585
+ const civilDay = firstLocalDay + dayOffset * 86_400_000;
586
+ if (latest != null && civilDay + 36 * 3_600_000 < latest) break;
587
+ const date = new Date(civilDay);
588
+ const year = date.getUTCFullYear();
589
+ const month = date.getUTCMonth() + 1;
590
+ const day = date.getUTCDate();
591
+ const dayOfWeek = date.getUTCDay();
592
+ if (
593
+ !sets[2].has(day) ||
594
+ !sets[3].has(month) ||
595
+ (!sets[4].has(dayOfWeek) && !(sets[4].has(7) && dayOfWeek === 0))
596
+ ) {
597
+ continue;
598
+ }
599
+ const offsets = localDayOffsets(year, month, day, timeZone);
600
+ for (const hour of hours) {
601
+ for (const minute of minutes) {
602
+ for (const epoch of epochCandidatesForCivilMinute(
603
+ { year, month, day, hour, minute },
604
+ timeZone,
605
+ offsets,
606
+ )) {
607
+ if (
608
+ epoch < from &&
609
+ epoch >= firstEpoch &&
610
+ (latest == null || epoch > latest)
611
+ ) {
612
+ latest = epoch;
613
+ }
614
+ }
615
+ }
616
+ }
617
+ }
618
+ return latest;
619
+ }
620
+
283
621
  /**
284
- * Next epoch-ms at which `expr` fires strictly after `fromMs` (local time,
285
- * minute granularity). Scans up to ~366 days then gives up (returns null) so a
286
- * never-matching expression can't loop forever.
622
+ * Next epoch-ms at which `expr` fires strictly after `fromMs`, at minute
623
+ * granularity. By default this preserves host-local cron behavior; an explicit
624
+ * IANA `timeZone` evaluates the same epoch minutes against that civil clock.
625
+ * A repeated fall-back wall minute therefore maps to both real instants, while
626
+ * a skipped spring-forward wall minute does not exist. Scans up to ~366 days
627
+ * then gives up (returns null) so a never-matching expression can't loop
628
+ * forever.
287
629
  */
288
- export function nextCronTime(expr, fromMs) {
630
+ export function nextCronTime(expr, fromMs, options = {}) {
289
631
  const sets = parseCron(expr);
632
+ const timeZone = normalizeTimeZone(
633
+ typeof options === "string" ? options : options?.timeZone,
634
+ );
635
+ if (timeZone) return nextCronTimeInTimeZone(sets, fromMs, timeZone);
290
636
  const cursor = new Date(fromMs);
291
637
  cursor.setSeconds(0, 0);
292
638
  cursor.setMinutes(cursor.getMinutes() + 1);
@@ -298,6 +644,25 @@ export function nextCronTime(expr, fromMs) {
298
644
  return null;
299
645
  }
300
646
 
647
+ /** Previous cron occurrence strictly before `fromMs`, bounded to one year. */
648
+ export function previousCronTime(expr, fromMs, options = {}) {
649
+ const sets = parseCron(expr);
650
+ const timeZone = normalizeTimeZone(
651
+ typeof options === "string" ? options : options?.timeZone,
652
+ );
653
+ if (timeZone) return previousCronTimeInTimeZone(sets, fromMs, timeZone);
654
+ const cursor = new Date(fromMs);
655
+ cursor.setSeconds(0, 0);
656
+ if (cursor.getTime() >= Number(fromMs))
657
+ cursor.setMinutes(cursor.getMinutes() - 1);
658
+ const limit = 366 * 24 * 60;
659
+ for (let i = 0; i < limit; i += 1) {
660
+ if (cronMatches(sets, cursor)) return cursor.getTime();
661
+ cursor.setMinutes(cursor.getMinutes() - 1);
662
+ }
663
+ return null;
664
+ }
665
+
301
666
  // ─── store ───────────────────────────────────────────────────────────────
302
667
 
303
668
  export class AgentScheduleStore {
@@ -390,12 +755,16 @@ export class AgentScheduleStore {
390
755
  expiresAt = null,
391
756
  expiresInMs = null,
392
757
  jitterMs = 0,
758
+ timeZone = null,
393
759
  } = options;
394
760
  if (!prompt || typeof prompt !== "string") {
395
761
  throw new Error("cron requires a prompt");
396
762
  }
397
763
  const now = this._now();
398
- const nextAt = nextCronTime(cron, now); // validates the expression too
764
+ const normalizedTimeZone = normalizeTimeZone(timeZone);
765
+ const nextAt = nextCronTime(cron, now, {
766
+ timeZone: normalizedTimeZone,
767
+ }); // validates the expression and optional IANA time zone
399
768
  if (nextAt == null) {
400
769
  throw new Error(`cron "${cron}" has no upcoming match`);
401
770
  }
@@ -406,6 +775,7 @@ export class AgentScheduleStore {
406
775
  kind: "cron",
407
776
  prompt,
408
777
  cron,
778
+ ...(normalizedTimeZone ? { timeZone: normalizedTimeZone } : {}),
409
779
  label: label || null,
410
780
  nextAt,
411
781
  expiresAt: resolveExpiresAt(expiresAt, expiresInMs, now),
@@ -532,15 +902,246 @@ export class AgentScheduleStore {
532
902
  return this.list().find((entry) => entry.id === id) || null;
533
903
  }
534
904
 
905
+ prepareSchedulerMigration(
906
+ id,
907
+ { migrationId, sourceDigest, targetJobId } = {},
908
+ ) {
909
+ const migration = normalizeMigrationField(migrationId, "migrationId");
910
+ const expectedDigest = normalizeMigrationField(
911
+ sourceDigest,
912
+ "sourceDigest",
913
+ );
914
+ const target = normalizeMigrationField(targetJobId, "targetJobId");
915
+ this._ensureDir();
916
+ return withFileLock(
917
+ path.join(this.dir, ".agenda-claims"),
918
+ () => {
919
+ for (const kind of SCHEDULE_KINDS) {
920
+ const entries = this._readAll(kind);
921
+ const entry = entries.find((candidate) => candidate.id === id);
922
+ if (!entry) continue;
923
+ if (!isSchedulableStatus(entry)) {
924
+ throw agendaStoreError(
925
+ "AGENDA_SCHEDULER_ENTRY_TERMINAL",
926
+ `Agenda entry is not schedulable: ${id}`,
927
+ );
928
+ }
929
+ const actualDigest = agendaMigrationSourceDigest(entry);
930
+ if (actualDigest !== expectedDigest) {
931
+ throw agendaStoreError(
932
+ "AGENDA_SCHEDULER_MIGRATION_SOURCE_CHANGED",
933
+ `Agenda entry changed before migration: ${id}`,
934
+ );
935
+ }
936
+ const prior = entry.schedulerMigration;
937
+ if (prior) {
938
+ if (
939
+ prior.schemaVersion !==
940
+ AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
941
+ prior.sourceDigest !== expectedDigest ||
942
+ prior.targetJobId !== target ||
943
+ prior.migrationId !== migration ||
944
+ prior.state !== "prepared"
945
+ ) {
946
+ throw agendaStoreError(
947
+ "AGENDA_SCHEDULER_MIGRATION_CONFLICT",
948
+ `Agenda entry already belongs to another scheduler migration: ${id}`,
949
+ );
950
+ }
951
+ return entry;
952
+ }
953
+ entry.schedulerMigration = {
954
+ schemaVersion: AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION,
955
+ state: "prepared",
956
+ migrationId: migration,
957
+ sourceDigest: expectedDigest,
958
+ targetJobId: target,
959
+ preparedAt: this._now(),
960
+ };
961
+ this._writeAll(kind, entries);
962
+ return entry;
963
+ }
964
+ throw agendaStoreError(
965
+ "AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
966
+ `Agenda entry does not exist: ${id}`,
967
+ );
968
+ },
969
+ { failIfUnavailable: true },
970
+ );
971
+ }
972
+
973
+ retireForSchedulerMigration(
974
+ id,
975
+ {
976
+ migrationId,
977
+ sourceDigest,
978
+ targetJobId,
979
+ retirementToken,
980
+ atMs = null,
981
+ } = {},
982
+ ) {
983
+ const migration = normalizeMigrationField(migrationId, "migrationId");
984
+ const expectedDigest = normalizeMigrationField(
985
+ sourceDigest,
986
+ "sourceDigest",
987
+ );
988
+ const target = normalizeMigrationField(targetJobId, "targetJobId");
989
+ const token = normalizeMigrationField(retirementToken, "retirementToken");
990
+ const now = atMs != null ? Number(atMs) : this._now();
991
+ this._ensureDir();
992
+ return withFileLock(
993
+ path.join(this.dir, ".agenda-claims"),
994
+ () => {
995
+ for (const kind of SCHEDULE_KINDS) {
996
+ const entries = this._readAll(kind);
997
+ const entry = entries.find((candidate) => candidate.id === id);
998
+ if (!entry) continue;
999
+ const prior = entry.schedulerMigration;
1000
+ if (
1001
+ prior?.schemaVersion !==
1002
+ AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
1003
+ prior.sourceDigest !== expectedDigest ||
1004
+ prior.targetJobId !== target ||
1005
+ prior.migrationId !== migration ||
1006
+ !["prepared", "retired"].includes(prior.state)
1007
+ ) {
1008
+ throw agendaStoreError(
1009
+ "AGENDA_SCHEDULER_MIGRATION_CONFLICT",
1010
+ `Agenda migration preparation changed before retirement: ${id}`,
1011
+ );
1012
+ }
1013
+ if (prior.state === "retired") {
1014
+ if (
1015
+ prior.retirementToken !== token ||
1016
+ !schedulerMigrationFence(entry, now)
1017
+ ) {
1018
+ throw agendaStoreError(
1019
+ "AGENDA_SCHEDULER_MIGRATION_CONFLICT",
1020
+ `Agenda migration retirement fence changed: ${id}`,
1021
+ );
1022
+ }
1023
+ return entry;
1024
+ }
1025
+ if (agendaMigrationSourceDigest(entry) !== expectedDigest) {
1026
+ throw agendaStoreError(
1027
+ "AGENDA_SCHEDULER_MIGRATION_SOURCE_CHANGED",
1028
+ `Agenda entry changed before retirement: ${id}`,
1029
+ );
1030
+ }
1031
+ if (hasLiveExecutionLease(entry, now)) {
1032
+ throw agendaStoreError(
1033
+ "AGENDA_SCHEDULER_LEGACY_CLAIM_ACTIVE",
1034
+ `Agenda entry is already claimed by a legacy runner: ${id}`,
1035
+ {
1036
+ retryable: true,
1037
+ retryAt: Number(entry.executionLease.expiresAt),
1038
+ },
1039
+ );
1040
+ }
1041
+ entry.schedulerMigration = {
1042
+ ...prior,
1043
+ state: "retired",
1044
+ retirementToken: token,
1045
+ retiredAt: now,
1046
+ };
1047
+ applySchedulerMigrationFence(entry, now);
1048
+ this._writeAll(kind, entries);
1049
+ return entry;
1050
+ }
1051
+ throw agendaStoreError(
1052
+ "AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
1053
+ `Agenda entry does not exist: ${id}`,
1054
+ );
1055
+ },
1056
+ { failIfUnavailable: true },
1057
+ );
1058
+ }
1059
+
1060
+ restoreFromSchedulerMigration(
1061
+ id,
1062
+ { migrationId, sourceDigest, targetJobId, retirementToken } = {},
1063
+ ) {
1064
+ const migrationIdValue = normalizeMigrationField(
1065
+ migrationId,
1066
+ "migrationId",
1067
+ );
1068
+ const expectedDigest = normalizeMigrationField(
1069
+ sourceDigest,
1070
+ "sourceDigest",
1071
+ );
1072
+ const target = normalizeMigrationField(targetJobId, "targetJobId");
1073
+ const token =
1074
+ retirementToken === null || retirementToken === undefined
1075
+ ? null
1076
+ : normalizeMigrationField(retirementToken, "retirementToken");
1077
+ this._ensureDir();
1078
+ return withFileLock(
1079
+ path.join(this.dir, ".agenda-claims"),
1080
+ () => {
1081
+ for (const kind of SCHEDULE_KINDS) {
1082
+ const entries = this._readAll(kind);
1083
+ const entry = entries.find((candidate) => candidate.id === id);
1084
+ if (!entry) continue;
1085
+ const migration = entry.schedulerMigration;
1086
+ if (!migration) {
1087
+ if (
1088
+ agendaMigrationSourceDigest(entry) !== expectedDigest ||
1089
+ String(entry.executionLease?.owner || "").startsWith(
1090
+ "scheduler-migration:",
1091
+ )
1092
+ ) {
1093
+ throw agendaStoreError(
1094
+ "AGENDA_SCHEDULER_MIGRATION_CONFLICT",
1095
+ `Agenda source changed after rollback restoration: ${id}`,
1096
+ );
1097
+ }
1098
+ return entry;
1099
+ }
1100
+ if (
1101
+ migration?.schemaVersion !==
1102
+ AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
1103
+ migration.sourceDigest !== expectedDigest ||
1104
+ agendaMigrationSourceDigest(entry) !== expectedDigest ||
1105
+ migration.targetJobId !== target ||
1106
+ migration.migrationId !== migrationIdValue ||
1107
+ !["prepared", "retired"].includes(migration.state) ||
1108
+ (migration.state === "retired" &&
1109
+ (migration.retirementToken !== token ||
1110
+ entry.executionLease?.owner !== `scheduler-migration:${token}`))
1111
+ ) {
1112
+ throw agendaStoreError(
1113
+ "AGENDA_SCHEDULER_MIGRATION_CONFLICT",
1114
+ `Agenda migration fence changed before rollback: ${id}`,
1115
+ );
1116
+ }
1117
+ delete entry.schedulerMigration;
1118
+ if (migration.state === "retired") delete entry.executionLease;
1119
+ this._writeAll(kind, entries);
1120
+ return entry;
1121
+ }
1122
+ throw agendaStoreError(
1123
+ "AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
1124
+ `Agenda entry does not exist: ${id}`,
1125
+ );
1126
+ },
1127
+ { failIfUnavailable: true },
1128
+ );
1129
+ }
1130
+
535
1131
  /** Entries whose time has come (pending wakeup / active cron|monitor). */
536
- due(kind = null, atMs = null) {
1132
+ due(kind = null, atMs = null, { schedulerMigration = false } = {}) {
537
1133
  const now = atMs != null ? atMs : this._now();
538
1134
  return this.list(kind).filter((entry) => {
539
1135
  // An entry past its expiry never fires — even if it is not yet retired
540
1136
  // (defense-in-depth: retireExpired need not have run first).
541
1137
  if (isEntryExpired(entry, now)) return false;
542
1138
  if (!isSchedulableStatus(entry)) return false;
543
- if (hasLiveExecutionLease(entry, now)) return false;
1139
+ if (
1140
+ hasLiveExecutionLease(entry, now) &&
1141
+ !(schedulerMigration && schedulerMigrationFence(entry, now))
1142
+ ) {
1143
+ return false;
1144
+ }
544
1145
  // The effective fire time applies the entry's OWN deterministic jitter
545
1146
  // (`entry.jitterMs`, default 0 → base fire time, byte-identical to the
546
1147
  // prior `dueAt`/`nextAt` comparison). A jittered entry fires slightly
@@ -609,7 +1210,7 @@ export class AgentScheduleStore {
609
1210
  }
610
1211
 
611
1212
  /**
612
- * Bind a scheduler claim to the legacy Agenda entry before agent execution.
1213
+ * Bind a scheduler claim to the legacy Agenda entry before execution.
613
1214
  * The same lock used by legacy `claimDue()` prevents an old/new driver from
614
1215
  * starting the entry concurrently during migration.
615
1216
  */
@@ -640,7 +1241,7 @@ export class AgentScheduleStore {
640
1241
  return withFileLock(
641
1242
  path.join(this.dir, ".agenda-claims"),
642
1243
  () => {
643
- for (const kind of ["wakeup", "cron"]) {
1244
+ for (const kind of SCHEDULE_KINDS) {
644
1245
  const entries = this._readAll(kind);
645
1246
  const entry = entries.find((candidate) => candidate.id === id);
646
1247
  if (!entry) continue;
@@ -658,7 +1259,8 @@ export class AgentScheduleStore {
658
1259
  ) {
659
1260
  return entry;
660
1261
  }
661
- if (hasLiveExecutionLease(entry, now)) {
1262
+ const migrationFence = schedulerMigrationFence(entry, now);
1263
+ if (hasLiveExecutionLease(entry, now) && !migrationFence) {
662
1264
  throw agendaStoreError(
663
1265
  "AGENDA_SCHEDULER_LEGACY_CLAIM_ACTIVE",
664
1266
  `Agenda entry is already claimed by a legacy runner: ${id}`,
@@ -678,7 +1280,8 @@ export class AgentScheduleStore {
678
1280
  // Fence an older CLI that only understands the legacy claim field.
679
1281
  // Terminal scheduler evidence clears this lease; an outcome-unknown
680
1282
  // execution intentionally keeps it non-expiring until adjudicated so
681
- // an old runner cannot duplicate an uncertain agent side effect.
1283
+ // an old runner cannot duplicate an uncertain external observation or
1284
+ // agent side effect.
682
1285
  entry.executionLease = {
683
1286
  owner: `scheduler:${occurrenceId}:${attempt}`,
684
1287
  claimedAt: now,
@@ -697,10 +1300,10 @@ export class AgentScheduleStore {
697
1300
  }
698
1301
 
699
1302
  /**
700
- * Atomically publish scheduler evidence and the legacy wakeup/cron state.
701
- * A successful cron advances exactly once in the same JSONL replacement that
702
- * records terminal evidence; failures keep the entry schedulable for the
703
- * scheduler kernel's bounded retry policy.
1303
+ * Atomically publish scheduler evidence and the legacy Agenda state. A
1304
+ * successful cron advances and a successful monitor records/re-arms exactly
1305
+ * once in the same JSONL replacement that records terminal evidence;
1306
+ * failures keep the entry schedulable for the kernel's bounded retry policy.
704
1307
  */
705
1308
  completeSchedulerExecution(
706
1309
  id,
@@ -711,6 +1314,7 @@ export class AgentScheduleStore {
711
1314
  outcome,
712
1315
  result = null,
713
1316
  error = null,
1317
+ monitorCheck = null,
714
1318
  atMs = null,
715
1319
  } = {},
716
1320
  ) {
@@ -725,7 +1329,7 @@ export class AgentScheduleStore {
725
1329
  return withFileLock(
726
1330
  path.join(this.dir, ".agenda-claims"),
727
1331
  () => {
728
- for (const kind of ["wakeup", "cron"]) {
1332
+ for (const kind of SCHEDULE_KINDS) {
729
1333
  const entries = this._readAll(kind);
730
1334
  const entry = entries.find((candidate) => candidate.id === id);
731
1335
  if (!entry) continue;
@@ -742,16 +1346,121 @@ export class AgentScheduleStore {
742
1346
  `Agenda scheduler evidence does not match the active claim: ${id}`,
743
1347
  );
744
1348
  }
1349
+ let storedResult = result;
745
1350
  if (outcome === "succeeded") {
746
- applyScheduledAgentSuccess(entry, now);
1351
+ if (entry.kind === "monitor") {
1352
+ applyScheduledMonitorSuccess(entry, monitorCheck, now);
1353
+ storedResult =
1354
+ result && typeof result === "object" && !Array.isArray(result)
1355
+ ? { ...result, status: entry.status }
1356
+ : result;
1357
+ } else {
1358
+ applyScheduledAgentSuccess(entry, now);
1359
+ }
747
1360
  }
748
1361
  entry.schedulerExecution = {
749
1362
  ...evidence,
750
1363
  status: outcome,
751
1364
  endedAt: now,
752
- ...(outcome === "succeeded" ? { result } : { error }),
1365
+ ...(outcome === "succeeded" ? { result: storedResult } : { error }),
1366
+ };
1367
+ delete entry.executionLease;
1368
+ if (isSchedulableStatus(entry))
1369
+ applySchedulerMigrationFence(entry, now);
1370
+ this._writeAll(kind, entries);
1371
+ return entry;
1372
+ }
1373
+ throw agendaStoreError(
1374
+ "AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
1375
+ `Agenda entry does not exist: ${id}`,
1376
+ );
1377
+ },
1378
+ { failIfUnavailable: true },
1379
+ );
1380
+ }
1381
+
1382
+ /**
1383
+ * Resolve start-only scheduler evidence after an operator has verified the
1384
+ * external outcome. This is intentionally separate from normal completion:
1385
+ * it preserves the adjudication identity and can release the non-expiring
1386
+ * legacy fence without pretending that a machine observed a terminal result.
1387
+ */
1388
+ adjudicateSchedulerExecution(
1389
+ id,
1390
+ {
1391
+ occurrenceId,
1392
+ snapshotDigest,
1393
+ attempt,
1394
+ decision,
1395
+ requestId,
1396
+ atMs = null,
1397
+ } = {},
1398
+ ) {
1399
+ if (!["confirmed_applied", "confirmed_not_applied"].includes(decision)) {
1400
+ throw agendaStoreError(
1401
+ "AGENDA_SCHEDULER_ADJUDICATION_INVALID",
1402
+ "Agenda scheduler adjudication decision is invalid",
1403
+ );
1404
+ }
1405
+ const now = atMs != null ? Number(atMs) : this._now();
1406
+ this._ensureDir();
1407
+ return withFileLock(
1408
+ path.join(this.dir, ".agenda-claims"),
1409
+ () => {
1410
+ for (const kind of SCHEDULE_KINDS) {
1411
+ const entries = this._readAll(kind);
1412
+ const entry = entries.find((candidate) => candidate.id === id);
1413
+ if (!entry) continue;
1414
+ const evidence = entry.schedulerExecution;
1415
+ if (
1416
+ evidence?.occurrenceId === occurrenceId &&
1417
+ evidence.snapshotDigest === snapshotDigest &&
1418
+ evidence.adjudication?.requestId === requestId &&
1419
+ evidence.adjudication.decision === decision &&
1420
+ evidence.status ===
1421
+ (decision === "confirmed_applied"
1422
+ ? "adjudicated_applied"
1423
+ : "adjudicated_not_applied")
1424
+ ) {
1425
+ return entry;
1426
+ }
1427
+ if (
1428
+ !evidence ||
1429
+ evidence.occurrenceId !== occurrenceId ||
1430
+ evidence.snapshotDigest !== snapshotDigest ||
1431
+ !Number.isSafeInteger(evidence.attempt) ||
1432
+ evidence.attempt < 1 ||
1433
+ evidence.attempt > attempt ||
1434
+ evidence.status !== "running"
1435
+ ) {
1436
+ throw agendaStoreError(
1437
+ "AGENDA_SCHEDULER_ADJUDICATION_EVIDENCE_MISMATCH",
1438
+ `Agenda scheduler adjudication evidence does not match: ${id}`,
1439
+ );
1440
+ }
1441
+ if (decision === "confirmed_applied") {
1442
+ if (entry.kind === "monitor") {
1443
+ // A generic operator decision proves the prior check ran, but
1444
+ // not whether its observation matched. Pause the monitor rather
1445
+ // than inventing a domain result or rearming a duplicate check.
1446
+ entry.status = "adjudicated";
1447
+ entry.lastCheckAt = now;
1448
+ } else {
1449
+ applyScheduledAgentSuccess(entry, now);
1450
+ }
1451
+ }
1452
+ entry.schedulerExecution = {
1453
+ ...evidence,
1454
+ status:
1455
+ decision === "confirmed_applied"
1456
+ ? "adjudicated_applied"
1457
+ : "adjudicated_not_applied",
1458
+ endedAt: now,
1459
+ adjudication: { requestId, decision },
753
1460
  };
754
1461
  delete entry.executionLease;
1462
+ if (isSchedulableStatus(entry))
1463
+ applySchedulerMigrationFence(entry, now);
755
1464
  this._writeAll(kind, entries);
756
1465
  return entry;
757
1466
  }
@@ -841,7 +1550,9 @@ export class AgentScheduleStore {
841
1550
  return this._mutate("cron", id, (entry) => {
842
1551
  entry.lastRunAt = now;
843
1552
  entry.runs = (entry.runs || 0) + 1;
844
- const next = nextCronTime(entry.cron, now);
1553
+ const next = nextCronTime(entry.cron, now, {
1554
+ timeZone: entry.timeZone || null,
1555
+ });
845
1556
  if (next == null) {
846
1557
  entry.status = "exhausted";
847
1558
  } else {
@@ -866,32 +1577,11 @@ export class AgentScheduleStore {
866
1577
  ) {
867
1578
  const now = atMs != null ? atMs : this._now();
868
1579
  return this._mutate("monitor", id, (entry) => {
869
- entry.checks = (entry.checks || 0) + 1;
870
- entry.lastCheckAt = now;
871
- // Record the mtime baseline the first time we see the file (for a
872
- // watchChange monitor); leave it fixed thereafter so "changed" always
873
- // means "changed since we started watching".
874
- if (mtimeMs != null && entry.lastMtimeMs == null) {
875
- entry.lastMtimeMs = mtimeMs;
876
- }
877
- if (matched) {
878
- entry.status = "matched";
879
- entry.matchedAt = now;
880
- // Audit record of the firing: the deterministic monitor-event id + the
881
- // (always SYSTEM/steer) authority the event was delivered under. Durable
882
- // on the entry so `cc agenda list --json` and a future resident daemon
883
- // can recognise / dedup the exact observation. Only stamped when the
884
- // caller supplies them, so legacy callers stay byte-identical.
885
- if (eventId != null) {
886
- entry.lastEventId = String(eventId);
887
- entry.lastEventAt = now;
888
- }
889
- if (authority != null) entry.lastAuthority = String(authority);
890
- } else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
891
- entry.status = "exhausted";
892
- } else {
893
- entry.nextAt = now + entry.intervalMs;
894
- }
1580
+ applyScheduledMonitorSuccess(
1581
+ entry,
1582
+ { matched: Boolean(matched), mtimeMs, eventId, authority },
1583
+ now,
1584
+ );
895
1585
  });
896
1586
  }
897
1587