@myagentroam/node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +17 -0
  2. package/dist/capabilities.d.ts +5 -0
  3. package/dist/capabilities.js +23 -0
  4. package/dist/capabilities.js.map +1 -0
  5. package/dist/claude-agent-sdk.d.ts +101 -0
  6. package/dist/claude-agent-sdk.js +341 -0
  7. package/dist/claude-agent-sdk.js.map +1 -0
  8. package/dist/claude-channel.d.ts +28 -0
  9. package/dist/claude-channel.js +45 -0
  10. package/dist/claude-channel.js.map +1 -0
  11. package/dist/codex-app-server.d.ts +96 -0
  12. package/dist/codex-app-server.js +361 -0
  13. package/dist/codex-app-server.js.map +1 -0
  14. package/dist/config.d.ts +17 -0
  15. package/dist/config.js +51 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/connector.d.ts +343 -0
  18. package/dist/connector.js +5525 -0
  19. package/dist/connector.js.map +1 -0
  20. package/dist/database.d.ts +282 -0
  21. package/dist/database.js +1347 -0
  22. package/dist/database.js.map +1 -0
  23. package/dist/event-buffer.d.ts +12 -0
  24. package/dist/event-buffer.js +29 -0
  25. package/dist/event-buffer.js.map +1 -0
  26. package/dist/fake-runner.d.ts +42 -0
  27. package/dist/fake-runner.js +130 -0
  28. package/dist/fake-runner.js.map +1 -0
  29. package/dist/health.d.ts +4 -0
  30. package/dist/health.js +4 -0
  31. package/dist/health.js.map +1 -0
  32. package/dist/main.d.ts +2 -0
  33. package/dist/main.js +47 -0
  34. package/dist/main.js.map +1 -0
  35. package/dist/native-session-history.d.ts +72 -0
  36. package/dist/native-session-history.js +647 -0
  37. package/dist/native-session-history.js.map +1 -0
  38. package/dist/operational.d.ts +30 -0
  39. package/dist/operational.js +82 -0
  40. package/dist/operational.js.map +1 -0
  41. package/dist/process-tree.d.ts +9 -0
  42. package/dist/process-tree.js +19 -0
  43. package/dist/process-tree.js.map +1 -0
  44. package/dist/runner-command-engine.d.ts +19 -0
  45. package/dist/runner-command-engine.js +47 -0
  46. package/dist/runner-command-engine.js.map +1 -0
  47. package/dist/runner-profiles.d.ts +11 -0
  48. package/dist/runner-profiles.js +138 -0
  49. package/dist/runner-profiles.js.map +1 -0
  50. package/dist/runner-usage.d.ts +33 -0
  51. package/dist/runner-usage.js +193 -0
  52. package/dist/runner-usage.js.map +1 -0
  53. package/dist/runtime-state.d.ts +174 -0
  54. package/dist/runtime-state.js +957 -0
  55. package/dist/runtime-state.js.map +1 -0
  56. package/dist/service.d.ts +4 -0
  57. package/dist/service.js +39 -0
  58. package/dist/service.js.map +1 -0
  59. package/dist/storage.d.ts +2 -0
  60. package/dist/storage.js +33 -0
  61. package/dist/storage.js.map +1 -0
  62. package/dist/terminal.d.ts +132 -0
  63. package/dist/terminal.js +417 -0
  64. package/dist/terminal.js.map +1 -0
  65. package/dist/workspace.d.ts +116 -0
  66. package/dist/workspace.js +732 -0
  67. package/dist/workspace.js.map +1 -0
  68. package/package.json +36 -0
