@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,860 @@
|
|
|
1
|
+
import { link, mkdir, open, readdir, readFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { SWARM_SCHEMA_VERSION, validateSwarmLiveEvent, validateSwarmLivePlan, validateSwarmLiveRevision } from "../../contracts/index.js";
|
|
6
|
+
import { isAuthorizedParentSwarmPipelineResult } from "./index.js";
|
|
7
|
+
export class SwarmLiveStoreError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
details;
|
|
10
|
+
constructor(code, message, details) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.details = details;
|
|
14
|
+
this.name = "SwarmLiveStoreError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const writerQueues = new Map();
|
|
18
|
+
export async function createSwarmLiveStore(input) {
|
|
19
|
+
assertValidPlan(input.plan);
|
|
20
|
+
const paths = resolveStorePaths(input.rootDir, input.plan.swarmId);
|
|
21
|
+
await mkdir(paths.directory, { recursive: true });
|
|
22
|
+
await publishJsonIfAbsent(paths.plan, input.plan);
|
|
23
|
+
const existingPlan = await readRequiredJson(paths.plan, "MISSING_PLAN");
|
|
24
|
+
assertValidPlan(existingPlan);
|
|
25
|
+
if (existingPlan.planHash !== input.plan.planHash) {
|
|
26
|
+
throw new SwarmLiveStoreError("PLAN_CONFLICT", "A different live plan already owns this swarm store.", { expectedPlanHash: existingPlan.planHash, actualPlanHash: input.plan.planHash });
|
|
27
|
+
}
|
|
28
|
+
const eventsHandle = await open(paths.events, "a");
|
|
29
|
+
await eventsHandle.close();
|
|
30
|
+
await mkdir(paths.eventClaims, { recursive: true });
|
|
31
|
+
const store = new FileSwarmLiveStore(paths, clone(input.plan));
|
|
32
|
+
await store.recover();
|
|
33
|
+
return store;
|
|
34
|
+
}
|
|
35
|
+
export async function openSwarmLiveStore(input) {
|
|
36
|
+
const paths = resolveStorePaths(input.rootDir, input.swarmId);
|
|
37
|
+
return openSwarmLiveStoreAtDirectory({ directory: paths.directory, swarmId: input.swarmId });
|
|
38
|
+
}
|
|
39
|
+
/** Internal canonical-path entrypoint; intentionally not exported from the Core package root. */
|
|
40
|
+
export async function openSwarmLiveStoreAtDirectory(input) {
|
|
41
|
+
const paths = resolveStorePathsAtDirectory(input.directory, input.swarmId);
|
|
42
|
+
const plan = await readRequiredJson(paths.plan, "MISSING_PLAN");
|
|
43
|
+
assertValidPlan(plan);
|
|
44
|
+
if (plan.swarmId !== input.swarmId) {
|
|
45
|
+
throw new SwarmLiveStoreError("PLAN_CONFLICT", "Canonical store directory contains a different swarm plan.");
|
|
46
|
+
}
|
|
47
|
+
const store = new FileSwarmLiveStore(paths, plan);
|
|
48
|
+
await store.recover();
|
|
49
|
+
return store;
|
|
50
|
+
}
|
|
51
|
+
/** Read-only evidence path. It validates derived files but never heals or rewrites them. */
|
|
52
|
+
export async function readCanonicalSwarmLiveEvidenceBundle(input) {
|
|
53
|
+
const paths = resolveStorePathsAtDirectory(input.directory, input.swarmId);
|
|
54
|
+
const planBytes = await readRequiredBytes(paths.plan, "MISSING_PLAN");
|
|
55
|
+
const plan = parseRequiredJson(planBytes, paths.plan);
|
|
56
|
+
assertValidPlan(plan);
|
|
57
|
+
if (plan.swarmId !== input.swarmId) {
|
|
58
|
+
throw new SwarmLiveStoreError("PLAN_CONFLICT", "Canonical store directory contains a different swarm plan.");
|
|
59
|
+
}
|
|
60
|
+
const startClaimBytes = await readRequiredBytes(paths.startClaim, "MISSING_START_CLAIM");
|
|
61
|
+
const startClaim = parseRequiredJson(startClaimBytes, paths.startClaim);
|
|
62
|
+
if (startClaim.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
63
|
+
|| startClaim.swarmId !== plan.swarmId
|
|
64
|
+
|| startClaim.planHash !== plan.planHash) {
|
|
65
|
+
throw new SwarmLiveStoreError("START_CLAIM_CONFLICT", "Start claim does not bind the canonical swarm plan.");
|
|
66
|
+
}
|
|
67
|
+
const authoritative = await loadAuthoritativeEventClaims(paths, plan, { strictEntries: true });
|
|
68
|
+
const terminalIndexes = authoritative.events.flatMap((event, index) => (isParentTerminalEvent(event) ? [index] : []));
|
|
69
|
+
if (terminalIndexes.length !== 1
|
|
70
|
+
|| terminalIndexes[0] !== authoritative.events.length - 1) {
|
|
71
|
+
throw new SwarmLiveStoreError("INVALID_TERMINAL_EVENT_CHAIN", "Canonical swarm evidence requires exactly one final parent terminal event.");
|
|
72
|
+
}
|
|
73
|
+
const snapshot = replaySnapshot(plan, authoritative.events);
|
|
74
|
+
if (snapshot.outcome.state === "running") {
|
|
75
|
+
throw new SwarmLiveStoreError("NON_TERMINAL_SWARM_EVIDENCE", "Swarm evidence can only be sealed after a parent terminal outcome.");
|
|
76
|
+
}
|
|
77
|
+
const eventsBytes = await readRequiredBytes(paths.events, "MISSING_EVENT_JOURNAL");
|
|
78
|
+
const expectedEvents = Buffer.from(eventJournalText(authoritative.events), "utf8");
|
|
79
|
+
if (!eventsBytes.equals(expectedEvents)) {
|
|
80
|
+
throw new SwarmLiveStoreError("EVENT_JOURNAL_MISMATCH", "Derived event journal does not match immutable event claims.");
|
|
81
|
+
}
|
|
82
|
+
const snapshotBytes = await readRequiredBytes(paths.snapshot, "MISSING_SNAPSHOT");
|
|
83
|
+
const expectedSnapshot = Buffer.from(jsonText(snapshot), "utf8");
|
|
84
|
+
if (!snapshotBytes.equals(expectedSnapshot)) {
|
|
85
|
+
throw new SwarmLiveStoreError("SNAPSHOT_MISMATCH", "Derived snapshot does not match immutable event claims.");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
plan: clone(plan),
|
|
89
|
+
events: authoritative.events.map(clone),
|
|
90
|
+
snapshot: clone(snapshot),
|
|
91
|
+
paths: { ...paths },
|
|
92
|
+
files: [
|
|
93
|
+
{ kind: "plan", path: paths.plan, bytes: planBytes },
|
|
94
|
+
{ kind: "start_claim", path: paths.startClaim, bytes: startClaimBytes },
|
|
95
|
+
...authoritative.claims.map((claim) => ({
|
|
96
|
+
kind: "event_claim",
|
|
97
|
+
path: claim.path,
|
|
98
|
+
bytes: claim.bytes
|
|
99
|
+
})),
|
|
100
|
+
{ kind: "events", path: paths.events, bytes: eventsBytes },
|
|
101
|
+
{ kind: "snapshot", path: paths.snapshot, bytes: snapshotBytes }
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
class FileSwarmLiveStore {
|
|
106
|
+
storePaths;
|
|
107
|
+
plan;
|
|
108
|
+
#listeners = new Set();
|
|
109
|
+
constructor(storePaths, plan) {
|
|
110
|
+
this.storePaths = storePaths;
|
|
111
|
+
this.plan = clone(plan);
|
|
112
|
+
}
|
|
113
|
+
paths() {
|
|
114
|
+
return { ...this.storePaths };
|
|
115
|
+
}
|
|
116
|
+
async claimStart() {
|
|
117
|
+
const claim = {
|
|
118
|
+
schemaVersion: SWARM_SCHEMA_VERSION,
|
|
119
|
+
swarmId: this.plan.swarmId,
|
|
120
|
+
planHash: this.plan.planHash
|
|
121
|
+
};
|
|
122
|
+
let handle;
|
|
123
|
+
try {
|
|
124
|
+
handle = await open(this.storePaths.startClaim, "wx");
|
|
125
|
+
await handle.writeFile(`${JSON.stringify(claim, null, 2)}\n`, "utf8");
|
|
126
|
+
await handle.sync();
|
|
127
|
+
await handle.close();
|
|
128
|
+
handle = undefined;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
await handle?.close();
|
|
132
|
+
if (!isNodeError(error, "EEXIST"))
|
|
133
|
+
throw error;
|
|
134
|
+
const existing = await readRequiredJson(this.storePaths.startClaim, "MISSING_START_CLAIM");
|
|
135
|
+
if (existing.schemaVersion !== SWARM_SCHEMA_VERSION
|
|
136
|
+
|| existing.swarmId !== this.plan.swarmId
|
|
137
|
+
|| existing.planHash !== this.plan.planHash) {
|
|
138
|
+
throw new SwarmLiveStoreError("START_CLAIM_CONFLICT", "The immutable live swarm start claim does not match this exact plan.");
|
|
139
|
+
}
|
|
140
|
+
throw new SwarmLiveStoreError("LIVE_SWARM_ALREADY_STARTED", "This exact live swarm has already started; replay is blocked before spend.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async recover() {
|
|
144
|
+
const events = await loadAuthoritativeEvents(this.storePaths, this.plan);
|
|
145
|
+
const snapshot = replaySnapshot(this.plan, events);
|
|
146
|
+
await Promise.all([
|
|
147
|
+
writeEventJournalAtomic(this.storePaths.events, events),
|
|
148
|
+
writeDerivedJsonAtomic(this.storePaths.snapshot, snapshot)
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
async append(input, options) {
|
|
152
|
+
if (input.type === "SWARM_VERIFIED") {
|
|
153
|
+
throw new SwarmLiveStoreError("PARENT_VERIFIED_AUTHORITY_REQUIRED", "SWARM_VERIFIED may only be persisted by the parent/global verifier pipeline.");
|
|
154
|
+
}
|
|
155
|
+
return this.appendInternal(input, options);
|
|
156
|
+
}
|
|
157
|
+
async persistParentOutcome(result, options) {
|
|
158
|
+
if (!isAuthorizedParentSwarmPipelineResult(result)) {
|
|
159
|
+
throw new SwarmLiveStoreError("PARENT_PIPELINE_AUTHORITY_REQUIRED", "Only the exact result object returned by runParentSwarmPipeline may persist a parent outcome.");
|
|
160
|
+
}
|
|
161
|
+
const parentResult = clone(result);
|
|
162
|
+
if (parentResult.record.swarmId !== this.plan.swarmId
|
|
163
|
+
|| parentResult.record.parentContract.policyVersion !== this.plan.parentContract.policyVersion
|
|
164
|
+
|| parentResult.outcome.state === "running"
|
|
165
|
+
|| parentResult.record.outcome.state !== parentResult.outcome.state
|
|
166
|
+
|| parentResult.record.outcome.reason !== parentResult.outcome.reason) {
|
|
167
|
+
throw new SwarmLiveStoreError("PARENT_OUTCOME_MISMATCH", "Parent pipeline outcome does not match this live plan.");
|
|
168
|
+
}
|
|
169
|
+
const type = parentResult.outcome.state === "verified"
|
|
170
|
+
? "SWARM_VERIFIED"
|
|
171
|
+
: parentResult.outcome.state === "stopped"
|
|
172
|
+
? "SWARM_STOPPED"
|
|
173
|
+
: "SWARM_NEEDS_REVIEW";
|
|
174
|
+
const terminalEvents = parentResult.record.events.filter((event) => isParentRecordTerminalEvent(event));
|
|
175
|
+
const authorityEvents = terminalEvents.filter((event) => event.type === "SWARM_VERIFIED");
|
|
176
|
+
const globalVerifierEvents = parentResult.record.events.filter((event) => ((event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED")
|
|
177
|
+
&& event.swarmId === this.plan.swarmId
|
|
178
|
+
&& event.parentPolicyVersion === this.plan.parentContract.policyVersion));
|
|
179
|
+
if (terminalEvents.length > 1
|
|
180
|
+
|| (terminalEvents[0] !== undefined && (terminalEvents[0] !== parentResult.record.events.at(-1)
|
|
181
|
+
|| terminalEvents[0].type !== type
|
|
182
|
+
|| terminalEvents[0].swarmId !== this.plan.swarmId
|
|
183
|
+
|| terminalEvents[0].parentPolicyVersion !== this.plan.parentContract.policyVersion))
|
|
184
|
+
|| (type === "SWARM_VERIFIED" && authorityEvents.length !== 1)) {
|
|
185
|
+
throw new SwarmLiveStoreError("PARENT_OUTCOME_MISMATCH", "Parent result lacks one exact final parent outcome authority event.");
|
|
186
|
+
}
|
|
187
|
+
if (globalVerifierEvents.length > 1
|
|
188
|
+
|| (type === "SWARM_VERIFIED" && globalVerifierEvents[0]?.type !== "GLOBAL_VERIFIER_PASSED")
|
|
189
|
+
|| (globalVerifierEvents[0] !== undefined && (parentResult.verification === undefined
|
|
190
|
+
|| globalVerifierEvents[0].payload.verificationId !== parentResult.verification.verificationId
|
|
191
|
+
|| globalVerifierEvents[0].payload.integratedTreeHash !== parentResult.integration.finalTreeHash))
|
|
192
|
+
|| (parentResult.verification !== undefined && globalVerifierEvents.length !== 1)) {
|
|
193
|
+
throw new SwarmLiveStoreError("PARENT_OUTCOME_MISMATCH", "Parent result has an invalid global-verifier event sequence.");
|
|
194
|
+
}
|
|
195
|
+
const resultSha256 = createHash("sha256").update(JSON.stringify(parentResult), "utf8").digest("hex");
|
|
196
|
+
const durableEvents = await loadAuthoritativeEvents(this.storePaths, this.plan);
|
|
197
|
+
const firstPersistedParentEvent = durableEvents.findIndex((event) => isPersistedParentPipelineEvent(event));
|
|
198
|
+
const durablePrefixLength = firstPersistedParentEvent < 0 ? durableEvents.length : firstPersistedParentEvent;
|
|
199
|
+
const recordPrefixOffset = canonicalUndurableBootstrapOffset(parentResult.record, durableEvents.slice(0, durablePrefixLength));
|
|
200
|
+
if (parentResult.record.events.length < recordPrefixOffset + durablePrefixLength
|
|
201
|
+
|| durableEvents.slice(0, durablePrefixLength).some((event, index) => (!sameRecordEvent(event, parentResult.record.events[recordPrefixOffset + index])))) {
|
|
202
|
+
throw new SwarmLiveStoreError("PARENT_PIPELINE_EVENT_MISMATCH", "Durable child lifecycle events do not match the authorized parent result.");
|
|
203
|
+
}
|
|
204
|
+
const parentRecordEvents = parentResult.record.events.slice(recordPrefixOffset + durablePrefixLength);
|
|
205
|
+
assertAuthorizedParentEventSequence(parentResult, parentRecordEvents, type);
|
|
206
|
+
const integrationInputs = parentRecordEvents.flatMap((event, index) => (isParentIntegrationEvent(event)
|
|
207
|
+
? [recordEventInput(event, `parent-integration:${resultSha256}:${String(index).padStart(6, "0")}`)]
|
|
208
|
+
: []));
|
|
209
|
+
let expectedRevision = options.expectedRevision;
|
|
210
|
+
const expectedInputs = [...integrationInputs];
|
|
211
|
+
const globalVerifierEvent = globalVerifierEvents[0];
|
|
212
|
+
if (globalVerifierEvent) {
|
|
213
|
+
expectedInputs.push({
|
|
214
|
+
idempotencyKey: `parent-global-verifier:${this.plan.swarmId}:${resultSha256}`,
|
|
215
|
+
type: globalVerifierEvent.type,
|
|
216
|
+
timestamp: globalVerifierEvent.timestamp,
|
|
217
|
+
payload: clone(globalVerifierEvent.payload)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
expectedInputs.push({
|
|
221
|
+
idempotencyKey: `parent-outcome:${this.plan.swarmId}:${parentResult.outcome.state}:${resultSha256}`,
|
|
222
|
+
type,
|
|
223
|
+
timestamp: terminalEvents[0]?.timestamp ?? parentResult.record.updatedAt,
|
|
224
|
+
payload: {
|
|
225
|
+
reason: parentResult.outcome.reason,
|
|
226
|
+
parentPipelineAuthority: "runParentSwarmPipeline:v1",
|
|
227
|
+
parentPipelineResultSha256: resultSha256,
|
|
228
|
+
...(globalVerifierEvent ? {
|
|
229
|
+
verificationId: globalVerifierEvent.payload.verificationId,
|
|
230
|
+
integratedTreeHash: globalVerifierEvent.payload.integratedTreeHash
|
|
231
|
+
} : {})
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
const durableParentEvents = durableEvents.slice(durablePrefixLength);
|
|
235
|
+
if (durableParentEvents.length > expectedInputs.length
|
|
236
|
+
|| durableParentEvents.some((event, index) => (expectedInputs[index] === undefined || !isExactPersistedParentResultEvent(event, expectedInputs[index])))) {
|
|
237
|
+
throw new SwarmLiveStoreError("PARENT_PIPELINE_EVENT_MISMATCH", "Durable parent events do not match this authorized parent result.");
|
|
238
|
+
}
|
|
239
|
+
let persisted;
|
|
240
|
+
for (const input of expectedInputs) {
|
|
241
|
+
persisted = await this.appendInternal(input, { expectedRevision }, input.type === "SWARM_VERIFIED");
|
|
242
|
+
expectedRevision = persisted.snapshot.revision;
|
|
243
|
+
}
|
|
244
|
+
if (!persisted) {
|
|
245
|
+
throw new SwarmLiveStoreError("PARENT_PIPELINE_EVENT_MISMATCH", "Parent result produced no terminal event.");
|
|
246
|
+
}
|
|
247
|
+
return persisted;
|
|
248
|
+
}
|
|
249
|
+
async appendInternal(input, options, parentAuthorized = false) {
|
|
250
|
+
if (input.type === "SWARM_VERIFIED" && !parentAuthorized) {
|
|
251
|
+
throw new SwarmLiveStoreError("PARENT_VERIFIED_AUTHORITY_REQUIRED", "SWARM_VERIFIED may only be persisted by the parent/global verifier pipeline.");
|
|
252
|
+
}
|
|
253
|
+
if (!validateSwarmLiveRevision(options.expectedRevision)) {
|
|
254
|
+
throw new SwarmLiveStoreError("INVALID_REVISION", "Expected revision must be a nonnegative safe integer.");
|
|
255
|
+
}
|
|
256
|
+
return withLocalWriter(this.storePaths.directory, async () => {
|
|
257
|
+
const events = await loadAuthoritativeEvents(this.storePaths, this.plan);
|
|
258
|
+
const snapshot = replaySnapshot(this.plan, events);
|
|
259
|
+
const duplicate = events.find((event) => event.idempotencyKey === input.idempotencyKey);
|
|
260
|
+
if (duplicate) {
|
|
261
|
+
if (!isExactPersistedParentResultEvent(duplicate, input)) {
|
|
262
|
+
throw new SwarmLiveStoreError("IDEMPOTENCY_CONFLICT", "An idempotency key cannot identify two different swarm events.", { idempotencyKey: input.idempotencyKey });
|
|
263
|
+
}
|
|
264
|
+
return { event: clone(duplicate), snapshot, duplicate: true };
|
|
265
|
+
}
|
|
266
|
+
if (snapshot.revision !== options.expectedRevision) {
|
|
267
|
+
throw new SwarmLiveStoreError("STALE_REVISION", "Live swarm state changed before this append.", { expectedRevision: options.expectedRevision, actualRevision: snapshot.revision });
|
|
268
|
+
}
|
|
269
|
+
if (input.type === "SWARM_VERIFIED" && snapshot.cancellation) {
|
|
270
|
+
throw new SwarmLiveStoreError("CANCELLATION_PRECEDES_PARENT_VERIFIED", "A durable cancellation request precedes the parent verified claim.");
|
|
271
|
+
}
|
|
272
|
+
const event = materializeEvent(this.plan, input, snapshot.lastSequence + 1);
|
|
273
|
+
const validation = validateSwarmLiveEvent(event, this.plan);
|
|
274
|
+
if (!validation.ok) {
|
|
275
|
+
throw new SwarmLiveStoreError("INVALID_EVENT", "Live swarm event validation failed.", {
|
|
276
|
+
errors: validation.errors
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
await publishEventClaim(this.storePaths.eventClaims, event);
|
|
280
|
+
const next = reduceSnapshot(snapshot, event);
|
|
281
|
+
await Promise.all([
|
|
282
|
+
writeEventJournalAtomic(this.storePaths.events, [...events, event]),
|
|
283
|
+
writeDerivedJsonAtomic(this.storePaths.snapshot, next)
|
|
284
|
+
]);
|
|
285
|
+
this.notify(next);
|
|
286
|
+
return { event: clone(event), snapshot: clone(next), duplicate: false };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async requestCancellation(request, options) {
|
|
290
|
+
return this.append({
|
|
291
|
+
idempotencyKey: request.idempotencyKey,
|
|
292
|
+
type: "SWARM_CANCEL_REQUESTED",
|
|
293
|
+
timestamp: request.requestedAt,
|
|
294
|
+
payload: {
|
|
295
|
+
reason: request.reason,
|
|
296
|
+
...(request.requestedBy === undefined ? {} : { requestedBy: request.requestedBy })
|
|
297
|
+
}
|
|
298
|
+
}, options);
|
|
299
|
+
}
|
|
300
|
+
async readSnapshot() {
|
|
301
|
+
return replaySnapshot(this.plan, await loadAuthoritativeEvents(this.storePaths, this.plan));
|
|
302
|
+
}
|
|
303
|
+
async readEvents() {
|
|
304
|
+
return (await loadAuthoritativeEvents(this.storePaths, this.plan)).map(clone);
|
|
305
|
+
}
|
|
306
|
+
async waitForRevision(afterRevision, signal) {
|
|
307
|
+
if (!validateSwarmLiveRevision(afterRevision)) {
|
|
308
|
+
throw new SwarmLiveStoreError("INVALID_REVISION", "Wait revision must be a nonnegative safe integer.");
|
|
309
|
+
}
|
|
310
|
+
if (signal?.aborted)
|
|
311
|
+
throw abortError(signal.reason);
|
|
312
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
313
|
+
let settled = false;
|
|
314
|
+
let watcher;
|
|
315
|
+
let polling;
|
|
316
|
+
const listener = (snapshot) => {
|
|
317
|
+
if (settled || snapshot.revision <= afterRevision)
|
|
318
|
+
return;
|
|
319
|
+
settled = true;
|
|
320
|
+
cleanup();
|
|
321
|
+
resolvePromise(clone(snapshot));
|
|
322
|
+
};
|
|
323
|
+
const onAbort = () => {
|
|
324
|
+
if (settled)
|
|
325
|
+
return;
|
|
326
|
+
settled = true;
|
|
327
|
+
cleanup();
|
|
328
|
+
rejectPromise(abortError(signal?.reason));
|
|
329
|
+
};
|
|
330
|
+
const cleanup = () => {
|
|
331
|
+
this.#listeners.delete(listener);
|
|
332
|
+
signal?.removeEventListener("abort", onAbort);
|
|
333
|
+
watcher?.close();
|
|
334
|
+
if (polling !== undefined)
|
|
335
|
+
clearInterval(polling);
|
|
336
|
+
};
|
|
337
|
+
const refresh = () => {
|
|
338
|
+
void this.readSnapshot().then(listener, (error) => {
|
|
339
|
+
if (settled)
|
|
340
|
+
return;
|
|
341
|
+
settled = true;
|
|
342
|
+
cleanup();
|
|
343
|
+
rejectPromise(error);
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
this.#listeners.add(listener);
|
|
347
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
348
|
+
if (signal?.aborted)
|
|
349
|
+
onAbort();
|
|
350
|
+
if (settled)
|
|
351
|
+
return;
|
|
352
|
+
watcher = watch(this.storePaths.eventClaims, { persistent: false }, refresh);
|
|
353
|
+
watcher.on("error", refresh);
|
|
354
|
+
polling = setInterval(refresh, 100);
|
|
355
|
+
polling.unref?.();
|
|
356
|
+
refresh();
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
notify(snapshot) {
|
|
360
|
+
for (const listener of [...this.#listeners])
|
|
361
|
+
listener(snapshot);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function materializeEvent(plan, input, sequence) {
|
|
365
|
+
return {
|
|
366
|
+
schemaVersion: SWARM_SCHEMA_VERSION,
|
|
367
|
+
sequence,
|
|
368
|
+
idempotencyKey: input.idempotencyKey,
|
|
369
|
+
type: input.type,
|
|
370
|
+
swarmId: plan.swarmId,
|
|
371
|
+
timestamp: input.timestamp,
|
|
372
|
+
parentPolicyVersion: plan.parentContract.policyVersion,
|
|
373
|
+
planHash: plan.planHash,
|
|
374
|
+
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
|
|
375
|
+
...(input.agentId === undefined ? {} : { agentId: input.agentId }),
|
|
376
|
+
...(input.childRunId === undefined ? {} : { childRunId: input.childRunId }),
|
|
377
|
+
...(input.failureClass === undefined ? {} : { failureClass: input.failureClass }),
|
|
378
|
+
payload: clone(input.payload)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function replaySnapshot(plan, events) {
|
|
382
|
+
let snapshot = {
|
|
383
|
+
schemaVersion: SWARM_SCHEMA_VERSION,
|
|
384
|
+
swarmId: plan.swarmId,
|
|
385
|
+
planHash: plan.planHash,
|
|
386
|
+
revision: 0,
|
|
387
|
+
lastSequence: 0,
|
|
388
|
+
eventCount: 0,
|
|
389
|
+
outcome: {
|
|
390
|
+
state: "running",
|
|
391
|
+
reason: "Live swarm execution has not reached a parent terminal decision."
|
|
392
|
+
},
|
|
393
|
+
updatedAt: plan.createdAt
|
|
394
|
+
};
|
|
395
|
+
for (const event of events)
|
|
396
|
+
snapshot = reduceSnapshot(snapshot, event);
|
|
397
|
+
return snapshot;
|
|
398
|
+
}
|
|
399
|
+
function reduceSnapshot(snapshot, event) {
|
|
400
|
+
const next = {
|
|
401
|
+
...snapshot,
|
|
402
|
+
revision: snapshot.revision + 1,
|
|
403
|
+
lastSequence: event.sequence,
|
|
404
|
+
eventCount: snapshot.eventCount + 1,
|
|
405
|
+
lastEventType: event.type,
|
|
406
|
+
updatedAt: event.timestamp,
|
|
407
|
+
outcome: { ...snapshot.outcome },
|
|
408
|
+
...(snapshot.cancellation ? { cancellation: { ...snapshot.cancellation } } : {})
|
|
409
|
+
};
|
|
410
|
+
if (event.type === "SWARM_CANCEL_REQUESTED") {
|
|
411
|
+
next.cancellation = {
|
|
412
|
+
requestedAt: event.timestamp,
|
|
413
|
+
reason: textPayload(event.payload.reason, "operator_requested"),
|
|
414
|
+
...(typeof event.payload.requestedBy === "string"
|
|
415
|
+
? { requestedBy: event.payload.requestedBy }
|
|
416
|
+
: {})
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
else if (event.type === "SWARM_STOPPED") {
|
|
420
|
+
next.outcome = { state: "stopped", reason: textPayload(event.payload.reason, "Parent stopped the swarm.") };
|
|
421
|
+
}
|
|
422
|
+
else if (event.type === "SWARM_NEEDS_REVIEW") {
|
|
423
|
+
next.outcome = { state: "needs_review", reason: textPayload(event.payload.reason, "Parent review is required.") };
|
|
424
|
+
}
|
|
425
|
+
else if (event.type === "SWARM_VERIFIED") {
|
|
426
|
+
next.outcome = {
|
|
427
|
+
state: "verified",
|
|
428
|
+
reason: textPayload(event.payload.reason, "Parent/global verifier passed."),
|
|
429
|
+
verifiedAt: event.timestamp
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return next;
|
|
433
|
+
}
|
|
434
|
+
async function loadAuthoritativeEvents(paths, plan) {
|
|
435
|
+
return (await loadAuthoritativeEventClaims(paths, plan)).events;
|
|
436
|
+
}
|
|
437
|
+
async function loadAuthoritativeEventClaims(paths, plan, options = {}) {
|
|
438
|
+
let entries;
|
|
439
|
+
try {
|
|
440
|
+
entries = await readdir(paths.eventClaims);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
if (isNodeError(error, "ENOENT")) {
|
|
444
|
+
if (options.strictEntries) {
|
|
445
|
+
throw new SwarmLiveStoreError("MISSING_EVENT_CLAIMS", "Immutable event claim directory is missing.");
|
|
446
|
+
}
|
|
447
|
+
return { events: [], claims: [] };
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
const invalidEntries = entries.filter((value) => !/^\d{12}\.json$/u.test(value));
|
|
452
|
+
if (options.strictEntries && invalidEntries.length > 0) {
|
|
453
|
+
throw new SwarmLiveStoreError("MALFORMED_EVENT_CLAIMS", "Event claim directory contains non-canonical entries.", {
|
|
454
|
+
entries: invalidEntries.sort()
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const authoritative = [];
|
|
458
|
+
const claims = [];
|
|
459
|
+
const idempotencyKeys = new Set();
|
|
460
|
+
for (const entry of entries.filter((value) => /^\d{12}\.json$/u.test(value)).sort()) {
|
|
461
|
+
const claimPath = resolve(paths.eventClaims, entry);
|
|
462
|
+
const claimBytes = await readRequiredBytes(claimPath, "MISSING_EVENT_CLAIM");
|
|
463
|
+
const claim = parseRequiredJson(claimBytes, claimPath);
|
|
464
|
+
validatePersistedEvent(claim, plan, `event claim ${entry}`);
|
|
465
|
+
const expectedSequence = authoritative.length + 1;
|
|
466
|
+
const expectedEntry = `${String(expectedSequence).padStart(12, "0")}.json`;
|
|
467
|
+
if (entry !== expectedEntry
|
|
468
|
+
|| claim.sequence !== expectedSequence
|
|
469
|
+
|| idempotencyKeys.has(claim.idempotencyKey)) {
|
|
470
|
+
throw new SwarmLiveStoreError("EVENT_CLAIM_GAP", "Immutable event claims must form a contiguous revision chain.", {
|
|
471
|
+
expectedSequence,
|
|
472
|
+
actualSequence: claim.sequence,
|
|
473
|
+
entry
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
idempotencyKeys.add(claim.idempotencyKey);
|
|
477
|
+
authoritative.push(claim);
|
|
478
|
+
claims.push({ path: claimPath, bytes: claimBytes });
|
|
479
|
+
}
|
|
480
|
+
return { events: authoritative, claims };
|
|
481
|
+
}
|
|
482
|
+
function validatePersistedEvent(event, plan, source) {
|
|
483
|
+
if (event.type === "SWARM_VERIFIED"
|
|
484
|
+
&& (event.payload.parentPipelineAuthority !== "runParentSwarmPipeline:v1"
|
|
485
|
+
|| typeof event.payload.parentPipelineResultSha256 !== "string"
|
|
486
|
+
|| !/^[0-9a-f]{64}$/u.test(event.payload.parentPipelineResultSha256))) {
|
|
487
|
+
throw new SwarmLiveStoreError("PARENT_VERIFIED_AUTHORITY_REQUIRED", `Generic operational ${source} cannot authorize SWARM_VERIFIED.`);
|
|
488
|
+
}
|
|
489
|
+
const validation = validateSwarmLiveEvent(event, plan);
|
|
490
|
+
if (!validation.ok) {
|
|
491
|
+
throw new SwarmLiveStoreError("MALFORMED_EVENT_LOG", `Invalid persisted swarm event in ${source}.`, {
|
|
492
|
+
errors: validation.errors
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function publishEventClaim(directory, event) {
|
|
497
|
+
await mkdir(directory, { recursive: true });
|
|
498
|
+
const claimPath = resolve(directory, `${String(event.sequence).padStart(12, "0")}.json`);
|
|
499
|
+
const temporary = resolve(directory, `.${String(event.sequence).padStart(12, "0")}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
500
|
+
let handle;
|
|
501
|
+
try {
|
|
502
|
+
handle = await open(temporary, "wx");
|
|
503
|
+
await handle.writeFile(`${JSON.stringify(event)}\n`, "utf8");
|
|
504
|
+
await handle.sync();
|
|
505
|
+
await handle.close();
|
|
506
|
+
handle = undefined;
|
|
507
|
+
try {
|
|
508
|
+
await link(temporary, claimPath);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (isNodeError(error, "EEXIST")) {
|
|
512
|
+
throw new SwarmLiveStoreError("STALE_REVISION", "Live swarm state changed before this append.", {
|
|
513
|
+
expectedRevision: event.sequence - 1,
|
|
514
|
+
actualRevision: event.sequence
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
await handle?.close();
|
|
522
|
+
await rm(temporary, { force: true });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function withLocalWriter(directory, action) {
|
|
526
|
+
const prior = writerQueues.get(directory) ?? Promise.resolve();
|
|
527
|
+
let releaseQueue;
|
|
528
|
+
const turn = new Promise((resolvePromise) => {
|
|
529
|
+
releaseQueue = resolvePromise;
|
|
530
|
+
});
|
|
531
|
+
const queued = prior.catch(() => undefined).then(() => turn);
|
|
532
|
+
writerQueues.set(directory, queued);
|
|
533
|
+
await prior.catch(() => undefined);
|
|
534
|
+
try {
|
|
535
|
+
return await action();
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
releaseQueue();
|
|
539
|
+
if (writerQueues.get(directory) === queued)
|
|
540
|
+
writerQueues.delete(directory);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function resolveStorePaths(rootDir, swarmId) {
|
|
544
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(swarmId)) {
|
|
545
|
+
throw new SwarmLiveStoreError("INVALID_SWARM_ID", "Swarm id is not safe for a contained store path.");
|
|
546
|
+
}
|
|
547
|
+
const directory = resolve(rootDir, "_swarms", swarmId);
|
|
548
|
+
return resolveStorePathsAtDirectory(directory, swarmId);
|
|
549
|
+
}
|
|
550
|
+
function resolveStorePathsAtDirectory(directory, swarmId) {
|
|
551
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(swarmId)) {
|
|
552
|
+
throw new SwarmLiveStoreError("INVALID_SWARM_ID", "Swarm id is not safe for a contained store path.");
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
directory,
|
|
556
|
+
plan: resolve(directory, "plan.json"),
|
|
557
|
+
events: resolve(directory, "events.jsonl"),
|
|
558
|
+
snapshot: resolve(directory, "snapshot.json"),
|
|
559
|
+
startClaim: resolve(directory, "start-claim.json"),
|
|
560
|
+
writerLock: resolve(directory, "writer.lock"),
|
|
561
|
+
eventClaims: resolve(directory, "event-claims")
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function assertValidPlan(plan) {
|
|
565
|
+
const validation = validateSwarmLivePlan(plan);
|
|
566
|
+
if (!validation.ok) {
|
|
567
|
+
throw new SwarmLiveStoreError("INVALID_PLAN", "Live swarm plan validation failed.", {
|
|
568
|
+
errors: validation.errors
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function publishJsonIfAbsent(path, value) {
|
|
573
|
+
const temporary = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
574
|
+
let handle;
|
|
575
|
+
try {
|
|
576
|
+
handle = await open(temporary, "wx");
|
|
577
|
+
await handle.writeFile(jsonText(value), "utf8");
|
|
578
|
+
await handle.sync();
|
|
579
|
+
await handle.close();
|
|
580
|
+
handle = undefined;
|
|
581
|
+
try {
|
|
582
|
+
await link(temporary, path);
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
if (!isNodeError(error, "EEXIST"))
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
await handle?.close();
|
|
591
|
+
await rm(temporary, { force: true });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async function writeDerivedJsonAtomic(path, value) {
|
|
595
|
+
await writeTextAtomic(path, jsonText(value), { contentionSafe: true });
|
|
596
|
+
}
|
|
597
|
+
async function writeEventJournalAtomic(path, events) {
|
|
598
|
+
await writeTextAtomic(path, eventJournalText(events), { contentionSafe: true });
|
|
599
|
+
}
|
|
600
|
+
async function writeTextAtomic(path, value, options = {}) {
|
|
601
|
+
if (options.contentionSafe && await readTextIfPresent(path) === value)
|
|
602
|
+
return;
|
|
603
|
+
const temporary = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
604
|
+
let handle;
|
|
605
|
+
try {
|
|
606
|
+
handle = await open(temporary, "wx");
|
|
607
|
+
await handle.writeFile(value, "utf8");
|
|
608
|
+
await handle.sync();
|
|
609
|
+
await handle.close();
|
|
610
|
+
handle = undefined;
|
|
611
|
+
try {
|
|
612
|
+
await rename(temporary, path);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
if (!options.contentionSafe || !isDerivedViewContention(error))
|
|
616
|
+
throw error;
|
|
617
|
+
// Claims are authoritative. A competing reconciler may win the derived-view
|
|
618
|
+
// replacement without being allowed to block or roll back live state.
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
await handle?.close();
|
|
623
|
+
await rm(temporary, { force: true });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async function readTextIfPresent(path) {
|
|
627
|
+
try {
|
|
628
|
+
return await readFile(path, "utf8");
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
if (isNodeError(error, "ENOENT"))
|
|
632
|
+
return undefined;
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function isDerivedViewContention(error) {
|
|
637
|
+
return isNodeError(error, "EPERM") || isNodeError(error, "EACCES") || isNodeError(error, "EBUSY");
|
|
638
|
+
}
|
|
639
|
+
async function readJsonIfPresent(path) {
|
|
640
|
+
try {
|
|
641
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
if (isNodeError(error, "ENOENT"))
|
|
645
|
+
return undefined;
|
|
646
|
+
if (error instanceof SyntaxError) {
|
|
647
|
+
throw new SwarmLiveStoreError("MALFORMED_JSON", `Malformed JSON at ${path}.`);
|
|
648
|
+
}
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async function readRequiredJson(path, code) {
|
|
653
|
+
const value = await readJsonIfPresent(path);
|
|
654
|
+
if (value === undefined)
|
|
655
|
+
throw new SwarmLiveStoreError(code, `Required swarm store file is missing: ${path}`);
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
async function readRequiredBytes(path, code) {
|
|
659
|
+
try {
|
|
660
|
+
return await readFile(path);
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
if (isNodeError(error, "ENOENT")) {
|
|
664
|
+
throw new SwarmLiveStoreError(code, `Required swarm store file is missing: ${path}`);
|
|
665
|
+
}
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function parseRequiredJson(bytes, path) {
|
|
670
|
+
try {
|
|
671
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
if (error instanceof SyntaxError) {
|
|
675
|
+
throw new SwarmLiveStoreError("MALFORMED_JSON", `Malformed JSON at ${path}.`);
|
|
676
|
+
}
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function jsonText(value) {
|
|
681
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
682
|
+
}
|
|
683
|
+
function eventJournalText(events) {
|
|
684
|
+
return events.length === 0 ? "" : `${events.map((event) => JSON.stringify(event)).join("\n")}\n`;
|
|
685
|
+
}
|
|
686
|
+
/** Internal replay identity seam; intentionally not exported from the Core root. */
|
|
687
|
+
export function isExactPersistedParentResultEvent(event, input) {
|
|
688
|
+
return event.idempotencyKey === input.idempotencyKey
|
|
689
|
+
&& event.type === input.type
|
|
690
|
+
&& event.timestamp === input.timestamp
|
|
691
|
+
&& event.taskId === input.taskId
|
|
692
|
+
&& event.agentId === input.agentId
|
|
693
|
+
&& event.childRunId === input.childRunId
|
|
694
|
+
&& event.failureClass === input.failureClass
|
|
695
|
+
&& JSON.stringify(event.payload) === JSON.stringify(input.payload);
|
|
696
|
+
}
|
|
697
|
+
const PARENT_INTEGRATION_EVENT_TYPES = new Set([
|
|
698
|
+
"CHILD_PATCH_ADMITTED",
|
|
699
|
+
"CHILD_PATCH_REJECTED",
|
|
700
|
+
"INTEGRATION_CONFLICT",
|
|
701
|
+
"INTEGRATION_COMPLETED"
|
|
702
|
+
]);
|
|
703
|
+
function isParentIntegrationEvent(event) {
|
|
704
|
+
return PARENT_INTEGRATION_EVENT_TYPES.has(event.type);
|
|
705
|
+
}
|
|
706
|
+
function isParentRecordTerminalEvent(event) {
|
|
707
|
+
return event.type === "SWARM_STOPPED"
|
|
708
|
+
|| event.type === "SWARM_NEEDS_REVIEW"
|
|
709
|
+
|| event.type === "SWARM_VERIFIED";
|
|
710
|
+
}
|
|
711
|
+
function isPersistedParentPipelineEvent(event) {
|
|
712
|
+
return event.idempotencyKey.startsWith("parent-integration:")
|
|
713
|
+
|| event.idempotencyKey.startsWith(`parent-global-verifier:${event.swarmId}:`)
|
|
714
|
+
|| event.idempotencyKey.startsWith(`parent-outcome:${event.swarmId}:`);
|
|
715
|
+
}
|
|
716
|
+
function sameRecordEvent(event, recordEvent) {
|
|
717
|
+
if (!recordEvent)
|
|
718
|
+
return false;
|
|
719
|
+
const typedRecordEvent = recordEvent;
|
|
720
|
+
const identityMatches = event.type === recordEvent.type
|
|
721
|
+
&& event.swarmId === recordEvent.swarmId
|
|
722
|
+
&& event.timestamp === recordEvent.timestamp
|
|
723
|
+
&& event.parentPolicyVersion === recordEvent.parentPolicyVersion
|
|
724
|
+
&& event.taskId === recordEvent.taskId
|
|
725
|
+
&& event.agentId === recordEvent.agentId
|
|
726
|
+
&& event.childRunId === recordEvent.childRunId
|
|
727
|
+
&& event.failureClass === typedRecordEvent.failureClass;
|
|
728
|
+
return identityMatches
|
|
729
|
+
&& JSON.stringify(event.payload) === JSON.stringify(recordEvent.payload);
|
|
730
|
+
}
|
|
731
|
+
function canonicalUndurableBootstrapOffset(record, durablePrefix) {
|
|
732
|
+
const bootstrap = record.events[0];
|
|
733
|
+
if (durablePrefix.length === 0
|
|
734
|
+
|| !bootstrap
|
|
735
|
+
|| bootstrap.type !== "SWARM_CREATED"
|
|
736
|
+
|| bootstrap.swarmId !== record.swarmId
|
|
737
|
+
|| bootstrap.parentPolicyVersion !== record.parentContract.policyVersion
|
|
738
|
+
|| JSON.stringify(bootstrap.payload) !== "{}"
|
|
739
|
+
|| bootstrap.taskId !== undefined
|
|
740
|
+
|| bootstrap.agentId !== undefined
|
|
741
|
+
|| bootstrap.childRunId !== undefined
|
|
742
|
+
|| Date.parse(bootstrap.timestamp) > Date.parse(durablePrefix[0].timestamp)
|
|
743
|
+
|| !sameRecordEvent(durablePrefix[0], record.events[1]))
|
|
744
|
+
return 0;
|
|
745
|
+
return 1;
|
|
746
|
+
}
|
|
747
|
+
function recordEventInput(event, idempotencyKey) {
|
|
748
|
+
const typedEvent = event;
|
|
749
|
+
return {
|
|
750
|
+
idempotencyKey,
|
|
751
|
+
type: event.type,
|
|
752
|
+
timestamp: event.timestamp,
|
|
753
|
+
...(event.taskId === undefined ? {} : { taskId: event.taskId }),
|
|
754
|
+
...(event.agentId === undefined ? {} : { agentId: event.agentId }),
|
|
755
|
+
...(event.childRunId === undefined ? {} : { childRunId: event.childRunId }),
|
|
756
|
+
...(typedEvent.failureClass === undefined ? {} : { failureClass: typedEvent.failureClass }),
|
|
757
|
+
payload: clone(event.payload)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function assertAuthorizedParentEventSequence(result, events, terminalType) {
|
|
761
|
+
let phase = "integration";
|
|
762
|
+
const admittedCandidateIds = [];
|
|
763
|
+
const seenCandidateIds = new Set();
|
|
764
|
+
let integrationCompleted = false;
|
|
765
|
+
for (const event of events) {
|
|
766
|
+
if (event.swarmId !== result.record.swarmId
|
|
767
|
+
|| event.parentPolicyVersion !== result.record.parentContract.policyVersion) {
|
|
768
|
+
throw parentPipelineEventMismatch("Parent event identity does not match the authorized record.");
|
|
769
|
+
}
|
|
770
|
+
if (isParentIntegrationEvent(event)) {
|
|
771
|
+
if (phase !== "integration") {
|
|
772
|
+
throw parentPipelineEventMismatch("Integration events must precede verifier and terminal events.");
|
|
773
|
+
}
|
|
774
|
+
if (event.type === "INTEGRATION_COMPLETED") {
|
|
775
|
+
if (integrationCompleted) {
|
|
776
|
+
throw parentPipelineEventMismatch("Integration completion may be persisted only once.");
|
|
777
|
+
}
|
|
778
|
+
integrationCompleted = true;
|
|
779
|
+
}
|
|
780
|
+
assertIntegrationEventBinding(result, event, admittedCandidateIds, seenCandidateIds);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (event.type === "GLOBAL_VERIFIER_PASSED" || event.type === "GLOBAL_VERIFIER_FAILED") {
|
|
784
|
+
if (phase !== "integration") {
|
|
785
|
+
throw parentPipelineEventMismatch("The parent result contains a repeated or reordered verifier event.");
|
|
786
|
+
}
|
|
787
|
+
phase = "verifier";
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (isParentRecordTerminalEvent(event)) {
|
|
791
|
+
if (phase === "terminal" || event.type !== terminalType || event !== events.at(-1)) {
|
|
792
|
+
throw parentPipelineEventMismatch("The parent terminal event is missing, contradictory, or reordered.");
|
|
793
|
+
}
|
|
794
|
+
phase = "terminal";
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
throw parentPipelineEventMismatch("The parent result contains an event outside the closed integration sequence.");
|
|
798
|
+
}
|
|
799
|
+
if (JSON.stringify(admittedCandidateIds) !== JSON.stringify(result.integration.admittedCandidateIds)) {
|
|
800
|
+
throw parentPipelineEventMismatch("Admitted integration events do not match the authorized candidate order.");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function assertIntegrationEventBinding(result, event, admittedCandidateIds, seenCandidateIds) {
|
|
804
|
+
if (event.type === "INTEGRATION_COMPLETED") {
|
|
805
|
+
if (!result.integration.completed
|
|
806
|
+
|| (event.payload.integratedTreeHash !== result.integration.finalTreeHash
|
|
807
|
+
&& event.payload.finalTreeHash !== result.integration.finalTreeHash)) {
|
|
808
|
+
throw parentPipelineEventMismatch("Integration completion does not bind the authorized final tree.");
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const candidateId = event.payload.candidateId;
|
|
813
|
+
if (typeof candidateId !== "string" || candidateId.trim().length === 0 || seenCandidateIds.has(candidateId)) {
|
|
814
|
+
throw parentPipelineEventMismatch("Integration candidate identity is missing or replayed.");
|
|
815
|
+
}
|
|
816
|
+
seenCandidateIds.add(candidateId);
|
|
817
|
+
const task = result.record.tasks.find((entry) => entry.taskId === event.taskId);
|
|
818
|
+
const agent = result.record.agents.find((entry) => entry.agentId === event.agentId);
|
|
819
|
+
if (!task || !agent || !event.childRunId || agent.childRunId !== event.childRunId) {
|
|
820
|
+
throw parentPipelineEventMismatch("Integration event task, agent, or child identity is not in the authorized record.");
|
|
821
|
+
}
|
|
822
|
+
if (event.type === "CHILD_PATCH_ADMITTED") {
|
|
823
|
+
if (!result.integration.admittedCandidateIds.includes(candidateId)) {
|
|
824
|
+
throw parentPipelineEventMismatch("Admitted integration event is absent from the authorized candidate set.");
|
|
825
|
+
}
|
|
826
|
+
admittedCandidateIds.push(candidateId);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (result.integration.admittedCandidateIds.includes(candidateId)) {
|
|
830
|
+
throw parentPipelineEventMismatch("A candidate cannot be both admitted and rejected by the parent result.");
|
|
831
|
+
}
|
|
832
|
+
if (event.type === "INTEGRATION_CONFLICT" && !result.record.conflicts.some((conflict) => (conflict.candidateId === candidateId
|
|
833
|
+
&& conflict.agentId === event.agentId
|
|
834
|
+
&& conflict.childRunId === event.childRunId
|
|
835
|
+
&& conflict.taskIds.includes(event.taskId ?? "")))) {
|
|
836
|
+
throw parentPipelineEventMismatch("Integration conflict does not match the authorized conflict record.");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function parentPipelineEventMismatch(message) {
|
|
840
|
+
return new SwarmLiveStoreError("PARENT_PIPELINE_EVENT_MISMATCH", message);
|
|
841
|
+
}
|
|
842
|
+
function textPayload(value, fallback) {
|
|
843
|
+
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
844
|
+
}
|
|
845
|
+
function isParentTerminalEvent(event) {
|
|
846
|
+
return event.type === "SWARM_STOPPED"
|
|
847
|
+
|| event.type === "SWARM_NEEDS_REVIEW"
|
|
848
|
+
|| event.type === "SWARM_VERIFIED";
|
|
849
|
+
}
|
|
850
|
+
function abortError(reason) {
|
|
851
|
+
const error = new Error(reason instanceof Error ? reason.message : "Live swarm revision wait was aborted.");
|
|
852
|
+
error.name = "AbortError";
|
|
853
|
+
return error;
|
|
854
|
+
}
|
|
855
|
+
function isNodeError(error, code) {
|
|
856
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
857
|
+
}
|
|
858
|
+
function clone(value) {
|
|
859
|
+
return structuredClone(value);
|
|
860
|
+
}
|