grov 0.5.2 → 0.5.3

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.
Files changed (48) hide show
  1. package/README.md +19 -1
  2. package/dist/cli.js +8 -0
  3. package/dist/lib/api-client.d.ts +18 -1
  4. package/dist/lib/api-client.js +57 -0
  5. package/dist/lib/llm-extractor.d.ts +14 -38
  6. package/dist/lib/llm-extractor.js +380 -406
  7. package/dist/lib/store/convenience.d.ts +40 -0
  8. package/dist/lib/store/convenience.js +104 -0
  9. package/dist/lib/store/database.d.ts +22 -0
  10. package/dist/lib/store/database.js +375 -0
  11. package/dist/lib/store/drift.d.ts +9 -0
  12. package/dist/lib/store/drift.js +89 -0
  13. package/dist/lib/store/index.d.ts +7 -0
  14. package/dist/lib/store/index.js +13 -0
  15. package/dist/lib/store/sessions.d.ts +32 -0
  16. package/dist/lib/store/sessions.js +240 -0
  17. package/dist/lib/store/steps.d.ts +40 -0
  18. package/dist/lib/store/steps.js +161 -0
  19. package/dist/lib/store/tasks.d.ts +33 -0
  20. package/dist/lib/store/tasks.js +133 -0
  21. package/dist/lib/store/types.d.ts +167 -0
  22. package/dist/lib/store/types.js +2 -0
  23. package/dist/lib/store.d.ts +1 -436
  24. package/dist/lib/store.js +2 -1478
  25. package/dist/proxy/cache.d.ts +36 -0
  26. package/dist/proxy/cache.js +51 -0
  27. package/dist/proxy/config.d.ts +1 -0
  28. package/dist/proxy/config.js +2 -0
  29. package/dist/proxy/extended-cache.d.ts +10 -0
  30. package/dist/proxy/extended-cache.js +155 -0
  31. package/dist/proxy/handlers/preprocess.d.ts +20 -0
  32. package/dist/proxy/handlers/preprocess.js +169 -0
  33. package/dist/proxy/injection/delta-tracking.d.ts +11 -0
  34. package/dist/proxy/injection/delta-tracking.js +93 -0
  35. package/dist/proxy/injection/injectors.d.ts +7 -0
  36. package/dist/proxy/injection/injectors.js +139 -0
  37. package/dist/proxy/request-processor.d.ts +18 -4
  38. package/dist/proxy/request-processor.js +151 -30
  39. package/dist/proxy/response-processor.js +93 -45
  40. package/dist/proxy/server.d.ts +0 -1
  41. package/dist/proxy/server.js +342 -566
  42. package/dist/proxy/types.d.ts +13 -0
  43. package/dist/proxy/types.js +2 -0
  44. package/dist/proxy/utils/extractors.d.ts +18 -0
  45. package/dist/proxy/utils/extractors.js +109 -0
  46. package/dist/proxy/utils/logging.d.ts +18 -0
  47. package/dist/proxy/utils/logging.js +42 -0
  48. package/package.json +5 -2
