@codemcp/workflows-core 3.1.20 → 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/.turbo/turbo-build.log +1 -1
- package/dist/conversation-manager.js +1 -1
- package/dist/conversation-manager.js.map +1 -1
- package/dist/database.d.ts +24 -34
- package/dist/database.js +185 -420
- package/dist/database.js.map +1 -1
- package/dist/project-docs-manager.js +5 -4
- package/dist/project-docs-manager.js.map +1 -1
- package/dist/state-machine-loader.d.ts +2 -1
- package/dist/state-machine-loader.js +45 -7
- package/dist/state-machine-loader.js.map +1 -1
- package/dist/template-manager.js +9 -7
- package/dist/template-manager.js.map +1 -1
- package/dist/workflow-manager.js +9 -7
- package/dist/workflow-manager.js.map +1 -1
- package/package.json +3 -2
- package/src/conversation-manager.ts +1 -4
- package/src/database.ts +266 -578
- package/src/project-docs-manager.ts +5 -4
- package/src/state-machine-loader.ts +71 -14
- package/src/template-manager.ts +15 -8
- package/src/workflow-manager.ts +12 -7
package/src/database.ts
CHANGED
@@ -1,685 +1,373 @@
|
|
1
1
|
/**
|
2
|
-
* Database
|
2
|
+
* Database Manager
|
3
3
|
*
|
4
|
-
*
|
5
|
-
*
|
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
|
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 {
|
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
|
-
|
25
|
-
|
26
|
-
|
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:
|
20
|
+
private db: BetterSqlite3.Database | null = null;
|
76
21
|
private dbPath: string;
|
77
22
|
|
78
|
-
constructor(
|
79
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
38
|
+
this.db = new BetterSqlite3(this.dbPath);
|
103
39
|
logger.debug('Database connection established');
|
104
40
|
|
105
|
-
// Create
|
106
|
-
await this.
|
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
|
-
*
|
51
|
+
* Create database tables if they don't exist
|
162
52
|
*/
|
163
|
-
private
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
*
|
94
|
+
* Save conversation state to database
|
205
95
|
*/
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
*
|
129
|
+
* Load conversation state from database
|
228
130
|
*/
|
229
|
-
async
|
131
|
+
async loadConversationState(
|
230
132
|
conversationId: string
|
231
133
|
): Promise<ConversationState | null> {
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
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:
|
247
|
-
projectPath:
|
248
|
-
gitBranch:
|
249
|
-
currentPhase:
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
row.
|
257
|
-
|
258
|
-
|
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
|
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
|
-
*
|
186
|
+
* Get conversation state by ID (alias for loadConversationState)
|
283
187
|
*/
|
284
|
-
async
|
285
|
-
|
286
|
-
|
287
|
-
|
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
|
-
*
|
195
|
+
* List all conversation states
|
326
196
|
*/
|
327
|
-
async
|
328
|
-
|
329
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
planFilePath:
|
349
|
-
workflowName:
|
350
|
-
gitCommitConfig:
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
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<
|
366
|
-
|
367
|
-
|
368
|
-
|
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
|
-
|
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
|
289
|
+
* Get interactions by conversation ID
|
412
290
|
*/
|
413
291
|
async getInteractionsByConversationId(
|
414
292
|
conversationId: string
|
415
293
|
): Promise<InteractionLog[]> {
|
416
|
-
|
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
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
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
|
335
|
+
* Soft delete interaction logs by marking them as reset
|
547
336
|
*/
|
548
|
-
async softDeleteInteractionLogs(
|
549
|
-
|
550
|
-
|
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
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
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
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
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
|
-
*
|
357
|
+
* Close database connection
|
616
358
|
*/
|
617
|
-
async
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
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
|
-
*
|
368
|
+
* Check if database is initialized
|
663
369
|
*/
|
664
|
-
|
665
|
-
|
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
|
}
|