agentgui 1.0.831 → 1.0.832
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/CHANGELOG.md +6 -0
- package/database-migrations-acp.js +133 -0
- package/database-migrations.js +149 -0
- package/database-schema.js +175 -0
- package/database.js +80 -650
- package/lib/ws-handlers-conv.js +0 -144
- package/lib/ws-handlers-conv2.js +169 -0
- package/package.json +1 -1
- package/server.js +11 -0
package/database.js
CHANGED
|
@@ -1,650 +1,80 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { createRequire } from 'module';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (process.env.BUN_BE_BUN && process.argv[1])
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
db =
|
|
35
|
-
db.run('PRAGMA
|
|
36
|
-
db.run('PRAGMA
|
|
37
|
-
db.run('PRAGMA
|
|
38
|
-
db.run('PRAGMA
|
|
39
|
-
db.run('PRAGMA
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
db =
|
|
48
|
-
db.pragma('
|
|
49
|
-
db.pragma('
|
|
50
|
-
db.pragma('
|
|
51
|
-
db.pragma('
|
|
52
|
-
db.pragma('
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
content TEXT NOT NULL,
|
|
82
|
-
created_at INTEGER NOT NULL,
|
|
83
|
-
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversationId);
|
|
87
|
-
|
|
88
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
89
|
-
id TEXT PRIMARY KEY,
|
|
90
|
-
conversationId TEXT NOT NULL,
|
|
91
|
-
status TEXT NOT NULL,
|
|
92
|
-
started_at INTEGER NOT NULL,
|
|
93
|
-
completed_at INTEGER,
|
|
94
|
-
response TEXT,
|
|
95
|
-
error TEXT,
|
|
96
|
-
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_conversation ON sessions(conversationId);
|
|
100
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(conversationId, status);
|
|
101
|
-
|
|
102
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
103
|
-
id TEXT PRIMARY KEY,
|
|
104
|
-
type TEXT NOT NULL,
|
|
105
|
-
conversationId TEXT,
|
|
106
|
-
sessionId TEXT,
|
|
107
|
-
data TEXT NOT NULL,
|
|
108
|
-
created_at INTEGER NOT NULL,
|
|
109
|
-
FOREIGN KEY (conversationId) REFERENCES conversations(id),
|
|
110
|
-
FOREIGN KEY (sessionId) REFERENCES sessions(id)
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
CREATE INDEX IF NOT EXISTS idx_events_conversation ON events(conversationId);
|
|
114
|
-
|
|
115
|
-
CREATE TABLE IF NOT EXISTS idempotencyKeys (
|
|
116
|
-
key TEXT PRIMARY KEY,
|
|
117
|
-
value TEXT NOT NULL,
|
|
118
|
-
created_at INTEGER NOT NULL,
|
|
119
|
-
ttl INTEGER NOT NULL
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
CREATE INDEX IF NOT EXISTS idx_idempotency_created ON idempotencyKeys(created_at);
|
|
123
|
-
|
|
124
|
-
CREATE TABLE IF NOT EXISTS stream_updates (
|
|
125
|
-
id TEXT PRIMARY KEY,
|
|
126
|
-
sessionId TEXT NOT NULL,
|
|
127
|
-
conversationId TEXT NOT NULL,
|
|
128
|
-
updateType TEXT NOT NULL,
|
|
129
|
-
content TEXT NOT NULL,
|
|
130
|
-
sequence INTEGER NOT NULL,
|
|
131
|
-
created_at INTEGER NOT NULL,
|
|
132
|
-
FOREIGN KEY (sessionId) REFERENCES sessions(id),
|
|
133
|
-
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
CREATE INDEX IF NOT EXISTS idx_stream_updates_session ON stream_updates(sessionId);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_stream_updates_created ON stream_updates(created_at);
|
|
138
|
-
|
|
139
|
-
CREATE TABLE IF NOT EXISTS chunks (
|
|
140
|
-
id TEXT PRIMARY KEY,
|
|
141
|
-
sessionId TEXT NOT NULL,
|
|
142
|
-
conversationId TEXT NOT NULL,
|
|
143
|
-
sequence INTEGER NOT NULL,
|
|
144
|
-
type TEXT NOT NULL,
|
|
145
|
-
data BLOB NOT NULL,
|
|
146
|
-
created_at INTEGER NOT NULL,
|
|
147
|
-
FOREIGN KEY (sessionId) REFERENCES sessions(id),
|
|
148
|
-
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(sessionId, sequence);
|
|
152
|
-
CREATE INDEX IF NOT EXISTS idx_chunks_conversation ON chunks(conversationId, sequence);
|
|
153
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_chunks_unique ON chunks(sessionId, sequence);
|
|
154
|
-
CREATE INDEX IF NOT EXISTS idx_chunks_conv_created ON chunks(conversationId, created_at);
|
|
155
|
-
CREATE INDEX IF NOT EXISTS idx_chunks_sess_created ON chunks(sessionId, created_at);
|
|
156
|
-
|
|
157
|
-
CREATE TABLE IF NOT EXISTS voice_cache (
|
|
158
|
-
id TEXT PRIMARY KEY,
|
|
159
|
-
conversationId TEXT NOT NULL,
|
|
160
|
-
text TEXT NOT NULL,
|
|
161
|
-
audioBlob BLOB,
|
|
162
|
-
byteSize INTEGER NOT NULL,
|
|
163
|
-
created_at INTEGER NOT NULL,
|
|
164
|
-
expires_at INTEGER NOT NULL,
|
|
165
|
-
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
CREATE INDEX IF NOT EXISTS idx_voice_cache_conv ON voice_cache(conversationId);
|
|
169
|
-
CREATE INDEX IF NOT EXISTS idx_voice_cache_expires ON voice_cache(expires_at);
|
|
170
|
-
|
|
171
|
-
CREATE TABLE IF NOT EXISTS tool_installations (
|
|
172
|
-
id TEXT PRIMARY KEY,
|
|
173
|
-
tool_id TEXT NOT NULL UNIQUE,
|
|
174
|
-
version TEXT,
|
|
175
|
-
installed_at INTEGER,
|
|
176
|
-
status TEXT NOT NULL DEFAULT 'not_installed',
|
|
177
|
-
last_check_at INTEGER,
|
|
178
|
-
error_message TEXT,
|
|
179
|
-
update_available INTEGER DEFAULT 0,
|
|
180
|
-
latest_version TEXT,
|
|
181
|
-
created_at INTEGER NOT NULL,
|
|
182
|
-
updated_at INTEGER NOT NULL
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
CREATE INDEX IF NOT EXISTS idx_tool_installations_status ON tool_installations(status);
|
|
186
|
-
CREATE INDEX IF NOT EXISTS idx_tool_installations_last_check ON tool_installations(last_check_at);
|
|
187
|
-
|
|
188
|
-
CREATE TABLE IF NOT EXISTS tool_install_history (
|
|
189
|
-
id TEXT PRIMARY KEY,
|
|
190
|
-
tool_id TEXT NOT NULL,
|
|
191
|
-
action TEXT NOT NULL,
|
|
192
|
-
started_at INTEGER NOT NULL,
|
|
193
|
-
completed_at INTEGER,
|
|
194
|
-
status TEXT NOT NULL,
|
|
195
|
-
error_message TEXT,
|
|
196
|
-
created_at INTEGER NOT NULL,
|
|
197
|
-
FOREIGN KEY (tool_id) REFERENCES tool_installations(tool_id)
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
CREATE INDEX IF NOT EXISTS idx_tool_install_history_tool ON tool_install_history(tool_id);
|
|
201
|
-
CREATE INDEX IF NOT EXISTS idx_tool_install_history_completed ON tool_install_history(completed_at);
|
|
202
|
-
|
|
203
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
204
|
-
id TEXT PRIMARY KEY,
|
|
205
|
-
workflowName TEXT NOT NULL,
|
|
206
|
-
workflowId TEXT,
|
|
207
|
-
runId TEXT,
|
|
208
|
-
sha TEXT,
|
|
209
|
-
branch TEXT,
|
|
210
|
-
status TEXT,
|
|
211
|
-
conclusion TEXT,
|
|
212
|
-
htmlUrl TEXT,
|
|
213
|
-
triggeredAt INTEGER NOT NULL,
|
|
214
|
-
completedAt INTEGER,
|
|
215
|
-
created_at INTEGER NOT NULL
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
CREATE INDEX IF NOT EXISTS idx_workflow_runs_name ON workflow_runs(workflowName);
|
|
219
|
-
CREATE INDEX IF NOT EXISTS idx_workflow_runs_sha ON workflow_runs(sha);
|
|
220
|
-
CREATE INDEX IF NOT EXISTS idx_workflow_runs_completed ON workflow_runs(completedAt);
|
|
221
|
-
|
|
222
|
-
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
|
223
|
-
id TEXT PRIMARY KEY,
|
|
224
|
-
provider TEXT NOT NULL,
|
|
225
|
-
token TEXT NOT NULL,
|
|
226
|
-
email TEXT,
|
|
227
|
-
expires_at INTEGER,
|
|
228
|
-
created_at INTEGER NOT NULL,
|
|
229
|
-
updated_at INTEGER NOT NULL
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
CREATE INDEX IF NOT EXISTS idx_oauth_tokens_provider ON oauth_tokens(provider);
|
|
233
|
-
|
|
234
|
-
`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function migrateFromJson() {
|
|
238
|
-
if (!fs.existsSync(oldJsonPath)) return;
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const content = fs.readFileSync(oldJsonPath, 'utf-8');
|
|
242
|
-
const data = JSON.parse(content);
|
|
243
|
-
|
|
244
|
-
const migrationStmt = db.transaction(() => {
|
|
245
|
-
if (data.conversations) {
|
|
246
|
-
for (const id in data.conversations) {
|
|
247
|
-
const conv = data.conversations[id];
|
|
248
|
-
db.prepare(
|
|
249
|
-
`INSERT OR REPLACE INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
|
|
250
|
-
).run(conv.id, conv.agentId, conv.title || null, conv.created_at, conv.updated_at, conv.status || 'active');
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (data.messages) {
|
|
255
|
-
for (const id in data.messages) {
|
|
256
|
-
const msg = data.messages[id];
|
|
257
|
-
// Ensure content is always a string (stringify objects)
|
|
258
|
-
const contentStr = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
259
|
-
db.prepare(
|
|
260
|
-
`INSERT OR REPLACE INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
|
|
261
|
-
).run(msg.id, msg.conversationId, msg.role, contentStr, msg.created_at);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (data.sessions) {
|
|
266
|
-
for (const id in data.sessions) {
|
|
267
|
-
const sess = data.sessions[id];
|
|
268
|
-
// Ensure response and error are strings, not objects
|
|
269
|
-
const responseStr = sess.response ? (typeof sess.response === 'string' ? sess.response : JSON.stringify(sess.response)) : null;
|
|
270
|
-
const errorStr = sess.error ? (typeof sess.error === 'string' ? sess.error : JSON.stringify(sess.error)) : null;
|
|
271
|
-
db.prepare(
|
|
272
|
-
`INSERT OR REPLACE INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
273
|
-
).run(sess.id, sess.conversationId, sess.status, sess.started_at, sess.completed_at || null, responseStr, errorStr);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (data.events) {
|
|
278
|
-
for (const id in data.events) {
|
|
279
|
-
const evt = data.events[id];
|
|
280
|
-
// Ensure data is always valid JSON string
|
|
281
|
-
const dataStr = typeof evt.data === 'string' ? evt.data : JSON.stringify(evt.data || {});
|
|
282
|
-
db.prepare(
|
|
283
|
-
`INSERT OR REPLACE INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
|
|
284
|
-
).run(evt.id, evt.type, evt.conversationId || null, evt.sessionId || null, dataStr, evt.created_at);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (data.idempotencyKeys) {
|
|
289
|
-
for (const key in data.idempotencyKeys) {
|
|
290
|
-
const entry = data.idempotencyKeys[key];
|
|
291
|
-
// Ensure value is always valid JSON string
|
|
292
|
-
const valueStr = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value || {});
|
|
293
|
-
// Ensure ttl is a number
|
|
294
|
-
const ttl = typeof entry.ttl === 'number' ? entry.ttl : (entry.ttl ? parseInt(entry.ttl) : 0);
|
|
295
|
-
db.prepare(
|
|
296
|
-
`INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)`
|
|
297
|
-
).run(key, valueStr, entry.created_at, ttl);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
migrationStmt();
|
|
303
|
-
fs.renameSync(oldJsonPath, `${oldJsonPath}.migrated`);
|
|
304
|
-
console.log('Migrated data from JSON to SQLite');
|
|
305
|
-
} catch (e) {
|
|
306
|
-
console.error('Error during migration:', e.message);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function migrateToACP() {
|
|
311
|
-
try {
|
|
312
|
-
const migrate = db.transaction(() => {
|
|
313
|
-
// Create new tables for ACP support
|
|
314
|
-
db.exec(`
|
|
315
|
-
CREATE TABLE IF NOT EXISTS thread_states (
|
|
316
|
-
id TEXT PRIMARY KEY,
|
|
317
|
-
thread_id TEXT NOT NULL,
|
|
318
|
-
checkpoint_id TEXT,
|
|
319
|
-
state_data TEXT NOT NULL,
|
|
320
|
-
created_at INTEGER NOT NULL,
|
|
321
|
-
FOREIGN KEY (thread_id) REFERENCES conversations(id) ON DELETE CASCADE,
|
|
322
|
-
FOREIGN KEY (checkpoint_id) REFERENCES checkpoints(id) ON DELETE SET NULL
|
|
323
|
-
)
|
|
324
|
-
`);
|
|
325
|
-
|
|
326
|
-
db.exec(`
|
|
327
|
-
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
328
|
-
id TEXT PRIMARY KEY,
|
|
329
|
-
thread_id TEXT NOT NULL,
|
|
330
|
-
checkpoint_name TEXT NOT NULL,
|
|
331
|
-
sequence INTEGER NOT NULL,
|
|
332
|
-
created_at INTEGER NOT NULL,
|
|
333
|
-
FOREIGN KEY (thread_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
334
|
-
)
|
|
335
|
-
`);
|
|
336
|
-
|
|
337
|
-
db.exec(`
|
|
338
|
-
CREATE TABLE IF NOT EXISTS run_metadata (
|
|
339
|
-
id TEXT PRIMARY KEY,
|
|
340
|
-
run_id TEXT NOT NULL UNIQUE,
|
|
341
|
-
thread_id TEXT,
|
|
342
|
-
agent_id TEXT NOT NULL,
|
|
343
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
344
|
-
input TEXT,
|
|
345
|
-
config TEXT,
|
|
346
|
-
webhook_url TEXT,
|
|
347
|
-
created_at INTEGER NOT NULL,
|
|
348
|
-
updated_at INTEGER NOT NULL,
|
|
349
|
-
FOREIGN KEY (thread_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
350
|
-
)
|
|
351
|
-
`);
|
|
352
|
-
|
|
353
|
-
// Add new columns to existing tables
|
|
354
|
-
const convCols = db.prepare("PRAGMA table_info(conversations)").all();
|
|
355
|
-
const convColNames = convCols.map(c => c.name);
|
|
356
|
-
|
|
357
|
-
if (!convColNames.includes('metadata')) {
|
|
358
|
-
db.exec('ALTER TABLE conversations ADD COLUMN metadata TEXT');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const sessCols = db.prepare("PRAGMA table_info(sessions)").all();
|
|
362
|
-
const sessColNames = sessCols.map(c => c.name);
|
|
363
|
-
|
|
364
|
-
const sessionCols = {
|
|
365
|
-
run_id: 'TEXT',
|
|
366
|
-
input: 'TEXT',
|
|
367
|
-
config: 'TEXT',
|
|
368
|
-
interrupt: 'TEXT'
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
for (const [colName, colType] of Object.entries(sessionCols)) {
|
|
372
|
-
if (!sessColNames.includes(colName)) {
|
|
373
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN ${colName} ${colType}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Create indexes
|
|
378
|
-
db.exec(`
|
|
379
|
-
CREATE INDEX IF NOT EXISTS idx_thread_states_thread ON thread_states(thread_id);
|
|
380
|
-
CREATE INDEX IF NOT EXISTS idx_thread_states_checkpoint ON thread_states(checkpoint_id);
|
|
381
|
-
CREATE INDEX IF NOT EXISTS idx_thread_states_created ON thread_states(created_at);
|
|
382
|
-
|
|
383
|
-
CREATE INDEX IF NOT EXISTS idx_checkpoints_thread ON checkpoints(thread_id);
|
|
384
|
-
CREATE INDEX IF NOT EXISTS idx_checkpoints_sequence ON checkpoints(thread_id, sequence);
|
|
385
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_checkpoints_unique_seq ON checkpoints(thread_id, sequence);
|
|
386
|
-
|
|
387
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_run_id ON run_metadata(run_id);
|
|
388
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_thread ON run_metadata(thread_id);
|
|
389
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_status ON run_metadata(status);
|
|
390
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_agent ON run_metadata(agent_id);
|
|
391
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_created ON run_metadata(created_at);
|
|
392
|
-
|
|
393
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_run_id ON sessions(run_id);
|
|
394
|
-
`);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
migrate();
|
|
398
|
-
} catch (err) {
|
|
399
|
-
console.error('[Migration] ACP schema migration error:', err.message);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
initSchema();
|
|
404
|
-
migrateFromJson();
|
|
405
|
-
migrateToACP();
|
|
406
|
-
|
|
407
|
-
// Migration: Add imported conversation columns if they don't exist
|
|
408
|
-
try {
|
|
409
|
-
const result = db.prepare("PRAGMA table_info(conversations)").all();
|
|
410
|
-
const columnNames = result.map(r => r.name);
|
|
411
|
-
const requiredColumns = {
|
|
412
|
-
agentType: 'TEXT',
|
|
413
|
-
source: 'TEXT DEFAULT "gui"',
|
|
414
|
-
externalId: 'TEXT',
|
|
415
|
-
firstPrompt: 'TEXT',
|
|
416
|
-
messageCount: 'INTEGER DEFAULT 0',
|
|
417
|
-
projectPath: 'TEXT',
|
|
418
|
-
gitBranch: 'TEXT',
|
|
419
|
-
sourcePath: 'TEXT',
|
|
420
|
-
lastSyncedAt: 'INTEGER',
|
|
421
|
-
workingDirectory: 'TEXT',
|
|
422
|
-
claudeSessionId: 'TEXT',
|
|
423
|
-
isStreaming: 'INTEGER DEFAULT 0',
|
|
424
|
-
model: 'TEXT',
|
|
425
|
-
subAgent: 'TEXT',
|
|
426
|
-
pinned: 'INTEGER DEFAULT 0',
|
|
427
|
-
tags: 'TEXT',
|
|
428
|
-
sortOrder: 'INTEGER DEFAULT 0'
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
let addedColumns = false;
|
|
432
|
-
for (const [colName, colDef] of Object.entries(requiredColumns)) {
|
|
433
|
-
if (!columnNames.includes(colName)) {
|
|
434
|
-
db.exec(`ALTER TABLE conversations ADD COLUMN ${colName} ${colDef}`);
|
|
435
|
-
console.log(`[Migration] Added column ${colName} to conversations table`);
|
|
436
|
-
addedColumns = true;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Add indexes for new columns
|
|
441
|
-
if (addedColumns) {
|
|
442
|
-
try {
|
|
443
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_external ON conversations(externalId)`);
|
|
444
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_agent_type ON conversations(agentType)`);
|
|
445
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_source ON conversations(source)`);
|
|
446
|
-
} catch (e) {
|
|
447
|
-
console.warn('[Migration] Index creation warning:', e.message);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
} catch (err) {
|
|
451
|
-
console.error('[Migration] Error:', err.message);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Migration: Add resume capability columns (disabled - incomplete migration)
|
|
455
|
-
// This migration block was incomplete and has been removed
|
|
456
|
-
|
|
457
|
-
// ============ ACP SCHEMA MIGRATION ============
|
|
458
|
-
try {
|
|
459
|
-
console.log('[Migration] Running ACP schema migration...');
|
|
460
|
-
|
|
461
|
-
// Add metadata column to conversations if not exists
|
|
462
|
-
const convColsACP = db.prepare("PRAGMA table_info(conversations)").all().map(c => c.name);
|
|
463
|
-
if (!convColsACP.includes('metadata')) {
|
|
464
|
-
db.exec('ALTER TABLE conversations ADD COLUMN metadata TEXT DEFAULT "{}"');
|
|
465
|
-
console.log('[Migration] Added metadata column to conversations');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Add run_id, input, config, interrupt to sessions if not exists
|
|
469
|
-
const sessColsACP = db.prepare("PRAGMA table_info(sessions)").all().map(c => c.name);
|
|
470
|
-
if (!sessColsACP.includes('run_id')) {
|
|
471
|
-
db.exec('ALTER TABLE sessions ADD COLUMN run_id TEXT');
|
|
472
|
-
console.log('[Migration] Added run_id column to sessions');
|
|
473
|
-
}
|
|
474
|
-
if (!sessColsACP.includes('input')) {
|
|
475
|
-
db.exec('ALTER TABLE sessions ADD COLUMN input TEXT');
|
|
476
|
-
console.log('[Migration] Added input column to sessions');
|
|
477
|
-
}
|
|
478
|
-
if (!sessColsACP.includes('config')) {
|
|
479
|
-
db.exec('ALTER TABLE sessions ADD COLUMN config TEXT');
|
|
480
|
-
console.log('[Migration] Added config column to sessions');
|
|
481
|
-
}
|
|
482
|
-
if (!sessColsACP.includes('interrupt')) {
|
|
483
|
-
db.exec('ALTER TABLE sessions ADD COLUMN interrupt TEXT');
|
|
484
|
-
console.log('[Migration] Added interrupt column to sessions');
|
|
485
|
-
}
|
|
486
|
-
if (!sessColsACP.includes('claudeSessionId')) {
|
|
487
|
-
db.exec('ALTER TABLE sessions ADD COLUMN claudeSessionId TEXT');
|
|
488
|
-
console.log('[Migration] Added claudeSessionId column to sessions');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Create ACP tables
|
|
492
|
-
db.exec(`
|
|
493
|
-
CREATE TABLE IF NOT EXISTS thread_states (
|
|
494
|
-
id TEXT PRIMARY KEY,
|
|
495
|
-
thread_id TEXT NOT NULL,
|
|
496
|
-
checkpoint_id TEXT NOT NULL,
|
|
497
|
-
state_data TEXT NOT NULL,
|
|
498
|
-
created_at INTEGER NOT NULL,
|
|
499
|
-
FOREIGN KEY (thread_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
CREATE INDEX IF NOT EXISTS idx_thread_states_thread ON thread_states(thread_id);
|
|
503
|
-
CREATE INDEX IF NOT EXISTS idx_thread_states_checkpoint ON thread_states(checkpoint_id);
|
|
504
|
-
CREATE INDEX IF NOT EXISTS idx_thread_states_created ON thread_states(created_at);
|
|
505
|
-
|
|
506
|
-
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
507
|
-
id TEXT PRIMARY KEY,
|
|
508
|
-
thread_id TEXT NOT NULL,
|
|
509
|
-
checkpoint_name TEXT,
|
|
510
|
-
sequence INTEGER NOT NULL,
|
|
511
|
-
created_at INTEGER NOT NULL,
|
|
512
|
-
FOREIGN KEY (thread_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
CREATE INDEX IF NOT EXISTS idx_checkpoints_thread ON checkpoints(thread_id);
|
|
516
|
-
CREATE INDEX IF NOT EXISTS idx_checkpoints_sequence ON checkpoints(thread_id, sequence);
|
|
517
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_checkpoints_unique ON checkpoints(thread_id, sequence);
|
|
518
|
-
|
|
519
|
-
CREATE TABLE IF NOT EXISTS run_metadata (
|
|
520
|
-
run_id TEXT PRIMARY KEY,
|
|
521
|
-
thread_id TEXT,
|
|
522
|
-
agent_id TEXT NOT NULL,
|
|
523
|
-
status TEXT NOT NULL,
|
|
524
|
-
input TEXT,
|
|
525
|
-
config TEXT,
|
|
526
|
-
webhook_url TEXT,
|
|
527
|
-
created_at INTEGER NOT NULL,
|
|
528
|
-
updated_at INTEGER NOT NULL,
|
|
529
|
-
FOREIGN KEY (thread_id) REFERENCES conversations(id) ON DELETE CASCADE,
|
|
530
|
-
FOREIGN KEY (run_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_thread ON run_metadata(thread_id);
|
|
534
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_agent ON run_metadata(agent_id);
|
|
535
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_status ON run_metadata(status);
|
|
536
|
-
CREATE INDEX IF NOT EXISTS idx_run_metadata_created ON run_metadata(created_at);
|
|
537
|
-
`);
|
|
538
|
-
|
|
539
|
-
console.log('[Migration] ACP schema migration complete');
|
|
540
|
-
} catch (err) {
|
|
541
|
-
console.error('[Migration] ACP schema migration error:', err.message);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Migration: Backfill messages for conversations imported without message content
|
|
545
|
-
try {
|
|
546
|
-
const emptyImported = db.prepare(`
|
|
547
|
-
SELECT c.id, c.sourcePath FROM conversations c
|
|
548
|
-
LEFT JOIN messages m ON c.id = m.conversationId
|
|
549
|
-
WHERE c.sourcePath IS NOT NULL AND c.status != 'deleted'
|
|
550
|
-
GROUP BY c.id HAVING COUNT(m.id) = 0
|
|
551
|
-
`).all();
|
|
552
|
-
|
|
553
|
-
if (emptyImported.length > 0) {
|
|
554
|
-
console.log(`[Migration] Backfilling messages for ${emptyImported.length} imported conversation(s)`);
|
|
555
|
-
const insertMsg = db.prepare(`INSERT OR IGNORE INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`);
|
|
556
|
-
const backfill = db.transaction(() => {
|
|
557
|
-
for (const conv of emptyImported) {
|
|
558
|
-
if (!fs.existsSync(conv.sourcePath)) continue;
|
|
559
|
-
try {
|
|
560
|
-
const lines = fs.readFileSync(conv.sourcePath, 'utf-8').split('\n');
|
|
561
|
-
let count = 0;
|
|
562
|
-
for (const line of lines) {
|
|
563
|
-
if (!line.trim()) continue;
|
|
564
|
-
try {
|
|
565
|
-
const obj = JSON.parse(line);
|
|
566
|
-
const msgId = obj.uuid || `msg-${Date.now()}-${Math.random().toString(36).substr(2,9)}`;
|
|
567
|
-
const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now();
|
|
568
|
-
if (obj.type === 'user' && obj.message?.content) {
|
|
569
|
-
const raw = obj.message.content;
|
|
570
|
-
const text = typeof raw === 'string' ? raw
|
|
571
|
-
: Array.isArray(raw) ? raw.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
572
|
-
: JSON.stringify(raw);
|
|
573
|
-
if (text && !text.startsWith('[{"tool_use_id"')) {
|
|
574
|
-
insertMsg.run(msgId, conv.id, 'user', text, ts);
|
|
575
|
-
count++;
|
|
576
|
-
}
|
|
577
|
-
} else if (obj.type === 'assistant' && obj.message?.content) {
|
|
578
|
-
const raw = obj.message.content;
|
|
579
|
-
const text = Array.isArray(raw)
|
|
580
|
-
? raw.filter(c => c.type === 'text' && c.text).map(c => c.text).join('\n\n')
|
|
581
|
-
: typeof raw === 'string' ? raw : '';
|
|
582
|
-
if (text) {
|
|
583
|
-
insertMsg.run(msgId, conv.id, 'assistant', text, ts);
|
|
584
|
-
count++;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
} catch (_) {}
|
|
588
|
-
}
|
|
589
|
-
if (count > 0) console.log(`[Migration] Backfilled ${count} messages for conversation ${conv.id}`);
|
|
590
|
-
} catch (e) {
|
|
591
|
-
console.error(`[Migration] Error backfilling ${conv.id}:`, e.message);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
backfill();
|
|
596
|
-
}
|
|
597
|
-
} catch (err) {
|
|
598
|
-
console.error('[Migration] Backfill error:', err.message);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
603
|
-
if (!hasFts) {
|
|
604
|
-
db.exec("CREATE VIRTUAL TABLE messages_fts USING fts5(content, conversationId UNINDEXED, role UNINDEXED, content_rowid='rowid')");
|
|
605
|
-
const msgs = db.prepare("SELECT rowid, content, conversationId, role FROM messages").all();
|
|
606
|
-
if (msgs.length > 0) {
|
|
607
|
-
const ins = db.prepare("INSERT INTO messages_fts(rowid, content, conversationId, role) VALUES (?, ?, ?, ?)");
|
|
608
|
-
const tx = db.transaction(() => { for (const m of msgs) ins.run(m.rowid, m.content, m.conversationId, m.role); });
|
|
609
|
-
tx();
|
|
610
|
-
console.log(`[Migration] FTS5 index created with ${msgs.length} messages`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
} catch (err) {
|
|
614
|
-
console.error('[Migration] FTS5 error:', err.message);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// One-time: enable incremental auto_vacuum on existing databases (requires VACUUM to activate)
|
|
618
|
-
try {
|
|
619
|
-
const autoVacuum = db.prepare('PRAGMA auto_vacuum').get();
|
|
620
|
-
const mode = autoVacuum?.auto_vacuum ?? autoVacuum;
|
|
621
|
-
if (mode !== 2) { // 2 = INCREMENTAL
|
|
622
|
-
console.log('[Migration] Enabling incremental auto_vacuum (one-time VACUUM)...');
|
|
623
|
-
db.exec('PRAGMA auto_vacuum = INCREMENTAL');
|
|
624
|
-
// VACUUM skipped intentionally — full VACUUM on large DBs blocks server startup
|
|
625
|
-
// INCREMENTAL auto_vacuum will apply to new pages going forward
|
|
626
|
-
console.log('[Migration] auto_vacuum = INCREMENTAL enabled (VACUUM skipped)');
|
|
627
|
-
}
|
|
628
|
-
} catch (err) {
|
|
629
|
-
console.error('[Migration] auto_vacuum setup error:', err.message);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const stmtCache = new Map();
|
|
633
|
-
function prep(sql) {
|
|
634
|
-
let s = stmtCache.get(sql);
|
|
635
|
-
if (!s) {
|
|
636
|
-
s = db.prepare(sql);
|
|
637
|
-
stmtCache.set(sql, s);
|
|
638
|
-
}
|
|
639
|
-
return s;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function generateId(prefix) {
|
|
643
|
-
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
import { createQueries } from './lib/db-queries.js';
|
|
647
|
-
|
|
648
|
-
export const queries = createQueries(db, prep, generateId);
|
|
649
|
-
|
|
650
|
-
export default { queries };
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { initSchema } from './database-schema.js';
|
|
6
|
+
import { migrateFromJson, migrateToACP, migrateConversationColumns } from './database-migrations.js';
|
|
7
|
+
import { migrateACPSchema, migrateBackfillMessages, migrateFTS, migrateAutoVacuum } from './database-migrations-acp.js';
|
|
8
|
+
import { createQueries } from './lib/db-queries.js';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
function getDataDir() {
|
|
13
|
+
if (process.env.PORTABLE_DATA_DIR) return process.env.PORTABLE_DATA_DIR;
|
|
14
|
+
const exeDir = process.pkg?.path ? path.dirname(process.pkg.path) : null;
|
|
15
|
+
if (exeDir) return path.join(exeDir, 'data');
|
|
16
|
+
if (process.env.BUN_BE_BUN && process.argv[1]) return path.join(path.dirname(process.argv[1]), 'data');
|
|
17
|
+
return path.join(os.homedir(), '.gmgui');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const dataDir = getDataDir();
|
|
21
|
+
const dbDir = dataDir;
|
|
22
|
+
const dbFilePath = path.join(dbDir, 'data.db');
|
|
23
|
+
const oldJsonPath = path.join(dbDir, 'data.json');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
let db;
|
|
28
|
+
try {
|
|
29
|
+
const Database = (await import('bun:sqlite')).default;
|
|
30
|
+
db = new Database(dbFilePath);
|
|
31
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
32
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
33
|
+
db.run('PRAGMA encoding = "UTF-8"');
|
|
34
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
35
|
+
db.run('PRAGMA busy_timeout = 5000');
|
|
36
|
+
db.run('PRAGMA cache_size = -64000');
|
|
37
|
+
db.run('PRAGMA mmap_size = 268435456');
|
|
38
|
+
db.run('PRAGMA temp_store = MEMORY');
|
|
39
|
+
db.run('PRAGMA auto_vacuum = INCREMENTAL');
|
|
40
|
+
} catch (e) {
|
|
41
|
+
try {
|
|
42
|
+
const sqlite3 = require('better-sqlite3');
|
|
43
|
+
db = new sqlite3(dbFilePath);
|
|
44
|
+
db.pragma('journal_mode = WAL');
|
|
45
|
+
db.pragma('foreign_keys = ON');
|
|
46
|
+
db.pragma('encoding = "UTF-8"');
|
|
47
|
+
db.pragma('synchronous = NORMAL');
|
|
48
|
+
db.pragma('busy_timeout = 5000');
|
|
49
|
+
db.pragma('cache_size = -64000');
|
|
50
|
+
db.pragma('mmap_size = 268435456');
|
|
51
|
+
db.pragma('temp_store = MEMORY');
|
|
52
|
+
db.pragma('auto_vacuum = INCREMENTAL');
|
|
53
|
+
} catch (e2) {
|
|
54
|
+
throw new Error('SQLite database is required. Please run with bun (recommended) or install better-sqlite3: npm install better-sqlite3');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initSchema(db);
|
|
59
|
+
migrateFromJson(db, oldJsonPath);
|
|
60
|
+
migrateToACP(db);
|
|
61
|
+
migrateConversationColumns(db);
|
|
62
|
+
migrateACPSchema(db);
|
|
63
|
+
migrateBackfillMessages(db);
|
|
64
|
+
migrateFTS(db);
|
|
65
|
+
migrateAutoVacuum(db);
|
|
66
|
+
|
|
67
|
+
const stmtCache = new Map();
|
|
68
|
+
function prep(sql) {
|
|
69
|
+
let s = stmtCache.get(sql);
|
|
70
|
+
if (!s) { s = db.prepare(sql); stmtCache.set(sql, s); }
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function generateId(prefix) {
|
|
75
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const queries = createQueries(db, prep, generateId);
|
|
79
|
+
|
|
80
|
+
export default { queries };
|