@@ -0,0 +1,1347 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
6
+ export class NodeDatabase {
7
+ now;
8
+ activeWorkspaceLeaseCount() {
9
+ const row = this.raw
10
+ .prepare("SELECT COUNT(*) AS count FROM workspace_leases WHERE state IN ('ACTIVE', 'SUSPECT')")
11
+ .get();
12
+ return row.count;
13
+ }
14
+ raw;
15
+ constructor(path, nodeId, now = Date.now) {
16
+ this.now = now;
17
+ this.nodeIdProvider = typeof nodeId === 'string' ? () => nodeId : nodeId;
18
+ if (path !== ':memory:')
19
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
20
+ this.raw = new DatabaseSync(path, {
21
+ enableForeignKeyConstraints: true,
22
+ timeout: 5_000,
23
+ defensive: true
24
+ });
25
+ this.raw.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
26
+ migrate(this.raw);
27
+ }
28
+ close() {
29
+ this.raw.close();
30
+ }
31
+ getRunnerDefaults() {
32
+ const rows = this.raw
33
+ .prepare('SELECT runner, model, effort, access FROM runner_defaults ORDER BY runner')
34
+ .all();
35
+ return rows.flatMap((row) => {
36
+ const parsed = runnerDefaultConfigurationSchema.safeParse({
37
+ runner: stringValue(row, 'runner'),
38
+ model: stringValue(row, 'model'),
39
+ effort: stringValue(row, 'effort'),
40
+ access: stringValue(row, 'access')
41
+ });
42
+ return parsed.success ? [parsed.data] : [];
43
+ });
44
+ }
45
+ saveRunnerDefaults(input) {
46
+ this.raw
47
+ .prepare(`INSERT INTO runner_defaults (runner, model, effort, access)
48
+ VALUES (?, ?, ?, ?)
49
+ ON CONFLICT(runner) DO UPDATE SET
50
+ model = excluded.model, effort = excluded.effort,
51
+ access = excluded.access`)
52
+ .run(input.runner, input.model, input.effort, input.access);
53
+ return input;
54
+ }
55
+ /**
56
+ * Stores only terminal lifecycle metadata. PTY input/output must remain in
57
+ * the Node-local bounded replay buffer and must never be passed here.
58
+ */
59
+ saveTerminalSession(input) {
60
+ this.raw
61
+ .prepare(`INSERT INTO terminal_sessions
62
+ (id, workspace_id, profile_id, state, exit_code, rows, cols, latest_sequence, created_at, updated_at)
63
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
64
+ ON CONFLICT(id) DO UPDATE SET workspace_id = excluded.workspace_id, profile_id = excluded.profile_id, state = excluded.state,
65
+ exit_code = excluded.exit_code, rows = excluded.rows, cols = excluded.cols,
66
+ latest_sequence = excluded.latest_sequence, updated_at = excluded.updated_at`)
67
+ .run(input.id, input.workspaceId, input.profileId, input.state, input.exitCode, input.rows, input.cols, input.latestSequence, input.createdAt, input.updatedAt);
68
+ return this.getTerminalSession(input.id);
69
+ }
70
+ getTerminalSession(id) {
71
+ const row = this.raw
72
+ .prepare(`SELECT id, workspace_id, profile_id, state, exit_code, rows, cols, latest_sequence, created_at, updated_at
73
+ FROM terminal_sessions WHERE id = ?`)
74
+ .get(id);
75
+ return row === undefined ? undefined : terminalSession(row);
76
+ }
77
+ listTerminalSessions() {
78
+ return this.raw
79
+ .prepare(`SELECT id, workspace_id, profile_id, state, exit_code, rows, cols, latest_sequence, created_at, updated_at
80
+ FROM terminal_sessions WHERE state = 'RUNNING' ORDER BY created_at ASC, id ASC`)
81
+ .all()
82
+ .map((row) => terminalSession(row));
83
+ }
84
+ /** A restart cannot safely reattach an orphaned PTY, so record it as closed. */
85
+ closeRunningTerminalSessions() {
86
+ this.raw
87
+ .prepare("UPDATE terminal_sessions SET state = 'CLOSED', updated_at = ? WHERE state = 'RUNNING'")
88
+ .run(this.now());
89
+ }
90
+ saveWorkspace(input) {
91
+ const current = this.raw.prepare('SELECT id FROM workspaces WHERE path = ?').get(input.path);
92
+ const workspace = {
93
+ id: current === undefined ? (input.id ?? randomUUID()) : stringValue(current, 'id'),
94
+ path: input.path,
95
+ kind: input.kind,
96
+ gitSummary: input.gitSummary
97
+ };
98
+ const timestamp = this.now();
99
+ this.raw
100
+ .prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at)
101
+ VALUES (?, ?, ?, ?, ?, ?)
102
+ ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,
103
+ git_summary = excluded.git_summary, updated_at = excluded.updated_at`)
104
+ .run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp);
105
+ return workspace;
106
+ }
107
+ listWorkspaces() {
108
+ return this.raw
109
+ .prepare('SELECT id, path, kind, git_summary FROM workspaces ORDER BY created_at ASC')
110
+ .all()
111
+ .map((row) => workspace(row));
112
+ }
113
+ getWorkspace(id) {
114
+ const row = this.raw
115
+ .prepare('SELECT id, path, kind, git_summary FROM workspaces WHERE id = ?')
116
+ .get(id);
117
+ return row === undefined ? undefined : workspace(row);
118
+ }
119
+ /** Unregisters a Workspace registration without deleting its local directory. */
120
+ removeWorkspace(id) {
121
+ const current = this.getWorkspace(id);
122
+ if (current === undefined)
123
+ throw new Error('WORKSPACE_NOT_FOUND');
124
+ this.raw.exec('BEGIN');
125
+ try {
126
+ // Session deletion cascades their runs, conversation and attachments. The workspace
127
+ // record itself never represents a filesystem deletion.
128
+ this.raw.prepare('DELETE FROM agent_sessions WHERE workspace_id = ?').run(id);
129
+ this.raw.prepare('DELETE FROM workspaces WHERE id = ?').run(id);
130
+ this.raw.exec('COMMIT');
131
+ return current;
132
+ }
133
+ catch (error) {
134
+ this.raw.exec('ROLLBACK');
135
+ throw error;
136
+ }
137
+ }
138
+ createAgentSession(input) {
139
+ if (this.getWorkspace(input.workspaceId) === undefined)
140
+ throw new Error('WORKSPACE_NOT_FOUND');
141
+ const session = {
142
+ id: randomUUID(),
143
+ nodeId: this.nodeIdProvider(),
144
+ workspaceId: input.workspaceId,
145
+ runner: input.runner,
146
+ externalSessionId: input.externalSessionId ?? null,
147
+ nativeControl: input.externalSessionId === undefined ? 'MAR_MANAGED' : (input.nativeControl ?? 'EXTERNAL'),
148
+ channelToken: null,
149
+ cwd: input.cwd,
150
+ model: optionValue(input.model),
151
+ effort: optionValue(input.effort),
152
+ access: optionValue(input.access),
153
+ customTitle: null,
154
+ runnerTitle: input.title?.trim().slice(0, 160) || null,
155
+ pinnedAt: null,
156
+ titleSource: input.title?.trim() ? 'RUNNER' : 'AUTO',
157
+ lastActivityAt: this.now(),
158
+ createdAt: this.now()
159
+ };
160
+ this.raw
161
+ .prepare(`INSERT INTO agent_sessions
162
+ (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
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
164
+ .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);
165
+ return session;
166
+ }
167
+ getAgentSession(id) {
168
+ 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 agent_sessions WHERE id = ?')
170
+ .get(id);
171
+ return row === undefined ? undefined : session(row, this.nodeIdProvider());
172
+ }
173
+ updateAgentSessionConfiguration(input) {
174
+ const result = this.raw
175
+ .prepare(`UPDATE agent_sessions
176
+ SET model = ?, effort = ?, access = ?, updated_at = ?
177
+ WHERE id = ?`)
178
+ .run(input.model, input.effort, input.access, this.now(), input.sessionId);
179
+ if (Number(result.changes) === 0)
180
+ throw new Error('SESSION_NOT_FOUND');
181
+ return this.getAgentSession(input.sessionId);
182
+ }
183
+ listAgentSessions() {
184
+ 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 agent_sessions ORDER BY pinned_at IS NULL, pinned_at DESC, last_activity_at DESC, id DESC')
186
+ .all()
187
+ .map((row) => session(row, this.nodeIdProvider()));
188
+ }
189
+ findAgentSessionByExternalSession(runner, externalSessionId) {
190
+ const row = this.raw
191
+ .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 agent_sessions WHERE runner = ? AND external_session_id = ?`)
193
+ .get(runner, externalSessionId);
194
+ return row === undefined ? undefined : session(row, this.nodeIdProvider());
195
+ }
196
+ setNativeControl(sessionId, nativeControl) {
197
+ const result = this.raw
198
+ .prepare('UPDATE agent_sessions SET native_control = ?, updated_at = ? WHERE id = ?')
199
+ .run(nativeControl, this.now(), sessionId);
200
+ if (Number(result.changes) === 0)
201
+ throw new Error('SESSION_NOT_FOUND');
202
+ }
203
+ setExternalSessionId(sessionId, externalSessionId) {
204
+ if (externalSessionId.length === 0)
205
+ throw new Error('SESSION_EXTERNAL_ID_INVALID');
206
+ const result = this.raw
207
+ .prepare("UPDATE agent_sessions SET external_session_id = ?, native_control = 'MAR_MANAGED', updated_at = ? WHERE id = ?")
208
+ .run(externalSessionId, this.now(), sessionId);
209
+ if (Number(result.changes) === 0)
210
+ throw new Error('SESSION_NOT_FOUND');
211
+ }
212
+ setChannelToken(sessionId, token) {
213
+ if (token.length < 16)
214
+ throw new Error('SESSION_CHANNEL_TOKEN_INVALID');
215
+ const result = this.raw
216
+ .prepare('UPDATE agent_sessions SET channel_token = ?, updated_at = ? WHERE id = ?')
217
+ .run(token, this.now(), sessionId);
218
+ if (Number(result.changes) === 0)
219
+ throw new Error('SESSION_NOT_FOUND');
220
+ }
221
+ updateSessionMetadata(input) {
222
+ const current = this.getAgentSession(input.sessionId);
223
+ if (current === undefined)
224
+ throw new Error('SESSION_NOT_FOUND');
225
+ const title = input.customTitle === undefined ? current.customTitle : input.customTitle?.trim() || null;
226
+ if (title !== null && title.length > 160)
227
+ throw new Error('SESSION_TITLE_INVALID');
228
+ const pinnedAt = input.pinned === undefined ? current.pinnedAt : input.pinned ? this.now() : null;
229
+ const titleSource = title === null ? 'AUTO' : 'CUSTOM';
230
+ this.raw
231
+ .prepare('UPDATE agent_sessions SET custom_title = ?, pinned_at = ?, title_source = ?, updated_at = ? WHERE id = ?')
232
+ .run(title, pinnedAt, titleSource, this.now(), input.sessionId);
233
+ return this.getAgentSession(input.sessionId);
234
+ }
235
+ getNativeSessionMetadata(runner, externalSessionId, workspaceId) {
236
+ const row = this.raw
237
+ .prepare(`SELECT custom_title, runner_title, runner_title_pending, pinned_at, model, effort, access FROM native_session_metadata
238
+ WHERE runner = ? AND external_session_id = ? AND workspace_id = ?`)
239
+ .get(runner, externalSessionId, workspaceId);
240
+ return row === undefined
241
+ ? undefined
242
+ : {
243
+ customTitle: nullableString(row, 'custom_title'),
244
+ runnerTitle: nullableString(row, 'runner_title'),
245
+ runnerTitlePending: row['runner_title_pending'] === 1,
246
+ pinnedAt: nullableNumber(row, 'pinned_at'),
247
+ model: nullableString(row, 'model'),
248
+ effort: nullableString(row, 'effort'),
249
+ access: nullableString(row, 'access')
250
+ };
251
+ }
252
+ saveNativeSessionMetadata(input) {
253
+ const current = this.getNativeSessionMetadata(input.runner, input.externalSessionId, input.workspaceId);
254
+ const customTitle = input.customTitle === undefined
255
+ ? (current?.customTitle ?? null)
256
+ : input.customTitle?.trim() || null;
257
+ const runnerTitle = input.runnerTitle === undefined
258
+ ? (current?.runnerTitle ?? null)
259
+ : input.runnerTitle?.trim() || null;
260
+ const runnerTitlePending = input.runnerTitlePending === undefined
261
+ ? (current?.runnerTitlePending ?? false)
262
+ : input.runnerTitlePending;
263
+ if (customTitle !== null && customTitle.length > 160)
264
+ throw new Error('SESSION_TITLE_INVALID');
265
+ if (runnerTitle !== null && runnerTitle.length > 160)
266
+ throw new Error('SESSION_TITLE_INVALID');
267
+ const pinnedAt = input.pinned === undefined ? (current?.pinnedAt ?? null) : input.pinned ? this.now() : null;
268
+ const model = input.model === undefined ? (current?.model ?? null) : input.model;
269
+ const effort = input.effort === undefined ? (current?.effort ?? null) : input.effort;
270
+ const access = input.access === undefined ? (current?.access ?? null) : input.access;
271
+ this.raw
272
+ .prepare(`INSERT INTO native_session_metadata
273
+ (runner, external_session_id, workspace_id, custom_title, runner_title, runner_title_pending, pinned_at, model, effort, access, updated_at)
274
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
275
+ ON CONFLICT(runner, external_session_id, workspace_id) DO UPDATE SET
276
+ custom_title = excluded.custom_title, runner_title = excluded.runner_title,
277
+ runner_title_pending = excluded.runner_title_pending, pinned_at = excluded.pinned_at,
278
+ model = excluded.model, effort = excluded.effort, access = excluded.access,
279
+ updated_at = excluded.updated_at`)
280
+ .run(input.runner, input.externalSessionId, input.workspaceId, customTitle, runnerTitle, runnerTitlePending ? 1 : 0, pinnedAt, model, effort, access, this.now());
281
+ return { customTitle, runnerTitle, runnerTitlePending, pinnedAt, model, effort, access };
282
+ }
283
+ /**
284
+ * Replaces only transcript-derived, run-less history. Managed Run turns are
285
+ * never touched by a vendor file scan.
286
+ */
287
+ syncNativeTranscript(sessionId, history) {
288
+ const current = this.getAgentSession(sessionId);
289
+ if (current === undefined)
290
+ throw new Error('SESSION_NOT_FOUND');
291
+ const checkpoint = this.raw
292
+ .prepare('SELECT digest FROM native_session_checkpoints WHERE session_id = ?')
293
+ .get(sessionId);
294
+ if (checkpoint !== undefined && stringValue(checkpoint, 'digest') === history.digest)
295
+ return false;
296
+ const now = this.now();
297
+ this.raw.exec('BEGIN IMMEDIATE');
298
+ try {
299
+ this.raw
300
+ .prepare('DELETE FROM conversation_turns WHERE session_id = ? AND run_id IS NULL')
301
+ .run(sessionId);
302
+ for (const [index, item] of history.items.entries()) {
303
+ if (item.kind !== 'message')
304
+ continue;
305
+ const turnId = randomUUID();
306
+ const itemId = randomUUID();
307
+ const createdAt = item.createdAt ?? now + index;
308
+ this.raw
309
+ .prepare(`INSERT INTO conversation_turns
310
+ (id, session_id, run_id, user_item_id, status, started_at, completed_at, created_at, updated_at)
311
+ VALUES (?, ?, NULL, ?, 'SUCCEEDED', ?, ?, ?, ?)`)
312
+ .run(turnId, sessionId, item.role === 'USER' ? itemId : null, createdAt, createdAt, createdAt, now);
313
+ this.raw
314
+ .prepare(`INSERT INTO conversation_items
315
+ (id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at, created_at, updated_at)
316
+ VALUES (?, ?, ?, NULL, NULL, ?, 0, ?, 'COMPLETED', ?, ?, ?, ?, ?)`)
317
+ .run(itemId, sessionId, turnId, index, item.role === 'USER' ? 'user_message' : 'assistant_message', JSON.stringify({ text: item.text, format: 'markdown', source: 'native-file' }), createdAt, createdAt, createdAt, now);
318
+ this.recordConversationItemChange(sessionId, itemId, 0);
319
+ }
320
+ const runnerTitle = history.title?.trim().slice(0, 160) || current.runnerTitle;
321
+ this.raw
322
+ .prepare(`UPDATE agent_sessions
323
+ SET runner_title = ?, title_source = CASE WHEN custom_title IS NULL THEN 'RUNNER' ELSE title_source END,
324
+ last_activity_at = CASE WHEN ? > last_activity_at THEN ? ELSE last_activity_at END, updated_at = ?
325
+ WHERE id = ?`)
326
+ .run(runnerTitle, now, now, now, sessionId);
327
+ this.raw
328
+ .prepare(`INSERT INTO native_session_checkpoints (session_id, digest, updated_at)
329
+ VALUES (?, ?, ?)
330
+ ON CONFLICT(session_id) DO UPDATE SET digest = excluded.digest, updated_at = excluded.updated_at`)
331
+ .run(sessionId, history.digest, now);
332
+ this.raw.exec('COMMIT');
333
+ return true;
334
+ }
335
+ catch (error) {
336
+ this.raw.exec('ROLLBACK');
337
+ throw error;
338
+ }
339
+ }
340
+ createMessageRun(input) {
341
+ const existing = this.raw
342
+ .prepare(`SELECT r.id, r.session_id, r.workspace_id, r.runner, r.status, r.recovery_state, r.version,
343
+ m.client_message_id, r.created_at, r.updated_at
344
+ FROM agent_runs r JOIN session_messages m ON m.id = r.message_id
345
+ WHERE m.session_id = ? AND m.client_message_id = ?`)
346
+ .get(input.sessionId, input.clientMessageId);
347
+ if (existing !== undefined)
348
+ return { run: run(existing, this.nodeIdProvider()), created: false };
349
+ const agentSession = this.getAgentSession(input.sessionId);
350
+ if (agentSession === undefined)
351
+ throw new Error('SESSION_NOT_FOUND');
352
+ const attachments = normalizeAttachments(input.attachments ?? []);
353
+ const value = {
354
+ id: randomUUID(),
355
+ sessionId: agentSession.id,
356
+ nodeId: this.nodeIdProvider(),
357
+ workspaceId: agentSession.workspaceId,
358
+ runner: agentSession.runner,
359
+ status: 'QUEUED',
360
+ recoveryState: 'ACTIVE',
361
+ version: 0,
362
+ clientMessageId: input.clientMessageId,
363
+ createdAt: this.now(),
364
+ updatedAt: this.now()
365
+ };
366
+ this.raw.exec('BEGIN IMMEDIATE');
367
+ try {
368
+ const messageId = randomUUID();
369
+ this.raw
370
+ .prepare(`INSERT INTO session_messages (id, session_id, client_message_id, role, content, created_at)
371
+ VALUES (?, ?, ?, 'USER', ?, ?)`)
372
+ .run(messageId, input.sessionId, input.clientMessageId, input.content, value.createdAt);
373
+ for (const attachment of attachments) {
374
+ this.raw
375
+ .prepare(`INSERT INTO message_attachments (id, message_id, name, mime, byte_size, data, created_at)
376
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
377
+ .run(randomUUID(), messageId, attachment.name, attachment.mime, attachment.data.length, attachment.data, value.createdAt);
378
+ }
379
+ this.raw
380
+ .prepare(`INSERT INTO agent_runs
381
+ (id, session_id, message_id, workspace_id, runner, status, recovery_state, version, created_at, updated_at)
382
+ VALUES (?, ?, ?, ?, ?, ?, 'ACTIVE', 0, ?, ?)`)
383
+ .run(value.id, value.sessionId, messageId, value.workspaceId, value.runner, value.status, value.createdAt, value.updatedAt);
384
+ this.raw
385
+ .prepare(`INSERT INTO workspace_leases (id, workspace_id, run_id, state, created_at, updated_at)
386
+ VALUES (?, ?, ?, 'ACTIVE', ?, ?)`)
387
+ .run(randomUUID(), value.workspaceId, value.id, value.createdAt, value.createdAt);
388
+ const turnId = randomUUID();
389
+ const userItemId = randomUUID();
390
+ this.raw
391
+ .prepare(`INSERT INTO conversation_turns
392
+ (id, session_id, run_id, user_item_id, status, model, effort, access, started_at, created_at, updated_at)
393
+ VALUES (?, ?, ?, ?, 'QUEUED', ?, ?, ?, NULL, ?, ?)`)
394
+ .run(turnId, value.sessionId, value.id, userItemId, agentSession.model, agentSession.effort, agentSession.access, value.createdAt, value.createdAt);
395
+ this.raw
396
+ .prepare(`INSERT INTO conversation_items
397
+ (id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at, created_at, updated_at)
398
+ VALUES (?, ?, ?, ?, NULL, NULL, 0, 'user_message', 'COMPLETED', ?, ?, ?, ?, ?)`)
399
+ .run(userItemId, value.sessionId, turnId, value.id, JSON.stringify({
400
+ clientMessageId: input.clientMessageId,
401
+ text: input.content,
402
+ contexts: [],
403
+ attachments: attachments.map(({ name, mime, data }) => ({
404
+ name,
405
+ mime,
406
+ byteSize: data.length
407
+ })),
408
+ delivery: 'ACCEPTED'
409
+ }), value.createdAt, value.createdAt, value.createdAt, value.createdAt);
410
+ this.recordConversationItemChange(value.sessionId, userItemId, 0);
411
+ this.raw
412
+ .prepare('UPDATE agent_sessions SET last_activity_at = ?, updated_at = ? WHERE id = ?')
413
+ .run(value.createdAt, value.createdAt, value.sessionId);
414
+ this.recordWorkspaceActivity(value.workspaceId, 'run.created', {
415
+ runId: value.id,
416
+ sessionId: value.sessionId,
417
+ runner: value.runner
418
+ });
419
+ this.raw.exec('COMMIT');
420
+ return { run: value, created: true };
421
+ }
422
+ catch (error) {
423
+ this.raw.exec('ROLLBACK');
424
+ if (isConstraint(error))
425
+ throw new Error('WORKSPACE_BUSY');
426
+ throw error;
427
+ }
428
+ }
429
+ getRun(id) {
430
+ const row = this.raw
431
+ .prepare(`SELECT r.id, r.session_id, r.workspace_id, r.runner, r.status, r.recovery_state, r.version,
432
+ m.client_message_id, r.created_at, r.updated_at
433
+ FROM agent_runs r JOIN session_messages m ON m.id = r.message_id WHERE r.id = ?`)
434
+ .get(id);
435
+ return row === undefined ? undefined : run(row, this.nodeIdProvider());
436
+ }
437
+ listRunsForSession(sessionId) {
438
+ if (this.getAgentSession(sessionId) === undefined)
439
+ throw new Error('SESSION_NOT_FOUND');
440
+ return this.raw
441
+ .prepare(`SELECT r.id, r.session_id, r.workspace_id, r.runner, r.status, r.recovery_state, r.version,
442
+ m.client_message_id, r.created_at, r.updated_at
443
+ FROM agent_runs r JOIN session_messages m ON m.id = r.message_id
444
+ WHERE r.session_id = ? ORDER BY r.created_at DESC, r.id DESC`)
445
+ .all(sessionId)
446
+ .map((row) => run(row, this.nodeIdProvider()));
447
+ }
448
+ getRunInput(id) {
449
+ const row = this.raw
450
+ .prepare('SELECT m.content FROM session_messages m JOIN agent_runs r ON r.message_id = m.id WHERE r.id = ?')
451
+ .get(id);
452
+ return row === undefined ? undefined : stringValue(row, 'content');
453
+ }
454
+ transitionRun(runId, next) {
455
+ const current = this.getRun(runId);
456
+ if (current === undefined)
457
+ throw new Error('RUN_NOT_FOUND');
458
+ if (current.recoveryState === 'LOST')
459
+ throw new Error('RUN_LOST');
460
+ if (!transitionAllowed(current.status, next))
461
+ throw new Error('RUN_STATE_INVALID');
462
+ const changed = this.raw
463
+ .prepare('UPDATE agent_runs SET status = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?')
464
+ .run(next, this.now(), runId, current.version);
465
+ if (Number(changed.changes) === 0)
466
+ throw new Error('RUN_VERSION_CONFLICT');
467
+ if (terminal(next))
468
+ this.releaseLease(runId);
469
+ return this.getRun(runId);
470
+ }
471
+ appendRunEvent(input) {
472
+ if (this.getRun(input.runId) === undefined)
473
+ throw new Error('RUN_NOT_FOUND');
474
+ const row = this.raw
475
+ .prepare('SELECT MAX(sequence) AS sequence FROM run_events WHERE run_id = ?')
476
+ .get(input.runId);
477
+ const lastSequence = row['sequence'] === null ? 0 : numberValue(row, 'sequence');
478
+ if (input.sequence > lastSequence + 1)
479
+ throw new Error('EVENT_SEQUENCE_GAP');
480
+ const timestamp = this.now();
481
+ const result = this.raw
482
+ .prepare(`INSERT INTO run_events (run_id, sequence, event_type, payload, created_at)
483
+ VALUES (?, ?, ?, ?, ?) ON CONFLICT(run_id, sequence) DO NOTHING`)
484
+ .run(input.runId, input.sequence, input.eventType, JSON.stringify(input.payload ?? null), timestamp);
485
+ if (Number(result.changes) === 0)
486
+ return undefined;
487
+ if (input.status !== undefined)
488
+ this.transitionRun(input.runId, input.status);
489
+ const stored = this.raw
490
+ .prepare('SELECT id, run_id, sequence, event_type, payload, created_at FROM run_events WHERE run_id = ? AND sequence = ?')
491
+ .get(input.runId, input.sequence);
492
+ const value = event(stored);
493
+ this.projectRunEvent(value, input.status);
494
+ const run = this.getRun(input.runId);
495
+ if (run !== undefined) {
496
+ this.recordWorkspaceActivity(run.workspaceId, 'run.updated', {
497
+ runId: run.id,
498
+ eventType: input.eventType,
499
+ status: input.status ?? null
500
+ });
501
+ }
502
+ return value;
503
+ }
504
+ listRunEvents(runId, after = 0) {
505
+ return this.raw
506
+ .prepare(`SELECT id, run_id, sequence, event_type, payload, created_at FROM run_events
507
+ WHERE run_id = ? AND sequence > ? ORDER BY sequence ASC`)
508
+ .all(runId, after)
509
+ .map((row) => event(row));
510
+ }
511
+ /**
512
+ * Returns durable final item snapshots. Run events stay internal to the
513
+ * replay path; callers never need to reconstruct native runner payloads.
514
+ */
515
+ listConversationTurns(input) {
516
+ if (this.getAgentSession(input.sessionId) === undefined)
517
+ throw new Error('SESSION_NOT_FOUND');
518
+ const limit = pageLimit(input.limit);
519
+ const cursor = decodeCursor(input.cursor, 'turn', input.sessionId);
520
+ const rows = this.raw
521
+ .prepare(`SELECT rowid AS cursor_id, id, session_id, run_id, user_item_id, status, model, effort, access, started_at, completed_at, created_at
522
+ FROM conversation_turns
523
+ WHERE session_id = ?
524
+ AND (? = 0 OR run_id IS NULL)
525
+ AND (? IS NULL OR rowid < ?)
526
+ ORDER BY rowid DESC LIMIT ?`)
527
+ .all(input.sessionId, input.nativeOnly === true ? 1 : 0, cursor?.value ?? null, cursor?.value ?? 0, limit + 1);
528
+ const page = rows.slice(0, limit);
529
+ const turns = page.map((row) => this.readConversationTurn(row));
530
+ const last = page.at(-1);
531
+ return {
532
+ turns,
533
+ nextCursor: rows.length > limit && last !== undefined
534
+ ? encodeCursor('turn', input.sessionId, numberValue(last, 'cursor_id'), '')
535
+ : null
536
+ };
537
+ }
538
+ hasNativeTranscript(sessionId) {
539
+ const row = this.raw
540
+ .prepare(`SELECT EXISTS(
541
+ SELECT 1 FROM native_session_checkpoints checkpoint
542
+ JOIN conversation_turns turn ON turn.session_id = checkpoint.session_id AND turn.run_id IS NULL
543
+ WHERE checkpoint.session_id = ?
544
+ ) AS found`)
545
+ .get(sessionId);
546
+ return numberValue(row, 'found') === 1;
547
+ }
548
+ /** Session detail cursor is intentionally distinct from history paging. */
549
+ listConversationItems(input) {
550
+ if (this.getAgentSession(input.sessionId) === undefined)
551
+ throw new Error('SESSION_NOT_FOUND');
552
+ const limit = pageLimit(input.limit);
553
+ const cursor = decodeCursor(input.cursor, 'session-item', input.sessionId);
554
+ const rows = this.raw
555
+ .prepare(`SELECT cie.id AS cursor_id, ci.id, ci.session_id, ci.turn_id, ci.run_id, ci.parent_item_id,
556
+ ci.source_sequence, ci.revision, ci.kind, ci.status, ci.payload, ci.started_at, ci.completed_at
557
+ FROM conversation_item_events cie
558
+ JOIN conversation_items ci ON ci.id = cie.item_id
559
+ WHERE cie.session_id = ? AND (? IS NULL OR cie.id > ?)
560
+ ORDER BY cie.id ASC LIMIT ?`)
561
+ .all(input.sessionId, cursor?.value ?? null, cursor?.value ?? 0, limit + 1);
562
+ const page = rows.slice(0, limit);
563
+ const last = page.at(-1);
564
+ return {
565
+ items: page.map((row) => conversationItem(row)),
566
+ nextCursor: rows.length > limit && last !== undefined
567
+ ? encodeCursor('session-item', input.sessionId, numberValue(last, 'cursor_id'), '')
568
+ : null
569
+ };
570
+ }
571
+ listWorkspaceActivity(input) {
572
+ if (this.getWorkspace(input.workspaceId) === undefined)
573
+ throw new Error('WORKSPACE_NOT_FOUND');
574
+ const limit = pageLimit(input.limit);
575
+ const cursor = decodeCursor(input.cursor, 'workspace-activity', input.workspaceId);
576
+ const rows = this.raw
577
+ .prepare(`SELECT id, workspace_id, event_type, payload, created_at FROM workspace_activity_events
578
+ WHERE workspace_id = ? AND (? IS NULL OR id > ?)
579
+ ORDER BY id ASC LIMIT ?`)
580
+ .all(input.workspaceId, cursor === undefined ? null : cursor.value, cursor?.value ?? 0, limit + 1);
581
+ const page = rows.slice(0, limit);
582
+ const last = page.at(-1);
583
+ return {
584
+ events: page.map((row) => workspaceActivityEvent(row)),
585
+ nextCursor: rows.length > limit && last !== undefined
586
+ ? encodeCursor('workspace-activity', input.workspaceId, numberValue(last, 'id'), '')
587
+ : null
588
+ };
589
+ }
590
+ createApproval(input) {
591
+ if (this.getRun(input.runId) === undefined)
592
+ throw new Error('RUN_NOT_FOUND');
593
+ if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= this.now())
594
+ throw new Error('APPROVAL_EXPIRED');
595
+ try {
596
+ this.raw
597
+ .prepare(`INSERT INTO approvals (id, run_id, status, payload, expires_at, created_at, updated_at)
598
+ VALUES (?, ?, 'PENDING', ?, ?, ?, ?)`)
599
+ .run(input.approvalId, input.runId, JSON.stringify(input.payload ?? null), input.expiresAt, this.now(), this.now());
600
+ }
601
+ catch (error) {
602
+ if (isConstraint(error))
603
+ throw new Error('APPROVAL_ALREADY_EXISTS');
604
+ throw error;
605
+ }
606
+ const result = this.getApproval(input.approvalId);
607
+ this.projectApproval(result);
608
+ return result;
609
+ }
610
+ getApproval(id) {
611
+ const row = this.raw
612
+ .prepare('SELECT id, run_id, status, payload, expires_at, created_at, decided_at FROM approvals WHERE id = ?')
613
+ .get(id);
614
+ if (row === undefined)
615
+ return undefined;
616
+ if (stringValue(row, 'status') === 'PENDING' && numberValue(row, 'expires_at') <= this.now()) {
617
+ return this.expireApproval(id);
618
+ }
619
+ return approval(row, this.now());
620
+ }
621
+ listApprovals(runId) {
622
+ return this.raw
623
+ .prepare('SELECT id FROM approvals WHERE run_id = ?')
624
+ .all(runId)
625
+ .map((row) => this.getApproval(stringValue(row, 'id')));
626
+ }
627
+ /** Persists an expiry so a later decision cannot resurrect an old prompt. */
628
+ expireApproval(id) {
629
+ const row = this.raw
630
+ .prepare('SELECT id, run_id, status, payload, expires_at, created_at, decided_at FROM approvals WHERE id = ?')
631
+ .get(id);
632
+ if (row === undefined)
633
+ throw new Error('APPROVAL_NOT_FOUND');
634
+ const storedStatus = stringValue(row, 'status');
635
+ if (storedStatus !== 'PENDING')
636
+ return approval(row, this.now());
637
+ if (numberValue(row, 'expires_at') > this.now())
638
+ throw new Error('APPROVAL_NOT_EXPIRED');
639
+ this.raw
640
+ .prepare(`UPDATE approvals SET status = 'EXPIRED', decided_at = ?, updated_at = ?
641
+ WHERE id = ? AND status = 'PENDING' AND expires_at <= ?`)
642
+ .run(this.now(), this.now(), id, this.now());
643
+ const result = this.getApproval(id);
644
+ this.projectApproval(result);
645
+ return result;
646
+ }
647
+ expireDueApprovals() {
648
+ const rows = this.raw
649
+ .prepare("SELECT id FROM approvals WHERE status = 'PENDING' AND expires_at <= ?")
650
+ .all(this.now());
651
+ return rows.map((row) => this.expireApproval(stringValue(row, 'id')));
652
+ }
653
+ decideApproval(id, decision) {
654
+ const current = this.getApproval(id);
655
+ if (current === undefined)
656
+ throw new Error('APPROVAL_NOT_FOUND');
657
+ if (current.status === 'EXPIRED')
658
+ throw new Error('APPROVAL_EXPIRED');
659
+ if (current.status !== 'PENDING')
660
+ throw new Error('APPROVAL_ALREADY_DECIDED');
661
+ const changed = this.raw
662
+ .prepare(`UPDATE approvals SET status = ?, decided_at = ?, updated_at = ?
663
+ WHERE id = ? AND status = 'PENDING' AND expires_at > ?`)
664
+ .run(decision, this.now(), this.now(), id, this.now());
665
+ if (Number(changed.changes) === 0) {
666
+ if (this.getApproval(id)?.status === 'EXPIRED')
667
+ throw new Error('APPROVAL_EXPIRED');
668
+ throw new Error('APPROVAL_ALREADY_DECIDED');
669
+ }
670
+ const result = this.getApproval(id);
671
+ this.projectApproval(result);
672
+ return result;
673
+ }
674
+ listNonterminalRuns() {
675
+ return this.raw
676
+ .prepare(`SELECT r.id, r.session_id, r.workspace_id, r.runner, r.status, r.recovery_state, r.version,
677
+ m.client_message_id, r.created_at, r.updated_at
678
+ FROM agent_runs r JOIN session_messages m ON m.id = r.message_id
679
+ WHERE r.status NOT IN ('SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED')
680
+ ORDER BY r.created_at ASC`)
681
+ .all()
682
+ .map((row) => run(row, this.nodeIdProvider()));
683
+ }
684
+ lastRunSequence(runId) {
685
+ const row = this.raw
686
+ .prepare('SELECT MAX(sequence) AS sequence FROM run_events WHERE run_id = ?')
687
+ .get(runId);
688
+ return row['sequence'] === null ? 0 : numberValue(row, 'sequence');
689
+ }
690
+ saveSnapshot(runId, snapshot) {
691
+ this.raw
692
+ .prepare(`INSERT INTO run_snapshots (run_id, snapshot, created_at) VALUES (?, ?, ?)
693
+ ON CONFLICT(run_id) DO UPDATE SET snapshot = excluded.snapshot, created_at = excluded.created_at`)
694
+ .run(runId, JSON.stringify(snapshot), this.now());
695
+ }
696
+ getSnapshot(runId) {
697
+ const row = this.raw
698
+ .prepare('SELECT snapshot FROM run_snapshots WHERE run_id = ?')
699
+ .get(runId);
700
+ return row === undefined ? undefined : JSON.parse(stringValue(row, 'snapshot'));
701
+ }
702
+ markActiveRunsSuspect() {
703
+ this.raw
704
+ .prepare("UPDATE agent_runs SET recovery_state = 'SUSPECT', recovery_updated_at = ? WHERE status NOT IN ('SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED')")
705
+ .run(this.now());
706
+ this.raw
707
+ .prepare("UPDATE workspace_leases SET state = 'SUSPECT', updated_at = ? WHERE state = 'ACTIVE'")
708
+ .run(this.now());
709
+ }
710
+ hasActiveWorkspaceLease(workspaceId) {
711
+ const row = this.raw
712
+ .prepare("SELECT 1 AS present FROM workspace_leases WHERE workspace_id = ? AND state IN ('ACTIVE', 'SUSPECT') LIMIT 1")
713
+ .get(workspaceId);
714
+ return row !== undefined;
715
+ }
716
+ reconcileRun(runId, active) {
717
+ const current = this.getRun(runId);
718
+ if (current === undefined || terminal(current.status))
719
+ return;
720
+ if (active) {
721
+ this.raw
722
+ .prepare("UPDATE agent_runs SET recovery_state = 'ACTIVE', recovery_updated_at = ? WHERE id = ?")
723
+ .run(this.now(), runId);
724
+ this.raw
725
+ .prepare("UPDATE workspace_leases SET state = 'ACTIVE', updated_at = ? WHERE run_id = ? AND state = 'SUSPECT'")
726
+ .run(this.now(), runId);
727
+ return;
728
+ }
729
+ this.raw
730
+ .prepare("UPDATE agent_runs SET recovery_state = 'LOST', recovery_updated_at = ? WHERE id = ?")
731
+ .run(this.now(), runId);
732
+ this.releaseLease(runId);
733
+ }
734
+ releaseLease(runId) {
735
+ this.raw
736
+ .prepare("UPDATE workspace_leases SET state = 'RELEASED', updated_at = ? WHERE run_id = ? AND state IN ('ACTIVE', 'SUSPECT')")
737
+ .run(this.now(), runId);
738
+ }
739
+ recordWorkspaceActivity(workspaceId, eventType, payload) {
740
+ this.raw
741
+ .prepare(`INSERT INTO workspace_activity_events (workspace_id, event_type, payload, created_at)
742
+ VALUES (?, ?, ?, ?)`)
743
+ .run(workspaceId, eventType, JSON.stringify(payload), this.now());
744
+ }
745
+ recordConversationItemChange(sessionId, itemId, revision) {
746
+ this.raw
747
+ .prepare(`INSERT INTO conversation_item_events (session_id, item_id, revision, created_at)
748
+ VALUES (?, ?, ?, ?)`)
749
+ .run(sessionId, itemId, revision, this.now());
750
+ }
751
+ projectApproval(value) {
752
+ const turn = this.raw
753
+ .prepare('SELECT id, session_id FROM conversation_turns WHERE run_id = ?')
754
+ .get(value.runId);
755
+ if (turn === undefined)
756
+ return;
757
+ const sessionId = stringValue(turn, 'session_id');
758
+ const turnId = stringValue(turn, 'id');
759
+ const status = value.status === 'PENDING'
760
+ ? 'PENDING'
761
+ : value.status === 'APPROVED'
762
+ ? 'COMPLETED'
763
+ : 'DECLINED';
764
+ const payload = value.payload;
765
+ const source = typeof payload?.method === 'string' ? payload.method : payload?.toolName;
766
+ const category = source === 'item/commandExecution/requestApproval' || source === 'Bash'
767
+ ? 'COMMAND'
768
+ : source === 'item/fileChange/requestApproval'
769
+ ? 'FILE_CHANGE'
770
+ : 'TOOL';
771
+ const timestamp = this.now();
772
+ this.raw
773
+ .prepare(`INSERT INTO conversation_items
774
+ (id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at, created_at, updated_at)
775
+ VALUES (?, ?, ?, ?, NULL, NULL, 0, 'approval', ?, ?, ?, ?, ?, ?)
776
+ ON CONFLICT(id) DO UPDATE SET revision = conversation_items.revision + 1,
777
+ status = excluded.status, payload = excluded.payload, completed_at = excluded.completed_at, updated_at = excluded.updated_at`)
778
+ .run(value.id, sessionId, turnId, value.runId, status, JSON.stringify({
779
+ approvalId: value.id,
780
+ category,
781
+ reason: null,
782
+ expiresAt: value.expiresAt,
783
+ resolvedDecision: value.status === 'PENDING' ? null : value.status
784
+ }), value.createdAt, value.status === 'PENDING' ? null : timestamp, value.createdAt, timestamp);
785
+ const item = this.raw
786
+ .prepare('SELECT revision FROM conversation_items WHERE id = ?')
787
+ .get(value.id);
788
+ this.recordConversationItemChange(sessionId, value.id, numberValue(item, 'revision'));
789
+ }
790
+ readConversationTurn(row) {
791
+ const turnId = stringValue(row, 'id');
792
+ const items = this.raw
793
+ .prepare(`SELECT id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at
794
+ FROM conversation_items WHERE turn_id = ?
795
+ ORDER BY source_sequence IS NOT NULL, source_sequence ASC, created_at ASC, id ASC`)
796
+ .all(turnId)
797
+ .map((item) => conversationItem(item));
798
+ return {
799
+ id: turnId,
800
+ sessionId: stringValue(row, 'session_id'),
801
+ runId: nullableString(row, 'run_id'),
802
+ userItemId: nullableString(row, 'user_item_id'),
803
+ status: stringValue(row, 'status'),
804
+ model: nullableString(row, 'model'),
805
+ effort: nullableString(row, 'effort'),
806
+ access: nullableString(row, 'access'),
807
+ items,
808
+ startedAt: nullableNumber(row, 'started_at'),
809
+ completedAt: nullableNumber(row, 'completed_at')
810
+ };
811
+ }
812
+ projectRunEvent(event, status) {
813
+ const turn = this.raw
814
+ .prepare('SELECT id, session_id, started_at FROM conversation_turns WHERE run_id = ?')
815
+ .get(event.runId);
816
+ if (turn === undefined)
817
+ return;
818
+ const turnId = stringValue(turn, 'id');
819
+ const sessionId = stringValue(turn, 'session_id');
820
+ const timestamp = this.now();
821
+ const payload = event.payload;
822
+ if ((event.eventType === 'text' || event.eventType === 'text.delta') &&
823
+ typeof payload?.text === 'string') {
824
+ const itemId = `${event.runId}:assistant`;
825
+ const existing = this.raw
826
+ .prepare('SELECT payload, revision FROM conversation_items WHERE id = ?')
827
+ .get(itemId);
828
+ const previous = existing === undefined
829
+ ? ''
830
+ : (JSON.parse(stringValue(existing, 'payload')).text ?? '');
831
+ const text = `${typeof previous === 'string' ? previous : ''}${payload.text}`;
832
+ const revision = existing === undefined ? 0 : numberValue(existing, 'revision') + 1;
833
+ this.raw
834
+ .prepare(`INSERT INTO conversation_items
835
+ (id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at, created_at, updated_at)
836
+ VALUES (?, ?, ?, ?, NULL, ?, ?, 'assistant_message', 'IN_PROGRESS', ?, ?, NULL, ?, ?)
837
+ ON CONFLICT(id) DO UPDATE SET source_sequence = excluded.source_sequence, revision = excluded.revision,
838
+ payload = excluded.payload, updated_at = excluded.updated_at`)
839
+ .run(itemId, sessionId, turnId, event.runId, event.sequence, revision, JSON.stringify({ text, format: 'markdown', model: null }), timestamp, timestamp, timestamp);
840
+ this.recordConversationItemChange(sessionId, itemId, revision);
841
+ }
842
+ if (event.eventType === 'tool' && typeof payload?.name === 'string') {
843
+ this.raw
844
+ .prepare(`INSERT INTO conversation_items
845
+ (id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at, created_at, updated_at)
846
+ VALUES (?, ?, ?, ?, NULL, ?, 0, 'tool_call', 'COMPLETED', ?, ?, ?, ?, ?)`)
847
+ .run(`${event.runId}:tool:${event.sequence}`, sessionId, turnId, event.runId, event.sequence, JSON.stringify({
848
+ namespace: 'runner',
849
+ toolName: payload.name,
850
+ title: payload.name,
851
+ inputSummary: payload.input === undefined ? null : JSON.stringify(payload.input),
852
+ outputSummary: null,
853
+ progressLabel: null,
854
+ errorCode: null,
855
+ truncated: false
856
+ }), timestamp, timestamp, timestamp, timestamp);
857
+ this.recordConversationItemChange(sessionId, `${event.runId}:tool:${event.sequence}`, 0);
858
+ }
859
+ if (status !== undefined) {
860
+ const turnStatus = status === 'SUCCEEDED'
861
+ ? 'SUCCEEDED'
862
+ : status === 'FAILED'
863
+ ? 'FAILED'
864
+ : status === 'CANCELLED'
865
+ ? 'CANCELLED'
866
+ : status === 'INTERRUPTED'
867
+ ? 'INTERRUPTED'
868
+ : status === 'STARTING' || status === 'RUNNING'
869
+ ? 'RUNNING'
870
+ : 'QUEUED';
871
+ const complete = terminal(status);
872
+ this.raw
873
+ .prepare('UPDATE conversation_turns SET status = ?, started_at = COALESCE(started_at, ?), completed_at = ?, updated_at = ? WHERE id = ?')
874
+ .run(turnStatus, timestamp, complete ? timestamp : null, timestamp, turnId);
875
+ if (complete) {
876
+ const pendingItems = this.raw
877
+ .prepare("SELECT id FROM conversation_items WHERE turn_id = ? AND status IN ('PENDING', 'IN_PROGRESS')")
878
+ .all(turnId);
879
+ this.raw
880
+ .prepare("UPDATE conversation_items SET status = ?, revision = revision + 1, completed_at = ?, updated_at = ? WHERE turn_id = ? AND status IN ('PENDING', 'IN_PROGRESS')")
881
+ .run(status === 'SUCCEEDED'
882
+ ? 'COMPLETED'
883
+ : status === 'FAILED'
884
+ ? 'FAILED'
885
+ : status === 'CANCELLED'
886
+ ? 'CANCELLED'
887
+ : 'INTERRUPTED', timestamp, timestamp, turnId);
888
+ for (const item of pendingItems) {
889
+ const itemId = stringValue(item, 'id');
890
+ const current = this.raw
891
+ .prepare('SELECT revision FROM conversation_items WHERE id = ?')
892
+ .get(itemId);
893
+ this.recordConversationItemChange(sessionId, itemId, numberValue(current, 'revision'));
894
+ }
895
+ const summaryId = `${event.runId}:summary`;
896
+ const startedAt = nullableNumber(turn, 'started_at');
897
+ const durationMs = startedAt === null ? null : Math.max(0, timestamp - startedAt);
898
+ this.raw
899
+ .prepare(`INSERT INTO conversation_items
900
+ (id, session_id, turn_id, run_id, parent_item_id, source_sequence, revision, kind, status, payload, started_at, completed_at, created_at, updated_at)
901
+ VALUES (?, ?, ?, ?, NULL, ?, 0, 'turn_summary', 'COMPLETED', ?, ?, ?, ?, ?)
902
+ ON CONFLICT(id) DO UPDATE SET revision = conversation_items.revision + 1,
903
+ status = excluded.status, payload = excluded.payload, completed_at = excluded.completed_at, updated_at = excluded.updated_at`)
904
+ .run(summaryId, sessionId, turnId, event.runId, event.sequence, JSON.stringify({ status, durationMs }), startedAt ?? timestamp, timestamp, timestamp, timestamp);
905
+ const summary = this.raw
906
+ .prepare('SELECT revision FROM conversation_items WHERE id = ?')
907
+ .get(summaryId);
908
+ this.recordConversationItemChange(sessionId, summaryId, numberValue(summary, 'revision'));
909
+ }
910
+ }
911
+ this.raw
912
+ .prepare('UPDATE agent_sessions SET last_activity_at = ?, updated_at = ? WHERE id = ?')
913
+ .run(timestamp, timestamp, sessionId);
914
+ }
915
+ nodeIdProvider;
916
+ }
917
+ function migrate(database) {
918
+ database.exec(`CREATE TABLE IF NOT EXISTS node_schema_migrations (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL) STRICT;
919
+ CREATE TABLE IF NOT EXISTS workspaces (
920
+ id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, kind TEXT NOT NULL CHECK (kind IN ('DIRECTORY', 'GIT_WORKSPACE')),
921
+ git_summary TEXT,
922
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
923
+ ) STRICT;
924
+ CREATE TABLE IF NOT EXISTS agent_sessions (
925
+ id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(id),
926
+ runner TEXT NOT NULL CHECK (runner IN ('codex', 'claude-code')), external_session_id TEXT,
927
+ native_control TEXT NOT NULL DEFAULT 'EXTERNAL' CHECK (native_control IN ('MAR_MANAGED', 'EXTERNAL')), channel_token TEXT,
928
+ cwd TEXT NOT NULL, model TEXT, effort TEXT, access TEXT, custom_title TEXT, runner_title TEXT, pinned_at INTEGER,
929
+ title_source TEXT NOT NULL DEFAULT 'AUTO' CHECK (title_source IN ('AUTO', 'RUNNER', 'CUSTOM')),
930
+ last_activity_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
931
+ ) STRICT;
932
+ CREATE TABLE IF NOT EXISTS native_session_checkpoints (
933
+ session_id TEXT PRIMARY KEY REFERENCES agent_sessions(id) ON DELETE CASCADE,
934
+ digest TEXT NOT NULL, updated_at INTEGER NOT NULL
935
+ ) STRICT;
936
+ CREATE TABLE IF NOT EXISTS native_session_metadata (
937
+ runner TEXT NOT NULL CHECK (runner IN ('codex', 'claude-code')),
938
+ external_session_id TEXT NOT NULL, workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
939
+ custom_title TEXT, runner_title TEXT, runner_title_pending INTEGER NOT NULL DEFAULT 0 CHECK (runner_title_pending IN (0, 1)), pinned_at INTEGER,
940
+ model TEXT, effort TEXT, access TEXT, updated_at INTEGER NOT NULL,
941
+ PRIMARY KEY (runner, external_session_id, workspace_id)
942
+ ) STRICT;
943
+ CREATE TABLE IF NOT EXISTS session_messages (
944
+ id TEXT PRIMARY KEY, session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
945
+ client_message_id TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('USER', 'ASSISTANT', 'SYSTEM')),
946
+ content TEXT NOT NULL, created_at INTEGER NOT NULL, UNIQUE(session_id, client_message_id)
947
+ ) STRICT;
948
+ CREATE TABLE IF NOT EXISTS message_attachments (
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`);
1084
+ }
1085
+ }
1086
+ // Pre-existing projections did not retain a detail cursor stream. Seed one
1087
+ // final snapshot event per item; native runner payload is never reconstructed.
1088
+ database.exec(`INSERT INTO conversation_item_events (session_id, item_id, revision, created_at)
1089
+ SELECT ci.session_id, ci.id, ci.revision, ci.updated_at
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)')
1096
+ .all();
1097
+ if (!terminalColumns.some((column) => column['name'] === 'workspace_id')) {
1098
+ database.exec('ALTER TABLE terminal_sessions ADD COLUMN workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL');
1099
+ }
1100
+ const approvalDefinition = database
1101
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'approvals'")
1102
+ .get();
1103
+ if (approvalDefinition !== undefined &&
1104
+ !stringValue(approvalDefinition, 'sql').includes("'EXPIRED'")) {
1105
+ database.exec(`BEGIN IMMEDIATE;
1106
+ CREATE TABLE approvals_next (
1107
+ id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
1108
+ status TEXT NOT NULL CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED')), payload TEXT NOT NULL,
1109
+ expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, decided_at INTEGER
1110
+ ) STRICT;
1111
+ INSERT INTO approvals_next (id, run_id, status, payload, expires_at, created_at, updated_at, decided_at)
1112
+ SELECT id, run_id, status, payload, expires_at, created_at, updated_at, decided_at FROM approvals;
1113
+ DROP TABLE approvals;
1114
+ ALTER TABLE approvals_next RENAME TO approvals;
1115
+ COMMIT;`);
1116
+ }
1117
+ }
1118
+ function workspace(row) {
1119
+ const summary = nullableString(row, 'git_summary');
1120
+ return {
1121
+ id: stringValue(row, 'id'),
1122
+ path: stringValue(row, 'path'),
1123
+ kind: stringValue(row, 'kind'),
1124
+ gitSummary: summary === null ? null : JSON.parse(summary)
1125
+ };
1126
+ }
1127
+ function terminalSession(row) {
1128
+ const state = stringValue(row, 'state');
1129
+ if (state !== 'RUNNING' && state !== 'EXITED' && state !== 'CLOSED')
1130
+ throw new Error('DATABASE_VALUE_INVALID:terminal_state');
1131
+ return {
1132
+ id: stringValue(row, 'id'),
1133
+ workspaceId: nullableString(row, 'workspace_id'),
1134
+ profileId: stringValue(row, 'profile_id'),
1135
+ state,
1136
+ exitCode: nullableNumber(row, 'exit_code'),
1137
+ rows: numberValue(row, 'rows'),
1138
+ cols: numberValue(row, 'cols'),
1139
+ latestSequence: numberValue(row, 'latest_sequence'),
1140
+ createdAt: numberValue(row, 'created_at'),
1141
+ updatedAt: numberValue(row, 'updated_at')
1142
+ };
1143
+ }
1144
+ function session(row, nodeId) {
1145
+ return {
1146
+ id: stringValue(row, 'id'),
1147
+ nodeId,
1148
+ workspaceId: stringValue(row, 'workspace_id'),
1149
+ runner: stringValue(row, 'runner'),
1150
+ externalSessionId: nullableString(row, 'external_session_id'),
1151
+ nativeControl: stringValue(row, 'native_control') === 'MAR_MANAGED' ? 'MAR_MANAGED' : 'EXTERNAL',
1152
+ channelToken: nullableString(row, 'channel_token'),
1153
+ cwd: stringValue(row, 'cwd'),
1154
+ model: nullableString(row, 'model'),
1155
+ effort: nullableString(row, 'effort'),
1156
+ access: nullableString(row, 'access'),
1157
+ customTitle: nullableString(row, 'custom_title'),
1158
+ runnerTitle: nullableString(row, 'runner_title'),
1159
+ pinnedAt: row['pinned_at'] === null ? null : numberValue(row, 'pinned_at'),
1160
+ titleSource: stringValue(row, 'title_source') === 'CUSTOM'
1161
+ ? 'CUSTOM'
1162
+ : stringValue(row, 'title_source') === 'RUNNER'
1163
+ ? 'RUNNER'
1164
+ : 'AUTO',
1165
+ lastActivityAt: numberValue(row, 'last_activity_at'),
1166
+ createdAt: numberValue(row, 'created_at')
1167
+ };
1168
+ }
1169
+ function run(row, nodeId) {
1170
+ const recovery = stringValue(row, 'recovery_state');
1171
+ return {
1172
+ id: stringValue(row, 'id'),
1173
+ sessionId: stringValue(row, 'session_id'),
1174
+ nodeId,
1175
+ workspaceId: stringValue(row, 'workspace_id'),
1176
+ runner: stringValue(row, 'runner'),
1177
+ status: stringValue(row, 'status'),
1178
+ recoveryState: recovery === 'SUSPECT' || recovery === 'LOST' ? recovery : 'ACTIVE',
1179
+ version: numberValue(row, 'version'),
1180
+ clientMessageId: stringValue(row, 'client_message_id'),
1181
+ createdAt: numberValue(row, 'created_at'),
1182
+ updatedAt: numberValue(row, 'updated_at')
1183
+ };
1184
+ }
1185
+ function event(row) {
1186
+ return {
1187
+ id: numberValue(row, 'id'),
1188
+ runId: stringValue(row, 'run_id'),
1189
+ sequence: numberValue(row, 'sequence'),
1190
+ eventType: stringValue(row, 'event_type'),
1191
+ payload: JSON.parse(stringValue(row, 'payload')),
1192
+ createdAt: numberValue(row, 'created_at')
1193
+ };
1194
+ }
1195
+ function conversationItem(row) {
1196
+ return {
1197
+ id: stringValue(row, 'id'),
1198
+ sessionId: stringValue(row, 'session_id'),
1199
+ turnId: stringValue(row, 'turn_id'),
1200
+ runId: nullableString(row, 'run_id'),
1201
+ parentItemId: nullableString(row, 'parent_item_id'),
1202
+ sourceSequence: nullableNumber(row, 'source_sequence'),
1203
+ revision: numberValue(row, 'revision'),
1204
+ kind: stringValue(row, 'kind'),
1205
+ status: stringValue(row, 'status'),
1206
+ payload: JSON.parse(stringValue(row, 'payload')),
1207
+ startedAt: nullableNumber(row, 'started_at'),
1208
+ completedAt: nullableNumber(row, 'completed_at')
1209
+ };
1210
+ }
1211
+ function workspaceActivityEvent(row) {
1212
+ return {
1213
+ id: numberValue(row, 'id'),
1214
+ workspaceId: stringValue(row, 'workspace_id'),
1215
+ eventType: stringValue(row, 'event_type'),
1216
+ payload: JSON.parse(stringValue(row, 'payload')),
1217
+ createdAt: numberValue(row, 'created_at')
1218
+ };
1219
+ }
1220
+ function pageLimit(value) {
1221
+ if (value === undefined)
1222
+ return 100;
1223
+ if (!Number.isSafeInteger(value) || value < 1 || value > 500)
1224
+ throw new Error('EVENT_CURSOR_INVALID');
1225
+ return value;
1226
+ }
1227
+ function encodeCursor(scope, streamId, value, id) {
1228
+ return Buffer.from(JSON.stringify({ scope, streamId, value, id }), 'utf8').toString('base64url');
1229
+ }
1230
+ function decodeCursor(token, scope, streamId) {
1231
+ if (token === undefined)
1232
+ return undefined;
1233
+ try {
1234
+ const value = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
1235
+ if (value.scope !== scope ||
1236
+ value.streamId !== streamId ||
1237
+ typeof value.value !== 'number' ||
1238
+ !Number.isSafeInteger(value.value) ||
1239
+ value.value < 0 ||
1240
+ typeof value.id !== 'string')
1241
+ throw new Error('EVENT_CURSOR_INVALID');
1242
+ return { value: value.value, id: value.id };
1243
+ }
1244
+ catch {
1245
+ throw new Error('EVENT_CURSOR_INVALID');
1246
+ }
1247
+ }
1248
+ function approval(row, now) {
1249
+ const expiresAt = numberValue(row, 'expires_at');
1250
+ const stored = stringValue(row, 'status');
1251
+ return {
1252
+ id: stringValue(row, 'id'),
1253
+ runId: stringValue(row, 'run_id'),
1254
+ status: stored === 'PENDING' && expiresAt <= now ? 'EXPIRED' : stored,
1255
+ payload: JSON.parse(stringValue(row, 'payload')),
1256
+ expiresAt,
1257
+ createdAt: numberValue(row, 'created_at'),
1258
+ decidedAt: row['decided_at'] === null ? null : numberValue(row, 'decided_at')
1259
+ };
1260
+ }
1261
+ function stringValue(row, key) {
1262
+ const value = row[key];
1263
+ if (typeof value !== 'string')
1264
+ throw new Error(`DATABASE_VALUE_INVALID:${key}`);
1265
+ return value;
1266
+ }
1267
+ function nullableString(row, key) {
1268
+ const value = row[key];
1269
+ if (value === null || typeof value === 'string')
1270
+ return value;
1271
+ throw new Error(`DATABASE_VALUE_INVALID:${key}`);
1272
+ }
1273
+ function optionValue(value) {
1274
+ if (value === undefined)
1275
+ return null;
1276
+ const normalized = value.trim();
1277
+ if (normalized.length === 0 || normalized.length > 120)
1278
+ throw new Error('SESSION_OPTION_INVALID');
1279
+ return normalized;
1280
+ }
1281
+ function normalizeAttachments(attachments) {
1282
+ if (attachments.length > 5)
1283
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
1284
+ let total = 0;
1285
+ return attachments.map((attachment) => {
1286
+ if (attachment.name.trim().length === 0 ||
1287
+ attachment.name.length > 255 ||
1288
+ attachment.mime.length > 120 ||
1289
+ !/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9!#$&^_.+-]+$/.test(attachment.mime))
1290
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
1291
+ if (attachment.dataBase64.length > 7 * 1024 * 1024 ||
1292
+ attachment.dataBase64.length % 4 !== 0 ||
1293
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(attachment.dataBase64))
1294
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
1295
+ let data;
1296
+ try {
1297
+ data = Buffer.from(attachment.dataBase64, 'base64');
1298
+ }
1299
+ catch {
1300
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
1301
+ }
1302
+ if (data.length === 0 || data.length > 5 * 1024 * 1024)
1303
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
1304
+ total += data.length;
1305
+ if (total > 10 * 1024 * 1024)
1306
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
1307
+ return { name: attachment.name.trim(), mime: attachment.mime, data };
1308
+ });
1309
+ }
1310
+ function nullableNumber(row, key) {
1311
+ const value = row[key];
1312
+ if (value === null || typeof value === 'number')
1313
+ return value;
1314
+ throw new Error(`DATABASE_VALUE_INVALID:${key}`);
1315
+ }
1316
+ function numberValue(row, key) {
1317
+ const value = row[key];
1318
+ if (typeof value !== 'number')
1319
+ throw new Error(`DATABASE_VALUE_INVALID:${key}`);
1320
+ return value;
1321
+ }
1322
+ function isConstraint(error) {
1323
+ return error instanceof Error && /constraint/i.test(error.message);
1324
+ }
1325
+ function terminal(status) {
1326
+ return (status === 'SUCCEEDED' ||
1327
+ status === 'FAILED' ||
1328
+ status === 'CANCELLED' ||
1329
+ status === 'INTERRUPTED');
1330
+ }
1331
+ function transitionAllowed(current, next) {
1332
+ if (current === next)
1333
+ return true;
1334
+ if (terminal(current))
1335
+ return false;
1336
+ return ((current === 'QUEUED' && (next === 'STARTING' || next === 'CANCELLED' || next === 'FAILED')) ||
1337
+ (current === 'STARTING' &&
1338
+ (next === 'RUNNING' || next === 'CANCELLING' || next === 'FAILED' || next === 'CANCELLED')) ||
1339
+ (current === 'RUNNING' &&
1340
+ (next === 'CANCELLING' ||
1341
+ next === 'SUCCEEDED' ||
1342
+ next === 'FAILED' ||
1343
+ next === 'INTERRUPTED')) ||
1344
+ (current === 'CANCELLING' &&
1345
+ (next === 'CANCELLED' || next === 'FAILED' || next === 'INTERRUPTED')));
1346
+ }
1347
+ //# sourceMappingURL=database.js.map