@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
@@ -3,17 +3,23 @@ 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
+ import { projectQueue } from '../core/jobQueue.js';
6
7
 
7
8
  export { defaultRuntimeStateDir };
8
9
 
9
10
  const NON_PERSISTED_EVENT_TYPES = new Set(['runtime_log']);
10
11
 
11
- const RUN_STATUS_BY_TERMINAL_EVENT = {
12
+ const RUN_STATUS_BY_EVENT = {
12
13
  run_done: 'done',
13
14
  run_error: 'error',
14
15
  run_cancelled: 'cancelled',
16
+ run_pending_approval: 'pending_approval',
17
+ run_approved: 'running',
15
18
  };
16
19
 
20
+ const RECOVERABLE_RUN_STATUSES = ['running', 'waiting', 'pending_approval'];
21
+ export const RECOVERABLE_QUEUE_STATUSES = ['waiting', 'queued', 'starting', 'running', 'blocked', 'pending_approval'];
22
+
17
23
  export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName = 'runtime.db' } = {}) {
18
24
  const resolvedStateDir = resolve(stateDir);
19
25
  mkdirSync(resolvedStateDir, { recursive: true });
@@ -23,11 +29,13 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
23
29
  db.exec('PRAGMA foreign_keys = ON');
24
30
  db.exec(`
25
31
  CREATE TABLE IF NOT EXISTS events (
26
- id TEXT PRIMARY KEY,
32
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ id TEXT NOT NULL UNIQUE,
27
34
  ts TEXT NOT NULL,
28
35
  type TEXT NOT NULL,
29
36
  run_id TEXT,
30
37
  turn_id TEXT,
38
+ workspace TEXT,
31
39
  origin TEXT,
32
40
  payload TEXT NOT NULL
33
41
  );
@@ -35,6 +43,7 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
35
43
  CREATE INDEX IF NOT EXISTS idx_events_run_id ON events(run_id);
36
44
  CREATE TABLE IF NOT EXISTS runs (
37
45
  id TEXT PRIMARY KEY,
46
+ workspace TEXT,
38
47
  status TEXT NOT NULL,
39
48
  input TEXT,
40
49
  created_at TEXT NOT NULL,
@@ -58,31 +67,69 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
58
67
  updated_at TEXT NOT NULL
59
68
  );
60
69
  `);
70
+ ensureColumn(db, 'events', 'sequence', 'INTEGER');
71
+ ensureColumn(db, 'events', 'workspace', 'TEXT');
72
+ ensureColumn(db, 'runs', 'workspace', 'TEXT');
73
+ backfillEventSequence(db);
74
+ db.exec('CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace)');
75
+ db.exec('CREATE INDEX IF NOT EXISTS idx_events_sequence ON events(sequence)');
61
76
 
62
77
  let lastEventId = null;
78
+ let lastEventSequence = null;
63
79
 
64
80
  const insertEvent = db.prepare(`
65
- INSERT OR IGNORE INTO events (id, ts, type, run_id, turn_id, origin, payload)
66
- VALUES (?, ?, ?, ?, ?, ?, ?)
81
+ INSERT OR IGNORE INTO events (sequence, id, ts, type, run_id, turn_id, workspace, origin, payload)
82
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
67
83
  `);
84
+ const nextEventSequenceStatement = db.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence FROM events');
68
85
  const listEventsStatement = db.prepare(`
69
- SELECT id, ts, type, run_id, turn_id, origin, payload
86
+ SELECT sequence, id, ts, type, run_id, turn_id, workspace, origin, payload
87
+ FROM events
88
+ ORDER BY sequence ASC
89
+ `);
90
+ const listEventsByWorkspaceStatement = db.prepare(`
91
+ SELECT sequence, id, ts, type, run_id, turn_id, workspace, origin, payload
70
92
  FROM events
71
- ORDER BY ts ASC, id ASC
93
+ WHERE workspace = ?
94
+ ORDER BY sequence ASC
72
95
  `);
73
96
  const upsertRun = db.prepare(`
74
- INSERT INTO runs (id, status, input, created_at, updated_at)
75
- VALUES (?, ?, ?, ?, ?)
97
+ INSERT INTO runs (id, workspace, status, input, created_at, updated_at)
98
+ VALUES (?, ?, ?, ?, ?, ?)
76
99
  ON CONFLICT(id) DO UPDATE SET
100
+ workspace = COALESCE(excluded.workspace, runs.workspace),
77
101
  status = excluded.status,
78
102
  input = COALESCE(excluded.input, runs.input),
79
103
  updated_at = excluded.updated_at
80
104
  `);
81
105
  const listRunsStatement = db.prepare(`
82
- SELECT id, status, input, created_at, updated_at
106
+ SELECT id, workspace, status, input, created_at, updated_at
83
107
  FROM runs
84
108
  ORDER BY created_at DESC
85
109
  `);
110
+ const listRunsByWorkspaceStatement = db.prepare(`
111
+ SELECT id, workspace, status, input, created_at, updated_at
112
+ FROM runs
113
+ WHERE workspace = ?
114
+ ORDER BY created_at DESC
115
+ `);
116
+ const listRecoverableRunsStatement = db.prepare(`
117
+ SELECT id, workspace, status, input, created_at, updated_at
118
+ FROM runs
119
+ WHERE status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
120
+ ORDER BY created_at ASC
121
+ `);
122
+ const listRecoverableRunsByWorkspaceStatement = db.prepare(`
123
+ SELECT id, workspace, status, input, created_at, updated_at
124
+ FROM runs
125
+ WHERE workspace = ? AND status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
126
+ ORDER BY created_at ASC
127
+ `);
128
+ const interruptRunsStatement = db.prepare(`
129
+ UPDATE runs
130
+ SET status = 'interrupted', updated_at = ?
131
+ WHERE workspace = ? AND status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
132
+ `);
86
133
  const upsertQueueItem = db.prepare(`
87
134
  INSERT INTO queue_items (
88
135
  id, workspace, server, tool, args, lock_key, status, reason, job_id, activity_key,
@@ -109,6 +156,11 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
109
156
  DELETE FROM queue_items
110
157
  WHERE id NOT IN (SELECT value FROM json_each(?))
111
158
  `);
159
+ const deleteMissingQueueItemsForWorkspace = db.prepare(`
160
+ DELETE FROM queue_items
161
+ WHERE workspace = ? AND id NOT IN (SELECT value FROM json_each(?))
162
+ `);
163
+ const clearQueueItemsForWorkspace = db.prepare('DELETE FROM queue_items WHERE workspace = ?');
112
164
  const clearQueueItems = db.prepare('DELETE FROM queue_items');
113
165
  const listQueueStatement = db.prepare(`
114
166
  SELECT id, workspace, server, tool, args, lock_key, status, reason, job_id, activity_key,
@@ -116,33 +168,58 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
116
168
  FROM queue_items
117
169
  ORDER BY COALESCE(created_at, updated_at) ASC, id ASC
118
170
  `);
171
+ const listQueueByWorkspaceStatement = db.prepare(`
172
+ SELECT id, workspace, server, tool, args, lock_key, status, reason, job_id, activity_key,
173
+ error, created_at, started_at, finished_at, updated_at
174
+ FROM queue_items
175
+ WHERE workspace = ?
176
+ ORDER BY COALESCE(created_at, updated_at) ASC, id ASC
177
+ `);
178
+ const listRecoverableWorkspacesStatement = db.prepare(`
179
+ SELECT DISTINCT workspace FROM runs
180
+ WHERE workspace IS NOT NULL AND status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
181
+ UNION
182
+ SELECT DISTINCT workspace FROM queue_items
183
+ WHERE workspace IS NOT NULL AND status IN (${RECOVERABLE_QUEUE_STATUSES.map(() => '?').join(', ')})
184
+ ORDER BY workspace
185
+ `);
119
186
 
120
187
  function persistEvent(event) {
121
188
  if (NON_PERSISTED_EVENT_TYPES.has(event.type)) return event;
122
- insertEvent.run(
189
+ const ws = event.workspace ?? event.payload?.workspace ?? null;
190
+ const sequence = nextEventSequenceStatement.get().next_sequence;
191
+ const result = insertEvent.run(
192
+ sequence,
123
193
  event.id,
124
194
  event.ts,
125
195
  event.type,
126
196
  event.runId ?? null,
127
197
  event.turnId ?? null,
198
+ ws,
128
199
  event.origin ?? null,
129
200
  JSON.stringify(event.payload ?? {}),
130
201
  );
131
- lastEventId = event.id;
202
+ if (Number(result.changes ?? 0) > 0) {
203
+ event.sequence = sequence;
204
+ lastEventId = event.id;
205
+ lastEventSequence = sequence;
206
+ }
132
207
  if (event.type === 'run_started') {
133
208
  persistRun({
134
209
  id: event.runId ?? event.payload?.runId ?? event.id,
135
210
  status: 'running',
136
211
  input: event.payload?.input ?? null,
212
+ workspace: ws,
137
213
  createdAt: event.ts,
138
214
  updatedAt: event.ts,
139
215
  });
140
- } else if (RUN_STATUS_BY_TERMINAL_EVENT[event.type]) {
216
+ } else if (RUN_STATUS_BY_EVENT[event.type]) {
141
217
  const runId = event.runId ?? event.payload?.runId ?? null;
142
218
  if (runId) {
143
219
  persistRun({
144
220
  id: runId,
145
- status: RUN_STATUS_BY_TERMINAL_EVENT[event.type],
221
+ status: RUN_STATUS_BY_EVENT[event.type],
222
+ workspace: ws,
146
223
  updatedAt: event.ts,
147
224
  });
148
225
  }
@@ -150,19 +227,26 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
150
227
  return event;
151
228
  }
152
229
 
153
- function persistRun({ id, status, input = null, createdAt = null, updatedAt = null }) {
230
+ function persistRun({ id, status, input = null, workspace = null, createdAt = null, updatedAt = null }) {
154
231
  if (!id) return;
155
232
  const now = new Date().toISOString();
156
- upsertRun.run(id, status, input, createdAt ?? now, updatedAt ?? now);
233
+ upsertRun.run(id, workspace, status, input, createdAt ?? now, updatedAt ?? now);
157
234
  }
158
235
 
159
- function listEvents() {
160
- return listEventsStatement.all().map(rowToEvent);
236
+ function listEvents({ workspace = null } = {}) {
237
+ const rows = workspace
238
+ ? listEventsByWorkspaceStatement.all(workspace)
239
+ : listEventsStatement.all();
240
+ return rows.map(rowToEvent);
161
241
  }
162
242
 
163
- function listRuns() {
164
- return listRunsStatement.all().map((row) => ({
243
+ function listRuns({ workspace = null } = {}) {
244
+ const rows = workspace
245
+ ? listRunsByWorkspaceStatement.all(workspace)
246
+ : listRunsStatement.all();
247
+ return rows.map((row) => ({
165
248
  id: row.id,
249
+ workspace: row.workspace ?? null,
166
250
  status: row.status,
167
251
  input: row.input,
168
252
  createdAt: row.created_at,
@@ -170,19 +254,48 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
170
254
  }));
171
255
  }
172
256
 
173
- function saveQueue(queue = []) {
257
+ function listRecoverableRuns({ workspace = null } = {}) {
258
+ const rows = workspace
259
+ ? listRecoverableRunsByWorkspaceStatement.all(workspace, ...RECOVERABLE_RUN_STATUSES)
260
+ : listRecoverableRunsStatement.all(...RECOVERABLE_RUN_STATUSES);
261
+ return rows.map((row) => ({
262
+ id: row.id,
263
+ workspace: row.workspace ?? null,
264
+ status: row.status,
265
+ input: row.input,
266
+ createdAt: row.created_at,
267
+ updatedAt: row.updated_at,
268
+ }));
269
+ }
270
+
271
+ function listRecoverableWorkspaces() {
272
+ return listRecoverableWorkspacesStatement
273
+ .all(...RECOVERABLE_RUN_STATUSES, ...RECOVERABLE_QUEUE_STATUSES)
274
+ .map((row) => row.workspace);
275
+ }
276
+
277
+ function interruptRuns({ workspace, reason = 'Runtime restart recovery failed.' } = {}) {
278
+ if (!workspace) return 0;
279
+ const now = new Date().toISOString();
280
+ const result = interruptRunsStatement.run(now, workspace, ...RECOVERABLE_RUN_STATUSES);
281
+ return Number(result.changes ?? 0);
282
+ }
283
+
284
+ function saveQueue(queue = [], { workspace = null } = {}) {
174
285
  const items = Array.isArray(queue) ? queue : [];
175
286
  const now = new Date().toISOString();
176
287
  db.exec('BEGIN');
177
288
  try {
178
289
  if (items.length === 0) {
179
- clearQueueItems.run();
290
+ if (workspace) clearQueueItemsForWorkspace.run(workspace);
291
+ else clearQueueItems.run();
180
292
  } else {
181
- deleteMissingQueueItems.run(JSON.stringify(items.map((item) => item.id)));
293
+ if (workspace) deleteMissingQueueItemsForWorkspace.run(workspace, JSON.stringify(items.map((item) => item.id)));
294
+ else deleteMissingQueueItems.run(JSON.stringify(items.map((item) => item.id)));
182
295
  for (const item of items) {
183
296
  upsertQueueItem.run(
184
297
  item.id,
185
- item.workspace ?? null,
298
+ item.workspace ?? workspace ?? null,
186
299
  item.server ?? 'production',
187
300
  item.tool ?? 'production_start_job',
188
301
  JSON.stringify(item.args ?? {}),
@@ -206,8 +319,11 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
206
319
  }
207
320
  }
208
321
 
209
- function listQueue() {
210
- return listQueueStatement.all().map((row) => ({
322
+ function listQueue({ workspace = null } = {}) {
323
+ const rows = workspace
324
+ ? listQueueByWorkspaceStatement.all(workspace)
325
+ : listQueueStatement.all();
326
+ return rows.map((row) => ({
211
327
  id: row.id,
212
328
  workspace: row.workspace ?? null,
213
329
  server: row.server,
@@ -226,34 +342,41 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
226
342
  }));
227
343
  }
228
344
 
229
- function replayEvents(session) {
230
- const events = listEvents();
345
+ function replayEvents(session, { workspace = null } = {}) {
346
+ const events = listEvents({ workspace });
231
347
  for (const event of events) {
232
348
  dispatchAgentEvent(session, event);
233
349
  }
234
- if (events.length > 0) lastEventId = events.at(-1).id;
350
+ if (events.length > 0) {
351
+ lastEventId = events.at(-1).id;
352
+ lastEventSequence = events.at(-1).sequence ?? null;
353
+ }
235
354
  return session.agentProjection ?? reduceAgentEvents([]);
236
355
  }
237
356
 
238
- function getProjection() {
239
- return reduceAgentEvents(listEvents());
357
+ function getProjection({ workspace = null } = {}) {
358
+ return reduceAgentEvents(listEvents({ workspace }));
240
359
  }
241
360
 
242
- function getState(session = null) {
243
- const projection = session?.agentProjection ?? reduceAgentEvents(listEvents());
244
- const queue = session?.queueStore?.list() ?? listQueue();
361
+ function getState(session = null, { workspace = null } = {}) {
362
+ const events = session?.agentProjection ? null : listEvents({ workspace });
363
+ const projection = session?.agentProjection ?? reduceAgentEvents(events);
364
+ const rawQueue = session?.queueStore?.list() ?? listQueue({ workspace });
245
365
  return {
246
366
  ...projection,
247
- runs: listRuns(),
248
- queue: queue.map((item) => ({ ...item })),
249
- eventsCursor: lastEventId,
367
+ runs: listRuns({ workspace }),
368
+ queue: projectQueue(projection.plan, rawQueue, { workspace }),
369
+ controlQueue: Array.isArray(projection.controlQueue)
370
+ ? projection.controlQueue.filter((item) => !workspace || item.workspace === workspace || !item.workspace).map((item) => ({ ...item }))
371
+ : [],
372
+ eventsCursor: session?.agentEvents?.at(-1)?.sequence ?? events?.at(-1)?.sequence ?? lastEventSequence,
250
373
  };
251
374
  }
252
375
 
253
- function hydrateSession(session) {
254
- const projection = replayEvents(session);
376
+ function hydrateSession(session, { workspace = null } = {}) {
377
+ const projection = replayEvents(session, { workspace });
255
378
  applyAgentProjectionToSession(session, projection);
256
- session.jobQueue = listQueue();
379
+ session.jobQueue = listQueue({ workspace });
257
380
  return projection;
258
381
  }
259
382
 
@@ -269,6 +392,9 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
269
392
  persistRun,
270
393
  listEvents,
271
394
  listRuns,
395
+ listRecoverableRuns,
396
+ listRecoverableWorkspaces,
397
+ interruptRuns,
272
398
  saveQueue,
273
399
  listQueue,
274
400
  replayEvents,
@@ -281,12 +407,28 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
281
407
 
282
408
  function rowToEvent(row) {
283
409
  return {
410
+ sequence: row.sequence ?? null,
284
411
  id: row.id,
285
412
  ts: row.ts,
286
413
  type: row.type,
287
414
  origin: row.origin ?? 'system',
288
415
  runId: row.run_id ?? null,
289
416
  turnId: row.turn_id ?? null,
417
+ workspace: row.workspace ?? null,
290
418
  payload: row.payload ? JSON.parse(row.payload) : {},
291
419
  };
292
420
  }
421
+
422
+ function ensureColumn(db, table, column, definition) {
423
+ const existing = db.prepare(`PRAGMA table_info(${table})`).all();
424
+ if (existing.some((row) => row.name === column)) return;
425
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
426
+ }
427
+
428
+ function backfillEventSequence(db) {
429
+ db.exec(`
430
+ UPDATE events
431
+ SET sequence = rowid
432
+ WHERE sequence IS NULL
433
+ `);
434
+ }