@tangle-network/agent-runtime 0.197.1 → 0.198.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/{improve-DZs0KXhK.d.ts → activation-DFRTurvU.d.ts} +99 -5
- package/dist/{activation-BxMZybuo.js → activation-IBEVN3VI.js} +2 -2
- package/dist/{activation-BxMZybuo.js.map → activation-IBEVN3VI.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/candidate-execution/index.d.ts +370 -2
- package/dist/candidate-execution/index.js +828 -4
- package/dist/candidate-execution/index.js.map +1 -0
- package/dist/{coordination-driver-zkCrfEbS.js → coordination-driver-NFvZ5ofi.js} +57 -26
- package/dist/coordination-driver-NFvZ5ofi.js.map +1 -0
- package/dist/{delegate-DHeUYU5E.js → delegate-dN5yooEj.js} +2 -2
- package/dist/{delegate-DHeUYU5E.js.map → delegate-dN5yooEj.js.map} +1 -1
- package/dist/durable.d.ts +2 -2
- package/dist/durable.js +4 -5
- package/dist/durable.js.map +1 -1
- package/dist/{graph-51SJOEcw.js → graph-CgCVtMuz.js} +3 -3
- package/dist/{graph-51SJOEcw.js.map → graph-CgCVtMuz.js.map} +1 -1
- package/dist/{improvement-cycle-Tw5nYT5R.js → improvement-cycle-Cfu6kDOs.js} +8 -10
- package/dist/{improvement-cycle-Tw5nYT5R.js.map → improvement-cycle-Cfu6kDOs.js.map} +1 -1
- package/dist/{index-CYOJsxSg.d.ts → index-CMTUgh-T.d.ts} +9 -11
- package/dist/index.d.ts +694 -16
- package/dist/index.js +1780 -17
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +3 -7
- package/dist/intelligence.js +5 -6
- package/dist/intelligence.js.map +1 -1
- package/dist/kernel.d.ts +2 -5
- package/dist/kernel.js +8 -10
- package/dist/{loop-runner-bin-DwrSB04m.d.ts → loop-runner-bin-CAf1OQot.d.ts} +3 -3
- package/dist/{loop-runner-bin-2LQSjRTB.js → loop-runner-bin-WQniYJ8C.js} +3 -3
- package/dist/{loop-runner-bin-2LQSjRTB.js.map → loop-runner-bin-WQniYJ8C.js.map} +1 -1
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +2 -3
- package/dist/mcp/index.js +4 -5
- package/dist/mcp/index.js.map +1 -1
- package/dist/{prepare-DDGp0-rW.js → prepare-CAO1yXov.js} +2407 -2407
- package/dist/prepare-CAO1yXov.js.map +1 -0
- package/dist/{protected-model-port-DxFN8DLS.js → protected-model-port-B5avcRiQ.js} +2 -2
- package/dist/{protected-model-port-DxFN8DLS.js.map → protected-model-port-B5avcRiQ.js.map} +1 -1
- package/dist/{provision-supervisor-CpMMShE_.js → provision-supervisor-BxsIaJ35.js} +3 -6
- package/dist/{provision-supervisor-CpMMShE_.js.map → provision-supervisor-BxsIaJ35.js.map} +1 -1
- package/dist/{redact-Cbl2O-4N.js → redact-DqfB7oB4.js} +5233 -2526
- package/dist/redact-DqfB7oB4.js.map +1 -0
- package/dist/{runtime-BSFz2z7h.js → runtime-CHEtvaTY.js} +427 -23
- package/dist/runtime-CHEtvaTY.js.map +1 -0
- package/dist/{server-COXa19sp.js → server-ccGua5tH.js} +4 -4
- package/dist/{server-COXa19sp.js.map → server-ccGua5tH.js.map} +1 -1
- package/dist/{types-DFLZMaeh.d.ts → stream-agent-turn-CLOQr497.d.ts} +1986 -6
- package/dist/{structural-rollout-DPbZWgEm.js → structural-rollout-BmDuXyR9.js} +1101 -8
- package/dist/structural-rollout-BmDuXyR9.js.map +1 -0
- package/dist/{supervise-b93YUaka.js → supervise-DVt8-TI-.js} +9 -5
- package/dist/supervise-DVt8-TI-.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +13 -13
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +1 -1
- package/dist/{workspace-archive-Ybomp7AN.js → workspace-archive-BMOnloFf.js} +3 -3
- package/dist/{workspace-archive-Ybomp7AN.js.map → workspace-archive-BMOnloFf.js.map} +1 -1
- package/package.json +1 -28
- package/dist/activation-DyWB0K6E.d.ts +0 -98
- package/dist/authored-code-URmkdgjv.js +0 -37
- package/dist/authored-code-URmkdgjv.js.map +0 -1
- package/dist/candidate-execution-nvqVIMyS.js +0 -829
- package/dist/candidate-execution-nvqVIMyS.js.map +0 -1
- package/dist/conversation-BxJ0SIBM.js +0 -1363
- package/dist/conversation-BxJ0SIBM.js.map +0 -1
- package/dist/conversation.d.ts +0 -2
- package/dist/conversation.js +0 -2
- package/dist/coordination-driver-zkCrfEbS.js.map +0 -1
- package/dist/environment-provider-1fKZh2zl.js +0 -2281
- package/dist/environment-provider-1fKZh2zl.js.map +0 -1
- package/dist/environment-provider-B-I2jlQy.d.ts +0 -143
- package/dist/environment-provider.d.ts +0 -2
- package/dist/environment-provider.js +0 -2
- package/dist/graph.d.ts +0 -753
- package/dist/graph.js +0 -2111
- package/dist/graph.js.map +0 -1
- package/dist/index-CUosKU4N.d.ts +0 -372
- package/dist/index-D9mb6fn2.d.ts +0 -691
- package/dist/index-ZnxSe6iK.d.ts +0 -138
- package/dist/jsonl-file-BEpaEYjT.js +0 -141
- package/dist/jsonl-file-BEpaEYjT.js.map +0 -1
- package/dist/knowledge-B_MsOtDG.js +0 -428
- package/dist/knowledge-B_MsOtDG.js.map +0 -1
- package/dist/knowledge.d.ts +0 -2
- package/dist/knowledge.js +0 -2
- package/dist/materialization-Cy0oM8tb.js +0 -672
- package/dist/materialization-Cy0oM8tb.js.map +0 -1
- package/dist/prepare-DDGp0-rW.js.map +0 -1
- package/dist/primeintellect/index.d.ts +0 -218
- package/dist/primeintellect/index.js +0 -739
- package/dist/primeintellect/index.js.map +0 -1
- package/dist/redact-Cbl2O-4N.js.map +0 -1
- package/dist/runtime-0xNaV6TJ.d.ts +0 -1699
- package/dist/runtime-BSFz2z7h.js.map +0 -1
- package/dist/stream-agent-turn-Dt5mZpc3.js +0 -1103
- package/dist/stream-agent-turn-Dt5mZpc3.js.map +0 -1
- package/dist/stream-agent-turn-urHpmO_Z.d.ts +0 -160
- package/dist/structural-rollout-DPbZWgEm.js.map +0 -1
- package/dist/supervise-b93YUaka.js.map +0 -1
|
@@ -1,5 +1,829 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as
|
|
3
|
-
import {
|
|
4
|
-
import { n as exactProcessProviderAsCandidateExecutor, t as createProtectedAgentCandidateModelPort } from "../protected-model-port-
|
|
1
|
+
import { a as canonicalCandidateBytes, l as embeddedCandidateArtifact } from "../protected-redaction-wGo44k2K.js";
|
|
2
|
+
import { $ as assertTerminalMatchesStaged, A as omitUndefinedObjectFields, C as agentCandidateProfileAsAgentProfile, G as InMemoryAgentCandidateExecutionClaimStore, H as scanMaterializedWorkspaceManifest, J as candidateCleanupTimeout, K as candidateClaimFileInternals, M as parseExactAgentProfile, N as parseExactAgentProfileDiff, O as freezeGenericAgentCandidateProfile, P as parseExactCandidateProfile, Q as assertTerminalMatchesClaim, T as assertCandidateProfileBinding, V as readVerifiedArtifact, W as verifyMaterializedWorkspace, X as assertRecoveryMatchesStaged, Y as withinCandidateCleanupDeadline, Z as assertTerminalAllowedInPhase, _ as beginPreparedCandidateDisposal, a as AGENT_CANDIDATE_EXECUTION_SUPPORT, at as terminalRecord, b as CANDIDATE_TRACE_TAGS, c as executePreparedAgentCandidate, d as persistCandidateOutputArtifact, et as recoveredTerminalRecord, f as sealAgentCandidateModelSettlement, g as assertPreparedCandidateIntegrity, h as candidateExecutionClaim, i as candidateKnowledgeExecutionPaths, it as sealTerminalDigest, l as RecoveryAgentCandidateTraceStore, m as sealAgentCandidateExecutorStopAcknowledgement, n as CANDIDATE_KNOWLEDGE_RETRIEVAL_CONFIG_ENV, nt as rejectedStage, q as candidateCleanupDeadline, r as CANDIDATE_KNOWLEDGE_ROOT_ENV, rt as requireStagedTerminal, s as verifyAgentCandidateBundle, t as prepareAgentCandidateExecution, tt as rejectedFinish, u as persistCandidateModelSettlementEvidence, v as consumePreparedCandidateExecution, w as applyExactAgentProfileDiff, y as CANDIDATE_TRACE_ENV, z as candidateWorkspaceManifest } from "../prepare-CAO1yXov.js";
|
|
3
|
+
import { i as sealAgentCandidateBundle, n as captureAgentCandidateWorkspaceFiles, r as createAgentCandidateWorkspacePort, t as captureAgentCandidateWorkspace } from "../workspace-archive-BMOnloFf.js";
|
|
4
|
+
import { n as exactProcessProviderAsCandidateExecutor, t as createProtectedAgentCandidateModelPort } from "../protected-model-port-B5avcRiQ.js";
|
|
5
|
+
import { agentCandidateExecutionPlanMaterialSchema, agentCandidateMaterializationReceiptSchema } from "@tangle-network/agent-interface";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { constants, linkSync } from "node:fs";
|
|
8
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { cp, lstat, mkdir, open, readdir, readlink, realpath, unlink } from "node:fs/promises";
|
|
10
|
+
import { verifyCodeSurface } from "@tangle-network/agent-eval/campaign";
|
|
11
|
+
//#region src/candidate-execution/builder.ts
|
|
12
|
+
/**
|
|
13
|
+
* Compile one measured profile/code candidate into the immutable execution
|
|
14
|
+
* contract. Code bytes are re-read and verified by agent-eval before they are
|
|
15
|
+
* embedded. The returned bundle is schema-validated, canonically digested, and
|
|
16
|
+
* deeply immutable; call `verifyAgentCandidateBundle` at the execution boundary
|
|
17
|
+
* to re-read external memory, repository, and workspace artifacts.
|
|
18
|
+
*/
|
|
19
|
+
function buildAgentCandidateBundle(input) {
|
|
20
|
+
return sealAgentCandidateBundle({
|
|
21
|
+
kind: "agent-candidate-bundle",
|
|
22
|
+
digestAlgorithm: "rfc8785-sha256",
|
|
23
|
+
profile: compileCandidateProfile(input.profile),
|
|
24
|
+
code: compileCandidateCode(input.code),
|
|
25
|
+
execution: input.execution,
|
|
26
|
+
...input.knowledge !== void 0 ? { knowledge: input.knowledge } : {},
|
|
27
|
+
memory: input.memory
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function compileCandidateProfile(source) {
|
|
31
|
+
if (source.kind === "candidate-profile") return parseExactCandidateProfile(source.profile);
|
|
32
|
+
if (source.kind === "profile") return freezeGenericAgentCandidateProfile(source.profile);
|
|
33
|
+
if (source.kind !== "profile-diffs") throw new Error(`unsupported candidate profile source: ${String(source.kind)}`);
|
|
34
|
+
if (source.diffs.length === 0) throw new Error("profile-diffs source requires at least one AgentProfileDiff");
|
|
35
|
+
let profile = parseExactAgentProfile(source.base, "base profile");
|
|
36
|
+
for (const [index, inputDiff] of source.diffs.entries()) {
|
|
37
|
+
const diff = parseExactAgentProfileDiff(inputDiff, `profile diff ${index}`);
|
|
38
|
+
profile = applyExactAgentProfileDiff(profile, diff, `profile diff ${index}`);
|
|
39
|
+
}
|
|
40
|
+
return freezeGenericAgentCandidateProfile(profile);
|
|
41
|
+
}
|
|
42
|
+
function compileCandidateCode(source) {
|
|
43
|
+
if (source.kind === "disabled" || source.kind === "no-op") return source;
|
|
44
|
+
if (source.kind !== "code-surface") throw new Error(`unsupported candidate code source: ${String(source.kind)}`);
|
|
45
|
+
const patch = embeddedCandidateArtifact(verifyCodeSurface(source.surface, source.worktreeDir).patchBytes);
|
|
46
|
+
if (patch.sha256 !== source.surface.patch.sha256 || patch.byteLength !== source.surface.patch.byteLength) throw new Error("verified CodeSurface patch bytes do not match its content identity");
|
|
47
|
+
return {
|
|
48
|
+
kind: "git-patch",
|
|
49
|
+
repository: source.repository,
|
|
50
|
+
baseCommit: source.surface.baseCommit,
|
|
51
|
+
baseTree: source.surface.baseTree,
|
|
52
|
+
candidateTree: source.surface.candidateTree,
|
|
53
|
+
patch: {
|
|
54
|
+
format: "git-diff-binary",
|
|
55
|
+
artifact: patch
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/candidate-execution/claim-file-store.ts
|
|
61
|
+
const { assertExpiredLease, assertLease, assertSameSlot, assertUnexpiredLease, attemptRecord, claimSlot, leaseDigest, newLease, readClaim, readClaimIfPresent, readTerminal, readTerminalIfPresent, readTransitionIfPresent, rejectedExistingClaim, rejectedRetry, retryRejection, sealAttemptRef, sealClaim, sealLease } = candidateClaimFileInternals;
|
|
62
|
+
/** Cross-process lifecycle implemented as fsynced, create-if-absent records. */
|
|
63
|
+
var FileAgentCandidateExecutionClaimStore = class {
|
|
64
|
+
directory;
|
|
65
|
+
now;
|
|
66
|
+
constructor(options) {
|
|
67
|
+
if (options.directory.length === 0) throw new Error("candidate execution claim directory must not be empty");
|
|
68
|
+
this.directory = options.directory;
|
|
69
|
+
this.now = options.now ?? Date.now;
|
|
70
|
+
}
|
|
71
|
+
async tryClaim(requested) {
|
|
72
|
+
const claim = sealClaim(requested);
|
|
73
|
+
await mkdir(this.directory, { recursive: true });
|
|
74
|
+
const claimPath = this.claimPath(claim);
|
|
75
|
+
const existing = await readClaimIfPresent(claimPath);
|
|
76
|
+
if (existing) {
|
|
77
|
+
assertSameSlot(existing.claim, claim, claimPath);
|
|
78
|
+
return rejectedExistingClaim(existing.claim, claim);
|
|
79
|
+
}
|
|
80
|
+
assertUnexpiredLease(claim.leaseExpiresAtMs, this.now());
|
|
81
|
+
const retryFailure = await this.retryFailure(claim);
|
|
82
|
+
if (retryFailure) return rejectedRetry(claim, retryFailure);
|
|
83
|
+
const lease = newLease(claim);
|
|
84
|
+
if (await writeRecordIfAbsent(this.directory, claimPath, {
|
|
85
|
+
...claim,
|
|
86
|
+
phase: "claimed",
|
|
87
|
+
leaseDigest: leaseDigest(lease)
|
|
88
|
+
})) return Object.freeze({
|
|
89
|
+
acquired: true,
|
|
90
|
+
claim,
|
|
91
|
+
lease
|
|
92
|
+
});
|
|
93
|
+
const winner = await readClaim(claimPath);
|
|
94
|
+
assertSameSlot(winner.claim, claim, claimPath);
|
|
95
|
+
return rejectedExistingClaim(winner.claim, claim);
|
|
96
|
+
}
|
|
97
|
+
async getAttempt(requestedAttempt) {
|
|
98
|
+
return await this.storedAttempt(sealAttemptRef(requestedAttempt));
|
|
99
|
+
}
|
|
100
|
+
async markCandidateMayRun(requestedLease) {
|
|
101
|
+
const lease = sealLease(requestedLease);
|
|
102
|
+
const claimPath = this.claimPath(lease);
|
|
103
|
+
const stored = await readClaim(claimPath);
|
|
104
|
+
assertSameSlot(stored.claim, lease, claimPath);
|
|
105
|
+
assertLease(stored.leaseDigest, stored.claim.leaseExpiresAtMs, lease);
|
|
106
|
+
const state = await this.transitionState(stored.claim);
|
|
107
|
+
if (state.phase === "candidate-may-run") return Object.freeze({
|
|
108
|
+
marked: false,
|
|
109
|
+
phase: "candidate-may-run"
|
|
110
|
+
});
|
|
111
|
+
if (state.staged) throw new Error("candidate execution terminal was staged before candidate-may-run phase");
|
|
112
|
+
assertUnexpiredLease(stored.claim.leaseExpiresAtMs, this.now());
|
|
113
|
+
if (await writeRecordIfAbsent(this.directory, this.transitionPath(stored.claim, 1), {
|
|
114
|
+
kind: "candidate-execution-phase",
|
|
115
|
+
executionId: stored.claim.executionId,
|
|
116
|
+
attempt: stored.claim.attempt,
|
|
117
|
+
executionPlanDigest: stored.claim.executionPlanDigest,
|
|
118
|
+
phase: "candidate-may-run"
|
|
119
|
+
}, this.ownerPublication(stored.claim))) return Object.freeze({
|
|
120
|
+
marked: true,
|
|
121
|
+
phase: "candidate-may-run"
|
|
122
|
+
});
|
|
123
|
+
if ((await this.transitionState(stored.claim)).phase === "candidate-may-run") return Object.freeze({
|
|
124
|
+
marked: false,
|
|
125
|
+
phase: "candidate-may-run"
|
|
126
|
+
});
|
|
127
|
+
throw new Error("candidate execution terminal was staged before candidate-may-run phase");
|
|
128
|
+
}
|
|
129
|
+
async stageTerminal(requestedLease, result) {
|
|
130
|
+
const lease = sealLease(requestedLease);
|
|
131
|
+
const claimPath = this.claimPath(lease);
|
|
132
|
+
const stored = await readClaim(claimPath);
|
|
133
|
+
assertSameSlot(stored.claim, lease, claimPath);
|
|
134
|
+
assertLease(stored.leaseDigest, stored.claim.leaseExpiresAtMs, lease);
|
|
135
|
+
const terminal = terminalRecord(stored.claim, result);
|
|
136
|
+
const state = await this.transitionState(stored.claim);
|
|
137
|
+
if (state.staged) return rejectedStage(state.staged, terminal);
|
|
138
|
+
assertTerminalAllowedInPhase(state.phase, terminal);
|
|
139
|
+
assertUnexpiredLease(stored.claim.leaseExpiresAtMs, this.now());
|
|
140
|
+
const transition = state.phase === "claimed" ? 1 : 2;
|
|
141
|
+
if (await writeRecordIfAbsent(this.directory, this.transitionPath(stored.claim, transition), {
|
|
142
|
+
kind: "candidate-execution-pending-terminal",
|
|
143
|
+
terminal
|
|
144
|
+
}, this.ownerPublication(stored.claim))) return Object.freeze({
|
|
145
|
+
staged: true,
|
|
146
|
+
terminal
|
|
147
|
+
});
|
|
148
|
+
const winner = await this.transitionState(stored.claim);
|
|
149
|
+
if (!winner.staged) throw new Error("candidate execution phase transition won while staging terminal");
|
|
150
|
+
return rejectedStage(winner.staged, terminal);
|
|
151
|
+
}
|
|
152
|
+
async finish(requestedLease, requestedTerminalDigest) {
|
|
153
|
+
const lease = sealLease(requestedLease);
|
|
154
|
+
const terminalDigest = sealTerminalDigest(requestedTerminalDigest);
|
|
155
|
+
const claimPath = this.claimPath(lease);
|
|
156
|
+
const stored = await readClaim(claimPath);
|
|
157
|
+
assertSameSlot(stored.claim, lease, claimPath);
|
|
158
|
+
assertLease(stored.leaseDigest, stored.claim.leaseExpiresAtMs, lease);
|
|
159
|
+
const staged = requireStagedTerminal((await this.transitionState(stored.claim)).staged, terminalDigest);
|
|
160
|
+
const terminalPath = this.terminalPath(lease);
|
|
161
|
+
const existing = await readTerminalIfPresent(terminalPath);
|
|
162
|
+
if (existing) {
|
|
163
|
+
assertTerminalMatchesClaim(existing, stored.claim, terminalPath);
|
|
164
|
+
assertTerminalMatchesStaged(existing, staged, terminalPath);
|
|
165
|
+
return rejectedFinish(existing, terminalDigest);
|
|
166
|
+
}
|
|
167
|
+
assertUnexpiredLease(stored.claim.leaseExpiresAtMs, this.now());
|
|
168
|
+
if (await writeRecordIfAbsent(this.directory, terminalPath, { terminal: staged }, this.ownerPublication(stored.claim))) return Object.freeze({
|
|
169
|
+
finished: true,
|
|
170
|
+
terminal: staged
|
|
171
|
+
});
|
|
172
|
+
const winner = await readTerminal(terminalPath);
|
|
173
|
+
assertSameSlot(winner, lease, terminalPath);
|
|
174
|
+
assertTerminalMatchesClaim(winner, stored.claim, terminalPath);
|
|
175
|
+
assertTerminalMatchesStaged(winner, staged, terminalPath);
|
|
176
|
+
return rejectedFinish(winner, terminalDigest);
|
|
177
|
+
}
|
|
178
|
+
async recoverExpired(requestedAttempt, evidence) {
|
|
179
|
+
const attempt = sealAttemptRef(requestedAttempt);
|
|
180
|
+
const record = await this.storedAttempt(attempt);
|
|
181
|
+
if (!record) throw new Error("candidate execution recovery does not name an acquired attempt");
|
|
182
|
+
const recovered = recoveredTerminalRecord(record.claim, record.phase, evidence);
|
|
183
|
+
if (record.staged) assertRecoveryMatchesStaged(record.staged, recovered);
|
|
184
|
+
const requestedDigest = record.staged?.terminalDigest ?? recovered.terminalDigest;
|
|
185
|
+
if (record.terminal) return rejectedFinish(record.terminal, requestedDigest);
|
|
186
|
+
assertExpiredLease(record.claim.leaseExpiresAtMs, this.now());
|
|
187
|
+
let staged = record.staged;
|
|
188
|
+
if (!staged) {
|
|
189
|
+
const transition = record.phase === "claimed" ? 1 : 2;
|
|
190
|
+
if (await writeRecordIfAbsent(this.directory, this.transitionPath(record.claim, transition), {
|
|
191
|
+
kind: "candidate-execution-pending-terminal",
|
|
192
|
+
terminal: recovered
|
|
193
|
+
})) staged = recovered;
|
|
194
|
+
else {
|
|
195
|
+
const winner = await this.transitionState(record.claim);
|
|
196
|
+
if (!winner.staged) throw new Error("candidate execution recovery lost terminal staging to a phase transition");
|
|
197
|
+
assertRecoveryMatchesStaged(winner.staged, recovered);
|
|
198
|
+
staged = winner.staged;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const terminalPath = this.terminalPath(attempt);
|
|
202
|
+
if (await writeRecordIfAbsent(this.directory, terminalPath, { terminal: staged })) return Object.freeze({
|
|
203
|
+
finished: true,
|
|
204
|
+
terminal: staged
|
|
205
|
+
});
|
|
206
|
+
const winner = await readTerminal(terminalPath);
|
|
207
|
+
assertSameSlot(winner, attempt, terminalPath);
|
|
208
|
+
assertTerminalMatchesClaim(winner, record.claim, terminalPath);
|
|
209
|
+
assertTerminalMatchesStaged(winner, staged, terminalPath);
|
|
210
|
+
return rejectedFinish(winner, staged.terminalDigest);
|
|
211
|
+
}
|
|
212
|
+
async storedAttempt(claim) {
|
|
213
|
+
const claimPath = this.claimPath(claim);
|
|
214
|
+
const stored = await readClaimIfPresent(claimPath);
|
|
215
|
+
if (!stored) return void 0;
|
|
216
|
+
assertSameSlot(stored.claim, claim, claimPath);
|
|
217
|
+
const terminalPath = this.terminalPath(claim);
|
|
218
|
+
const terminal = await readTerminalIfPresent(terminalPath);
|
|
219
|
+
const state = await this.transitionState(stored.claim);
|
|
220
|
+
if (terminal) {
|
|
221
|
+
assertTerminalMatchesClaim(terminal, stored.claim, terminalPath);
|
|
222
|
+
if (!state.staged) throw new Error(`candidate execution terminal record at ${terminalPath} has no staged outbox`);
|
|
223
|
+
assertTerminalMatchesStaged(terminal, state.staged, terminalPath);
|
|
224
|
+
}
|
|
225
|
+
return attemptRecord(stored.claim, state.phase, state.staged, terminal);
|
|
226
|
+
}
|
|
227
|
+
async transitionState(claim) {
|
|
228
|
+
const firstPath = this.transitionPath(claim, 1);
|
|
229
|
+
const first = await readTransitionIfPresent(firstPath, claim);
|
|
230
|
+
if (!first) return { phase: "claimed" };
|
|
231
|
+
if (first.kind === "pending") return {
|
|
232
|
+
phase: "claimed",
|
|
233
|
+
staged: first.terminal
|
|
234
|
+
};
|
|
235
|
+
const secondPath = this.transitionPath(claim, 2);
|
|
236
|
+
const second = await readTransitionIfPresent(secondPath, claim);
|
|
237
|
+
if (!second) return { phase: "candidate-may-run" };
|
|
238
|
+
if (second.kind !== "pending") throw new Error(`candidate execution transition at ${secondPath} repeats the phase marker`);
|
|
239
|
+
return {
|
|
240
|
+
phase: "candidate-may-run",
|
|
241
|
+
staged: second.terminal
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async retryFailure(claim) {
|
|
245
|
+
if (claim.attempt === 1) return void 0;
|
|
246
|
+
return retryRejection(claim, await this.storedAttempt({
|
|
247
|
+
executionId: claim.executionId,
|
|
248
|
+
attempt: claim.attempt - 1
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
ownerPublication(claim) {
|
|
252
|
+
return { authorizePublish: () => {
|
|
253
|
+
assertUnexpiredLease(claim.leaseExpiresAtMs, this.now());
|
|
254
|
+
return true;
|
|
255
|
+
} };
|
|
256
|
+
}
|
|
257
|
+
claimPath(claim) {
|
|
258
|
+
return join(this.directory, `${claimSlot(claim)}.claim.json`);
|
|
259
|
+
}
|
|
260
|
+
terminalPath(claim) {
|
|
261
|
+
return join(this.directory, `${claimSlot(claim)}.terminal.json`);
|
|
262
|
+
}
|
|
263
|
+
transitionPath(claim, ordinal) {
|
|
264
|
+
return join(this.directory, `${claimSlot(claim)}.transition-${ordinal}.json`);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Candidate records keep this async writer because lease authorization must run after the
|
|
269
|
+
* asynchronous temp-file fsync and immediately before synchronous link publication. The
|
|
270
|
+
* supervisor durability helper is synchronous and has no lease hook, so combining them would
|
|
271
|
+
* either move authorization earlier or add an async race to the claim boundary.
|
|
272
|
+
*/
|
|
273
|
+
async function writeRecordIfAbsent(directory, destination, record, options = {}) {
|
|
274
|
+
const temporaryPath = join(directory, `.candidate-execution-${process.pid}-${randomUUID()}.tmp`);
|
|
275
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
276
|
+
try {
|
|
277
|
+
await handle.writeFile(Buffer.concat([Buffer.from(canonicalCandidateBytes(record)), Buffer.from("\n")]));
|
|
278
|
+
await handle.sync();
|
|
279
|
+
} finally {
|
|
280
|
+
await handle.close();
|
|
281
|
+
}
|
|
282
|
+
let written = false;
|
|
283
|
+
try {
|
|
284
|
+
if (options.authorizePublish && options.authorizePublish() !== true) throw new Error("candidate execution record publication was not authorized");
|
|
285
|
+
linkSync(temporaryPath, destination);
|
|
286
|
+
written = true;
|
|
287
|
+
await syncDirectory(directory);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!isNodeError(error, "EEXIST")) throw error;
|
|
290
|
+
} finally {
|
|
291
|
+
await unlink(temporaryPath).catch((error) => {
|
|
292
|
+
if (!isNodeError(error, "ENOENT")) throw error;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return written;
|
|
296
|
+
}
|
|
297
|
+
async function syncDirectory(directory) {
|
|
298
|
+
const handle = await open(directory, "r");
|
|
299
|
+
try {
|
|
300
|
+
await handle.sync();
|
|
301
|
+
} finally {
|
|
302
|
+
await handle.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function isNodeError(error, code) {
|
|
306
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === code;
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/candidate-execution/dispose.ts
|
|
310
|
+
/** Revoke reservations held by a prepared candidate that will not be executed. */
|
|
311
|
+
async function disposePreparedAgentCandidateExecution(prepared, options = {}) {
|
|
312
|
+
const initialState = assertPreparedCandidateIntegrity(prepared);
|
|
313
|
+
const cleanupTimeoutMs = options.cleanupTimeoutMs ?? initialState.cleanupTimeoutMs;
|
|
314
|
+
if (cleanupTimeoutMs > initialState.cleanupTimeoutMs) throw new Error("disposal cleanup timeout exceeds the frozen preparation bound");
|
|
315
|
+
const cleanupDeadlineAtMs = candidateCleanupDeadline(cleanupTimeoutMs);
|
|
316
|
+
const state = beginPreparedCandidateDisposal(prepared);
|
|
317
|
+
const cleanup = [];
|
|
318
|
+
if (state.memory.mode === "isolated") {
|
|
319
|
+
const reservation = state.memoryReservation;
|
|
320
|
+
if (!reservation) throw new Error("isolated memory reservation is missing");
|
|
321
|
+
cleanup.push(withinCandidateCleanupDeadline(async () => {
|
|
322
|
+
const closed = await state.ports.memory.close({
|
|
323
|
+
executionId: state.executionId,
|
|
324
|
+
preparationId: reservation.preparationId,
|
|
325
|
+
accessDigest: reservation.accessDigest,
|
|
326
|
+
effectiveNamespace: reservation.effectiveNamespace,
|
|
327
|
+
reason: "abandoned"
|
|
328
|
+
});
|
|
329
|
+
if (closed.closed !== true || Object.keys(closed).some((key) => key !== "closed")) throw new Error("abandoned isolated memory access did not acknowledge closure");
|
|
330
|
+
}, cleanupDeadlineAtMs, "isolated memory disposal"));
|
|
331
|
+
}
|
|
332
|
+
cleanup.push(withinCandidateCleanupDeadline(async () => {
|
|
333
|
+
if (sealAgentCandidateModelSettlement(await state.ports.models.settleGrant({
|
|
334
|
+
executionId: state.executionId,
|
|
335
|
+
preparationId: state.preparationId,
|
|
336
|
+
grantDigest: state.modelReservation.digest,
|
|
337
|
+
resolved: state.resolvedModel,
|
|
338
|
+
reason: "abandoned"
|
|
339
|
+
}), {
|
|
340
|
+
preparationId: state.preparationId,
|
|
341
|
+
grantDigest: state.modelReservation.digest,
|
|
342
|
+
model: state.resolvedModel.model
|
|
343
|
+
}).usage.modelCalls !== 0) throw new Error("unexecuted model reservation unexpectedly contains calls");
|
|
344
|
+
}, cleanupDeadlineAtMs, "model reservation disposal"));
|
|
345
|
+
const failures = (await Promise.allSettled(cleanup)).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
346
|
+
if (failures.length > 0) {
|
|
347
|
+
consumePreparedCandidateExecution(prepared, "disposal-failed");
|
|
348
|
+
throw new AggregateError(failures, "candidate preparation disposal failed");
|
|
349
|
+
}
|
|
350
|
+
consumePreparedCandidateExecution(prepared, "disposed");
|
|
351
|
+
return Object.freeze({ disposed: true });
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/candidate-execution/protected-model-grant.ts
|
|
355
|
+
const SETTLEMENT_RETRY_INITIAL_DELAY_MS = 25;
|
|
356
|
+
const SETTLEMENT_RETRY_MAX_DELAY_MS = 1e3;
|
|
357
|
+
/**
|
|
358
|
+
* Run one bounded unit under a protected model grant.
|
|
359
|
+
*
|
|
360
|
+
* Runtime owns the grant lifecycle; callers own the unit boundary and any
|
|
361
|
+
* durable scheduling or accounting around it. A reserved grant is settled
|
|
362
|
+
* after activation failure or callback failure, and the callback error is
|
|
363
|
+
* preserved when settlement also fails.
|
|
364
|
+
*/
|
|
365
|
+
async function runProtectedAgentCandidateModelGrant(options) {
|
|
366
|
+
if (typeof options.execute !== "function") throw new TypeError("protected model grant execute callback is required");
|
|
367
|
+
const resolved = await options.port.resolve(options.resolve);
|
|
368
|
+
const reservation = await options.port.reserveGrant({
|
|
369
|
+
...options.reserve,
|
|
370
|
+
resolved
|
|
371
|
+
});
|
|
372
|
+
let value;
|
|
373
|
+
let executionError;
|
|
374
|
+
let executionFailed = false;
|
|
375
|
+
let activated = false;
|
|
376
|
+
let reason = "preparation-failed";
|
|
377
|
+
try {
|
|
378
|
+
const activation = await options.port.activateGrant({
|
|
379
|
+
executionId: options.reserve.executionId,
|
|
380
|
+
preparationId: options.reserve.preparationId,
|
|
381
|
+
grantDigest: reservation.digest,
|
|
382
|
+
resolved,
|
|
383
|
+
deadlineAtMs: options.deadlineAtMs
|
|
384
|
+
});
|
|
385
|
+
activated = true;
|
|
386
|
+
value = await options.execute({
|
|
387
|
+
activation,
|
|
388
|
+
reservation,
|
|
389
|
+
resolved
|
|
390
|
+
});
|
|
391
|
+
reason = "completed";
|
|
392
|
+
} catch (error) {
|
|
393
|
+
executionError = error;
|
|
394
|
+
executionFailed = true;
|
|
395
|
+
if (activated) reason = "failed";
|
|
396
|
+
}
|
|
397
|
+
let settlement;
|
|
398
|
+
try {
|
|
399
|
+
settlement = await settleProtectedModelGrant(options, {
|
|
400
|
+
executionId: options.reserve.executionId,
|
|
401
|
+
preparationId: options.reserve.preparationId,
|
|
402
|
+
grantDigest: reservation.digest,
|
|
403
|
+
resolved,
|
|
404
|
+
reason
|
|
405
|
+
});
|
|
406
|
+
} catch (settlementError) {
|
|
407
|
+
if (executionFailed) throw new AggregateError([executionError, settlementError], "protected model execution failed and its grant did not settle");
|
|
408
|
+
throw settlementError;
|
|
409
|
+
}
|
|
410
|
+
if (executionFailed) throw executionError;
|
|
411
|
+
return {
|
|
412
|
+
value,
|
|
413
|
+
resolved,
|
|
414
|
+
reservation,
|
|
415
|
+
settlement
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function settleProtectedModelGrant(options, input) {
|
|
419
|
+
let delayMs = SETTLEMENT_RETRY_INITIAL_DELAY_MS;
|
|
420
|
+
for (;;) try {
|
|
421
|
+
return await options.port.settleGrant(input);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (!isCandidateGrantDrainingError(error)) throw error;
|
|
424
|
+
const remainingMs = options.deadlineAtMs - Date.now();
|
|
425
|
+
if (remainingMs <= 0) throw error;
|
|
426
|
+
await waitForSettlementRetry(Math.min(delayMs, remainingMs));
|
|
427
|
+
if (Date.now() >= options.deadlineAtMs) throw error;
|
|
428
|
+
delayMs = Math.min(delayMs * 2, SETTLEMENT_RETRY_MAX_DELAY_MS);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function waitForSettlementRetry(delayMs) {
|
|
432
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
433
|
+
}
|
|
434
|
+
/** Only the gateway's explicit draining state is retryable; auth and ledger errors fail closed. */
|
|
435
|
+
function isCandidateGrantDrainingError(error) {
|
|
436
|
+
if (typeof error !== "object" || error === null) return false;
|
|
437
|
+
const candidate = error;
|
|
438
|
+
if (candidate.code === "candidate_grant_draining") return true;
|
|
439
|
+
return typeof candidate.message === "string" && /\bcandidate_grant_draining\b/u.test(candidate.message);
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/candidate-execution/recover.ts
|
|
443
|
+
/** Close an expired crashed attempt from persisted non-secret handles, then record failure. */
|
|
444
|
+
async function recoverExpiredAgentCandidateExecution(options) {
|
|
445
|
+
const record = await options.claimStore.getAttempt(options.attempt);
|
|
446
|
+
if (!record) throw new Error("candidate execution recovery attempt is missing");
|
|
447
|
+
if (record.terminal) return Object.freeze({
|
|
448
|
+
finished: false,
|
|
449
|
+
terminal: record.terminal,
|
|
450
|
+
exactReplay: true
|
|
451
|
+
});
|
|
452
|
+
const cleanupTimeoutMs = candidateCleanupTimeout(options.cleanupTimeoutMs ?? record.claim.cleanup.cleanupTimeoutMs);
|
|
453
|
+
if (cleanupTimeoutMs > record.claim.cleanup.cleanupTimeoutMs) throw new Error("recovery cleanup timeout exceeds the frozen preparation bound");
|
|
454
|
+
if ((options.now ?? Date.now)() < record.claim.leaseExpiresAtMs) throw new Error("candidate execution lease has not expired");
|
|
455
|
+
const cleanupDeadlineAtMs = candidateCleanupDeadline(cleanupTimeoutMs);
|
|
456
|
+
const recoveryTraceStore = new RecoveryAgentCandidateTraceStore(options.traceStore);
|
|
457
|
+
const preparation = withinCandidateCleanupDeadline(() => readRecoveryPreparation(record.claim, options.outputArtifacts), cleanupDeadlineAtMs, "expired candidate preparation evidence read");
|
|
458
|
+
const processClosure = withinCandidateCleanupDeadline(async (cleanupSignal) => {
|
|
459
|
+
sealAgentCandidateExecutorStopAcknowledgement(await options.executor.stop({
|
|
460
|
+
executionId: record.claim.executionId,
|
|
461
|
+
executionPlanDigest: record.claim.executionPlanDigest
|
|
462
|
+
}, {
|
|
463
|
+
traceStore: recoveryTraceStore,
|
|
464
|
+
reason: "failed",
|
|
465
|
+
signal: cleanupSignal,
|
|
466
|
+
deadlineAtMs: cleanupDeadlineAtMs
|
|
467
|
+
}));
|
|
468
|
+
if (options.executor.dispose) {
|
|
469
|
+
const disposed = await options.executor.dispose({
|
|
470
|
+
executionId: record.claim.executionId,
|
|
471
|
+
executionPlanDigest: record.claim.executionPlanDigest
|
|
472
|
+
}, { signal: cleanupSignal });
|
|
473
|
+
if (disposed.disposed !== true || Object.keys(disposed).some((key) => key !== "disposed")) throw new Error("expired candidate executor did not acknowledge resource disposal");
|
|
474
|
+
}
|
|
475
|
+
return { stopped: true };
|
|
476
|
+
}, cleanupDeadlineAtMs, "expired candidate process termination");
|
|
477
|
+
const modelClosure = withinCandidateCleanupDeadline(async () => sealAgentCandidateModelSettlement(await options.ports.models.settleGrant({
|
|
478
|
+
executionId: record.claim.executionId,
|
|
479
|
+
preparationId: record.claim.cleanup.preparationId,
|
|
480
|
+
grantDigest: record.claim.cleanup.modelGrantDigest,
|
|
481
|
+
resolved: record.claim.cleanup.resolvedModel,
|
|
482
|
+
reason: "failed"
|
|
483
|
+
}), {
|
|
484
|
+
preparationId: record.claim.cleanup.preparationId,
|
|
485
|
+
grantDigest: record.claim.cleanup.modelGrantDigest,
|
|
486
|
+
model: record.claim.cleanup.resolvedModel.model
|
|
487
|
+
}), cleanupDeadlineAtMs, "expired candidate model settlement");
|
|
488
|
+
const memoryClosure = record.claim.cleanup.memory ? withinCandidateCleanupDeadline(async () => {
|
|
489
|
+
const memory = record.claim.cleanup.memory;
|
|
490
|
+
if (!memory) throw new Error("expired candidate memory handle is missing");
|
|
491
|
+
const closed = await options.ports.memory.close({
|
|
492
|
+
executionId: record.claim.executionId,
|
|
493
|
+
preparationId: record.claim.cleanup.preparationId,
|
|
494
|
+
accessDigest: memory.accessDigest,
|
|
495
|
+
effectiveNamespace: memory.effectiveNamespace,
|
|
496
|
+
reason: "failed"
|
|
497
|
+
});
|
|
498
|
+
if (closed.closed !== true || Object.keys(closed).some((key) => key !== "closed")) throw new Error("expired candidate memory did not acknowledge closure");
|
|
499
|
+
return { closed: true };
|
|
500
|
+
}, cleanupDeadlineAtMs, "expired candidate memory closure") : void 0;
|
|
501
|
+
const closureOutcomes = Promise.allSettled([modelClosure, memoryClosure ?? Promise.resolve(void 0)]);
|
|
502
|
+
const [preparationOutcome, processOutcome] = await Promise.allSettled([preparation, processClosure]);
|
|
503
|
+
const failures = [preparationOutcome, processOutcome].filter((outcome) => outcome.status === "rejected").map((outcome) => outcome.reason);
|
|
504
|
+
const [modelOutcome, memoryOutcome] = await closureOutcomes;
|
|
505
|
+
for (const outcome of [modelOutcome, memoryOutcome]) if (outcome.status === "rejected") failures.push(outcome.reason);
|
|
506
|
+
if (failures.length > 0) throw new AggregateError(failures, "expired candidate cleanup could not be proven");
|
|
507
|
+
if (modelOutcome.status !== "fulfilled") throw new Error("expired candidate model settlement was not recovered");
|
|
508
|
+
const model = modelOutcome.value;
|
|
509
|
+
const modelSettlement = await withinCandidateCleanupDeadline(() => persistCandidateModelSettlementEvidence({
|
|
510
|
+
executionId: record.claim.executionId,
|
|
511
|
+
executionPlanDigest: record.claim.executionPlanDigest,
|
|
512
|
+
resolvedModel: record.claim.cleanup.resolvedModel
|
|
513
|
+
}, model, options.outputArtifacts), cleanupDeadlineAtMs, "expired candidate model-settlement persistence");
|
|
514
|
+
const failureClass = record.phase === "claimed" && model.usage.modelCalls === 0 ? "pre-model-infrastructure" : "unknown";
|
|
515
|
+
const failureEvidence = await withinCandidateCleanupDeadline(() => persistCandidateOutputArtifact(options.outputArtifacts, {
|
|
516
|
+
executionId: record.claim.executionId,
|
|
517
|
+
purpose: "failure-evidence",
|
|
518
|
+
bytes: canonicalCandidateBytes({
|
|
519
|
+
kind: "agent-candidate-recovery-failure",
|
|
520
|
+
executionId: record.claim.executionId,
|
|
521
|
+
attempt: record.claim.attempt,
|
|
522
|
+
bundleDigest: record.claim.bundleDigest,
|
|
523
|
+
executionPlanDigest: record.claim.executionPlanDigest,
|
|
524
|
+
materializationReceiptDigest: record.claim.preparationEvidence.materializationReceipt.sha256,
|
|
525
|
+
failureClass,
|
|
526
|
+
processStopped: true,
|
|
527
|
+
modelSettlementDigest: modelSettlement.digest,
|
|
528
|
+
memoryClosed: record.claim.cleanup.memory !== void 0
|
|
529
|
+
})
|
|
530
|
+
}), cleanupDeadlineAtMs, "expired candidate failure-evidence persistence");
|
|
531
|
+
const memory = record.claim.cleanup.memory;
|
|
532
|
+
return await options.claimStore.recoverExpired(options.attempt, {
|
|
533
|
+
failureClass,
|
|
534
|
+
usage: model.usage,
|
|
535
|
+
modelSettlement: modelSettlement.artifact,
|
|
536
|
+
failureEvidence,
|
|
537
|
+
process: {
|
|
538
|
+
stopped: true,
|
|
539
|
+
executionPlanDigest: record.claim.executionPlanDigest
|
|
540
|
+
},
|
|
541
|
+
model: {
|
|
542
|
+
closed: true,
|
|
543
|
+
preparationId: record.claim.cleanup.preparationId,
|
|
544
|
+
grantDigest: record.claim.cleanup.modelGrantDigest
|
|
545
|
+
},
|
|
546
|
+
...memory ? { memory: {
|
|
547
|
+
closed: true,
|
|
548
|
+
preparationId: record.claim.cleanup.preparationId,
|
|
549
|
+
accessDigest: memory.accessDigest,
|
|
550
|
+
effectiveNamespace: memory.effectiveNamespace
|
|
551
|
+
} } : {}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
async function readRecoveryPreparation(claim, artifacts) {
|
|
555
|
+
const [planBytes, receiptBytes] = await Promise.all([readVerifiedArtifact(claim.preparationEvidence.executionPlan, artifacts), readVerifiedArtifact(claim.preparationEvidence.materializationReceipt, artifacts)]);
|
|
556
|
+
const plan = agentCandidateExecutionPlanMaterialSchema.parse(parseCanonicalBytes(planBytes, "plan"));
|
|
557
|
+
if (claim.preparationEvidence.executionPlan.sha256 !== claim.executionPlanDigest) throw new Error("recovery execution plan artifact does not match the claimed digest");
|
|
558
|
+
const receiptMaterial = parseCanonicalBytes(receiptBytes, "materialization receipt");
|
|
559
|
+
const receipt = agentCandidateMaterializationReceiptSchema.parse({
|
|
560
|
+
...receiptMaterial,
|
|
561
|
+
digest: claim.preparationEvidence.materializationReceipt.sha256
|
|
562
|
+
});
|
|
563
|
+
if (receipt.bundleDigest !== claim.bundleDigest || receipt.executionPlan.digest !== claim.executionPlanDigest || plan.runCell.bundleDigest !== claim.bundleDigest || plan.executionId !== claim.executionId) throw new Error("recovery preparation evidence does not match the durable claim");
|
|
564
|
+
return {
|
|
565
|
+
plan,
|
|
566
|
+
receipt
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function parseCanonicalBytes(bytes, label) {
|
|
570
|
+
let parsed;
|
|
571
|
+
try {
|
|
572
|
+
parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw new Error(`candidate recovery ${label} is not JSON`, { cause: error });
|
|
575
|
+
}
|
|
576
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`candidate recovery ${label} must be an object`);
|
|
577
|
+
if (!Buffer.from(canonicalCandidateBytes(parsed)).equals(Buffer.from(bytes))) throw new Error(`candidate recovery ${label} bytes are not canonical`);
|
|
578
|
+
return parsed;
|
|
579
|
+
}
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/candidate-execution/workspace-tree.ts
|
|
582
|
+
/**
|
|
583
|
+
* A content digest over a DIRECTORY TREE, and the per-file seed that fills one.
|
|
584
|
+
*
|
|
585
|
+
* The question this answers is "is this workspace the same workspace" — the question every launch
|
|
586
|
+
* and every close of a branchable run has to answer, about a tree that a live process is still
|
|
587
|
+
* writing to. That is a different question from the one
|
|
588
|
+
* {@link scanMaterializedWorkspaceManifest} answers, and the difference decides the policies here:
|
|
589
|
+
*
|
|
590
|
+
* - A candidate manifest describes ONE EXACT regular-file workspace. It refuses a symbolic link
|
|
591
|
+
* outright, because `AgentCandidateWorkspaceManifestMaterial` has no representation for one, and
|
|
592
|
+
* it refuses an entry that disappears, because a signed manifest describes a settled tree.
|
|
593
|
+
* - A tree descriptor describes what a RUN PRODUCED. A python venv writes
|
|
594
|
+
* `bin/python -> /usr/bin/python3` as a matter of course, and a work directory can lose an entry
|
|
595
|
+
* between two directory reads. Refusing on either aborts the close, and a run that never wrote
|
|
596
|
+
* its digest can never be branched from.
|
|
597
|
+
*
|
|
598
|
+
* So the two policies are the caller's, one for links and one for vanishing entries, and both
|
|
599
|
+
* default to `'refuse'` — the strict reading, correct for an INPUT seed, where a link that leaves
|
|
600
|
+
* the tree means the seed describes bytes it does not contain. `'exclude'` is the reading a close
|
|
601
|
+
* needs: the entry is never followed, it is recorded, and the exclusion itself is hashed, so two
|
|
602
|
+
* trees that differ only in an excluded entry cannot share a digest.
|
|
603
|
+
*
|
|
604
|
+
* NOTHING is held in memory. A file reaches the tree digest as its own streamed sha-256 plus its
|
|
605
|
+
* length, so the largest file in the tree does not decide whether the tree can be described:
|
|
606
|
+
* `readFile` refuses anything above 2 GiB with `ERR_FS_FILE_TOO_LARGE`, and a single multi-gigabyte
|
|
607
|
+
* artifact is exactly what a run leaves behind.
|
|
608
|
+
*
|
|
609
|
+
* @module
|
|
610
|
+
* @experimental
|
|
611
|
+
*/
|
|
612
|
+
const TREE_ALGORITHMS = /* @__PURE__ */ new Set(["tree-v1", "portable-tree-v1"]);
|
|
613
|
+
const ENTRY_POLICIES = /* @__PURE__ */ new Set(["refuse", "exclude"]);
|
|
614
|
+
/**
|
|
615
|
+
* Describe one directory tree by content, streaming every file.
|
|
616
|
+
*
|
|
617
|
+
* The digest covers, for every entry in sorted order: its kind, its tree-relative path, its mode
|
|
618
|
+
* under the selected algorithm, and then — for a file, its length and its own sha-256; for a kept
|
|
619
|
+
* link, its target; for an excluded entry, the reason and the target. A file's content therefore
|
|
620
|
+
* never enters the tree hash directly, which is what removes any in-memory size ceiling: the tree
|
|
621
|
+
* hash consumes 32 bytes per file however large the file is.
|
|
622
|
+
*
|
|
623
|
+
* A hard-linked regular file is described like any other regular file. Its content is what the
|
|
624
|
+
* digest is about, and a package manager that links a store into `node_modules` is ordinary
|
|
625
|
+
* content, not a reason to refuse a workspace.
|
|
626
|
+
*/
|
|
627
|
+
async function describeWorkspaceTree(directory, options = {}) {
|
|
628
|
+
const algorithm = options.algorithm ?? "tree-v1";
|
|
629
|
+
const onEscapingLink = options.onEscapingLink ?? "refuse";
|
|
630
|
+
const onMissingEntry = options.onMissingEntry ?? "refuse";
|
|
631
|
+
if (!TREE_ALGORITHMS.has(algorithm)) throw new Error(`unsupported workspace tree algorithm: ${algorithm}`);
|
|
632
|
+
for (const [name, policy] of [["onEscapingLink", onEscapingLink], ["onMissingEntry", onMissingEntry]]) if (!ENTRY_POLICIES.has(policy)) throw new Error(`unsupported workspace tree ${name} policy: ${policy}`);
|
|
633
|
+
const root = resolve(directory);
|
|
634
|
+
const rootStats = await lstatIfPresent(root);
|
|
635
|
+
if (rootStats === null || !rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error(`workspace tree root must be a real directory: ${directory}`);
|
|
636
|
+
const physicalRoot = await realpath(root);
|
|
637
|
+
const hash = createHash("sha256");
|
|
638
|
+
hash.update(`workspace-tree\0${algorithm}\0`);
|
|
639
|
+
const counts = {
|
|
640
|
+
files: 0,
|
|
641
|
+
directories: 0,
|
|
642
|
+
symlinks: 0,
|
|
643
|
+
bytes: 0
|
|
644
|
+
};
|
|
645
|
+
const excluded = [];
|
|
646
|
+
const exclude = (path, reason, target) => {
|
|
647
|
+
const name = treePath(root, path);
|
|
648
|
+
excluded.push(Object.freeze(target === void 0 ? {
|
|
649
|
+
path: name,
|
|
650
|
+
reason
|
|
651
|
+
} : {
|
|
652
|
+
path: name,
|
|
653
|
+
reason,
|
|
654
|
+
target
|
|
655
|
+
}));
|
|
656
|
+
hash.update(`excluded\0${name}\0${reason}\0${target ?? ""}\0`);
|
|
657
|
+
};
|
|
658
|
+
/** True when the caller asked to continue past a vanished entry, and it was not the root. */
|
|
659
|
+
const recordMissing = (path) => {
|
|
660
|
+
if (onMissingEntry === "refuse" || path === root) return false;
|
|
661
|
+
exclude(path, "entry-disappeared");
|
|
662
|
+
return true;
|
|
663
|
+
};
|
|
664
|
+
const visit = async (path) => {
|
|
665
|
+
const info = await lstatIfPresent(path);
|
|
666
|
+
if (info === null) {
|
|
667
|
+
if (recordMissing(path)) return;
|
|
668
|
+
throw new Error(`workspace tree entry disappeared during the walk: ${treePath(root, path)}`);
|
|
669
|
+
}
|
|
670
|
+
const name = treePath(root, path);
|
|
671
|
+
const mode = entryMode(info.mode, algorithm, info.isDirectory(), info.isSymbolicLink());
|
|
672
|
+
if (info.isDirectory()) {
|
|
673
|
+
let entries;
|
|
674
|
+
try {
|
|
675
|
+
entries = await readdir(path);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (isMissing(error) && recordMissing(path)) return;
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
counts.directories += 1;
|
|
681
|
+
hash.update(`directory\0${name}\0${mode}\0`);
|
|
682
|
+
for (const entry of entries.sort()) await visit(join(path, entry));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (info.isSymbolicLink()) {
|
|
686
|
+
await visitSymbolicLink(path, name, mode);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (!info.isFile()) throw new Error(`workspace tree contains an unsupported entry: ${name}`);
|
|
690
|
+
const read = await digestFile(path);
|
|
691
|
+
if (read === null) {
|
|
692
|
+
if (recordMissing(path)) return;
|
|
693
|
+
throw new Error(`workspace tree entry disappeared during the walk: ${name}`);
|
|
694
|
+
}
|
|
695
|
+
counts.files += 1;
|
|
696
|
+
counts.bytes += read.byteLength;
|
|
697
|
+
hash.update(`file\0${name}\0${mode}\0${read.byteLength}\0${read.sha256}\0`);
|
|
698
|
+
};
|
|
699
|
+
const visitSymbolicLink = async (path, name, mode) => {
|
|
700
|
+
let target;
|
|
701
|
+
try {
|
|
702
|
+
target = await readlink(path);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (isMissing(error) && recordMissing(path)) return;
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
const refuse = (reason) => {
|
|
708
|
+
if (onEscapingLink === "refuse") throw new Error(`workspace tree contains a link that ${reasonText(reason)}: ${name} -> ${target}`);
|
|
709
|
+
exclude(path, reason, target);
|
|
710
|
+
};
|
|
711
|
+
if (isAbsolute(target)) return refuse("absolute-symlink");
|
|
712
|
+
if (!isWithin(root, resolve(dirname(path), target))) return refuse("symlink-escapes-tree");
|
|
713
|
+
let physicalTarget;
|
|
714
|
+
try {
|
|
715
|
+
physicalTarget = await realpath(path);
|
|
716
|
+
} catch {
|
|
717
|
+
return refuse("unresolved-symlink");
|
|
718
|
+
}
|
|
719
|
+
if (!isWithin(physicalRoot, physicalTarget)) return refuse("symlink-resolves-outside-tree");
|
|
720
|
+
counts.symlinks += 1;
|
|
721
|
+
hash.update(`symlink\0${name}\0${mode}\0${target}\0`);
|
|
722
|
+
};
|
|
723
|
+
await visit(root);
|
|
724
|
+
return Object.freeze({
|
|
725
|
+
algorithm,
|
|
726
|
+
digest: `sha256:${hash.digest("hex")}`,
|
|
727
|
+
files: counts.files,
|
|
728
|
+
directories: counts.directories,
|
|
729
|
+
symlinks: counts.symlinks,
|
|
730
|
+
bytes: counts.bytes,
|
|
731
|
+
excluded: Object.freeze(excluded)
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Seed a workspace from a directory, one entry at a time, and return the digest of what was
|
|
736
|
+
* seeded.
|
|
737
|
+
*
|
|
738
|
+
* Nothing is packed: `cp` walks and copies file by file, so a multi-gigabyte seed costs one file
|
|
739
|
+
* handle rather than one archive in memory. Existing destination entries are never overwritten —
|
|
740
|
+
* a seed that could replace a file the workspace already holds would make the resulting tree
|
|
741
|
+
* depend on the order two seeds ran in.
|
|
742
|
+
*
|
|
743
|
+
* Links are copied verbatim, exactly as they were written. Following them would copy bytes from
|
|
744
|
+
* outside the seed into the workspace, which is the same rule {@link describeWorkspaceTree}
|
|
745
|
+
* applies to the digest.
|
|
746
|
+
*/
|
|
747
|
+
async function seedWorkspaceTree(input) {
|
|
748
|
+
const source = resolve(input.source);
|
|
749
|
+
const destination = resolve(input.destination);
|
|
750
|
+
if (source === destination || isWithin(source, destination)) throw new Error("workspace seed must not contain its destination");
|
|
751
|
+
const destinationStats = await lstatIfPresent(destination);
|
|
752
|
+
if (destinationStats === null || !destinationStats.isDirectory()) throw new Error(`workspace seed destination must be an existing directory: ${input.destination}`);
|
|
753
|
+
const descriptor = await describeWorkspaceTree(source, { ...input.algorithm === void 0 ? {} : { algorithm: input.algorithm } });
|
|
754
|
+
for (const name of (await readdir(source)).sort()) {
|
|
755
|
+
if (await lstatIfPresent(join(destination, name)) !== null) throw new Error(`workspace seed would overwrite an existing entry: ${name}`);
|
|
756
|
+
await cp(join(source, name), join(destination, name), {
|
|
757
|
+
recursive: true,
|
|
758
|
+
force: false,
|
|
759
|
+
errorOnExist: true,
|
|
760
|
+
verbatimSymlinks: true
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
return descriptor;
|
|
764
|
+
}
|
|
765
|
+
/** Stream one regular file into its own digest, or `null` when it vanished mid-read. */
|
|
766
|
+
async function digestFile(path) {
|
|
767
|
+
let descriptor;
|
|
768
|
+
try {
|
|
769
|
+
descriptor = await open(path, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0));
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (isMissing(error)) return null;
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const hash = createHash("sha256");
|
|
776
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
777
|
+
let total = 0;
|
|
778
|
+
while (true) {
|
|
779
|
+
const { bytesRead } = await descriptor.read(buffer, 0, buffer.byteLength, null);
|
|
780
|
+
if (bytesRead === 0) break;
|
|
781
|
+
total += bytesRead;
|
|
782
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
sha256: `sha256:${hash.digest("hex")}`,
|
|
786
|
+
byteLength: total
|
|
787
|
+
};
|
|
788
|
+
} finally {
|
|
789
|
+
await descriptor.close();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** Git records exactly two file modes; a portable tree records the same two, plus 755 for a
|
|
793
|
+
* directory and 777 for a link, which is what every filesystem reports for one. */
|
|
794
|
+
function entryMode(mode, algorithm, isDirectory, isSymbolicLink) {
|
|
795
|
+
const bits = Number(mode);
|
|
796
|
+
if (algorithm !== "portable-tree-v1") return String(bits & 511);
|
|
797
|
+
if (isDirectory) return "755";
|
|
798
|
+
if (isSymbolicLink) return "777";
|
|
799
|
+
return (bits & 73) !== 0 ? "755" : "644";
|
|
800
|
+
}
|
|
801
|
+
function reasonText(reason) {
|
|
802
|
+
switch (reason) {
|
|
803
|
+
case "absolute-symlink": return "is absolute";
|
|
804
|
+
case "symlink-escapes-tree": return "escapes its tree";
|
|
805
|
+
case "symlink-resolves-outside-tree": return "resolves outside its tree";
|
|
806
|
+
default: return "does not resolve";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function treePath(root, path) {
|
|
810
|
+
return relative(root, path).split(sep).join("/") || ".";
|
|
811
|
+
}
|
|
812
|
+
function isWithin(root, path) {
|
|
813
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
814
|
+
}
|
|
815
|
+
function isMissing(error) {
|
|
816
|
+
return error?.code === "ENOENT";
|
|
817
|
+
}
|
|
818
|
+
async function lstatIfPresent(path) {
|
|
819
|
+
try {
|
|
820
|
+
return await lstat(path);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (isMissing(error)) return null;
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
5
827
|
export { AGENT_CANDIDATE_EXECUTION_SUPPORT, CANDIDATE_KNOWLEDGE_RETRIEVAL_CONFIG_ENV, CANDIDATE_KNOWLEDGE_ROOT_ENV, CANDIDATE_TRACE_ENV, CANDIDATE_TRACE_TAGS, FileAgentCandidateExecutionClaimStore, InMemoryAgentCandidateExecutionClaimStore, agentCandidateProfileAsAgentProfile, applyExactAgentProfileDiff, assertCandidateProfileBinding, buildAgentCandidateBundle, candidateExecutionClaim, candidateKnowledgeExecutionPaths, candidateWorkspaceManifest, captureAgentCandidateWorkspace, captureAgentCandidateWorkspaceFiles, createAgentCandidateWorkspacePort, createProtectedAgentCandidateModelPort, describeWorkspaceTree, disposePreparedAgentCandidateExecution, exactProcessProviderAsCandidateExecutor, executePreparedAgentCandidate, freezeGenericAgentCandidateProfile, omitUndefinedObjectFields, parseExactAgentProfile, parseExactAgentProfileDiff, parseExactCandidateProfile, persistCandidateOutputArtifact, prepareAgentCandidateExecution, recoverExpiredAgentCandidateExecution, runProtectedAgentCandidateModelGrant, scanMaterializedWorkspaceManifest, sealAgentCandidateBundle, seedWorkspaceTree, verifyAgentCandidateBundle, verifyMaterializedWorkspace };
|
|
828
|
+
|
|
829
|
+
//# sourceMappingURL=index.js.map
|