@remnic/core 9.7.10 → 9.7.12

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 (297) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +7 -7
  3. package/dist/access-boundary.d.ts +7 -7
  4. package/dist/access-boundary.js +8 -8
  5. package/dist/access-cli.js +31 -31
  6. package/dist/access-http.d.ts +8 -7
  7. package/dist/access-http.js +12 -12
  8. package/dist/access-identity-continuity-surface.d.ts +5 -5
  9. package/dist/access-identity-continuity-surface.js +7 -7
  10. package/dist/access-lcm-surface.d.ts +6 -6
  11. package/dist/access-lcm-surface.js +7 -7
  12. package/dist/access-mcp.d.ts +6 -6
  13. package/dist/access-mcp.js +11 -11
  14. package/dist/access-observe-write-surface.d.ts +6 -6
  15. package/dist/access-observe-write-surface.js +7 -7
  16. package/dist/access-operations-batch.js +9 -9
  17. package/dist/access-operations.d.ts +10 -10
  18. package/dist/access-operations.js +10 -10
  19. package/dist/access-recall-surface.d.ts +6 -6
  20. package/dist/access-recall-surface.js +7 -7
  21. package/dist/access-schema.d.ts +64 -64
  22. package/dist/{access-service-DSuzikHQ.d.ts → access-service-tHd3eQpd.d.ts} +4 -4
  23. package/dist/access-service.d.ts +6 -6
  24. package/dist/access-service.js +7 -7
  25. package/dist/access-surface-catalog.d.ts +6 -6
  26. package/dist/access-surface-catalog.js +2 -0
  27. package/dist/access-surface-catalog.js.map +1 -1
  28. package/dist/action-confidence.d.ts +1 -1
  29. package/dist/active-memory-bridge.d.ts +1 -1
  30. package/dist/active-recall.d.ts +1 -1
  31. package/dist/active-recall.js +1 -1
  32. package/dist/behavior-learner.d.ts +1 -1
  33. package/dist/behavior-signals.d.ts +1 -1
  34. package/dist/bootstrap.d.ts +5 -5
  35. package/dist/briefing.d.ts +2 -2
  36. package/dist/briefing.js +3 -3
  37. package/dist/buffer-surprise-report.d.ts +1 -1
  38. package/dist/buffer.d.ts +2 -2
  39. package/dist/bulk-import/index.d.ts +3 -3
  40. package/dist/calibration.d.ts +1 -1
  41. package/dist/capabilities.d.ts +1 -1
  42. package/dist/{catalog-BNwjACa5.d.ts → catalog-DEMiw6HZ.d.ts} +48 -1
  43. package/dist/causal-behavior.d.ts +1 -1
  44. package/dist/causal-consolidation.d.ts +1 -1
  45. package/dist/causal-consolidation.js +4 -4
  46. package/dist/causal-trajectory-graph.d.ts +1 -1
  47. package/dist/{chunk-U4KHQTJ6.js → chunk-2AL7N3BH.js} +4 -2
  48. package/dist/{chunk-U4KHQTJ6.js.map → chunk-2AL7N3BH.js.map} +1 -1
  49. package/dist/{chunk-L4H6MQ7I.js → chunk-2RSL6YRA.js} +2 -2
  50. package/dist/{chunk-7EX4YOKH.js → chunk-2ZPQUTMW.js} +4 -4
  51. package/dist/{chunk-673NPXE3.js → chunk-3SIC3E65.js} +2 -2
  52. package/dist/{chunk-OVMS3DCB.js → chunk-675YU5IK.js} +2 -2
  53. package/dist/{chunk-7LMEY5KZ.js → chunk-6N7R7TR2.js} +34 -1
  54. package/dist/chunk-6N7R7TR2.js.map +1 -0
  55. package/dist/{chunk-7CKS4AXY.js → chunk-AOWPZ7TM.js} +17 -17
  56. package/dist/{chunk-TGBJ2OMK.js → chunk-BQTSGK5Y.js} +2 -2
  57. package/dist/{chunk-UZRIKUFS.js → chunk-CE3GMHT4.js} +4 -2
  58. package/dist/chunk-CE3GMHT4.js.map +1 -0
  59. package/dist/{chunk-YXOAQAH2.js → chunk-CLFY35ED.js} +3 -3
  60. package/dist/{chunk-HQ7QZJM5.js → chunk-D4TLWDYR.js} +2 -2
  61. package/dist/{chunk-LQKITFGB.js → chunk-FUUF7FMX.js} +2 -2
  62. package/dist/{chunk-3ZVFLMFQ.js → chunk-G5PVJJRA.js} +1 -1
  63. package/dist/chunk-G5PVJJRA.js.map +1 -0
  64. package/dist/{chunk-MXRMNKSY.js → chunk-I3GKTDM2.js} +3 -3
  65. package/dist/{chunk-EQF5763Z.js → chunk-IQULRIZM.js} +2 -2
  66. package/dist/{chunk-DJCKXI4M.js → chunk-NVXQTRBL.js} +2 -2
  67. package/dist/{chunk-VDLMAJXS.js → chunk-OCY3CRPG.js} +3 -3
  68. package/dist/{chunk-YLTG6BO4.js → chunk-OIFZII6J.js} +2 -2
  69. package/dist/{chunk-XF4KOVDK.js → chunk-PA6DMYEK.js} +6 -6
  70. package/dist/{chunk-3OQ4IZJN.js → chunk-QPZCAVDC.js} +7 -7
  71. package/dist/{chunk-DLWYAN6A.js → chunk-T6U7HDIY.js} +2 -2
  72. package/dist/chunk-TITSEN2M.js +1794 -0
  73. package/dist/chunk-TITSEN2M.js.map +1 -0
  74. package/dist/{chunk-JZM5QUTE.js → chunk-TMGK6YE7.js} +171 -9
  75. package/dist/chunk-TMGK6YE7.js.map +1 -0
  76. package/dist/{chunk-KXLRMM35.js → chunk-TRH533CA.js} +30 -7
  77. package/dist/chunk-TRH533CA.js.map +1 -0
  78. package/dist/{chunk-UTDA43TB.js → chunk-TWGGNUWJ.js} +2 -2
  79. package/dist/{chunk-SBFDL2EQ.js → chunk-UTZW3X2R.js} +2 -2
  80. package/dist/{chunk-SECMZACR.js → chunk-XUQUBZ7O.js} +2 -2
  81. package/dist/{chunk-N3LWQJ6Y.js → chunk-YI7XUXLX.js} +309 -16
  82. package/dist/chunk-YI7XUXLX.js.map +1 -0
  83. package/dist/{chunk-QVALQE4P.js → chunk-ZASJ53ML.js} +128 -10
  84. package/dist/chunk-ZASJ53ML.js.map +1 -0
  85. package/dist/{chunk-WUDBMLC6.js → chunk-ZC3HG22I.js} +2 -2
  86. package/dist/{chunk-VCEH5UYS.js → chunk-ZRXSHGXT.js} +2 -2
  87. package/dist/{cli-DACyuK8-.d.ts → cli-D8Tsa4Sh.d.ts} +5 -5
  88. package/dist/cli.d.ts +8 -8
  89. package/dist/cli.js +25 -25
  90. package/dist/compounding/engine.d.ts +2 -2
  91. package/dist/compounding/engine.js +3 -3
  92. package/dist/compounding/preference-consolidator.d.ts +1 -1
  93. package/dist/compression-optimizer.d.ts +1 -1
  94. package/dist/config.d.ts +1 -1
  95. package/dist/config.js +1 -1
  96. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  97. package/dist/connectors/codex-materialize-runner.js +3 -3
  98. package/dist/connectors/codex-materialize.d.ts +1 -1
  99. package/dist/connectors/index.d.ts +1 -1
  100. package/dist/connectors/index.js +3 -3
  101. package/dist/consolidation-provenance-check.d.ts +2 -2
  102. package/dist/consolidation-undo.d.ts +2 -2
  103. package/dist/contradiction/index.d.ts +2 -2
  104. package/dist/conversation-index/backend.d.ts +1 -1
  105. package/dist/conversation-index/chunker.d.ts +1 -1
  106. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  107. package/dist/conversation-index/indexer.d.ts +1 -1
  108. package/dist/conversation-index/search.d.ts +1 -1
  109. package/dist/day-summary.d.ts +1 -1
  110. package/dist/delinearize.d.ts +1 -1
  111. package/dist/direct-answer-wiring.d.ts +1 -1
  112. package/dist/direct-answer.d.ts +1 -1
  113. package/dist/embedding-fallback.d.ts +1 -1
  114. package/dist/enrichment/index.d.ts +1 -1
  115. package/dist/entity-retrieval.d.ts +2 -2
  116. package/dist/entity-retrieval.js +3 -3
  117. package/dist/entity-schema.d.ts +1 -1
  118. package/dist/explicit-capture.d.ts +5 -5
  119. package/dist/extraction-faithfulness.d.ts +1 -1
  120. package/dist/extraction-judge-telemetry.d.ts +1 -1
  121. package/dist/extraction-judge-training.d.ts +1 -1
  122. package/dist/extraction-judge.d.ts +1 -1
  123. package/dist/extraction.d.ts +1 -1
  124. package/dist/fallback-llm.d.ts +1 -1
  125. package/dist/graph-dashboard-diff.d.ts +1 -1
  126. package/dist/graph-dashboard-key.d.ts +1 -1
  127. package/dist/graph-dashboard-parser.d.ts +1 -1
  128. package/dist/graph-edge-reinforcement.d.ts +1 -1
  129. package/dist/graph-snapshot.d.ts +1 -1
  130. package/dist/graph.d.ts +1 -1
  131. package/dist/identity-continuity.d.ts +1 -1
  132. package/dist/importance.d.ts +1 -1
  133. package/dist/importers/index.d.ts +1 -1
  134. package/dist/in-flight-reads.d.ts +1 -1
  135. package/dist/index.d.ts +4002 -16
  136. package/dist/index.js +78 -31
  137. package/dist/intent.d.ts +1 -1
  138. package/dist/lcm/engine.d.ts +1 -1
  139. package/dist/lcm/index.d.ts +1 -1
  140. package/dist/lcm/tools.d.ts +1 -1
  141. package/dist/lifecycle.d.ts +1 -1
  142. package/dist/live-connectors-runner.d.ts +1 -1
  143. package/dist/local-llm.d.ts +1 -1
  144. package/dist/local-model-endpoint.d.ts +1 -1
  145. package/dist/maintenance/memory-governance.d.ts +1 -1
  146. package/dist/maintenance/memory-governance.js +3 -3
  147. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  148. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  149. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  150. package/dist/memory-action-policy.d.ts +1 -1
  151. package/dist/memory-cache.d.ts +1 -1
  152. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  153. package/dist/memory-projection-store.d.ts +1 -1
  154. package/dist/memory-provenance.d.ts +1 -1
  155. package/dist/memory-worth-outcomes.d.ts +2 -2
  156. package/dist/models-json.d.ts +1 -1
  157. package/dist/namespaces/migrate.d.ts +3 -3
  158. package/dist/namespaces/migrate.js +4 -4
  159. package/dist/namespaces/principal.d.ts +1 -1
  160. package/dist/namespaces/search.d.ts +1 -1
  161. package/dist/namespaces/storage.d.ts +3 -3
  162. package/dist/namespaces/storage.js +3 -3
  163. package/dist/native-knowledge.d.ts +1 -1
  164. package/dist/operator-toolkit.d.ts +2 -2
  165. package/dist/operator-toolkit.js +8 -8
  166. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  167. package/dist/orchestration/maintenance.d.ts +2 -2
  168. package/dist/orchestration/maintenance.js +5 -5
  169. package/dist/{orchestrator-D83AvrPf.d.ts → orchestrator-B_WXBmuE.d.ts} +4 -4
  170. package/dist/orchestrator.d.ts +5 -5
  171. package/dist/orchestrator.js +31 -31
  172. package/dist/patterns-cli.d.ts +1 -1
  173. package/dist/{pipeline-BSqGpYl1.d.ts → pipeline-Dvtvt141.d.ts} +1 -1
  174. package/dist/policy-runtime.d.ts +1 -1
  175. package/dist/provenance.d.ts +1 -1
  176. package/dist/{qmd-CutnpRVe.d.ts → qmd-DwBLh_hl.d.ts} +1 -1
  177. package/dist/qmd-preflight.d.ts +2 -2
  178. package/dist/qmd-recall-cache.d.ts +1 -1
  179. package/dist/qmd.d.ts +2 -2
  180. package/dist/recall-disclosure-escalation.d.ts +1 -1
  181. package/dist/recall-explain-renderer.d.ts +1 -1
  182. package/dist/recall-explain-renderer.js +3 -3
  183. package/dist/recall-planner-llm.d.ts +1 -1
  184. package/dist/recall-state.d.ts +1 -1
  185. package/dist/recall-tag-filter.d.ts +1 -1
  186. package/dist/recall-timings.d.ts +1 -1
  187. package/dist/recall-xray-cli.d.ts +1 -1
  188. package/dist/recall-xray-cli.js +4 -4
  189. package/dist/recall-xray-renderer.d.ts +1 -1
  190. package/dist/recall-xray-renderer.js +3 -3
  191. package/dist/recall-xray.d.ts +1 -1
  192. package/dist/recall-xray.js +2 -2
  193. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  194. package/dist/replay/normalizers/claude.d.ts +1 -1
  195. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  196. package/dist/replay/normalizers/shared.d.ts +1 -1
  197. package/dist/replay/runner.d.ts +1 -1
  198. package/dist/replay/types.d.ts +1 -1
  199. package/dist/resolve-auth-token.d.ts +1 -1
  200. package/dist/resume-bundles.js +2 -2
  201. package/dist/retrieval-agents.d.ts +2 -2
  202. package/dist/retrieval-tiers.d.ts +1 -1
  203. package/dist/routing/engine.d.ts +1 -1
  204. package/dist/routing/store.d.ts +1 -1
  205. package/dist/schemas.d.ts +52 -52
  206. package/dist/search/embed-helper.d.ts +1 -1
  207. package/dist/search/factory.d.ts +1 -1
  208. package/dist/search/index.d.ts +1 -1
  209. package/dist/search/lancedb-backend.d.ts +1 -1
  210. package/dist/search/meilisearch-backend.d.ts +1 -1
  211. package/dist/search/noop-backend.d.ts +1 -1
  212. package/dist/search/orama-backend.d.ts +1 -1
  213. package/dist/search/port.d.ts +1 -1
  214. package/dist/search/remote-backend.d.ts +1 -1
  215. package/dist/{semantic-consolidation-btIVRC4l.d.ts → semantic-consolidation-Dia_6FOn.d.ts} +1 -1
  216. package/dist/semantic-consolidation.d.ts +2 -2
  217. package/dist/semantic-consolidation.js +4 -4
  218. package/dist/semantic-rule-promotion.js +3 -3
  219. package/dist/semantic-rule-verifier.d.ts +1 -1
  220. package/dist/semantic-rule-verifier.js +3 -3
  221. package/dist/session-observer-bands.d.ts +1 -1
  222. package/dist/session-observer-state.d.ts +1 -1
  223. package/dist/shared-context/manager.d.ts +9 -9
  224. package/dist/signal.d.ts +1 -1
  225. package/dist/{storage-BVv_Mqa0.d.ts → storage-RshM7i8-.d.ts} +18 -1
  226. package/dist/storage.d.ts +2 -2
  227. package/dist/storage.js +2 -2
  228. package/dist/summarizer.d.ts +1 -1
  229. package/dist/summary-snapshot.d.ts +1 -1
  230. package/dist/temporal-supersession.d.ts +2 -2
  231. package/dist/temporal-timeline-recall.d.ts +1 -1
  232. package/dist/temporal-validity.d.ts +1 -1
  233. package/dist/threading.d.ts +1 -1
  234. package/dist/tier-migration.d.ts +2 -2
  235. package/dist/tier-routing.d.ts +1 -1
  236. package/dist/topics.d.ts +1 -1
  237. package/dist/transcript.d.ts +1 -1
  238. package/dist/transfer/types.d.ts +66 -66
  239. package/dist/trust-score-stage.d.ts +1 -1
  240. package/dist/trust-score.d.ts +1 -1
  241. package/dist/{types-Cmkz7csa.d.ts → types-CHSXOq7s.d.ts} +34 -0
  242. package/dist/types.d.ts +1 -1
  243. package/dist/types.js +1 -1
  244. package/dist/utility-runtime.d.ts +1 -1
  245. package/dist/verified-recall.js +3 -3
  246. package/package.json +2 -2
  247. package/src/access-boundary.ts +2 -0
  248. package/src/access-http.ts +147 -6
  249. package/src/access-mcp-cancellation.test.ts +2 -2
  250. package/src/access-operations.ts +2 -0
  251. package/src/access-surface-catalog.ts +2 -0
  252. package/src/config.test.ts +44 -0
  253. package/src/config.ts +43 -0
  254. package/src/index.ts +53 -0
  255. package/src/namespaces/catalog.test.ts +258 -0
  256. package/src/namespaces/catalog.ts +235 -10
  257. package/src/namespaces/storage.ts +9 -0
  258. package/src/orchestrator.ts +3 -0
  259. package/src/relay/mission-access.test.ts +379 -0
  260. package/src/relay/mission-access.ts +125 -0
  261. package/src/relay/mission-fixture.ts +301 -0
  262. package/src/relay/mission.test.ts +964 -0
  263. package/src/relay/mission.ts +1725 -0
  264. package/src/storage-catalog-touch-gating.test.ts +70 -0
  265. package/src/storage.ts +31 -5
  266. package/src/types.ts +34 -0
  267. package/dist/chunk-3ZVFLMFQ.js.map +0 -1
  268. package/dist/chunk-7LMEY5KZ.js.map +0 -1
  269. package/dist/chunk-FNJX2O6G.js +0 -467
  270. package/dist/chunk-FNJX2O6G.js.map +0 -1
  271. package/dist/chunk-JZM5QUTE.js.map +0 -1
  272. package/dist/chunk-KXLRMM35.js.map +0 -1
  273. package/dist/chunk-N3LWQJ6Y.js.map +0 -1
  274. package/dist/chunk-QVALQE4P.js.map +0 -1
  275. package/dist/chunk-UZRIKUFS.js.map +0 -1
  276. /package/dist/{chunk-L4H6MQ7I.js.map → chunk-2RSL6YRA.js.map} +0 -0
  277. /package/dist/{chunk-7EX4YOKH.js.map → chunk-2ZPQUTMW.js.map} +0 -0
  278. /package/dist/{chunk-673NPXE3.js.map → chunk-3SIC3E65.js.map} +0 -0
  279. /package/dist/{chunk-OVMS3DCB.js.map → chunk-675YU5IK.js.map} +0 -0
  280. /package/dist/{chunk-7CKS4AXY.js.map → chunk-AOWPZ7TM.js.map} +0 -0
  281. /package/dist/{chunk-TGBJ2OMK.js.map → chunk-BQTSGK5Y.js.map} +0 -0
  282. /package/dist/{chunk-YXOAQAH2.js.map → chunk-CLFY35ED.js.map} +0 -0
  283. /package/dist/{chunk-HQ7QZJM5.js.map → chunk-D4TLWDYR.js.map} +0 -0
  284. /package/dist/{chunk-LQKITFGB.js.map → chunk-FUUF7FMX.js.map} +0 -0
  285. /package/dist/{chunk-MXRMNKSY.js.map → chunk-I3GKTDM2.js.map} +0 -0
  286. /package/dist/{chunk-EQF5763Z.js.map → chunk-IQULRIZM.js.map} +0 -0
  287. /package/dist/{chunk-DJCKXI4M.js.map → chunk-NVXQTRBL.js.map} +0 -0
  288. /package/dist/{chunk-VDLMAJXS.js.map → chunk-OCY3CRPG.js.map} +0 -0
  289. /package/dist/{chunk-YLTG6BO4.js.map → chunk-OIFZII6J.js.map} +0 -0
  290. /package/dist/{chunk-XF4KOVDK.js.map → chunk-PA6DMYEK.js.map} +0 -0
  291. /package/dist/{chunk-3OQ4IZJN.js.map → chunk-QPZCAVDC.js.map} +0 -0
  292. /package/dist/{chunk-DLWYAN6A.js.map → chunk-T6U7HDIY.js.map} +0 -0
  293. /package/dist/{chunk-UTDA43TB.js.map → chunk-TWGGNUWJ.js.map} +0 -0
  294. /package/dist/{chunk-SBFDL2EQ.js.map → chunk-UTZW3X2R.js.map} +0 -0
  295. /package/dist/{chunk-SECMZACR.js.map → chunk-XUQUBZ7O.js.map} +0 -0
  296. /package/dist/{chunk-WUDBMLC6.js.map → chunk-ZC3HG22I.js.map} +0 -0
  297. /package/dist/{chunk-VCEH5UYS.js.map → chunk-ZRXSHGXT.js.map} +0 -0
