grov 0.2.3 → 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.
- package/README.md +44 -5
- package/dist/cli.js +40 -2
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +115 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +13 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +127 -0
- package/dist/lib/api-client.d.ts +57 -0
- package/dist/lib/api-client.js +174 -0
- package/dist/lib/cloud-sync.d.ts +33 -0
- package/dist/lib/cloud-sync.js +176 -0
- package/dist/lib/credentials.d.ts +53 -0
- package/dist/lib/credentials.js +201 -0
- package/dist/lib/llm-extractor.d.ts +15 -39
- package/dist/lib/llm-extractor.js +400 -418
- package/dist/lib/store/convenience.d.ts +40 -0
- package/dist/lib/store/convenience.js +104 -0
- package/dist/lib/store/database.d.ts +22 -0
- package/dist/lib/store/database.js +375 -0
- package/dist/lib/store/drift.d.ts +9 -0
- package/dist/lib/store/drift.js +89 -0
- package/dist/lib/store/index.d.ts +7 -0
- package/dist/lib/store/index.js +13 -0
- package/dist/lib/store/sessions.d.ts +32 -0
- package/dist/lib/store/sessions.js +240 -0
- package/dist/lib/store/steps.d.ts +40 -0
- package/dist/lib/store/steps.js +161 -0
- package/dist/lib/store/tasks.d.ts +33 -0
- package/dist/lib/store/tasks.js +133 -0
- package/dist/lib/store/types.d.ts +167 -0
- package/dist/lib/store/types.js +2 -0
- package/dist/lib/store.d.ts +1 -406
- package/dist/lib/store.js +2 -1356
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +45 -0
- package/dist/proxy/action-parser.d.ts +10 -2
- package/dist/proxy/action-parser.js +4 -2
- package/dist/proxy/cache.d.ts +36 -0
- package/dist/proxy/cache.js +51 -0
- package/dist/proxy/config.d.ts +1 -0
- package/dist/proxy/config.js +2 -0
- package/dist/proxy/extended-cache.d.ts +10 -0
- package/dist/proxy/extended-cache.js +155 -0
- package/dist/proxy/forwarder.d.ts +7 -1
- package/dist/proxy/forwarder.js +157 -7
- package/dist/proxy/handlers/preprocess.d.ts +20 -0
- package/dist/proxy/handlers/preprocess.js +169 -0
- package/dist/proxy/injection/delta-tracking.d.ts +11 -0
- package/dist/proxy/injection/delta-tracking.js +93 -0
- package/dist/proxy/injection/injectors.d.ts +7 -0
- package/dist/proxy/injection/injectors.js +139 -0
- package/dist/proxy/request-processor.d.ts +18 -3
- package/dist/proxy/request-processor.js +151 -28
- package/dist/proxy/response-processor.js +116 -47
- package/dist/proxy/server.d.ts +4 -1
- package/dist/proxy/server.js +592 -253
- package/dist/proxy/types.d.ts +13 -0
- package/dist/proxy/types.js +2 -0
- package/dist/proxy/utils/extractors.d.ts +18 -0
- package/dist/proxy/utils/extractors.js +109 -0
- package/dist/proxy/utils/logging.d.ts +18 -0
- package/dist/proxy/utils/logging.js +42 -0
- package/package.json +22 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SessionMode } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Update token count for a session
|
|
4
|
+
*/
|
|
5
|
+
export declare function updateTokenCount(sessionId: string, tokenCount: number): void;
|
|
6
|
+
/**
|
|
7
|
+
* Update session mode
|
|
8
|
+
*/
|
|
9
|
+
export declare function updateSessionMode(sessionId: string, mode: SessionMode): void;
|
|
10
|
+
/**
|
|
11
|
+
* Mark session as waiting for recovery
|
|
12
|
+
*/
|
|
13
|
+
export declare function markWaitingForRecovery(sessionId: string, waiting: boolean): void;
|
|
14
|
+
/**
|
|
15
|
+
* Increment escalation count
|
|
16
|
+
*/
|
|
17
|
+
export declare function incrementEscalation(sessionId: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Update last clear timestamp and reset token count
|
|
20
|
+
*/
|
|
21
|
+
export declare function markCleared(sessionId: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Mark session as completed (instead of deleting)
|
|
24
|
+
* Session will be cleaned up after 1 hour
|
|
25
|
+
*/
|
|
26
|
+
export declare function markSessionCompleted(sessionId: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Cleanup sessions completed more than 24 hours ago
|
|
29
|
+
* Also deletes associated steps and drift_log entries
|
|
30
|
+
* Skips sessions that have active children (RESTRICT approach)
|
|
31
|
+
* Returns number of sessions cleaned up
|
|
32
|
+
*/
|
|
33
|
+
export declare function cleanupOldCompletedSessions(maxAgeMs?: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Cleanup stale active sessions (no activity for maxAgeMs)
|
|
36
|
+
* Marks them as 'abandoned' so they won't be picked up by getActiveSessionForUser
|
|
37
|
+
* This prevents old sessions from being reused in fresh Claude sessions
|
|
38
|
+
* Returns number of sessions marked as abandoned
|
|
39
|
+
*/
|
|
40
|
+
export declare function cleanupStaleActiveSessions(maxAgeMs?: number): number;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Convenience wrappers and cleanup functions
|
|
2
|
+
import { getDb } from './database.js';
|
|
3
|
+
import { getSessionState, updateSessionState } from './sessions.js';
|
|
4
|
+
/**
|
|
5
|
+
* Update token count for a session
|
|
6
|
+
*/
|
|
7
|
+
export function updateTokenCount(sessionId, tokenCount) {
|
|
8
|
+
updateSessionState(sessionId, { token_count: tokenCount });
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Update session mode
|
|
12
|
+
*/
|
|
13
|
+
export function updateSessionMode(sessionId, mode) {
|
|
14
|
+
updateSessionState(sessionId, { session_mode: mode });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mark session as waiting for recovery
|
|
18
|
+
*/
|
|
19
|
+
export function markWaitingForRecovery(sessionId, waiting) {
|
|
20
|
+
updateSessionState(sessionId, { waiting_for_recovery: waiting });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Increment escalation count
|
|
24
|
+
*/
|
|
25
|
+
export function incrementEscalation(sessionId) {
|
|
26
|
+
const session = getSessionState(sessionId);
|
|
27
|
+
if (session) {
|
|
28
|
+
updateSessionState(sessionId, { escalation_count: session.escalation_count + 1 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Update last clear timestamp and reset token count
|
|
33
|
+
*/
|
|
34
|
+
export function markCleared(sessionId) {
|
|
35
|
+
updateSessionState(sessionId, {
|
|
36
|
+
last_clear_at: Date.now(),
|
|
37
|
+
token_count: 0
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Mark session as completed (instead of deleting)
|
|
42
|
+
* Session will be cleaned up after 1 hour
|
|
43
|
+
*/
|
|
44
|
+
export function markSessionCompleted(sessionId) {
|
|
45
|
+
const database = getDb();
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
database.prepare(`
|
|
48
|
+
UPDATE session_states
|
|
49
|
+
SET status = 'completed', completed_at = ?, last_update = ?
|
|
50
|
+
WHERE session_id = ?
|
|
51
|
+
`).run(now, now, sessionId);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Cleanup sessions completed more than 24 hours ago
|
|
55
|
+
* Also deletes associated steps and drift_log entries
|
|
56
|
+
* Skips sessions that have active children (RESTRICT approach)
|
|
57
|
+
* Returns number of sessions cleaned up
|
|
58
|
+
*/
|
|
59
|
+
export function cleanupOldCompletedSessions(maxAgeMs = 86400000) {
|
|
60
|
+
const database = getDb();
|
|
61
|
+
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
|
|
62
|
+
// Get sessions to cleanup, excluding those with active children
|
|
63
|
+
const oldSessions = database.prepare(`
|
|
64
|
+
SELECT session_id FROM session_states
|
|
65
|
+
WHERE status = 'completed'
|
|
66
|
+
AND completed_at < ?
|
|
67
|
+
AND session_id NOT IN (
|
|
68
|
+
SELECT DISTINCT parent_session_id
|
|
69
|
+
FROM session_states
|
|
70
|
+
WHERE parent_session_id IS NOT NULL
|
|
71
|
+
AND status != 'completed'
|
|
72
|
+
)
|
|
73
|
+
`).all(cutoff);
|
|
74
|
+
if (oldSessions.length === 0) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
// Delete in correct order to respect FK constraints
|
|
78
|
+
for (const session of oldSessions) {
|
|
79
|
+
// 1. Delete from drift_log (FK to session_states)
|
|
80
|
+
database.prepare('DELETE FROM drift_log WHERE session_id = ?').run(session.session_id);
|
|
81
|
+
// 2. Delete from steps (FK to session_states)
|
|
82
|
+
database.prepare('DELETE FROM steps WHERE session_id = ?').run(session.session_id);
|
|
83
|
+
// 3. Now safe to delete session_states
|
|
84
|
+
database.prepare('DELETE FROM session_states WHERE session_id = ?').run(session.session_id);
|
|
85
|
+
}
|
|
86
|
+
return oldSessions.length;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup stale active sessions (no activity for maxAgeMs)
|
|
90
|
+
* Marks them as 'abandoned' so they won't be picked up by getActiveSessionForUser
|
|
91
|
+
* This prevents old sessions from being reused in fresh Claude sessions
|
|
92
|
+
* Returns number of sessions marked as abandoned
|
|
93
|
+
*/
|
|
94
|
+
export function cleanupStaleActiveSessions(maxAgeMs = 3600000) {
|
|
95
|
+
const database = getDb();
|
|
96
|
+
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
|
|
97
|
+
const now = new Date().toISOString();
|
|
98
|
+
const result = database.prepare(`
|
|
99
|
+
UPDATE session_states
|
|
100
|
+
SET status = 'abandoned', completed_at = ?
|
|
101
|
+
WHERE status = 'active' AND last_update < ?
|
|
102
|
+
`).run(now, cutoff);
|
|
103
|
+
return result.changes;
|
|
104
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Safely parse JSON with fallback to provided default.
|
|
4
|
+
*/
|
|
5
|
+
export declare function safeJsonParse<T>(value: unknown, fallback: T): T;
|
|
6
|
+
/**
|
|
7
|
+
* Get the database path
|
|
8
|
+
*/
|
|
9
|
+
export declare function getDatabasePath(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Get initialized database connection.
|
|
12
|
+
* Internal helper for other store modules.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getDb(): Database.Database;
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the database connection and create tables
|
|
17
|
+
*/
|
|
18
|
+
export declare function initDatabase(): Database.Database;
|
|
19
|
+
/**
|
|
20
|
+
* Close the database connection
|
|
21
|
+
*/
|
|
22
|
+
export declare function closeDatabase(): void;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// Database connection, schema, and utilities
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { existsSync, mkdirSync, chmodSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
const GROV_DIR = join(homedir(), '.grov');
|
|
7
|
+
const DB_PATH = join(GROV_DIR, 'memory.db');
|
|
8
|
+
let db = null;
|
|
9
|
+
/**
|
|
10
|
+
* Escape LIKE pattern special characters to prevent SQL injection.
|
|
11
|
+
* SECURITY: Prevents wildcard injection in LIKE queries.
|
|
12
|
+
*/
|
|
13
|
+
function escapeLikePattern(str) {
|
|
14
|
+
return str.replace(/[%_\\]/g, '\\$&');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Safely parse JSON with fallback to provided default.
|
|
18
|
+
*/
|
|
19
|
+
export function safeJsonParse(value, fallback) {
|
|
20
|
+
if (typeof value !== 'string' || !value) {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the database path
|
|
32
|
+
*/
|
|
33
|
+
export function getDatabasePath() {
|
|
34
|
+
return DB_PATH;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get initialized database connection.
|
|
38
|
+
* Internal helper for other store modules.
|
|
39
|
+
*/
|
|
40
|
+
export function getDb() {
|
|
41
|
+
return initDatabase();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the database connection and create tables
|
|
45
|
+
*/
|
|
46
|
+
export function initDatabase() {
|
|
47
|
+
if (db)
|
|
48
|
+
return db;
|
|
49
|
+
// Ensure .grov directory exists with secure permissions
|
|
50
|
+
if (!existsSync(GROV_DIR)) {
|
|
51
|
+
mkdirSync(GROV_DIR, { recursive: true, mode: 0o700 });
|
|
52
|
+
}
|
|
53
|
+
db = new Database(DB_PATH);
|
|
54
|
+
// Set secure file permissions on the database
|
|
55
|
+
try {
|
|
56
|
+
chmodSync(DB_PATH, 0o600);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.warn('Warning: Could not set restrictive permissions on ~/.grov/memory.db');
|
|
60
|
+
console.warn('Please ensure the file has appropriate permissions for your system.');
|
|
61
|
+
}
|
|
62
|
+
// OPTIMIZATION: Enable WAL mode for better concurrent performance
|
|
63
|
+
db.pragma('journal_mode = WAL');
|
|
64
|
+
// Create all tables in a single transaction for efficiency
|
|
65
|
+
db.exec(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
project_path TEXT NOT NULL,
|
|
69
|
+
user TEXT,
|
|
70
|
+
original_query TEXT NOT NULL,
|
|
71
|
+
goal TEXT,
|
|
72
|
+
reasoning_trace JSON DEFAULT '[]',
|
|
73
|
+
files_touched JSON DEFAULT '[]',
|
|
74
|
+
decisions JSON DEFAULT '[]',
|
|
75
|
+
constraints JSON DEFAULT '[]',
|
|
76
|
+
status TEXT NOT NULL CHECK(status IN ('complete', 'question', 'partial', 'abandoned')),
|
|
77
|
+
trigger_reason TEXT CHECK(trigger_reason IN ('complete', 'threshold', 'abandoned')),
|
|
78
|
+
linked_commit TEXT,
|
|
79
|
+
parent_task_id TEXT,
|
|
80
|
+
turn_number INTEGER,
|
|
81
|
+
tags JSON DEFAULT '[]',
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
synced_at TEXT,
|
|
84
|
+
sync_error TEXT,
|
|
85
|
+
FOREIGN KEY (parent_task_id) REFERENCES tasks(id)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_project ON tasks(project_path);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_status ON tasks(status);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at);
|
|
91
|
+
`);
|
|
92
|
+
// Migration: add new columns to existing tasks table
|
|
93
|
+
try {
|
|
94
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN decisions JSON DEFAULT '[]'`);
|
|
95
|
+
}
|
|
96
|
+
catch { /* column exists */ }
|
|
97
|
+
try {
|
|
98
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN constraints JSON DEFAULT '[]'`);
|
|
99
|
+
}
|
|
100
|
+
catch { /* column exists */ }
|
|
101
|
+
try {
|
|
102
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN trigger_reason TEXT`);
|
|
103
|
+
}
|
|
104
|
+
catch { /* column exists */ }
|
|
105
|
+
try {
|
|
106
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN synced_at TEXT`);
|
|
107
|
+
}
|
|
108
|
+
catch { /* column exists */ }
|
|
109
|
+
try {
|
|
110
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN sync_error TEXT`);
|
|
111
|
+
}
|
|
112
|
+
catch { /* column exists */ }
|
|
113
|
+
// Create session_states table (temporary per-session tracking)
|
|
114
|
+
db.exec(`
|
|
115
|
+
CREATE TABLE IF NOT EXISTS session_states (
|
|
116
|
+
session_id TEXT PRIMARY KEY,
|
|
117
|
+
user_id TEXT,
|
|
118
|
+
project_path TEXT NOT NULL,
|
|
119
|
+
original_goal TEXT,
|
|
120
|
+
expected_scope JSON DEFAULT '[]',
|
|
121
|
+
constraints JSON DEFAULT '[]',
|
|
122
|
+
keywords JSON DEFAULT '[]',
|
|
123
|
+
token_count INTEGER DEFAULT 0,
|
|
124
|
+
escalation_count INTEGER DEFAULT 0,
|
|
125
|
+
session_mode TEXT DEFAULT 'normal' CHECK(session_mode IN ('normal', 'drifted', 'forced')),
|
|
126
|
+
waiting_for_recovery INTEGER DEFAULT 0,
|
|
127
|
+
last_checked_at INTEGER DEFAULT 0,
|
|
128
|
+
last_clear_at INTEGER,
|
|
129
|
+
start_time TEXT NOT NULL,
|
|
130
|
+
last_update TEXT NOT NULL,
|
|
131
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned')),
|
|
132
|
+
completed_at TEXT,
|
|
133
|
+
parent_session_id TEXT,
|
|
134
|
+
task_type TEXT DEFAULT 'main' CHECK(task_type IN ('main', 'subtask', 'parallel')),
|
|
135
|
+
pending_correction TEXT,
|
|
136
|
+
FOREIGN KEY (parent_session_id) REFERENCES session_states(session_id)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_session_project ON session_states(project_path);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_session_status ON session_states(status);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_session_parent ON session_states(parent_session_id);
|
|
142
|
+
`);
|
|
143
|
+
// Migration: add new columns to existing session_states table
|
|
144
|
+
try {
|
|
145
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN expected_scope JSON DEFAULT '[]'`);
|
|
146
|
+
}
|
|
147
|
+
catch { /* column exists */ }
|
|
148
|
+
try {
|
|
149
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN constraints JSON DEFAULT '[]'`);
|
|
150
|
+
}
|
|
151
|
+
catch { /* column exists */ }
|
|
152
|
+
try {
|
|
153
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN keywords JSON DEFAULT '[]'`);
|
|
154
|
+
}
|
|
155
|
+
catch { /* column exists */ }
|
|
156
|
+
try {
|
|
157
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN token_count INTEGER DEFAULT 0`);
|
|
158
|
+
}
|
|
159
|
+
catch { /* column exists */ }
|
|
160
|
+
try {
|
|
161
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN escalation_count INTEGER DEFAULT 0`);
|
|
162
|
+
}
|
|
163
|
+
catch { /* column exists */ }
|
|
164
|
+
try {
|
|
165
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN session_mode TEXT DEFAULT 'normal'`);
|
|
166
|
+
}
|
|
167
|
+
catch { /* column exists */ }
|
|
168
|
+
try {
|
|
169
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN waiting_for_recovery INTEGER DEFAULT 0`);
|
|
170
|
+
}
|
|
171
|
+
catch { /* column exists */ }
|
|
172
|
+
try {
|
|
173
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
|
|
174
|
+
}
|
|
175
|
+
catch { /* column exists */ }
|
|
176
|
+
try {
|
|
177
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN last_clear_at INTEGER`);
|
|
178
|
+
}
|
|
179
|
+
catch { /* column exists */ }
|
|
180
|
+
try {
|
|
181
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN parent_session_id TEXT`);
|
|
182
|
+
}
|
|
183
|
+
catch { /* column exists */ }
|
|
184
|
+
try {
|
|
185
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN task_type TEXT DEFAULT 'main'`);
|
|
186
|
+
}
|
|
187
|
+
catch { /* column exists */ }
|
|
188
|
+
try {
|
|
189
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN completed_at TEXT`);
|
|
190
|
+
}
|
|
191
|
+
catch { /* column exists */ }
|
|
192
|
+
try {
|
|
193
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_parent ON session_states(parent_session_id)`);
|
|
194
|
+
}
|
|
195
|
+
catch { /* index exists */ }
|
|
196
|
+
try {
|
|
197
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_completed ON session_states(completed_at)`);
|
|
198
|
+
}
|
|
199
|
+
catch { /* index exists */ }
|
|
200
|
+
// Create file_reasoning table (file-level reasoning with anchoring)
|
|
201
|
+
db.exec(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS file_reasoning (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
task_id TEXT,
|
|
205
|
+
file_path TEXT NOT NULL,
|
|
206
|
+
anchor TEXT,
|
|
207
|
+
line_start INTEGER,
|
|
208
|
+
line_end INTEGER,
|
|
209
|
+
code_hash TEXT,
|
|
210
|
+
change_type TEXT CHECK(change_type IN ('read', 'write', 'edit', 'create', 'delete')),
|
|
211
|
+
reasoning TEXT NOT NULL,
|
|
212
|
+
created_at TEXT NOT NULL,
|
|
213
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
CREATE INDEX IF NOT EXISTS idx_file_task ON file_reasoning(task_id);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_file_path ON file_reasoning(file_path);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_file_path_created ON file_reasoning(file_path, created_at DESC);
|
|
219
|
+
`);
|
|
220
|
+
// Migration: Add drift detection columns to session_states
|
|
221
|
+
const columns = db.pragma('table_info(session_states)');
|
|
222
|
+
const existingColumns = new Set(columns.map(c => c.name));
|
|
223
|
+
if (!existingColumns.has('expected_scope')) {
|
|
224
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN expected_scope JSON DEFAULT '[]'`);
|
|
225
|
+
}
|
|
226
|
+
if (!existingColumns.has('constraints')) {
|
|
227
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN constraints JSON DEFAULT '[]'`);
|
|
228
|
+
}
|
|
229
|
+
if (!existingColumns.has('keywords')) {
|
|
230
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN keywords JSON DEFAULT '[]'`);
|
|
231
|
+
}
|
|
232
|
+
if (!existingColumns.has('escalation_count')) {
|
|
233
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN escalation_count INTEGER DEFAULT 0`);
|
|
234
|
+
}
|
|
235
|
+
if (!existingColumns.has('last_checked_at')) {
|
|
236
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN last_checked_at INTEGER DEFAULT 0`);
|
|
237
|
+
}
|
|
238
|
+
if (!existingColumns.has('success_criteria')) {
|
|
239
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN success_criteria JSON DEFAULT '[]'`);
|
|
240
|
+
}
|
|
241
|
+
if (!existingColumns.has('last_drift_score')) {
|
|
242
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN last_drift_score INTEGER`);
|
|
243
|
+
}
|
|
244
|
+
if (!existingColumns.has('pending_recovery_plan')) {
|
|
245
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN pending_recovery_plan JSON`);
|
|
246
|
+
}
|
|
247
|
+
if (!existingColumns.has('drift_history')) {
|
|
248
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN drift_history JSON DEFAULT '[]'`);
|
|
249
|
+
}
|
|
250
|
+
if (!existingColumns.has('token_count')) {
|
|
251
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN token_count INTEGER DEFAULT 0`);
|
|
252
|
+
}
|
|
253
|
+
if (!existingColumns.has('session_mode')) {
|
|
254
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN session_mode TEXT DEFAULT 'normal'`);
|
|
255
|
+
}
|
|
256
|
+
if (!existingColumns.has('waiting_for_recovery')) {
|
|
257
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN waiting_for_recovery INTEGER DEFAULT 0`);
|
|
258
|
+
}
|
|
259
|
+
if (!existingColumns.has('last_clear_at')) {
|
|
260
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN last_clear_at INTEGER`);
|
|
261
|
+
}
|
|
262
|
+
if (!existingColumns.has('completed_at')) {
|
|
263
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN completed_at TEXT`);
|
|
264
|
+
}
|
|
265
|
+
if (!existingColumns.has('parent_session_id')) {
|
|
266
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN parent_session_id TEXT`);
|
|
267
|
+
}
|
|
268
|
+
if (!existingColumns.has('task_type')) {
|
|
269
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN task_type TEXT DEFAULT 'main'`);
|
|
270
|
+
}
|
|
271
|
+
if (!existingColumns.has('actions_taken')) {
|
|
272
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN actions_taken JSON DEFAULT '[]'`);
|
|
273
|
+
}
|
|
274
|
+
if (!existingColumns.has('files_explored')) {
|
|
275
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN files_explored JSON DEFAULT '[]'`);
|
|
276
|
+
}
|
|
277
|
+
if (!existingColumns.has('current_intent')) {
|
|
278
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN current_intent TEXT`);
|
|
279
|
+
}
|
|
280
|
+
if (!existingColumns.has('drift_warnings')) {
|
|
281
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN drift_warnings JSON DEFAULT '[]'`);
|
|
282
|
+
}
|
|
283
|
+
if (!existingColumns.has('pending_correction')) {
|
|
284
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN pending_correction TEXT`);
|
|
285
|
+
}
|
|
286
|
+
if (!existingColumns.has('pending_clear_summary')) {
|
|
287
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN pending_clear_summary TEXT`);
|
|
288
|
+
}
|
|
289
|
+
if (!existingColumns.has('pending_forced_recovery')) {
|
|
290
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN pending_forced_recovery TEXT`);
|
|
291
|
+
}
|
|
292
|
+
if (!existingColumns.has('final_response')) {
|
|
293
|
+
db.exec(`ALTER TABLE session_states ADD COLUMN final_response TEXT`);
|
|
294
|
+
}
|
|
295
|
+
// Create steps table (action log for current session)
|
|
296
|
+
db.exec(`
|
|
297
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
298
|
+
id TEXT PRIMARY KEY,
|
|
299
|
+
session_id TEXT NOT NULL,
|
|
300
|
+
action_type TEXT NOT NULL CHECK(action_type IN ('edit', 'write', 'bash', 'read', 'glob', 'grep', 'task', 'other')),
|
|
301
|
+
files JSON DEFAULT '[]',
|
|
302
|
+
folders JSON DEFAULT '[]',
|
|
303
|
+
command TEXT,
|
|
304
|
+
reasoning TEXT,
|
|
305
|
+
drift_score INTEGER,
|
|
306
|
+
drift_type TEXT CHECK(drift_type IN ('none', 'minor', 'major', 'critical')),
|
|
307
|
+
is_key_decision INTEGER DEFAULT 0,
|
|
308
|
+
is_validated INTEGER DEFAULT 1,
|
|
309
|
+
correction_given TEXT,
|
|
310
|
+
correction_level TEXT CHECK(correction_level IN ('nudge', 'correct', 'intervene', 'halt')),
|
|
311
|
+
keywords JSON DEFAULT '[]',
|
|
312
|
+
timestamp INTEGER NOT NULL,
|
|
313
|
+
FOREIGN KEY (session_id) REFERENCES session_states(session_id)
|
|
314
|
+
);
|
|
315
|
+
CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_steps_timestamp ON steps(timestamp);
|
|
317
|
+
`);
|
|
318
|
+
// Migration: add new columns to existing steps table
|
|
319
|
+
try {
|
|
320
|
+
db.exec(`ALTER TABLE steps ADD COLUMN drift_type TEXT`);
|
|
321
|
+
}
|
|
322
|
+
catch { /* column exists */ }
|
|
323
|
+
try {
|
|
324
|
+
db.exec(`ALTER TABLE steps ADD COLUMN is_key_decision INTEGER DEFAULT 0`);
|
|
325
|
+
}
|
|
326
|
+
catch { /* column exists */ }
|
|
327
|
+
try {
|
|
328
|
+
db.exec(`ALTER TABLE steps ADD COLUMN is_validated INTEGER DEFAULT 1`);
|
|
329
|
+
}
|
|
330
|
+
catch { /* column exists */ }
|
|
331
|
+
try {
|
|
332
|
+
db.exec(`ALTER TABLE steps ADD COLUMN correction_given TEXT`);
|
|
333
|
+
}
|
|
334
|
+
catch { /* column exists */ }
|
|
335
|
+
try {
|
|
336
|
+
db.exec(`ALTER TABLE steps ADD COLUMN correction_level TEXT`);
|
|
337
|
+
}
|
|
338
|
+
catch { /* column exists */ }
|
|
339
|
+
try {
|
|
340
|
+
db.exec(`ALTER TABLE steps ADD COLUMN keywords JSON DEFAULT '[]'`);
|
|
341
|
+
}
|
|
342
|
+
catch { /* column exists */ }
|
|
343
|
+
try {
|
|
344
|
+
db.exec(`ALTER TABLE steps ADD COLUMN reasoning TEXT`);
|
|
345
|
+
}
|
|
346
|
+
catch { /* column exists */ }
|
|
347
|
+
// Create drift_log table (rejected actions for audit)
|
|
348
|
+
db.exec(`
|
|
349
|
+
CREATE TABLE IF NOT EXISTS drift_log (
|
|
350
|
+
id TEXT PRIMARY KEY,
|
|
351
|
+
session_id TEXT NOT NULL,
|
|
352
|
+
timestamp INTEGER NOT NULL,
|
|
353
|
+
action_type TEXT,
|
|
354
|
+
files JSON DEFAULT '[]',
|
|
355
|
+
drift_score INTEGER NOT NULL,
|
|
356
|
+
drift_reason TEXT,
|
|
357
|
+
correction_given TEXT,
|
|
358
|
+
recovery_plan JSON,
|
|
359
|
+
FOREIGN KEY (session_id) REFERENCES session_states(session_id)
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_drift_log_session ON drift_log(session_id);
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_drift_log_timestamp ON drift_log(timestamp);
|
|
364
|
+
`);
|
|
365
|
+
return db;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Close the database connection
|
|
369
|
+
*/
|
|
370
|
+
export function closeDatabase() {
|
|
371
|
+
if (db) {
|
|
372
|
+
db.close();
|
|
373
|
+
db = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DriftLogEntry, CreateDriftLogInput, CorrectionLevel, RecoveryPlan } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Update session drift metrics after a prompt check
|
|
4
|
+
*/
|
|
5
|
+
export declare function updateSessionDrift(sessionId: string, driftScore: number, correctionLevel: CorrectionLevel | null, promptSummary: string, recoveryPlan?: RecoveryPlan): void;
|
|
6
|
+
/**
|
|
7
|
+
* Log a drift event (for rejected actions)
|
|
8
|
+
*/
|
|
9
|
+
export declare function logDriftEvent(input: CreateDriftLogInput): DriftLogEntry;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Drift detection and drift log operations
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { getDb, safeJsonParse } from './database.js';
|
|
4
|
+
import { getSessionState } from './sessions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Convert database row to DriftLogEntry object
|
|
7
|
+
*/
|
|
8
|
+
function rowToDriftLogEntry(row) {
|
|
9
|
+
return {
|
|
10
|
+
id: row.id,
|
|
11
|
+
session_id: row.session_id,
|
|
12
|
+
timestamp: row.timestamp,
|
|
13
|
+
action_type: row.action_type,
|
|
14
|
+
files: safeJsonParse(row.files, []),
|
|
15
|
+
drift_score: row.drift_score,
|
|
16
|
+
drift_reason: row.drift_reason,
|
|
17
|
+
correction_given: row.correction_given,
|
|
18
|
+
recovery_plan: row.recovery_plan ? safeJsonParse(row.recovery_plan, {}) : undefined
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Update session drift metrics after a prompt check
|
|
23
|
+
*/
|
|
24
|
+
export function updateSessionDrift(sessionId, driftScore, correctionLevel, promptSummary, recoveryPlan) {
|
|
25
|
+
const database = getDb();
|
|
26
|
+
const session = getSessionState(sessionId);
|
|
27
|
+
if (!session)
|
|
28
|
+
return;
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
// Calculate new escalation count
|
|
31
|
+
let newEscalation = session.escalation_count;
|
|
32
|
+
if (driftScore >= 8) {
|
|
33
|
+
// Recovery - decrease escalation
|
|
34
|
+
newEscalation = Math.max(0, newEscalation - 1);
|
|
35
|
+
}
|
|
36
|
+
else if (correctionLevel && correctionLevel !== 'nudge') {
|
|
37
|
+
// Significant drift - increase escalation
|
|
38
|
+
newEscalation = Math.min(3, newEscalation + 1);
|
|
39
|
+
}
|
|
40
|
+
// Add to drift history
|
|
41
|
+
const driftEvent = {
|
|
42
|
+
timestamp: now,
|
|
43
|
+
score: driftScore,
|
|
44
|
+
level: correctionLevel || 'none',
|
|
45
|
+
prompt_summary: promptSummary.substring(0, 100)
|
|
46
|
+
};
|
|
47
|
+
const newHistory = [...(session.drift_history || []), driftEvent];
|
|
48
|
+
// Add to drift_warnings if correction was given
|
|
49
|
+
const currentWarnings = session.drift_warnings || [];
|
|
50
|
+
const newWarnings = correctionLevel
|
|
51
|
+
? [...currentWarnings, `[${now}] ${correctionLevel}: score ${driftScore}`]
|
|
52
|
+
: currentWarnings;
|
|
53
|
+
const stmt = database.prepare(`
|
|
54
|
+
UPDATE session_states SET
|
|
55
|
+
last_drift_score = ?,
|
|
56
|
+
escalation_count = ?,
|
|
57
|
+
pending_recovery_plan = ?,
|
|
58
|
+
drift_history = ?,
|
|
59
|
+
drift_warnings = ?,
|
|
60
|
+
last_update = ?
|
|
61
|
+
WHERE session_id = ?
|
|
62
|
+
`);
|
|
63
|
+
stmt.run(driftScore, newEscalation, recoveryPlan ? JSON.stringify(recoveryPlan) : null, JSON.stringify(newHistory), JSON.stringify(newWarnings), now, sessionId);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Log a drift event (for rejected actions)
|
|
67
|
+
*/
|
|
68
|
+
export function logDriftEvent(input) {
|
|
69
|
+
const database = getDb();
|
|
70
|
+
const entry = {
|
|
71
|
+
id: randomUUID(),
|
|
72
|
+
session_id: input.session_id,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
action_type: input.action_type,
|
|
75
|
+
files: input.files || [],
|
|
76
|
+
drift_score: input.drift_score,
|
|
77
|
+
drift_reason: input.drift_reason,
|
|
78
|
+
correction_given: input.correction_given,
|
|
79
|
+
recovery_plan: input.recovery_plan
|
|
80
|
+
};
|
|
81
|
+
const stmt = database.prepare(`
|
|
82
|
+
INSERT INTO drift_log (
|
|
83
|
+
id, session_id, timestamp, action_type, files,
|
|
84
|
+
drift_score, drift_reason, correction_given, recovery_plan
|
|
85
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
`);
|
|
87
|
+
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);
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
export { initDatabase, closeDatabase, getDatabasePath } from './database.js';
|
|
3
|
+
export { createTask, getTasksForProject, getTaskCount, getUnsyncedTasks, markTaskSynced, setTaskSyncError } from './tasks.js';
|
|
4
|
+
export { createSessionState, getSessionState, updateSessionState, deleteSessionState, getActiveSessionForUser, getActiveSessionsForStatus, getCompletedSessionForProject } from './sessions.js';
|
|
5
|
+
export { createStep, getStepsForSession, getRecentSteps, getValidatedSteps, getKeyDecisions, getEditedFiles, deleteStepsForSession, updateRecentStepsReasoning, updateLastChecked } from './steps.js';
|
|
6
|
+
export { updateSessionDrift, logDriftEvent } from './drift.js';
|
|
7
|
+
export { updateTokenCount, updateSessionMode, markWaitingForRecovery, incrementEscalation, markCleared, markSessionCompleted, cleanupOldCompletedSessions, cleanupStaleActiveSessions } from './convenience.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Store module - barrel exports for backward compatibility
|
|
2
|
+
// Re-export database functions
|
|
3
|
+
export { initDatabase, closeDatabase, getDatabasePath } from './database.js';
|
|
4
|
+
// Re-export task functions
|
|
5
|
+
export { createTask, getTasksForProject, getTaskCount, getUnsyncedTasks, markTaskSynced, setTaskSyncError } from './tasks.js';
|
|
6
|
+
// Re-export session functions
|
|
7
|
+
export { createSessionState, getSessionState, updateSessionState, deleteSessionState, getActiveSessionForUser, getActiveSessionsForStatus, getCompletedSessionForProject } from './sessions.js';
|
|
8
|
+
// Re-export step functions
|
|
9
|
+
export { createStep, getStepsForSession, getRecentSteps, getValidatedSteps, getKeyDecisions, getEditedFiles, deleteStepsForSession, updateRecentStepsReasoning, updateLastChecked } from './steps.js';
|
|
10
|
+
// Re-export drift functions
|
|
11
|
+
export { updateSessionDrift, logDriftEvent } from './drift.js';
|
|
12
|
+
// Re-export convenience functions
|
|
13
|
+
export { updateTokenCount, updateSessionMode, markWaitingForRecovery, incrementEscalation, markCleared, markSessionCompleted, cleanupOldCompletedSessions, cleanupStaleActiveSessions } from './convenience.js';
|