chainlesschain 0.165.9 → 0.166.0

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 (190) hide show
  1. package/completions/_cc +2 -1
  2. package/completions/cc.bash +2 -2
  3. package/completions/cc.fish +2 -2
  4. package/completions/cc.ps1 +2 -1
  5. package/package.json +2 -2
  6. package/src/assets/web-panel/assets/{AIOps-DxwjrnEA.js → AIOps-D5_pwQ7M.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-JdbYowzn.js → ActionButton-DgoYO32D.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-C06GCahv.js → Analytics-BIpoZ4QH.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-BUVEEyi1.js → AppLayout-BJ3XrCEe.js} +3 -3
  10. package/src/assets/web-panel/assets/{Artifacts-D_ORzkJd.js → Artifacts-DUFs_izZ.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-8o_Y5ejG.js → Audit-BZdPwwfL.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-DrkHd5VU.js → BackgroundAgents-DK9Fn-8B.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-CKBZCX11.js → Backup-Bd6bOhV_.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-C8HkI399.js → BaseInput-CRnWlXrd.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-zb2H2Wux.js → Chat-D1G_yEl6.js} +6 -6
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BDcNpU_F.js → ChatBubbleRenderer-DDAlEtm-.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-MwPtQmNz.js → Checkbox-CP35SH7R.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-3p5QCkFK.js → Codegen-DugeL4pj.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-BPLlhn52.js → Col-DPuJsvKg.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-C-3dBNOM.js → Community-CZqNjNNp.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-BUcy-F_b.js → Compact-eeyIPEAb.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-CI8NxT8m.js → Compliance-MGzTw87k.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-DaDEBS-Q.js → Cowork-D3goVIA5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Cron-BSv9HXu5.js → Cron-9HA8hLLG.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-yuiKlJmv.js → Crosschain-DCSOX3l6.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-D6LfldB0.js → DID-ji3lN5Bc.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-CBhLqG61.js → Dashboard-DlZvF1E5.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-BG8pIl3O.js → Dropdown-2FyZcow9.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-CY92VIE6.js → EmailListRenderer-qH2bSpSY.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-1Q_ehauM.js → FamilyGuardDashboard-Bfx47wZj.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-D3XirJIU.js → Federation-DhGcZbIv.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-DfI9UuGZ.js → FormItemContext-BMgi9_YQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-IeCZk2m7.js → GenericCardRenderer-DugMac-4.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-DzK4IJAZ.js → Git-D943BpH-.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BHf5KfmQ.js → Governance-DZ66a8Aa.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-BmaxSALS.js → Inference-B00Mp2tN.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-BgePFLA9.js → KnowledgeGraph-BdwXXO2Q.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-nqgxpOc4.js → Logs-_MVXyYWc.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-hOTeNyi7.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-Bu8to-86.js → Marketplace-CxeHRsnO.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-BFWTzEk0.js → McpTools-BOieTxZ4.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-B026G2GA.js → Memory-BQD26CM3.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-CM2Eeg6I.js → MobileBridge-CPJ8ZlvR.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-B79Ob3hy.js → MobileProjects-D7qRVu0y.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-CxevSZuV.js → Mtc-BOyndGuk.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-X396cPEX.js → MtcAudit-CXQ_8ZoR.js} +4 -4
  47. package/src/assets/web-panel/assets/{Multisig-HWIB_aZ1.js → Multisig-32Oh9KbH.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-gnYD1Xwy.js → NLProgramming-CFAd00CX.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-Dn2Gf_J1.js → Notes-BBCY_qJA.js} +3 -3
  50. package/src/assets/web-panel/assets/{NotificationSettings-BRKBQ5rq.js → NotificationSettings-uaQ0PdmW.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-Dy___Cgo.js → OrderTableRenderer-kpBxJ-SG.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-cqfccxnt.js → Organization-CmFn7Oeo.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-CSX4FHN3.js → Overflow-R_avat3a.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-Bwa4aghp.js → P2P-BIhLFmkY.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dhszyjig.js → PdhVaultBrowser-DTJjk-_i.js} +3 -3
  56. package/src/assets/web-panel/assets/{Permissions-CXzTyk6e.js → Permissions-CkzYv8Yb.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-CFCAAJbb.js → PersonalDataHub--0QvN2Nz.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-BPJbx6xt.js → Pipeline-Bfw5gQCH.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-BbiXvEHz.js → Privacy-DicjKfax.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-B-s7-wSD.js → ProjectInit-DUl_ykBp.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-Dyug206y.js → ProjectSettings-CPYDvN8a.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-BtAI5Fwo.js → Projects-DnZ9_fAr.js} +1 -1
  63. package/src/assets/web-panel/assets/{Providers-JpIPmz6N.js → Providers-BX1jP31w.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-CZdoN64A.js → QrScannerModal-COab5qUl.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BwvnoRIT.js → QuickAsk-CdKQ_JZ0.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-ASMlMQrD.js → Recommend-BPFHg8pK.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-BiJzKFKi.js → RemoteSession-CqrfTJvO.js} +3 -3
  68. package/src/assets/web-panel/assets/{Reputation-C80TWLIO.js → Reputation-DM-l8Pb3.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-C1VNmjmX.js → Row-CKGBTeCw.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-T2MyGUlG.js → RssFeed-Wb0YHIkc.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-DXM6O83L.js → Search-DiBjxKv-.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-nv69kU7W.js → Security-BAVEmbWP.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-AyHWe7Qj.js → Services-A_a3TIRn.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-D6PWLxFI.js → Skeleton-CloHJ-I7.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-BqVeOmNQ.js → Skills-CkvV5hKa.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-BegIPl9M.js → Sla-BYozxxsV.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-CKJn3xk-.js → SpeechSettings-CWianiZF.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-Th4cnAMl.js → SyncSettings-BR4G75do.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-BuKfBroh.js → Tasks-D6gyUVrM.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-CMuWN8Ac.js → Templates-DbfYIRzT.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-C4je8E8Q.js → Tenant-BHutvukT.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-agJESOcO.js → Terminal-CYEiy2wJ.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-D2qDuVZ_.js → TimelineRenderer-D9s1AxfE.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-Cvmn99rH.js → Tokens-ij9vxnoy.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-D_QYIHH8.js → Trigger-Dq_DCVeQ.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-BmzvQLnX.js → Trust-Bs6wdfoH.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-D_LopHwH.js → UkeySign-CLazyr5t.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-DoWl3PhQ.js → VideoEditing-viWotIHA.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-5NYcAkxe.js → Wallet-DlZ-A78d.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-B-_Q3ewp.js → WebAuthn-DSmOND3Y.js} +3 -3
  91. package/src/assets/web-panel/assets/{WorkflowEditor-CHET-gi2.js → WorkflowEditor-BempLOJi.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-C6nvb-n6.js → chat-CyJzf4JI.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-CTDE9LTX.js → colors-C62q7WrL.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-KnemdF8D.js → compact-item-7dYM1KW3.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-DUJR3Uek.js → createContext-CYTy7z2Q.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-BdTg_AMF.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-CFNTaby8.js → hasIn-DHoclUqm.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-65HO56Bv.js → index-2C8Xx2UO.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-ox6mIwE2.js → index-3pI7AfmD.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-JGTiDeOW.js → index-4gWv-Ubg.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-rVDD7AAR.js → index-94ktimZG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-OmuhFYKE.js → index-9uR7rc9a.js} +1 -1
  103. package/src/assets/web-panel/assets/index-B6WvFV4N.js +1 -0
  104. package/src/assets/web-panel/assets/index-BKgfptzA.js +1 -0
  105. package/src/assets/web-panel/assets/{index-CquKnw5r.js → index-BMRmxFU_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DzReoLyh.js → index-BMxvWauP.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DP-PbO11.js → index-BghdJ87W.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CSD9_I2_.js → index-Bzz0aJ86.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DF1Y0Vuw.js → index-C2efftJb.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B8RtbSkV.js → index-C5YWYlW9.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CHfUdnJH.js → index-CVTnruDt.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BwZM1rod.js → index-CW86F2Zw.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CyEtHvEE.js → index-CZRBvN6i.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Ceu92a8D.js → index-CjeViaU-.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DJyPeBBk.js → index-CnEZ4ZHj.js} +3 -3
  116. package/src/assets/web-panel/assets/{index-BdiC4LOs.js → index-D6BC4mmz.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-k6uorYmf.js → index-DA-_807C.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DqPpJkte.js → index-DAYanuSk.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-NtAcnJYW.js → index-DHgSRimK.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Wlc7o_C8.js → index-DJzZaPlb.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CKvBF3Ov.js → index-DK84lAS0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CcF0D7UZ.js → index-DKdv8cju.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CnLpuTAU.js → index-DNLD3nZY.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DZgCRMxk.js → index-DOFAwpUK.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-v-mtxmWH.js → index-DYKwGrAn.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cc7dbl_p.js → index-DghnbMlY.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BuizJo2E.js → index-DhJjifZe.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-NUR0If7O.js → index-DkczsbiP.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-u0h6QqVV.js → index-DvEtHpLc.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-bxTPfdri.js → index-Dyq9tK5u.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DfJzS3Qa.js → index-LK1MZoBj.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DF-fcTvQ.js → index-NuNCstRO.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BXWg1crv.js → index-QXtrdrub.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BTetEUWd.js → index-XrsAHHQN.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-BHqSqnW7.js → index-ZXYp76IB.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-C96N9xeL.js → index-lHfNxiB5.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-qEt8dI-t.js → initDefaultProps-CwCl5sFQ.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-DetKPWy-.js → motion-BFJFy70N.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-BeNYeDV2.js → move-ByrtVGq8.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-C0q0MK_T.js → mtc-parser-BFiH1mT3.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-DLKkrKpE.js → omit-bdvgpivj.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-Bw1tWsY8.js → pickAttrs-BlfuuE3s.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-DeesBLbd.js → placementArrow-t2SC3DMy.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-bqLhxHhn.js → responsiveObserve-CoEXv2Li.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-DGCXRoQm.js → slide-BfXCf0Fu.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-H-re3Egl.js → statusUtils-DNsdyOuS.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-BXg4qGX_.js → styleChecker-9aHukmoG.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-CKLb2KA6.js → useFlexGapSupport-dTkPxzHN.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-CTAhHeyw.js → useFs-DINwfItP.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BQ4pDh5g.js → usePersonalDataHub-Bj_RriBV.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-jrSK6j4j.js → vnode-BzRAZrTc.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-fOQE24pw.js → zoom-CamPjkeK.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +4 -4
  155. package/src/command-manifest.json +1 -1
  156. package/src/commands/agent.js +1 -1
  157. package/src/commands/graph.js +100 -0
  158. package/src/commands/mcp.js +49 -21
  159. package/src/commands/orchestrate.js +85 -18
  160. package/src/commands/serve.js +39 -0
  161. package/src/commands/team.js +2 -0
  162. package/src/data/changelog.json +26 -0
  163. package/src/generated/cc-agent-protocol.schema.json +1964 -0
  164. package/src/lib/agent-sandbox.js +4 -7
  165. package/src/lib/app-server/bounded-queue.js +87 -0
  166. package/src/lib/app-server/cli-agent-kernel-adapter.js +309 -0
  167. package/src/lib/app-server/protocol.js +125 -0
  168. package/src/lib/app-server/rollout-store.js +603 -0
  169. package/src/lib/app-server/server.js +922 -0
  170. package/src/lib/app-server/sqlite-rollout-store.js +315 -0
  171. package/src/lib/app-server/stdio-transport.js +118 -0
  172. package/src/lib/codex-app-server-adapter.js +249 -0
  173. package/src/lib/graph-kernel/adapters.js +238 -0
  174. package/src/lib/graph-kernel/compiler.js +911 -0
  175. package/src/lib/graph-kernel/eval.js +207 -0
  176. package/src/lib/graph-kernel/event-store.js +101 -0
  177. package/src/lib/graph-kernel/index.js +7 -0
  178. package/src/lib/graph-kernel/runtime.js +2660 -0
  179. package/src/lib/graph-kernel/trace-reducer.js +315 -0
  180. package/src/lib/graph-kernel/trigger-adapter.js +183 -0
  181. package/src/lib/orchestrator.js +2 -1
  182. package/src/lib/record-replay/index.js +1 -0
  183. package/src/lib/record-replay/skill-recorder.js +386 -0
  184. package/src/lib/webhook-security.js +159 -0
  185. package/src/repl/agent-repl.js +5 -4
  186. package/src/repl/prompt-interactions.js +11 -1
  187. package/src/assets/web-panel/assets/MarkdownRenderer-xkX6izBt.js +0 -1
  188. package/src/assets/web-panel/assets/devWarning-1DqLH1KX.js +0 -1
  189. package/src/assets/web-panel/assets/index-BhJtcPWo.js +0 -1
  190. package/src/assets/web-panel/assets/index-Cq8MWPDT.js +0 -1
@@ -0,0 +1,2660 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ assertCompiledGraph,
4
+ compileGraphDefinition,
5
+ graphDigest,
6
+ writeScopesOverlap,
7
+ } from "./compiler.js";
8
+ import { GraphEventStore } from "./event-store.js";
9
+
10
+ export const GRAPH_RUN_PHASES = Object.freeze(["open", "sealed"]);
11
+ export const GRAPH_RUN_STATUSES = Object.freeze([
12
+ "running",
13
+ "waiting_input",
14
+ "waiting_external",
15
+ "waiting_human",
16
+ "reconciliation_required",
17
+ "succeeded",
18
+ "failed",
19
+ "partial",
20
+ "cancelled",
21
+ "blocked",
22
+ "deadlocked",
23
+ "budget_exhausted",
24
+ ]);
25
+ export const GRAPH_NODE_STATUSES = Object.freeze([
26
+ "pending",
27
+ "running",
28
+ "waiting_human",
29
+ "reconciliation_required",
30
+ "succeeded",
31
+ "failed",
32
+ "skipped",
33
+ "upstream_failed",
34
+ "cancelled",
35
+ "blocked",
36
+ "timed_out",
37
+ "budget_exhausted",
38
+ ]);
39
+
40
+ const TERMINAL_RUN = new Set([
41
+ "succeeded",
42
+ "failed",
43
+ "partial",
44
+ "cancelled",
45
+ "blocked",
46
+ "deadlocked",
47
+ "budget_exhausted",
48
+ ]);
49
+ const TERMINAL_NODE = new Set([
50
+ "succeeded",
51
+ "failed",
52
+ "skipped",
53
+ "upstream_failed",
54
+ "cancelled",
55
+ "blocked",
56
+ "timed_out",
57
+ "budget_exhausted",
58
+ ]);
59
+ const SUCCESS_NODE = new Set(["succeeded"]);
60
+ const FAILURE_NODE = new Set([
61
+ "failed",
62
+ "upstream_failed",
63
+ "blocked",
64
+ "timed_out",
65
+ "budget_exhausted",
66
+ ]);
67
+ const ACTIVE_ATTEMPT = new Set(["active", "waiting_human", "cancelling"]);
68
+ const SENSITIVITY_RANK = Object.freeze({
69
+ public: 0,
70
+ internal: 1,
71
+ confidential: 2,
72
+ secret: 3,
73
+ });
74
+ const TRUST_RANK = Object.freeze({
75
+ unknown: 0,
76
+ untrusted_content: 1,
77
+ authenticated_user: 2,
78
+ trusted_host: 3,
79
+ });
80
+
81
+ function kernelError(code, message, details = {}) {
82
+ const error = new Error(message);
83
+ error.name = "GraphKernelError";
84
+ error.code = code;
85
+ Object.assign(error, details);
86
+ return error;
87
+ }
88
+
89
+ function clone(value) {
90
+ return value == null ? value : JSON.parse(JSON.stringify(value));
91
+ }
92
+
93
+ function mapSnapshot(map) {
94
+ return [...map.entries()].map(([key, value]) => [key, clone(value)]);
95
+ }
96
+
97
+ function mapRestore(entries) {
98
+ return new Map((entries || []).map(([key, value]) => [key, clone(value)]));
99
+ }
100
+
101
+ function finitePositive(value, fallback) {
102
+ const number = Number(value);
103
+ return Number.isFinite(number) && number > 0 ? number : fallback;
104
+ }
105
+
106
+ function safeIdentifier(value, label = "identifier") {
107
+ const id = String(value || "").trim();
108
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/u.test(id)) {
109
+ throw kernelError("CC_GRAPH_INVALID_ARGUMENT", `${label} is invalid`);
110
+ }
111
+ return id;
112
+ }
113
+
114
+ function nowIso(now) {
115
+ return new Date(now()).toISOString();
116
+ }
117
+
118
+ function attemptIsLive(attempt, now) {
119
+ return (
120
+ ACTIVE_ATTEMPT.has(attempt.status) && Number(attempt.expiresAtMs) > now
121
+ );
122
+ }
123
+
124
+ function producerIsLive(lease, now) {
125
+ return lease.status === "active" && Number(lease.expiresAtMs) > now;
126
+ }
127
+
128
+ function hasTerminalEvidence(evidence) {
129
+ return Boolean(
130
+ evidence &&
131
+ (evidence.outputDigest ||
132
+ evidence.commit ||
133
+ (Array.isArray(evidence.artifactIds) && evidence.artifactIds.length) ||
134
+ (Array.isArray(evidence.testReceiptIds) &&
135
+ evidence.testReceiptIds.length)),
136
+ );
137
+ }
138
+
139
+ function dependencySatisfied(policy, status) {
140
+ if (policy === "always") return TERMINAL_NODE.has(status);
141
+ if (policy === "failure") return FAILURE_NODE.has(status);
142
+ if (policy === "timeout") return status === "timed_out";
143
+ if (policy === "cancel") return status === "cancelled";
144
+ return SUCCESS_NODE.has(status);
145
+ }
146
+
147
+ function dependencyMismatchStatus(policy, status) {
148
+ if (policy === "success" && FAILURE_NODE.has(status)) {
149
+ return "upstream_failed";
150
+ }
151
+ if (policy === "success" && status === "cancelled") return "cancelled";
152
+ return "skipped";
153
+ }
154
+
155
+ function validateDataPolicy(policy) {
156
+ if (
157
+ !policy ||
158
+ typeof policy.origin !== "string" ||
159
+ !Object.hasOwn(TRUST_RANK, policy.trust) ||
160
+ !Object.hasOwn(SENSITIVITY_RANK, policy.sensitivity) ||
161
+ !Array.isArray(policy.allowedSinks)
162
+ ) {
163
+ throw kernelError(
164
+ "CC_GRAPH_DATA_POLICY_REQUIRED",
165
+ "origin/trust/sensitivity/allowedSinks must be assigned at a trusted boundary",
166
+ );
167
+ }
168
+ return clone(policy);
169
+ }
170
+
171
+ function sinkAllowed(policy, sink) {
172
+ return policy.allowedSinks.some(
173
+ (allowed) =>
174
+ allowed === "*" ||
175
+ allowed === sink ||
176
+ (allowed.endsWith("*") && sink.startsWith(allowed.slice(0, -1))),
177
+ );
178
+ }
179
+
180
+ function containsAuthorityPayload(value, state = { nodes: 0 }) {
181
+ state.nodes += 1;
182
+ if (state.nodes > 10_000) return true;
183
+ if (Array.isArray(value)) {
184
+ return value.some((item) => containsAuthorityPayload(item, state));
185
+ }
186
+ if (!value || typeof value !== "object") return false;
187
+ for (const [key, child] of Object.entries(value)) {
188
+ if (
189
+ [
190
+ "approval",
191
+ "capability",
192
+ "permission",
193
+ "controlEdge",
194
+ "authority",
195
+ ].includes(key)
196
+ ) {
197
+ return true;
198
+ }
199
+ if (containsAuthorityPayload(child, state)) return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ export function deriveDataPolicy(policies, { declassification = null } = {}) {
205
+ const values = policies.map(validateDataPolicy);
206
+ if (!values.length) {
207
+ throw kernelError(
208
+ "CC_GRAPH_DATA_POLICY_REQUIRED",
209
+ "at least one source data policy is required",
210
+ );
211
+ }
212
+ const trust = values.reduce(
213
+ (lowest, policy) =>
214
+ TRUST_RANK[policy.trust] < TRUST_RANK[lowest] ? policy.trust : lowest,
215
+ values[0].trust,
216
+ );
217
+ const sensitivity = values.reduce(
218
+ (highest, policy) =>
219
+ SENSITIVITY_RANK[policy.sensitivity] > SENSITIVITY_RANK[highest]
220
+ ? policy.sensitivity
221
+ : highest,
222
+ values[0].sensitivity,
223
+ );
224
+ const allowedSinks = values
225
+ .map((policy) => new Set(policy.allowedSinks))
226
+ .reduce(
227
+ (intersection, current) =>
228
+ new Set([...intersection].filter((sink) => current.has(sink))),
229
+ );
230
+ const output = {
231
+ origin: `derived:${graphDigest(values, "cc.graph.data-origin/v1")}`,
232
+ trust,
233
+ sensitivity,
234
+ allowedSinks: [...allowedSinks].sort(),
235
+ };
236
+ if (declassification) {
237
+ if (
238
+ typeof declassification.decisionId !== "string" ||
239
+ !Array.isArray(declassification.allowedSinks)
240
+ ) {
241
+ throw kernelError(
242
+ "CC_GRAPH_DECLASSIFICATION_INVALID",
243
+ "declassification requires an audited decision and explicit sinks",
244
+ );
245
+ }
246
+ output.allowedSinks = [...new Set(declassification.allowedSinks)].sort();
247
+ output.declassificationDecisionId = declassification.decisionId;
248
+ }
249
+ return Object.freeze(output);
250
+ }
251
+
252
+ function stateSnapshot(run) {
253
+ return {
254
+ version: 1,
255
+ id: run.id,
256
+ definition: run.compiled.definition,
257
+ phase: run.phase,
258
+ status: run.status,
259
+ graphRevision: run.graphRevision,
260
+ revisionDigest: run.revisionDigest,
261
+ occurrenceRef: run.occurrenceRef,
262
+ authorityDigest: run.authorityDigest,
263
+ budget: run.budget,
264
+ budgetUsed: run.budgetUsed,
265
+ createdAt: run.createdAt,
266
+ completedAt: run.completedAt,
267
+ cancellationRequested: run.cancellationRequested,
268
+ progressEpoch: run.progressEpoch,
269
+ nodeStates: mapSnapshot(run.nodeStates),
270
+ attempts: mapSnapshot(run.attempts),
271
+ agents: mapSnapshot(run.agents),
272
+ producerLeases: mapSnapshot(run.producerLeases),
273
+ artifacts: mapSnapshot(run.artifacts),
274
+ effects: mapSnapshot(run.effects),
275
+ messages: mapSnapshot(run.messages),
276
+ messageConsumers: mapSnapshot(run.messageConsumers),
277
+ handoffs: mapSnapshot(run.handoffs),
278
+ humanTasks: mapSnapshot(run.humanTasks),
279
+ waitReasons: mapSnapshot(run.waitReasons),
280
+ timers: mapSnapshot(run.timers),
281
+ requestCache: mapSnapshot(run.requestCache),
282
+ progressDigests: mapSnapshot(run.progressDigests),
283
+ fenceCounters: mapSnapshot(run.fenceCounters),
284
+ };
285
+ }
286
+
287
+ function restoreState(run, snapshot, compiled = null) {
288
+ run.compiled = compiled || compileGraphDefinition(snapshot.definition);
289
+ run.phase = snapshot.phase;
290
+ run.status = snapshot.status;
291
+ run.graphRevision = snapshot.graphRevision;
292
+ run.revisionDigest = snapshot.revisionDigest;
293
+ run.occurrenceRef = clone(snapshot.occurrenceRef);
294
+ run.authorityDigest = snapshot.authorityDigest;
295
+ run.budget = clone(snapshot.budget);
296
+ run.budgetUsed = clone(snapshot.budgetUsed);
297
+ run.createdAt = snapshot.createdAt;
298
+ run.completedAt = snapshot.completedAt;
299
+ run.cancellationRequested = snapshot.cancellationRequested === true;
300
+ run.progressEpoch = snapshot.progressEpoch;
301
+ for (const field of [
302
+ "nodeStates",
303
+ "attempts",
304
+ "agents",
305
+ "producerLeases",
306
+ "artifacts",
307
+ "effects",
308
+ "messages",
309
+ "messageConsumers",
310
+ "handoffs",
311
+ "humanTasks",
312
+ "waitReasons",
313
+ "timers",
314
+ "requestCache",
315
+ "progressDigests",
316
+ "fenceCounters",
317
+ ]) {
318
+ run[field] = mapRestore(snapshot[field]);
319
+ }
320
+ return run;
321
+ }
322
+
323
+ function nodeProjection(run, nodeId) {
324
+ const state = run.nodeStates.get(nodeId);
325
+ return Object.freeze({
326
+ nodeId,
327
+ status: state.status,
328
+ attemptIds: Object.freeze([...state.attemptIds]),
329
+ acceptedAttemptId: state.acceptedAttemptId,
330
+ blockedRoot: state.blockedRoot,
331
+ createdAt: state.createdAt,
332
+ updatedAt: state.updatedAt,
333
+ });
334
+ }
335
+
336
+ function attemptProjection(attempt) {
337
+ return Object.freeze(clone(attempt));
338
+ }
339
+
340
+ function runProjection(run) {
341
+ return Object.freeze({
342
+ id: run.id,
343
+ definitionId: run.compiled.definitionId,
344
+ revision: run.compiled.revision,
345
+ revisionDigest: run.revisionDigest,
346
+ occurrenceRef: clone(run.occurrenceRef),
347
+ phase: run.phase,
348
+ status: run.status,
349
+ graphRevision: run.graphRevision,
350
+ authorityDigest: run.authorityDigest,
351
+ budget: clone(run.budget),
352
+ budgetUsed: clone(run.budgetUsed),
353
+ createdAt: run.createdAt,
354
+ completedAt: run.completedAt,
355
+ progressEpoch: run.progressEpoch,
356
+ nodes: Object.freeze(
357
+ run.compiled.topologicalOrder.map((id) => nodeProjection(run, id)),
358
+ ),
359
+ attempts: Object.freeze(
360
+ [...run.attempts.values()]
361
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
362
+ .map(attemptProjection),
363
+ ),
364
+ artifactIds: Object.freeze([...run.artifacts.keys()].sort()),
365
+ effectIds: Object.freeze([...run.effects.keys()].sort()),
366
+ reconciliationEffectIds: Object.freeze(
367
+ [...run.effects.values()]
368
+ .filter((effect) => effect.status === "unknown")
369
+ .map((effect) => effect.id)
370
+ .sort(),
371
+ ),
372
+ pendingHumanTaskIds: Object.freeze(
373
+ [...run.humanTasks.values()]
374
+ .filter((task) => ["open", "claimed"].includes(task.status))
375
+ .map((task) => task.id)
376
+ .sort(),
377
+ ),
378
+ });
379
+ }
380
+
381
+ function buildRun(compiled, options, now) {
382
+ const createdAt = nowIso(now);
383
+ const run = {
384
+ id: safeIdentifier(options.runId || randomUUID(), "runId"),
385
+ compiled,
386
+ phase: "open",
387
+ status: "running",
388
+ graphRevision: compiled.revision,
389
+ revisionDigest: compiled.revisionDigest,
390
+ occurrenceRef: clone(options.occurrenceRef || null),
391
+ authorityDigest:
392
+ options.authorityDigest ||
393
+ graphDigest(
394
+ {
395
+ definition: compiled.revisionDigest,
396
+ capabilities: compiled.definition.allowedCapabilities,
397
+ workspace: options.workspaceDigest || null,
398
+ },
399
+ "cc.graph.authority/v1",
400
+ ),
401
+ budget: clone(options.budget || compiled.definition.budget || {}),
402
+ budgetUsed: {
403
+ turns: 0,
404
+ tokens: 0,
405
+ costUsd: 0,
406
+ wallMs: 0,
407
+ spawnCount: 0,
408
+ },
409
+ createdAt,
410
+ completedAt: null,
411
+ cancellationRequested: false,
412
+ progressEpoch: 0,
413
+ nodeStates: new Map(),
414
+ attempts: new Map(),
415
+ agents: new Map(),
416
+ producerLeases: new Map(),
417
+ artifacts: new Map(),
418
+ effects: new Map(),
419
+ messages: new Map(),
420
+ messageConsumers: new Map(),
421
+ handoffs: new Map(),
422
+ humanTasks: new Map(),
423
+ waitReasons: new Map(),
424
+ timers: new Map(),
425
+ requestCache: new Map(),
426
+ progressDigests: new Map(),
427
+ fenceCounters: new Map(),
428
+ lastEvent: null,
429
+ };
430
+ for (const nodeId of compiled.topologicalOrder) {
431
+ run.nodeStates.set(nodeId, {
432
+ status: "pending",
433
+ attemptIds: [],
434
+ acceptedAttemptId: null,
435
+ blockedRoot: null,
436
+ createdAt,
437
+ updatedAt: createdAt,
438
+ });
439
+ run.fenceCounters.set(nodeId, 0);
440
+ }
441
+ return run;
442
+ }
443
+
444
+ function eventDetails(value) {
445
+ if (value === undefined) return null;
446
+ return clone(value);
447
+ }
448
+
449
+ function checkBudget(run, usage) {
450
+ for (const field of ["turns", "tokens", "costUsd", "wallMs", "spawnCount"]) {
451
+ const delta = Number(usage?.[field] || 0);
452
+ if (!Number.isFinite(delta) || delta < 0) {
453
+ throw kernelError(
454
+ "CC_GRAPH_USAGE_INVALID",
455
+ `usage.${field} must be a non-negative finite number`,
456
+ );
457
+ }
458
+ const cap = run.budget?.[field];
459
+ if (cap != null && run.budgetUsed[field] + delta > cap) {
460
+ throw kernelError(
461
+ "CC_GRAPH_BUDGET_EXHAUSTED",
462
+ `GraphRun ${field} budget would be exceeded`,
463
+ { field, cap, used: run.budgetUsed[field], requested: delta },
464
+ );
465
+ }
466
+ }
467
+ }
468
+
469
+ function addUsage(run, usage) {
470
+ checkBudget(run, usage);
471
+ for (const field of Object.keys(run.budgetUsed)) {
472
+ run.budgetUsed[field] += Number(usage?.[field] || 0);
473
+ }
474
+ }
475
+
476
+ function nodeReadiness(run, nodeId) {
477
+ const node = run.compiled.nodes[nodeId];
478
+ const dependencies = run.compiled.dependencies[nodeId] || [];
479
+ if (!dependencies.length) return { ready: true };
480
+ const policies = run.compiled.dependencyPolicies[nodeId] || {};
481
+ const observations = dependencies.map((dependency) => {
482
+ const status = run.nodeStates.get(dependency).status;
483
+ const policy = policies[dependency] || "success";
484
+ return {
485
+ dependency,
486
+ status,
487
+ policy,
488
+ terminal: TERMINAL_NODE.has(status),
489
+ satisfied: dependencySatisfied(policy, status),
490
+ };
491
+ });
492
+ const satisfied = observations.filter((entry) => entry.satisfied).length;
493
+ const possible = observations.filter(
494
+ (entry) => entry.satisfied || !entry.terminal,
495
+ ).length;
496
+ const join = node.join || "all";
497
+ const threshold =
498
+ join === "any" || join === "race"
499
+ ? 1
500
+ : join === "quorum"
501
+ ? Math.min(node.quorum || dependencies.length, dependencies.length)
502
+ : dependencies.length;
503
+ if (satisfied >= threshold) return { ready: true, observations };
504
+ if (possible >= threshold)
505
+ return { ready: false, waiting: true, observations };
506
+ const mismatch = observations.find(
507
+ (entry) => entry.terminal && !entry.satisfied,
508
+ );
509
+ return {
510
+ ready: false,
511
+ waiting: false,
512
+ terminalStatus: dependencyMismatchStatus(
513
+ mismatch?.policy || "success",
514
+ mismatch?.status || "blocked",
515
+ ),
516
+ blockedRoot: mismatch?.dependency || null,
517
+ observations,
518
+ };
519
+ }
520
+
521
+ function propagate(run, now) {
522
+ let changed = false;
523
+ for (;;) {
524
+ let passChanged = false;
525
+ for (const nodeId of run.compiled.topologicalOrder) {
526
+ const state = run.nodeStates.get(nodeId);
527
+ if (state.status !== "pending") continue;
528
+ const readiness = nodeReadiness(run, nodeId);
529
+ if (!readiness.terminalStatus) continue;
530
+ state.status = readiness.terminalStatus;
531
+ state.blockedRoot = readiness.blockedRoot;
532
+ state.updatedAt = nowIso(now);
533
+ passChanged = true;
534
+ changed = true;
535
+ }
536
+ if (!passChanged) break;
537
+ }
538
+ return changed;
539
+ }
540
+
541
+ function effectivePriority(run, nodeId, now, agingWindowMs) {
542
+ const node = run.compiled.nodes[nodeId];
543
+ const state = run.nodeStates.get(nodeId);
544
+ const base = Number(node.priority || 0);
545
+ const ageMs = Math.max(0, now - Date.parse(state.createdAt));
546
+ const aging = Math.min(1000, Math.floor(ageMs / agingWindowMs));
547
+ let donation = base;
548
+ for (const descendantId of run.compiled.descendants[nodeId] || []) {
549
+ const descendantState = run.nodeStates.get(descendantId);
550
+ if (!TERMINAL_NODE.has(descendantState.status)) {
551
+ donation = Math.max(
552
+ donation,
553
+ Number(run.compiled.nodes[descendantId].priority || 0),
554
+ );
555
+ }
556
+ }
557
+ const criticalPathBoost = (run.compiled.descendants[nodeId] || []).filter(
558
+ (id) => !TERMINAL_NODE.has(run.nodeStates.get(id).status),
559
+ ).length;
560
+ return {
561
+ base,
562
+ donation: Math.max(0, donation - base),
563
+ aging,
564
+ criticalPathBoost,
565
+ total: Math.max(base, donation) + aging + criticalPathBoost,
566
+ queueWaitMs: ageMs,
567
+ };
568
+ }
569
+
570
+ function activeAttemptsForAgent(run, agentId, now) {
571
+ return [...run.attempts.values()].filter(
572
+ (attempt) => attempt.agentId === agentId && attemptIsLive(attempt, now),
573
+ );
574
+ }
575
+
576
+ function activeWriteConflict(run, node, now) {
577
+ if (node.effectClass !== "workspace_write") return null;
578
+ if (node.workspaceIsolation === "worktree") return null;
579
+ for (const attempt of run.attempts.values()) {
580
+ if (!attemptIsLive(attempt, now) || attempt.role !== "executor") continue;
581
+ const activeNode = run.compiled.nodes[attempt.nodeId];
582
+ if (!activeNode || activeNode.effectClass !== "workspace_write") continue;
583
+ if (
584
+ (node.writeSet || []).some((left) =>
585
+ (activeNode.writeSet || []).some((right) =>
586
+ writeScopesOverlap(left, right),
587
+ ),
588
+ )
589
+ ) {
590
+ return attempt;
591
+ }
592
+ }
593
+ return null;
594
+ }
595
+
596
+ function sweepExpirations(run, now) {
597
+ let changed = false;
598
+ for (const lease of run.producerLeases.values()) {
599
+ if (lease.status === "active" && lease.expiresAtMs <= now) {
600
+ lease.status = "expired";
601
+ lease.updatedAt = nowIso(() => now);
602
+ changed = true;
603
+ }
604
+ }
605
+ for (const attempt of run.attempts.values()) {
606
+ if (ACTIVE_ATTEMPT.has(attempt.status) && attempt.expiresAtMs <= now) {
607
+ attempt.status = "expired";
608
+ attempt.updatedAt = nowIso(() => now);
609
+ const nodeState = run.nodeStates.get(attempt.nodeId);
610
+ if (
611
+ nodeState.status === "running" &&
612
+ nodeState.acceptedAttemptId == null
613
+ ) {
614
+ nodeState.status = "pending";
615
+ nodeState.updatedAt = nowIso(() => now);
616
+ }
617
+ changed = true;
618
+ }
619
+ }
620
+ for (const handoff of run.handoffs.values()) {
621
+ if (
622
+ ["offered", "accepted"].includes(handoff.status) &&
623
+ handoff.expiresAtMs <= now
624
+ ) {
625
+ handoff.status = "expired";
626
+ handoff.updatedAt = nowIso(() => now);
627
+ changed = true;
628
+ }
629
+ }
630
+ for (const task of run.humanTasks.values()) {
631
+ if (["open", "claimed"].includes(task.status) && task.expiresAtMs <= now) {
632
+ task.status = "expired";
633
+ task.updatedAt = nowIso(() => now);
634
+ const state = run.nodeStates.get(task.nodeId);
635
+ state.status = "failed";
636
+ state.blockedRoot = task.id;
637
+ state.updatedAt = nowIso(() => now);
638
+ changed = true;
639
+ } else if (task.status === "claimed" && task.claimExpiresAtMs <= now) {
640
+ task.status = "open";
641
+ task.claimActorId = null;
642
+ task.claimLeaseId = null;
643
+ task.claimExpiresAtMs = null;
644
+ task.updatedAt = nowIso(() => now);
645
+ changed = true;
646
+ }
647
+ }
648
+ for (const timer of run.timers.values()) {
649
+ if (timer.status === "pending" && timer.dueAtMs <= now) {
650
+ timer.status = "ready";
651
+ timer.updatedAt = nowIso(() => now);
652
+ changed = true;
653
+ }
654
+ }
655
+ return changed;
656
+ }
657
+
658
+ function waitGraphCycles(run) {
659
+ const adjacency = new Map();
660
+ for (const [nodeId, reason] of run.waitReasons) {
661
+ if (reason.ownerNodeId) {
662
+ const values = adjacency.get(nodeId) || [];
663
+ values.push(reason.ownerNodeId);
664
+ adjacency.set(nodeId, values);
665
+ }
666
+ }
667
+ let index = 0;
668
+ const stack = [];
669
+ const state = new Map();
670
+ const components = [];
671
+ const visit = (node) => {
672
+ const entry = { index, low: index, onStack: true };
673
+ index += 1;
674
+ state.set(node, entry);
675
+ stack.push(node);
676
+ for (const target of adjacency.get(node) || []) {
677
+ if (!state.has(target)) {
678
+ visit(target);
679
+ entry.low = Math.min(entry.low, state.get(target).low);
680
+ } else if (state.get(target).onStack) {
681
+ entry.low = Math.min(entry.low, state.get(target).index);
682
+ }
683
+ }
684
+ if (entry.low === entry.index) {
685
+ const component = [];
686
+ for (;;) {
687
+ const value = stack.pop();
688
+ state.get(value).onStack = false;
689
+ component.push(value);
690
+ if (value === node) break;
691
+ }
692
+ if (
693
+ component.length > 1 ||
694
+ (component.length === 1 &&
695
+ (adjacency.get(component[0]) || []).includes(component[0]))
696
+ ) {
697
+ components.push(component.sort());
698
+ }
699
+ }
700
+ };
701
+ for (const node of adjacency.keys()) if (!state.has(node)) visit(node);
702
+ return components.sort((left, right) => left[0].localeCompare(right[0]));
703
+ }
704
+
705
+ function terminalAlgebra(run) {
706
+ const statuses = [...run.nodeStates.values()].map((state) => state.status);
707
+ const successes = statuses.filter((status) => status === "succeeded").length;
708
+ const failures = statuses.filter((status) => FAILURE_NODE.has(status)).length;
709
+ const cancelled = statuses.filter((status) => status === "cancelled").length;
710
+ const skipped = statuses.filter((status) => status === "skipped").length;
711
+ if (statuses.every((status) => status === "cancelled")) return "cancelled";
712
+ if (statuses.every((status) => status === "skipped")) return "blocked";
713
+ if (failures > 0 && successes > 0) return "partial";
714
+ if (failures > 0) return "failed";
715
+ if (cancelled > 0 && successes > 0) return "partial";
716
+ if (cancelled > 0) return "cancelled";
717
+ if (successes > 0 || skipped > 0) return "succeeded";
718
+ return "blocked";
719
+ }
720
+
721
+ function classifyQuiescence(run, now) {
722
+ if (TERMINAL_RUN.has(run.status)) return run.status;
723
+ if (run.phase === "open") return "running";
724
+ if (
725
+ [...run.producerLeases.values()].some((lease) => producerIsLive(lease, now))
726
+ ) {
727
+ return "running";
728
+ }
729
+ if (
730
+ [...run.attempts.values()].some((attempt) =>
731
+ ACTIVE_ATTEMPT.has(attempt.status),
732
+ )
733
+ ) {
734
+ return "running";
735
+ }
736
+ if ([...run.effects.values()].some((effect) => effect.status === "started")) {
737
+ return "running";
738
+ }
739
+ if (
740
+ [...run.attempts.values()].some(
741
+ (attempt) => attempt.status === "unknown",
742
+ ) ||
743
+ [...run.effects.values()].some((effect) => effect.status === "unknown") ||
744
+ [...run.nodeStates.values()].some(
745
+ (state) => state.status === "reconciliation_required",
746
+ )
747
+ ) {
748
+ return "reconciliation_required";
749
+ }
750
+ if (
751
+ [...run.humanTasks.values()].some((task) =>
752
+ ["open", "claimed"].includes(task.status),
753
+ )
754
+ ) {
755
+ return "waiting_human";
756
+ }
757
+ if (
758
+ [...run.messages.values()].some((message) =>
759
+ ["admitted", "delivered", "read"].includes(message.status),
760
+ ) ||
761
+ [...run.timers.values()].some((timer) => timer.status === "pending")
762
+ ) {
763
+ return "waiting_external";
764
+ }
765
+ const cycles = waitGraphCycles(run);
766
+ if (cycles.length) return "deadlocked";
767
+ const pending = [...run.nodeStates.entries()].filter(
768
+ ([, state]) => state.status === "pending",
769
+ );
770
+ const ready = pending.filter(([nodeId]) => nodeReadiness(run, nodeId).ready);
771
+ if (ready.length) return "running";
772
+ if (pending.length) {
773
+ const reasons = pending.map(([nodeId]) => run.waitReasons.get(nodeId));
774
+ if (reasons.some((reason) => reason?.kind === "input"))
775
+ return "waiting_input";
776
+ if (reasons.some(Boolean)) return "waiting_external";
777
+ return "blocked";
778
+ }
779
+ if (
780
+ [...run.nodeStates.values()].every((state) =>
781
+ TERMINAL_NODE.has(state.status),
782
+ )
783
+ ) {
784
+ return terminalAlgebra(run);
785
+ }
786
+ return "running";
787
+ }
788
+
789
+ export class GraphKernel {
790
+ constructor({
791
+ eventStore = new GraphEventStore(),
792
+ now = Date.now,
793
+ createId = randomUUID,
794
+ agingWindowMs = 1000,
795
+ maxPendingMessagesPerAgent = 100,
796
+ maxLivelockRepeats = 8,
797
+ } = {}) {
798
+ this.eventStore = eventStore;
799
+ this.now = now;
800
+ this.createId = createId;
801
+ this.agingWindowMs = Math.max(1, Number(agingWindowMs) || 1000);
802
+ this.maxPendingMessagesPerAgent = Math.max(
803
+ 1,
804
+ Number(maxPendingMessagesPerAgent) || 100,
805
+ );
806
+ this.maxLivelockRepeats = Math.max(2, Number(maxLivelockRepeats) || 8);
807
+ this.runs = new Map();
808
+ this.occurrences = new Map();
809
+ }
810
+
811
+ _run(runId) {
812
+ const run = this.runs.get(safeIdentifier(runId, "runId"));
813
+ if (!run) {
814
+ throw kernelError(
815
+ "CC_GRAPH_RUN_NOT_FOUND",
816
+ `GraphRun not found: ${runId}`,
817
+ );
818
+ }
819
+ return run;
820
+ }
821
+
822
+ _transaction(run, type, details, mutator, options = {}) {
823
+ if (TERMINAL_RUN.has(run.status) && options.allowTerminal !== true) {
824
+ throw kernelError(
825
+ "CC_GRAPH_RUN_TERMINAL",
826
+ `GraphRun is terminal: ${run.status}`,
827
+ );
828
+ }
829
+ const before = stateSnapshot(run);
830
+ try {
831
+ const result = mutator();
832
+ run.progressEpoch += 1;
833
+ const snapshot = stateSnapshot(run);
834
+ const event = this.eventStore.append(
835
+ run.id,
836
+ type,
837
+ { details: eventDetails(details), state: snapshot },
838
+ {
839
+ idempotencyKey: options.idempotencyKey || null,
840
+ traceId: options.traceId || null,
841
+ },
842
+ );
843
+ run.lastEvent = event;
844
+ return result;
845
+ } catch (error) {
846
+ restoreState(run, before);
847
+ throw error;
848
+ }
849
+ }
850
+
851
+ startRun(compiledGraph, options = {}) {
852
+ const compiled = assertCompiledGraph(compiledGraph);
853
+ const occurrence = options.occurrenceRef || null;
854
+ const occurrenceKey = occurrence
855
+ ? `${occurrence.jobRevision || ""}\0${occurrence.occurrenceId || occurrence.idempotencyKey || ""}`
856
+ : null;
857
+ if (occurrenceKey && this.occurrences.has(occurrenceKey)) {
858
+ const existing = this._run(this.occurrences.get(occurrenceKey));
859
+ if (existing.revisionDigest !== compiled.revisionDigest) {
860
+ throw kernelError(
861
+ "CC_GRAPH_OCCURRENCE_CONFLICT",
862
+ "occurrence was already bound to a different GraphRevision",
863
+ );
864
+ }
865
+ return runProjection(existing);
866
+ }
867
+ const run = buildRun(compiled, options, this.now);
868
+ if (this.runs.has(run.id)) {
869
+ throw kernelError(
870
+ "CC_GRAPH_RUN_CONFLICT",
871
+ `GraphRun id already exists: ${run.id}`,
872
+ );
873
+ }
874
+ this.eventStore.start(run.id, {
875
+ definitionId: compiled.definitionId,
876
+ revisionDigest: compiled.revisionDigest,
877
+ });
878
+ this.runs.set(run.id, run);
879
+ try {
880
+ this._transaction(
881
+ run,
882
+ "run.started",
883
+ {
884
+ definitionId: compiled.definitionId,
885
+ revisionDigest: compiled.revisionDigest,
886
+ occurrenceRef: occurrence,
887
+ },
888
+ () => {},
889
+ { idempotencyKey: `run-start:${run.id}` },
890
+ );
891
+ } catch (error) {
892
+ this.runs.delete(run.id);
893
+ throw error;
894
+ }
895
+ if (occurrenceKey) this.occurrences.set(occurrenceKey, run.id);
896
+ return runProjection(run);
897
+ }
898
+
899
+ recoverRun(runId) {
900
+ const id = safeIdentifier(runId, "runId");
901
+ const events = this.eventStore.read(id);
902
+ const latest = [...events]
903
+ .reverse()
904
+ .find((event) => event.payload?.state?.version === 1);
905
+ if (!latest) {
906
+ throw kernelError(
907
+ "CC_GRAPH_RECOVERY_UNAVAILABLE",
908
+ `GraphRun has no recoverable state: ${id}`,
909
+ );
910
+ }
911
+ const snapshot = latest.payload.state;
912
+ const run = { id, lastEvent: latest };
913
+ restoreState(run, snapshot);
914
+ run.lastEvent = latest;
915
+ this.runs.set(id, run);
916
+ if (run.occurrenceRef) {
917
+ const key = `${run.occurrenceRef.jobRevision || ""}\0${run.occurrenceRef.occurrenceId || run.occurrenceRef.idempotencyKey || ""}`;
918
+ this.occurrences.set(key, id);
919
+ }
920
+ const uncertainEffects = [...run.effects.values()].filter(
921
+ (effect) => effect.status === "started",
922
+ );
923
+ if (uncertainEffects.length) {
924
+ this._transaction(
925
+ run,
926
+ "effect.recovery_unknown",
927
+ { effectIds: uncertainEffects.map((effect) => effect.id).sort() },
928
+ () => {
929
+ for (const effect of uncertainEffects) {
930
+ effect.status = "unknown";
931
+ effect.updatedAt = nowIso(this.now);
932
+ const attempt = run.attempts.get(effect.attemptId);
933
+ if (attempt && ACTIVE_ATTEMPT.has(attempt.status)) {
934
+ attempt.status = "unknown";
935
+ attempt.participationStatus = "reconciliation_required";
936
+ attempt.updatedAt = nowIso(this.now);
937
+ }
938
+ const state = run.nodeStates.get(effect.nodeId);
939
+ if (state) {
940
+ state.status = "reconciliation_required";
941
+ state.updatedAt = nowIso(this.now);
942
+ }
943
+ }
944
+ run.status = "reconciliation_required";
945
+ },
946
+ { idempotencyKey: `effect-recovery:${latest.hash}` },
947
+ );
948
+ }
949
+ return runProjection(run);
950
+ }
951
+
952
+ getRun(runId) {
953
+ return runProjection(this._run(runId));
954
+ }
955
+
956
+ events(runId, options = {}) {
957
+ return this.eventStore.read(safeIdentifier(runId, "runId"), options);
958
+ }
959
+
960
+ registerAgent(runId, { agentId, capacity = 1, resident = true } = {}) {
961
+ const run = this._run(runId);
962
+ const id = safeIdentifier(agentId, "agentId");
963
+ return this._transaction(
964
+ run,
965
+ "agent.registered",
966
+ { agentId: id, capacity, resident },
967
+ () => {
968
+ const existing = run.agents.get(id);
969
+ const agent = {
970
+ id,
971
+ capacity: Math.max(1, Number(capacity) || 1),
972
+ resident: resident !== false,
973
+ status: "idle",
974
+ registeredAt: existing?.registeredAt || nowIso(this.now),
975
+ updatedAt: nowIso(this.now),
976
+ };
977
+ run.agents.set(id, agent);
978
+ return Object.freeze(clone(agent));
979
+ },
980
+ {},
981
+ );
982
+ }
983
+
984
+ acquireProducerLease(
985
+ runId,
986
+ { producerId, ttlMs = 60_000, leaseId = this.createId() } = {},
987
+ ) {
988
+ const run = this._run(runId);
989
+ const id = safeIdentifier(leaseId, "leaseId");
990
+ const owner = safeIdentifier(producerId, "producerId");
991
+ return this._transaction(
992
+ run,
993
+ "producer.acquired",
994
+ { producerId: owner, leaseId: id },
995
+ () => {
996
+ if (run.phase !== "open") {
997
+ throw kernelError(
998
+ "CC_GRAPH_SEALED",
999
+ "producer leases cannot be acquired after the graph is sealed",
1000
+ );
1001
+ }
1002
+ const now = this.now();
1003
+ sweepExpirations(run, now);
1004
+ const existing = [...run.producerLeases.values()].find(
1005
+ (lease) => lease.producerId === owner && producerIsLive(lease, now),
1006
+ );
1007
+ if (existing) return Object.freeze(clone(existing));
1008
+ const fence =
1009
+ Math.max(
1010
+ 0,
1011
+ ...[...run.producerLeases.values()]
1012
+ .filter((lease) => lease.producerId === owner)
1013
+ .map((lease) => lease.fence),
1014
+ ) + 1;
1015
+ const lease = {
1016
+ id,
1017
+ producerId: owner,
1018
+ fence,
1019
+ status: "active",
1020
+ createdAt: nowIso(() => now),
1021
+ updatedAt: nowIso(() => now),
1022
+ expiresAt: nowIso(() => now + finitePositive(ttlMs, 60_000)),
1023
+ expiresAtMs: now + finitePositive(ttlMs, 60_000),
1024
+ };
1025
+ run.producerLeases.set(id, lease);
1026
+ return Object.freeze(clone(lease));
1027
+ },
1028
+ { idempotencyKey: `producer-acquire:${id}` },
1029
+ );
1030
+ }
1031
+
1032
+ releaseProducerLease(runId, leaseId, fence) {
1033
+ const run = this._run(runId);
1034
+ const id = safeIdentifier(leaseId, "leaseId");
1035
+ return this._transaction(run, "producer.released", { leaseId: id }, () => {
1036
+ const lease = run.producerLeases.get(id);
1037
+ if (
1038
+ !lease ||
1039
+ lease.fence !== Number(fence) ||
1040
+ lease.status !== "active"
1041
+ ) {
1042
+ throw kernelError(
1043
+ "CC_GRAPH_STALE_PRODUCER_LEASE",
1044
+ "producer lease is missing, stale, or inactive",
1045
+ );
1046
+ }
1047
+ lease.status = "released";
1048
+ lease.updatedAt = nowIso(this.now);
1049
+ return Object.freeze(clone(lease));
1050
+ });
1051
+ }
1052
+
1053
+ appendGraph(
1054
+ runId,
1055
+ {
1056
+ expectedGraphRevision,
1057
+ requestId,
1058
+ producerLeaseId,
1059
+ producerFence,
1060
+ nodes = [],
1061
+ edges = [],
1062
+ loops = [],
1063
+ subgraphCalls = [],
1064
+ } = {},
1065
+ ) {
1066
+ const run = this._run(runId);
1067
+ const key = safeIdentifier(requestId, "requestId");
1068
+ const request = {
1069
+ expectedGraphRevision,
1070
+ producerLeaseId,
1071
+ producerFence,
1072
+ nodes,
1073
+ edges,
1074
+ loops,
1075
+ subgraphCalls,
1076
+ };
1077
+ const requestDigest = graphDigest(request, "cc.graph.append-request/v1");
1078
+ const replay = run.requestCache.get(key);
1079
+ if (replay) {
1080
+ if (replay.digest !== requestDigest) {
1081
+ throw kernelError(
1082
+ "CC_GRAPH_REQUEST_ID_CONFLICT",
1083
+ "dynamic graph requestId was reused with different content",
1084
+ );
1085
+ }
1086
+ return Object.freeze(clone(replay.result));
1087
+ }
1088
+ return this._transaction(
1089
+ run,
1090
+ "graph.revised",
1091
+ { requestId: key, requestDigest },
1092
+ () => {
1093
+ if (run.phase !== "open") {
1094
+ throw kernelError(
1095
+ "CC_GRAPH_SEALED",
1096
+ "a sealed GraphRun cannot be extended",
1097
+ );
1098
+ }
1099
+ if (Number(expectedGraphRevision) !== run.graphRevision) {
1100
+ throw kernelError(
1101
+ "CC_GRAPH_REVISION_CONFLICT",
1102
+ "expectedGraphRevision does not match the current revision",
1103
+ { expectedGraphRevision, currentGraphRevision: run.graphRevision },
1104
+ );
1105
+ }
1106
+ const now = this.now();
1107
+ sweepExpirations(run, now);
1108
+ const lease = run.producerLeases.get(
1109
+ safeIdentifier(producerLeaseId, "producerLeaseId"),
1110
+ );
1111
+ if (
1112
+ !lease ||
1113
+ lease.fence !== Number(producerFence) ||
1114
+ !producerIsLive(lease, now)
1115
+ ) {
1116
+ throw kernelError(
1117
+ "CC_GRAPH_STALE_PRODUCER_LEASE",
1118
+ "dynamic graph mutation requires the current producer lease",
1119
+ );
1120
+ }
1121
+ const definition = {
1122
+ ...clone(run.compiled.definition),
1123
+ revision: run.compiled.definition.revision + 1,
1124
+ nodes: [...clone(run.compiled.definition.nodes), ...clone(nodes)],
1125
+ edges: [...clone(run.compiled.definition.edges), ...clone(edges)],
1126
+ loops: [...clone(run.compiled.definition.loops), ...clone(loops)],
1127
+ subgraphCalls: [
1128
+ ...clone(run.compiled.definition.subgraphCalls),
1129
+ ...clone(subgraphCalls),
1130
+ ],
1131
+ };
1132
+ const compiled = compileGraphDefinition(definition);
1133
+ const createdAt = nowIso(() => now);
1134
+ for (const node of nodes) {
1135
+ run.nodeStates.set(node.id, {
1136
+ status: "pending",
1137
+ attemptIds: [],
1138
+ acceptedAttemptId: null,
1139
+ blockedRoot: null,
1140
+ createdAt,
1141
+ updatedAt: createdAt,
1142
+ });
1143
+ run.fenceCounters.set(node.id, 0);
1144
+ }
1145
+ run.compiled = compiled;
1146
+ run.graphRevision += 1;
1147
+ run.revisionDigest = compiled.revisionDigest;
1148
+ const result = {
1149
+ runId: run.id,
1150
+ graphRevision: run.graphRevision,
1151
+ revisionDigest: run.revisionDigest,
1152
+ addedNodeIds: nodes.map((node) => node.id).sort(),
1153
+ };
1154
+ run.requestCache.set(key, { digest: requestDigest, result });
1155
+ return Object.freeze(clone(result));
1156
+ },
1157
+ { idempotencyKey: `graph-append:${key}` },
1158
+ );
1159
+ }
1160
+
1161
+ sealRun(runId) {
1162
+ const run = this._run(runId);
1163
+ return this._transaction(
1164
+ run,
1165
+ "graph.sealed",
1166
+ { graphRevision: run.graphRevision },
1167
+ () => {
1168
+ const now = this.now();
1169
+ sweepExpirations(run, now);
1170
+ const active = [...run.producerLeases.values()].filter((lease) =>
1171
+ producerIsLive(lease, now),
1172
+ );
1173
+ if (active.length) {
1174
+ throw kernelError(
1175
+ "CC_GRAPH_PRODUCERS_ACTIVE",
1176
+ "GraphRun cannot be sealed while producer leases are active",
1177
+ { producerLeaseIds: active.map((lease) => lease.id) },
1178
+ );
1179
+ }
1180
+ run.phase = "sealed";
1181
+ propagate(run, () => now);
1182
+ run.status = classifyQuiescence(run, now);
1183
+ if (TERMINAL_RUN.has(run.status)) run.completedAt = nowIso(() => now);
1184
+ return runProjection(run);
1185
+ },
1186
+ { idempotencyKey: `graph-seal:${run.id}:${run.graphRevision}` },
1187
+ );
1188
+ }
1189
+
1190
+ tick(runId) {
1191
+ const run = this._run(runId);
1192
+ return this._transaction(run, "run.ticked", {}, () => {
1193
+ const now = this.now();
1194
+ sweepExpirations(run, now);
1195
+ propagate(run, () => now);
1196
+ run.status = classifyQuiescence(run, now);
1197
+ if (TERMINAL_RUN.has(run.status)) run.completedAt ||= nowIso(() => now);
1198
+ return runProjection(run);
1199
+ });
1200
+ }
1201
+
1202
+ readyNodes(runId) {
1203
+ const run = this._run(runId);
1204
+ const now = this.now();
1205
+ return run.compiled.topologicalOrder
1206
+ .filter((nodeId) => {
1207
+ const state = run.nodeStates.get(nodeId);
1208
+ return state.status === "pending" && nodeReadiness(run, nodeId).ready;
1209
+ })
1210
+ .map((nodeId) => ({
1211
+ nodeId,
1212
+ priority: effectivePriority(run, nodeId, now, this.agingWindowMs),
1213
+ }))
1214
+ .sort(
1215
+ (left, right) =>
1216
+ right.priority.total - left.priority.total ||
1217
+ left.nodeId.localeCompare(right.nodeId),
1218
+ )
1219
+ .map((entry) => Object.freeze(entry));
1220
+ }
1221
+
1222
+ assignNext(runId, agentId, options = {}) {
1223
+ const ready = this.readyNodes(runId);
1224
+ if (!ready.length) return null;
1225
+ let lastConflict = null;
1226
+ for (const candidate of ready) {
1227
+ try {
1228
+ return this.assignNode(runId, candidate.nodeId, agentId, options);
1229
+ } catch (error) {
1230
+ if (
1231
+ ["CC_GRAPH_WRITE_SCOPE_BUSY", "CC_GRAPH_AGENT_CAPACITY"].includes(
1232
+ error?.code,
1233
+ )
1234
+ ) {
1235
+ lastConflict = error;
1236
+ continue;
1237
+ }
1238
+ throw error;
1239
+ }
1240
+ }
1241
+ if (lastConflict) throw lastConflict;
1242
+ return null;
1243
+ }
1244
+
1245
+ assignNode(
1246
+ runId,
1247
+ nodeId,
1248
+ agentId,
1249
+ {
1250
+ role = "executor",
1251
+ ttlMs = 60_000,
1252
+ leaseId = this.createId(),
1253
+ attemptId = this.createId(),
1254
+ grant = {},
1255
+ } = {},
1256
+ ) {
1257
+ const run = this._run(runId);
1258
+ const safeNodeId = safeIdentifier(nodeId, "nodeId");
1259
+ const safeAgentId = safeIdentifier(agentId, "agentId");
1260
+ return this._transaction(
1261
+ run,
1262
+ "assignment.started",
1263
+ { nodeId: safeNodeId, agentId: safeAgentId, role },
1264
+ () => {
1265
+ const now = this.now();
1266
+ sweepExpirations(run, now);
1267
+ propagate(run, () => now);
1268
+ const node = run.compiled.nodes[safeNodeId];
1269
+ const state = run.nodeStates.get(safeNodeId);
1270
+ if (!node || !state) {
1271
+ throw kernelError(
1272
+ "CC_GRAPH_NODE_NOT_FOUND",
1273
+ `Graph node not found: ${safeNodeId}`,
1274
+ );
1275
+ }
1276
+ if (
1277
+ state.status !== "pending" ||
1278
+ !nodeReadiness(run, safeNodeId).ready
1279
+ ) {
1280
+ throw kernelError(
1281
+ "CC_GRAPH_NODE_NOT_READY",
1282
+ `Graph node is not ready: ${safeNodeId}`,
1283
+ );
1284
+ }
1285
+ let agent = run.agents.get(safeAgentId);
1286
+ if (!agent) {
1287
+ agent = {
1288
+ id: safeAgentId,
1289
+ capacity: 1,
1290
+ resident: true,
1291
+ status: "idle",
1292
+ registeredAt: nowIso(() => now),
1293
+ updatedAt: nowIso(() => now),
1294
+ };
1295
+ run.agents.set(safeAgentId, agent);
1296
+ }
1297
+ if (
1298
+ activeAttemptsForAgent(run, safeAgentId, now).length >= agent.capacity
1299
+ ) {
1300
+ throw kernelError(
1301
+ "CC_GRAPH_AGENT_CAPACITY",
1302
+ `agent capacity is exhausted: ${safeAgentId}`,
1303
+ );
1304
+ }
1305
+ if (role === "executor") {
1306
+ const conflict = activeWriteConflict(run, node, now);
1307
+ if (conflict) {
1308
+ throw kernelError(
1309
+ "CC_GRAPH_WRITE_SCOPE_BUSY",
1310
+ `write scope is held by attempt ${conflict.id}`,
1311
+ { holderAttemptId: conflict.id },
1312
+ );
1313
+ }
1314
+ }
1315
+ const fence = (run.fenceCounters.get(safeNodeId) || 0) + 1;
1316
+ run.fenceCounters.set(safeNodeId, fence);
1317
+ const id = safeIdentifier(attemptId, "attemptId");
1318
+ const attempt = {
1319
+ id,
1320
+ runId: run.id,
1321
+ nodeId: safeNodeId,
1322
+ agentId: safeAgentId,
1323
+ role,
1324
+ leaseId: safeIdentifier(leaseId, "leaseId"),
1325
+ fence,
1326
+ status: "active",
1327
+ participationStatus: "active",
1328
+ grant: clone(grant),
1329
+ createdAt: nowIso(() => now),
1330
+ updatedAt: nowIso(() => now),
1331
+ expiresAt: nowIso(() => now + finitePositive(ttlMs, 60_000)),
1332
+ expiresAtMs: now + finitePositive(ttlMs, 60_000),
1333
+ terminalEvidence: null,
1334
+ usage: null,
1335
+ };
1336
+ run.attempts.set(id, attempt);
1337
+ state.attemptIds.push(id);
1338
+ state.status = "running";
1339
+ state.updatedAt = nowIso(() => now);
1340
+ agent.status = "busy";
1341
+ agent.updatedAt = nowIso(() => now);
1342
+ return attemptProjection(attempt);
1343
+ },
1344
+ { idempotencyKey: `assignment:${attemptId}` },
1345
+ );
1346
+ }
1347
+
1348
+ renewAttempt(runId, attemptId, leaseId, fence, ttlMs = 60_000) {
1349
+ const run = this._run(runId);
1350
+ return this._transaction(run, "assignment.renewed", { attemptId }, () => {
1351
+ const attempt = this._requireAttemptLease(run, attemptId, leaseId, fence);
1352
+ const now = this.now();
1353
+ attempt.expiresAtMs = now + finitePositive(ttlMs, 60_000);
1354
+ attempt.expiresAt = nowIso(() => attempt.expiresAtMs);
1355
+ attempt.updatedAt = nowIso(() => now);
1356
+ return attemptProjection(attempt);
1357
+ });
1358
+ }
1359
+
1360
+ _requireAttemptLease(run, attemptId, leaseId, fence, statuses = ["active"]) {
1361
+ const attempt = run.attempts.get(safeIdentifier(attemptId, "attemptId"));
1362
+ const now = this.now();
1363
+ if (
1364
+ !attempt ||
1365
+ attempt.leaseId !== safeIdentifier(leaseId, "leaseId") ||
1366
+ attempt.fence !== Number(fence) ||
1367
+ !statuses.includes(attempt.status) ||
1368
+ attempt.expiresAtMs <= now
1369
+ ) {
1370
+ throw kernelError(
1371
+ "CC_GRAPH_STALE_ATTEMPT_LEASE",
1372
+ "attempt lease is missing, expired, fenced, or inactive",
1373
+ );
1374
+ }
1375
+ return attempt;
1376
+ }
1377
+
1378
+ beginEffect(
1379
+ runId,
1380
+ {
1381
+ effectId = this.createId(),
1382
+ attemptId,
1383
+ leaseId,
1384
+ fence,
1385
+ idempotencyKey,
1386
+ operationDigest,
1387
+ compensationEffectId = null,
1388
+ } = {},
1389
+ ) {
1390
+ const run = this._run(runId);
1391
+ const key = String(idempotencyKey || "").trim();
1392
+ const existing = [...run.effects.values()].find(
1393
+ (effect) => effect.idempotencyKey === key,
1394
+ );
1395
+ if (existing) {
1396
+ if (existing.operationDigest !== operationDigest) {
1397
+ throw kernelError(
1398
+ "CC_GRAPH_EFFECT_IDEMPOTENCY_CONFLICT",
1399
+ "effect idempotency key was reused for a different operation",
1400
+ );
1401
+ }
1402
+ return Object.freeze(clone(existing));
1403
+ }
1404
+ const id = safeIdentifier(effectId, "effectId");
1405
+ return this._transaction(
1406
+ run,
1407
+ "effect.started",
1408
+ { effectId: id, attemptId },
1409
+ () => {
1410
+ const attempt = this._requireAttemptLease(
1411
+ run,
1412
+ attemptId,
1413
+ leaseId,
1414
+ fence,
1415
+ );
1416
+ const node = run.compiled.nodes[attempt.nodeId];
1417
+ if (
1418
+ !node ||
1419
+ !["workspace_write", "external"].includes(node.effectClass)
1420
+ ) {
1421
+ throw kernelError(
1422
+ "CC_GRAPH_EFFECT_NOT_DECLARED",
1423
+ "node did not declare an effectful execution class",
1424
+ );
1425
+ }
1426
+ if (!key || key !== node.idempotencyKey) {
1427
+ throw kernelError(
1428
+ "CC_GRAPH_EFFECT_IDEMPOTENCY_REQUIRED",
1429
+ "effect must use the idempotency key authenticated by Graph Compiler",
1430
+ );
1431
+ }
1432
+ if (!/^sha256:[a-f0-9]{64}$/u.test(String(operationDigest || ""))) {
1433
+ throw kernelError(
1434
+ "CC_GRAPH_EFFECT_OPERATION_DIGEST_INVALID",
1435
+ "effect operationDigest must be sha256",
1436
+ );
1437
+ }
1438
+ const effect = {
1439
+ id,
1440
+ runId: run.id,
1441
+ nodeId: attempt.nodeId,
1442
+ attemptId: attempt.id,
1443
+ leaseId: attempt.leaseId,
1444
+ fence: attempt.fence,
1445
+ idempotencyKey: key,
1446
+ operationDigest,
1447
+ status: "started",
1448
+ receipt: null,
1449
+ compensationEffectId:
1450
+ compensationEffectId == null
1451
+ ? null
1452
+ : safeIdentifier(compensationEffectId, "compensationEffectId"),
1453
+ createdAt: nowIso(this.now),
1454
+ updatedAt: nowIso(this.now),
1455
+ };
1456
+ run.effects.set(id, effect);
1457
+ return Object.freeze(clone(effect));
1458
+ },
1459
+ { idempotencyKey: `effect-start:${id}` },
1460
+ );
1461
+ }
1462
+
1463
+ settleEffect(
1464
+ runId,
1465
+ { effectId, attemptId, leaseId, fence, outcome, receipt = null } = {},
1466
+ ) {
1467
+ const run = this._run(runId);
1468
+ const id = safeIdentifier(effectId, "effectId");
1469
+ const current = run.effects.get(id);
1470
+ if (current && current.status !== "started") {
1471
+ if (current.status === outcome) return Object.freeze(clone(current));
1472
+ throw kernelError(
1473
+ "CC_GRAPH_EFFECT_TERMINAL_CONFLICT",
1474
+ "effect already has a different terminal outcome",
1475
+ );
1476
+ }
1477
+ return this._transaction(
1478
+ run,
1479
+ "effect.settled",
1480
+ { effectId: id, outcome },
1481
+ () => {
1482
+ const effect = run.effects.get(id);
1483
+ if (!effect || effect.attemptId !== attemptId) {
1484
+ throw kernelError(
1485
+ "CC_GRAPH_EFFECT_NOT_FOUND",
1486
+ `effect was not started by this attempt: ${id}`,
1487
+ );
1488
+ }
1489
+ this._requireAttemptLease(run, attemptId, leaseId, fence);
1490
+ if (!["committed", "failed", "unknown"].includes(outcome)) {
1491
+ throw kernelError(
1492
+ "CC_GRAPH_EFFECT_OUTCOME_INVALID",
1493
+ `unsupported effect outcome: ${outcome}`,
1494
+ );
1495
+ }
1496
+ if (
1497
+ outcome === "committed" &&
1498
+ !/^sha256:[a-f0-9]{64}$/u.test(String(receipt?.receiptDigest || ""))
1499
+ ) {
1500
+ throw kernelError(
1501
+ "CC_GRAPH_EFFECT_RECEIPT_REQUIRED",
1502
+ "committed effect requires an immutable receipt digest",
1503
+ );
1504
+ }
1505
+ effect.status = outcome;
1506
+ effect.receipt = receipt
1507
+ ? {
1508
+ ...clone(receipt),
1509
+ recordedAt: receipt.recordedAt || nowIso(this.now),
1510
+ }
1511
+ : null;
1512
+ effect.updatedAt = nowIso(this.now);
1513
+ if (outcome === "unknown") {
1514
+ const attempt = run.attempts.get(effect.attemptId);
1515
+ attempt.status = "unknown";
1516
+ attempt.participationStatus = "reconciliation_required";
1517
+ const state = run.nodeStates.get(effect.nodeId);
1518
+ state.status = "reconciliation_required";
1519
+ state.updatedAt = nowIso(this.now);
1520
+ run.status = "reconciliation_required";
1521
+ }
1522
+ return Object.freeze(clone(effect));
1523
+ },
1524
+ { idempotencyKey: `effect-settle:${id}:${outcome}` },
1525
+ );
1526
+ }
1527
+
1528
+ reconcileEffect(
1529
+ runId,
1530
+ { effectId, decision, receipt = null, auditDecisionId } = {},
1531
+ ) {
1532
+ const run = this._run(runId);
1533
+ const id = safeIdentifier(effectId, "effectId");
1534
+ if (!auditDecisionId) {
1535
+ throw kernelError(
1536
+ "CC_GRAPH_RECONCILIATION_AUDIT_REQUIRED",
1537
+ "effect reconciliation requires an audited decision",
1538
+ );
1539
+ }
1540
+ return this._transaction(
1541
+ run,
1542
+ "effect.reconciled",
1543
+ { effectId: id, decision, auditDecisionId },
1544
+ () => {
1545
+ const effect = run.effects.get(id);
1546
+ if (!effect || effect.status !== "unknown") {
1547
+ throw kernelError(
1548
+ "CC_GRAPH_EFFECT_NOT_RECONCILABLE",
1549
+ "only an unknown effect can be reconciled",
1550
+ );
1551
+ }
1552
+ if (!["committed", "failed"].includes(decision)) {
1553
+ throw kernelError(
1554
+ "CC_GRAPH_EFFECT_OUTCOME_INVALID",
1555
+ "reconciliation decision must be committed or failed",
1556
+ );
1557
+ }
1558
+ if (
1559
+ decision === "committed" &&
1560
+ !/^sha256:[a-f0-9]{64}$/u.test(String(receipt?.receiptDigest || ""))
1561
+ ) {
1562
+ throw kernelError(
1563
+ "CC_GRAPH_EFFECT_RECEIPT_REQUIRED",
1564
+ "committed reconciliation requires an immutable receipt digest",
1565
+ );
1566
+ }
1567
+ effect.status = decision;
1568
+ effect.receipt = receipt
1569
+ ? {
1570
+ ...clone(receipt),
1571
+ recordedAt: receipt.recordedAt || nowIso(this.now),
1572
+ }
1573
+ : null;
1574
+ effect.auditDecisionId = String(auditDecisionId);
1575
+ effect.updatedAt = nowIso(this.now);
1576
+ const attempt = run.attempts.get(effect.attemptId);
1577
+ if (attempt?.status === "unknown") {
1578
+ attempt.status = "expired";
1579
+ attempt.participationStatus = "reconciled";
1580
+ attempt.updatedAt = nowIso(this.now);
1581
+ const agent = run.agents.get(attempt.agentId);
1582
+ if (agent) agent.status = "idle";
1583
+ }
1584
+ const state = run.nodeStates.get(effect.nodeId);
1585
+ state.status = "pending";
1586
+ state.updatedAt = nowIso(this.now);
1587
+ run.status = classifyQuiescence(run, this.now());
1588
+ return Object.freeze(clone(effect));
1589
+ },
1590
+ { idempotencyKey: `effect-reconcile:${id}:${auditDecisionId}` },
1591
+ );
1592
+ }
1593
+
1594
+ registerArtifact(
1595
+ runId,
1596
+ {
1597
+ artifactId = this.createId(),
1598
+ attemptId,
1599
+ leaseId,
1600
+ fence,
1601
+ digest: artifactDigest,
1602
+ schema = {},
1603
+ commit = null,
1604
+ validationEvidence = [],
1605
+ consumerNodeIds = [],
1606
+ dataPolicy,
1607
+ } = {},
1608
+ ) {
1609
+ const run = this._run(runId);
1610
+ const id = safeIdentifier(artifactId, "artifactId");
1611
+ return this._transaction(
1612
+ run,
1613
+ "artifact.recorded",
1614
+ { artifactId: id, attemptId },
1615
+ () => {
1616
+ const existing = run.artifacts.get(id);
1617
+ if (existing) {
1618
+ if (existing.digest !== artifactDigest) {
1619
+ throw kernelError(
1620
+ "CC_GRAPH_ARTIFACT_IMMUTABLE",
1621
+ "artifact id was reused with a different digest",
1622
+ );
1623
+ }
1624
+ return Object.freeze(clone(existing));
1625
+ }
1626
+ const attempt = this._requireAttemptLease(
1627
+ run,
1628
+ attemptId,
1629
+ leaseId,
1630
+ fence,
1631
+ );
1632
+ if (!/^sha256:[a-f0-9]{64}$/u.test(String(artifactDigest))) {
1633
+ throw kernelError(
1634
+ "CC_GRAPH_ARTIFACT_DIGEST_INVALID",
1635
+ "artifact digest must be sha256",
1636
+ );
1637
+ }
1638
+ const policy = validateDataPolicy(dataPolicy);
1639
+ const artifact = {
1640
+ id,
1641
+ digest: artifactDigest,
1642
+ producerNodeId: attempt.nodeId,
1643
+ producerAttemptId: attempt.id,
1644
+ producerLeaseId: attempt.leaseId,
1645
+ producerFence: attempt.fence,
1646
+ schema: clone(schema),
1647
+ commit,
1648
+ validationEvidence: [...validationEvidence],
1649
+ consumerNodeIds: [...consumerNodeIds],
1650
+ dataPolicy: policy,
1651
+ createdAt: nowIso(this.now),
1652
+ };
1653
+ run.artifacts.set(id, artifact);
1654
+ return Object.freeze(clone(artifact));
1655
+ },
1656
+ { idempotencyKey: `artifact:${id}` },
1657
+ );
1658
+ }
1659
+
1660
+ settleAttempt(
1661
+ runId,
1662
+ {
1663
+ attemptId,
1664
+ leaseId,
1665
+ fence,
1666
+ outcome,
1667
+ evidence = null,
1668
+ usage = {},
1669
+ error = null,
1670
+ } = {},
1671
+ ) {
1672
+ const run = this._run(runId);
1673
+ return this._transaction(
1674
+ run,
1675
+ "assignment.settled",
1676
+ { attemptId, outcome },
1677
+ () => {
1678
+ const attempt = this._requireAttemptLease(
1679
+ run,
1680
+ attemptId,
1681
+ leaseId,
1682
+ fence,
1683
+ );
1684
+ const state = run.nodeStates.get(attempt.nodeId);
1685
+ const node = run.compiled.nodes[attempt.nodeId];
1686
+ if (state.acceptedAttemptId && state.acceptedAttemptId !== attempt.id) {
1687
+ attempt.status = "rejected";
1688
+ attempt.participationStatus = "loser";
1689
+ throw kernelError(
1690
+ "CC_GRAPH_ATTEMPT_LOST_RACE",
1691
+ "another attempt already finalized this task",
1692
+ { acceptedAttemptId: state.acceptedAttemptId },
1693
+ );
1694
+ }
1695
+ addUsage(run, usage);
1696
+ attempt.usage = clone(usage);
1697
+ attempt.updatedAt = nowIso(this.now);
1698
+ if (outcome === "unknown") {
1699
+ attempt.status = "unknown";
1700
+ attempt.participationStatus = "reconciliation_required";
1701
+ attempt.terminalEvidence = clone(evidence);
1702
+ state.status = "reconciliation_required";
1703
+ state.updatedAt = nowIso(this.now);
1704
+ run.status = "reconciliation_required";
1705
+ return attemptProjection(attempt);
1706
+ }
1707
+ if (outcome === "succeeded") {
1708
+ const unresolvedEffect = [...run.effects.values()].find(
1709
+ (effect) =>
1710
+ effect.attemptId === attempt.id && effect.status !== "committed",
1711
+ );
1712
+ if (unresolvedEffect) {
1713
+ throw kernelError(
1714
+ "CC_GRAPH_EFFECT_RECEIPT_REQUIRED",
1715
+ `attempt cannot succeed before effect receipt is committed: ${unresolvedEffect.id}`,
1716
+ );
1717
+ }
1718
+ if (!hasTerminalEvidence(evidence)) {
1719
+ throw kernelError(
1720
+ "CC_GRAPH_TERMINAL_EVIDENCE_REQUIRED",
1721
+ "successful attempts require output/artifact/commit/test evidence",
1722
+ );
1723
+ }
1724
+ for (const artifactId of evidence.artifactIds || []) {
1725
+ const artifact = run.artifacts.get(artifactId);
1726
+ if (
1727
+ !artifact ||
1728
+ artifact.producerAttemptId !== attempt.id ||
1729
+ artifact.producerLeaseId !== attempt.leaseId ||
1730
+ artifact.producerFence !== attempt.fence
1731
+ ) {
1732
+ throw kernelError(
1733
+ "CC_GRAPH_ARTIFACT_PROVENANCE_INVALID",
1734
+ `terminal artifact is not owned by the settling attempt: ${artifactId}`,
1735
+ );
1736
+ }
1737
+ }
1738
+ attempt.status = "accepted";
1739
+ attempt.participationStatus = "accepted_winner";
1740
+ attempt.terminalEvidence = clone(evidence);
1741
+ state.status = "succeeded";
1742
+ state.acceptedAttemptId = attempt.id;
1743
+ state.updatedAt = nowIso(this.now);
1744
+ for (const other of run.attempts.values()) {
1745
+ if (
1746
+ other.nodeId === attempt.nodeId &&
1747
+ other.id !== attempt.id &&
1748
+ ACTIVE_ATTEMPT.has(other.status)
1749
+ ) {
1750
+ other.status = "cancelled";
1751
+ other.participationStatus = "loser";
1752
+ other.updatedAt = nowIso(this.now);
1753
+ }
1754
+ }
1755
+ } else if (outcome === "failed" || outcome === "timed_out") {
1756
+ attempt.status = "rejected";
1757
+ attempt.participationStatus = "failed";
1758
+ attempt.error = error == null ? null : String(error).slice(0, 4096);
1759
+ attempt.terminalEvidence = clone(evidence);
1760
+ const attemptNumber = state.attemptIds.length;
1761
+ if (attemptNumber <= Number(node.retryLimit || 0)) {
1762
+ state.status = "pending";
1763
+ } else {
1764
+ state.status = outcome === "timed_out" ? "timed_out" : "failed";
1765
+ state.blockedRoot = attempt.nodeId;
1766
+ }
1767
+ state.updatedAt = nowIso(this.now);
1768
+ } else if (outcome === "cancelled") {
1769
+ attempt.status = "cancelled";
1770
+ attempt.participationStatus = "cancelled";
1771
+ state.status = run.cancellationRequested ? "cancelled" : "pending";
1772
+ state.updatedAt = nowIso(this.now);
1773
+ } else {
1774
+ throw kernelError(
1775
+ "CC_GRAPH_OUTCOME_INVALID",
1776
+ `unsupported attempt outcome: ${outcome}`,
1777
+ );
1778
+ }
1779
+ const agent = run.agents.get(attempt.agentId);
1780
+ if (agent) {
1781
+ agent.status = "idle";
1782
+ agent.updatedAt = nowIso(this.now);
1783
+ }
1784
+ propagate(run, this.now);
1785
+ run.status = classifyQuiescence(run, this.now());
1786
+ if (TERMINAL_RUN.has(run.status)) run.completedAt = nowIso(this.now);
1787
+ return attemptProjection(attempt);
1788
+ },
1789
+ { idempotencyKey: `attempt-settle:${attemptId}:${outcome}` },
1790
+ );
1791
+ }
1792
+
1793
+ async cancelRun(runId, { reason = "cancelled", interrupt = null } = {}) {
1794
+ const run = this._run(runId);
1795
+ const active = this._transaction(
1796
+ run,
1797
+ "run.cancel_requested",
1798
+ { reason },
1799
+ () => {
1800
+ run.cancellationRequested = true;
1801
+ run.phase = "sealed";
1802
+ for (const lease of run.producerLeases.values()) {
1803
+ if (lease.status === "active") lease.status = "revoked";
1804
+ }
1805
+ const attempts = [];
1806
+ for (const attempt of run.attempts.values()) {
1807
+ if (attempt.status === "active") {
1808
+ attempt.status = "cancelling";
1809
+ attempt.updatedAt = nowIso(this.now);
1810
+ attempts.push(clone(attempt));
1811
+ }
1812
+ }
1813
+ for (const state of run.nodeStates.values()) {
1814
+ if (state.status === "pending") {
1815
+ state.status = "cancelled";
1816
+ state.updatedAt = nowIso(this.now);
1817
+ }
1818
+ }
1819
+ return attempts;
1820
+ },
1821
+ );
1822
+ const failures = [];
1823
+ await Promise.all(
1824
+ active.map(async (attempt) => {
1825
+ try {
1826
+ if (typeof interrupt === "function") await interrupt(attempt);
1827
+ } catch (error) {
1828
+ failures.push({
1829
+ attemptId: attempt.id,
1830
+ error: String(error?.message || error).slice(0, 4096),
1831
+ });
1832
+ }
1833
+ }),
1834
+ );
1835
+ return this._transaction(
1836
+ run,
1837
+ failures.length ? "run.cancel_reconciliation" : "run.cancelled",
1838
+ { reason, failures },
1839
+ () => {
1840
+ for (const attempt of run.attempts.values()) {
1841
+ if (attempt.status !== "cancelling") continue;
1842
+ const failure = failures.find(
1843
+ (entry) => entry.attemptId === attempt.id,
1844
+ );
1845
+ attempt.status = failure ? "unknown" : "cancelled";
1846
+ attempt.participationStatus = failure
1847
+ ? "reconciliation_required"
1848
+ : "cancelled";
1849
+ attempt.updatedAt = nowIso(this.now);
1850
+ const state = run.nodeStates.get(attempt.nodeId);
1851
+ state.status = failure ? "reconciliation_required" : "cancelled";
1852
+ state.updatedAt = nowIso(this.now);
1853
+ }
1854
+ const unsettledEffects = [...run.effects.values()].filter(
1855
+ (effect) => effect.status === "started",
1856
+ );
1857
+ for (const effect of unsettledEffects) {
1858
+ effect.status = "unknown";
1859
+ effect.updatedAt = nowIso(this.now);
1860
+ const state = run.nodeStates.get(effect.nodeId);
1861
+ state.status = "reconciliation_required";
1862
+ state.updatedAt = nowIso(this.now);
1863
+ }
1864
+ const requiresReconciliation =
1865
+ failures.length > 0 || unsettledEffects.length > 0;
1866
+ run.status = requiresReconciliation
1867
+ ? "reconciliation_required"
1868
+ : "cancelled";
1869
+ run.completedAt = requiresReconciliation ? null : nowIso(this.now);
1870
+ return runProjection(run);
1871
+ },
1872
+ );
1873
+ }
1874
+
1875
+ sendMessage(
1876
+ runId,
1877
+ {
1878
+ messageId = this.createId(),
1879
+ fromAttemptId,
1880
+ leaseId,
1881
+ fence,
1882
+ toAgentId,
1883
+ mode = "send",
1884
+ payload,
1885
+ dataPolicy,
1886
+ causationId = null,
1887
+ correlationId = null,
1888
+ ttlMs = 24 * 60 * 60 * 1000,
1889
+ } = {},
1890
+ ) {
1891
+ const run = this._run(runId);
1892
+ const id = safeIdentifier(messageId, "messageId");
1893
+ const recipient = safeIdentifier(toAgentId, "toAgentId");
1894
+ const contentDigest = graphDigest(
1895
+ { recipient, mode, payload, causationId, correlationId },
1896
+ "cc.graph.message/v1",
1897
+ );
1898
+ const existing = run.messages.get(id);
1899
+ if (existing) {
1900
+ if (existing.payloadDigest !== contentDigest) {
1901
+ throw kernelError(
1902
+ "CC_GRAPH_MESSAGE_ID_CONFLICT",
1903
+ "messageId was reused with different content",
1904
+ );
1905
+ }
1906
+ return Object.freeze(clone(existing));
1907
+ }
1908
+ return this._transaction(
1909
+ run,
1910
+ "message.admitted",
1911
+ { messageId: id, toAgentId: recipient, mode },
1912
+ () => {
1913
+ const attempt = this._requireAttemptLease(
1914
+ run,
1915
+ fromAttemptId,
1916
+ leaseId,
1917
+ fence,
1918
+ );
1919
+ const policy = validateDataPolicy(dataPolicy);
1920
+ const sink = `agent:${recipient}`;
1921
+ if (!sinkAllowed(policy, sink)) {
1922
+ throw kernelError(
1923
+ "CC_GRAPH_MESSAGE_SINK_DENIED",
1924
+ `message data policy does not allow ${sink}`,
1925
+ );
1926
+ }
1927
+ if (
1928
+ policy.trust === "untrusted_content" &&
1929
+ containsAuthorityPayload(payload)
1930
+ ) {
1931
+ throw kernelError(
1932
+ "CC_GRAPH_UNTRUSTED_AUTHORITY_PAYLOAD",
1933
+ "untrusted content cannot create approval, capability, or control authority",
1934
+ );
1935
+ }
1936
+ const pending = [...run.messages.values()].filter(
1937
+ (message) =>
1938
+ message.toAgentId === recipient &&
1939
+ ["admitted", "delivered", "read"].includes(message.status),
1940
+ );
1941
+ if (pending.length >= this.maxPendingMessagesPerAgent) {
1942
+ throw kernelError(
1943
+ "CC_GRAPH_MESSAGE_BACKPRESSURE",
1944
+ `message queue is full for ${recipient}`,
1945
+ { retryAfterMs: 100 },
1946
+ );
1947
+ }
1948
+ const now = this.now();
1949
+ const message = {
1950
+ id,
1951
+ runId: run.id,
1952
+ fromAttemptId: attempt.id,
1953
+ fromLeaseId: attempt.leaseId,
1954
+ fromFence: attempt.fence,
1955
+ toAgentId: recipient,
1956
+ causationId,
1957
+ correlationId,
1958
+ mode: mode === "followup" ? "followup" : "send",
1959
+ status: "admitted",
1960
+ payload: clone(payload),
1961
+ payloadDigest: contentDigest,
1962
+ dataPolicy: policy,
1963
+ createdAt: nowIso(() => now),
1964
+ updatedAt: nowIso(() => now),
1965
+ expiresAt: nowIso(() => now + finitePositive(ttlMs, 86_400_000)),
1966
+ expiresAtMs: now + finitePositive(ttlMs, 86_400_000),
1967
+ deliveryCount: 0,
1968
+ readAt: null,
1969
+ processedAt: null,
1970
+ };
1971
+ run.messages.set(id, message);
1972
+ return Object.freeze(clone(message));
1973
+ },
1974
+ { idempotencyKey: `message:${id}` },
1975
+ );
1976
+ }
1977
+
1978
+ deliverMessage(runId, messageId) {
1979
+ const run = this._run(runId);
1980
+ const id = safeIdentifier(messageId, "messageId");
1981
+ return this._transaction(
1982
+ run,
1983
+ "message.delivered",
1984
+ { messageId: id },
1985
+ () => {
1986
+ const message = run.messages.get(id);
1987
+ if (!message) {
1988
+ throw kernelError(
1989
+ "CC_GRAPH_MESSAGE_NOT_FOUND",
1990
+ `message not found: ${id}`,
1991
+ );
1992
+ }
1993
+ if (message.status === "processed")
1994
+ return Object.freeze(clone(message));
1995
+ if (message.expiresAtMs <= this.now()) {
1996
+ message.status = "dead_letter";
1997
+ message.reason = "expired";
1998
+ } else {
1999
+ message.status = "delivered";
2000
+ message.deliveryCount += 1;
2001
+ }
2002
+ message.updatedAt = nowIso(this.now);
2003
+ return Object.freeze(clone(message));
2004
+ },
2005
+ );
2006
+ }
2007
+
2008
+ receiveMessages(runId, agentId, { markRead = true } = {}) {
2009
+ const run = this._run(runId);
2010
+ const recipient = safeIdentifier(agentId, "agentId");
2011
+ return this._transaction(
2012
+ run,
2013
+ markRead ? "message.read" : "message.received",
2014
+ { agentId: recipient },
2015
+ () => {
2016
+ const messages = [...run.messages.values()]
2017
+ .filter(
2018
+ (message) =>
2019
+ message.toAgentId === recipient &&
2020
+ ["delivered", "read"].includes(message.status),
2021
+ )
2022
+ .sort(
2023
+ (left, right) =>
2024
+ left.createdAt.localeCompare(right.createdAt) ||
2025
+ left.id.localeCompare(right.id),
2026
+ );
2027
+ if (markRead) {
2028
+ for (const message of messages) {
2029
+ message.status = "read";
2030
+ message.readAt ||= nowIso(this.now);
2031
+ message.updatedAt = nowIso(this.now);
2032
+ }
2033
+ }
2034
+ return Object.freeze(
2035
+ messages.map((message) => Object.freeze(clone(message))),
2036
+ );
2037
+ },
2038
+ );
2039
+ }
2040
+
2041
+ processMessage(runId, messageId, agentId, consumerKey) {
2042
+ const run = this._run(runId);
2043
+ const id = safeIdentifier(messageId, "messageId");
2044
+ const recipient = safeIdentifier(agentId, "agentId");
2045
+ const key = safeIdentifier(consumerKey, "consumerKey");
2046
+ const dedupKey = `${recipient}\0${id}\0${key}`;
2047
+ if (run.messageConsumers.has(dedupKey)) {
2048
+ return Object.freeze(clone(run.messageConsumers.get(dedupKey)));
2049
+ }
2050
+ return this._transaction(
2051
+ run,
2052
+ "message.processed",
2053
+ { messageId: id, agentId: recipient, consumerKey: key },
2054
+ () => {
2055
+ const message = run.messages.get(id);
2056
+ if (!message || message.toAgentId !== recipient) {
2057
+ throw kernelError(
2058
+ "CC_GRAPH_MESSAGE_NOT_FOUND",
2059
+ `message is not addressed to ${recipient}`,
2060
+ );
2061
+ }
2062
+ if (!["delivered", "read", "processed"].includes(message.status)) {
2063
+ throw kernelError(
2064
+ "CC_GRAPH_MESSAGE_NOT_DELIVERED",
2065
+ "message must be delivered before processing",
2066
+ );
2067
+ }
2068
+ message.status = "processed";
2069
+ message.readAt ||= nowIso(this.now);
2070
+ message.processedAt ||= nowIso(this.now);
2071
+ message.updatedAt = nowIso(this.now);
2072
+ const receipt = {
2073
+ messageId: id,
2074
+ agentId: recipient,
2075
+ consumerKey: key,
2076
+ payloadDigest: message.payloadDigest,
2077
+ processedAt: message.processedAt,
2078
+ };
2079
+ run.messageConsumers.set(dedupKey, receipt);
2080
+ return Object.freeze(clone(receipt));
2081
+ },
2082
+ {
2083
+ idempotencyKey: `message-processed:${graphDigest(
2084
+ dedupKey,
2085
+ "cc.graph.message-consumer/v1",
2086
+ )}`,
2087
+ },
2088
+ );
2089
+ }
2090
+
2091
+ deadLetterMessage(runId, messageId, reason = "poison_message") {
2092
+ const run = this._run(runId);
2093
+ const id = safeIdentifier(messageId, "messageId");
2094
+ return this._transaction(
2095
+ run,
2096
+ "message.dead_lettered",
2097
+ { messageId: id, reason },
2098
+ () => {
2099
+ const message = run.messages.get(id);
2100
+ if (!message) {
2101
+ throw kernelError(
2102
+ "CC_GRAPH_MESSAGE_NOT_FOUND",
2103
+ `message not found: ${id}`,
2104
+ );
2105
+ }
2106
+ message.status = "dead_letter";
2107
+ message.reason = String(reason).slice(0, 1024);
2108
+ message.updatedAt = nowIso(this.now);
2109
+ return Object.freeze(clone(message));
2110
+ },
2111
+ );
2112
+ }
2113
+
2114
+ offerHandoff(
2115
+ runId,
2116
+ {
2117
+ handoffId = this.createId(),
2118
+ fromAttemptId,
2119
+ leaseId,
2120
+ fence,
2121
+ toAgentId,
2122
+ artifactIds = [],
2123
+ preconditions = {},
2124
+ ttlMs = 60_000,
2125
+ } = {},
2126
+ ) {
2127
+ const run = this._run(runId);
2128
+ const id = safeIdentifier(handoffId, "handoffId");
2129
+ return this._transaction(
2130
+ run,
2131
+ "handoff.offered",
2132
+ { handoffId: id, fromAttemptId, toAgentId },
2133
+ () => {
2134
+ const existing = run.handoffs.get(id);
2135
+ if (existing) return Object.freeze(clone(existing));
2136
+ const attempt = this._requireAttemptLease(
2137
+ run,
2138
+ fromAttemptId,
2139
+ leaseId,
2140
+ fence,
2141
+ );
2142
+ for (const artifactId of artifactIds) {
2143
+ if (!run.artifacts.has(artifactId)) {
2144
+ throw kernelError(
2145
+ "CC_GRAPH_ARTIFACT_NOT_FOUND",
2146
+ `handoff artifact not found: ${artifactId}`,
2147
+ );
2148
+ }
2149
+ }
2150
+ const now = this.now();
2151
+ const handoff = {
2152
+ id,
2153
+ runId: run.id,
2154
+ nodeId: attempt.nodeId,
2155
+ fromAttemptId: attempt.id,
2156
+ fromLeaseId: attempt.leaseId,
2157
+ fromFence: attempt.fence,
2158
+ toAgentId: safeIdentifier(toAgentId, "toAgentId"),
2159
+ revisionDigest: run.revisionDigest,
2160
+ authorityDigest: run.authorityDigest,
2161
+ artifactIds: [...artifactIds],
2162
+ preconditions: clone(preconditions),
2163
+ status: "offered",
2164
+ createdAt: nowIso(() => now),
2165
+ updatedAt: nowIso(() => now),
2166
+ expiresAt: nowIso(() => now + finitePositive(ttlMs, 60_000)),
2167
+ expiresAtMs: now + finitePositive(ttlMs, 60_000),
2168
+ };
2169
+ run.handoffs.set(id, handoff);
2170
+ return Object.freeze(clone(handoff));
2171
+ },
2172
+ { idempotencyKey: `handoff:${id}` },
2173
+ );
2174
+ }
2175
+
2176
+ acceptHandoff(runId, handoffId, agentId) {
2177
+ const run = this._run(runId);
2178
+ const id = safeIdentifier(handoffId, "handoffId");
2179
+ const recipient = safeIdentifier(agentId, "agentId");
2180
+ return this._transaction(
2181
+ run,
2182
+ "handoff.accepted",
2183
+ { handoffId: id, agentId: recipient },
2184
+ () => {
2185
+ const handoff = run.handoffs.get(id);
2186
+ if (
2187
+ !handoff ||
2188
+ handoff.status !== "offered" ||
2189
+ handoff.toAgentId !== recipient ||
2190
+ handoff.expiresAtMs <= this.now()
2191
+ ) {
2192
+ throw kernelError(
2193
+ "CC_GRAPH_HANDOFF_NOT_ACCEPTABLE",
2194
+ "handoff is missing, expired, or addressed to another agent",
2195
+ );
2196
+ }
2197
+ handoff.status = "accepted";
2198
+ handoff.updatedAt = nowIso(this.now);
2199
+ return Object.freeze(clone(handoff));
2200
+ },
2201
+ );
2202
+ }
2203
+
2204
+ rejectHandoff(runId, handoffId, agentId, reason = "rejected") {
2205
+ const run = this._run(runId);
2206
+ const id = safeIdentifier(handoffId, "handoffId");
2207
+ const recipient = safeIdentifier(agentId, "agentId");
2208
+ return this._transaction(
2209
+ run,
2210
+ "handoff.rejected",
2211
+ { handoffId: id, reason },
2212
+ () => {
2213
+ const handoff = run.handoffs.get(id);
2214
+ if (
2215
+ !handoff ||
2216
+ handoff.status !== "offered" ||
2217
+ handoff.toAgentId !== recipient
2218
+ ) {
2219
+ throw kernelError(
2220
+ "CC_GRAPH_HANDOFF_NOT_REJECTABLE",
2221
+ "handoff cannot be rejected in its current state",
2222
+ );
2223
+ }
2224
+ handoff.status = "rejected";
2225
+ handoff.reason = String(reason).slice(0, 1024);
2226
+ handoff.updatedAt = nowIso(this.now);
2227
+ return Object.freeze(clone(handoff));
2228
+ },
2229
+ );
2230
+ }
2231
+
2232
+ commitHandoff(
2233
+ runId,
2234
+ handoffId,
2235
+ {
2236
+ leaseId = this.createId(),
2237
+ attemptId = this.createId(),
2238
+ ttlMs = 60_000,
2239
+ } = {},
2240
+ ) {
2241
+ const run = this._run(runId);
2242
+ const id = safeIdentifier(handoffId, "handoffId");
2243
+ return this._transaction(
2244
+ run,
2245
+ "handoff.committed",
2246
+ { handoffId: id },
2247
+ () => {
2248
+ const handoff = run.handoffs.get(id);
2249
+ if (!handoff || handoff.status !== "accepted") {
2250
+ throw kernelError(
2251
+ "CC_GRAPH_HANDOFF_NOT_COMMITTABLE",
2252
+ "handoff must be accepted before custody can transfer",
2253
+ );
2254
+ }
2255
+ if (
2256
+ handoff.revisionDigest !== run.revisionDigest ||
2257
+ handoff.authorityDigest !== run.authorityDigest
2258
+ ) {
2259
+ throw kernelError(
2260
+ "CC_GRAPH_HANDOFF_BINDING_STALE",
2261
+ "handoff binding no longer matches the GraphRun",
2262
+ );
2263
+ }
2264
+ const sender = this._requireAttemptLease(
2265
+ run,
2266
+ handoff.fromAttemptId,
2267
+ handoff.fromLeaseId,
2268
+ handoff.fromFence,
2269
+ );
2270
+ const now = this.now();
2271
+ sender.status = "expired";
2272
+ sender.participationStatus = "handed_off";
2273
+ sender.updatedAt = nowIso(() => now);
2274
+ const fence = (run.fenceCounters.get(sender.nodeId) || 0) + 1;
2275
+ run.fenceCounters.set(sender.nodeId, fence);
2276
+ const newAttempt = {
2277
+ id: safeIdentifier(attemptId, "attemptId"),
2278
+ runId: run.id,
2279
+ nodeId: sender.nodeId,
2280
+ agentId: handoff.toAgentId,
2281
+ role: "executor",
2282
+ leaseId: safeIdentifier(leaseId, "leaseId"),
2283
+ fence,
2284
+ status: "active",
2285
+ participationStatus: "active",
2286
+ grant: clone(sender.grant),
2287
+ createdAt: nowIso(() => now),
2288
+ updatedAt: nowIso(() => now),
2289
+ expiresAt: nowIso(() => now + finitePositive(ttlMs, 60_000)),
2290
+ expiresAtMs: now + finitePositive(ttlMs, 60_000),
2291
+ terminalEvidence: null,
2292
+ usage: null,
2293
+ };
2294
+ run.attempts.set(newAttempt.id, newAttempt);
2295
+ run.nodeStates.get(sender.nodeId).attemptIds.push(newAttempt.id);
2296
+ handoff.status = "committed";
2297
+ handoff.committedAttemptId = newAttempt.id;
2298
+ handoff.updatedAt = nowIso(() => now);
2299
+ return Object.freeze({
2300
+ handoff: Object.freeze(clone(handoff)),
2301
+ assignmentAttempt: attemptProjection(newAttempt),
2302
+ });
2303
+ },
2304
+ );
2305
+ }
2306
+
2307
+ revokeHandoff(runId, handoffId, fromAttemptId, leaseId, fence) {
2308
+ const run = this._run(runId);
2309
+ const id = safeIdentifier(handoffId, "handoffId");
2310
+ return this._transaction(run, "handoff.revoked", { handoffId: id }, () => {
2311
+ const handoff = run.handoffs.get(id);
2312
+ if (!handoff || !["offered", "accepted"].includes(handoff.status)) {
2313
+ throw kernelError(
2314
+ "CC_GRAPH_HANDOFF_NOT_REVOKABLE",
2315
+ "committed or terminal handoffs cannot be revoked",
2316
+ );
2317
+ }
2318
+ this._requireAttemptLease(run, fromAttemptId, leaseId, fence);
2319
+ if (handoff.fromAttemptId !== fromAttemptId) {
2320
+ throw kernelError(
2321
+ "CC_GRAPH_HANDOFF_SENDER_MISMATCH",
2322
+ "only the current custody holder can revoke the handoff",
2323
+ );
2324
+ }
2325
+ handoff.status = "revoked";
2326
+ handoff.updatedAt = nowIso(this.now);
2327
+ return Object.freeze(clone(handoff));
2328
+ });
2329
+ }
2330
+
2331
+ createHumanTask(
2332
+ runId,
2333
+ {
2334
+ humanTaskId = this.createId(),
2335
+ attemptId,
2336
+ leaseId,
2337
+ fence,
2338
+ operation,
2339
+ nonce = this.createId(),
2340
+ ttlMs = 24 * 60 * 60 * 1000,
2341
+ quorum = 1,
2342
+ separationOfDuties = false,
2343
+ } = {},
2344
+ ) {
2345
+ const run = this._run(runId);
2346
+ const id = safeIdentifier(humanTaskId, "humanTaskId");
2347
+ return this._transaction(
2348
+ run,
2349
+ "human_task.created",
2350
+ { humanTaskId: id, attemptId },
2351
+ () => {
2352
+ const attempt = this._requireAttemptLease(
2353
+ run,
2354
+ attemptId,
2355
+ leaseId,
2356
+ fence,
2357
+ );
2358
+ const now = this.now();
2359
+ attempt.status = "waiting_human";
2360
+ attempt.participationStatus = "waiting_human";
2361
+ attempt.updatedAt = nowIso(() => now);
2362
+ const state = run.nodeStates.get(attempt.nodeId);
2363
+ state.status = "waiting_human";
2364
+ state.updatedAt = nowIso(() => now);
2365
+ const agent = run.agents.get(attempt.agentId);
2366
+ if (agent) {
2367
+ agent.status = "idle";
2368
+ agent.updatedAt = nowIso(() => now);
2369
+ }
2370
+ const task = {
2371
+ id,
2372
+ runId: run.id,
2373
+ revisionDigest: run.revisionDigest,
2374
+ nodeId: attempt.nodeId,
2375
+ attemptId: attempt.id,
2376
+ operation: clone(operation),
2377
+ operationDigest: graphDigest(
2378
+ operation,
2379
+ "cc.graph.human-operation/v1",
2380
+ ),
2381
+ authorityDigest: run.authorityDigest,
2382
+ status: "open",
2383
+ nonce: safeIdentifier(nonce, "nonce"),
2384
+ quorum: Math.max(1, Number(quorum) || 1),
2385
+ separationOfDuties: separationOfDuties === true,
2386
+ decisions: [],
2387
+ claimActorId: null,
2388
+ claimLeaseId: null,
2389
+ claimExpiresAtMs: null,
2390
+ createdAt: nowIso(() => now),
2391
+ updatedAt: nowIso(() => now),
2392
+ expiresAt: nowIso(() => now + finitePositive(ttlMs, 86_400_000)),
2393
+ expiresAtMs: now + finitePositive(ttlMs, 86_400_000),
2394
+ decision: null,
2395
+ };
2396
+ run.humanTasks.set(id, task);
2397
+ run.status = "waiting_human";
2398
+ return Object.freeze(clone(task));
2399
+ },
2400
+ { idempotencyKey: `human-task:${id}` },
2401
+ );
2402
+ }
2403
+
2404
+ claimHumanTask(
2405
+ runId,
2406
+ humanTaskId,
2407
+ actorId,
2408
+ { claimLeaseId = this.createId(), ttlMs = 5 * 60 * 1000 } = {},
2409
+ ) {
2410
+ const run = this._run(runId);
2411
+ const id = safeIdentifier(humanTaskId, "humanTaskId");
2412
+ const actor = safeIdentifier(actorId, "actorId");
2413
+ return this._transaction(
2414
+ run,
2415
+ "human_task.claimed",
2416
+ { humanTaskId: id, actorId: actor },
2417
+ () => {
2418
+ const task = run.humanTasks.get(id);
2419
+ const now = this.now();
2420
+ if (
2421
+ !task ||
2422
+ !["open", "claimed"].includes(task.status) ||
2423
+ task.expiresAtMs <= now
2424
+ ) {
2425
+ throw kernelError(
2426
+ "CC_GRAPH_HUMAN_TASK_NOT_CLAIMABLE",
2427
+ "human task is missing, expired, or terminal",
2428
+ );
2429
+ }
2430
+ if (
2431
+ task.status === "claimed" &&
2432
+ task.claimExpiresAtMs > now &&
2433
+ task.claimActorId !== actor
2434
+ ) {
2435
+ throw kernelError(
2436
+ "CC_GRAPH_HUMAN_TASK_CLAIMED",
2437
+ "human task is already claimed",
2438
+ );
2439
+ }
2440
+ task.status = "claimed";
2441
+ task.claimActorId = actor;
2442
+ task.claimLeaseId = safeIdentifier(claimLeaseId, "claimLeaseId");
2443
+ task.claimExpiresAtMs = now + finitePositive(ttlMs, 300_000);
2444
+ task.updatedAt = nowIso(() => now);
2445
+ return Object.freeze(clone(task));
2446
+ },
2447
+ );
2448
+ }
2449
+
2450
+ decideHumanTask(
2451
+ runId,
2452
+ humanTaskId,
2453
+ {
2454
+ actorId,
2455
+ claimLeaseId,
2456
+ revisionDigest,
2457
+ operationDigest,
2458
+ nonce,
2459
+ decision,
2460
+ } = {},
2461
+ ) {
2462
+ const run = this._run(runId);
2463
+ const id = safeIdentifier(humanTaskId, "humanTaskId");
2464
+ return this._transaction(
2465
+ run,
2466
+ "human_task.decided",
2467
+ { humanTaskId: id, actorId },
2468
+ () => {
2469
+ const task = run.humanTasks.get(id);
2470
+ const now = this.now();
2471
+ if (
2472
+ !task ||
2473
+ task.status !== "claimed" ||
2474
+ task.claimActorId !== safeIdentifier(actorId, "actorId") ||
2475
+ task.claimLeaseId !== safeIdentifier(claimLeaseId, "claimLeaseId") ||
2476
+ task.claimExpiresAtMs <= now
2477
+ ) {
2478
+ throw kernelError(
2479
+ "CC_GRAPH_HUMAN_TASK_STALE_CLAIM",
2480
+ "human task claim is stale or owned by another actor",
2481
+ );
2482
+ }
2483
+ if (
2484
+ task.revisionDigest !== revisionDigest ||
2485
+ task.operationDigest !== operationDigest ||
2486
+ task.nonce !== nonce ||
2487
+ task.authorityDigest !== run.authorityDigest
2488
+ ) {
2489
+ throw kernelError(
2490
+ "CC_GRAPH_HUMAN_TASK_BINDING_MISMATCH",
2491
+ "human decision does not match the bound revision, operation, nonce, or authority",
2492
+ );
2493
+ }
2494
+ if (
2495
+ !decision ||
2496
+ ![
2497
+ "acceptOnce",
2498
+ "acceptForTurn",
2499
+ "acceptForSession",
2500
+ "decline",
2501
+ "cancel",
2502
+ ].includes(decision.kind)
2503
+ ) {
2504
+ throw kernelError(
2505
+ "CC_GRAPH_HUMAN_DECISION_INVALID",
2506
+ "human decision kind is invalid",
2507
+ );
2508
+ }
2509
+ if (
2510
+ task.separationOfDuties &&
2511
+ task.decisions.some((entry) => entry.actorId === actorId)
2512
+ ) {
2513
+ throw kernelError(
2514
+ "CC_GRAPH_HUMAN_SEPARATION_OF_DUTIES",
2515
+ "the same actor cannot satisfy multiple separated decisions",
2516
+ );
2517
+ }
2518
+ task.decisions.push({
2519
+ actorId,
2520
+ decision: clone(decision),
2521
+ decidedAt: nowIso(() => now),
2522
+ });
2523
+ const accepted = task.decisions.filter((entry) =>
2524
+ ["acceptOnce", "acceptForTurn", "acceptForSession"].includes(
2525
+ entry.decision.kind,
2526
+ ),
2527
+ ).length;
2528
+ const isDecline = ["decline", "cancel"].includes(decision.kind);
2529
+ if (isDecline || accepted >= task.quorum) {
2530
+ task.status = "decided";
2531
+ task.decision = clone(decision);
2532
+ const attempt = run.attempts.get(task.attemptId);
2533
+ if (attempt && attempt.status === "waiting_human") {
2534
+ attempt.status = "expired";
2535
+ attempt.participationStatus = "human_decided";
2536
+ attempt.updatedAt = nowIso(() => now);
2537
+ }
2538
+ const state = run.nodeStates.get(task.nodeId);
2539
+ state.status = isDecline
2540
+ ? decision.kind === "cancel"
2541
+ ? "cancelled"
2542
+ : "failed"
2543
+ : "pending";
2544
+ state.blockedRoot = isDecline ? task.id : null;
2545
+ state.updatedAt = nowIso(() => now);
2546
+ } else {
2547
+ task.status = "open";
2548
+ task.claimActorId = null;
2549
+ task.claimLeaseId = null;
2550
+ task.claimExpiresAtMs = null;
2551
+ }
2552
+ task.updatedAt = nowIso(() => now);
2553
+ run.status = classifyQuiescence(run, now);
2554
+ return Object.freeze(clone(task));
2555
+ },
2556
+ );
2557
+ }
2558
+
2559
+ cancelHumanTask(runId, humanTaskId, reason = "cancelled") {
2560
+ const run = this._run(runId);
2561
+ const id = safeIdentifier(humanTaskId, "humanTaskId");
2562
+ return this._transaction(
2563
+ run,
2564
+ "human_task.cancelled",
2565
+ { humanTaskId: id, reason },
2566
+ () => {
2567
+ const task = run.humanTasks.get(id);
2568
+ if (!task || !["open", "claimed"].includes(task.status)) {
2569
+ throw kernelError(
2570
+ "CC_GRAPH_HUMAN_TASK_TERMINAL",
2571
+ "human task is already terminal",
2572
+ );
2573
+ }
2574
+ task.status = "cancelled";
2575
+ task.reason = String(reason).slice(0, 1024);
2576
+ task.updatedAt = nowIso(this.now);
2577
+ const state = run.nodeStates.get(task.nodeId);
2578
+ state.status = "cancelled";
2579
+ state.blockedRoot = task.id;
2580
+ state.updatedAt = nowIso(this.now);
2581
+ return Object.freeze(clone(task));
2582
+ },
2583
+ );
2584
+ }
2585
+
2586
+ setWaitReason(runId, nodeId, reason) {
2587
+ const run = this._run(runId);
2588
+ const id = safeIdentifier(nodeId, "nodeId");
2589
+ return this._transaction(
2590
+ run,
2591
+ "wait.updated",
2592
+ { nodeId: id, reason },
2593
+ () => {
2594
+ if (!run.nodeStates.has(id)) {
2595
+ throw kernelError(
2596
+ "CC_GRAPH_NODE_NOT_FOUND",
2597
+ `Graph node not found: ${id}`,
2598
+ );
2599
+ }
2600
+ if (reason == null) {
2601
+ run.waitReasons.delete(id);
2602
+ return null;
2603
+ }
2604
+ const normalized = {
2605
+ kind: String(reason.kind || "external"),
2606
+ resourceId:
2607
+ reason.resourceId == null
2608
+ ? null
2609
+ : String(reason.resourceId).slice(0, 256),
2610
+ ownerNodeId:
2611
+ reason.ownerNodeId == null
2612
+ ? null
2613
+ : safeIdentifier(reason.ownerNodeId, "ownerNodeId"),
2614
+ since: nowIso(this.now),
2615
+ };
2616
+ run.waitReasons.set(id, normalized);
2617
+ return Object.freeze(clone(normalized));
2618
+ },
2619
+ );
2620
+ }
2621
+
2622
+ recordProgressDigest(runId, value) {
2623
+ const run = this._run(runId);
2624
+ const digest = graphDigest(value, "cc.graph.progress/v1");
2625
+ return this._transaction(run, "progress.observed", { digest }, () => {
2626
+ const count = (run.progressDigests.get(digest) || 0) + 1;
2627
+ run.progressDigests.set(digest, count);
2628
+ if (count >= this.maxLivelockRepeats) {
2629
+ run.status = "reconciliation_required";
2630
+ }
2631
+ return Object.freeze({
2632
+ digest,
2633
+ count,
2634
+ livelockSuspected: count >= this.maxLivelockRepeats,
2635
+ });
2636
+ });
2637
+ }
2638
+
2639
+ classify(runId) {
2640
+ const run = this._run(runId);
2641
+ return Object.freeze({
2642
+ status: classifyQuiescence(run, this.now()),
2643
+ waitCycles: Object.freeze(waitGraphCycles(run).map(Object.freeze)),
2644
+ readyNodeIds: Object.freeze(
2645
+ this.readyNodes(runId).map((entry) => entry.nodeId),
2646
+ ),
2647
+ });
2648
+ }
2649
+ }
2650
+
2651
+ export const _graphKernelInternals = Object.freeze({
2652
+ classifyQuiescence,
2653
+ dependencySatisfied,
2654
+ nodeReadiness,
2655
+ propagate,
2656
+ restoreState,
2657
+ stateSnapshot,
2658
+ terminalAlgebra,
2659
+ waitGraphCycles,
2660
+ });