@dotdrelle/wiki-manager 0.11.10 → 0.12.1

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 (85) hide show
  1. package/README.md +32 -7
  2. package/agents.docker-compose.yml +3 -0
  3. package/docker-compose.yml +1 -0
  4. package/package.json +3 -2
  5. package/src/activity/activityAggregator.js +109 -0
  6. package/src/activity/activityAggregator.test.js +60 -0
  7. package/src/activity/activityDeduplicator.js +50 -0
  8. package/src/activity/progressCalculator.js +61 -0
  9. package/src/activity/runSynthesis.js +15 -0
  10. package/src/agent/graph.js +101 -38
  11. package/src/agent/graph.test.js +75 -5
  12. package/src/cli/wiki-manager.js +22 -0
  13. package/src/contracts/schemas.js +249 -13
  14. package/src/contracts/schemas.test.js +147 -2
  15. package/src/core/activity.js +8 -5
  16. package/src/core/agentEvents.js +260 -23
  17. package/src/core/agentEvents.test.js +53 -0
  18. package/src/core/compose.js +6 -1
  19. package/src/core/dockerCompose.test.js +12 -0
  20. package/src/core/documentIntake.js +33 -38
  21. package/src/core/documentIntake.test.js +25 -0
  22. package/src/core/env.js +11 -1
  23. package/src/core/jobQueue.js +26 -2
  24. package/src/core/mcp.js +30 -3
  25. package/src/core/mcp.test.js +69 -1
  26. package/src/core/plan.js +46 -4
  27. package/src/core/plan.test.js +16 -1
  28. package/src/core/planPatch.js +64 -3
  29. package/src/core/planPatch.test.js +110 -1
  30. package/src/core/queueStore.test.js +21 -0
  31. package/src/core/runtimeLog.js +119 -0
  32. package/src/core/runtimeLog.test.js +84 -0
  33. package/src/core/wikirc.js +22 -0
  34. package/src/core/wikirc.test.js +49 -1
  35. package/src/core/workflow.js +14 -3
  36. package/src/graph/graphAggregator.js +5 -0
  37. package/src/graph/graphPatch.js +8 -0
  38. package/src/graph/graphSnapshot.js +14 -0
  39. package/src/graph/graphVisibilityPolicy.js +40 -0
  40. package/src/graph/runGraphProjector.js +87 -0
  41. package/src/graph/runGraphProjector.test.js +111 -0
  42. package/src/orchestrator/agentRegistry.js +172 -0
  43. package/src/orchestrator/agentRegistry.test.js +114 -0
  44. package/src/orchestrator/approvalPolicy.js +126 -0
  45. package/src/orchestrator/approvalPolicy.test.js +78 -0
  46. package/src/orchestrator/assignmentManager.js +80 -0
  47. package/src/orchestrator/attemptManager.js +168 -0
  48. package/src/orchestrator/attemptManager.test.js +160 -0
  49. package/src/orchestrator/budgetManager.js +127 -0
  50. package/src/orchestrator/capabilityRegistry.js +64 -0
  51. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  52. package/src/orchestrator/capabilityResolver.js +151 -0
  53. package/src/orchestrator/capabilityResolver.test.js +127 -0
  54. package/src/orchestrator/dependencyResolver.js +112 -0
  55. package/src/orchestrator/dispatcher.js +302 -0
  56. package/src/orchestrator/lockManager.js +51 -0
  57. package/src/orchestrator/planIntegrator.js +268 -0
  58. package/src/orchestrator/planIntegrator.test.js +213 -0
  59. package/src/orchestrator/planValidator.js +534 -0
  60. package/src/orchestrator/planValidator.test.js +262 -0
  61. package/src/orchestrator/resultAggregator.js +248 -0
  62. package/src/orchestrator/resultAggregator.test.js +211 -0
  63. package/src/orchestrator/scheduler.js +103 -0
  64. package/src/orchestrator/scheduler.test.js +138 -0
  65. package/src/runtime/approvals.js +130 -1
  66. package/src/runtime/controlMessages.js +43 -0
  67. package/src/runtime/controlMessages.test.js +21 -0
  68. package/src/runtime/donna-contract.test.js +296 -33
  69. package/src/runtime/recoveryManager.js +176 -0
  70. package/src/runtime/recoveryManager.test.js +162 -0
  71. package/src/runtime/runner.e2e.test.js +98 -12
  72. package/src/runtime/runner.js +261 -227
  73. package/src/runtime/runner.test.js +249 -452
  74. package/src/runtime/server.js +142 -24
  75. package/src/runtime/server.test.js +192 -62
  76. package/src/runtime/store.js +837 -2
  77. package/src/runtime/store.test.js +269 -28
  78. package/src/runtime/supervisor.js +33 -1
  79. package/src/runtime/supervisor.test.js +47 -1
  80. package/src/shell/RightPane.tsx +8 -5
  81. package/src/shell/repl.js +7 -11
  82. package/src/shell/repl.test.js +26 -5
  83. package/src/shell/tui.tsx +1 -0
  84. package/src/shell/useAgent.ts +5 -1
  85. package/src/shell/useSession.ts +75 -19
@@ -1,10 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { applyAgentProjectionToSession, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
5
5
  import { defaultRuntimeStateDir } from '../core/env.js';
6
6
  import { projectQueue } from '../core/jobQueue.js';
7
7
  import { projectWorkflow } from '../core/workflow.js';
8
+ import { createCapabilityRegistry } from '../orchestrator/capabilityRegistry.js';
8
9
 
9
10
  export { defaultRuntimeStateDir };
10
11
 
@@ -78,6 +79,130 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
78
79
  finished_at TEXT,
79
80
  updated_at TEXT NOT NULL
80
81
  );
