@codemcp/workflows-core 3.1.19 → 3.1.21-fix-build-after-monorepo.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/src/database.ts CHANGED
@@ -1,685 +1,373 @@
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 better-sqlite3 for reliable cross-platform native bindings.
7
6
  */
8
7
 
9
- import sqlite3 from 'sqlite3';
8
+ import BetterSqlite3 from 'better-sqlite3';
10
9
  import { mkdir } from 'node:fs/promises';
11
10
  import { dirname } from 'node:path';
12
-
13
- import { join } from 'node:path';
14
11
  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';
12
+ import type { ConversationState, InteractionLog } from './types.js';
21
13
 
22
14
  const logger = createLogger('Database');
23
15
 
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
-
16
+ /**
17
+ * Database connection and operations manager
18
+ */
74
19
  export class Database {
75
- private db: sqlite3.Database | null = null;
20
+ private db: BetterSqlite3.Database | null = null;
76
21
  private dbPath: string;
77
22
 
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
- });
23
+ constructor(dbPath: string) {
24
+ this.dbPath = dbPath;
86
25
  }
87
26
 
88
27
  /**
89
28
  * Initialize database connection and create tables
90
29
  */
91
30
  async initialize(): Promise<void> {
92
- logger.debug('Initializing database', { dbPath: this.dbPath });
93
-
94
31
  try {
95
32
  // Ensure directory exists
96
- await mkdir(dirname(this.dbPath), { recursive: true });
97
- logger.debug('Database directory ensured', {
98
- directory: dirname(this.dbPath),
99
- });
33
+ const dbDir = dirname(this.dbPath);
34
+ await mkdir(dbDir, { recursive: true });
35
+ logger.debug('Database directory ensured', { dbDir });
100
36
 
101
37
  // Create database connection
102
- this.db = new sqlite3.Database(this.dbPath);
38
+ this.db = new BetterSqlite3(this.dbPath);
103
39
  logger.debug('Database connection established');
104
40
 
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();
150
-
41
+ // Create tables
42
+ await this.createTables();
151
43
  logger.info('Database initialized successfully', { dbPath: this.dbPath });
152
44
  } catch (error) {
153
- logger.error('Failed to initialize database', error as Error, {
154
- dbPath: this.dbPath,
155
- });
45
+ logger.error('Failed to initialize database', error as Error);
156
46
  throw error;
157
47
  }
158
48
  }
159
49
 
160
50
  /**
161
- * Helper method to run queries with promises
51
+ * Create database tables if they don't exist
162
52
  */
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
- }
169
-
170
- this.db.run(sql, params, function (err) {
171
- if (err) {
172
- reject(err);
173
- } else {
174
- resolve();
175
- }
176
- });
177
- });
178
- }
53
+ private async createTables(): Promise<void> {
54
+ if (!this.db) {
55
+ throw new Error('Database not initialized');
56
+ }
179
57
 
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
- }
192
-
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
- });
58
+ const createConversationStateTable = `
59
+ CREATE TABLE IF NOT EXISTS conversation_state (
60
+ conversationId TEXT PRIMARY KEY,
61
+ projectPath TEXT NOT NULL,
62
+ gitBranch TEXT NOT NULL,
63
+ currentPhase TEXT NOT NULL,
64
+ planFilePath TEXT NOT NULL,
65
+ workflowName TEXT NOT NULL,
66
+ gitCommitConfig TEXT,
67
+ requireReviewsBeforePhaseTransition INTEGER NOT NULL DEFAULT 0,
68
+ createdAt TEXT NOT NULL,
69
+ updatedAt TEXT NOT NULL
70
+ )
71
+ `;
72
+
73
+ const createInteractionLogTable = `
74
+ CREATE TABLE IF NOT EXISTS interaction_log (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ conversationId TEXT NOT NULL,
77
+ toolName TEXT NOT NULL,
78
+ inputParams TEXT NOT NULL,
79
+ responseData TEXT NOT NULL,
80
+ currentPhase TEXT NOT NULL,
81
+ timestamp TEXT NOT NULL,
82
+ isReset INTEGER DEFAULT 0,
83
+ resetAt TEXT,
84
+ FOREIGN KEY (conversationId) REFERENCES conversation_state(conversationId)
85
+ )
86
+ `;
87
+
88
+ this.db.exec(createConversationStateTable);
89
+ this.db.exec(createInteractionLogTable);
90
+ logger.debug('Tables created successfully');
201
91
  }
