@agent-native/core 0.111.4 → 0.112.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.
Files changed (104) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/cli/code-agent-executor.ts +17 -5
  5. package/corpus/core/src/cli/code-agent-runs.ts +43 -2
  6. package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
  7. package/corpus/core/src/coding-tools/index.ts +27 -8
  8. package/corpus/core/src/embeddings/index.ts +233 -0
  9. package/corpus/core/src/index.ts +2 -0
  10. package/corpus/core/src/search/index.ts +413 -0
  11. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
  12. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
  13. package/corpus/templates/brain/README.md +34 -3
  14. package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
  15. package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
  16. package/corpus/templates/brain/actions/create-source.ts +1 -1
  17. package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
  18. package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
  19. package/corpus/templates/brain/actions/import-capture.ts +21 -9
  20. package/corpus/templates/brain/actions/import-transcript.ts +42 -14
  21. package/corpus/templates/brain/actions/list-captures.ts +10 -1
  22. package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
  23. package/corpus/templates/brain/actions/list-projects.ts +51 -0
  24. package/corpus/templates/brain/actions/list-proposals.ts +80 -4
  25. package/corpus/templates/brain/actions/list-sources.ts +17 -3
  26. package/corpus/templates/brain/actions/manage-project.ts +118 -0
  27. package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
  28. package/corpus/templates/brain/actions/navigate.ts +1 -0
  29. package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
  30. package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
  31. package/corpus/templates/brain/actions/review-proposal.ts +2 -0
  32. package/corpus/templates/brain/actions/search-everything.ts +10 -1
  33. package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
  34. package/corpus/templates/brain/actions/set-settings.ts +36 -1
  35. package/corpus/templates/brain/actions/update-proposal.ts +2 -0
  36. package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
  37. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
  38. package/corpus/templates/brain/app/i18n-data.ts +731 -2
  39. package/corpus/templates/brain/app/lib/brain.ts +33 -1
  40. package/corpus/templates/brain/app/routes/ops.tsx +47 -0
  41. package/corpus/templates/brain/app/routes/review.tsx +41 -2
  42. package/corpus/templates/brain/app/routes/search.tsx +33 -2
  43. package/corpus/templates/brain/app/routes/settings.tsx +158 -0
  44. package/corpus/templates/brain/app/routes/sources.tsx +31 -0
  45. package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
  46. package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
  47. package/corpus/templates/brain/package.json +1 -0
  48. package/corpus/templates/brain/server/db/index.ts +18 -0
  49. package/corpus/templates/brain/server/db/schema.ts +197 -1
  50. package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
  51. package/corpus/templates/brain/server/lib/audiences.ts +708 -0
  52. package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
  53. package/corpus/templates/brain/server/lib/brain.ts +877 -50
  54. package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
  55. package/corpus/templates/brain/server/lib/connectors.ts +556 -70
  56. package/corpus/templates/brain/server/lib/demo.ts +17 -4
  57. package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
  58. package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
  59. package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
  60. package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
  61. package/corpus/templates/brain/server/lib/search-index.ts +714 -0
  62. package/corpus/templates/brain/server/lib/search.ts +120 -4
  63. package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
  64. package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
  65. package/corpus/templates/brain/server/onboarding.ts +29 -0
  66. package/corpus/templates/brain/server/plugins/db.ts +204 -0
  67. package/corpus/templates/brain/server/register-secrets.ts +46 -0
  68. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
  69. package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
  70. package/corpus/templates/brain/shared/types.ts +10 -0
  71. package/dist/cli/code-agent-executor.d.ts +8 -9
  72. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  73. package/dist/cli/code-agent-executor.js +9 -3
  74. package/dist/cli/code-agent-executor.js.map +1 -1
  75. package/dist/cli/code-agent-runs.d.ts +5 -0
  76. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  77. package/dist/cli/code-agent-runs.js +33 -2
  78. package/dist/cli/code-agent-runs.js.map +1 -1
  79. package/dist/cli/multi-frontier-runs.d.ts +131 -0
  80. package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
  81. package/dist/cli/multi-frontier-runs.js +529 -0
  82. package/dist/cli/multi-frontier-runs.js.map +1 -0
  83. package/dist/coding-tools/index.d.ts +1 -0
  84. package/dist/coding-tools/index.d.ts.map +1 -1
  85. package/dist/coding-tools/index.js +25 -7
  86. package/dist/coding-tools/index.js.map +1 -1
  87. package/dist/collab/awareness.d.ts +2 -2
  88. package/dist/collab/awareness.d.ts.map +1 -1
  89. package/dist/embeddings/index.d.ts +24 -0
  90. package/dist/embeddings/index.d.ts.map +1 -0
  91. package/dist/embeddings/index.js +172 -0
  92. package/dist/embeddings/index.js.map +1 -0
  93. package/dist/index.d.ts +2 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +2 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/notifications/routes.d.ts +3 -3
  98. package/dist/progress/routes.d.ts +1 -1
  99. package/dist/resources/handlers.d.ts +1 -1
  100. package/dist/search/index.d.ts +88 -0
  101. package/dist/search/index.d.ts.map +1 -0
  102. package/dist/search/index.js +267 -0
  103. package/dist/search/index.js.map +1 -0
  104. package/package.json +3 -1