82
+ CREATE TABLE IF NOT EXISTS agents (
83
+ instance_id TEXT PRIMARY KEY,
84
+ agent_type TEXT NOT NULL,
85
+ display_name TEXT,
86
+ contract_version TEXT NOT NULL,
87
+ health TEXT NOT NULL,
88
+ description TEXT NOT NULL,
89
+ first_seen_at TEXT,
90
+ last_seen_at TEXT
91
+ );
92
+ CREATE TABLE IF NOT EXISTS agent_capabilities (
93
+ instance_id TEXT NOT NULL,
94
+ capability_id TEXT NOT NULL,
95
+ version TEXT NOT NULL,
96
+ default_requires_approval INTEGER DEFAULT 0,
97
+ declaration TEXT NOT NULL,
98
+ PRIMARY KEY (instance_id, capability_id, version)
99
+ );
100
+ CREATE TABLE IF NOT EXISTS task_groups (
101
+ run_id TEXT NOT NULL,
102
+ id TEXT NOT NULL,
103
+ local_id TEXT,
104
+ label TEXT NOT NULL,
105
+ recommended_concurrency INTEGER,
106
+ progress_weight REAL,
107
+ declaration TEXT NOT NULL,
108
+ created_at TEXT,
109
+ updated_at TEXT,
110
+ PRIMARY KEY (run_id, id),
111
+ FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE
112
+ );
113
+ CREATE TABLE IF NOT EXISTS tasks (
114
+ run_id TEXT NOT NULL,
115
+ id TEXT NOT NULL,
116
+ local_id TEXT,
117
+ agent_instance_id TEXT,
118
+ label TEXT NOT NULL,
119
+ required_capability TEXT NOT NULL,
120
+ operation TEXT NOT NULL,
121
+ arguments TEXT NOT NULL,
122
+ group_id TEXT,
123
+ depends_on_group TEXT,
124
+ barrier INTEGER DEFAULT 0,
125
+ parallelizable INTEGER DEFAULT 0,
126
+ recommended_concurrency INTEGER,
127
+ input_refs TEXT NOT NULL,
128
+ expected_output_refs TEXT NOT NULL,
129
+ locks TEXT NOT NULL,
130
+ requires_approval INTEGER DEFAULT 0,
131
+ approval_class TEXT,
132
+ approval_summary TEXT,
133
+ idempotency_key TEXT,
134
+ progress_weight REAL NOT NULL,
135
+ priority REAL,
136
+ retry_policy TEXT,
137
+ declaration TEXT NOT NULL,
138
+ status TEXT NOT NULL,
139
+ created_at TEXT,
140
+ updated_at TEXT,
141
+ PRIMARY KEY (run_id, id),
142
+ FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE
143
+ );
144
+ CREATE TABLE IF NOT EXISTS task_dependencies (
145
+ run_id TEXT NOT NULL,
146
+ task_id TEXT NOT NULL,
147
+ depends_on_task_id TEXT NOT NULL,
148
+ created_at TEXT,
149
+ PRIMARY KEY (run_id, task_id, depends_on_task_id),
150
+ FOREIGN KEY (run_id, task_id) REFERENCES tasks(run_id, id) ON DELETE CASCADE
151
+ );
152
+ CREATE TABLE IF NOT EXISTS plan_revisions (
153
+ run_id TEXT NOT NULL,
154
+ revision INTEGER NOT NULL,
155
+ previous_revision INTEGER,
156
+ reason TEXT,
157
+ task_ids TEXT NOT NULL,
158
+ created_at TEXT,
159
+ PRIMARY KEY (run_id, revision),
160
+ FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE
161
+ );
162
+ CREATE TABLE IF NOT EXISTS task_assignments (
163
+ task_id TEXT NOT NULL,
164
+ attempt_id TEXT NOT NULL,
165
+ agent_instance_id TEXT NOT NULL,
166
+ agent_id TEXT,
167
+ pool_id TEXT,
168
+ assigned_at TEXT NOT NULL,
169
+ PRIMARY KEY (task_id, attempt_id)
170
+ );
171
+ CREATE TABLE IF NOT EXISTS task_attempts (
172
+ attempt_id TEXT PRIMARY KEY,
173
+ task_id TEXT NOT NULL,
174
+ run_id TEXT NOT NULL,
175
+ status TEXT NOT NULL,
176
+ job_id TEXT,
177
+ started_at TEXT,
178
+ finished_at TEXT,
179
+ error TEXT
180
+ );
181
+ CREATE TABLE IF NOT EXISTS task_results (
182
+ attempt_id TEXT PRIMARY KEY,
183
+ task_id TEXT NOT NULL,
184
+ status TEXT NOT NULL,
185
+ summary TEXT,
186
+ output_refs TEXT,
187
+ metrics TEXT,
188
+ error TEXT,
189
+ created_at TEXT
190
+ );
191
+ CREATE TABLE IF NOT EXISTS approval_grants (
192
+ id TEXT PRIMARY KEY,
193
+ run_id TEXT NOT NULL,
194
+ workspace_id TEXT,
195
+ plan_revision INTEGER,
196
+ scope TEXT NOT NULL,
197
+ task_id TEXT,
198
+ group_id TEXT,
199
+ approval_classes TEXT NOT NULL,
200
+ status TEXT NOT NULL,
201
+ reason TEXT,
202
+ created_at TEXT NOT NULL,
203
+ granted_at TEXT,
204
+ rejected_at TEXT
205
+ );
81
206
  `);
82
207
  ensureColumn(db, 'events', 'sequence', 'INTEGER');
83
208
  ensureColumn(db, 'events', 'workspace', 'TEXT');
@@ -86,6 +211,11 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
86
211
  backfillEventSequence(db);
87
212
  db.exec('CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace)');
88
213
  db.exec('CREATE INDEX IF NOT EXISTS idx_events_sequence ON events(sequence)');
214
+ db.exec('CREATE INDEX IF NOT EXISTS idx_tasks_run_status ON tasks(run_id, status)');
215
+ db.exec('CREATE INDEX IF NOT EXISTS idx_task_attempts_task ON task_attempts(task_id)');
216
+ db.exec('CREATE INDEX IF NOT EXISTS idx_task_attempts_run ON task_attempts(run_id)');
217
+ db.exec('CREATE INDEX IF NOT EXISTS idx_task_results_task ON task_results(task_id)');
218
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_grants_run_revision ON approval_grants(run_id, workspace_id, plan_revision, status)');
89
219
  db.exec(`PRAGMA user_version = ${RUNTIME_STORE_SCHEMA_VERSION}`);
90
220
  purgeOldTerminalRuns(db);
91
221
 
@@ -117,6 +247,10 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
117
247
  input = COALESCE(excluded.input, runs.input),
118
248
  updated_at = excluded.updated_at
119
249
  `);