package/dist/lib/store.js CHANGED
@@ -1,1478 +1,2 @@
1
- // SQLite store for task reasoning at ~/.grov/memory.db
2
- import Database from 'better-sqlite3';
3
- import { existsSync, mkdirSync, chmodSync } from 'fs';
4
- import { homedir } from 'os';
5
- import { join } from 'path';
6
- import { randomUUID } from 'crypto';
7
- /**
8
- * Escape LIKE pattern special characters to prevent SQL injection.
9
- * SECURITY: Prevents wildcard injection in LIKE queries.
10
- */
11
- function escapeLikePattern(str) {
12
- return str.replace(/[%_\\]/g, '\\$&');
13
- }
14
- const GROV_DIR = join(homedir(), '.grov');
15
- const DB_PATH = join(GROV_DIR, 'memory.db');
16
- let db = null;
17
- // PERFORMANCE: Statement cache to avoid re-preparing frequently used queries
18
- const statementCache = new Map();
19
- /**
20
- * Get a cached prepared statement or create a new one.
21
- * PERFORMANCE: Avoids overhead of re-preparing the same SQL.
22
- */
23
- function getCachedStatement(database, sql) {
24
- let stmt = statementCache.get(sql);
25
- if (!stmt) {
26
- stmt = database.prepare(sql);
27
- statementCache.set(sql, stmt);
28
- }
29
- return stmt;
30
- }
31
- /**
32
- * Initialize the database connection and create tables
33
- */
34
- export function initDatabase() {
35
- if (db)
36
- return db;
37
- // Ensure .grov directory exists with secure permissions
38
- if (!existsSync(GROV_DIR)) {
39
- mkdirSync(GROV_DIR, { recursive: true, mode: 0o700 });
40
- }
41
- db = new Database(DB_PATH);
42
- // Set secure file permissions on the database
43
- try {
44
- chmodSync(DB_PATH, 0o600);
45
- }
46
- catch {
47
- // SECURITY: Warn user if permissions can't be set (e.g., on Windows)
48
- // The database may be world-readable on some systems
49
- console.warn('Warning: Could not set restrictive permissions on ~/.grov/memory.db');
50
- console.warn('Please ensure the file has appropriate permissions for your system.');
51
- }
52
- // OPTIMIZATION: Enable WAL mode for better concurrent performance
53
- db.pragma('journal_mode = WAL');
54
- // Create all tables in a single transaction for efficiency
55
- db.exec(`
56
- CREATE TABLE IF NOT EXISTS tasks (
57
- id TEXT PRIMARY KEY,
58
- project_path TEXT NOT NULL,
59
- user TEXT,
60
- original_query TEXT NOT NULL,
61
- goal TEXT,
62
- reasoning_trace JSON DEFAULT '[]',
63
- files_touched JSON DEFAULT '[]',
64
- decisions JSON DEFAULT '[]',
65
- constraints JSON DEFAULT '[]',
66
- status TEXT NOT NULL CHECK(status IN ('complete', 'question', 'partial', 'abandoned')),
67
- trigger_reason TEXT CHECK(trigger_reason IN ('complete', 'threshold', 'abandoned')),
68
- linked_commit TEXT,
69
- parent_task_id TEXT,
70
- turn_number INTEGER,
71
- tags JSON DEFAULT '[]',
72
- created_at TEXT NOT NULL,
73
- synced_at TEXT,
74
- sync_error TEXT,
75
- FOREIGN KEY (parent_task_id) REFERENCES tasks(id)
76
- );
77
-
78
- CREATE INDEX IF NOT EXISTS idx_project ON tasks(project_path);
79
- CREATE INDEX IF NOT EXISTS idx_status ON tasks(status);
80
- CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at);
81
- `);
82
- // Migration: add new columns to existing tasks table
83
- try {
84
- db.exec(`ALTER TABLE tasks ADD COLUMN decisions JSON DEFAULT '[]'`);
85
- }
86
- catch { /* column exists */ }
87
- try {
88
- db.exec(`ALTER TABLE tasks ADD COLUMN constraints JSON DEFAULT '[]'`);
89
- }
90
- catch { /* column exists */ }
91
- try {
92
- db.exec(`ALTER TABLE tasks ADD COLUMN trigger_reason TEXT`);
93
- }
94
- catch { /* column exists */ }
95
- try {
96
- db.exec(`ALTER TABLE tasks ADD COLUMN synced_at TEXT`);
97
- }
98
- catch { /* column exists */ }
99
- try {
100
- db.exec(`ALTER TABLE tasks ADD COLUMN sync_error TEXT`);
101
- }
102
- catch { /* column exists */ }
103
- // Create session_states table (temporary per-session tracking)
104
- db.exec(`
105
- CREATE TABLE IF NOT EXISTS session_states (
106
- session_id TEXT PRIMARY KEY,
107
- user_id TEXT,
108
- project_path TEXT NOT NULL,
109
- original_goal TEXT,
110
- expected_scope JSON DEFAULT '[]',
111
- constraints JSON DEFAULT '[]',
112
- keywords JSON DEFAULT '[]',
113
- token_count INTEGER DEFAULT 0,
114
- escalation_count INTEGER DEFAULT 0,
115
- session_mode TEXT DEFAULT 'normal' CHECK(session_mode IN ('normal', 'drifted', 'forced')),
116
- waiting_for_recovery INTEGER DEFAULT 0,
117
- last_checked_at INTEGER DEFAULT 0,
118
- last_clear_at INTEGER,
119
- start_time TEXT NOT NULL,
120
- last_update TEXT NOT NULL,
121
- status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned')),
122
- completed_at TEXT,
123
- parent_session_id TEXT,
124
- task_type TEXT DEFAULT 'main' CHECK(task_type IN ('main', 'subtask', 'parallel')),
125
- pending_correction TEXT,
126
- FOREIGN KEY (parent_session_id) REFERENCES session_states(session_id)
127
- );
128
-
129
- CREATE INDEX IF NOT EXISTS idx_session_project ON session_states(project_path);
130
- CREATE INDEX IF NOT EXISTS idx_session_status ON session_states(status);
131
- CREATE INDEX IF NOT EXISTS idx_session_parent ON session_states(parent_session_id);
132
- `);
133
- // Migration: add new columns to existing session_states table
134
- try {
135
- db.exec(`ALTER TABLE session_states ADD COLUMN expected_scope JSON DEFAULT '[]'`);
136
- }
137
- catch { /* column exists */ }
138
- try {
139
- db.exec(`ALTER TABLE session_states ADD COLUMN constraints JSON DEFAULT '[]'`);
140
- }
141
- catch { /* column exists */ }
142
- try {
143
- db.exec(`ALTER TABLE session_states ADD COLUMN keywords JSON DEFAULT '[]'`);
144
- }
145
- catch { /* column exists */ }
146
- try {
147
- db.exec(`ALTER TABLE session_states ADD COLUMN token_count INTEGER DEFAULT 0`);
148
- }
149
- catch { /* column exists */ }
150
- try {
151
- db.exec(`ALTER TABLE session_states ADD COLUMN escalation_count INTEGER DEFAULT 0`);
152
- }
153
- catch { /* column exists */ }
154
- try {
155
- db.exec(`ALTER TABLE session_states ADD COLUMN session_mode TEXT DEFAULT 'normal'`);
156
- }
157
- catch { /* column exists */ }
158
- try {
159
- db.exec(`ALTER TABLE session_states ADD COLUMN waiting_for_recovery INTEGER DEFAULT 0`);
160
- }
161
- catch { /* column exists */ }
162
- try {
163
- db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
164
- }
165
- catch { /* column exists */ }
166
- try {
167
- db.exec(`ALTER TABLE session_states ADD COLUMN last_clear_at INTEGER`);
168
- }
169
- catch { /* column exists */ }
170
- try {
171
- db.exec(`ALTER TABLE session_states ADD COLUMN parent_session_id TEXT`);
172
- }
173
- catch { /* column exists */ }
174
- try {
175
- db.exec(`ALTER TABLE session_states ADD COLUMN task_type TEXT DEFAULT 'main'`);
176
- }
177
- catch { /* column exists */ }
178
- try {
179
- db.exec(`ALTER TABLE session_states ADD COLUMN completed_at TEXT`);
180
- }
181
- catch { /* column exists */ }
182
- try {
183
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_parent ON session_states(parent_session_id)`);
184
- }
185
- catch { /* index exists */ }
186
- try {
187
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_completed ON session_states(completed_at)`);
188
- }
189
- catch { /* index exists */ }
190
- // Create file_reasoning table (file-level reasoning with anchoring)
191
- db.exec(`
192
- CREATE TABLE IF NOT EXISTS file_reasoning (
193
- id TEXT PRIMARY KEY,
194
- task_id TEXT,
195
- file_path TEXT NOT NULL,
196
- anchor TEXT,
197
- line_start INTEGER,
198
- line_end INTEGER,
199
- code_hash TEXT,
200
- change_type TEXT CHECK(change_type IN ('read', 'write', 'edit', 'create', 'delete')),
201
- reasoning TEXT NOT NULL,
202
- created_at TEXT NOT NULL,
203
- FOREIGN KEY (task_id) REFERENCES tasks(id)
204
- );
205
-
206
- CREATE INDEX IF NOT EXISTS idx_file_task ON file_reasoning(task_id);
207
- CREATE INDEX IF NOT EXISTS idx_file_path ON file_reasoning(file_path);
208
- -- PERFORMANCE: Composite index for common query pattern (file_path + ORDER BY created_at)
209
- CREATE INDEX IF NOT EXISTS idx_file_path_created ON file_reasoning(file_path, created_at DESC);
210
- `);
211
- // Migration: Add drift detection columns to session_states (safe to run multiple times)
212
- const columns = db.pragma('table_info(session_states)');
213
- const existingColumns = new Set(columns.map(c => c.name));
214
- // Shared columns
215
- if (!existingColumns.has('expected_scope')) {
216
- db.exec(`ALTER TABLE session_states ADD COLUMN expected_scope JSON DEFAULT '[]'`);
217
- }
218
- if (!existingColumns.has('constraints')) {
219
- db.exec(`ALTER TABLE session_states ADD COLUMN constraints JSON DEFAULT '[]'`);
220
- }
221
- if (!existingColumns.has('keywords')) {
222
- db.exec(`ALTER TABLE session_states ADD COLUMN keywords JSON DEFAULT '[]'`);
223
- }
224
- if (!existingColumns.has('escalation_count')) {
225
- db.exec(`ALTER TABLE session_states ADD COLUMN escalation_count INTEGER DEFAULT 0`);
226
- }
227
- if (!existingColumns.has('last_checked_at')) {
228
- db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
229
- }
230
- // Hook-specific columns
231
- if (!existingColumns.has('success_criteria')) {
232
- db.exec(`ALTER TABLE session_states ADD COLUMN success_criteria JSON DEFAULT '[]'`);
233
- }
234
- if (!existingColumns.has('last_drift_score')) {
235
- db.exec(`ALTER TABLE session_states ADD COLUMN last_drift_score INTEGER`);
236
- }
237
- if (!existingColumns.has('pending_recovery_plan')) {
238
- db.exec(`ALTER TABLE session_states ADD COLUMN pending_recovery_plan JSON`);
239
- }
240
- if (!existingColumns.has('drift_history')) {
241
- db.exec(`ALTER TABLE session_states ADD COLUMN drift_history JSON DEFAULT '[]'`);
242
- }
243
- // Proxy-specific columns
244
- if (!existingColumns.has('token_count')) {
245
- db.exec(`ALTER TABLE session_states ADD COLUMN token_count INTEGER DEFAULT 0`);
246
- }
247
- if (!existingColumns.has('session_mode')) {
248
- db.exec(`ALTER TABLE session_states ADD COLUMN session_mode TEXT DEFAULT 'normal'`);
249
- }
250
- if (!existingColumns.has('waiting_for_recovery')) {
251
- db.exec(`ALTER TABLE session_states ADD COLUMN waiting_for_recovery INTEGER DEFAULT 0`);
252
- }
253
- if (!existingColumns.has('last_clear_at')) {
254
- db.exec(`ALTER TABLE session_states ADD COLUMN last_clear_at INTEGER`);
255
- }
256
- if (!existingColumns.has('completed_at')) {
257
- db.exec(`ALTER TABLE session_states ADD COLUMN completed_at TEXT`);
258
- }
259
- if (!existingColumns.has('parent_session_id')) {
260
- db.exec(`ALTER TABLE session_states ADD COLUMN parent_session_id TEXT`);
261
- }
262
- if (!existingColumns.has('task_type')) {
263
- db.exec(`ALTER TABLE session_states ADD COLUMN task_type TEXT DEFAULT 'main'`);
264
- }
265
- // Additional hook fields
266
- if (!existingColumns.has('actions_taken')) {
267
- db.exec(`ALTER TABLE session_states ADD COLUMN actions_taken JSON DEFAULT '[]'`);
268
- }
269
- if (!existingColumns.has('files_explored')) {
270
- db.exec(`ALTER TABLE session_states ADD COLUMN files_explored JSON DEFAULT '[]'`);
271
- }
272
- if (!existingColumns.has('current_intent')) {
273
- db.exec(`ALTER TABLE session_states ADD COLUMN current_intent TEXT`);
274
- }
275
- if (!existingColumns.has('drift_warnings')) {
276
- db.exec(`ALTER TABLE session_states ADD COLUMN drift_warnings JSON DEFAULT '[]'`);
277
- }
278
- if (!existingColumns.has('pending_correction')) {
279
- db.exec(`ALTER TABLE session_states ADD COLUMN pending_correction TEXT`);
280
- }
281
- if (!existingColumns.has('pending_clear_summary')) {
282
- db.exec(`ALTER TABLE session_states ADD COLUMN pending_clear_summary TEXT`);
283
- }
284
- if (!existingColumns.has('pending_forced_recovery')) {
285
- db.exec(`ALTER TABLE session_states ADD COLUMN pending_forced_recovery TEXT`);
286
- }
287
- if (!existingColumns.has('final_response')) {
288
- db.exec(`ALTER TABLE session_states ADD COLUMN final_response TEXT`);
289
- }
290
- // Create steps table (action log for current session)
291
- db.exec(`
292
- CREATE TABLE IF NOT EXISTS steps (
293
- id TEXT PRIMARY KEY,
294
- session_id TEXT NOT NULL,
295
- action_type TEXT NOT NULL CHECK(action_type IN ('edit', 'write', 'bash', 'read', 'glob', 'grep', 'task', 'other')),
296
- files JSON DEFAULT '[]',
297
- folders JSON DEFAULT '[]',
298
- command TEXT,
299
- reasoning TEXT,
300
- drift_score INTEGER,
301
- drift_type TEXT CHECK(drift_type IN ('none', 'minor', 'major', 'critical')),
302
- is_key_decision INTEGER DEFAULT 0,
303
- is_validated INTEGER DEFAULT 1,
304
- correction_given TEXT,
305
- correction_level TEXT CHECK(correction_level IN ('nudge', 'correct', 'intervene', 'halt')),
306
- keywords JSON DEFAULT '[]',
307
- timestamp INTEGER NOT NULL,
308
- FOREIGN KEY (session_id) REFERENCES session_states(session_id)
309
- );
310
- CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
311
- CREATE INDEX IF NOT EXISTS idx_steps_timestamp ON steps(timestamp);
312
- `);
313
- // Migration: add new columns to existing steps table
314
- try {
315
- db.exec(`ALTER TABLE steps ADD COLUMN drift_type TEXT`);
316
- }
317
- catch { /* column exists */ }
318
- try {
319
- db.exec(`ALTER TABLE steps ADD COLUMN is_key_decision INTEGER DEFAULT 0`);
320
- }
321
- catch { /* column exists */ }
322
- try {
323
- db.exec(`ALTER TABLE steps ADD COLUMN is_validated INTEGER DEFAULT 1`);
324
- }
325
- catch { /* column exists */ }
326
- try {
327
- db.exec(`ALTER TABLE steps ADD COLUMN correction_given TEXT`);
328
- }
329
- catch { /* column exists */ }
330
- try {
331
- db.exec(`ALTER TABLE steps ADD COLUMN correction_level TEXT`);
332
- }
333
- catch { /* column exists */ }
334
- try {
335
- db.exec(`ALTER TABLE steps ADD COLUMN keywords JSON DEFAULT '[]'`);
336
- }
337
- catch { /* column exists */ }
338
- try {
339
- db.exec(`ALTER TABLE steps ADD COLUMN reasoning TEXT`);
340
- }
341
- catch { /* column exists */ }
342
- // Create drift_log table (rejected actions for audit)
343
- db.exec(`
344
- CREATE TABLE IF NOT EXISTS drift_log (
345
- id TEXT PRIMARY KEY,
346
- session_id TEXT NOT NULL,
347
- timestamp INTEGER NOT NULL,
348
- action_type TEXT,
349
- files JSON DEFAULT '[]',
350
- drift_score INTEGER NOT NULL,
351
- drift_reason TEXT,
352
- correction_given TEXT,
353
- recovery_plan JSON,
354
- FOREIGN KEY (session_id) REFERENCES session_states(session_id)
355
- );
356
-
357
- CREATE INDEX IF NOT EXISTS idx_drift_log_session ON drift_log(session_id);
358
- CREATE INDEX IF NOT EXISTS idx_drift_log_timestamp ON drift_log(timestamp);
359
- `);
360
- return db;
361
- }
362
- /**
363
- * Close the database connection
364
- */
365
- export function closeDatabase() {
366
- if (db) {
367
- db.close();
368
- db = null;
369
- // PERFORMANCE: Clear statement cache when database is closed
370
- statementCache.clear();
371
- }
372
- }
373
- /**
374
- * Create a new task
375
- */
376
- export function createTask(input) {
377
- const database = initDatabase();
378
- const task = {
379
- id: randomUUID(),
380
- project_path: input.project_path,
381
- user: input.user,
382
- original_query: input.original_query,
383
- goal: input.goal,
384
- reasoning_trace: input.reasoning_trace || [],
385
- files_touched: input.files_touched || [],
386
- decisions: input.decisions || [],
387
- constraints: input.constraints || [],
388
- status: input.status,
389
- trigger_reason: input.trigger_reason,
390
- linked_commit: input.linked_commit,
391
- parent_task_id: input.parent_task_id,
392
- turn_number: input.turn_number,
393
- tags: input.tags || [],
394
- created_at: new Date().toISOString(),
395
- synced_at: null,
396
- sync_error: null
397
- };
398
- const stmt = database.prepare(`
399
- INSERT INTO tasks (
400
- id, project_path, user, original_query, goal,
401
- reasoning_trace, files_touched, decisions, constraints,
402
- status, trigger_reason, linked_commit,
403
- parent_task_id, turn_number, tags, created_at, synced_at, sync_error
404
- ) VALUES (
405
- ?, ?, ?, ?, ?,
406
- ?, ?, ?, ?,
407
- ?, ?, ?,
408
- ?, ?, ?, ?, ?, ?
409
- )
410
- `);
411
- stmt.run(task.id, task.project_path, task.user || null, task.original_query, task.goal || null, JSON.stringify(task.reasoning_trace), JSON.stringify(task.files_touched), JSON.stringify(task.decisions), JSON.stringify(task.constraints), task.status, task.trigger_reason || null, task.linked_commit || null, task.parent_task_id || null, task.turn_number || null, JSON.stringify(task.tags), task.created_at, task.synced_at, task.sync_error);
412
- return task;
413
- }
414
- /**
415
- * Get tasks for a project
416
- */
417
- export function getTasksForProject(projectPath, options = {}) {
418
- const database = initDatabase();
419
- let sql = 'SELECT * FROM tasks WHERE project_path = ?';
420
- const params = [projectPath];
421
- if (options.status) {
422
- sql += ' AND status = ?';
423
- params.push(options.status);
424
- }
425
- sql += ' ORDER BY created_at DESC';
426
- if (options.limit) {
427
- sql += ' LIMIT ?';
428
- params.push(options.limit);
429
- }
430
- const stmt = database.prepare(sql);
431
- const rows = stmt.all(...params);
432
- return rows.map(rowToTask);
433
- }
434
- /**
435
- * Get tasks that touched specific files.
436
- * SECURITY: Uses json_each for proper array handling and escaped LIKE patterns.
437
- */
438
- // SECURITY: Maximum files per query to prevent SQL DoS
439
- const MAX_FILES_PER_QUERY = 100;
440
- export function getTasksByFiles(projectPath, files, options = {}) {
441
- const database = initDatabase();
442
- if (files.length === 0) {
443
- return [];
444
- }
445
- // SECURITY: Limit file count to prevent SQL DoS via massive query generation
446
- const limitedFiles = files.length > MAX_FILES_PER_QUERY
447
- ? files.slice(0, MAX_FILES_PER_QUERY)
448
- : files;
449
- // Use json_each for proper array iteration with escaped LIKE patterns
450
- const fileConditions = limitedFiles.map(() => "EXISTS (SELECT 1 FROM json_each(files_touched) WHERE value LIKE ? ESCAPE '\\')").join(' OR ');
451
- let sql = `SELECT * FROM tasks WHERE project_path = ? AND (${fileConditions})`;
452
- // Escape LIKE special characters to prevent injection
453
- const params = [
454
- projectPath,
455
- ...limitedFiles.map(f => `%${escapeLikePattern(f)}%`)
456
- ];
457
- if (options.status) {
458
- sql += ' AND status = ?';
459
- params.push(options.status);
460
- }
461
- sql += ' ORDER BY created_at DESC';
462
- if (options.limit) {
463
- sql += ' LIMIT ?';
464
- params.push(options.limit);
465
- }
466
- const stmt = database.prepare(sql);
467
- const rows = stmt.all(...params);
468
- return rows.map(rowToTask);
469
- }
470
- /**
471
- * Get a task by ID
472
- */
473
- export function getTaskById(id) {
474
- const database = initDatabase();
475
- const stmt = database.prepare('SELECT * FROM tasks WHERE id = ?');
476
- const row = stmt.get(id);
477
- return row ? rowToTask(row) : null;
478
- }
479
- /**
480
- * Update a task's status
481
- */
482
- export function updateTaskStatus(id, status) {
483
- const database = initDatabase();
484
- const stmt = database.prepare('UPDATE tasks SET status = ? WHERE id = ?');
485
- stmt.run(status, id);
486
- }
487
- /**
488
- * Get task count for a project
489
- */
490
- export function getTaskCount(projectPath) {
491
- const database = initDatabase();
492
- const stmt = database.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_path = ?');
493
- const row = stmt.get(projectPath);
494
- return row?.count ?? 0;
495
- }
496
- /**
497
- * Get unsynced tasks for a project (synced_at is NULL)
498
- */
499
- export function getUnsyncedTasks(projectPath, limit) {
500
- const database = initDatabase();
501
- let sql = 'SELECT * FROM tasks WHERE project_path = ? AND synced_at IS NULL ORDER BY created_at DESC';
502
- const params = [projectPath];
503
- if (limit) {
504
- sql += ' LIMIT ?';
505
- params.push(limit);
506
- }
507
- const stmt = database.prepare(sql);
508
- const rows = stmt.all(...params);
509
- return rows.map(rowToTask);
510
- }
511
- /**
512
- * Mark a task as synced and clear any previous sync error
513
- */
514
- export function markTaskSynced(id) {
515
- const database = initDatabase();
516
- const now = new Date().toISOString();
517
- database.prepare('UPDATE tasks SET synced_at = ?, sync_error = NULL WHERE id = ?').run(now, id);
518
- }
519
- /**
520
- * Record a sync error for a task
521
- */
522
- export function setTaskSyncError(id, error) {
523
- const database = initDatabase();
524
- database.prepare('UPDATE tasks SET sync_error = ? WHERE id = ?').run(error, id);
525
- }
526
- /**
527
- * Safely parse JSON with fallback to empty array.
528
- */
529
- function safeJsonParse(value, fallback) {
530
- if (typeof value !== 'string' || !value) {
531
- return fallback;
532
- }
533
- try {
534
- return JSON.parse(value);
535
- }
536
- catch {
537
- return fallback;
538
- }
539
- }
540
- /**
541
- * Convert database row to Task object
542
- */
543
- function rowToTask(row) {
544
- return {
545
- id: row.id,
546
- project_path: row.project_path,
547
- user: row.user,
548
- original_query: row.original_query,
549
- goal: row.goal,
550
- reasoning_trace: safeJsonParse(row.reasoning_trace, []),
551
- files_touched: safeJsonParse(row.files_touched, []),
552
- decisions: safeJsonParse(row.decisions, []),
553
- constraints: safeJsonParse(row.constraints, []),
554
- status: row.status,
555
- trigger_reason: row.trigger_reason,
556
- linked_commit: row.linked_commit,
557
- parent_task_id: row.parent_task_id,
558
- turn_number: row.turn_number,
559
- tags: safeJsonParse(row.tags, []),
560
- created_at: row.created_at,
561
- synced_at: row.synced_at,
562
- sync_error: row.sync_error
563
- };
564
- }
565
- // ============================================
566
- // SESSION STATE CRUD OPERATIONS
567
- // ============================================
568
- /**
569
- * Create a new session state.
570
- * FIXED: Uses INSERT OR IGNORE to handle race conditions safely.
571
- */
572
- export function createSessionState(input) {
573
- const database = initDatabase();
574
- const now = new Date().toISOString();
575
- const sessionState = {
576
- // Base fields
577
- session_id: input.session_id,
578
- user_id: input.user_id,
579
- project_path: input.project_path,
580
- original_goal: input.original_goal,
581
- expected_scope: input.expected_scope || [],
582
- constraints: input.constraints || [],
583
- keywords: input.keywords || [],
584
- escalation_count: 0,
585
- last_checked_at: 0,
586
- start_time: now,
587
- last_update: now,
588
- status: 'active',
589
- // Hook-specific fields
590
- success_criteria: input.success_criteria || [],
591
- last_drift_score: undefined,
592
- pending_recovery_plan: undefined,
593
- drift_history: [],
594
- actions_taken: [],
595
- files_explored: [],
596
- current_intent: undefined,
597
- drift_warnings: [],
598
- // Proxy-specific fields
599
- token_count: 0,
600
- session_mode: 'normal',
601
- waiting_for_recovery: false,
602
- last_clear_at: undefined,
603
- completed_at: undefined,
604
- parent_session_id: input.parent_session_id,
605
- task_type: input.task_type || 'main',
606
- };
607
- const stmt = database.prepare(`
608
- INSERT OR IGNORE INTO session_states (
609
- session_id, user_id, project_path, original_goal,
610
- expected_scope, constraints, keywords,
611
- token_count, escalation_count, session_mode,
612
- waiting_for_recovery, last_checked_at, last_clear_at,
613
- start_time, last_update, status,
614
- parent_session_id, task_type,
615
- success_criteria, last_drift_score, pending_recovery_plan, drift_history,
616
- completed_at
617
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
618
- `);
619
- stmt.run(sessionState.session_id, sessionState.user_id || null, sessionState.project_path, sessionState.original_goal || null, JSON.stringify(sessionState.expected_scope), JSON.stringify(sessionState.constraints), JSON.stringify(sessionState.keywords), sessionState.token_count, sessionState.escalation_count, sessionState.session_mode, sessionState.waiting_for_recovery ? 1 : 0, sessionState.last_checked_at, sessionState.last_clear_at || null, sessionState.start_time, sessionState.last_update, sessionState.status, sessionState.parent_session_id || null, sessionState.task_type, JSON.stringify(sessionState.success_criteria || []), sessionState.last_drift_score || null, sessionState.pending_recovery_plan ? JSON.stringify(sessionState.pending_recovery_plan) : null, JSON.stringify(sessionState.drift_history || []), sessionState.completed_at || null);
620
- return sessionState;
621
- }
622
- /**
623
- * Get a session state by ID
624
- */
625
- export function getSessionState(sessionId) {
626
- const database = initDatabase();
627
- const stmt = database.prepare('SELECT * FROM session_states WHERE session_id = ?');
628
- const row = stmt.get(sessionId);
629
- return row ? rowToSessionState(row) : null;
630
- }
631
- /**
632
- * Update a session state.
633
- * SECURITY: Uses transaction for atomic updates to prevent race conditions.
634
- */
635
- export function updateSessionState(sessionId, updates) {
636
- const database = initDatabase();
637
- const setClauses = [];
638
- const params = [];
639
- if (updates.user_id !== undefined) {
640
- setClauses.push('user_id = ?');
641
- params.push(updates.user_id || null);
642
- }
643
- if (updates.project_path !== undefined) {
644
- setClauses.push('project_path = ?');
645
- params.push(updates.project_path);
646
- }
647
- if (updates.original_goal !== undefined) {
648
- setClauses.push('original_goal = ?');
649
- params.push(updates.original_goal || null);
650
- }
651
- if (updates.expected_scope !== undefined) {
652
- setClauses.push('expected_scope = ?');
653
- params.push(JSON.stringify(updates.expected_scope));
654
- }
655
- if (updates.constraints !== undefined) {
656
- setClauses.push('constraints = ?');
657
- params.push(JSON.stringify(updates.constraints));
658
- }
659
- if (updates.keywords !== undefined) {
660
- setClauses.push('keywords = ?');
661
- params.push(JSON.stringify(updates.keywords));
662
- }
663
- if (updates.token_count !== undefined) {
664
- setClauses.push('token_count = ?');
665
- params.push(updates.token_count);
666
- }
667
- if (updates.escalation_count !== undefined) {
668
- setClauses.push('escalation_count = ?');
669
- params.push(updates.escalation_count);
670
- }
671
- if (updates.session_mode !== undefined) {
672
- setClauses.push('session_mode = ?');
673
- params.push(updates.session_mode);
674
- }
675
- if (updates.waiting_for_recovery !== undefined) {
676
- setClauses.push('waiting_for_recovery = ?');
677
- params.push(updates.waiting_for_recovery ? 1 : 0);
678
- }
679
- if (updates.last_checked_at !== undefined) {
680
- setClauses.push('last_checked_at = ?');
681
- params.push(updates.last_checked_at);
682
- }
683
- if (updates.last_clear_at !== undefined) {
684
- setClauses.push('last_clear_at = ?');
685
- params.push(updates.last_clear_at);
686
- }
687
- if (updates.status !== undefined) {
688
- setClauses.push('status = ?');
689
- params.push(updates.status);
690
- }
691
- if (updates.pending_correction !== undefined) {
692
- setClauses.push('pending_correction = ?');
693
- params.push(updates.pending_correction || null);
694
- }
695
- if (updates.pending_forced_recovery !== undefined) {
696
- setClauses.push('pending_forced_recovery = ?');
697
- params.push(updates.pending_forced_recovery || null);
698
- }
699
- if (updates.pending_clear_summary !== undefined) {
700
- setClauses.push('pending_clear_summary = ?');
701
- params.push(updates.pending_clear_summary || null);
702
- }
703
- if (updates.final_response !== undefined) {
704
- setClauses.push('final_response = ?');
705
- params.push(updates.final_response || null);
706
- }
707
- // Always update last_update
708
- setClauses.push('last_update = ?');
709
- params.push(new Date().toISOString());
710
- if (setClauses.length === 0)
711
- return;
712
- params.push(sessionId);
713
- const sql = `UPDATE session_states SET ${setClauses.join(', ')} WHERE session_id = ?`;
714
- // SECURITY: Use transaction for atomic updates to prevent race conditions
715
- const transaction = database.transaction(() => {
716
- database.prepare(sql).run(...params);
717
- });
718
- transaction();
719
- }
720
- /**
721
- * Delete a session state
722
- */
723
- export function deleteSessionState(sessionId) {
724
- const database = initDatabase();
725
- database.prepare('DELETE FROM session_states WHERE session_id = ?').run(sessionId);
726
- }
727
- /**
728
- * Get active sessions for a project
729
- */
730
- export function getActiveSessionsForProject(projectPath) {
731
- const database = initDatabase();
732
- const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND status = 'active' ORDER BY start_time DESC");
733
- const rows = stmt.all(projectPath);
734
- return rows.map(rowToSessionState);
735
- }
736
- /**
737
- * Get child sessions (subtasks and parallel tasks) for a parent session
738
- */
739
- export function getChildSessions(parentSessionId) {
740
- const database = initDatabase();
741
- const stmt = database.prepare('SELECT * FROM session_states WHERE parent_session_id = ? ORDER BY start_time DESC');
742
- const rows = stmt.all(parentSessionId);
743
- return rows.map(rowToSessionState);
744
- }
745
- /**
746
- * Get active session for a specific user in a project
747
- */
748
- export function getActiveSessionForUser(projectPath, userId) {
749
- const database = initDatabase();
750
- if (userId) {
751
- const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND user_id = ? AND status = 'active' ORDER BY last_update DESC LIMIT 1");
752
- const row = stmt.get(projectPath, userId);
753
- return row ? rowToSessionState(row) : null;
754
- }
755
- else {
756
- const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND status = 'active' ORDER BY last_update DESC LIMIT 1");
757
- const row = stmt.get(projectPath);
758
- return row ? rowToSessionState(row) : null;
759
- }
760
- }
761
- /**
762
- * Get all active sessions (for proxy-status command)
763
- */
764
- export function getActiveSessionsForStatus() {
765
- const database = initDatabase();
766
- const stmt = database.prepare("SELECT * FROM session_states WHERE status = 'active' ORDER BY last_update DESC LIMIT 20");
767
- const rows = stmt.all();
768
- return rows.map(rowToSessionState);
769
- }
770
- /**
771
- * Convert database row to SessionState object
772
- */
773
- function rowToSessionState(row) {
774
- return {
775
- // Base fields
776
- session_id: row.session_id,
777
- user_id: row.user_id,
778
- project_path: row.project_path,
779
- original_goal: row.original_goal,
780
- expected_scope: safeJsonParse(row.expected_scope, []),
781
- constraints: safeJsonParse(row.constraints, []),
782
- keywords: safeJsonParse(row.keywords, []),
783
- escalation_count: row.escalation_count || 0,
784
- last_checked_at: row.last_checked_at || 0,
785
- start_time: row.start_time,
786
- last_update: row.last_update,
787
- status: row.status,
788
- // Hook-specific fields
789
- success_criteria: safeJsonParse(row.success_criteria, []),
790
- last_drift_score: row.last_drift_score,
791
- pending_recovery_plan: safeJsonParse(row.pending_recovery_plan, undefined),
792
- drift_history: safeJsonParse(row.drift_history, []),
793
- actions_taken: safeJsonParse(row.actions_taken, []),
794
- files_explored: safeJsonParse(row.files_explored, []),
795
- current_intent: row.current_intent,
796
- drift_warnings: safeJsonParse(row.drift_warnings, []),
797
- // Proxy-specific fields
798
- token_count: row.token_count || 0,
799
- session_mode: row.session_mode || 'normal',
800
- waiting_for_recovery: Boolean(row.waiting_for_recovery),
801
- last_clear_at: row.last_clear_at,
802
- completed_at: row.completed_at,
803
- parent_session_id: row.parent_session_id,
804
- task_type: row.task_type || 'main',
805
- pending_correction: row.pending_correction,
806
- pending_forced_recovery: row.pending_forced_recovery,
807
- pending_clear_summary: row.pending_clear_summary,
808
- final_response: row.final_response,
809
- };
810
- }
811
- // ============================================
812
- // DRIFT DETECTION OPERATIONS (hook uses these)
813
- // ============================================
814
- /**
815
- * Update session drift metrics after a prompt check
816
- */
817
- export function updateSessionDrift(sessionId, driftScore, correctionLevel, promptSummary, recoveryPlan) {
818
- const database = initDatabase();
819
- const session = getSessionState(sessionId);
820
- if (!session)
821
- return;
822
- const now = new Date().toISOString();
823
- // Calculate new escalation count
824
- let newEscalation = session.escalation_count;
825
- if (driftScore >= 8) {
826
- // Recovery - decrease escalation
827
- newEscalation = Math.max(0, newEscalation - 1);
828
- }
829
- else if (correctionLevel && correctionLevel !== 'nudge') {
830
- // Significant drift - increase escalation
831
- newEscalation = Math.min(3, newEscalation + 1);
832
- }
833
- // Add to drift history
834
- const driftEvent = {
835
- timestamp: now,
836
- score: driftScore,
837
- level: correctionLevel || 'none',
838
- prompt_summary: promptSummary.substring(0, 100)
839
- };
840
- const newHistory = [...(session.drift_history || []), driftEvent];
841
- // Add to drift_warnings if correction was given
842
- const currentWarnings = session.drift_warnings || [];
843
- const newWarnings = correctionLevel
844
- ? [...currentWarnings, `[${now}] ${correctionLevel}: score ${driftScore}`]
845
- : currentWarnings;
846
- const stmt = database.prepare(`
847
- UPDATE session_states SET
848
- last_drift_score = ?,
849
- escalation_count = ?,
850
- pending_recovery_plan = ?,
851
- drift_history = ?,
852
- drift_warnings = ?,
853
- last_update = ?
854
- WHERE session_id = ?
855
- `);
856
- stmt.run(driftScore, newEscalation, recoveryPlan ? JSON.stringify(recoveryPlan) : null, JSON.stringify(newHistory), JSON.stringify(newWarnings), now, sessionId);
857
- }
858
- /**
859
- * Check if a session should be flagged for review
860
- * Returns true if: status=drifted OR warnings>=3 OR avg_score<6
861
- */
862
- export function shouldFlagForReview(sessionId) {
863
- const session = getSessionState(sessionId);
864
- if (!session)
865
- return false;
866
- // Check number of warnings
867
- const warnings = session.drift_warnings || [];
868
- if (warnings.length >= 3) {
869
- return true;
870
- }
871
- // Check drift history for average score
872
- const history = session.drift_history || [];
873
- if (history.length >= 2) {
874
- const totalScore = history.reduce((sum, e) => sum + e.score, 0);
875
- const avgScore = totalScore / history.length;
876
- if (avgScore < 6) {
877
- return true;
878
- }
879
- }
880
- // Check if any HALT level drift occurred
881
- if (history.some(e => e.level === 'halt')) {
882
- return true;
883
- }
884
- // Check current escalation level
885
- if (session.escalation_count >= 2) {
886
- return true;
887
- }
888
- return false;
889
- }
890
- /**
891
- * Get drift summary for a session (used by capture)
892
- */
893
- export function getDriftSummary(sessionId) {
894
- const session = getSessionState(sessionId);
895
- const history = session?.drift_history || [];
896
- if (!session || history.length === 0) {
897
- return { totalEvents: 0, resolved: true, finalScore: null, hadHalt: false };
898
- }
899
- const lastEvent = history[history.length - 1];
900
- return {
901
- totalEvents: history.length,
902
- resolved: lastEvent.score >= 8,
903
- finalScore: lastEvent.score,
904
- hadHalt: history.some(e => e.level === 'halt')
905
- };
906
- }
907
- // ============================================
908
- // FILE REASONING CRUD OPERATIONS
909
- // ============================================
910
- /**
911
- * Create a new file reasoning entry
912
- */
913
- export function createFileReasoning(input) {
914
- const database = initDatabase();
915
- const fileReasoning = {
916
- id: randomUUID(),
917
- task_id: input.task_id,
918
- file_path: input.file_path,
919
- anchor: input.anchor,
920
- line_start: input.line_start,
921
- line_end: input.line_end,
922
- code_hash: input.code_hash,
923
- change_type: input.change_type,
924
- reasoning: input.reasoning,
925
- created_at: new Date().toISOString()
926
- };
927
- const stmt = database.prepare(`
928
- INSERT INTO file_reasoning (
929
- id, task_id, file_path, anchor, line_start, line_end,
930
- code_hash, change_type, reasoning, created_at
931
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
932
- `);
933
- stmt.run(fileReasoning.id, fileReasoning.task_id || null, fileReasoning.file_path, fileReasoning.anchor || null, fileReasoning.line_start || null, fileReasoning.line_end || null, fileReasoning.code_hash || null, fileReasoning.change_type || null, fileReasoning.reasoning, fileReasoning.created_at);
934
- return fileReasoning;
935
- }
936
- /**
937
- * Get file reasoning entries for a task
938
- */
939
- export function getFileReasoningForTask(taskId) {
940
- const database = initDatabase();
941
- const stmt = database.prepare('SELECT * FROM file_reasoning WHERE task_id = ? ORDER BY created_at DESC');
942
- const rows = stmt.all(taskId);
943
- return rows.map(rowToFileReasoning);
944
- }
945
- /**
946
- * Get file reasoning entries by file path
947
- */
948
- export function getFileReasoningByPath(filePath, limit = 10) {
949
- const database = initDatabase();
950
- const stmt = database.prepare('SELECT * FROM file_reasoning WHERE file_path = ? ORDER BY created_at DESC LIMIT ?');
951
- const rows = stmt.all(filePath, limit);
952
- return rows.map(rowToFileReasoning);
953
- }
954
- /**
955
- * Get file reasoning entries matching a pattern (for files in a project).
956
- * SECURITY: Uses escaped LIKE patterns to prevent injection.
957
- */
958
- export function getFileReasoningByPathPattern(pathPattern, limit = 20) {
959
- const database = initDatabase();
960
- // Escape LIKE special characters to prevent injection
961
- const escapedPattern = escapeLikePattern(pathPattern);
962
- const stmt = database.prepare("SELECT * FROM file_reasoning WHERE file_path LIKE ? ESCAPE '\\' ORDER BY created_at DESC LIMIT ?");
963
- const rows = stmt.all(`%${escapedPattern}%`, limit);
964
- return rows.map(rowToFileReasoning);
965
- }
966
- /**
967
- * Convert database row to FileReasoning object
968
- */
969
- function rowToFileReasoning(row) {
970
- return {
971
- id: row.id,
972
- task_id: row.task_id,
973
- file_path: row.file_path,
974
- anchor: row.anchor,
975
- line_start: row.line_start,
976
- line_end: row.line_end,
977
- code_hash: row.code_hash,
978
- change_type: row.change_type,
979
- reasoning: row.reasoning,
980
- created_at: row.created_at
981
- };
982
- }
983
- /**
984
- * Get the database path
985
- */
986
- export function getDatabasePath() {
987
- return DB_PATH;
988
- }
989
- // ============================================
990
- // STEPS CRUD OPERATIONS (Proxy uses these)
991
- // ============================================
992
- /**
993
- * Create a new step record (proxy version)
994
- */
995
- export function createStep(input) {
996
- const database = initDatabase();
997
- const step = {
998
- id: randomUUID(),
999
- session_id: input.session_id,
1000
- action_type: input.action_type,
1001
- files: input.files || [],
1002
- folders: input.folders || [],
1003
- command: input.command,
1004
- reasoning: input.reasoning,
1005
- drift_score: input.drift_score,
1006
- drift_type: input.drift_type,
1007
- is_key_decision: input.is_key_decision || false,
1008
- is_validated: input.is_validated !== false,
1009
- correction_given: input.correction_given,
1010
- correction_level: input.correction_level,
1011
- keywords: input.keywords || [],
1012
- timestamp: Date.now()
1013
- };
1014
- const stmt = database.prepare(`
1015
- INSERT INTO steps (
1016
- id, session_id, action_type, files, folders, command, reasoning,
1017
- drift_score, drift_type, is_key_decision, is_validated,
1018
- correction_given, correction_level, keywords, timestamp
1019
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1020
- `);
1021
- stmt.run(step.id, step.session_id, step.action_type, JSON.stringify(step.files), JSON.stringify(step.folders), step.command || null, step.reasoning || null, step.drift_score || null, step.drift_type || null, step.is_key_decision ? 1 : 0, step.is_validated ? 1 : 0, step.correction_given || null, step.correction_level || null, JSON.stringify(step.keywords), step.timestamp);
1022
- return step;
1023
- }
1024
- /**
1025
- * Get steps for a session
1026
- */
1027
- export function getStepsForSession(sessionId, limit) {
1028
- const database = initDatabase();
1029
- let sql = 'SELECT * FROM steps WHERE session_id = ? ORDER BY timestamp DESC';
1030
- const params = [sessionId];
1031
- if (limit) {
1032
- sql += ' LIMIT ?';
1033
- params.push(limit);
1034
- }
1035
- const stmt = database.prepare(sql);
1036
- const rows = stmt.all(...params);
1037
- return rows.map(rowToStep);
1038
- }
1039
- /**
1040
- * Get recent steps for a session (most recent N)
1041
- */
1042
- export function getRecentSteps(sessionId, count = 10) {
1043
- return getStepsForSession(sessionId, count);
1044
- }
1045
- /**
1046
- * Get validated steps only (for summary generation)
1047
- */
1048
- export function getValidatedSteps(sessionId) {
1049
- const database = initDatabase();
1050
- const stmt = database.prepare('SELECT * FROM steps WHERE session_id = ? AND is_validated = 1 ORDER BY timestamp ASC');
1051
- const rows = stmt.all(sessionId);
1052
- return rows.map(rowToStep);
1053
- }
1054
- /**
1055
- * Get key decision steps for a session (is_key_decision = 1)
1056
- * Used for user message injection - important decisions with reasoning
1057
- */
1058
- export function getKeyDecisions(sessionId, limit = 5) {
1059
- const database = initDatabase();
1060
- const stmt = database.prepare(`SELECT * FROM steps
1061
- WHERE session_id = ? AND is_key_decision = 1 AND reasoning IS NOT NULL
1062
- ORDER BY timestamp DESC
1063
- LIMIT ?`);
1064
- const rows = stmt.all(sessionId, limit);
1065
- return rows.map(rowToStep);
1066
- }
1067
- /**
1068
- * Get edited files for a session (action_type IN ('edit', 'write'))
1069
- * Used for user message injection - prevent re-work
1070
- */
1071
- export function getEditedFiles(sessionId) {
1072
- const database = initDatabase();
1073
- const stmt = database.prepare(`SELECT DISTINCT files FROM steps
1074
- WHERE session_id = ? AND action_type IN ('edit', 'write')
1075
- ORDER BY timestamp DESC`);
1076
- const rows = stmt.all(sessionId);
1077
- const allFiles = [];
1078
- for (const row of rows) {
1079
- try {
1080
- const files = JSON.parse(row.files || '[]');
1081
- if (Array.isArray(files)) {
1082
- allFiles.push(...files);
1083
- }
1084
- }
1085
- catch {
1086
- // Skip invalid JSON
1087
- }
1088
- }
1089
- return [...new Set(allFiles)];
1090
- }
1091
- /**
1092
- * Delete steps for a session
1093
- */
1094
- export function deleteStepsForSession(sessionId) {
1095
- const database = initDatabase();
1096
- database.prepare('DELETE FROM steps WHERE session_id = ?').run(sessionId);
1097
- }
1098
- /**
1099
- * Update reasoning for recent steps that don't have reasoning yet
1100
- * Called at end_turn to backfill reasoning from Claude's text response
1101
- */
1102
- export function updateRecentStepsReasoning(sessionId, reasoning, limit = 10) {
1103
- const database = initDatabase();
1104
- const stmt = database.prepare(`
1105
- UPDATE steps
1106
- SET reasoning = ?
1107
- WHERE session_id = ?
1108
- AND (reasoning IS NULL OR reasoning = '')
1109
- AND id IN (
1110
- SELECT id FROM steps
1111
- WHERE session_id = ?
1112
- ORDER BY timestamp DESC
1113
- LIMIT ?
1114
- )
1115
- `);
1116
- const result = stmt.run(reasoning, sessionId, sessionId, limit);
1117
- return result.changes;
1118
- }
1119
- /**
1120
- * Get relevant steps (key decisions and write/edit actions) - proxy version
1121
- * Reference: plan_proxy_local.md Section 2.2
1122
- */
1123
- export function getRelevantStepsSimple(sessionId, limit = 20) {
1124
- const database = initDatabase();
1125
- const stmt = database.prepare(`
1126
- SELECT * FROM steps
1127
- WHERE session_id = ?
1128
- AND (is_key_decision = 1 OR action_type IN ('edit', 'write', 'bash'))
1129
- AND is_validated = 1
1130
- ORDER BY timestamp DESC
1131
- LIMIT ?
1132
- `);
1133
- const rows = stmt.all(sessionId, limit);
1134
- return rows.map(rowToStep);
1135
- }
1136
- /**
1137
- * Convert database row to StepRecord object (proxy version - all fields)
1138
- */
1139
- function rowToStep(row) {
1140
- return {
1141
- id: row.id,
1142
- session_id: row.session_id,
1143
- action_type: row.action_type,
1144
- files: safeJsonParse(row.files, []),
1145
- folders: safeJsonParse(row.folders, []),
1146
- command: row.command,
1147
- reasoning: row.reasoning,
1148
- drift_score: row.drift_score,
1149
- drift_type: row.drift_type,
1150
- is_key_decision: Boolean(row.is_key_decision),
1151
- is_validated: Boolean(row.is_validated),
1152
- correction_given: row.correction_given,
1153
- correction_level: row.correction_level,
1154
- keywords: safeJsonParse(row.keywords, []),
1155
- timestamp: row.timestamp
1156
- };
1157
- }
1158
- // ============================================
1159
- // STEPS CRUD (Hook uses these)
1160
- // ============================================
1161
- /**
1162
- * Save a Claude action as a step (hook version - uses ClaudeAction)
1163
- */
1164
- export function saveStep(sessionId, action, driftScore, isKeyDecision = false, keywords = []) {
1165
- const database = initDatabase();
1166
- // Extract folders from files
1167
- const folders = [...new Set(action.files
1168
- .map(f => f.split('/').slice(0, -1).join('/'))
1169
- .filter(f => f.length > 0))];
1170
- database.prepare(`
1171
- INSERT INTO steps (id, session_id, action_type, files, folders, command, drift_score, is_key_decision, keywords, timestamp)
1172
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1173
- `).run(randomUUID(), sessionId, action.type, JSON.stringify(action.files), JSON.stringify(folders), action.command || null, driftScore, isKeyDecision ? 1 : 0, JSON.stringify(keywords), action.timestamp);
1174
- }
1175
- /**
1176
- * Update last_checked_at timestamp for a session
1177
- */
1178
- export function updateLastChecked(sessionId, timestamp) {
1179
- const database = initDatabase();
1180
- database.prepare(`
1181
- UPDATE session_states SET last_checked_at = ? WHERE session_id = ?
1182
- `).run(timestamp, sessionId);
1183
- }
1184
- // ============================================
1185
- // 4-QUERY RETRIEVAL (Hook uses these - from deep_dive.md)
1186
- // ============================================
1187
- /**
1188
- * Get steps that touched specific files
1189
- */
1190
- export function getStepsByFiles(sessionId, files, limit = 5) {
1191
- if (files.length === 0)
1192
- return [];
1193
- const database = initDatabase();
1194
- const placeholders = files.map(() => `files LIKE ?`).join(' OR ');
1195
- const patterns = files.map(f => `%"${escapeLikePattern(f)}"%`);
1196
- const rows = database.prepare(`
1197
- SELECT * FROM steps
1198
- WHERE session_id = ? AND drift_score >= 5 AND (${placeholders})
1199
- ORDER BY timestamp DESC LIMIT ?
1200
- `).all(sessionId, ...patterns, limit);
1201
- return rows.map(rowToStepRecord);
1202
- }
1203
- /**
1204
- * Get steps that touched specific folders
1205
- */
1206
- export function getStepsByFolders(sessionId, folders, limit = 5) {
1207
- if (folders.length === 0)
1208
- return [];
1209
- const database = initDatabase();
1210
- const placeholders = folders.map(() => `folders LIKE ?`).join(' OR ');
1211
- const patterns = folders.map(f => `%"${escapeLikePattern(f)}"%`);
1212
- const rows = database.prepare(`
1213
- SELECT * FROM steps
1214
- WHERE session_id = ? AND drift_score >= 5 AND (${placeholders})
1215
- ORDER BY timestamp DESC LIMIT ?
1216
- `).all(sessionId, ...patterns, limit);
1217
- return rows.map(rowToStepRecord);
1218
- }
1219
- /**
1220
- * Get steps matching keywords
1221
- */
1222
- export function getStepsByKeywords(sessionId, keywords, limit = 5) {
1223
- if (keywords.length === 0)
1224
- return [];
1225
- const database = initDatabase();
1226
- const conditions = keywords.map(() => `keywords LIKE ?`).join(' OR ');
1227
- const patterns = keywords.map(k => `%"${escapeLikePattern(k)}"%`);
1228
- const rows = database.prepare(`
1229
- SELECT * FROM steps
1230
- WHERE session_id = ? AND drift_score >= 5 AND (${conditions})
1231
- ORDER BY timestamp DESC LIMIT ?
1232
- `).all(sessionId, ...patterns, limit);
1233
- return rows.map(rowToStepRecord);
1234
- }
1235
- /**
1236
- * Get key decision steps
1237
- */
1238
- export function getKeyDecisionSteps(sessionId, limit = 5) {
1239
- const database = initDatabase();
1240
- const rows = database.prepare(`
1241
- SELECT * FROM steps
1242
- WHERE session_id = ? AND is_key_decision = 1
1243
- ORDER BY timestamp DESC LIMIT ?
1244
- `).all(sessionId, limit);
1245
- return rows.map(rowToStepRecord);
1246
- }
1247
- /**
1248
- * Get steps reasoning by file path (for proxy team memory injection)
1249
- * Searches across sessions, returns file-level reasoning from steps table
1250
- * @param excludeSessionId - Optional session ID to exclude (for filtering current session)
1251
- */
1252
- export function getStepsReasoningByPath(filePath, limit = 5, excludeSessionId) {
1253
- const database = initDatabase();
1254
- // Search steps where files JSON contains this path and reasoning exists
1255
- const pattern = `%"${escapeLikePattern(filePath)}"%`;
1256
- let sql = `
1257
- SELECT files, reasoning
1258
- FROM steps
1259
- WHERE files LIKE ? AND reasoning IS NOT NULL AND reasoning != ''
1260
- `;
1261
- const params = [pattern];
1262
- // Exclude current session if specified (for team memory from PAST sessions only)
1263
- if (excludeSessionId) {
1264
- sql += ` AND session_id != ?`;
1265
- params.push(excludeSessionId);
1266
- }
1267
- sql += ` ORDER BY timestamp DESC LIMIT ?`;
1268
- params.push(limit);
1269
- const rows = database.prepare(sql).all(...params);
1270
- return rows.map(row => {
1271
- const files = safeJsonParse(row.files, []);
1272
- // Find the matching file path from the files array
1273
- const matchedFile = files.find(f => f.includes(filePath)) || filePath;
1274
- return {
1275
- file_path: matchedFile,
1276
- reasoning: row.reasoning,
1277
- };
1278
- });
1279
- }
1280
- /**
1281
- * Combined retrieval: runs all 4 queries and deduplicates (hook version)
1282
- * Priority: key decisions > files > folders > keywords
1283
- */
1284
- export function getRelevantSteps(sessionId, currentFiles, currentFolders, keywords, limit = 10) {
1285
- const byFiles = getStepsByFiles(sessionId, currentFiles, 5);
1286
- const byFolders = getStepsByFolders(sessionId, currentFolders, 5);
1287
- const byKeywords = getStepsByKeywords(sessionId, keywords, 5);
1288
- const keyDecisions = getKeyDecisionSteps(sessionId, 5);
1289
- const seen = new Set();
1290
- const results = [];
1291
- // Priority order: key decisions > files > folders > keywords
1292
- for (const step of [...keyDecisions, ...byFiles, ...byFolders, ...byKeywords]) {
1293
- if (!seen.has(step.id)) {
1294
- seen.add(step.id);
1295
- results.push(step);
1296
- if (results.length >= limit)
1297
- break;
1298
- }
1299
- }
1300
- return results;
1301
- }
1302
- /**
1303
- * Convert database row to StepRecord (hook version - basic fields)
1304
- */
1305
- function rowToStepRecord(row) {
1306
- return {
1307
- id: row.id,
1308
- session_id: row.session_id,
1309
- action_type: row.action_type,
1310
- files: safeJsonParse(row.files, []),
1311
- folders: safeJsonParse(row.folders, []),
1312
- command: row.command,
1313
- reasoning: row.reasoning,
1314
- drift_score: row.drift_score || 0,
1315
- drift_type: row.drift_type,
1316
- is_key_decision: Boolean(row.is_key_decision),
1317
- is_validated: Boolean(row.is_validated),
1318
- correction_given: row.correction_given,
1319
- correction_level: row.correction_level,
1320
- keywords: safeJsonParse(row.keywords, []),
1321
- timestamp: row.timestamp
1322
- };
1323
- }
1324
- // ============================================
1325
- // DRIFT LOG CRUD OPERATIONS (Proxy uses these)
1326
- // ============================================
1327
- /**
1328
- * Log a drift event (for rejected actions)
1329
- */
1330
- export function logDriftEvent(input) {
1331
- const database = initDatabase();
1332
- const entry = {
1333
- id: randomUUID(),
1334
- session_id: input.session_id,
1335
- timestamp: Date.now(),
1336
- action_type: input.action_type,
1337
- files: input.files || [],
1338
- drift_score: input.drift_score,
1339
- drift_reason: input.drift_reason,
1340
- correction_given: input.correction_given,
1341
- recovery_plan: input.recovery_plan
1342
- };
1343
- const stmt = database.prepare(`
1344
- INSERT INTO drift_log (
1345
- id, session_id, timestamp, action_type, files,
1346
- drift_score, drift_reason, correction_given, recovery_plan
1347
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1348
- `);
1349
- stmt.run(entry.id, entry.session_id, entry.timestamp, entry.action_type || null, JSON.stringify(entry.files), entry.drift_score, entry.drift_reason || null, entry.correction_given || null, entry.recovery_plan ? JSON.stringify(entry.recovery_plan) : null);
1350
- return entry;
1351
- }
1352
- /**
1353
- * Get drift log for a session
1354
- */
1355
- export function getDriftLog(sessionId, limit = 50) {
1356
- const database = initDatabase();
1357
- const stmt = database.prepare('SELECT * FROM drift_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?');
1358
- const rows = stmt.all(sessionId, limit);
1359
- return rows.map(rowToDriftLogEntry);
1360
- }
1361
- /**
1362
- * Convert database row to DriftLogEntry object
1363
- */
1364
- function rowToDriftLogEntry(row) {
1365
- return {
1366
- id: row.id,
1367
- session_id: row.session_id,
1368
- timestamp: row.timestamp,
1369
- action_type: row.action_type,
1370
- files: safeJsonParse(row.files, []),
1371
- drift_score: row.drift_score,
1372
- drift_reason: row.drift_reason,
1373
- correction_given: row.correction_given,
1374
- recovery_plan: row.recovery_plan ? safeJsonParse(row.recovery_plan, {}) : undefined
1375
- };
1376
- }
1377
- // ============================================
1378
- // CONVENIENCE FUNCTIONS FOR PROXY
1379
- // ============================================
1380
- /**
1381
- * Update token count for a session
1382
- */
1383
- export function updateTokenCount(sessionId, tokenCount) {
1384
- updateSessionState(sessionId, { token_count: tokenCount });
1385
- }
1386
- /**
1387
- * Update session mode
1388
- */
1389
- export function updateSessionMode(sessionId, mode) {
1390
- updateSessionState(sessionId, { session_mode: mode });
1391
- }
1392
- /**
1393
- * Mark session as waiting for recovery
1394
- */
1395
- export function markWaitingForRecovery(sessionId, waiting) {
1396
- updateSessionState(sessionId, { waiting_for_recovery: waiting });
1397
- }
1398
- /**
1399
- * Increment escalation count
1400
- */
1401
- export function incrementEscalation(sessionId) {
1402
- const session = getSessionState(sessionId);
1403
- if (session) {
1404
- updateSessionState(sessionId, { escalation_count: session.escalation_count + 1 });
1405
- }
1406
- }
1407
- /**
1408
- * Update last clear timestamp and reset token count
1409
- */
1410
- export function markCleared(sessionId) {
1411
- updateSessionState(sessionId, {
1412
- last_clear_at: Date.now(),
1413
- token_count: 0
1414
- });
1415
- }
1416
- /**
1417
- * Mark session as completed (instead of deleting)
1418
- * Session will be cleaned up after 1 hour
1419
- */
1420
- export function markSessionCompleted(sessionId) {
1421
- const database = initDatabase();
1422
- const now = new Date().toISOString();
1423
- database.prepare(`
1424
- UPDATE session_states
1425
- SET status = 'completed', completed_at = ?, last_update = ?
1426
- WHERE session_id = ?
1427
- `).run(now, now, sessionId);
1428
- }
1429
- /**
1430
- * Cleanup sessions completed more than 24 hours ago
1431
- * Also deletes associated steps and drift_log entries
1432
- * Skips sessions that have active children (RESTRICT approach)
1433
- * Returns number of sessions cleaned up
1434
- */
1435
- export function cleanupOldCompletedSessions(maxAgeMs = 86400000) {
1436
- const database = initDatabase();
1437
- const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
1438
- // Get sessions to cleanup, excluding those with active children
1439
- // RESTRICT approach: don't delete parent if children still active
1440
- const oldSessions = database.prepare(`
1441
- SELECT session_id FROM session_states
1442
- WHERE status = 'completed'
1443
- AND completed_at < ?
1444
- AND session_id NOT IN (
1445
- SELECT DISTINCT parent_session_id
1446
- FROM session_states
1447
- WHERE parent_session_id IS NOT NULL
1448
- AND status != 'completed'
1449
- )
1450
- `).all(cutoff);
1451
- if (oldSessions.length === 0) {
1452
- return 0;
1453
- }
1454
- // Delete in correct order to respect FK constraints
1455
- for (const session of oldSessions) {
1456
- // 1. Delete from drift_log (FK to session_states)
1457
- database.prepare('DELETE FROM drift_log WHERE session_id = ?').run(session.session_id);
1458
- // 2. Delete from steps (FK to session_states)
1459
- database.prepare('DELETE FROM steps WHERE session_id = ?').run(session.session_id);
1460
- // 3. Now safe to delete session_states
1461
- database.prepare('DELETE FROM session_states WHERE session_id = ?').run(session.session_id);
1462
- }
1463
- return oldSessions.length;
1464
- }
1465
- /**
1466
- * Get completed session for project (for new_task detection)
1467
- * Returns most recent completed session if exists
1468
- */
1469
- export function getCompletedSessionForProject(projectPath) {
1470
- const database = initDatabase();
1471
- const row = database.prepare(`
1472
- SELECT * FROM session_states
1473
- WHERE project_path = ? AND status = 'completed'
1474
- ORDER BY completed_at DESC
1475
- LIMIT 1
1476
- `).get(projectPath);
1477
- return row ? rowToSessionState(row) : null;
1478
- }
1
+ // Re-export from modular store for backward compatibility
2
+ export * from './store/index.js';