@@ -0,0 +1,841 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { codeAgentStoreRoot } from "./code-agent-runs.js";
6
+
7
+ export type MultiFrontierPhase =
8
+ | "proposing"
9
+ | "cross_review"
10
+ | "converging"
11
+ | "awaiting_go"
12
+ | "implementing"
13
+ | "checkpoint_review"
14
+ | "paused"
15
+ | "completed"
16
+ | "failed"
17
+ | "canceled";
18
+
19
+ export type MultiFrontierParticipantRole = "driver" | "watchdog";
20
+ export type MultiFrontierParticipantPermission =
21
+ | "read_only"
22
+ | "workspace_write";
23
+ export type MultiFrontierParticipantStatus =
24
+ | "idle"
25
+ | "running"
26
+ | "waiting"
27
+ | "failed"
28
+ | "completed";
29
+ export type MultiFrontierApprovalState =
30
+ | "not_required"
31
+ | "pending"
32
+ | "approved"
33
+ | "rejected";
34
+ export type MultiFrontierRecoveryReason =
35
+ | "main_process_restarted"
36
+ | "driver_crashed"
37
+ | "watchdog_crashed"
38
+ | "app_quit"
39
+ | "canceled";
40
+
41
+ export interface MultiFrontierParticipantState {
42
+ participantId: string;
43
+ provider: string;
44
+ runtime: string;
45
+ model?: string;
46
+ capabilities?: string[];
47
+ sessionRef?: string;
48
+ role: MultiFrontierParticipantRole;
49
+ permission: MultiFrontierParticipantPermission;
50
+ status: MultiFrontierParticipantStatus;
51
+ }
52
+
53
+ export interface MultiFrontierDriverLease {
54
+ participantId: string;
55
+ generation: number;
56
+ leaseState: "inactive" | "active" | "revoked";
57
+ }
58
+
59
+ export interface MultiFrontierApproval {
60
+ state: MultiFrontierApprovalState;
61
+ proposalId?: string;
62
+ reviewPacketId?: string;
63
+ }
64
+
65
+ export interface MultiFrontierRecovery {
66
+ reason: MultiFrontierRecoveryReason;
67
+ recoveredAt: string;
68
+ resumablePhase: MultiFrontierPhase;
69
+ checkpointId?: string;
70
+ }
71
+
72
+ /**
73
+ * The durable state contract shared by the main process and every participant.
74
+ * Only the main-process coordinator writes it through this module.
75
+ */
76
+ export interface MultiFrontierRunState {
77
+ schemaVersion: 1;
78
+ collaborationId: string;
79
+ phase: MultiFrontierPhase;
80
+ participants: MultiFrontierParticipantState[];
81
+ driver: MultiFrontierDriverLease | null;
82
+ approval: MultiFrontierApproval;
83
+ checkpointIds: string[];
84
+ round: number;
85
+ proposalIds: string[];
86
+ reviewIds: string[];
87
+ recovery?: MultiFrontierRecovery;
88
+ }
89
+
90
+ export interface CreateMultiFrontierRunInput {
91
+ collaborationId: string;
92
+ phase?: MultiFrontierPhase;
93
+ participants: MultiFrontierParticipantState[];
94
+ approval?: MultiFrontierApproval;
95
+ checkpointIds?: string[];
96
+ }
97
+
98
+ export interface RecoverMultiFrontierRunInput {
99
+ now: string;
100
+ reason: MultiFrontierRecoveryReason;
101
+ }
102
+
103
+ export interface MultiFrontierParticipantEvent {
104
+ participantId: string;
105
+ generation?: number;
106
+ permission: MultiFrontierParticipantPermission;
107
+ }
108
+
109
+ export interface PersistedMultiFrontierParticipantEvent extends MultiFrontierParticipantEvent {
110
+ schemaVersion: 1;
111
+ id: string;
112
+ collaborationId: string;
113
+ createdAt: string;
114
+ status?: MultiFrontierParticipantStatus;
115
+ }
116
+
117
+ export interface AppendMultiFrontierParticipantEventInput extends MultiFrontierParticipantEvent {
118
+ id: string;
119
+ collaborationId: string;
120
+ createdAt?: string;
121
+ status?: MultiFrontierParticipantStatus;
122
+ }
123
+
124
+ export interface MultiFrontierStoredRun extends MultiFrontierRunState {
125
+ createdAt: string;
126
+ updatedAt: string;
127
+ }
128
+
129
+ export type AppendMultiFrontierParticipantEventResult =
130
+ | {
131
+ accepted: true;
132
+ deduplicated: boolean;
133
+ event: PersistedMultiFrontierParticipantEvent;
134
+ run: MultiFrontierStoredRun;
135
+ }
136
+ | {
137
+ accepted: false;
138
+ reason:
139
+ | "missing-run"
140
+ | "stale-driver"
141
+ | "missing-participant"
142
+ | "event-conflict"
143
+ | "terminal-participant";
144
+ run: MultiFrontierStoredRun | null;
145
+ };
146
+
147
+ const TERMINAL_PHASES = new Set<MultiFrontierPhase>([
148
+ "completed",
149
+ "failed",
150
+ "canceled",
151
+ ]);
152
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,191}$/;
153
+
154
+ /** All multi-frontier state stays alongside the existing local Code store. */
155
+ export function multiFrontierRunsStoreRoot(): string {
156
+ return path.join(codeAgentStoreRoot(), "multi-frontier");
157
+ }
158
+
159
+ export function multiFrontierRunsDir(): string {
160
+ return path.join(multiFrontierRunsStoreRoot(), "runs");
161
+ }
162
+
163
+ export function multiFrontierParticipantEventsDir(): string {
164
+ return path.join(multiFrontierRunsStoreRoot(), "events");
165
+ }
166
+
167
+ export function createMultiFrontierRun(
168
+ input: CreateMultiFrontierRunInput,
169
+ ): MultiFrontierStoredRun {
170
+ assertSafeId(input.collaborationId, "collaboration id");
171
+ assertParticipants(input.participants);
172
+ if (getMultiFrontierRun(input.collaborationId)) {
173
+ throw new Error(
174
+ `Multi-frontier run already exists: ${input.collaborationId}`,
175
+ );
176
+ }
177
+ const now = new Date().toISOString();
178
+ const state: MultiFrontierStoredRun = {
179
+ schemaVersion: 1,
180
+ collaborationId: input.collaborationId,
181
+ phase: input.phase ?? "proposing",
182
+ participants: input.participants.map((participant) => ({
183
+ ...participant,
184
+ capabilities: [...(participant.capabilities ?? [])],
185
+ role: "watchdog",
186
+ permission: "read_only",
187
+ })),
188
+ driver: null,
189
+ approval: { ...(input.approval ?? { state: "not_required" }) },
190
+ checkpointIds: [...(input.checkpointIds ?? [])],
191
+ round: 1,
192
+ proposalIds: [],
193
+ reviewIds: [],
194
+ createdAt: now,
195
+ updatedAt: now,
196
+ };
197
+ writeNewMultiFrontierRun(state);
198
+ return state;
199
+ }
200
+
201
+ export function getMultiFrontierRun(
202
+ collaborationId: string,
203
+ ): MultiFrontierStoredRun | null {
204
+ if (!SAFE_ID.test(collaborationId)) return null;
205
+ return readStoredRun(multiFrontierRunPath(collaborationId));
206
+ }
207
+
208
+ export function listMultiFrontierRuns(): MultiFrontierStoredRun[] {
209
+ const directory = multiFrontierRunsDir();
210
+ if (!fs.existsSync(directory)) return [];
211
+ return fs
212
+ .readdirSync(directory)
213
+ .filter((file) => file.endsWith(".json"))
214
+ .map((file) => readStoredRun(path.join(directory, file)))
215
+ .filter((run): run is MultiFrontierStoredRun => run !== null)
216
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
217
+ }
218
+
219
+ /**
220
+ * Recovery never resumes a native participant automatically. Any in-flight
221
+ * driver lease is revoked and requires an explicit, newly fenced activation.
222
+ */
223
+ export function recoverMultiFrontierRun(
224
+ state: MultiFrontierRunState,
225
+ input: RecoverMultiFrontierRunInput,
226
+ ): MultiFrontierRunState {
227
+ assertTimestamp(input.now, "recovery time");
228
+ if (TERMINAL_PHASES.has(state.phase) || state.phase === "paused") {
229
+ return state;
230
+ }
231
+
232
+ const checkpointId = state.checkpointIds.at(-1);
233
+ return {
234
+ ...state,
235
+ phase: "paused",
236
+ participants: state.participants.map((participant) => ({
237
+ ...participant,
238
+ permission: "read_only",
239
+ status: participant.status === "running" ? "waiting" : participant.status,
240
+ })),
241
+ driver: state.driver ? { ...state.driver, leaseState: "revoked" } : null,
242
+ recovery: {
243
+ reason: input.reason,
244
+ recoveredAt: input.now,
245
+ resumablePhase: state.phase,
246
+ ...(checkpointId ? { checkpointId } : {}),
247
+ },
248
+ };
249
+ }
250
+
251
+ /** Persist the recovery decision made by the main-process coordinator. */
252
+ export function recoverStoredMultiFrontierRun(
253
+ collaborationId: string,
254
+ input: RecoverMultiFrontierRunInput,
255
+ ): MultiFrontierStoredRun | null {
256
+ const current = getMultiFrontierRun(collaborationId);
257
+ if (!current) return null;
258
+ const recovered = recoverMultiFrontierRun(current, input);
259
+ if (recovered === current) return current;
260
+ return writeNextStoredRun(current, recovered, input.now);
261
+ }
262
+
263
+ /**
264
+ * Coordinators must call this before applying a participant event. Write
265
+ * events are accepted only from the active driver generation.
266
+ */
267
+ export function canApplyMultiFrontierParticipantEvent(
268
+ state: MultiFrontierRunState,
269
+ event: MultiFrontierParticipantEvent,
270
+ ): boolean {
271
+ if (TERMINAL_PHASES.has(state.phase) || state.phase === "paused") {
272
+ return false;
273
+ }
274
+ if (
275
+ !state.participants.some(
276
+ (candidate) => candidate.participantId === event.participantId,
277
+ )
278
+ ) {
279
+ return false;
280
+ }
281
+ if (event.permission === "read_only") return true;
282
+ const driver = state.driver;
283
+ return Boolean(
284
+ state.phase === "implementing" &&
285
+ driver?.leaseState === "active" &&
286
+ driver.participantId === event.participantId &&
287
+ driver.generation === event.generation,
288
+ );
289
+ }
290
+
291
+ /**
292
+ * Explicitly restores a write lease after recovery. The generation increments
293
+ * so an event from the interrupted process can never regain write authority.
294
+ */
295
+ export function reactivateMultiFrontierDriver(
296
+ state: MultiFrontierRunState,
297
+ participantId: string,
298
+ ): MultiFrontierRunState | null {
299
+ if (
300
+ state.phase !== "paused" ||
301
+ !state.recovery ||
302
+ state.recovery.resumablePhase !== "implementing" ||
303
+ state.approval.state === "pending"
304
+ ) {
305
+ return null;
306
+ }
307
+ const participant = state.participants.find(
308
+ (candidate) => candidate.participantId === participantId,
309
+ );
310
+ if (!participant || participant.status === "failed") return null;
311
+
312
+ const nextGeneration = (state.driver?.generation ?? 0) + 1;
313
+ return {
314
+ ...state,
315
+ phase: state.recovery.resumablePhase,
316
+ participants: state.participants.map((candidate) => ({
317
+ ...candidate,
318
+ role: candidate.participantId === participantId ? "driver" : "watchdog",
319
+ permission:
320
+ candidate.participantId === participantId
321
+ ? "workspace_write"
322
+ : "read_only",
323
+ status:
324
+ candidate.participantId === participantId
325
+ ? "waiting"
326
+ : candidate.status,
327
+ })),
328
+ driver: {
329
+ participantId,
330
+ generation: nextGeneration,
331
+ leaseState: "active",
332
+ },
333
+ recovery: undefined,
334
+ };
335
+ }
336
+
337
+ /** Assign the first explicit write lease for an implementing run. */
338
+ export function activateMultiFrontierDriver(
339
+ state: MultiFrontierRunState,
340
+ participantId: string,
341
+ ): MultiFrontierRunState | null {
342
+ if (
343
+ state.phase !== "implementing" ||
344
+ state.approval.state === "pending" ||
345
+ state.driver?.leaseState === "active"
346
+ ) {
347
+ return null;
348
+ }
349
+ const participant = state.participants.find(
350
+ (candidate) => candidate.participantId === participantId,
351
+ );
352
+ if (!participant || participant.status === "failed") return null;
353
+
354
+ return {
355
+ ...state,
356
+ participants: state.participants.map((candidate) => ({
357
+ ...candidate,
358
+ role: candidate.participantId === participantId ? "driver" : "watchdog",
359
+ permission:
360
+ candidate.participantId === participantId
361
+ ? "workspace_write"
362
+ : "read_only",
363
+ })),
364
+ driver: {
365
+ participantId,
366
+ generation: (state.driver?.generation ?? 0) + 1,
367
+ leaseState: "active",
368
+ },
369
+ };
370
+ }
371
+
372
+ /** Persist the first explicit driver activation from the main-process coordinator. */
373
+ export function activateStoredMultiFrontierDriver(
374
+ collaborationId: string,
375
+ participantId: string,
376
+ now = new Date().toISOString(),
377
+ ): MultiFrontierStoredRun | null {
378
+ assertTimestamp(now, "activation time");
379
+ const current = getMultiFrontierRun(collaborationId);
380
+ if (!current) return null;
381
+ const activated = activateMultiFrontierDriver(current, participantId);
382
+ return activated ? writeNextStoredRun(current, activated, now) : null;
383
+ }
384
+
385
+ /** Persist an explicit driver reactivation after the human-approved resume. */
386
+ export function reactivateStoredMultiFrontierDriver(
387
+ collaborationId: string,
388
+ participantId: string,
389
+ now = new Date().toISOString(),
390
+ ): MultiFrontierStoredRun | null {
391
+ assertTimestamp(now, "reactivation time");
392
+ const current = getMultiFrontierRun(collaborationId);
393
+ if (!current) return null;
394
+ const reactivated = reactivateMultiFrontierDriver(current, participantId);
395
+ return reactivated ? writeNextStoredRun(current, reactivated, now) : null;
396
+ }
397
+
398
+ /**
399
+ * Appends an idempotent participant event after the coordinator has fenced it.
400
+ * The event journal is diagnostic; state is mutated only through this guarded
401
+ * coordinator entry point.
402
+ */
403
+ export function appendMultiFrontierParticipantEvent(
404
+ input: AppendMultiFrontierParticipantEventInput,
405
+ ): AppendMultiFrontierParticipantEventResult {
406
+ assertSafeId(input.id, "event id");
407
+ assertSafeId(input.collaborationId, "collaboration id");
408
+ assertSafeId(input.participantId, "participant id");
409
+ const createdAt = input.createdAt ?? new Date().toISOString();
410
+ assertTimestamp(createdAt, "event time");
411
+
412
+ const current = getMultiFrontierRun(input.collaborationId);
413
+ if (!current) return { accepted: false, reason: "missing-run", run: null };
414
+ const existing = listMultiFrontierParticipantEvents(
415
+ input.collaborationId,
416
+ ).find((event) => event.id === input.id);
417
+ if (existing) {
418
+ if (!isMatchingParticipantEvent(existing, input)) {
419
+ return { accepted: false, reason: "event-conflict", run: current };
420
+ }
421
+ return {
422
+ accepted: true,
423
+ deduplicated: true,
424
+ event: existing,
425
+ run: current,
426
+ };
427
+ }
428
+ if (
429
+ !current.participants.some(
430
+ (candidate) => candidate.participantId === input.participantId,
431
+ )
432
+ ) {
433
+ return { accepted: false, reason: "missing-participant", run: current };
434
+ }
435
+ if (!canApplyMultiFrontierParticipantEvent(current, input)) {
436
+ return { accepted: false, reason: "stale-driver", run: current };
437
+ }
438
+ const participant = current.participants.find(
439
+ (candidate) => candidate.participantId === input.participantId,
440
+ );
441
+ if (
442
+ input.status !== undefined &&
443
+ (participant?.status === "completed" || participant?.status === "failed") &&
444
+ input.status !== participant.status
445
+ ) {
446
+ return { accepted: false, reason: "terminal-participant", run: current };
447
+ }
448
+
449
+ const event: PersistedMultiFrontierParticipantEvent = {
450
+ schemaVersion: 1,
451
+ id: input.id,
452
+ collaborationId: input.collaborationId,
453
+ participantId: input.participantId,
454
+ permission: input.permission,
455
+ createdAt,
456
+ ...(input.generation === undefined ? {} : { generation: input.generation }),
457
+ ...(input.status === undefined ? {} : { status: input.status }),
458
+ };
459
+ appendParticipantEvent(event);
460
+ const participants = input.status
461
+ ? current.participants.map((candidate) =>
462
+ candidate.participantId === input.participantId
463
+ ? { ...candidate, status: input.status }
464
+ : candidate,
465
+ )
466
+ : current.participants;
467
+ const run = writeNextStoredRun(
468
+ current,
469
+ { ...current, participants },
470
+ createdAt,
471
+ );
472
+ return { accepted: true, deduplicated: false, event, run };
473
+ }
474
+
475
+ export function listMultiFrontierParticipantEvents(
476
+ collaborationId: string,
477
+ ): PersistedMultiFrontierParticipantEvent[] {
478
+ if (!SAFE_ID.test(collaborationId)) return [];
479
+ const eventPath = multiFrontierParticipantEventsPath(collaborationId);
480
+ if (!fs.existsSync(eventPath)) return [];
481
+ return fs
482
+ .readFileSync(eventPath, "utf-8")
483
+ .split(/\r?\n/)
484
+ .filter(Boolean)
485
+ .map(readParticipantEvent)
486
+ .filter(
487
+ (event): event is PersistedMultiFrontierParticipantEvent =>
488
+ event !== null,
489
+ );
490
+ }
491
+
492
+ function writeMultiFrontierRun(run: MultiFrontierStoredRun): void {
493
+ fs.mkdirSync(multiFrontierRunsDir(), { recursive: true });
494
+ writeFileAtomically(
495
+ multiFrontierRunPath(run.collaborationId),
496
+ `${JSON.stringify(run, null, 2)}\n`,
497
+ );
498
+ }
499
+
500
+ function writeNewMultiFrontierRun(run: MultiFrontierStoredRun): void {
501
+ fs.mkdirSync(multiFrontierRunsDir(), { recursive: true });
502
+ try {
503
+ fs.writeFileSync(
504
+ multiFrontierRunPath(run.collaborationId),
505
+ `${JSON.stringify(run, null, 2)}\n`,
506
+ { encoding: "utf-8", flag: "wx" },
507
+ );
508
+ } catch (error) {
509
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") {
510
+ throw new Error(
511
+ `Multi-frontier run already exists: ${run.collaborationId}`,
512
+ );
513
+ }
514
+ throw error;
515
+ }
516
+ }
517
+
518
+ function writeNextStoredRun(
519
+ current: MultiFrontierStoredRun,
520
+ nextState: MultiFrontierRunState,
521
+ updatedAt: string,
522
+ ): MultiFrontierStoredRun {
523
+ const next: MultiFrontierStoredRun = {
524
+ ...nextState,
525
+ createdAt: current.createdAt,
526
+ updatedAt,
527
+ };
528
+ writeMultiFrontierRun(next);
529
+ return next;
530
+ }
531
+
532
+ function appendParticipantEvent(
533
+ event: PersistedMultiFrontierParticipantEvent,
534
+ ): void {
535
+ fs.mkdirSync(multiFrontierParticipantEventsDir(), { recursive: true });
536
+ fs.appendFileSync(
537
+ multiFrontierParticipantEventsPath(event.collaborationId),
538
+ `${JSON.stringify(event)}\n`,
539
+ );
540
+ }
541
+
542
+ function multiFrontierRunPath(collaborationId: string): string {
543
+ return path.join(multiFrontierRunsDir(), `${collaborationId}.json`);
544
+ }
545
+
546
+ function multiFrontierParticipantEventsPath(collaborationId: string): string {
547
+ return path.join(
548
+ multiFrontierParticipantEventsDir(),
549
+ `${collaborationId}.jsonl`,
550
+ );
551
+ }
552
+
553
+ function readStoredRun(filePath: string): MultiFrontierStoredRun | null {
554
+ try {
555
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
556
+ if (!raw || typeof raw !== "object") return null;
557
+ const run = raw as Partial<MultiFrontierStoredRun>;
558
+ if (
559
+ run.schemaVersion !== 1 ||
560
+ typeof run.collaborationId !== "string" ||
561
+ !SAFE_ID.test(run.collaborationId) ||
562
+ !isPhase(run.phase) ||
563
+ !isStoredRunParticipantsAndDriverValid(run.participants, run.driver) ||
564
+ !isApproval(run.approval) ||
565
+ !Array.isArray(run.checkpointIds) ||
566
+ !run.checkpointIds.every(
567
+ (checkpointId) => typeof checkpointId === "string",
568
+ ) ||
569
+ typeof run.round !== "number" ||
570
+ !Number.isInteger(run.round) ||
571
+ run.round < 1 ||
572
+ !isStringArray(run.proposalIds) ||
573
+ !isStringArray(run.reviewIds) ||
574
+ (run.recovery !== undefined && !isRecovery(run.recovery)) ||
575
+ typeof run.createdAt !== "string" ||
576
+ Number.isNaN(Date.parse(run.createdAt)) ||
577
+ typeof run.updatedAt !== "string" ||
578
+ Number.isNaN(Date.parse(run.updatedAt))
579
+ ) {
580
+ return null;
581
+ }
582
+ return run as MultiFrontierStoredRun;
583
+ } catch {
584
+ return null;
585
+ }
586
+ }
587
+
588
+ function readParticipantEvent(
589
+ line: string,
590
+ ): PersistedMultiFrontierParticipantEvent | null {
591
+ try {
592
+ const raw = JSON.parse(line) as unknown;
593
+ if (!raw || typeof raw !== "object") return null;
594
+ const event = raw as Partial<PersistedMultiFrontierParticipantEvent>;
595
+ if (
596
+ event.schemaVersion !== 1 ||
597
+ typeof event.id !== "string" ||
598
+ typeof event.collaborationId !== "string" ||
599
+ typeof event.participantId !== "string" ||
600
+ !isPermission(event.permission) ||
601
+ (event.generation !== undefined &&
602
+ typeof event.generation !== "number") ||
603
+ (event.status !== undefined && !isParticipantStatus(event.status)) ||
604
+ typeof event.createdAt !== "string" ||
605
+ Number.isNaN(Date.parse(event.createdAt))
606
+ ) {
607
+ return null;
608
+ }
609
+ return event as PersistedMultiFrontierParticipantEvent;
610
+ } catch {
611
+ return null;
612
+ }
613
+ }
614
+
615
+ function assertParticipants(
616
+ participants: MultiFrontierParticipantState[],
617
+ ): void {
618
+ if (participants.length === 0) {
619
+ throw new Error("A multi-frontier run needs at least one participant.");
620
+ }
621
+ const participantIds = new Set<string>();
622
+ for (const participant of participants) {
623
+ if (!isParticipant(participant))
624
+ throw new Error("Invalid multi-frontier participant.");
625
+ assertSafeId(participant.participantId, "participant id");
626
+ if (participantIds.has(participant.participantId)) {
627
+ throw new Error(
628
+ `Duplicate multi-frontier participant id: ${participant.participantId}`,
629
+ );
630
+ }
631
+ participantIds.add(participant.participantId);
632
+ }
633
+ }
634
+
635
+ function isParticipant(value: unknown): value is MultiFrontierParticipantState {
636
+ if (!value || typeof value !== "object") return false;
637
+ const participant = value as Partial<MultiFrontierParticipantState>;
638
+ return (
639
+ typeof participant.participantId === "string" &&
640
+ SAFE_ID.test(participant.participantId) &&
641
+ typeof participant.provider === "string" &&
642
+ participant.provider.length > 0 &&
643
+ typeof participant.runtime === "string" &&
644
+ participant.runtime.length > 0 &&
645
+ (participant.model === undefined ||
646
+ typeof participant.model === "string") &&
647
+ (participant.capabilities === undefined ||
648
+ isStringArray(participant.capabilities)) &&
649
+ (participant.sessionRef === undefined ||
650
+ typeof participant.sessionRef === "string") &&
651
+ (participant.role === "driver" || participant.role === "watchdog") &&
652
+ isPermission(participant.permission) &&
653
+ isParticipantStatus(participant.status)
654
+ );
655
+ }
656
+
657
+ function areStoredParticipantsValid(
658
+ value: unknown,
659
+ ): value is MultiFrontierParticipantState[] {
660
+ if (
661
+ !Array.isArray(value) ||
662
+ value.length === 0 ||
663
+ !value.every(isParticipant)
664
+ ) {
665
+ return false;
666
+ }
667
+ return (
668
+ new Set(value.map((participant) => participant.participantId)).size ===
669
+ value.length
670
+ );
671
+ }
672
+
673
+ function isStoredRunParticipantsAndDriverValid(
674
+ participants: unknown,
675
+ driver: unknown,
676
+ ): participants is MultiFrontierParticipantState[] {
677
+ if (!areStoredParticipantsValid(participants) || !isDriver(driver)) {
678
+ return false;
679
+ }
680
+ if (driver?.leaseState !== "active") {
681
+ return participants.every(
682
+ (participant) => participant.permission === "read_only",
683
+ );
684
+ }
685
+ if (
686
+ !participants.some(
687
+ (participant) => participant.participantId === driver.participantId,
688
+ )
689
+ ) {
690
+ return false;
691
+ }
692
+ return participants.every((participant) => {
693
+ if (participant.participantId !== driver.participantId) {
694
+ return (
695
+ participant.role === "watchdog" &&
696
+ participant.permission === "read_only"
697
+ );
698
+ }
699
+ return (
700
+ participant.role === "driver" &&
701
+ participant.permission === "workspace_write"
702
+ );
703
+ });
704
+ }
705
+
706
+ function isDriver(value: unknown): value is MultiFrontierDriverLease | null {
707
+ if (value === null) return true;
708
+ if (!value || typeof value !== "object") return false;
709
+ const driver = value as Partial<MultiFrontierDriverLease>;
710
+ return (
711
+ typeof driver.participantId === "string" &&
712
+ typeof driver.generation === "number" &&
713
+ (driver.leaseState === "inactive" ||
714
+ driver.leaseState === "active" ||
715
+ driver.leaseState === "revoked")
716
+ );
717
+ }
718
+
719
+ function isApproval(value: unknown): value is MultiFrontierApproval {
720
+ if (!value || typeof value !== "object") return false;
721
+ const approval = value as Partial<MultiFrontierApproval>;
722
+ return (
723
+ (approval.state === "not_required" ||
724
+ approval.state === "pending" ||
725
+ approval.state === "approved" ||
726
+ approval.state === "rejected") &&
727
+ (approval.proposalId === undefined ||
728
+ typeof approval.proposalId === "string") &&
729
+ (approval.reviewPacketId === undefined ||
730
+ typeof approval.reviewPacketId === "string")
731
+ );
732
+ }
733
+
734
+ function isRecovery(value: unknown): value is MultiFrontierRecovery {
735
+ if (!value || typeof value !== "object") return false;
736
+ const recovery = value as Partial<MultiFrontierRecovery>;
737
+ return (
738
+ isRecoveryReason(recovery.reason) &&
739
+ typeof recovery.recoveredAt === "string" &&
740
+ !Number.isNaN(Date.parse(recovery.recoveredAt)) &&
741
+ isPhase(recovery.resumablePhase) &&
742
+ (recovery.checkpointId === undefined ||
743
+ typeof recovery.checkpointId === "string")
744
+ );
745
+ }
746
+
747
+ function isPhase(value: unknown): value is MultiFrontierPhase {
748
+ return (
749
+ typeof value === "string" &&
750
+ [
751
+ "proposing",
752
+ "cross_review",
753
+ "converging",
754
+ "awaiting_go",
755
+ "implementing",
756
+ "checkpoint_review",
757
+ "paused",
758
+ "completed",
759
+ "failed",
760
+ "canceled",
761
+ ].includes(value)
762
+ );
763
+ }
764
+
765
+ function isParticipantStatus(
766
+ value: unknown,
767
+ ): value is MultiFrontierParticipantStatus {
768
+ return (
769
+ value === "idle" ||
770
+ value === "running" ||
771
+ value === "waiting" ||
772
+ value === "failed" ||
773
+ value === "completed"
774
+ );
775
+ }
776
+
777
+ function isPermission(
778
+ value: unknown,
779
+ ): value is MultiFrontierParticipantPermission {
780
+ return value === "read_only" || value === "workspace_write";
781
+ }
782
+
783
+ function isMatchingParticipantEvent(
784
+ existing: PersistedMultiFrontierParticipantEvent,
785
+ input: AppendMultiFrontierParticipantEventInput,
786
+ ): boolean {
787
+ return (
788
+ existing.collaborationId === input.collaborationId &&
789
+ existing.participantId === input.participantId &&
790
+ existing.permission === input.permission &&
791
+ existing.generation === input.generation &&
792
+ existing.status === input.status &&
793
+ (input.createdAt === undefined || existing.createdAt === input.createdAt)
794
+ );
795
+ }
796
+
797
+ function isStringArray(value: unknown): value is string[] {
798
+ return (
799
+ Array.isArray(value) && value.every((item) => typeof item === "string")
800
+ );
801
+ }
802
+
803
+ function isRecoveryReason(
804
+ value: unknown,
805
+ ): value is MultiFrontierRecoveryReason {
806
+ return (
807
+ value === "main_process_restarted" ||
808
+ value === "driver_crashed" ||
809
+ value === "watchdog_crashed" ||
810
+ value === "app_quit" ||
811
+ value === "canceled"
812
+ );
813
+ }
814
+
815
+ function assertSafeId(value: string, label: string): void {
816
+ if (!SAFE_ID.test(value)) throw new Error(`Invalid multi-frontier ${label}.`);
817
+ }
818
+
819
+ function assertTimestamp(value: string, label: string): void {
820
+ if (Number.isNaN(Date.parse(value))) {
821
+ throw new Error(`Invalid multi-frontier ${label}.`);
822
+ }
823
+ }
824
+
825
+ function writeFileAtomically(filePath: string, contents: string): void {
826
+ const directory = path.dirname(filePath);
827
+ const temporaryPath = path.join(
828
+ directory,
829
+ `.${path.basename(filePath)}.tmp-${process.pid}-${crypto.randomUUID()}`,
830
+ );
831
+ try {
832
+ fs.writeFileSync(temporaryPath, contents, {
833
+ encoding: "utf-8",
834
+ flag: "wx",
835
+ });
836
+ fs.renameSync(temporaryPath, filePath);
837
+ } catch (error) {
838
+ fs.rmSync(temporaryPath, { force: true });
839
+ throw error;
840
+ }
841
+ }