@ian-pascoe/pi-minimal-subagents 0.1.0 → 0.2.0

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.
@@ -1,219 +1,1592 @@
1
+ import type { JsonValue } from "@earendil-works/pi-ai";
2
+ import { Value } from "typebox/value";
3
+ import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
4
+ import {
5
+ createDeliveryLedger,
6
+ deliveryLedgerSnapshot,
7
+ isDeliveryLedgerTurnClaimed,
8
+ pruneDeliveryLedgerAgents,
9
+ settleCoordinationDelivery,
10
+ settleTerminalDelivery,
11
+ upsertCoordinationDelivery,
12
+ upsertTerminalDelivery,
13
+ WAIT_TERMINAL_RETENTION_LIMIT,
14
+ claimDeliveryLedgerTurn,
15
+ releaseDeliveryLedgerTurn,
16
+ releaseEmptyDeliveryLedgerTurn,
17
+ type DeliveryLedger,
18
+ } from "./minimal-subagents-delivery-ledger.js";
19
+ import {
20
+ RegistryAgentCreatedEventWireSchema,
21
+ RegistryAgentDeletedEventWireSchema,
22
+ RegistryCheckpointEventWireSchema,
23
+ RegistryCoordinationPendingEventWireSchema,
24
+ RegistryCoordinationSettledEventWireSchema,
25
+ RegistryDeliveryPendingEventWireSchema,
26
+ RegistryDeliveryPrunedEventWireSchema,
27
+ RegistryDeliverySettledEventWireSchema,
28
+ RegistryDeliveryTurnEventWireSchema,
29
+ RegistryEnvelopeWireSchema,
30
+ RegistryEventDiscriminantWireSchema,
31
+ RegistryJsonValueWireSchema,
32
+ RegistryLooseEnvelopeWireSchema,
33
+ RegistryMessageRecordedEventWireSchema,
34
+ RegistryRootProbeWireSchema,
35
+ RegistryTurnSettledEventWireSchema,
36
+ RegistryTurnStartedEventWireSchema,
37
+ type RegistryAgentWire,
38
+ type RegistryCoordinationDeliveryWire,
39
+ type RegistryCoordinatorMessageWire,
40
+ type RegistryLaunchContractWire,
41
+ type RegistryRecentMessageWire,
42
+ type RegistrySnapshotWire,
43
+ type RegistryTerminalDeliveryWire,
44
+ type RegistryTurnResultWire,
45
+ } from "./minimal-subagents-registry-wire.js";
1
46
  import type {
47
+ CoordinatorMessage,
48
+ LaunchContract,
2
49
  PersistedAgent,
50
+ PersistedCoordinationDelivery,
3
51
  PersistedDelivery,
52
+ RecentAgentMessage,
4
53
  RegistrySnapshot,
5
54
  TurnResult,
6
55
  } from "./minimal-subagents-types.js";
7
56
 
8
- /** Names append-only root conversation entries that own the persistent agent registry. */
57
+ /** Names append-only root conversation entries that own the persistent agent Registry. */
9
58
  export const REGISTRY_ENTRY_TYPE = "minimal-subagents.registry";
10
59
  /** Names the first custom entry that promotes a child JSONL session to persistent identity. */
11
60
  export const CHILD_IDENTITY_ENTRY_TYPE = "minimal-subagents.identity";
61
+ /** Names destination-root ownership records appended to forked child sessions. */
62
+ export const FORK_OWNERSHIP_ENTRY_TYPE = "minimal-subagents.fork-ownership";
63
+ /** Names source-session provenance records appended while cloning child sessions. */
64
+ export const FORK_CLONE_ENTRY_TYPE = "minimal-subagents.fork-clone";
12
65
 
