@devflow-tools/database 0.15.0 → 0.16.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/dist/database.js CHANGED
@@ -1,9 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DevFlowDatabase = void 0;
4
+ exports.getGlobalDevFlowDbPath = getGlobalDevFlowDbPath;
5
+ exports.openGlobalDevFlowDatabase = openGlobalDevFlowDatabase;
4
6
  const node_sqlite_1 = require("./node-sqlite");
5
7
  const path_1 = require("path");
6
8
  const fs_1 = require("fs");
9
+ const os_1 = require("os");
10
+ function getGlobalDevFlowDbPath(home = (0, os_1.homedir)()) {
11
+ const stateDir = process.env.DEVFLOW_STATE_DIR ?? (0, path_1.join)(home, '.devflow', 'global');
12
+ return (0, path_1.join)(stateDir, 'devflow.db');
13
+ }
14
+ function openGlobalDevFlowDatabase(home = (0, os_1.homedir)(), options) {
15
+ return new DevFlowDatabase(home, {
16
+ dbPath: getGlobalDevFlowDbPath(home),
17
+ busyTimeoutMs: options?.busyTimeoutMs,
18
+ });
19
+ }
7
20
  class DevFlowDatabase {
8
21
  constructor(projectRoot, opts) {
9
22
  let dbPath;
@@ -19,9 +32,9 @@ class DevFlowDatabase {
19
32
  (0, fs_1.mkdirSync)(devflowDir, { recursive: true });
20
33
  dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
21
34
  }
22
- this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath);
35
+ this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
23
36
  this.db.exec('PRAGMA journal_mode = WAL');
24
- this.db.exec('PRAGMA busy_timeout = 5000');
37
+ this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
25
38
  this.db.exec('PRAGMA foreign_keys = OFF');
26
39
  this.initializeSchema();
27
40
  }
@@ -123,7 +136,9 @@ class DevFlowDatabase {
123
136
  subagent_id TEXT,
124
137
  error TEXT,
125
138
  blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
126
- block_reason TEXT
139
+ block_reason TEXT,
140
+ failure_category TEXT,
141
+ tool_use_id TEXT
127
142
  );
128
143
 
129
144
  CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
@@ -146,11 +161,99 @@ class DevFlowDatabase {
146
161
  mcp_tool_calls INTEGER DEFAULT 0,
147
162
  direct_tool_calls INTEGER DEFAULT 0,
148
163
  subagent_count INTEGER DEFAULT 0,
149
- status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
164
+ status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
165
+ metadata TEXT
150
166
  );
151
167
 
152
168
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
153
169
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
170
+
171
+ CREATE TABLE IF NOT EXISTS feedback (
172
+ id TEXT PRIMARY KEY,
173
+ project_root TEXT NOT NULL,
174
+ query TEXT NOT NULL,
175
+ rating TEXT NOT NULL CHECK (rating IN ('hit', 'partial', 'miss')),
176
+ task_type TEXT,
177
+ context_mode TEXT,
178
+ run_id TEXT,
179
+ token_count INTEGER DEFAULT 0,
180
+ created_at INTEGER NOT NULL
181
+ );
182
+
183
+ CREATE INDEX IF NOT EXISTS idx_feedback_project ON feedback(project_root);
184
+ CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at DESC);
185
+
186
+ CREATE TABLE IF NOT EXISTS telemetry_failures (
187
+ id TEXT PRIMARY KEY,
188
+ operation TEXT NOT NULL,
189
+ payload TEXT,
190
+ error TEXT NOT NULL,
191
+ created_at INTEGER NOT NULL,
192
+ resolved_at INTEGER
193
+ );
194
+
195
+ CREATE INDEX IF NOT EXISTS idx_telemetry_failures_created ON telemetry_failures(created_at DESC);
196
+ CREATE INDEX IF NOT EXISTS idx_telemetry_failures_unresolved ON telemetry_failures(resolved_at) WHERE resolved_at IS NULL;
197
+
198
+ CREATE TABLE IF NOT EXISTS devflow_rules (
199
+ id TEXT PRIMARY KEY,
200
+ name TEXT NOT NULL,
201
+ level TEXT NOT NULL DEFAULT 'project',
202
+ gate INTEGER NOT NULL DEFAULT 1,
203
+ condition_json TEXT NOT NULL,
204
+ action TEXT NOT NULL DEFAULT 'warn',
205
+ message TEXT NOT NULL,
206
+ enabled INTEGER NOT NULL DEFAULT 1,
207
+ created_at INTEGER NOT NULL,
208
+ updated_at INTEGER NOT NULL
209
+ );
210
+
211
+ CREATE TABLE IF NOT EXISTS devflow_audit_chain (
212
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
213
+ ts INTEGER NOT NULL,
214
+ tool TEXT NOT NULL,
215
+ args_hash TEXT NOT NULL,
216
+ decision TEXT NOT NULL,
217
+ rule_id TEXT,
218
+ session_id TEXT NOT NULL,
219
+ prev_hash TEXT NOT NULL,
220
+ signature TEXT NOT NULL
221
+ );
222
+
223
+ CREATE INDEX IF NOT EXISTS idx_devflow_audit_ts ON devflow_audit_chain(ts DESC);
224
+
225
+ CREATE TABLE IF NOT EXISTS devflow_hook_receipts (
226
+ project_root TEXT PRIMARY KEY,
227
+ last_mcp_call INTEGER,
228
+ bypass_count INTEGER NOT NULL DEFAULT 0,
229
+ updated_at INTEGER NOT NULL
230
+ );
231
+
232
+ CREATE TABLE IF NOT EXISTS devflow_context_receipts (
233
+ project_root TEXT NOT NULL,
234
+ session_id TEXT NOT NULL,
235
+ execution_id TEXT NOT NULL,
236
+ context_hash TEXT NOT NULL,
237
+ issued_at INTEGER NOT NULL,
238
+ expires_at INTEGER NOT NULL,
239
+ PRIMARY KEY (project_root, session_id, execution_id)
240
+ );
241
+
242
+ CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
243
+ ON devflow_context_receipts(expires_at);
244
+
245
+ CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
246
+ id TEXT PRIMARY KEY,
247
+ project_root TEXT NOT NULL,
248
+ session_id TEXT,
249
+ trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
250
+ pending_events INTEGER NOT NULL DEFAULT 0,
251
+ released_leases INTEGER NOT NULL DEFAULT 0,
252
+ created_at INTEGER NOT NULL
253
+ );
254
+
255
+ CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
256
+ ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
154
257
  `);
155
258
  // Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
156
259
  try {
@@ -177,6 +280,37 @@ class DevFlowDatabase {
177
280
  this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT');
178
281
  }
179
282
  catch { }
283
+ try {
284
+ this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT');
285
+ }
286
+ catch { }
287
+ try {
288
+ this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT');
289
+ }
290
+ catch { }
291
+ try {
292
+ this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT');
293
+ }
294
+ catch { }
295
+ try {
296
+ this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT');
297
+ }
298
+ catch { }
299
+ try {
300
+ this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
301
+ ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
302
+ }
303
+ catch { }
304
+ try {
305
+ this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1');
306
+ }
307
+ catch { }
308
+ try {
309
+ this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER');
310
+ }
311
+ catch { }
312
+ this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
313
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
180
314
  // Migration: tool_metrics table for per-tool metrics collection
181
315
  try {
182
316
  this.db.exec(`
