grov 0.6.15 → 0.6.16

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.
@@ -1 +1,3 @@
1
- export declare function doctor(agent?: string): Promise<void>;
1
+ export declare function doctor(agent?: string, options?: {
2
+ repair?: boolean;
3
+ }): Promise<void>;
@@ -5,11 +5,16 @@ import { join } from 'path';
5
5
  import { request } from 'undici';
6
6
  import { readCredentials, getSyncStatus } from '../../core/cloud/credentials.js';
7
7
  import { initDatabase } from '../../core/store/database.js';
8
+ import { getOrphanedSessionCount, repairOrphanedSessions } from '../../core/store/sessions.js';
8
9
  import { getAllCliAgents, getCliAgentById } from '../agents/registry.js';
9
10
  const DB_PATH = join(homedir(), '.grov', 'memory.db');
10
- export async function doctor(agent) {
11
+ export async function doctor(agent, options) {
11
12
  console.log('\nGrov Doctor');
12
13
  console.log('===========\n');
14
+ if (options?.repair) {
15
+ await runRepair();
16
+ return;
17
+ }
13
18
  if (!agent) {
14
19
  await runGeneralChecks();
15
20
  console.log('\n--- Agent Status ---\n');
@@ -60,6 +65,31 @@ async function runGeneralChecks() {
60
65
  const dbOk = dbStats.tasks > 0 || dbStats.sessions > 0;
61
66
  const dbMsg = `${dbStats.tasks} tasks, ${dbStats.unsynced} unsynced, ${dbStats.sessions} active`;
62
67
  printCheck('Local Database', dbOk, dbMsg, 'Empty', 'Use Claude/Codex with proxy, or Cursor with MCP');
68
+ const orphans = checkOrphanedSessions();
69
+ printCheck('Session Integrity', orphans === 0, 'No orphaned sessions', `${orphans} orphaned session(s)`, 'grov doctor --repair');
70
+ }
71
+ async function runRepair() {
72
+ console.log('Database Repair\n');
73
+ if (!existsSync(DB_PATH)) {
74
+ console.log('\x1b[31m✗\x1b[0m No database found');
75
+ return;
76
+ }
77
+ initDatabase();
78
+ const orphans = getOrphanedSessionCount();
79
+ if (orphans === 0) {
80
+ console.log('\x1b[32m✓\x1b[0m No orphaned sessions found');
81
+ return;
82
+ }
83
+ console.log(`Found ${orphans} orphaned session(s)`);
84
+ const repaired = repairOrphanedSessions();
85
+ console.log(`\x1b[32m✓\x1b[0m Repaired ${repaired} session(s)`);
86
+ }
87
+ function checkOrphanedSessions() {
88
+ if (!existsSync(DB_PATH)) {
89
+ return 0;
90
+ }
91
+ initDatabase();
92
+ return getOrphanedSessionCount();
63
93
  }
64
94
  async function runClaudeChecks() {
65
95
  console.log('Claude Code Checks\n');
package/dist/cli/index.js CHANGED
@@ -166,11 +166,21 @@ program
166
166
  program
167
167
  .command('doctor [agent]')
168
168
  .description('Check grov setup and diagnose issues (optional: claude, codex)')
169
- .action(safeAction(async (agent) => {
170
- const { doctor } = await import('./commands/doctor.js');
171
- const agentName = agent === 'claude' || agent === 'codex' ? agent : undefined;
172
- await doctor(agentName);
173
- }));
169
+ .option('--repair', 'Repair database integrity issues')
170
+ .action(async (agent, options) => {
171
+ try {
172
+ const { doctor } = await import('./commands/doctor.js');
173
+ const agentName = agent === 'claude' || agent === 'codex' ? agent : undefined;
174
+ await doctor(agentName, options);
175
+ }
176
+ catch (error) {
177
+ console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
178
+ process.exit(1);
179
+ }
180
+ finally {
181
+ closeDatabase();
182
+ }
183
+ });
174
184
  // grov agents - List supported agents
175
185
  program
176
186
  .command('agents')
@@ -26,6 +26,7 @@ export declare function markCleared(sessionId: string): void;
26
26
  export declare function markSessionCompleted(sessionId: string): void;
27
27
  /**
28
28
  * Cleanup old completed/abandoned sessions and their steps/drift_log
29
+ * Orders deletion by hierarchy depth (children first) to respect FK constraints
29
30
  */
30
31
  export declare function cleanupOldCompletedSessions(maxAgeMs?: number): number;
31
32
  /**
@@ -52,33 +52,48 @@ export function markSessionCompleted(sessionId) {
52
52
  }
53
53
  /**
54
54
  * Cleanup old completed/abandoned sessions and their steps/drift_log
55
+ * Orders deletion by hierarchy depth (children first) to respect FK constraints
55
56
  */
56
57
  export function cleanupOldCompletedSessions(maxAgeMs = 86400000) {
57
58
  const database = getDb();
58
59
  const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
60
+ // Get sessions ordered by depth: leaves first, then parents
59
61
  const oldSessions = database.prepare(`
60
- SELECT session_id FROM session_states
61
- WHERE status IN ('completed', 'abandoned')
62
- AND completed_at < ?
63
- AND session_id NOT IN (
64
- SELECT DISTINCT parent_session_id
65
- FROM session_states
66
- WHERE parent_session_id IS NOT NULL
67
- AND status != 'completed'
62
+ WITH RECURSIVE session_depth AS (
63
+ SELECT s.session_id, 0 as depth
64
+ FROM session_states s
65
+ WHERE s.status IN ('completed', 'abandoned')
66
+ AND s.completed_at < ?
67
+ AND NOT EXISTS (
68
+ SELECT 1 FROM session_states child
69
+ WHERE child.parent_session_id = s.session_id
70
+ )
71
+ UNION ALL
72
+ SELECT parent.session_id, sd.depth + 1
73
+ FROM session_states parent
74
+ INNER JOIN session_depth sd ON sd.session_id IN (
75
+ SELECT session_id FROM session_states
76
+ WHERE parent_session_id = parent.session_id
68
77
  )
69
- `).all(cutoff);
78
+ WHERE parent.status IN ('completed', 'abandoned')
79
+ AND parent.completed_at < ?
80
+ )
81
+ SELECT DISTINCT session_id, MAX(depth) as depth
82
+ FROM session_depth
83
+ GROUP BY session_id
84
+ ORDER BY depth ASC
85
+ `).all(cutoff, cutoff);
70
86
  if (oldSessions.length === 0) {
71
87
  return 0;
72
88
  }
73
- // Delete in correct order to respect FK constraints
74
- for (const session of oldSessions) {
75
- // 1. Delete from drift_log (FK to session_states)
76
- database.prepare('DELETE FROM drift_log WHERE session_id = ?').run(session.session_id);
77
- // 2. Delete from steps (FK to session_states)
78
- database.prepare('DELETE FROM steps WHERE session_id = ?').run(session.session_id);
79
- // 3. Now safe to delete session_states
80
- database.prepare('DELETE FROM session_states WHERE session_id = ?').run(session.session_id);
81
- }
89
+ const deleteTransaction = database.transaction(() => {
90
+ for (const session of oldSessions) {
91
+ database.prepare('DELETE FROM drift_log WHERE session_id = ?').run(session.session_id);
92
+ database.prepare('DELETE FROM steps WHERE session_id = ?').run(session.session_id);
93
+ database.prepare('DELETE FROM session_states WHERE session_id = ?').run(session.session_id);
94
+ }
95
+ });
96
+ deleteTransaction();
82
97
  return oldSessions.length;
83
98
  }
84
99
  /**
@@ -377,6 +377,13 @@ export function initDatabase() {
377
377
  CREATE INDEX IF NOT EXISTS idx_drift_log_session ON drift_log(session_id);
378
378
  CREATE INDEX IF NOT EXISTS idx_drift_log_timestamp ON drift_log(timestamp);
379
379
  `);
380
+ // Nullify orphaned parent references
381
+ db.prepare(`
382
+ UPDATE session_states
383
+ SET parent_session_id = NULL
384
+ WHERE parent_session_id IS NOT NULL
385
+ AND parent_session_id NOT IN (SELECT session_id FROM session_states)
386
+ `).run();
380
387
  return db;
381
388
  }
382
389
  /**
@@ -1,7 +1,7 @@
1
1
  export type { TaskStatus, TriggerReason, SessionStatus, SessionMode, TaskType, StepActionType, DriftType, CorrectionLevel, Task, CreateTaskInput, RecoveryPlan, DriftEvent, SessionState, CreateSessionStateInput, StepRecord, CreateStepInput, DriftLogEntry, CreateDriftLogInput, } from './types.js';
2
2
  export { initDatabase, closeDatabase, getDatabasePath } from './database.js';
3
3
  export { createTask, getTasksForProject, getTaskCount, getUnsyncedTasks, markTaskSynced, setTaskSyncError, getSyncedTaskCount, cleanupOldSyncedTasks, cleanupFailedSyncTasks } from './tasks.js';
4
- export { createSessionState, getSessionState, updateSessionState, deleteSessionState, getActiveSessionForUser, getActiveSessionsForStatus, getCompletedSessionForProject, clearStalePendingCorrections } from './sessions.js';
4
+ export { createSessionState, getSessionState, updateSessionState, deleteSessionState, getActiveSessionForUser, getActiveSessionsForStatus, getCompletedSessionForProject, clearStalePendingCorrections, getOrphanedSessionCount, repairOrphanedSessions } from './sessions.js';
5
5
  export { createStep, getStepsForSession, getRecentSteps, getValidatedSteps, getKeyDecisions, getEditedFiles, deleteStepsForSession, updateRecentStepsReasoning, updateLastChecked } from './steps.js';
6
6
  export { updateSessionDrift, logDriftEvent } from './drift.js';
7
7
  export { updateTokenCount, updateSessionMode, markWaitingForRecovery, incrementEscalation, markCleared, markSessionCompleted, cleanupOldCompletedSessions, cleanupStaleActiveSessions } from './convenience.js';
@@ -4,7 +4,7 @@ export { initDatabase, closeDatabase, getDatabasePath } from './database.js';
4
4
  // Re-export task functions
5
5
  export { createTask, getTasksForProject, getTaskCount, getUnsyncedTasks, markTaskSynced, setTaskSyncError, getSyncedTaskCount, cleanupOldSyncedTasks, cleanupFailedSyncTasks } from './tasks.js';
6
6
  // Re-export session functions
7
- export { createSessionState, getSessionState, updateSessionState, deleteSessionState, getActiveSessionForUser, getActiveSessionsForStatus, getCompletedSessionForProject, clearStalePendingCorrections } from './sessions.js';
7
+ export { createSessionState, getSessionState, updateSessionState, deleteSessionState, getActiveSessionForUser, getActiveSessionsForStatus, getCompletedSessionForProject, clearStalePendingCorrections, getOrphanedSessionCount, repairOrphanedSessions } from './sessions.js';
8
8
  // Re-export step functions
9
9
  export { createStep, getStepsForSession, getRecentSteps, getValidatedSteps, getKeyDecisions, getEditedFiles, deleteStepsForSession, updateRecentStepsReasoning, updateLastChecked } from './steps.js';
10
10
  // Re-export drift functions
@@ -37,3 +37,11 @@ export declare function getCompletedSessionForProject(projectPath: string): Sess
37
37
  * Returns number of sessions cleared
38
38
  */
39
39
  export declare function clearStalePendingCorrections(): number;
40
+ /**
41
+ * Detect orphaned parent_session_id references
42
+ */
43
+ export declare function getOrphanedSessionCount(): number;
44
+ /**
45
+ * Nullify orphaned parent_session_id references
46
+ */
47
+ export declare function repairOrphanedSessions(): number;
@@ -255,3 +255,28 @@ export function clearStalePendingCorrections() {
255
255
  `).run();
256
256
  return result.changes;
257
257
  }
258
+ /**
259
+ * Detect orphaned parent_session_id references
260
+ */
261
+ export function getOrphanedSessionCount() {
262
+ const database = getDb();
263
+ const result = database.prepare(`
264
+ SELECT COUNT(*) as count FROM session_states
265
+ WHERE parent_session_id IS NOT NULL
266
+ AND parent_session_id NOT IN (SELECT session_id FROM session_states)
267
+ `).get();
268
+ return result.count;
269
+ }
270
+ /**
271
+ * Nullify orphaned parent_session_id references
272
+ */
273
+ export function repairOrphanedSessions() {
274
+ const database = getDb();
275
+ const result = database.prepare(`
276
+ UPDATE session_states
277
+ SET parent_session_id = NULL
278
+ WHERE parent_session_id IS NOT NULL
279
+ AND parent_session_id NOT IN (SELECT session_id FROM session_states)
280
+ `).run();
281
+ return result.changes;
282
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grov",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
4
4
  "description": "Collective AI memory for Coding Agents - captures reasoning from sessions and injects context into future sessions",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",