@dotdrelle/wiki-manager 0.7.3 → 0.9.3

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 (39) hide show
  1. package/.env.example +20 -0
  2. package/README.md +50 -1
  3. package/docker-compose.yml +1 -23
  4. package/mcp.endpoints.example.json +13 -0
  5. package/package.json +2 -2
  6. package/src/agent/graph.js +101 -15
  7. package/src/agent/graph.test.js +145 -0
  8. package/src/cli/wiki-manager.js +306 -53
  9. package/src/commands/slash.js +4 -24
  10. package/src/core/agentEvents.js +169 -4
  11. package/src/core/agentEvents.test.js +176 -4
  12. package/src/core/agentLoop.js +3 -0
  13. package/src/core/compose.js +1 -2
  14. package/src/core/dockerCompose.test.js +5 -5
  15. package/src/core/jobQueue.js +29 -12
  16. package/src/core/mcp.js +120 -10
  17. package/src/core/mcp.test.js +121 -1
  18. package/src/core/plan.js +33 -0
  19. package/src/core/queueStore.test.js +1 -0
  20. package/src/core/sessionConfig.js +24 -0
  21. package/src/core/wikiWorkspace.test.js +24 -0
  22. package/src/runtime/approvals.js +113 -0
  23. package/src/runtime/auth.test.js +8 -0
  24. package/src/runtime/client.js +52 -6
  25. package/src/runtime/lifecycle.js +27 -3
  26. package/src/runtime/queueStore.js +3 -3
  27. package/src/runtime/runner.js +340 -0
  28. package/src/runtime/runner.test.js +270 -0
  29. package/src/runtime/server.js +252 -33
  30. package/src/runtime/server.test.js +577 -0
  31. package/src/runtime/store.js +181 -39
  32. package/src/runtime/store.test.js +363 -4
  33. package/src/runtime/supervisor.js +6 -0
  34. package/src/runtime/supervisor.test.js +141 -0
  35. package/src/shell/RightPane.tsx +1 -1
  36. package/src/shell/repl.js +22 -6
  37. package/src/shell/useAgent.ts +1 -1
  38. package/src/shell/useSession.ts +10 -5
  39. package/wiki-workspace +198 -4
@@ -1,11 +1,20 @@
1
1
  import { normalizeActivity } from './activity.js';
2
- import { syncActivitiesToPlan } from './plan.js';
2
+ import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
3
3
 