@@ -221,6 +355,28 @@ class DevFlowDatabase {
221
355
  CREATE INDEX IF NOT EXISTS idx_accuracy_queries_engine ON accuracy_queries(engine);
222
356
  CREATE INDEX IF NOT EXISTS idx_accuracy_queries_session ON accuracy_queries(session_id);
223
357
  CREATE INDEX IF NOT EXISTS idx_accuracy_queries_feedback ON accuracy_queries(relevance_feedback);
358
+ `);
359
+ }
360
+ catch { }
361
+ // Canonical benchmark reports live in the global DevFlow database.
362
+ try {
363
+ this.db.exec(`
364
+ CREATE TABLE IF NOT EXISTS benchmark_reports (
365
+ run_id TEXT PRIMARY KEY,
366
+ suite_id TEXT NOT NULL,
367
+ suite_version TEXT NOT NULL,
368
+ project_root TEXT NOT NULL,
369
+ commit_sha TEXT,
370
+ task_count INTEGER NOT NULL,
371
+ status TEXT NOT NULL CHECK(status IN ('completed')),
372
+ report_json TEXT NOT NULL,
373
+ created_at INTEGER NOT NULL
374
+ );
375
+
376
+ CREATE INDEX IF NOT EXISTS idx_benchmark_reports_created
377
+ ON benchmark_reports(created_at DESC);
378
+ CREATE INDEX IF NOT EXISTS idx_benchmark_reports_suite
379
+ ON benchmark_reports(suite_id, suite_version, created_at DESC);
224
380
  `);
225
381
  }
226
382
  catch { }
@@ -272,7 +428,7 @@ class DevFlowDatabase {
272
428
  }
273
429
  insertRun(run) {
274
430
  this.db.prepare(`
275
- INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
431
+ INSERT OR IGNORE INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
276
432
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
277
433
  `).run(run.id, run.source, run.tool, JSON.stringify(run.input) ?? null, run.status, run.startedAt, run.finishedAt ?? null, run.tokenUsed ?? 0, JSON.stringify(run.metadata) ?? null, Date.now());
278
434
  }
@@ -383,6 +539,22 @@ class DevFlowDatabase {
383
539
  VALUES (?, ?, ?, ?, 'running')
384
540
  `).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
385
541
  }
542
+ ensureSession(params) {
543
+ if (!params.id.trim())
544
+ throw new Error('Canonical session ID is required');
545
+ const startedAt = params.startedAt ?? Date.now();
546
+ this.db.prepare(`
547
+ INSERT INTO sessions (id, project_root, label, started_at, status)
548
+ VALUES (?, ?, ?, ?, 'running')
549
+ ON CONFLICT(id) DO UPDATE SET
550
+ project_root = CASE
551
+ WHEN sessions.project_root = '' OR sessions.project_root = 'unknown' THEN excluded.project_root
552
+ ELSE sessions.project_root
553
+ END,
554
+ label = COALESCE(sessions.label, excluded.label),
555
+ started_at = MIN(sessions.started_at, excluded.started_at)
556
+ `).run(params.id, params.projectRoot || 'unknown', params.label ?? null, startedAt);
557
+ }
386
558
  getSession(id) {
387
559
  const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
388
560
  if (!row)
@@ -399,6 +571,7 @@ class DevFlowDatabase {
399
571
  directToolCalls: row.direct_tool_calls,
400
572
  subagentCount: row.subagent_count,
401
573
  status: row.status,
574
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
402
575
  };
403
576
  }
