@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,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { DatabaseSync } from 'node:sqlite';
@@ -11,6 +11,26 @@ function runtimeStateDir() {
11
11
  return join(mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-')), '.wiki-manager');
12
12
  }
13
13
 
14
+ function plannedTask(id) {
15
+ return {
16
+ id,
17
+ label: 'Build A',
18
+ description: 'Build A',
19
+ requiredCapability: 'document.build',
20
+ operation: 'build',
21
+ arguments: { templates: ['templates/a.md'] },
22
+ dependsOn: [],
23
+ parallelizable: true,
24
+ inputRefs: [],
25
+ expectedOutputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
26
+ locks: ['deliverable:deliverables/a.md'],
27
+ requiresApproval: false,
28
+ idempotencyKey: 'test-key',
29
+ progressWeight: 1,
30
+ status: 'pending',
31
+ };
32
+ }
33
+
14
34
  test('runtime store persists and replays agent events into a projection', () => {
15
35
  const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
16
36
  const store = openRuntimeStore({ stateDir });
@@ -33,15 +53,184 @@ test('runtime store persists and replays agent events into a projection', () =>
33
53
  reopened.close();
34
54
  });
35
55
 
56
+ test('runtime store persists agent registry tables and exposes registry in state', () => {
57
+ const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
58
+ const store = openRuntimeStore({ stateDir });
59
+ const agent = {
60
+ agentInstanceId: 'production-main',
61
+ health: 'available',
62
+ firstSeenAt: '2026-01-01T00:00:00.000Z',
63
+ lastSeenAt: '2026-01-01T00:00:00.000Z',
64
+ description: {
65
+ contractVersion: '1',
66
+ agentType: 'production',
67
+ agentInstanceId: 'production-main',
68
+ displayName: 'Production',
69
+ capabilities: [{
70
+ id: 'knowledge.update',
71
+ version: '1',
72
+ description: 'Update wiki knowledge',
73
+ inputSchema: {},
74
+ outputSchema: {},
75
+ supportedOperations: ['ingest'],
76
+ defaultRequiresApproval: true,
77
+ }],
78
+ orchestration: {
79
+ canPlan: true,
80
+ canExpandPlan: false,
81
+ canExecute: true,
82
+ canCancel: true,
83
+ canResume: false,
84
+ supportsIdempotency: false,
85
+ supportsParallelWorkers: true,
86
+ },
87
+ limits: {
88
+ recommendedConcurrency: 1,
89
+ maxConcurrency: 2,
90
+ },
91
+ health: { status: 'available' },
92
+ },
93
+ };
94
+
95
+ store.persistEvent(createAgentEvent('agent.registered', {
96
+ origin: 'agent_registry',
97
+ payload: { agent },
98
+ }));
99
+
100
+ assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM agents').get().n, 1);
101
+ assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM agent_capabilities').get().n, 1);
102
+ assert.equal(store.listAgents()[0].agentInstanceId, 'production-main');
103
+ const state = store.getState();
104
+ assert.equal(state.agents[0].agentInstanceId, 'production-main');
105
+ assert.equal(state.capabilityRegistry['knowledge.update@1'][0].agentInstanceId, 'production-main');
106
+ store.close();
107
+ });
108
+
109
+ test('runtime store persists task assignments attempts and results from events', () => {
110
+ const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
111
+ const store = openRuntimeStore({ stateDir });
112
+ const runId = 'run-task-persistence';
113
+ const taskId = 'run-task-persistence:build-a';
114
+ const attemptId = 'attempt-1';
115
+
116
+ for (const event of [
117
+ createAgentEvent('run_started', {
118
+ origin: 'runtime',
119
+ runId,
120
+ workspace: 'docs',
121
+ payload: { input: 'build docs', workspace: 'docs' },
122
+ }),
123
+ createAgentEvent('task.created', {
124
+ origin: 'plan_integrator',
125
+ runId,
126
+ taskId,
127
+ workspace: 'docs',
128
+ payload: { runId, task: plannedTask(taskId) },
129
+ }),
130
+ createAgentEvent('task.assigned', {
131
+ origin: 'assignment_manager',
132
+ runId,
133
+ taskId,
134
+ workspace: 'docs',
135
+ payload: {
136
+ runId,
137
+ taskId,
138
+ attemptId,
139
+ assignment: {
140
+ attemptId,
141
+ agentInstanceId: 'production-main',
142
+ agentId: 'production',
143
+ poolId: 'production-pool',
144
+ },
145
+ },
146
+ }),
147
+ createAgentEvent('task.started', {
148
+ origin: 'dispatcher',
149
+ runId,
150
+ taskId,
151
+ workspace: 'docs',
152
+ payload: { runId, taskId, attemptId, jobId: 'job-1', startedAt: '2026-07-07T10:00:00.000Z' },
153
+ }),
154
+ createAgentEvent('task.result_returned', {
155
+ origin: 'result_aggregator',
156
+ runId,
157
+ taskId,
158
+ workspace: 'docs',
159
+ payload: {
160
+ runId,
161
+ taskId,
162
+ attemptId,
163
+ result: {
164
+ attemptId,
165
+ taskId,
166
+ jobId: 'job-1',
167
+ status: 'succeeded',
168
+ summary: 'Built A',
169
+ outputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
170
+ metrics: { durationMs: 42 },
171
+ },
172
+ },
173
+ }),
174
+ createAgentEvent('task.completed', {
175
+ origin: 'result_aggregator',
176
+ runId,
177
+ taskId,
178
+ workspace: 'docs',
179
+ payload: { runId, taskId, attemptId, result: { attemptId, taskId, status: 'succeeded' } },
180
+ }),
181
+ createAgentEvent('task.retry_scheduled', {
182
+ origin: 'attempt_manager',
183
+ runId,
184
+ taskId,
185
+ workspace: 'docs',
186
+ payload: {
187
+ runId,
188
+ taskId,
189
+ attemptId: 'attempt-2',
190
+ reason: 'retryable_error',
191
+ error: { code: 'rate_limit', retryable: true },
192
+ },
193
+ }),
194
+ ]) {
195
+ store.persistEvent(event);
196
+ }
197
+
198
+ assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM task_assignments').get().n, 1);
199
+ assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM task_attempts').get().n, 2);
200
+ assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM task_results').get().n, 1);
201
+ assert.equal(store.listTasks({ runId })[0].id, taskId);
202
+ assert.deepEqual(store.listTaskAssignments({ taskId })[0], {
203
+ taskId,
204
+ attemptId,
205
+ agentInstanceId: 'production-main',
206
+ agentId: 'production',
207
+ poolId: 'production-pool',
208
+ assignedAt: store.listTaskAssignments({ taskId })[0].assignedAt,
209
+ });
210
+ assert.deepEqual(store.listTaskAttempts({ taskId }).map((attempt) => attempt.status), ['done', 'retry_scheduled']);
211
+ assert.deepEqual(store.getTaskResult({ taskId }).outputRefs, [{ type: 'file', ref: 'deliverables/a.md' }]);
212
+ assert.equal(store.getTaskResult({ taskId }).metrics.durationMs, 42);
213
+ store.close();
214
+
215
+ const reopened = openRuntimeStore({ stateDir });
216
+ const session = { activities: {}, headlessPlan: null };
217
+ reopened.hydrateSession(session, { workspace: 'docs' });
218
+ assert.ok(reopened.getState(session, { workspace: 'docs' }).logs.some((line) => line === `Task assigned: ${taskId}`));
219
+ assert.equal(reopened.listTaskAttempts({ taskId })[0].jobId, 'job-1');
220
+ assert.equal(reopened.getTaskResult({ taskId }).status, 'succeeded');
221
+ reopened.close();
222
+ });
223
+
36
224
  test('runtime store writes schema versions to sqlite and meta file', () => {
37
225
  const root = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
38
226
  const stateDir = join(root, '.wiki-manager');
39
227
  const store = openRuntimeStore({ stateDir });
40
228
  const version = store.db.prepare('PRAGMA user_version').get().user_version;
41
- const meta = JSON.parse(readFileSync(join(root, '.wiki', 'meta.json'), 'utf8'));
229
+ const meta = JSON.parse(readFileSync(join(root, '.wiki-manager', 'meta.json'), 'utf8'));
42
230
 
43
231
  assert.equal(version, 1);
44
232
  assert.equal(meta.schemaVersion, 1);
233
+ assert.equal(existsSync(join(root, '.wiki', 'meta.json')), false);
45
234
  store.close();
46
235
  });
47
236
 
@@ -61,8 +250,8 @@ test('runtime store refuses unknown sqlite schema versions', () => {
61
250
  test('runtime store refuses unknown runtime metadata versions', () => {
62
251
  const root = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
63
252
  const stateDir = join(root, '.wiki-manager');
64
- mkdirSync(join(root, '.wiki'), { recursive: true });
65
- writeFileSync(join(root, '.wiki', 'meta.json'), '{"schemaVersion":99}\n', 'utf8');
253
+ mkdirSync(stateDir, { recursive: true });
254
+ writeFileSync(join(stateDir, 'meta.json'), '{"schemaVersion":99}\n', 'utf8');
66
255
 
67
256
  assert.throws(
68
257
  () => openRuntimeStore({ stateDir }),
@@ -70,6 +259,21 @@ test('runtime store refuses unknown runtime metadata versions', () => {
70
259
  );
71
260
  });
72
261
 
262
+ test('runtime store migrates legacy .wiki metadata to .wiki-manager', () => {
263
+ const root = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
264
+ const stateDir = join(root, '.wiki-manager');
265
+ mkdirSync(join(root, '.wiki'), { recursive: true });
266
+ writeFileSync(join(root, '.wiki', 'meta.json'), '{"schemaVersion":1,"legacy":true}\n', 'utf8');
267
+
268
+ const store = openRuntimeStore({ stateDir });
269
+ const meta = JSON.parse(readFileSync(join(stateDir, 'meta.json'), 'utf8'));
270
+
271
+ assert.equal(meta.schemaVersion, 1);
272
+ assert.equal(meta.legacy, true);
273
+ assert.equal(existsSync(join(root, '.wiki', 'meta.json')), false);
274
+ store.close();
275
+ });
276
+
73
277
  test('runtime store persists duplicate events idempotently', () => {
74
278
  const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
75
279
  const store = openRuntimeStore({ stateDir });
@@ -77,8 +281,8 @@ test('runtime store persists duplicate events idempotently', () => {
77
281
  origin: 'test',
78
282
  runId: 'run-1',
79
283
  turnId: 'run-1:turn-0',
80
- workspace: 'juno',
81
- payload: { input: 'build wiki', workspace: 'juno' },
284
+ workspace: 'acme',
285
+ payload: { input: 'build wiki', workspace: 'acme' },
82
286
  });
83
287
 
84
288
  store.persistEvent(event);
@@ -88,7 +292,7 @@ test('runtime store persists duplicate events idempotently', () => {
88
292
  assert.equal(store.listEvents()[0].sequence, 1);
89
293
  assert.equal(store.getState().eventsCursor, 1);
90
294
  assert.equal(store.listRuns()[0].id, 'run-1');
91
- assert.equal(store.listRuns()[0].workspace, 'juno');
295
+ assert.equal(store.listRuns()[0].workspace, 'acme');
92
296
  assert.equal(store.listRuns()[0].status, 'running');
93
297
  store.close();
94
298
  });
@@ -299,6 +503,43 @@ test('runtime store tracks pending approval run status', () => {
299
503
  store.close();
300
504
  });
301
505
 
506
+ test('runtime store persists bounded approval grants', () => {
507
+ const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
508
+ const store = openRuntimeStore({ stateDir });
509
+ store.persistEvent(createAgentEvent('approval.granted', {
510
+ origin: 'runtime',
511
+ runId: 'run-approval-grant',
512
+ workspace: 'docs',
513
+ payload: {
514
+ id: 'grant-1',
515
+ scope: 'run',
516
+ runId: 'run-approval-grant',
517
+ workspaceId: 'docs',
518
+ planRevision: 3,
519
+ approvalClasses: ['workspace-write'],
520
+ reason: 'valide tout',
521
+ },
522
+ }));
523
+
524
+ const grants = store.listApprovalGrants({ runId: 'run-approval-grant' });
525
+ assert.deepEqual(grants, [{
526
+ id: 'grant-1',
527
+ runId: 'run-approval-grant',
528
+ workspaceId: 'docs',
529
+ planRevision: 3,
530
+ scope: 'run',
531
+ taskId: null,
532
+ groupId: null,
533
+ approvalClasses: ['workspace-write'],
534
+ status: 'approved',
535
+ reason: 'valide tout',
536
+ createdAt: grants[0].createdAt,
537
+ grantedAt: grants[0].grantedAt,
538
+ rejectedAt: null,
539
+ }]);
540
+ store.close();
541
+ });
542
+
302
543
  test('runtime store orders events by durable sequence', () => {
303
544
  const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
304
545
  const store = openRuntimeStore({ stateDir });
@@ -349,19 +590,19 @@ test('runtime store persists cancelled run status', () => {
349
590
  origin: 'test',
350
591
  runId: 'run-1',
351
592
  turnId: 'run-1:turn-0',
352
- workspace: 'juno',
353
- payload: { input: 'build wiki', workspace: 'juno' },
593
+ workspace: 'acme',
594
+ payload: { input: 'build wiki', workspace: 'acme' },
354
595
  }));
355
596
  store.persistEvent(createAgentEvent('run_cancelled', {
356
597
  origin: 'test',
357
598
  runId: 'run-1',
358
599
  turnId: 'run-1:turn-1',
359
- workspace: 'juno',
360
- payload: { runId: 'run-1', workspace: 'juno' },
600
+ workspace: 'acme',
601
+ payload: { runId: 'run-1', workspace: 'acme' },
361
602
  }));
362
603
 
363
604
  assert.equal(store.listRuns()[0].status, 'cancelled');
364
- assert.equal(store.listRuns()[0].workspace, 'juno');
605
+ assert.equal(store.listRuns()[0].workspace, 'acme');
365
606
  store.close();
366
607
  });
367
608
 
@@ -483,10 +724,10 @@ test('runtime store filters events runs and queue by workspace', () => {
483
724
  const store = openRuntimeStore({ stateDir });
484
725
  store.persistEvent(createAgentEvent('run_started', {
485
726
  origin: 'test',
486
- runId: 'run-juno',
487
- turnId: 'run-juno:turn-0',
488
- workspace: 'juno',
489
- payload: { input: 'juno', workspace: 'juno' },
727
+ runId: 'run-acme',
728
+ turnId: 'run-acme:turn-0',
729
+ workspace: 'acme',
730
+ payload: { input: 'acme', workspace: 'acme' },
490
731
  }));
491
732
  store.persistEvent(createAgentEvent('run_started', {
492
733
  origin: 'test',
@@ -495,12 +736,12 @@ test('runtime store filters events runs and queue by workspace', () => {
495
736
  workspace: 'docs',
496
737
  payload: { input: 'docs', workspace: 'docs' },
497
738
  }));
498
- store.saveQueue([{ id: 'q-juno', workspace: 'juno', status: 'waiting' }], { workspace: 'juno' });
739
+ store.saveQueue([{ id: 'q-acme', workspace: 'acme', status: 'waiting' }], { workspace: 'acme' });
499
740
  store.saveQueue([{ id: 'q-docs', workspace: 'docs', status: 'waiting' }], { workspace: 'docs' });
500
741
 
501
- assert.deepEqual(store.listEvents({ workspace: 'juno' }).map((event) => event.runId), ['run-juno']);
742
+ assert.deepEqual(store.listEvents({ workspace: 'acme' }).map((event) => event.runId), ['run-acme']);
502
743
  assert.deepEqual(store.listRuns({ workspace: 'docs' }).map((run) => run.id), ['run-docs']);
503
- assert.deepEqual(store.listQueue({ workspace: 'juno' }).map((item) => item.id), ['q-juno']);
744
+ assert.deepEqual(store.listQueue({ workspace: 'acme' }).map((item) => item.id), ['q-acme']);
504
745
 
505
746
  const session = { activities: {}, headlessPlan: null };
506
747
  store.hydrateSession(session, { workspace: 'docs' });
@@ -553,8 +794,8 @@ test('runtime store persists and replays control queue events without mixing MCP
553
794
  }));
554
795
  store.persistEvent(createAgentEvent('control_enqueued', {
555
796
  origin: 'runtime',
556
- workspace: 'juno',
557
- payload: { id: 'control-juno', workspace: 'juno', input: 'other' },
797
+ workspace: 'acme',
798
+ payload: { id: 'control-acme', workspace: 'acme', input: 'other' },
558
799
  }));
559
800
  store.close();
560
801
 
@@ -684,10 +925,10 @@ test('runtime store identifies recoverable workspaces and interrupts stale runs'
684
925
  const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
685
926
  const store = openRuntimeStore({ stateDir });
686
927
  store.persistRun({
687
- id: 'run-juno',
688
- workspace: 'juno',
928
+ id: 'run-acme',
929
+ workspace: 'acme',
689
930
  status: 'running',
690
- input: 'build juno',
931
+ input: 'build acme',
691
932
  });
692
933
  store.persistRun({
693
934
  id: 'run-done',
@@ -697,9 +938,9 @@ test('runtime store identifies recoverable workspaces and interrupts stale runs'
697
938
  });
698
939
  store.saveQueue([{ id: 'q-docs', workspace: 'docs', status: 'waiting' }], { workspace: 'docs' });
699
940
 
700
- assert.deepEqual(store.listRecoverableWorkspaces(), ['docs', 'juno']);
701
- assert.deepEqual(store.listRecoverableRuns({ workspace: 'juno' }).map((run) => run.id), ['run-juno']);
702
- assert.equal(store.interruptRuns({ workspace: 'juno' }), 1);
703
- assert.equal(store.listRuns({ workspace: 'juno' })[0].status, 'interrupted');
941
+ assert.deepEqual(store.listRecoverableWorkspaces(), ['acme', 'docs']);
942
+ assert.deepEqual(store.listRecoverableRuns({ workspace: 'acme' }).map((run) => run.id), ['run-acme']);
943
+ assert.equal(store.interruptRuns({ workspace: 'acme' }), 1);
944
+ assert.equal(store.listRuns({ workspace: 'acme' })[0].status, 'interrupted');
704
945
  store.close();
705
946
  });
@@ -1,16 +1,22 @@
1
1
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
2
2
  import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
3
3
  import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
4
+ import { normalizeRuntimeLog } from '../core/runtimeLog.js';
4
5
  import { startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
6
+ import { createAgentRegistry } from '../orchestrator/agentRegistry.js';
5
7
 
6
8
  export function startActivitySupervisor(session, {
7
9
  intervalMs = 1000,
8
10
  queueIntervalMs = 10000,
11
+ agentRegistryIntervalMs = registryIntervalFromEnv(),
12
+ agentRegistry = null,
9
13
  callTool = callMcpTool,
10
14
  } = {}) {
11
15
  const pollBusy = new Set();
12
16
  let stopped = false;
13
17
  let runSignal = null;
18
+ const registry = agentRegistry ?? createAgentRegistry({ callTool });
19
+ session.agentRegistry ??= registry;
14
20
 
15
21
  const pollTimer = setInterval(() => {
16
22
  void pollActivitiesOnce(session, { pollBusy, callTool, signal: runSignal });
@@ -24,7 +30,14 @@ export function startActivitySupervisor(session, {
24
30
  }
25
31
  }, queueIntervalMs);
26
32
 
33
+ const agentRegistryTimer = agentRegistryIntervalMs > 0
34
+ ? setInterval(() => {
35
+ void discoverAgentsOnce(session, { registry, signal: runSignal });
36
+ }, agentRegistryIntervalMs)
37
+ : null;
38
+
27
39
  void pollActivitiesOnce(session, { pollBusy, callTool, signal: runSignal });
40
+ void discoverAgentsOnce(session, { registry, signal: runSignal });
28
41
 
29
42
  return {
30
43
  pollBusy,
@@ -36,11 +49,21 @@ export function startActivitySupervisor(session, {
36
49
  stopped = true;
37
50
  clearInterval(pollTimer);
38
51
  clearInterval(queueTimer);
52
+ if (agentRegistryTimer) clearInterval(agentRegistryTimer);
39
53
  pollBusy.clear();
40
54
  },
41
55
  };
42
56
  }
43
57
 
58
+ export async function discoverAgentsOnce(session, {
59
+ registry = session?.agentRegistry ?? createAgentRegistry(),
60
+ signal = null,
61
+ } = {}) {
62
+ if (!session) return [];
63
+ session.agentRegistry ??= registry;
64
+ return registry.discover(session, { signal });
65
+ }
66
+
44
67
  export async function pollActivitiesOnce(session, {
45
68
  pollBusy = new Set(),
46
69
  callTool = callMcpTool,
@@ -97,8 +120,17 @@ export async function pollActivitiesOnce(session, {
97
120
  }
98
121
 
99
122
  export function emitRuntimeLog(session, message) {
123
+ const payload = normalizeRuntimeLog(message, { session });
100
124
  dispatchAgentEvent(session, createAgentEvent('runtime_log', {
101
125
  origin: 'runtime',
102
- payload: { message },
126
+ runId: payload.runId ?? null,
127
+ taskId: payload.taskId ?? null,
128
+ workspace: payload.workspaceId ?? null,
129
+ payload,
103
130
  }));
104
131
  }
132
+
133
+ function registryIntervalFromEnv() {
134
+ const value = Number(process.env.WIKI_MANAGER_AGENT_REGISTRY_INTERVAL_MS);
135
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 60000;
136
+ }
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
4
- import { pollActivitiesOnce, startActivitySupervisor } from './supervisor.js';
4
+ import { discoverAgentsOnce, pollActivitiesOnce, startActivitySupervisor } from './supervisor.js';
5
5
 
6
6
  test('pollActivitiesOnce updates activity through the event reducer', async () => {
7
7
  const session = {
@@ -154,6 +154,52 @@ test('startActivitySupervisor passes the active run signal to background polls',
154
154
  }
155
155
  });
156
156
 
157
+ test('startActivitySupervisor periodically re-scans the agent registry', async () => {
158
+ const session = {
159
+ mcp: {},
160
+ activities: {},
161
+ headlessPlan: null,
162
+ jobQueue: [],
163
+ };
164
+ let discoveries = 0;
165
+ const registry = {
166
+ async discover() {
167
+ discoveries += 1;
168
+ return [];
169
+ },
170
+ snapshot() {
171
+ return [];
172
+ },
173
+ };
174
+
175
+ const supervisor = startActivitySupervisor(session, {
176
+ intervalMs: 1000,
177
+ queueIntervalMs: 1000,
178
+ agentRegistryIntervalMs: 10,
179
+ agentRegistry: registry,
180
+ });
181
+
182
+ try {
183
+ await waitFor(() => discoveries >= 2);
184
+ assert.equal(session.agentRegistry, registry);
185
+ } finally {
186
+ supervisor.stop();
187
+ }
188
+ });
189
+
190
+ test('discoverAgentsOnce uses the session registry and returns discovered agents', async () => {
191
+ const expected = [{ agentInstanceId: 'a' }];
192
+ const registry = {
193
+ async discover(session) {
194
+ assert.equal(session.workspace, 'docs');
195
+ return expected;
196
+ },
197
+ };
198
+ const session = { workspace: 'docs', agentRegistry: registry };
199
+
200
+ assert.equal(await discoverAgentsOnce(session), expected);
201
+ });
202
+
157
203
  test('pollActivitiesOnce fires _onActivitiesTerminal when last activity becomes terminal', async () => {
158
204
  let fired = false;
159
205
  const session = {
@@ -1,5 +1,6 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import { createMemo, Index, Show } from 'solid-js';
3
+ import { filterRuntimeLogs } from '../core/runtimeLog.js';
3
4
  import { fit } from './textFit';
4
5
 
5
6
  type PlanStep = { step: number; description: string; status: string };
@@ -182,9 +183,10 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
182
183
  );
183
184
  }
184
185
 
185
- export function LogPanel(props: { logs: string[]; width: number }) {
186
+ export function LogPanel(props: { logs: string[]; width: number; filter?: string }) {
186
187
  const lineWidth = () => Math.max(8, props.width - 2);
187
- const visibleLines = () => props.logs.slice(-12).flatMap((line) => wrapLine(line, lineWidth())).slice(-24);
188
+ const filteredLogs = () => filterRuntimeLogs(props.logs, props.filter ?? '');
189
+ const visibleLines = () => filteredLogs().flatMap((line) => wrapLine(line, lineWidth())).slice(-24);
188
190
  const logLineAt = (index: number) => visibleLines()[index] ?? '';
189
191
  return (
190
192
  <box flexGrow={1} flexDirection="column" padding={1}>
@@ -202,8 +204,8 @@ export function LogPanel(props: { logs: string[]; width: number }) {
202
204
  fallback={<text width={lineWidth()} fg="#AAB7C4" content={line()} />}
203
205
  >
204
206
  <box height={1} flexDirection="row" overflow="hidden">
205
- <text width={timeWidth()} fg="#89B4FA" content={fit(`${parts().time} `, timeWidth())} />
206
- <text width={messageWidth()} fg="#AAB7C4" content={fit(parts().message, messageWidth())} />
207
+ <text width={timeWidth()} fg="#89B4FA" content={`${parts().time} `} />
208
+ <text width={messageWidth()} fg="#AAB7C4" content={parts().message} />
207
209
  </box>
208
210
  </Show>
209
211
  );
@@ -284,6 +286,7 @@ export function RightPane(props: {
284
286
  queueItems: QueueItem[];
285
287
  queueInfo: QueueInfo;
286
288
  activeTab: 'plan' | 'queue';
289
+ logFilter?: string;
287
290
  onTabClick: (tab: 'plan' | 'queue') => void;
288
291
  }) {
289
292
  const planJobName = () => activityJobName(
@@ -302,7 +305,7 @@ export function RightPane(props: {
302
305
  )}>
303
306
  <QueuePanel width={props.width} items={props.queueItems} info={props.queueInfo} />
304
307
  </Show>
305
- <LogPanel width={props.width} logs={props.logs} />
308
+ <LogPanel width={props.width} logs={props.logs} filter={props.logFilter} />
306
309
  </box>
307
310
  );
308
311
  }
package/src/shell/repl.js CHANGED
@@ -797,14 +797,6 @@ export function applyRuntimeStateToShellSession(session, state) {
797
797
  updatedAt: production.updatedAt,
798
798
  };
799
799
  }
800
- const runtimeConversation = session.agentProjection.conversation;
801
- if (runtimeConversation.length > 0) {
802
- session.conversations ??= { [GLOBAL_CONVERSATION_KEY]: [] };
803
- session.conversations[conversationKey(session)] = runtimeConversation.map((message) => ({
804
- role: message.role === 'assistant' ? 'donna' : message.role,
805
- content: String(message.content ?? ''),
806
- }));
807
- }
808
800
  return true;
809
801
  }
810
802
 
@@ -814,7 +806,10 @@ export function applyRuntimeStateToShellSession(session, state) {
814
806
  export async function submitRuntimeRun(line, { runtime, session }) {
815
807
  const workspace = session.workspace ?? null;
816
808
  try {
817
- await postRuntimeRun(line, { url: runtime.url, workspace });
809
+ const result = await postRuntimeRun(line, { url: runtime.url, workspace });
810
+ if (result?.queued || result?.kind === 'enqueue_run' || result?.kind === 'enqueue') {
811
+ return { kind: 'queued', result };
812
+ }
818
813
  return { kind: 'accepted' };
819
814
  } catch (err) {
820
815
  if (err?.status !== 409) {
@@ -1585,14 +1580,15 @@ async function runTuiShell({ agent, packageJson, session, runtime = null }) {
1585
1580
  if (outcome.kind === 'accepted') {
1586
1581
  activityLines = [...activityLines, 'runtime: run accepted'].slice(-LOWER_DETAIL_ROWS);
1587
1582
  } else if (outcome.kind === 'queued') {
1588
- conversationMessages(session).push({ role: 'command', content: 'Runtime is busy — request added to the control queue, it will start automatically.' });
1583
+ // Server-localized acknowledgement (src/runtime/controlMessages.js).
1584
+ conversationMessages(session).push({ role: 'command', content: String(outcome.result?.explanation ?? 'Request added to the queue.') });
1589
1585
  activityLines = [...activityLines, 'runtime: control queued'].slice(-LOWER_DETAIL_ROWS);
1590
1586
  } else if (outcome.kind === 'observe' || outcome.kind === 'converse' || outcome.kind === 'mutate') {
1591
1587
  const explanation = outcome.result?.explanation ?? 'Runtime control message accepted.';
1592
1588
  conversationMessages(session).push({ role: 'command', content: explanation });
1593
1589
  activityLines = [...activityLines, `runtime: ${outcome.kind}`].slice(-LOWER_DETAIL_ROWS);
1594
1590
  } else if (outcome.kind === 'ambiguous') {
1595
- conversationMessages(session).push({ role: 'command', content: 'Runtime could not classify that message. Use /queue for a future run, or ask/status more explicitly.' });
1591
+ conversationMessages(session).push({ role: 'command', content: String(outcome.result?.explanation ?? 'Runtime could not classify that message. Use /queue for a future run, or ask/status more explicitly.') });
1596
1592
  activityLines = [...activityLines, 'runtime: ambiguous control'].slice(-LOWER_DETAIL_ROWS);
1597
1593
  } else {
1598
1594
  conversationMessages(session).push({ role: 'command', content: `Runtime error: ${outcome.message}` });