@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.
- package/README.md +17 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +101 -0
- package/dist/claude-agent-sdk.js +341 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/claude-channel.d.ts +28 -0
- package/dist/claude-channel.js +45 -0
- package/dist/claude-channel.js.map +1 -0
- package/dist/codex-app-server.d.ts +96 -0
- package/dist/codex-app-server.js +361 -0
- package/dist/codex-app-server.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -0
- package/dist/connector.d.ts +343 -0
- package/dist/connector.js +5525 -0
- package/dist/connector.js.map +1 -0
- package/dist/database.d.ts +282 -0
- package/dist/database.js +1347 -0
- package/dist/database.js.map +1 -0
- package/dist/event-buffer.d.ts +12 -0
- package/dist/event-buffer.js +29 -0
- package/dist/event-buffer.js.map +1 -0
- package/dist/fake-runner.d.ts +42 -0
- package/dist/fake-runner.js +130 -0
- package/dist/fake-runner.js.map +1 -0
- package/dist/health.d.ts +4 -0
- package/dist/health.js +4 -0
- package/dist/health.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +47 -0
- package/dist/main.js.map +1 -0
- package/dist/native-session-history.d.ts +72 -0
- package/dist/native-session-history.js +647 -0
- package/dist/native-session-history.js.map +1 -0
- package/dist/operational.d.ts +30 -0
- package/dist/operational.js +82 -0
- package/dist/operational.js.map +1 -0
- package/dist/process-tree.d.ts +9 -0
- package/dist/process-tree.js +19 -0
- package/dist/process-tree.js.map +1 -0
- package/dist/runner-command-engine.d.ts +19 -0
- package/dist/runner-command-engine.js +47 -0
- package/dist/runner-command-engine.js.map +1 -0
- package/dist/runner-profiles.d.ts +11 -0
- package/dist/runner-profiles.js +138 -0
- package/dist/runner-profiles.js.map +1 -0
- package/dist/runner-usage.d.ts +33 -0
- package/dist/runner-usage.js +193 -0
- package/dist/runner-usage.js.map +1 -0
- package/dist/runtime-state.d.ts +174 -0
- package/dist/runtime-state.js +957 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/service.d.ts +4 -0
- package/dist/service.js +39 -0
- package/dist/service.js.map +1 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +33 -0
- package/dist/storage.js.map +1 -0
- package/dist/terminal.d.ts +132 -0
- package/dist/terminal.js +417 -0
- package/dist/terminal.js.map +1 -0
- package/dist/workspace.d.ts +116 -0
- package/dist/workspace.js +732 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +36 -0
package/dist/database.js
ADDED
|
@@ -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
|