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.
Files changed (283) hide show
  1. package/README.md +236 -112
  2. package/dist/audit.d.ts +73 -15
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +131 -61
  5. package/dist/audit.js.map +1 -1
  6. package/dist/canonical-state.d.ts +114 -40
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +276 -182
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +41 -12
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +0 -2
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +0 -4
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/committed-round.d.ts +97 -156
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +223 -419
  20. package/dist/committed-round.js.map +1 -1
  21. package/dist/constraint-lifecycle.d.ts +73 -0
  22. package/dist/constraint-lifecycle.d.ts.map +1 -0
  23. package/dist/constraint-lifecycle.js +174 -0
  24. package/dist/constraint-lifecycle.js.map +1 -0
  25. package/dist/context-boundary.d.ts +83 -0
  26. package/dist/context-boundary.d.ts.map +1 -0
  27. package/dist/context-boundary.js +101 -0
  28. package/dist/context-boundary.js.map +1 -0
  29. package/dist/contract-items.d.ts +12 -8
  30. package/dist/contract-items.d.ts.map +1 -1
  31. package/dist/contract-items.js +31 -26
  32. package/dist/contract-items.js.map +1 -1
  33. package/dist/contract-requirement.d.ts +47 -0
  34. package/dist/contract-requirement.d.ts.map +1 -0
  35. package/dist/contract-requirement.js +51 -0
  36. package/dist/contract-requirement.js.map +1 -0
  37. package/dist/criterion-status.d.ts +35 -0
  38. package/dist/criterion-status.d.ts.map +1 -0
  39. package/dist/criterion-status.js +89 -0
  40. package/dist/criterion-status.js.map +1 -0
  41. package/dist/enforcement-gate.d.ts +26 -38
  42. package/dist/enforcement-gate.d.ts.map +1 -1
  43. package/dist/enforcement-gate.js +196 -319
  44. package/dist/enforcement-gate.js.map +1 -1
  45. package/dist/engine.d.ts +9 -103
  46. package/dist/engine.d.ts.map +1 -1
  47. package/dist/engine.js +27 -897
  48. package/dist/engine.js.map +1 -1
  49. package/dist/errors.d.ts +41 -0
  50. package/dist/errors.d.ts.map +1 -0
  51. package/dist/errors.js +69 -0
  52. package/dist/errors.js.map +1 -0
  53. package/dist/evidence-claims.d.ts +33 -31
  54. package/dist/evidence-claims.d.ts.map +1 -1
  55. package/dist/evidence-claims.js +58 -72
  56. package/dist/evidence-claims.js.map +1 -1
  57. package/dist/evidence-posture-view.d.ts +19 -0
  58. package/dist/evidence-posture-view.d.ts.map +1 -0
  59. package/dist/evidence-posture-view.js +116 -0
  60. package/dist/evidence-posture-view.js.map +1 -0
  61. package/dist/evidence-posture.d.ts +73 -0
  62. package/dist/evidence-posture.d.ts.map +1 -0
  63. package/dist/evidence-posture.js +281 -0
  64. package/dist/evidence-posture.js.map +1 -0
  65. package/dist/evidence-provider.d.ts +55 -37
  66. package/dist/evidence-provider.d.ts.map +1 -1
  67. package/dist/evidence-provider.js +182 -332
  68. package/dist/evidence-provider.js.map +1 -1
  69. package/dist/explain.d.ts +166 -27
  70. package/dist/explain.d.ts.map +1 -1
  71. package/dist/explain.js +372 -58
  72. package/dist/explain.js.map +1 -1
  73. package/dist/field-accounting.d.ts.map +1 -1
  74. package/dist/field-accounting.js +3 -22
  75. package/dist/field-accounting.js.map +1 -1
  76. package/dist/generate-schema.js +13 -8
  77. package/dist/generate-schema.js.map +1 -1
  78. package/dist/git-boundary.d.ts +128 -0
  79. package/dist/git-boundary.d.ts.map +1 -0
  80. package/dist/git-boundary.js +625 -0
  81. package/dist/git-boundary.js.map +1 -0
  82. package/dist/git-facts.d.ts +22 -0
  83. package/dist/git-facts.d.ts.map +1 -0
  84. package/dist/git-facts.js +29 -0
  85. package/dist/git-facts.js.map +1 -0
  86. package/dist/git-motion.d.ts +37 -0
  87. package/dist/git-motion.d.ts.map +1 -0
  88. package/dist/git-motion.js +59 -0
  89. package/dist/git-motion.js.map +1 -0
  90. package/dist/history.d.ts +44 -0
  91. package/dist/history.d.ts.map +1 -0
  92. package/dist/history.js +77 -0
  93. package/dist/history.js.map +1 -0
  94. package/dist/index.d.ts +19 -9
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +12 -4
  97. package/dist/index.js.map +1 -1
  98. package/dist/init.d.ts +16 -0
  99. package/dist/init.d.ts.map +1 -1
  100. package/dist/init.js +80 -14
  101. package/dist/init.js.map +1 -1
  102. package/dist/loop-compiler.d.ts +39 -23
  103. package/dist/loop-compiler.d.ts.map +1 -1
  104. package/dist/loop-compiler.js +368 -565
  105. package/dist/loop-compiler.js.map +1 -1
  106. package/dist/loop-store.d.ts +6 -5
  107. package/dist/loop-store.d.ts.map +1 -1
  108. package/dist/loop-store.js +153 -35
  109. package/dist/loop-store.js.map +1 -1
  110. package/dist/mcp/round-lifecycle.d.ts +255 -81
  111. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  112. package/dist/mcp/round-lifecycle.js +1552 -539
  113. package/dist/mcp/round-lifecycle.js.map +1 -1
  114. package/dist/mcp/server.d.ts.map +1 -1
  115. package/dist/mcp/server.js +42 -18
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/mcp/session.d.ts +81 -39
  118. package/dist/mcp/session.d.ts.map +1 -1
  119. package/dist/mcp/session.js +956 -282
  120. package/dist/mcp/session.js.map +1 -1
  121. package/dist/mcp/submission-boundary.d.ts +23 -31
  122. package/dist/mcp/submission-boundary.d.ts.map +1 -1
  123. package/dist/mcp/submission-boundary.js +242 -92
  124. package/dist/mcp/submission-boundary.js.map +1 -1
  125. package/dist/mcp/submission-handle.d.ts +66 -0
  126. package/dist/mcp/submission-handle.d.ts.map +1 -0
  127. package/dist/mcp/submission-handle.js +173 -0
  128. package/dist/mcp/submission-handle.js.map +1 -0
  129. package/dist/mcp/tools.d.ts +17 -7
  130. package/dist/mcp/tools.d.ts.map +1 -1
  131. package/dist/mcp/tools.js +707 -244
  132. package/dist/mcp/tools.js.map +1 -1
  133. package/dist/open-question.d.ts +7 -0
  134. package/dist/open-question.d.ts.map +1 -0
  135. package/dist/open-question.js +14 -0
  136. package/dist/open-question.js.map +1 -0
  137. package/dist/policy-metrics.d.ts +31 -21
  138. package/dist/policy-metrics.d.ts.map +1 -1
  139. package/dist/policy-metrics.js +117 -64
  140. package/dist/policy-metrics.js.map +1 -1
  141. package/dist/policy.d.ts +11 -39
  142. package/dist/policy.d.ts.map +1 -1
  143. package/dist/policy.js +12 -12
  144. package/dist/policy.js.map +1 -1
  145. package/dist/progress.d.ts +45 -0
  146. package/dist/progress.d.ts.map +1 -0
  147. package/dist/progress.js +136 -0
  148. package/dist/progress.js.map +1 -0
  149. package/dist/prompt-assembler.d.ts +20 -6
  150. package/dist/prompt-assembler.d.ts.map +1 -1
  151. package/dist/prompt-assembler.js +475 -479
  152. package/dist/prompt-assembler.js.map +1 -1
  153. package/dist/prompt-coverage.d.ts +0 -6
  154. package/dist/prompt-coverage.d.ts.map +1 -1
  155. package/dist/prompt-coverage.js +4 -8
  156. package/dist/prompt-coverage.js.map +1 -1
  157. package/dist/prompt-freshness.d.ts +6 -17
  158. package/dist/prompt-freshness.d.ts.map +1 -1
  159. package/dist/prompt-freshness.js +27 -22
  160. package/dist/prompt-freshness.js.map +1 -1
  161. package/dist/prompt-policy.d.ts +14 -7
  162. package/dist/prompt-policy.d.ts.map +1 -1
  163. package/dist/prompt-policy.js +21 -5
  164. package/dist/prompt-policy.js.map +1 -1
  165. package/dist/protocol.d.ts +843 -424
  166. package/dist/protocol.d.ts.map +1 -1
  167. package/dist/protocol.js +73 -102
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/recovery-guidance.d.ts +13 -0
  170. package/dist/recovery-guidance.d.ts.map +1 -0
  171. package/dist/recovery-guidance.js +15 -0
  172. package/dist/recovery-guidance.js.map +1 -0
  173. package/dist/recovery.d.ts +68 -0
  174. package/dist/recovery.d.ts.map +1 -0
  175. package/dist/recovery.js +234 -0
  176. package/dist/recovery.js.map +1 -0
  177. package/dist/replay.d.ts +42 -8
  178. package/dist/replay.d.ts.map +1 -1
  179. package/dist/replay.js +311 -84
  180. package/dist/replay.js.map +1 -1
  181. package/dist/result-facts.d.ts +45 -0
  182. package/dist/result-facts.d.ts.map +1 -0
  183. package/dist/result-facts.js +86 -0
  184. package/dist/result-facts.js.map +1 -0
  185. package/dist/round-contract.d.ts +38 -8
  186. package/dist/round-contract.d.ts.map +1 -1
  187. package/dist/round-contract.js +71 -31
  188. package/dist/round-contract.js.map +1 -1
  189. package/dist/round-coordinator.d.ts +41 -89
  190. package/dist/round-coordinator.d.ts.map +1 -1
  191. package/dist/round-coordinator.js +250 -267
  192. package/dist/round-coordinator.js.map +1 -1
  193. package/dist/round-driver.d.ts +56 -16
  194. package/dist/round-driver.d.ts.map +1 -1
  195. package/dist/round-driver.js +225 -39
  196. package/dist/round-driver.js.map +1 -1
  197. package/dist/round-facts.d.ts +33 -52
  198. package/dist/round-facts.d.ts.map +1 -1
  199. package/dist/round-facts.js +51 -160
  200. package/dist/round-facts.js.map +1 -1
  201. package/dist/round-guidance.d.ts +56 -0
  202. package/dist/round-guidance.d.ts.map +1 -0
  203. package/dist/round-guidance.js +161 -0
  204. package/dist/round-guidance.js.map +1 -0
  205. package/dist/round-outcome.d.ts +21 -0
  206. package/dist/round-outcome.d.ts.map +1 -0
  207. package/dist/round-outcome.js +31 -0
  208. package/dist/round-outcome.js.map +1 -0
  209. package/dist/round-transaction.d.ts +168 -21
  210. package/dist/round-transaction.d.ts.map +1 -1
  211. package/dist/round-transaction.js +837 -38
  212. package/dist/round-transaction.js.map +1 -1
  213. package/dist/self-eval.d.ts +37 -61
  214. package/dist/self-eval.d.ts.map +1 -1
  215. package/dist/self-eval.js +413 -392
  216. package/dist/self-eval.js.map +1 -1
  217. package/dist/storage.d.ts +16 -0
  218. package/dist/storage.d.ts.map +1 -1
  219. package/dist/storage.js +54 -4
  220. package/dist/storage.js.map +1 -1
  221. package/dist/subgoal-state.d.ts +2 -3
  222. package/dist/subgoal-state.d.ts.map +1 -1
  223. package/dist/subgoal-state.js +2 -3
  224. package/dist/subgoal-state.js.map +1 -1
  225. package/dist/submission-guidance.d.ts +29 -21
  226. package/dist/submission-guidance.d.ts.map +1 -1
  227. package/dist/submission-guidance.js +308 -260
  228. package/dist/submission-guidance.js.map +1 -1
  229. package/dist/submission-receipt.d.ts +16 -8
  230. package/dist/submission-receipt.d.ts.map +1 -1
  231. package/dist/submission-receipt.js +145 -51
  232. package/dist/submission-receipt.js.map +1 -1
  233. package/dist/token-utils.d.ts +28 -7
  234. package/dist/token-utils.d.ts.map +1 -1
  235. package/dist/token-utils.js +27 -11
  236. package/dist/token-utils.js.map +1 -1
  237. package/dist/transaction-identity.d.ts +9 -0
  238. package/dist/transaction-identity.d.ts.map +1 -0
  239. package/dist/transaction-identity.js +69 -0
  240. package/dist/transaction-identity.js.map +1 -0
  241. package/dist/verification-actions.d.ts.map +1 -1
  242. package/dist/verification-actions.js +10 -11
  243. package/dist/verification-actions.js.map +1 -1
  244. package/dist/verification-gate.d.ts +23 -47
  245. package/dist/verification-gate.d.ts.map +1 -1
  246. package/dist/verification-gate.js +202 -423
  247. package/dist/verification-gate.js.map +1 -1
  248. package/dist/version.d.ts +1 -1
  249. package/dist/version.js +1 -1
  250. package/dist/working-memory.d.ts +187 -0
  251. package/dist/working-memory.d.ts.map +1 -0
  252. package/dist/working-memory.js +767 -0
  253. package/dist/working-memory.js.map +1 -0
  254. package/dist/workspace-checkpoint.d.ts +88 -0
  255. package/dist/workspace-checkpoint.d.ts.map +1 -0
  256. package/dist/workspace-checkpoint.js +326 -0
  257. package/dist/workspace-checkpoint.js.map +1 -0
  258. package/dist/workspace-restore.d.ts +68 -0
  259. package/dist/workspace-restore.d.ts.map +1 -0
  260. package/dist/workspace-restore.js +604 -0
  261. package/dist/workspace-restore.js.map +1 -0
  262. package/dist/workspace.d.ts +0 -9
  263. package/dist/workspace.d.ts.map +1 -1
  264. package/dist/workspace.js +0 -17
  265. package/dist/workspace.js.map +1 -1
  266. package/loop_policy.json +4 -18
  267. package/package.json +68 -68
  268. package/skills/forge/SKILL.md +212 -0
  269. package/skills/forge/references/evidence-and-contracts.md +204 -0
  270. package/skills/forge/references/recovery-and-views.md +188 -0
  271. package/dist/cognitive-facts.d.ts +0 -30
  272. package/dist/cognitive-facts.d.ts.map +0 -1
  273. package/dist/cognitive-facts.js +0 -113
  274. package/dist/cognitive-facts.js.map +0 -1
  275. package/dist/loop-extras-parser.d.ts +0 -81
  276. package/dist/loop-extras-parser.d.ts.map +0 -1
  277. package/dist/loop-extras-parser.js +0 -213
  278. package/dist/loop-extras-parser.js.map +0 -1
  279. package/dist/loop-projection.d.ts +0 -6
  280. package/dist/loop-projection.d.ts.map +0 -1
  281. package/dist/loop-projection.js +0 -28
  282. package/dist/loop-projection.js.map +0 -1
  283. 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 { deriveObservationDeltas } from "./evidence-provider.js";
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 { GIT_OBSERVATION_SCHEMA_VERSION, PROMPT_ARTIFACT_SCHEMA_VERSION, } from "./protocol.js";
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
- /** v3.9.1: schema 3 embeds Git observation schema 2 (complete boundary
17
- * fingerprints) and later adds optional diagnostics/manifest fields. */
18
- export const ROUND_TRANSACTION_SCHEMA_VERSION = 3;
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
- roundId: makeRoundId(loopId, round),
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
- return ["continue", "stop", "reject", "terminate", "backtrack"].includes(String(value.action)) && Array.isArray(value.verificationFlags);
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. A
144
- // mismatched Git observation is a hard break, reported by
145
- // legacyTransactionRounds as an "observation" envelope loss.
146
- const expectedVersion = item.kind === "git"
147
- ? GIT_OBSERVATION_SCHEMA_VERSION
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 round always carries its attempted manifest. Its
218
- // absence is not a supported legacy shape.
219
- if ((value.phase === "evaluated" || value.phase === "committed") &&
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(engine, store, commitStore) {
271
- this.engine = engine;
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
- this.engine.autoFeedback(input.selfEval, snapshot.loopId, snapshot.round, input.task, metadata);
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
- if (result.action === "backtrack") {
375
- this.engine.invalidateHydrationCache(snapshot.loopId);
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 = this.readCommitted(snapshot);
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.roundId, opts?.skipBacktrack === true);
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, expectedRoundId, skipBacktrack = false) {
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 !== expectedRoundId)
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
- // R4/R9 terminate guards never run because evaluation is short-circuited).
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.