@martinloop/mcp 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/vendor/adapters/claude-cli.d.ts +11 -1
- package/dist/vendor/adapters/claude-cli.js +285 -71
- package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
- package/dist/vendor/adapters/cli-bridge.js +129 -149
- package/dist/vendor/adapters/codex-cli.d.ts +3 -3
- package/dist/vendor/adapters/codex-cli.js +10 -24
- package/dist/vendor/adapters/codex-host.js +127 -132
- package/dist/vendor/adapters/index.d.ts +2 -1
- package/dist/vendor/adapters/index.js +2 -1
- package/dist/vendor/adapters/openai-compatible.js +88 -6
- package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
- package/dist/vendor/adapters/process-supervisor.js +541 -0
- package/dist/vendor/adapters/verifier-only.d.ts +10 -1
- package/dist/vendor/adapters/verifier-only.js +43 -1
- package/dist/vendor/contracts/external-outcome.d.ts +102 -0
- package/dist/vendor/contracts/external-outcome.js +226 -0
- package/dist/vendor/contracts/index.d.ts +8 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
- package/dist/vendor/contracts/swarm-hosted.js +629 -0
- package/dist/vendor/contracts/swarm.d.ts +449 -0
- package/dist/vendor/contracts/swarm.js +550 -0
- package/dist/vendor/core/external-outcome.d.ts +34 -0
- package/dist/vendor/core/external-outcome.js +515 -0
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/index.d.ts +47 -1
- package/dist/vendor/core/index.js +53 -3
- package/dist/vendor/core/leash.d.ts +1 -0
- package/dist/vendor/core/leash.js +13 -0
- package/dist/vendor/core/persistence/integrity.d.ts +4 -0
- package/dist/vendor/core/persistence/integrity.js +19 -6
- package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
- package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
- package/dist/vendor/core/swarm/candidates.d.ts +78 -0
- package/dist/vendor/core/swarm/candidates.js +810 -0
- package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
- package/dist/vendor/core/swarm/evidence-projection.js +128 -0
- package/dist/vendor/core/swarm/evidence.d.ts +40 -0
- package/dist/vendor/core/swarm/evidence.js +575 -0
- package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
- package/dist/vendor/core/swarm/hosted-export.js +362 -0
- package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
- package/dist/vendor/core/swarm/hosted-projection.js +258 -0
- package/dist/vendor/core/swarm/index.d.ts +110 -0
- package/dist/vendor/core/swarm/index.js +1060 -0
- package/dist/vendor/core/swarm/integration.d.ts +76 -0
- package/dist/vendor/core/swarm/integration.js +615 -0
- package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
- package/dist/vendor/core/swarm/lifecycle.js +252 -0
- package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
- package/dist/vendor/core/swarm/live-runtime.js +1482 -0
- package/dist/vendor/core/swarm/live-store.d.ts +99 -0
- package/dist/vendor/core/swarm/live-store.js +860 -0
- package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
- package/dist/vendor/core/swarm/operational-reader.js +96 -0
- package/dist/vendor/core/swarm/operations.d.ts +29 -0
- package/dist/vendor/core/swarm/operations.js +128 -0
- package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
- package/dist/vendor/core/swarm/parent-receipt.js +489 -0
- package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
- package/dist/vendor/core/swarm/receipt-projection.js +120 -0
- package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
- package/dist/vendor/core/swarm/scheduler.js +555 -0
- package/dist/vendor/core/swarm/verification.d.ts +70 -0
- package/dist/vendor/core/swarm/verification.js +295 -0
- package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
- package/dist/vendor/core/swarm/workspaces.js +488 -0
- package/dist/vendor/core/verified-handoff.d.ts +4 -0
- package/dist/vendor/core/verified-handoff.js +4 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,1482 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { createSwarmRunRecord, validateSwarmLivePlan } from "../../contracts/index.js";
|
|
5
|
+
import { runMartin } from "../index.js";
|
|
6
|
+
import { redactSecretsFromText } from "../leash.js";
|
|
7
|
+
import { createFileRunStore, runDir, verifyReceiptIntegrityFromFiles } from "../persistence/index.js";
|
|
8
|
+
import { authenticateSwarmReceiptSealCommit, resolveSwarmReceiptIntegrityPath, verifySwarmReceiptIntegrityFromFiles, writeSwarmReceiptIntegrityMaterial, } from "../persistence/swarm-integrity.js";
|
|
9
|
+
export { readSwarmReceiptProjection, verifySwarmReceiptProjection, } from "./receipt-projection.js";
|
|
10
|
+
import { readAndSealSwarmEvidence } from "./evidence.js";
|
|
11
|
+
import { buildParentSwarmReceipt } from "./parent-receipt.js";
|
|
12
|
+
import { captureAndAdmitSwarmCandidate, inventorySwarmCandidatePaths } from "./candidates.js";
|
|
13
|
+
import { runParentSwarmPipeline } from "./index.js";
|
|
14
|
+
import { createSwarmLiveStore } from "./live-store.js";
|
|
15
|
+
import { createSwarmBudgetLedger, extendSwarmBudgetLease, releaseSwarmBudgetLease, reserveSwarmBudgetLease, selectNextSwarmBatch, settleSwarmBudgetLease } from "./scheduler.js";
|
|
16
|
+
import { assertSwarmPathIdentifier, createSwarmWorkspaceManager } from "./workspaces.js";
|
|
17
|
+
const authoritativeVerifiedResults = new WeakSet();
|
|
18
|
+
/** Checks in-process Core authority; structurally forged CLI/test objects cannot pass. */
|
|
19
|
+
export function isAuthoritativeLiveSwarmVerified(result) {
|
|
20
|
+
if (!authoritativeVerifiedResults.has(result))
|
|
21
|
+
return false;
|
|
22
|
+
const verifiedEvents = result.record.events.filter((event) => event.type === "SWARM_VERIFIED");
|
|
23
|
+
return result.outcome.state === "verified"
|
|
24
|
+
&& result.parent?.outcome.state === "verified"
|
|
25
|
+
&& result.parent.disposition === "ready"
|
|
26
|
+
&& result.parent.blockingInvariants.length === 0
|
|
27
|
+
&& verifiedEvents.length === 1
|
|
28
|
+
&& verifiedEvents[0]?.swarmId === result.record.swarmId
|
|
29
|
+
&& verifiedEvents[0]?.parentPolicyVersion === result.record.parentContract.policyVersion;
|
|
30
|
+
}
|
|
31
|
+
const productionDependencies = {
|
|
32
|
+
captureCandidate: captureAndAdmitSwarmCandidate,
|
|
33
|
+
inventoryCandidatePaths: inventorySwarmCandidatePaths,
|
|
34
|
+
parentPipeline: runParentSwarmPipeline
|
|
35
|
+
};
|
|
36
|
+
/** Public high-level live entrypoint. Raw lifecycle and finalization authority stay internal. */
|
|
37
|
+
export function runLiveSwarm(input) {
|
|
38
|
+
return runLiveSwarmWithDependencies(input, productionDependencies);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Production composition boundary for CLI/operator callers. It accepts only
|
|
42
|
+
* validated operator paths plus narrow child/verifier executors. Raw store,
|
|
43
|
+
* worktree, evidence, lifecycle, and parent-finalization authority remain
|
|
44
|
+
* inside Core and are never exported from the package root.
|
|
45
|
+
*/
|
|
46
|
+
export async function runProductionLiveSwarm(input) {
|
|
47
|
+
const validation = validateSwarmLivePlan(input.plan);
|
|
48
|
+
if (!validation.ok)
|
|
49
|
+
throw codedRuntimeError("INVALID_LIVE_PLAN", "Core rejected the live swarm plan.");
|
|
50
|
+
assertSwarmPathIdentifier(input.plan.swarmId, "swarm ID");
|
|
51
|
+
const paths = await resolveProductionRuntimePaths(input);
|
|
52
|
+
const storeRoot = paths.storeRoot;
|
|
53
|
+
const liveStore = await createSwarmLiveStore({ rootDir: storeRoot, plan: input.plan });
|
|
54
|
+
const workspaceManager = await createSwarmWorkspaceManager({
|
|
55
|
+
canonicalRoot: resolve(input.canonicalRoot),
|
|
56
|
+
ownedRoot: paths.ownedRoot,
|
|
57
|
+
swarmId: input.plan.swarmId,
|
|
58
|
+
...(input.workspaceIsolationMode ? { isolationMode: input.workspaceIsolationMode } : {}),
|
|
59
|
+
});
|
|
60
|
+
if (workspaceManager.baselineCommit.toLowerCase() !== input.plan.baselineCommit.toLowerCase()) {
|
|
61
|
+
throw codedRuntimeError("LIVE_PLAN_BASELINE_MISMATCH", "Live swarm plan baseline no longer matches the canonical repository.");
|
|
62
|
+
}
|
|
63
|
+
await liveStore.claimStart();
|
|
64
|
+
const claimedSnapshot = await liveStore.readSnapshot();
|
|
65
|
+
if (claimedSnapshot.eventCount > 0) {
|
|
66
|
+
throw codedRuntimeError("LIVE_SWARM_ALREADY_STARTED", "This live swarm contains pre-claim execution state; replay remains blocked before spend.");
|
|
67
|
+
}
|
|
68
|
+
const evidenceStore = new FileSwarmLiveRuntimeEvidenceStore(paths.evidenceRoot);
|
|
69
|
+
return runLiveSwarmWithDependencies({
|
|
70
|
+
plan: input.plan,
|
|
71
|
+
liveStore,
|
|
72
|
+
workspaceManager,
|
|
73
|
+
evidenceStore,
|
|
74
|
+
adapterFactory: input.adapterFactory,
|
|
75
|
+
verifierExecutor: input.verifierExecutor,
|
|
76
|
+
runsRoot: paths.runsRoot,
|
|
77
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
78
|
+
}, {
|
|
79
|
+
...productionDependencies,
|
|
80
|
+
sealTerminal: async (result) => {
|
|
81
|
+
try {
|
|
82
|
+
await sealProductionSwarmTerminal(paths, input.plan, result);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
await persistSwarmSealFailure(paths.evidenceRoot, input.plan, "terminal_evidence_sealing_failed");
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function resolveProductionRuntimePaths(input) {
|
|
92
|
+
const runsRoot = await realpath(resolve(input.runsRoot));
|
|
93
|
+
const requestedStoreRoot = resolve(input.storeRoot);
|
|
94
|
+
const canonicalRequestedStoreRoot = await resolveForContainment(requestedStoreRoot);
|
|
95
|
+
assertContainedOrSame(runsRoot, canonicalRequestedStoreRoot, "Live swarm store root escaped the caller runs root.");
|
|
96
|
+
await mkdir(requestedStoreRoot, { recursive: true });
|
|
97
|
+
const storeRoot = await realpath(requestedStoreRoot);
|
|
98
|
+
assertContainedOrSame(runsRoot, storeRoot, "Live swarm store root escaped the real caller runs root.");
|
|
99
|
+
const swarmsRoot = await resolveOrCreateStrictlyContained(storeRoot, join(storeRoot, "_swarms"));
|
|
100
|
+
const swarmRoot = await resolveOrCreateStrictlyContained(swarmsRoot, join(swarmsRoot, input.plan.swarmId));
|
|
101
|
+
const expectedRequestedOwnedRoot = join(requestedStoreRoot, "_swarms", input.plan.swarmId, "worktrees");
|
|
102
|
+
const expectedOwnedRoot = join(swarmRoot, "worktrees");
|
|
103
|
+
if (resolve(input.ownedRoot) !== resolve(expectedRequestedOwnedRoot)) {
|
|
104
|
+
throw codedRuntimeError("LIVE_SWARM_PATH_ESCAPE", "Live swarm worktree root must use the governed run layout.");
|
|
105
|
+
}
|
|
106
|
+
const ownedRoot = await resolveOrCreateStrictlyContained(swarmRoot, expectedOwnedRoot);
|
|
107
|
+
const evidenceRoot = await resolveOrCreateStrictlyContained(swarmRoot, join(swarmRoot, "evidence"));
|
|
108
|
+
return { runsRoot, storeRoot, ownedRoot, evidenceRoot };
|
|
109
|
+
}
|
|
110
|
+
async function sealProductionSwarmTerminal(paths, plan, result) {
|
|
111
|
+
const sealed = await readAndSealSwarmEvidence({ rootDir: paths.runsRoot, swarmId: plan.swarmId });
|
|
112
|
+
const receipt = buildParentSwarmReceipt(sealed);
|
|
113
|
+
if (receipt.parentOutcome.state !== result.outcome.state || receipt.planHash !== plan.planHash) {
|
|
114
|
+
throw codedRuntimeError("SWARM_SEAL_OUTCOME_MISMATCH", "Sealed evidence disagrees with the terminal runtime outcome.");
|
|
115
|
+
}
|
|
116
|
+
const receiptPath = join(paths.evidenceRoot, "parent-receipt.json");
|
|
117
|
+
const receiptBytes = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
118
|
+
await writeExclusiveIdempotent(receiptPath, receiptBytes);
|
|
119
|
+
const integrity = await writeSwarmReceiptIntegrityMaterial({
|
|
120
|
+
runsRoot: paths.runsRoot,
|
|
121
|
+
swarmId: plan.swarmId,
|
|
122
|
+
signedAt: sealed.index.sealedAt
|
|
123
|
+
});
|
|
124
|
+
if (!integrity) {
|
|
125
|
+
throw codedRuntimeError("SWARM_SEAL_KEY_UNAVAILABLE", "Swarm receipt integrity key material was unavailable.");
|
|
126
|
+
}
|
|
127
|
+
const verified = await verifySwarmReceiptIntegrityFromFiles({ runsRoot: paths.runsRoot, swarmId: plan.swarmId });
|
|
128
|
+
if (verified.state !== "verified") {
|
|
129
|
+
throw codedRuntimeError("SWARM_SEAL_INTEGRITY_FAILED", "Swarm receipt integrity verification failed before commit.");
|
|
130
|
+
}
|
|
131
|
+
const integrityPath = resolveSwarmReceiptIntegrityPath(paths.runsRoot, plan.swarmId);
|
|
132
|
+
const integrityBytes = await readFile(integrityPath, "utf8");
|
|
133
|
+
const sealBase = {
|
|
134
|
+
schemaVersion: "martin.swarm-receipt-seal.v1",
|
|
135
|
+
swarmId: plan.swarmId,
|
|
136
|
+
planHash: plan.planHash,
|
|
137
|
+
receiptSha256: sha256Text(receiptBytes),
|
|
138
|
+
evidenceIndexSha256: sha256Text(await readFile(sealed.indexPath, "utf8")),
|
|
139
|
+
integrityMaterialSha256: sha256Text(integrityBytes),
|
|
140
|
+
committedAt: sealed.index.sealedAt
|
|
141
|
+
};
|
|
142
|
+
const seal = {
|
|
143
|
+
...sealBase,
|
|
144
|
+
commitHmacSha256: await authenticateSwarmReceiptSealCommit({
|
|
145
|
+
runsRoot: paths.runsRoot,
|
|
146
|
+
swarmId: plan.swarmId,
|
|
147
|
+
commit: sealBase
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
await writeExclusiveIdempotent(join(paths.evidenceRoot, "swarm-receipt-seal.json"), `${JSON.stringify(seal, null, 2)}\n`);
|
|
151
|
+
}
|
|
152
|
+
async function persistSwarmSealFailure(evidenceRoot, plan, reason) {
|
|
153
|
+
await writeExclusiveIdempotent(join(evidenceRoot, "swarm-receipt-seal-status.json"), `${JSON.stringify({
|
|
154
|
+
schemaVersion: "martin.swarm-receipt-seal-status.v1",
|
|
155
|
+
swarmId: plan.swarmId,
|
|
156
|
+
planHash: plan.planHash,
|
|
157
|
+
state: "needs_review",
|
|
158
|
+
reason
|
|
159
|
+
}, null, 2)}\n`);
|
|
160
|
+
}
|
|
161
|
+
function sha256Text(value) {
|
|
162
|
+
return createHash("sha256").update(value).digest("hex");
|
|
163
|
+
}
|
|
164
|
+
async function resolveOrCreateStrictlyContained(parent, requested) {
|
|
165
|
+
assertStrictlyContained(parent, requested, "Live swarm runtime path escaped its governed ancestor.");
|
|
166
|
+
let resolved;
|
|
167
|
+
try {
|
|
168
|
+
resolved = await realpath(requested);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!isNodeError(error, "ENOENT"))
|
|
172
|
+
throw error;
|
|
173
|
+
await mkdir(requested, { recursive: true });
|
|
174
|
+
resolved = await realpath(requested);
|
|
175
|
+
}
|
|
176
|
+
assertStrictlyContained(parent, resolved, "Live swarm runtime realpath escaped its governed ancestor.");
|
|
177
|
+
return resolved;
|
|
178
|
+
}
|
|
179
|
+
async function resolveForContainment(requested) {
|
|
180
|
+
let cursor = resolve(requested);
|
|
181
|
+
const suffix = [];
|
|
182
|
+
while (true) {
|
|
183
|
+
try {
|
|
184
|
+
const canonical = await realpath(cursor);
|
|
185
|
+
return resolve(canonical, ...suffix);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!isNodeError(error, "ENOENT"))
|
|
189
|
+
throw error;
|
|
190
|
+
const parent = dirname(cursor);
|
|
191
|
+
if (parent === cursor)
|
|
192
|
+
throw error;
|
|
193
|
+
suffix.unshift(basename(cursor));
|
|
194
|
+
cursor = parent;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function assertContainedOrSame(parent, candidate, message) {
|
|
199
|
+
const relation = relative(parent, candidate);
|
|
200
|
+
if (relation === "")
|
|
201
|
+
return;
|
|
202
|
+
assertStrictlyContained(parent, candidate, message);
|
|
203
|
+
}
|
|
204
|
+
function assertStrictlyContained(parent, candidate, message) {
|
|
205
|
+
const relation = relative(parent, candidate);
|
|
206
|
+
if (!relation || relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
|
|
207
|
+
throw codedRuntimeError("LIVE_SWARM_PATH_ESCAPE", message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function isNodeError(error, code) {
|
|
211
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
212
|
+
}
|
|
213
|
+
function codedRuntimeError(code, message) {
|
|
214
|
+
return Object.assign(new Error(message), { code });
|
|
215
|
+
}
|
|
216
|
+
/** Internal test seam; intentionally not re-exported from the Core package root. */
|
|
217
|
+
export async function runLiveSwarmWithDependencies(input, dependencies) {
|
|
218
|
+
assertRuntimeInput(input);
|
|
219
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
220
|
+
const nowMs = input.nowMs ?? (() => Date.now());
|
|
221
|
+
const parentDeadlineMs = nowMs() + input.plan.parentContract.maxWallClockMs;
|
|
222
|
+
const createId = input.createId ?? ((prefix) => `${prefix}-${Math.random().toString(36).slice(2, 10)}`);
|
|
223
|
+
const parentController = new AbortController();
|
|
224
|
+
const cancellationMonitorController = new AbortController();
|
|
225
|
+
let schedulingOpen = true;
|
|
226
|
+
const parentDeadlineTimer = setTimeout(() => {
|
|
227
|
+
schedulingOpen = false;
|
|
228
|
+
if (!parentController.signal.aborted) {
|
|
229
|
+
parentController.abort(new Error("Parent swarm wall-clock deadline reached."));
|
|
230
|
+
}
|
|
231
|
+
}, Math.min(input.plan.parentContract.maxWallClockMs, 2_147_483_647));
|
|
232
|
+
parentDeadlineTimer.unref();
|
|
233
|
+
const abortParent = () => {
|
|
234
|
+
schedulingOpen = false;
|
|
235
|
+
if (!parentController.signal.aborted)
|
|
236
|
+
parentController.abort(input.signal?.reason);
|
|
237
|
+
};
|
|
238
|
+
if (input.signal?.aborted)
|
|
239
|
+
abortParent();
|
|
240
|
+
else {
|
|
241
|
+
input.signal?.addEventListener("abort", abortParent, { once: true });
|
|
242
|
+
if (input.signal?.aborted)
|
|
243
|
+
abortParent();
|
|
244
|
+
}
|
|
245
|
+
const observeDurableCancellation = async () => {
|
|
246
|
+
const snapshot = await input.liveStore.readSnapshot();
|
|
247
|
+
if (!snapshot.cancellation)
|
|
248
|
+
return false;
|
|
249
|
+
schedulingOpen = false;
|
|
250
|
+
if (!parentController.signal.aborted) {
|
|
251
|
+
parentController.abort(new Error(`Durable swarm cancellation requested: ${snapshot.cancellation.reason}`));
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
};
|
|
255
|
+
let cancellationMonitorFailure;
|
|
256
|
+
const cancellationMonitor = (async () => {
|
|
257
|
+
let revision = (await input.liveStore.readSnapshot()).revision;
|
|
258
|
+
if (await observeDurableCancellation())
|
|
259
|
+
return;
|
|
260
|
+
while (!cancellationMonitorController.signal.aborted && !parentController.signal.aborted) {
|
|
261
|
+
const snapshot = await input.liveStore.waitForRevision(revision, cancellationMonitorController.signal);
|
|
262
|
+
revision = snapshot.revision;
|
|
263
|
+
if (snapshot.cancellation) {
|
|
264
|
+
schedulingOpen = false;
|
|
265
|
+
if (!parentController.signal.aborted) {
|
|
266
|
+
parentController.abort(new Error(`Durable swarm cancellation requested: ${snapshot.cancellation.reason}`));
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
})().catch((error) => {
|
|
272
|
+
if (cancellationMonitorController.signal.aborted)
|
|
273
|
+
return;
|
|
274
|
+
cancellationMonitorFailure = error;
|
|
275
|
+
schedulingOpen = false;
|
|
276
|
+
if (!parentController.signal.aborted)
|
|
277
|
+
parentController.abort(error);
|
|
278
|
+
});
|
|
279
|
+
let record = createSwarmRunRecord({
|
|
280
|
+
swarmId: input.plan.swarmId,
|
|
281
|
+
workspaceId: input.plan.workspaceId,
|
|
282
|
+
projectId: input.plan.projectId,
|
|
283
|
+
parentContract: input.plan.parentContract,
|
|
284
|
+
tasks: input.plan.tasks.map((task) => ({ ...task, status: "queued" })),
|
|
285
|
+
agents: input.plan.agents.map((agent) => ({ ...agent, status: "queued", childRunId: undefined })),
|
|
286
|
+
budgetLedger: createSwarmBudgetLedger({
|
|
287
|
+
capUsd: input.plan.parentContract.budget.maxUsd,
|
|
288
|
+
...(input.plan.parentContract.budget.maxTokens === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: { capTokens: input.plan.parentContract.budget.maxTokens })
|
|
291
|
+
}),
|
|
292
|
+
createdAt: now(),
|
|
293
|
+
updatedAt: now()
|
|
294
|
+
}, { now: now() });
|
|
295
|
+
const runStore = createFileRunStore({ runsRoot: input.runsRoot });
|
|
296
|
+
const active = new Map();
|
|
297
|
+
const candidateIds = [];
|
|
298
|
+
const reassignmentCounts = new Map();
|
|
299
|
+
const preboundAttemptIds = new Map();
|
|
300
|
+
const observedUsageByLease = new Map();
|
|
301
|
+
try {
|
|
302
|
+
await appendEvent({
|
|
303
|
+
idempotencyKey: `${input.plan.planHash}:created`,
|
|
304
|
+
type: "SWARM_CREATED",
|
|
305
|
+
timestamp: now(),
|
|
306
|
+
payload: { engine: input.plan.engine.engine, model: input.plan.engine.model }
|
|
307
|
+
});
|
|
308
|
+
while (true) {
|
|
309
|
+
await observeDurableCancellation();
|
|
310
|
+
if (nowMs() >= parentDeadlineMs && !parentController.signal.aborted) {
|
|
311
|
+
parentController.abort(new Error("Parent swarm wall-clock deadline reached."));
|
|
312
|
+
}
|
|
313
|
+
if (parentController.signal.aborted) {
|
|
314
|
+
record = await cancelActiveChildren("cancelled");
|
|
315
|
+
if (cancellationMonitorFailure !== undefined) {
|
|
316
|
+
record = withTerminal(record, "needs_review", safeError(cancellationMonitorFailure), now());
|
|
317
|
+
}
|
|
318
|
+
const needsReview = record.outcome.state === "needs_review";
|
|
319
|
+
const terminalReason = needsReview ? record.outcome.reason : "parent_cancelled";
|
|
320
|
+
await appendEvent({
|
|
321
|
+
idempotencyKey: `${input.plan.planHash}:${needsReview ? "needs-review" : "stopped"}:cancelled`,
|
|
322
|
+
type: needsReview ? "SWARM_NEEDS_REVIEW" : "SWARM_STOPPED",
|
|
323
|
+
timestamp: now(),
|
|
324
|
+
payload: { reason: terminalReason }
|
|
325
|
+
});
|
|
326
|
+
return finalizeTerminalResult(needsReview
|
|
327
|
+
? { record, outcome: record.outcome, candidateIds: Object.freeze([...candidateIds]) }
|
|
328
|
+
: terminalResult(record, candidateIds, "stopped", "Parent cancelled the live swarm."));
|
|
329
|
+
}
|
|
330
|
+
if (schedulingOpen) {
|
|
331
|
+
const batch = selectNextSwarmBatch({
|
|
332
|
+
swarmId: record.swarmId,
|
|
333
|
+
tasks: record.tasks,
|
|
334
|
+
maxConcurrency: record.parentContract.maxConcurrency,
|
|
335
|
+
parentContract: record.parentContract
|
|
336
|
+
});
|
|
337
|
+
if (!batch.ok) {
|
|
338
|
+
schedulingOpen = false;
|
|
339
|
+
record = withTerminal(record, "needs_review", batch.errors.map((error) => error.code).join(","), now());
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
for (const task of batch.tasks) {
|
|
343
|
+
await observeDurableCancellation();
|
|
344
|
+
if (!schedulingOpen || parentController.signal.aborted)
|
|
345
|
+
break;
|
|
346
|
+
const launched = await launchTask(task);
|
|
347
|
+
if (!launched) {
|
|
348
|
+
schedulingOpen = false;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (parentController.signal.aborted)
|
|
355
|
+
continue;
|
|
356
|
+
if (active.size === 0) {
|
|
357
|
+
const incompleteRequired = record.tasks.filter((task) => task.required && task.status !== "accepted");
|
|
358
|
+
if (incompleteRequired.length > 0) {
|
|
359
|
+
if (record.outcome.state === "running") {
|
|
360
|
+
record = withTerminal(record, "needs_review", "Required tasks could not reach accepted state.", now());
|
|
361
|
+
}
|
|
362
|
+
return finalizeTerminalResult({ record, outcome: record.outcome, candidateIds: Object.freeze([...candidateIds]) });
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
const completed = await Promise.race([...active.values()].map((attempt) => attempt.completion));
|
|
367
|
+
const terminal = await settleAttemptOnce(completed, parentController.signal.aborted);
|
|
368
|
+
if (terminal)
|
|
369
|
+
schedulingOpen = false;
|
|
370
|
+
}
|
|
371
|
+
if (parentController.signal.aborted || record.outcome.state !== "running") {
|
|
372
|
+
return finalizeTerminalResult({ record, outcome: record.outcome, candidateIds: Object.freeze([...candidateIds]) });
|
|
373
|
+
}
|
|
374
|
+
const parent = await dependencies.parentPipeline({
|
|
375
|
+
record,
|
|
376
|
+
candidateIds,
|
|
377
|
+
workspaceManager: input.workspaceManager,
|
|
378
|
+
evidenceStore: input.evidenceStore,
|
|
379
|
+
integrationWorkspaceId: createId("integration-workspace"),
|
|
380
|
+
verifierWorkspaceId: createId("verifier-workspace"),
|
|
381
|
+
verifierExecutor: input.verifierExecutor,
|
|
382
|
+
signal: parentController.signal,
|
|
383
|
+
now,
|
|
384
|
+
createId
|
|
385
|
+
});
|
|
386
|
+
const verifiedEvents = parent.record.events.filter((event) => (event.type === "SWARM_VERIFIED"
|
|
387
|
+
&& event.swarmId === record.swarmId
|
|
388
|
+
&& event.parentPolicyVersion === record.parentContract.policyVersion));
|
|
389
|
+
if (parent.outcome.state === "verified"
|
|
390
|
+
&& (parent.disposition !== "ready"
|
|
391
|
+
|| parent.blockingInvariants.length !== 0
|
|
392
|
+
|| verifiedEvents.length !== 1)) {
|
|
393
|
+
const rejected = withTerminal(parent.record, "needs_review", "Parent result lacked one exact SWARM_VERIFIED authority event.", now());
|
|
394
|
+
await appendEvent({
|
|
395
|
+
idempotencyKey: `${input.plan.planHash}:needs-review:parent-verified-event`,
|
|
396
|
+
type: "SWARM_NEEDS_REVIEW",
|
|
397
|
+
timestamp: now(),
|
|
398
|
+
payload: { reason: rejected.outcome.reason }
|
|
399
|
+
});
|
|
400
|
+
return finalizeTerminalResult({ record: rejected, outcome: rejected.outcome, candidateIds: Object.freeze([...candidateIds]), parent });
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const snapshot = await input.liveStore.readSnapshot();
|
|
404
|
+
await input.liveStore.persistParentOutcome(parent, { expectedRevision: snapshot.revision });
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
const rejected = withTerminal(parent.record, "needs_review", "Parent pipeline outcome lacked operational-store authority or could not be persisted.", now());
|
|
408
|
+
await appendEvent({
|
|
409
|
+
idempotencyKey: `${input.plan.planHash}:needs-review:parent-outcome-authority`,
|
|
410
|
+
type: "SWARM_NEEDS_REVIEW",
|
|
411
|
+
timestamp: now(),
|
|
412
|
+
payload: { reason: rejected.outcome.reason }
|
|
413
|
+
});
|
|
414
|
+
return finalizeTerminalResult({ record: rejected, outcome: rejected.outcome, candidateIds: Object.freeze([...candidateIds]), parent });
|
|
415
|
+
}
|
|
416
|
+
const result = {
|
|
417
|
+
record: parent.record,
|
|
418
|
+
outcome: parent.outcome,
|
|
419
|
+
candidateIds: Object.freeze([...candidateIds]),
|
|
420
|
+
parent
|
|
421
|
+
};
|
|
422
|
+
return finalizeTerminalResult(result);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
schedulingOpen = false;
|
|
426
|
+
clearTimeout(parentDeadlineTimer);
|
|
427
|
+
input.signal?.removeEventListener("abort", abortParent);
|
|
428
|
+
cancellationMonitorController.abort("runtime closed");
|
|
429
|
+
await cancellationMonitor;
|
|
430
|
+
}
|
|
431
|
+
async function launchTask(task) {
|
|
432
|
+
if (await observeDurableCancellation())
|
|
433
|
+
return false;
|
|
434
|
+
const agent = record.agents.find((candidate) => candidate.agentId === task.assignedAgentId);
|
|
435
|
+
if (!agent) {
|
|
436
|
+
record = withTerminal(record, "needs_review", `Task ${task.taskId} has no assigned agent.`, now());
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const leaseId = createId("lease");
|
|
440
|
+
const attemptId = preboundAttemptIds.get(task.taskId) ?? createId("attempt");
|
|
441
|
+
preboundAttemptIds.delete(task.taskId);
|
|
442
|
+
const reservationBudget = {
|
|
443
|
+
...agent.contract.budget,
|
|
444
|
+
maxIterations: Math.min(agent.contract.budget.maxIterations, input.plan.childMaxIterations)
|
|
445
|
+
};
|
|
446
|
+
const budget = {
|
|
447
|
+
...reservationBudget,
|
|
448
|
+
maxUsd: agent.contract.hardMaxUsd ?? input.plan.parentContract.budget.maxUsd,
|
|
449
|
+
softLimitUsd: agent.contract.hardMaxUsd ?? input.plan.parentContract.budget.maxUsd,
|
|
450
|
+
...(agent.contract.hardMaxTokens !== undefined
|
|
451
|
+
? { maxTokens: agent.contract.hardMaxTokens }
|
|
452
|
+
: input.plan.parentContract.budget.maxTokens !== undefined
|
|
453
|
+
? { maxTokens: input.plan.parentContract.budget.maxTokens }
|
|
454
|
+
: {})
|
|
455
|
+
};
|
|
456
|
+
const reservation = reserveSwarmBudgetLease(record.budgetLedger, {
|
|
457
|
+
leaseId,
|
|
458
|
+
agentId: agent.agentId,
|
|
459
|
+
taskId: task.taskId,
|
|
460
|
+
reservedUsd: reservationBudget.maxUsd,
|
|
461
|
+
reservedTokens: reservationBudget.maxTokens ?? 0
|
|
462
|
+
});
|
|
463
|
+
if (!reservation.ok) {
|
|
464
|
+
await appendBlockedAction({
|
|
465
|
+
taskId: task.taskId,
|
|
466
|
+
agentId: agent.agentId,
|
|
467
|
+
attemptId,
|
|
468
|
+
action: "budget_reservation",
|
|
469
|
+
reason: reservation.errors.map((error) => error.code).join(",")
|
|
470
|
+
});
|
|
471
|
+
record = withTerminal(record, "stopped", reservation.errors.map((error) => error.code).join(","), now());
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
record = { ...record, budgetLedger: reservation.ledger, updatedAt: now() };
|
|
475
|
+
const childRunId = createId("child-run");
|
|
476
|
+
const workspaceId = createId("child-workspace");
|
|
477
|
+
const proposalId = createId("proposal");
|
|
478
|
+
const candidateId = task.mutationMode === "write" ? createId("candidate") : undefined;
|
|
479
|
+
let workspace;
|
|
480
|
+
try {
|
|
481
|
+
workspace = await input.workspaceManager.createWorkspace({
|
|
482
|
+
kind: "child",
|
|
483
|
+
workspaceId,
|
|
484
|
+
childRunId,
|
|
485
|
+
agentId: agent.agentId,
|
|
486
|
+
taskIds: [task.taskId]
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
const released = releaseSwarmBudgetLease(record.budgetLedger, leaseId);
|
|
491
|
+
if (released.ok)
|
|
492
|
+
record = { ...record, budgetLedger: released.ledger };
|
|
493
|
+
const lease = record.budgetLedger.leases.find((candidate) => candidate.leaseId === leaseId);
|
|
494
|
+
await appendBlockedAction({
|
|
495
|
+
taskId: task.taskId,
|
|
496
|
+
agentId: agent.agentId,
|
|
497
|
+
childRunId,
|
|
498
|
+
attemptId,
|
|
499
|
+
action: "workspace_creation",
|
|
500
|
+
reason: safeError(error),
|
|
501
|
+
details: {
|
|
502
|
+
leaseId,
|
|
503
|
+
leaseState: lease?.status,
|
|
504
|
+
reservedUsd: lease?.reservedUsd,
|
|
505
|
+
reservedTokens: lease?.reservedTokens,
|
|
506
|
+
actualUsd: 0,
|
|
507
|
+
actualTokens: 0
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
record = withTerminal(record, "needs_review", safeError(error), now());
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
let adapter;
|
|
514
|
+
try {
|
|
515
|
+
if (await observeDurableCancellation()) {
|
|
516
|
+
await cancelPreparedLaunch({ task, agent, childRunId, leaseId, workspace, attemptId });
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
const factoryResult = input.adapterFactory({
|
|
520
|
+
engine: input.plan.engine,
|
|
521
|
+
task,
|
|
522
|
+
agent,
|
|
523
|
+
childRunId,
|
|
524
|
+
workspace,
|
|
525
|
+
budget
|
|
526
|
+
});
|
|
527
|
+
adapter = isAdapterHandle(factoryResult) ? factoryResult.adapter : factoryResult;
|
|
528
|
+
if (adapter.metadata.providerId !== input.plan.engine.engine) {
|
|
529
|
+
throw new Error("LIVE_ENGINE_PROVIDER_MISMATCH: every child must use the parent-selected provider.");
|
|
530
|
+
}
|
|
531
|
+
if (adapter.metadata.model !== input.plan.engine.model) {
|
|
532
|
+
throw new Error("LIVE_ENGINE_MODEL_MISMATCH: every child must use the parent-selected model.");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
await failPreparedLaunch({ task, agent, childRunId, leaseId, workspace, attemptId }, error);
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
const controller = new AbortController();
|
|
540
|
+
const relayAbort = () => controller.abort(parentController.signal.reason);
|
|
541
|
+
if (parentController.signal.aborted)
|
|
542
|
+
relayAbort();
|
|
543
|
+
else {
|
|
544
|
+
parentController.signal.addEventListener("abort", relayAbort, { once: true });
|
|
545
|
+
if (parentController.signal.aborted)
|
|
546
|
+
relayAbort();
|
|
547
|
+
}
|
|
548
|
+
record = updateTaskAndAgent(record, task.taskId, agent.agentId, childRunId, "running", "running", now());
|
|
549
|
+
try {
|
|
550
|
+
await appendEvent({
|
|
551
|
+
idempotencyKey: `${childRunId}:started`,
|
|
552
|
+
type: "CHILD_STARTED",
|
|
553
|
+
timestamp: now(),
|
|
554
|
+
taskId: task.taskId,
|
|
555
|
+
agentId: agent.agentId,
|
|
556
|
+
childRunId,
|
|
557
|
+
payload: {
|
|
558
|
+
attemptId,
|
|
559
|
+
engine: input.plan.engine.engine,
|
|
560
|
+
model: input.plan.engine.model,
|
|
561
|
+
workspaceId,
|
|
562
|
+
leaseId,
|
|
563
|
+
reservedUsd: record.budgetLedger.leases.find((lease) => lease.leaseId === leaseId)?.reservedUsd,
|
|
564
|
+
reservedTokens: record.budgetLedger.leases.find((lease) => lease.leaseId === leaseId)?.reservedTokens,
|
|
565
|
+
maxIterations: budget.maxIterations
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
parentController.signal.removeEventListener("abort", relayAbort);
|
|
571
|
+
await failPreparedLaunch({ task, agent, childRunId, leaseId, workspace, attemptId }, error);
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
const effectiveWallClockMs = Math.max(1, Math.min(agent.contract.maxWallClockMs, parentDeadlineMs - nowMs()));
|
|
575
|
+
const childDeadlineTimer = setTimeout(() => {
|
|
576
|
+
if (!controller.signal.aborted) {
|
|
577
|
+
controller.abort(new Error("Child swarm wall-clock deadline reached."));
|
|
578
|
+
}
|
|
579
|
+
}, Math.min(effectiveWallClockMs, 2_147_483_647));
|
|
580
|
+
childDeadlineTimer.unref();
|
|
581
|
+
const observedUsageGovernor = (observation) => {
|
|
582
|
+
const currentLease = record.budgetLedger.leases.find((lease) => lease.leaseId === leaseId);
|
|
583
|
+
const prior = observedUsageByLease.get(leaseId) ?? { usd: 0, tokens: 0 };
|
|
584
|
+
const grantedUsd = currentLease?.reservedUsd ?? 0;
|
|
585
|
+
const grantedTokens = currentLease?.reservedTokens ?? 0;
|
|
586
|
+
if (record.outcome.state !== "running" || currentLease?.status !== "reserved") {
|
|
587
|
+
return { action: "terminate", grantedUsd, grantedTokens, reason: "parent_terminal" };
|
|
588
|
+
}
|
|
589
|
+
if (!Number.isFinite(observation.cumulativeUsd)
|
|
590
|
+
|| observation.cumulativeUsd < prior.usd
|
|
591
|
+
|| !Number.isInteger(observation.cumulativeTokens)
|
|
592
|
+
|| observation.cumulativeTokens < prior.tokens) {
|
|
593
|
+
return { action: "terminate", grantedUsd, grantedTokens, reason: "non_monotonic_usage_observation" };
|
|
594
|
+
}
|
|
595
|
+
if ((agent.contract.hardMaxUsd !== undefined && observation.cumulativeUsd > agent.contract.hardMaxUsd)
|
|
596
|
+
|| (agent.contract.hardMaxTokens !== undefined && observation.cumulativeTokens > agent.contract.hardMaxTokens)) {
|
|
597
|
+
return { action: "terminate", grantedUsd, grantedTokens, reason: "child_hard_cap_exceeded" };
|
|
598
|
+
}
|
|
599
|
+
const protectedUsage = Object.fromEntries(observedUsageByLease);
|
|
600
|
+
protectedUsage[leaseId] = { usd: observation.cumulativeUsd, tokens: observation.cumulativeTokens };
|
|
601
|
+
const extended = extendSwarmBudgetLease(record.budgetLedger, {
|
|
602
|
+
leaseId,
|
|
603
|
+
requiredUsd: observation.cumulativeUsd,
|
|
604
|
+
requiredTokens: observation.cumulativeTokens,
|
|
605
|
+
protectedUsage
|
|
606
|
+
});
|
|
607
|
+
if (!extended.ok) {
|
|
608
|
+
return {
|
|
609
|
+
action: "terminate",
|
|
610
|
+
grantedUsd,
|
|
611
|
+
grantedTokens,
|
|
612
|
+
reason: extended.errors.map((error) => error.code).join(",")
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
observedUsageByLease.set(leaseId, {
|
|
616
|
+
usd: observation.cumulativeUsd,
|
|
617
|
+
tokens: observation.cumulativeTokens
|
|
618
|
+
});
|
|
619
|
+
record = { ...record, budgetLedger: extended.ledger, updatedAt: now() };
|
|
620
|
+
return {
|
|
621
|
+
action: "continue",
|
|
622
|
+
grantedUsd: extended.lease.reservedUsd,
|
|
623
|
+
grantedTokens: extended.lease.reservedTokens
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
const runPromise = runMartin({
|
|
627
|
+
workspaceId,
|
|
628
|
+
projectId: input.plan.projectId,
|
|
629
|
+
task: {
|
|
630
|
+
title: task.title,
|
|
631
|
+
objective: task.objective,
|
|
632
|
+
verificationPlan: agent.contract.permissions.commands,
|
|
633
|
+
repoRoot: workspace.path,
|
|
634
|
+
// Scope controls what the child may inspect; mutationMode controls
|
|
635
|
+
// whether any of that scope may be changed. Keeping those concerns
|
|
636
|
+
// separate lets CLI and remote/model adapters share one Swarm contract.
|
|
637
|
+
allowedPaths: agent.contract.scope.allowedPaths,
|
|
638
|
+
deniedPaths: agent.contract.scope.deniedPaths,
|
|
639
|
+
allowedNetworkDomains: agent.contract.permissions.networkDomains,
|
|
640
|
+
approvalPolicy: agent.contract.approvalPolicy,
|
|
641
|
+
providerExecutionTimeoutMs: effectiveWallClockMs,
|
|
642
|
+
mutationMode: task.mutationMode === "write" ? "edit" : "read_only"
|
|
643
|
+
},
|
|
644
|
+
budget,
|
|
645
|
+
adapter,
|
|
646
|
+
store: runStore,
|
|
647
|
+
receiptScope: {
|
|
648
|
+
repoRoot: workspace.path,
|
|
649
|
+
workingDirectory: workspace.path,
|
|
650
|
+
runsRoot: input.runsRoot,
|
|
651
|
+
swarmChild: {
|
|
652
|
+
parentSwarmId: record.swarmId,
|
|
653
|
+
agentId: agent.agentId,
|
|
654
|
+
attemptId,
|
|
655
|
+
taskIds: [task.taskId]
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
metadata: {
|
|
659
|
+
"swarm.parentId": record.swarmId,
|
|
660
|
+
"swarm.agentId": agent.agentId,
|
|
661
|
+
"swarm.taskIds": JSON.stringify([task.taskId]),
|
|
662
|
+
"swarm.attemptId": attemptId,
|
|
663
|
+
"swarm.proposalId": proposalId,
|
|
664
|
+
"swarm.baselineCommit": input.plan.baselineCommit,
|
|
665
|
+
"swarm.engine": input.plan.engine.engine,
|
|
666
|
+
"swarm.model": input.plan.engine.model
|
|
667
|
+
},
|
|
668
|
+
verificationExecutionOwner: "host_only",
|
|
669
|
+
observedUsageGovernor,
|
|
670
|
+
signal: controller.signal,
|
|
671
|
+
now,
|
|
672
|
+
idFactory: childIdFactory(childRunId, createId)
|
|
673
|
+
});
|
|
674
|
+
const activeAttempt = {};
|
|
675
|
+
Object.assign(activeAttempt, {
|
|
676
|
+
task,
|
|
677
|
+
agent,
|
|
678
|
+
childRunId,
|
|
679
|
+
leaseId,
|
|
680
|
+
workspace,
|
|
681
|
+
controller,
|
|
682
|
+
attemptId,
|
|
683
|
+
proposalId,
|
|
684
|
+
...(candidateId ? { candidateId } : {}),
|
|
685
|
+
completion: runPromise.then((result) => ({ active: activeAttempt, result }), (error) => ({ active: activeAttempt, error })).finally(() => {
|
|
686
|
+
clearTimeout(childDeadlineTimer);
|
|
687
|
+
parentController.signal.removeEventListener("abort", relayAbort);
|
|
688
|
+
})
|
|
689
|
+
});
|
|
690
|
+
active.set(childRunId, activeAttempt);
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
async function failPreparedLaunch(attempt, error) {
|
|
694
|
+
const released = releaseSwarmBudgetLease(record.budgetLedger, attempt.leaseId);
|
|
695
|
+
if (released.ok)
|
|
696
|
+
record = { ...record, budgetLedger: released.ledger, updatedAt: now() };
|
|
697
|
+
const closurePersisted = await persistProcessClosure(attempt);
|
|
698
|
+
const cleanup = closurePersisted
|
|
699
|
+
? await cleanupWorkspace(attempt.workspace, true)
|
|
700
|
+
: await preserveWorkspaceForMissingEvidence(attempt.workspace);
|
|
701
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
702
|
+
const reason = !closurePersisted
|
|
703
|
+
? "process_closure_evidence_not_persisted"
|
|
704
|
+
: cleanup.state === "completed" ? safeError(error) : "cleanup_pending";
|
|
705
|
+
await appendBlockedAction({
|
|
706
|
+
taskId: attempt.task.taskId,
|
|
707
|
+
agentId: attempt.agent.agentId,
|
|
708
|
+
childRunId: attempt.childRunId,
|
|
709
|
+
attemptId: attempt.attemptId,
|
|
710
|
+
action: "child_launch",
|
|
711
|
+
reason
|
|
712
|
+
});
|
|
713
|
+
record = withTerminal(record, "needs_review", safeError(error), now());
|
|
714
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", reason, now(), "environment_mismatch", budgetEventDetails(attempt))).catch(() => undefined);
|
|
715
|
+
}
|
|
716
|
+
async function cancelPreparedLaunch(attempt) {
|
|
717
|
+
const released = releaseSwarmBudgetLease(record.budgetLedger, attempt.leaseId);
|
|
718
|
+
if (released.ok)
|
|
719
|
+
record = { ...record, budgetLedger: released.ledger, updatedAt: now() };
|
|
720
|
+
const closurePersisted = await persistProcessClosure(attempt);
|
|
721
|
+
const cleanup = closurePersisted
|
|
722
|
+
? await cleanupWorkspace(attempt.workspace, true)
|
|
723
|
+
: await preserveWorkspaceForMissingEvidence(attempt.workspace);
|
|
724
|
+
const clean = closurePersisted && cleanup.state === "completed";
|
|
725
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, clean ? "stopped" : "needs_review", clean ? "stopped" : "needs_review", now());
|
|
726
|
+
if (!clean)
|
|
727
|
+
record = withTerminal(record, "needs_review", "cleanup_pending", now());
|
|
728
|
+
await appendEvent(childTerminalEvent(attempt, clean ? "CHILD_STOPPED" : "CHILD_NEEDS_REVIEW", clean ? "parent_cancelled" : "cleanup_pending", now(), undefined, budgetEventDetails(attempt)));
|
|
729
|
+
}
|
|
730
|
+
function settleAttemptOnce(completed, cancellationRequested) {
|
|
731
|
+
const attempt = completed.active;
|
|
732
|
+
if (!attempt.settlement) {
|
|
733
|
+
attempt.settlement = settleAttempt(completed, cancellationRequested).finally(() => {
|
|
734
|
+
active.delete(attempt.childRunId);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
return attempt.settlement;
|
|
738
|
+
}
|
|
739
|
+
async function settleAttempt(completed, cancellationRequested = false) {
|
|
740
|
+
const { active: attempt } = completed;
|
|
741
|
+
const closurePersisted = await persistProcessClosure(attempt);
|
|
742
|
+
if (completed.error || !completed.result) {
|
|
743
|
+
const released = releaseSwarmBudgetLease(record.budgetLedger, attempt.leaseId);
|
|
744
|
+
if (released.ok)
|
|
745
|
+
record = { ...record, budgetLedger: released.ledger };
|
|
746
|
+
const cleanup = closurePersisted
|
|
747
|
+
? await cleanupWorkspace(attempt.workspace, true)
|
|
748
|
+
: await preserveWorkspaceForMissingEvidence(attempt.workspace);
|
|
749
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
750
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", !closurePersisted
|
|
751
|
+
? "process_closure_evidence_not_persisted"
|
|
752
|
+
: cleanup.state === "completed" ? "child_crash" : "cleanup_pending", now(), "environment_mismatch", budgetEventDetails(attempt)));
|
|
753
|
+
record = withTerminal(record, "needs_review", completed.error ? safeError(completed.error) : "Child execution returned no result.", now());
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
const actualUsd = completed.result.loop.cost.actualUsd;
|
|
757
|
+
const actualTokens = completed.result.loop.cost.tokensIn + completed.result.loop.cost.tokensOut;
|
|
758
|
+
const childPassed = completed.result.loop.status === "completed"
|
|
759
|
+
&& completed.result.decision.lifecycleState === "completed";
|
|
760
|
+
const withinExplicitChildCap = ((attempt.agent.contract.hardMaxUsd === undefined || actualUsd <= attempt.agent.contract.hardMaxUsd)
|
|
761
|
+
&& (attempt.agent.contract.hardMaxTokens === undefined || actualTokens <= attempt.agent.contract.hardMaxTokens));
|
|
762
|
+
if (withinExplicitChildCap) {
|
|
763
|
+
const protectedUsage = Object.fromEntries(observedUsageByLease);
|
|
764
|
+
const priorObserved = observedUsageByLease.get(attempt.leaseId) ?? { usd: 0, tokens: 0 };
|
|
765
|
+
const finalObserved = {
|
|
766
|
+
usd: Math.max(priorObserved.usd, actualUsd),
|
|
767
|
+
tokens: Math.max(priorObserved.tokens, actualTokens)
|
|
768
|
+
};
|
|
769
|
+
protectedUsage[attempt.leaseId] = finalObserved;
|
|
770
|
+
const reconciled = extendSwarmBudgetLease(record.budgetLedger, {
|
|
771
|
+
leaseId: attempt.leaseId,
|
|
772
|
+
requiredUsd: finalObserved.usd,
|
|
773
|
+
requiredTokens: finalObserved.tokens,
|
|
774
|
+
protectedUsage
|
|
775
|
+
});
|
|
776
|
+
if (reconciled.ok) {
|
|
777
|
+
record = { ...record, budgetLedger: reconciled.ledger, updatedAt: now() };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const settlement = settleSwarmBudgetLease(record.budgetLedger, {
|
|
781
|
+
leaseId: attempt.leaseId,
|
|
782
|
+
actualUsd,
|
|
783
|
+
actualTokens,
|
|
784
|
+
provenance: completed.result.loop.cost.provenance
|
|
785
|
+
});
|
|
786
|
+
observedUsageByLease.delete(attempt.leaseId);
|
|
787
|
+
if (!settlement.ok) {
|
|
788
|
+
const leaseOverspent = settlement.errors.some((error) => error.code === "LEASE_OVERSPEND");
|
|
789
|
+
if (leaseOverspent) {
|
|
790
|
+
record = { ...record, budgetLedger: settlement.ledger, updatedAt: now() };
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
const released = releaseSwarmBudgetLease(record.budgetLedger, attempt.leaseId);
|
|
794
|
+
if (released.ok)
|
|
795
|
+
record = { ...record, budgetLedger: released.ledger, updatedAt: now() };
|
|
796
|
+
}
|
|
797
|
+
if (!closurePersisted) {
|
|
798
|
+
await terminalizeMissingClosureEvidence(attempt, { actualUsd, actualTokens });
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true);
|
|
802
|
+
if (leaseOverspent && cleanup.state === "completed") {
|
|
803
|
+
await persistCompletion(attempt, completed.result, "overspent", cleanup);
|
|
804
|
+
}
|
|
805
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "stopped", "stopped", now());
|
|
806
|
+
const originalFailureReason = completed.result.decision.reason;
|
|
807
|
+
const originalFailureClass = childFailureClass(completed.result);
|
|
808
|
+
// A completed child with no structural/provider failure that exits only
|
|
809
|
+
// because accounting crossed its limit is a budget-primary failure.
|
|
810
|
+
// Concrete provider/safety classes remain primary and are preserved.
|
|
811
|
+
const budgetIsPrimary = childPassed
|
|
812
|
+
|| originalFailureClass === undefined
|
|
813
|
+
|| originalFailureClass === "budget_pressure";
|
|
814
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_STOPPED", cleanup.state === "completed"
|
|
815
|
+
? (!budgetIsPrimary ? "child_terminal_failure" : leaseOverspent ? "lease_overage" : "budget_settlement_failed")
|
|
816
|
+
: "cleanup_pending", now(), budgetIsPrimary ? "budget_pressure" : originalFailureClass, {
|
|
817
|
+
actualUsd,
|
|
818
|
+
actualTokens,
|
|
819
|
+
reservedUsd: record.budgetLedger.leases.find((lease) => lease.leaseId === attempt.leaseId)?.reservedUsd,
|
|
820
|
+
reservedTokens: record.budgetLedger.leases.find((lease) => lease.leaseId === attempt.leaseId)?.reservedTokens,
|
|
821
|
+
leaseState: record.budgetLedger.leases.find((lease) => lease.leaseId === attempt.leaseId)?.status,
|
|
822
|
+
budgetStatus: leaseOverspent ? "exceeded" : "settlement_failed",
|
|
823
|
+
settlementStatus: "failed",
|
|
824
|
+
...(!budgetIsPrimary ? { originalFailureReason } : {})
|
|
825
|
+
}));
|
|
826
|
+
record = withTerminal(record, "stopped", !budgetIsPrimary
|
|
827
|
+
? originalFailureReason
|
|
828
|
+
: leaseOverspent
|
|
829
|
+
? "Child actual usage exceeded its reserved lease."
|
|
830
|
+
: "Child budget settlement failed.", now());
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
record = { ...record, budgetLedger: settlement.ledger, updatedAt: now() };
|
|
834
|
+
if (!closurePersisted) {
|
|
835
|
+
await terminalizeMissingClosureEvidence(attempt, { actualUsd, actualTokens });
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
if (record.outcome.state !== "running" || !schedulingOpen) {
|
|
839
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true);
|
|
840
|
+
if (cleanup.state === "completed") {
|
|
841
|
+
await persistCompletion(attempt, completed.result, "settled", cleanup);
|
|
842
|
+
}
|
|
843
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "stopped", "stopped", now());
|
|
844
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_STOPPED", cleanup.state === "completed" ? "parent_terminal_not_admitted" : "cleanup_pending", now(), childFailureClass(completed.result), budgetEventDetails(attempt, actualUsd, actualTokens)));
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
if (cancellationRequested || parentController.signal.aborted) {
|
|
848
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true);
|
|
849
|
+
if (cleanup.state === "completed") {
|
|
850
|
+
await persistCompletion(attempt, completed.result, "settled", cleanup);
|
|
851
|
+
}
|
|
852
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "stopped", "stopped", now());
|
|
853
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_STOPPED", cleanup.state === "completed" ? "parent_cancelled" : "cleanup_pending", now(), childFailureClass(completed.result), budgetEventDetails(attempt, actualUsd, actualTokens)));
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
if (!childPassed) {
|
|
857
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true);
|
|
858
|
+
if (cleanup.state === "completed") {
|
|
859
|
+
await persistCompletion(attempt, completed.result, "settled", cleanup);
|
|
860
|
+
}
|
|
861
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "stopped", "stopped", now());
|
|
862
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_STOPPED", "child_terminal_failure", now(), childFailureClass(completed.result), budgetEventDetails(attempt, actualUsd, actualTokens)));
|
|
863
|
+
if (cleanup.state !== "completed")
|
|
864
|
+
return true;
|
|
865
|
+
return await reassignTask(attempt.task.taskId, attempt.agent.agentId, attempt.attemptId, "child_terminal_failure");
|
|
866
|
+
}
|
|
867
|
+
const integrity = await verifyChildReceipt(attempt);
|
|
868
|
+
if (integrity.state !== "verified") {
|
|
869
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true);
|
|
870
|
+
if (cleanup.state === "completed") {
|
|
871
|
+
await persistCompletion(attempt, completed.result, "settled", cleanup, integrity);
|
|
872
|
+
}
|
|
873
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
874
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", cleanup.state === "completed" ? "receipt_not_verified" : "cleanup_pending", now(), undefined, budgetEventDetails(attempt, actualUsd, actualTokens)));
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
let candidateId;
|
|
878
|
+
if (attempt.task.mutationMode === "write") {
|
|
879
|
+
candidateId = attempt.candidateId;
|
|
880
|
+
if (!candidateId)
|
|
881
|
+
throw new Error("LIVE_CANDIDATE_ID_MISSING");
|
|
882
|
+
const paths = runPaths(input.runsRoot, attempt.childRunId);
|
|
883
|
+
let declaredPaths;
|
|
884
|
+
try {
|
|
885
|
+
declaredPaths = await dependencies.inventoryCandidatePaths(attempt.workspace.path);
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true);
|
|
889
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
890
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", cleanup.state === "completed" ? "git_inventory_failed" : "cleanup_pending", now(), undefined, { ...budgetEventDetails(attempt, actualUsd, actualTokens), diagnostic: safeError(error) }));
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
const admission = await dependencies.captureCandidate({
|
|
894
|
+
swarmRecord: record,
|
|
895
|
+
canonicalRoot: input.workspaceManager.canonicalRoot,
|
|
896
|
+
swarmId: record.swarmId,
|
|
897
|
+
candidateId,
|
|
898
|
+
admissionId: createId("admission"),
|
|
899
|
+
attemptId: attempt.attemptId,
|
|
900
|
+
proposalId: attempt.proposalId,
|
|
901
|
+
childRunId: attempt.childRunId,
|
|
902
|
+
agentId: attempt.agent.agentId,
|
|
903
|
+
taskIds: [attempt.task.taskId],
|
|
904
|
+
expectedBaselineCommit: input.plan.baselineCommit,
|
|
905
|
+
workspace: attempt.workspace,
|
|
906
|
+
childState: "verified",
|
|
907
|
+
declaredPaths,
|
|
908
|
+
childAllowedPaths: attempt.agent.contract.scope.allowedPaths,
|
|
909
|
+
inheritedDeniedPaths: [...new Set([
|
|
910
|
+
...record.parentContract.scope.deniedPaths,
|
|
911
|
+
...attempt.agent.contract.scope.deniedPaths
|
|
912
|
+
])],
|
|
913
|
+
taskWriteScope: attempt.task.writeScope,
|
|
914
|
+
runsRoot: input.runsRoot,
|
|
915
|
+
loopRecordPath: paths.loopRecord,
|
|
916
|
+
ledgerPath: paths.ledger,
|
|
917
|
+
artifactWriter: input.evidenceStore,
|
|
918
|
+
now
|
|
919
|
+
});
|
|
920
|
+
if (admission.admission.state !== "admitted" || !admission.cleanupAuthorized) {
|
|
921
|
+
const cleanup = admission.cleanupAuthorized
|
|
922
|
+
? await cleanupWorkspace(attempt.workspace, true)
|
|
923
|
+
: undefined;
|
|
924
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
925
|
+
await appendBlockedAction({
|
|
926
|
+
taskId: attempt.task.taskId,
|
|
927
|
+
agentId: attempt.agent.agentId,
|
|
928
|
+
childRunId: attempt.childRunId,
|
|
929
|
+
attemptId: attempt.attemptId,
|
|
930
|
+
action: "candidate_admission",
|
|
931
|
+
reason: admission.admission.reasonCode
|
|
932
|
+
});
|
|
933
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", cleanup && cleanup.state !== "completed" ? "cleanup_pending" : admission.admission.reasonCode, now(), undefined, budgetEventDetails(attempt, actualUsd, actualTokens)));
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
candidateIds.push(candidateId);
|
|
937
|
+
}
|
|
938
|
+
const cleanup = await cleanupWorkspace(attempt.workspace, true, candidateId !== undefined);
|
|
939
|
+
if (cleanup.state !== "completed") {
|
|
940
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
941
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", "cleanup_pending", now(), undefined, budgetEventDetails(attempt, actualUsd, actualTokens)));
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
await persistCompletion(attempt, completed.result, "settled", cleanup, integrity);
|
|
945
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "accepted", "verified", now());
|
|
946
|
+
await appendEvent({
|
|
947
|
+
idempotencyKey: `${attempt.childRunId}:verified`,
|
|
948
|
+
type: "CHILD_VERIFIED",
|
|
949
|
+
timestamp: now(),
|
|
950
|
+
taskId: attempt.task.taskId,
|
|
951
|
+
agentId: attempt.agent.agentId,
|
|
952
|
+
childRunId: attempt.childRunId,
|
|
953
|
+
payload: {
|
|
954
|
+
processCloseState: "closed",
|
|
955
|
+
...budgetEventDetails(attempt, actualUsd, actualTokens),
|
|
956
|
+
cleanupState: cleanup.state,
|
|
957
|
+
...(candidateId ? { candidateId } : {})
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
async function cleanupWorkspace(workspace, evidencePersisted, force = false) {
|
|
963
|
+
let cleanup;
|
|
964
|
+
try {
|
|
965
|
+
cleanup = await input.workspaceManager.removeWorkspace(workspace, {
|
|
966
|
+
evidencePersisted,
|
|
967
|
+
processTreeClosed: true,
|
|
968
|
+
...(force ? { force: true } : {})
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
cleanup = {
|
|
973
|
+
schemaVersion: "martin.swarm.v1",
|
|
974
|
+
cleanupId: createId("cleanup-pending"),
|
|
975
|
+
swarmId: record.swarmId,
|
|
976
|
+
workspaceId: workspace.record.workspaceId,
|
|
977
|
+
workspaceKind: workspace.record.kind,
|
|
978
|
+
evidencePersisted,
|
|
979
|
+
processCloseState: "closed",
|
|
980
|
+
removalState: "failed",
|
|
981
|
+
state: "cleanup_pending",
|
|
982
|
+
attemptedAt: now(),
|
|
983
|
+
errorCode: safeErrorCode(error)
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
await input.evidenceStore.persistCleanupEvidence(cleanup);
|
|
988
|
+
return cleanup;
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
return {
|
|
992
|
+
...cleanup,
|
|
993
|
+
evidencePersisted: false,
|
|
994
|
+
removalState: "failed",
|
|
995
|
+
state: "cleanup_pending",
|
|
996
|
+
completedAt: undefined,
|
|
997
|
+
errorCode: "CLEANUP_EVIDENCE_NOT_PERSISTED"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async function persistProcessClosure(attempt) {
|
|
1002
|
+
const closure = {
|
|
1003
|
+
swarmId: record.swarmId,
|
|
1004
|
+
workspaceId: attempt.workspace.record.workspaceId,
|
|
1005
|
+
childRunId: attempt.childRunId,
|
|
1006
|
+
agentId: attempt.agent.agentId,
|
|
1007
|
+
state: "closed",
|
|
1008
|
+
completedAt: now()
|
|
1009
|
+
};
|
|
1010
|
+
try {
|
|
1011
|
+
await input.evidenceStore.persistProcessClosure(closure);
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async function preserveWorkspaceForMissingEvidence(workspace) {
|
|
1019
|
+
const cleanup = {
|
|
1020
|
+
schemaVersion: "martin.swarm.v1",
|
|
1021
|
+
cleanupId: createId("cleanup-pending"),
|
|
1022
|
+
swarmId: record.swarmId,
|
|
1023
|
+
workspaceId: workspace.record.workspaceId,
|
|
1024
|
+
workspaceKind: workspace.record.kind,
|
|
1025
|
+
evidencePersisted: false,
|
|
1026
|
+
processCloseState: "closed",
|
|
1027
|
+
removalState: "failed",
|
|
1028
|
+
state: "cleanup_pending",
|
|
1029
|
+
attemptedAt: now(),
|
|
1030
|
+
errorCode: "PROCESS_CLOSURE_EVIDENCE_NOT_PERSISTED"
|
|
1031
|
+
};
|
|
1032
|
+
try {
|
|
1033
|
+
await input.evidenceStore.persistCleanupEvidence(cleanup);
|
|
1034
|
+
return cleanup;
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
return {
|
|
1038
|
+
...cleanup,
|
|
1039
|
+
errorCode: "CLEANUP_EVIDENCE_NOT_PERSISTED"
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async function terminalizeMissingClosureEvidence(attempt, usage) {
|
|
1044
|
+
await preserveWorkspaceForMissingEvidence(attempt.workspace);
|
|
1045
|
+
record = updateTaskAndAgent(record, attempt.task.taskId, attempt.agent.agentId, attempt.childRunId, "needs_review", "needs_review", now());
|
|
1046
|
+
record = withTerminal(record, "needs_review", "Child process-closure evidence could not be persisted; workspace retained.", now());
|
|
1047
|
+
await appendEvent(childTerminalEvent(attempt, "CHILD_NEEDS_REVIEW", "process_closure_evidence_not_persisted", now(), "environment_mismatch", budgetEventDetails(attempt, usage.actualUsd, usage.actualTokens))).catch(() => undefined);
|
|
1048
|
+
}
|
|
1049
|
+
async function verifyChildReceipt(attempt) {
|
|
1050
|
+
const paths = runPaths(input.runsRoot, attempt.childRunId);
|
|
1051
|
+
return verifyReceiptIntegrityFromFiles({
|
|
1052
|
+
runId: attempt.childRunId,
|
|
1053
|
+
runsRoot: input.runsRoot,
|
|
1054
|
+
loopRecordPath: paths.loopRecord,
|
|
1055
|
+
ledgerPath: paths.ledger
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function budgetEventDetails(attempt, actualUsd = 0, actualTokens = 0) {
|
|
1059
|
+
const lease = record.budgetLedger.leases.find((candidate) => candidate.leaseId === attempt.leaseId);
|
|
1060
|
+
return {
|
|
1061
|
+
leaseId: attempt.leaseId,
|
|
1062
|
+
leaseState: lease?.status,
|
|
1063
|
+
reservedUsd: lease?.reservedUsd,
|
|
1064
|
+
reservedTokens: lease?.reservedTokens,
|
|
1065
|
+
actualUsd,
|
|
1066
|
+
actualTokens
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async function persistCompletion(attempt, result, leaseState, cleanup, preverifiedIntegrity) {
|
|
1070
|
+
const integrity = preverifiedIntegrity ?? await verifyChildReceipt(attempt);
|
|
1071
|
+
const receiptId = `receipt-${attempt.childRunId}`;
|
|
1072
|
+
const receipt = {
|
|
1073
|
+
receiptId,
|
|
1074
|
+
swarmId: record.swarmId,
|
|
1075
|
+
childRunId: attempt.childRunId,
|
|
1076
|
+
agentId: attempt.agent.agentId,
|
|
1077
|
+
attemptId: attempt.attemptId,
|
|
1078
|
+
taskIds: [attempt.task.taskId],
|
|
1079
|
+
integrity,
|
|
1080
|
+
bindingSha256: receiptBindingSha256({
|
|
1081
|
+
receiptId,
|
|
1082
|
+
swarmId: record.swarmId,
|
|
1083
|
+
childRunId: attempt.childRunId,
|
|
1084
|
+
agentId: attempt.agent.agentId,
|
|
1085
|
+
attemptId: attempt.attemptId,
|
|
1086
|
+
taskIds: [attempt.task.taskId],
|
|
1087
|
+
integrity
|
|
1088
|
+
})
|
|
1089
|
+
};
|
|
1090
|
+
await input.evidenceStore.persistChildCompletion({
|
|
1091
|
+
swarmId: record.swarmId,
|
|
1092
|
+
childRunId: attempt.childRunId,
|
|
1093
|
+
agentId: attempt.agent.agentId,
|
|
1094
|
+
attemptId: attempt.attemptId,
|
|
1095
|
+
taskIds: [attempt.task.taskId],
|
|
1096
|
+
workspaceId: attempt.workspace.record.workspaceId,
|
|
1097
|
+
processCloseState: "closed",
|
|
1098
|
+
leaseState,
|
|
1099
|
+
receipt,
|
|
1100
|
+
evidencePersisted: true,
|
|
1101
|
+
workspaceCleanupState: "completed",
|
|
1102
|
+
cleanup
|
|
1103
|
+
});
|
|
1104
|
+
void result;
|
|
1105
|
+
}
|
|
1106
|
+
async function reassignTask(taskId, fromAgentId, fromAttemptId, reason) {
|
|
1107
|
+
const used = reassignmentCounts.get(taskId) ?? 0;
|
|
1108
|
+
if (used >= record.parentContract.recoveryPolicy.maxReassignmentsPerTask) {
|
|
1109
|
+
record = withTerminal(record, "needs_review", `Reassignment cap reached for ${taskId}.`, now());
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
const alternate = record.agents.find((agent) => (agent.agentId !== fromAgentId
|
|
1113
|
+
&& agent.status === "queued"
|
|
1114
|
+
&& agent.contract.taskIds.includes(taskId)));
|
|
1115
|
+
if (!alternate) {
|
|
1116
|
+
record = withTerminal(record, "needs_review", `No eligible reassignment agent for ${taskId}.`, now());
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
const toAttemptId = createId("attempt");
|
|
1120
|
+
preboundAttemptIds.set(taskId, toAttemptId);
|
|
1121
|
+
reassignmentCounts.set(taskId, used + 1);
|
|
1122
|
+
record = {
|
|
1123
|
+
...record,
|
|
1124
|
+
tasks: record.tasks.map((task) => task.taskId === taskId
|
|
1125
|
+
? { ...task, assignedAgentId: alternate.agentId, status: "queued" }
|
|
1126
|
+
: task),
|
|
1127
|
+
updatedAt: now()
|
|
1128
|
+
};
|
|
1129
|
+
await appendEvent({
|
|
1130
|
+
idempotencyKey: `${taskId}:reassigned:${used + 1}`,
|
|
1131
|
+
type: "TASK_REASSIGNED",
|
|
1132
|
+
timestamp: now(),
|
|
1133
|
+
taskId,
|
|
1134
|
+
agentId: alternate.agentId,
|
|
1135
|
+
payload: {
|
|
1136
|
+
fromAgentId,
|
|
1137
|
+
toAgentId: alternate.agentId,
|
|
1138
|
+
fromAttemptId,
|
|
1139
|
+
toAttemptId,
|
|
1140
|
+
reason,
|
|
1141
|
+
reassignment: used + 1
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
async function appendBlockedAction(input) {
|
|
1147
|
+
await appendEvent({
|
|
1148
|
+
idempotencyKey: `${input.attemptId}:blocked:${input.action}`,
|
|
1149
|
+
type: "ACTION_BLOCKED",
|
|
1150
|
+
timestamp: now(),
|
|
1151
|
+
taskId: input.taskId,
|
|
1152
|
+
agentId: input.agentId,
|
|
1153
|
+
...(input.childRunId ? { childRunId: input.childRunId } : {}),
|
|
1154
|
+
payload: {
|
|
1155
|
+
attemptId: input.attemptId,
|
|
1156
|
+
action: input.action,
|
|
1157
|
+
reason: input.reason,
|
|
1158
|
+
...input.details
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async function cancelActiveChildren(reason) {
|
|
1163
|
+
if (active.size === 0) {
|
|
1164
|
+
return record.outcome.state === "running" ? withTerminal(record, "stopped", reason, now()) : record;
|
|
1165
|
+
}
|
|
1166
|
+
const attempts = [...active.values()];
|
|
1167
|
+
for (const attempt of attempts) {
|
|
1168
|
+
if (!attempt.controller.signal.aborted)
|
|
1169
|
+
attempt.controller.abort(reason);
|
|
1170
|
+
}
|
|
1171
|
+
const completions = await Promise.all(attempts.map((attempt) => attempt.completion));
|
|
1172
|
+
await Promise.all(completions.map((completion) => settleAttemptOnce(completion, true)));
|
|
1173
|
+
return record.outcome.state === "running" ? withTerminal(record, "stopped", reason, now()) : record;
|
|
1174
|
+
}
|
|
1175
|
+
async function appendEvent(event) {
|
|
1176
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1177
|
+
const snapshot = await input.liveStore.readSnapshot();
|
|
1178
|
+
try {
|
|
1179
|
+
const appended = await input.liveStore.append(event, { expectedRevision: snapshot.revision });
|
|
1180
|
+
const recordEvent = {
|
|
1181
|
+
type: appended.event.type,
|
|
1182
|
+
swarmId: appended.event.swarmId,
|
|
1183
|
+
timestamp: appended.event.timestamp,
|
|
1184
|
+
parentPolicyVersion: appended.event.parentPolicyVersion,
|
|
1185
|
+
...(appended.event.taskId ? { taskId: appended.event.taskId } : {}),
|
|
1186
|
+
...(appended.event.agentId ? { agentId: appended.event.agentId } : {}),
|
|
1187
|
+
...(appended.event.childRunId ? { childRunId: appended.event.childRunId } : {}),
|
|
1188
|
+
...(appended.event.failureClass ? { failureClass: appended.event.failureClass } : {}),
|
|
1189
|
+
payload: { ...appended.event.payload }
|
|
1190
|
+
};
|
|
1191
|
+
if (!record.events.some((candidate) => sameCanonicalSwarmEvent(candidate, recordEvent))) {
|
|
1192
|
+
record = { ...record, events: [...record.events, recordEvent], updatedAt: recordEvent.timestamp };
|
|
1193
|
+
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
catch (error) {
|
|
1197
|
+
if (!isStaleRevision(error) || parentController.signal.aborted)
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
throw new Error("SWARM_LIVE_EVENT_CONTENTION: event revision did not settle.");
|
|
1202
|
+
}
|
|
1203
|
+
async function finalizeTerminalResult(proposed) {
|
|
1204
|
+
let canonical = proposed;
|
|
1205
|
+
let terminalEvents = (await input.liveStore.readEvents()).filter((event) => (event.type === "SWARM_STOPPED" || event.type === "SWARM_NEEDS_REVIEW" || event.type === "SWARM_VERIFIED"));
|
|
1206
|
+
if (terminalEvents.length === 0) {
|
|
1207
|
+
if (canonical.outcome.state === "verified") {
|
|
1208
|
+
const rejected = withTerminal(canonical.record, "needs_review", "Parent verification lacked a durable terminal authority event.", now());
|
|
1209
|
+
canonical = { ...canonical, record: rejected, outcome: rejected.outcome };
|
|
1210
|
+
}
|
|
1211
|
+
else if (canonical.outcome.state === "running") {
|
|
1212
|
+
const rejected = withTerminal(canonical.record, "needs_review", "Terminal outcome was incomplete.", now());
|
|
1213
|
+
canonical = { ...canonical, record: rejected, outcome: rejected.outcome };
|
|
1214
|
+
}
|
|
1215
|
+
const type = canonical.outcome.state === "stopped" ? "SWARM_STOPPED" : "SWARM_NEEDS_REVIEW";
|
|
1216
|
+
await appendEvent({
|
|
1217
|
+
idempotencyKey: `${input.plan.planHash}:terminal:${canonical.outcome.state}`,
|
|
1218
|
+
type,
|
|
1219
|
+
timestamp: now(),
|
|
1220
|
+
payload: { reason: canonical.outcome.reason }
|
|
1221
|
+
});
|
|
1222
|
+
terminalEvents = (await input.liveStore.readEvents()).filter((event) => (event.type === "SWARM_STOPPED" || event.type === "SWARM_NEEDS_REVIEW" || event.type === "SWARM_VERIFIED"));
|
|
1223
|
+
}
|
|
1224
|
+
if (terminalEvents.length !== 1) {
|
|
1225
|
+
const rejected = withTerminal(canonical.record, "needs_review", "Parent terminal event cardinality is invalid.", now());
|
|
1226
|
+
return { ...canonical, record: rejected, outcome: rejected.outcome };
|
|
1227
|
+
}
|
|
1228
|
+
if (!dependencies.sealTerminal) {
|
|
1229
|
+
if (canonical.outcome.state !== "verified")
|
|
1230
|
+
return canonical;
|
|
1231
|
+
const rejected = withTerminal(canonical.record, "needs_review", "terminal_evidence_sealing_required", now());
|
|
1232
|
+
return { ...canonical, record: rejected, outcome: rejected.outcome };
|
|
1233
|
+
}
|
|
1234
|
+
try {
|
|
1235
|
+
await dependencies.sealTerminal(canonical);
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
// A seal failure must never erase the primary stopped/needs-review
|
|
1239
|
+
// cause. VERIFIED still fails closed because no verified claim is
|
|
1240
|
+
// authoritative without its committed seal.
|
|
1241
|
+
if (canonical.outcome.state !== "verified")
|
|
1242
|
+
return canonical;
|
|
1243
|
+
const rejected = withTerminal(canonical.record, "needs_review", "terminal_evidence_sealing_failed", now());
|
|
1244
|
+
return { ...canonical, record: rejected, outcome: rejected.outcome };
|
|
1245
|
+
}
|
|
1246
|
+
if (canonical.outcome.state === "verified")
|
|
1247
|
+
authoritativeVerifiedResults.add(canonical);
|
|
1248
|
+
return canonical;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function assertRuntimeInput(input) {
|
|
1252
|
+
const validation = validateSwarmLivePlan(input.plan);
|
|
1253
|
+
if (!validation.ok)
|
|
1254
|
+
throw new Error("INVALID_LIVE_SWARM_PLAN");
|
|
1255
|
+
if (input.liveStore.plan.planHash !== input.plan.planHash)
|
|
1256
|
+
throw new Error("LIVE_PLAN_STORE_MISMATCH");
|
|
1257
|
+
if (input.workspaceManager.baselineCommit !== input.plan.baselineCommit)
|
|
1258
|
+
throw new Error("LIVE_BASELINE_MISMATCH");
|
|
1259
|
+
}
|
|
1260
|
+
class FileSwarmLiveRuntimeEvidenceStore {
|
|
1261
|
+
root;
|
|
1262
|
+
constructor(root) {
|
|
1263
|
+
this.root = root;
|
|
1264
|
+
}
|
|
1265
|
+
async loadProcessClosure(childRunId) {
|
|
1266
|
+
return this.readJson("process-closures", childRunId);
|
|
1267
|
+
}
|
|
1268
|
+
async persistProcessClosure(evidence) {
|
|
1269
|
+
await this.writeJson("process-closures", evidence.childRunId, evidence);
|
|
1270
|
+
}
|
|
1271
|
+
async persistCandidate(artifact) {
|
|
1272
|
+
const path = await this.resolvePath("candidates", artifact.candidate.candidateId);
|
|
1273
|
+
const payload = JSON.stringify({
|
|
1274
|
+
...artifact,
|
|
1275
|
+
patch: artifact.patch.toString("base64"),
|
|
1276
|
+
manifest: artifact.manifest.toString("base64"),
|
|
1277
|
+
});
|
|
1278
|
+
await writeExclusive(path, payload);
|
|
1279
|
+
}
|
|
1280
|
+
async loadPersistedCandidate(candidateId) {
|
|
1281
|
+
const stored = await this.readJson("candidates", candidateId);
|
|
1282
|
+
if (!stored)
|
|
1283
|
+
return undefined;
|
|
1284
|
+
return {
|
|
1285
|
+
...stored,
|
|
1286
|
+
patch: Buffer.from(String(stored.patch ?? ""), "base64"),
|
|
1287
|
+
manifest: Buffer.from(String(stored.manifest ?? ""), "base64"),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
async persistDecision(decision) {
|
|
1291
|
+
await this.writeJson("decisions", decision.candidateId, decision);
|
|
1292
|
+
}
|
|
1293
|
+
async loadPersistedAdmission(candidateId) {
|
|
1294
|
+
const integrated = await this.readJson("integration-outcomes", candidateId);
|
|
1295
|
+
if (integrated !== undefined)
|
|
1296
|
+
return integrated.decision;
|
|
1297
|
+
return this.readJson("decisions", candidateId);
|
|
1298
|
+
}
|
|
1299
|
+
async claimCandidateIntegration(input) {
|
|
1300
|
+
const path = await this.resolvePath("integration-claims", `${input.swarmId}:${input.candidateId}`);
|
|
1301
|
+
try {
|
|
1302
|
+
await writeExclusive(path, JSON.stringify(input));
|
|
1303
|
+
return "claimed";
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
if (error.code === "EEXIST")
|
|
1307
|
+
return "already_claimed";
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
async persistOutcome(bundle) {
|
|
1312
|
+
await this.writeJson("integration-outcomes", bundle.decision.candidateId, bundle);
|
|
1313
|
+
}
|
|
1314
|
+
async persistReconstruction(record) {
|
|
1315
|
+
await this.writeJson("integration-reconstructions", record.reconstructionId, record);
|
|
1316
|
+
}
|
|
1317
|
+
async persistGlobalVerification(evidence) {
|
|
1318
|
+
await this.writeJson("global-verification", evidence.verificationId, evidence);
|
|
1319
|
+
}
|
|
1320
|
+
async persistChildCompletion(evidence) {
|
|
1321
|
+
await this.writeJson("child-completions", evidence.childRunId, evidence);
|
|
1322
|
+
}
|
|
1323
|
+
async loadPersistedChildCompletion(childRunId) {
|
|
1324
|
+
return this.readJson("child-completions", childRunId);
|
|
1325
|
+
}
|
|
1326
|
+
async persistWorkspaceFailureEvidence(evidence) {
|
|
1327
|
+
await this.writeJson("workspace-failures", evidence.failureId, evidence);
|
|
1328
|
+
}
|
|
1329
|
+
async persistCleanupEvidence(cleanup) {
|
|
1330
|
+
await this.writeJson("cleanup", cleanup.cleanupId, cleanup);
|
|
1331
|
+
}
|
|
1332
|
+
async persistTerminalRecord(record) {
|
|
1333
|
+
await this.writeJson("terminal", record.swarmId, record);
|
|
1334
|
+
}
|
|
1335
|
+
async persistBeforeCleanup(evidence) {
|
|
1336
|
+
await this.writeJson("pre-cleanup", evidence.terminal.childRunId, evidence);
|
|
1337
|
+
}
|
|
1338
|
+
async writeJson(directory, id, value) {
|
|
1339
|
+
const path = await this.resolvePath(directory, id);
|
|
1340
|
+
await writeExclusiveIdempotent(path, JSON.stringify(value, null, 2));
|
|
1341
|
+
}
|
|
1342
|
+
async readJson(directory, id) {
|
|
1343
|
+
const path = await this.resolvePath(directory, id);
|
|
1344
|
+
try {
|
|
1345
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
if (error.code === "ENOENT")
|
|
1349
|
+
return undefined;
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async resolvePath(directory, id) {
|
|
1354
|
+
const targetDirectory = join(this.root, directory);
|
|
1355
|
+
await mkdir(targetDirectory, { recursive: true });
|
|
1356
|
+
return join(targetDirectory, `${createHash("sha256").update(id).digest("hex")}.json`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async function writeExclusive(path, contents) {
|
|
1360
|
+
const handle = await open(path, "wx");
|
|
1361
|
+
try {
|
|
1362
|
+
await handle.writeFile(contents, "utf8");
|
|
1363
|
+
}
|
|
1364
|
+
finally {
|
|
1365
|
+
await handle.close();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async function writeExclusiveIdempotent(path, contents) {
|
|
1369
|
+
const existing = await lstat(path).catch((error) => {
|
|
1370
|
+
if (isNodeError(error, "ENOENT"))
|
|
1371
|
+
return undefined;
|
|
1372
|
+
throw error;
|
|
1373
|
+
});
|
|
1374
|
+
if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
|
|
1375
|
+
throw codedRuntimeError("SWARM_SEAL_PATH_ALIAS", "Immutable evidence output path is not an exact regular file.");
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
await writeExclusive(path, contents);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
if (error.code !== "EEXIST")
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
if (await readFile(path, "utf8") === contents)
|
|
1386
|
+
return;
|
|
1387
|
+
throw Object.assign(new Error("Evidence identity already contains different immutable bytes."), {
|
|
1388
|
+
code: "SWARM_EVIDENCE_IDENTITY_CONFLICT",
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
function updateTaskAndAgent(record, taskId, agentId, childRunId, taskStatus, agentStatus, updatedAt) {
|
|
1392
|
+
return {
|
|
1393
|
+
...record,
|
|
1394
|
+
tasks: record.tasks.map((task) => task.taskId === taskId ? { ...task, status: taskStatus, assignedAgentId: agentId } : task),
|
|
1395
|
+
agents: record.agents.map((agent) => agent.agentId === agentId ? { ...agent, status: agentStatus, childRunId } : agent),
|
|
1396
|
+
updatedAt
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function childTerminalEvent(attempt, type, reason, timestamp, failureClass, details = {}) {
|
|
1400
|
+
return {
|
|
1401
|
+
idempotencyKey: `${attempt.childRunId}:${type.toLowerCase()}`,
|
|
1402
|
+
type,
|
|
1403
|
+
timestamp,
|
|
1404
|
+
taskId: attempt.task.taskId,
|
|
1405
|
+
agentId: attempt.agent.agentId,
|
|
1406
|
+
childRunId: attempt.childRunId,
|
|
1407
|
+
...(failureClass ? { failureClass } : {}),
|
|
1408
|
+
payload: { reason, ...(failureClass ? { failureClass } : {}), ...details, attemptId: attempt.attemptId }
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function withTerminal(record, state, reason, updatedAt) {
|
|
1412
|
+
return { ...record, outcome: { state, reason }, updatedAt };
|
|
1413
|
+
}
|
|
1414
|
+
function terminalResult(record, candidateIds, state, reason) {
|
|
1415
|
+
const finalRecord = withTerminal(record, state, reason, record.updatedAt);
|
|
1416
|
+
return { record: finalRecord, outcome: finalRecord.outcome, candidateIds: Object.freeze([...candidateIds]) };
|
|
1417
|
+
}
|
|
1418
|
+
function runPaths(runsRoot, childRunId) {
|
|
1419
|
+
const directory = runDir(runsRoot, childRunId);
|
|
1420
|
+
return { loopRecord: join(directory, "loop-record.json"), ledger: join(directory, "ledger.jsonl") };
|
|
1421
|
+
}
|
|
1422
|
+
function receiptBindingSha256(input) {
|
|
1423
|
+
return createHash("sha256").update(JSON.stringify({
|
|
1424
|
+
receiptId: input.receiptId,
|
|
1425
|
+
swarmId: input.swarmId,
|
|
1426
|
+
childRunId: input.childRunId,
|
|
1427
|
+
agentId: input.agentId,
|
|
1428
|
+
attemptId: input.attemptId,
|
|
1429
|
+
taskIds: [...input.taskIds].sort(),
|
|
1430
|
+
integrity: {
|
|
1431
|
+
state: input.integrity.state,
|
|
1432
|
+
keyId: input.integrity.keyId,
|
|
1433
|
+
loopRecordSha256: input.integrity.loopRecordSha256,
|
|
1434
|
+
ledgerSha256: input.integrity.ledgerSha256,
|
|
1435
|
+
ledgerHeadHash: input.integrity.ledgerHeadHash
|
|
1436
|
+
}
|
|
1437
|
+
}), "utf8").digest("hex");
|
|
1438
|
+
}
|
|
1439
|
+
function childIdFactory(childRunId, createId) {
|
|
1440
|
+
let issuedLoop = false;
|
|
1441
|
+
return (prefix) => {
|
|
1442
|
+
if (prefix === "loop" && !issuedLoop) {
|
|
1443
|
+
issuedLoop = true;
|
|
1444
|
+
return childRunId;
|
|
1445
|
+
}
|
|
1446
|
+
return createId(prefix);
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function isAdapterHandle(value) {
|
|
1450
|
+
return typeof value === "object" && value !== null && "adapter" in value;
|
|
1451
|
+
}
|
|
1452
|
+
function isStaleRevision(error) {
|
|
1453
|
+
return (typeof error === "object" && error !== null && "code" in error && error.code === "STALE_REVISION")
|
|
1454
|
+
|| (error instanceof Error && error.message.includes("STALE_REVISION"));
|
|
1455
|
+
}
|
|
1456
|
+
/** Internal production identity seam; intentionally not exported from the Core root. */
|
|
1457
|
+
export function sameCanonicalSwarmEvent(left, right) {
|
|
1458
|
+
const leftFailureClass = left.failureClass;
|
|
1459
|
+
const rightFailureClass = right.failureClass;
|
|
1460
|
+
return left.type === right.type
|
|
1461
|
+
&& left.swarmId === right.swarmId
|
|
1462
|
+
&& left.parentPolicyVersion === right.parentPolicyVersion
|
|
1463
|
+
&& left.taskId === right.taskId
|
|
1464
|
+
&& left.agentId === right.agentId
|
|
1465
|
+
&& left.childRunId === right.childRunId
|
|
1466
|
+
&& left.timestamp === right.timestamp
|
|
1467
|
+
&& leftFailureClass === rightFailureClass
|
|
1468
|
+
&& JSON.stringify(left.payload) === JSON.stringify(right.payload);
|
|
1469
|
+
}
|
|
1470
|
+
function safeError(error) {
|
|
1471
|
+
return redactSecretsFromText(error instanceof Error ? error.message : String(error));
|
|
1472
|
+
}
|
|
1473
|
+
function safeErrorCode(error) {
|
|
1474
|
+
if (error instanceof Error && "code" in error) {
|
|
1475
|
+
const code = String(error.code ?? "WORKSPACE_CLEANUP_FAILED");
|
|
1476
|
+
return redactSecretsFromText(code).slice(0, 96);
|
|
1477
|
+
}
|
|
1478
|
+
return "WORKSPACE_CLEANUP_FAILED";
|
|
1479
|
+
}
|
|
1480
|
+
function childFailureClass(result) {
|
|
1481
|
+
return result.decision.failureClass ?? result.loop.attempts.at(-1)?.failureClass;
|
|
1482
|
+
}
|