@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/.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 +30 -31
- package/dist/database.js +262 -404
- 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 +309 -567
- 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,427 @@
|
|
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 @sqlite.org/sqlite-wasm for reliable cross-platform WebAssembly bindings.
|
7
6
|
*/
|
8
7
|
|
9
|
-
import
|
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 {
|
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
|
-
|
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
|
-
|
19
|
+
/**
|
20
|
+
* Database connection and operations manager
|
21
|
+
*/
|
74
22
|
export class Database {
|
75
|
-
private db:
|
23
|
+
private db: SqliteDatabase | null = null;
|
24
|
+
private sqlite3: Sqlite3Static | null = null;
|
76
25
|
private dbPath: string;
|
77
26
|
|
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
|
-
});
|
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
|
-
//
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
102
|
-
this.
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
*
|
61
|
+
* Load database content from file
|
162
62
|
*/
|
163
|
-
private
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
return;
|
168
|
-
}
|
63
|
+
private async loadFromFile(): Promise<void> {
|
64
|
+
if (!this.db || !this.dbPath || this.dbPath === ':memory:') {
|
65
|
+
return;
|
66
|
+
}
|
169
67
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
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
|
-
|
194
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
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
|
-
*
|
113
|
+
* Save database content to file
|
228
114
|
*/
|
229
|
-
async
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
236
|
-
|
237
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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.
|
275
|
-
|
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
|
-
*
|
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
|
-
|
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
|
-
*
|
223
|
+
* Get conversation state by ID
|
326
224
|
*/
|
327
|
-
async
|
328
|
-
|
329
|
-
gitBranch: string
|
225
|
+
async getConversationState(
|
226
|
+
conversationId: string
|
330
227
|
): Promise<ConversationState | null> {
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
);
|
228
|
+
if (!this.db) {
|
229
|
+
throw new Error('Database not initialized');
|
230
|
+
}
|
335
231
|
|
336
|
-
|
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:
|
342
|
-
projectPath:
|
343
|
-
gitBranch:
|
344
|
-
currentPhase:
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
*
|
258
|
+
* Get all conversation states
|
364
259
|
*/
|
365
|
-
async
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
);
|
370
|
-
}
|
260
|
+
async getAllConversationStates(): Promise<ConversationState[]> {
|
261
|
+
if (!this.db) {
|
262
|
+
throw new Error('Database not initialized');
|
263
|
+
}
|
371
264
|
|
372
|
-
|
373
|
-
|
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
|
-
|
382
|
-
|
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
|
-
*
|
289
|
+
* Delete conversation state
|
412
290
|
*/
|
413
|
-
async
|
414
|
-
|
415
|
-
|
416
|
-
|
291
|
+
async deleteConversationState(conversationId: string): Promise<boolean> {
|
292
|
+
if (!this.db) {
|
293
|
+
throw new Error('Database not initialized');
|
294
|
+
}
|
417
295
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
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
|
-
*
|
309
|
+
* Log interaction
|
453
310
|
*/
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
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
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
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
|
-
|
529
|
-
|
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
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
}
|
333
|
+
logger.debug('Interaction logged', {
|
334
|
+
conversationId: log.conversationId,
|
335
|
+
toolName: log.toolName,
|
336
|
+
});
|
543
337
|
}
|
544
338
|
|
545
339
|
/**
|
546
|
-
*
|
340
|
+
* Get interaction logs for a conversation
|
547
341
|
*/
|
548
|
-
async
|
549
|
-
|
550
|
-
|
551
|
-
|
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
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
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
|
369
|
+
* Get interaction logs for a conversation (alias for compatibility)
|
576
370
|
*/
|
577
|
-
async
|
371
|
+
async getInteractionsByConversationId(
|
578
372
|
conversationId: string
|
579
373
|
): Promise<InteractionLog[]> {
|
580
|
-
|
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
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
throw
|
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
|
-
*
|
397
|
+
* Reset conversation state (for testing)
|
616
398
|
*/
|
617
|
-
async
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
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
|
-
|
625
|
-
|
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
|
-
|
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
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
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
|
}
|