202
92
 
203
93
  /**
204
- * Helper method to get multiple rows with promises
94
+ * Save conversation state to database
205
95
  */
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;
214
- }
215
-
216
- this.db.all(sql, params, (err, rows) => {
217
- if (err) {
218
- reject(err);
219
- } else {
220
- resolve(rows as SqliteRow[]);
221
- }
222
- });
96
+ async saveConversationState(state: ConversationState): Promise<void> {
97
+ if (!this.db) {
98
+ throw new Error('Database not initialized');
99
+ }
100
+
101
+ const stmt = this.db.prepare(`
102
+ INSERT OR REPLACE INTO conversation_state
103
+ (conversationId, projectPath, gitBranch, currentPhase, planFilePath, workflowName,
104
+ gitCommitConfig, requireReviewsBeforePhaseTransition, createdAt, updatedAt)
105
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
106
+ `);
107
+
108
+ stmt.run(
109
+ state.conversationId,
110
+ state.projectPath,
111
+ state.gitBranch,
112
+ state.currentPhase,
113
+ state.planFilePath,
114
+ state.workflowName,
115
+ state.gitCommitConfig ? JSON.stringify(state.gitCommitConfig) : null,
116
+ state.requireReviewsBeforePhaseTransition ? 1 : 0,
117
+ state.createdAt,
118
+ state.updatedAt
119
+ );
120
+
121
+ logger.debug('Conversation state saved', {
122
+ conversationId: state.conversationId,
123
+ workflowName: state.workflowName,
124
+ currentPhase: state.currentPhase,
223
125
  });
224
126
  }
225
127
 
226
128
  /**
227
- * Get conversation state by ID
129
+ * Load conversation state from database
228
130
  */
