@codemcp/workflows-core 3.1.20 → 3.1.21-fix-build-after-monorepo.1

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/src/database.ts CHANGED
@@ -1,685 +1,427 @@
1
1
  /**
2
- * Database module for persistent state storage
2
+ * Database Manager
3
3
  *
4
- * Manages SQLite database for conversation state persistence.
5
- * Stores minimal state information to survive server restarts.
6
- * Also stores interaction logs for auditing and debugging.
4
+ * Handles SQLite database operations for conversation state persistence.
5
+ * Uses @sqlite.org/sqlite-wasm for reliable cross-platform WebAssembly bindings.
7
6
  */
8
7
 
9
- import sqlite3 from 'sqlite3';
8
+ import sqlite3InitModule, {
9
+ type Database as SqliteDatabase,
10
+ type Sqlite3Static,
11
+ } from '@sqlite.org/sqlite-wasm';
10
12
  import { mkdir } from 'node:fs/promises';
11
13
  import { dirname } from 'node:path';
12
-
13
- import { join } from 'node:path';
14
14
  import { createLogger } from './logger.js';
15
- import type { DevelopmentPhase } from './state-machine.js';
16
- import type {
17
- ConversationState,
18
- InteractionLog,
19
- GitCommitConfig,
20
- } from './types.js';
15
+ import type { ConversationState, InteractionLog } from './types.js';
21
16
 
22
17
  const logger = createLogger('Database');
23
18
 