250
+ const ensureRun = db.prepare(`
251
+ INSERT OR IGNORE INTO runs (id, workspace, status, input, created_at, updated_at)
252
+ VALUES (?, ?, 'running', NULL, ?, ?)
253
+ `);
120
254
  const listRunsStatement = db.prepare(`
121
255
  SELECT id, workspace, status, input, created_at, updated_at
122
256
  FROM runs
@@ -198,6 +332,191 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
198
332
  WHERE workspace IS NOT NULL AND status IN (${RECOVERABLE_QUEUE_STATUSES.map(() => '?').join(', ')})
199
333
  ORDER BY workspace
200
334
  `);
335
+ const upsertAgentStatement = db.prepare(`
336
+ INSERT INTO agents (
337
+ instance_id, agent_type, display_name, contract_version, health,
338
+ description, first_seen_at, last_seen_at
339
+ )
340
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
341
+ ON CONFLICT(instance_id) DO UPDATE SET
342
+ agent_type = excluded.agent_type,
343
+ display_name = excluded.display_name,
344
+ contract_version = excluded.contract_version,
345
+ health = excluded.health,
346
+ description = excluded.description,
347
+ first_seen_at = COALESCE(agents.first_seen_at, excluded.first_seen_at),
348
+ last_seen_at = excluded.last_seen_at
349
+ `);
350
+ const deleteAgentCapabilitiesStatement = db.prepare('DELETE FROM agent_capabilities WHERE instance_id = ?');
351
+ const upsertAgentCapabilityStatement = db.prepare(`
352
+ INSERT INTO agent_capabilities (
353
+ instance_id, capability_id, version, default_requires_approval, declaration
354
+ )
355
+ VALUES (?, ?, ?, ?, ?)
356
+ ON CONFLICT(instance_id, capability_id, version) DO UPDATE SET
357
+ default_requires_approval = excluded.default_requires_approval,
358
+ declaration = excluded.declaration
359
+ `);
360
+ const listAgentsStatement = db.prepare(`
361
+ SELECT instance_id, agent_type, display_name, contract_version, health,
362
+ description, first_seen_at, last_seen_at
363
+ FROM agents
364
+ ORDER BY instance_id ASC
365
+ `);
366
+ const upsertTaskGroupStatement = db.prepare(`
367
+ INSERT INTO task_groups (
368
+ run_id, id, local_id, label, recommended_concurrency, progress_weight,
369
+ declaration, created_at, updated_at
370
+ )
371
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
372
+ ON CONFLICT(run_id, id) DO UPDATE SET
373
+ label = excluded.label,
374
+ recommended_concurrency = excluded.recommended_concurrency,
375
+ progress_weight = excluded.progress_weight,
376
+ declaration = excluded.declaration,
377
+ updated_at = excluded.updated_at
378
+ `);
379
+ const upsertTaskStatement = db.prepare(`
380
+ INSERT INTO tasks (
381
+ run_id, id, local_id, agent_instance_id, label, required_capability,
382
+ operation, arguments, group_id, depends_on_group, barrier, parallelizable,
383
+ recommended_concurrency, input_refs, expected_output_refs, locks,
384
+ requires_approval, approval_class, approval_summary, idempotency_key,
385
+ progress_weight, priority, retry_policy, declaration, status, created_at,
386
+ updated_at
387
+ )
388
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
389
+ ON CONFLICT(run_id, id) DO UPDATE SET
390
+ agent_instance_id = excluded.agent_instance_id,
391
+ label = excluded.label,
392
+ required_capability = excluded.required_capability,
393
+ operation = excluded.operation,
394
+ arguments = excluded.arguments,
395
+ group_id = excluded.group_id,
396
+ depends_on_group = excluded.depends_on_group,
397
+ barrier = excluded.barrier,
398
+ parallelizable = excluded.parallelizable,
399
+ recommended_concurrency = excluded.recommended_concurrency,
400
+ input_refs = excluded.input_refs,
401
+ expected_output_refs = excluded.expected_output_refs,
402
+ locks = excluded.locks,
403
+ requires_approval = excluded.requires_approval,
404
+ approval_class = excluded.approval_class,
405
+ approval_summary = excluded.approval_summary,
406
+ idempotency_key = excluded.idempotency_key,
407
+ progress_weight = excluded.progress_weight,
408
+ priority = excluded.priority,
409
+ retry_policy = excluded.retry_policy,
410
+ declaration = excluded.declaration,
411
+ status = excluded.status,
412
+ updated_at = excluded.updated_at
413
+ `);
414
+ const deleteTaskDependenciesStatement = db.prepare('DELETE FROM task_dependencies WHERE run_id = ? AND task_id = ?');
415
+ const updateTaskStatusStatement = db.prepare('UPDATE tasks SET status = ?, declaration = ?, updated_at = ? WHERE run_id = ? AND id = ?');
416
+ const insertTaskDependencyStatement = db.prepare(`
417
+ INSERT OR IGNORE INTO task_dependencies (run_id, task_id, depends_on_task_id, created_at)
418
+ VALUES (?, ?, ?, ?)
419
+ `);
420
+ const insertPlanRevisionStatement = db.prepare(`
421
+ INSERT OR REPLACE INTO plan_revisions (
422
+ run_id, revision, previous_revision, reason, task_ids, created_at
423
+ )
424
+ VALUES (?, ?, ?, ?, ?, ?)
425
+ `);
426
+ const listTasksStatement = db.prepare(`
427
+ SELECT * FROM tasks ORDER BY run_id ASC, id ASC
428
+ `);
429
+ const listTasksByRunStatement = db.prepare(`
430
+ SELECT * FROM tasks WHERE run_id = ? ORDER BY id ASC
431
+ `);
432
+ const listTaskDependenciesStatement = db.prepare(`
433
+ SELECT * FROM task_dependencies ORDER BY run_id ASC, task_id ASC, depends_on_task_id ASC
434
+ `);
435
+ const listTaskDependenciesByRunStatement = db.prepare(`
436
+ SELECT * FROM task_dependencies WHERE run_id = ? ORDER BY task_id ASC, depends_on_task_id ASC
437
+ `);
438
+ const listPlanRevisionsStatement = db.prepare(`
439
+ SELECT * FROM plan_revisions ORDER BY run_id ASC, revision ASC
440
+ `);
441
+ const listPlanRevisionsByRunStatement = db.prepare(`
442
+ SELECT * FROM plan_revisions WHERE run_id = ? ORDER BY revision ASC
443
+ `);
444
+ const upsertTaskAssignmentStatement = db.prepare(`
445
+ INSERT INTO task_assignments (
446
+ task_id, attempt_id, agent_instance_id, agent_id, pool_id, assigned_at
447
+ )
448
+ VALUES (?, ?, ?, ?, ?, ?)
449
+ ON CONFLICT(task_id, attempt_id) DO UPDATE SET
450
+ agent_instance_id = excluded.agent_instance_id,
451
+ agent_id = excluded.agent_id,
452
+ pool_id = excluded.pool_id,
453
+ assigned_at = excluded.assigned_at
454
+ `);
455
+ const upsertTaskAttemptStatement = db.prepare(`
456
+ INSERT INTO task_attempts (
457
+ attempt_id, task_id, run_id, status, job_id, started_at, finished_at, error
458
+ )
459
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
460
+ ON CONFLICT(attempt_id) DO UPDATE SET
461
+ task_id = excluded.task_id,
462
+ run_id = excluded.run_id,
463
+ status = excluded.status,
464
+ job_id = COALESCE(excluded.job_id, task_attempts.job_id),
465
+ started_at = COALESCE(task_attempts.started_at, excluded.started_at),
466
+ finished_at = COALESCE(excluded.finished_at, task_attempts.finished_at),
467
+ error = COALESCE(excluded.error, task_attempts.error)
468
+ `);
469
+ const upsertTaskResultStatement = db.prepare(`
470
+ INSERT INTO task_results (
471
+ attempt_id, task_id, status, summary, output_refs, metrics, error, created_at
472
+ )
473
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
474
+ ON CONFLICT(attempt_id) DO UPDATE SET
475
+ task_id = excluded.task_id,
476
+ status = excluded.status,
477
+ summary = COALESCE(excluded.summary, task_results.summary),
478
+ output_refs = CASE WHEN excluded.output_refs = '[]' THEN task_results.output_refs ELSE excluded.output_refs END,
479
+ metrics = CASE WHEN excluded.metrics = '{}' THEN task_results.metrics ELSE excluded.metrics END,
480
+ error = COALESCE(excluded.error, task_results.error),
481
+ created_at = excluded.created_at
482
+ `);
483
+ const listTaskAssignmentsStatement = db.prepare(`
484
+ SELECT * FROM task_assignments WHERE task_id = ? ORDER BY assigned_at ASC, attempt_id ASC
485
+ `);
486
+ const listTaskAttemptsStatement = db.prepare(`
487
+ SELECT * FROM task_attempts WHERE task_id = ? ORDER BY COALESCE(started_at, finished_at, attempt_id) ASC, attempt_id ASC
488
+ `);
489
+ const latestTaskAttemptStatement = db.prepare(`
490
+ SELECT * FROM task_attempts WHERE task_id = ? ORDER BY COALESCE(started_at, finished_at, attempt_id) DESC, attempt_id DESC LIMIT 1
491
+ `);
492
+ const latestTaskResultStatement = db.prepare(`
493
+ SELECT * FROM task_results WHERE task_id = ? ORDER BY COALESCE(created_at, attempt_id) DESC, attempt_id DESC LIMIT 1
494
+ `);
495
+ const upsertApprovalGrantStatement = db.prepare(`
496
+ INSERT INTO approval_grants (
497
+ id, run_id, workspace_id, plan_revision, scope, task_id, group_id,
498
+ approval_classes, status, reason, created_at, granted_at, rejected_at
499
+ )
500
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
501
+ ON CONFLICT(id) DO UPDATE SET
502
+ run_id = excluded.run_id,
503
+ workspace_id = excluded.workspace_id,
504
+ plan_revision = excluded.plan_revision,
505
+ scope = excluded.scope,
506
+ task_id = excluded.task_id,
507
+ group_id = excluded.group_id,
508
+ approval_classes = excluded.approval_classes,
509
+ status = excluded.status,
510
+ reason = COALESCE(excluded.reason, approval_grants.reason),
511
+ granted_at = COALESCE(excluded.granted_at, approval_grants.granted_at),
512
+ rejected_at = COALESCE(excluded.rejected_at, approval_grants.rejected_at)
513
+ `);
514
+ const listApprovalGrantsStatement = db.prepare(`
515
+ SELECT * FROM approval_grants ORDER BY created_at ASC, id ASC
516
+ `);
517
+ const listApprovalGrantsByRunStatement = db.prepare(`
518
+ SELECT * FROM approval_grants WHERE run_id = ? ORDER BY created_at ASC, id ASC
519
+ `);
201
520
 