404
577
  listSessions(limit, offset, projectRoot) {
@@ -423,6 +596,7 @@ class DevFlowDatabase {
423
596
  directToolCalls: row.direct_tool_calls,
424
597
  subagentCount: row.subagent_count,
425
598
  status: row.status,
599
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
426
600
  }));
427
601
  }
428
602
  closeSession(id, finishedAt) {
@@ -476,6 +650,8 @@ class DevFlowDatabase {
476
650
  error: row.error,
477
651
  blocked: row.blocked === 1,
478
652
  blockReason: row.block_reason,
653
+ failureCategory: row.failure_category,
654
+ toolUseId: row.tool_use_id,
479
655
  tokensUsed: row.tokens_used ?? 0,
480
656
  }));
481
657
  }
@@ -509,6 +685,8 @@ class DevFlowDatabase {
509
685
  error: row.error,
510
686
  blocked: row.blocked === 1,
511
687
  blockReason: row.block_reason,
688
+ failureCategory: row.failure_category,
689
+ toolUseId: row.tool_use_id,
512
690
  tokensUsed: row.tokens_used ?? 0,
513
691
  });
514
692
  }
@@ -517,7 +695,7 @@ class DevFlowDatabase {
517
695
  // ---- Skill Executions ----
518
696
  insertSkillExecution(params) {
519
697
  this.db.prepare(`
520
- INSERT INTO skill_executions
698
+ INSERT OR IGNORE INTO skill_executions
521
699
  (execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
522
700
  VALUES (?, ?, ?, ?, ?, ?, ?)
523
701
  `).run(params.executionId, params.sessionId ?? null, params.skillName, params.startedAt, params.status, params.requiredMcpTools ? JSON.stringify(params.requiredMcpTools) : null, params.availableMcpTools ? JSON.stringify(params.availableMcpTools) : null);
@@ -543,6 +721,7 @@ class DevFlowDatabase {
543
721
  mcpComplianceRate: row.mcp_compliance_rate,
544
722
  missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
545
723
  createdAt: row.created_at,
724
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
546
725
  };
547
726
  }
548
727
  updateSkillExecution(executionId, updates) {
@@ -584,6 +763,10 @@ class DevFlowDatabase {
584
763
  sets.push('subagent_count = ?');
585
764
  values.push(updates.subagentCount);
586
765
  }
766
+ if (updates.metadata !== undefined) {
767
+ sets.push('metadata = ?');
768
+ values.push(JSON.stringify(updates.metadata));
769
+ }
587
770
  if (sets.length > 0) {
588
771
  values.push(executionId);
589
772
  this.db.prepare(`UPDATE skill_executions SET ${sets.join(', ')} WHERE execution_id = ?`).run(...values);
@@ -612,26 +795,47 @@ class DevFlowDatabase {
612
795
  subagentCount: row.subagent_count,
613
796
  totalDuration: row.total_duration,
614
797
  mcpComplianceRate: row.mcp_compliance_rate,
798
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
615
799
  }));
616
800
  }
617
801
  // ---- Tool Call Events ----
618
802
  insertToolCallEvent(params) {
619
- this.db.prepare(`
620
- INSERT INTO tool_call_events
621
- (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
622
- mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, duration,
623
- parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id)
624
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
625
- `).run(params.eventId, params.executionId || null, params.sessionId ?? null, params.timestamp, params.toolName, params.toolType, params.isMcpTool ? 1 : 0, params.mcpToolName || null, params.mcpEnforced ? 1 : 0, params.mcpFallback ? 1 : 0, params.kind ?? 'tool_use', JSON.stringify(params.input) ?? null, params.output ? JSON.stringify(params.output) : null, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null, params.workflowRunId || null);
626
- // Update execution summary counters
627
- this.db.prepare(`
628
- UPDATE skill_executions
629
- SET total_tool_calls = total_tool_calls + 1,
630
- mcp_tool_calls = mcp_tool_calls + ?,
631
- direct_tool_calls = direct_tool_calls + ?,
632
- subagent_count = subagent_count + ?
633
- WHERE execution_id = ?
634
- `).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.executionId);
803
+ return this.db.transaction(() => {
804
+ const result = this.db.prepare(`
805
+ INSERT OR IGNORE INTO tool_call_events
806
+ (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
807
+ mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
808
+ parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
809
+ failure_category, tool_use_id)
810
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
811
+ `).run(params.eventId, params.executionId || '', params.sessionId ?? null, params.timestamp, params.toolName, params.toolType, params.isMcpTool ? 1 : 0, params.mcpToolName || null, params.mcpEnforced ? 1 : 0, params.mcpFallback ? 1 : 0, params.kind ?? 'tool_use', JSON.stringify(params.input) ?? null, params.output !== undefined ? JSON.stringify(params.output) : null, params.tokensUsed ?? 0, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null, params.workflowRunId || null, params.failureCategory || null, params.toolUseId || null);
812
+ if (result.changes === 0)
813
+ return false;
814
+ this.db.prepare(`
815
+ UPDATE skill_executions
816
+ SET total_tool_calls = total_tool_calls + 1,
817
+ mcp_tool_calls = mcp_tool_calls + ?,
818
+ direct_tool_calls = direct_tool_calls + ?,
819
+ subagent_count = subagent_count + ?,
820
+ total_duration = MAX(total_duration, ? - started_at),
821
+ mcp_compliance_rate = ROUND(
822
+ 100.0 * (mcp_tool_calls + ?) / NULLIF(total_tool_calls + 1, 0), 2
823
+ )
824
+ WHERE execution_id = ?
825
+ `).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.timestamp, params.isMcpTool ? 1 : 0, params.executionId);
826
+ if (params.sessionId) {
827
+ this.db.prepare(`
828
+ UPDATE sessions
829
+ SET total_tool_calls = total_tool_calls + 1,
830
+ mcp_tool_calls = mcp_tool_calls + ?,
831
+ direct_tool_calls = direct_tool_calls + ?,
832
+ subagent_count = subagent_count + ?,
833
+ duration_ms = MAX(duration_ms, ? - started_at)
834
+ WHERE id = ?
835
+ `).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.timestamp, params.sessionId);
836
+ }
837
+ return true;
838
+ });
635
839
  }
