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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-D5_pwQ7M.js → AIOps-BnZ24WRk.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DgoYO32D.js → ActionButton-ivXY7MGb.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BIpoZ4QH.js → Analytics-BQS4OD-k.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BJ3XrCEe.js → AppLayout-wOBuOKTO.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-DUFs_izZ.js → Artifacts-BYuC4nw-.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BZdPwwfL.js → Audit-BIeVfBt6.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DK9Fn-8B.js → BackgroundAgents-hIuyhmJ7.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Bd6bOhV_.js → Backup-B6ohdgPQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CRnWlXrd.js → BaseInput-BzgYCw6h.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D1G_yEl6.js → Chat-CwahvZU1.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDAlEtm-.js → ChatBubbleRenderer-B-1IZ_k8.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CP35SH7R.js → Checkbox-26EDP2x0.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DugeL4pj.js → Codegen-DYLw1Zvd.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DPuJsvKg.js → Col-DH_zAvD-.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CZqNjNNp.js → Community-DURrTXJu.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-eeyIPEAb.js → Compact-CQyBqN0p.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-MGzTw87k.js → Compliance-a0DfgKHD.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D3goVIA5.js → Cowork-C6fY_5Ke.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-9HA8hLLG.js → Cron-DOwmpbTe.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DCSOX3l6.js → Crosschain-DEPOIEUs.js} +1 -1
- package/src/assets/web-panel/assets/{DID-ji3lN5Bc.js → DID-DaKc-CEV.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DlZvF1E5.js → Dashboard-C-0KLXq5.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-2FyZcow9.js → Dropdown-CnsDukzr.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-qH2bSpSY.js → EmailListRenderer-Bi_ACD4N.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bfx47wZj.js → FamilyGuardDashboard-BZASpnx3.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DhGcZbIv.js → Federation-43oTtzyp.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BMgi9_YQ.js → FormItemContext-oyVadnkZ.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DugMac-4.js → GenericCardRenderer-Cwvfd6TZ.js} +1 -1
- package/src/assets/web-panel/assets/{Git-D943BpH-.js → Git-Cgi2wvU0.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DZ66a8Aa.js → Governance-l8TWDKZ0.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-B00Mp2tN.js → Inference-Dc8j6TaO.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BdwXXO2Q.js → KnowledgeGraph-DgcO-Fwz.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-_MVXyYWc.js → Logs-DjiZpiwa.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-hOTeNyi7.js → MarkdownRenderer-B5MFCi0N.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-CxeHRsnO.js → Marketplace-BeDn1hFc.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BOieTxZ4.js → McpTools-PGdVfWK6.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-BQD26CM3.js → Memory-e76lY3a1.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CPJ8ZlvR.js → MobileBridge-DKlQqot_.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-D7qRVu0y.js → MobileProjects-C4g6WOEr.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BOyndGuk.js → Mtc-1j3NpRQq.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-CXQ_8ZoR.js → MtcAudit-DgwiVd1C.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-32Oh9KbH.js → Multisig-Dzg5lZZY.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CFAd00CX.js → NLProgramming-D7GYhst-.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BBCY_qJA.js → Notes-Blcfvaic.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-uaQ0PdmW.js → NotificationSettings-Dc4Y6vfr.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-dgM87XIn.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CmFn7Oeo.js → Organization-DBXq024L.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-R_avat3a.js → Overflow-CFPbdx1Y.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BIhLFmkY.js → P2P-w7z61NR5.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DTJjk-_i.js → PdhVaultBrowser-B7eGGZOa.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CkzYv8Yb.js → Permissions-Df5Cl50D.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub--0QvN2Nz.js → PersonalDataHub-C-We7RF6.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Bfw5gQCH.js → Pipeline-B6fHPnKy.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DicjKfax.js → Privacy-B85BdiIc.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DUl_ykBp.js → ProjectInit-CcOyazSe.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CPYDvN8a.js → ProjectSettings-BE53KfuU.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DnZ9_fAr.js → Projects-DOvMOoe_.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BX1jP31w.js → Providers-5l7sT9lq.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-COab5qUl.js → QrScannerModal-DE37RXy4.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CdKQ_JZ0.js → QuickAsk-BGmGCo7j.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BPFHg8pK.js → Recommend-BFdx8Nrm.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-CqrfTJvO.js → RemoteSession-C1w-j_hB.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DM-l8Pb3.js → Reputation-BOvn-10y.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CKGBTeCw.js → Row-db-a1YO9.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Wb0YHIkc.js → RssFeed-DeS43ZGM.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DiBjxKv-.js → Search-BaQYNfeb.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BAVEmbWP.js → Security-yt1ESHDg.js} +4 -4
- package/src/assets/web-panel/assets/{Services-A_a3TIRn.js → Services-BcNqPUsS.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CloHJ-I7.js → Skeleton-X3ceUQWf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CkvV5hKa.js → Skills-DmYeax6C.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BYozxxsV.js → Sla-C_fdfbuJ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CWianiZF.js → SpeechSettings-CDqohXXQ.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BR4G75do.js → SyncSettings-B25maYEY.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-D6gyUVrM.js → Tasks-3ILwauYw.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DbfYIRzT.js → Templates-DUH0iuc4.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BHutvukT.js → Tenant-DrvhHf9T.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CYEiy2wJ.js → Terminal-C2lJVYHA.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D9s1AxfE.js → TimelineRenderer-C1Hf_jYn.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-ij9vxnoy.js → Tokens-DniS1NpS.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dq_DCVeQ.js → Trigger-4jrcb8oA.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Bs6wdfoH.js → Trust-DiBte55w.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CLazyr5t.js → UkeySign-CZRtc4VB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-viWotIHA.js → VideoEditing-BtYGJDmg.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DlZ-A78d.js → Wallet-D1nrTm7H.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DSmOND3Y.js → WebAuthn-DI91acpx.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BempLOJi.js → WorkflowEditor-BtQjI50d.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CyJzf4JI.js → chat-I4A-f4FC.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C62q7WrL.js → colors-QZkDkFVt.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-7dYM1KW3.js → compact-item--FtCdzpr.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CYTy7z2Q.js → createContext-CaIKIKxP.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-D7qVrkrO.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DHoclUqm.js → hasIn-DLYUII6-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW86F2Zw.js → index-0DYBQ-6e.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2efftJb.js → index-21I79XSf.js} +1 -1
- package/src/assets/web-panel/assets/{index-4gWv-Ubg.js → index-3rVtgqa2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXtrdrub.js → index-B65QxJaG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMxvWauP.js → index-BGWSmE3G.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dyq9tK5u.js → index-BLGJmgmx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYKwGrAn.js → index-BcgtvJbC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMRmxFU_.js → index-BebTT7iR.js} +1 -1
- package/src/assets/web-panel/assets/{index-XrsAHHQN.js → index-BkJgWW_L.js} +1 -1
- package/src/assets/web-panel/assets/{index-2C8Xx2UO.js → index-Bs0qPPFb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnEZ4ZHj.js → index-BvdwNnHC.js} +3 -3
- package/src/assets/web-panel/assets/{index-DkczsbiP.js → index-BxPoPofq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvEtHpLc.js → index-C3PnIEOH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DK84lAS0.js → index-CBtII-nl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKdv8cju.js → index-CL4c5D6G.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjeViaU-.js → index-CQ7OBHka.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhJjifZe.js → index-CXaeZCRP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BghdJ87W.js → index-CgMm85BY.js} +1 -1
- package/src/assets/web-panel/assets/{index-lHfNxiB5.js → index-Ch0IFpGy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJzZaPlb.js → index-CisYjy88.js} +1 -1
- package/src/assets/web-panel/assets/{index-DghnbMlY.js → index-D2xETCkj.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5YWYlW9.js → index-D4CXFU-w.js} +1 -1
- package/src/assets/web-panel/assets/index-D9Y_ZwZn.js +1 -0
- package/src/assets/web-panel/assets/{index-LK1MZoBj.js → index-DA_TPdzX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOFAwpUK.js → index-DAuf8r5o.js} +1 -1
- package/src/assets/web-panel/assets/{index-DA-_807C.js → index-DGLHSCHM.js} +1 -1
- package/src/assets/web-panel/assets/{index-9uR7rc9a.js → index-DJrd_CR3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVTnruDt.js → index-DK2r6Bv7.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZXYp76IB.js → index-DVitI3UQ.js} +1 -1
- package/src/assets/web-panel/assets/index-D_cPes4G.js +1 -0
- package/src/assets/web-panel/assets/{index-Bzz0aJ86.js → index-Deg2S9ZJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHgSRimK.js → index-Du2wUavy.js} +1 -1
- package/src/assets/web-panel/assets/{index-94ktimZG.js → index-OQPIS_am.js} +1 -1
- package/src/assets/web-panel/assets/{index-3pI7AfmD.js → index-Ooo_5jHo.js} +1 -1
- package/src/assets/web-panel/assets/{index-DNLD3nZY.js → index-Ygi2rYL1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZRBvN6i.js → index-eTqL91aV.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6BC4mmz.js → index-hfiZ-i2b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAYanuSk.js → index-vfiDxdN2.js} +1 -1
- package/src/assets/web-panel/assets/{index-NuNCstRO.js → index-yayaayEA.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CwCl5sFQ.js → initDefaultProps-DDkTvw8k.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BFJFy70N.js → motion-fkr96tN5.js} +1 -1
- package/src/assets/web-panel/assets/{move-ByrtVGq8.js → move-CGBV0SV2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BFiH1mT3.js → mtc-parser-DAsCliFt.js} +1 -1
- package/src/assets/web-panel/assets/{omit-bdvgpivj.js → omit-BM8PMmKN.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BlfuuE3s.js → pickAttrs-C6SdWoTK.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-t2SC3DMy.js → placementArrow-BG0EVu-d.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CoEXv2Li.js → responsiveObserve-L10IvfYC.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BfXCf0Fu.js → slide-TddIkbrn.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DNsdyOuS.js → statusUtils-FlveF2Yn.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-9aHukmoG.js → styleChecker-BrXRyebg.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-dTkPxzHN.js → useFlexGapSupport-B7_oiDXZ.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DINwfItP.js → useFs-CQYeRYWb.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Bj_RriBV.js → usePersonalDataHub-DspIkhVi.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BzRAZrTc.js → vnode-BCwmwMAW.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CamPjkeK.js → zoom-B0cEGluV.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/team.js +74 -2
- package/src/data/changelog.json +26 -0
- package/src/generated/cc-agent-protocol.schema.json +49 -0
- package/src/lib/agent-team/team-mailbox.js +508 -2
- package/src/lib/agent-team/team-message-bridge.js +644 -0
- package/src/lib/agent-team/team-message-tools.js +198 -0
- package/src/lib/agent-team/team-runner.js +57 -3
- package/src/lib/graph-kernel/compiler.js +548 -16
- package/src/lib/graph-kernel/runtime.js +1158 -30
- package/src/lib/graph-kernel/trace-reducer.js +101 -7
- package/src/lib/subagent-inheritance.js +5 -3
- package/src/runtime/agent-core.js +38 -0
- package/src/runtime/headless-runner.js +36 -3
- package/src/assets/web-panel/assets/OrderTableRenderer-kpBxJ-SG.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BdTg_AMF.js +0 -1
- package/src/assets/web-panel/assets/index-B6WvFV4N.js +0 -1
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1278
|
-
|
|
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 =
|
|
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:${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1785
|
-
|
|
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"),
|