@humain/humain-code-contracts 0.3.2

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 (51) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +92 -0
  3. package/dist/index.d.ts +224 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +218 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/orchestration.d.ts +327 -0
  8. package/dist/orchestration.d.ts.map +1 -0
  9. package/dist/orchestration.js +307 -0
  10. package/dist/orchestration.js.map +1 -0
  11. package/dist/rpc.d.ts +63 -0
  12. package/dist/rpc.d.ts.map +1 -0
  13. package/dist/rpc.js +147 -0
  14. package/dist/rpc.js.map +1 -0
  15. package/dist/subagent-progress.d.ts +106 -0
  16. package/dist/subagent-progress.d.ts.map +1 -0
  17. package/dist/subagent-progress.js +440 -0
  18. package/dist/subagent-progress.js.map +1 -0
  19. package/dist/testing.d.ts +390 -0
  20. package/dist/testing.d.ts.map +1 -0
  21. package/dist/testing.js +431 -0
  22. package/dist/testing.js.map +1 -0
  23. package/dist/trace-api.d.ts +293 -0
  24. package/dist/trace-api.d.ts.map +1 -0
  25. package/dist/trace-api.js +202 -0
  26. package/dist/trace-api.js.map +1 -0
  27. package/dist/trace-artifact.d.ts +94 -0
  28. package/dist/trace-artifact.d.ts.map +1 -0
  29. package/dist/trace-artifact.js +168 -0
  30. package/dist/trace-artifact.js.map +1 -0
  31. package/dist/trace-canonical.d.ts +23 -0
  32. package/dist/trace-canonical.d.ts.map +1 -0
  33. package/dist/trace-canonical.js +240 -0
  34. package/dist/trace-canonical.js.map +1 -0
  35. package/dist/trace-export.d.ts +94 -0
  36. package/dist/trace-export.d.ts.map +1 -0
  37. package/dist/trace-export.js +374 -0
  38. package/dist/trace-export.js.map +1 -0
  39. package/dist/trace-http.d.ts +1294 -0
  40. package/dist/trace-http.d.ts.map +1 -0
  41. package/dist/trace-http.js +1481 -0
  42. package/dist/trace-http.js.map +1 -0
  43. package/dist/trace-payload.d.ts +425 -0
  44. package/dist/trace-payload.d.ts.map +1 -0
  45. package/dist/trace-payload.js +1519 -0
  46. package/dist/trace-payload.js.map +1 -0
  47. package/dist/trace.d.ts +257 -0
  48. package/dist/trace.d.ts.map +1 -0
  49. package/dist/trace.js +1014 -0
  50. package/dist/trace.js.map +1 -0
  51. package/package.json +64 -0