229
- async getConversationState(
131
+ async loadConversationState(
230
132
  conversationId: string
231
133
  ): Promise<ConversationState | null> {
232
- logger.debug('Retrieving conversation state', { conversationId });
233
-
234
- try {
235
- const row = await this.getRow(
236
- 'SELECT * FROM conversation_states WHERE conversation_id = ?',
237
- [conversationId]
238
- );
134
+ if (!this.db) {
135
+ throw new Error('Database not initialized');
136
+ }
239
137
 
240
- if (!row) {
241
- logger.debug('Conversation state not found', { conversationId });
242
- return null;
243
- }
138
+ const stmt = this.db.prepare(
139
+ 'SELECT * FROM conversation_state WHERE conversationId = ?'
140
+ );
141
+ const row = stmt.get(conversationId) as
142
+ | {
143
+ conversationId: string;
144
+ projectPath: string;
145
+ gitBranch: string;
146
+ currentPhase: string;
147
+ planFilePath: string;
148
+ workflowName: string;
149
+ gitCommitConfig: string;
150
+ requireReviewsBeforePhaseTransition: number;
151
+ createdAt: string;
152
+ updatedAt: string;
153
+ }
154
+ | undefined;
244
155
 
156
+ if (row) {
245
157
  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'),
158
+ conversationId: row.conversationId,
159
+ projectPath: row.projectPath,
160
+ gitBranch: row.gitBranch,
161
+ currentPhase: row.currentPhase,
162
+ planFilePath: row.planFilePath,
163
+ workflowName: row.workflowName,
164
+ gitCommitConfig: row.gitCommitConfig
165
+ ? JSON.parse(row.gitCommitConfig)
166
+ : undefined,
167
+ requireReviewsBeforePhaseTransition:
168
+ row.requireReviewsBeforePhaseTransition === 1,
169
+ createdAt: row.createdAt,
170
+ updatedAt: row.updatedAt,
264
171
  };
265
172
 
266
- logger.debug('Conversation state retrieved', {
173
+ logger.debug('Conversation state loaded', {
267
174
  conversationId,
175
+ workflowName: state.workflowName,
268
176
  currentPhase: state.currentPhase,
269
- projectPath: state.projectPath,
270
177
  });
271
-
272
178
  return state;
273
- } catch (error) {
274
- logger.error('Failed to retrieve conversation state', error as Error, {
275
- conversationId,
276
- });
277
- throw error;
278
179
  }
180
+
181
+ logger.debug('No conversation state found', { conversationId });
182
+ return null;
279
183
  }
280
184
 
281
185
  /**
282
- * Save or update conversation state
186
+ * Get conversation state by ID (alias for loadConversationState)
283
187
  */
284
- async saveConversationState(state: ConversationState): Promise<void> {
285
- logger.debug('Saving conversation state', {
286
- conversationId: state.conversationId,
287
- currentPhase: state.currentPhase,
288
- projectPath: state.projectPath,
289
- workflowName: state.workflowName,
290
- });
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
- }
188
+ async getConversationState(
189
+ conversationId: string
190
+ ): Promise<ConversationState | null> {
191
+ return this.loadConversationState(conversationId);
322
192
  }
323
193
 
324
194
  /**
325
- * Find conversation by project path and git branch
195
+ * List all conversation states
326
196
  */
327
- async findConversationByProject(
328
- projectPath: string,
329
- gitBranch: string
330
- ): Promise<ConversationState | null> {
331
- const row = await this.getRow(
332
- 'SELECT * FROM conversation_states WHERE project_path = ? AND git_branch = ?',
333
- [projectPath, gitBranch]
334
- );
335
-
336
- if (!row) {
337
- return null;
197
+ async listConversationStates(): Promise<ConversationState[]> {
198
+ if (!this.db) {
199
+ throw new Error('Database not initialized');
338
200
  }
339
201
 
340
- 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'),
359
- };
202
+ const stmt = this.db.prepare(
203
+ 'SELECT * FROM conversation_state ORDER BY updatedAt DESC'
204
+ );
205
+ const rows = stmt.all() as {
206
+ conversationId: string;
207
+ projectPath: string;
208
+ gitBranch: string;
209
+ currentPhase: string;
210
+ planFilePath: string;
211
+ workflowName: string;
212
+ gitCommitConfig: string;
213
+ requireReviewsBeforePhaseTransition: number;
214
+ createdAt: string;
215
+ updatedAt: string;
216
+ }[];
217
+
218
+ const states = rows.map(row => ({
219
+ conversationId: row.conversationId,
220
+ projectPath: row.projectPath,
221
+ gitBranch: row.gitBranch,
222
+ currentPhase: row.currentPhase,
223
+ planFilePath: row.planFilePath,
224
+ workflowName: row.workflowName,
225
+ gitCommitConfig: row.gitCommitConfig
226
+ ? JSON.parse(row.gitCommitConfig)
227
+ : undefined,
228
+ requireReviewsBeforePhaseTransition:
229
+ row.requireReviewsBeforePhaseTransition === 1,
230
+ createdAt: row.createdAt,
231
+ updatedAt: row.updatedAt,
232
+ }));
233
+
234
+ logger.debug('Listed conversation states', { count: states.length });
235
+ return states;
360
236
  }
361
237
 
362
238
  /**
363
239
  * Delete conversation state
364
240
  */
365
- async deleteConversationState(conversationId: string): Promise<void> {
366
- await this.runQuery(
367
- 'DELETE FROM conversation_states WHERE conversation_id = ?',
368
- [conversationId]
241
+ async deleteConversationState(conversationId: string): Promise<boolean> {
242
+ if (!this.db) {
243
+ throw new Error('Database not initialized');
244
+ }
245
+
246
+ const stmt = this.db.prepare(
247
+ 'DELETE FROM conversation_state WHERE conversationId = ?'
369
248
  );
249
+ const result = stmt.run(conversationId);
250
+
251
+ const deleted = result.changes > 0;
252
+ logger.debug('Conversation state deletion', { conversationId, deleted });
253
+ return deleted;
370
254
  }
371
255
 
372
256
  /**
373
257
  * Log an interaction to the database
374
258
  */
375
259
  async logInteraction(log: InteractionLog): Promise<void> {
376
- logger.debug('Logging interaction to database', {
260
+ if (!this.db) {
261
+ throw new Error('Database not initialized');
262
+ }
263
+
264
+ const stmt = this.db.prepare(`
265
+ INSERT INTO interaction_log
266
+ (conversationId, toolName, inputParams, responseData, currentPhase, timestamp, isReset, resetAt)
267
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
268
+ `);
269
+
270
+ stmt.run(
271
+ log.conversationId,
272
+ log.toolName,
273
+ log.inputParams,
274
+ log.responseData,
275
+ log.currentPhase,
276
+ log.timestamp,
277
+ log.isReset ? 1 : 0,
278
+ log.resetAt || null
279
+ );
280
+
281
+ logger.debug('Interaction logged', {
377
282
  conversationId: log.conversationId,
378
283
  toolName: log.toolName,
284
+ timestamp: log.timestamp,
379
285
  });
380
-
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;
407
- }
408
286
  }
409
287
 
410
288
  /**
411
- * Get all interactions for a specific conversation
289
+ * Get interactions by conversation ID
412
290
  */
413
291
  async getInteractionsByConversationId(
414
292
  conversationId: string
415
293
  ): Promise<InteractionLog[]> {
416
- logger.debug('Getting interactions by conversation ID', { conversationId });
417
-
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
- });
441
-
442
- return logs;
443
- } catch (error) {
444
- logger.error('Failed to get interaction logs', error as Error, {
445
- conversationId,
446
- });
447
- throw error;
294
+ if (!this.db) {
295
+ throw new Error('Database not initialized');
448
296
  }
449
- }
450
-
451
- /**
452
- * Run database migrations to add new columns
453
- */
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
- }
518
297
 
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
- }
527
-
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
- }
537
-
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
- }
298
+ const stmt = this.db.prepare(`
299
+ SELECT * FROM interaction_log
300
+ WHERE conversationId = ? AND (isReset = 0 OR isReset IS NULL)
301
+ ORDER BY timestamp ASC
302
+ `);
303
+ const rows = stmt.all(conversationId) as {
304
+ id: number;
305
+ conversationId: string;
306
+ toolName: string;
307
+ inputParams: string;
308
+ responseData: string;
309
+ currentPhase: string;
310
+ timestamp: string;
311
+ isReset: number;
312
+ resetAt: string;
313
+ }[];
314
+
315
+ const logs = rows.map(row => ({
316
+ id: row.id,
317
+ conversationId: row.conversationId,
318
+ toolName: row.toolName,
319
+ inputParams: row.inputParams,
320
+ responseData: row.responseData,
321
+ currentPhase: row.currentPhase,
322
+ timestamp: row.timestamp,
323
+ isReset: row.isReset === 1,
324
+ resetAt: row.resetAt,
325
+ }));
326
+
327
+ logger.debug('Retrieved interaction logs', {
328
+ conversationId,
329
+ count: logs.length,
330
+ });
331
+ return logs;
543
332
  }