13
- interface RegistryEventBaseV1 {
14
- version: 1;
66
+ interface RegistryEventBaseV2 {
67
+ version: 2;
15
68
  root_session_id: string;
16
69
  timestamp: string;
17
70
  }
18
71
 
19
- /** Stores one complete versioned root registry checkpoint. */
20
- export interface RegistryCheckpointV1 extends RegistryEventBaseV1 {
21
- event: "checkpoint";
22
- snapshot: RegistrySnapshot;
72
+ /** Defines unversioned Registry V2 payloads accepted by the write-time event factory. */
73
+ export type RegistryEventData =
74
+ | { event: "checkpoint"; snapshot: RegistrySnapshot }
75
+ | { event: "agent-created"; agent: PersistedAgent }
76
+ | { event: "turn-started"; agent_id: string; turn_id: string; started_at: string }
77
+ | {
78
+ event: "turn-settled";
79
+ result: TurnResult;
80
+ settled_at?: string;
81
+ session_leaf_id?: string;
82
+ }
83
+ | { event: "delivery-pending"; delivery: PersistedDelivery }
84
+ | { event: "delivery-settled"; source_agent_id: string; source_turn_id: string; error?: string }
85
+ | {
86
+ event: "delivery-pruned";
87
+ source_agent_id: string;
88
+ source_turn_id: string;
89
+ reason: "retention-limit";
90
+ }
91
+ | { event: "coordination-delivery-pending"; delivery: PersistedCoordinationDelivery }
92
+ | { event: "coordination-delivery-settled"; delivery_id: string; error?: string }
93
+ | { event: "delivery-turn-claimed"; source_agent_id: string; source_turn_id: string }
94
+ | { event: "delivery-turn-released"; source_agent_id: string; source_turn_id: string }
95
+ | {
96
+ event: "agent-message-recorded";
97
+ agent_id: string;
98
+ message: RecentAgentMessage;
99
+ recorded_at: string;
100
+ }
101
+ | { event: "agent-deleted"; agent_ids: string[] };
102
+
103
+ /** Unites parsed and migrated append-only Registry V2 events. */
104
+ export type RegistryEventV2 = RegistryEventBaseV2 & RegistryEventData;
105
+
106
+ /** Lists stable semantic reasons for rejecting one owned Registry record. */
107
+ export type RegistryReplayDiagnosticCode =
108
+ | "invalid-envelope"
109
+ | "unsupported-version"
110
+ | "invalid-event-fields"
111
+ | "invalid-checkpoint"
112
+ | "invalid-agent-hierarchy"
113
+ | "invalid-event-reference"
114
+ | "invalid-delivery-identity"
115
+ | "invalid-delivery-sequence"
116
+ | "invalid-delivery-adjacency";
117
+
118
+ /** Identifies one malformed or semantically invalid active-branch Registry record. */
119
+ export interface RegistryReplayDiagnostic {
120
+ entry_index: number;
121
+ code: RegistryReplayDiagnosticCode;
122
+ message: string;
23
123
  }
24
124
 
25
- /** Records one persistent child identity before runtime initialization. */
26
- export interface AgentCreatedV1 extends RegistryEventBaseV1 {
27
- event: "agent-created";
28
- agent: PersistedAgent;
125
+ type RegistryEventFactoryArguments = {
126
+ [TEvent in RegistryEventData["event"]]: [
127
+ event: TEvent,
128
+ data: Omit<Extract<RegistryEventData, { event: TEvent }>, "event">,
129
+ timestamp?: string,
130
+ ];
131
+ }[RegistryEventData["event"]];
132
+
133
+ /** Add an explicit Registry V2 root-ownership envelope to one append-only event. */
134
+ export function createRegistryEvent<TArguments extends RegistryEventFactoryArguments>(
135
+ rootSessionId: string,
136
+ ...args: TArguments
137
+ ): RegistryEventBaseV2 & Extract<RegistryEventData, { event: TArguments[0] }>;
138
+ export function createRegistryEvent(
139
+ rootSessionId: string,
140
+ ...[event, data, suppliedTimestamp]: RegistryEventFactoryArguments
141
+ ): RegistryEventV2 {
142
+ const timestamp = suppliedTimestamp ?? new Date().toISOString();
143
+ const envelope = { version: 2 as const, root_session_id: rootSessionId, timestamp };
144
+ switch (event) {
145
+ case "checkpoint": {
146
+ const ledger = deliveryLedgerSnapshot(
147
+ createDeliveryLedger({
148
+ deliveries: data.snapshot.deliveries,
149
+ coordination_deliveries: data.snapshot.coordination_deliveries,
150
+ wait_claimed_turns: data.snapshot.wait_claimed_turns,
151
+ next_delivery_sequence: data.snapshot.next_delivery_sequence,
152
+ }),
153
+ );
154
+ return {
155
+ ...envelope,
156
+ event,
157
+ snapshot: { ...structuredClone(data.snapshot), ...ledger },
158
+ };
159
+ }
160
+ case "agent-created":
161
+ return { ...envelope, event, agent: data.agent };
162
+ case "turn-started":
163
+ return {
164
+ ...envelope,
165
+ event,
166
+ agent_id: data.agent_id,
167
+ turn_id: data.turn_id,
168
+ started_at: data.started_at,
169
+ };
170
+ case "turn-settled": {
171
+ const result: RegistryEventV2 & { event: "turn-settled" } = {
172
+ ...envelope,
173
+ event,
174
+ result: data.result,
175
+ };
176
+ if (data.settled_at !== undefined) result.settled_at = data.settled_at;
177
+ if (data.session_leaf_id !== undefined) result.session_leaf_id = data.session_leaf_id;
178
+ return result;
179
+ }
180
+ case "delivery-pending":
181
+ return { ...envelope, event, delivery: data.delivery };
182
+ case "delivery-settled": {
183
+ const result: RegistryEventV2 & { event: "delivery-settled" } = {
184
+ ...envelope,
185
+ event,
186
+ source_agent_id: data.source_agent_id,
187
+ source_turn_id: data.source_turn_id,
188
+ };
189
+ if (data.error !== undefined) result.error = data.error;
190
+ return result;
191
+ }
192
+ case "delivery-pruned":
193
+ return {
194
+ ...envelope,
195
+ event,
196
+ source_agent_id: data.source_agent_id,
197
+ source_turn_id: data.source_turn_id,
198
+ reason: data.reason,
199
+ };
200
+ case "coordination-delivery-pending":
201
+ return { ...envelope, event, delivery: data.delivery };
202
+ case "coordination-delivery-settled": {
203
+ const result: RegistryEventV2 & { event: "coordination-delivery-settled" } = {
204
+ ...envelope,
205
+ event,
206
+ delivery_id: data.delivery_id,
207
+ };
208
+ if (data.error !== undefined) result.error = data.error;
209
+ return result;
210
+ }
211
+ case "delivery-turn-claimed":
212
+ return {
213
+ ...envelope,
214
+ event,
215
+ source_agent_id: data.source_agent_id,
216
+ source_turn_id: data.source_turn_id,
217
+ };
218
+ case "delivery-turn-released":
219
+ return {
220
+ ...envelope,
221
+ event,
222
+ source_agent_id: data.source_agent_id,
223
+ source_turn_id: data.source_turn_id,
224
+ };
225
+ case "agent-message-recorded":
226
+ return {
227
+ ...envelope,
228
+ event,
229
+ agent_id: data.agent_id,
230
+ message: data.message,
231
+ recorded_at: data.recorded_at,
232
+ };
233
+ case "agent-deleted":
234
+ return { ...envelope, event, agent_ids: data.agent_ids };
235
+ }
29
236
  }
30
237
 
31
- /** Records one collision-resistant active turn identity and start time. */
32
- export interface TurnStartedV1 extends RegistryEventBaseV1 {
33
- event: "turn-started";
34
- agent_id: string;
35
- turn_id: string;
36
- started_at: string;
238
+ interface RegistryEntryLike {
239
+ type?: string;
240
+ customType?: string;
241
+ data?: unknown;
37
242
  }
38
243
 
39
- /** Records one terminal turn result for waits, status, and delivery recovery. */
40
- export interface TurnSettledV1 extends RegistryEventBaseV1 {
41
- event: "turn-settled";
42
- result: TurnResult;
244
+ type ParsedRegistryEvent =
245
+ | { kind: "event"; event: RegistryEventV2 }
246
+ | { kind: "foreign-root" }
247
+ | { kind: "invalid"; code: RegistryReplayDiagnosticCode; message: string };
248
+
249
+ const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
250
+ const CANONICAL_AGENT_ID_PATTERN =
251
+ /^(?:root\.)?[A-Za-z0-9][A-Za-z0-9_-]{0,63}(?:\.[A-Za-z0-9][A-Za-z0-9_-]{0,63})*$/;
252
+ const COORDINATOR_TOOL_NAME_SET = new Set<string>(COORDINATOR_TOOL_NAMES);
253
+
254
+ function isCanonicalModelId(value: string): boolean {
255
+ const separatorIndex = value.indexOf("/");
256
+ return separatorIndex > 0 && separatorIndex < value.length - 1;
43
257
  }
44
258
 
45
- /** Records successful output awaiting keyed destination evidence. */
46
- export interface DeliveryPendingV1 extends RegistryEventBaseV1 {
47
- event: "delivery-pending";
48
- delivery: PersistedDelivery;
259
+ function isOwnedTurnId(sourceAgentId: string, turnId: string): boolean {
260
+ return turnId.startsWith(`${sourceAgentId}:`) && !turnId.includes("\u0000");
49
261
  }
50
262
 
51
- /** Records keyed destination evidence or the latest delivery error. */
52
- export interface DeliverySettledV1 extends RegistryEventBaseV1 {
53
- event: "delivery-settled";
54
- source_agent_id: string;
55
- source_turn_id: string;
56
- error?: string;
263
+ function isIsoTimestamp(value: string): boolean {
264
+ const timestamp = Date.parse(value);
265
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
57
266
  }
58
267
 
59
- /** Records durable deletion tombstones for canonical agent identities. */
60
- export interface AgentDeletedV1 extends RegistryEventBaseV1 {
61
- event: "agent-deleted";
62
- agent_ids: string[];
268
+ function isUniqueNonEmptyStringArray(value: readonly string[]): boolean {
269
+ return value.every((item) => item.length > 0) && new Set(value).size === value.length;
63
270
  }
64
271
 
65
- /** Unites all version-one append-only registry event envelopes. */
66
- export type RegistryEventV1 =
67
- | RegistryCheckpointV1
68
- | AgentCreatedV1
69
- | TurnStartedV1
70
- | TurnSettledV1
71
- | DeliveryPendingV1
72
- | DeliverySettledV1
73
- | AgentDeletedV1;
272
+ function isSafeSequence(value: number): boolean {
273
+ return Number.isSafeInteger(value) && value >= 1 && value < Number.MAX_SAFE_INTEGER;
274
+ }
74
275
 
75
- /** Defines unversioned registry payloads accepted by the event factory. */
76
- export type RegistryEventData =
77
- | { event: "checkpoint"; snapshot: RegistrySnapshot }
78
- | { event: "agent-created"; agent: PersistedAgent }
79
- | { event: "turn-started"; agent_id: string; turn_id: string; started_at: string }
80
- | { event: "turn-settled"; result: TurnResult }
81
- | { event: "delivery-pending"; delivery: PersistedDelivery }
82
- | { event: "delivery-settled"; source_agent_id: string; source_turn_id: string; error?: string }
83
- | { event: "agent-deleted"; agent_ids: string[] };
276
+ function cloneRegistryTurnResult(value: RegistryTurnResultWire): TurnResult {
277
+ const result: TurnResult = {
278
+ agent_id: value.agent_id,
279
+ turn_id: value.turn_id,
280
+ status: value.status,
281
+ output: value.output,
282
+ };
283
+ if (value.error !== undefined) result.error = value.error;
284
+ if (value.usage !== undefined) result.usage = structuredClone(value.usage);
285
+ if (value.elapsed_ms !== undefined) result.elapsed_ms = value.elapsed_ms;
286
+ return result;
287
+ }
84
288
 
85
- /** Add the versioned root ownership envelope to one append-only registry event. */
86
- export function createRegistryEvent<TEvent extends RegistryEventData["event"]>(
87
- rootSessionId: string,
88
- event: TEvent,
89
- data: Omit<Extract<RegistryEventData, { event: TEvent }>, "event">,
90
- timestamp = new Date().toISOString(),
91
- ): RegistryEventV1 {
289
+ function cloneRegistryRecentMessage(value: RegistryRecentMessageWire): RecentAgentMessage {
92
290
  return {
93
- version: 1,
94
- root_session_id: rootSessionId,
95
- timestamp,
96
- event,
97
- ...data,
98
- } as unknown as RegistryEventV1;
291
+ source_agent_id: value.source_agent_id,
292
+ turn_id: value.turn_id,
293
+ content: value.content,
294
+ };
99
295
  }
100
296
 
101
- interface RegistryEntryLike {
102
- type?: string;
103
- customType?: string;
104
- data?: unknown;
297
+ function excludesCoordinatorToolNames(toolNames: readonly string[]): boolean {
298
+ return toolNames.every((toolName) => !COORDINATOR_TOOL_NAME_SET.has(toolName));
299
+ }
300
+
301
+ function parseRegistryLaunchContract(
302
+ value: RegistryLaunchContractWire,
303
+ ): LaunchContract | undefined {
304
+ if (
305
+ !isCanonicalModelId(value.model) ||
306
+ !isUniqueNonEmptyStringArray(value.ordinary_tools) ||
307
+ !excludesCoordinatorToolNames(value.ordinary_tools) ||
308
+ (Array.isArray(value.tools) &&
309
+ (!isUniqueNonEmptyStringArray(value.tools) || !excludesCoordinatorToolNames(value.tools)))
310
+ ) {
311
+ return undefined;
312
+ }
313
+ const launchContract: LaunchContract = {
314
+ session_context: value.session_context,
315
+ project_context: value.project_context,
316
+ model: value.model,
317
+ thinking_level: value.thinking_level,
318
+ tools: value.tools === undefined ? undefined : structuredClone(value.tools),
319
+ ordinary_tools: [...value.ordinary_tools],
320
+ };
321
+ if (value.delegation !== undefined) launchContract.delegation = value.delegation;
322
+ return launchContract;
323
+ }
324
+
325
+ function parseRegistryAgent(value: RegistryAgentWire, version: 1 | 2): PersistedAgent | undefined {
326
+ const launchContract = parseRegistryLaunchContract(value.launch_contract);
327
+ if (
328
+ value.agent_id === "root" ||
329
+ !CANONICAL_AGENT_ID_PATTERN.test(value.agent_id) ||
330
+ value.friendly_id === "root" ||
331
+ value.friendly_id === "parent" ||
332
+ !FRIENDLY_AGENT_ID_PATTERN.test(value.friendly_id) ||
333
+ !isIsoTimestamp(value.created_at) ||
334
+ launchContract === undefined ||
335
+ !isUniqueNonEmptyStringArray(value.capability_ceiling) ||
336
+ !isUniqueNonEmptyStringArray(value.missing_dependencies) ||
337
+ (value.latest_activity_at !== undefined && !isIsoTimestamp(value.latest_activity_at)) ||
338
+ (value.active_turn_started_at !== undefined && !isIsoTimestamp(value.active_turn_started_at))
339
+ ) {
340
+ return undefined;
341
+ }
342
+ if ((value.active_turn_id === undefined) !== (value.active_turn_started_at === undefined)) {
343
+ return undefined;
344
+ }
345
+ if ((value.session_file === undefined) !== (value.session_id === undefined)) return undefined;
346
+ if (version === 2) {
347
+ if (value.session_leaf_id !== undefined && value.session_id === undefined) return undefined;
348
+ if (value.session_id !== undefined && value.session_leaf_id === undefined) return undefined;
349
+ }
350
+ if (value.clone_error !== undefined && value.availability !== "unavailable") return undefined;
351
+ if (!excludesCoordinatorToolNames(value.capability_ceiling)) return undefined;
352
+ const capabilityCeiling = new Set(value.capability_ceiling);
353
+ if (
354
+ launchContract.ordinary_tools.length !== capabilityCeiling.size ||
355
+ launchContract.ordinary_tools.some((toolName) => !capabilityCeiling.has(toolName))
356
+ ) {
357
+ return undefined;
358
+ }
359
+ if (value.active_turn_id !== undefined && !isOwnedTurnId(value.agent_id, value.active_turn_id)) {
360
+ return undefined;
361
+ }
362
+ const expectedAgentId =
363
+ value.parent_id === "root"
364
+ ? [value.friendly_id, `root.${value.friendly_id}`]
365
+ : [`${value.parent_id}.${value.friendly_id}`];
366
+ if (!expectedAgentId.includes(value.agent_id)) return undefined;
367
+ if (
368
+ value.latest_result !== undefined &&
369
+ (value.latest_result.agent_id !== value.agent_id ||
370
+ !isOwnedTurnId(value.agent_id, value.latest_result.turn_id) ||
371
+ value.latest_result.turn_id === value.active_turn_id)
372
+ ) {
373
+ return undefined;
374
+ }
375
+
376
+ const agent: PersistedAgent = {
377
+ agent_id: value.agent_id,
378
+ friendly_id: value.friendly_id,
379
+ parent_id: value.parent_id,
380
+ created_at: value.created_at,
381
+ spawn_entry_id: value.spawn_entry_id,
382
+ launch_contract: launchContract,
383
+ capability_ceiling: [...value.capability_ceiling],
384
+ availability: value.availability,
385
+ missing_dependencies: [...value.missing_dependencies],
386
+ recent_messages: value.recent_messages.map(cloneRegistryRecentMessage),
387
+ };
388
+ if (value.task !== undefined) agent.task = value.task;
389
+ if (value.latest_activity_at !== undefined) agent.latest_activity_at = value.latest_activity_at;
390
+ if (value.session_file !== undefined) agent.session_file = value.session_file;
391
+ if (value.session_id !== undefined) agent.session_id = value.session_id;
392
+ if (value.session_leaf_id !== undefined) agent.session_leaf_id = value.session_leaf_id;
393
+ if (value.clone_error !== undefined) agent.clone_error = value.clone_error;
394
+ if (value.active_turn_id !== undefined) agent.active_turn_id = value.active_turn_id;
395
+ if (value.active_turn_started_at !== undefined) {
396
+ agent.active_turn_started_at = value.active_turn_started_at;
397
+ }
398
+ if (value.latest_result !== undefined) {
399
+ agent.latest_result = cloneRegistryTurnResult(value.latest_result);
400
+ }
401
+ if (value.unavailable_reason !== undefined) agent.unavailable_reason = value.unavailable_reason;
402
+ if (value.deleted !== undefined) agent.deleted = value.deleted;
403
+ return agent;
404
+ }
405
+
406
+ function parseRegistryTerminalDelivery(
407
+ value: RegistryTerminalDeliveryWire,
408
+ version: 1 | 2,
409
+ ): PersistedDelivery | undefined {
410
+ if (
411
+ (version === 2 &&
412
+ (value.sequence === undefined || value.result === undefined || value.settled !== false)) ||
413
+ (value.sequence !== undefined && !isSafeSequence(value.sequence))
414
+ ) {
415
+ return undefined;
416
+ }
417
+ const delivery: PersistedDelivery = {
418
+ source_agent_id: value.source_agent_id,
419
+ source_turn_id: value.source_turn_id,
420
+ destination_agent_id: value.destination_agent_id,
421
+ path: value.path,
422
+ settled: value.settled,
423
+ };
424
+ if (value.sequence !== undefined) delivery.sequence = value.sequence;
425
+ if (value.result !== undefined) delivery.result = cloneRegistryTurnResult(value.result);
426
+ if (value.error !== undefined) delivery.error = value.error;
427
+ return delivery;
428
+ }
429
+
430
+ function parseRegistryCoordinatorMessage(
431
+ value: RegistryCoordinatorMessageWire,
432
+ ): CoordinatorMessage | undefined {
433
+ if (
434
+ value.customType !== "minimal-subagents.message" ||
435
+ value.details.destination_agent_id === undefined ||
436
+ value.details.delivery_id === undefined ||
437
+ value.details.status !== undefined ||
438
+ value.details.elapsed_ms !== undefined ||
439
+ value.details.usage !== undefined
440
+ ) {
441
+ return undefined;
442
+ }
443
+ return {
444
+ customType: "minimal-subagents.message",
445
+ content: value.content,
446
+ details: {
447
+ source_agent_id: value.details.source_agent_id,
448
+ destination_agent_id: value.details.destination_agent_id,
449
+ source_turn_id: value.details.source_turn_id,
450
+ message_id: value.details.message_id,
451
+ delivery_id: value.details.delivery_id,
452
+ },
453
+ };
454
+ }
455
+
456
+ function parseRegistryCoordinationDelivery(
457
+ value: RegistryCoordinationDeliveryWire,
458
+ version: 1 | 2,
459
+ ): PersistedCoordinationDelivery | undefined {
460
+ const message = parseRegistryCoordinatorMessage(value.message);
461
+ if (
462
+ !isSafeSequence(value.sequence) ||
463
+ message === undefined ||
464
+ (version === 2 && value.settled)
465
+ ) {
466
+ return undefined;
467
+ }
468
+ const delivery: PersistedCoordinationDelivery = {
469
+ delivery_id: value.delivery_id,
470
+ sequence: value.sequence,
471
+ destination_agent_id: value.destination_agent_id,
472
+ path: value.path,
473
+ settled: value.settled,
474
+ message,
475
+ };
476
+ if (value.error !== undefined) delivery.error = value.error;
477
+ return delivery;
478
+ }
479
+
480
+ function hasValidAgentHierarchy(agents: readonly PersistedAgent[]): boolean {
481
+ const byId = new Map<string, PersistedAgent>();
482
+ for (const agent of agents) {
483
+ if (byId.has(agent.agent_id)) return false;
484
+ byId.set(agent.agent_id, agent);
485
+ }
486
+ for (const agent of agents) {
487
+ const visited = new Set<string>([agent.agent_id]);
488
+ let parentId = agent.parent_id;
489
+ while (parentId !== "root") {
490
+ if (visited.has(parentId)) return false;
491
+ visited.add(parentId);
492
+ const parent = byId.get(parentId);
493
+ if (!parent) return false;
494
+ parentId = parent.parent_id;
495
+ }
496
+ }
497
+ return true;
105
498
  }
106
499
 
107
- function isRegistryEvent(value: unknown, rootSessionId: string): value is RegistryEventV1 {
108
- if (!value || typeof value !== "object") return false;
109
- const candidate = value as Partial<RegistryEventV1>;
500
+ function areAdjacentAgents(
501
+ sourceAgentId: string,
502
+ destinationAgentId: string,
503
+ agentsById: ReadonlyMap<string, PersistedAgent>,
504
+ ): boolean {
505
+ if (sourceAgentId === destinationAgentId) return false;
506
+ const source = sourceAgentId === "root" ? undefined : agentsById.get(sourceAgentId);
507
+ const destination =
508
+ destinationAgentId === "root" ? undefined : agentsById.get(destinationAgentId);
509
+ if (sourceAgentId === "root") return destination?.parent_id === "root";
510
+ if (destinationAgentId === "root") return source?.parent_id === "root";
511
+ if (!source || !destination) return false;
110
512
  return (
111
- candidate.version === 1 &&
112
- candidate.root_session_id === rootSessionId &&
113
- typeof candidate.timestamp === "string" &&
114
- typeof candidate.event === "string"
513
+ source.parent_id === destinationAgentId ||
514
+ destination.parent_id === sourceAgentId ||
515
+ source.parent_id === destination.parent_id
115
516
  );
116
517
  }
117
518
 
118
- function cloneSnapshot(snapshot: RegistrySnapshot): RegistrySnapshot {
119
- return structuredClone(snapshot);
519
+ function validateDeliveryIdentity(delivery: PersistedDelivery, version: 1 | 2): string | undefined {
520
+ if (!isOwnedTurnId(delivery.source_agent_id, delivery.source_turn_id))
521
+ return "terminal source_turn_id must belong to source_agent_id";
522
+ if (delivery.result) {
523
+ if (delivery.result.agent_id !== delivery.source_agent_id)
524
+ return "terminal result agent_id must equal delivery source_agent_id";
525
+ if (delivery.result.turn_id !== delivery.source_turn_id)
526
+ return "terminal result turn_id must equal delivery source_turn_id";
527
+ if (delivery.result.status !== "completed")
528
+ return "pending terminal delivery result must be completed";
529
+ } else if (version === 2) {
530
+ return "Registry V2 terminal delivery must retain its result";
531
+ }
532
+ return undefined;
533
+ }
534
+
535
+ function validateCoordinationIdentity(delivery: PersistedCoordinationDelivery): string | undefined {
536
+ const details = delivery.message.details;
537
+ if (!isOwnedTurnId(details.source_agent_id, details.source_turn_id))
538
+ return "Coordination Message source_turn_id must belong to source_agent_id";
539
+ if (delivery.delivery_id !== `message:${details.message_id}`)
540
+ return "Coordination delivery_id must equal message:<message_id>";
541
+ if (details.delivery_id !== delivery.delivery_id)
542
+ return "Coordination message delivery_id must equal delivery delivery_id";
543
+ if (details.destination_agent_id !== delivery.destination_agent_id)
544
+ return "Coordination message destination_agent_id must equal delivery destination_agent_id";
545
+ return undefined;
120
546
  }
121
547
 
122
- function deliveryKey(sourceAgentId: string, sourceTurnId: string): string {
123
- return `${sourceAgentId}\u0000${sourceTurnId}`;
548
+ function isObservableSourceTurn(
549
+ agent: PersistedAgent,
550
+ sourceTurnId: string,
551
+ deliveries: readonly PersistedDelivery[],
552
+ coordinationDeliveries: readonly PersistedCoordinationDelivery[],
553
+ ): boolean {
554
+ return (
555
+ agent.active_turn_id === sourceTurnId ||
556
+ agent.latest_result?.turn_id === sourceTurnId ||
557
+ deliveries.some(
558
+ (delivery) =>
559
+ !delivery.settled &&
560
+ delivery.source_agent_id === agent.agent_id &&
561
+ delivery.source_turn_id === sourceTurnId,
562
+ ) ||
563
+ coordinationDeliveries.some(
564
+ (delivery) =>
565
+ !delivery.settled &&
566
+ delivery.message.details.source_agent_id === agent.agent_id &&
567
+ delivery.message.details.source_turn_id === sourceTurnId,
568
+ )
569
+ );
124
570
  }
125
571
 
126
- /** Replay root-session-wide registry entries, starting from the latest complete checkpoint. */
572
+ type RegistrySnapshotValidationResult =
573
+ | { kind: "valid"; snapshot: RegistrySnapshot }
574
+ | {
575
+ kind: "invalid";
576
+ code: RegistryReplayDiagnosticCode;
577
+ message: string;
578
+ };
579
+
580
+ function invalidRegistrySnapshot(
581
+ code: RegistryReplayDiagnosticCode,
582
+ message: string,
583
+ ): RegistrySnapshotValidationResult {
584
+ return { kind: "invalid", code, message };
585
+ }
586
+
587
+ function validateRegistrySnapshot(
588
+ value: RegistrySnapshotWire,
589
+ version: 1 | 2,
590
+ ): RegistrySnapshotValidationResult {
591
+ const agents: PersistedAgent[] = [];
592
+ for (const wireAgent of value.agents) {
593
+ const agent = parseRegistryAgent(wireAgent, version);
594
+ if (agent === undefined) {
595
+ return invalidRegistrySnapshot(
596
+ "invalid-checkpoint",
597
+ "snapshot agents are incomplete or invalid",
598
+ );
599
+ }
600
+ agents.push(agent);
601
+ }
602
+ if (!hasValidAgentHierarchy(agents)) {
603
+ return invalidRegistrySnapshot(
604
+ "invalid-agent-hierarchy",
605
+ "snapshot agent hierarchy is invalid",
606
+ );
607
+ }
608
+ if (!isUniqueNonEmptyStringArray(value.tombstones)) {
609
+ return invalidRegistrySnapshot(
610
+ "invalid-checkpoint",
611
+ "snapshot tombstones must be unique agent IDs",
612
+ );
613
+ }
614
+ const tombstones = [...value.tombstones];
615
+ if (
616
+ tombstones.some((agentId) => agentId === "root" || !CANONICAL_AGENT_ID_PATTERN.test(agentId))
617
+ ) {
618
+ return invalidRegistrySnapshot(
619
+ "invalid-agent-hierarchy",
620
+ "tombstone agent ID is not canonical",
621
+ );
622
+ }
623
+ if (agents.some((agent) => agent.deleted === true)) {
624
+ return invalidRegistrySnapshot(
625
+ "invalid-agent-hierarchy",
626
+ "live snapshot agents cannot be deleted",
627
+ );
628
+ }
629
+ const agentIds = new Set(agents.map((agent) => agent.agent_id));
630
+ if (tombstones.some((agentId) => agentIds.has(agentId))) {
631
+ return invalidRegistrySnapshot(
632
+ "invalid-agent-hierarchy",
633
+ "live agents cannot also be tombstoned",
634
+ );
635
+ }
636
+
637
+ const deliveries: PersistedDelivery[] = [];
638
+ for (const wireDelivery of value.deliveries) {
639
+ const delivery = parseRegistryTerminalDelivery(wireDelivery, version);
640
+ if (delivery === undefined) {
641
+ return invalidRegistrySnapshot(
642
+ "invalid-checkpoint",
643
+ "snapshot terminal deliveries are invalid",
644
+ );
645
+ }
646
+ deliveries.push(delivery);
647
+ }
648
+ if (version === 2 && value.coordination_deliveries === undefined) {
649
+ return invalidRegistrySnapshot(
650
+ "invalid-checkpoint",
651
+ "Registry V2 checkpoint must include coordination_deliveries",
652
+ );
653
+ }
654
+ const coordinationDeliveries: PersistedCoordinationDelivery[] = [];
655
+ for (const wireDelivery of value.coordination_deliveries ?? []) {
656
+ const delivery = parseRegistryCoordinationDelivery(wireDelivery, version);
657
+ if (delivery === undefined) {
658
+ return invalidRegistrySnapshot(
659
+ "invalid-checkpoint",
660
+ "snapshot Coordination Messages are invalid",
661
+ );
662
+ }
663
+ coordinationDeliveries.push(delivery);
664
+ }
665
+ if (version === 2 && value.wait_claimed_turns === undefined) {
666
+ return invalidRegistrySnapshot(
667
+ "invalid-checkpoint",
668
+ "Registry V2 checkpoint must include wait_claimed_turns",
669
+ );
670
+ }
671
+ const waitClaimedTurns = value.wait_claimed_turns ?? [];
672
+ if (
673
+ !isUniqueNonEmptyStringArray(waitClaimedTurns) ||
674
+ waitClaimedTurns.some(
675
+ (key) => key.indexOf("\u0000") <= 0 || key.indexOf("\u0000") !== key.lastIndexOf("\u0000"),
676
+ )
677
+ ) {
678
+ return invalidRegistrySnapshot("invalid-checkpoint", "snapshot wait claims are invalid");
679
+ }
680
+ if (version === 2 && value.next_delivery_sequence === undefined) {
681
+ return invalidRegistrySnapshot(
682
+ "invalid-delivery-sequence",
683
+ "Registry V2 checkpoint must include next_delivery_sequence",
684
+ );
685
+ }
686
+ if (value.next_delivery_sequence !== undefined && !isSafeSequence(value.next_delivery_sequence)) {
687
+ return invalidRegistrySnapshot(
688
+ "invalid-delivery-sequence",
689
+ "next_delivery_sequence must be a positive safe integer",
690
+ );
691
+ }
692
+
693
+ const agentsById = new Map(agents.map((agent) => [agent.agent_id, agent]));
694
+ for (const agent of agents) {
695
+ if (version === 2 && agent.recent_messages.length > 20) {
696
+ return invalidRegistrySnapshot(
697
+ "invalid-checkpoint",
698
+ "agent recent_messages exceeds the 20-item projection limit",
699
+ );
700
+ }
701
+ for (const message of agent.recent_messages) {
702
+ if (
703
+ !isOwnedTurnId(message.source_agent_id, message.turn_id) ||
704
+ !areAdjacentAgents(message.source_agent_id, agent.agent_id, agentsById)
705
+ ) {
706
+ return invalidRegistrySnapshot(
707
+ "invalid-delivery-adjacency",
708
+ "recent message endpoints and source turn must identify adjacent agents",
709
+ );
710
+ }
711
+ }
712
+ }
713
+ const terminalDeliveryKeys = deliveries.map(
714
+ (delivery) => `${delivery.source_agent_id}\u0000${delivery.source_turn_id}`,
715
+ );
716
+ if (new Set(terminalDeliveryKeys).size !== terminalDeliveryKeys.length) {
717
+ return invalidRegistrySnapshot(
718
+ "invalid-delivery-identity",
719
+ "terminal delivery source-turn identities must be unique",
720
+ );
721
+ }
722
+ const coordinationDeliveryIds = coordinationDeliveries.map((delivery) => delivery.delivery_id);
723
+ if (new Set(coordinationDeliveryIds).size !== coordinationDeliveryIds.length) {
724
+ return invalidRegistrySnapshot(
725
+ "invalid-delivery-identity",
726
+ "Coordination Message delivery IDs must be unique",
727
+ );
728
+ }
729
+ const waitTerminalCounts = new Map<string, number>();
730
+ for (const delivery of deliveries) {
731
+ if (delivery.path === "wait") {
732
+ const count = (waitTerminalCounts.get(delivery.source_agent_id) ?? 0) + 1;
733
+ waitTerminalCounts.set(delivery.source_agent_id, count);
734
+ if (version === 2 && count > WAIT_TERMINAL_RETENTION_LIMIT) {
735
+ return invalidRegistrySnapshot(
736
+ "invalid-checkpoint",
737
+ `wait-only terminal retention exceeds ${WAIT_TERMINAL_RETENTION_LIMIT} items for one source agent`,
738
+ );
739
+ }
740
+ }
741
+ const identityError = validateDeliveryIdentity(delivery, version);
742
+ if (identityError) return invalidRegistrySnapshot("invalid-delivery-identity", identityError);
743
+ if (!isOwnedTurnId(delivery.source_agent_id, delivery.source_turn_id)) {
744
+ return invalidRegistrySnapshot(
745
+ "invalid-delivery-identity",
746
+ "terminal source_turn_id must belong to source_agent_id",
747
+ );
748
+ }
749
+ const source = agentsById.get(delivery.source_agent_id);
750
+ if (!source) {
751
+ return invalidRegistrySnapshot(
752
+ "invalid-event-reference",
753
+ "terminal delivery source is not live",
754
+ );
755
+ }
756
+ if (source.parent_id !== delivery.destination_agent_id) {
757
+ return invalidRegistrySnapshot(
758
+ "invalid-delivery-adjacency",
759
+ "terminal delivery destination must be the source agent direct parent",
760
+ );
761
+ }
762
+ }
763
+ for (const delivery of coordinationDeliveries) {
764
+ const identityError = validateCoordinationIdentity(delivery);
765
+ if (identityError) return invalidRegistrySnapshot("invalid-delivery-identity", identityError);
766
+ if (
767
+ !isOwnedTurnId(
768
+ delivery.message.details.source_agent_id,
769
+ delivery.message.details.source_turn_id,
770
+ )
771
+ ) {
772
+ return invalidRegistrySnapshot(
773
+ "invalid-delivery-identity",
774
+ "Coordination Message source_turn_id must belong to source_agent_id",
775
+ );
776
+ }
777
+ if (
778
+ !areAdjacentAgents(
779
+ delivery.message.details.source_agent_id,
780
+ delivery.destination_agent_id,
781
+ agentsById,
782
+ )
783
+ ) {
784
+ return invalidRegistrySnapshot(
785
+ "invalid-delivery-adjacency",
786
+ "Coordination Message endpoints must be adjacent live agents",
787
+ );
788
+ }
789
+ }
790
+ const sequences = [
791
+ ...deliveries.flatMap((delivery) =>
792
+ delivery.sequence === undefined ? [] : [delivery.sequence],
793
+ ),
794
+ ...coordinationDeliveries.map((delivery) => delivery.sequence),
795
+ ];
796
+ if (new Set(sequences).size !== sequences.length) {
797
+ return invalidRegistrySnapshot(
798
+ "invalid-delivery-sequence",
799
+ "delivery sequences must be unique",
800
+ );
801
+ }
802
+ if (
803
+ value.next_delivery_sequence !== undefined &&
804
+ sequences.some((sequence) => sequence >= Number(value.next_delivery_sequence))
805
+ ) {
806
+ return invalidRegistrySnapshot(
807
+ "invalid-delivery-sequence",
808
+ "next_delivery_sequence must be greater than every retained sequence",
809
+ );
810
+ }
811
+ for (const key of waitClaimedTurns) {
812
+ const separatorIndex = key.indexOf("\u0000");
813
+ const sourceAgentId = key.slice(0, separatorIndex);
814
+ const sourceTurnId = key.slice(separatorIndex + 1);
815
+ const sourceAgent = agentsById.get(sourceAgentId);
816
+ if (
817
+ !sourceAgent ||
818
+ !isOwnedTurnId(sourceAgentId, sourceTurnId) ||
819
+ !isObservableSourceTurn(sourceAgent, sourceTurnId, deliveries, coordinationDeliveries)
820
+ ) {
821
+ return invalidRegistrySnapshot(
822
+ "invalid-event-reference",
823
+ "wait claim must reference an active, latest, or retained source turn",
824
+ );
825
+ }
826
+ }
827
+
828
+ if (version === 1) {
829
+ for (const agent of agents) {
830
+ if (!agent.session_id) agent.session_leaf_id = undefined;
831
+ agent.recent_messages = agent.recent_messages.slice(-20);
832
+ }
833
+ }
834
+ const snapshot: RegistrySnapshot = { agents, tombstones, deliveries };
835
+ if (coordinationDeliveries.length > 0 || value.coordination_deliveries !== undefined) {
836
+ snapshot.coordination_deliveries = coordinationDeliveries;
837
+ }
838
+ if (waitClaimedTurns.length > 0 || value.wait_claimed_turns !== undefined) {
839
+ snapshot.wait_claimed_turns = [...waitClaimedTurns];
840
+ }
841
+ if (value.next_delivery_sequence !== undefined) {
842
+ snapshot.next_delivery_sequence = value.next_delivery_sequence;
843
+ }
844
+ const normalizedLedger = deliveryLedgerSnapshot(
845
+ createDeliveryLedger({
846
+ deliveries: snapshot.deliveries,
847
+ coordination_deliveries: snapshot.coordination_deliveries,
848
+ wait_claimed_turns: snapshot.wait_claimed_turns,
849
+ next_delivery_sequence: snapshot.next_delivery_sequence,
850
+ }),
851
+ );
852
+ snapshot.deliveries = normalizedLedger.deliveries;
853
+ snapshot.coordination_deliveries = normalizedLedger.coordination_deliveries;
854
+ snapshot.wait_claimed_turns = normalizedLedger.wait_claimed_turns;
855
+ snapshot.next_delivery_sequence = normalizedLedger.next_delivery_sequence;
856
+ return { kind: "valid", snapshot };
857
+ }
858
+
859
+ function invalidParsedEvent(
860
+ code: RegistryReplayDiagnosticCode,
861
+ message: string,
862
+ ): ParsedRegistryEvent {
863
+ return { kind: "invalid", code, message };
864
+ }
865
+
866
+ function validParsedEvent(event: RegistryEventV2): ParsedRegistryEvent {
867
+ return { kind: "event", event };
868
+ }
869
+
870
+ type RegistryParseInput = JsonValue | RegistryEventV2;
871
+
872
+ function parseRegistryEventRecord(
873
+ value: RegistryParseInput,
874
+ rootSessionId: string,
875
+ ): ParsedRegistryEvent {
876
+ if (Value.Check(RegistryRootProbeWireSchema, value) && value.root_session_id !== rootSessionId) {
877
+ return { kind: "foreign-root" };
878
+ }
879
+ if (!Value.Check(RegistryLooseEnvelopeWireSchema, value) || !isIsoTimestamp(value.timestamp)) {
880
+ return invalidParsedEvent(
881
+ "invalid-envelope",
882
+ "root_session_id and timestamp must be valid strings",
883
+ );
884
+ }
885
+ if (value.version !== 1 && value.version !== 2) {
886
+ return invalidParsedEvent("unsupported-version", "Registry version must be 1 or 2");
887
+ }
888
+ if (!Value.Check(RegistryEventDiscriminantWireSchema, value)) {
889
+ return invalidParsedEvent("invalid-event-fields", "event discriminant must be a string");
890
+ }
891
+ if (!Value.Check(RegistryEnvelopeWireSchema, value)) {
892
+ return invalidParsedEvent("invalid-envelope", "Registry envelope is invalid");
893
+ }
894
+ const version = value.version;
895
+ const timestamp = value.timestamp;
896
+ switch (value.event) {
897
+ case "checkpoint": {
898
+ if (!Value.Check(RegistryCheckpointEventWireSchema, value)) {
899
+ return invalidParsedEvent("invalid-checkpoint", "snapshot must be an object");
900
+ }
901
+ const parsed = validateRegistrySnapshot(value.snapshot, version);
902
+ if (parsed.kind === "invalid") return invalidParsedEvent(parsed.code, parsed.message);
903
+ return validParsedEvent(
904
+ createRegistryEvent(rootSessionId, "checkpoint", { snapshot: parsed.snapshot }, timestamp),
905
+ );
906
+ }
907
+ case "agent-created": {
908
+ if (!Value.Check(RegistryAgentCreatedEventWireSchema, value)) {
909
+ return invalidParsedEvent("invalid-event-fields", "agent-created agent is incomplete");
910
+ }
911
+ const agent = parseRegistryAgent(value.agent, version);
912
+ if (agent === undefined || (version === 2 && agent.recent_messages.length > 20)) {
913
+ return invalidParsedEvent("invalid-event-fields", "agent-created agent is incomplete");
914
+ }
915
+ if (version === 1) {
916
+ if (!agent.session_id) agent.session_leaf_id = undefined;
917
+ agent.recent_messages = agent.recent_messages.slice(-20);
918
+ }
919
+ return validParsedEvent(
920
+ createRegistryEvent(rootSessionId, "agent-created", { agent }, timestamp),
921
+ );
922
+ }
923
+ case "turn-started":
924
+ if (
925
+ !Value.Check(RegistryTurnStartedEventWireSchema, value) ||
926
+ !isOwnedTurnId(value.agent_id, value.turn_id) ||
927
+ !isIsoTimestamp(value.started_at)
928
+ ) {
929
+ return invalidParsedEvent("invalid-event-fields", "turn-started fields are invalid");
930
+ }
931
+ return validParsedEvent(
932
+ createRegistryEvent(
933
+ rootSessionId,
934
+ "turn-started",
935
+ {
936
+ agent_id: value.agent_id,
937
+ turn_id: value.turn_id,
938
+ started_at: value.started_at,
939
+ },
940
+ timestamp,
941
+ ),
942
+ );
943
+ case "turn-settled": {
944
+ if (
945
+ !Value.Check(RegistryTurnSettledEventWireSchema, value) ||
946
+ !isOwnedTurnId(value.result.agent_id, value.result.turn_id) ||
947
+ (value.settled_at !== undefined && !isIsoTimestamp(value.settled_at))
948
+ ) {
949
+ return invalidParsedEvent("invalid-event-fields", "turn-settled fields are invalid");
950
+ }
951
+ const data: Omit<Extract<RegistryEventData, { event: "turn-settled" }>, "event"> = {
952
+ result: cloneRegistryTurnResult(value.result),
953
+ };
954
+ if (value.settled_at !== undefined) data.settled_at = value.settled_at;
955
+ if (value.session_leaf_id !== undefined) data.session_leaf_id = value.session_leaf_id;
956
+ return validParsedEvent(createRegistryEvent(rootSessionId, "turn-settled", data, timestamp));
957
+ }
958
+ case "delivery-pending": {
959
+ if (!Value.Check(RegistryDeliveryPendingEventWireSchema, value)) {
960
+ return invalidParsedEvent("invalid-event-fields", "terminal delivery fields are invalid");
961
+ }
962
+ const delivery = parseRegistryTerminalDelivery(value.delivery, version);
963
+ if (delivery === undefined) {
964
+ return invalidParsedEvent("invalid-event-fields", "terminal delivery fields are invalid");
965
+ }
966
+ const identityError = validateDeliveryIdentity(delivery, version);
967
+ if (identityError !== undefined) {
968
+ return invalidParsedEvent("invalid-delivery-identity", identityError);
969
+ }
970
+ return validParsedEvent(
971
+ createRegistryEvent(rootSessionId, "delivery-pending", { delivery }, timestamp),
972
+ );
973
+ }
974
+ case "delivery-settled": {
975
+ if (
976
+ !Value.Check(RegistryDeliverySettledEventWireSchema, value) ||
977
+ !isOwnedTurnId(value.source_agent_id, value.source_turn_id)
978
+ ) {
979
+ return invalidParsedEvent("invalid-event-fields", "delivery-settled fields are invalid");
980
+ }
981
+ const data: Omit<Extract<RegistryEventData, { event: "delivery-settled" }>, "event"> = {
982
+ source_agent_id: value.source_agent_id,
983
+ source_turn_id: value.source_turn_id,
984
+ };
985
+ if (value.error !== undefined) data.error = value.error;
986
+ return validParsedEvent(
987
+ createRegistryEvent(rootSessionId, "delivery-settled", data, timestamp),
988
+ );
989
+ }
990
+ case "delivery-pruned":
991
+ if (
992
+ version !== 2 ||
993
+ !Value.Check(RegistryDeliveryPrunedEventWireSchema, value) ||
994
+ !isOwnedTurnId(value.source_agent_id, value.source_turn_id)
995
+ ) {
996
+ return invalidParsedEvent("invalid-event-fields", "delivery-pruned fields are invalid");
997
+ }
998
+ return validParsedEvent(
999
+ createRegistryEvent(
1000
+ rootSessionId,
1001
+ "delivery-pruned",
1002
+ {
1003
+ source_agent_id: value.source_agent_id,
1004
+ source_turn_id: value.source_turn_id,
1005
+ reason: "retention-limit",
1006
+ },
1007
+ timestamp,
1008
+ ),
1009
+ );
1010
+ case "coordination-delivery-pending": {
1011
+ if (!Value.Check(RegistryCoordinationPendingEventWireSchema, value)) {
1012
+ return invalidParsedEvent(
1013
+ "invalid-event-fields",
1014
+ "Coordination Message delivery fields are invalid",
1015
+ );
1016
+ }
1017
+ const delivery = parseRegistryCoordinationDelivery(value.delivery, version);
1018
+ if (delivery === undefined) {
1019
+ return invalidParsedEvent(
1020
+ "invalid-event-fields",
1021
+ "Coordination Message delivery fields are invalid",
1022
+ );
1023
+ }
1024
+ const identityError = validateCoordinationIdentity(delivery);
1025
+ if (identityError !== undefined) {
1026
+ return invalidParsedEvent("invalid-delivery-identity", identityError);
1027
+ }
1028
+ return validParsedEvent(
1029
+ createRegistryEvent(
1030
+ rootSessionId,
1031
+ "coordination-delivery-pending",
1032
+ { delivery },
1033
+ timestamp,
1034
+ ),
1035
+ );
1036
+ }
1037
+ case "coordination-delivery-settled": {
1038
+ if (!Value.Check(RegistryCoordinationSettledEventWireSchema, value)) {
1039
+ return invalidParsedEvent(
1040
+ "invalid-event-fields",
1041
+ "coordination-delivery-settled fields are invalid",
1042
+ );
1043
+ }
1044
+ const data: Omit<
1045
+ Extract<RegistryEventData, { event: "coordination-delivery-settled" }>,
1046
+ "event"
1047
+ > = { delivery_id: value.delivery_id };
1048
+ if (value.error !== undefined) data.error = value.error;
1049
+ return validParsedEvent(
1050
+ createRegistryEvent(rootSessionId, "coordination-delivery-settled", data, timestamp),
1051
+ );
1052
+ }
1053
+ case "delivery-turn-claimed":
1054
+ case "delivery-turn-released":
1055
+ if (
1056
+ !Value.Check(RegistryDeliveryTurnEventWireSchema, value) ||
1057
+ !isOwnedTurnId(value.source_agent_id, value.source_turn_id)
1058
+ ) {
1059
+ return invalidParsedEvent("invalid-event-fields", `${value.event} fields are invalid`);
1060
+ }
1061
+ if (value.event === "delivery-turn-claimed") {
1062
+ return validParsedEvent(
1063
+ createRegistryEvent(
1064
+ rootSessionId,
1065
+ "delivery-turn-claimed",
1066
+ {
1067
+ source_agent_id: value.source_agent_id,
1068
+ source_turn_id: value.source_turn_id,
1069
+ },
1070
+ timestamp,
1071
+ ),
1072
+ );
1073
+ }
1074
+ return validParsedEvent(
1075
+ createRegistryEvent(
1076
+ rootSessionId,
1077
+ "delivery-turn-released",
1078
+ {
1079
+ source_agent_id: value.source_agent_id,
1080
+ source_turn_id: value.source_turn_id,
1081
+ },
1082
+ timestamp,
1083
+ ),
1084
+ );
1085
+ case "agent-message-recorded":
1086
+ if (
1087
+ !Value.Check(RegistryMessageRecordedEventWireSchema, value) ||
1088
+ !isOwnedTurnId(value.message.source_agent_id, value.message.turn_id) ||
1089
+ (version === 2 && (value.recorded_at === undefined || !isIsoTimestamp(value.recorded_at)))
1090
+ ) {
1091
+ return invalidParsedEvent("invalid-event-fields", "agent activity fields are invalid");
1092
+ }
1093
+ const recordedAt = version === 1 ? timestamp : value.recorded_at;
1094
+ if (recordedAt === undefined) {
1095
+ return invalidParsedEvent("invalid-event-fields", "agent activity fields are invalid");
1096
+ }
1097
+ return validParsedEvent(
1098
+ createRegistryEvent(
1099
+ rootSessionId,
1100
+ "agent-message-recorded",
1101
+ {
1102
+ agent_id: value.agent_id,
1103
+ message: cloneRegistryRecentMessage(value.message),
1104
+ recorded_at: recordedAt,
1105
+ },
1106
+ timestamp,
1107
+ ),
1108
+ );
1109
+ case "agent-deleted":
1110
+ if (
1111
+ !Value.Check(RegistryAgentDeletedEventWireSchema, value) ||
1112
+ !isUniqueNonEmptyStringArray(value.agent_ids) ||
1113
+ value.agent_ids.length === 0 ||
1114
+ value.agent_ids.some(
1115
+ (agentId) => agentId === "root" || !CANONICAL_AGENT_ID_PATTERN.test(agentId),
1116
+ )
1117
+ ) {
1118
+ return invalidParsedEvent(
1119
+ "invalid-event-fields",
1120
+ "agent-deleted IDs must be unique canonical non-root agent IDs",
1121
+ );
1122
+ }
1123
+ return validParsedEvent(
1124
+ createRegistryEvent(
1125
+ rootSessionId,
1126
+ "agent-deleted",
1127
+ { agent_ids: [...value.agent_ids] },
1128
+ timestamp,
1129
+ ),
1130
+ );
1131
+ default:
1132
+ return invalidParsedEvent("invalid-event-fields", `unknown Registry event: ${value.event}`);
1133
+ }
1134
+ }
1135
+
1136
+ /** Parse one V1 or V2 Registry payload into the current V2 representation without throwing. */
1137
+ export function parseRegistryEvent(
1138
+ value: RegistryParseInput,
1139
+ rootSessionId: string,
1140
+ ): RegistryEventV2 | undefined {
1141
+ const parsed = parseRegistryEventRecord(value, rootSessionId);
1142
+ return parsed.kind === "event" ? parsed.event : undefined;
1143
+ }
1144
+
1145
+ function reportDiagnostic(
1146
+ diagnostics: RegistryReplayDiagnostic[],
1147
+ entryIndex: number,
1148
+ code: RegistryReplayDiagnosticCode,
1149
+ message: string,
1150
+ ): void {
1151
+ diagnostics.push({ entry_index: entryIndex, code, message });
1152
+ }
1153
+
1154
+ interface ReplayDeliverySequenceState {
1155
+ readonly terminalSequences: Map<string, number>;
1156
+ readonly coordinationSequences: Map<string, number>;
1157
+ highWaterMark: number;
1158
+ }
1159
+
1160
+ function terminalDeliveryIdentity(delivery: PersistedDelivery): string {
1161
+ return `${delivery.source_agent_id}\u0000${delivery.source_turn_id}`;
1162
+ }
1163
+
1164
+ function observeTerminalEventSequence(
1165
+ state: ReplayDeliverySequenceState,
1166
+ delivery: PersistedDelivery,
1167
+ ): PersistedDelivery | undefined {
1168
+ const identity = terminalDeliveryIdentity(delivery);
1169
+ const existingSequence = state.terminalSequences.get(identity);
1170
+ if (existingSequence !== undefined) {
1171
+ if (delivery.sequence !== undefined && delivery.sequence !== existingSequence) return undefined;
1172
+ return { ...delivery, sequence: existingSequence };
1173
+ }
1174
+ const sequence = delivery.sequence ?? state.highWaterMark + 1;
1175
+ if (
1176
+ !isSafeSequence(sequence) ||
1177
+ sequence >= Number.MAX_SAFE_INTEGER ||
1178
+ sequence <= state.highWaterMark
1179
+ )
1180
+ return undefined;
1181
+ state.terminalSequences.set(identity, sequence);
1182
+ state.highWaterMark = sequence;
1183
+ return { ...delivery, sequence };
1184
+ }
1185
+
1186
+ function observeCoordinationEventSequence(
1187
+ state: ReplayDeliverySequenceState,
1188
+ delivery: PersistedCoordinationDelivery,
1189
+ ): PersistedCoordinationDelivery | undefined {
1190
+ const existingSequence = state.coordinationSequences.get(delivery.delivery_id);
1191
+ if (existingSequence !== undefined) {
1192
+ return delivery.sequence === existingSequence ? delivery : undefined;
1193
+ }
1194
+ if (delivery.sequence >= Number.MAX_SAFE_INTEGER || delivery.sequence <= state.highWaterMark)
1195
+ return undefined;
1196
+ state.coordinationSequences.set(delivery.delivery_id, delivery.sequence);
1197
+ state.highWaterMark = delivery.sequence;
1198
+ return delivery;
1199
+ }
1200
+
1201
+ function validateEventAgentReference(
1202
+ agentId: string,
1203
+ agents: ReadonlyMap<string, PersistedAgent>,
1204
+ ): boolean {
1205
+ return agentId === "root" || agents.has(agentId);
1206
+ }
1207
+
1208
+ /** Replay active-branch Registry V1/V2 entries and report precise semantic diagnostics. */
127
1209
  export function replayRegistryEntries(
128
1210
  entries: readonly RegistryEntryLike[],
129
1211
  rootSessionId: string,
1212
+ reportInvalidRecords?: (diagnostics: RegistryReplayDiagnostic[]) => void,
130
1213
  ): RegistrySnapshot {
131
- const events = entries
132
- .filter((entry) => entry.type === "custom" && entry.customType === REGISTRY_ENTRY_TYPE)
133
- .map((entry) => entry.data)
134
- .filter((data): data is RegistryEventV1 => isRegistryEvent(data, rootSessionId));
1214
+ const parsedEvents: Array<{ entryIndex: number; event: RegistryEventV2 }> = [];
1215
+ const diagnostics: RegistryReplayDiagnostic[] = [];
1216
+ entries.forEach((entry, entryIndex) => {
1217
+ if (entry.type !== "custom" || entry.customType !== REGISTRY_ENTRY_TYPE) return;
1218
+ if (!Value.Check(RegistryJsonValueWireSchema, entry.data)) {
1219
+ reportDiagnostic(diagnostics, entryIndex, "invalid-envelope", "record must be JSON");
1220
+ return;
1221
+ }
1222
+ const parsed = parseRegistryEventRecord(entry.data, rootSessionId);
1223
+ if (parsed.kind === "foreign-root") return;
1224
+ if (parsed.kind === "event") {
1225
+ parsedEvents.push({ entryIndex, event: parsed.event });
1226
+ } else {
1227
+ reportDiagnostic(diagnostics, entryIndex, parsed.code, parsed.message);
1228
+ }
1229
+ });
1230
+
135
1231
  let checkpointIndex = -1;
136
- for (let index = events.length - 1; index >= 0; index--) {
137
- if (events[index]?.event === "checkpoint") {
1232
+ for (let index = parsedEvents.length - 1; index >= 0; index--) {
1233
+ if (parsedEvents[index]?.event.event === "checkpoint") {
138
1234
  checkpointIndex = index;
139
1235
  break;
140
1236
  }
141
1237
  }
142
-
143
- let snapshot: RegistrySnapshot = { agents: [], tombstones: [], deliveries: [] };
144
- if (checkpointIndex >= 0) {
145
- snapshot = cloneSnapshot((events[checkpointIndex] as RegistryCheckpointV1).snapshot);
146
- }
147
- const agents = new Map(snapshot.agents.map((agent) => [agent.agent_id, agent]));
148
- const tombstones = new Set(snapshot.tombstones);
149
- const deliveries = new Map(
150
- snapshot.deliveries.map((delivery) => [
151
- deliveryKey(delivery.source_agent_id, delivery.source_turn_id),
152
- delivery,
153
- ]),
1238
+ const checkpointEvent = checkpointIndex >= 0 ? parsedEvents[checkpointIndex]?.event : undefined;
1239
+ const checkpoint = checkpointEvent?.event === "checkpoint" ? checkpointEvent.snapshot : undefined;
1240
+ const agents = new Map(
1241
+ (checkpoint?.agents ?? []).map((agent) => [agent.agent_id, structuredClone(agent)]),
154
1242
  );
1243
+ const tombstones = new Set(checkpoint?.tombstones ?? []);
1244
+ let ledger: DeliveryLedger = createDeliveryLedger({
1245
+ deliveries: checkpoint?.deliveries,
1246
+ coordination_deliveries: checkpoint?.coordination_deliveries,
1247
+ wait_claimed_turns: checkpoint?.wait_claimed_turns,
1248
+ next_delivery_sequence: checkpoint?.next_delivery_sequence,
1249
+ });
1250
+ const sequenceState: ReplayDeliverySequenceState = {
1251
+ terminalSequences: new Map(
1252
+ ledger.terminalDeliveries.map((delivery) => [
1253
+ terminalDeliveryIdentity(delivery),
1254
+ delivery.sequence ?? 0,
1255
+ ]),
1256
+ ),
1257
+ coordinationSequences: new Map(
1258
+ ledger.coordinationDeliveries.map((delivery) => [delivery.delivery_id, delivery.sequence]),
1259
+ ),
1260
+ highWaterMark: ledger.nextSequence - 1,
1261
+ };
155
1262
 
156
- for (const event of events.slice(checkpointIndex + 1)) {
1263
+ for (const { entryIndex, event } of parsedEvents.slice(checkpointIndex + 1)) {
157
1264
  switch (event.event) {
158
- case "checkpoint": {
159
- snapshot = cloneSnapshot(event.snapshot);
160
- agents.clear();
161
- for (const agent of snapshot.agents) agents.set(agent.agent_id, agent);
162
- tombstones.clear();
163
- for (const agentId of snapshot.tombstones) tombstones.add(agentId);
164
- deliveries.clear();
165
- for (const delivery of snapshot.deliveries) {
166
- deliveries.set(deliveryKey(delivery.source_agent_id, delivery.source_turn_id), delivery);
167
- }
1265
+ case "checkpoint":
168
1266
  break;
169
- }
170
1267
  case "agent-created":
171
- if (!tombstones.has(event.agent.agent_id))
1268
+ if (
1269
+ event.agent.active_turn_id ||
1270
+ event.agent.latest_result ||
1271
+ event.agent.recent_messages.length > 0 ||
1272
+ event.agent.deleted
1273
+ ) {
1274
+ reportDiagnostic(
1275
+ diagnostics,
1276
+ entryIndex,
1277
+ "invalid-event-fields",
1278
+ `agent-created contains post-creation state for ${event.agent.agent_id}`,
1279
+ );
1280
+ } else if (tombstones.has(event.agent.agent_id) || agents.has(event.agent.agent_id)) {
1281
+ reportDiagnostic(
1282
+ diagnostics,
1283
+ entryIndex,
1284
+ "invalid-event-reference",
1285
+ `agent-created duplicates or resurrects ${event.agent.agent_id}`,
1286
+ );
1287
+ } else if (event.agent.parent_id !== "root" && !agents.has(event.agent.parent_id)) {
1288
+ reportDiagnostic(
1289
+ diagnostics,
1290
+ entryIndex,
1291
+ "invalid-agent-hierarchy",
1292
+ `agent-created parent is missing for ${event.agent.agent_id}`,
1293
+ );
1294
+ } else {
172
1295
  agents.set(event.agent.agent_id, structuredClone(event.agent));
1296
+ }
173
1297
  break;
174
1298
  case "turn-started": {
175
1299
  const agent = agents.get(event.agent_id);
176
- if (agent) {
177
- agent.active_turn_id = event.turn_id;
178
- agent.active_turn_started_at = event.started_at;
1300
+ if (!agent || agent.active_turn_id) {
1301
+ reportDiagnostic(
1302
+ diagnostics,
1303
+ entryIndex,
1304
+ "invalid-event-reference",
1305
+ `turn-started cannot target ${event.agent_id}`,
1306
+ );
1307
+ break;
179
1308
  }
1309
+ agent.active_turn_id = event.turn_id;
1310
+ agent.active_turn_started_at = event.started_at;
1311
+ agent.latest_activity_at = event.started_at;
180
1312
  break;
181
1313
  }
182
1314
  case "turn-settled": {
183
1315
  const agent = agents.get(event.result.agent_id);
184
- if (agent) {
185
- agent.active_turn_id = undefined;
186
- agent.active_turn_started_at = undefined;
187
- agent.latest_result = structuredClone(event.result);
1316
+ if (!agent || agent.active_turn_id !== event.result.turn_id) {
1317
+ reportDiagnostic(
1318
+ diagnostics,
1319
+ entryIndex,
1320
+ "invalid-event-reference",
1321
+ `turn-settled does not match ${event.result.agent_id} active turn`,
1322
+ );
1323
+ break;
188
1324
  }
1325
+ agent.active_turn_id = undefined;
1326
+ agent.active_turn_started_at = undefined;
1327
+ agent.latest_result = structuredClone(event.result);
1328
+ agent.latest_activity_at = event.settled_at ?? event.timestamp;
1329
+ if (event.session_leaf_id) agent.session_leaf_id = event.session_leaf_id;
189
1330
  break;
190
1331
  }
191
- case "delivery-pending":
192
- deliveries.set(
193
- deliveryKey(event.delivery.source_agent_id, event.delivery.source_turn_id),
194
- structuredClone(event.delivery),
195
- );
1332
+ case "delivery-pending": {
1333
+ const delivery = observeTerminalEventSequence(sequenceState, event.delivery);
1334
+ if (!delivery) {
1335
+ reportDiagnostic(
1336
+ diagnostics,
1337
+ entryIndex,
1338
+ "invalid-delivery-sequence",
1339
+ "new terminal delivery sequences must strictly increase and updates must remain stable",
1340
+ );
1341
+ break;
1342
+ }
1343
+ const source = agents.get(delivery.source_agent_id);
1344
+ const identityError = validateDeliveryIdentity(delivery, delivery.result ? 2 : 1);
1345
+ if (identityError) {
1346
+ reportDiagnostic(diagnostics, entryIndex, "invalid-delivery-identity", identityError);
1347
+ } else if (!source) {
1348
+ reportDiagnostic(
1349
+ diagnostics,
1350
+ entryIndex,
1351
+ "invalid-event-reference",
1352
+ "terminal delivery source is not live",
1353
+ );
1354
+ } else if (source.parent_id !== delivery.destination_agent_id) {
1355
+ reportDiagnostic(
1356
+ diagnostics,
1357
+ entryIndex,
1358
+ "invalid-delivery-adjacency",
1359
+ "terminal delivery destination must be the direct parent",
1360
+ );
1361
+ } else {
1362
+ ledger = upsertTerminalDelivery(ledger, delivery).ledger;
1363
+ }
196
1364
  break;
1365
+ }
197
1366
  case "delivery-settled": {
198
- const delivery = deliveries.get(deliveryKey(event.source_agent_id, event.source_turn_id));
199
- if (delivery) {
200
- delivery.settled = event.error === undefined;
201
- delivery.error = event.error;
1367
+ const delivery = ledger.terminalDeliveries.find(
1368
+ (candidate) =>
1369
+ candidate.source_agent_id === event.source_agent_id &&
1370
+ candidate.source_turn_id === event.source_turn_id,
1371
+ );
1372
+ if (!delivery) {
1373
+ reportDiagnostic(
1374
+ diagnostics,
1375
+ entryIndex,
1376
+ "invalid-event-reference",
1377
+ "delivery-settled has no pending terminal item",
1378
+ );
1379
+ } else if (event.error !== undefined) {
1380
+ ledger = upsertTerminalDelivery(ledger, { ...delivery, error: event.error }).ledger;
1381
+ } else {
1382
+ ledger = settleTerminalDelivery(
1383
+ ledger,
1384
+ event.source_agent_id,
1385
+ event.source_turn_id,
1386
+ ).ledger;
1387
+ }
1388
+ break;
1389
+ }
1390
+ case "delivery-pruned": {
1391
+ const delivery = ledger.terminalDeliveries.find(
1392
+ (candidate) =>
1393
+ candidate.source_agent_id === event.source_agent_id &&
1394
+ candidate.source_turn_id === event.source_turn_id,
1395
+ );
1396
+ if (!delivery || delivery.path !== "wait") {
1397
+ reportDiagnostic(
1398
+ diagnostics,
1399
+ entryIndex,
1400
+ "invalid-event-reference",
1401
+ "delivery-pruned must target one pending wait-only terminal item",
1402
+ );
1403
+ break;
202
1404
  }
1405
+ ledger = settleTerminalDelivery(ledger, event.source_agent_id, event.source_turn_id).ledger;
1406
+ ledger = releaseEmptyDeliveryLedgerTurn(
1407
+ ledger,
1408
+ event.source_agent_id,
1409
+ event.source_turn_id,
1410
+ agents.get(event.source_agent_id)?.active_turn_id === event.source_turn_id,
1411
+ ).ledger;
203
1412
  break;
204
1413
  }
205
- case "agent-deleted":
1414
+ case "coordination-delivery-pending": {
1415
+ const delivery = observeCoordinationEventSequence(sequenceState, event.delivery);
1416
+ if (!delivery) {
1417
+ reportDiagnostic(
1418
+ diagnostics,
1419
+ entryIndex,
1420
+ "invalid-delivery-sequence",
1421
+ "new Coordination Message sequences must strictly increase and updates must remain stable",
1422
+ );
1423
+ break;
1424
+ }
1425
+ const identityError = validateCoordinationIdentity(delivery);
1426
+ if (identityError) {
1427
+ reportDiagnostic(diagnostics, entryIndex, "invalid-delivery-identity", identityError);
1428
+ } else if (
1429
+ !areAdjacentAgents(
1430
+ delivery.message.details.source_agent_id,
1431
+ delivery.destination_agent_id,
1432
+ agents,
1433
+ )
1434
+ ) {
1435
+ reportDiagnostic(
1436
+ diagnostics,
1437
+ entryIndex,
1438
+ "invalid-delivery-adjacency",
1439
+ "Coordination Message endpoints must be adjacent live agents",
1440
+ );
1441
+ } else {
1442
+ ledger = upsertCoordinationDelivery(ledger, delivery).ledger;
1443
+ }
1444
+ break;
1445
+ }
1446
+ case "coordination-delivery-settled": {
1447
+ const delivery = ledger.coordinationDeliveries.find(
1448
+ (candidate) => candidate.delivery_id === event.delivery_id,
1449
+ );
1450
+ if (!delivery) {
1451
+ reportDiagnostic(
1452
+ diagnostics,
1453
+ entryIndex,
1454
+ "invalid-event-reference",
1455
+ "coordination-delivery-settled has no pending item",
1456
+ );
1457
+ } else if (event.error !== undefined) {
1458
+ ledger = upsertCoordinationDelivery(ledger, { ...delivery, error: event.error }).ledger;
1459
+ } else {
1460
+ ledger = settleCoordinationDelivery(ledger, event.delivery_id).ledger;
1461
+ }
1462
+ break;
1463
+ }
1464
+ case "delivery-turn-claimed": {
1465
+ const source = agents.get(event.source_agent_id);
1466
+ if (
1467
+ !source ||
1468
+ !isObservableSourceTurn(
1469
+ source,
1470
+ event.source_turn_id,
1471
+ ledger.terminalDeliveries,
1472
+ ledger.coordinationDeliveries,
1473
+ ) ||
1474
+ isDeliveryLedgerTurnClaimed(ledger, event.source_agent_id, event.source_turn_id)
1475
+ ) {
1476
+ reportDiagnostic(
1477
+ diagnostics,
1478
+ entryIndex,
1479
+ "invalid-event-reference",
1480
+ "delivery-turn-claimed must reference one observable unclaimed live turn",
1481
+ );
1482
+ } else {
1483
+ ledger = claimDeliveryLedgerTurn(
1484
+ ledger,
1485
+ event.source_agent_id,
1486
+ event.source_turn_id,
1487
+ ).ledger;
1488
+ }
1489
+ break;
1490
+ }
1491
+ case "delivery-turn-released":
1492
+ if (!isDeliveryLedgerTurnClaimed(ledger, event.source_agent_id, event.source_turn_id)) {
1493
+ reportDiagnostic(
1494
+ diagnostics,
1495
+ entryIndex,
1496
+ "invalid-event-reference",
1497
+ "delivery-turn-released must reference one existing wait claim",
1498
+ );
1499
+ } else {
1500
+ ledger = releaseDeliveryLedgerTurn(
1501
+ ledger,
1502
+ event.source_agent_id,
1503
+ event.source_turn_id,
1504
+ ).ledger;
1505
+ }
1506
+ break;
1507
+ case "agent-message-recorded": {
1508
+ const agent = agents.get(event.agent_id);
1509
+ if (!agent) {
1510
+ reportDiagnostic(
1511
+ diagnostics,
1512
+ entryIndex,
1513
+ "invalid-event-reference",
1514
+ `agent-message-recorded target is not live: ${event.agent_id}`,
1515
+ );
1516
+ } else if (
1517
+ !validateEventAgentReference(event.message.source_agent_id, agents) ||
1518
+ !areAdjacentAgents(event.message.source_agent_id, event.agent_id, agents)
1519
+ ) {
1520
+ reportDiagnostic(
1521
+ diagnostics,
1522
+ entryIndex,
1523
+ "invalid-delivery-adjacency",
1524
+ "recorded message endpoints must be adjacent live agents",
1525
+ );
1526
+ } else {
1527
+ agent.recent_messages.push(structuredClone(event.message));
1528
+ agent.recent_messages = agent.recent_messages.slice(-20);
1529
+ agent.latest_activity_at = event.recorded_at;
1530
+ }
1531
+ break;
1532
+ }
1533
+ case "agent-deleted": {
1534
+ const deleting = new Set(event.agent_ids);
1535
+ if (event.agent_ids.some((agentId) => !agents.has(agentId))) {
1536
+ reportDiagnostic(
1537
+ diagnostics,
1538
+ entryIndex,
1539
+ "invalid-event-reference",
1540
+ "agent-deleted must target only currently live agents",
1541
+ );
1542
+ break;
1543
+ }
1544
+ const leavesOrphan = [...agents.values()].some(
1545
+ (agent) => deleting.has(agent.parent_id) && !deleting.has(agent.agent_id),
1546
+ );
1547
+ if (leavesOrphan) {
1548
+ reportDiagnostic(
1549
+ diagnostics,
1550
+ entryIndex,
1551
+ "invalid-agent-hierarchy",
1552
+ "agent-deleted must contain every live descendant in each deleted subtree",
1553
+ );
1554
+ break;
1555
+ }
1556
+ const belongsToDeletedSubtree = (agentId: string) =>
1557
+ event.agent_ids.some(
1558
+ (deletedId) => agentId === deletedId || agentId.startsWith(`${deletedId}.`),
1559
+ );
206
1560
  for (const agentId of event.agent_ids) {
207
1561
  agents.delete(agentId);
208
1562
  tombstones.add(agentId);
209
1563
  }
1564
+ for (const agent of agents.values()) {
1565
+ agent.recent_messages = agent.recent_messages.filter(
1566
+ (message) => !belongsToDeletedSubtree(message.source_agent_id),
1567
+ );
1568
+ }
1569
+ ledger = pruneDeliveryLedgerAgents(ledger, event.agent_ids).ledger;
210
1570
  break;
1571
+ }
211
1572
  }
212
1573
  }
213
1574
 
214
- return {
1575
+ if (diagnostics.length > 0) reportInvalidRecords?.(diagnostics);
1576
+ const ledgerSnapshot = deliveryLedgerSnapshot(ledger);
1577
+ const result: RegistrySnapshot = {
215
1578
  agents: [...agents.values()],
216
1579
  tombstones: [...tombstones],
217
- deliveries: [...deliveries.values()],
1580
+ deliveries: ledgerSnapshot.deliveries,
218
1581
  };
1582
+ if (ledgerSnapshot.coordination_deliveries.length > 0)
1583
+ result.coordination_deliveries = ledgerSnapshot.coordination_deliveries;
1584
+ if (ledgerSnapshot.wait_claimed_turns.length > 0)
1585
+ result.wait_claimed_turns = ledgerSnapshot.wait_claimed_turns;
1586
+ const nextDeliverySequence = Math.max(
1587
+ ledgerSnapshot.next_delivery_sequence,
1588
+ sequenceState.highWaterMark + 1,
1589
+ );
1590
+ if (nextDeliverySequence > 1) result.next_delivery_sequence = nextDeliverySequence;
1591
+ return result;
219
1592
  }