636
840
  listToolCallEvents(executionId) {
637
841
  const rows = this.db.prepare('SELECT * FROM tool_call_events WHERE execution_id = ? ORDER BY timestamp ASC').all(executionId);
@@ -655,6 +859,8 @@ class DevFlowDatabase {
655
859
  error: row.error,
656
860
  blocked: row.blocked === 1,
657
861
  blockReason: row.block_reason,
862
+ failureCategory: row.failure_category,
863
+ toolUseId: row.tool_use_id,
658
864
  }));
659
865
  }
660
866
  updateToolCallEvent(eventId, updates) {
@@ -674,8 +880,41 @@ class DevFlowDatabase {
674
880
  }
675
881
  if (sets.length > 0) {
676
882
  values.push(eventId);
677
- this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
883
+ const result = this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
884
+ return result.changes > 0;
678
885
  }
886
+ return false;
887
+ }
888
+ markToolCallBlocked(eventId, reason) {
889
+ return this.db.prepare(`
890
+ UPDATE tool_call_events
891
+ SET blocked = 1, block_reason = ?, error = COALESCE(error, ?)
892
+ WHERE event_id = ?
893
+ `).run(reason, reason, eventId).changes > 0;
894
+ }
895
+ reconcileSkillExecution(executionId, status, finishedAt = Date.now(), metadata) {
896
+ const row = this.db.prepare(`
897
+ SELECT COUNT(*) AS total,
898
+ SUM(CASE WHEN is_mcp_tool = 1 THEN 1 ELSE 0 END) AS mcp,
899
+ SUM(CASE WHEN is_mcp_tool = 0 THEN 1 ELSE 0 END) AS direct,
900
+ SUM(CASE WHEN tool_type = 'subagent' THEN 1 ELSE 0 END) AS subagents,
901
+ MIN(timestamp) AS first_at,
902
+ MAX(timestamp) AS last_at
903
+ FROM tool_call_events WHERE execution_id = ?
904
+ `).get(executionId);
905
+ const total = Number(row?.total ?? 0);
906
+ const mcp = Number(row?.mcp ?? 0);
907
+ this.updateSkillExecution(executionId, {
908
+ status,
909
+ finishedAt,
910
+ totalToolCalls: total,
911
+ mcpToolCalls: mcp,
912
+ directToolCalls: Number(row?.direct ?? 0),
913
+ subagentCount: Number(row?.subagents ?? 0),
914
+ totalDuration: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
915
+ mcpComplianceRate: total > 0 ? Math.round((mcp / total) * 10000) / 100 : 0,
916
+ metadata,
917
+ });
679
918
  }
680
919
  // ---- MCP Compliance ----
681
920
  getMcpCompliance() {
@@ -718,14 +957,16 @@ class DevFlowDatabase {
718
957
  cleanupStaleRecords() {
719
958
  const cutoff = Date.now() - 30 * 60 * 1000;
720
959
  try {
721
- this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
722
- this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
960
+ this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at = ?, metadata = ?
961
+ WHERE status = 'running' AND started_at < ?`).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
962
+ this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ?, metadata = ?
963
+ WHERE status = 'running' AND started_at < ?`).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
723
964
  }
724
965
  catch { /* best-effort */ }
725
966
  }
726
967
  insertToolMetric(metric) {
727
968
  this.db.prepare(`
728
- INSERT OR REPLACE INTO tool_metrics (id, tool_call_event_id, session_id, tool_name, query, status, result_count, result_size_bytes, latency_ms, engine, accuracy_query_id, metadata, created_at)
969
+ INSERT OR IGNORE INTO tool_metrics (id, tool_call_event_id, session_id, tool_name, query, status, result_count, result_size_bytes, latency_ms, engine, accuracy_query_id, metadata, created_at)
729
970
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
730
971
  `).run(metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName, metric.query ?? null, metric.status ?? 'success', metric.resultCount ?? 0, metric.resultSizeBytes ?? 0, metric.latencyMs ?? 0, metric.engine ?? null, metric.accuracyQueryId ?? null, metric.metadata ?? null, metric.createdAt);
731
972
  }
@@ -766,6 +1007,111 @@ class DevFlowDatabase {
766
1007
  avgLatency: Math.round(r.avg_latency),
767
1008
  }));