@@ -0,0 +1,1725 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { promises as fs, constants as fsConstants } from "node:fs";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+
6
+ import { serializeMutations, withHeldFileLock } from "../utils/serialize-mutations.js";
7
+
8
+ export const RELAY_MISSION_SCHEMA_VERSION = "1" as const;
9
+ export const RELAY_MISSION_MAX_FILE_BYTES = 4 * 1024 * 1024;
10
+ export const RELAY_MISSION_MAX_LINE_BYTES = 64 * 1024;
11
+ export const RELAY_MISSION_MAX_EVENTS = 2_000;
12
+ export const RELAY_MISSION_DEFAULT_EVENT_LIMIT = 200;
13
+ export const RELAY_MISSION_MAX_EVENT_LIMIT = 500;
14
+
15
+ export const RelayMissionIdSchema = z
16
+ .string()
17
+ .min(1)
18
+ .max(64)
19
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
20
+ message: "must contain only lowercase letters, numbers, and single hyphens",
21
+ });
22
+
23
+ const identifierSchema = z
24
+ .string()
25
+ .min(1)
26
+ .max(128)
27
+ .regex(/^[A-Za-z0-9](?:[A-Za-z0-9._:@-]{0,126}[A-Za-z0-9])?$/, {
28
+ message: "must be a bounded identifier",
29
+ });
30
+
31
+ const namespaceSchema = z.string().trim().min(1).max(128);
32
+ const authenticatedPrincipalSchema = z.string().trim().min(1).max(256);
33
+ const shortTextSchema = z.string().trim().min(1).max(240);
34
+ const statementSchema = z.string().trim().min(1).max(4_000);
35
+ const uniqueIdentifierListSchema = (minimum: number, maximum: number) =>
36
+ z
37
+ .array(identifierSchema)
38
+ .min(minimum)
39
+ .max(maximum)
40
+ .refine((values) => new Set(values).size === values.length, "must not contain duplicate identifiers");
41
+
42
+ export const RelayIsoDateTimeSchema = z
43
+ .string()
44
+ .datetime({ offset: true })
45
+ .refine((value) => Number.isFinite(Date.parse(value)), "must be a representable timestamp");
46
+
47
+ export const RelayEvidenceRefSchema = z
48
+ .object({
49
+ kind: z.enum(["memory", "recall_audit", "source", "test", "commit", "correction", "agent_output", "approval"]),
50
+ id: identifierSchema,
51
+ label: shortTextSchema,
52
+ locator: z.string().trim().min(1).max(2_048).optional(),
53
+ capture: z.enum(["at_action", "historical_lookup", "fixture"]),
54
+ })
55
+ .strict();
56
+
57
+ export type RelayEvidenceRef = z.infer<typeof RelayEvidenceRefSchema>;
58
+
59
+ const evidenceListSchema = z.array(RelayEvidenceRefSchema).max(16);
60
+ const agentIdentityFields = {
61
+ agentId: identifierSchema,
62
+ sessionId: identifierSchema,
63
+ } as const;
64
+
65
+ const RelayMissionPayloadUnionSchema = z.discriminatedUnion("kind", [
66
+ z
67
+ .object({
68
+ kind: z.literal("mission_started"),
69
+ title: shortTextSchema,
70
+ objective: statementSchema,
71
+ runMode: z.enum(["live", "replay", "fixture"]),
72
+ evidence: evidenceListSchema.default([]),
73
+ })
74
+ .strict(),
75
+ z
76
+ .object({
77
+ kind: z.literal("agent_status"),
78
+ ...agentIdentityFields,
79
+ label: shortTextSchema,
80
+ role: shortTextSchema,
81
+ status: z.enum(["idle", "working", "blocked", "complete", "failed"]),
82
+ detail: statementSchema.optional(),
83
+ evidence: evidenceListSchema.default([]),
84
+ })
85
+ .strict(),
86
+ z
87
+ .object({
88
+ kind: z.literal("agent_output"),
89
+ ...agentIdentityFields,
90
+ outputId: identifierSchema,
91
+ summary: statementSchema,
92
+ evidence: evidenceListSchema.default([]),
93
+ })
94
+ .strict(),
95
+ z
96
+ .object({
97
+ kind: z.literal("belief_observed"),
98
+ ...agentIdentityFields,
99
+ beliefId: identifierSchema,
100
+ decisionId: identifierSchema,
101
+ statement: statementSchema,
102
+ confidence: z.number().finite().min(0).max(1).optional(),
103
+ evidence: evidenceListSchema.default([]),
104
+ })
105
+ .strict(),
106
+ z
107
+ .object({
108
+ kind: z.literal("conflict_detected"),
109
+ conflictId: identifierSchema,
110
+ decisionIds: uniqueIdentifierListSchema(2, 8),
111
+ agentIds: uniqueIdentifierListSchema(2, 16),
112
+ summary: statementSchema,
113
+ evidence: evidenceListSchema.default([]),
114
+ })
115
+ .strict(),
116
+ z
117
+ .object({
118
+ kind: z.literal("test_result"),
119
+ testId: identifierSchema,
120
+ decisionId: identifierSchema,
121
+ correctionId: identifierSchema.optional(),
122
+ command: z.string().trim().min(1).max(1_000),
123
+ status: z.enum(["passed", "failed", "error"]),
124
+ summary: statementSchema,
125
+ durationMs: z.number().int().nonnegative().max(3_600_000).optional(),
126
+ evidence: evidenceListSchema.default([]),
127
+ })
128
+ .strict(),
129
+ z
130
+ .object({
131
+ kind: z.literal("correction_proposed"),
132
+ correctionId: identifierSchema,
133
+ conflictId: identifierSchema,
134
+ proposedDecisionId: identifierSchema,
135
+ supersedesDecisionIds: uniqueIdentifierListSchema(1, 8),
136
+ statement: statementSchema,
137
+ rationale: statementSchema,
138
+ proposedBy: identifierSchema,
139
+ evidence: evidenceListSchema.default([]),
140
+ })
141
+ .strict(),
142
+ z
143
+ .object({
144
+ kind: z.literal("correction_approved"),
145
+ correctionId: identifierSchema,
146
+ approvedBy: z
147
+ .object({
148
+ kind: z.enum(["human", "agent"]),
149
+ id: identifierSchema,
150
+ label: shortTextSchema,
151
+ })
152
+ .strict(),
153
+ note: statementSchema.optional(),
154
+ evidence: evidenceListSchema.default([]),
155
+ })
156
+ .strict(),
157
+ z
158
+ .object({
159
+ kind: z.literal("decision_superseded"),
160
+ decisionId: identifierSchema,
161
+ replacementDecisionId: identifierSchema,
162
+ correctionId: identifierSchema,
163
+ evidence: evidenceListSchema.default([]),
164
+ })
165
+ .strict(),
166
+ z
167
+ .object({
168
+ kind: z.literal("recall_observed"),
169
+ ...agentIdentityFields,
170
+ recallReceiptId: identifierSchema,
171
+ decisionId: identifierSchema,
172
+ query: statementSchema,
173
+ capturedAtAction: z.boolean(),
174
+ evidence: evidenceListSchema.default([]),
175
+ })
176
+ .strict(),
177
+ z
178
+ .object({
179
+ kind: z.literal("propagation_verified"),
180
+ ...agentIdentityFields,
181
+ correctionId: identifierSchema,
182
+ decisionId: identifierSchema,
183
+ recallReceiptId: identifierSchema,
184
+ staleDecisionAbsent: z.boolean(),
185
+ evidence: evidenceListSchema.default([]),
186
+ })
187
+ .strict(),
188
+ z
189
+ .object({
190
+ kind: z.literal("mission_completed"),
191
+ outcome: z.enum(["recovered", "failed", "inconclusive"]),
192
+ summary: statementSchema,
193
+ evidence: evidenceListSchema.default([]),
194
+ })
195
+ .strict(),
196
+ ]);
197
+
198
+ export const RelayMissionPayloadSchema = RelayMissionPayloadUnionSchema.superRefine((value, ctx) => {
199
+ if (value.kind === "decision_superseded" && value.decisionId === value.replacementDecisionId) {
200
+ ctx.addIssue({
201
+ code: z.ZodIssueCode.custom,
202
+ message: "a decision cannot supersede itself",
203
+ path: ["replacementDecisionId"],
204
+ });
205
+ }
206
+ if (value.kind === "correction_approved" && !value.evidence.some((item) => item.kind === "approval")) {
207
+ ctx.addIssue({
208
+ code: z.ZodIssueCode.custom,
209
+ message: "correction approval requires approval evidence",
210
+ path: ["evidence"],
211
+ });
212
+ }
213
+ if (
214
+ value.kind === "decision_superseded" &&
215
+ !value.evidence.some((item) => item.kind === "correction" && item.id === value.correctionId)
216
+ ) {
217
+ ctx.addIssue({
218
+ code: z.ZodIssueCode.custom,
219
+ message: "decision supersession requires matching correction evidence",
220
+ path: ["evidence"],
221
+ });
222
+ }
223
+ if (value.kind !== "recall_observed" && value.kind !== "propagation_verified") return;
224
+ const matchingRecallEvidence = value.evidence.find(
225
+ (item) => item.kind === "recall_audit" && item.id === value.recallReceiptId
226
+ );
227
+ if (!matchingRecallEvidence) {
228
+ ctx.addIssue({
229
+ code: z.ZodIssueCode.custom,
230
+ message: "recall proof requires a matching recall-audit reference",
231
+ path: ["evidence"],
232
+ });
233
+ return;
234
+ }
235
+ if (value.kind === "recall_observed") {
236
+ const expectedCapture = value.capturedAtAction ? "at_action" : "historical_lookup";
237
+ if (matchingRecallEvidence.capture !== expectedCapture && matchingRecallEvidence.capture !== "fixture") {
238
+ ctx.addIssue({
239
+ code: z.ZodIssueCode.custom,
240
+ message: `recall evidence must be labeled ${expectedCapture}`,
241
+ path: ["evidence", value.evidence.indexOf(matchingRecallEvidence), "capture"],
242
+ });
243
+ }
244
+ }
245
+ });
246
+
247
+ export type RelayMissionPayload = z.infer<typeof RelayMissionPayloadSchema>;
248
+
249
+ export const RelayMissionEventInputSchema = z
250
+ .object({
251
+ occurredAt: RelayIsoDateTimeSchema.optional(),
252
+ idempotencyKey: identifierSchema.optional(),
253
+ payload: RelayMissionPayloadSchema,
254
+ })
255
+ .strict();
256
+
257
+ export type RelayMissionEventInput = z.infer<typeof RelayMissionEventInputSchema>;
258
+
259
+ export const RelayMissionEventSchema = z
260
+ .object({
261
+ schemaVersion: z.literal(RELAY_MISSION_SCHEMA_VERSION),
262
+ eventId: identifierSchema,
263
+ missionId: RelayMissionIdSchema,
264
+ namespace: namespaceSchema,
265
+ authenticatedPrincipal: authenticatedPrincipalSchema.optional(),
266
+ recordedAt: RelayIsoDateTimeSchema,
267
+ occurredAt: RelayIsoDateTimeSchema,
268
+ idempotencyKey: identifierSchema.optional(),
269
+ payload: RelayMissionPayloadSchema,
270
+ })
271
+ .strict();
272
+
273
+ export type RelayMissionEvent = z.infer<typeof RelayMissionEventSchema>;
274
+
275
+ export const RelayMissionReadOptionsSchema = z
276
+ .object({
277
+ since: RelayIsoDateTimeSchema.optional(),
278
+ until: RelayIsoDateTimeSchema.optional(),
279
+ limit: z.number().int().min(1).max(RELAY_MISSION_MAX_EVENT_LIMIT).default(RELAY_MISSION_DEFAULT_EVENT_LIMIT),
280
+ })
281
+ .strict()
282
+ .refine(
283
+ (value) =>
284
+ value.since === undefined || value.until === undefined || Date.parse(value.since) < Date.parse(value.until),
285
+ { message: "since must be earlier than until", path: ["until"] }
286
+ );
287
+
288
+ export type RelayMissionReadOptions = z.input<typeof RelayMissionReadOptionsSchema>;
289
+
290
+ export type RelayMissionStatus =
291
+ | "not_started"
292
+ | "active"
293
+ | "awaiting_approval"
294
+ | "correcting"
295
+ | "verified"
296
+ | "completed"
297
+ | "failed";
298
+
299
+ export interface RelayAgentOutputSnapshot {
300
+ outputId: string;
301
+ summary: string;
302
+ occurredAt: string;
303
+ evidence: RelayEvidenceRef[];
304
+ }
305
+
306
+ export interface RelayRecallSnapshot {
307
+ recallReceiptId: string;
308
+ sessionId: string;
309
+ decisionId: string;
310
+ query: string;
311
+ capturedAtAction: boolean;
312
+ coldStart: boolean;
313
+ occurredAt: string;
314
+ evidence: RelayEvidenceRef[];
315
+ }
316
+
317
+ export interface RelayAgentSnapshot {
318
+ agentId: string;
319
+ label: string;
320
+ role: string;
321
+ status: "idle" | "working" | "blocked" | "complete" | "failed";
322
+ sessionIds: string[];
323
+ outputs: RelayAgentOutputSnapshot[];
324
+ recalls: RelayRecallSnapshot[];
325
+ lastSeenAt: string;
326
+ }
327
+
328
+ export interface RelayDecisionSnapshot {
329
+ decisionId: string;
330
+ statement: string;
331
+ status: "active" | "superseded" | "proposed";
332
+ heldByAgentIds: string[];
333
+ beliefIds: string[];
334
+ evidence: RelayEvidenceRef[];
335
+ firstObservedAt: string;
336
+ supersededAt?: string;
337
+ supersededBy?: string;
338
+ correctionId?: string;
339
+ }
340
+
341
+ export interface RelayConflictSnapshot {
342
+ conflictId: string;
343
+ decisionIds: string[];
344
+ agentIds: string[];
345
+ summary: string;
346
+ status: "open" | "proposed" | "resolved";
347
+ correctionId?: string;
348
+ occurredAt: string;
349
+ evidence: RelayEvidenceRef[];
350
+ }
351
+
352
+ export interface RelayCorrectionSnapshot {
353
+ correctionId: string;
354
+ conflictId: string;
355
+ proposedDecisionId: string;
356
+ supersedesDecisionIds: string[];
357
+ statement: string;
358
+ rationale: string;
359
+ proposedBy: string;
360
+ status: "proposed" | "approved" | "applied" | "propagated";
361
+ proposedAt: string;
362
+ approvedAt?: string;
363
+ approvedBy?: { kind: "human" | "agent"; id: string; label: string };
364
+ approvalNote?: string;
365
+ approvalPrincipal?: string;
366
+ appliedAt?: string;
367
+ propagatedAt?: string;
368
+ evidence: RelayEvidenceRef[];
369
+ }
370
+
371
+ export interface RelayTestSnapshot {
372
+ testId: string;
373
+ decisionId: string;
374
+ correctionId?: string;
375
+ command: string;
376
+ status: "passed" | "failed" | "error";
377
+ summary: string;
378
+ durationMs?: number;
379
+ occurredAt: string;
380
+ evidence: RelayEvidenceRef[];
381
+ }
382
+
383
+ export interface RelayPropagationSnapshot {
384
+ correctionId: string;
385
+ agentId: string;
386
+ sessionId: string;
387
+ decisionId: string;
388
+ recallReceiptId: string;
389
+ staleDecisionAbsent: boolean;
390
+ occurredAt: string;
391
+ evidence: RelayEvidenceRef[];
392
+ }
393
+
394
+ export interface RelayMissionSnapshot {
395
+ schemaVersion: typeof RELAY_MISSION_SCHEMA_VERSION;
396
+ missionId: string;
397
+ namespace: string;
398
+ found: boolean;
399
+ readHealth: "empty" | "ok" | "partial";
400
+ status: RelayMissionStatus;
401
+ mission: {
402
+ title: string | null;
403
+ objective: string | null;
404
+ runMode: "live" | "replay" | "fixture" | null;
405
+ startedAt: string | null;
406
+ completedAt: string | null;
407
+ };
408
+ agents: RelayAgentSnapshot[];
409
+ decisions: RelayDecisionSnapshot[];
410
+ conflicts: RelayConflictSnapshot[];
411
+ corrections: RelayCorrectionSnapshot[];
412
+ tests: RelayTestSnapshot[];
413
+ propagation: RelayPropagationSnapshot[];
414
+ outcome: {
415
+ result: "recovered" | "failed" | "inconclusive";
416
+ summary: string;
417
+ occurredAt: string;
418
+ evidence: RelayEvidenceRef[];
419
+ } | null;
420
+ receipt: {
421
+ complete: boolean;
422
+ missingEvidence: string[];
423
+ activeDecisionIds: string[];
424
+ supersededDecisionIds: string[];
425
+ coldStartVerified: boolean;
426
+ passingOutcomeVerified: boolean;
427
+ };
428
+ bounds: {
429
+ totalEvents: number;
430
+ returnedEvents: number;
431
+ corruptLines: number;
432
+ truncated: boolean;
433
+ since: string | null;
434
+ until: string | null;
435
+ };
436
+ events: RelayMissionEvent[];
437
+ }
438
+
439
+ export type RelayMissionStoreErrorCode =
440
+ | "idempotency_conflict"
441
+ | "limit_exceeded"
442
+ | "lock_unavailable"
443
+ | "read_failed"
444
+ | "unsafe_path"
445
+ | "write_failed";
446
+
447
+ export class RelayMissionStoreError extends Error {
448
+ constructor(
449
+ public readonly code: RelayMissionStoreErrorCode,
450
+ message: string,
451
+ options?: ErrorOptions
452
+ ) {
453
+ super(message, options);
454
+ this.name = "RelayMissionStoreError";
455
+ }
456
+ }
457
+
458
+ export interface RelayMissionStoreOptions {
459
+ rootDir: string;
460
+ namespace: string;
461
+ now?: () => Date;
462
+ createEventId?: () => string;
463
+ maxFileBytes?: number;
464
+ maxEvents?: number;
465
+ lockMaxWaitMs?: number;
466
+ }
467
+
468
+ interface MissionFileRead {
469
+ exists: boolean;
470
+ events: RelayMissionEvent[];
471
+ corruptLines: number;
472
+ rawBytes: number;
473
+ }
474
+
475
+ interface PinnedMissionDirectory {
476
+ logicalPath: string;
477
+ accessPath: string;
478
+ handle: import("node:fs/promises").FileHandle;
479
+ }
480
+
481
+ export interface RelayMissionAppendResult {
482
+ appended: boolean;
483
+ replayed: boolean;
484
+ event: RelayMissionEvent;
485
+ }
486
+
487
+ export class RelayMissionStore {
488
+ private readonly rootDir: string;
489
+ private readonly namespace: string;
490
+ private readonly now: () => Date;
491
+ private readonly createEventId: () => string;
492
+ private readonly maxFileBytes: number;
493
+ private readonly maxEvents: number;
494
+ private readonly lockMaxWaitMs: number;
495
+
496
+ constructor(options: RelayMissionStoreOptions) {
497
+ if (typeof options.rootDir !== "string" || options.rootDir.trim().length === 0) {
498
+ throw new TypeError("RelayMissionStore rootDir must be a non-empty string");
499
+ }
500
+ this.rootDir = path.resolve(options.rootDir);
501
+ this.namespace = namespaceSchema.parse(options.namespace);
502
+ this.now = options.now ?? (() => new Date());
503
+ this.createEventId = options.createEventId ?? (() => `relay-${randomUUID()}`);
504
+ this.maxFileBytes = positiveIntegerOption(options.maxFileBytes, "maxFileBytes", RELAY_MISSION_MAX_FILE_BYTES);
505
+ this.maxEvents = positiveIntegerOption(options.maxEvents, "maxEvents", RELAY_MISSION_MAX_EVENTS);
506
+ this.lockMaxWaitMs = positiveIntegerOption(options.lockMaxWaitMs, "lockMaxWaitMs", 5_000);
507
+ }
508
+
509
+ async append(
510
+ missionIdValue: string,
511
+ inputValue: RelayMissionEventInput,
512
+ hooks: { authenticatedPrincipal?: string; beforeAppend?: () => void | Promise<void> } = {}
513
+ ): Promise<RelayMissionAppendResult> {
514
+ const missionId = RelayMissionIdSchema.parse(missionIdValue);
515
+ const input = RelayMissionEventInputSchema.parse(inputValue);
516
+ const authenticatedPrincipal =
517
+ hooks.authenticatedPrincipal === undefined
518
+ ? undefined
519
+ : authenticatedPrincipalSchema.parse(hooks.authenticatedPrincipal);
520
+ const missionDir = await this.openMissionDirectory(true);
521
+ const filePath = path.join(missionDir.accessPath, `${missionId}.jsonl`);
522
+ const logicalFilePath = path.join(missionDir.logicalPath, `${missionId}.jsonl`);
523
+ const lockPath = `${filePath}.lock`;
524
+
525
+ try {
526
+ return await serializeMutations(`relay-mission:${logicalFilePath}`, () =>
527
+ withHeldFileLock(
528
+ lockPath,
529
+ { staleMs: 30_000, maxWaitMs: this.lockMaxWaitMs, heartbeatMs: 10_000 },
530
+ async (acquired) => {
531
+ if (!acquired) {
532
+ throw new RelayMissionStoreError("lock_unavailable", "could not acquire Relay mission lock");
533
+ }
534
+
535
+ const existing = await this.readFile(filePath, missionId);
536
+ if (input.idempotencyKey) {
537
+ const replay = existing.events.find((event) => event.idempotencyKey === input.idempotencyKey);
538
+ if (replay) {
539
+ if (!sameIdempotentInput(replay, input, authenticatedPrincipal)) {
540
+ throw new RelayMissionStoreError(
541
+ "idempotency_conflict",
542
+ `idempotency key ${input.idempotencyKey} was already used for different input`
543
+ );
544
+ }
545
+ return { appended: false, replayed: true, event: replay };
546
+ }
547
+ }
548
+
549
+ if (existing.events.length >= this.maxEvents) {
550
+ throw new RelayMissionStoreError(
551
+ "limit_exceeded",
552
+ `mission ${missionId} reached the ${this.maxEvents}-event limit`
553
+ );
554
+ }
555
+
556
+ // Transport quotas run only after idempotency replay detection and
557
+ // while this mission's mutation lock is held. A response-lost retry
558
+ // remains replayable even when the original append used the final
559
+ // quota slot.
560
+ await hooks.beforeAppend?.();
561
+
562
+ const recordedAt = validNow(this.now);
563
+ const event = RelayMissionEventSchema.parse({
564
+ schemaVersion: RELAY_MISSION_SCHEMA_VERSION,
565
+ eventId: this.createEventId(),
566
+ missionId,
567
+ namespace: this.namespace,
568
+ ...(authenticatedPrincipal === undefined ? {} : { authenticatedPrincipal }),
569
+ recordedAt,
570
+ occurredAt: input.occurredAt ?? recordedAt,
571
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
572
+ payload: input.payload,
573
+ });
574
+ const line = `${JSON.stringify(event)}\n`;
575
+ const lineBytes = Buffer.byteLength(line, "utf8");
576
+ if (lineBytes > RELAY_MISSION_MAX_LINE_BYTES) {
577
+ throw new RelayMissionStoreError(
578
+ "limit_exceeded",
579
+ `Relay mission event exceeds ${RELAY_MISSION_MAX_LINE_BYTES} bytes`
580
+ );
581
+ }
582
+ if (existing.rawBytes + lineBytes > this.maxFileBytes) {
583
+ throw new RelayMissionStoreError(
584
+ "limit_exceeded",
585
+ `mission ${missionId} would exceed the ${this.maxFileBytes}-byte limit`
586
+ );
587
+ }
588
+
589
+ await this.appendFile(filePath, line);
590
+ return { appended: true, replayed: false, event };
591
+ }
592
+ )
593
+ );
594
+ } finally {
595
+ await missionDir.handle.close().catch(() => undefined);
596
+ }
597
+ }
598
+
599
+ async read(missionIdValue: string, readOptions: RelayMissionReadOptions = {}): Promise<RelayMissionSnapshot> {
600
+ const missionId = RelayMissionIdSchema.parse(missionIdValue);
601
+ const options = RelayMissionReadOptionsSchema.parse(readOptions);
602
+ const missionDir = await this.openMissionDirectory(false);
603
+ if (missionDir === null) {
604
+ return reduceRelayMission({
605
+ missionId,
606
+ namespace: this.namespace,
607
+ events: [],
608
+ fileExists: false,
609
+ options,
610
+ });
611
+ }
612
+ const filePath = path.join(missionDir.accessPath, `${missionId}.jsonl`);
613
+ const logicalFilePath = path.join(missionDir.logicalPath, `${missionId}.jsonl`);
614
+
615
+ try {
616
+ // Coordinate with writers in this process without creating a lock file.
617
+ // Cross-process appends are single bounded O_APPEND records; a torn or
618
+ // malformed record remains explicit partial evidence rather than being
619
+ // silently accepted.
620
+ return await serializeMutations(`relay-mission:${logicalFilePath}`, async () => {
621
+ const read = await this.readFile(filePath, missionId);
622
+ return reduceRelayMission({
623
+ missionId,
624
+ namespace: this.namespace,
625
+ events: read.events,
626
+ corruptLines: read.corruptLines,
627
+ fileExists: read.exists,
628
+ options,
629
+ });
630
+ });
631
+ } finally {
632
+ await missionDir.handle.close().catch(() => undefined);
633
+ }
634
+ }
635
+
636
+ private async openMissionDirectory(create: true): Promise<PinnedMissionDirectory>;
637
+ private async openMissionDirectory(create: false): Promise<PinnedMissionDirectory | null>;
638
+ private async openMissionDirectory(create: boolean): Promise<PinnedMissionDirectory | null> {
639
+ let currentHandle: import("node:fs/promises").FileHandle | undefined;
640
+ try {
641
+ currentHandle = await openVerifiedDirectory(this.rootDir, "Relay root must be a real directory");
642
+ let logicalPath = this.rootDir;
643
+ for (const segment of ["state", "relay", "missions"]) {
644
+ let childHandle: import("node:fs/promises").FileHandle;
645
+ try {
646
+ childHandle = create
647
+ ? await openOrCreatePinnedChildDirectory(currentHandle, segment)
648
+ : await openVerifiedDirectory(
649
+ path.join(pinnedDirectoryPath(currentHandle), segment),
650
+ "Relay directory symlinks are rejected"
651
+ );
652
+ } catch (error) {
653
+ if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") {
654
+ await currentHandle.close();
655
+ return null;
656
+ }
657
+ throw error;
658
+ }
659
+ await currentHandle.close();
660
+ currentHandle = childHandle;
661
+ logicalPath = path.join(logicalPath, segment);
662
+ }
663
+ return {
664
+ logicalPath,
665
+ accessPath: pinnedDirectoryPath(currentHandle),
666
+ handle: currentHandle,
667
+ };
668
+ } catch (error) {
669
+ await currentHandle?.close().catch(() => undefined);
670
+ if (error instanceof RelayMissionStoreError) throw error;
671
+ throw new RelayMissionStoreError("unsafe_path", "Relay mission directory is unavailable or unsafe", {
672
+ cause: error,
673
+ });
674
+ }
675
+ }
676
+
677
+ private async readFile(filePath: string, missionId: string): Promise<MissionFileRead> {
678
+ let handle: import("node:fs/promises").FileHandle | undefined;
679
+ try {
680
+ handle = await fs.open(filePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
681
+ const info = await handle.stat();
682
+ if (!info.isFile()) {
683
+ throw new RelayMissionStoreError("unsafe_path", "Relay mission path is not a file");
684
+ }
685
+ if (info.nlink !== 1) {
686
+ throw new RelayMissionStoreError("unsafe_path", "Relay mission hard links are rejected");
687
+ }
688
+ if (info.size > this.maxFileBytes) {
689
+ throw new RelayMissionStoreError(
690
+ "limit_exceeded",
691
+ `mission ${missionId} exceeds the ${this.maxFileBytes}-byte read limit`
692
+ );
693
+ }
694
+ const raw = await handle.readFile("utf8");
695
+ const parsed = parseMissionJsonl(raw, missionId, this.namespace, this.maxEvents);
696
+ return { exists: true, rawBytes: Buffer.byteLength(raw, "utf8"), ...parsed };
697
+ } catch (error) {
698
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
699
+ return { exists: false, events: [], corruptLines: 0, rawBytes: 0 };
700
+ }
701
+ if (error instanceof RelayMissionStoreError) throw error;
702
+ throw new RelayMissionStoreError("read_failed", "failed to read Relay mission", {
703
+ cause: error,
704
+ });
705
+ } finally {
706
+ await handle?.close().catch(() => undefined);
707
+ }
708
+ }
709
+
710
+ private async appendFile(filePath: string, line: string): Promise<void> {
711
+ let handle: import("node:fs/promises").FileHandle | undefined;
712
+ try {
713
+ handle = await fs.open(
714
+ filePath,
715
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_NOFOLLOW,
716
+ 0o600
717
+ );
718
+ const info = await handle.stat();
719
+ if (!info.isFile()) {
720
+ throw new RelayMissionStoreError("unsafe_path", "Relay mission path is not a file");
721
+ }
722
+ if (info.nlink !== 1) {
723
+ throw new RelayMissionStoreError("unsafe_path", "Relay mission hard links are rejected");
724
+ }
725
+ await handle.chmod(0o600);
726
+ await handle.writeFile(line, "utf8");
727
+ await handle.sync();
728
+ } catch (error) {
729
+ if (error instanceof RelayMissionStoreError) throw error;
730
+ if ((error as NodeJS.ErrnoException).code === "ELOOP") {
731
+ throw new RelayMissionStoreError("unsafe_path", "Relay mission symlink is rejected", {
732
+ cause: error,
733
+ });
734
+ }
735
+ throw new RelayMissionStoreError("write_failed", "failed to append Relay mission", {
736
+ cause: error,
737
+ });
738
+ } finally {
739
+ await handle?.close().catch(() => undefined);
740
+ }
741
+ }
742
+ }
743
+
744
+ interface ReduceRelayMissionInput {
745
+ missionId: string;
746
+ namespace: string;
747
+ events: RelayMissionEvent[];
748
+ corruptLines?: number;
749
+ fileExists?: boolean;
750
+ options?: RelayMissionReadOptions;
751
+ }
752
+
753
+ export function reduceRelayMission(input: ReduceRelayMissionInput): RelayMissionSnapshot {
754
+ const missionId = RelayMissionIdSchema.parse(input.missionId);
755
+ const namespace = namespaceSchema.parse(input.namespace);
756
+ const options = RelayMissionReadOptionsSchema.parse(input.options ?? {});
757
+ const corruptLines = input.corruptLines ?? 0;
758
+ const sinceMs = options.since ? Date.parse(options.since) : undefined;
759
+ const untilMs = options.until ? Date.parse(options.until) : undefined;
760
+ const appendPositions = new Map(input.events.map((event, index) => [event, index] as const));
761
+ const orderedEvents = [...input.events].sort(compareRelayEvents);
762
+ const firstStartEvent = input.events.find((event) => event.payload.kind === "mission_started");
763
+ const firstCompletionEvent = input.events.find((event) => event.payload.kind === "mission_completed");
764
+ const firstStartPosition = firstStartEvent === undefined ? -1 : (appendPositions.get(firstStartEvent) ?? -1);
765
+ const firstCompletionPosition =
766
+ firstCompletionEvent === undefined ? -1 : (appendPositions.get(firstCompletionEvent) ?? -1);
767
+ const filteredEvents = orderedEvents.filter((event) => {
768
+ const occurredAt = Date.parse(event.occurredAt);
769
+ return (sinceMs === undefined || occurredAt >= sinceMs) && (untilMs === undefined || occurredAt < untilMs);
770
+ });
771
+
772
+ const agents = new Map<string, RelayAgentSnapshot>();
773
+ const decisions = new Map<string, RelayDecisionSnapshot>();
774
+ const conflicts = new Map<string, RelayConflictSnapshot>();
775
+ const corrections = new Map<string, RelayCorrectionSnapshot>();
776
+ const tests: RelayTestSnapshot[] = [];
777
+ const propagation: RelayPropagationSnapshot[] = [];
778
+ const beliefAppendPositions = new Map<string, number>();
779
+ const conflictAppendPositions = new Map<string, number>();
780
+ const correctionProposalAppendPositions = new Map<string, number>();
781
+ const correctionApprovalAppendPositions = new Map<string, number>();
782
+ const correctionAppliedAppendPositions = new Map<string, number>();
783
+ const recallAppendPositions = new Map<RelayRecallSnapshot, number>();
784
+ const propagationAppendPositions = new Map<RelayPropagationSnapshot, number>();
785
+ const testAppendPositions = new Map<RelayTestSnapshot, number>();
786
+ const missingEvidence = new Set<string>();
787
+ if (corruptLines > 0) missingEvidence.add("mission:corrupt-events");
788
+ const substantiveAgentIds = new Set<string>();
789
+ const substantiveSessionIds = new Set<string>();
790
+ const conflictDisagreementIds = new Set<string>();
791
+ const groundedCorrectionIds = new Set<string>();
792
+ let title: string | null = null;
793
+ let objective: string | null = null;
794
+ let runMode: "live" | "replay" | "fixture" | null = null;
795
+ let startedAt: string | null = null;
796
+ let completedAt: string | null = null;
797
+ let status: RelayMissionStatus = "not_started";
798
+ let outcome: RelayMissionSnapshot["outcome"] = null;
799
+
800
+ const ensureAgent = (agentId: string, sessionId: string, occurredAt: string): RelayAgentSnapshot => {
801
+ const existing = agents.get(agentId);
802
+ if (existing) {
803
+ if (!existing.sessionIds.includes(sessionId)) existing.sessionIds.push(sessionId);
804
+ existing.lastSeenAt = occurredAt;
805
+ return existing;
806
+ }
807
+ const created: RelayAgentSnapshot = {
808
+ agentId,
809
+ label: agentId,
810
+ role: "Codex agent",
811
+ status: "working",
812
+ sessionIds: [sessionId],
813
+ outputs: [],
814
+ recalls: [],
815
+ lastSeenAt: occurredAt,
816
+ };
817
+ agents.set(agentId, created);
818
+ return created;
819
+ };
820
+
821
+ const markSubstantiveParticipation = (agentId: string, sessionId?: string): void => {
822
+ substantiveAgentIds.add(agentId);
823
+ if (sessionId) substantiveSessionIds.add(sessionId);
824
+ };
825
+
826
+ for (const event of orderedEvents) {
827
+ const payload = event.payload;
828
+ const appendPosition = appendPositions.get(event) ?? -1;
829
+ if (
830
+ payload.kind !== "mission_started" &&
831
+ firstStartEvent !== undefined &&
832
+ (appendPosition < firstStartPosition || compareRelayEvents(event, firstStartEvent) < 0)
833
+ ) {
834
+ missingEvidence.add(`mission:event-before-start:${event.eventId}`);
835
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
836
+ continue;
837
+ }
838
+ if (
839
+ payload.kind !== "mission_completed" &&
840
+ (outcome !== null || (firstCompletionPosition >= 0 && appendPosition > firstCompletionPosition))
841
+ ) {
842
+ missingEvidence.add(`mission:event-after-completion:${event.eventId}`);
843
+ if (payload.kind === "mission_started") missingEvidence.add("mission:lifecycle-order");
844
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
845
+ continue;
846
+ }
847
+ switch (payload.kind) {
848
+ case "mission_started":
849
+ if (startedAt !== null) {
850
+ missingEvidence.add("mission:duplicate-start");
851
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
852
+ break;
853
+ }
854
+ title = payload.title;
855
+ objective = payload.objective;
856
+ runMode = payload.runMode;
857
+ startedAt = event.occurredAt;
858
+ if (outcome === null) status = "active";
859
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
860
+ break;
861
+ case "agent_status": {
862
+ const agent = ensureAgent(payload.agentId, payload.sessionId, event.occurredAt);
863
+ markSubstantiveParticipation(payload.agentId, payload.sessionId);
864
+ agent.label = payload.label;
865
+ agent.role = payload.role;
866
+ agent.status = payload.status;
867
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
868
+ break;
869
+ }
870
+ case "agent_output": {
871
+ const agent = ensureAgent(payload.agentId, payload.sessionId, event.occurredAt);
872
+ markSubstantiveParticipation(payload.agentId, payload.sessionId);
873
+ agent.outputs.push({
874
+ outputId: payload.outputId,
875
+ summary: payload.summary,
876
+ occurredAt: event.occurredAt,
877
+ evidence: payload.evidence,
878
+ });
879
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
880
+ break;
881
+ }
882
+ case "belief_observed": {
883
+ ensureAgent(payload.agentId, payload.sessionId, event.occurredAt);
884
+ markSubstantiveParticipation(payload.agentId, payload.sessionId);
885
+ recordEarliestAppendPosition(
886
+ beliefAppendPositions,
887
+ beliefAppendPositionKey(payload.agentId, payload.decisionId),
888
+ appendPosition
889
+ );
890
+ const existing = decisions.get(payload.decisionId);
891
+ if (existing) {
892
+ if (!decisionStatementsMatch(existing.statement, payload.statement)) {
893
+ missingEvidence.add(`decision:${payload.decisionId}:statement-consistency`);
894
+ }
895
+ if (!existing.heldByAgentIds.includes(payload.agentId)) {
896
+ existing.heldByAgentIds.push(payload.agentId);
897
+ }
898
+ if (!existing.beliefIds.includes(payload.beliefId)) existing.beliefIds.push(payload.beliefId);
899
+ existing.evidence.push(...payload.evidence);
900
+ } else {
901
+ decisions.set(payload.decisionId, {
902
+ decisionId: payload.decisionId,
903
+ statement: payload.statement,
904
+ status: "active",
905
+ heldByAgentIds: [payload.agentId],
906
+ beliefIds: [payload.beliefId],
907
+ evidence: [...payload.evidence],
908
+ firstObservedAt: event.occurredAt,
909
+ });
910
+ }
911
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
912
+ break;
913
+ }
914
+ case "conflict_detected": {
915
+ recordEarliestAppendPosition(conflictAppendPositions, payload.conflictId, appendPosition);
916
+ const normalizedDecisionStatements = payload.decisionIds
917
+ .map((decisionId) => decisions.get(decisionId)?.statement)
918
+ .filter((statement): statement is string => statement !== undefined)
919
+ .map(normalizeDecisionStatement);
920
+ if (
921
+ normalizedDecisionStatements.length !== payload.decisionIds.length ||
922
+ new Set(normalizedDecisionStatements).size < 2
923
+ ) {
924
+ missingEvidence.add(`conflict:${payload.conflictId}:distinct-decision-statements`);
925
+ } else {
926
+ conflictDisagreementIds.add(payload.conflictId);
927
+ }
928
+ const participantHoldings = payload.agentIds.map((agentId) => ({
929
+ agentId,
930
+ decisionIds: uniqueSorted(
931
+ payload.decisionIds.filter((decisionId) => decisions.get(decisionId)?.heldByAgentIds.includes(agentId))
932
+ ),
933
+ }));
934
+ for (const participant of participantHoldings) {
935
+ if (participant.decisionIds.length === 0) {
936
+ missingEvidence.add(`conflict:${payload.conflictId}:participant:${participant.agentId}:decision-link`);
937
+ }
938
+ }
939
+ for (const decisionId of payload.decisionIds) {
940
+ if (!participantHoldings.some((participant) => participant.decisionIds.includes(decisionId))) {
941
+ missingEvidence.add(`conflict:${payload.conflictId}:decision:${decisionId}:participant-link`);
942
+ }
943
+ if (
944
+ !payload.agentIds.some(
945
+ (agentId) =>
946
+ (beliefAppendPositions.get(beliefAppendPositionKey(agentId, decisionId)) ?? Number.POSITIVE_INFINITY) <
947
+ appendPosition
948
+ )
949
+ ) {
950
+ missingEvidence.add(`conflict:${payload.conflictId}:decision:${decisionId}:append-order`);
951
+ }
952
+ }
953
+ for (const agentId of payload.agentIds) {
954
+ if (
955
+ !payload.decisionIds.some(
956
+ (decisionId) =>
957
+ (beliefAppendPositions.get(beliefAppendPositionKey(agentId, decisionId)) ?? Number.POSITIVE_INFINITY) <
958
+ appendPosition
959
+ )
960
+ ) {
961
+ missingEvidence.add(`conflict:${payload.conflictId}:participant:${agentId}:append-order`);
962
+ }
963
+ }
964
+ if (
965
+ participantHoldings.every((participant) => participant.decisionIds.length > 0) &&
966
+ new Set(participantHoldings.map((participant) => participant.decisionIds.join("\0"))).size < 2
967
+ ) {
968
+ missingEvidence.add(`conflict:${payload.conflictId}:distinct-participant-beliefs`);
969
+ }
970
+ if (conflicts.has(payload.conflictId)) {
971
+ missingEvidence.add(`conflict:${payload.conflictId}:duplicate`);
972
+ } else {
973
+ conflicts.set(payload.conflictId, {
974
+ conflictId: payload.conflictId,
975
+ decisionIds: uniqueSorted(payload.decisionIds),
976
+ agentIds: uniqueSorted(payload.agentIds),
977
+ summary: payload.summary,
978
+ status: "open",
979
+ occurredAt: event.occurredAt,
980
+ evidence: payload.evidence,
981
+ });
982
+ }
983
+ for (const agentId of payload.agentIds) substantiveAgentIds.add(agentId);
984
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
985
+ break;
986
+ }
987
+ case "test_result": {
988
+ const testSnapshot: RelayTestSnapshot = {
989
+ testId: payload.testId,
990
+ decisionId: payload.decisionId,
991
+ ...(payload.correctionId === undefined ? {} : { correctionId: payload.correctionId }),
992
+ command: payload.command,
993
+ status: payload.status,
994
+ summary: payload.summary,
995
+ ...(payload.durationMs === undefined ? {} : { durationMs: payload.durationMs }),
996
+ occurredAt: event.occurredAt,
997
+ evidence: payload.evidence,
998
+ };
999
+ tests.push(testSnapshot);
1000
+ testAppendPositions.set(testSnapshot, appendPosition);
1001
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1002
+ break;
1003
+ }
1004
+ case "correction_proposed": {
1005
+ if (corrections.has(payload.correctionId)) {
1006
+ missingEvidence.add(`correction:${payload.correctionId}:duplicate-proposal`);
1007
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1008
+ break;
1009
+ }
1010
+ substantiveAgentIds.add(payload.proposedBy);
1011
+ const correction: RelayCorrectionSnapshot = {
1012
+ correctionId: payload.correctionId,
1013
+ conflictId: payload.conflictId,
1014
+ proposedDecisionId: payload.proposedDecisionId,
1015
+ supersedesDecisionIds: uniqueSorted(payload.supersedesDecisionIds),
1016
+ statement: payload.statement,
1017
+ rationale: payload.rationale,
1018
+ proposedBy: payload.proposedBy,
1019
+ status: "proposed",
1020
+ proposedAt: event.occurredAt,
1021
+ evidence: [...payload.evidence],
1022
+ };
1023
+ corrections.set(payload.correctionId, correction);
1024
+ correctionProposalAppendPositions.set(payload.correctionId, appendPosition);
1025
+ const existingProposedDecision = decisions.get(payload.proposedDecisionId);
1026
+ const replacementStatementMatches =
1027
+ existingProposedDecision === undefined ||
1028
+ decisionStatementsMatch(existingProposedDecision.statement, payload.statement);
1029
+ if (!replacementStatementMatches) {
1030
+ missingEvidence.add(`correction:${payload.correctionId}:replacement-statement`);
1031
+ }
1032
+ if (existingProposedDecision === undefined) {
1033
+ decisions.set(payload.proposedDecisionId, {
1034
+ decisionId: payload.proposedDecisionId,
1035
+ statement: payload.statement,
1036
+ status: "proposed",
1037
+ heldByAgentIds: [],
1038
+ beliefIds: [],
1039
+ evidence: [...payload.evidence],
1040
+ firstObservedAt: event.occurredAt,
1041
+ });
1042
+ }
1043
+ const conflict = conflicts.get(payload.conflictId);
1044
+ const conflictAppendPosition = conflictAppendPositions.get(payload.conflictId);
1045
+ const conflictPredatesProposal =
1046
+ conflictAppendPosition !== undefined && conflictAppendPosition < appendPosition;
1047
+ if (!conflictPredatesProposal) {
1048
+ missingEvidence.add(`correction:${payload.correctionId}:conflict-append-order`);
1049
+ }
1050
+ const correctionSourceIds = sourceEvidenceIds(payload.evidence);
1051
+ if (correctionSourceIds.size === 0) {
1052
+ missingEvidence.add(`correction:${payload.correctionId}:source`);
1053
+ }
1054
+ if (conflict) {
1055
+ const conflictSourceIds = sourceEvidenceIds(conflict.evidence);
1056
+ if (conflictSourceIds.size === 0) {
1057
+ missingEvidence.add(`conflict:${payload.conflictId}:source`);
1058
+ }
1059
+ const sourceLinked = [...correctionSourceIds].some((sourceId) => conflictSourceIds.has(sourceId));
1060
+ if (!sourceLinked) {
1061
+ missingEvidence.add(`correction:${payload.correctionId}:source-link`);
1062
+ }
1063
+ if (!correctionCoversConflict(correction, conflict)) {
1064
+ missingEvidence.add(`conflict:${payload.conflictId}:decision-link`);
1065
+ } else if (
1066
+ sourceLinked &&
1067
+ conflictPredatesProposal &&
1068
+ conflictDisagreementIds.has(payload.conflictId) &&
1069
+ replacementStatementMatches
1070
+ ) {
1071
+ groundedCorrectionIds.add(payload.correctionId);
1072
+ conflict.status = "proposed";
1073
+ conflict.correctionId = payload.correctionId;
1074
+ }
1075
+ } else {
1076
+ missingEvidence.add(`conflict:${payload.conflictId}:observation`);
1077
+ }
1078
+ status = "awaiting_approval";
1079
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1080
+ break;
1081
+ }
1082
+ case "correction_approved": {
1083
+ if (payload.approvedBy.kind === "agent") substantiveAgentIds.add(payload.approvedBy.id);
1084
+ const correction = corrections.get(payload.correctionId);
1085
+ if (correction) {
1086
+ const proposalAppendPosition = correctionProposalAppendPositions.get(payload.correctionId);
1087
+ const proposalPredatesApproval =
1088
+ proposalAppendPosition !== undefined && proposalAppendPosition < appendPosition;
1089
+ const authenticatedHumanApproval =
1090
+ payload.approvedBy.kind !== "human" ||
1091
+ runMode !== "live" ||
1092
+ event.authenticatedPrincipal === payload.approvedBy.id;
1093
+ if (!proposalPredatesApproval) {
1094
+ missingEvidence.add(`correction:${payload.correctionId}:approval-append-order`);
1095
+ } else if (!authenticatedHumanApproval) {
1096
+ missingEvidence.add(`correction:${payload.correctionId}:authenticated-human-approval`);
1097
+ } else if (correction.approvedAt === undefined) {
1098
+ correction.approvedAt = event.occurredAt;
1099
+ correction.approvedBy = payload.approvedBy;
1100
+ correction.approvalNote = payload.note;
1101
+ if (event.authenticatedPrincipal !== undefined) {
1102
+ correction.approvalPrincipal = event.authenticatedPrincipal;
1103
+ }
1104
+ correctionApprovalAppendPositions.set(payload.correctionId, appendPosition);
1105
+ if (correction.status === "proposed") correction.status = "approved";
1106
+ status = "correcting";
1107
+ } else {
1108
+ missingEvidence.add(`correction:${payload.correctionId}:duplicate-approval`);
1109
+ }
1110
+ correction.evidence.push(...payload.evidence);
1111
+ } else {
1112
+ missingEvidence.add(`correction:${payload.correctionId}:proposal`);
1113
+ }
1114
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1115
+ break;
1116
+ }
1117
+ case "decision_superseded": {
1118
+ const correction = corrections.get(payload.correctionId);
1119
+ if (!correction) {
1120
+ missingEvidence.add(`correction:${payload.correctionId}:proposal`);
1121
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1122
+ break;
1123
+ }
1124
+ if (correction.approvedAt === undefined || correction.approvedBy === undefined) {
1125
+ missingEvidence.add(`correction:${payload.correctionId}:approval`);
1126
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1127
+ break;
1128
+ }
1129
+ const approvalAppendPosition = correctionApprovalAppendPositions.get(payload.correctionId);
1130
+ if (approvalAppendPosition === undefined || approvalAppendPosition >= appendPosition) {
1131
+ missingEvidence.add(`correction:${payload.correctionId}:approval-append-order`);
1132
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1133
+ break;
1134
+ }
1135
+ const conflict = conflicts.get(correction.conflictId);
1136
+ if (!conflict || !correctionCoversConflict(correction, conflict)) {
1137
+ missingEvidence.add(`conflict:${correction.conflictId}:decision-link`);
1138
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1139
+ break;
1140
+ }
1141
+ if (!groundedCorrectionIds.has(payload.correctionId)) {
1142
+ missingEvidence.add(`correction:${payload.correctionId}:source-link`);
1143
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1144
+ break;
1145
+ }
1146
+ if (
1147
+ !correction.supersedesDecisionIds.includes(payload.decisionId) ||
1148
+ correction.proposedDecisionId !== payload.replacementDecisionId
1149
+ ) {
1150
+ missingEvidence.add(`correction:${payload.correctionId}:supersession-link`);
1151
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1152
+ break;
1153
+ }
1154
+
1155
+ const stale = decisions.get(payload.decisionId);
1156
+ const replacement = decisions.get(payload.replacementDecisionId);
1157
+ if (!stale) missingEvidence.add(`decision:${payload.decisionId}:observation`);
1158
+ if (!replacement) missingEvidence.add(`decision:${payload.replacementDecisionId}:replacement`);
1159
+ if (!stale || !replacement) {
1160
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1161
+ break;
1162
+ }
1163
+
1164
+ stale.status = "superseded";
1165
+ stale.supersededAt = event.occurredAt;
1166
+ stale.supersededBy = payload.replacementDecisionId;
1167
+ stale.correctionId = payload.correctionId;
1168
+ stale.evidence.push(...payload.evidence);
1169
+ replacement.status = "active";
1170
+ replacement.evidence.push(...payload.evidence);
1171
+ correction.evidence.push(...payload.evidence);
1172
+ if (correctionSupersessionComplete(correction, decisions)) {
1173
+ correction.appliedAt ??= event.occurredAt;
1174
+ recordEarliestAppendPosition(correctionAppliedAppendPositions, payload.correctionId, appendPosition);
1175
+ if (correction.status !== "propagated") correction.status = "applied";
1176
+ conflict.status = "resolved";
1177
+ }
1178
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1179
+ break;
1180
+ }
1181
+ case "recall_observed": {
1182
+ const agent = ensureAgent(payload.agentId, payload.sessionId, event.occurredAt);
1183
+ const coldStart =
1184
+ !substantiveAgentIds.has(payload.agentId) &&
1185
+ !substantiveSessionIds.has(payload.sessionId) &&
1186
+ agent.recalls.length === 0;
1187
+ const recallSnapshot: RelayRecallSnapshot = {
1188
+ recallReceiptId: payload.recallReceiptId,
1189
+ sessionId: payload.sessionId,
1190
+ decisionId: payload.decisionId,
1191
+ query: payload.query,
1192
+ capturedAtAction: payload.capturedAtAction,
1193
+ coldStart,
1194
+ occurredAt: event.occurredAt,
1195
+ evidence: payload.evidence,
1196
+ };
1197
+ agent.recalls.push(recallSnapshot);
1198
+ recallAppendPositions.set(recallSnapshot, appendPosition);
1199
+ markSubstantiveParticipation(payload.agentId, payload.sessionId);
1200
+ const decision = decisions.get(payload.decisionId);
1201
+ if (decision && !decision.heldByAgentIds.includes(payload.agentId)) {
1202
+ decision.heldByAgentIds.push(payload.agentId);
1203
+ }
1204
+ if (!decision) missingEvidence.add(`decision:${payload.decisionId}:recall-target`);
1205
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1206
+ break;
1207
+ }
1208
+ case "propagation_verified": {
1209
+ const agent = ensureAgent(payload.agentId, payload.sessionId, event.occurredAt);
1210
+ const correction = corrections.get(payload.correctionId);
1211
+ if (!correction) {
1212
+ missingEvidence.add(`correction:${payload.correctionId}:proposal`);
1213
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1214
+ break;
1215
+ }
1216
+ if (correction.approvedAt === undefined || correction.approvedBy === undefined) {
1217
+ missingEvidence.add(`correction:${payload.correctionId}:approval`);
1218
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1219
+ break;
1220
+ }
1221
+ if (
1222
+ (correction.status !== "applied" && correction.status !== "propagated") ||
1223
+ correction.appliedAt === undefined ||
1224
+ correction.proposedDecisionId !== payload.decisionId
1225
+ ) {
1226
+ missingEvidence.add(`correction:${payload.correctionId}:supersession`);
1227
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1228
+ break;
1229
+ }
1230
+ const recalled = agent.recalls.find(
1231
+ (recall) =>
1232
+ recall.recallReceiptId === payload.recallReceiptId &&
1233
+ recall.sessionId === payload.sessionId &&
1234
+ recall.decisionId === payload.decisionId
1235
+ );
1236
+ if (!recalled) {
1237
+ missingEvidence.add(`recall:${payload.recallReceiptId}:observation`);
1238
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1239
+ break;
1240
+ }
1241
+ const appliedAppendPosition = correctionAppliedAppendPositions.get(payload.correctionId);
1242
+ const recallAppendPosition = recallAppendPositions.get(recalled);
1243
+ if (
1244
+ appliedAppendPosition === undefined ||
1245
+ recallAppendPosition === undefined ||
1246
+ appliedAppendPosition >= recallAppendPosition
1247
+ ) {
1248
+ missingEvidence.add(`recall:${payload.recallReceiptId}:post-application-append-order`);
1249
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1250
+ break;
1251
+ }
1252
+ if (recallAppendPosition >= appendPosition) {
1253
+ missingEvidence.add(`recall:${payload.recallReceiptId}:propagation-append-order`);
1254
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1255
+ break;
1256
+ }
1257
+ if (!recalled.capturedAtAction) {
1258
+ missingEvidence.add(`recall:${payload.recallReceiptId}:at-action`);
1259
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1260
+ break;
1261
+ }
1262
+ if (!recalled.coldStart) {
1263
+ missingEvidence.add(`recall:${payload.recallReceiptId}:cold-start`);
1264
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1265
+ break;
1266
+ }
1267
+ if (compareInstants(correction.appliedAt, recalled.occurredAt) >= 0) {
1268
+ missingEvidence.add(`recall:${payload.recallReceiptId}:post-application`);
1269
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1270
+ break;
1271
+ }
1272
+ if (compareInstants(recalled.occurredAt, event.occurredAt) >= 0) {
1273
+ missingEvidence.add(`recall:${payload.recallReceiptId}:propagation-order`);
1274
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1275
+ break;
1276
+ }
1277
+
1278
+ const propagationSnapshot: RelayPropagationSnapshot = {
1279
+ correctionId: payload.correctionId,
1280
+ agentId: payload.agentId,
1281
+ sessionId: payload.sessionId,
1282
+ decisionId: payload.decisionId,
1283
+ recallReceiptId: payload.recallReceiptId,
1284
+ staleDecisionAbsent: payload.staleDecisionAbsent,
1285
+ occurredAt: event.occurredAt,
1286
+ evidence: payload.evidence,
1287
+ };
1288
+ propagation.push(propagationSnapshot);
1289
+ propagationAppendPositions.set(propagationSnapshot, appendPosition);
1290
+ correction.evidence.push(...payload.evidence);
1291
+ if (payload.staleDecisionAbsent) {
1292
+ correction.status = "propagated";
1293
+ correction.propagatedAt = event.occurredAt;
1294
+ }
1295
+ status = payload.staleDecisionAbsent ? "verified" : "correcting";
1296
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1297
+ break;
1298
+ }
1299
+ case "mission_completed":
1300
+ if (outcome !== null) {
1301
+ missingEvidence.add("mission:duplicate-completion");
1302
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1303
+ break;
1304
+ }
1305
+ if (startedAt === null) missingEvidence.add("mission:start");
1306
+ outcome = {
1307
+ result: payload.outcome,
1308
+ summary: payload.summary,
1309
+ occurredAt: event.occurredAt,
1310
+ evidence: payload.evidence,
1311
+ };
1312
+ completedAt = event.occurredAt;
1313
+ status = payload.outcome === "failed" ? "failed" : "completed";
1314
+ collectMissingEvidence(missingEvidence, event, payload.evidence);
1315
+ break;
1316
+ }
1317
+ }
1318
+
1319
+ for (const conflict of conflicts.values()) {
1320
+ if (sourceEvidenceIds(conflict.evidence).size === 0) {
1321
+ missingEvidence.add(`conflict:${conflict.conflictId}:source`);
1322
+ }
1323
+ for (const decisionId of conflict.decisionIds) {
1324
+ if (!decisions.has(decisionId)) missingEvidence.add(`decision:${decisionId}:conflict-target`);
1325
+ }
1326
+ for (const agentId of conflict.agentIds) {
1327
+ if (!agents.has(agentId)) missingEvidence.add(`agent:${agentId}:conflict-participant`);
1328
+ }
1329
+ }
1330
+ for (const correction of corrections.values()) {
1331
+ if (sourceEvidenceIds(correction.evidence).size === 0) {
1332
+ missingEvidence.add(`correction:${correction.correctionId}:source`);
1333
+ }
1334
+ if (correction.approvedAt === undefined || correction.approvedBy === undefined) {
1335
+ missingEvidence.add(`correction:${correction.correctionId}:approval`);
1336
+ } else if (correction.approvedBy.kind !== "human") {
1337
+ missingEvidence.add(`correction:${correction.correctionId}:human-approval`);
1338
+ } else if (runMode === "live" && correction.approvalPrincipal !== correction.approvedBy.id) {
1339
+ missingEvidence.add(`correction:${correction.correctionId}:authenticated-human-approval`);
1340
+ }
1341
+ if (correction.status === "approved" || correction.status === "proposed") {
1342
+ missingEvidence.add(`correction:${correction.correctionId}:supersession`);
1343
+ }
1344
+ if (correction.status === "applied") {
1345
+ missingEvidence.add(`correction:${correction.correctionId}:propagation`);
1346
+ }
1347
+ }
1348
+
1349
+ const liveFixtureEvidenceEventIds =
1350
+ runMode === "live"
1351
+ ? orderedEvents
1352
+ .filter((event) => event.payload.evidence.some((item) => item.capture === "fixture"))
1353
+ .map((event) => event.eventId)
1354
+ : [];
1355
+ for (const eventId of liveFixtureEvidenceEventIds) {
1356
+ missingEvidence.add(`mission:live-fixture-evidence:${eventId}`);
1357
+ }
1358
+ const captureTrustValid = liveFixtureEvidenceEventIds.length === 0;
1359
+
1360
+ const sortedDecisions = [...decisions.values()]
1361
+ .map((decision) => ({
1362
+ ...decision,
1363
+ heldByAgentIds: uniqueSorted(decision.heldByAgentIds),
1364
+ beliefIds: uniqueSorted(decision.beliefIds),
1365
+ evidence: uniqueEvidence(decision.evidence),
1366
+ }))
1367
+ .sort((a, b) => compareText(a.decisionId, b.decisionId));
1368
+ const coldStartVerified = captureTrustValid && propagation.some((item) => item.staleDecisionAbsent);
1369
+ const passingOutcomeVerified =
1370
+ captureTrustValid &&
1371
+ outcome?.result === "recovered" &&
1372
+ tests.some((test) => {
1373
+ const testAppendPosition = testAppendPositions.get(test);
1374
+ if (
1375
+ test.status !== "passed" ||
1376
+ test.correctionId === undefined ||
1377
+ testAppendPosition === undefined ||
1378
+ firstCompletionPosition < 0 ||
1379
+ testAppendPosition >= firstCompletionPosition ||
1380
+ compareInstants(test.occurredAt, outcome.occurredAt) >= 0
1381
+ ) {
1382
+ return false;
1383
+ }
1384
+ const correction = corrections.get(test.correctionId);
1385
+ if (
1386
+ correction?.status !== "propagated" ||
1387
+ correction.proposedDecisionId !== test.decisionId ||
1388
+ !test.evidence.some((item) => item.kind === "correction" && item.id === correction.correctionId) ||
1389
+ !test.evidence.some((item) => item.kind === "test")
1390
+ ) {
1391
+ return false;
1392
+ }
1393
+ return propagation.some((item) => {
1394
+ const propagationAppendPosition = propagationAppendPositions.get(item);
1395
+ return (
1396
+ item.staleDecisionAbsent &&
1397
+ item.correctionId === correction.correctionId &&
1398
+ item.decisionId === test.decisionId &&
1399
+ propagationAppendPosition !== undefined &&
1400
+ propagationAppendPosition < testAppendPosition &&
1401
+ compareInstants(item.occurredAt, test.occurredAt) < 0
1402
+ );
1403
+ });
1404
+ });
1405
+ if (outcome && !passingOutcomeVerified) missingEvidence.add("outcome:passing-test");
1406
+ if (outcome?.result === "recovered" && !coldStartVerified) {
1407
+ missingEvidence.add("outcome:cold-start-propagation");
1408
+ }
1409
+ if (input.events.length > 0 && startedAt === null) missingEvidence.add("mission:start");
1410
+ if (startedAt !== null && completedAt !== null && compareInstants(startedAt, completedAt) >= 0) {
1411
+ missingEvidence.add("mission:lifecycle-order");
1412
+ }
1413
+ if (
1414
+ firstStartEvent !== undefined &&
1415
+ firstCompletionEvent !== undefined &&
1416
+ (firstStartPosition >= firstCompletionPosition || compareRelayEvents(firstStartEvent, firstCompletionEvent) >= 0)
1417
+ ) {
1418
+ missingEvidence.add("mission:lifecycle-order");
1419
+ }
1420
+
1421
+ const eventWindow = filteredEvents.slice(Math.max(0, filteredEvents.length - options.limit));
1422
+ // `found` describes whether the mission has any valid persisted evidence,
1423
+ // not whether the caller's optional time window happened to select an
1424
+ // event. A known mission with an empty [since, until) window is distinct
1425
+ // from a mission that has never existed.
1426
+ const found = input.events.length > 0;
1427
+ return {
1428
+ schemaVersion: RELAY_MISSION_SCHEMA_VERSION,
1429
+ missionId,
1430
+ namespace,
1431
+ found,
1432
+ readHealth: corruptLines > 0 ? "partial" : found || input.fileExists ? "ok" : "empty",
1433
+ status,
1434
+ mission: { title, objective, runMode, startedAt, completedAt },
1435
+ agents: [...agents.values()]
1436
+ .map((agent) => ({
1437
+ ...agent,
1438
+ sessionIds: uniqueSorted(agent.sessionIds),
1439
+ outputs: agent.outputs.sort((a, b) => compareTimedIds(a, b, "outputId")),
1440
+ recalls: agent.recalls.sort((a, b) => compareTimedIds(a, b, "recallReceiptId")),
1441
+ }))
1442
+ .sort((a, b) => compareText(a.agentId, b.agentId)),
1443
+ decisions: sortedDecisions,
1444
+ conflicts: [...conflicts.values()].sort((a, b) => compareText(a.conflictId, b.conflictId)),
1445
+ corrections: [...corrections.values()]
1446
+ .map((correction) => ({ ...correction, evidence: uniqueEvidence(correction.evidence) }))
1447
+ .sort((a, b) => compareText(a.correctionId, b.correctionId)),
1448
+ tests: tests.sort((a, b) => compareTimedIds(a, b, "testId")),
1449
+ propagation: propagation.sort((a, b) => {
1450
+ const byTime = compareInstants(a.occurredAt, b.occurredAt);
1451
+ if (byTime !== 0) return byTime;
1452
+ const byAgent = compareText(a.agentId, b.agentId);
1453
+ return byAgent !== 0 ? byAgent : compareText(a.recallReceiptId, b.recallReceiptId);
1454
+ }),
1455
+ outcome,
1456
+ receipt: {
1457
+ complete:
1458
+ status === "completed" &&
1459
+ outcome?.result === "recovered" &&
1460
+ passingOutcomeVerified &&
1461
+ coldStartVerified &&
1462
+ missingEvidence.size === 0,
1463
+ missingEvidence: [...missingEvidence].sort(compareText),
1464
+ activeDecisionIds: sortedDecisions
1465
+ .filter((decision) => decision.status === "active")
1466
+ .map((decision) => decision.decisionId),
1467
+ supersededDecisionIds: sortedDecisions
1468
+ .filter((decision) => decision.status === "superseded")
1469
+ .map((decision) => decision.decisionId),
1470
+ coldStartVerified,
1471
+ passingOutcomeVerified,
1472
+ },
1473
+ bounds: {
1474
+ totalEvents: filteredEvents.length,
1475
+ returnedEvents: eventWindow.length,
1476
+ corruptLines,
1477
+ truncated: filteredEvents.length > eventWindow.length,
1478
+ since: options.since ?? null,
1479
+ until: options.until ?? null,
1480
+ },
1481
+ events: eventWindow,
1482
+ };
1483
+ }
1484
+
1485
+ export function compareRelayEvents(a: RelayMissionEvent, b: RelayMissionEvent): number {
1486
+ const byOccurrence = compareInstants(a.occurredAt, b.occurredAt);
1487
+ if (byOccurrence !== 0) return byOccurrence;
1488
+ const byRecord = compareInstants(a.recordedAt, b.recordedAt);
1489
+ if (byRecord !== 0) return byRecord;
1490
+ return compareText(a.eventId, b.eventId);
1491
+ }
1492
+
1493
+ function parseMissionJsonl(
1494
+ raw: string,
1495
+ missionId: string,
1496
+ namespace: string,
1497
+ maxEvents: number
1498
+ ): Pick<MissionFileRead, "events" | "corruptLines"> {
1499
+ const events: RelayMissionEvent[] = [];
1500
+ let corruptLines = 0;
1501
+ for (const line of raw.split("\n")) {
1502
+ if (line.trim().length === 0) continue;
1503
+ if (Buffer.byteLength(line, "utf8") > RELAY_MISSION_MAX_LINE_BYTES) {
1504
+ corruptLines += 1;
1505
+ continue;
1506
+ }
1507
+ let candidate: unknown;
1508
+ try {
1509
+ candidate = JSON.parse(line);
1510
+ } catch {
1511
+ corruptLines += 1;
1512
+ continue;
1513
+ }
1514
+ const parsed = RelayMissionEventSchema.safeParse(candidate);
1515
+ if (!parsed.success || parsed.data.missionId !== missionId || parsed.data.namespace !== namespace) {
1516
+ corruptLines += 1;
1517
+ continue;
1518
+ }
1519
+ if (events.length >= maxEvents) {
1520
+ throw new RelayMissionStoreError(
1521
+ "limit_exceeded",
1522
+ `mission ${missionId} exceeds the ${maxEvents}-event read limit`
1523
+ );
1524
+ }
1525
+ events.push(parsed.data);
1526
+ }
1527
+ return { events, corruptLines };
1528
+ }
1529
+
1530
+ function collectMissingEvidence(missing: Set<string>, event: RelayMissionEvent, evidence: RelayEvidenceRef[]): void {
1531
+ if (evidence.length === 0) missing.add(`event:${event.eventId}:evidence`);
1532
+ }
1533
+
1534
+ function sameIdempotentInput(
1535
+ event: RelayMissionEvent,
1536
+ input: z.infer<typeof RelayMissionEventInputSchema>,
1537
+ authenticatedPrincipal: string | undefined
1538
+ ): boolean {
1539
+ if (event.authenticatedPrincipal !== authenticatedPrincipal) return false;
1540
+ if (input.occurredAt !== undefined && event.occurredAt !== input.occurredAt) return false;
1541
+ return stableJson(event.payload) === stableJson(input.payload);
1542
+ }
1543
+
1544
+ function stableJson(value: unknown): string {
1545
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1546
+ if (value && typeof value === "object") {
1547
+ const entries = Object.entries(value as Record<string, unknown>)
1548
+ .sort(([a], [b]) => compareText(a, b))
1549
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`);
1550
+ return `{${entries.join(",")}}`;
1551
+ }
1552
+ return JSON.stringify(value) ?? "null";
1553
+ }
1554
+
1555
+ function uniqueEvidence(evidence: RelayEvidenceRef[]): RelayEvidenceRef[] {
1556
+ const byKey = new Map<string, RelayEvidenceRef>();
1557
+ for (const item of evidence) byKey.set(`${item.kind}\0${item.id}`, item);
1558
+ return [...byKey.values()].sort((a, b) => {
1559
+ const byKind = compareText(a.kind, b.kind);
1560
+ return byKind !== 0 ? byKind : compareText(a.id, b.id);
1561
+ });
1562
+ }
1563
+
1564
+ function uniqueSorted(values: string[]): string[] {
1565
+ return [...new Set(values)].sort(compareText);
1566
+ }
1567
+
1568
+ function compareText(a: string, b: string): number {
1569
+ if (a < b) return -1;
1570
+ if (a > b) return 1;
1571
+ return 0;
1572
+ }
1573
+
1574
+ function compareInstants(a: string, b: string): number {
1575
+ const aMs = Date.parse(a);
1576
+ const bMs = Date.parse(b);
1577
+ if (aMs < bMs) return -1;
1578
+ if (aMs > bMs) return 1;
1579
+ return 0;
1580
+ }
1581
+
1582
+ function compareTimedIds<T extends { occurredAt: string }, K extends keyof T>(a: T, b: T, idKey: K): number {
1583
+ const byTime = compareInstants(a.occurredAt, b.occurredAt);
1584
+ if (byTime !== 0) return byTime;
1585
+ return compareText(String(a[idKey]), String(b[idKey]));
1586
+ }
1587
+
1588
+ function beliefAppendPositionKey(agentId: string, decisionId: string): string {
1589
+ return `${agentId}\0${decisionId}`;
1590
+ }
1591
+
1592
+ function normalizeDecisionStatement(statement: string): string {
1593
+ return statement
1594
+ .normalize("NFKC")
1595
+ .trim()
1596
+ .toLowerCase()
1597
+ .replace(/\s+/g, " ")
1598
+ .replace(/[.!?]+$/, "");
1599
+ }
1600
+
1601
+ function decisionStatementsMatch(left: string, right: string): boolean {
1602
+ return normalizeDecisionStatement(left) === normalizeDecisionStatement(right);
1603
+ }
1604
+
1605
+ function recordEarliestAppendPosition(positions: Map<string, number>, key: string, position: number): void {
1606
+ const current = positions.get(key);
1607
+ if (current === undefined || position < current) positions.set(key, position);
1608
+ }
1609
+
1610
+ function correctionSupersessionComplete(
1611
+ correction: RelayCorrectionSnapshot,
1612
+ decisions: ReadonlyMap<string, RelayDecisionSnapshot>
1613
+ ): boolean {
1614
+ return correction.supersedesDecisionIds.every((decisionId) => {
1615
+ const decision = decisions.get(decisionId);
1616
+ return (
1617
+ decision?.status === "superseded" &&
1618
+ decision.supersededBy === correction.proposedDecisionId &&
1619
+ decision.correctionId === correction.correctionId
1620
+ );
1621
+ });
1622
+ }
1623
+
1624
+ function correctionCoversConflict(
1625
+ correction: Pick<RelayCorrectionSnapshot, "proposedDecisionId" | "supersedesDecisionIds">,
1626
+ conflict: Pick<RelayConflictSnapshot, "decisionIds">
1627
+ ): boolean {
1628
+ const expectedSuperseded = uniqueSorted(
1629
+ conflict.decisionIds.filter((decisionId) => decisionId !== correction.proposedDecisionId)
1630
+ );
1631
+ const declaredSuperseded = uniqueSorted(correction.supersedesDecisionIds);
1632
+ return (
1633
+ expectedSuperseded.length === declaredSuperseded.length &&
1634
+ expectedSuperseded.every((decisionId, index) => decisionId === declaredSuperseded[index])
1635
+ );
1636
+ }
1637
+
1638
+ function sourceEvidenceIds(evidence: readonly RelayEvidenceRef[]): Set<string> {
1639
+ return new Set(evidence.filter((item) => item.kind === "source").map((item) => item.id));
1640
+ }
1641
+
1642
+ function positiveIntegerOption(value: number | undefined, name: string, fallback: number): number {
1643
+ if (value === undefined) return fallback;
1644
+ if (!Number.isInteger(value) || value <= 0) {
1645
+ throw new TypeError(`RelayMissionStore ${name} must be a positive integer`);
1646
+ }
1647
+ return value;
1648
+ }
1649
+
1650
+ function validNow(now: () => Date): string {
1651
+ const value = now();
1652
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
1653
+ throw new TypeError("RelayMissionStore now() must return a valid Date");
1654
+ }
1655
+ return value.toISOString();
1656
+ }
1657
+
1658
+ async function openOrCreatePinnedChildDirectory(
1659
+ parentHandle: import("node:fs/promises").FileHandle,
1660
+ segment: string
1661
+ ): Promise<import("node:fs/promises").FileHandle> {
1662
+ const directory = path.join(pinnedDirectoryPath(parentHandle), segment);
1663
+ try {
1664
+ await fs.mkdir(directory, { mode: 0o700 });
1665
+ } catch (error) {
1666
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
1667
+ }
1668
+ return openVerifiedDirectory(directory, "Relay directory symlinks are rejected");
1669
+ }
1670
+
1671
+ async function openVerifiedDirectory(
1672
+ directory: string,
1673
+ unsafeMessage: string
1674
+ ): Promise<import("node:fs/promises").FileHandle> {
1675
+ const before = await fs.lstat(directory);
1676
+ if (before.isSymbolicLink() || !before.isDirectory()) {
1677
+ throw new RelayMissionStoreError("unsafe_path", unsafeMessage);
1678
+ }
1679
+ const handle = await fs.open(directory, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
1680
+ try {
1681
+ const openInfo = await handle.stat();
1682
+ const after = await fs.lstat(directory);
1683
+ if (
1684
+ after.isSymbolicLink() ||
1685
+ !after.isDirectory() ||
1686
+ before.dev !== openInfo.dev ||
1687
+ before.ino !== openInfo.ino ||
1688
+ after.dev !== openInfo.dev ||
1689
+ after.ino !== openInfo.ino
1690
+ ) {
1691
+ throw new RelayMissionStoreError("unsafe_path", "Relay directory changed during validation");
1692
+ }
1693
+ return handle;
1694
+ } catch (error) {
1695
+ await handle.close().catch(() => undefined);
1696
+ throw error;
1697
+ }
1698
+ }
1699
+
1700
+ function pinnedDirectoryPath(handle: import("node:fs/promises").FileHandle): string {
1701
+ if (process.platform === "linux") return `/proc/self/fd/${handle.fd}`;
1702
+ if (process.platform === "darwin" || process.platform === "freebsd" || process.platform === "openbsd") {
1703
+ return `/dev/fd/${handle.fd}`;
1704
+ }
1705
+ throw new RelayMissionStoreError(
1706
+ "unsafe_path",
1707
+ `Relay pinned directory access is unsupported on ${process.platform}`
1708
+ );
1709
+ }
1710
+
1711
+ export function relayMissionReceiptDigest(snapshot: RelayMissionSnapshot): string {
1712
+ const receipt = {
1713
+ schemaVersion: snapshot.schemaVersion,
1714
+ missionId: snapshot.missionId,
1715
+ namespace: snapshot.namespace,
1716
+ status: snapshot.status,
1717
+ decisions: snapshot.decisions,
1718
+ corrections: snapshot.corrections,
1719
+ propagation: snapshot.propagation,
1720
+ tests: snapshot.tests,
1721
+ outcome: snapshot.outcome,
1722
+ receipt: snapshot.receipt,
1723
+ };
1724
+ return createHash("sha256").update(stableJson(receipt)).digest("hex");
1725
+ }