@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.
- package/README.md +77 -9
- package/package.json +6 -6
- package/src/minimal-subagents-capabilities.ts +8 -8
- package/src/minimal-subagents-config.ts +222 -73
- package/src/minimal-subagents-context.ts +7 -1
- package/src/minimal-subagents-coordinator.ts +943 -116
- package/src/minimal-subagents-delivery-ledger.ts +529 -0
- package/src/minimal-subagents-extension.ts +382 -64
- package/src/minimal-subagents-fork-lifecycle.ts +22 -12
- package/src/minimal-subagents-message-envelope.ts +20 -0
- package/src/minimal-subagents-registry-wire.ts +269 -0
- package/src/minimal-subagents-registry.ts +1505 -132
- package/src/minimal-subagents-render-contract.ts +301 -0
- package/src/minimal-subagents-rendering.ts +513 -372
- package/src/minimal-subagents-session-wire.ts +42 -0
- package/src/minimal-subagents-sessions.ts +437 -101
- package/src/minimal-subagents-shutdown.ts +1 -1
- package/src/minimal-subagents-tool-schemas.ts +21 -7
- package/src/minimal-subagents-tools.ts +75 -25
- package/src/minimal-subagents-types.ts +76 -21
- package/src/minimal-subagents-ui.ts +214 -23
|
@@ -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
|
|
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
|
|
14
|
-
version:
|
|
66
|
+
interface RegistryEventBaseV2 {
|
|
67
|
+
version: 2;
|
|
15
68
|
root_session_id: string;
|
|
16
69
|
timestamp: string;
|
|
17
70
|
}
|
|
18
71
|
|
|
19
|
-
/**
|
|
20
|
-
export
|
|
21
|
-
event: "checkpoint";
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
119
|
-
|
|
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
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.
|
|
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 =
|
|
137
|
-
if (
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 "
|
|
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
|
-
|
|
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:
|
|
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
|
}
|