202
521
  function persistEvent(event) {
203
522
  if (NON_PERSISTED_EVENT_TYPES.has(event.type)) return event;
@@ -240,6 +559,10 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
240
559
  });
241
560
  }
242
561
  }
562
+ persistAgentFromEvent(event);
563
+ persistPlanFromEvent(event);
564
+ persistTaskLifecycleFromEvent(event);
565
+ persistApprovalGrantFromEvent(event);
243
566
  return event;
244
567
  }
245
568
 
@@ -341,6 +664,324 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
341
664
  }
342
665
  }
343
666
 
667
+ function persistAgent(agent) {
668
+ if (!agent?.agentInstanceId || !agent.description) return;
669
+ const description = agent.description;
670
+ const capabilities = Array.isArray(description.capabilities) ? description.capabilities : [];
671
+ const firstSeenAt = agent.firstSeenAt ?? agent.lastSeenAt ?? new Date().toISOString();
672
+ const lastSeenAt = agent.lastSeenAt ?? firstSeenAt;
673
+ db.exec('BEGIN');
674
+ try {
675
+ upsertAgentStatement.run(
676
+ agent.agentInstanceId,
677
+ description.agentType ?? agent.serverName ?? 'unknown',
678
+ description.displayName ?? agent.agentInstanceId,
679
+ description.contractVersion ?? 'legacy',
680
+ agent.health ?? description.health?.status ?? 'unavailable',
681
+ JSON.stringify(description),
682
+ firstSeenAt,
683
+ lastSeenAt,
684
+ );
685
+ deleteAgentCapabilitiesStatement.run(agent.agentInstanceId);
686
+ for (const capability of capabilities) {
687
+ upsertAgentCapabilityStatement.run(
688
+ agent.agentInstanceId,
689
+ capability.id,
690
+ capability.version,
691
+ capability.defaultRequiresApproval === true ? 1 : 0,
692
+ JSON.stringify(capability),
693
+ );
694
+ }
695
+ db.exec('COMMIT');
696
+ } catch (error) {
697
+ db.exec('ROLLBACK');
698
+ throw error;
699
+ }
700
+ }
701
+
702
+ function persistAgentFromEvent(event) {
703
+ if (event.type !== 'agent.registered' && event.type !== 'agent.health_changed') return;
704
+ persistAgent(event.payload?.agent);
705
+ }
706
+
707
+ function persistPlanFromEvent(event) {
708
+ if (!['task_group.created', 'task.created', 'plan.revision_changed'].includes(event.type)) return;
709
+ const runId = event.runId ?? event.payload?.runId ?? null;
710
+ if (!runId) return;
711
+ ensureRun.run(runId, event.workspace ?? event.payload?.workspace ?? null, event.ts, event.ts);
712
+ if (event.type === 'task_group.created') persistTaskGroup(runId, event.payload?.group, event.ts);
713
+ else if (event.type === 'task.created') persistTask(runId, event.payload?.task, event.ts);
714
+ else persistPlanRevision(runId, event.payload ?? {}, event.ts);
715
+ }
716
+
717
+ function persistTaskGroup(runId, group, ts) {
718
+ if (!group?.id) return;
719
+ upsertTaskGroupStatement.run(
720
+ runId,
721
+ group.id,
722
+ group.localId ?? null,
723
+ group.label ?? group.id,
724
+ group.recommendedConcurrency ?? null,
725
+ group.progressWeight ?? null,
726
+ JSON.stringify(group),
727
+ group.createdAt ?? ts,
728
+ group.updatedAt ?? ts,
729
+ );
730
+ }
731
+
732
+ function persistTask(runId, task, ts) {
733
+ if (!task?.id) return;
734
+ db.exec('BEGIN');
735
+ try {
736
+ upsertTaskStatement.run(
737
+ runId,
738
+ task.id,
739
+ task.localId ?? null,
740
+ task.agentInstanceId ?? null,
741
+ task.label ?? task.description ?? task.id,
742
+ task.requiredCapability ?? '',
743
+ task.operation ?? '',
744
+ JSON.stringify(task.arguments ?? {}),
745
+ task.groupId ?? null,
746
+ task.dependsOnGroup ?? null,
747
+ task.barrier === true ? 1 : 0,
748
+ task.parallelizable === true ? 1 : 0,
749
+ task.recommendedConcurrency ?? null,
750
+ JSON.stringify(task.inputRefs ?? []),
751
+ JSON.stringify(task.expectedOutputRefs ?? []),
752
+ JSON.stringify(task.locks ?? []),
753
+ task.requiresApproval === true ? 1 : 0,
754
+ task.approvalClass ?? null,
755
+ task.approvalSummary ?? null,
756
+ task.idempotencyKey ?? null,
757
+ task.progressWeight ?? 1,
758
+ task.priority ?? null,
759
+ task.retryPolicy == null ? null : JSON.stringify(task.retryPolicy),
760
+ JSON.stringify(task),
761
+ task.status ?? 'pending',
762
+ task.createdAt ?? ts,
763
+ task.updatedAt ?? ts,
764
+ );
765
+ deleteTaskDependenciesStatement.run(runId, task.id);
766
+ for (const dependencyId of task.dependsOn ?? []) {
767
+ insertTaskDependencyStatement.run(runId, task.id, dependencyId, ts);
768
+ }
769
+ db.exec('COMMIT');
770
+ } catch (error) {
771
+ db.exec('ROLLBACK');
772
+ throw error;
773
+ }
774
+ }
775
+
776
+ function persistPlanRevision(runId, payload, ts) {
777
+ insertPlanRevisionStatement.run(
778
+ runId,
779
+ Number(payload.planRevision ?? payload.revision ?? 0),
780
+ payload.previousRevision ?? null,
781
+ payload.reason ?? null,
782
+ JSON.stringify(payload.taskIds ?? []),
783
+ ts,
784
+ );
785
+ if (Array.isArray(payload.tasks)) {
786
+ for (const task of payload.tasks) persistTask(runId, task, ts);
787
+ }
788
+ }
789
+
790
+ function persistTaskLifecycleFromEvent(event) {
791
+ const taskId = lifecycleTaskId(event);
792
+ const runId = event.runId ?? event.payload?.runId ?? event.payload?.result?.runId ?? null;
793
+ if (!taskId || !runId) return;
794
+
795
+ if (event.type === 'plan_step_updated') {
796
+ const task = event.payload?.task;
797
+ const status = event.payload?.status ?? task?.status ?? null;
798
+ if (status) {
799
+ const existing = listTasksByRunStatement.all(runId).map(rowToTask).find((item) => item.id === taskId);
800
+ if (existing) {
801
+ updateTaskStatusStatement.run(
802
+ String(status),
803
+ JSON.stringify({ ...existing.declaration, ...task, status: String(status) }),
804
+ event.ts,
805
+ runId,
806
+ taskId,
807
+ );
808
+ }
809
+ }
810
+ return;
811
+ }
812
+
813
+ if (![
814
+ 'task.assigned',
815
+ 'task.started',
816
+ 'task.retry_scheduled',
817
+ 'task.result_returned',
818
+ 'task.completed',
819
+ 'task.failed',
820
+ ].includes(event.type)) return;
821
+ const attemptId = lifecycleAttemptId(event, taskId);
822
+
823
+ if (event.type === 'task.assigned') {
824
+ const assignment = event.payload?.assignment ?? {};
825
+ const agentInstanceId = event.payload?.agentInstanceId ?? assignment.agentInstanceId;
826
+ if (agentInstanceId) {
827
+ upsertTaskAssignmentStatement.run(
828
+ taskId,
829
+ attemptId,
830
+ String(agentInstanceId),
831
+ event.payload?.agentId ?? assignment.agentId ?? null,
832
+ event.payload?.poolId ?? assignment.poolId ?? null,
833
+ event.payload?.assignedAt ?? event.ts,
834
+ );
835
+ }
836
+ persistTaskAttempt({
837
+ attemptId,
838
+ taskId,
839
+ runId,
840
+ status: event.payload?.status ?? 'assigned',
841
+ startedAt: null,
842
+ finishedAt: null,
843
+ jobId: null,
844
+ error: null,
845
+ });
846
+ return;
847
+ }
848
+
849
+ if (event.type === 'task.started') {
850
+ persistTaskAttempt({
851
+ attemptId,
852
+ taskId,
853
+ runId,
854
+ status: event.payload?.status ?? 'running',
855
+ startedAt: event.payload?.startedAt ?? event.ts,
856
+ finishedAt: null,
857
+ jobId: event.payload?.jobId ?? event.payload?.result?.jobId ?? null,
858
+ error: null,
859
+ });
860
+ return;
861
+ }
862
+
863
+ if (event.type === 'task.retry_scheduled') {
864
+ persistTaskAttempt({
865
+ attemptId,
866
+ taskId,
867
+ runId,
868
+ status: event.payload?.status ?? 'retry_scheduled',
869
+ startedAt: null,
870
+ finishedAt: null,
871
+ jobId: event.payload?.jobId ?? null,
872
+ error: stringifyError(event.payload?.error ?? event.payload?.reason ?? null),
873
+ });
874
+ return;
875
+ }
876
+
877
+ const result = event.payload?.result ?? {};
878
+ const status = resultStatusForEvent(event, result);
879
+ persistTaskAttempt({
880
+ attemptId,
881
+ taskId,
882
+ runId,
883
+ status: attemptStatusForResult(status, event.type),
884
+ startedAt: null,
885
+ finishedAt: event.ts,
886
+ jobId: result.jobId ?? event.payload?.jobId ?? null,
887
+ error: stringifyError(result.error ?? event.payload?.error ?? null),
888
+ });
889
+ upsertTaskResultStatement.run(
890
+ attemptId,
891
+ taskId,
892
+ status,
893
+ result.summary ?? event.payload?.summary ?? null,
894
+ JSON.stringify(result.outputRefs ?? result.result?.outputRefs ?? []),
895
+ JSON.stringify(result.metrics ?? result.result?.metrics ?? {}),
896
+ stringifyError(result.error ?? event.payload?.error ?? null),
897
+ event.payload?.createdAt ?? event.ts,
898
+ );
899
+ }
900
+
901
+ function persistTaskAttempt({ attemptId, taskId, runId, status, jobId, startedAt, finishedAt, error }) {
902
+ upsertTaskAttemptStatement.run(
903
+ attemptId,
904
+ taskId,
905
+ runId,
906
+ status,
907
+ jobId,
908
+ startedAt,
909
+ finishedAt,
910
+ error,
911
+ );
912
+ }
913
+
914
+ function persistApprovalGrantFromEvent(event) {
915
+ if (!['approval.requested', 'approval.granted', 'approval.rejected'].includes(event.type)) return;
916
+ const payload = event.payload ?? {};
917
+ const runId = event.runId ?? payload.runId ?? null;
918
+ if (!runId) return;
919
+ const id = payload.id ?? payload.approvalId ?? payload.taskId ?? payload.itemId ?? payload.groupId ?? event.id;
920
+ const status = event.type === 'approval.requested'
921
+ ? 'pending_approval'
922
+ : event.type === 'approval.rejected'
923
+ ? 'rejected'
924
+ : 'approved';
925
+ upsertApprovalGrantStatement.run(
926
+ String(id),
927
+ String(runId),
928
+ event.workspace ?? payload.workspaceId ?? payload.workspace ?? null,
929
+ payload.planRevision == null ? null : Number(payload.planRevision),
930
+ String(payload.scope ?? 'run'),
931
+ payload.taskId ?? payload.itemId ?? event.taskId ?? null,
932
+ payload.groupId ?? null,
933
+ JSON.stringify(normalizeApprovalClasses(payload.approvalClasses ?? payload.approvalClass)),
934
+ status,
935
+ payload.reason ?? null,
936
+ payload.createdAt ?? event.ts,
937
+ status === 'approved' ? (payload.grantedAt ?? event.ts) : null,
938
+ status === 'rejected' ? (payload.rejectedAt ?? event.ts) : null,
939
+ );
940
+ }
941
+
942
+ function lifecycleTaskId(event) {
943
+ const taskId = event.taskId ?? event.payload?.taskId ?? event.payload?.result?.taskId;
944
+ return taskId == null ? null : String(taskId);
945
+ }
946
+
947
+ function lifecycleAttemptId(event, taskId) {
948
+ const attemptId = event.payload?.attemptId
949
+ ?? event.payload?.assignment?.attemptId
950
+ ?? event.payload?.result?.attemptId;
951
+ if (attemptId != null) return String(attemptId);
952
+ return latestTaskAttemptStatement.get(taskId)?.attempt_id ?? `${taskId}:attempt-1`;
953
+ }
954
+
955
+ function resultStatusForEvent(event, result) {
956
+ if (result?.status) return String(result.status);
957
+ if (event.type === 'task.completed') return 'succeeded';
958
+ if (event.type === 'task.failed') return 'failed';
959
+ return 'unknown';
960
+ }
961
+
962
+ function attemptStatusForResult(status, eventType) {
963
+ const normalized = String(status ?? '').toLowerCase();
964
+ if (['succeeded', 'success', 'done', 'complete', 'completed'].includes(normalized)) return 'done';
965
+ if (['cancelled', 'canceled'].includes(normalized)) return 'cancelled';
966
+ if (eventType === 'task.failed') return 'failed';
967
+ return normalized || 'finished';
968
+ }
969
+
970
+ function listAgents() {
971
+ return listAgentsStatement.all().map((row) => {
972
+ const description = row.description ? JSON.parse(row.description) : null;
973
+ return {
974
+ agentInstanceId: row.instance_id,
975
+ description,
976
+ health: row.health,
977
+ firstSeenAt: row.first_seen_at ?? null,
978
+ lastSeenAt: row.last_seen_at ?? null,
979
+ legacy: description?.contractVersion === 'legacy',
980
+ orchestrable: description?.contractVersion !== 'legacy',
981
+ };
982
+ });
983
+ }
984
+
344
985
  function listQueue({ workspace = null } = {}) {
345
986
  const rows = workspace
346
987
  ? listQueueByWorkspaceStatement.all(workspace)
@@ -364,6 +1005,54 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
364
1005
  }));
