@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.
- package/.env.example +20 -0
- package/README.md +50 -1
- package/docker-compose.yml +1 -23
- package/mcp.endpoints.example.json +13 -0
- package/package.json +2 -2
- package/src/agent/graph.js +101 -15
- package/src/agent/graph.test.js +145 -0
- package/src/cli/wiki-manager.js +306 -53
- package/src/commands/slash.js +4 -24
- package/src/core/agentEvents.js +169 -4
- package/src/core/agentEvents.test.js +176 -4
- package/src/core/agentLoop.js +3 -0
- package/src/core/compose.js +1 -2
- package/src/core/dockerCompose.test.js +5 -5
- package/src/core/jobQueue.js +29 -12
- package/src/core/mcp.js +120 -10
- package/src/core/mcp.test.js +121 -1
- package/src/core/plan.js +33 -0
- package/src/core/queueStore.test.js +1 -0
- package/src/core/sessionConfig.js +24 -0
- package/src/core/wikiWorkspace.test.js +24 -0
- package/src/runtime/approvals.js +113 -0
- package/src/runtime/auth.test.js +8 -0
- package/src/runtime/client.js +52 -6
- package/src/runtime/lifecycle.js +27 -3
- package/src/runtime/queueStore.js +3 -3
- package/src/runtime/runner.js +340 -0
- package/src/runtime/runner.test.js +270 -0
- package/src/runtime/server.js +252 -33
- package/src/runtime/server.test.js +577 -0
- package/src/runtime/store.js +181 -39
- package/src/runtime/store.test.js +363 -4
- package/src/runtime/supervisor.js +6 -0
- package/src/runtime/supervisor.test.js +141 -0
- package/src/shell/RightPane.tsx +1 -1
- package/src/shell/repl.js +22 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +10 -5
- package/wiki-workspace +198 -4
package/src/runtime/store.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
290
|
+
if (workspace) clearQueueItemsForWorkspace.run(workspace);
|
|
291
|
+
else clearQueueItems.run();
|
|
180
292
|
} else {
|
|
181
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
244
|
-
const
|
|
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:
|
|
249
|
-
|
|
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
|
+
}
|