loopforge 3.9.2 → 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 +152 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +382 -206
- 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 +105 -141
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +249 -372
- 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 +32 -67
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +203 -439
- 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 +28 -887
- 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 +167 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +373 -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 +51 -19
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +423 -507
- 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 +285 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1629 -574
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +61 -19
- 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 +963 -271
- 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 -82
- 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 +763 -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 +15 -40
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -13
- 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 +28 -9
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +592 -603
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +39 -0
- package/dist/prompt-coverage.d.ts.map +1 -0
- package/dist/prompt-coverage.js +80 -0
- package/dist/prompt-coverage.js.map +1 -0
- package/dist/prompt-freshness.d.ts +25 -0
- package/dist/prompt-freshness.d.ts.map +1 -0
- package/dist/prompt-freshness.js +88 -0
- package/dist/prompt-freshness.js.map +1 -0
- package/dist/prompt-policy.d.ts +14 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +29 -10
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +875 -419
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -99
- 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 +45 -91
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +254 -271
- 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 +54 -157
- 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 +856 -39
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +40 -60
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +455 -384
- 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 +43 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +368 -262
- 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 +146 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +47 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +66 -15
- 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 +13 -0
- package/dist/verification-actions.d.ts.map +1 -0
- package/dist/verification-actions.js +44 -0
- package/dist/verification-actions.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +218 -427
- 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 +5 -19
- package/package.json +2 -2
- 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 -370
|
@@ -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
|
}
|
|
@@ -229,12 +826,57 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
229
826
|
// is not committed history, and `legacyTransactionRounds` reports the
|
|
230
827
|
// loss explicitly instead of the loop looking complete while rounds are
|
|
231
828
|
// missing.
|
|
829
|
+
// v3.9.4: schema 4 requires the ID-level coverage proof. A missing or
|
|
830
|
+
// malformed coverage object is a hard break, never an empty projection.
|
|
831
|
+
const coverage = artifact.coverage;
|
|
832
|
+
const coverageValid = isRecord(coverage) &&
|
|
833
|
+
Array.isArray(coverage.active) &&
|
|
834
|
+
coverage.active.every((entry) => isRecord(entry) &&
|
|
835
|
+
typeof entry.id === "string" &&
|
|
836
|
+
typeof entry.kind === "string") &&
|
|
837
|
+
Array.isArray(coverage.rendered) &&
|
|
838
|
+
coverage.rendered.every((id) => typeof id === "string") &&
|
|
839
|
+
Array.isArray(coverage.omitted) &&
|
|
840
|
+
coverage.omitted.every((entry) => isRecord(entry) &&
|
|
841
|
+
typeof entry.id === "string" &&
|
|
842
|
+
typeof entry.reason === "string") &&
|
|
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);
|
|
232
856
|
if (artifact.schemaVersion !== PROMPT_ARTIFACT_SCHEMA_VERSION ||
|
|
233
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 ||
|
|
234
862
|
typeof artifact.renderedPrompt !== "string" ||
|
|
235
863
|
typeof artifact.promptHash !== "string" ||
|
|
236
864
|
typeof artifact.stateHash !== "string" ||
|
|
237
|
-
!["l0", "l1", "l2"].includes(String(artifact.level))
|
|
865
|
+
!["l0", "l1", "l2"].includes(String(artifact.level)) ||
|
|
866
|
+
!contextModeValid ||
|
|
867
|
+
!contextReasonValid ||
|
|
868
|
+
!instructionValid ||
|
|
869
|
+
!coverageValid)
|
|
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)
|
|
238
880
|
return null;
|
|
239
881
|
}
|
|
240
882
|
const snapshot = {
|
|
@@ -248,12 +890,10 @@ export function parseRoundTransactionSnapshot(value) {
|
|
|
248
890
|
return snapshot;
|
|
249
891
|
}
|
|
250
892
|
export class RoundTransactionCoordinator {
|
|
251
|
-
engine;
|
|
252
893
|
store;
|
|
253
894
|
commitStore;
|
|
254
|
-
constructor(
|
|
255
|
-
this.
|
|
256
|
-
this.store = store ?? engine.getStore();
|
|
895
|
+
constructor(store, commitStore) {
|
|
896
|
+
this.store = store;
|
|
257
897
|
this.commitStore = commitStore ?? new VaultRoundCommitStore(this.store);
|
|
258
898
|
}
|
|
259
899
|
process(input) {
|
|
@@ -292,18 +932,20 @@ export class RoundTransactionCoordinator {
|
|
|
292
932
|
task: input.task,
|
|
293
933
|
currentRound: snapshot.round,
|
|
294
934
|
maxRounds: input.maxRounds,
|
|
935
|
+
...(input.history ? { history: input.history } : {}),
|
|
295
936
|
selfEval: input.selfEval,
|
|
296
|
-
lastSelfEval: input.lastSelfEval,
|
|
297
937
|
consecutiveRejections: input.consecutiveRejections,
|
|
298
938
|
lastRejectionCheck: input.lastRejectionCheck,
|
|
299
939
|
evidenceDeltas: roundDeltas,
|
|
300
940
|
beforeEvidence: snapshot.beforeEvidence,
|
|
301
|
-
successTrajectory: input.successTrajectory,
|
|
302
|
-
backtrackSkippedFiles: input.backtrackSkippedFiles,
|
|
303
|
-
backtrackSkippedFingerprints: input.backtrackSkippedFingerprints,
|
|
304
|
-
backtrackTargetGitHead: input.backtrackTargetGitHead,
|
|
305
|
-
backtrackTargetDirty: input.backtrackTargetDirty,
|
|
306
941
|
entrypointTrust: input.entrypointTrust,
|
|
942
|
+
// v3.9.4: the SAME manifest the receipt reads.
|
|
943
|
+
submittedFields: input.submittedFields ?? snapshot.submittedFields,
|
|
944
|
+
...(input.beforeCheckpoint ? { beforeCheckpoint: input.beforeCheckpoint } : {}),
|
|
945
|
+
afterEvidence: input.actualEvidence,
|
|
946
|
+
...(input.resolveCheckpointManifest
|
|
947
|
+
? { resolveCheckpointManifest: input.resolveCheckpointManifest }
|
|
948
|
+
: {}),
|
|
307
949
|
});
|
|
308
950
|
const evaluated = {
|
|
309
951
|
...snapshot,
|
|
@@ -312,6 +954,8 @@ export class RoundTransactionCoordinator {
|
|
|
312
954
|
afterEvidence: input.actualEvidence,
|
|
313
955
|
evaluation: input.selfEval,
|
|
314
956
|
result,
|
|
957
|
+
// A terminated attempt has no next retry; transient facts die with it.
|
|
958
|
+
...(result.action === "terminate" ? { retryContext: undefined } : {}),
|
|
315
959
|
...(input.normalizationDiagnostics !== undefined
|
|
316
960
|
? { normalizationDiagnostics: input.normalizationDiagnostics }
|
|
317
961
|
: {}),
|
|
@@ -330,9 +974,40 @@ export class RoundTransactionCoordinator {
|
|
|
330
974
|
replayed: false,
|
|
331
975
|
});
|
|
332
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
|
+
}
|
|
333
1005
|
const committedSnapshot = {
|
|
334
1006
|
...evaluated,
|
|
335
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,
|
|
336
1011
|
// v3.8: the contract binding is resolved HERE, at commit time, because
|
|
337
1012
|
// the command configuration in force at declaration is not recoverable
|
|
338
1013
|
// from the Vault afterwards. Machine-recomputed — never agent-supplied.
|
|
@@ -342,20 +1017,32 @@ export class RoundTransactionCoordinator {
|
|
|
342
1017
|
})(),
|
|
343
1018
|
updatedAt: Date.now(),
|
|
344
1019
|
};
|
|
1020
|
+
if (!snapshot.compiled || !snapshot.operatorAcceptance) {
|
|
1021
|
+
throw new Error("An evaluation commit requires compile facts and the operator acceptance snapshot");
|
|
1022
|
+
}
|
|
345
1023
|
const metadata = {
|
|
346
1024
|
schema_version: ROUND_TRANSACTION_SCHEMA_VERSION,
|
|
347
1025
|
round_id: snapshot.roundId,
|
|
348
1026
|
snapshot: committedSnapshot,
|
|
349
1027
|
result,
|
|
350
1028
|
};
|
|
351
|
-
|
|
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
|
+
}
|
|
352
1037
|
// v3.7.1: a backtrack decision commits as the CURRENT round, which the
|
|
353
1038
|
// incremental hydration cache never re-reads before the restore compile
|
|
354
1039
|
// (it targets the same round). Drop the cache so the rollback — and its
|
|
355
1040
|
// Recovery Brief facts — is visible to the very next compile.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1041
|
+
logEvent("round_complete", {
|
|
1042
|
+
loopId: snapshot.loopId,
|
|
1043
|
+
round: snapshot.round,
|
|
1044
|
+
agentSuccessClaim: result.agentSuccessClaim,
|
|
1045
|
+
});
|
|
359
1046
|
const persisted = this.readCommitted(committedSnapshot);
|
|
360
1047
|
if (!persisted) {
|
|
361
1048
|
throw new Error(`Round transaction commit failed: ${snapshot.roundId}`);
|
|
@@ -368,9 +1055,126 @@ export class RoundTransactionCoordinator {
|
|
|
368
1055
|
});
|
|
369
1056
|
return finish({ snapshot: committedSnapshot, result, replayed: false });
|
|
370
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
|
+
}
|
|
371
1173
|
/** Recover an already committed decision without evaluating or writing. */
|
|
372
1174
|
recover(snapshot) {
|
|
373
|
-
const outcome =
|
|
1175
|
+
const outcome = snapshot.kind === "rollback"
|
|
1176
|
+
? this.readRecoveryCommitted(snapshot.loopId, snapshot.round, snapshot.transactionId)
|
|
1177
|
+
: this.readCommitted(snapshot);
|
|
374
1178
|
if (outcome) {
|
|
375
1179
|
policyMetrics.recordRound(snapshot.loopId, outcome.result, true);
|
|
376
1180
|
}
|
|
@@ -382,28 +1186,41 @@ export class RoundTransactionCoordinator {
|
|
|
382
1186
|
for (const entry of entries) {
|
|
383
1187
|
if (entry.task_id !== taskId)
|
|
384
1188
|
continue;
|
|
385
|
-
const outcome = this.outcomeFromEntry(entry, expected
|
|
1189
|
+
const outcome = this.outcomeFromEntry(entry, expected, opts?.skipBacktrack === true);
|
|
386
1190
|
if (outcome)
|
|
387
1191
|
return outcome;
|
|
388
1192
|
}
|
|
389
1193
|
return null;
|
|
390
1194
|
}
|
|
391
|
-
outcomeFromEntry(entry,
|
|
1195
|
+
outcomeFromEntry(entry, expected, skipBacktrack = false) {
|
|
392
1196
|
const lineage = isRecord(entry.loop_lineage) ? entry.loop_lineage : null;
|
|
393
1197
|
const transaction = lineage && isRecord(lineage.round_transaction)
|
|
394
1198
|
? lineage.round_transaction
|
|
395
1199
|
: null;
|
|
396
|
-
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
|
+
}))
|
|
397
1208
|
return null;
|
|
398
1209
|
const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
|
|
399
1210
|
const result = transaction.result;
|
|
400
1211
|
if (!snapshot || !isProcessResult(result))
|
|
401
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;
|
|
402
1219
|
// v3.2.1: a committed backtrack is a roll-back directive, not a terminal
|
|
403
1220
|
// decision. The restored round reuses the same roundId, so replaying it
|
|
404
1221
|
// would silently discard the agent's redo submission and re-emit the
|
|
405
1222
|
// backtrack prompt forever (the redo's work is never evaluated, and the
|
|
406
|
-
//
|
|
1223
|
+
// the terminate guards never run because evaluation is short-circuited).
|
|
407
1224
|
// Crash recovery still replays backtracks (recover() → reconcileCommittedRound
|
|
408
1225
|
// resets the round counter from the committed decision); only the advance
|
|
409
1226
|
// path skips them so the redo is evaluated normally.
|