loopforge 3.9.4 → 4.0.7
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 +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +114 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -182
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +97 -156
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +223 -419
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +26 -38
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +196 -319
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -897
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +166 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +372 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +39 -23
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +368 -565
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +255 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1552 -539
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +42 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +956 -282
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -92
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +707 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +11 -39
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +12 -12
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +20 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +475 -479
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +0 -6
- package/dist/prompt-coverage.d.ts.map +1 -1
- package/dist/prompt-coverage.js +4 -8
- package/dist/prompt-coverage.js.map +1 -1
- package/dist/prompt-freshness.d.ts +6 -17
- package/dist/prompt-freshness.d.ts.map +1 -1
- package/dist/prompt-freshness.js +27 -22
- package/dist/prompt-freshness.js.map +1 -1
- package/dist/prompt-policy.d.ts +14 -7
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +21 -5
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +843 -424
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -102
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +41 -89
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +250 -267
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +51 -160
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +837 -38
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +37 -61
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +413 -392
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +29 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +308 -260
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +145 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +28 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +27 -11
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts.map +1 -1
- package/dist/verification-actions.js +10 -11
- package/dist/verification-actions.js.map +1 -1
- package/dist/verification-gate.d.ts +23 -47
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +202 -423
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +4 -18
- package/package.json +68 -68
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -389
|
@@ -5,17 +5,64 @@
|
|
|
5
5
|
* feedback storage. The committed feedback embeds the decision so replay after
|
|
6
6
|
* a process crash is idempotent.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { deriveObservationDeltas, observationVersionIssue } from "./evidence-provider.js";
|
|
10
|
+
import { EVIDENCE_POSTURE_CAPS, rollbackEvidencePosture, } from "./evidence-posture.js";
|
|
9
11
|
import { RoundCoordinator, } from "./round-coordinator.js";
|
|
10
12
|
import { logEvent } from "./observability.js";
|
|
11
13
|
import { deriveContractId, deriveContractItemIds, isRecord } from "./token-utils.js";
|
|
12
|
-
import {
|
|
14
|
+
import { PROMPT_ARTIFACT_SCHEMA_VERSION, isPromptContextMode, isPromptContextReason, } from "./protocol.js";
|
|
15
|
+
import { promptContextPairIssue } from "./context-boundary.js";
|
|
13
16
|
import { commandConfigHash, getPolicy } from "./policy.js";
|
|
14
17
|
import { policyMetrics } from "./policy-metrics.js";
|
|
15
18
|
import { VaultRoundCommitStore } from "./storage.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
import { transactionIdentityIssue } from "./transaction-identity.js";
|
|
20
|
+
/** v3.9.7: schema 5 adds the transaction before-image, explicit mid-round
|
|
21
|
+
* checkpoints and the redo restore marker. v4.0.3: schema 7 requires the
|
|
22
|
+
* persisted `EvidencePosture` on every result and adds the transient
|
|
23
|
+
* `RetryContext`. v4.0.4: schema 8 embeds the schema-10 PromptArtifact
|
|
24
|
+
* (`fresh_invocation`) and the renamed `SelfEvaluation.agent_stop_reason`.
|
|
25
|
+
* v4.0.6: schema 9 is a BREAKING shape break — the committed evaluation
|
|
26
|
+
* carries a required `outcome`, one `execution_report.claims` channel
|
|
27
|
+
* (`cr-`/`rci-`) and `round_contract.supersedes`; `success`,
|
|
28
|
+
* `agent_stop_reason`, `contract_transition`, `tests_reported`,
|
|
29
|
+
* `criterion_claims` and `contract_item_claims` are gone. Every earlier
|
|
30
|
+
* version is reported as legacy by `legacyTransactionRounds`; there is no
|
|
31
|
+
* compatibility migration. */
|
|
32
|
+
export const ROUND_TRANSACTION_SCHEMA_VERSION = 9;
|
|
33
|
+
/** Maximum distinct mid-round checkpoints one transaction may reference. */
|
|
34
|
+
export const MAX_MID_CHECKPOINTS_PER_TRANSACTION = 32;
|
|
35
|
+
/** The root branch identity for every loop. */
|
|
36
|
+
export function rootBranchId(loopId) {
|
|
37
|
+
return `loop:${loopId}:branch:root`;
|
|
38
|
+
}
|
|
39
|
+
/** v3.9.6: deterministic transaction identity. The logical round identity is
|
|
40
|
+
* still `roundId`; this id prevents a deep redo from replaying an abandoned
|
|
41
|
+
* commit at the same numeric round. */
|
|
42
|
+
export function deriveTransactionId(input) {
|
|
43
|
+
const payload = [
|
|
44
|
+
input.kind,
|
|
45
|
+
input.branchId,
|
|
46
|
+
input.roundId,
|
|
47
|
+
String(input.attempt),
|
|
48
|
+
input.promptHash ?? "",
|
|
49
|
+
input.kind === "rollback" && input.recovery
|
|
50
|
+
? [
|
|
51
|
+
input.recovery.target.checkpoint_id,
|
|
52
|
+
input.recovery.target.content_hash,
|
|
53
|
+
String(input.recovery.target.redo_round),
|
|
54
|
+
input.recovery.reason,
|
|
55
|
+
input.recovery.target.transaction_id,
|
|
56
|
+
(input.recovery.abandonedRounds ?? []).join(","),
|
|
57
|
+
(input.recovery.failedRounds ?? []).join(","),
|
|
58
|
+
[...(input.recovery.observedSkippedFiles ?? [])].sort().join(","),
|
|
59
|
+
[...(input.recovery.reportedSkippedFiles ?? [])].sort().join(","),
|
|
60
|
+
[...(input.recovery.triggerFindings ?? [])].sort().join(","),
|
|
61
|
+
].join("\n")
|
|
62
|
+
: "",
|
|
63
|
+
].join("\n");
|
|
64
|
+
return `tx-${createHash("sha256").update(payload).digest("hex").slice(0, 24)}`;
|
|
65
|
+
}
|
|
19
66
|
/** v3.8: the closed set of machine-observation statuses. */
|
|
20
67
|
const OBSERVATION_STATUSES = [
|
|
21
68
|
"observed", "passed", "failed", "timeout", "unavailable", "error", "aborted",
|
|
@@ -64,16 +111,45 @@ export function makeRoundId(loopId, round) {
|
|
|
64
111
|
}
|
|
65
112
|
return `loop:${loopId}:round:${round}`;
|
|
66
113
|
}
|
|
67
|
-
export function prepareRoundTransaction(loopId, round, beforeEvidence, promptArtifact) {
|
|
114
|
+
export function prepareRoundTransaction(loopId, round, beforeEvidence, promptArtifact, opts = {}) {
|
|
68
115
|
const now = Date.now();
|
|
116
|
+
const roundId = makeRoundId(loopId, round);
|
|
117
|
+
const branchId = opts.branchId ?? rootBranchId(loopId);
|
|
118
|
+
const kind = opts.kind ?? "evaluation";
|
|
119
|
+
const observedGit = beforeEvidence.some((observation) => observation.kind === "git" && observation.status === "observed");
|
|
120
|
+
// A direct library caller may have no checkpoint store attached. The
|
|
121
|
+
// schema-5 invariant still records the explicit machine fact.
|
|
122
|
+
const beforeCheckpointError = opts.beforeCheckpointError ??
|
|
123
|
+
(observedGit && !opts.beforeCheckpoint
|
|
124
|
+
? {
|
|
125
|
+
code: "store_unavailable",
|
|
126
|
+
detail: "no workspace checkpoint store was attached to this prepare",
|
|
127
|
+
at: now,
|
|
128
|
+
}
|
|
129
|
+
: undefined);
|
|
69
130
|
return {
|
|
70
131
|
schemaVersion: ROUND_TRANSACTION_SCHEMA_VERSION,
|
|
71
|
-
|
|
132
|
+
branchId,
|
|
133
|
+
transactionId: deriveTransactionId({
|
|
134
|
+
kind,
|
|
135
|
+
branchId,
|
|
136
|
+
roundId,
|
|
137
|
+
attempt: 1,
|
|
138
|
+
promptHash: promptArtifact?.promptHash,
|
|
139
|
+
}),
|
|
140
|
+
kind,
|
|
141
|
+
roundId,
|
|
72
142
|
loopId,
|
|
73
143
|
round,
|
|
74
144
|
attempt: 1,
|
|
75
145
|
phase: promptArtifact ? "prompted" : "prepared",
|
|
76
146
|
beforeEvidence,
|
|
147
|
+
...(opts.beforeCheckpoint ? { beforeCheckpoint: opts.beforeCheckpoint } : {}),
|
|
148
|
+
...(beforeCheckpointError ? { beforeCheckpointError } : {}),
|
|
149
|
+
...(opts.operatorAcceptance
|
|
150
|
+
? { operatorAcceptance: opts.operatorAcceptance }
|
|
151
|
+
: {}),
|
|
152
|
+
...(opts.compiled ? { compiled: opts.compiled } : {}),
|
|
77
153
|
createdAt: now,
|
|
78
154
|
updatedAt: now,
|
|
79
155
|
promptArtifact,
|
|
@@ -82,7 +158,7 @@ export function prepareRoundTransaction(loopId, round, beforeEvidence, promptArt
|
|
|
82
158
|
/** Attach the next prompt attempt to a rejected logical round without changing
|
|
83
159
|
* its identity or evidence baseline. Evaluation fields belong to the previous
|
|
84
160
|
* attempt and are cleared before the Agent receives the retry prompt. */
|
|
85
|
-
export function prepareRejectedAttempt(rejected, promptArtifact) {
|
|
161
|
+
export function prepareRejectedAttempt(rejected, promptArtifact, compiled, operatorAcceptance) {
|
|
86
162
|
if (rejected.phase !== "rejected") {
|
|
87
163
|
throw new Error(`Cannot retry transaction in phase ${rejected.phase}`);
|
|
88
164
|
}
|
|
@@ -92,13 +168,49 @@ export function prepareRejectedAttempt(rejected, promptArtifact) {
|
|
|
92
168
|
if (promptArtifact.attempt !== rejected.attempt + 1) {
|
|
93
169
|
throw new Error(`Retry prompt attempt mismatch: ${promptArtifact.attempt} !== ${rejected.attempt + 1}`);
|
|
94
170
|
}
|
|
171
|
+
if (rejected.retryContext) {
|
|
172
|
+
if (rejected.retryContext.rejectedAttempt !== rejected.attempt ||
|
|
173
|
+
rejected.retryContext.attempt !== promptArtifact.attempt) {
|
|
174
|
+
throw new Error("Retry context identity does not match the rejected attempt");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const transactionId = deriveTransactionId({
|
|
178
|
+
kind: rejected.kind,
|
|
179
|
+
branchId: rejected.branchId,
|
|
180
|
+
roundId: rejected.roundId,
|
|
181
|
+
attempt: promptArtifact.attempt,
|
|
182
|
+
promptHash: promptArtifact.promptHash,
|
|
183
|
+
});
|
|
184
|
+
// The round-start before-image is the SAME workspace state across attempts;
|
|
185
|
+
// only its owning transaction identity changes. Mid-round savepoints belong
|
|
186
|
+
// to the rejected attempt and are not carried into the retry.
|
|
187
|
+
const beforeCheckpoint = rejected.beforeCheckpoint
|
|
188
|
+
? { ...rejected.beforeCheckpoint, transactionId }
|
|
189
|
+
: undefined;
|
|
95
190
|
return {
|
|
96
191
|
...rejected,
|
|
192
|
+
transactionId,
|
|
193
|
+
...(beforeCheckpoint ? { beforeCheckpoint } : {}),
|
|
194
|
+
...(rejected.beforeCheckpointError
|
|
195
|
+
? { beforeCheckpointError: rejected.beforeCheckpointError }
|
|
196
|
+
: {}),
|
|
197
|
+
...(rejected.restore ? { restore: rejected.restore } : {}),
|
|
198
|
+
...(compiled !== undefined
|
|
199
|
+
? { compiled }
|
|
200
|
+
: rejected.compiled
|
|
201
|
+
? { compiled: rejected.compiled }
|
|
202
|
+
: {}),
|
|
203
|
+
...(operatorAcceptance !== undefined
|
|
204
|
+
? { operatorAcceptance }
|
|
205
|
+
: rejected.operatorAcceptance
|
|
206
|
+
? { operatorAcceptance: rejected.operatorAcceptance }
|
|
207
|
+
: {}),
|
|
97
208
|
attempt: promptArtifact.attempt,
|
|
98
209
|
phase: "prompted",
|
|
99
210
|
afterEvidence: undefined,
|
|
100
211
|
evaluation: undefined,
|
|
101
212
|
result: undefined,
|
|
213
|
+
checkpoints: undefined,
|
|
102
214
|
// The rejected attempt's diagnostics were returned in its receipt; the
|
|
103
215
|
// retry has not submitted a new payload yet.
|
|
104
216
|
normalizationDiagnostics: undefined,
|
|
@@ -107,6 +219,82 @@ export function prepareRejectedAttempt(rejected, promptArtifact) {
|
|
|
107
219
|
updatedAt: Date.now(),
|
|
108
220
|
};
|
|
109
221
|
}
|
|
222
|
+
/** v4.0.5: re-bind a held RETRY prompt to a newly compiled artifact for the
|
|
223
|
+
* SAME logical round and attempt. This is not a new attempt: attempt, round,
|
|
224
|
+
* before evidence, retry facts, restore marker and branch stay byte-identical;
|
|
225
|
+
* only the prompt-derived identity (artifact, prompt hash, transaction id and
|
|
226
|
+
* the owning ids on checkpoints) changes.
|
|
227
|
+
*
|
|
228
|
+
* Context preservation is enforced here, not by convention:
|
|
229
|
+
* - the artifact identity must equal the held identity;
|
|
230
|
+
* - an invocation-recovery recompile must be
|
|
231
|
+
* `fresh_invocation / invocation_recovery`;
|
|
232
|
+
* - every other recompile must keep the held mode and reason exactly.
|
|
233
|
+
*
|
|
234
|
+
* Evaluation/result fields of the rejected attempt are cleared before the
|
|
235
|
+
* Agent receives the new prompt, exactly like `prepareRejectedAttempt`. */
|
|
236
|
+
export function rebindHeldPrompt(held, artifact, compiled, operatorAcceptance) {
|
|
237
|
+
if (held.phase !== "prompted" || !held.promptArtifact) {
|
|
238
|
+
throw new Error(`Cannot recompile a prompt in phase ${held.phase}`);
|
|
239
|
+
}
|
|
240
|
+
if (artifact.roundId !== held.roundId) {
|
|
241
|
+
throw new Error(`Held prompt identity mismatch: ${artifact.roundId} !== ${held.roundId}`);
|
|
242
|
+
}
|
|
243
|
+
if (artifact.round !== held.round || artifact.attempt !== held.attempt) {
|
|
244
|
+
throw new Error("Held prompt round/attempt mismatch: a recompile must keep the same " +
|
|
245
|
+
`round/attempt (${artifact.round}/${artifact.attempt} !== ` +
|
|
246
|
+
`${held.round}/${held.attempt})`);
|
|
247
|
+
}
|
|
248
|
+
const previous = held.promptArtifact;
|
|
249
|
+
if (artifact.contextReason === "invocation_recovery") {
|
|
250
|
+
if (artifact.contextMode !== "fresh_invocation") {
|
|
251
|
+
throw new Error("invocation_recovery requires a fresh_invocation recompile");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (artifact.contextMode !== previous.contextMode ||
|
|
255
|
+
artifact.contextReason !== previous.contextReason) {
|
|
256
|
+
throw new Error("A held-prompt recompile must preserve the context boundary: " +
|
|
257
|
+
`${previous.contextMode}/${previous.contextReason} !== ` +
|
|
258
|
+
`${artifact.contextMode}/${artifact.contextReason}`);
|
|
259
|
+
}
|
|
260
|
+
const transactionId = deriveTransactionId({
|
|
261
|
+
kind: held.kind,
|
|
262
|
+
branchId: held.branchId,
|
|
263
|
+
roundId: held.roundId,
|
|
264
|
+
attempt: artifact.attempt,
|
|
265
|
+
promptHash: artifact.promptHash,
|
|
266
|
+
});
|
|
267
|
+
const beforeCheckpoint = held.beforeCheckpoint
|
|
268
|
+
? { ...held.beforeCheckpoint, transactionId }
|
|
269
|
+
: undefined;
|
|
270
|
+
const checkpoints = held.checkpoints
|
|
271
|
+
? held.checkpoints.map((item) => ({ ...item, transactionId }))
|
|
272
|
+
: undefined;
|
|
273
|
+
return {
|
|
274
|
+
...held,
|
|
275
|
+
transactionId,
|
|
276
|
+
...(beforeCheckpoint ? { beforeCheckpoint } : {}),
|
|
277
|
+
...(checkpoints ? { checkpoints } : {}),
|
|
278
|
+
...(compiled !== undefined
|
|
279
|
+
? { compiled }
|
|
280
|
+
: held.compiled
|
|
281
|
+
? { compiled: held.compiled }
|
|
282
|
+
: {}),
|
|
283
|
+
...(operatorAcceptance !== undefined
|
|
284
|
+
? { operatorAcceptance }
|
|
285
|
+
: held.operatorAcceptance
|
|
286
|
+
? { operatorAcceptance: held.operatorAcceptance }
|
|
287
|
+
: {}),
|
|
288
|
+
phase: "prompted",
|
|
289
|
+
afterEvidence: undefined,
|
|
290
|
+
evaluation: undefined,
|
|
291
|
+
result: undefined,
|
|
292
|
+
normalizationDiagnostics: undefined,
|
|
293
|
+
submittedFields: undefined,
|
|
294
|
+
promptArtifact: artifact,
|
|
295
|
+
updatedAt: Date.now(),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
110
298
|
/** v3.8: Shape check for a persisted contract binding. */
|
|
111
299
|
function isContractBinding(value) {
|
|
112
300
|
if (!isRecord(value))
|
|
@@ -117,10 +305,309 @@ function isContractBinding(value) {
|
|
|
117
305
|
return false;
|
|
118
306
|
return isRecord(value.config_hash_by_command);
|
|
119
307
|
}
|
|
308
|
+
const EVIDENCE_POSTURE_STATUSES = [
|
|
309
|
+
"verified", "insufficient", "contradicted", "unknown", "not_applicable",
|
|
310
|
+
];
|
|
311
|
+
const CONTRACT_ITEM_STATUSES = [
|
|
312
|
+
"pending", "insufficient", "contradicted", "verified",
|
|
313
|
+
];
|
|
314
|
+
const CONTRACT_CLOSURES = [
|
|
315
|
+
"open", "verified", "blocked", "superseded",
|
|
316
|
+
];
|
|
317
|
+
const FILE_RECONCILIATIONS = [
|
|
318
|
+
"match", "mismatch", "unknown", "not_applicable",
|
|
319
|
+
];
|
|
320
|
+
function isNonNegativeInteger(value) {
|
|
321
|
+
return Number.isInteger(value) && value >= 0;
|
|
322
|
+
}
|
|
323
|
+
function isVerificationFlag(value) {
|
|
324
|
+
if (!isRecord(value))
|
|
325
|
+
return false;
|
|
326
|
+
// `severity` and `check` are the structural facts every consumer keys on.
|
|
327
|
+
// `field`/`detail` are present on every runtime-generated flag but are
|
|
328
|
+
// optional here so an older committed flag shape does not make the whole
|
|
329
|
+
// round unreadable.
|
|
330
|
+
return (value.severity === "info" || value.severity === "warn" ||
|
|
331
|
+
value.severity === "error") &&
|
|
332
|
+
typeof value.check === "string" &&
|
|
333
|
+
(value.field === undefined || typeof value.field === "string") &&
|
|
334
|
+
(value.detail === undefined || typeof value.detail === "string") &&
|
|
335
|
+
(value.ref === undefined || typeof value.ref === "string");
|
|
336
|
+
}
|
|
337
|
+
/** v4.0.3: schema 7 structural validation of the persisted evidence posture.
|
|
338
|
+
* A committed round without a well-formed posture is a HARD break — it is not
|
|
339
|
+
* committed history and is reported as legacy, never silently re-derived. */
|
|
340
|
+
export function isEvidencePosture(value) {
|
|
341
|
+
if (!isRecord(value))
|
|
342
|
+
return false;
|
|
343
|
+
if (!Number.isInteger(value.round) || value.round < 1)
|
|
344
|
+
return false;
|
|
345
|
+
if (value.kind !== "evaluation" && value.kind !== "rollback")
|
|
346
|
+
return false;
|
|
347
|
+
if (!EVIDENCE_POSTURE_STATUSES.includes(String(value.status))) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (typeof value.machineBackedSuccess !== "boolean" ||
|
|
351
|
+
typeof value.verificationConfigured !== "boolean")
|
|
352
|
+
return false;
|
|
353
|
+
const workspace = value.workspace;
|
|
354
|
+
if (!isRecord(workspace) ||
|
|
355
|
+
(workspace.status !== "observed" && workspace.status !== "unknown" &&
|
|
356
|
+
workspace.status !== "not_applicable") ||
|
|
357
|
+
!isNonNegativeInteger(workspace.changedPathCount))
|
|
358
|
+
return false;
|
|
359
|
+
if (value.files !== null) {
|
|
360
|
+
const files = value.files;
|
|
361
|
+
if (!isRecord(files) ||
|
|
362
|
+
!FILE_RECONCILIATIONS.includes(String(files.reconciliation)) ||
|
|
363
|
+
!isNonNegativeInteger(files.reported) ||
|
|
364
|
+
!isNonNegativeInteger(files.observed))
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (!Array.isArray(value.providers) ||
|
|
368
|
+
value.providers.length > EVIDENCE_POSTURE_CAPS.providers)
|
|
369
|
+
return false;
|
|
370
|
+
for (const provider of value.providers) {
|
|
371
|
+
if (!isRecord(provider) ||
|
|
372
|
+
typeof provider.providerId !== "string" ||
|
|
373
|
+
(provider.kind !== "git" && provider.kind !== "custom") ||
|
|
374
|
+
!OBSERVATION_STATUSES.includes(String(provider.status))) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (!Array.isArray(value.commands) ||
|
|
379
|
+
value.commands.length > EVIDENCE_POSTURE_CAPS.commands)
|
|
380
|
+
return false;
|
|
381
|
+
for (const command of value.commands) {
|
|
382
|
+
if (!isRecord(command) ||
|
|
383
|
+
typeof command.commandId !== "string" ||
|
|
384
|
+
!OBSERVATION_STATUSES.includes(String(command.status)) ||
|
|
385
|
+
typeof command.required !== "boolean")
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (value.contract !== null) {
|
|
389
|
+
const contract = value.contract;
|
|
390
|
+
if (!isRecord(contract) ||
|
|
391
|
+
typeof contract.id !== "string" ||
|
|
392
|
+
contract.id.length === 0 ||
|
|
393
|
+
!CONTRACT_CLOSURES.includes(String(contract.closure)) ||
|
|
394
|
+
!isNonNegativeInteger(contract.verifiedCount) ||
|
|
395
|
+
!isNonNegativeInteger(contract.insufficientCount) ||
|
|
396
|
+
!isNonNegativeInteger(contract.contradictedCount) ||
|
|
397
|
+
!isNonNegativeInteger(contract.pendingCount) ||
|
|
398
|
+
!Array.isArray(contract.items) ||
|
|
399
|
+
contract.items.length > EVIDENCE_POSTURE_CAPS.contractItems)
|
|
400
|
+
return false;
|
|
401
|
+
for (const item of contract.items) {
|
|
402
|
+
if (!isRecord(item) ||
|
|
403
|
+
typeof item.id !== "string" ||
|
|
404
|
+
!CONTRACT_ITEM_STATUSES.includes(String(item.status)) ||
|
|
405
|
+
!stringArray(item.criterionRefs) ||
|
|
406
|
+
!stringArray(item.reasons))
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!Array.isArray(value.findings) ||
|
|
411
|
+
value.findings.length > EVIDENCE_POSTURE_CAPS.findings)
|
|
412
|
+
return false;
|
|
413
|
+
for (const finding of value.findings) {
|
|
414
|
+
if (!isRecord(finding) ||
|
|
415
|
+
typeof finding.id !== "string" ||
|
|
416
|
+
typeof finding.check !== "string" ||
|
|
417
|
+
(finding.severity !== "info" && finding.severity !== "warn" &&
|
|
418
|
+
finding.severity !== "error") ||
|
|
419
|
+
(finding.ref !== undefined && typeof finding.ref !== "string"))
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const omitted = value.omitted;
|
|
423
|
+
if (!isRecord(omitted) ||
|
|
424
|
+
!isNonNegativeInteger(omitted.providers) ||
|
|
425
|
+
!isNonNegativeInteger(omitted.commands) ||
|
|
426
|
+
!isNonNegativeInteger(omitted.contractItems) ||
|
|
427
|
+
!isNonNegativeInteger(omitted.findings))
|
|
428
|
+
return false;
|
|
429
|
+
if (value.kind === "rollback") {
|
|
430
|
+
// A directive abandons work; it never carries evaluation evidence.
|
|
431
|
+
if (value.status !== "not_applicable" ||
|
|
432
|
+
value.machineBackedSuccess !== false ||
|
|
433
|
+
value.verificationConfigured !== false ||
|
|
434
|
+
workspace.status !== "not_applicable" ||
|
|
435
|
+
workspace.changedPathCount !== 0 ||
|
|
436
|
+
value.files !== null ||
|
|
437
|
+
value.providers.length > 0 ||
|
|
438
|
+
value.commands.length > 0 ||
|
|
439
|
+
value.contract !== null ||
|
|
440
|
+
value.findings.length > 0 ||
|
|
441
|
+
omitted.providers !== 0 ||
|
|
442
|
+
omitted.commands !== 0 ||
|
|
443
|
+
omitted.contractItems !== 0 ||
|
|
444
|
+
omitted.findings !== 0)
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
/** v4.0.3: structural validation of the transient retry context. It is only
|
|
450
|
+
* legal on a rejected attempt or its held retry prompt. */
|
|
451
|
+
export function isRetryContext(value) {
|
|
452
|
+
if (!isRecord(value))
|
|
453
|
+
return false;
|
|
454
|
+
if (!Number.isInteger(value.attempt) || value.attempt < 2)
|
|
455
|
+
return false;
|
|
456
|
+
if (!Number.isInteger(value.rejectedAttempt) ||
|
|
457
|
+
value.rejectedAttempt < 1)
|
|
458
|
+
return false;
|
|
459
|
+
if (value.attempt !== value.rejectedAttempt + 1)
|
|
460
|
+
return false;
|
|
461
|
+
if (typeof value.rejectionNotice !== "string")
|
|
462
|
+
return false;
|
|
463
|
+
if (!Array.isArray(value.verificationFlags) ||
|
|
464
|
+
!value.verificationFlags.every(isVerificationFlag))
|
|
465
|
+
return false;
|
|
466
|
+
if (!isEvidencePosture(value.evidencePosture))
|
|
467
|
+
return false;
|
|
468
|
+
if (value.evidencePosture.kind !== "evaluation")
|
|
469
|
+
return false;
|
|
470
|
+
if (typeof value.rejectedAt !== "number")
|
|
471
|
+
return false;
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
120
474
|
export function isProcessResult(value) {
|
|
121
475
|
if (!isRecord(value))
|
|
122
476
|
return false;
|
|
123
|
-
|
|
477
|
+
// v4.0.6: every committed decision carries a well-formed Agent outcome
|
|
478
|
+
// (null only for a rollback directive) and its derived success projection.
|
|
479
|
+
const outcome = value.agentOutcome;
|
|
480
|
+
const validOutcome = outcome === null || outcome === "success" ||
|
|
481
|
+
outcome === "partial" || outcome === "failed" || outcome === "blocked";
|
|
482
|
+
return ["continue", "stop", "reject", "terminate", "backtrack", "recovery"].includes(String(value.action)) && validOutcome &&
|
|
483
|
+
typeof value.agentSuccessClaim === "boolean" &&
|
|
484
|
+
Array.isArray(value.verificationFlags) &&
|
|
485
|
+
value.verificationFlags.every(isVerificationFlag) &&
|
|
486
|
+
isEvidencePosture(value.evidencePosture);
|
|
487
|
+
}
|
|
488
|
+
function stringArray(value) {
|
|
489
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
490
|
+
}
|
|
491
|
+
function isOperatorAcceptance(value) {
|
|
492
|
+
if (!isRecord(value))
|
|
493
|
+
return false;
|
|
494
|
+
return typeof value.task === "string" &&
|
|
495
|
+
stringArray(value.success_criteria) &&
|
|
496
|
+
stringArray(value.hard_constraints);
|
|
497
|
+
}
|
|
498
|
+
function isCheckpointRole(value) {
|
|
499
|
+
return value === "before" || value === "mid";
|
|
500
|
+
}
|
|
501
|
+
function isCompiledRoundFacts(value) {
|
|
502
|
+
if (!isRecord(value))
|
|
503
|
+
return false;
|
|
504
|
+
return typeof value.task === "string" &&
|
|
505
|
+
typeof value.goalId === "string" &&
|
|
506
|
+
stringArray(value.constraintsActive) &&
|
|
507
|
+
stringArray(value.constraintsRetired);
|
|
508
|
+
}
|
|
509
|
+
const CHECKPOINT_ID_RE = /^ck-[a-f0-9]{24}$/;
|
|
510
|
+
const CONTENT_HASH_RE = /^[a-f0-9]{64}$/;
|
|
511
|
+
function isCheckpointRef(value) {
|
|
512
|
+
if (!isRecord(value))
|
|
513
|
+
return false;
|
|
514
|
+
if (typeof value.checkpointId !== "string" ||
|
|
515
|
+
!CHECKPOINT_ID_RE.test(value.checkpointId))
|
|
516
|
+
return false;
|
|
517
|
+
if (typeof value.contentHash !== "string" ||
|
|
518
|
+
!CONTENT_HASH_RE.test(value.contentHash))
|
|
519
|
+
return false;
|
|
520
|
+
if (!isCheckpointRole(value.role))
|
|
521
|
+
return false;
|
|
522
|
+
if (!Number.isInteger(value.round) || value.round < 1)
|
|
523
|
+
return false;
|
|
524
|
+
if (typeof value.roundId !== "string" || value.roundId.length === 0)
|
|
525
|
+
return false;
|
|
526
|
+
if (typeof value.transactionId !== "string" || value.transactionId.length === 0)
|
|
527
|
+
return false;
|
|
528
|
+
if (typeof value.branchId !== "string" || value.branchId.length === 0)
|
|
529
|
+
return false;
|
|
530
|
+
if (!Number.isInteger(value.redoRound) || value.redoRound < 1)
|
|
531
|
+
return false;
|
|
532
|
+
if (typeof value.createdAt !== "number")
|
|
533
|
+
return false;
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
function isRecoveryTarget(value) {
|
|
537
|
+
if (!isRecord(value))
|
|
538
|
+
return false;
|
|
539
|
+
if (typeof value.checkpoint_id !== "string" ||
|
|
540
|
+
!CHECKPOINT_ID_RE.test(value.checkpoint_id))
|
|
541
|
+
return false;
|
|
542
|
+
if (typeof value.content_hash !== "string" ||
|
|
543
|
+
!CONTENT_HASH_RE.test(value.content_hash))
|
|
544
|
+
return false;
|
|
545
|
+
if (!isCheckpointRole(value.role))
|
|
546
|
+
return false;
|
|
547
|
+
if (!Number.isInteger(value.round) || value.round < 1)
|
|
548
|
+
return false;
|
|
549
|
+
if (typeof value.round_id !== "string" || value.round_id.length === 0)
|
|
550
|
+
return false;
|
|
551
|
+
if (typeof value.transaction_id !== "string" || value.transaction_id.length === 0)
|
|
552
|
+
return false;
|
|
553
|
+
if (!Number.isInteger(value.redo_round) || value.redo_round < 1)
|
|
554
|
+
return false;
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
function isRecoveryDirective(value) {
|
|
558
|
+
if (!isRecord(value))
|
|
559
|
+
return false;
|
|
560
|
+
if (value.origin !== "agent" && value.origin !== "machine")
|
|
561
|
+
return false;
|
|
562
|
+
if (typeof value.reason !== "string" ||
|
|
563
|
+
typeof value.transaction_id !== "string" ||
|
|
564
|
+
value.transaction_id.length === 0) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
if (!isRecoveryTarget(value.target))
|
|
568
|
+
return false;
|
|
569
|
+
if (value.trigger_rule !== undefined && typeof value.trigger_rule !== "string")
|
|
570
|
+
return false;
|
|
571
|
+
if (!stringArray(value.skipped_files) ||
|
|
572
|
+
!stringArray(value.skipped_discoveries) ||
|
|
573
|
+
!stringArray(value.approaches) ||
|
|
574
|
+
!stringArray(value.wrong_assumptions))
|
|
575
|
+
return false;
|
|
576
|
+
if (!Array.isArray(value.failed_rounds) ||
|
|
577
|
+
!value.failed_rounds.every((item) => Number.isInteger(item)))
|
|
578
|
+
return false;
|
|
579
|
+
if (value.observed_skipped_files !== undefined &&
|
|
580
|
+
!stringArray(value.observed_skipped_files))
|
|
581
|
+
return false;
|
|
582
|
+
if (value.reported_skipped_files !== undefined &&
|
|
583
|
+
!stringArray(value.reported_skipped_files))
|
|
584
|
+
return false;
|
|
585
|
+
if (value.abandoned_rounds !== undefined &&
|
|
586
|
+
(!Array.isArray(value.abandoned_rounds) ||
|
|
587
|
+
!value.abandoned_rounds.every((item) => Number.isInteger(item)))) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (value.trigger_findings !== undefined &&
|
|
591
|
+
(!Array.isArray(value.trigger_findings) ||
|
|
592
|
+
!value.trigger_findings.every(isVerificationFlag)))
|
|
593
|
+
return false;
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
function isBeforeCheckpointError(value) {
|
|
597
|
+
if (!isRecord(value))
|
|
598
|
+
return false;
|
|
599
|
+
return typeof value.code === "string" &&
|
|
600
|
+
typeof value.detail === "string" &&
|
|
601
|
+
typeof value.at === "number";
|
|
602
|
+
}
|
|
603
|
+
function isRestoreMarker(value) {
|
|
604
|
+
if (!isRecord(value))
|
|
605
|
+
return false;
|
|
606
|
+
return typeof value.directiveTransactionId === "string" &&
|
|
607
|
+
value.directiveTransactionId.length > 0 &&
|
|
608
|
+
Number.isInteger(value.redoRound) &&
|
|
609
|
+
value.redoRound >= 1 &&
|
|
610
|
+
typeof value.verifiedAt === "number";
|
|
124
611
|
}
|
|
125
612
|
function parseMachineObservations(value) {
|
|
126
613
|
if (!Array.isArray(value))
|
|
@@ -140,14 +627,13 @@ function parseMachineObservations(value) {
|
|
|
140
627
|
!isRecord(item.data))
|
|
141
628
|
return null;
|
|
142
629
|
// v3.9.1: Git observations moved to schema 2 (complete boundary
|
|
143
|
-
// fingerprints); command/custom observations remain schema 1.
|
|
144
|
-
//
|
|
145
|
-
// legacyTransactionRounds
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: 1;
|
|
149
|
-
if (item.schemaVersion !== expectedVersion)
|
|
630
|
+
// fingerprints); command/custom observations remain schema 1. Custom
|
|
631
|
+
// observation data is opaque — it never carries a progress fact. ONE
|
|
632
|
+
// judgment is shared with legacyTransactionRounds, so the parser can never
|
|
633
|
+
// reject a round the reporter does not report (or the reverse).
|
|
634
|
+
if (observationVersionIssue(item.kind, item.schemaVersion, item.data, item.status)) {
|
|
150
635
|
return null;
|
|
636
|
+
}
|
|
151
637
|
if (item.kind === "git" && item.status === "observed") {
|
|
152
638
|
const data = item.data;
|
|
153
639
|
if (!Array.isArray(data.dirty) ||
|
|
@@ -169,6 +655,30 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
169
655
|
return null;
|
|
170
656
|
if (value.schemaVersion !== ROUND_TRANSACTION_SCHEMA_VERSION)
|
|
171
657
|
return null;
|
|
658
|
+
if (value.kind !== "evaluation" && value.kind !== "rollback")
|
|
659
|
+
return null;
|
|
660
|
+
if (typeof value.branchId !== "string" || value.branchId.length === 0)
|
|
661
|
+
return null;
|
|
662
|
+
if (typeof value.transactionId !== "string" || value.transactionId.length === 0) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if (value.operatorAcceptance !== undefined &&
|
|
666
|
+
!isOperatorAcceptance(value.operatorAcceptance))
|
|
667
|
+
return null;
|
|
668
|
+
if (value.compiled !== undefined && !isCompiledRoundFacts(value.compiled)) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
if (value.kind === "rollback") {
|
|
672
|
+
if (!isRecoveryDirective(value.recovery))
|
|
673
|
+
return null;
|
|
674
|
+
if (!isOperatorAcceptance(value.operatorAcceptance))
|
|
675
|
+
return null;
|
|
676
|
+
if (value.compiled !== undefined)
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
else if (value.recovery !== undefined) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
172
682
|
if (typeof value.roundId !== "string" || typeof value.loopId !== "string") {
|
|
173
683
|
return null;
|
|
174
684
|
}
|
|
@@ -179,6 +689,17 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
179
689
|
if (!["prepared", "prompted", "evaluated", "rejected", "committed", "terminated"]
|
|
180
690
|
.includes(String(value.phase)))
|
|
181
691
|
return null;
|
|
692
|
+
// Schema 6 structural invariant: a submitted evaluation always carries the
|
|
693
|
+
// compile facts and the immutable operator surface. A round without them
|
|
694
|
+
// cannot be re-derived from the same document, so it is not committed
|
|
695
|
+
// history.
|
|
696
|
+
if (value.kind === "evaluation" &&
|
|
697
|
+
(value.phase === "evaluated" || value.phase === "committed")) {
|
|
698
|
+
if (!isOperatorAcceptance(value.operatorAcceptance))
|
|
699
|
+
return null;
|
|
700
|
+
if (!isCompiledRoundFacts(value.compiled))
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
182
703
|
const beforeEvidence = parseMachineObservations(value.beforeEvidence);
|
|
183
704
|
if (!beforeEvidence)
|
|
184
705
|
return null;
|
|
@@ -187,8 +708,83 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
187
708
|
: parseMachineObservations(value.afterEvidence);
|
|
188
709
|
if (value.afterEvidence !== undefined && !afterEvidence)
|
|
189
710
|
return null;
|
|
711
|
+
const snapshotRound = value.round;
|
|
712
|
+
const snapshotRoundId = value.roundId;
|
|
713
|
+
const snapshotTransactionId = value.transactionId;
|
|
714
|
+
const snapshotBranchId = value.branchId;
|
|
715
|
+
if (value.kind === "rollback") {
|
|
716
|
+
// A rollback directive may carry the abandoned transaction's evidence,
|
|
717
|
+
// but never that transaction's savepoints or restore marker. The
|
|
718
|
+
// directive's `target` is the only checkpoint reference.
|
|
719
|
+
if (value.beforeCheckpoint !== undefined ||
|
|
720
|
+
value.beforeCheckpointError !== undefined ||
|
|
721
|
+
value.checkpoints !== undefined ||
|
|
722
|
+
value.restore !== undefined)
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
if (value.beforeCheckpoint !== undefined) {
|
|
727
|
+
if (!isCheckpointRef(value.beforeCheckpoint))
|
|
728
|
+
return null;
|
|
729
|
+
const ref = value.beforeCheckpoint;
|
|
730
|
+
if (ref.role !== "before" ||
|
|
731
|
+
ref.round !== snapshotRound ||
|
|
732
|
+
ref.roundId !== snapshotRoundId ||
|
|
733
|
+
ref.transactionId !== snapshotTransactionId ||
|
|
734
|
+
ref.branchId !== snapshotBranchId ||
|
|
735
|
+
ref.redoRound !== snapshotRound)
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
if (value.beforeCheckpointError !== undefined &&
|
|
739
|
+
!isBeforeCheckpointError(value.beforeCheckpointError))
|
|
740
|
+
return null;
|
|
741
|
+
if ((value.beforeCheckpoint === undefined) ===
|
|
742
|
+
(value.beforeCheckpointError === undefined)) {
|
|
743
|
+
const observedGitBefore = beforeEvidence.some((observation) => observation.kind === "git" && observation.status === "observed");
|
|
744
|
+
// A Git-observed transaction owes a before-image or an explicit error.
|
|
745
|
+
if (observedGitBefore)
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
if (value.checkpoints !== undefined) {
|
|
749
|
+
if (!Array.isArray(value.checkpoints) ||
|
|
750
|
+
value.checkpoints.length > MAX_MID_CHECKPOINTS_PER_TRANSACTION)
|
|
751
|
+
return null;
|
|
752
|
+
const seen = new Set();
|
|
753
|
+
for (const item of value.checkpoints) {
|
|
754
|
+
if (!isCheckpointRef(item))
|
|
755
|
+
return null;
|
|
756
|
+
if (item.role !== "mid" ||
|
|
757
|
+
item.round !== snapshotRound ||
|
|
758
|
+
item.roundId !== snapshotRoundId ||
|
|
759
|
+
item.transactionId !== snapshotTransactionId ||
|
|
760
|
+
item.branchId !== snapshotBranchId ||
|
|
761
|
+
item.redoRound !== snapshotRound)
|
|
762
|
+
return null;
|
|
763
|
+
if (seen.has(item.checkpointId))
|
|
764
|
+
return null;
|
|
765
|
+
seen.add(item.checkpointId);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (value.restore !== undefined) {
|
|
769
|
+
if (!isRestoreMarker(value.restore))
|
|
770
|
+
return null;
|
|
771
|
+
if (value.restore.redoRound !== snapshotRound)
|
|
772
|
+
return null;
|
|
773
|
+
if (value.restore.directiveTransactionId !== snapshotBranchId)
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
190
777
|
if (value.result !== undefined && !isProcessResult(value.result))
|
|
191
778
|
return null;
|
|
779
|
+
if (value.retryContext !== undefined) {
|
|
780
|
+
if (!isRetryContext(value.retryContext))
|
|
781
|
+
return null;
|
|
782
|
+
// Transient retry facts exist only between a rejection and the next
|
|
783
|
+
// attempt. A committed/terminated document that carries them would leak
|
|
784
|
+
// rejected-attempt evidence into history.
|
|
785
|
+
if (value.phase !== "rejected" && value.phase !== "prompted")
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
192
788
|
if (value.contractBinding !== undefined && !isContractBinding(value.contractBinding))
|
|
193
789
|
return null;
|
|
194
790
|
if (value.normalizationDiagnostics !== undefined) {
|
|
@@ -214,9 +810,10 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
214
810
|
return null;
|
|
215
811
|
}
|
|
216
812
|
}
|
|
217
|
-
// A normalized/committed
|
|
218
|
-
//
|
|
219
|
-
if (
|
|
813
|
+
// A normalized/committed evaluation always carries its attempted manifest.
|
|
814
|
+
// A rollback directive is not an evaluation and has no manifest.
|
|
815
|
+
if (value.kind === "evaluation" &&
|
|
816
|
+
(value.phase === "evaluated" || value.phase === "committed") &&
|
|
220
817
|
value.submittedFields === undefined) {
|
|
221
818
|
return null;
|
|
222
819
|
}
|
|
@@ -244,14 +841,43 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
244
841
|
typeof entry.id === "string" &&
|
|
245
842
|
typeof entry.reason === "string") &&
|
|
246
843
|
typeof coverage.complete === "boolean";
|
|
844
|
+
// v4.0.2/v4.0.4: the context boundary is a REQUIRED artifact fact. An
|
|
845
|
+
// artifact without a legal pair is a hard break, never an implicit
|
|
846
|
+
// "fresh_invocation" default.
|
|
847
|
+
// v4.0.5: the shared runtime whitelists are the only legality source; the
|
|
848
|
+
// optional invocation instruction, when present, must be a non-empty
|
|
849
|
+
// string. A malformed value is reported as legacy, never silently
|
|
850
|
+
// accepted.
|
|
851
|
+
const contextModeValid = isPromptContextMode(artifact.contextMode);
|
|
852
|
+
const contextReasonValid = isPromptContextReason(artifact.contextReason);
|
|
853
|
+
const instructionValid = artifact.currentUserInstruction === undefined ||
|
|
854
|
+
(typeof artifact.currentUserInstruction === "string" &&
|
|
855
|
+
artifact.currentUserInstruction.trim().length > 0);
|
|
247
856
|
if (artifact.schemaVersion !== PROMPT_ARTIFACT_SCHEMA_VERSION ||
|
|
248
857
|
typeof artifact.roundId !== "string" ||
|
|
858
|
+
// v4.0.3: the artifact identity is part of the transaction identity.
|
|
859
|
+
artifact.roundId !== snapshotRoundId ||
|
|
860
|
+
artifact.round !== snapshotRound ||
|
|
861
|
+
artifact.attempt !== value.attempt ||
|
|
249
862
|
typeof artifact.renderedPrompt !== "string" ||
|
|
250
863
|
typeof artifact.promptHash !== "string" ||
|
|
251
864
|
typeof artifact.stateHash !== "string" ||
|
|
252
865
|
!["l0", "l1", "l2"].includes(String(artifact.level)) ||
|
|
866
|
+
!contextModeValid ||
|
|
867
|
+
!contextReasonValid ||
|
|
868
|
+
!instructionValid ||
|
|
253
869
|
!coverageValid)
|
|
254
870
|
return null;
|
|
871
|
+
// v4.0.4: the SAME legality guard the assembler enforces. An illegal
|
|
872
|
+
// pair (for example a retry reason on attempt 1) is not committed history.
|
|
873
|
+
if (promptContextPairIssue({
|
|
874
|
+
level: artifact.level,
|
|
875
|
+
mode: artifact.contextMode,
|
|
876
|
+
reason: artifact.contextReason,
|
|
877
|
+
attempt: artifact.attempt,
|
|
878
|
+
round: artifact.round,
|
|
879
|
+
}) !== null)
|
|
880
|
+
return null;
|
|
255
881
|
}
|
|
256
882
|
const snapshot = {
|
|
257
883
|
...value,
|
|
@@ -264,12 +890,10 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
264
890
|
return snapshot;
|
|
265
891
|
}
|
|
266
892
|
export class RoundTransactionCoordinator {
|
|
267
|
-
engine;
|
|
268
893
|
store;
|
|
269
894
|
commitStore;
|
|
270
|
-
constructor(
|
|
271
|
-
this.
|
|
272
|
-
this.store = store ?? engine.getStore();
|
|
895
|
+
constructor(store, commitStore) {
|
|
896
|
+
this.store = store;
|
|
273
897
|
this.commitStore = commitStore ?? new VaultRoundCommitStore(this.store);
|
|
274
898
|
}
|
|
275
899
|
process(input) {
|
|
@@ -308,20 +932,20 @@ export class RoundTransactionCoordinator {
|
|
|
308
932
|
task: input.task,
|
|
309
933
|
currentRound: snapshot.round,
|
|
310
934
|
maxRounds: input.maxRounds,
|
|
935
|
+
...(input.history ? { history: input.history } : {}),
|
|
311
936
|
selfEval: input.selfEval,
|
|
312
|
-
lastSelfEval: input.lastSelfEval,
|
|
313
937
|
consecutiveRejections: input.consecutiveRejections,
|
|
314
938
|
lastRejectionCheck: input.lastRejectionCheck,
|
|
315
939
|
evidenceDeltas: roundDeltas,
|
|
316
940
|
beforeEvidence: snapshot.beforeEvidence,
|
|
317
|
-
successTrajectory: input.successTrajectory,
|
|
318
|
-
backtrackSkippedFiles: input.backtrackSkippedFiles,
|
|
319
|
-
backtrackSkippedFingerprints: input.backtrackSkippedFingerprints,
|
|
320
|
-
backtrackTargetGitHead: input.backtrackTargetGitHead,
|
|
321
|
-
backtrackTargetDirty: input.backtrackTargetDirty,
|
|
322
941
|
entrypointTrust: input.entrypointTrust,
|
|
323
942
|
// v3.9.4: the SAME manifest the receipt reads.
|
|
324
943
|
submittedFields: input.submittedFields ?? snapshot.submittedFields,
|
|
944
|
+
...(input.beforeCheckpoint ? { beforeCheckpoint: input.beforeCheckpoint } : {}),
|
|
945
|
+
afterEvidence: input.actualEvidence,
|
|
946
|
+
...(input.resolveCheckpointManifest
|
|
947
|
+
? { resolveCheckpointManifest: input.resolveCheckpointManifest }
|
|
948
|
+
: {}),
|
|
325
949
|
});
|
|
326
950
|
const evaluated = {
|
|
327
951
|
...snapshot,
|
|
@@ -330,6 +954,8 @@ export class RoundTransactionCoordinator {
|
|
|
330
954
|
afterEvidence: input.actualEvidence,
|
|
331
955
|
evaluation: input.selfEval,
|
|
332
956
|
result,
|
|
957
|
+
// A terminated attempt has no next retry; transient facts die with it.
|
|
958
|
+
...(result.action === "terminate" ? { retryContext: undefined } : {}),
|
|
333
959
|
...(input.normalizationDiagnostics !== undefined
|
|
334
960
|
? { normalizationDiagnostics: input.normalizationDiagnostics }
|
|
335
961
|
: {}),
|
|
@@ -348,9 +974,40 @@ export class RoundTransactionCoordinator {
|
|
|
348
974
|
replayed: false,
|
|
349
975
|
});
|
|
350
976
|
}
|
|
977
|
+
if ((result.action === "recovery" || result.action === "backtrack") && result.recovery) {
|
|
978
|
+
const recovery = {
|
|
979
|
+
...result.recovery,
|
|
980
|
+
transaction_id: deriveTransactionId({
|
|
981
|
+
kind: "rollback",
|
|
982
|
+
branchId: snapshot.branchId,
|
|
983
|
+
roundId: snapshot.roundId,
|
|
984
|
+
attempt: 1,
|
|
985
|
+
promptHash: snapshot.promptArtifact?.promptHash,
|
|
986
|
+
recovery: {
|
|
987
|
+
target: result.recovery.target,
|
|
988
|
+
reason: result.recovery.reason,
|
|
989
|
+
abandonedRounds: result.recovery.abandoned_rounds,
|
|
990
|
+
failedRounds: result.recovery.failed_rounds,
|
|
991
|
+
observedSkippedFiles: result.recovery.observed_skipped_files,
|
|
992
|
+
reportedSkippedFiles: result.recovery.reported_skipped_files,
|
|
993
|
+
triggerFindings: (result.recovery.trigger_findings ?? [])
|
|
994
|
+
.map((flag) => flag.check),
|
|
995
|
+
},
|
|
996
|
+
}),
|
|
997
|
+
};
|
|
998
|
+
return this.commitRecovery({
|
|
999
|
+
snapshot,
|
|
1000
|
+
loopId: snapshot.loopId,
|
|
1001
|
+
recovery,
|
|
1002
|
+
actualEvidence: input.actualEvidence,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
351
1005
|
const committedSnapshot = {
|
|
352
1006
|
...evaluated,
|
|
353
1007
|
phase: "committed",
|
|
1008
|
+
// v4.0.3: transient retry facts die at the commit boundary. The retry
|
|
1009
|
+
// prompt artifact remains the record of what the Agent was asked.
|
|
1010
|
+
retryContext: undefined,
|
|
354
1011
|
// v3.8: the contract binding is resolved HERE, at commit time, because
|
|
355
1012
|
// the command configuration in force at declaration is not recoverable
|
|
356
1013
|
// from the Vault afterwards. Machine-recomputed — never agent-supplied.
|
|
@@ -360,20 +1017,32 @@ export class RoundTransactionCoordinator {
|
|
|
360
1017
|
})(),
|
|
361
1018
|
updatedAt: Date.now(),
|
|
362
1019
|
};
|
|
1020
|
+
if (!snapshot.compiled || !snapshot.operatorAcceptance) {
|
|
1021
|
+
throw new Error("An evaluation commit requires compile facts and the operator acceptance snapshot");
|
|
1022
|
+
}
|
|
363
1023
|
const metadata = {
|
|
364
1024
|
schema_version: ROUND_TRANSACTION_SCHEMA_VERSION,
|
|
365
1025
|
round_id: snapshot.roundId,
|
|
366
1026
|
snapshot: committedSnapshot,
|
|
367
1027
|
result,
|
|
368
1028
|
};
|
|
369
|
-
|
|
1029
|
+
try {
|
|
1030
|
+
this.commitStore.appendFeedback(snapshot.loopId, snapshot.round, metadata);
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
policyMetrics.recordVaultWriteError("feedback_persist");
|
|
1034
|
+
logEvent("vault_write_error", { error: "feedback_persist" });
|
|
1035
|
+
throw error;
|
|
1036
|
+
}
|
|
370
1037
|
// v3.7.1: a backtrack decision commits as the CURRENT round, which the
|
|
371
1038
|
// incremental hydration cache never re-reads before the restore compile
|
|
372
1039
|
// (it targets the same round). Drop the cache so the rollback — and its
|
|
373
1040
|
// Recovery Brief facts — is visible to the very next compile.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1041
|
+
logEvent("round_complete", {
|
|
1042
|
+
loopId: snapshot.loopId,
|
|
1043
|
+
round: snapshot.round,
|
|
1044
|
+
agentSuccessClaim: result.agentSuccessClaim,
|
|
1045
|
+
});
|
|
377
1046
|
const persisted = this.readCommitted(committedSnapshot);
|
|
378
1047
|
if (!persisted) {
|
|
379
1048
|
throw new Error(`Round transaction commit failed: ${snapshot.roundId}`);
|
|
@@ -386,9 +1055,126 @@ export class RoundTransactionCoordinator {
|
|
|
386
1055
|
});
|
|
387
1056
|
return finish({ snapshot: committedSnapshot, result, replayed: false });
|
|
388
1057
|
}
|
|
1058
|
+
/** v3.9.6: commit an Agent-initiated rollback directive. This path does not
|
|
1059
|
+
* run verification or enforcement: abandoning work is an action, not a
|
|
1060
|
+
* success claim. The directive is a typed event, not a `:feedback` round. */
|
|
1061
|
+
commitRecovery(input) {
|
|
1062
|
+
const { snapshot, recovery } = input;
|
|
1063
|
+
if (snapshot.kind !== "evaluation") {
|
|
1064
|
+
throw new Error(`Cannot recover from a ${snapshot.kind} transaction`);
|
|
1065
|
+
}
|
|
1066
|
+
const existing = this.readRecoveryCommitted(input.loopId, snapshot.round, recovery.transaction_id);
|
|
1067
|
+
if (existing) {
|
|
1068
|
+
policyMetrics.recordRound(input.loopId, existing.result, true);
|
|
1069
|
+
return existing;
|
|
1070
|
+
}
|
|
1071
|
+
const committedSnapshot = {
|
|
1072
|
+
...snapshot,
|
|
1073
|
+
kind: "rollback",
|
|
1074
|
+
transactionId: recovery.transaction_id,
|
|
1075
|
+
phase: "committed",
|
|
1076
|
+
afterEvidence: input.actualEvidence,
|
|
1077
|
+
// A machine recovery keeps the triggering evaluation as the record of
|
|
1078
|
+
// what was abandoned. An agent recovery has no evaluation at all.
|
|
1079
|
+
evaluation: recovery.origin === "machine" ? snapshot.evaluation : undefined,
|
|
1080
|
+
result: undefined,
|
|
1081
|
+
// The operator surface is immutable and survives the rollback. The
|
|
1082
|
+
// directive carries the same byte-identical copy as the abandoned
|
|
1083
|
+
// evaluation so a cold process can still recover it.
|
|
1084
|
+
...(snapshot.operatorAcceptance
|
|
1085
|
+
? { operatorAcceptance: snapshot.operatorAcceptance }
|
|
1086
|
+
: {}),
|
|
1087
|
+
compiled: undefined,
|
|
1088
|
+
// The abandoned transaction's savepoints/restore marker belong to that
|
|
1089
|
+
// attempt, not to the child branch. The directive target is the only
|
|
1090
|
+
// recovery point recorded by this event.
|
|
1091
|
+
beforeCheckpoint: undefined,
|
|
1092
|
+
beforeCheckpointError: undefined,
|
|
1093
|
+
checkpoints: undefined,
|
|
1094
|
+
restore: undefined,
|
|
1095
|
+
retryContext: undefined,
|
|
1096
|
+
recovery,
|
|
1097
|
+
submittedFields: recovery.origin === "machine"
|
|
1098
|
+
? (snapshot.submittedFields ?? [])
|
|
1099
|
+
: [],
|
|
1100
|
+
normalizationDiagnostics: recovery.origin === "machine"
|
|
1101
|
+
? (snapshot.normalizationDiagnostics ?? [])
|
|
1102
|
+
: [],
|
|
1103
|
+
updatedAt: Date.now(),
|
|
1104
|
+
};
|
|
1105
|
+
const result = {
|
|
1106
|
+
action: recovery.origin === "machine" ? "backtrack" : "recovery",
|
|
1107
|
+
origin: recovery.origin,
|
|
1108
|
+
reason: recovery.reason,
|
|
1109
|
+
recovery,
|
|
1110
|
+
verificationFlags: [],
|
|
1111
|
+
agentSuccessClaim: false,
|
|
1112
|
+
agentOutcome: null,
|
|
1113
|
+
gateContradicted: false,
|
|
1114
|
+
evidencePosture: rollbackEvidencePosture(snapshot.round),
|
|
1115
|
+
newConsecutiveRejections: 0,
|
|
1116
|
+
shouldPushSuccessTrajectory: false,
|
|
1117
|
+
};
|
|
1118
|
+
if (!committedSnapshot.operatorAcceptance) {
|
|
1119
|
+
throw new Error("A rollback directive requires the immutable operator acceptance snapshot");
|
|
1120
|
+
}
|
|
1121
|
+
const envelope = {
|
|
1122
|
+
schema_version: ROUND_TRANSACTION_SCHEMA_VERSION,
|
|
1123
|
+
round_id: snapshot.roundId,
|
|
1124
|
+
snapshot: committedSnapshot,
|
|
1125
|
+
result,
|
|
1126
|
+
};
|
|
1127
|
+
try {
|
|
1128
|
+
this.commitStore.appendRollbackDirective(input.loopId, snapshot.round, recovery.transaction_id, envelope);
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
policyMetrics.recordVaultWriteError("rollback_persist");
|
|
1132
|
+
logEvent("vault_write_error", { error: "rollback_persist" });
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
const persisted = this.readRecoveryCommitted(input.loopId, snapshot.round, recovery.transaction_id);
|
|
1136
|
+
if (!persisted) {
|
|
1137
|
+
throw new Error(`Rollback directive read-back failed: ${recovery.transaction_id}`);
|
|
1138
|
+
}
|
|
1139
|
+
policyMetrics.recordRound(input.loopId, result, false);
|
|
1140
|
+
logEvent("rollback_directive_committed", {
|
|
1141
|
+
loopId: input.loopId,
|
|
1142
|
+
round: snapshot.round,
|
|
1143
|
+
transactionId: recovery.transaction_id,
|
|
1144
|
+
origin: recovery.origin,
|
|
1145
|
+
target: recovery.target,
|
|
1146
|
+
});
|
|
1147
|
+
return { snapshot: committedSnapshot, result, replayed: false };
|
|
1148
|
+
}
|
|
1149
|
+
readRecoveryCommitted(loopId, round, transactionId) {
|
|
1150
|
+
const entry = this.commitStore.findRollback(loopId, round, transactionId);
|
|
1151
|
+
if (!entry)
|
|
1152
|
+
return null;
|
|
1153
|
+
const lineage = isRecord(entry.loop_lineage) ? entry.loop_lineage : null;
|
|
1154
|
+
const transaction = lineage && isRecord(lineage.round_transaction)
|
|
1155
|
+
? lineage.round_transaction
|
|
1156
|
+
: null;
|
|
1157
|
+
if (!transaction)
|
|
1158
|
+
return null;
|
|
1159
|
+
const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
|
|
1160
|
+
const result = transaction.result;
|
|
1161
|
+
if (!snapshot || snapshot.kind !== "rollback" || !isProcessResult(result)) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
if (transactionIdentityIssue(entry, {
|
|
1165
|
+
loopId,
|
|
1166
|
+
round,
|
|
1167
|
+
schemaVersion: ROUND_TRANSACTION_SCHEMA_VERSION,
|
|
1168
|
+
transactionId,
|
|
1169
|
+
}))
|
|
1170
|
+
return null;
|
|
1171
|
+
return { snapshot, result, replayed: true };
|
|
1172
|
+
}
|
|
389
1173
|
/** Recover an already committed decision without evaluating or writing. */
|
|
390
1174
|
recover(snapshot) {
|
|
391
|
-
const outcome =
|
|
1175
|
+
const outcome = snapshot.kind === "rollback"
|
|
1176
|
+
? this.readRecoveryCommitted(snapshot.loopId, snapshot.round, snapshot.transactionId)
|
|
1177
|
+
: this.readCommitted(snapshot);
|
|
392
1178
|
if (outcome) {
|
|
393
1179
|
policyMetrics.recordRound(snapshot.loopId, outcome.result, true);
|
|
394
1180
|
}
|
|
@@ -400,28 +1186,41 @@ export class RoundTransactionCoordinator {
|
|
|
400
1186
|
for (const entry of entries) {
|
|
401
1187
|
if (entry.task_id !== taskId)
|
|
402
1188
|
continue;
|
|
403
|
-
const outcome = this.outcomeFromEntry(entry, expected
|
|
1189
|
+
const outcome = this.outcomeFromEntry(entry, expected, opts?.skipBacktrack === true);
|
|
404
1190
|
if (outcome)
|
|
405
1191
|
return outcome;
|
|
406
1192
|
}
|
|
407
1193
|
return null;
|
|
408
1194
|
}
|
|
409
|
-
outcomeFromEntry(entry,
|
|
1195
|
+
outcomeFromEntry(entry, expected, skipBacktrack = false) {
|
|
410
1196
|
const lineage = isRecord(entry.loop_lineage) ? entry.loop_lineage : null;
|
|
411
1197
|
const transaction = lineage && isRecord(lineage.round_transaction)
|
|
412
1198
|
? lineage.round_transaction
|
|
413
1199
|
: null;
|
|
414
|
-
if (!transaction || transaction.round_id !==
|
|
1200
|
+
if (!transaction || transaction.round_id !== expected.roundId)
|
|
1201
|
+
return null;
|
|
1202
|
+
if (transactionIdentityIssue(entry, {
|
|
1203
|
+
loopId: expected.loopId,
|
|
1204
|
+
round: expected.round,
|
|
1205
|
+
schemaVersion: ROUND_TRANSACTION_SCHEMA_VERSION,
|
|
1206
|
+
transactionId: expected.transactionId,
|
|
1207
|
+
}))
|
|
415
1208
|
return null;
|
|
416
1209
|
const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
|
|
417
1210
|
const result = transaction.result;
|
|
418
1211
|
if (!snapshot || !isProcessResult(result))
|
|
419
1212
|
return null;
|
|
1213
|
+
// v3.9.7: the numeric round identity is NOT enough. A deep restore reuses
|
|
1214
|
+
// `target.redo_round`, where the abandoned branch already committed a
|
|
1215
|
+
// feedback round with the same roundId. Replaying it would silently discard
|
|
1216
|
+
// the redo submission. The transaction id is the branch-scoped identity.
|
|
1217
|
+
if (snapshot.transactionId !== expected.transactionId)
|
|
1218
|
+
return null;
|
|
420
1219
|
// v3.2.1: a committed backtrack is a roll-back directive, not a terminal
|
|
421
1220
|
// decision. The restored round reuses the same roundId, so replaying it
|
|
422
1221
|
// would silently discard the agent's redo submission and re-emit the
|
|
423
1222
|
// backtrack prompt forever (the redo's work is never evaluated, and the
|
|
424
|
-
//
|
|
1223
|
+
// the terminate guards never run because evaluation is short-circuited).
|
|
425
1224
|
// Crash recovery still replays backtracks (recover() → reconcileCommittedRound
|
|
426
1225
|
// resets the round counter from the committed decision); only the advance
|
|
427
1226
|
// path skips them so the redo is evaluated normally.
|