768
1009
  }
1010
+ aggregatePendingToolMetrics(limit = 1000) {
1011
+ const rows = this.db.prepare(`
1012
+ SELECT e.*
1013
+ FROM tool_call_events e
1014
+ LEFT JOIN tool_metrics m ON m.tool_call_event_id = e.event_id
1015
+ WHERE m.id IS NULL AND e.output IS NOT NULL
1016
+ ORDER BY e.timestamp ASC
1017
+ LIMIT ?
1018
+ `).all(limit);
1019
+ let inserted = 0;
1020
+ this.db.transaction(() => {
1021
+ for (const row of rows) {
1022
+ const input = parseJson(row.input);
1023
+ const output = parseJson(row.output);
1024
+ const toolName = row.mcp_tool_name || row.tool_name;
1025
+ const resultCount = countTelemetryResults(output);
1026
+ const resultSizeBytes = Buffer.byteLength(row.output ?? '', 'utf8');
1027
+ const status = row.error || row.blocked
1028
+ ? 'error'
1029
+ : resultCount === 0 ? 'empty' : 'success';
1030
+ const result = this.db.prepare(`
1031
+ INSERT OR IGNORE INTO tool_metrics
1032
+ (id, tool_call_event_id, session_id, tool_name, query, status, result_count,
1033
+ result_size_bytes, latency_ms, engine, metadata, created_at)
1034
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1035
+ `).run(`metric:${row.event_id}`, row.event_id, row.session_id || '', toolName, deriveQuery(input), status, resultCount, resultSizeBytes, row.duration ?? 0, deriveEngine(toolName), JSON.stringify({ executionId: row.execution_id, kind: row.kind }), row.timestamp);
1036
+ inserted += result.changes;
1037
+ }
1038
+ });
1039
+ return inserted;
1040
+ }
1041
+ // ---- Context Feedback ----
1042
+ insertFeedback(feedback) {
1043
+ this.db.prepare(`
1044
+ INSERT OR IGNORE INTO feedback
1045
+ (id, project_root, query, rating, task_type, context_mode, run_id, token_count, created_at)
1046
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1047
+ `).run(feedback.id, feedback.projectRoot, feedback.query, feedback.rating, feedback.taskType ?? null, feedback.contextMode ?? null, feedback.runId ?? null, feedback.tokenCount ?? 0, feedback.createdAt);
1048
+ }
1049
+ listFeedback(projectRoot, limit = 100) {
1050
+ const rows = projectRoot
1051
+ ? this.db.prepare('SELECT * FROM feedback WHERE project_root = ? ORDER BY created_at DESC LIMIT ?').all(projectRoot, limit)
1052
+ : this.db.prepare('SELECT * FROM feedback ORDER BY created_at DESC LIMIT ?').all(limit);
1053
+ return rows.map(row => ({
1054
+ id: row.id,
1055
+ projectRoot: row.project_root,
1056
+ query: row.query,
1057
+ rating: row.rating,
1058
+ taskType: row.task_type ?? undefined,
1059
+ contextMode: row.context_mode ?? undefined,
1060
+ runId: row.run_id ?? undefined,
1061
+ tokenCount: row.token_count ?? 0,
1062
+ createdAt: row.created_at,
1063
+ }));
1064
+ }
1065
+ getFeedbackSummary(projectRoot) {
1066
+ const row = (projectRoot
1067
+ ? this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback WHERE project_root = ?`).get(projectRoot)
1068
+ : this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback`).get());
1069
+ const total = Number(row?.total ?? 0);
1070
+ const hits = Number(row?.hits ?? 0);
1071
+ const partials = Number(row?.partials ?? 0);
1072
+ const misses = Number(row?.misses ?? 0);
1073
+ return { total, hits, partials, misses, hitRate: total > 0 ? hits / total : 0 };
1074
+ }
1075
+ // ---- Telemetry Failures ----
1076
+ insertTelemetryFailure(failure) {
1077
+ this.db.prepare(`
1078
+ INSERT OR IGNORE INTO telemetry_failures
1079
+ (id, operation, payload, error, created_at, resolved_at)
1080
+ VALUES (?, ?, ?, ?, ?, ?)
1081
+ `).run(failure.id, failure.operation, JSON.stringify(failure.payload) ?? null, failure.error, failure.createdAt, failure.resolvedAt ?? null);
1082
+ }
1083
+ resolveTelemetryFailure(id, resolvedAt = Date.now()) {
1084
+ this.db.prepare('UPDATE telemetry_failures SET resolved_at = ? WHERE id = ?').run(resolvedAt, id);
1085
+ }
1086
+ listTelemetryFailures(options) {
1087
+ const where = options?.unresolvedOnly ? 'WHERE resolved_at IS NULL' : '';
1088
+ const rows = this.db.prepare(`SELECT * FROM telemetry_failures ${where} ORDER BY created_at DESC LIMIT ?`)
1089
+ .all(options?.limit ?? 100);
1090
+ return rows.map(row => ({
1091
+ id: row.id,
1092
+ operation: row.operation,
1093
+ payload: parseJson(row.payload),
1094
+ error: row.error,
1095
+ createdAt: row.created_at,
1096
+ resolvedAt: row.resolved_at ?? undefined,
1097
+ }));
1098
+ }
1099
+ countTelemetryFailures(unresolvedOnly = false) {
1100
+ const row = this.db.prepare(`SELECT COUNT(*) count FROM telemetry_failures ${unresolvedOnly ? 'WHERE resolved_at IS NULL' : ''}`).get();
1101
+ return Number(row?.count ?? 0);
1102
+ }
1103
+ trimTelemetryFailures(maxRows = 500) {
1104
+ const limit = Math.max(1, Math.floor(maxRows));
1105
+ const result = this.db.prepare(`
1106
+ DELETE FROM telemetry_failures
1107
+ WHERE id IN (
1108
+ SELECT id FROM telemetry_failures
1109
+ ORDER BY created_at DESC, id DESC
1110
+ LIMIT -1 OFFSET ?
1111
+ )
1112
+ `).run(limit);
1113
+ return Number(result.changes);
1114
+ }
769
1115
  // ---- Accuracy Queries ----
