chainlesschain 0.166.0 → 0.166.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-D5_pwQ7M.js → AIOps-BnZ24WRk.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DgoYO32D.js → ActionButton-ivXY7MGb.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BIpoZ4QH.js → Analytics-BQS4OD-k.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-BJ3XrCEe.js → AppLayout-wOBuOKTO.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-DUFs_izZ.js → Artifacts-BYuC4nw-.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-BZdPwwfL.js → Audit-BIeVfBt6.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DK9Fn-8B.js → BackgroundAgents-hIuyhmJ7.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Bd6bOhV_.js → Backup-B6ohdgPQ.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CRnWlXrd.js → BaseInput-BzgYCw6h.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-D1G_yEl6.js → Chat-CwahvZU1.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDAlEtm-.js → ChatBubbleRenderer-B-1IZ_k8.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-CP35SH7R.js → Checkbox-26EDP2x0.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-DugeL4pj.js → Codegen-DYLw1Zvd.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DPuJsvKg.js → Col-DH_zAvD-.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CZqNjNNp.js → Community-DURrTXJu.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-eeyIPEAb.js → Compact-CQyBqN0p.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-MGzTw87k.js → Compliance-a0DfgKHD.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-D3goVIA5.js → Cowork-C6fY_5Ke.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-9HA8hLLG.js → Cron-DOwmpbTe.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DCSOX3l6.js → Crosschain-DEPOIEUs.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-ji3lN5Bc.js → DID-DaKc-CEV.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-DlZvF1E5.js → Dashboard-C-0KLXq5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-2FyZcow9.js → Dropdown-CnsDukzr.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-qH2bSpSY.js → EmailListRenderer-Bi_ACD4N.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bfx47wZj.js → FamilyGuardDashboard-BZASpnx3.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DhGcZbIv.js → Federation-43oTtzyp.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-BMgi9_YQ.js → FormItemContext-oyVadnkZ.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DugMac-4.js → GenericCardRenderer-Cwvfd6TZ.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-D943BpH-.js → Git-Cgi2wvU0.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DZ66a8Aa.js → Governance-l8TWDKZ0.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-B00Mp2tN.js → Inference-Dc8j6TaO.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BdwXXO2Q.js → KnowledgeGraph-DgcO-Fwz.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-_MVXyYWc.js → Logs-DjiZpiwa.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-hOTeNyi7.js → MarkdownRenderer-B5MFCi0N.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-CxeHRsnO.js → Marketplace-BeDn1hFc.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BOieTxZ4.js → McpTools-PGdVfWK6.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-BQD26CM3.js → Memory-e76lY3a1.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CPJ8ZlvR.js → MobileBridge-DKlQqot_.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-D7qRVu0y.js → MobileProjects-C4g6WOEr.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-BOyndGuk.js → Mtc-1j3NpRQq.js} +4 -4
  42. package/src/assets/web-panel/assets/{MtcAudit-CXQ_8ZoR.js → MtcAudit-DgwiVd1C.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-32Oh9KbH.js → Multisig-Dzg5lZZY.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CFAd00CX.js → NLProgramming-D7GYhst-.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-BBCY_qJA.js → Notes-Blcfvaic.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-uaQ0PdmW.js → NotificationSettings-Dc4Y6vfr.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-dgM87XIn.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-CmFn7Oeo.js → Organization-DBXq024L.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-R_avat3a.js → Overflow-CFPbdx1Y.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BIhLFmkY.js → P2P-w7z61NR5.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DTJjk-_i.js → PdhVaultBrowser-B7eGGZOa.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-CkzYv8Yb.js → Permissions-Df5Cl50D.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub--0QvN2Nz.js → PersonalDataHub-C-We7RF6.js} +5 -5
  54. package/src/assets/web-panel/assets/{Pipeline-Bfw5gQCH.js → Pipeline-B6fHPnKy.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-DicjKfax.js → Privacy-B85BdiIc.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-DUl_ykBp.js → ProjectInit-CcOyazSe.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-CPYDvN8a.js → ProjectSettings-BE53KfuU.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-DnZ9_fAr.js → Projects-DOvMOoe_.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-BX1jP31w.js → Providers-5l7sT9lq.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-COab5qUl.js → QrScannerModal-DE37RXy4.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CdKQ_JZ0.js → QuickAsk-BGmGCo7j.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BPFHg8pK.js → Recommend-BFdx8Nrm.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-CqrfTJvO.js → RemoteSession-C1w-j_hB.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-DM-l8Pb3.js → Reputation-BOvn-10y.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-CKGBTeCw.js → Row-db-a1YO9.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-Wb0YHIkc.js → RssFeed-DeS43ZGM.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DiBjxKv-.js → Search-BaQYNfeb.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-BAVEmbWP.js → Security-yt1ESHDg.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-A_a3TIRn.js → Services-BcNqPUsS.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-CloHJ-I7.js → Skeleton-X3ceUQWf.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CkvV5hKa.js → Skills-DmYeax6C.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-BYozxxsV.js → Sla-C_fdfbuJ.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-CWianiZF.js → SpeechSettings-CDqohXXQ.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-BR4G75do.js → SyncSettings-B25maYEY.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-D6gyUVrM.js → Tasks-3ILwauYw.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-DbfYIRzT.js → Templates-DUH0iuc4.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BHutvukT.js → Tenant-DrvhHf9T.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-CYEiy2wJ.js → Terminal-C2lJVYHA.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-D9s1AxfE.js → TimelineRenderer-C1Hf_jYn.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-ij9vxnoy.js → Tokens-DniS1NpS.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Dq_DCVeQ.js → Trigger-4jrcb8oA.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-Bs6wdfoH.js → Trust-DiBte55w.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CLazyr5t.js → UkeySign-CZRtc4VB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-viWotIHA.js → VideoEditing-BtYGJDmg.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DlZ-A78d.js → Wallet-D1nrTm7H.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-DSmOND3Y.js → WebAuthn-DI91acpx.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-BempLOJi.js → WorkflowEditor-BtQjI50d.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CyJzf4JI.js → chat-I4A-f4FC.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-C62q7WrL.js → colors-QZkDkFVt.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-7dYM1KW3.js → compact-item--FtCdzpr.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CYTy7z2Q.js → createContext-CaIKIKxP.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-D7qVrkrO.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DHoclUqm.js → hasIn-DLYUII6-.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CW86F2Zw.js → index-0DYBQ-6e.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-C2efftJb.js → index-21I79XSf.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-4gWv-Ubg.js → index-3rVtgqa2.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-QXtrdrub.js → index-B65QxJaG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BMxvWauP.js → index-BGWSmE3G.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dyq9tK5u.js → index-BLGJmgmx.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DYKwGrAn.js → index-BcgtvJbC.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BMRmxFU_.js → index-BebTT7iR.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-XrsAHHQN.js → index-BkJgWW_L.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-2C8Xx2UO.js → index-Bs0qPPFb.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CnEZ4ZHj.js → index-BvdwNnHC.js} +3 -3
  105. package/src/assets/web-panel/assets/{index-DkczsbiP.js → index-BxPoPofq.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DvEtHpLc.js → index-C3PnIEOH.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DK84lAS0.js → index-CBtII-nl.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DKdv8cju.js → index-CL4c5D6G.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CjeViaU-.js → index-CQ7OBHka.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DhJjifZe.js → index-CXaeZCRP.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BghdJ87W.js → index-CgMm85BY.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-lHfNxiB5.js → index-Ch0IFpGy.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DJzZaPlb.js → index-CisYjy88.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DghnbMlY.js → index-D2xETCkj.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C5YWYlW9.js → index-D4CXFU-w.js} +1 -1
  116. package/src/assets/web-panel/assets/index-D9Y_ZwZn.js +1 -0
  117. package/src/assets/web-panel/assets/{index-LK1MZoBj.js → index-DA_TPdzX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DOFAwpUK.js → index-DAuf8r5o.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DA-_807C.js → index-DGLHSCHM.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-9uR7rc9a.js → index-DJrd_CR3.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CVTnruDt.js → index-DK2r6Bv7.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-ZXYp76IB.js → index-DVitI3UQ.js} +1 -1
  123. package/src/assets/web-panel/assets/index-D_cPes4G.js +1 -0
  124. package/src/assets/web-panel/assets/{index-Bzz0aJ86.js → index-Deg2S9ZJ.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DHgSRimK.js → index-Du2wUavy.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-94ktimZG.js → index-OQPIS_am.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-3pI7AfmD.js → index-Ooo_5jHo.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DNLD3nZY.js → index-Ygi2rYL1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CZRBvN6i.js → index-eTqL91aV.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-D6BC4mmz.js → index-hfiZ-i2b.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DAYanuSk.js → index-vfiDxdN2.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-NuNCstRO.js → index-yayaayEA.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-CwCl5sFQ.js → initDefaultProps-DDkTvw8k.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BFJFy70N.js → motion-fkr96tN5.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-ByrtVGq8.js → move-CGBV0SV2.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-BFiH1mT3.js → mtc-parser-DAsCliFt.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-bdvgpivj.js → omit-BM8PMmKN.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BlfuuE3s.js → pickAttrs-C6SdWoTK.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-t2SC3DMy.js → placementArrow-BG0EVu-d.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-CoEXv2Li.js → responsiveObserve-L10IvfYC.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BfXCf0Fu.js → slide-TddIkbrn.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-DNsdyOuS.js → statusUtils-FlveF2Yn.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-9aHukmoG.js → styleChecker-BrXRyebg.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-dTkPxzHN.js → useFlexGapSupport-B7_oiDXZ.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DINwfItP.js → useFs-CQYeRYWb.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-Bj_RriBV.js → usePersonalDataHub-DspIkhVi.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-BzRAZrTc.js → vnode-BCwmwMAW.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CamPjkeK.js → zoom-B0cEGluV.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/team.js +74 -2
  151. package/src/data/changelog.json +26 -0
  152. package/src/generated/cc-agent-protocol.schema.json +49 -0
  153. package/src/lib/agent-team/team-mailbox.js +508 -2
  154. package/src/lib/agent-team/team-message-bridge.js +644 -0
  155. package/src/lib/agent-team/team-message-tools.js +198 -0
  156. package/src/lib/agent-team/team-runner.js +57 -3
  157. package/src/lib/graph-kernel/compiler.js +548 -16
  158. package/src/lib/graph-kernel/runtime.js +1158 -30
  159. package/src/lib/graph-kernel/trace-reducer.js +101 -7
  160. package/src/lib/subagent-inheritance.js +5 -3
  161. package/src/runtime/agent-core.js +38 -0
  162. package/src/runtime/headless-runner.js +36 -3
  163. package/src/assets/web-panel/assets/OrderTableRenderer-kpBxJ-SG.js +0 -1
  164. package/src/assets/web-panel/assets/devWarning-BdTg_AMF.js +0 -1
  165. package/src/assets/web-panel/assets/index-B6WvFV4N.js +0 -1
  166. package/src/assets/web-panel/assets/index-BKgfptzA.js +0 -1
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import {
3
3
  assertCompiledGraph,
4
4
  compileGraphDefinition,
5
+ executionAttemptId,
5
6
  graphDigest,
6
7
  writeScopesOverlap,
7
8
  } from "./compiler.js";
@@ -77,6 +78,13 @@ const TRUST_RANK = Object.freeze({
77
78
  authenticated_user: 2,
78
79
  trusted_host: 3,
79
80
  });
81
+ const BUDGET_FIELDS = Object.freeze([
82
+ "turns",
83
+ "tokens",
84
+ "costUsd",
85
+ "wallMs",
86
+ "spawnCount",
87
+ ]);
80
88
 
81
89
  function kernelError(code, message, details = {}) {
82
90
  const error = new Error(message);
@@ -98,6 +106,95 @@ function mapRestore(entries) {
98
106
  return new Map((entries || []).map(([key, value]) => [key, clone(value)]));
99
107
  }
100
108
 
109
+ function emptyBudgetUsage() {
110
+ return Object.fromEntries(BUDGET_FIELDS.map((field) => [field, 0]));
111
+ }
112
+
113
+ function budgetValue(budget, field) {
114
+ return Number(budget?.[field] || 0);
115
+ }
116
+
117
+ function persistedSubgraphRegistry(definitions = {}) {
118
+ return new Map(
119
+ Object.entries(definitions).map(([id, definition]) => [
120
+ id,
121
+ {
122
+ definition,
123
+ revisionDigest: graphDigest(
124
+ definition,
125
+ `cc.graph.definition/${definition.schemaVersion}`,
126
+ ),
127
+ },
128
+ ]),
129
+ );
130
+ }
131
+
132
+ function idleCompensationState() {
133
+ return {
134
+ status: "idle",
135
+ triggerNodeId: null,
136
+ reason: null,
137
+ terminalStatus: null,
138
+ plan: [],
139
+ currentIndex: 0,
140
+ completedNodeIds: [],
141
+ completedEntries: [],
142
+ failure: null,
143
+ startedAt: null,
144
+ completedAt: null,
145
+ };
146
+ }
147
+
148
+ function iterationFrameId(runId, regionId, iterationPath) {
149
+ const digest = graphDigest(
150
+ { runId, regionId, iterationPath },
151
+ "cc.graph.iteration-frame/v1",
152
+ );
153
+ return `iteration-${digest.slice(7, 39)}`;
154
+ }
155
+
156
+ function deterministicSubgraphRunId(parentRunId, nodeId, revisionDigest) {
157
+ const digest = graphDigest(
158
+ { parentRunId, nodeId, revisionDigest },
159
+ "cc.graph.subgraph-run/v1",
160
+ );
161
+ return `subgraph-${digest.slice(7, 39)}`;
162
+ }
163
+
164
+ export function iterationEffectIdempotencyKey(
165
+ baseIdempotencyKey,
166
+ iterationPath = [],
167
+ ) {
168
+ const base = String(baseIdempotencyKey || "").trim();
169
+ if (!base) {
170
+ throw kernelError(
171
+ "CC_GRAPH_EFFECT_IDEMPOTENCY_REQUIRED",
172
+ "effect idempotency key is required",
173
+ );
174
+ }
175
+ if (!Array.isArray(iterationPath)) {
176
+ throw kernelError(
177
+ "CC_GRAPH_INVALID_ARGUMENT",
178
+ "iterationPath must be an array",
179
+ );
180
+ }
181
+ const path = iterationPath.map((value) => {
182
+ const number = Number(value);
183
+ if (!Number.isInteger(number) || number < 0) {
184
+ throw kernelError(
185
+ "CC_GRAPH_INVALID_ARGUMENT",
186
+ "iterationPath must contain non-negative integers",
187
+ );
188
+ }
189
+ return number;
190
+ });
191
+ if (path.length === 0) return base;
192
+ return graphDigest(
193
+ { baseIdempotencyKey: base, iterationPath: path },
194
+ "cc.graph.iteration-effect-idempotency/v1",
195
+ );
196
+ }
197
+
101
198
  function finitePositive(value, fallback) {
102
199
  const number = Number(value);
103
200
  return Number.isFinite(number) && number > 0 ? number : fallback;
@@ -254,6 +351,7 @@ function stateSnapshot(run) {
254
351
  version: 1,
255
352
  id: run.id,
256
353
  definition: run.compiled.definition,
354
+ subgraphDefinitions: run.compiled.subgraphDefinitions,
257
355
  phase: run.phase,
258
356
  status: run.status,
259
357
  graphRevision: run.graphRevision,
@@ -262,10 +360,19 @@ function stateSnapshot(run) {
262
360
  authorityDigest: run.authorityDigest,
263
361
  budget: run.budget,
264
362
  budgetUsed: run.budgetUsed,
363
+ budgetReserved: run.budgetReserved,
265
364
  createdAt: run.createdAt,
266
365
  completedAt: run.completedAt,
267
366
  cancellationRequested: run.cancellationRequested,
268
367
  progressEpoch: run.progressEpoch,
368
+ compensation: clone(run.compensation),
369
+ parentRunRef: clone(run.parentRunRef),
370
+ subgraphDepth: run.subgraphDepth,
371
+ definitionPath: clone(run.definitionPath),
372
+ loopStates: mapSnapshot(run.loopStates),
373
+ iterationFrames: mapSnapshot(run.iterationFrames),
374
+ loopDecisionCache: mapSnapshot(run.loopDecisionCache),
375
+ subgraphRuns: mapSnapshot(run.subgraphRuns),
269
376
  nodeStates: mapSnapshot(run.nodeStates),
270
377
  attempts: mapSnapshot(run.attempts),
271
378
  agents: mapSnapshot(run.agents),
@@ -285,7 +392,12 @@ function stateSnapshot(run) {
285
392
  }
286
393
 
287
394
  function restoreState(run, snapshot, compiled = null) {
288
- run.compiled = compiled || compileGraphDefinition(snapshot.definition);
395
+ run.compiled =
396
+ compiled ||
397
+ compileGraphDefinition(snapshot.definition, {
398
+ subgraphs: persistedSubgraphRegistry(snapshot.subgraphDefinitions),
399
+ allowUnresolvedSubgraphs: snapshot.subgraphDefinitions == null,
400
+ });
289
401
  run.phase = snapshot.phase;
290
402
  run.status = snapshot.status;
291
403
  run.graphRevision = snapshot.graphRevision;
@@ -294,11 +406,23 @@ function restoreState(run, snapshot, compiled = null) {
294
406
  run.authorityDigest = snapshot.authorityDigest;
295
407
  run.budget = clone(snapshot.budget);
296
408
  run.budgetUsed = clone(snapshot.budgetUsed);
409
+ run.budgetReserved = clone(snapshot.budgetReserved || emptyBudgetUsage());
297
410
  run.createdAt = snapshot.createdAt;
298
411
  run.completedAt = snapshot.completedAt;
299
412
  run.cancellationRequested = snapshot.cancellationRequested === true;
300
413
  run.progressEpoch = snapshot.progressEpoch;
414
+ run.compensation = clone(snapshot.compensation || idleCompensationState());
415
+ run.compensation.completedEntries ||= [];
416
+ run.parentRunRef = clone(snapshot.parentRunRef || null);
417
+ run.subgraphDepth = Number(snapshot.subgraphDepth || 0);
418
+ run.definitionPath = clone(
419
+ snapshot.definitionPath || [run.compiled.definitionId],
420
+ );
301
421
  for (const field of [
422
+ "loopStates",
423
+ "iterationFrames",
424
+ "loopDecisionCache",
425
+ "subgraphRuns",
302
426
  "nodeStates",
303
427
  "attempts",
304
428
  "agents",
@@ -328,6 +452,7 @@ function nodeProjection(run, nodeId) {
328
452
  attemptIds: Object.freeze([...state.attemptIds]),
329
453
  acceptedAttemptId: state.acceptedAttemptId,
330
454
  blockedRoot: state.blockedRoot,
455
+ iterationPath: Object.freeze([...(state.iterationPath || [])]),
331
456
  createdAt: state.createdAt,
332
457
  updatedAt: state.updatedAt,
333
458
  });
@@ -350,9 +475,33 @@ function runProjection(run) {
350
475
  authorityDigest: run.authorityDigest,
351
476
  budget: clone(run.budget),
352
477
  budgetUsed: clone(run.budgetUsed),
478
+ budgetReserved: clone(run.budgetReserved),
353
479
  createdAt: run.createdAt,
354
480
  completedAt: run.completedAt,
355
481
  progressEpoch: run.progressEpoch,
482
+ compensation: Object.freeze(clone(run.compensation)),
483
+ parentRunRef: Object.freeze(clone(run.parentRunRef)),
484
+ subgraphDepth: run.subgraphDepth,
485
+ definitionPath: Object.freeze([...run.definitionPath]),
486
+ loops: Object.freeze(
487
+ [...run.loopStates.values()]
488
+ .sort((left, right) => left.regionId.localeCompare(right.regionId))
489
+ .map((state) => Object.freeze(clone(state))),
490
+ ),
491
+ iterationFrames: Object.freeze(
492
+ [...run.iterationFrames.values()]
493
+ .sort((left, right) =>
494
+ left.iterationPath
495
+ .join(".")
496
+ .localeCompare(right.iterationPath.join(".")),
497
+ )
498
+ .map((frame) => Object.freeze(clone(frame))),
499
+ ),
500
+ subgraphRuns: Object.freeze(
501
+ [...run.subgraphRuns.values()]
502
+ .sort((left, right) => left.nodeId.localeCompare(right.nodeId))
503
+ .map((relation) => Object.freeze(clone(relation))),
504
+ ),
356
505
  nodes: Object.freeze(
357
506
  run.compiled.topologicalOrder.map((id) => nodeProjection(run, id)),
358
507
  ),
@@ -399,17 +548,20 @@ function buildRun(compiled, options, now) {
399
548
  "cc.graph.authority/v1",
400
549
  ),
401
550
  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
- },
551
+ budgetUsed: emptyBudgetUsage(),
552
+ budgetReserved: emptyBudgetUsage(),
409
553
  createdAt,
410
554
  completedAt: null,
411
555
  cancellationRequested: false,
412
556
  progressEpoch: 0,
557
+ compensation: idleCompensationState(),
558
+ parentRunRef: clone(options.parentRunRef || null),
559
+ subgraphDepth: Number(options.subgraphDepth || 0),
560
+ definitionPath: clone(options.definitionPath || [compiled.definitionId]),
561
+ loopStates: new Map(),
562
+ iterationFrames: new Map(),
563
+ loopDecisionCache: new Map(),
564
+ subgraphRuns: new Map(),
413
565
  nodeStates: new Map(),
414
566
  attempts: new Map(),
415
567
  agents: new Map(),
@@ -427,12 +579,41 @@ function buildRun(compiled, options, now) {
427
579
  fenceCounters: new Map(),
428
580
  lastEvent: null,
429
581
  };
582
+ for (const loop of compiled.definition.loops || []) {
583
+ const iterationPath = [0];
584
+ const frameId = iterationFrameId(run.id, loop.id, iterationPath);
585
+ run.loopStates.set(loop.id, {
586
+ regionId: loop.id,
587
+ status: "active",
588
+ iteration: 0,
589
+ iterationPath,
590
+ frameId,
591
+ maxIterations: loop.maxIterations,
592
+ condition: loop.condition || null,
593
+ conditionEvidenceDigest: null,
594
+ });
595
+ run.iterationFrames.set(frameId, {
596
+ id: frameId,
597
+ runId: run.id,
598
+ regionId: loop.id,
599
+ parentFrameId: null,
600
+ iterationPath,
601
+ status: "active",
602
+ startedAt: createdAt,
603
+ completedAt: null,
604
+ });
605
+ }
606
+ const compensationNodeIds = new Set(compiled.compensationNodeIds || []);
430
607
  for (const nodeId of compiled.topologicalOrder) {
608
+ const loopId = compiled.loopByNode?.[nodeId];
431
609
  run.nodeStates.set(nodeId, {
432
- status: "pending",
610
+ status: compensationNodeIds.has(nodeId) ? "skipped" : "pending",
433
611
  attemptIds: [],
434
612
  acceptedAttemptId: null,
435
613
  blockedRoot: null,
614
+ iterationPath: loopId
615
+ ? [...run.loopStates.get(loopId).iterationPath]
616
+ : [],
436
617
  createdAt,
437
618
  updatedAt: createdAt,
438
619
  });
@@ -447,7 +628,7 @@ function eventDetails(value) {
447
628
  }
448
629
 
449
630
  function checkBudget(run, usage) {
450
- for (const field of ["turns", "tokens", "costUsd", "wallMs", "spawnCount"]) {
631
+ for (const field of BUDGET_FIELDS) {
451
632
  const delta = Number(usage?.[field] || 0);
452
633
  if (!Number.isFinite(delta) || delta < 0) {
453
634
  throw kernelError(
@@ -456,11 +637,18 @@ function checkBudget(run, usage) {
456
637
  );
457
638
  }
458
639
  const cap = run.budget?.[field];
459
- if (cap != null && run.budgetUsed[field] + delta > cap) {
640
+ const reserved = budgetValue(run.budgetReserved, field);
641
+ if (cap != null && run.budgetUsed[field] + reserved + delta > cap) {
460
642
  throw kernelError(
461
643
  "CC_GRAPH_BUDGET_EXHAUSTED",
462
644
  `GraphRun ${field} budget would be exceeded`,
463
- { field, cap, used: run.budgetUsed[field], requested: delta },
645
+ {
646
+ field,
647
+ cap,
648
+ used: run.budgetUsed[field],
649
+ reserved,
650
+ requested: delta,
651
+ },
464
652
  );
465
653
  }
466
654
  }
@@ -468,18 +656,92 @@ function checkBudget(run, usage) {
468
656
 
469
657
  function addUsage(run, usage) {
470
658
  checkBudget(run, usage);
471
- for (const field of Object.keys(run.budgetUsed)) {
659
+ for (const field of BUDGET_FIELDS) {
472
660
  run.budgetUsed[field] += Number(usage?.[field] || 0);
473
661
  }
474
662
  }
475
663
 
664
+ function reserveBudget(run, budget) {
665
+ checkBudget(run, budget);
666
+ for (const field of BUDGET_FIELDS) {
667
+ run.budgetReserved[field] += budgetValue(budget, field);
668
+ }
669
+ }
670
+
671
+ function settleSubgraphBudget(run, relation, usage) {
672
+ if (relation.budgetSettled === true) return;
673
+ for (const field of BUDGET_FIELDS) {
674
+ const reserved = budgetValue(relation.budgetSlice, field);
675
+ const actual = budgetValue(usage, field);
676
+ if (actual > reserved) {
677
+ throw kernelError(
678
+ "CC_GRAPH_SUBGRAPH_BUDGET_INVARIANT",
679
+ `child ${field} usage exceeds its reserved budget slice`,
680
+ { field, reserved, actual, childRunId: relation.childRunId },
681
+ );
682
+ }
683
+ run.budgetReserved[field] = Math.max(
684
+ 0,
685
+ budgetValue(run.budgetReserved, field) - reserved,
686
+ );
687
+ }
688
+ addUsage(run, usage);
689
+ relation.budgetUsed = Object.fromEntries(
690
+ BUDGET_FIELDS.map((field) => [field, budgetValue(usage, field)]),
691
+ );
692
+ relation.budgetSettled = true;
693
+ }
694
+
695
+ function effectiveDependencyStatus(run, dependency) {
696
+ const status = run.nodeStates.get(dependency).status;
697
+ const loopId = run.compiled.loopByExitNode?.[dependency];
698
+ const loopState = loopId ? run.loopStates.get(loopId) : null;
699
+ if (loopState && ["active", "waiting_condition"].includes(loopState.status)) {
700
+ return "running";
701
+ }
702
+ if (loopState?.status === "failed") return "failed";
703
+ if (loopState?.status === "cancelled") return "cancelled";
704
+ if (loopState?.status === "exhausted") return "budget_exhausted";
705
+ return status;
706
+ }
707
+
708
+ function refreshLoopStates(run, now) {
709
+ let changed = false;
710
+ for (const loop of run.compiled.definition.loops || []) {
711
+ const loopState = run.loopStates.get(loop.id);
712
+ if (!loopState || loopState.status !== "active") continue;
713
+ const statuses = loop.nodeIds.map(
714
+ (nodeId) => run.nodeStates.get(nodeId).status,
715
+ );
716
+ const failure = statuses.find((status) => FAILURE_NODE.has(status));
717
+ const cancelled = statuses.find((status) => status === "cancelled");
718
+ const frame = run.iterationFrames.get(loopState.frameId);
719
+ if (failure || cancelled) {
720
+ loopState.status = failure ? "failed" : "cancelled";
721
+ if (frame) {
722
+ frame.status = failure ? "failed" : "cancelled";
723
+ frame.completedAt = nowIso(now);
724
+ }
725
+ changed = true;
726
+ } else if (statuses.every((status) => status === "succeeded")) {
727
+ loopState.status = "waiting_condition";
728
+ if (frame) {
729
+ frame.status = "succeeded";
730
+ frame.completedAt = nowIso(now);
731
+ }
732
+ changed = true;
733
+ }
734
+ }
735
+ return changed;
736
+ }
737
+
476
738
  function nodeReadiness(run, nodeId) {
477
739
  const node = run.compiled.nodes[nodeId];
478
740
  const dependencies = run.compiled.dependencies[nodeId] || [];
479
741
  if (!dependencies.length) return { ready: true };
480
742
  const policies = run.compiled.dependencyPolicies[nodeId] || {};
481
743
  const observations = dependencies.map((dependency) => {
482
- const status = run.nodeStates.get(dependency).status;
744
+ const status = effectiveDependencyStatus(run, dependency);
483
745
  const policy = policies[dependency] || "success";
484
746
  return {
485
747
  dependency,
@@ -702,6 +964,121 @@ function waitGraphCycles(run) {
702
964
  return components.sort((left, right) => left[0].localeCompare(right[0]));
703
965
  }
704
966
 
967
+ function currentCompensationEntry(run) {
968
+ if (run.compensation?.status !== "running") return null;
969
+ return run.compensation.plan[run.compensation.currentIndex] || null;
970
+ }
971
+
972
+ function compensationAssignment(run, nodeId) {
973
+ const entry = currentCompensationEntry(run);
974
+ return entry?.compensationNodeId === nodeId ? entry : null;
975
+ }
976
+
977
+ function sameIterationPath(left, right) {
978
+ return JSON.stringify(left || []) === JSON.stringify(right || []);
979
+ }
980
+
981
+ function compareIterationPath(left, right) {
982
+ const a = left || [];
983
+ const b = right || [];
984
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
985
+ if (a[index] == null) return -1;
986
+ if (b[index] == null) return 1;
987
+ if (a[index] !== b[index]) return a[index] - b[index];
988
+ }
989
+ return 0;
990
+ }
991
+
992
+ function committedEffectsForNode(run, nodeId, iterationPath = null) {
993
+ return [...run.effects.values()]
994
+ .filter(
995
+ (effect) =>
996
+ effect.nodeId === nodeId &&
997
+ effect.status === "committed" &&
998
+ (iterationPath == null ||
999
+ sameIterationPath(effect.iterationPath, iterationPath)),
1000
+ )
1001
+ .sort(
1002
+ (left, right) =>
1003
+ compareIterationPath(left.iterationPath, right.iterationPath) ||
1004
+ left.createdAt.localeCompare(right.createdAt) ||
1005
+ left.id.localeCompare(right.id),
1006
+ );
1007
+ }
1008
+
1009
+ function completeCompensationStep(run, entry, now) {
1010
+ const compensationEffects = committedEffectsForNode(
1011
+ run,
1012
+ entry.compensationNodeId,
1013
+ entry.iterationPath,
1014
+ );
1015
+ if (compensationEffects.length === 0) {
1016
+ throw kernelError(
1017
+ "CC_GRAPH_COMPENSATION_RECEIPT_REQUIRED",
1018
+ `compensation node requires a committed effect receipt: ${entry.compensationNodeId}`,
1019
+ );
1020
+ }
1021
+ const compensationEffect = compensationEffects.at(-1);
1022
+ const sourceEffects = entry.effectIds.map((effectId) =>
1023
+ run.effects.get(effectId),
1024
+ );
1025
+ if (
1026
+ sourceEffects.some(
1027
+ (effect) =>
1028
+ !effect ||
1029
+ effect.status !== "committed" ||
1030
+ !sameIterationPath(effect.iterationPath, entry.iterationPath),
1031
+ )
1032
+ ) {
1033
+ throw kernelError(
1034
+ "CC_GRAPH_COMPENSATION_SOURCE_RECEIPT_INVALID",
1035
+ `compensation source receipts changed for ${entry.nodeId}`,
1036
+ );
1037
+ }
1038
+ for (const effect of sourceEffects) {
1039
+ effect.status = "compensated";
1040
+ effect.compensationEffectId = compensationEffect.id;
1041
+ effect.updatedAt = nowIso(() => now);
1042
+ }
1043
+ run.compensation.completedNodeIds.push(entry.nodeId);
1044
+ run.compensation.completedEntries.push({
1045
+ nodeId: entry.nodeId,
1046
+ iterationPath: [...entry.iterationPath],
1047
+ effectIds: [...entry.effectIds],
1048
+ compensationEffectId: compensationEffect.id,
1049
+ });
1050
+ run.compensation.currentIndex += 1;
1051
+ if (run.compensation.currentIndex >= run.compensation.plan.length) {
1052
+ run.compensation.status = "completed";
1053
+ run.compensation.completedAt = nowIso(() => now);
1054
+ run.status = run.compensation.terminalStatus;
1055
+ run.completedAt = nowIso(() => now);
1056
+ } else {
1057
+ const next = run.compensation.plan[run.compensation.currentIndex];
1058
+ const state = run.nodeStates.get(next.compensationNodeId);
1059
+ state.status = "pending";
1060
+ state.acceptedAttemptId = null;
1061
+ state.blockedRoot = null;
1062
+ state.iterationPath = [...next.iterationPath];
1063
+ state.updatedAt = nowIso(() => now);
1064
+ run.status = "running";
1065
+ }
1066
+ }
1067
+
1068
+ function failCompensation(run, entry, error, now) {
1069
+ run.compensation.status = "failed";
1070
+ run.compensation.failure = {
1071
+ nodeId: entry.nodeId,
1072
+ compensationNodeId: entry.compensationNodeId,
1073
+ iterationPath: [...entry.iterationPath],
1074
+ effectIds: [...entry.effectIds],
1075
+ error: String(error || "compensation_failed").slice(0, 4096),
1076
+ };
1077
+ run.compensation.completedAt = nowIso(() => now);
1078
+ run.status = "reconciliation_required";
1079
+ run.completedAt = null;
1080
+ }
1081
+
705
1082
  function terminalAlgebra(run) {
706
1083
  const statuses = [...run.nodeStates.values()].map((state) => state.status);
707
1084
  const successes = statuses.filter((status) => status === "succeeded").length;
@@ -719,6 +1096,12 @@ function terminalAlgebra(run) {
719
1096
  }
720
1097
 
721
1098
  function classifyQuiescence(run, now) {
1099
+ if (run.compensation?.status === "failed") {
1100
+ return "reconciliation_required";
1101
+ }
1102
+ if (run.compensation?.status === "completed") {
1103
+ return run.compensation.terminalStatus || "failed";
1104
+ }
722
1105
  if (TERMINAL_RUN.has(run.status)) return run.status;
723
1106
  if (run.phase === "open") return "running";
724
1107
  if (
@@ -762,6 +1145,16 @@ function classifyQuiescence(run, now) {
762
1145
  ) {
763
1146
  return "waiting_external";
764
1147
  }
1148
+ if (
1149
+ [...run.loopStates.values()].some(
1150
+ (loopState) => loopState.status === "waiting_condition",
1151
+ ) ||
1152
+ [...run.subgraphRuns.values()].some((relation) =>
1153
+ ["starting", "running"].includes(relation.status),
1154
+ )
1155
+ ) {
1156
+ return "waiting_external";
1157
+ }
765
1158
  const cycles = waitGraphCycles(run);
766
1159
  if (cycles.length) return "deadlocked";
767
1160
  const pending = [...run.nodeStates.entries()].filter(
@@ -819,6 +1212,20 @@ export class GraphKernel {
819
1212
  return run;
820
1213
  }
821
1214
 
1215
+ _loadRun(runId) {
1216
+ const id = safeIdentifier(runId, "runId");
1217
+ if (this.runs.has(id)) return this.runs.get(id);
1218
+ try {
1219
+ if (this.eventStore.read(id).length > 0) {
1220
+ this.recoverRun(id);
1221
+ return this.runs.get(id);
1222
+ }
1223
+ } catch (error) {
1224
+ if (error?.code !== "CC_ROLLOUT_THREAD_NOT_FOUND") throw error;
1225
+ }
1226
+ return null;
1227
+ }
1228
+
822
1229
  _transaction(run, type, details, mutator, options = {}) {
823
1230
  if (TERMINAL_RUN.has(run.status) && options.allowTerminal !== true) {
824
1231
  throw kernelError(
@@ -884,6 +1291,9 @@ export class GraphKernel {
884
1291
  definitionId: compiled.definitionId,
885
1292
  revisionDigest: compiled.revisionDigest,
886
1293
  occurrenceRef: occurrence,
1294
+ parentRunRef: run.parentRunRef,
1295
+ subgraphDepth: run.subgraphDepth,
1296
+ definitionPath: run.definitionPath,
887
1297
  },
888
1298
  () => {},
889
1299
  { idempotencyKey: `run-start:${run.id}` },
@@ -1129,19 +1539,72 @@ export class GraphKernel {
1129
1539
  ...clone(subgraphCalls),
1130
1540
  ],
1131
1541
  };
1132
- const compiled = compileGraphDefinition(definition);
1542
+ const compiled = compileGraphDefinition(definition, {
1543
+ subgraphs: persistedSubgraphRegistry(
1544
+ run.compiled.subgraphDefinitions,
1545
+ ),
1546
+ });
1133
1547
  const createdAt = nowIso(() => now);
1548
+ const compensationNodeIds = new Set(compiled.compensationNodeIds || []);
1549
+ const addedNodeIds = new Set(nodes.map((node) => node.id));
1550
+ for (const loop of loops) {
1551
+ if (loop.nodeIds.some((nodeId) => !addedNodeIds.has(nodeId))) {
1552
+ throw kernelError(
1553
+ "CC_GRAPH_DYNAMIC_LOOP_BOUNDARY",
1554
+ "a dynamic loop must be added atomically with all of its nodes",
1555
+ { loopId: loop.id },
1556
+ );
1557
+ }
1558
+ const iterationPath = [0];
1559
+ const frameId = iterationFrameId(run.id, loop.id, iterationPath);
1560
+ run.loopStates.set(loop.id, {
1561
+ regionId: loop.id,
1562
+ status: "active",
1563
+ iteration: 0,
1564
+ iterationPath,
1565
+ frameId,
1566
+ maxIterations: loop.maxIterations,
1567
+ condition: loop.condition || null,
1568
+ conditionEvidenceDigest: null,
1569
+ });
1570
+ run.iterationFrames.set(frameId, {
1571
+ id: frameId,
1572
+ runId: run.id,
1573
+ regionId: loop.id,
1574
+ parentFrameId: null,
1575
+ iterationPath,
1576
+ status: "active",
1577
+ startedAt: createdAt,
1578
+ completedAt: null,
1579
+ });
1580
+ }
1134
1581
  for (const node of nodes) {
1582
+ const loopId = compiled.loopByNode?.[node.id];
1135
1583
  run.nodeStates.set(node.id, {
1136
- status: "pending",
1584
+ status: compensationNodeIds.has(node.id) ? "skipped" : "pending",
1137
1585
  attemptIds: [],
1138
1586
  acceptedAttemptId: null,
1139
1587
  blockedRoot: null,
1588
+ iterationPath: loopId
1589
+ ? [...run.loopStates.get(loopId).iterationPath]
1590
+ : [],
1140
1591
  createdAt,
1141
1592
  updatedAt: createdAt,
1142
1593
  });
1143
1594
  run.fenceCounters.set(node.id, 0);
1144
1595
  }
1596
+ for (const compensationNodeId of compensationNodeIds) {
1597
+ const state = run.nodeStates.get(compensationNodeId);
1598
+ if (!state || state.status === "skipped") continue;
1599
+ if (state.status !== "pending" || state.attemptIds.length > 0) {
1600
+ throw kernelError(
1601
+ "CC_GRAPH_COMPENSATION_TARGET_ACTIVE",
1602
+ `dynamic revision cannot convert an active node into a compensation target: ${compensationNodeId}`,
1603
+ );
1604
+ }
1605
+ state.status = "skipped";
1606
+ state.updatedAt = createdAt;
1607
+ }
1145
1608
  run.compiled = compiled;
1146
1609
  run.graphRevision += 1;
1147
1610
  run.revisionDigest = compiled.revisionDigest;
@@ -1179,6 +1642,7 @@ export class GraphKernel {
1179
1642
  }
1180
1643
  run.phase = "sealed";
1181
1644
  propagate(run, () => now);
1645
+ if (refreshLoopStates(run, () => now)) propagate(run, () => now);
1182
1646
  run.status = classifyQuiescence(run, now);
1183
1647
  if (TERMINAL_RUN.has(run.status)) run.completedAt = nowIso(() => now);
1184
1648
  return runProjection(run);
@@ -1193,15 +1657,423 @@ export class GraphKernel {
1193
1657
  const now = this.now();
1194
1658
  sweepExpirations(run, now);
1195
1659
  propagate(run, () => now);
1660
+ if (refreshLoopStates(run, () => now)) propagate(run, () => now);
1196
1661
  run.status = classifyQuiescence(run, now);
1197
1662
  if (TERMINAL_RUN.has(run.status)) run.completedAt ||= nowIso(() => now);
1198
1663
  return runProjection(run);
1199
1664
  });
1200
1665
  }
1201
1666
 
1667
+ advanceLoop(
1668
+ runId,
1669
+ { regionId, continueLoop, conditionEvidenceDigest, requestId = null } = {},
1670
+ ) {
1671
+ const run = this._run(runId);
1672
+ const safeRegionId = safeIdentifier(regionId, "regionId");
1673
+ const evidenceDigest = String(conditionEvidenceDigest || "");
1674
+ if (!/^sha256:[a-f0-9]{64}$/u.test(evidenceDigest)) {
1675
+ throw kernelError(
1676
+ "CC_GRAPH_LOOP_EVIDENCE_REQUIRED",
1677
+ "loop advancement requires a sha256 condition evidence digest",
1678
+ );
1679
+ }
1680
+ if (typeof continueLoop !== "boolean") {
1681
+ throw kernelError(
1682
+ "CC_GRAPH_LOOP_DECISION_INVALID",
1683
+ "continueLoop must be a boolean",
1684
+ );
1685
+ }
1686
+ const loop = run.compiled.loops?.[safeRegionId];
1687
+ const loopState = run.loopStates.get(safeRegionId);
1688
+ if (!loop || !loopState) {
1689
+ throw kernelError(
1690
+ "CC_GRAPH_LOOP_NOT_FOUND",
1691
+ `loop region not found: ${safeRegionId}`,
1692
+ );
1693
+ }
1694
+ if (requestId == null) {
1695
+ throw kernelError(
1696
+ "CC_GRAPH_LOOP_REQUEST_ID_REQUIRED",
1697
+ "loop advancement requires a stable requestId for crash-safe replay",
1698
+ );
1699
+ }
1700
+ const decisionKey = safeIdentifier(requestId, "requestId");
1701
+ const decisionDigest = graphDigest(
1702
+ {
1703
+ regionId: safeRegionId,
1704
+ continueLoop,
1705
+ conditionEvidenceDigest: evidenceDigest,
1706
+ },
1707
+ "cc.graph.loop-decision/v1",
1708
+ );
1709
+ const replay = run.loopDecisionCache.get(decisionKey);
1710
+ if (replay) {
1711
+ if (replay.digest !== decisionDigest) {
1712
+ throw kernelError(
1713
+ "CC_GRAPH_REQUEST_ID_CONFLICT",
1714
+ "loop requestId was reused with a different decision",
1715
+ );
1716
+ }
1717
+ return runProjection(run);
1718
+ }
1719
+ this._transaction(
1720
+ run,
1721
+ continueLoop ? "loop.continued" : "loop.exited",
1722
+ {
1723
+ regionId: safeRegionId,
1724
+ iteration: loopState.iteration,
1725
+ continueLoop,
1726
+ conditionEvidenceDigest: evidenceDigest,
1727
+ requestId: decisionKey,
1728
+ },
1729
+ () => {
1730
+ if (loopState.status !== "waiting_condition") {
1731
+ throw kernelError(
1732
+ "CC_GRAPH_LOOP_NOT_WAITING",
1733
+ `loop body is not awaiting a condition decision: ${safeRegionId}`,
1734
+ );
1735
+ }
1736
+ const now = this.now();
1737
+ loopState.conditionEvidenceDigest = evidenceDigest;
1738
+ if (!continueLoop) {
1739
+ loopState.status = "succeeded";
1740
+ run.loopDecisionCache.set(decisionKey, {
1741
+ digest: decisionDigest,
1742
+ result: "exited",
1743
+ });
1744
+ propagate(run, () => now);
1745
+ refreshLoopStates(run, () => now);
1746
+ run.status = classifyQuiescence(run, now);
1747
+ if (TERMINAL_RUN.has(run.status)) {
1748
+ run.completedAt = nowIso(() => now);
1749
+ }
1750
+ return runProjection(run);
1751
+ }
1752
+
1753
+ const nextIteration = loopState.iteration + 1;
1754
+ if (nextIteration >= loop.maxIterations) {
1755
+ loopState.status = "exhausted";
1756
+ const frame = run.iterationFrames.get(loopState.frameId);
1757
+ if (frame) frame.status = "exhausted";
1758
+ const exitState = run.nodeStates.get(loop.exitNodeId);
1759
+ exitState.status = "budget_exhausted";
1760
+ exitState.blockedRoot = loop.id;
1761
+ exitState.updatedAt = nowIso(() => now);
1762
+ run.loopDecisionCache.set(decisionKey, {
1763
+ digest: decisionDigest,
1764
+ result: "exhausted",
1765
+ });
1766
+ propagate(run, () => now);
1767
+ run.status = "budget_exhausted";
1768
+ run.completedAt = nowIso(() => now);
1769
+ return runProjection(run);
1770
+ }
1771
+
1772
+ const iterationPath = [nextIteration];
1773
+ const frameId = iterationFrameId(run.id, safeRegionId, iterationPath);
1774
+ loopState.status = "active";
1775
+ loopState.iteration = nextIteration;
1776
+ loopState.iterationPath = iterationPath;
1777
+ loopState.frameId = frameId;
1778
+ loopState.conditionEvidenceDigest = null;
1779
+ run.iterationFrames.set(frameId, {
1780
+ id: frameId,
1781
+ runId: run.id,
1782
+ regionId: safeRegionId,
1783
+ parentFrameId: null,
1784
+ iterationPath,
1785
+ status: "active",
1786
+ startedAt: nowIso(() => now),
1787
+ completedAt: null,
1788
+ });
1789
+ for (const nodeId of loop.nodeIds) {
1790
+ const state = run.nodeStates.get(nodeId);
1791
+ state.status = "pending";
1792
+ state.acceptedAttemptId = null;
1793
+ state.blockedRoot = null;
1794
+ state.iterationPath = [...iterationPath];
1795
+ state.updatedAt = nowIso(() => now);
1796
+ }
1797
+ run.loopDecisionCache.set(decisionKey, {
1798
+ digest: decisionDigest,
1799
+ result: "continued",
1800
+ });
1801
+ run.status = "running";
1802
+ run.completedAt = null;
1803
+ return runProjection(run);
1804
+ },
1805
+ { idempotencyKey: `loop-decision:${decisionKey}` },
1806
+ );
1807
+ return runProjection(run);
1808
+ }
1809
+
1810
+ startSubgraph(
1811
+ parentRunId,
1812
+ nodeId,
1813
+ compiledGraph,
1814
+ { childRunId = null, seal = true } = {},
1815
+ ) {
1816
+ const childCompiled = assertCompiledGraph(compiledGraph);
1817
+ const parent = this._run(parentRunId);
1818
+ const safeNodeId = safeIdentifier(nodeId, "nodeId");
1819
+ const call = parent.compiled.subgraphCalls?.[safeNodeId];
1820
+ const state = parent.nodeStates.get(safeNodeId);
1821
+ if (!call || !state) {
1822
+ throw kernelError(
1823
+ "CC_GRAPH_SUBGRAPH_CALL_NOT_FOUND",
1824
+ `subgraph call not found: ${safeNodeId}`,
1825
+ );
1826
+ }
1827
+ if (
1828
+ call.definitionId !== childCompiled.definitionId ||
1829
+ call.revisionDigest !== childCompiled.revisionDigest
1830
+ ) {
1831
+ throw kernelError(
1832
+ "CC_GRAPH_SUBGRAPH_DIGEST_MISMATCH",
1833
+ `subgraph definition does not match the pinned call: ${safeNodeId}`,
1834
+ );
1835
+ }
1836
+ const childDepth = parent.subgraphDepth + 1;
1837
+ if (childDepth > call.maxDepth) {
1838
+ throw kernelError(
1839
+ "CC_GRAPH_SUBGRAPH_DEPTH_EXCEEDED",
1840
+ `subgraph depth ${childDepth} exceeds maxDepth ${call.maxDepth}`,
1841
+ );
1842
+ }
1843
+ if (parent.definitionPath.includes(childCompiled.definitionId)) {
1844
+ throw kernelError(
1845
+ "CC_GRAPH_SUBGRAPH_RECURSION",
1846
+ `recursive subgraph call rejected: ${[
1847
+ ...parent.definitionPath,
1848
+ childCompiled.definitionId,
1849
+ ].join(" -> ")}`,
1850
+ );
1851
+ }
1852
+ const id = safeIdentifier(
1853
+ childRunId ||
1854
+ deterministicSubgraphRunId(
1855
+ parent.id,
1856
+ safeNodeId,
1857
+ childCompiled.revisionDigest,
1858
+ ),
1859
+ "childRunId",
1860
+ );
1861
+ let relation = parent.subgraphRuns.get(safeNodeId);
1862
+ if (relation) {
1863
+ if (
1864
+ relation.childRunId !== id ||
1865
+ relation.revisionDigest !== childCompiled.revisionDigest
1866
+ ) {
1867
+ throw kernelError(
1868
+ "CC_GRAPH_SUBGRAPH_CALL_CONFLICT",
1869
+ "subgraph call was already bound to a different child run",
1870
+ );
1871
+ }
1872
+ } else {
1873
+ if (parent.phase !== "sealed") {
1874
+ throw kernelError(
1875
+ "CC_GRAPH_SUBGRAPH_REQUIRES_SEALED_PARENT",
1876
+ "subgraph calls require a sealed parent GraphRun",
1877
+ );
1878
+ }
1879
+ if (
1880
+ state.status !== "pending" ||
1881
+ !nodeReadiness(parent, safeNodeId).ready
1882
+ ) {
1883
+ throw kernelError(
1884
+ "CC_GRAPH_NODE_NOT_READY",
1885
+ `Graph node is not ready: ${safeNodeId}`,
1886
+ );
1887
+ }
1888
+ relation = this._transaction(
1889
+ parent,
1890
+ "subgraph.starting",
1891
+ {
1892
+ nodeId: safeNodeId,
1893
+ childRunId: id,
1894
+ definitionId: childCompiled.definitionId,
1895
+ revisionDigest: childCompiled.revisionDigest,
1896
+ depth: childDepth,
1897
+ budgetSlice: call.budget,
1898
+ inputBindings: call.inputBindings || [],
1899
+ outputBindings: call.outputBindings || [],
1900
+ },
1901
+ () => {
1902
+ const createdAt = nowIso(this.now);
1903
+ reserveBudget(parent, call.budget || {});
1904
+ const value = {
1905
+ nodeId: safeNodeId,
1906
+ childRunId: id,
1907
+ definitionId: childCompiled.definitionId,
1908
+ revisionDigest: childCompiled.revisionDigest,
1909
+ depth: childDepth,
1910
+ inputBindings: clone(call.inputBindings || []),
1911
+ outputBindings: clone(call.outputBindings || []),
1912
+ budgetSlice: clone(call.budget || {}),
1913
+ budgetUsed: null,
1914
+ budgetSettled: false,
1915
+ status: "starting",
1916
+ createdAt,
1917
+ updatedAt: createdAt,
1918
+ completedAt: null,
1919
+ };
1920
+ parent.subgraphRuns.set(safeNodeId, value);
1921
+ state.status = "running";
1922
+ state.updatedAt = createdAt;
1923
+ parent.status = "waiting_external";
1924
+ return clone(value);
1925
+ },
1926
+ { idempotencyKey: `subgraph-starting:${safeNodeId}:${id}` },
1927
+ );
1928
+ }
1929
+
1930
+ let child = this._loadRun(id);
1931
+ if (child) {
1932
+ if (
1933
+ child.revisionDigest !== childCompiled.revisionDigest ||
1934
+ child.parentRunRef?.runId !== parent.id ||
1935
+ child.parentRunRef?.nodeId !== safeNodeId
1936
+ ) {
1937
+ throw kernelError(
1938
+ "CC_GRAPH_SUBGRAPH_RUN_CONFLICT",
1939
+ `child run id is already bound to another call: ${id}`,
1940
+ );
1941
+ }
1942
+ } else {
1943
+ this.startRun(childCompiled, {
1944
+ runId: id,
1945
+ parentRunRef: {
1946
+ runId: parent.id,
1947
+ nodeId: safeNodeId,
1948
+ revisionDigest: parent.revisionDigest,
1949
+ },
1950
+ subgraphDepth: childDepth,
1951
+ definitionPath: [...parent.definitionPath, childCompiled.definitionId],
1952
+ budget: call.budget || {},
1953
+ });
1954
+ child = this._run(id);
1955
+ }
1956
+ if (seal !== false && child.phase === "open") this.sealRun(id);
1957
+
1958
+ relation = parent.subgraphRuns.get(safeNodeId);
1959
+ if (relation.status === "starting") {
1960
+ this._transaction(
1961
+ parent,
1962
+ "subgraph.started",
1963
+ { nodeId: safeNodeId, childRunId: id },
1964
+ () => {
1965
+ relation.status = "running";
1966
+ relation.updatedAt = nowIso(this.now);
1967
+ parent.status = "waiting_external";
1968
+ },
1969
+ { idempotencyKey: `subgraph-started:${safeNodeId}:${id}` },
1970
+ );
1971
+ }
1972
+ return Object.freeze({
1973
+ relation: Object.freeze(clone(parent.subgraphRuns.get(safeNodeId))),
1974
+ parentRun: runProjection(parent),
1975
+ childRun: runProjection(child),
1976
+ });
1977
+ }
1978
+
1979
+ settleSubgraph(parentRunId, nodeId) {
1980
+ const parent = this._run(parentRunId);
1981
+ const safeNodeId = safeIdentifier(nodeId, "nodeId");
1982
+ const relation = parent.subgraphRuns.get(safeNodeId);
1983
+ if (!relation) {
1984
+ throw kernelError(
1985
+ "CC_GRAPH_SUBGRAPH_NOT_STARTED",
1986
+ `subgraph has not been started: ${safeNodeId}`,
1987
+ );
1988
+ }
1989
+ if (["succeeded", "failed", "cancelled"].includes(relation.status)) {
1990
+ return runProjection(parent);
1991
+ }
1992
+ const child = this._loadRun(relation.childRunId);
1993
+ if (!child) {
1994
+ throw kernelError(
1995
+ "CC_GRAPH_SUBGRAPH_RUN_NOT_FOUND",
1996
+ `subgraph child run not found: ${relation.childRunId}`,
1997
+ );
1998
+ }
1999
+ if (!TERMINAL_RUN.has(child.status)) {
2000
+ throw kernelError(
2001
+ "CC_GRAPH_SUBGRAPH_ACTIVE",
2002
+ `subgraph child run is not terminal: ${child.status}`,
2003
+ );
2004
+ }
2005
+ return this._transaction(
2006
+ parent,
2007
+ "subgraph.settled",
2008
+ {
2009
+ nodeId: safeNodeId,
2010
+ childRunId: child.id,
2011
+ childStatus: child.status,
2012
+ },
2013
+ () => {
2014
+ const now = this.now();
2015
+ const state = parent.nodeStates.get(safeNodeId);
2016
+ const succeeded = child.status === "succeeded";
2017
+ relation.status = succeeded
2018
+ ? "succeeded"
2019
+ : child.status === "cancelled"
2020
+ ? "cancelled"
2021
+ : "failed";
2022
+ relation.updatedAt = nowIso(() => now);
2023
+ relation.completedAt = nowIso(() => now);
2024
+ relation.childStatus = child.status;
2025
+ settleSubgraphBudget(parent, relation, child.budgetUsed);
2026
+ state.status = succeeded
2027
+ ? "succeeded"
2028
+ : child.status === "cancelled"
2029
+ ? "cancelled"
2030
+ : child.status === "budget_exhausted"
2031
+ ? "budget_exhausted"
2032
+ : "failed";
2033
+ state.blockedRoot = succeeded ? null : child.id;
2034
+ state.updatedAt = nowIso(() => now);
2035
+ propagate(parent, () => now);
2036
+ refreshLoopStates(parent, () => now);
2037
+ parent.status = classifyQuiescence(parent, now);
2038
+ if (TERMINAL_RUN.has(parent.status)) {
2039
+ parent.completedAt = nowIso(() => now);
2040
+ }
2041
+ return runProjection(parent);
2042
+ },
2043
+ {
2044
+ idempotencyKey: `subgraph-settle:${safeNodeId}:${child.id}:${child.status}`,
2045
+ },
2046
+ );
2047
+ }
2048
+
1202
2049
  readyNodes(runId) {
1203
2050
  const run = this._run(runId);
1204
2051
  const now = this.now();
2052
+ const compensationEntry = currentCompensationEntry(run);
2053
+ if (compensationEntry) {
2054
+ const state = run.nodeStates.get(compensationEntry.compensationNodeId);
2055
+ if (state?.status !== "pending") return [];
2056
+ return [
2057
+ Object.freeze({
2058
+ nodeId: compensationEntry.compensationNodeId,
2059
+ compensationForNodeId: compensationEntry.nodeId,
2060
+ iterationPath: Object.freeze([...compensationEntry.iterationPath]),
2061
+ effectIdempotencyKey: iterationEffectIdempotencyKey(
2062
+ run.compiled.nodes[compensationEntry.compensationNodeId]
2063
+ .idempotencyKey,
2064
+ compensationEntry.iterationPath,
2065
+ ),
2066
+ priority: Object.freeze({
2067
+ base: 1000,
2068
+ donation: 0,
2069
+ aging: 0,
2070
+ criticalPathBoost: 0,
2071
+ total: 1000,
2072
+ queueWaitMs: 0,
2073
+ }),
2074
+ }),
2075
+ ];
2076
+ }
1205
2077
  return run.compiled.topologicalOrder
1206
2078
  .filter((nodeId) => {
1207
2079
  const state = run.nodeStates.get(nodeId);
@@ -1209,6 +2081,32 @@ export class GraphKernel {
1209
2081
  })
1210
2082
  .map((nodeId) => ({
1211
2083
  nodeId,
2084
+ iterationPath: Object.freeze([
2085
+ ...(run.nodeStates.get(nodeId)?.iterationPath || []),
2086
+ ]),
2087
+ ...(["workspace_write", "external"].includes(
2088
+ run.compiled.nodes[nodeId].effectClass,
2089
+ )
2090
+ ? {
2091
+ effectIdempotencyKey: iterationEffectIdempotencyKey(
2092
+ run.compiled.nodes[nodeId].idempotencyKey,
2093
+ run.nodeStates.get(nodeId)?.iterationPath || [],
2094
+ ),
2095
+ }
2096
+ : {}),
2097
+ ...(run.compiled.subgraphCalls?.[nodeId]
2098
+ ? {
2099
+ dispatch: "subgraph",
2100
+ definitionId: run.compiled.subgraphCalls[nodeId].definitionId,
2101
+ revisionDigest: run.compiled.subgraphCalls[nodeId].revisionDigest,
2102
+ maxDepth: run.compiled.subgraphCalls[nodeId].maxDepth,
2103
+ inputBindings:
2104
+ run.compiled.subgraphCalls[nodeId].inputBindings || [],
2105
+ outputBindings:
2106
+ run.compiled.subgraphCalls[nodeId].outputBindings || [],
2107
+ budget: run.compiled.subgraphCalls[nodeId].budget || {},
2108
+ }
2109
+ : {}),
1212
2110
  priority: effectivePriority(run, nodeId, now, this.agingWindowMs),
1213
2111
  }))
1214
2112
  .sort(
@@ -1224,6 +2122,7 @@ export class GraphKernel {
1224
2122
  if (!ready.length) return null;
1225
2123
  let lastConflict = null;
1226
2124
  for (const candidate of ready) {
2125
+ if (candidate.dispatch === "subgraph") continue;
1227
2126
  try {
1228
2127
  return this.assignNode(runId, candidate.nodeId, agentId, options);
1229
2128
  } catch (error) {
@@ -1250,13 +2149,30 @@ export class GraphKernel {
1250
2149
  role = "executor",
1251
2150
  ttlMs = 60_000,
1252
2151
  leaseId = this.createId(),
1253
- attemptId = this.createId(),
2152
+ attemptId = null,
1254
2153
  grant = {},
1255
2154
  } = {},
1256
2155
  ) {
1257
2156
  const run = this._run(runId);
1258
2157
  const safeNodeId = safeIdentifier(nodeId, "nodeId");
1259
2158
  const safeAgentId = safeIdentifier(agentId, "agentId");
2159
+ const plannedCompensation = compensationAssignment(run, safeNodeId);
2160
+ const iterationPath = [
2161
+ ...(plannedCompensation?.iterationPath ||
2162
+ run.nodeStates.get(safeNodeId)?.iterationPath ||
2163
+ []),
2164
+ ];
2165
+ const attemptNumber =
2166
+ [...run.attempts.values()].filter(
2167
+ (candidate) =>
2168
+ candidate.nodeId === safeNodeId &&
2169
+ JSON.stringify(candidate.iterationPath || []) ===
2170
+ JSON.stringify(iterationPath),
2171
+ ).length + 1;
2172
+ const requestedAttemptId = safeIdentifier(
2173
+ attemptId || executionAttemptId(safeNodeId, iterationPath, attemptNumber),
2174
+ "attemptId",
2175
+ );
1260
2176
  return this._transaction(
1261
2177
  run,
1262
2178
  "assignment.started",
@@ -1265,6 +2181,7 @@ export class GraphKernel {
1265
2181
  const now = this.now();
1266
2182
  sweepExpirations(run, now);
1267
2183
  propagate(run, () => now);
2184
+ refreshLoopStates(run, () => now);
1268
2185
  const node = run.compiled.nodes[safeNodeId];
1269
2186
  const state = run.nodeStates.get(safeNodeId);
1270
2187
  if (!node || !state) {
@@ -1273,10 +2190,19 @@ export class GraphKernel {
1273
2190
  `Graph node not found: ${safeNodeId}`,
1274
2191
  );
1275
2192
  }
1276
- if (
1277
- state.status !== "pending" ||
1278
- !nodeReadiness(run, safeNodeId).ready
1279
- ) {
2193
+ if (run.compiled.subgraphCalls?.[safeNodeId]) {
2194
+ throw kernelError(
2195
+ "CC_GRAPH_SUBGRAPH_DISPATCH_REQUIRED",
2196
+ `subgraph nodes must be started through startSubgraph(): ${safeNodeId}`,
2197
+ );
2198
+ }
2199
+ const compensationEntry = compensationAssignment(run, safeNodeId);
2200
+ const ready = compensationEntry
2201
+ ? state.status === "pending"
2202
+ : run.compensation.status !== "running" &&
2203
+ state.status === "pending" &&
2204
+ nodeReadiness(run, safeNodeId).ready;
2205
+ if (!ready) {
1280
2206
  throw kernelError(
1281
2207
  "CC_GRAPH_NODE_NOT_READY",
1282
2208
  `Graph node is not ready: ${safeNodeId}`,
@@ -1314,13 +2240,21 @@ export class GraphKernel {
1314
2240
  }
1315
2241
  const fence = (run.fenceCounters.get(safeNodeId) || 0) + 1;
1316
2242
  run.fenceCounters.set(safeNodeId, fence);
1317
- const id = safeIdentifier(attemptId, "attemptId");
2243
+ const id = requestedAttemptId;
1318
2244
  const attempt = {
1319
2245
  id,
1320
2246
  runId: run.id,
1321
2247
  nodeId: safeNodeId,
2248
+ iterationPath,
2249
+ attempt: attemptNumber,
1322
2250
  agentId: safeAgentId,
1323
- role,
2251
+ role: compensationEntry ? "executor" : role,
2252
+ compensationForNodeId: compensationEntry?.nodeId || null,
2253
+ effectIdempotencyKey: ["workspace_write", "external"].includes(
2254
+ node.effectClass,
2255
+ )
2256
+ ? iterationEffectIdempotencyKey(node.idempotencyKey, iterationPath)
2257
+ : null,
1324
2258
  leaseId: safeIdentifier(leaseId, "leaseId"),
1325
2259
  fence,
1326
2260
  status: "active",
@@ -1341,7 +2275,7 @@ export class GraphKernel {
1341
2275
  agent.updatedAt = nowIso(() => now);
1342
2276
  return attemptProjection(attempt);
1343
2277
  },
1344
- { idempotencyKey: `assignment:${attemptId}` },
2278
+ { idempotencyKey: `assignment:${requestedAttemptId}` },
1345
2279
  );
1346
2280
  }
1347
2281
 
@@ -1423,7 +2357,11 @@ export class GraphKernel {
1423
2357
  "node did not declare an effectful execution class",
1424
2358
  );
1425
2359
  }
1426
- if (!key || key !== node.idempotencyKey) {
2360
+ const authenticatedKey = iterationEffectIdempotencyKey(
2361
+ node.idempotencyKey,
2362
+ attempt.iterationPath,
2363
+ );
2364
+ if (!key || key !== authenticatedKey) {
1427
2365
  throw kernelError(
1428
2366
  "CC_GRAPH_EFFECT_IDEMPOTENCY_REQUIRED",
1429
2367
  "effect must use the idempotency key authenticated by Graph Compiler",
@@ -1440,9 +2378,11 @@ export class GraphKernel {
1440
2378
  runId: run.id,
1441
2379
  nodeId: attempt.nodeId,
1442
2380
  attemptId: attempt.id,
2381
+ iterationPath: [...attempt.iterationPath],
1443
2382
  leaseId: attempt.leaseId,
1444
2383
  fence: attempt.fence,
1445
2384
  idempotencyKey: key,
2385
+ baseIdempotencyKey: node.idempotencyKey,
1446
2386
  operationDigest,
1447
2387
  status: "started",
1448
2388
  receipt: null,
@@ -1683,6 +2623,7 @@ export class GraphKernel {
1683
2623
  );
1684
2624
  const state = run.nodeStates.get(attempt.nodeId);
1685
2625
  const node = run.compiled.nodes[attempt.nodeId];
2626
+ const compensationEntry = compensationAssignment(run, attempt.nodeId);
1686
2627
  if (state.acceptedAttemptId && state.acceptedAttemptId !== attempt.id) {
1687
2628
  attempt.status = "rejected";
1688
2629
  attempt.participationStatus = "loser";
@@ -1701,7 +2642,16 @@ export class GraphKernel {
1701
2642
  attempt.terminalEvidence = clone(evidence);
1702
2643
  state.status = "reconciliation_required";
1703
2644
  state.updatedAt = nowIso(this.now);
1704
- run.status = "reconciliation_required";
2645
+ if (compensationEntry) {
2646
+ failCompensation(
2647
+ run,
2648
+ compensationEntry,
2649
+ error || "compensation outcome is unknown",
2650
+ this.now(),
2651
+ );
2652
+ } else {
2653
+ run.status = "reconciliation_required";
2654
+ }
1705
2655
  return attemptProjection(attempt);
1706
2656
  }
1707
2657
  if (outcome === "succeeded") {
@@ -1757,8 +2707,7 @@ export class GraphKernel {
1757
2707
  attempt.participationStatus = "failed";
1758
2708
  attempt.error = error == null ? null : String(error).slice(0, 4096);
1759
2709
  attempt.terminalEvidence = clone(evidence);
1760
- const attemptNumber = state.attemptIds.length;
1761
- if (attemptNumber <= Number(node.retryLimit || 0)) {
2710
+ if (attempt.attempt <= Number(node.retryLimit || 0)) {
1762
2711
  state.status = "pending";
1763
2712
  } else {
1764
2713
  state.status = outcome === "timed_out" ? "timed_out" : "failed";
@@ -1781,8 +2730,22 @@ export class GraphKernel {
1781
2730
  agent.status = "idle";
1782
2731
  agent.updatedAt = nowIso(this.now);
1783
2732
  }
1784
- propagate(run, this.now);
1785
- run.status = classifyQuiescence(run, this.now());
2733
+ if (compensationEntry) {
2734
+ if (outcome === "succeeded") {
2735
+ completeCompensationStep(run, compensationEntry, this.now());
2736
+ } else if (state.status !== "pending") {
2737
+ failCompensation(
2738
+ run,
2739
+ compensationEntry,
2740
+ error || outcome,
2741
+ this.now(),
2742
+ );
2743
+ }
2744
+ } else {
2745
+ propagate(run, this.now);
2746
+ if (refreshLoopStates(run, this.now)) propagate(run, this.now);
2747
+ run.status = classifyQuiescence(run, this.now());
2748
+ }
1786
2749
  if (TERMINAL_RUN.has(run.status)) run.completedAt = nowIso(this.now);
1787
2750
  return attemptProjection(attempt);
1788
2751
  },
@@ -1832,6 +2795,20 @@ export class GraphKernel {
1832
2795
  }
1833
2796
  }),
1834
2797
  );
2798
+ for (const relation of run.subgraphRuns.values()) {
2799
+ if (!["starting", "running"].includes(relation.status)) continue;
2800
+ try {
2801
+ const child = this._loadRun(relation.childRunId);
2802
+ if (child && !TERMINAL_RUN.has(child.status)) {
2803
+ await this.cancelRun(child.id, { reason, interrupt });
2804
+ }
2805
+ } catch (error) {
2806
+ failures.push({
2807
+ subgraphRunId: relation.childRunId,
2808
+ error: String(error?.message || error).slice(0, 4096),
2809
+ });
2810
+ }
2811
+ }
1835
2812
  return this._transaction(
1836
2813
  run,
1837
2814
  failures.length ? "run.cancel_reconciliation" : "run.cancelled",
@@ -1851,6 +2828,27 @@ export class GraphKernel {
1851
2828
  state.status = failure ? "reconciliation_required" : "cancelled";
1852
2829
  state.updatedAt = nowIso(this.now);
1853
2830
  }
2831
+ for (const relation of run.subgraphRuns.values()) {
2832
+ if (!["starting", "running"].includes(relation.status)) continue;
2833
+ const failure = failures.find(
2834
+ (entry) => entry.subgraphRunId === relation.childRunId,
2835
+ );
2836
+ const child = this._loadRun(relation.childRunId);
2837
+ if (!failure) {
2838
+ settleSubgraphBudget(
2839
+ run,
2840
+ relation,
2841
+ child?.budgetUsed || emptyBudgetUsage(),
2842
+ );
2843
+ }
2844
+ relation.status = failure ? "failed" : "cancelled";
2845
+ relation.updatedAt = nowIso(this.now);
2846
+ relation.completedAt = failure ? null : nowIso(this.now);
2847
+ const state = run.nodeStates.get(relation.nodeId);
2848
+ state.status = failure ? "reconciliation_required" : "cancelled";
2849
+ state.blockedRoot = relation.childRunId;
2850
+ state.updatedAt = nowIso(this.now);
2851
+ }
1854
2852
  const unsettledEffects = [...run.effects.values()].filter(
1855
2853
  (effect) => effect.status === "started",
1856
2854
  );
@@ -1861,6 +2859,7 @@ export class GraphKernel {
1861
2859
  state.status = "reconciliation_required";
1862
2860
  state.updatedAt = nowIso(this.now);
1863
2861
  }
2862
+ refreshLoopStates(run, this.now);
1864
2863
  const requiresReconciliation =
1865
2864
  failures.length > 0 || unsettledEffects.length > 0;
1866
2865
  run.status = requiresReconciliation
@@ -1872,6 +2871,125 @@ export class GraphKernel {
1872
2871
  );
1873
2872
  }
1874
2873
 
2874
+ beginCompensation(
2875
+ runId,
2876
+ { triggerNodeId = null, reason = "forward_failure" } = {},
2877
+ ) {
2878
+ const run = this._run(runId);
2879
+ const trigger =
2880
+ triggerNodeId == null
2881
+ ? null
2882
+ : safeIdentifier(triggerNodeId, "triggerNodeId");
2883
+ if (run.compensation.status !== "idle") {
2884
+ if (
2885
+ run.compensation.triggerNodeId === trigger &&
2886
+ run.compensation.reason === String(reason)
2887
+ ) {
2888
+ return runProjection(run);
2889
+ }
2890
+ throw kernelError(
2891
+ "CC_GRAPH_COMPENSATION_ALREADY_STARTED",
2892
+ "compensation has already been started for this run",
2893
+ );
2894
+ }
2895
+ return this._transaction(
2896
+ run,
2897
+ "compensation.started",
2898
+ { triggerNodeId: trigger, reason },
2899
+ () => {
2900
+ if (
2901
+ !["failed", "partial", "cancelled", "budget_exhausted"].includes(
2902
+ run.status,
2903
+ )
2904
+ ) {
2905
+ throw kernelError(
2906
+ "CC_GRAPH_COMPENSATION_NOT_AVAILABLE",
2907
+ "compensation requires a failed, partial, cancelled, or budget-exhausted terminal run",
2908
+ );
2909
+ }
2910
+ if (trigger && !run.compiled.nodes[trigger]) {
2911
+ throw kernelError(
2912
+ "CC_GRAPH_NODE_NOT_FOUND",
2913
+ `Graph node not found: ${trigger}`,
2914
+ );
2915
+ }
2916
+ const activeAttempts = [...run.attempts.values()].filter((attempt) =>
2917
+ ACTIVE_ATTEMPT.has(attempt.status),
2918
+ );
2919
+ const unsettledEffects = [...run.effects.values()].filter((effect) =>
2920
+ ["started", "unknown"].includes(effect.status),
2921
+ );
2922
+ if (activeAttempts.length || unsettledEffects.length) {
2923
+ throw kernelError(
2924
+ "CC_GRAPH_COMPENSATION_UNSETTLED",
2925
+ "compensation cannot start before attempts and effects are settled",
2926
+ {
2927
+ attemptIds: activeAttempts.map((attempt) => attempt.id).sort(),
2928
+ effectIds: unsettledEffects.map((effect) => effect.id).sort(),
2929
+ },
2930
+ );
2931
+ }
2932
+ const plan = (run.compiled.forwardTopologicalOrder || [])
2933
+ .filter((nodeId) => run.compiled.compensationByNode?.[nodeId])
2934
+ .reverse()
2935
+ .flatMap((nodeId) =>
2936
+ committedEffectsForNode(run, nodeId)
2937
+ .reverse()
2938
+ .map((effect) => ({
2939
+ nodeId,
2940
+ compensationNodeId: run.compiled.compensationByNode[nodeId],
2941
+ iterationPath: [...(effect.iterationPath || [])],
2942
+ effectIds: [effect.id],
2943
+ })),
2944
+ );
2945
+ if (plan.length === 0) {
2946
+ throw kernelError(
2947
+ "CC_GRAPH_COMPENSATION_NOT_REQUIRED",
2948
+ "run has no committed compensatable effects",
2949
+ );
2950
+ }
2951
+ const now = this.now();
2952
+ const preparedTargets = new Set();
2953
+ for (const entry of plan) {
2954
+ if (preparedTargets.has(entry.compensationNodeId)) continue;
2955
+ const state = run.nodeStates.get(entry.compensationNodeId);
2956
+ if (!state || state.status !== "skipped") {
2957
+ throw kernelError(
2958
+ "CC_GRAPH_COMPENSATION_TARGET_INVALID",
2959
+ `compensation target is not isolated and idle: ${entry.compensationNodeId}`,
2960
+ );
2961
+ }
2962
+ state.status = "pending";
2963
+ state.blockedRoot = null;
2964
+ state.iterationPath = [...entry.iterationPath];
2965
+ state.updatedAt = nowIso(() => now);
2966
+ preparedTargets.add(entry.compensationNodeId);
2967
+ }
2968
+ run.compensation = {
2969
+ status: "running",
2970
+ triggerNodeId: trigger,
2971
+ reason: String(reason),
2972
+ terminalStatus: run.status,
2973
+ plan,
2974
+ currentIndex: 0,
2975
+ completedNodeIds: [],
2976
+ completedEntries: [],
2977
+ failure: null,
2978
+ startedAt: nowIso(() => now),
2979
+ completedAt: null,
2980
+ };
2981
+ run.status = "running";
2982
+ run.completedAt = null;
2983
+ run.phase = "sealed";
2984
+ return runProjection(run);
2985
+ },
2986
+ {
2987
+ allowTerminal: true,
2988
+ idempotencyKey: `compensation-start:${run.id}:${trigger || "terminal"}`,
2989
+ },
2990
+ );
2991
+ }
2992
+
1875
2993
  sendMessage(
1876
2994
  runId,
1877
2995
  {
@@ -2273,10 +3391,20 @@ export class GraphKernel {
2273
3391
  sender.updatedAt = nowIso(() => now);
2274
3392
  const fence = (run.fenceCounters.get(sender.nodeId) || 0) + 1;
2275
3393
  run.fenceCounters.set(sender.nodeId, fence);
3394
+ const iterationPath = [...(sender.iterationPath || [])];
3395
+ const attemptNumber =
3396
+ [...run.attempts.values()].filter(
3397
+ (candidate) =>
3398
+ candidate.nodeId === sender.nodeId &&
3399
+ JSON.stringify(candidate.iterationPath || []) ===
3400
+ JSON.stringify(iterationPath),
3401
+ ).length + 1;
2276
3402
  const newAttempt = {
2277
3403
  id: safeIdentifier(attemptId, "attemptId"),
2278
3404
  runId: run.id,
2279
3405
  nodeId: sender.nodeId,
3406
+ iterationPath,
3407
+ attempt: attemptNumber,
2280
3408
  agentId: handoff.toAgentId,
2281
3409
  role: "executor",
2282
3410
  leaseId: safeIdentifier(leaseId, "leaseId"),