24
- // SQLite parameter types
25
- type SqliteParam = string | number | boolean | null | undefined | Buffer;
26
- type SqliteRow = Record<string, SqliteParam>;
27
- type SqliteColumnInfo = {
28
- cid: number;
29
- name: string;
30
- type: string;
31
- notnull: number;
32
- dflt_value: SqliteParam;
33
- pk: number;
34
- };
35
-
36
- // Database row validation utilities
37
- function validateString(value: SqliteParam, fieldName: string): string {
38
- if (typeof value === 'string') {
39
- return value;
40
- }
41
- throw new Error(
42
- `Database field '${fieldName}' expected string but got ${typeof value}: ${value}`
43
- );
44
- }
45
-
46
- function parseJsonSafely(value: SqliteParam, fieldName: string): unknown {
47
- if (!value) {
48
- return undefined;
49
- }
50
- const stringValue = validateString(value, fieldName);
51
- try {
52
- return JSON.parse(stringValue);
53
- } catch (error) {
54
- throw new Error(
55
- `Failed to parse JSON in field '${fieldName}': ${error instanceof Error ? error.message : String(error)}`
56
- );
57
- }
58
- }
59
-
60
- function mapRowToInteractionLog(row: SqliteRow): InteractionLog {
61
- return {
62
- id: typeof row.id === 'number' ? row.id : undefined,
63
- conversationId: validateString(row.conversationId, 'conversationId'),
64
- toolName: validateString(row.toolName, 'toolName'),
65
- inputParams: validateString(row.inputParams, 'inputParams'),
66
- responseData: validateString(row.responseData, 'responseData'),
67
- currentPhase: validateString(row.currentPhase, 'currentPhase'),
68
- timestamp: validateString(row.timestamp, 'timestamp'),
69
- isReset: typeof row.isReset === 'number' ? Boolean(row.isReset) : undefined,
70
- resetAt: row.resetAt ? validateString(row.resetAt, 'resetAt') : undefined,
71
- };
72
- }
73
-
19
+ /**
20
+ * Database connection and operations manager
21
+ */
74
22
  export class Database {
75
- private db: sqlite3.Database | null = null;
23
+ private db: SqliteDatabase | null = null;
24
+ private sqlite3: Sqlite3Static | null = null;
76
25
  private dbPath: string;
77
26
 
78
- constructor(projectPath: string) {
79
- // Store database in .vibe subfolder of the project
80
- const vibeDir = join(projectPath, '.vibe');
81
- this.dbPath = join(vibeDir, 'conversation-state.sqlite');
82
- logger.debug('Database path configured', {
83
- projectPath,
84
- dbPath: this.dbPath,
85
- });
27
+ constructor(dbPath: string) {
28
+ this.dbPath = dbPath;
86
29
  }
87
30
 
88
31
  /**
89
32
  * Initialize database connection and create tables
90
33
  */
91
34
  async initialize(): Promise<void> {
92
- logger.debug('Initializing database', { dbPath: this.dbPath });
93
-
94
35
  try {
95
- // Ensure directory exists
96
- await mkdir(dirname(this.dbPath), { recursive: true });
97
- logger.debug('Database directory ensured', {
98
- directory: dirname(this.dbPath),
36
+ // Initialize SQLite WASM
37
+ this.sqlite3 = await sqlite3InitModule();
38
+
39
+ // Always use in-memory database (sqlite-wasm Node.js limitation)
40
+ this.db = new this.sqlite3.oo1.DB();
41
+ logger.debug('Database connection established (in-memory)', {
42
+ originalPath: this.dbPath,
99
43
  });
100
44
 
101
- // Create database connection
102
- this.db = new sqlite3.Database(this.dbPath);
103
- logger.debug('Database connection established');
104
-
105
- // Create conversation_states table
106
- await this.runQuery(`
107
- CREATE TABLE IF NOT EXISTS conversation_states (
108
- conversation_id TEXT PRIMARY KEY,
109
- project_path TEXT NOT NULL,
110
- git_branch TEXT NOT NULL,
111
- current_phase TEXT NOT NULL,
112
- plan_file_path TEXT NOT NULL,
113
- workflow_name TEXT DEFAULT 'waterfall',
114
- git_commit_config TEXT, -- JSON string for GitCommitConfig
115
- created_at TEXT NOT NULL,
116
- updated_at TEXT NOT NULL
117
- )
118
- `);
119
-
120
- // Create index for efficient lookups
121
- await this.runQuery(`
122
- CREATE INDEX IF NOT EXISTS idx_project_branch
123
- ON conversation_states(project_path, git_branch)
124
- `);
125
-
126
- // Create interaction_logs table
127
- await this.runQuery(`
128
- CREATE TABLE IF NOT EXISTS interaction_logs (
129
- id INTEGER PRIMARY KEY AUTOINCREMENT,
130
- conversation_id TEXT NOT NULL,
131
- tool_name TEXT NOT NULL,
132
- input_params TEXT NOT NULL,
133
- response_data TEXT NOT NULL,
134
- current_phase TEXT NOT NULL,
135
- timestamp TEXT NOT NULL,
136
- is_reset BOOLEAN DEFAULT FALSE,
137
- reset_at TEXT,
138
- FOREIGN KEY (conversation_id) REFERENCES conversation_states(conversation_id)
139
- )
140
- `);
141
-
142
- // Create index for efficient lookups of interaction logs
143
- await this.runQuery(`
144
- CREATE INDEX IF NOT EXISTS idx_interaction_conversation_id
145
- ON interaction_logs(conversation_id)
146
- `);
147
-
148
- // Run migrations to add any missing columns
149
- await this.runMigrations();
45
+ // Create tables
46
+ await this.createTables();
47
+
48
+ // Load existing data from file if it exists
49
+ if (this.dbPath !== ':memory:' && this.dbPath) {
50
+ await this.loadFromFile();
51
+ }
150
52
 
151
53
  logger.info('Database initialized successfully', { dbPath: this.dbPath });
152
54
  } catch (error) {
153
- logger.error('Failed to initialize database', error as Error, {
154
- dbPath: this.dbPath,
155
- });
55
+ logger.error('Failed to initialize database', error as Error);
156
56
  throw error;
157
57
  }
158
58
  }
159
59
 
160
60
  /**
161
- * Helper method to run queries with promises
61
+ * Load database content from file
162
62
  */
163
- private runQuery(sql: string, params: SqliteParam[] = []): Promise<void> {
164
- return new Promise((resolve, reject) => {
165
- if (!this.db) {
166
- reject(new Error('Database not initialized'));
167
- return;
168
- }
63
+ private async loadFromFile(): Promise<void> {
64
+ if (!this.db || !this.dbPath || this.dbPath === ':memory:') {
65
+ return;
66
+ }
169
67
 
170
- this.db.run(sql, params, function (err) {
171
- if (err) {
172
- reject(err);
173
- } else {
174
- resolve();
68
+ try {
69
+ const { readFile, access } = await import('node:fs/promises');
70
+ await access(this.dbPath);
71
+
72
+ const data = await readFile(this.dbPath);
73
+ if (data.length > 0) {
74
+ // Close current in-memory DB and create new one from file data
75
+ this.db.close();
76
+ // Create new DB and deserialize data into it
77
+
78
+ //eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79
+ this.db = new this.sqlite3!.oo1.DB();
80
+ if (!this.db.pointer) {
81
+ throw new Error('Failed to create database');
175
82
  }
176
- });
177
- });
178
- }
179
83
 
180
- /**
181
- * Helper method to get single row with promises
182
- */
183
- private getRow(
184
- sql: string,
185
- params: SqliteParam[] = []
186
- ): Promise<SqliteRow | null> {
187
- return new Promise((resolve, reject) => {
188
- if (!this.db) {
189
- reject(new Error('Database not initialized'));
190
- return;
191
- }
84
+ // Convert Buffer to Uint8Array
85
+ const uint8Data = new Uint8Array(data);
192
86
 
193
- this.db.get(sql, params, (err, row) => {
194
- if (err) {
195
- reject(err);
196
- } else {
197
- resolve(row as SqliteRow | null);
198
- }
199
- });
200
- });
201
- }
87
+ //eslint-disable-next-line @typescript-eslint/no-non-null-assertion
88
+ const wasmPtr = this.sqlite3!.wasm.allocFromTypedArray(uint8Data);
202
89
 
203
- /**
204
- * Helper method to get multiple rows with promises
205
- */
206
- private getAllRows(
207
- sql: string,
208
- params: SqliteParam[] = []
209
- ): Promise<SqliteRow[]> {
210
- return new Promise((resolve, reject) => {
211
- if (!this.db) {
212
- reject(new Error('Database not initialized'));
213
- return;
90
+ //eslint-disable-next-line @typescript-eslint/no-non-null-assertion
91
+ this.sqlite3!.capi.sqlite3_deserialize(
92
+ this.db.pointer,
93
+ 'main',
94
+ wasmPtr,
95
+ data.length,
96
+ data.length,
97
+ 0x01 // SQLITE_DESERIALIZE_FREEONCLOSE
98
+ );
99
+ logger.debug('Loaded database from file', {
100
+ dbPath: this.dbPath,
101
+ size: data.length,
102
+ });
214
103
  }
215
-
216
- this.db.all(sql, params, (err, rows) => {
217
- if (err) {
218
- reject(err);
219
- } else {
220
- resolve(rows as SqliteRow[]);
221
- }
104
+ } catch {
105
+ // File doesn't exist - that's OK for new databases
106
+ logger.debug('No existing database file to load', {
107
+ dbPath: this.dbPath,
222
108
  });
223
- });
109
+ }
224
110
  }
225
111
 
226
112
  /**
227
- * Get conversation state by ID
113
+ * Save database content to file
228
114
  */
229
- async getConversationState(
230
- conversationId: string
231
- ): Promise<ConversationState | null> {
232
- logger.debug('Retrieving conversation state', { conversationId });
115
+ private async saveToFile(): Promise<void> {
116
+ if (!this.db || !this.dbPath || this.dbPath === ':memory:') {
117
+ return;
118
+ }
233
119
 
234
120
  try {
235
- const row = await this.getRow(
236
- 'SELECT * FROM conversation_states WHERE conversation_id = ?',
237
- [conversationId]
238
- );
239
-
240
- if (!row) {
241
- logger.debug('Conversation state not found', { conversationId });
242
- return null;
243
- }
121
+ const { writeFile } = await import('node:fs/promises');
122
+ const dbDir = dirname(this.dbPath);
123
+ await mkdir(dbDir, { recursive: true });
244
124
 
245
- const state: ConversationState = {
246
- conversationId: validateString(row.conversation_id, 'conversation_id'),
247
- projectPath: validateString(row.project_path, 'project_path'),
248
- gitBranch: validateString(row.git_branch, 'git_branch'),
249
- currentPhase: validateString(
250
- row.current_phase,
251
- 'current_phase'
252
- ) as DevelopmentPhase,
253
- planFilePath: validateString(row.plan_file_path, 'plan_file_path'),
254
- workflowName: validateString(row.workflow_name, 'workflow_name'),
255
- gitCommitConfig: parseJsonSafely(
256
- row.git_commit_config,
257
- 'git_commit_config'
258
- ) as GitCommitConfig | undefined,
259
- requireReviewsBeforePhaseTransition: Boolean(
260
- row.require_reviews_before_phase_transition
261
- ),
262
- createdAt: validateString(row.created_at, 'created_at'),
263
- updatedAt: validateString(row.updated_at, 'updated_at'),
264
- };
265
-
266
- logger.debug('Conversation state retrieved', {
267
- conversationId,
268
- currentPhase: state.currentPhase,
269
- projectPath: state.projectPath,
125
+ // Export database to Uint8Array and save to file
126
+ if (!this.db.pointer) {
127
+ throw new Error('Database pointer is invalid');
128
+ }
129
+ //eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130
+ const data = this.sqlite3!.capi.sqlite3_js_db_export(this.db.pointer);
131
+ await writeFile(this.dbPath, data);
132
+ logger.debug('Saved database to file', {
133
+ dbPath: this.dbPath,
134
+ size: data.length,
270
135
  });
271
-
272
- return state;
273
136
  } catch (error) {
274
- logger.error('Failed to retrieve conversation state', error as Error, {
275
- conversationId,
137
+ logger.warn('Failed to save database to file', {
138
+ error: error as Error,
139
+ dbPath: this.dbPath,
276
140
  });
277
- throw error;
278
141
  }
279
142
  }
280
143
 
281
144
  /**
282
- * Save or update conversation state
145
+ * Create database tables if they don't exist
146
+ */
147
+ private async createTables(): Promise<void> {
148
+ if (!this.db) {
149
+ throw new Error('Database not initialized');
150
+ }
151
+
152
+ const createConversationStateTable = `
153
+ CREATE TABLE IF NOT EXISTS conversation_state (
154
+ conversationId TEXT PRIMARY KEY,
155
+ projectPath TEXT NOT NULL,
156
+ gitBranch TEXT NOT NULL,
157
+ currentPhase TEXT NOT NULL,
158
+ planFilePath TEXT NOT NULL,
159
+ workflowName TEXT NOT NULL,
160
+ gitCommitConfig TEXT,
161
+ requireReviewsBeforePhaseTransition INTEGER NOT NULL DEFAULT 0,
162
+ createdAt TEXT NOT NULL,
163
+ updatedAt TEXT NOT NULL
164
+ )
165
+ `;
166
+
167
+ const createInteractionLogTable = `
168
+ CREATE TABLE IF NOT EXISTS interaction_log (
169
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
170
+ conversationId TEXT NOT NULL,
171
+ toolName TEXT NOT NULL,
172
+ inputParams TEXT NOT NULL,
173
+ responseData TEXT NOT NULL,
174
+ currentPhase TEXT NOT NULL,
175
+ timestamp TEXT NOT NULL,
176
+ FOREIGN KEY (conversationId) REFERENCES conversation_state(conversationId)
177
+ )
178
+ `;
179
+
180
+ this.db.exec(createConversationStateTable);
181
+ this.db.exec(createInteractionLogTable);
182
+
183
+ logger.debug('Database tables created');
184
+ }
185
+
186
+ /**
187
+ * Save conversation state to database
283
188
  */
284
189
  async saveConversationState(state: ConversationState): Promise<void> {
285
- logger.debug('Saving conversation state', {
190
+ if (!this.db) {
191
+ throw new Error('Database not initialized');
192
+ }
193
+
194
+ this.db.exec({
195
+ sql: `INSERT OR REPLACE INTO conversation_state
196
+ (conversationId, projectPath, gitBranch, currentPhase, planFilePath, workflowName,
197
+ gitCommitConfig, requireReviewsBeforePhaseTransition, createdAt, updatedAt)
198
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
199
+ bind: [
200
+ state.conversationId,
201
+ state.projectPath,
202
+ state.gitBranch,
203
+ state.currentPhase,
204
+ state.planFilePath,
205
+ state.workflowName,
206
+ state.gitCommitConfig ? JSON.stringify(state.gitCommitConfig) : null,
207
+ state.requireReviewsBeforePhaseTransition ? 1 : 0,
208
+ state.createdAt,
209
+ state.updatedAt,
210
+ ],
211
+ });
212
+
213
+ // Persist to file
214
+ await this.saveToFile();
215
+
216
+ logger.debug('Conversation state saved', {
286
217
  conversationId: state.conversationId,
287
218
  currentPhase: state.currentPhase,
288
- projectPath: state.projectPath,
289
- workflowName: state.workflowName,
290
219
  });
291
-
292
- try {
293
- await this.runQuery(
294
- `INSERT OR REPLACE INTO conversation_states (
295
- conversation_id, project_path, git_branch, current_phase,
296
- plan_file_path, workflow_name, git_commit_config, require_reviews_before_phase_transition, created_at, updated_at
297
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
298
- [
299
- state.conversationId,
300
- state.projectPath,
301
- state.gitBranch,
302
- state.currentPhase,
303
- state.planFilePath,
304
- state.workflowName,
305
- state.gitCommitConfig ? JSON.stringify(state.gitCommitConfig) : null,
306
- state.requireReviewsBeforePhaseTransition,
307
- state.createdAt,
308
- state.updatedAt,
309
- ]
310
- );
311
-
312
- logger.info('Conversation state saved successfully', {
313
- conversationId: state.conversationId,
314
- currentPhase: state.currentPhase,
315
- });
316
- } catch (error) {
317
- logger.error('Failed to save conversation state', error as Error, {
318
- conversationId: state.conversationId,
319
- });
320
- throw error;
321
- }
322
220
  }
323
221
 
324
222
  /**
325
- * Find conversation by project path and git branch
223
+ * Get conversation state by ID
326
224
  */
327
- async findConversationByProject(
328
- projectPath: string,
329
- gitBranch: string
225
+ async getConversationState(
226
+ conversationId: string
330
227
  ): Promise<ConversationState | null> {
331
- const row = await this.getRow(
332
- 'SELECT * FROM conversation_states WHERE project_path = ? AND git_branch = ?',
333
- [projectPath, gitBranch]
334
- );
228
+ if (!this.db) {
229
+ throw new Error('Database not initialized');
230
+ }
335
231
 
336
- if (!row) {
232
+ const result = this.db.exec({
233
+ sql: 'SELECT * FROM conversation_state WHERE conversationId = ?',
234
+ bind: [conversationId],
235
+ returnValue: 'resultRows',
236
+ });
237
+
238
+ if (!result || result.length === 0) {
337
239
  return null;
338
240
  }
339
241
 
242
+ const row = result[0];
340
243
  return {
341
- conversationId: validateString(row.conversation_id, 'conversation_id'),
342
- projectPath: validateString(row.project_path, 'project_path'),
343
- gitBranch: validateString(row.git_branch, 'git_branch'),
344
- currentPhase: validateString(
345
- row.current_phase,
346
- 'current_phase'
347
- ) as DevelopmentPhase,
348
- planFilePath: validateString(row.plan_file_path, 'plan_file_path'),
349
- workflowName: validateString(row.workflow_name, 'workflow_name'),
350
- gitCommitConfig: parseJsonSafely(
351
- row.git_commit_config,
352
- 'git_commit_config'
353
- ) as GitCommitConfig | undefined,
354
- requireReviewsBeforePhaseTransition: Boolean(
355
- row.require_reviews_before_phase_transition
356
- ),
357
- createdAt: validateString(row.created_at, 'created_at'),
358
- updatedAt: validateString(row.updated_at, 'updated_at'),
244
+ conversationId: row[0] as string,
245
+ projectPath: row[1] as string,
246
+ gitBranch: row[2] as string,
247
+ currentPhase: row[3] as string,
248
+ planFilePath: row[4] as string,
249
+ workflowName: row[5] as string,
250
+ gitCommitConfig: row[6] ? JSON.parse(row[6] as string) : null,
251
+ requireReviewsBeforePhaseTransition: Boolean(row[7]),
252
+ createdAt: row[8] as string,
253
+ updatedAt: row[9] as string,
359
254
  };
360
255
  }
361
256
 
362
257
  /**
363
- * Delete conversation state
258
+ * Get all conversation states
364
259
  */
365
- async deleteConversationState(conversationId: string): Promise<void> {
366
- await this.runQuery(
367
- 'DELETE FROM conversation_states WHERE conversation_id = ?',
368
- [conversationId]
369
- );
370
- }
260
+ async getAllConversationStates(): Promise<ConversationState[]> {
261
+ if (!this.db) {
262
+ throw new Error('Database not initialized');
263
+ }
371
264
 
372
- /**
373
- * Log an interaction to the database
374
- */
375
- async logInteraction(log: InteractionLog): Promise<void> {
376
- logger.debug('Logging interaction to database', {
377
- conversationId: log.conversationId,
378
- toolName: log.toolName,
265
+ const result = this.db.exec({
266
+ sql: 'SELECT * FROM conversation_state ORDER BY updatedAt DESC',
267
+ returnValue: 'resultRows',
379
268
  });
380
269
 
381
- try {
382
- await this.runQuery(
383
- `INSERT INTO interaction_logs (
384
- conversation_id, tool_name, input_params, response_data,
385
- current_phase, timestamp
386
- ) VALUES (?, ?, ?, ?, ?, ?)`,
387
- [
388
- log.conversationId,
389
- log.toolName,
390
- log.inputParams,
391
- log.responseData,
392
- log.currentPhase,
393
- log.timestamp,
394
- ]
395
- );
396
-
397
- logger.debug('Interaction logged successfully', {
398
- conversationId: log.conversationId,
399
- toolName: log.toolName,
400
- timestamp: log.timestamp,
401
- });
402
- } catch (error) {
403
- logger.error('Failed to log interaction', error as Error, {
404
- conversationId: log.conversationId,
405
- });
406
- throw error;
270
+ if (!result) {
271
+ return [];
407
272
  }
273
+
274
+ return result.map(row => ({
275
+ conversationId: row[0] as string,
276
+ projectPath: row[1] as string,
277
+ gitBranch: row[2] as string,
278
+ currentPhase: row[3] as string,
279
+ planFilePath: row[4] as string,
280
+ workflowName: row[5] as string,
281
+ gitCommitConfig: row[6] ? JSON.parse(row[6] as string) : null,
282
+ requireReviewsBeforePhaseTransition: Boolean(row[7]),
283
+ createdAt: row[8] as string,
284
+ updatedAt: row[9] as string,
285
+ }));
408
286
  }
409
287
 
410
288
  /**
411
- * Get all interactions for a specific conversation
289
+ * Delete conversation state
412
290
  */
413
- async getInteractionsByConversationId(
414
- conversationId: string
415
- ): Promise<InteractionLog[]> {
416
- logger.debug('Getting interactions by conversation ID', { conversationId });
291
+ async deleteConversationState(conversationId: string): Promise<boolean> {
292
+ if (!this.db) {
293
+ throw new Error('Database not initialized');
294
+ }
417
295
 
418
- try {
419
- const rows = await this.getAllRows(
420
- 'SELECT * FROM interaction_logs WHERE conversation_id = ? ORDER BY timestamp ASC',
421
- [conversationId]
422
- );
423
-
424
- const logs: InteractionLog[] = rows.map(row => ({
425
- id: typeof row.id === 'number' ? row.id : undefined,
426
- conversationId: validateString(row.conversation_id, 'conversation_id'),
427
- toolName: validateString(row.tool_name, 'tool_name'),
428
- inputParams: validateString(row.input_params, 'input_params'),
429
- responseData: validateString(row.response_data, 'response_data'),
430
- currentPhase: validateString(
431
- row.current_phase,
432
- 'current_phase'
433
- ) as DevelopmentPhase,
434
- timestamp: validateString(row.timestamp, 'timestamp'),
435
- }));
436
-
437
- logger.debug('Retrieved interaction logs', {
438
- conversationId,
439
- count: logs.length,
440
- });
296
+ this.db.exec({
297
+ sql: 'DELETE FROM conversation_state WHERE conversationId = ?',
298
+ bind: [conversationId],
299
+ });
441
300
 
442
- return logs;
443
- } catch (error) {
444
- logger.error('Failed to get interaction logs', error as Error, {
445
- conversationId,
446
- });
447
- throw error;
448
- }
301
+ // Persist to file
302
+ await this.saveToFile();
303
+
304
+ logger.debug('Conversation state deleted', { conversationId });
305
+ return true;
449
306
  }
450
307
 
451
308
  /**
452
- * Run database migrations to add new columns
309
+ * Log interaction
453
310
  */
454
- private async runMigrations(): Promise<void> {
455
- logger.debug('Running database migrations');
456
-
457
- try {
458
- // Check if interaction_logs table exists first
459
- const tables = await this.getAllRows(
460
- "SELECT name FROM sqlite_master WHERE type='table' AND name='interaction_logs'"
461
- );
462
-
463
- if (tables.length > 0) {
464
- // Table exists, check for missing columns
465
- const tableInfo = await this.getAllRows(
466
- 'PRAGMA table_info(interaction_logs)'
467
- );
468
- const hasIsReset = (tableInfo as unknown as SqliteColumnInfo[]).some(
469
- (col: SqliteColumnInfo) => col.name === 'is_reset'
470
- );
471
- const hasResetAt = (tableInfo as unknown as SqliteColumnInfo[]).some(
472
- (col: SqliteColumnInfo) => col.name === 'reset_at'
473
- );
474
-
475
- if (!hasIsReset) {
476
- logger.info('Adding is_reset column to interaction_logs table');
477
- await this.runQuery(
478
- 'ALTER TABLE interaction_logs ADD COLUMN is_reset BOOLEAN DEFAULT FALSE'
479
- );
480
- }
481
-
482
- if (!hasResetAt) {
483
- logger.info('Adding reset_at column to interaction_logs table');
484
- await this.runQuery(
485
- 'ALTER TABLE interaction_logs ADD COLUMN reset_at TEXT'
486
- );
487
- }
488
- }
489
-
490
- // Check if conversation_states table exists and has workflow_name column
491
- const conversationTables = await this.getAllRows(
492
- "SELECT name FROM sqlite_master WHERE type='table' AND name='conversation_states'"
493
- );
494
-
495
- if (conversationTables.length > 0) {
496
- const conversationTableInfo = (await this.getAllRows(
497
- 'PRAGMA table_info(conversation_states)'
498
- )) as SqliteColumnInfo[];
499
- const hasWorkflowName = conversationTableInfo.some(
500
- (col: SqliteColumnInfo) => col.name === 'workflow_name'
501
- );
502
- const hasGitCommitConfig = conversationTableInfo.some(
503
- (col: SqliteColumnInfo) => col.name === 'git_commit_config'
504
- );
505
- const hasRequireReviews = conversationTableInfo.some(
506
- (col: SqliteColumnInfo) =>
507
- col.name === 'require_reviews_before_phase_transition'
508
- );
509
-
510
- if (!hasWorkflowName) {
511
- logger.info(
512
- 'Adding workflow_name column to conversation_states table'
513
- );
514
- await this.runQuery(
515
- "ALTER TABLE conversation_states ADD COLUMN workflow_name TEXT DEFAULT 'waterfall'"
516
- );
517
- }
311
+ async logInteraction(log: InteractionLog): Promise<void> {
312
+ if (!this.db) {
313
+ throw new Error('Database not initialized');
314
+ }
518
315
 
519
- if (!hasGitCommitConfig) {
520
- logger.info(
521
- 'Adding git_commit_config column to conversation_states table'
522
- );
523
- await this.runQuery(
524
- 'ALTER TABLE conversation_states ADD COLUMN git_commit_config TEXT'
525
- );
526
- }
316
+ this.db.exec({
317
+ sql: `INSERT INTO interaction_log
318
+ (conversationId, toolName, inputParams, responseData, currentPhase, timestamp)
319
+ VALUES (?, ?, ?, ?, ?, ?)`,
320
+ bind: [
321
+ log.conversationId,
322
+ log.toolName,
323
+ JSON.stringify(log.inputParams),
324
+ JSON.stringify(log.responseData),
325
+ log.currentPhase,
326
+ log.timestamp,
327
+ ],
328
+ });
527
329
 
528
- if (!hasRequireReviews) {
529
- logger.info(
530
- 'Adding require_reviews_before_phase_transition column to conversation_states table'
531
- );
532
- await this.runQuery(
533
- 'ALTER TABLE conversation_states ADD COLUMN require_reviews_before_phase_transition BOOLEAN DEFAULT FALSE'
534
- );
535
- }
536
- }
330
+ // Persist to file
331
+ await this.saveToFile();
537
332
 
538
- logger.debug('Database migrations completed successfully');
539
- } catch (error) {
540
- logger.error('Failed to run database migrations', error as Error);
541
- throw error;
542
- }
333
+ logger.debug('Interaction logged', {
334
+ conversationId: log.conversationId,
335
+ toolName: log.toolName,
336
+ });
543
337
  }
544
338
 
545
339
  /**
546
- * Soft delete interaction logs for a conversation
340
+ * Get interaction logs for a conversation
547
341
  */
548
- async softDeleteInteractionLogs(
549
- conversationId: string,
550
- reason?: string
551
- ): Promise<void> {
552
- logger.debug('Soft deleting interaction logs', { conversationId, reason });
342
+ async getInteractionLogs(conversationId: string): Promise<InteractionLog[]> {
343
+ if (!this.db) {
344
+ throw new Error('Database not initialized');
345
+ }
553
346
 
554
- try {
555
- const resetAt = new Date().toISOString();
556
- await this.runQuery(
557
- 'UPDATE interaction_logs SET is_reset = TRUE, reset_at = ? WHERE conversation_id = ? AND is_reset = FALSE',
558
- [resetAt, conversationId]
559
- );
560
-
561
- logger.info('Interaction logs soft deleted successfully', {
562
- conversationId,
563
- reason,
564
- resetAt,
565
- });
566
- } catch (error) {
567
- logger.error('Failed to soft delete interaction logs', error as Error, {
568
- conversationId,
569
- });
570
- throw error;
347
+ const result = this.db.exec({
348
+ sql: 'SELECT * FROM interaction_log WHERE conversationId = ? ORDER BY timestamp ASC',
349
+ bind: [conversationId],
350
+ returnValue: 'resultRows',
351
+ });
352
+
353
+ if (!result) {
354
+ return [];
571
355
  }
356
+
357
+ return result.map(row => ({
358
+ id: row[0] as number,
359
+ conversationId: row[1] as string,
360
+ toolName: row[2] as string,
361
+ inputParams: JSON.parse(row[3] as string),
362
+ responseData: JSON.parse(row[4] as string),
363
+ currentPhase: row[5] as string,
364
+ timestamp: row[6] as string,
365
+ }));
572
366
  }
573
367
 
574
368
  /**
575
- * Get active (non-reset) interaction logs for a conversation
369
+ * Get interaction logs for a conversation (alias for compatibility)
576
370
  */
577
- async getActiveInteractionLogs(
371
+ async getInteractionsByConversationId(
578
372
  conversationId: string
579
373
  ): Promise<InteractionLog[]> {
580
- logger.debug('Getting active interaction logs', { conversationId });
581
-
582
- try {
583
- const rows = await this.getAllRows(
584
- 'SELECT * FROM interaction_logs WHERE conversation_id = ? AND (is_reset = FALSE OR is_reset IS NULL) ORDER BY timestamp ASC',
585
- [conversationId]
586
- );
587
-
588
- const logs: InteractionLog[] = rows.map(row =>
589
- mapRowToInteractionLog({
590
- id: row.id,
591
- conversationId: row.conversation_id,
592
- toolName: row.tool_name,
593
- inputParams: row.input_params,
594
- responseData: row.response_data,
595
- currentPhase: row.current_phase,
596
- timestamp: row.timestamp,
597
- })
598
- );
599
-
600
- logger.debug('Retrieved active interaction logs', {
601
- conversationId,
602
- count: logs.length,
603
- });
374
+ return this.getInteractionLogs(conversationId);
375
+ }
604
376
 
605
- return logs;
606
- } catch (error) {
607
- logger.error('Failed to get active interaction logs', error as Error, {
608
- conversationId,
609
- });
610
- throw error;
377
+ /**
378
+ * Soft delete interaction logs (for compatibility - actually deletes them)
379
+ */
380
+ async softDeleteInteractionLogs(conversationId: string): Promise<void> {
381
+ if (!this.db) {
382
+ throw new Error('Database not initialized');
611
383
  }
384
+
385
+ this.db.exec({
386
+ sql: 'DELETE FROM interaction_log WHERE conversationId = ?',
387
+ bind: [conversationId],
388
+ });
389
+
390
+ // Persist to file
391
+ await this.saveToFile();
392
+
393
+ logger.debug('Interaction logs deleted', { conversationId });
612
394
  }
613
395
 
614
396
  /**
615
- * Get all interaction logs including reset ones for a conversation
397
+ * Reset conversation state (for testing)
616
398
  */
617
- async getAllInteractionLogsIncludingReset(
618
- conversationId: string
619
- ): Promise<InteractionLog[]> {
620
- logger.debug('Getting all interaction logs including reset', {
621
- conversationId,
399
+ async resetConversationState(conversationId: string): Promise<void> {
400
+ if (!this.db) {
401
+ throw new Error('Database not initialized');
402
+ }
403
+
404
+ const resetAt = new Date().toISOString();
405
+
406
+ this.db.exec({
407
+ sql: 'UPDATE conversation_state SET updatedAt = ? WHERE conversationId = ?',
408
+ bind: [resetAt, conversationId],
622
409
  });
623
410
 
624
- try {
625
- const rows = await this.getAllRows(
626
- 'SELECT * FROM interaction_logs WHERE conversation_id = ? ORDER BY timestamp ASC',
627
- [conversationId]
628
- );
629
-
630
- const logs: InteractionLog[] = rows.map(row =>
631
- mapRowToInteractionLog({
632
- id: row.id,
633
- conversationId: row.conversation_id,
634
- toolName: row.tool_name,
635
- inputParams: row.input_params,
636
- responseData: row.response_data,
637
- currentPhase: row.current_phase,
638
- timestamp: row.timestamp,
639
- isReset: row.is_reset,
640
- resetAt: row.reset_at,
641
- })
642
- );
643
-
644
- logger.debug('Retrieved all interaction logs including reset', {
645
- conversationId,
646
- count: logs.length,
647
- resetCount: logs.filter(log => log.isReset).length,
648
- });
411
+ // Persist to file
412
+ await this.saveToFile();
649
413
 
650
- return logs;
651
- } catch (error) {
652
- logger.error(
653
- 'Failed to get all interaction logs including reset',
654
- error as Error,
655
- { conversationId }
656
- );
657
- throw error;
658
- }
414
+ logger.debug('Conversation state reset', { conversationId, resetAt });
659
415
  }
660
416
 
661
417
  /**
662
418
  * Close database connection
663
419
  */
664
420
  async close(): Promise<void> {
665
- logger.debug('Closing database connection');
666
-
667
- return new Promise((resolve, reject) => {
668
- if (this.db) {
669
- this.db.close(err => {
670
- if (err) {
671
- logger.error('Failed to close database connection', err);
672
- reject(err);
673
- } else {
674
- this.db = null;
675
- logger.info('Database connection closed successfully');
676
- resolve();
677
- }
678
- });
679
- } else {
680
- logger.debug('Database connection already closed');
681
- resolve();
682
- }
683
- });
421
+ if (this.db) {
422
+ this.db.close();
423
+ this.db = null;
424
+ logger.debug('Database connection closed');
425
+ }
684
426
  }
685
427
  }