@myagentroam/node 0.1.0 → 0.1.2
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/README.md +19 -2
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +58 -3
- package/dist/capabilities.js.map +1 -1
- package/dist/claude-agent-sdk.d.ts +1 -0
- package/dist/claude-agent-sdk.js +36 -2
- package/dist/claude-agent-sdk.js.map +1 -1
- package/dist/codex-app-server.d.ts +1 -0
- package/dist/codex-app-server.js +23 -5
- package/dist/codex-app-server.js.map +1 -1
- package/dist/connector.d.ts +41 -3
- package/dist/connector.js +625 -134
- package/dist/connector.js.map +1 -1
- package/dist/database.d.ts +13 -0
- package/dist/database.js +284 -239
- package/dist/database.js.map +1 -1
- package/dist/migrations/v001.d.ts +3 -0
- package/dist/migrations/v001.js +249 -0
- package/dist/migrations/v001.js.map +1 -0
- package/dist/migrations/v002.d.ts +4 -0
- package/dist/migrations/v002.js +144 -0
- package/dist/migrations/v002.js.map +1 -0
- package/dist/operational.d.ts +9 -0
- package/dist/operational.js +30 -0
- package/dist/operational.js.map +1 -1
- package/dist/workspace.d.ts +36 -8
- package/dist/workspace.js +315 -99
- package/dist/workspace.js.map +1 -1
- package/package.json +3 -2
package/dist/database.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
|
|
6
|
+
import { assertNodeSchema, NODE_SCHEMA_V1 } from './migrations/v001.js';
|
|
7
|
+
import { assertNodeSchemaV2, migrateNodeSchemaV2, NODE_SCHEMA_VERSION_V2 } from './migrations/v002.js';
|
|
8
|
+
const NODE_SCHEMA_VERSION = NODE_SCHEMA_VERSION_V2;
|
|
6
9
|
export class NodeDatabase {
|
|
7
10
|
now;
|
|
8
11
|
activeWorkspaceLeaseCount() {
|
|
@@ -17,13 +20,20 @@ export class NodeDatabase {
|
|
|
17
20
|
this.nodeIdProvider = typeof nodeId === 'string' ? () => nodeId : nodeId;
|
|
18
21
|
if (path !== ':memory:')
|
|
19
22
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const releaseMigrationLock = acquireNodeMigrationLock(path);
|
|
24
|
+
try {
|
|
25
|
+
this.raw = new DatabaseSync(path, {
|
|
26
|
+
enableForeignKeyConstraints: true,
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
defensive: true
|
|
29
|
+
});
|
|
30
|
+
this.raw.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
31
|
+
migrate(this.raw);
|
|
32
|
+
initializeEphemeralRuntimeTables(this.raw);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
releaseMigrationLock();
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
close() {
|
|
29
39
|
this.raw.close();
|
|
@@ -126,6 +136,7 @@ export class NodeDatabase {
|
|
|
126
136
|
// Session deletion cascades their runs, conversation and attachments. The workspace
|
|
127
137
|
// record itself never represents a filesystem deletion.
|
|
128
138
|
this.raw.prepare('DELETE FROM agent_sessions WHERE workspace_id = ?').run(id);
|
|
139
|
+
this.raw.prepare('DELETE FROM legacy_agent_sessions WHERE workspace_id = ?').run(id);
|
|
129
140
|
this.raw.prepare('DELETE FROM workspaces WHERE id = ?').run(id);
|
|
130
141
|
this.raw.exec('COMMIT');
|
|
131
142
|
return current;
|
|
@@ -158,7 +169,7 @@ export class NodeDatabase {
|
|
|
158
169
|
createdAt: this.now()
|
|
159
170
|
};
|
|
160
171
|
this.raw
|
|
161
|
-
.prepare(`INSERT INTO
|
|
172
|
+
.prepare(`INSERT INTO legacy_agent_sessions
|
|
162
173
|
(id, workspace_id, runner, external_session_id, native_control, channel_token, cwd, model, effort, access, custom_title, runner_title, pinned_at, title_source, last_activity_at, created_at, updated_at)
|
|
163
174
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
164
175
|
.run(session.id, session.workspaceId, session.runner, session.externalSessionId, session.nativeControl, session.channelToken, session.cwd, session.model, session.effort, session.access, session.customTitle, session.runnerTitle, session.pinnedAt, session.titleSource, session.lastActivityAt, session.createdAt, session.createdAt);
|
|
@@ -166,13 +177,13 @@ export class NodeDatabase {
|
|
|
166
177
|
}
|
|
167
178
|
getAgentSession(id) {
|
|
168
179
|
const row = this.raw
|
|
169
|
-
.prepare('SELECT id, workspace_id, runner, external_session_id, native_control, channel_token, cwd, model, effort, access, custom_title, runner_title, pinned_at, title_source, last_activity_at, created_at FROM
|
|
180
|
+
.prepare('SELECT id, workspace_id, runner, external_session_id, native_control, channel_token, cwd, model, effort, access, custom_title, runner_title, pinned_at, title_source, last_activity_at, created_at FROM legacy_agent_sessions WHERE id = ?')
|
|
170
181
|
.get(id);
|
|
171
182
|
return row === undefined ? undefined : session(row, this.nodeIdProvider());
|
|
172
183
|
}
|
|
173
184
|
updateAgentSessionConfiguration(input) {
|
|
174
185
|
const result = this.raw
|
|
175
|
-
.prepare(`UPDATE
|
|
186
|
+
.prepare(`UPDATE legacy_agent_sessions
|
|
176
187
|
SET model = ?, effort = ?, access = ?, updated_at = ?
|
|
177
188
|
WHERE id = ?`)
|
|
178
189
|
.run(input.model, input.effort, input.access, this.now(), input.sessionId);
|
|
@@ -182,20 +193,20 @@ export class NodeDatabase {
|
|
|
182
193
|
}
|
|
183
194
|
listAgentSessions() {
|
|
184
195
|
return this.raw
|
|
185
|
-
.prepare('SELECT id, workspace_id, runner, external_session_id, native_control, channel_token, cwd, model, effort, access, custom_title, runner_title, pinned_at, title_source, last_activity_at, created_at FROM
|
|
196
|
+
.prepare('SELECT id, workspace_id, runner, external_session_id, native_control, channel_token, cwd, model, effort, access, custom_title, runner_title, pinned_at, title_source, last_activity_at, created_at FROM legacy_agent_sessions ORDER BY pinned_at IS NULL, pinned_at DESC, last_activity_at DESC, id DESC')
|
|
186
197
|
.all()
|
|
187
198
|
.map((row) => session(row, this.nodeIdProvider()));
|
|
188
199
|
}
|
|
189
200
|
findAgentSessionByExternalSession(runner, externalSessionId) {
|
|
190
201
|
const row = this.raw
|
|
191
202
|
.prepare(`SELECT id, workspace_id, runner, external_session_id, native_control, channel_token, cwd, model, effort, access, custom_title, runner_title, pinned_at, title_source, last_activity_at, created_at
|
|
192
|
-
FROM
|
|
203
|
+
FROM legacy_agent_sessions WHERE runner = ? AND external_session_id = ?`)
|
|
193
204
|
.get(runner, externalSessionId);
|
|
194
205
|
return row === undefined ? undefined : session(row, this.nodeIdProvider());
|
|
195
206
|
}
|
|
196
207
|
setNativeControl(sessionId, nativeControl) {
|
|
197
208
|
const result = this.raw
|
|
198
|
-
.prepare('UPDATE
|
|
209
|
+
.prepare('UPDATE legacy_agent_sessions SET native_control = ?, updated_at = ? WHERE id = ?')
|
|
199
210
|
.run(nativeControl, this.now(), sessionId);
|
|
200
211
|
if (Number(result.changes) === 0)
|
|
201
212
|
throw new Error('SESSION_NOT_FOUND');
|
|
@@ -204,7 +215,7 @@ export class NodeDatabase {
|
|
|
204
215
|
if (externalSessionId.length === 0)
|
|
205
216
|
throw new Error('SESSION_EXTERNAL_ID_INVALID');
|
|
206
217
|
const result = this.raw
|
|
207
|
-
.prepare("UPDATE
|
|
218
|
+
.prepare("UPDATE legacy_agent_sessions SET external_session_id = ?, native_control = 'MAR_MANAGED', updated_at = ? WHERE id = ?")
|
|
208
219
|
.run(externalSessionId, this.now(), sessionId);
|
|
209
220
|
if (Number(result.changes) === 0)
|
|
210
221
|
throw new Error('SESSION_NOT_FOUND');
|
|
@@ -213,7 +224,7 @@ export class NodeDatabase {
|
|
|
213
224
|
if (token.length < 16)
|
|
214
225
|
throw new Error('SESSION_CHANNEL_TOKEN_INVALID');
|
|
215
226
|
const result = this.raw
|
|
216
|
-
.prepare('UPDATE
|
|
227
|
+
.prepare('UPDATE legacy_agent_sessions SET channel_token = ?, updated_at = ? WHERE id = ?')
|
|
217
228
|
.run(token, this.now(), sessionId);
|
|
218
229
|
if (Number(result.changes) === 0)
|
|
219
230
|
throw new Error('SESSION_NOT_FOUND');
|
|
@@ -228,26 +239,21 @@ export class NodeDatabase {
|
|
|
228
239
|
const pinnedAt = input.pinned === undefined ? current.pinnedAt : input.pinned ? this.now() : null;
|
|
229
240
|
const titleSource = title === null ? 'AUTO' : 'CUSTOM';
|
|
230
241
|
this.raw
|
|
231
|
-
.prepare('UPDATE
|
|
242
|
+
.prepare('UPDATE legacy_agent_sessions SET custom_title = ?, pinned_at = ?, title_source = ?, updated_at = ? WHERE id = ?')
|
|
232
243
|
.run(title, pinnedAt, titleSource, this.now(), input.sessionId);
|
|
233
244
|
return this.getAgentSession(input.sessionId);
|
|
234
245
|
}
|
|
235
246
|
getNativeSessionMetadata(runner, externalSessionId, workspaceId) {
|
|
236
247
|
const row = this.raw
|
|
237
|
-
.prepare(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
pinnedAt: nullableNumber(row, 'pinned_at'),
|
|
247
|
-
model: nullableString(row, 'model'),
|
|
248
|
-
effort: nullableString(row, 'effort'),
|
|
249
|
-
access: nullableString(row, 'access')
|
|
250
|
-
};
|
|
248
|
+
.prepare('SELECT pin_order, metadata FROM agent_sessions WHERE id = ? AND runner = ? AND workspace_id = ?')
|
|
249
|
+
.get(externalSessionId, runner, workspaceId);
|
|
250
|
+
if (row === undefined)
|
|
251
|
+
return undefined;
|
|
252
|
+
return {
|
|
253
|
+
...sessionMetadata(row),
|
|
254
|
+
pinnedAt: row['pin_order'] === null ? null : 1,
|
|
255
|
+
pinOrder: nullableNumber(row, 'pin_order')
|
|
256
|
+
};
|
|
251
257
|
}
|
|
252
258
|
saveNativeSessionMetadata(input) {
|
|
253
259
|
const current = this.getNativeSessionMetadata(input.runner, input.externalSessionId, input.workspaceId);
|
|
@@ -264,21 +270,99 @@ export class NodeDatabase {
|
|
|
264
270
|
throw new Error('SESSION_TITLE_INVALID');
|
|
265
271
|
if (runnerTitle !== null && runnerTitle.length > 160)
|
|
266
272
|
throw new Error('SESSION_TITLE_INVALID');
|
|
267
|
-
|
|
273
|
+
let pinOrder = current?.pinOrder ?? null;
|
|
274
|
+
const pinAtTop = input.pinned === true && pinOrder === null;
|
|
275
|
+
if (pinAtTop)
|
|
276
|
+
pinOrder = 0;
|
|
277
|
+
else if (input.pinned === false)
|
|
278
|
+
pinOrder = null;
|
|
268
279
|
const model = input.model === undefined ? (current?.model ?? null) : input.model;
|
|
269
280
|
const effort = input.effort === undefined ? (current?.effort ?? null) : input.effort;
|
|
270
281
|
const access = input.access === undefined ? (current?.access ?? null) : input.access;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
+
const metadata = JSON.stringify({
|
|
283
|
+
version: 1,
|
|
284
|
+
customTitle,
|
|
285
|
+
runnerTitle,
|
|
286
|
+
runnerTitlePending,
|
|
287
|
+
model,
|
|
288
|
+
effort,
|
|
289
|
+
access
|
|
290
|
+
});
|
|
291
|
+
this.raw.exec('BEGIN IMMEDIATE');
|
|
292
|
+
try {
|
|
293
|
+
if (pinAtTop)
|
|
294
|
+
this.raw
|
|
295
|
+
.prepare('UPDATE agent_sessions SET pin_order = pin_order + 1 WHERE workspace_id = ? AND pin_order IS NOT NULL')
|
|
296
|
+
.run(input.workspaceId);
|
|
297
|
+
this.raw
|
|
298
|
+
.prepare(`INSERT INTO agent_sessions (id, workspace_id, runner, pin_order, metadata)
|
|
299
|
+
VALUES (?, ?, ?, ?, ?)
|
|
300
|
+
ON CONFLICT(id) DO UPDATE SET workspace_id = excluded.workspace_id,
|
|
301
|
+
runner = excluded.runner, pin_order = excluded.pin_order, metadata = excluded.metadata`)
|
|
302
|
+
.run(input.externalSessionId, input.workspaceId, input.runner, pinOrder, metadata);
|
|
303
|
+
this.raw.exec('COMMIT');
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
this.raw.exec('ROLLBACK');
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
customTitle,
|
|
311
|
+
runnerTitle,
|
|
312
|
+
runnerTitlePending,
|
|
313
|
+
pinnedAt: pinOrder === null ? null : 1,
|
|
314
|
+
pinOrder,
|
|
315
|
+
model,
|
|
316
|
+
effort,
|
|
317
|
+
access
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
listSessionRecords(workspaceId) {
|
|
321
|
+
return this.raw
|
|
322
|
+
.prepare('SELECT id, workspace_id, runner, pin_order, metadata FROM agent_sessions WHERE workspace_id = ? ORDER BY pin_order IS NULL, pin_order, id')
|
|
323
|
+
.all(workspaceId).map((row) => ({
|
|
324
|
+
id: stringValue(row, 'id'),
|
|
325
|
+
workspaceId: stringValue(row, 'workspace_id'),
|
|
326
|
+
runner: stringValue(row, 'runner'),
|
|
327
|
+
pinOrder: nullableNumber(row, 'pin_order'),
|
|
328
|
+
metadata: {
|
|
329
|
+
...sessionMetadata(row),
|
|
330
|
+
pinnedAt: row['pin_order'] === null ? null : 1,
|
|
331
|
+
pinOrder: nullableNumber(row, 'pin_order')
|
|
332
|
+
}
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
movePinnedSession(sessionId, direction) {
|
|
336
|
+
const current = this.raw
|
|
337
|
+
.prepare('SELECT workspace_id, pin_order FROM agent_sessions WHERE id = ?')
|
|
338
|
+
.get(sessionId);
|
|
339
|
+
if (current === undefined || current['pin_order'] === null)
|
|
340
|
+
throw new Error('SESSION_NOT_PINNED');
|
|
341
|
+
const order = numberValue(current, 'pin_order');
|
|
342
|
+
const adjacent = this.raw
|
|
343
|
+
.prepare(direction === 'up'
|
|
344
|
+
? 'SELECT id, pin_order FROM agent_sessions WHERE workspace_id = ? AND pin_order < ? ORDER BY pin_order DESC LIMIT 1'
|
|
345
|
+
: 'SELECT id, pin_order FROM agent_sessions WHERE workspace_id = ? AND pin_order > ? ORDER BY pin_order ASC LIMIT 1')
|
|
346
|
+
.get(stringValue(current, 'workspace_id'), order);
|
|
347
|
+
if (adjacent === undefined)
|
|
348
|
+
return;
|
|
349
|
+
this.raw.exec('BEGIN IMMEDIATE');
|
|
350
|
+
try {
|
|
351
|
+
this.raw
|
|
352
|
+
.prepare('UPDATE agent_sessions SET pin_order = ? WHERE id = ?')
|
|
353
|
+
.run(order, stringValue(adjacent, 'id'));
|
|
354
|
+
this.raw
|
|
355
|
+
.prepare('UPDATE agent_sessions SET pin_order = ? WHERE id = ?')
|
|
356
|
+
.run(numberValue(adjacent, 'pin_order'), sessionId);
|
|
357
|
+
this.raw.exec('COMMIT');
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
this.raw.exec('ROLLBACK');
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
deleteSessionRecord(sessionId) {
|
|
365
|
+
return (Number(this.raw.prepare('DELETE FROM agent_sessions WHERE id = ?').run(sessionId).changes) > 0);
|
|
282
366
|
}
|
|
283
367
|
/**
|
|
284
368
|
* Replaces only transcript-derived, run-less history. Managed Run turns are
|
|
@@ -319,7 +403,7 @@ export class NodeDatabase {
|
|
|
319
403
|
}
|
|
320
404
|
const runnerTitle = history.title?.trim().slice(0, 160) || current.runnerTitle;
|
|
321
405
|
this.raw
|
|
322
|
-
.prepare(`UPDATE
|
|
406
|
+
.prepare(`UPDATE legacy_agent_sessions
|
|
323
407
|
SET runner_title = ?, title_source = CASE WHEN custom_title IS NULL THEN 'RUNNER' ELSE title_source END,
|
|
324
408
|
last_activity_at = CASE WHEN ? > last_activity_at THEN ? ELSE last_activity_at END, updated_at = ?
|
|
325
409
|
WHERE id = ?`)
|
|
@@ -409,7 +493,7 @@ export class NodeDatabase {
|
|
|
409
493
|
}), value.createdAt, value.createdAt, value.createdAt, value.createdAt);
|
|
410
494
|
this.recordConversationItemChange(value.sessionId, userItemId, 0);
|
|
411
495
|
this.raw
|
|
412
|
-
.prepare('UPDATE
|
|
496
|
+
.prepare('UPDATE legacy_agent_sessions SET last_activity_at = ?, updated_at = ? WHERE id = ?')
|
|
413
497
|
.run(value.createdAt, value.createdAt, value.sessionId);
|
|
414
498
|
this.recordWorkspaceActivity(value.workspaceId, 'run.created', {
|
|
415
499
|
runId: value.id,
|
|
@@ -909,210 +993,171 @@ export class NodeDatabase {
|
|
|
909
993
|
}
|
|
910
994
|
}
|
|
911
995
|
this.raw
|
|
912
|
-
.prepare('UPDATE
|
|
996
|
+
.prepare('UPDATE legacy_agent_sessions SET last_activity_at = ?, updated_at = ? WHERE id = ?')
|
|
913
997
|
.run(timestamp, timestamp, sessionId);
|
|
914
998
|
}
|
|
915
999
|
nodeIdProvider;
|
|
916
1000
|
}
|
|
917
1001
|
function migrate(database) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
id TEXT PRIMARY KEY, message_id TEXT NOT NULL REFERENCES session_messages(id) ON DELETE CASCADE,
|
|
950
|
-
name TEXT NOT NULL, mime TEXT NOT NULL, byte_size INTEGER NOT NULL CHECK (byte_size >= 0),
|
|
951
|
-
data BLOB NOT NULL, created_at INTEGER NOT NULL
|
|
952
|
-
) STRICT;
|
|
953
|
-
CREATE TABLE IF NOT EXISTS agent_runs (
|
|
954
|
-
id TEXT PRIMARY KEY, session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
|
955
|
-
message_id TEXT NOT NULL UNIQUE REFERENCES session_messages(id) ON DELETE CASCADE,
|
|
956
|
-
workspace_id TEXT NOT NULL REFERENCES workspaces(id), runner TEXT NOT NULL CHECK (runner IN ('codex', 'claude-code')),
|
|
957
|
-
status TEXT NOT NULL CHECK (status IN ('QUEUED', 'STARTING', 'RUNNING', 'CANCELLING', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED')),
|
|
958
|
-
recovery_state TEXT NOT NULL CHECK (recovery_state IN ('ACTIVE', 'SUSPECT', 'LOST')),
|
|
959
|
-
recovery_updated_at INTEGER NOT NULL DEFAULT 0, version INTEGER NOT NULL CHECK (version >= 0),
|
|
960
|
-
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
961
|
-
) STRICT;
|
|
962
|
-
CREATE TABLE IF NOT EXISTS run_events (
|
|
963
|
-
id INTEGER PRIMARY KEY, run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
|
|
964
|
-
sequence INTEGER NOT NULL CHECK (sequence > 0), event_type TEXT NOT NULL, payload TEXT NOT NULL,
|
|
965
|
-
created_at INTEGER NOT NULL, UNIQUE(run_id, sequence)
|
|
966
|
-
) STRICT;
|
|
967
|
-
CREATE TABLE IF NOT EXISTS conversation_turns (
|
|
968
|
-
id TEXT PRIMARY KEY, session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
|
969
|
-
run_id TEXT UNIQUE REFERENCES agent_runs(id) ON DELETE SET NULL, user_item_id TEXT,
|
|
970
|
-
status TEXT NOT NULL, model TEXT, effort TEXT, access TEXT,
|
|
971
|
-
started_at INTEGER, completed_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
972
|
-
) STRICT;
|
|
973
|
-
CREATE TABLE IF NOT EXISTS conversation_items (
|
|
974
|
-
id TEXT PRIMARY KEY, session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
|
975
|
-
turn_id TEXT NOT NULL REFERENCES conversation_turns(id) ON DELETE CASCADE,
|
|
976
|
-
run_id TEXT REFERENCES agent_runs(id) ON DELETE SET NULL, parent_item_id TEXT,
|
|
977
|
-
source_sequence INTEGER, revision INTEGER NOT NULL CHECK (revision >= 0), kind TEXT NOT NULL,
|
|
978
|
-
status TEXT NOT NULL, payload TEXT NOT NULL, started_at INTEGER, completed_at INTEGER,
|
|
979
|
-
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, UNIQUE(turn_id, id)
|
|
980
|
-
) STRICT;
|
|
981
|
-
CREATE TABLE IF NOT EXISTS conversation_item_events (
|
|
982
|
-
id INTEGER PRIMARY KEY, session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
|
983
|
-
item_id TEXT NOT NULL REFERENCES conversation_items(id) ON DELETE CASCADE,
|
|
984
|
-
revision INTEGER NOT NULL CHECK (revision >= 0), created_at INTEGER NOT NULL
|
|
985
|
-
) STRICT;
|
|
986
|
-
CREATE TABLE IF NOT EXISTS workspace_activity_events (
|
|
987
|
-
id INTEGER PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
988
|
-
event_type TEXT NOT NULL, payload TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
989
|
-
) STRICT;
|
|
990
|
-
CREATE TABLE IF NOT EXISTS approvals (
|
|
991
|
-
id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
|
|
992
|
-
status TEXT NOT NULL CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED')), payload TEXT NOT NULL,
|
|
993
|
-
expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, decided_at INTEGER
|
|
994
|
-
) STRICT;
|
|
995
|
-
CREATE TABLE IF NOT EXISTS workspace_leases (
|
|
996
|
-
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
997
|
-
run_id TEXT NOT NULL UNIQUE REFERENCES agent_runs(id) ON DELETE CASCADE,
|
|
998
|
-
state TEXT NOT NULL CHECK (state IN ('ACTIVE', 'SUSPECT', 'RELEASED')), created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
999
|
-
) STRICT;
|
|
1000
|
-
CREATE UNIQUE INDEX IF NOT EXISTS workspace_leases_active_workspace_idx
|
|
1001
|
-
ON workspace_leases(workspace_id) WHERE state IN ('ACTIVE', 'SUSPECT');
|
|
1002
|
-
CREATE TABLE IF NOT EXISTS run_snapshots (
|
|
1003
|
-
run_id TEXT PRIMARY KEY REFERENCES agent_runs(id) ON DELETE CASCADE, snapshot TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
1004
|
-
) STRICT;
|
|
1005
|
-
CREATE TABLE IF NOT EXISTS terminal_sessions (
|
|
1006
|
-
id TEXT PRIMARY KEY, workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL, profile_id TEXT NOT NULL,
|
|
1007
|
-
state TEXT NOT NULL CHECK (state IN ('RUNNING', 'EXITED', 'CLOSED')),
|
|
1008
|
-
exit_code INTEGER, rows INTEGER NOT NULL CHECK (rows > 0 AND rows <= 1000),
|
|
1009
|
-
cols INTEGER NOT NULL CHECK (cols > 0 AND cols <= 1000),
|
|
1010
|
-
latest_sequence INTEGER NOT NULL CHECK (latest_sequence >= 0),
|
|
1011
|
-
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
1012
|
-
) STRICT;
|
|
1013
|
-
CREATE TABLE IF NOT EXISTS runner_defaults (
|
|
1014
|
-
runner TEXT PRIMARY KEY CHECK (runner IN ('codex', 'claude-code')),
|
|
1015
|
-
model TEXT NOT NULL, effort TEXT NOT NULL, access TEXT NOT NULL
|
|
1016
|
-
) STRICT;
|
|
1017
|
-
CREATE INDEX IF NOT EXISTS agent_runs_session_idx ON agent_runs(session_id, created_at);
|
|
1018
|
-
CREATE INDEX IF NOT EXISTS run_events_cursor_idx ON run_events(run_id, sequence);
|
|
1019
|
-
CREATE INDEX IF NOT EXISTS conversation_turns_session_idx ON conversation_turns(session_id, created_at, id);
|
|
1020
|
-
CREATE INDEX IF NOT EXISTS conversation_items_turn_idx ON conversation_items(turn_id, source_sequence);
|
|
1021
|
-
CREATE INDEX IF NOT EXISTS conversation_items_session_cursor_idx ON conversation_items(session_id, updated_at, id);
|
|
1022
|
-
CREATE INDEX IF NOT EXISTS conversation_item_events_session_cursor_idx ON conversation_item_events(session_id, id);
|
|
1023
|
-
CREATE INDEX IF NOT EXISTS workspace_activity_cursor_idx ON workspace_activity_events(workspace_id, id);`);
|
|
1024
|
-
database.exec('DROP TABLE IF EXISTS run_change_snapshots');
|
|
1025
|
-
const workspaceColumns = database
|
|
1026
|
-
.prepare('PRAGMA table_info(workspaces)')
|
|
1027
|
-
.all();
|
|
1028
|
-
// A short-lived legacy schema required this field without a default. The
|
|
1029
|
-
// current Workspace model has no sharing flag, so retaining it would reject
|
|
1030
|
-
// every subsequent registration performed by the current INSERT statement.
|
|
1031
|
-
if (workspaceColumns.some((column) => column['name'] === 'share_allowed'))
|
|
1032
|
-
database.exec('ALTER TABLE workspaces DROP COLUMN share_allowed');
|
|
1033
|
-
const nativeMetadataColumns = database
|
|
1034
|
-
.prepare('PRAGMA table_info(native_session_metadata)')
|
|
1035
|
-
.all();
|
|
1036
|
-
if (!nativeMetadataColumns.some((column) => column['name'] === 'runner_title'))
|
|
1037
|
-
database.exec('ALTER TABLE native_session_metadata ADD COLUMN runner_title TEXT');
|
|
1038
|
-
if (!nativeMetadataColumns.some((column) => column['name'] === 'runner_title_pending'))
|
|
1039
|
-
database.exec('ALTER TABLE native_session_metadata ADD COLUMN runner_title_pending INTEGER NOT NULL DEFAULT 0 CHECK (runner_title_pending IN (0, 1))');
|
|
1040
|
-
for (const column of ['model', 'effort', 'access']) {
|
|
1041
|
-
if (!nativeMetadataColumns.some((value) => value['name'] === column))
|
|
1042
|
-
database.exec(`ALTER TABLE native_session_metadata ADD COLUMN ${column} TEXT`);
|
|
1043
|
-
}
|
|
1044
|
-
const runnerDefaultColumns = database
|
|
1045
|
-
.prepare('PRAGMA table_info(runner_defaults)')
|
|
1046
|
-
.all();
|
|
1047
|
-
if (runnerDefaultColumns.some((column) => column['name'] === 'singleton')) {
|
|
1048
|
-
database.exec(`BEGIN IMMEDIATE;
|
|
1049
|
-
CREATE TABLE runner_defaults_next (
|
|
1050
|
-
runner TEXT PRIMARY KEY CHECK (runner IN ('codex', 'claude-code')),
|
|
1051
|
-
model TEXT NOT NULL, effort TEXT NOT NULL, access TEXT NOT NULL
|
|
1052
|
-
) STRICT;
|
|
1053
|
-
INSERT INTO runner_defaults_next (runner, model, effort, access)
|
|
1054
|
-
SELECT runner, model, effort, access FROM runner_defaults;
|
|
1055
|
-
DROP TABLE runner_defaults;
|
|
1056
|
-
ALTER TABLE runner_defaults_next RENAME TO runner_defaults;
|
|
1057
|
-
COMMIT;`);
|
|
1058
|
-
}
|
|
1059
|
-
const columns = database.prepare('PRAGMA table_info(agent_sessions)').all();
|
|
1060
|
-
if (!columns.some((column) => column['name'] === 'native_control')) {
|
|
1061
|
-
// Existing databases cannot prove how an old native ID was obtained. The
|
|
1062
|
-
// safe migration is therefore OBSERVE_ONLY until the user creates or
|
|
1063
|
-
// resumes it through this Node again.
|
|
1064
|
-
database.exec("ALTER TABLE agent_sessions ADD COLUMN native_control TEXT NOT NULL DEFAULT 'EXTERNAL' CHECK (native_control IN ('MAR_MANAGED', 'EXTERNAL'))");
|
|
1065
|
-
}
|
|
1066
|
-
if (!columns.some((column) => column['name'] === 'custom_title')) {
|
|
1067
|
-
database.exec('ALTER TABLE agent_sessions ADD COLUMN custom_title TEXT');
|
|
1068
|
-
}
|
|
1069
|
-
if (!columns.some((column) => column['name'] === 'runner_title')) {
|
|
1070
|
-
database.exec('ALTER TABLE agent_sessions ADD COLUMN runner_title TEXT');
|
|
1071
|
-
}
|
|
1072
|
-
if (!columns.some((column) => column['name'] === 'pinned_at')) {
|
|
1073
|
-
database.exec('ALTER TABLE agent_sessions ADD COLUMN pinned_at INTEGER');
|
|
1074
|
-
}
|
|
1075
|
-
if (!columns.some((column) => column['name'] === 'title_source')) {
|
|
1076
|
-
database.exec("ALTER TABLE agent_sessions ADD COLUMN title_source TEXT NOT NULL DEFAULT 'AUTO'");
|
|
1077
|
-
}
|
|
1078
|
-
if (!columns.some((column) => column['name'] === 'last_activity_at')) {
|
|
1079
|
-
database.exec('ALTER TABLE agent_sessions ADD COLUMN last_activity_at INTEGER NOT NULL DEFAULT 0');
|
|
1080
|
-
}
|
|
1081
|
-
for (const column of ['model', 'effort', 'access']) {
|
|
1082
|
-
if (!columns.some((value) => value['name'] === column)) {
|
|
1083
|
-
database.exec(`ALTER TABLE agent_sessions ADD COLUMN ${column} TEXT`);
|
|
1002
|
+
const existingApplicationTables = database
|
|
1003
|
+
.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN ('workspaces', 'agent_sessions', 'runner_defaults')")
|
|
1004
|
+
.get();
|
|
1005
|
+
const existingMigrationTable = database
|
|
1006
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'node_schema_migrations'")
|
|
1007
|
+
.get();
|
|
1008
|
+
const existingMigrationRows = existingMigrationTable === undefined
|
|
1009
|
+
? 0
|
|
1010
|
+
: numberValue(database.prepare('SELECT COUNT(*) AS count FROM node_schema_migrations').get(), 'count');
|
|
1011
|
+
let currentUnversionedSchema = false;
|
|
1012
|
+
if (numberValue(existingApplicationTables, 'count') > 0 && existingMigrationRows === 0) {
|
|
1013
|
+
try {
|
|
1014
|
+
assertNodeSchemaV2(database);
|
|
1015
|
+
currentUnversionedSchema = true;
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
assertNodeSchema(database);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (currentUnversionedSchema) {
|
|
1022
|
+
database.exec('BEGIN IMMEDIATE');
|
|
1023
|
+
try {
|
|
1024
|
+
database
|
|
1025
|
+
.prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?), (?, ?)')
|
|
1026
|
+
.run(1, Date.now(), 2, Date.now());
|
|
1027
|
+
database.exec('COMMIT');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
database.exec('ROLLBACK');
|
|
1032
|
+
throw error;
|
|
1084
1033
|
}
|
|
1085
1034
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
FROM conversation_items ci
|
|
1091
|
-
WHERE NOT EXISTS (
|
|
1092
|
-
SELECT 1 FROM conversation_item_events cie WHERE cie.item_id = ci.id
|
|
1093
|
-
)`);
|
|
1094
|
-
const terminalColumns = database
|
|
1095
|
-
.prepare('PRAGMA table_info(terminal_sessions)')
|
|
1035
|
+
if (numberValue(existingApplicationTables, 'count') === 0)
|
|
1036
|
+
database.exec(NODE_SCHEMA_V1);
|
|
1037
|
+
const migrations = database
|
|
1038
|
+
.prepare('SELECT version FROM node_schema_migrations ORDER BY version')
|
|
1096
1039
|
.all();
|
|
1097
|
-
|
|
1098
|
-
|
|
1040
|
+
const versions = migrations.map((row) => numberValue(row, 'version'));
|
|
1041
|
+
const current = versions.at(-1) ?? 0;
|
|
1042
|
+
if (versions.some((version) => version > NODE_SCHEMA_VERSION))
|
|
1043
|
+
throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
|
|
1044
|
+
if (versions.some((version, index) => version !== index + 1))
|
|
1045
|
+
throw new Error('NODE_DATABASE_MIGRATION_HISTORY_INVALID');
|
|
1046
|
+
const hasApplicationTables = database
|
|
1047
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name IN ('workspaces', 'agent_sessions', 'runner_defaults') LIMIT 1")
|
|
1048
|
+
.get();
|
|
1049
|
+
if (current === 0 && hasApplicationTables !== undefined) {
|
|
1050
|
+
assertNodeSchema(database);
|
|
1051
|
+
database.exec('BEGIN IMMEDIATE');
|
|
1052
|
+
try {
|
|
1053
|
+
database
|
|
1054
|
+
.prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
|
|
1055
|
+
.run(1, Date.now());
|
|
1056
|
+
database.exec('COMMIT');
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
database.exec('ROLLBACK');
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
migrateNodeSchemaV2(database);
|
|
1063
|
+
assertNodeSchemaV2(database);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (current === 0) {
|
|
1067
|
+
database.exec('BEGIN IMMEDIATE');
|
|
1068
|
+
try {
|
|
1069
|
+
database.exec('CREATE TABLE IF NOT EXISTS node_schema_migrations (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL) STRICT');
|
|
1070
|
+
database
|
|
1071
|
+
.prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
|
|
1072
|
+
.run(1, Date.now());
|
|
1073
|
+
database.exec('COMMIT');
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
database.exec('ROLLBACK');
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1099
1079
|
}
|
|
1100
|
-
const
|
|
1101
|
-
.prepare(
|
|
1080
|
+
const afterBaseline = database
|
|
1081
|
+
.prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
|
|
1102
1082
|
.get();
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1083
|
+
if (numberValue(afterBaseline, 'version') === 1)
|
|
1084
|
+
migrateNodeSchemaV2(database);
|
|
1085
|
+
if (NODE_SCHEMA_VERSION === 2) {
|
|
1086
|
+
assertNodeSchemaV2(database);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
assertNodeSchema(database);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function sessionMetadata(row) {
|
|
1093
|
+
const raw = JSON.parse(stringValue(row, 'metadata'));
|
|
1094
|
+
if (raw['version'] !== 1)
|
|
1095
|
+
throw new Error('SESSION_METADATA_VERSION_UNSUPPORTED');
|
|
1096
|
+
const value = (key) => typeof raw[key] === 'string' ? raw[key] : null;
|
|
1097
|
+
return {
|
|
1098
|
+
customTitle: value('customTitle'),
|
|
1099
|
+
runnerTitle: value('runnerTitle'),
|
|
1100
|
+
runnerTitlePending: raw['runnerTitlePending'] === true,
|
|
1101
|
+
model: value('model'),
|
|
1102
|
+
effort: value('effort'),
|
|
1103
|
+
access: value('access')
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Legacy database helpers are still used by focused lifecycle tests, but the
|
|
1108
|
+
* product runtime lives in NodeRuntimeState. Keep those helpers explicitly
|
|
1109
|
+
* process-local so no Session/Run/Conversation state reaches the Node file.
|
|
1110
|
+
*/
|
|
1111
|
+
function initializeEphemeralRuntimeTables(database) {
|
|
1112
|
+
const runtimeTables = new Set([
|
|
1113
|
+
'agent_sessions',
|
|
1114
|
+
'native_session_checkpoints',
|
|
1115
|
+
'session_messages',
|
|
1116
|
+
'message_attachments',
|
|
1117
|
+
'agent_runs',
|
|
1118
|
+
'run_events',
|
|
1119
|
+
'conversation_turns',
|
|
1120
|
+
'conversation_items',
|
|
1121
|
+
'conversation_item_events',
|
|
1122
|
+
'workspace_activity_events',
|
|
1123
|
+
'approvals',
|
|
1124
|
+
'workspace_leases',
|
|
1125
|
+
'run_snapshots'
|
|
1126
|
+
]);
|
|
1127
|
+
for (const raw of NODE_SCHEMA_V1.split(';')) {
|
|
1128
|
+
const statement = raw.trim();
|
|
1129
|
+
const match = statement.match(/^CREATE TABLE IF NOT EXISTS ([a-z_]+)/);
|
|
1130
|
+
if (match === null || !runtimeTables.has(match[1] ?? ''))
|
|
1131
|
+
continue;
|
|
1132
|
+
database.exec(statement
|
|
1133
|
+
.replace(/^CREATE TABLE IF NOT EXISTS/, 'CREATE TEMP TABLE IF NOT EXISTS')
|
|
1134
|
+
.replace('CREATE TEMP TABLE IF NOT EXISTS agent_sessions', 'CREATE TEMP TABLE IF NOT EXISTS legacy_agent_sessions')
|
|
1135
|
+
.replaceAll('REFERENCES agent_sessions', 'REFERENCES legacy_agent_sessions')
|
|
1136
|
+
.replaceAll(/ REFERENCES workspaces\(id\)(?: ON DELETE (?:CASCADE|SET NULL))?/g, '') + ';');
|
|
1137
|
+
}
|
|
1138
|
+
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS temp.workspace_leases_active_workspace_idx
|
|
1139
|
+
ON workspace_leases(workspace_id) WHERE state IN ('ACTIVE', 'SUSPECT')`);
|
|
1140
|
+
}
|
|
1141
|
+
function acquireNodeMigrationLock(databasePath) {
|
|
1142
|
+
if (databasePath === ':memory:')
|
|
1143
|
+
return () => undefined;
|
|
1144
|
+
const lockPath = `${databasePath}.migration.lock`;
|
|
1145
|
+
const deadline = Date.now() + 5_000;
|
|
1146
|
+
while (true) {
|
|
1147
|
+
try {
|
|
1148
|
+
const descriptor = openSync(lockPath, 'wx', 0o600);
|
|
1149
|
+
return () => {
|
|
1150
|
+
closeSync(descriptor);
|
|
1151
|
+
if (existsSync(lockPath))
|
|
1152
|
+
rmSync(lockPath);
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
const code = error instanceof Error && 'code' in error ? error.code : undefined;
|
|
1157
|
+
if (code !== 'EEXIST' || Date.now() >= deadline)
|
|
1158
|
+
throw new Error('NODE_DATABASE_MIGRATION_LOCK_UNAVAILABLE');
|
|
1159
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
1160
|
+
}
|
|
1116
1161
|
}
|
|
1117
1162
|
}
|
|
1118
1163
|
function workspace(row) {
|