package/dist/trace.js ADDED
@@ -0,0 +1,1014 @@
1
+ import { Type } from "typebox";
2
+ import { Compile } from "typebox/compile";
3
+ import { assertDhPlainData, DhContractError, dhCanonicalText, parseDhRawJson } from "./trace-canonical.js";
4
+ import { assertDhEventPayload, validateDhPayloadGraph } from "./trace-payload.js";
5
+ export * from "./trace-api.js";
6
+ export * from "./trace-artifact.js";
7
+ export * from "./trace-canonical.js";
8
+ export * from "./trace-export.js";
9
+ export * from "./trace-http.js";
10
+ export * from "./trace-payload.js";
11
+ /**
12
+ * HUMAIN Code agent-activity trace wire contract (v1), shared by Code, CLI and Flow producers.
13
+ *
14
+ * Three independent axes are never conflated: execution outcome (`outcome`), capture coverage
15
+ * (`DhCoverage`) and transfer state (`DhAck`). Producers redact before upload; the server
16
+ * re-checks `tenantScope`, which is producer-declared only and never trusted on its own.
17
+ */
18
+ const StrictObject = (properties) => Type.Object(properties, { additionalProperties: false });
19
+ const PositiveInteger = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
20
+ const NonnegativeInteger = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
21
+ const Digest = Type.String({ pattern: "^[a-f0-9]{64}$" });
22
+ const SafeArtifactReference = Type.String({
23
+ minLength: 1,
24
+ maxLength: 512,
25
+ pattern: "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$",
26
+ });
27
+ const BoundedString = (maxLength) => Type.String({ minLength: 1, maxLength });
28
+ const ShortString = BoundedString(256);
29
+ const DetailString = Type.String({ minLength: 1, maxLength: 1024 });
30
+ const IsoUtcTimestamp = Type.String({
31
+ minLength: 20,
32
+ maxLength: 30,
33
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?Z$",
34
+ });
35
+ export const DH_TRACE_CONTRACT_VERSION = 1;
36
+ export const DH_EVENT_SCHEMA_ID = "dh.event/1";
37
+ export const DH_ARTIFACT_SCHEMA_ID = "dh.artifact/1";
38
+ export const DH_ACK_SCHEMA_ID = "dh.ack/1";
39
+ export const DH_EVENT_MAX_UTF8_BYTES = 65536;
40
+ export const DH_ARTIFACT_MAX_BYTES = 64 * 1024 * 1024;
41
+ export const DH_MAX_EXTERNAL_IDS = 16;
42
+ export const DH_MAX_PAYLOAD_PROPERTIES = 128;
43
+ export const DH_MAX_FIELD_COVERAGE_ENTRIES = 64;
44
+ export const DH_MAX_ARTIFACT_REFS = 32;
45
+ export const DH_MAX_REMOVED_FIELD_PATHS = 256;
46
+ export const DH_MAX_ACK_IDS = 1000;
47
+ /** Maximum container nesting of `payload` (the payload object itself is depth 1). */
48
+ export const DH_PAYLOAD_MAX_DEPTH = 16;
49
+ /** Maximum number of JSON values (containers and scalars, root included) in one `payload`. */
50
+ export const DH_PAYLOAD_MAX_NODES = 4096;
51
+ /** Maximum length of any object key inside `payload`, at any depth. */
52
+ export const DH_PAYLOAD_MAX_KEY_LENGTH = 128;
53
+ /** Maximum length of a `fields` key (field path). */
54
+ export const DH_FIELD_PATH_MAX_LENGTH = 256;
55
+ // ---------------------------------------------------------------------------
56
+ // External ids and canonical trace ids
57
+ // ---------------------------------------------------------------------------
58
+ export const DH_ISSUERS = ["code", "cli", "flow"];
59
+ export const DhIssuerSchema = Type.Enum(DH_ISSUERS);
60
+ /** Producer-native id mapped onto the canonical trace id space (e.g. code.run, cli.session, flow.launch). */
61
+ export const DhExternalIdSchema = StrictObject({
62
+ issuer: DhIssuerSchema,
63
+ namespace: Type.String({ pattern: "^[a-z][a-z0-9_.-]{0,63}$", maxLength: 64 }),
64
+ value: ShortString,
65
+ mappingVersion: PositiveInteger,
66
+ });
67
+ const prefixedId = (prefix) => Type.String({
68
+ pattern: `^${prefix}_[A-Za-z0-9_-]{8,128}$`,
69
+ minLength: prefix.length + 9,
70
+ maxLength: prefix.length + 129,
71
+ });
72
+ export const DhGenerationIdSchema = prefixedId("gen");
73
+ export const DhAgentIdSchema = prefixedId("agt");
74
+ export const DhAttemptIdSchema = prefixedId("att");
75
+ export const DhEventIdSchema = prefixedId("evt");
76
+ export const DhArtifactIdSchema = prefixedId("art");
77
+ // ---------------------------------------------------------------------------
78
+ // Coverage
79
+ // ---------------------------------------------------------------------------
80
+ export const DH_COVERAGE_STATES = [
81
+ "captured",
82
+ "redacted",
83
+ "truncated",
84
+ "unavailable",
85
+ "not-applicable",
86
+ "inferred",
87
+ ];
88
+ export const DH_COVERAGE_REASONS = [
89
+ "secret_redacted",
90
+ "policy_redacted",
91
+ "size_limit",
92
+ "producer_lost",
93
+ "worker_exited_before_flush",
94
+ "legacy_producer",
95
+ "not_captured_by_version",
96
+ "provider_hidden",
97
+ "not_applicable",
98
+ "derived_from_summary",
99
+ ];
100
+ /**
101
+ * Per-field or per-artifact capture coverage. A `reason` is REQUIRED for every state except
102
+ * `captured` (enforced by the parse functions, not by the schema alone).
103
+ *
104
+ * `inferred` means the value was derived (e.g. from a summary) rather than recorded. Consumers
105
+ * MUST NOT present an `inferred` value as a recorded fact.
106
+ */
107
+ export const DhCoverageSchema = StrictObject({
108
+ state: Type.Enum(DH_COVERAGE_STATES),
109
+ reason: Type.Optional(Type.Enum(DH_COVERAGE_REASONS)),
110
+ detail: Type.Optional(DetailString),
111
+ });
112
+ // ---------------------------------------------------------------------------
113
+ // Shared sub-schemas
114
+ // ---------------------------------------------------------------------------
115
+ export const DhRedactionSchema = StrictObject({
116
+ policyId: ShortString,
117
+ policyVersion: ShortString,
118
+ removedFieldPaths: Type.Array(BoundedString(512), { maxItems: DH_MAX_REMOVED_FIELD_PATHS }),
119
+ });
120
+ /** Producer-declared scope. The server re-checks it against the authenticated producer binding. */
121
+ export const DH_SCOPE_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9_.-]{0,255}$";
122
+ export const DhScopeIdSchema = Type.String({ minLength: 1, maxLength: 256, pattern: DH_SCOPE_ID_PATTERN });
123
+ export const DhStreamIdSchema = Type.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]{1,128}$" });
124
+ export const DhTenantScopeSchema = StrictObject({
125
+ environmentId: DhScopeIdSchema,
126
+ projectId: DhScopeIdSchema,
127
+ });
128
+ export const DH_EVENT_KINDS = [
129
+ "generation.started",
130
+ "generation.finished",
131
+ "agent.spawned",
132
+ "attempt.started",
133
+ "attempt.finished",
134
+ "attempt.retry_scheduled",
135
+ "model.request",
136
+ "model.response",
137
+ "tool.call",
138
+ "tool.result",
139
+ "file.change",
140
+ "orchestration.decision",
141
+ "cancellation.requested",
142
+ "cancellation.outcome",
143
+ "usage.reported",
144
+ // Added with typed payloads (dh.payload.*/1): no legacy freeform shape, `payload.schema` is required.
145
+ // Amended into dh.event/1 before any deployment (C0 freeze doc, section 1.15). The exact list is pinned by
146
+ // test/trace-payload-conformance.test.ts and the freeze digest: a further kind is a deliberate, reviewed change.
147
+ "capture.health",
148
+ "control.correlation",
149
+ "event.amendment",
150
+ ];
151
+ export const DH_OUTCOMES = ["succeeded", "failed", "cancelled", "unknown"];
152
+ export const DhFailureSchema = StrictObject({
153
+ code: Type.String({ pattern: "^[a-z][a-z0-9_.-]{0,63}$", maxLength: 64 }),
154
+ message: Type.Optional(DetailString),
155
+ });
156
+ // ---------------------------------------------------------------------------
157
+ // Event
158
+ // ---------------------------------------------------------------------------
159
+ export const DhEventV1Schema = StrictObject({
160
+ schema: Type.Literal(DH_EVENT_SCHEMA_ID),
161
+ eventId: DhEventIdSchema,
162
+ generationId: DhGenerationIdSchema,
163
+ agentId: DhAgentIdSchema,
164
+ attemptId: Type.Optional(DhAttemptIdSchema),
165
+ parentAgentId: Type.Optional(DhAgentIdSchema),
166
+ producer: StrictObject({
167
+ issuer: DhIssuerSchema,
168
+ version: ShortString,
169
+ mode: ShortString,
170
+ }),
171
+ externalIds: Type.Array(DhExternalIdSchema, { maxItems: DH_MAX_EXTERNAL_IDS }),
172
+ /** Monotonic per producer stream. */
173
+ sequence: NonnegativeInteger,
174
+ streamId: DhStreamIdSchema,
175
+ occurredAt: IsoUtcTimestamp,
176
+ recordedAt: IsoUtcTimestamp,
177
+ kind: Type.Enum(DH_EVENT_KINDS),
178
+ outcome: Type.Optional(Type.Enum(DH_OUTCOMES)),
179
+ failure: Type.Optional(DhFailureSchema),
180
+ retryOfAttemptId: Type.Optional(DhAttemptIdSchema),
181
+ attemptNumber: Type.Optional(PositiveInteger),
182
+ /**
183
+ * JSON-safe values only. TypeBox cannot express the depth, node, key-length or forbidden-key
184
+ * limits (and drops `maxLength` on Record key schemas), so `parseDhEvent` enforces them with
185
+ * `assertJsonPayload` before cloning.
186
+ */
187
+ payload: Type.Record(Type.String(), Type.Unknown(), {
188
+ maxProperties: DH_MAX_PAYLOAD_PROPERTIES,
189
+ }),
190
+ /**
191
+ * Field path (e.g. `payload.messages`) -> coverage for that field. Key length and pattern are
192
+ * enforced by `parseDhEvent` (`assertFieldPaths`), not by the schema.
193
+ */
194
+ fields: Type.Record(Type.String(), DhCoverageSchema, {
195
+ maxProperties: DH_MAX_FIELD_COVERAGE_ENTRIES,
196
+ }),
197
+ artifacts: Type.Optional(Type.Array(DhArtifactIdSchema, { maxItems: DH_MAX_ARTIFACT_REFS })),
198
+ redaction: DhRedactionSchema,
199
+ tenantScope: DhTenantScopeSchema,
200
+ });
201
+ // ---------------------------------------------------------------------------
202
+ // Artifact
203
+ // ---------------------------------------------------------------------------
204
+ export const DH_ARTIFACT_KINDS = [
205
+ "file-diff",
206
+ "file-snapshot",
207
+ "tool-output",
208
+ "model-context",
209
+ "log",
210
+ "bundle",
211
+ ];
212
+ export const DH_ARTIFACT_MEDIA_TYPES = [
213
+ "text/plain",
214
+ "text/x-diff",
215
+ "application/json",
216
+ "application/octet-stream",
217
+ ];
218
+ export const DhArtifactV1Schema = StrictObject({
219
+ schema: Type.Literal(DH_ARTIFACT_SCHEMA_ID),
220
+ artifactId: DhArtifactIdSchema,
221
+ generationId: DhGenerationIdSchema,
222
+ agentId: DhAgentIdSchema,
223
+ attemptId: Type.Optional(DhAttemptIdSchema),
224
+ eventId: DhEventIdSchema,
225
+ kind: Type.Enum(DH_ARTIFACT_KINDS),
226
+ mediaType: Type.Enum(DH_ARTIFACT_MEDIA_TYPES),
227
+ sizeBytes: Type.Integer({ minimum: 0, maximum: DH_ARTIFACT_MAX_BYTES }),
228
+ sha256: Digest,
229
+ logicalPath: Type.Optional(SafeArtifactReference),
230
+ coverage: DhCoverageSchema,
231
+ redaction: DhRedactionSchema,
232
+ tenantScope: DhTenantScopeSchema,
233
+ });
234
+ // ---------------------------------------------------------------------------
235
+ // Ack
236
+ // ---------------------------------------------------------------------------
237
+ export const DH_REJECTION_CODES = [
238
+ "duplicate_conflict",
239
+ "schema_invalid",
240
+ "scope_denied",
241
+ "tombstoned",
242
+ "too_large",
243
+ "rate_limited",
244
+ ];
245
+ /**
246
+ * Server acknowledgement for a producer stream. Delivering an identical record again is accepted
247
+ * idempotently (it appears in `accepted`); delivering the same id with different content is
248
+ * rejected as `duplicate_conflict` and never overwrites the stored record.
249
+ */
250
+ export const DhAckV1Schema = StrictObject({
251
+ schema: Type.Literal(DH_ACK_SCHEMA_ID),
252
+ streamId: DhStreamIdSchema,
253
+ ackedThroughSequence: NonnegativeInteger,
254
+ accepted: StrictObject({
255
+ eventIds: Type.Array(DhEventIdSchema, { maxItems: DH_MAX_ACK_IDS }),
256
+ artifactIds: Type.Array(DhArtifactIdSchema, { maxItems: DH_MAX_ACK_IDS }),
257
+ }),
258
+ rejected: Type.Array(StrictObject({
259
+ id: Type.String({ pattern: "^(?:evt|art)_[A-Za-z0-9_-]{8,128}$", maxLength: 136 }),
260
+ code: Type.Enum(DH_REJECTION_CODES),
261
+ retryable: Type.Boolean(),
262
+ }), { maxItems: DH_MAX_ACK_IDS }),
263
+ receivedAt: IsoUtcTimestamp,
264
+ });
265
+ // ---------------------------------------------------------------------------
266
+ // Parsing
267
+ // ---------------------------------------------------------------------------
268
+ const EventValidator = Compile(DhEventV1Schema);
269
+ const ArtifactValidator = Compile(DhArtifactV1Schema);
270
+ const AckValidator = Compile(DhAckV1Schema);
271
+ const textEncoder = new TextEncoder();
272
+ function schemaError(label, errors) {
273
+ const field = errors[0]?.instancePath.split("/")[1];
274
+ const properties = label === "dh.event/1"
275
+ ? DhEventV1Schema.properties
276
+ : label === "dh.artifact/1"
277
+ ? DhArtifactV1Schema.properties
278
+ : DhAckV1Schema.properties;
279
+ const where = field !== undefined && Object.hasOwn(properties, field) ? ` at /${field}` : "";
280
+ return new DhContractError("schema_invalid", `Invalid ${label}${where}`);
281
+ }
282
+ function assertCoverage(label, coverage) {
283
+ if (coverage.state !== "captured" && coverage.reason === undefined) {
284
+ throw new DhContractError("schema_invalid", `Invalid ${label}: coverage has state but no reason`);
285
+ }
286
+ }
287
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
288
+ const FIELD_PATH_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)*$/;
289
+ const TIMESTAMP_PARTS = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/;
290
+ /**
291
+ * Returns the UTC instant (epoch milliseconds, sub-millisecond digits ignored) of a calendar-valid
292
+ * timestamp, or undefined. `Date.parse` rolls impossible dates over (2025-02-30 becomes 03-02), so
293
+ * the parsed instant must round-trip to the same calendar fields.
294
+ */
295
+ function parseInstant(timestamp) {
296
+ const parts = TIMESTAMP_PARTS.exec(timestamp);
297
+ if (parts === null)
298
+ return undefined;
299
+ const [, year, month, day, hour, minute, second, fraction] = parts;
300
+ const base = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
301
+ const whole = Date.parse(`${base}.000Z`);
302
+ if (Number.isNaN(whole))
303
+ return undefined;
304
+ if (new Date(whole).toISOString().slice(0, 19) !== base)
305
+ return undefined;
306
+ return whole + (fraction === undefined ? 0 : Number(fraction.slice(0, 3).padEnd(3, "0")));
307
+ }
308
+ function assertTimestamp(label, path, timestamp) {
309
+ if (parseInstant(timestamp) === undefined) {
310
+ throw new DhContractError("schema_invalid", `Invalid ${label}: ${path} is not a calendar-valid UTC instant`);
311
+ }
312
+ }
313
+ function assertFieldPaths(fields) {
314
+ for (const path of Object.keys(fields)) {
315
+ if (path.length > DH_FIELD_PATH_MAX_LENGTH) {
316
+ throw new DhContractError("schema_invalid", `Invalid dh.event/1: fields key exceeds ${DH_FIELD_PATH_MAX_LENGTH} characters`);
317
+ }
318
+ if (!FIELD_PATH_PATTERN.test(path) || path.split(".").some((segment) => FORBIDDEN_KEYS.has(segment))) {
319
+ throw new DhContractError("schema_invalid", "Invalid dh.event/1: fields key is not a valid field path");
320
+ }
321
+ }
322
+ }
323
+ function fail(_path, reason) {
324
+ throw new DhContractError(reason.startsWith("exceeds") ? "too_large" : "schema_invalid", `Invalid dh.event/1: payload ${reason}`);
325
+ }
326
+ /**
327
+ * Iterative (non-recursive) walk that admits only JSON-safe values: null, boolean, finite number,
328
+ * string, array and plain object. Enforces depth, total node count and key limits, and rejects the
329
+ * keys `__proto__`, `constructor` and `prototype` at any depth, plus accessor properties on both
330
+ * objects and arrays. Counting happens when a value is queued, so the work stack never exceeds
331
+ * `DH_PAYLOAD_MAX_NODES` entries. Keys come from `Object.keys`, which includes own `__proto__`
332
+ * properties created by `JSON.parse`, so the same walk is safe to run on a parsed result.
333
+ */
334
+ function assertJsonPayload(payload) {
335
+ const stack = [{ value: payload, depth: 1, path: "payload" }];
336
+ let nodes = 1;
337
+ while (stack.length > 0) {
338
+ const item = stack.pop();
339
+ if (item === undefined)
340
+ break;
341
+ const { value, depth, path } = item;
342
+ if (value === null || typeof value === "boolean" || typeof value === "string")
343
+ continue;
344
+ if (typeof value === "number") {
345
+ if (!Number.isFinite(value))
346
+ fail(path, "is not a finite number");
347
+ continue;
348
+ }
349
+ if (typeof value !== "object")
350
+ fail(path, `has non-JSON type ${typeof value}`);
351
+ if (depth > DH_PAYLOAD_MAX_DEPTH)
352
+ fail(path, `exceeds maximum nesting depth ${DH_PAYLOAD_MAX_DEPTH}`);
353
+ const container = value;
354
+ const children = [];
355
+ if (Array.isArray(container)) {
356
+ if (nodes + container.length > DH_PAYLOAD_MAX_NODES) {
357
+ fail(path, `exceeds maximum of ${DH_PAYLOAD_MAX_NODES} payload nodes`);
358
+ }
359
+ for (let index = 0; index < container.length; index++) {
360
+ const descriptor = Object.getOwnPropertyDescriptor(container, String(index));
361
+ if (descriptor === undefined)
362
+ fail(path, "is a sparse array");
363
+ if (!("value" in descriptor))
364
+ fail(`${path}[${index}]`, "is an accessor property");
365
+ children.push([`${path}[${index}]`, descriptor.value]);
366
+ }
367
+ }
368
+ else {
369
+ const proto = Object.getPrototypeOf(container);
370
+ if (proto !== Object.prototype && proto !== null)
371
+ fail(path, "is not a plain object");
372
+ const keys = Object.keys(container);
373
+ if (keys.length > DH_MAX_PAYLOAD_PROPERTIES)
374
+ fail(path, `exceeds maximum of ${DH_MAX_PAYLOAD_PROPERTIES} payload properties`);
375
+ if (nodes + keys.length > DH_PAYLOAD_MAX_NODES) {
376
+ fail(path, `exceeds maximum of ${DH_PAYLOAD_MAX_NODES} payload nodes`);
377
+ }
378
+ for (const key of keys) {
379
+ if (key.length > DH_PAYLOAD_MAX_KEY_LENGTH) {
380
+ fail(path, `has a key longer than ${DH_PAYLOAD_MAX_KEY_LENGTH} characters`);
381
+ }
382
+ if (FORBIDDEN_KEYS.has(key))
383
+ fail(path, `has forbidden key "${key}"`);
384
+ const descriptor = Object.getOwnPropertyDescriptor(container, key);
385
+ if (descriptor === undefined || !("value" in descriptor))
386
+ fail(`${path}.${key}`, "is an accessor property");
387
+ children.push([`${path}.${key}`, descriptor?.value]);
388
+ }
389
+ }
390
+ nodes += children.length;
391
+ for (const [childPath, child] of children)
392
+ stack.push({ value: child, depth: depth + 1, path: childPath });
393
+ }
394
+ }
395
+ /**
396
+ * Attempt-linked fields need an `attemptId`: attempt lifecycle kinds, retry links and attempt
397
+ * numbers are meaningless (and would skip attempt validation) without one. Returns the violated
398
+ * rule, or undefined.
399
+ */
400
+ function attemptIdViolation(event) {
401
+ if (event.attemptId !== undefined)
402
+ return undefined;
403
+ if (event.kind === "attempt.started" ||
404
+ event.kind === "attempt.finished" ||
405
+ event.kind === "attempt.retry_scheduled") {
406
+ return "attemptId is required on attempt lifecycle events";
407
+ }
408
+ if (event.retryOfAttemptId !== undefined)
409
+ return "attemptId is required when retryOfAttemptId is present";
410
+ if (event.attemptNumber !== undefined)
411
+ return "attemptId is required when attemptNumber is present";
412
+ return undefined;
413
+ }
414
+ function deepFreeze(root) {
415
+ const stack = [root];
416
+ while (stack.length > 0) {
417
+ const value = stack.pop();
418
+ if (value === null || typeof value !== "object" || Object.isFrozen(value))
419
+ continue;
420
+ Object.freeze(value);
421
+ for (const child of Object.values(value))
422
+ stack.push(child);
423
+ }
424
+ return root;
425
+ }
426
+ /** Content checks the schema cannot express. Run on the input and again on the serialized clone. */
427
+ function assertEventContent(event) {
428
+ assertJsonPayload(event.payload);
429
+ assertFieldPaths(event.fields);
430
+ assertTimestamp("dh.event/1", "occurredAt", event.occurredAt);
431
+ assertTimestamp("dh.event/1", "recordedAt", event.recordedAt);
432
+ for (const coverage of Object.values(event.fields)) {
433
+ assertCoverage("dh.event/1", coverage);
434
+ }
435
+ const violation = attemptIdViolation(event);
436
+ if (violation !== undefined)
437
+ throw new DhContractError("schema_invalid", `Invalid dh.event/1: ${violation}`);
438
+ }
439
+ /**
440
+ * The input is validated, serialized exactly once, and the serialized string is parsed and fully
441
+ * re-validated. The shared descriptor gate rejects getters and toJSON before invocation.
442
+ * The byte limit is measured on the same canonical string returned by serialization.
443
+ */
444
+ export function parseDhEvent(value) {
445
+ if (value instanceof Uint8Array)
446
+ value = parseDhRawJson(value, { maxBytes: DH_EVENT_MAX_UTF8_BYTES });
447
+ assertDhPlainData(value);
448
+ if (!EventValidator.Check(value))
449
+ throw schemaError("dh.event/1", EventValidator.Errors(value));
450
+ assertEventContent(value);
451
+ const text = dhCanonicalText(value);
452
+ if (textEncoder.encode(text).length > DH_EVENT_MAX_UTF8_BYTES) {
453
+ throw new DhContractError("too_large", "Invalid dh.event/1: exceeds UTF-8 bytes limit");
454
+ }
455
+ const parsed = JSON.parse(text);
456
+ if (!EventValidator.Check(parsed))
457
+ throw schemaError("dh.event/1", EventValidator.Errors(parsed));
458
+ assertEventContent(parsed);
459
+ assertDhEventPayload(parsed);
460
+ return deepFreeze(parsed);
461
+ }
462
+ export function parseDhArtifact(value) {
463
+ assertDhPlainData(value);
464
+ if (!ArtifactValidator.Check(value))
465
+ throw schemaError("dh.artifact/1", ArtifactValidator.Errors(value));
466
+ assertCoverage("dh.artifact/1", value.coverage);
467
+ const parsed = JSON.parse(dhCanonicalText(value));
468
+ if (!ArtifactValidator.Check(parsed))
469
+ throw schemaError("dh.artifact/1", ArtifactValidator.Errors(parsed));
470
+ assertCoverage("dh.artifact/1", parsed.coverage);
471
+ return deepFreeze(parsed);
472
+ }
473
+ export function parseDhAck(value) {
474
+ assertDhPlainData(value);
475
+ if (!AckValidator.Check(value))
476
+ throw schemaError("dh.ack/1", AckValidator.Errors(value));
477
+ assertTimestamp("dh.ack/1", "receivedAt", value.receivedAt);
478
+ const parsed = JSON.parse(dhCanonicalText(value));
479
+ if (!AckValidator.Check(parsed))
480
+ throw schemaError("dh.ack/1", AckValidator.Errors(parsed));
481
+ assertTimestamp("dh.ack/1", "receivedAt", parsed.receivedAt);
482
+ return deepFreeze(parsed);
483
+ }
484
+ /** Single-record shape only. It never treats a retry link as proof that its source failed. */
485
+ export function assertDhManagedLifecycle(input) {
486
+ const event = parseDhEvent(input);
487
+ const payload = event.payload;
488
+ if (!["attempt.started", "attempt.finished", "attempt.retry_scheduled"].includes(event.kind) ||
489
+ payload.schema !== "dh.payload.attempt-lifecycle/1")
490
+ throw new DhContractError("schema_invalid");
491
+ if (event.retryOfAttemptId !== undefined && event.kind !== "attempt.started")
492
+ throw new DhContractError("schema_invalid");
493
+ if (event.attemptNumber !== undefined &&
494
+ (!Number.isSafeInteger(event.attemptNumber) || event.attemptNumber < 1 || event.attemptNumber > 10000))
495
+ throw new DhContractError("schema_invalid");
496
+ if (event.kind === "attempt.started" &&
497
+ (event.attemptNumber === undefined ||
498
+ payload.attemptNumber !== event.attemptNumber ||
499
+ event.outcome !== undefined ||
500
+ event.failure !== undefined))
501
+ throw new DhContractError("schema_invalid");
502
+ if (event.kind === "attempt.retry_scheduled" &&
503
+ (typeof payload.nextAttemptNumber !== "number" ||
504
+ payload.nextAttemptNumber < 2 ||
505
+ payload.nextAttemptNumber > 10000))
506
+ throw new DhContractError("schema_invalid");
507
+ }
508
+ /** Original terminals remain immutable. Only committed, explicitly authorized same-owner resolutions apply. */
509
+ export function resolveDhEffectiveTerminals(events, ownership) {
510
+ const terminals = new Map();
511
+ const byId = new Map();
512
+ for (const event of events) {
513
+ byId.set(event.eventId, event);
514
+ if (event.kind !== "attempt.finished" || event.attemptId === undefined)
515
+ continue;
516
+ const prior = terminals.get(event.attemptId);
517
+ terminals.set(event.attemptId, {
518
+ event: prior?.event ?? event,
519
+ outcome: prior?.outcome ?? event.outcome,
520
+ conflict: prior !== undefined,
521
+ authorityUnresolved: prior?.authorityUnresolved ?? false,
522
+ });
523
+ }
524
+ const resolutions = new Set();
525
+ for (const amendment of events) {
526
+ if (amendment.kind !== "event.amendment" ||
527
+ amendment.payload.schema !== "dh.payload.amendment/1" ||
528
+ amendment.payload.relation !== "resolves-unknown")
529
+ continue;
530
+ const target = byId.get(String(amendment.payload.amendsEventId));
531
+ if (target?.kind !== "attempt.finished" || target.attemptId === undefined)
532
+ continue;
533
+ const terminal = terminals.get(target.attemptId);
534
+ if (!terminal)
535
+ continue;
536
+ const replacement = amendment.payload.replacement;
537
+ const outcome = replacement !== null && typeof replacement === "object" && !Array.isArray(replacement)
538
+ ? replacement.outcome
539
+ : undefined;
540
+ const a = ownership?.receiptOwners.get(target.eventId);
541
+ const b = ownership?.receiptOwners.get(amendment.eventId);
542
+ const sameOwner = a !== undefined &&
543
+ b !== undefined &&
544
+ a.bindingId === b.bindingId &&
545
+ a.originProducerInstanceId === b.originProducerInstanceId &&
546
+ a.originStreamId === b.originStreamId &&
547
+ a.originIssuer === b.originIssuer &&
548
+ a.originStreamId === target.streamId &&
549
+ b.originStreamId === amendment.streamId &&
550
+ a.originIssuer === target.producer.issuer &&
551
+ b.originIssuer === amendment.producer.issuer;
552
+ const conflict = terminal.conflict ||
553
+ resolutions.has(target.eventId) ||
554
+ target.outcome !== "unknown" ||
555
+ target.agentId !== amendment.agentId ||
556
+ target.attemptId !== amendment.attemptId ||
557
+ target.generationId !== amendment.generationId ||
558
+ scopeKey(target.tenantScope) !== scopeKey(amendment.tenantScope) ||
559
+ target.streamId !== amendment.streamId ||
560
+ target.sequence >= amendment.sequence ||
561
+ amendment.payload.amendsKind !== target.kind ||
562
+ !outcome ||
563
+ outcome === "unknown";
564
+ const authorized = ownership?.authorizedAmendmentEventIds.has(amendment.eventId) === true && sameOwner;
565
+ resolutions.add(target.eventId);
566
+ terminals.set(target.attemptId, {
567
+ event: terminal.event,
568
+ outcome: !conflict && authorized ? outcome : terminal.outcome,
569
+ ...(authorized && !conflict ? { resolutionEventId: amendment.eventId } : {}),
570
+ conflict,
571
+ authorityUnresolved: terminal.authorityUnresolved || !authorized,
572
+ });
573
+ }
574
+ return terminals;
575
+ }
576
+ /** Eligibility uses durable authenticated context, not source absence or producer timestamps. */
577
+ export function assertDhRetryAdmission(start, context) {
578
+ assertDhManagedLifecycle(start);
579
+ if (start.kind !== "attempt.started")
580
+ throw new DhContractError("schema_invalid");
581
+ function deny() {
582
+ throw new DhContractError("not_eligible");
583
+ }
584
+ const target = context.starts.find((event) => event.attemptId === start.attemptId);
585
+ if (target) {
586
+ if (start.retryOfAttemptId !== undefined &&
587
+ context.retryClaims.get(start.retryOfAttemptId) === start.attemptId &&
588
+ dhCanonicalText(target) === dhCanonicalText(start))
589
+ return;
590
+ deny();
591
+ }
592
+ const priorId = start.retryOfAttemptId;
593
+ const starts = context.starts.filter((event) => event.kind === "attempt.started" && event.agentId === start.agentId);
594
+ if (priorId === undefined) {
595
+ if (starts.length !== 0 || start.attemptNumber !== 1)
596
+ deny();
597
+ return;
598
+ }
599
+ const source = context.starts.find((event) => event.attemptId === priorId);
600
+ if (source?.kind !== "attempt.started" ||
601
+ source.agentId !== start.agentId ||
602
+ source.generationId !== start.generationId ||
603
+ scopeKey(source.tenantScope) !== scopeKey(start.tenantScope))
604
+ deny();
605
+ try {
606
+ assertDhManagedLifecycle(source);
607
+ }
608
+ catch {
609
+ deny();
610
+ }
611
+ if (source.attemptNumber === undefined ||
612
+ start.attemptNumber !== source.attemptNumber + 1 ||
613
+ starts.some((event) => event.attemptNumber === undefined || event.attemptNumber > source.attemptNumber))
614
+ deny();
615
+ if (new Set(starts.map((event) => event.attemptNumber)).size !== starts.length)
616
+ deny();
617
+ const terminal = resolveDhEffectiveTerminals([...context.terminals, ...context.amendments], context).get(priorId);
618
+ if (terminal?.outcome !== "failed" ||
619
+ terminal.conflict ||
620
+ terminal.authorityUnresolved ||
621
+ terminal.event.agentId !== start.agentId ||
622
+ terminal.event.generationId !== start.generationId ||
623
+ scopeKey(terminal.event.tenantScope) !== scopeKey(start.tenantScope))
624
+ deny();
625
+ const claim = context.retryClaims.get(priorId);
626
+ if (claim !== undefined && claim !== start.attemptId)
627
+ deny();
628
+ }
629
+ function scopeKey(scope) {
630
+ return JSON.stringify([scope.environmentId, scope.projectId]);
631
+ }
632
+ /**
633
+ * Validates cross-record integrity of already-parsed records. Does not re-run schema checks, but
634
+ * reports (rather than compares) timestamps that are not calendar-valid UTC instants.
635
+ */
636
+ export function validateDhTraceBundle(bundle, ownership) {
637
+ const errors = [];
638
+ const { events, artifacts } = bundle;
639
+ const generationIds = new Set();
640
+ const scopes = new Set();
641
+ for (const record of [...events, ...artifacts]) {
642
+ generationIds.add(record.generationId);
643
+ scopes.add(scopeKey(record.tenantScope));
644
+ }
645
+ if (generationIds.size > 1)
646
+ errors.push("records span multiple generationIds; expected one");
647
+ if (scopes.size > 1)
648
+ errors.push("records span multiple tenantScopes; expected one");
649
+ // Timestamps are compared only when calendar-valid; invalid ones are reported once.
650
+ const occurredAt = new Map();
651
+ for (const event of events) {
652
+ const occurred = parseInstant(event.occurredAt);
653
+ if (occurred === undefined)
654
+ errors.push("occurredAt is not a calendar-valid UTC instant");
655
+ else
656
+ occurredAt.set(event, occurred);
657
+ if (parseInstant(event.recordedAt) === undefined) {
658
+ errors.push("recordedAt is not a calendar-valid UTC instant");
659
+ }
660
+ }
661
+ const eventsById = new Map();
662
+ const streamSequences = new Set();
663
+ for (const event of events) {
664
+ if (eventsById.has(event.eventId))
665
+ errors.push("duplicate eventId");
666
+ else
667
+ eventsById.set(event.eventId, event);
668
+ const key = JSON.stringify([event.streamId, event.sequence]);
669
+ if (streamSequences.has(key))
670
+ errors.push("duplicate (streamId, sequence)");
671
+ streamSequences.add(key);
672
+ }
673
+ // Artifact links are reciprocal: artifact.eventId <-> event.artifacts, with matching owner.
674
+ const artifactsById = new Map();
675
+ for (const artifact of artifacts) {
676
+ if (artifactsById.has(artifact.artifactId))
677
+ errors.push("duplicate artifactId");
678
+ else
679
+ artifactsById.set(artifact.artifactId, artifact);
680
+ const event = eventsById.get(artifact.eventId);
681
+ if (event === undefined) {
682
+ errors.push("artifact references unknown eventId");
683
+ continue;
684
+ }
685
+ if (!(event.artifacts ?? []).includes(artifact.artifactId)) {
686
+ errors.push("artifact is not listed in artifacts of event");
687
+ }
688
+ if (artifact.agentId !== event.agentId) {
689
+ errors.push("artifact agentId does not match event");
690
+ }
691
+ if (artifact.attemptId !== event.attemptId) {
692
+ errors.push("artifact attemptId does not match event");
693
+ }
694
+ }
695
+ for (const event of events) {
696
+ for (const ref of event.artifacts ?? []) {
697
+ const artifact = artifactsById.get(ref);
698
+ if (artifact === undefined)
699
+ errors.push("event references unknown artifact");
700
+ else if (artifact.eventId !== event.eventId) {
701
+ errors.push("event lists artifact which names a different event");
702
+ }
703
+ }
704
+ }
705
+ validateAgentHierarchy(events, errors);
706
+ validateAttempts(events, occurredAt, errors, ownership);
707
+ validateDelegations(events, eventsById, errors);
708
+ errors.push(...validateDhPayloadGraph(events, artifacts, ownership));
709
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
710
+ }
711
+ /** Validate declared subagent provenance without requiring a result from unfinished work. */
712
+ function validateDelegations(events, eventsById, errors) {
713
+ const calls = new Map();
714
+ const spawns = new Map();
715
+ const childrenByCall = new Map();
716
+ for (const event of events) {
717
+ if (event.kind === "tool.call" && typeof event.payload.callId === "string") {
718
+ const key = JSON.stringify([event.agentId, event.attemptId ?? null, event.payload.callId]);
719
+ if (calls.has(key))
720
+ errors.push("duplicate tool call identity");
721
+ else
722
+ calls.set(key, event);
723
+ }
724
+ if (event.kind !== "agent.spawned")
725
+ continue;
726
+ if (spawns.has(event.agentId))
727
+ errors.push("duplicate agent.spawned");
728
+ else
729
+ spawns.set(event.agentId, event);
730
+ if (event.payload.via !== "subagent" && event.payload.spawnedByEventId === undefined)
731
+ continue;
732
+ const sourceId = event.payload.spawnedByEventId;
733
+ if (typeof sourceId !== "string" || sourceId.length === 0) {
734
+ const coverage = event.fields["payload.spawnedByEventId"] ?? event.fields.payload;
735
+ if (!coverage || !["unavailable", "redacted", "truncated"].includes(coverage.state) || !coverage.reason) {
736
+ errors.push("subagent spawn has no call provenance or loss coverage");
737
+ }
738
+ continue;
739
+ }
740
+ const call = eventsById.get(sourceId);
741
+ if (call?.kind !== "tool.call" || call.payload.toolName !== "subagent") {
742
+ errors.push("spawn references no subagent tool.call");
743
+ }
744
+ else if (call.agentId !== event.parentAgentId || typeof call.payload.callId !== "string") {
745
+ errors.push("spawn does not belong to the parent call");
746
+ }
747
+ else {
748
+ if (call.streamId === event.streamId && call.sequence >= event.sequence) {
749
+ errors.push("spawn precedes its parent call in the producer stream");
750
+ }
751
+ const children = childrenByCall.get(call.eventId) ?? new Set();
752
+ children.add(event.agentId);
753
+ childrenByCall.set(call.eventId, children);
754
+ }
755
+ }
756
+ for (const event of events) {
757
+ if (event.kind !== "tool.result" || event.payload.toolName !== "subagent")
758
+ continue;
759
+ const callId = event.payload.callId;
760
+ const key = JSON.stringify([event.agentId, event.attemptId ?? null, callId]);
761
+ const call = typeof callId === "string" ? calls.get(key) : undefined;
762
+ if (call?.payload.toolName !== "subagent") {
763
+ errors.push("subagent result has no matching parent call");
764
+ continue;
765
+ }
766
+ if (call.streamId === event.streamId && call.sequence >= event.sequence) {
767
+ errors.push("result precedes its parent call in the producer stream");
768
+ }
769
+ const one = event.payload.childAgentId;
770
+ const many = event.payload.childAgentIds;
771
+ if (one !== undefined && many !== undefined) {
772
+ errors.push("use childAgentId or childAgentIds, not both");
773
+ continue;
774
+ }
775
+ if (one !== undefined && typeof one !== "string") {
776
+ errors.push("childAgentId must be a string");
777
+ continue;
778
+ }
779
+ if (many !== undefined && (!Array.isArray(many) || many.some((id) => typeof id !== "string"))) {
780
+ errors.push("childAgentIds must be an array of strings");
781
+ continue;
782
+ }
783
+ const children = typeof one === "string" ? [one] : Array.isArray(many) ? many : [];
784
+ const seen = new Set();
785
+ for (const child of children) {
786
+ if (typeof child !== "string")
787
+ continue;
788
+ if (seen.has(child))
789
+ errors.push("duplicate child");
790
+ seen.add(child);
791
+ const spawn = spawns.get(child);
792
+ if (!spawn || spawn.parentAgentId !== event.agentId || spawn.payload.spawnedByEventId !== call.eventId) {
793
+ errors.push("child was not spawned by this call");
794
+ }
795
+ else if (spawn.streamId === event.streamId && spawn.sequence >= event.sequence) {
796
+ errors.push("result precedes child in the producer stream");
797
+ }
798
+ }
799
+ const coverage = event.fields["payload.childAgentIds"] ?? event.fields["payload.childAgentId"] ?? event.fields.payload;
800
+ const missingCovered = coverage && ["unavailable", "redacted", "truncated"].includes(coverage.state) && coverage.reason;
801
+ if (!missingCovered) {
802
+ for (const child of childrenByCall.get(call.eventId) ?? []) {
803
+ if (!seen.has(child))
804
+ errors.push("result omits observed child");
805
+ }
806
+ }
807
+ }
808
+ }
809
+ /** One parent per agent, no ancestry cycles, exactly one root agent per generation. */
810
+ function validateAgentHierarchy(events, errors) {
811
+ const parentOf = new Map();
812
+ const agentsWithoutParentEvent = new Set();
813
+ const spawnedAgents = new Set();
814
+ const generationOf = new Map();
815
+ for (const event of events) {
816
+ generationOf.set(event.agentId, event.generationId);
817
+ if (event.kind === "agent.spawned")
818
+ spawnedAgents.add(event.agentId);
819
+ if (event.parentAgentId === undefined) {
820
+ agentsWithoutParentEvent.add(event.agentId);
821
+ continue;
822
+ }
823
+ const known = parentOf.get(event.agentId);
824
+ if (known === undefined)
825
+ parentOf.set(event.agentId, event.parentAgentId);
826
+ else if (known !== event.parentAgentId) {
827
+ errors.push("agent has conflicting parentAgentId values");
828
+ }
829
+ }
830
+ for (const agentId of agentsWithoutParentEvent) {
831
+ if (parentOf.has(agentId))
832
+ errors.push("agent has events both with and without parentAgentId");
833
+ }
834
+ for (const [agentId, parent] of parentOf) {
835
+ if (parent === agentId)
836
+ errors.push("agent is its own parent");
837
+ else if (!generationOf.has(parent))
838
+ errors.push("unknown parentAgentId");
839
+ else if (parentOf.has(parent) && !spawnedAgents.has(parent)) {
840
+ errors.push("parentAgentId was never spawned in this generation");
841
+ }
842
+ }
843
+ // Cycle detection: walk each ancestry chain once; revisiting a node on the current walk is a cycle.
844
+ const settled = new Set();
845
+ for (const start of parentOf.keys()) {
846
+ const walk = new Set();
847
+ let current = start;
848
+ while (current !== undefined && !settled.has(current)) {
849
+ if (walk.has(current)) {
850
+ errors.push("agent ancestry cycle");
851
+ break;
852
+ }
853
+ walk.add(current);
854
+ current = parentOf.get(current);
855
+ }
856
+ for (const visited of walk)
857
+ settled.add(visited);
858
+ }
859
+ const rootsByGeneration = new Map();
860
+ for (const [agentId, generationId] of generationOf) {
861
+ if (parentOf.has(agentId))
862
+ continue;
863
+ const roots = rootsByGeneration.get(generationId) ?? [];
864
+ roots.push(agentId);
865
+ rootsByGeneration.set(generationId, roots);
866
+ }
867
+ for (const generationId of new Set(generationOf.values())) {
868
+ const roots = rootsByGeneration.get(generationId) ?? [];
869
+ if (roots.length !== 1) {
870
+ errors.push("generation must have exactly one root agent");
871
+ }
872
+ }
873
+ }
874
+ /** Attempt ownership, uniqueness, chronology and retry linkage. */
875
+ function validateAttempts(events, occurredAt, errors, ownership) {
876
+ const effective = resolveDhEffectiveTerminals(events, ownership);
877
+ const retryClaims = new Map();
878
+ const started = new Map();
879
+ const finished = new Map();
880
+ for (const event of events) {
881
+ if (event.attemptId === undefined)
882
+ continue;
883
+ if (event.kind === "attempt.started") {
884
+ if (started.has(event.attemptId))
885
+ errors.push("duplicate attempt.started for attempt");
886
+ else
887
+ started.set(event.attemptId, event);
888
+ }
889
+ else if (event.kind === "attempt.finished") {
890
+ if (finished.has(event.attemptId))
891
+ errors.push("duplicate attempt.finished for attempt");
892
+ else
893
+ finished.set(event.attemptId, event);
894
+ }
895
+ }
896
+ for (const event of events) {
897
+ const violation = attemptIdViolation(event);
898
+ if (violation !== undefined)
899
+ errors.push(violation);
900
+ if (event.retryOfAttemptId !== undefined && event.kind !== "attempt.started") {
901
+ errors.push("retryOfAttemptId is only allowed on attempt.started");
902
+ }
903
+ if (event.attemptId === undefined)
904
+ continue;
905
+ const start = started.get(event.attemptId);
906
+ if (start === undefined) {
907
+ errors.push(event.kind === "attempt.finished"
908
+ ? "attempt.finished for attempt has no matching attempt.started"
909
+ : "attempt has no attempt.started");
910
+ continue;
911
+ }
912
+ if (start.agentId !== event.agentId) {
913
+ errors.push("attempt belongs to a different agent");
914
+ }
915
+ if (event.kind === "attempt.finished" && finished.get(event.attemptId) === event) {
916
+ const startedAt = occurredAt.get(start);
917
+ const finishedAt = occurredAt.get(event);
918
+ if (start.streamId === event.streamId &&
919
+ startedAt !== undefined &&
920
+ finishedAt !== undefined &&
921
+ finishedAt < startedAt) {
922
+ errors.push("attempt finished before it started");
923
+ }
924
+ }
925
+ }
926
+ const previousByAttempt = new Map();
927
+ const startedByAgent = new Map();
928
+ for (const event of started.values()) {
929
+ const list = startedByAgent.get(event.agentId) ?? [];
930
+ list.push(event);
931
+ startedByAgent.set(event.agentId, list);
932
+ }
933
+ for (const list of startedByAgent.values()) {
934
+ list.sort((a, b) => (a.attemptNumber ?? a.sequence + 1) - (b.attemptNumber ?? b.sequence + 1));
935
+ list.forEach((event, index) => {
936
+ const previous = list[index - 1];
937
+ if (previous && event.attemptId)
938
+ previousByAttempt.set(event.attemptId, previous);
939
+ if (event.payload.schema === "dh.payload.attempt-lifecycle/1") {
940
+ try {
941
+ assertDhManagedLifecycle(event);
942
+ }
943
+ catch {
944
+ errors.push("schema_invalid: invalid managed lifecycle");
945
+ }
946
+ if ((index === 0 && event.retryOfAttemptId !== undefined) ||
947
+ (index > 0 && event.retryOfAttemptId !== previous?.attemptId))
948
+ errors.push("not_eligible: attempt start must link its latest recorded predecessor");
949
+ }
950
+ if (event.attemptNumber !== undefined && event.attemptNumber !== index + 1) {
951
+ errors.push("attemptNumber does not match the attempt order");
952
+ }
953
+ });
954
+ }
955
+ const scheduled = new Map();
956
+ for (const event of events) {
957
+ if (event.kind !== "attempt.retry_scheduled" || !event.attemptId)
958
+ continue;
959
+ const list = scheduled.get(event.attemptId) ?? [];
960
+ list.push(event);
961
+ scheduled.set(event.attemptId, list);
962
+ }
963
+ for (const event of started.values()) {
964
+ const prior = event.retryOfAttemptId;
965
+ if (prior === undefined)
966
+ continue;
967
+ if (prior === event.attemptId) {
968
+ errors.push("attempt cannot retry itself");
969
+ continue;
970
+ }
971
+ for (const schedule of scheduled.get(prior) ?? []) {
972
+ if (schedule.agentId !== event.agentId ||
973
+ schedule.generationId !== event.generationId ||
974
+ scopeKey(schedule.tenantScope) !== scopeKey(event.tenantScope) ||
975
+ schedule.payload.nextAttemptNumber !== event.attemptNumber)
976
+ errors.push("not_eligible: retry schedule does not match source and next attempt number");
977
+ }
978
+ const priorStart = started.get(prior);
979
+ const priorFinish = finished.get(prior);
980
+ if (priorFinish === undefined || priorStart === undefined) {
981
+ errors.push("retryOfAttemptId is not a known finished attempt");
982
+ continue;
983
+ }
984
+ if (priorStart.agentId !== event.agentId) {
985
+ errors.push("retryOfAttemptId belongs to a different agent");
986
+ }
987
+ const terminal = effective.get(prior);
988
+ if (terminal?.outcome !== "failed" || terminal.conflict || terminal.authorityUnresolved) {
989
+ errors.push("retryOfAttemptId did not finish with outcome failed");
990
+ }
991
+ if (priorStart.generationId !== event.generationId ||
992
+ scopeKey(priorStart.tenantScope) !== scopeKey(event.tenantScope))
993
+ errors.push("not_eligible: retry source has a foreign generation or scope");
994
+ if (retryClaims.has(prior) && retryClaims.get(prior) !== event.attemptId)
995
+ errors.push("not_eligible: retry source already claimed");
996
+ retryClaims.set(prior, event.attemptId);
997
+ if (event.attemptId && previousByAttempt.get(event.attemptId)?.attemptId !== prior)
998
+ errors.push("not_eligible: retry source is not the latest attempt");
999
+ const priorFinishedAt = occurredAt.get(priorFinish);
1000
+ const retryStartedAt = occurredAt.get(event);
1001
+ if (priorFinish.streamId === event.streamId &&
1002
+ priorFinishedAt !== undefined &&
1003
+ retryStartedAt !== undefined &&
1004
+ priorFinishedAt > retryStartedAt) {
1005
+ errors.push("retryOfAttemptId finished after the retry");
1006
+ }
1007
+ if (event.attemptNumber !== undefined &&
1008
+ priorStart.attemptNumber !== undefined &&
1009
+ event.attemptNumber !== priorStart.attemptNumber + 1) {
1010
+ errors.push("attemptNumber does not follow the prior attemptNumber");
1011
+ }
1012
+ }
1013
+ }
1014
+ //# sourceMappingURL=trace.js.map