4
4
  const SESSION_PROJECTION_EVENTS = new Set([
5
5
  'run_started',
6
6
  'plan_set',
7
7
  'plan_step_updated',
8
8
  'activity_upserted',
9
+ 'run_evaluated',
10
+ 'run_replanned',
11
+ 'run_pending_approval',
12
+ 'run_approved',
13
+ 'tool_pending_approval',
14
+ 'tool_approved',
15
+ 'control_enqueued',
16
+ 'control_started',
17
+ 'run_done',
9
18
  'run_error',
10
19
  'run_cancelled',
11
20
  'runtime_log',
@@ -19,9 +28,16 @@ const PLAN_MUTATING_EVENTS = new Set([
19
28
  'plan_set',
20
29
  'plan_step_updated',
21
30
  'activity_upserted',
31
+ 'run_done',
22
32
  ]);
23
33
 
24
- export function createAgentEvent(type, { origin = 'system', payload = {}, runId = null, turnId = null } = {}) {
34
+ export function createAgentEvent(type, {
35
+ origin = 'system',
36
+ payload = {},
37
+ runId = null,
38
+ turnId = null,
39
+ workspace = null,
40
+ } = {}) {
25
41
  return {
26
42
  id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
27
43
  ts: new Date().toISOString(),
@@ -29,12 +45,16 @@ export function createAgentEvent(type, { origin = 'system', payload = {}, runId
29
45
  origin,
30
46
  runId,
31
47
  turnId,
48
+ workspace,
32
49
  payload,
33
50
  };
34
51
  }
35
52
 
36
53
  export function dispatchAgentEvent(session, event) {
37
- const normalized = event.id && event.ts ? event : createAgentEvent(event.type, event);
54
+ const normalized = withSessionRunIdentity(
55
+ event.id && event.ts ? event : createAgentEvent(event.type, event),
56
+ session,
57
+ );
38
58
  const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
39
59
  const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
40
60
  session.agentEvents ??= [];
@@ -52,6 +72,17 @@ export function dispatchAgentEvent(session, event) {
52
72
  return normalized;
53
73
  }
54
74
 
75
+ function withSessionRunIdentity(event, session) {
76
+ const identity = session?._currentRunIdentity;
77
+ if (!identity) return event;
78
+ return {
79
+ ...event,
80
+ runId: event.runId ?? identity.runId ?? null,
81
+ turnId: event.turnId ?? identity.turnId ?? null,
82
+ workspace: event.workspace ?? identity.workspace ?? null,
83
+ };
84
+ }
85
+
55
86
  export function reduceAgentEvents(events = []) {
56
87
  const state = createProjectionState();
57
88
 
@@ -69,6 +100,10 @@ function createProjectionState() {
69
100
  plan: null,
70
101
  activities: {},
71
102
  logs: [],
103
+ evaluation: null,
104
+ replans: [],
105
+ approvals: [],
106
+ controlQueue: [],
72
107
  summary: null,
73
108
  status: 'idle',
74
109
  };
@@ -81,6 +116,10 @@ function publicProjection(state) {
81
116
  plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
82
117
  activities: sortedActivities(state.activities).map((activity) => ({ ...activity })),
83
118
  logs: [...state.logs],
119
+ evaluation: state.evaluation ? { ...state.evaluation } : null,
120
+ replans: state.replans.map((replan) => ({ ...replan, plan: [...(replan.plan ?? [])] })),
121
+ approvals: state.approvals.map((approval) => ({ ...approval })),
122
+ controlQueue: state.controlQueue.map((item) => ({ ...item })),
84
123
  summary: state.summary,
85
124
  status: state.status,
86
125
  };
@@ -89,6 +128,7 @@ function publicProjection(state) {
89
128
  export function applyAgentProjectionToSession(session, projection) {
90
129
  session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
91
130
  session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
131
+ session.controlQueue = (projection.controlQueue ?? []).map((item) => ({ ...item }));
92
132
  const production = (projection.activities ?? []).filter((activity) => activity.source === 'production').at(-1);
93
133
  session.productionActivity = production ? {
94
134
  jobId: production.id,
@@ -103,10 +143,13 @@ function applyEvent(state, event) {
103
143
  switch (event.type) {
104
144
  case 'run_started':
105
145
  state.status = 'running';
106
- state.plan = null;
146
+ state.plan = defaultRunPlan();
107
147
  state.chain = [];
108
148
  state.activities = {};
109
149
  state.logs = [];
150
+ state.evaluation = null;
151
+ state.replans = [];
152
+ state.approvals = [];
110
153
  state.summary = null;
111
154
  return;
112
155
  case 'user_message':
@@ -144,16 +187,80 @@ function applyEvent(state, event) {
144
187
  state.summary = String(event.payload?.content ?? '');
145
188
  if (state.summary) state.conversation.push({ role: 'assistant', content: state.summary });
146
189
  return;
190
+ case 'run_evaluated':
191
+ state.evaluation = {
192
+ ok: event.payload?.ok === true,
193
+ reason: String(event.payload?.reason ?? ''),
194
+ suggestedAction: event.payload?.suggestedAction ?? null,
195
+ runId: event.runId ?? event.payload?.runId ?? null,
196
+ };
197
+ return;
198
+ case 'run_replanned':
199
+ state.replans.push({
200
+ reason: String(event.payload?.reason ?? ''),
201
+ plan: Array.isArray(event.payload?.plan) ? event.payload.plan.map(String) : [],
202
+ replansLeft: Number(event.payload?.replansLeft ?? 0),
203
+ runId: event.runId ?? event.payload?.runId ?? null,
204
+ });
205
+ return;
206
+ case 'run_pending_approval':
207
+ case 'tool_pending_approval':
208
+ upsertApproval(state, {
209
+ id: event.payload?.approvalId ?? event.payload?.runId ?? event.payload?.itemId ?? event.id,
210
+ scope: event.type === 'run_pending_approval' ? 'run' : 'tool',
211
+ status: 'pending_approval',
212
+ runId: event.runId ?? event.payload?.runId ?? null,
213
+ itemId: event.payload?.itemId ?? null,
214
+ reason: event.payload?.reason ?? null,
215
+ tool: event.payload?.tool ?? null,
216
+ plan: event.payload?.plan ?? null,
217
+ createdAt: event.ts,
218
+ });
219
+ return;
220
+ case 'run_approved':
221
+ case 'tool_approved':
222
+ upsertApproval(state, {
223
+ id: event.payload?.approvalId ?? event.payload?.runId ?? event.payload?.itemId ?? event.id,
224
+ scope: event.type === 'run_approved' ? 'run' : 'tool',
225
+ status: 'approved',
226
+ runId: event.runId ?? event.payload?.runId ?? null,
227
+ itemId: event.payload?.itemId ?? null,
228
+ approvedAt: event.ts,
229
+ });
230
+ return;
147
231
  case 'run_done':
148
232
  state.status = 'done';
233
+ finishPendingPlanSteps(state.plan);
234
+ finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'done', event.ts);
149
235
  return;
150
236
  case 'run_cancelled':
151
237
  state.status = 'cancelled';
152
238
  state.logs.push(String(event.payload?.message ?? 'Agent run cancelled.'));
239
+ finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'cancelled', event.ts);
153
240
  return;
154
241
  case 'run_error':
155
242
  state.status = 'error';
156
243
  state.logs.push(String(event.payload?.message ?? 'Agent run failed.'));
244
+ finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'failed', event.ts);
245
+ return;
246
+ case 'control_enqueued':
247
+ upsertControlItem(state.controlQueue, {
248
+ id: event.payload?.id ?? event.id,
249
+ workspace: event.workspace ?? event.payload?.workspace ?? null,
250
+ input: String(event.payload?.input ?? ''),
251
+ status: 'queued',
252
+ createdAt: event.payload?.createdAt ?? event.ts,
253
+ updatedAt: event.ts,
254
+ });
255
+ return;
256
+ case 'control_started':
257
+ upsertControlItem(state.controlQueue, {
258
+ id: event.payload?.id ?? null,
259
+ runId: event.runId ?? event.payload?.runId ?? null,
260
+ status: 'running',
261
+ startedAt: event.payload?.startedAt ?? event.ts,
262
+ updatedAt: event.ts,
263
+ });
157
264
  return;
158
265
  case 'runtime_log':
159
266
  state.logs.push(String(event.payload?.message ?? ''));
@@ -164,6 +271,32 @@ function applyEvent(state, event) {
164
271
  }
165
272
  }
166
273
 
274
+ function upsertById(list, next) {
275
+ const index = list.findIndex((item) => item.id === next.id);
276
+ if (index === -1) {
277
+ list.push(next);
278
+ return;
279
+ }
280
+ list[index] = {
281
+ ...list[index],
282
+ ...next,
283
+ };
284
+ }
285
+
286
+ function upsertControlItem(queue, next) {
287
+ if (!next.id) return;
288
+ upsertById(queue, next);
289
+ }
290
+
291
+ function finishControlByRun(queue, runId, status, finishedAt) {
292
+ if (!runId) return;
293
+ const item = queue.find((entry) => entry.runId === runId && entry.status === 'running');
294
+ if (!item) return;
295
+ item.status = status;
296
+ item.finishedAt = finishedAt;
297
+ item.updatedAt = finishedAt;
298
+ }
299
+
167
300
  function appendAssistantDelta(state, delta) {
168
301
  if (!delta) return;
169
302
  const last = state.conversation.at(-1);
@@ -198,6 +331,18 @@ function finishToolCall(state, payload = {}) {
198
331
  if (!existing) state.chain.push(step);
199
332
  }
200
333
 
334
+ function upsertApproval(state, next) {
335
+ upsertById(state.approvals, next);
336
+ }
337
+
338
+ function finishPendingPlanSteps(plan) {
339
+ for (const step of plan ?? []) {
340
+ if (step.status === 'running' || step.status === 'pending') {
341
+ step.status = 'done';
342
+ }
343
+ }
344
+ }
345
+
201
346
  function upsertActivity(state, rawActivity) {
202
347
  const activity = normalizeActivity(rawActivity);
203
348
  if (!activity) return;
@@ -211,6 +356,10 @@ function upsertActivity(state, rawActivity) {
211
356
 
212
357
  function ensurePlanFromActivityProjection(state, activity) {
213
358
  const actKey = activity.key ?? null;
359
+ if (state.plan?.some((step) => step.owner === 'orchestrator')) {
360
+ attachActivityToExistingPlan(state.plan, activity);
361
+ return;
362
+ }
214
363
  if (state.plan && actKey !== null && state.plan[0]?._activityKey === actKey) return;
215
364
  const steps = activity.plan?.steps;
216
365
  if (Array.isArray(steps) && steps.length > 0) {
@@ -219,6 +368,8 @@ function ensurePlanFromActivityProjection(state, activity) {
219
368
  id: step.id ?? null,
220
369
  description: step.label,
221
370
  status: 'pending',
371
+ owner: 'activity',
372
+ ownerActivityKey: activity.key,
222
373
  _activityKey: activity.key,
223
374
  }));
224
375
  return;
@@ -228,12 +379,16 @@ function ensurePlanFromActivityProjection(state, activity) {
228
379
  id: null,
229
380
  description: activity.label,
230
381
  status: 'pending',
382
+ owner: 'activity',
383
+ ownerActivityKey: activity.key,
231
384
  _activityKey: activity.key,
232
385
  }];
233
386
  }
234
387
 
235
388
  function normalizePlan(steps, payload = {}) {
236
389
  if (!Array.isArray(steps)) return null;
390
+ const owner = payload.owner ?? 'orchestrator';
391
+ const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
237
392
  return steps.map((raw, i) => {
238
393
  const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
239
394
  return {
@@ -241,11 +396,21 @@ function normalizePlan(steps, payload = {}) {
241
396
  id: item.id ?? null,
242
397
  description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
243
398
  status: item.status ?? 'pending',
399
+ owner: item.owner ?? owner,
400
+ ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
244
401
  _activityKey: item._activityKey ?? payload.activityKey ?? null,
245
402
  };
246
403
  });
247
404
  }
248
405
 
406
+ function defaultRunPlan() {
407
+ return [
408
+ { step: 1, id: 'analyze', description: 'Analyze the request', status: 'running', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
409
+ { step: 2, id: 'execute', description: 'Execute the required actions', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
410
+ { step: 3, id: 'verify', description: 'Verify the result', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
411
+ ];
412
+ }
413
+
249
414
  function updatePlanStep(plan, payload) {
250
415
  if (!plan) return;
251
416
  const step = plan.find((item) => item.step === Number(payload.step));
@@ -22,7 +22,9 @@ test('reduceAgentEvents: run_started clears stale plan', () => {
22
22
  }),
23
23
  createAgentEvent('run_started', { origin: 'user' }),
24
24
  ]);
25
- assert.equal(projection.plan, null);
25
+ assert.equal(projection.plan.length, 3);
26
+ assert.equal(projection.plan[0].description, 'Analyze the request');
27
+ assert.equal(projection.plan[0].owner, 'orchestrator');
26
28
  assert.equal(projection.activities.length, 0);
27
29
  assert.equal(projection.status, 'running');
28
30
  });
@@ -67,7 +69,7 @@ test('reduceAgentEvents: activity with plan creates visible plan and progress',
67
69
  assert.equal(projection.plan[1].status, 'pending');
68
70
  });
69
71
 
70
- test('reduceAgentEvents: real activity replaces minimal MCP plan', () => {
72
+ test('reduceAgentEvents: activity attaches to orchestrator plan without replacing it', () => {
71
73
  const projection = reduceAgentEvents([
72
74
  createAgentEvent('plan_set', {
73
75
  origin: 'tool',
@@ -87,8 +89,178 @@ test('reduceAgentEvents: real activity replaces minimal MCP plan', () => {
87
89
  }),
88
90
  ]);
89
91
  assert.equal(projection.plan.length, 1);
90
- assert.equal(projection.plan[0].description, 'CME export');
91
- assert.equal(projection.plan[0]._activityKey, 'cme:export-1');
92
+ assert.equal(projection.plan[0].description, 'cme.cme_export_run');
93
+ assert.equal(projection.plan[0].owner, 'orchestrator');
94
+ assert.equal(projection.plan[0].activityKey, 'cme:export-1');
95
+ assert.equal(projection.plan[0].ownerActivityKey, 'cme:export-1');
96
+ });
97
+
98
+ test('reduceAgentEvents: run activity attaches to execution step of default plan', () => {
99
+ const projection = reduceAgentEvents([
100
+ createAgentEvent('run_started', { origin: 'runtime' }),
101
+ createAgentEvent('activity_upserted', {
102
+ origin: 'tool',
103
+ payload: {
104
+ activity: {
105
+ key: 'production:build-1',
106
+ id: 'build-1',
107
+ source: 'production',
108
+ label: 'Production build',
109
+ status: 'running',
110
+ },
111
+ },
112
+ }),
113
+ ]);
114
+
115
+ assert.equal(projection.plan[0].status, 'done');
116
+ assert.equal(projection.plan[1].status, 'running');
117
+ assert.equal(projection.plan[1].activityKey, 'production:build-1');
118
+ assert.equal(projection.plan[2].status, 'pending');
119
+ });
120
+
121
+ test('reduceAgentEvents: run_done finalizes running and pending plan steps', () => {
122
+ const projection = reduceAgentEvents([
123
+ createAgentEvent('run_started', { origin: 'runtime' }),
124
+ createAgentEvent('run_done', { origin: 'runtime' }),
125
+ ]);
126
+
127
+ assert.deepEqual(projection.plan.map((step) => step.status), ['done', 'done', 'done']);
128
+ assert.equal(projection.status, 'done');
129
+ });
130
+
131
+ test('dispatchAgentEvent: run_done finalizes session plan', () => {
132
+ const session = {};
133
+ dispatchAgentEvent(session, createAgentEvent('run_started', { origin: 'runtime' }));
134
+ dispatchAgentEvent(session, createAgentEvent('run_done', { origin: 'runtime' }));
135
+
136
+ assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done', 'done']);
137
+ assert.equal(session.agentProjection.status, 'done');
138
+ });
139
+
140
+ test('reduceAgentEvents: run_evaluated exposes evaluator verdict', () => {
141
+ const projection = reduceAgentEvents([
142
+ createAgentEvent('run_started', { origin: 'runtime', runId: 'run-1' }),
143
+ createAgentEvent('run_evaluated', {
144
+ origin: 'runtime',
145
+ runId: 'run-1',
146
+ payload: {
147
+ ok: false,
148
+ reason: 'Missing export.',
149
+ suggestedAction: 'Run export step.',
150
+ },
151
+ }),
152
+ ]);
153
+
154
+ assert.deepEqual(projection.evaluation, {
155
+ ok: false,
156
+ reason: 'Missing export.',
157
+ suggestedAction: 'Run export step.',
158
+ runId: 'run-1',
159
+ });
160
+ assert.equal(projection.status, 'running');
161
+ });
162
+
163
+ test('reduceAgentEvents: run_replanned records replan trace', () => {
164
+ const projection = reduceAgentEvents([
165
+ createAgentEvent('run_started', { origin: 'runtime', runId: 'run-1' }),
166
+ createAgentEvent('run_replanned', {
167
+ origin: 'runtime',
168
+ runId: 'run-1',
169
+ payload: {
170
+ reason: 'Export file missing.',
171
+ plan: ['Run export again'],
172
+ replansLeft: 1,
173
+ },
174
+ }),
175
+ ]);
176
+
177
+ assert.deepEqual(projection.replans, [{
178
+ reason: 'Export file missing.',
179
+ plan: ['Run export again'],
180
+ replansLeft: 1,
181
+ runId: 'run-1',
182
+ }]);
183
+ });
184
+
185
+ test('reduceAgentEvents: approvals move from pending to approved', () => {
186
+ const projection = reduceAgentEvents([
187
+ createAgentEvent('run_pending_approval', {
188
+ origin: 'runtime',
189
+ runId: 'run-1',
190
+ payload: {
191
+ approvalId: 'approval-1',
192
+ runId: 'run-1',
193
+ reason: 'Approve plan.',
194
+ plan: ['Build'],
195
+ },
196
+ }),
197
+ createAgentEvent('run_approved', {
198
+ origin: 'runtime',
199
+ runId: 'run-1',
200
+ payload: {
201
+ approvalId: 'approval-1',
202
+ runId: 'run-1',
203
+ },
204
+ }),
205
+ ]);
206
+
207
+ assert.equal(projection.approvals.length, 1);
208
+ assert.equal(projection.approvals[0].status, 'approved');
209
+ assert.equal(projection.approvals[0].scope, 'run');
210
+ assert.deepEqual(projection.approvals[0].plan, ['Build']);
211
+ });
212
+
213
+ test('reduceAgentEvents: control queue is event sourced and follows run status', () => {
214
+ const projection = reduceAgentEvents([
215
+ createAgentEvent('control_enqueued', {
216
+ origin: 'runtime',
217
+ workspace: 'docs',
218
+ payload: {
219
+ id: 'control-1',
220
+ workspace: 'docs',
221
+ input: 'Run after current task',
222
+ createdAt: '2026-01-01T00:00:00.000Z',
223
+ },
224
+ }),
225
+ createAgentEvent('control_started', {
226
+ origin: 'runtime',
227
+ runId: 'run-control-1',
228
+ workspace: 'docs',
229
+ payload: { id: 'control-1', runId: 'run-control-1' },
230
+ }),
231
+ createAgentEvent('run_done', {
232
+ origin: 'runtime',
233
+ runId: 'run-control-1',
234
+ workspace: 'docs',
235
+ }),
236
+ ]);
237
+
238
+ assert.equal(projection.controlQueue.length, 1);
239
+ assert.equal(projection.controlQueue[0].id, 'control-1');
240
+ assert.equal(projection.controlQueue[0].status, 'done');
241
+ assert.equal(projection.controlQueue[0].runId, 'run-control-1');
242
+ });
243
+
244
+ test('reduceAgentEvents: activity-owned plan is used when no orchestrator plan exists', () => {
245
+ const projection = reduceAgentEvents([
246
+ createAgentEvent('activity_upserted', {
247
+ origin: 'tool',
248
+ payload: {
249
+ activity: {
250
+ key: 'production:job-1',
251
+ id: 'job-1',
252
+ source: 'production',
253
+ label: 'Production build',
254
+ status: 'running',
255
+ },
256
+ },
257
+ }),
258
+ ]);
259
+
260
+ assert.equal(projection.plan.length, 1);
261
+ assert.equal(projection.plan[0].description, 'Production build');
262
+ assert.equal(projection.plan[0].owner, 'activity');
263
+ assert.equal(projection.plan[0].ownerActivityKey, 'production:job-1');
92
264
  });
93
265
 
94
266
  test('dispatchAgentEvent: writes compatibility projections to session', () => {
@@ -72,6 +72,9 @@ export async function runAgenticLoop(agent, session, initialInput, {
72
72
 
73
73
  for (let turn = 1; turn <= maxTurns; turn += 1) {
74
74
  throwIfAborted(signal, abortMessage);
75
+ if (session._currentRunIdentity && runId) {
76
+ session._currentRunIdentity.turnId = `${runId}:turn-${turn}`;
77
+ }
75
78
  onTurnStart?.({ turn, maxTurns });
76
79
 
77
80
  const snapshot = activitySnapshot(session);
@@ -9,7 +9,7 @@ import { managerRoot } from './workspaces.js';
9
9
 
10
10
  const execFileAsync = promisify(execFile);
11
11
 
12
- export const COMPOSE_SERVICES = ['serve', 'mcp-http', 'agent-runtime', 'production-mcp'];
12
+ export const COMPOSE_SERVICES = ['serve', 'mcp-http', 'production-mcp'];
13
13
  const SERVICE_DESCRIPTION_LABEL = 'wiki-manager.description';
14
14
 
15
15
  const DEFAULT_SERVICE_ALIASES = {
@@ -17,7 +17,6 @@ const DEFAULT_SERVICE_ALIASES = {
17
17
  ui: ['serve'],
18
18
  wiki: ['mcp-http'],
19
19
  mcp: ['mcp-http'],
20
- runtime: ['agent-runtime'],
21
20
  production: ['production-mcp'],
22
21
  };
23
22
 
@@ -3,12 +3,12 @@ import { test } from 'node:test';
3
3
  import assert from 'node:assert/strict';
4
4
  import YAML from 'yaml';
5
5
 
6
- test('agent-runtime compose command passes runtime args to the image entrypoint', async () => {
6
+ test('workspace compose does not start a per-workspace agent runtime', async () => {
7
7
  const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
8
8
  const compose = YAML.parse(raw);
9
- const service = compose.services['agent-runtime'];
9
+ const aliases = compose['x-wiki-manager']['service-aliases'];
10
10
 
11
- assert.equal(service.image, 'dotdrelle/llm-wiki-manager:latest');
12
- assert.equal(service.command, 'runtime --host 0.0.0.0 --port 7788 --state-dir /state');
13
- assert.ok(!service.command.startsWith('wiki-manager '));
11
+ assert.equal(compose.services['agent-runtime'], undefined);
12
+ assert.deepEqual(aliases.all.targets, ['serve', 'mcp-http', 'production-mcp']);
13
+ assert.equal(aliases.runtime, undefined);
14
14
  });
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { extractActivity, parseJsonText, sessionActivities } from './activity.js';
2
3
  import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
3
4
  import { callMcpTool, formatMcpToolResult } from './mcp.js';
@@ -14,7 +15,7 @@ function notifyQueueUpdate(session) {
14
15
  }
15
16
 
16
17
  function shortId() {
17
- return `q-${Math.random().toString(16).slice(2, 6)}`;
18
+ return `q-${randomUUID()}`;
18
19
  }
19
20
 
20
21
  function terminalStatus(status) {
@@ -61,17 +62,33 @@ export function queueSummary(args = {}) {
61
62
  }
62
63
 
63
64
  export function queueCounts(session) {
64
- let active = 0;
65
- let current = 0;
66
- let frozen = 0;
67
- for (const item of ensureJobQueue(session)) {
68
- if (['waiting', 'starting', 'running'].includes(item.status)) {
69
- active += 1;
70
- if (item.workspace === session.workspace) current += 1;
71
- else frozen += 1;
72
- }
73
- }
74
- return { active, current, frozen };
65
+ const queue = projectQueue(session.headlessPlan, ensureJobQueue(session), { workspace: session.workspace ?? null });
66
+ return {
67
+ active: queue.length,
68
+ current: queue.filter((item) => item.workspace === session.workspace || !item.workspace).length,
69
+ frozen: queue.filter((item) => item.workspace && item.workspace !== session.workspace).length,
70
+ };
71
+ }
72
+
73
+ export function projectQueue(plan, queue, { workspace = null } = {}) {
74
+ const blockedJobs = (queue ?? [])
75
+ .filter((item) => ['waiting', 'blocked', 'pending_approval'].includes(String(item.status ?? '').toLowerCase()))
76
+ .map((item) => ({
77
+ ...item,
78
+ queueType: 'blocked_job',
79
+ }));
80
+ const pendingSteps = (plan ?? [])
81
+ .filter((step) => step.status === 'pending')
82
+ .map((step) => ({
83
+ id: `plan-${step.step}`,
84
+ queueType: 'pending_step',
85
+ status: 'pending',
86
+ workspace,
87
+ step: step.step,
88
+ activityKey: step.activityKey ?? step.ownerActivityKey ?? undefined,
89
+ args: { type: step.description },
90
+ }));
91
+ return [...blockedJobs, ...pendingSteps];
75
92
  }
76
93
 
77
94
  export function formatQueue(session) {