grov 0.1.2 → 0.2.2
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 +66 -87
- package/dist/cli.js +23 -37
- package/dist/commands/capture.js +1 -1
- package/dist/commands/disable.d.ts +1 -0
- package/dist/commands/disable.js +14 -0
- package/dist/commands/drift-test.js +56 -68
- package/dist/commands/init.js +29 -17
- package/dist/commands/proxy-status.d.ts +1 -0
- package/dist/commands/proxy-status.js +32 -0
- package/dist/commands/unregister.js +7 -1
- package/dist/lib/correction-builder-proxy.d.ts +16 -0
- package/dist/lib/correction-builder-proxy.js +125 -0
- package/dist/lib/correction-builder.js +1 -1
- package/dist/lib/drift-checker-proxy.d.ts +63 -0
- package/dist/lib/drift-checker-proxy.js +373 -0
- package/dist/lib/drift-checker.js +1 -1
- package/dist/lib/hooks.d.ts +11 -0
- package/dist/lib/hooks.js +33 -0
- package/dist/lib/llm-extractor.d.ts +60 -11
- package/dist/lib/llm-extractor.js +419 -98
- package/dist/lib/settings.d.ts +19 -0
- package/dist/lib/settings.js +63 -0
- package/dist/lib/store.d.ts +201 -43
- package/dist/lib/store.js +653 -90
- package/dist/proxy/action-parser.d.ts +58 -0
- package/dist/proxy/action-parser.js +196 -0
- package/dist/proxy/config.d.ts +26 -0
- package/dist/proxy/config.js +67 -0
- package/dist/proxy/forwarder.d.ts +24 -0
- package/dist/proxy/forwarder.js +119 -0
- package/dist/proxy/index.d.ts +1 -0
- package/dist/proxy/index.js +30 -0
- package/dist/proxy/request-processor.d.ts +12 -0
- package/dist/proxy/request-processor.js +94 -0
- package/dist/proxy/response-processor.d.ts +14 -0
- package/dist/proxy/response-processor.js +128 -0
- package/dist/proxy/server.d.ts +9 -0
- package/dist/proxy/server.js +911 -0
- 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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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.
|
|
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.
|
|
409
|
-
setClauses.push('
|
|
410
|
-
params.push(JSON.stringify(updates.
|
|
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.
|
|
413
|
-
setClauses.push('
|
|
414
|
-
params.push(JSON.stringify(updates.
|
|
602
|
+
if (updates.keywords !== undefined) {
|
|
603
|
+
setClauses.push('keywords = ?');
|
|
604
|
+
params.push(JSON.stringify(updates.keywords));
|
|
415
605
|
}
|
|
416
|
-
if (updates.
|
|
417
|
-
setClauses.push('
|
|
418
|
-
params.push(updates.
|
|
606
|
+
if (updates.token_count !== undefined) {
|
|
607
|
+
setClauses.push('token_count = ?');
|
|
608
|
+
params.push(updates.token_count);
|
|
419
609
|
}
|
|
420
|
-
if (updates.
|
|
421
|
-
setClauses.push('
|
|
422
|
-
params.push(
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
? [...
|
|
517
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
822
|
+
const lastEvent = history[history.length - 1];
|
|
569
823
|
return {
|
|
570
|
-
totalEvents:
|
|
824
|
+
totalEvents: history.length,
|
|
571
825
|
resolved: lastEvent.score >= 8,
|
|
572
826
|
finalScore: lastEvent.score,
|
|
573
|
-
hadHalt:
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|