@dotdrelle/wiki-manager 0.11.9 → 0.12.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 (81) 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 +65 -30
  11. package/src/agent/graph.test.js +28 -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 +145 -0
  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 +1 -1
  25. package/src/core/plan.js +46 -4
  26. package/src/core/plan.test.js +16 -1
  27. package/src/core/planPatch.js +64 -3
  28. package/src/core/planPatch.test.js +110 -1
  29. package/src/core/queueStore.test.js +21 -0
  30. package/src/core/runtimeLog.js +119 -0
  31. package/src/core/runtimeLog.test.js +84 -0
  32. package/src/core/wikirc.js +22 -0
  33. package/src/core/wikirc.test.js +49 -1
  34. package/src/core/workflow.js +14 -3
  35. package/src/graph/graphAggregator.js +5 -0
  36. package/src/graph/graphPatch.js +8 -0
  37. package/src/graph/graphSnapshot.js +14 -0
  38. package/src/graph/graphVisibilityPolicy.js +40 -0
  39. package/src/graph/runGraphProjector.js +87 -0
  40. package/src/graph/runGraphProjector.test.js +111 -0
  41. package/src/orchestrator/agentRegistry.js +172 -0
  42. package/src/orchestrator/agentRegistry.test.js +114 -0
  43. package/src/orchestrator/approvalPolicy.js +126 -0
  44. package/src/orchestrator/approvalPolicy.test.js +78 -0
  45. package/src/orchestrator/assignmentManager.js +80 -0
  46. package/src/orchestrator/attemptManager.js +168 -0
  47. package/src/orchestrator/attemptManager.test.js +160 -0
  48. package/src/orchestrator/budgetManager.js +127 -0
  49. package/src/orchestrator/capabilityRegistry.js +64 -0
  50. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  51. package/src/orchestrator/capabilityResolver.js +151 -0
  52. package/src/orchestrator/capabilityResolver.test.js +127 -0
  53. package/src/orchestrator/dependencyResolver.js +112 -0
  54. package/src/orchestrator/dispatcher.js +302 -0
  55. package/src/orchestrator/lockManager.js +51 -0
  56. package/src/orchestrator/planIntegrator.js +268 -0
  57. package/src/orchestrator/planIntegrator.test.js +213 -0
  58. package/src/orchestrator/planValidator.js +534 -0
  59. package/src/orchestrator/planValidator.test.js +262 -0
  60. package/src/orchestrator/resultAggregator.js +248 -0
  61. package/src/orchestrator/resultAggregator.test.js +211 -0
  62. package/src/orchestrator/scheduler.js +103 -0
  63. package/src/orchestrator/scheduler.test.js +138 -0
  64. package/src/runtime/approvals.js +130 -1
  65. package/src/runtime/donna-contract.test.js +293 -30
  66. package/src/runtime/recoveryManager.js +176 -0
  67. package/src/runtime/recoveryManager.test.js +162 -0
  68. package/src/runtime/runner.e2e.test.js +98 -12
  69. package/src/runtime/runner.js +261 -227
  70. package/src/runtime/runner.test.js +249 -452
  71. package/src/runtime/server.js +137 -20
  72. package/src/runtime/server.test.js +137 -7
  73. package/src/runtime/store.js +837 -2
  74. package/src/runtime/store.test.js +245 -4
  75. package/src/runtime/supervisor.js +33 -1
  76. package/src/runtime/supervisor.test.js +47 -1
  77. package/src/shell/RightPane.tsx +8 -5
  78. package/src/shell/repl.js +4 -9
  79. package/src/shell/repl.test.js +25 -4
  80. package/src/shell/tui.tsx +1 -0
  81. package/src/shell/useSession.ts +38 -10
@@ -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 });
@@ -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 });
@@ -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) {
@@ -59,6 +59,10 @@ test('applyRuntimeStateToShellSession projects runtime state into shell session'
59
59
 
60
60
  assert.equal(applied, true);
61
61
  assert.equal(session.agentProjection.status, 'running');
62
+ assert.deepEqual(session.agentProjection.conversation, [
63
+ { role: 'user', content: 'Build docs' },
64
+ { role: 'assistant', content: 'Working.' },
65
+ ]);
62
66
  assert.equal(session.headlessPlan[0].description, 'Build');
63
67
  assert.equal(session.activities['production:job-1'].status, 'running');
64
68
  assert.equal(session.productionActivity.jobId, 'job-1');
@@ -66,10 +70,7 @@ test('applyRuntimeStateToShellSession projects runtime state into shell session'
66
70
  assert.equal(session.workflow.nodes[0].id, 'task:build');
67
71
  assert.equal(session.workflow.relations[0].type, 'contains');
68
72
  assert.deepEqual(session.workflow.waitingReasons, ['queue:q-1']);
69
- assert.deepEqual(conversationMessages(session), [
70
- { role: 'user', content: 'Build docs' },
71
- { role: 'donna', content: 'Working.' },
72
- ]);
73
+ assert.deepEqual(conversationMessages(session), []);
73
74
  });
74
75
 
75
76
  test('submitRuntimeRun reports acceptance without throwing', async () => {
@@ -108,6 +109,26 @@ test('submitRuntimeRun routes busy runtime input through the control lane', asyn
108
109
  }
109
110
  });
110
111
 
112
+ test('submitRuntimeRun reports queued runs from non-blocking runtime', async () => {
113
+ const restore = stubFetch(async (url) => {
114
+ assert.equal(pathOf(url), '/run');
115
+ return jsonResponse(202, {
116
+ accepted: true,
117
+ queued: true,
118
+ kind: 'enqueue_run',
119
+ item: { id: 'control-1', status: 'queued' },
120
+ });
121
+ });
122
+ try {
123
+ const session = createSession();
124
+ const outcome = await submitRuntimeRun('build the doc', { runtime: { url: 'http://runtime.test' }, session });
125
+ assert.equal(outcome.kind, 'queued');
126
+ assert.equal(outcome.result.item.id, 'control-1');
127
+ } finally {
128
+ restore();
129
+ }
130
+ });
131
+
111
132
  test('submitRuntimeRun reports a non-409 error without throwing or calling /control', async () => {
112
133
  let controlCalled = false;
113
134
  const restore = stubFetch(async (url) => {
package/src/shell/tui.tsx CHANGED
@@ -336,6 +336,7 @@ function App(props: {
336
336
  queueItems={state.queueItems()}
337
337
  queueInfo={state.queueInfo()}
338
338
  activeTab={state.rightTab()}
339
+ logFilter={state.runtimeLogFilter()}
339
340
  onTabClick={state.selectRightTab}
340
341
  />
341
342
  <SlashDialog context={state.activeEditor() ? null : state.slash()} />