770
1116
  insertAccuracyQuery(q) {
771
1117
  this.db.prepare(`
@@ -830,6 +1176,241 @@ class DevFlowDatabase {
830
1176
  }
831
1177
  return statsByEngine;
832
1178
  }
1179
+ // ---- Benchmark Reports ----
1180
+ insertBenchmarkReport(report) {
1181
+ this.db.prepare(`
1182
+ INSERT INTO benchmark_reports
1183
+ (run_id, suite_id, suite_version, project_root, commit_sha, task_count, status, report_json, created_at)
1184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1185
+ ON CONFLICT(run_id) DO UPDATE SET
1186
+ suite_id = excluded.suite_id,
1187
+ suite_version = excluded.suite_version,
1188
+ project_root = excluded.project_root,
1189
+ commit_sha = excluded.commit_sha,
1190
+ task_count = excluded.task_count,
1191
+ status = excluded.status,
1192
+ report_json = excluded.report_json,
1193
+ created_at = excluded.created_at
1194
+ `).run(report.runId, report.suiteId, report.suiteVersion, report.projectRoot, report.commit, report.tasks.length, report.status, JSON.stringify(report), report.createdAt);
1195
+ }
1196
+ getBenchmarkReport(runId) {
1197
+ const row = this.db.prepare('SELECT report_json FROM benchmark_reports WHERE run_id = ?').get(runId);
1198
+ return row?.report_json
1199
+ ? JSON.parse(row.report_json)
1200
+ : null;
1201
+ }
1202
+ getLatestBenchmarkReport(suiteId) {
1203
+ const row = (suiteId
1204
+ ? this.db.prepare('SELECT report_json FROM benchmark_reports WHERE suite_id = ? ORDER BY created_at DESC LIMIT 1').get(suiteId)
1205
+ : this.db.prepare('SELECT report_json FROM benchmark_reports ORDER BY created_at DESC LIMIT 1').get());
1206
+ return row?.report_json
1207
+ ? JSON.parse(row.report_json)
1208
+ : null;
1209
+ }
1210
+ listBenchmarkReports(limit = 50) {
1211
+ const rows = this.db.prepare(`
1212
+ SELECT run_id, suite_id, suite_version, project_root, commit_sha,
1213
+ task_count, status, created_at
1214
+ FROM benchmark_reports
1215
+ ORDER BY created_at DESC
1216
+ LIMIT ?
1217
+ `).all(Math.max(1, Math.min(limit, 500)));
1218
+ return rows.map((row) => ({
1219
+ runId: row.run_id,
1220
+ suiteId: row.suite_id,
1221
+ suiteVersion: row.suite_version,
1222
+ projectRoot: row.project_root,
1223
+ commit: row.commit_sha,
1224
+ taskCount: row.task_count,
1225
+ createdAt: row.created_at,
1226
+ status: row.status,
1227
+ }));
1228
+ }
1229
+ // ---- Enforcer Governance ----
1230
+ listGovernanceRules(includeDisabled = true) {
1231
+ const where = includeDisabled ? '' : 'WHERE enabled = 1';
1232
+ const rows = this.db.prepare(`
1233
+ SELECT id, name, gate, level, condition_json, action, message,
1234
+ enabled, created_at, updated_at
1235
+ FROM devflow_rules ${where}
1236
+ ORDER BY gate, id
1237
+ `).all();
1238
+ return rows.map(row => ({
1239
+ id: row.id,
1240
+ name: row.name,
1241
+ gate: row.gate,
1242
+ level: row.level,
1243
+ condition: parseJson(row.condition_json),
1244
+ action: row.action,
1245
+ message: row.message,
1246
+ enabled: row.enabled === 1,
1247
+ createdAt: row.created_at,
1248
+ updatedAt: row.updated_at,
1249
+ }));
1250
+ }
1251
+ getGovernanceRule(id) {
1252
+ return this.listGovernanceRules(true).find(rule => rule.id === id) ?? null;
1253
+ }
1254
+ upsertGovernanceRule(rule) {
1255
+ const now = Date.now();
1256
+ this.db.prepare(`
1257
+ INSERT INTO devflow_rules
1258
+ (id, name, gate, level, condition_json, action, message, enabled, created_at, updated_at)
1259
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1260
+ ON CONFLICT(id) DO UPDATE SET
1261
+ name = excluded.name,
1262
+ gate = excluded.gate,
1263
+ level = excluded.level,
1264
+ condition_json = excluded.condition_json,
1265
+ action = excluded.action,
1266
+ message = excluded.message,
1267
+ enabled = excluded.enabled,
1268
+ updated_at = excluded.updated_at
1269
+ `).run(rule.id, rule.name, rule.gate, rule.level, JSON.stringify(rule.condition), rule.action, rule.message, rule.enabled ? 1 : 0, rule.createdAt ?? now, now);
1270
+ return this.getGovernanceRule(rule.id);
1271
+ }
1272
+ deleteGovernanceRule(id) {
1273
+ return this.db.prepare('DELETE FROM devflow_rules WHERE id = ?').run(id).changes > 0;
1274
+ }
1275
+ listGovernanceAudit(limit = 100, offset = 0) {
1276
+ const boundedLimit = Math.max(1, Math.min(limit, 500));
1277
+ const boundedOffset = Math.max(0, offset);
1278
+ const rows = this.db.prepare(`
1279
+ SELECT seq, ts, tool, args_hash, decision, rule_id, session_id, prev_hash, signature
1280
+ FROM devflow_audit_chain
1281
+ ORDER BY seq DESC
1282
+ LIMIT ? OFFSET ?
1283
+ `).all(boundedLimit, boundedOffset);
1284
+ const count = this.db.prepare('SELECT COUNT(*) AS total FROM devflow_audit_chain').get();
1285
+ return {
1286
+ items: rows.map(row => ({
1287
+ seq: row.seq,
1288
+ ts: row.ts,
1289
+ tool: row.tool,
1290
+ argsHash: row.args_hash,
1291
+ decision: row.decision,
1292
+ ruleId: row.rule_id,
1293
+ sessionId: row.session_id,
1294
+ prevHash: row.prev_hash,
1295
+ signature: row.signature,
1296
+ })),
1297
+ total: Number(count?.total ?? 0),
1298
+ };
1299
+ }
1300
+ // ---- Hook Lifecycle ----
1301
+ getHookReceipt(projectRoot) {
1302
+ const row = this.db.prepare(`
1303
+ SELECT project_root, last_mcp_call, bypass_count, updated_at
1304
+ FROM devflow_hook_receipts WHERE project_root = ?
1305
+ `).get(projectRoot);
1306
+ if (!row)
1307
+ return null;
1308
+ return {
1309
+ projectRoot: row.project_root,
1310
+ lastMcpCall: row.last_mcp_call ?? undefined,
1311
+ bypassCount: Number(row.bypass_count ?? 0),
1312
+ updatedAt: Number(row.updated_at),
1313
+ };
1314
+ }
1315
+ updateHookReceipt(projectRoot, updater) {
1316
+ this.db.exec('BEGIN IMMEDIATE');
1317
+ try {
1318
+ const current = this.getHookReceipt(projectRoot);
1319
+ const next = updater(current);
1320
+ const updatedAt = Date.now();
1321
+ this.db.prepare(`
1322
+ INSERT INTO devflow_hook_receipts
1323
+ (project_root, last_mcp_call, bypass_count, updated_at)
1324
+ VALUES (?, ?, ?, ?)
1325
+ ON CONFLICT(project_root) DO UPDATE SET
1326
+ last_mcp_call = excluded.last_mcp_call,
1327
+ bypass_count = excluded.bypass_count,
1328
+ updated_at = excluded.updated_at
1329
+ `).run(projectRoot, next.lastMcpCall ?? null, next.bypassCount ?? 0, updatedAt);
1330
+ this.db.exec('COMMIT');
1331
+ return {
1332
+ projectRoot,
1333
+ lastMcpCall: next.lastMcpCall,
1334
+ bypassCount: next.bypassCount ?? 0,
1335
+ updatedAt,
1336
+ };
1337
+ }
1338
+ catch (error) {
1339
+ try {
1340
+ this.db.exec('ROLLBACK');
1341
+ }
1342
+ catch { }
1343
+ throw error;
1344
+ }
1345
+ }
1346
+ deleteHookReceipt(projectRoot) {
1347
+ return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
1348
+ .run(projectRoot).changes > 0;
1349
+ }
1350
+ upsertContextReceipt(receipt) {
1351
+ this.db.prepare(`
1352
+ INSERT INTO devflow_context_receipts
1353
+ (project_root, session_id, execution_id, context_hash, issued_at, expires_at)
1354
+ VALUES (?, ?, ?, ?, ?, ?)
1355
+ ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
1356
+ context_hash = excluded.context_hash,
1357
+ issued_at = excluded.issued_at,
1358
+ expires_at = excluded.expires_at
1359
+ `).run(receipt.projectRoot, receipt.sessionId, receipt.executionId, receipt.contextHash, receipt.issuedAt, receipt.expiresAt);
1360
+ }
1361
+ getContextReceipt(projectRoot, sessionId, executionId) {
1362
+ const row = this.db.prepare(`
1363
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
1364
+ FROM devflow_context_receipts
1365
+ WHERE project_root = ? AND session_id = ? AND execution_id = ?
1366
+ `).get(projectRoot, sessionId, executionId);
1367
+ return row ? {
1368
+ projectRoot: row.project_root,
1369
+ sessionId: row.session_id,
1370
+ executionId: row.execution_id,
1371
+ contextHash: row.context_hash,
1372
+ issuedAt: row.issued_at,
1373
+ expiresAt: row.expires_at,
1374
+ } : null;
1375
+ }
1376
+ deleteContextReceipt(projectRoot, sessionId, executionId) {
1377
+ return executionId
1378
+ ? this.db.prepare(`DELETE FROM devflow_context_receipts
1379
+ WHERE project_root = ? AND session_id = ? AND execution_id = ?`)
1380
+ .run(projectRoot, sessionId, executionId).changes
1381
+ : this.db.prepare(`DELETE FROM devflow_context_receipts
1382
+ WHERE project_root = ? AND session_id = ?`)
1383
+ .run(projectRoot, sessionId).changes;
1384
+ }
1385
+ purgeExpiredContextReceipts(now = Date.now()) {
1386
+ return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
1387
+ .run(now).changes;
1388
+ }
1389
+ recordMemoryDistillCheckpoint(checkpoint) {
1390
+ this.db.prepare(`
1391
+ INSERT INTO devflow_memory_distill_checkpoints
1392
+ (id, project_root, session_id, trigger, pending_events, released_leases, created_at)
1393
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1394
+ `).run(checkpoint.id, checkpoint.projectRoot, checkpoint.sessionId ?? null, checkpoint.trigger, checkpoint.pendingEvents, checkpoint.releasedLeases, checkpoint.createdAt);
1395
+ }
1396
+ listMemoryDistillCheckpoints(projectRoot, limit = 50) {
1397
+ const rows = this.db.prepare(`
1398
+ SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
1399
+ FROM devflow_memory_distill_checkpoints
1400
+ WHERE project_root = ?
1401
+ ORDER BY created_at DESC
1402
+ LIMIT ?
1403
+ `).all(projectRoot, Math.max(1, Math.min(limit, 500)));
1404
+ return rows.map(row => ({
1405
+ id: row.id,
1406
+ projectRoot: row.project_root,
1407
+ sessionId: row.session_id ?? undefined,
1408
+ trigger: row.trigger,
1409
+ pendingEvents: Number(row.pending_events),
1410
+ releasedLeases: Number(row.released_leases),
1411
+ createdAt: Number(row.created_at),
1412
+ }));
1413
+ }
833
1414
  // Convenience methods for raw SQL queries (used by AutoChecker)
834
1415
  all(sql, ...params) {
835
1416
  return this.db.prepare(sql).all(...params);
@@ -842,3 +1423,54 @@ class DevFlowDatabase {
842
1423
  }
843
1424
  }
844
1425
  exports.DevFlowDatabase = DevFlowDatabase;
1426
+ function parseJson(value) {
1427
+ if (typeof value !== 'string')
1428
+ return value;
1429
+ try {
1430
+ return JSON.parse(value);
1431
+ }
1432
+ catch {
1433
+ return value;
1434
+ }
1435
+ }
1436
+ function deriveQuery(input) {
1437
+ if (!input || typeof input !== 'object')
1438
+ return null;
1439
+ const record = input;
1440
+ for (const key of ['query', 'task', 'prompt', 'name', 'command']) {
1441
+ if (typeof record[key] === 'string' && record[key])
1442
+ return record[key];
1443
+ }
1444
+ return null;
1445
+ }
1446
+ function deriveEngine(toolName) {
1447
+ if (/project_context|symbol|dependency_graph|codegraph/.test(toolName))
1448
+ return 'codegraph';
1449
+ if (/knowledge|docs/.test(toolName))
1450
+ return 'knowledge';
1451
+ if (/memory/.test(toolName))
1452
+ return 'memory';
1453
+ return null;
1454
+ }
1455
+ function countTelemetryResults(value, depth = 0) {
1456
+ if (value == null || depth > 2)
1457
+ return 0;
1458
+ if (Array.isArray(value))
1459
+ return value.length;
1460
+ if (typeof value !== 'object')
1461
+ return value === '' ? 0 : 1;
1462
+ const record = value;
1463
+ const unwrapped = record.data ?? record.structuredContent;
1464
+ if (unwrapped !== undefined && unwrapped !== value)
1465
+ return countTelemetryResults(unwrapped, depth + 1);
1466
+ let count = 0;
1467
+ for (const key of ['files', 'results', 'result', 'memories', 'nodes', 'chunks', 'findings', 'symbols', 'keySymbols']) {
1468
+ if (Array.isArray(record[key]))
1469
+ count += record[key].length;
1470
+ }
1471
+ if (count > 0)
1472
+ return count;
1473
+ if (record.error)
1474
+ return 0;
1475
+ return Object.keys(record).length > 0 ? 1 : 0;
1476
+ }