544
333
 
545
334
  /**
546
- * Soft delete interaction logs for a conversation
335
+ * Soft delete interaction logs by marking them as reset
547
336
  */
548
- async softDeleteInteractionLogs(
549
- conversationId: string,
550
- reason?: string
551
- ): Promise<void> {
552
- logger.debug('Soft deleting interaction logs', { conversationId, reason });
553
-
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;
337
+ async softDeleteInteractionLogs(conversationId: string): Promise<void> {
338
+ if (!this.db) {
339
+ throw new Error('Database not initialized');
571
340
  }
572
- }
573
341
 
574
- /**
575
- * Get active (non-reset) interaction logs for a conversation
576
- */
577
- async getActiveInteractionLogs(
578
- conversationId: string
579
- ): 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
- });
342
+ const resetAt = new Date().toISOString();
343
+ const stmt = this.db.prepare(`
344
+ UPDATE interaction_log
345
+ SET isReset = 1, resetAt = ?
346
+ WHERE conversationId = ? AND (isReset = 0 OR isReset IS NULL)
347
+ `);
604
348
 
605
- return logs;
606
- } catch (error) {
607
- logger.error('Failed to get active interaction logs', error as Error, {
608
- conversationId,
609
- });
610
- throw error;
611
- }
349
+ const result = stmt.run(resetAt, conversationId);
350
+ logger.debug('Soft deleted interaction logs', {
351
+ conversationId,
352
+ affectedRows: result.changes,
353
+ });
612
354
  }
613
355
 
614
356
  /**
615
- * Get all interaction logs including reset ones for a conversation
357
+ * Close database connection
616
358
  */
617
- async getAllInteractionLogsIncludingReset(
618
- conversationId: string
619
- ): Promise<InteractionLog[]> {
620
- logger.debug('Getting all interaction logs including reset', {
621
- conversationId,
622
- });
623
-
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
- });
649
-
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;
359
+ async close(): Promise<void> {
360
+ if (this.db) {
361
+ this.db.close();
362
+ this.db = null;
363
+ logger.debug('Database connection closed');
658
364
  }
659
365
  }
660
366
 
661
367
  /**
662
- * Close database connection
368
+ * Check if database is initialized
663
369
  */
664
- 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
- });
370
+ isInitialized(): boolean {
371
+ return this.db !== null;
684
372
  }
685
373
  }