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.
- package/dist/cli/commands/doctor.d.ts +3 -1
- package/dist/cli/commands/doctor.js +31 -1
- package/dist/cli/index.js +15 -5
- package/dist/core/store/convenience.d.ts +1 -0
- package/dist/core/store/convenience.js +33 -18
- package/dist/core/store/database.js +7 -0
- package/dist/core/store/index.d.ts +1 -1
- package/dist/core/store/index.js +1 -1
- package/dist/core/store/sessions.d.ts +8 -0
- package/dist/core/store/sessions.js +25 -0
- package/package.json +1 -1
|
@@ -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
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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';
|
package/dist/core/store/index.js
CHANGED
|
@@ -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