agentgui 1.0.110 → 1.0.112
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/.prd +281 -0
- package/database.js +87 -0
- package/package.json +2 -2
- package/server.js +25 -2
- package/static/index.html +50 -4
- package/static/js/client.js +14 -7
- package/static/js/conversations.js +41 -2
- package/conversation-importer.js +0 -63
- package/hot-reload-manager.js +0 -186
- package/lib/database-service.ts +0 -640
- package/lib/machines.ts +0 -190
- package/lib/schemas.ts +0 -190
- package/lib/sync-service.ts +0 -615
- package/lib/types.ts +0 -413
package/lib/database-service.ts
DELETED
|
@@ -1,640 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DATABASE-SERVICE.TS - Isolated database layer
|
|
3
|
-
* All database operations go through this service
|
|
4
|
-
* Type-safe, validated, and fully testable
|
|
5
|
-
* Zero data loss guarantees with transactions and WAL mode
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
Conversation,
|
|
10
|
-
ConversationCreateInput,
|
|
11
|
-
ConversationUpdateInput,
|
|
12
|
-
Message,
|
|
13
|
-
MessageCreateInput,
|
|
14
|
-
Session,
|
|
15
|
-
ValidationResult,
|
|
16
|
-
ValidationError,
|
|
17
|
-
SyncError,
|
|
18
|
-
ExecutionMetadata,
|
|
19
|
-
StreamingEvent,
|
|
20
|
-
} from './types';
|
|
21
|
-
import {
|
|
22
|
-
validateConversation,
|
|
23
|
-
validateMessage,
|
|
24
|
-
ConversationCreateInputSchema,
|
|
25
|
-
MessageCreateInputSchema,
|
|
26
|
-
} from './schemas';
|
|
27
|
-
|
|
28
|
-
interface Database {
|
|
29
|
-
prepare: (sql: string) => any;
|
|
30
|
-
transaction: (fn: () => void) => () => void;
|
|
31
|
-
exec: (sql: string) => void;
|
|
32
|
-
pragma: (pragma: string) => any;
|
|
33
|
-
close: () => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* DatabaseService - Complete isolation of database operations
|
|
38
|
-
* All reads/writes validated, all operations transactional
|
|
39
|
-
*/
|
|
40
|
-
export class DatabaseService {
|
|
41
|
-
private db: Database;
|
|
42
|
-
private closed = false;
|
|
43
|
-
|
|
44
|
-
constructor(db: Database) {
|
|
45
|
-
this.db = db;
|
|
46
|
-
this.ensurePragma();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private ensurePragma() {
|
|
50
|
-
try {
|
|
51
|
-
this.db.pragma('journal_mode = WAL');
|
|
52
|
-
this.db.pragma('foreign_keys = ON');
|
|
53
|
-
this.db.pragma('synchronous = FULL');
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.error('[DatabaseService] Failed to set pragmas:', err);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private checkClosed() {
|
|
60
|
-
if (this.closed) {
|
|
61
|
-
throw new SyncError('DATABASE_ERROR', 'Database connection is closed', false);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// =========================================================================
|
|
66
|
-
// CONVERSATION OPERATIONS
|
|
67
|
-
// =========================================================================
|
|
68
|
-
|
|
69
|
-
createConversation(input: ConversationCreateInput): Conversation {
|
|
70
|
-
this.checkClosed();
|
|
71
|
-
const validation = ConversationCreateInputSchema.safeParse(input);
|
|
72
|
-
if (!validation.success) {
|
|
73
|
-
throw new SyncError('VALIDATION_ERROR', `Invalid conversation input: ${validation.error.message}`, false);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const id = `conv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const stmt = this.db.prepare(
|
|
81
|
-
'INSERT INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)'
|
|
82
|
-
);
|
|
83
|
-
stmt.run(id, input.agentId, input.title || null, now, now, 'active');
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
id,
|
|
87
|
-
agentId: input.agentId,
|
|
88
|
-
title: input.title || null,
|
|
89
|
-
created_at: now,
|
|
90
|
-
updated_at: now,
|
|
91
|
-
status: 'active',
|
|
92
|
-
};
|
|
93
|
-
} catch (err) {
|
|
94
|
-
throw new SyncError(
|
|
95
|
-
'DATABASE_ERROR',
|
|
96
|
-
`Failed to create conversation: ${(err as Error).message}`,
|
|
97
|
-
true,
|
|
98
|
-
{ input }
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
getConversation(id: string): Conversation | null {
|
|
104
|
-
this.checkClosed();
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const stmt = this.db.prepare(
|
|
108
|
-
'SELECT id, agentId, title, created_at, updated_at, status FROM conversations WHERE id = ? AND status != ?'
|
|
109
|
-
);
|
|
110
|
-
const row = stmt.get(id, 'deleted');
|
|
111
|
-
|
|
112
|
-
if (!row) return null;
|
|
113
|
-
|
|
114
|
-
const validation = validateConversation(row);
|
|
115
|
-
if (!validation.valid) {
|
|
116
|
-
throw new SyncError('VALIDATION_ERROR', `Invalid conversation data from DB: ${validation.error}`, false);
|
|
117
|
-
}
|
|
118
|
-
return validation.data;
|
|
119
|
-
} catch (err) {
|
|
120
|
-
throw new SyncError(
|
|
121
|
-
'DATABASE_ERROR',
|
|
122
|
-
`Failed to get conversation: ${(err as Error).message}`,
|
|
123
|
-
true,
|
|
124
|
-
{ id }
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
getConversationsList(): Conversation[] {
|
|
130
|
-
this.checkClosed();
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const stmt = this.db.prepare(
|
|
134
|
-
'SELECT id, agentId, title, created_at, updated_at, status FROM conversations WHERE status != ? ORDER BY updated_at DESC'
|
|
135
|
-
);
|
|
136
|
-
const rows = stmt.all('deleted');
|
|
137
|
-
|
|
138
|
-
return rows.map((row) => {
|
|
139
|
-
const validation = validateConversation(row);
|
|
140
|
-
if (!validation.valid) {
|
|
141
|
-
console.warn('[DatabaseService] Invalid conversation in list:', row);
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
return validation.data;
|
|
145
|
-
}).filter((c): c is Conversation => c !== null);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
throw new SyncError(
|
|
148
|
-
'DATABASE_ERROR',
|
|
149
|
-
`Failed to get conversations list: ${(err as Error).message}`,
|
|
150
|
-
true
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
updateConversation(id: string, input: ConversationUpdateInput): Conversation {
|
|
156
|
-
this.checkClosed();
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
const existing = this.getConversation(id);
|
|
160
|
-
if (!existing) {
|
|
161
|
-
throw new SyncError('NOT_FOUND', `Conversation not found: ${id}`, false);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const now = Date.now();
|
|
165
|
-
const title = input.title !== undefined ? input.title : existing.title;
|
|
166
|
-
const status = input.status !== undefined ? input.status : existing.status;
|
|
167
|
-
|
|
168
|
-
const stmt = this.db.prepare(
|
|
169
|
-
'UPDATE conversations SET title = ?, status = ?, updated_at = ? WHERE id = ?'
|
|
170
|
-
);
|
|
171
|
-
stmt.run(title, status, now, id);
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
...existing,
|
|
175
|
-
title,
|
|
176
|
-
status,
|
|
177
|
-
updated_at: now,
|
|
178
|
-
};
|
|
179
|
-
} catch (err) {
|
|
180
|
-
if (err instanceof SyncError) throw err;
|
|
181
|
-
throw new SyncError(
|
|
182
|
-
'DATABASE_ERROR',
|
|
183
|
-
`Failed to update conversation: ${(err as Error).message}`,
|
|
184
|
-
true,
|
|
185
|
-
{ id, input }
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
deleteConversation(id: string): boolean {
|
|
191
|
-
this.checkClosed();
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const stmt = this.db.prepare('UPDATE conversations SET status = ? WHERE id = ?');
|
|
195
|
-
const result = stmt.run('deleted', id);
|
|
196
|
-
return (result.changes || 0) > 0;
|
|
197
|
-
} catch (err) {
|
|
198
|
-
throw new SyncError(
|
|
199
|
-
'DATABASE_ERROR',
|
|
200
|
-
`Failed to delete conversation: ${(err as Error).message}`,
|
|
201
|
-
true,
|
|
202
|
-
{ id }
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// =========================================================================
|
|
208
|
-
// MESSAGE OPERATIONS
|
|
209
|
-
// =========================================================================
|
|
210
|
-
|
|
211
|
-
createMessage(conversationId: string, input: Omit<MessageCreateInput, 'conversationId'>): Message {
|
|
212
|
-
this.checkClosed();
|
|
213
|
-
const validation = MessageCreateInputSchema.omit({ conversationId: true }).safeParse(input);
|
|
214
|
-
if (!validation.success) {
|
|
215
|
-
throw new SyncError('VALIDATION_ERROR', `Invalid message input: ${validation.error.message}`, false);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Verify conversation exists
|
|
219
|
-
const conversation = this.getConversation(conversationId);
|
|
220
|
-
if (!conversation) {
|
|
221
|
-
throw new SyncError('NOT_FOUND', `Conversation not found: ${conversationId}`, false);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const id = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
225
|
-
const now = Date.now();
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const stmt = this.db.prepare(
|
|
229
|
-
'INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)'
|
|
230
|
-
);
|
|
231
|
-
stmt.run(id, conversationId, input.role, input.content, now);
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
id,
|
|
235
|
-
conversationId,
|
|
236
|
-
role: input.role,
|
|
237
|
-
content: input.content,
|
|
238
|
-
created_at: now,
|
|
239
|
-
};
|
|
240
|
-
} catch (err) {
|
|
241
|
-
throw new SyncError(
|
|
242
|
-
'DATABASE_ERROR',
|
|
243
|
-
`Failed to create message: ${(err as Error).message}`,
|
|
244
|
-
true,
|
|
245
|
-
{ conversationId, input }
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
getMessage(id: string): Message | null {
|
|
251
|
-
this.checkClosed();
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const stmt = this.db.prepare('SELECT id, conversationId, role, content, created_at FROM messages WHERE id = ?');
|
|
255
|
-
const row = stmt.get(id);
|
|
256
|
-
|
|
257
|
-
if (!row) return null;
|
|
258
|
-
|
|
259
|
-
const validation = validateMessage(row);
|
|
260
|
-
if (!validation.valid) {
|
|
261
|
-
throw new SyncError('VALIDATION_ERROR', `Invalid message data from DB: ${validation.error}`, false);
|
|
262
|
-
}
|
|
263
|
-
return validation.data;
|
|
264
|
-
} catch (err) {
|
|
265
|
-
throw new SyncError(
|
|
266
|
-
'DATABASE_ERROR',
|
|
267
|
-
`Failed to get message: ${(err as Error).message}`,
|
|
268
|
-
true,
|
|
269
|
-
{ id }
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
getConversationMessages(conversationId: string, limit = 50, offset = 0): Message[] {
|
|
275
|
-
this.checkClosed();
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const stmt = this.db.prepare(
|
|
279
|
-
'SELECT id, conversationId, role, content, created_at FROM messages WHERE conversationId = ? ORDER BY created_at ASC LIMIT ? OFFSET ?'
|
|
280
|
-
);
|
|
281
|
-
const rows = stmt.all(conversationId, limit, offset);
|
|
282
|
-
|
|
283
|
-
return rows.map((row) => {
|
|
284
|
-
const validation = validateMessage(row);
|
|
285
|
-
if (!validation.valid) {
|
|
286
|
-
console.warn('[DatabaseService] Invalid message in list:', row);
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
return validation.data;
|
|
290
|
-
}).filter((m): m is Message => m !== null);
|
|
291
|
-
} catch (err) {
|
|
292
|
-
throw new SyncError(
|
|
293
|
-
'DATABASE_ERROR',
|
|
294
|
-
`Failed to get messages: ${(err as Error).message}`,
|
|
295
|
-
true,
|
|
296
|
-
{ conversationId, limit, offset }
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
deleteMessage(id: string): boolean {
|
|
302
|
-
this.checkClosed();
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
const stmt = this.db.prepare('DELETE FROM messages WHERE id = ?');
|
|
306
|
-
const result = stmt.run(id);
|
|
307
|
-
return (result.changes || 0) > 0;
|
|
308
|
-
} catch (err) {
|
|
309
|
-
throw new SyncError(
|
|
310
|
-
'DATABASE_ERROR',
|
|
311
|
-
`Failed to delete message: ${(err as Error).message}`,
|
|
312
|
-
true,
|
|
313
|
-
{ id }
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// =========================================================================
|
|
319
|
-
// BATCH OPERATIONS
|
|
320
|
-
// =========================================================================
|
|
321
|
-
|
|
322
|
-
createMessagesBatch(conversationId: string, messages: Array<Omit<MessageCreateInput, 'conversationId'>>): Message[] {
|
|
323
|
-
this.checkClosed();
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
const transaction = this.db.transaction(() => {
|
|
327
|
-
return messages.map((msg) => this.createMessage(conversationId, msg));
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
return transaction();
|
|
331
|
-
} catch (err) {
|
|
332
|
-
throw new SyncError(
|
|
333
|
-
'DATABASE_ERROR',
|
|
334
|
-
`Failed to batch create messages: ${(err as Error).message}`,
|
|
335
|
-
true,
|
|
336
|
-
{ conversationId, count: messages.length }
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// =========================================================================
|
|
342
|
-
// INTEGRITY CHECKS
|
|
343
|
-
// =========================================================================
|
|
344
|
-
|
|
345
|
-
validateIntegrity(): { valid: boolean; errors: string[] } {
|
|
346
|
-
this.checkClosed();
|
|
347
|
-
const errors: string[] = [];
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
// Check for orphaned messages
|
|
351
|
-
const orphaned = this.db.prepare(
|
|
352
|
-
'SELECT COUNT(*) as count FROM messages WHERE conversationId NOT IN (SELECT id FROM conversations WHERE status != ?)'
|
|
353
|
-
).get('deleted');
|
|
354
|
-
|
|
355
|
-
if (orphaned.count > 0) {
|
|
356
|
-
errors.push(`Found ${orphaned.count} orphaned messages`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Check for duplicate conversation IDs
|
|
360
|
-
const duplicates = this.db.prepare(
|
|
361
|
-
'SELECT COUNT(*) as count FROM (SELECT id FROM conversations GROUP BY id HAVING COUNT(*) > 1)'
|
|
362
|
-
).get();
|
|
363
|
-
|
|
364
|
-
if (duplicates.count > 0) {
|
|
365
|
-
errors.push(`Found ${duplicates.count} duplicate conversation IDs`);
|
|
366
|
-
}
|
|
367
|
-
} catch (err) {
|
|
368
|
-
errors.push(`Integrity check failed: ${(err as Error).message}`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return { valid: errors.length === 0, errors };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// =========================================================================
|
|
375
|
-
// STREAMING EXECUTION OPERATIONS
|
|
376
|
-
// =========================================================================
|
|
377
|
-
|
|
378
|
-
storeExecutionEvent(sessionId: string, event: StreamingEvent): void {
|
|
379
|
-
this.checkClosed();
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
const stmt = this.db.prepare(
|
|
383
|
-
'INSERT INTO execution_events (sessionId, eventType, eventData, timestamp) VALUES (?, ?, ?, ?)'
|
|
384
|
-
);
|
|
385
|
-
stmt.run(sessionId, event.type, JSON.stringify(event), event.timestamp || Date.now());
|
|
386
|
-
} catch (err) {
|
|
387
|
-
throw new SyncError(
|
|
388
|
-
'DATABASE_ERROR',
|
|
389
|
-
`Failed to store execution event: ${(err as Error).message}`,
|
|
390
|
-
true,
|
|
391
|
-
{ sessionId, eventType: event.type }
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
storeExecutionMetadata(sessionId: string, metadata: ExecutionMetadata): void {
|
|
397
|
-
this.checkClosed();
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
const stmt = this.db.prepare(
|
|
401
|
-
'INSERT OR REPLACE INTO execution_metadata (sessionId, metadata, updated_at) VALUES (?, ?, ?)'
|
|
402
|
-
);
|
|
403
|
-
stmt.run(sessionId, JSON.stringify(metadata), Date.now());
|
|
404
|
-
} catch (err) {
|
|
405
|
-
throw new SyncError(
|
|
406
|
-
'DATABASE_ERROR',
|
|
407
|
-
`Failed to store execution metadata: ${(err as Error).message}`,
|
|
408
|
-
true,
|
|
409
|
-
{ sessionId }
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
getSessionExecutionHistory(sessionId: string, limit = 1000, offset = 0): StreamingEvent[] {
|
|
415
|
-
this.checkClosed();
|
|
416
|
-
|
|
417
|
-
try {
|
|
418
|
-
const stmt = this.db.prepare(
|
|
419
|
-
'SELECT eventData FROM execution_events WHERE sessionId = ? ORDER BY timestamp ASC LIMIT ? OFFSET ?'
|
|
420
|
-
);
|
|
421
|
-
const rows = stmt.all(sessionId, limit, offset);
|
|
422
|
-
|
|
423
|
-
return rows.map((row) => {
|
|
424
|
-
try {
|
|
425
|
-
return JSON.parse(row.eventData);
|
|
426
|
-
} catch (e) {
|
|
427
|
-
console.warn('[DatabaseService] Invalid execution event JSON:', row.eventData);
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
}).filter((e): e is StreamingEvent => e !== null);
|
|
431
|
-
} catch (err) {
|
|
432
|
-
throw new SyncError(
|
|
433
|
-
'DATABASE_ERROR',
|
|
434
|
-
`Failed to get execution history: ${(err as Error).message}`,
|
|
435
|
-
true,
|
|
436
|
-
{ sessionId, limit, offset }
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
getExecutionMetadata(sessionId: string): ExecutionMetadata | null {
|
|
442
|
-
this.checkClosed();
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
const stmt = this.db.prepare('SELECT metadata FROM execution_metadata WHERE sessionId = ?');
|
|
446
|
-
const row = stmt.get(sessionId);
|
|
447
|
-
|
|
448
|
-
if (!row) return null;
|
|
449
|
-
|
|
450
|
-
try {
|
|
451
|
-
return JSON.parse(row.metadata);
|
|
452
|
-
} catch (e) {
|
|
453
|
-
throw new SyncError('VALIDATION_ERROR', `Invalid metadata JSON for session ${sessionId}`, false);
|
|
454
|
-
}
|
|
455
|
-
} catch (err) {
|
|
456
|
-
if (err instanceof SyncError) throw err;
|
|
457
|
-
throw new SyncError(
|
|
458
|
-
'DATABASE_ERROR',
|
|
459
|
-
`Failed to get execution metadata: ${(err as Error).message}`,
|
|
460
|
-
true,
|
|
461
|
-
{ sessionId }
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
batchStoreExecutionEvents(sessionId: string, events: StreamingEvent[]): { count: number; latencyMs: number } {
|
|
467
|
-
this.checkClosed();
|
|
468
|
-
|
|
469
|
-
const startTime = Date.now();
|
|
470
|
-
try {
|
|
471
|
-
const transaction = this.db.transaction(() => {
|
|
472
|
-
const stmt = this.db.prepare(
|
|
473
|
-
'INSERT INTO execution_events (sessionId, eventType, eventData, timestamp) VALUES (?, ?, ?, ?)'
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
for (const event of events) {
|
|
477
|
-
stmt.run(sessionId, event.type, JSON.stringify(event), event.timestamp || Date.now());
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
transaction();
|
|
482
|
-
const latencyMs = Date.now() - startTime;
|
|
483
|
-
|
|
484
|
-
if (latencyMs > 100) {
|
|
485
|
-
console.warn(`[DatabaseService] Batch write latency ${latencyMs}ms for ${events.length} events`);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return { count: events.length, latencyMs };
|
|
489
|
-
} catch (err) {
|
|
490
|
-
if (err instanceof SyncError) throw err;
|
|
491
|
-
throw new SyncError(
|
|
492
|
-
'DATABASE_ERROR',
|
|
493
|
-
`Failed to batch store execution events: ${(err as Error).message}`,
|
|
494
|
-
true,
|
|
495
|
-
{ sessionId, count: events.length }
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
batchStoreExecutionEventsOptimized(
|
|
501
|
-
sessionId: string,
|
|
502
|
-
events: StreamingEvent[],
|
|
503
|
-
commitInterval = 100
|
|
504
|
-
): { count: number; batches: number; totalLatencyMs: number } {
|
|
505
|
-
this.checkClosed();
|
|
506
|
-
|
|
507
|
-
const startTime = Date.now();
|
|
508
|
-
let processedCount = 0;
|
|
509
|
-
let batchCount = 0;
|
|
510
|
-
|
|
511
|
-
try {
|
|
512
|
-
const stmt = this.db.prepare(
|
|
513
|
-
'INSERT INTO execution_events (sessionId, eventType, eventData, timestamp) VALUES (?, ?, ?, ?)'
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
// Process in batches with commit points
|
|
517
|
-
for (let i = 0; i < events.length; i += commitInterval) {
|
|
518
|
-
const batch = events.slice(i, i + commitInterval);
|
|
519
|
-
const transaction = this.db.transaction(() => {
|
|
520
|
-
for (const event of batch) {
|
|
521
|
-
stmt.run(sessionId, event.type, JSON.stringify(event), event.timestamp || Date.now());
|
|
522
|
-
processedCount++;
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
transaction();
|
|
527
|
-
batchCount++;
|
|
528
|
-
|
|
529
|
-
const elapsed = Date.now() - startTime;
|
|
530
|
-
if (elapsed > 5000) {
|
|
531
|
-
console.warn(`[DatabaseService] Long batch write: ${elapsed}ms for ${processedCount}/${events.length} events`);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const totalLatencyMs = Date.now() - startTime;
|
|
536
|
-
return { count: events.length, batches: batchCount, totalLatencyMs };
|
|
537
|
-
} catch (err) {
|
|
538
|
-
throw new SyncError(
|
|
539
|
-
'DATABASE_ERROR',
|
|
540
|
-
`Failed to batch store execution events (processed ${processedCount}/${events.length}): ${(err as Error).message}`,
|
|
541
|
-
true,
|
|
542
|
-
{ sessionId, count: events.length, processed: processedCount }
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
markSessionIncomplete(sessionId: string, reason: string): void {
|
|
548
|
-
this.checkClosed();
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
const stmt = this.db.prepare(
|
|
552
|
-
'UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?'
|
|
553
|
-
);
|
|
554
|
-
stmt.run('incomplete', reason, Date.now(), sessionId);
|
|
555
|
-
} catch (err) {
|
|
556
|
-
throw new SyncError(
|
|
557
|
-
'DATABASE_ERROR',
|
|
558
|
-
`Failed to mark session incomplete: ${(err as Error).message}`,
|
|
559
|
-
true,
|
|
560
|
-
{ sessionId, reason }
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
getIncompleteSessionsOlderThan(ageMinutes = 30): Array<{ id: string; started_at: number }> {
|
|
566
|
-
this.checkClosed();
|
|
567
|
-
|
|
568
|
-
try {
|
|
569
|
-
const cutoffTime = Date.now() - (ageMinutes * 60 * 1000);
|
|
570
|
-
const stmt = this.db.prepare(
|
|
571
|
-
'SELECT id, started_at FROM sessions WHERE status = ? AND started_at < ?'
|
|
572
|
-
);
|
|
573
|
-
return stmt.all('incomplete', cutoffTime);
|
|
574
|
-
} catch (err) {
|
|
575
|
-
throw new SyncError(
|
|
576
|
-
'DATABASE_ERROR',
|
|
577
|
-
`Failed to get incomplete sessions: ${(err as Error).message}`,
|
|
578
|
-
true,
|
|
579
|
-
{ ageMinutes }
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
markSessionComplete(sessionId: string, status: 'completed' | 'error' | 'timeout'): void {
|
|
585
|
-
this.checkClosed();
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
const stmt = this.db.prepare(
|
|
589
|
-
'UPDATE sessions SET status = ?, completed_at = ? WHERE id = ?'
|
|
590
|
-
);
|
|
591
|
-
stmt.run(status, Date.now(), sessionId);
|
|
592
|
-
} catch (err) {
|
|
593
|
-
throw new SyncError(
|
|
594
|
-
'DATABASE_ERROR',
|
|
595
|
-
`Failed to mark session complete: ${(err as Error).message}`,
|
|
596
|
-
true,
|
|
597
|
-
{ sessionId, status }
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
cleanupOrphanedSessions(maxAgeDays = 7): number {
|
|
603
|
-
this.checkClosed();
|
|
604
|
-
|
|
605
|
-
try {
|
|
606
|
-
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
607
|
-
const cutoffTime = Date.now() - maxAgeMs;
|
|
608
|
-
|
|
609
|
-
const stmt = this.db.prepare(
|
|
610
|
-
'DELETE FROM sessions WHERE status NOT IN (?, ?, ?) AND started_at < ?'
|
|
611
|
-
);
|
|
612
|
-
const result = stmt.run('processing', 'pending', 'incomplete', cutoffTime);
|
|
613
|
-
return (result.changes || 0);
|
|
614
|
-
} catch (err) {
|
|
615
|
-
throw new SyncError(
|
|
616
|
-
'DATABASE_ERROR',
|
|
617
|
-
`Failed to cleanup orphaned sessions: ${(err as Error).message}`,
|
|
618
|
-
true,
|
|
619
|
-
{ maxAgeDays }
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// =========================================================================
|
|
625
|
-
// LIFECYCLE
|
|
626
|
-
// =========================================================================
|
|
627
|
-
|
|
628
|
-
close() {
|
|
629
|
-
if (!this.closed) {
|
|
630
|
-
try {
|
|
631
|
-
this.db.close();
|
|
632
|
-
this.closed = true;
|
|
633
|
-
} catch (err) {
|
|
634
|
-
console.error('[DatabaseService] Error closing database:', err);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
export default DatabaseService;
|