grov 0.1.2 → 0.2.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 (39) hide show
  1. package/README.md +73 -88
  2. package/dist/cli.js +23 -37
  3. package/dist/commands/capture.js +1 -1
  4. package/dist/commands/disable.d.ts +1 -0
  5. package/dist/commands/disable.js +14 -0
  6. package/dist/commands/drift-test.js +56 -68
  7. package/dist/commands/init.js +29 -17
  8. package/dist/commands/proxy-status.d.ts +1 -0
  9. package/dist/commands/proxy-status.js +32 -0
  10. package/dist/commands/unregister.js +7 -1
  11. package/dist/lib/correction-builder-proxy.d.ts +16 -0
  12. package/dist/lib/correction-builder-proxy.js +125 -0
  13. package/dist/lib/correction-builder.js +1 -1
  14. package/dist/lib/drift-checker-proxy.d.ts +63 -0
  15. package/dist/lib/drift-checker-proxy.js +373 -0
  16. package/dist/lib/drift-checker.js +1 -1
  17. package/dist/lib/hooks.d.ts +11 -0
  18. package/dist/lib/hooks.js +33 -0
  19. package/dist/lib/llm-extractor.d.ts +60 -11
  20. package/dist/lib/llm-extractor.js +431 -98
  21. package/dist/lib/settings.d.ts +19 -0
  22. package/dist/lib/settings.js +63 -0
  23. package/dist/lib/store.d.ts +201 -43
  24. package/dist/lib/store.js +653 -90
  25. package/dist/proxy/action-parser.d.ts +58 -0
  26. package/dist/proxy/action-parser.js +196 -0
  27. package/dist/proxy/config.d.ts +26 -0
  28. package/dist/proxy/config.js +67 -0
  29. package/dist/proxy/forwarder.d.ts +24 -0
  30. package/dist/proxy/forwarder.js +119 -0
  31. package/dist/proxy/index.d.ts +1 -0
  32. package/dist/proxy/index.js +30 -0
  33. package/dist/proxy/request-processor.d.ts +12 -0
  34. package/dist/proxy/request-processor.js +120 -0
  35. package/dist/proxy/response-processor.d.ts +14 -0
  36. package/dist/proxy/response-processor.js +138 -0
  37. package/dist/proxy/server.d.ts +9 -0
  38. package/dist/proxy/server.js +904 -0
  39. package/package.json +8 -3
