@remnic/core 9.7.9 → 9.7.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +5 -5
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +6 -6
- package/dist/access-cli.js +36 -36
- package/dist/access-http.d.ts +2 -1
- package/dist/access-http.js +10 -10
- package/dist/access-identity-continuity-surface.js +5 -5
- package/dist/access-lcm-surface.js +5 -5
- package/dist/access-mcp.js +9 -9
- package/dist/access-observe-write-surface.js +5 -5
- package/dist/access-operations-batch.js +7 -7
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +8 -8
- package/dist/access-recall-surface.js +5 -5
- package/dist/access-schema.d.ts +64 -64
- package/dist/access-service.js +5 -5
- package/dist/access-surface-catalog.js +2 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-HHAA35A3.js → chunk-2VN4JAX7.js} +548 -161
- package/dist/chunk-2VN4JAX7.js.map +1 -0
- package/dist/{chunk-MLT5EG6K.js → chunk-3OQ4IZJN.js} +4 -4
- package/dist/{chunk-DLPXS7BE.js → chunk-5U65RIVI.js} +348 -49
- package/dist/chunk-5U65RIVI.js.map +1 -0
- package/dist/{chunk-QKVX4Y6D.js → chunk-673NPXE3.js} +2 -2
- package/dist/{chunk-POUD5BLV.js → chunk-6LM6PPDX.js} +4 -2
- package/dist/chunk-6LM6PPDX.js.map +1 -0
- package/dist/chunk-7XRUYC6G.js +1794 -0
- package/dist/chunk-7XRUYC6G.js.map +1 -0
- package/dist/{chunk-5CFHYQU3.js → chunk-DLWYAN6A.js} +2 -2
- package/dist/{chunk-TY43GBX2.js → chunk-EQF5763Z.js} +2 -2
- package/dist/{chunk-SKXBOZDF.js → chunk-GHVJASQ4.js} +3 -3
- package/dist/{chunk-YGAOLHKN.js → chunk-JYRFO4EH.js} +4 -4
- package/dist/{chunk-F3FC7SMO.js → chunk-JZM5QUTE.js} +2 -2
- package/dist/{chunk-PV7E2FGY.js → chunk-KXLRMM35.js} +6 -6
- package/dist/{chunk-PHXCPVSX.js → chunk-L4H6MQ7I.js} +2 -2
- package/dist/{chunk-FKCJH7KP.js → chunk-LQKITFGB.js} +2 -2
- package/dist/{chunk-27VEKQQA.js → chunk-MXRMNKSY.js} +3 -3
- package/dist/{chunk-EIKBKQ6L.js → chunk-QHT4W3RU.js} +127 -9
- package/dist/chunk-QHT4W3RU.js.map +1 -0
- package/dist/{chunk-CDGGQGUU.js → chunk-SBFDL2EQ.js} +2 -2
- package/dist/{chunk-I2THZUAF.js → chunk-SECMZACR.js} +2 -2
- package/dist/{chunk-JU3GHO6J.js → chunk-TGBJ2OMK.js} +2 -2
- package/dist/{chunk-DE6ENYLG.js → chunk-TXLGHDHS.js} +2 -2
- package/dist/{chunk-KGNFFCZJ.js → chunk-U4KHQTJ6.js} +2 -2
- package/dist/{chunk-Y5U35ZVS.js → chunk-UTDA43TB.js} +2 -2
- package/dist/{chunk-CK6RI52P.js → chunk-VCEH5UYS.js} +2 -2
- package/dist/{chunk-UXWGPODH.js → chunk-VRAD2FGO.js} +7 -7
- package/dist/{chunk-KKASNHGF.js → chunk-W6R43NLA.js} +19 -19
- package/dist/{chunk-UP25ZZCN.js → chunk-WUDBMLC6.js} +2 -2
- package/dist/{chunk-XG5CGR3K.js → chunk-XF4KOVDK.js} +5 -5
- package/dist/{chunk-KPXAJ77O.js → chunk-YLTG6BO4.js} +2 -2
- package/dist/cli.js +23 -23
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/index.d.ts +3990 -4
- package/dist/index.js +90 -43
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +8 -8
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-toolkit.js +7 -7
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/orchestrator.js +36 -36
- package/dist/retrieval-agents.js +2 -2
- package/dist/schemas.d.ts +74 -74
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage.js +2 -2
- package/dist/temporal-index-compat.d.ts +1 -0
- package/dist/temporal-index-compat.js +16 -0
- package/dist/temporal-index-compat.js.map +1 -0
- package/dist/temporal-index.d.ts +58 -7
- package/dist/temporal-index.js +23 -3
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -0
- package/src/access-http.ts +147 -6
- package/src/access-mcp-cancellation.test.ts +2 -2
- package/src/access-operations.ts +2 -0
- package/src/access-surface-catalog.ts +2 -0
- package/src/index.ts +53 -0
- package/src/orchestration/consolidation-run.ts +5 -5
- package/src/orchestration/contradiction-linking-coordinator.ts +2 -2
- package/src/orchestration/lifecycle-policy-coordinator.ts +2 -2
- package/src/orchestration/persistence-index.ts +6 -6
- package/src/orchestration/recall-search-pipeline.ts +36 -21
- package/src/orchestration/semantic-consolidation-coordinator.ts +2 -2
- package/src/orchestrator.ts +0 -4
- package/src/relay/mission-access.test.ts +379 -0
- package/src/relay/mission-access.ts +125 -0
- package/src/relay/mission-fixture.ts +301 -0
- package/src/relay/mission.test.ts +964 -0
- package/src/relay/mission.ts +1725 -0
- package/src/temporal-index-compat.ts +311 -0
- package/src/temporal-index.test.ts +273 -22
- package/src/temporal-index.ts +427 -178
- package/dist/chunk-DLPXS7BE.js.map +0 -1
- package/dist/chunk-EIKBKQ6L.js.map +0 -1
- package/dist/chunk-HHAA35A3.js.map +0 -1
- package/dist/chunk-POUD5BLV.js.map +0 -1
- package/dist/chunk-VCFADTFS.js +0 -467
- package/dist/chunk-VCFADTFS.js.map +0 -1
- /package/dist/{chunk-MLT5EG6K.js.map → chunk-3OQ4IZJN.js.map} +0 -0
- /package/dist/{chunk-QKVX4Y6D.js.map → chunk-673NPXE3.js.map} +0 -0
- /package/dist/{chunk-5CFHYQU3.js.map → chunk-DLWYAN6A.js.map} +0 -0
- /package/dist/{chunk-TY43GBX2.js.map → chunk-EQF5763Z.js.map} +0 -0
- /package/dist/{chunk-SKXBOZDF.js.map → chunk-GHVJASQ4.js.map} +0 -0
- /package/dist/{chunk-YGAOLHKN.js.map → chunk-JYRFO4EH.js.map} +0 -0
- /package/dist/{chunk-F3FC7SMO.js.map → chunk-JZM5QUTE.js.map} +0 -0
- /package/dist/{chunk-PV7E2FGY.js.map → chunk-KXLRMM35.js.map} +0 -0
- /package/dist/{chunk-PHXCPVSX.js.map → chunk-L4H6MQ7I.js.map} +0 -0
- /package/dist/{chunk-FKCJH7KP.js.map → chunk-LQKITFGB.js.map} +0 -0
- /package/dist/{chunk-27VEKQQA.js.map → chunk-MXRMNKSY.js.map} +0 -0
- /package/dist/{chunk-CDGGQGUU.js.map → chunk-SBFDL2EQ.js.map} +0 -0
- /package/dist/{chunk-I2THZUAF.js.map → chunk-SECMZACR.js.map} +0 -0
- /package/dist/{chunk-JU3GHO6J.js.map → chunk-TGBJ2OMK.js.map} +0 -0
- /package/dist/{chunk-DE6ENYLG.js.map → chunk-TXLGHDHS.js.map} +0 -0
- /package/dist/{chunk-KGNFFCZJ.js.map → chunk-U4KHQTJ6.js.map} +0 -0
- /package/dist/{chunk-Y5U35ZVS.js.map → chunk-UTDA43TB.js.map} +0 -0
- /package/dist/{chunk-CK6RI52P.js.map → chunk-VCEH5UYS.js.map} +0 -0
- /package/dist/{chunk-UXWGPODH.js.map → chunk-VRAD2FGO.js.map} +0 -0
- /package/dist/{chunk-KKASNHGF.js.map → chunk-W6R43NLA.js.map} +0 -0
- /package/dist/{chunk-UP25ZZCN.js.map → chunk-WUDBMLC6.js.map} +0 -0
- /package/dist/{chunk-XG5CGR3K.js.map → chunk-XF4KOVDK.js.map} +0 -0
- /package/dist/{chunk-KPXAJ77O.js.map → chunk-YLTG6BO4.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
|
+
}
|