365
1006
  }
366
1007
 
1008
+ function listTasks({ runId = null } = {}) {
1009
+ const rows = runId ? listTasksByRunStatement.all(runId) : listTasksStatement.all();
1010
+ return rows.map(rowToTask);
1011
+ }
1012
+
1013
+ function listTaskAssignments({ taskId }) {
1014
+ if (!taskId) return [];
1015
+ return listTaskAssignmentsStatement.all(String(taskId)).map(rowToTaskAssignment);
1016
+ }
1017
+
1018
+ function listTaskAttempts({ taskId }) {
1019
+ if (!taskId) return [];
1020
+ return listTaskAttemptsStatement.all(String(taskId)).map(rowToTaskAttempt);
1021
+ }
1022
+
1023
+ function getTaskResult({ taskId }) {
1024
+ if (!taskId) return null;
1025
+ const row = latestTaskResultStatement.get(String(taskId));
1026
+ return row ? rowToTaskResult(row) : null;
1027
+ }
1028
+
1029
+ function listApprovalGrants({ runId = null } = {}) {
1030
+ const rows = runId ? listApprovalGrantsByRunStatement.all(String(runId)) : listApprovalGrantsStatement.all();
1031
+ return rows.map(rowToApprovalGrant);
1032
+ }
1033
+
1034
+ function listTaskDependencies({ runId = null } = {}) {
1035
+ const rows = runId ? listTaskDependenciesByRunStatement.all(runId) : listTaskDependenciesStatement.all();
1036
+ return rows.map((row) => ({
1037
+ runId: row.run_id,
1038
+ taskId: row.task_id,
1039
+ dependsOnTaskId: row.depends_on_task_id,
1040
+ createdAt: row.created_at ?? null,
1041
+ }));
1042
+ }
1043
+
1044
+ function listPlanRevisions({ runId = null } = {}) {
1045
+ const rows = runId ? listPlanRevisionsByRunStatement.all(runId) : listPlanRevisionsStatement.all();
1046
+ return rows.map((row) => ({
1047
+ runId: row.run_id,
1048
+ revision: row.revision,
1049
+ previousRevision: row.previous_revision ?? null,
1050
+ reason: row.reason ?? null,
1051
+ taskIds: row.task_ids ? JSON.parse(row.task_ids) : [],
1052
+ createdAt: row.created_at ?? null,
1053
+ }));
1054
+ }
1055
+
367
1056
  function replayEvents(session, { workspace = null } = {}) {
368
1057
  const events = listEvents({ workspace });
369
1058
  for (const event of events) {
@@ -388,10 +1077,17 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
388
1077
  const controlQueue = Array.isArray(projection.controlQueue)
389
1078
  ? projection.controlQueue.filter((item) => !workspace || item.workspace === workspace || !item.workspace).map((item) => ({ ...item }))
390
1079
  : [];
1080
+ const agents = mergeAgents(
1081
+ listAgents(),
1082
+ projection.agents ?? [],
1083
+ session?.agentRegistry?.snapshot?.() ?? [],
1084
+ );
391
1085
  const baseState = {
392
1086
  ...projection,
393
1087
  queue,
394
1088
  controlQueue,
1089
+ agents,
1090
+ capabilityRegistry: createCapabilityRegistry({ agents }).snapshot(),
395
1091
  eventsCursor: session?.agentEvents?.at(-1)?.sequence ?? events?.at(-1)?.sequence ?? lastEventSequence,
396
1092
  };
397
1093
  const runs = listRuns({ workspace });
@@ -427,6 +1123,15 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
427
1123
  interruptRuns,
428
1124
  saveQueue,
429
1125
  listQueue,
1126
+ listTasks,
1127
+ listTaskAssignments,
1128
+ listTaskAttempts,
1129
+ getTaskResult,
1130
+ listApprovalGrants,
1131
+ listTaskDependencies,
1132
+ listPlanRevisions,
1133
+ persistAgent,
1134
+ listAgents,
430
1135
  replayEvents,
431
1136
  hydrateSession,
432
1137
  getProjection,
@@ -436,7 +1141,7 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
436
1141
  }
437
1142
 
438
1143
  function defaultRuntimeMetaPath(stateDir) {
439
- return join(dirname(stateDir), '.wiki', 'meta.json');
1144
+ return join(stateDir, 'meta.json');
440
1145
  }
441
1146
 
442
1147
  function ensureKnownStoreVersion(db, dbPath) {
@@ -448,6 +1153,7 @@ function ensureKnownStoreVersion(db, dbPath) {
448
1153
  }
449
1154
 
450
1155
  function ensureRuntimeMeta(metaPath) {
1156
+ migrateLegacyRuntimeMeta(metaPath);
451
1157
  mkdirSync(dirname(metaPath), { recursive: true });
452
1158
  if (!existsSync(metaPath)) {
453
1159
  writeFileSync(metaPath, `${JSON.stringify({ schemaVersion: RUNTIME_STORE_SCHEMA_VERSION }, null, 2)}\n`, 'utf8');
@@ -468,6 +1174,14 @@ function ensureRuntimeMeta(metaPath) {
468
1174
  }
469
1175
  }
470
1176
 
1177
+ function migrateLegacyRuntimeMeta(metaPath) {
1178
+ if (existsSync(metaPath)) return;
1179
+ const legacyPath = join(dirname(metaPath), '..', '.wiki', 'meta.json');
1180
+ if (!existsSync(legacyPath)) return;
1181
+ mkdirSync(dirname(metaPath), { recursive: true });
1182
+ renameSync(legacyPath, metaPath);
1183
+ }
1184
+
471
1185
  function purgeOldTerminalRuns(db, now = new Date()) {
472
1186
  const cutoff = new Date(now.getTime() - RUNTIME_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
473
1187
  const placeholders = TERMINAL_RUN_STATUSES.map(() => '?').join(', ');
@@ -508,6 +1222,112 @@ function rowToEvent(row) {
508
1222
  };
509
1223
  }
510
1224
 
1225
+ function rowToTask(row) {
1226
+ return {
1227
+ runId: row.run_id,
1228
+ id: row.id,
1229
+ localId: row.local_id ?? null,
1230
+ agentInstanceId: row.agent_instance_id ?? null,
1231
+ label: row.label,
1232
+ requiredCapability: row.required_capability,
1233
+ operation: row.operation,
1234
+ arguments: row.arguments ? JSON.parse(row.arguments) : {},
1235
+ groupId: row.group_id ?? null,
1236
+ dependsOnGroup: row.depends_on_group ?? null,
1237
+ barrier: Boolean(row.barrier),
1238
+ parallelizable: Boolean(row.parallelizable),
1239
+ recommendedConcurrency: row.recommended_concurrency ?? null,
1240
+ inputRefs: row.input_refs ? JSON.parse(row.input_refs) : [],
1241
+ expectedOutputRefs: row.expected_output_refs ? JSON.parse(row.expected_output_refs) : [],
1242
+ locks: row.locks ? JSON.parse(row.locks) : [],
1243
+ requiresApproval: Boolean(row.requires_approval),
1244
+ approvalClass: row.approval_class ?? null,
1245
+ approvalSummary: row.approval_summary ?? null,
1246
+ idempotencyKey: row.idempotency_key ?? null,
1247
+ progressWeight: row.progress_weight,
1248
+ priority: row.priority ?? null,
1249
+ retryPolicy: row.retry_policy ? JSON.parse(row.retry_policy) : null,
1250
+ declaration: row.declaration ? JSON.parse(row.declaration) : null,
1251
+ status: row.status,
1252
+ createdAt: row.created_at ?? null,
1253
+ updatedAt: row.updated_at ?? null,
1254
+ };
1255
+ }
1256
+
1257
+ function rowToTaskAssignment(row) {
1258
+ return {
1259
+ taskId: row.task_id,
1260
+ attemptId: row.attempt_id,
1261
+ agentInstanceId: row.agent_instance_id,
1262
+ agentId: row.agent_id ?? null,
1263
+ poolId: row.pool_id ?? null,
1264
+ assignedAt: row.assigned_at,
1265
+ };
1266
+ }
1267
+
1268
+ function rowToTaskAttempt(row) {
1269
+ return {
1270
+ attemptId: row.attempt_id,
1271
+ taskId: row.task_id,
1272
+ runId: row.run_id,
1273
+ status: row.status,
1274
+ jobId: row.job_id ?? null,
1275
+ startedAt: row.started_at ?? null,
1276
+ finishedAt: row.finished_at ?? null,
1277
+ error: row.error ? parseJsonMaybe(row.error) : null,
1278
+ };
1279
+ }
1280
+
1281
+ function rowToTaskResult(row) {
1282
+ return {
1283
+ attemptId: row.attempt_id,
1284
+ taskId: row.task_id,
1285
+ status: row.status,
1286
+ summary: row.summary ?? null,
1287
+ outputRefs: row.output_refs ? JSON.parse(row.output_refs) : [],
1288
+ metrics: row.metrics ? JSON.parse(row.metrics) : {},
1289
+ error: row.error ? parseJsonMaybe(row.error) : null,
1290
+ createdAt: row.created_at ?? null,
1291
+ };
1292
+ }
1293
+
1294
+ function rowToApprovalGrant(row) {
1295
+ return {
1296
+ id: row.id,
1297
+ runId: row.run_id,
1298
+ workspaceId: row.workspace_id ?? null,
1299
+ planRevision: row.plan_revision ?? null,
1300
+ scope: row.scope,
1301
+ taskId: row.task_id ?? null,
1302
+ groupId: row.group_id ?? null,
1303
+ approvalClasses: row.approval_classes ? JSON.parse(row.approval_classes) : [],
1304
+ status: row.status,
1305
+ reason: row.reason ?? null,
1306
+ createdAt: row.created_at,
1307
+ grantedAt: row.granted_at ?? null,
1308
+ rejectedAt: row.rejected_at ?? null,
1309
+ };
1310
+ }
1311
+
1312
+ function normalizeApprovalClasses(value) {
1313
+ if (value == null) return [];
1314
+ const values = Array.isArray(value) ? value : [value];
1315
+ return [...new Set(values.map((item) => String(item).trim()).filter(Boolean))];
1316
+ }
1317
+
1318
+ function parseJsonMaybe(value) {
1319
+ try {
1320
+ return JSON.parse(value);
1321
+ } catch {
1322
+ return value;
1323
+ }
1324
+ }
1325
+
1326
+ function stringifyError(value) {
1327
+ if (value == null) return null;
1328
+ return typeof value === 'string' ? value : JSON.stringify(value);
1329
+ }
1330
+
511
1331
  function eventToAuditEntry(event) {
512
1332
  const payload = event.payload ?? {};
513
1333
  const activity = payload.activity ?? {};
@@ -538,6 +1358,21 @@ function auditSummary(event) {
538
1358
  return event.type;
539
1359
  }
540
1360
 
1361
+ function mergeAgents(...groups) {
1362
+ const byId = new Map();
1363
+ for (const agents of groups) {
1364
+ for (const agent of agents ?? []) {
1365
+ if (!agent?.agentInstanceId) continue;
1366
+ byId.set(agent.agentInstanceId, {
1367
+ ...(byId.get(agent.agentInstanceId) ?? {}),
1368
+ ...agent,
1369
+ description: agent.description ?? byId.get(agent.agentInstanceId)?.description ?? null,
1370
+ });
1371
+ }
1372
+ }
1373
+ return [...byId.values()].sort((a, b) => a.agentInstanceId.localeCompare(b.agentInstanceId));
1374
+ }
1375
+
541
1376
  function ensureColumn(db, table, column, definition) {
542
1377
  const existing = db.prepare(`PRAGMA table_info(${table})`).all();
543
1378
  if (existing.some((row) => row.name === column)) return;