package/dist/lib/store.js CHANGED
@@ -61,7 +61,10 @@ export function initDatabase() {
61
61
  goal TEXT,
62
62
  reasoning_trace JSON DEFAULT '[]',
63
63
  files_touched JSON DEFAULT '[]',
64
+ decisions JSON DEFAULT '[]',
65
+ constraints JSON DEFAULT '[]',
64
66
  status TEXT NOT NULL CHECK(status IN ('complete', 'question', 'partial', 'abandoned')),
67
+ trigger_reason TEXT CHECK(trigger_reason IN ('complete', 'threshold', 'abandoned')),
65
68
  linked_commit TEXT,
66
69
  parent_task_id TEXT,
67
70
  turn_number INTEGER,
@@ -74,6 +77,19 @@ export function initDatabase() {
74
77
  CREATE INDEX IF NOT EXISTS idx_status ON tasks(status);
75
78
  CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at);
76
79
  `);
80
+ // Migration: add new columns to existing tasks table
81
+ try {
82
+ db.exec(`ALTER TABLE tasks ADD COLUMN decisions JSON DEFAULT '[]'`);
83
+ }
84
+ catch { /* column exists */ }
85
+ try {
86
+ db.exec(`ALTER TABLE tasks ADD COLUMN constraints JSON DEFAULT '[]'`);
87
+ }
88
+ catch { /* column exists */ }
89
+ try {
90
+ db.exec(`ALTER TABLE tasks ADD COLUMN trigger_reason TEXT`);
91
+ }
92
+ catch { /* column exists */ }
77
93
  // Create session_states table (temporary per-session tracking)
78
94
  db.exec(`
79
95
  CREATE TABLE IF NOT EXISTS session_states (
@@ -81,18 +97,85 @@ export function initDatabase() {
81
97
  user_id TEXT,
82
98
  project_path TEXT NOT NULL,
83
99
  original_goal TEXT,
84
- actions_taken JSON DEFAULT '[]',
85
- files_explored JSON DEFAULT '[]',
86
- current_intent TEXT,
87
- drift_warnings JSON DEFAULT '[]',
100
+ expected_scope JSON DEFAULT '[]',
101
+ constraints JSON DEFAULT '[]',
102
+ keywords JSON DEFAULT '[]',
103
+ token_count INTEGER DEFAULT 0,
104
+ escalation_count INTEGER DEFAULT 0,
105
+ session_mode TEXT DEFAULT 'normal' CHECK(session_mode IN ('normal', 'drifted', 'forced')),
106
+ waiting_for_recovery INTEGER DEFAULT 0,
107
+ last_checked_at INTEGER DEFAULT 0,
108
+ last_clear_at INTEGER,
88
109
  start_time TEXT NOT NULL,
89
110
  last_update TEXT NOT NULL,
90
- status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned'))
111
+ status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned')),
112
+ completed_at TEXT,
113
+ parent_session_id TEXT,
114
+ task_type TEXT DEFAULT 'main' CHECK(task_type IN ('main', 'subtask', 'parallel')),
115
+ FOREIGN KEY (parent_session_id) REFERENCES session_states(session_id)
91
116
  );
92
117
 
93
118
  CREATE INDEX IF NOT EXISTS idx_session_project ON session_states(project_path);
94
119
  CREATE INDEX IF NOT EXISTS idx_session_status ON session_states(status);
120
+ CREATE INDEX IF NOT EXISTS idx_session_parent ON session_states(parent_session_id);
95
121
  `);
122
+ // Migration: add new columns to existing session_states table
123
+ try {
124
+ db.exec(`ALTER TABLE session_states ADD COLUMN expected_scope JSON DEFAULT '[]'`);
125
+ }
126
+ catch { /* column exists */ }
127
+ try {
128
+ db.exec(`ALTER TABLE session_states ADD COLUMN constraints JSON DEFAULT '[]'`);
129
+ }
130
+ catch { /* column exists */ }
131
+ try {
132
+ db.exec(`ALTER TABLE session_states ADD COLUMN keywords JSON DEFAULT '[]'`);
133
+ }
134
+ catch { /* column exists */ }
135
+ try {
136
+ db.exec(`ALTER TABLE session_states ADD COLUMN token_count INTEGER DEFAULT 0`);
137
+ }
138
+ catch { /* column exists */ }
139
+ try {
140
+ db.exec(`ALTER TABLE session_states ADD COLUMN escalation_count INTEGER DEFAULT 0`);
141
+ }
142
+ catch { /* column exists */ }
143
+ try {
144
+ db.exec(`ALTER TABLE session_states ADD COLUMN session_mode TEXT DEFAULT 'normal'`);
145
+ }
146
+ catch { /* column exists */ }
147
+ try {
148
+ db.exec(`ALTER TABLE session_states ADD COLUMN waiting_for_recovery INTEGER DEFAULT 0`);
149
+ }
150
+ catch { /* column exists */ }
151
+ try {
152
+ db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
153
+ }
154
+ catch { /* column exists */ }
155
+ try {
156
+ db.exec(`ALTER TABLE session_states ADD COLUMN last_clear_at INTEGER`);
157
+ }
158
+ catch { /* column exists */ }
159
+ try {
160
+ db.exec(`ALTER TABLE session_states ADD COLUMN parent_session_id TEXT`);
161
+ }
162
+ catch { /* column exists */ }
163
+ try {
164
+ db.exec(`ALTER TABLE session_states ADD COLUMN task_type TEXT DEFAULT 'main'`);
165
+ }
166
+ catch { /* column exists */ }
167
+ try {
168
+ db.exec(`ALTER TABLE session_states ADD COLUMN completed_at TEXT`);
169
+ }
170
+ catch { /* column exists */ }
171
+ try {
172
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_session_parent ON session_states(parent_session_id)`);
173
+ }
174
+ catch { /* index exists */ }
175
+ try {
176
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_session_completed ON session_states(completed_at)`);
177
+ }
178
+ catch { /* index exists */ }
96
179
  // Create file_reasoning table (file-level reasoning with anchoring)
97
180
  db.exec(`
98
181
  CREATE TABLE IF NOT EXISTS file_reasoning (
@@ -117,51 +200,139 @@ export function initDatabase() {
117
200
  // Migration: Add drift detection columns to session_states (safe to run multiple times)
118
201
  const columns = db.pragma('table_info(session_states)');
119
202
  const existingColumns = new Set(columns.map(c => c.name));
203
+ // Shared columns
120
204
  if (!existingColumns.has('expected_scope')) {
121
205
  db.exec(`ALTER TABLE session_states ADD COLUMN expected_scope JSON DEFAULT '[]'`);
122
206
  }
123
207
  if (!existingColumns.has('constraints')) {
124
208
  db.exec(`ALTER TABLE session_states ADD COLUMN constraints JSON DEFAULT '[]'`);
125
209
  }
126
- if (!existingColumns.has('success_criteria')) {
127
- db.exec(`ALTER TABLE session_states ADD COLUMN success_criteria JSON DEFAULT '[]'`);
128
- }
129
210
  if (!existingColumns.has('keywords')) {
130
211
  db.exec(`ALTER TABLE session_states ADD COLUMN keywords JSON DEFAULT '[]'`);
131
212
  }
132
- if (!existingColumns.has('last_drift_score')) {
133
- db.exec(`ALTER TABLE session_states ADD COLUMN last_drift_score INTEGER`);
134
- }
135
213
  if (!existingColumns.has('escalation_count')) {
136
214
  db.exec(`ALTER TABLE session_states ADD COLUMN escalation_count INTEGER DEFAULT 0`);
137
215
  }
216
+ if (!existingColumns.has('last_checked_at')) {
217
+ db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
218
+ }
219
+ // Hook-specific columns
220
+ if (!existingColumns.has('success_criteria')) {
221
+ db.exec(`ALTER TABLE session_states ADD COLUMN success_criteria JSON DEFAULT '[]'`);
222
+ }
223
+ if (!existingColumns.has('last_drift_score')) {
224
+ db.exec(`ALTER TABLE session_states ADD COLUMN last_drift_score INTEGER`);
225
+ }
138
226
  if (!existingColumns.has('pending_recovery_plan')) {
139
227
  db.exec(`ALTER TABLE session_states ADD COLUMN pending_recovery_plan JSON`);
140
228
  }
141
229
  if (!existingColumns.has('drift_history')) {
142
230
  db.exec(`ALTER TABLE session_states ADD COLUMN drift_history JSON DEFAULT '[]'`);
143
231
  }
144
- if (!existingColumns.has('last_checked_at')) {
145
- db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
232
+ // Proxy-specific columns
233
+ if (!existingColumns.has('token_count')) {
234
+ db.exec(`ALTER TABLE session_states ADD COLUMN token_count INTEGER DEFAULT 0`);
235
+ }
236
+ if (!existingColumns.has('session_mode')) {
237
+ db.exec(`ALTER TABLE session_states ADD COLUMN session_mode TEXT DEFAULT 'normal'`);
238
+ }
239
+ if (!existingColumns.has('waiting_for_recovery')) {
240
+ db.exec(`ALTER TABLE session_states ADD COLUMN waiting_for_recovery INTEGER DEFAULT 0`);
241
+ }
242
+ if (!existingColumns.has('last_clear_at')) {
243
+ db.exec(`ALTER TABLE session_states ADD COLUMN last_clear_at INTEGER`);
244
+ }
245
+ if (!existingColumns.has('completed_at')) {
246
+ db.exec(`ALTER TABLE session_states ADD COLUMN completed_at TEXT`);
247
+ }
248
+ if (!existingColumns.has('parent_session_id')) {
249
+ db.exec(`ALTER TABLE session_states ADD COLUMN parent_session_id TEXT`);
250
+ }
251
+ if (!existingColumns.has('task_type')) {
252
+ db.exec(`ALTER TABLE session_states ADD COLUMN task_type TEXT DEFAULT 'main'`);
253
+ }
254
+ // Additional hook fields
255
+ if (!existingColumns.has('actions_taken')) {
256
+ db.exec(`ALTER TABLE session_states ADD COLUMN actions_taken JSON DEFAULT '[]'`);
146
257
  }
147
- // Create steps table (Claude's actions for drift detection)
258
+ if (!existingColumns.has('files_explored')) {
259
+ db.exec(`ALTER TABLE session_states ADD COLUMN files_explored JSON DEFAULT '[]'`);
260
+ }
261
+ if (!existingColumns.has('current_intent')) {
262
+ db.exec(`ALTER TABLE session_states ADD COLUMN current_intent TEXT`);
263
+ }
264
+ if (!existingColumns.has('drift_warnings')) {
265
+ db.exec(`ALTER TABLE session_states ADD COLUMN drift_warnings JSON DEFAULT '[]'`);
266
+ }
267
+ // Create steps table (action log for current session)
148
268
  db.exec(`
149
269
  CREATE TABLE IF NOT EXISTS steps (
150
270
  id TEXT PRIMARY KEY,
151
271
  session_id TEXT NOT NULL,
152
- action_type TEXT NOT NULL,
272
+ action_type TEXT NOT NULL CHECK(action_type IN ('edit', 'write', 'bash', 'read', 'glob', 'grep', 'task', 'other')),
153
273
  files JSON DEFAULT '[]',
154
274
  folders JSON DEFAULT '[]',
155
275
  command TEXT,
156
276
  reasoning TEXT,
157
277
  drift_score INTEGER,
158
- is_key_decision BOOLEAN DEFAULT 0,
278
+ drift_type TEXT CHECK(drift_type IN ('none', 'minor', 'major', 'critical')),
279
+ is_key_decision INTEGER DEFAULT 0,
280
+ is_validated INTEGER DEFAULT 1,
281
+ correction_given TEXT,
282
+ correction_level TEXT CHECK(correction_level IN ('nudge', 'correct', 'intervene', 'halt')),
159
283
  keywords JSON DEFAULT '[]',
160
284
  timestamp INTEGER NOT NULL,
161
285
  FOREIGN KEY (session_id) REFERENCES session_states(session_id)
162
286
  );
163
287
  CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
164
288
  CREATE INDEX IF NOT EXISTS idx_steps_timestamp ON steps(timestamp);
289
+ `);
290
+ // Migration: add new columns to existing steps table
291
+ try {
292
+ db.exec(`ALTER TABLE steps ADD COLUMN drift_type TEXT`);
293
+ }
294
+ catch { /* column exists */ }
295
+ try {
296
+ db.exec(`ALTER TABLE steps ADD COLUMN is_key_decision INTEGER DEFAULT 0`);
297
+ }
298
+ catch { /* column exists */ }
299
+ try {
300
+ db.exec(`ALTER TABLE steps ADD COLUMN is_validated INTEGER DEFAULT 1`);
301
+ }
302
+ catch { /* column exists */ }
303
+ try {
304
+ db.exec(`ALTER TABLE steps ADD COLUMN correction_given TEXT`);
305
+ }
306
+ catch { /* column exists */ }
307
+ try {
308
+ db.exec(`ALTER TABLE steps ADD COLUMN correction_level TEXT`);
309
+ }
310
+ catch { /* column exists */ }
311
+ try {
312
+ db.exec(`ALTER TABLE steps ADD COLUMN keywords JSON DEFAULT '[]'`);
313
+ }
314
+ catch { /* column exists */ }
315
+ try {
316
+ db.exec(`ALTER TABLE steps ADD COLUMN reasoning TEXT`);
317
+ }
318
+ catch { /* column exists */ }
319
+ // Create drift_log table (rejected actions for audit)
320
+ db.exec(`
321
+ CREATE TABLE IF NOT EXISTS drift_log (
322
+ id TEXT PRIMARY KEY,
323
+ session_id TEXT NOT NULL,
324
+ timestamp INTEGER NOT NULL,
325
+ action_type TEXT,
326
+ files JSON DEFAULT '[]',
327
+ drift_score INTEGER NOT NULL,
328
+ drift_reason TEXT,
329
+ correction_given TEXT,
330
+ recovery_plan JSON,
331
+ FOREIGN KEY (session_id) REFERENCES session_states(session_id)
332
+ );
333
+
334
+ CREATE INDEX IF NOT EXISTS idx_drift_log_session ON drift_log(session_id);
335
+ CREATE INDEX IF NOT EXISTS idx_drift_log_timestamp ON drift_log(timestamp);
165
336
  `);
166
337
  return db;
167
338
  }
@@ -189,7 +360,10 @@ export function createTask(input) {
189
360
  goal: input.goal,
190
361
  reasoning_trace: input.reasoning_trace || [],
191
362
  files_touched: input.files_touched || [],
363
+ decisions: input.decisions || [],
364
+ constraints: input.constraints || [],
192
365
  status: input.status,
366
+ trigger_reason: input.trigger_reason,
193
367
  linked_commit: input.linked_commit,
194
368
  parent_task_id: input.parent_task_id,
195
369
  turn_number: input.turn_number,
@@ -199,15 +373,17 @@ export function createTask(input) {
199
373
  const stmt = database.prepare(`
200
374
  INSERT INTO tasks (
201
375
  id, project_path, user, original_query, goal,
202
- reasoning_trace, files_touched, status, linked_commit,
376
+ reasoning_trace, files_touched, decisions, constraints,
377
+ status, trigger_reason, linked_commit,
203
378
  parent_task_id, turn_number, tags, created_at
204
379
  ) VALUES (
205
380
  ?, ?, ?, ?, ?,
206
381
  ?, ?, ?, ?,
382
+ ?, ?, ?,
207
383
  ?, ?, ?, ?
208
384
  )
209
385
  `);
210
- 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), task.status, task.linked_commit || null, task.parent_task_id || null, task.turn_number || null, JSON.stringify(task.tags), task.created_at);
386
+ 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);
211
387
  return task;
212
388
  }
213
389
  /**
@@ -318,7 +494,10 @@ function rowToTask(row) {
318
494
  goal: row.goal,
319
495
  reasoning_trace: safeJsonParse(row.reasoning_trace, []),
320
496
  files_touched: safeJsonParse(row.files_touched, []),
497
+ decisions: safeJsonParse(row.decisions, []),
498
+ constraints: safeJsonParse(row.constraints, []),
321
499
  status: row.status,
500
+ trigger_reason: row.trigger_reason,
322
501
  linked_commit: row.linked_commit,
323
502
  parent_task_id: row.parent_task_id,
324
503
  turn_number: row.turn_number,
@@ -337,43 +516,50 @@ export function createSessionState(input) {
337
516
  const database = initDatabase();
338
517
  const now = new Date().toISOString();
339
518
  const sessionState = {
519
+ // Base fields
340
520
  session_id: input.session_id,
341
521
  user_id: input.user_id,
342
522
  project_path: input.project_path,
343
523
  original_goal: input.original_goal,
344
- actions_taken: [],
345
- files_explored: [],
346
- current_intent: undefined,
347
- drift_warnings: [],
524
+ expected_scope: input.expected_scope || [],
525
+ constraints: input.constraints || [],
526
+ keywords: input.keywords || [],
527
+ escalation_count: 0,
528
+ last_checked_at: 0,
348
529
  start_time: now,
349
530
  last_update: now,
350
531
  status: 'active',
351
- // Drift detection fields
352
- expected_scope: input.expected_scope || [],
353
- constraints: input.constraints || [],
532
+ // Hook-specific fields
354
533
  success_criteria: input.success_criteria || [],
355
- keywords: input.keywords || [],
356
- // Drift tracking fields
357
534
  last_drift_score: undefined,
358
- escalation_count: 0,
359
535
  pending_recovery_plan: undefined,
360
536
  drift_history: [],
361
- // Action tracking
362
- last_checked_at: 0
537
+ actions_taken: [],
538
+ files_explored: [],
539
+ current_intent: undefined,
540
+ drift_warnings: [],
541
+ // Proxy-specific fields
542
+ token_count: 0,
543
+ session_mode: 'normal',
544
+ waiting_for_recovery: false,
545
+ last_clear_at: undefined,
546
+ completed_at: undefined,
547
+ parent_session_id: input.parent_session_id,
548
+ task_type: input.task_type || 'main',
363
549
  };
364
- // Use INSERT OR IGNORE to safely handle race conditions where
365
- // multiple processes might try to create the same session
366
550
  const stmt = database.prepare(`
367
551
  INSERT OR IGNORE INTO session_states (
368
552
  session_id, user_id, project_path, original_goal,
369
- actions_taken, files_explored, current_intent, drift_warnings,
553
+ expected_scope, constraints, keywords,
554
+ token_count, escalation_count, session_mode,
555
+ waiting_for_recovery, last_checked_at, last_clear_at,
370
556
  start_time, last_update, status,
371
- expected_scope, constraints, success_criteria, keywords,
372
- last_drift_score, escalation_count, pending_recovery_plan, drift_history,
373
- last_checked_at
374
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
557
+ parent_session_id, task_type,
558
+ success_criteria, last_drift_score, pending_recovery_plan, drift_history,
559
+ completed_at
560
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
375
561
  `);
376
- stmt.run(sessionState.session_id, sessionState.user_id || null, sessionState.project_path, sessionState.original_goal || null, JSON.stringify(sessionState.actions_taken), JSON.stringify(sessionState.files_explored), sessionState.current_intent || null, JSON.stringify(sessionState.drift_warnings), sessionState.start_time, sessionState.last_update, sessionState.status, JSON.stringify(sessionState.expected_scope), JSON.stringify(sessionState.constraints), JSON.stringify(sessionState.success_criteria), JSON.stringify(sessionState.keywords), sessionState.last_drift_score || null, sessionState.escalation_count, sessionState.pending_recovery_plan ? JSON.stringify(sessionState.pending_recovery_plan) : null, JSON.stringify(sessionState.drift_history), sessionState.last_checked_at);
562
+ 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);
377
563
  return sessionState;
378
564
  }
379
565
  /**
@@ -405,21 +591,41 @@ export function updateSessionState(sessionId, updates) {
405
591
  setClauses.push('original_goal = ?');
406
592
  params.push(updates.original_goal || null);
407
593
  }
408
- if (updates.actions_taken !== undefined) {
409
- setClauses.push('actions_taken = ?');
410
- params.push(JSON.stringify(updates.actions_taken));
594
+ if (updates.expected_scope !== undefined) {
595
+ setClauses.push('expected_scope = ?');
596
+ params.push(JSON.stringify(updates.expected_scope));
597
+ }
598
+ if (updates.constraints !== undefined) {
599
+ setClauses.push('constraints = ?');
600
+ params.push(JSON.stringify(updates.constraints));
411
601
  }
412
- if (updates.files_explored !== undefined) {
413
- setClauses.push('files_explored = ?');
414
- params.push(JSON.stringify(updates.files_explored));
602
+ if (updates.keywords !== undefined) {
603
+ setClauses.push('keywords = ?');
604
+ params.push(JSON.stringify(updates.keywords));
415
605
  }
416
- if (updates.current_intent !== undefined) {
417
- setClauses.push('current_intent = ?');
418
- params.push(updates.current_intent || null);
606
+ if (updates.token_count !== undefined) {
607
+ setClauses.push('token_count = ?');
608
+ params.push(updates.token_count);
419
609
  }
420
- if (updates.drift_warnings !== undefined) {
421
- setClauses.push('drift_warnings = ?');
422
- params.push(JSON.stringify(updates.drift_warnings));
610
+ if (updates.escalation_count !== undefined) {
611
+ setClauses.push('escalation_count = ?');
612
+ params.push(updates.escalation_count);
613
+ }
614
+ if (updates.session_mode !== undefined) {
615
+ setClauses.push('session_mode = ?');
616
+ params.push(updates.session_mode);
617
+ }
618
+ if (updates.waiting_for_recovery !== undefined) {
619
+ setClauses.push('waiting_for_recovery = ?');
620
+ params.push(updates.waiting_for_recovery ? 1 : 0);
621
+ }
622
+ if (updates.last_checked_at !== undefined) {
623
+ setClauses.push('last_checked_at = ?');
624
+ params.push(updates.last_checked_at);
625
+ }
626
+ if (updates.last_clear_at !== undefined) {
627
+ setClauses.push('last_clear_at = ?');
628
+ params.push(updates.last_clear_at);
423
629
  }
424
630
  if (updates.status !== undefined) {
425
631
  setClauses.push('status = ?');
@@ -454,36 +660,80 @@ export function getActiveSessionsForProject(projectPath) {
454
660
  const rows = stmt.all(projectPath);
455
661
  return rows.map(rowToSessionState);
456
662
  }
663
+ /**
664
+ * Get child sessions (subtasks and parallel tasks) for a parent session
665
+ */
666
+ export function getChildSessions(parentSessionId) {
667
+ const database = initDatabase();
668
+ const stmt = database.prepare('SELECT * FROM session_states WHERE parent_session_id = ? ORDER BY start_time DESC');
669
+ const rows = stmt.all(parentSessionId);
670
+ return rows.map(rowToSessionState);
671
+ }
672
+ /**
673
+ * Get active session for a specific user in a project
674
+ */
675
+ export function getActiveSessionForUser(projectPath, userId) {
676
+ const database = initDatabase();
677
+ if (userId) {
678
+ const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND user_id = ? AND status = 'active' ORDER BY last_update DESC LIMIT 1");
679
+ const row = stmt.get(projectPath, userId);
680
+ return row ? rowToSessionState(row) : null;
681
+ }
682
+ else {
683
+ const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND status = 'active' ORDER BY last_update DESC LIMIT 1");
684
+ const row = stmt.get(projectPath);
685
+ return row ? rowToSessionState(row) : null;
686
+ }
687
+ }
688
+ /**
689
+ * Get all active sessions (for proxy-status command)
690
+ */
691
+ export function getActiveSessionsForStatus() {
692
+ const database = initDatabase();
693
+ const stmt = database.prepare("SELECT * FROM session_states WHERE status = 'active' ORDER BY last_update DESC LIMIT 20");
694
+ const rows = stmt.all();
695
+ return rows.map(rowToSessionState);
696
+ }
457
697
  /**
458
698
  * Convert database row to SessionState object
459
699
  */
460
700
  function rowToSessionState(row) {
461
701
  return {
702
+ // Base fields
462
703
  session_id: row.session_id,
463
704
  user_id: row.user_id,
464
705
  project_path: row.project_path,
465
706
  original_goal: row.original_goal,
466
- actions_taken: safeJsonParse(row.actions_taken, []),
467
- files_explored: safeJsonParse(row.files_explored, []),
468
- current_intent: row.current_intent,
469
- drift_warnings: safeJsonParse(row.drift_warnings, []),
707
+ expected_scope: safeJsonParse(row.expected_scope, []),
708
+ constraints: safeJsonParse(row.constraints, []),
709
+ keywords: safeJsonParse(row.keywords, []),
710
+ escalation_count: row.escalation_count || 0,
711
+ last_checked_at: row.last_checked_at || 0,
470
712
  start_time: row.start_time,
471
713
  last_update: row.last_update,
472
714
  status: row.status,
473
- // Drift detection fields
474
- expected_scope: safeJsonParse(row.expected_scope, []),
475
- constraints: safeJsonParse(row.constraints, []),
715
+ // Hook-specific fields
476
716
  success_criteria: safeJsonParse(row.success_criteria, []),
477
- keywords: safeJsonParse(row.keywords, []),
478
- // Drift tracking fields
479
717
  last_drift_score: row.last_drift_score,
480
- escalation_count: row.escalation_count || 0,
481
718
  pending_recovery_plan: safeJsonParse(row.pending_recovery_plan, undefined),
482
719
  drift_history: safeJsonParse(row.drift_history, []),
483
- // Action tracking
484
- last_checked_at: row.last_checked_at || 0
720
+ actions_taken: safeJsonParse(row.actions_taken, []),
721
+ files_explored: safeJsonParse(row.files_explored, []),
722
+ current_intent: row.current_intent,
723
+ drift_warnings: safeJsonParse(row.drift_warnings, []),
724
+ // Proxy-specific fields
725
+ token_count: row.token_count || 0,
726
+ session_mode: row.session_mode || 'normal',
727
+ waiting_for_recovery: Boolean(row.waiting_for_recovery),
728
+ last_clear_at: row.last_clear_at,
729
+ completed_at: row.completed_at,
730
+ parent_session_id: row.parent_session_id,
731
+ task_type: row.task_type || 'main',
485
732
  };
486
733
  }
734
+ // ============================================
735
+ // DRIFT DETECTION OPERATIONS (hook uses these)
736
+ // ============================================
487
737
  /**
488
738
  * Update session drift metrics after a prompt check
489
739
  */
@@ -510,11 +760,12 @@ export function updateSessionDrift(sessionId, driftScore, correctionLevel, promp
510
760
  level: correctionLevel || 'none',
511
761
  prompt_summary: promptSummary.substring(0, 100)
512
762
  };
513
- const newHistory = [...session.drift_history, driftEvent];
763
+ const newHistory = [...(session.drift_history || []), driftEvent];
514
764
  // Add to drift_warnings if correction was given
765
+ const currentWarnings = session.drift_warnings || [];
515
766
  const newWarnings = correctionLevel
516
- ? [...session.drift_warnings, `[${now}] ${correctionLevel}: score ${driftScore}`]
517
- : session.drift_warnings;
767
+ ? [...currentWarnings, `[${now}] ${correctionLevel}: score ${driftScore}`]
768
+ : currentWarnings;
518
769
  const stmt = database.prepare(`
519
770
  UPDATE session_states SET
520
771
  last_drift_score = ?,
@@ -536,19 +787,21 @@ export function shouldFlagForReview(sessionId) {
536
787
  if (!session)
537
788
  return false;
538
789
  // Check number of warnings
539
- if (session.drift_warnings.length >= 3) {
790
+ const warnings = session.drift_warnings || [];
791
+ if (warnings.length >= 3) {
540
792
  return true;
541
793
  }
542
794
  // Check drift history for average score
543
- if (session.drift_history.length >= 2) {
544
- const totalScore = session.drift_history.reduce((sum, e) => sum + e.score, 0);
545
- const avgScore = totalScore / session.drift_history.length;
795
+ const history = session.drift_history || [];
796
+ if (history.length >= 2) {
797
+ const totalScore = history.reduce((sum, e) => sum + e.score, 0);
798
+ const avgScore = totalScore / history.length;
546
799
  if (avgScore < 6) {
547
800
  return true;
548
801
  }
549
802
  }
550
803
  // Check if any HALT level drift occurred
551
- if (session.drift_history.some(e => e.level === 'halt')) {
804
+ if (history.some(e => e.level === 'halt')) {
552
805
  return true;
553
806
  }
554
807
  // Check current escalation level
@@ -562,15 +815,16 @@ export function shouldFlagForReview(sessionId) {
562
815
  */
563
816
  export function getDriftSummary(sessionId) {
564
817
  const session = getSessionState(sessionId);
565
- if (!session || session.drift_history.length === 0) {
818
+ const history = session?.drift_history || [];
819
+ if (!session || history.length === 0) {
566
820
  return { totalEvents: 0, resolved: true, finalScore: null, hadHalt: false };
567
821
  }
568
- const lastEvent = session.drift_history[session.drift_history.length - 1];
822
+ const lastEvent = history[history.length - 1];
569
823
  return {
570
- totalEvents: session.drift_history.length,
824
+ totalEvents: history.length,
571
825
  resolved: lastEvent.score >= 8,
572
826
  finalScore: lastEvent.score,
573
- hadHalt: session.drift_history.some(e => e.level === 'halt')
827
+ hadHalt: history.some(e => e.level === 'halt')
574
828
  };
575
829
  }
576
830
  // ============================================
@@ -655,8 +909,143 @@ function rowToFileReasoning(row) {
655
909
  export function getDatabasePath() {
656
910
  return DB_PATH;
657
911
  }
912
+ // ============================================
913
+ // STEPS CRUD OPERATIONS (Proxy uses these)
914
+ // ============================================
915
+ /**
916
+ * Create a new step record (proxy version)
917
+ */
918
+ export function createStep(input) {
919
+ const database = initDatabase();
920
+ const step = {
921
+ id: randomUUID(),
922
+ session_id: input.session_id,
923
+ action_type: input.action_type,
924
+ files: input.files || [],
925
+ folders: input.folders || [],
926
+ command: input.command,
927
+ reasoning: input.reasoning,
928
+ drift_score: input.drift_score,
929
+ drift_type: input.drift_type,
930
+ is_key_decision: input.is_key_decision || false,
931
+ is_validated: input.is_validated !== false,
932
+ correction_given: input.correction_given,
933
+ correction_level: input.correction_level,
934
+ keywords: input.keywords || [],
935
+ timestamp: Date.now()
936
+ };
937
+ const stmt = database.prepare(`
938
+ INSERT INTO steps (
939
+ id, session_id, action_type, files, folders, command, reasoning,
940
+ drift_score, drift_type, is_key_decision, is_validated,
941
+ correction_given, correction_level, keywords, timestamp
942
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
943
+ `);
944
+ 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);
945
+ return step;
946
+ }
947
+ /**
948
+ * Get steps for a session
949
+ */
950
+ export function getStepsForSession(sessionId, limit) {
951
+ const database = initDatabase();
952
+ let sql = 'SELECT * FROM steps WHERE session_id = ? ORDER BY timestamp DESC';
953
+ const params = [sessionId];
954
+ if (limit) {
955
+ sql += ' LIMIT ?';
956
+ params.push(limit);
957
+ }
958
+ const stmt = database.prepare(sql);
959
+ const rows = stmt.all(...params);
960
+ return rows.map(rowToStep);
961
+ }
962
+ /**
963
+ * Get recent steps for a session (most recent N)
964
+ */
965
+ export function getRecentSteps(sessionId, count = 10) {
966
+ return getStepsForSession(sessionId, count);
967
+ }
968
+ /**
969
+ * Get validated steps only (for summary generation)
970
+ */
971
+ export function getValidatedSteps(sessionId) {
972
+ const database = initDatabase();
973
+ const stmt = database.prepare('SELECT * FROM steps WHERE session_id = ? AND is_validated = 1 ORDER BY timestamp ASC');
974
+ const rows = stmt.all(sessionId);
975
+ return rows.map(rowToStep);
976
+ }
977
+ /**
978
+ * Delete steps for a session
979
+ */
980
+ export function deleteStepsForSession(sessionId) {
981
+ const database = initDatabase();
982
+ database.prepare('DELETE FROM steps WHERE session_id = ?').run(sessionId);
983
+ }
984
+ /**
985
+ * Update reasoning for recent steps that don't have reasoning yet
986
+ * Called at end_turn to backfill reasoning from Claude's text response
987
+ */
988
+ export function updateRecentStepsReasoning(sessionId, reasoning, limit = 10) {
989
+ const database = initDatabase();
990
+ const stmt = database.prepare(`
991
+ UPDATE steps
992
+ SET reasoning = ?
993
+ WHERE session_id = ?
994
+ AND (reasoning IS NULL OR reasoning = '')
995
+ AND id IN (
996
+ SELECT id FROM steps
997
+ WHERE session_id = ?
998
+ ORDER BY timestamp DESC
999
+ LIMIT ?
1000
+ )
1001
+ `);
1002
+ const result = stmt.run(reasoning, sessionId, sessionId, limit);
1003
+ return result.changes;
1004
+ }
1005
+ /**
1006
+ * Get relevant steps (key decisions and write/edit actions) - proxy version
1007
+ * Reference: plan_proxy_local.md Section 2.2
1008
+ */
1009
+ export function getRelevantStepsSimple(sessionId, limit = 20) {
1010
+ const database = initDatabase();
1011
+ const stmt = database.prepare(`
1012
+ SELECT * FROM steps
1013
+ WHERE session_id = ?
1014
+ AND (is_key_decision = 1 OR action_type IN ('edit', 'write', 'bash'))
1015
+ AND is_validated = 1
1016
+ ORDER BY timestamp DESC
1017
+ LIMIT ?
1018
+ `);
1019
+ const rows = stmt.all(sessionId, limit);
1020
+ return rows.map(rowToStep);
1021
+ }
1022
+ /**
1023
+ * Convert database row to StepRecord object (proxy version - all fields)
1024
+ */
1025
+ function rowToStep(row) {
1026
+ return {
1027
+ id: row.id,
1028
+ session_id: row.session_id,
1029
+ action_type: row.action_type,
1030
+ files: safeJsonParse(row.files, []),
1031
+ folders: safeJsonParse(row.folders, []),
1032
+ command: row.command,
1033
+ reasoning: row.reasoning,
1034
+ drift_score: row.drift_score,
1035
+ drift_type: row.drift_type,
1036
+ is_key_decision: Boolean(row.is_key_decision),
1037
+ is_validated: Boolean(row.is_validated),
1038
+ correction_given: row.correction_given,
1039
+ correction_level: row.correction_level,
1040
+ keywords: safeJsonParse(row.keywords, []),
1041
+ timestamp: row.timestamp
1042
+ };
1043
+ }
1044
+ // ============================================
1045
+ // STEPS CRUD (Hook uses these)
1046
+ // ============================================
658
1047
  /**
659
- * Save a Claude action as a step
1048
+ * Save a Claude action as a step (hook version - uses ClaudeAction)
660
1049
  */
661
1050
  export function saveStep(sessionId, action, driftScore, isKeyDecision = false, keywords = []) {
662
1051
  const database = initDatabase();
@@ -669,16 +1058,6 @@ export function saveStep(sessionId, action, driftScore, isKeyDecision = false, k
669
1058
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
670
1059
  `).run(randomUUID(), sessionId, action.type, JSON.stringify(action.files), JSON.stringify(folders), action.command || null, driftScore, isKeyDecision ? 1 : 0, JSON.stringify(keywords), action.timestamp);
671
1060
  }
672
- /**
673
- * Get recent steps for a session (most recent first)
674
- */
675
- export function getRecentSteps(sessionId, limit = 10) {
676
- const database = initDatabase();
677
- const rows = database.prepare(`
678
- SELECT * FROM steps WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?
679
- `).all(sessionId, limit);
680
- return rows.map(rowToStepRecord);
681
- }
682
1061
  /**
683
1062
  * Update last_checked_at timestamp for a session
684
1063
  */
@@ -689,7 +1068,7 @@ export function updateLastChecked(sessionId, timestamp) {
689
1068
  `).run(timestamp, sessionId);
690
1069
  }
691
1070
  // ============================================
692
- // 4-QUERY RETRIEVAL (from deep_dive.md)
1071
+ // 4-QUERY RETRIEVAL (Hook uses these - from deep_dive.md)
693
1072
  // ============================================
694
1073
  /**
695
1074
  * Get steps that touched specific files
@@ -752,7 +1131,32 @@ export function getKeyDecisionSteps(sessionId, limit = 5) {
752
1131
  return rows.map(rowToStepRecord);
753
1132
  }
754
1133
  /**
755
- * Combined retrieval: runs all 4 queries and deduplicates
1134
+ * Get steps reasoning by file path (for proxy team memory injection)
1135
+ * Searches across ALL sessions, returns file-level reasoning from steps table
1136
+ */
1137
+ export function getStepsReasoningByPath(filePath, limit = 5) {
1138
+ const database = initDatabase();
1139
+ // Search steps where files JSON contains this path and reasoning exists
1140
+ const pattern = `%"${escapeLikePattern(filePath)}"%`;
1141
+ const rows = database.prepare(`
1142
+ SELECT files, reasoning
1143
+ FROM steps
1144
+ WHERE files LIKE ? AND reasoning IS NOT NULL AND reasoning != ''
1145
+ ORDER BY timestamp DESC
1146
+ LIMIT ?
1147
+ `).all(pattern, limit);
1148
+ return rows.map(row => {
1149
+ const files = safeJsonParse(row.files, []);
1150
+ // Find the matching file path from the files array
1151
+ const matchedFile = files.find(f => f.includes(filePath)) || filePath;
1152
+ return {
1153
+ file_path: matchedFile,
1154
+ reasoning: row.reasoning,
1155
+ };
1156
+ });
1157
+ }
1158
+ /**
1159
+ * Combined retrieval: runs all 4 queries and deduplicates (hook version)
756
1160
  * Priority: key decisions > files > folders > keywords
757
1161
  */
758
1162
  export function getRelevantSteps(sessionId, currentFiles, currentFolders, keywords, limit = 10) {
@@ -774,7 +1178,7 @@ export function getRelevantSteps(sessionId, currentFiles, currentFolders, keywor
774
1178
  return results;
775
1179
  }
776
1180
  /**
777
- * Convert database row to StepRecord
1181
+ * Convert database row to StepRecord (hook version - basic fields)
778
1182
  */
779
1183
  function rowToStepRecord(row) {
780
1184
  return {
@@ -785,9 +1189,168 @@ function rowToStepRecord(row) {
785
1189
  folders: safeJsonParse(row.folders, []),
786
1190
  command: row.command,
787
1191
  reasoning: row.reasoning,
788
- drift_score: row.drift_score,
789
- is_key_decision: row.is_key_decision === 1,
1192
+ drift_score: row.drift_score || 0,
1193
+ drift_type: row.drift_type,
1194
+ is_key_decision: Boolean(row.is_key_decision),
1195
+ is_validated: Boolean(row.is_validated),
1196
+ correction_given: row.correction_given,
1197
+ correction_level: row.correction_level,
790
1198
  keywords: safeJsonParse(row.keywords, []),
791
1199
  timestamp: row.timestamp
792
1200
  };
793
1201
  }
1202
+ // ============================================
1203
+ // DRIFT LOG CRUD OPERATIONS (Proxy uses these)
1204
+ // ============================================
1205
+ /**
1206
+ * Log a drift event (for rejected actions)
1207
+ */
1208
+ export function logDriftEvent(input) {
1209
+ const database = initDatabase();
1210
+ const entry = {
1211
+ id: randomUUID(),
1212
+ session_id: input.session_id,
1213
+ timestamp: Date.now(),
1214
+ action_type: input.action_type,
1215
+ files: input.files || [],
1216
+ drift_score: input.drift_score,
1217
+ drift_reason: input.drift_reason,
1218
+ correction_given: input.correction_given,
1219
+ recovery_plan: input.recovery_plan
1220
+ };
1221
+ const stmt = database.prepare(`
1222
+ INSERT INTO drift_log (
1223
+ id, session_id, timestamp, action_type, files,
1224
+ drift_score, drift_reason, correction_given, recovery_plan
1225
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1226
+ `);
1227
+ 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);
1228
+ return entry;
1229
+ }
1230
+ /**
1231
+ * Get drift log for a session
1232
+ */
1233
+ export function getDriftLog(sessionId, limit = 50) {
1234
+ const database = initDatabase();
1235
+ const stmt = database.prepare('SELECT * FROM drift_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?');
1236
+ const rows = stmt.all(sessionId, limit);
1237
+ return rows.map(rowToDriftLogEntry);
1238
+ }
1239
+ /**
1240
+ * Convert database row to DriftLogEntry object
1241
+ */
1242
+ function rowToDriftLogEntry(row) {
1243
+ return {
1244
+ id: row.id,
1245
+ session_id: row.session_id,
1246
+ timestamp: row.timestamp,
1247
+ action_type: row.action_type,
1248
+ files: safeJsonParse(row.files, []),
1249
+ drift_score: row.drift_score,
1250
+ drift_reason: row.drift_reason,
1251
+ correction_given: row.correction_given,
1252
+ recovery_plan: row.recovery_plan ? safeJsonParse(row.recovery_plan, {}) : undefined
1253
+ };
1254
+ }
1255
+ // ============================================
1256
+ // CONVENIENCE FUNCTIONS FOR PROXY
1257
+ // ============================================
1258
+ /**
1259
+ * Update token count for a session
1260
+ */
1261
+ export function updateTokenCount(sessionId, tokenCount) {
1262
+ updateSessionState(sessionId, { token_count: tokenCount });
1263
+ }
1264
+ /**
1265
+ * Update session mode
1266
+ */
1267
+ export function updateSessionMode(sessionId, mode) {
1268
+ updateSessionState(sessionId, { session_mode: mode });
1269
+ }
1270
+ /**
1271
+ * Mark session as waiting for recovery
1272
+ */
1273
+ export function markWaitingForRecovery(sessionId, waiting) {
1274
+ updateSessionState(sessionId, { waiting_for_recovery: waiting });
1275
+ }
1276
+ /**
1277
+ * Increment escalation count
1278
+ */
1279
+ export function incrementEscalation(sessionId) {
1280
+ const session = getSessionState(sessionId);
1281
+ if (session) {
1282
+ updateSessionState(sessionId, { escalation_count: session.escalation_count + 1 });
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Update last clear timestamp and reset token count
1287
+ */
1288
+ export function markCleared(sessionId) {
1289
+ updateSessionState(sessionId, {
1290
+ last_clear_at: Date.now(),
1291
+ token_count: 0
1292
+ });
1293
+ }
1294
+ /**
1295
+ * Mark session as completed (instead of deleting)
1296
+ * Session will be cleaned up after 1 hour
1297
+ */
1298
+ export function markSessionCompleted(sessionId) {
1299
+ const database = initDatabase();
1300
+ const now = new Date().toISOString();
1301
+ database.prepare(`
1302
+ UPDATE session_states
1303
+ SET status = 'completed', completed_at = ?, last_update = ?
1304
+ WHERE session_id = ?
1305
+ `).run(now, now, sessionId);
1306
+ }
1307
+ /**
1308
+ * Cleanup sessions completed more than 24 hours ago
1309
+ * Also deletes associated steps and drift_log entries
1310
+ * Skips sessions that have active children (RESTRICT approach)
1311
+ * Returns number of sessions cleaned up
1312
+ */
1313
+ export function cleanupOldCompletedSessions(maxAgeMs = 86400000) {
1314
+ const database = initDatabase();
1315
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
1316
+ // Get sessions to cleanup, excluding those with active children
1317
+ // RESTRICT approach: don't delete parent if children still active
1318
+ const oldSessions = database.prepare(`
1319
+ SELECT session_id FROM session_states
1320
+ WHERE status = 'completed'
1321
+ AND completed_at < ?
1322
+ AND session_id NOT IN (
1323
+ SELECT DISTINCT parent_session_id
1324
+ FROM session_states
1325
+ WHERE parent_session_id IS NOT NULL
1326
+ AND status != 'completed'
1327
+ )
1328
+ `).all(cutoff);
1329
+ if (oldSessions.length === 0) {
1330
+ return 0;
1331
+ }
1332
+ // Delete in correct order to respect FK constraints
1333
+ for (const session of oldSessions) {
1334
+ // 1. Delete from drift_log (FK to session_states)
1335
+ database.prepare('DELETE FROM drift_log WHERE session_id = ?').run(session.session_id);
1336
+ // 2. Delete from steps (FK to session_states)
1337
+ database.prepare('DELETE FROM steps WHERE session_id = ?').run(session.session_id);
1338
+ // 3. Now safe to delete session_states
1339
+ database.prepare('DELETE FROM session_states WHERE session_id = ?').run(session.session_id);
1340
+ }
1341
+ return oldSessions.length;
1342
+ }
1343
+ /**
1344
+ * Get completed session for project (for new_task detection)
1345
+ * Returns most recent completed session if exists
1346
+ */
1347
+ export function getCompletedSessionForProject(projectPath) {
1348
+ const database = initDatabase();
1349
+ const row = database.prepare(`
1350
+ SELECT * FROM session_states
1351
+ WHERE project_path = ? AND status = 'completed'
1352
+ ORDER BY completed_at DESC
1353
+ LIMIT 1
1354
+ `).get(projectPath);
1355
+ return row ? rowToSessionState(row) : null;
1356
+ }