@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/src/database.ts CHANGED
@@ -1,11 +1,121 @@
1
1
  import { NodeSqliteDatabase } from './node-sqlite';
2
2
  import { join, dirname } from 'path';
3
3
  import { existsSync, mkdirSync } from 'fs';
4
+ import { homedir } from 'os';
5
+
6
+ export interface BenchmarkReportRecord {
7
+ runId: string;
8
+ suiteId: string;
9
+ suiteVersion: string;
10
+ projectRoot: string;
11
+ commit: string | null;
12
+ createdAt: number;
13
+ status: 'completed';
14
+ tasks: unknown[];
15
+ }
16
+
17
+ export interface BenchmarkReportMetaRecord {
18
+ runId: string;
19
+ suiteId: string;
20
+ suiteVersion: string;
21
+ projectRoot: string;
22
+ commit: string | null;
23
+ taskCount: number;
24
+ createdAt: number;
25
+ status: 'completed';
26
+ }
27
+
28
+ export interface FeedbackRecord {
29
+ id: string;
30
+ projectRoot: string;
31
+ query: string;
32
+ rating: 'hit' | 'partial' | 'miss';
33
+ taskType?: string;
34
+ contextMode?: 'general' | 'task';
35
+ runId?: string;
36
+ tokenCount?: number;
37
+ createdAt: number;
38
+ }
39
+
40
+ export interface TelemetryFailureRecord {
41
+ id: string;
42
+ operation: string;
43
+ payload: unknown;
44
+ error: string;
45
+ createdAt: number;
46
+ resolvedAt?: number;
47
+ }
48
+
49
+ export interface GovernanceRuleRecord {
50
+ id: string;
51
+ name: string;
52
+ gate: 1 | 2 | 3 | 4;
53
+ level: 'constitutional' | 'project' | 'session';
54
+ condition: Record<string, unknown>;
55
+ action: 'deny' | 'warn' | 'allow';
56
+ message: string;
57
+ enabled: boolean;
58
+ createdAt?: number;
59
+ updatedAt?: number;
60
+ }
61
+
62
+ export interface GovernanceAuditRecord {
63
+ seq: number;
64
+ ts: number;
65
+ tool: string;
66
+ argsHash: string;
67
+ decision: 'allow' | 'deny' | 'warn';
68
+ ruleId: string | null;
69
+ sessionId: string;
70
+ prevHash: string;
71
+ signature: string;
72
+ }
73
+
74
+ export interface HookReceiptRecord {
75
+ projectRoot: string;
76
+ lastMcpCall?: number;
77
+ bypassCount?: number;
78
+ updatedAt: number;
79
+ }
80
+
81
+ export interface ContextReceiptRecord {
82
+ projectRoot: string;
83
+ sessionId: string;
84
+ executionId: string;
85
+ contextHash: string;
86
+ issuedAt: number;
87
+ expiresAt: number;
88
+ }
89
+
90
+ export interface MemoryDistillCheckpointRecord {
91
+ id: string;
92
+ projectRoot: string;
93
+ sessionId?: string;
94
+ trigger: 'pre_compact' | 'session_end';
95
+ pendingEvents: number;
96
+ releasedLeases: number;
97
+ createdAt: number;
98
+ }
99
+
100
+ export function getGlobalDevFlowDbPath(home = homedir()): string {
101
+ const stateDir = process.env.DEVFLOW_STATE_DIR ?? join(home, '.devflow', 'global');
102
+ return join(stateDir, 'devflow.db');
103
+ }
104
+
105
+ export function openGlobalDevFlowDatabase(
106
+ home = homedir(),
107
+ options?: { busyTimeoutMs?: number },
108
+ ): DevFlowDatabase {
109
+ return new DevFlowDatabase(home, {
110
+ dbPath: getGlobalDevFlowDbPath(home),
111
+ busyTimeoutMs: options?.busyTimeoutMs,
112
+ });
113
+ }
4
114
 
5
115
  export class DevFlowDatabase {
6
116
  private db: NodeSqliteDatabase;
7
117
 
8
- constructor(projectRoot: string, opts?: { dbPath?: string }) {
118
+ constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number }) {
9
119
  let dbPath: string;
10
120
  if (opts?.dbPath) {
11
121
  const dir = dirname(opts.dbPath);
@@ -16,10 +126,10 @@ export class DevFlowDatabase {
16
126
  if (!existsSync(devflowDir)) mkdirSync(devflowDir, { recursive: true });
17
127
  dbPath = join(devflowDir, 'devflow.db');
18
128
  }
19
- this.db = new NodeSqliteDatabase(dbPath);
129
+ this.db = new NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
20
130
 
21
131
  this.db.exec('PRAGMA journal_mode = WAL');
22
- this.db.exec('PRAGMA busy_timeout = 5000');
132
+ this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
23
133
  this.db.exec('PRAGMA foreign_keys = OFF');
24
134
 
25
135
  this.initializeSchema();
@@ -123,7 +233,9 @@ export class DevFlowDatabase {
123
233
  subagent_id TEXT,
124
234
  error TEXT,
125
235
  blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
126
- block_reason TEXT
236
+ block_reason TEXT,
237
+ failure_category TEXT,
238
+ tool_use_id TEXT
127
239
  );
128
240
 
129
241
  CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
@@ -146,11 +258,99 @@ export class DevFlowDatabase {
146
258
  mcp_tool_calls INTEGER DEFAULT 0,
147
259
  direct_tool_calls INTEGER DEFAULT 0,
148
260
  subagent_count INTEGER DEFAULT 0,
149
- status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
261
+ status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
262
+ metadata TEXT
150
263
  );
151
264
 
152
265
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
153
266
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
267
+
268
+ CREATE TABLE IF NOT EXISTS feedback (
269
+ id TEXT PRIMARY KEY,
270
+ project_root TEXT NOT NULL,
271
+ query TEXT NOT NULL,
272
+ rating TEXT NOT NULL CHECK (rating IN ('hit', 'partial', 'miss')),
273
+ task_type TEXT,
274
+ context_mode TEXT,
275
+ run_id TEXT,
276
+ token_count INTEGER DEFAULT 0,
277
+ created_at INTEGER NOT NULL
278
+ );
279
+
280
+ CREATE INDEX IF NOT EXISTS idx_feedback_project ON feedback(project_root);
281
+ CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at DESC);
282
+
283
+ CREATE TABLE IF NOT EXISTS telemetry_failures (
284
+ id TEXT PRIMARY KEY,
285
+ operation TEXT NOT NULL,
286
+ payload TEXT,
287
+ error TEXT NOT NULL,
288
+ created_at INTEGER NOT NULL,
289
+ resolved_at INTEGER
290
+ );
291
+
292
+ CREATE INDEX IF NOT EXISTS idx_telemetry_failures_created ON telemetry_failures(created_at DESC);
293
+ CREATE INDEX IF NOT EXISTS idx_telemetry_failures_unresolved ON telemetry_failures(resolved_at) WHERE resolved_at IS NULL;
294
+
295
+ CREATE TABLE IF NOT EXISTS devflow_rules (
296
+ id TEXT PRIMARY KEY,
297
+ name TEXT NOT NULL,
298
+ level TEXT NOT NULL DEFAULT 'project',
299
+ gate INTEGER NOT NULL DEFAULT 1,
300
+ condition_json TEXT NOT NULL,
301
+ action TEXT NOT NULL DEFAULT 'warn',
302
+ message TEXT NOT NULL,
303
+ enabled INTEGER NOT NULL DEFAULT 1,
304
+ created_at INTEGER NOT NULL,
305
+ updated_at INTEGER NOT NULL
306
+ );
307
+
308
+ CREATE TABLE IF NOT EXISTS devflow_audit_chain (
309
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
310
+ ts INTEGER NOT NULL,
311
+ tool TEXT NOT NULL,
312
+ args_hash TEXT NOT NULL,
313
+ decision TEXT NOT NULL,
314
+ rule_id TEXT,
315
+ session_id TEXT NOT NULL,
316
+ prev_hash TEXT NOT NULL,
317
+ signature TEXT NOT NULL
318
+ );
319
+
320
+ CREATE INDEX IF NOT EXISTS idx_devflow_audit_ts ON devflow_audit_chain(ts DESC);
321
+
322
+ CREATE TABLE IF NOT EXISTS devflow_hook_receipts (
323
+ project_root TEXT PRIMARY KEY,
324
+ last_mcp_call INTEGER,
325
+ bypass_count INTEGER NOT NULL DEFAULT 0,
326
+ updated_at INTEGER NOT NULL
327
+ );
328
+
329
+ CREATE TABLE IF NOT EXISTS devflow_context_receipts (
330
+ project_root TEXT NOT NULL,
331
+ session_id TEXT NOT NULL,
332
+ execution_id TEXT NOT NULL,
333
+ context_hash TEXT NOT NULL,
334
+ issued_at INTEGER NOT NULL,
335
+ expires_at INTEGER NOT NULL,
336
+ PRIMARY KEY (project_root, session_id, execution_id)
337
+ );
338
+
339
+ CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
340
+ ON devflow_context_receipts(expires_at);
341
+
342
+ CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
343
+ id TEXT PRIMARY KEY,
344
+ project_root TEXT NOT NULL,
345
+ session_id TEXT,
346
+ trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
347
+ pending_events INTEGER NOT NULL DEFAULT 0,
348
+ released_leases INTEGER NOT NULL DEFAULT 0,
349
+ created_at INTEGER NOT NULL
350
+ );
351
+
352
+ CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
353
+ ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
154
354
  `);
155
355
 
156
356
  // Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
@@ -163,6 +363,18 @@ export class DevFlowDatabase {
163
363
  try { this.db.exec("ALTER TABLE tool_call_events ADD COLUMN kind TEXT DEFAULT 'tool_use'"); } catch {}
164
364
  // Migration: workflow run persistence
165
365
  try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT'); } catch {}
366
+ try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT'); } catch {}
367
+ try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT'); } catch {}
368
+ try { this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT'); } catch {}
369
+ try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT'); } catch {}
370
+ try {
371
+ this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
372
+ ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
373
+ } catch {}
374
+ try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1'); } catch {}
375
+ try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER'); } catch {}
376
+ this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
377
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
166
378
 
167
379
  // Migration: tool_metrics table for per-tool metrics collection
168
380
  try {
@@ -211,6 +423,28 @@ export class DevFlowDatabase {
211
423
  `);
212
424
  } catch {}
213
425
 
426
+ // Canonical benchmark reports live in the global DevFlow database.
427
+ try {
428
+ this.db.exec(`
429
+ CREATE TABLE IF NOT EXISTS benchmark_reports (
430
+ run_id TEXT PRIMARY KEY,
431
+ suite_id TEXT NOT NULL,
432
+ suite_version TEXT NOT NULL,
433
+ project_root TEXT NOT NULL,
434
+ commit_sha TEXT,
435
+ task_count INTEGER NOT NULL,
436
+ status TEXT NOT NULL CHECK(status IN ('completed')),
437
+ report_json TEXT NOT NULL,
438
+ created_at INTEGER NOT NULL
439
+ );
440
+
441
+ CREATE INDEX IF NOT EXISTS idx_benchmark_reports_created
442
+ ON benchmark_reports(created_at DESC);
443
+ CREATE INDEX IF NOT EXISTS idx_benchmark_reports_suite
444
+ ON benchmark_reports(suite_id, suite_version, created_at DESC);
445
+ `);
446
+ } catch {}
447
+
214
448
  // Migration: logging system views for agent queries
215
449
  try {
216
450
  this.db.exec(`
@@ -259,7 +493,7 @@ export class DevFlowDatabase {
259
493
 
260
494
  insertRun(run: any): void {
261
495
  this.db.prepare(`
262
- INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
496
+ INSERT OR IGNORE INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
263
497
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
264
498
  `).run(
265
499
  run.id,
@@ -428,6 +662,27 @@ export class DevFlowDatabase {
428
662
  `).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
429
663
  }
430
664
 
665
+ ensureSession(params: {
666
+ id: string;
667
+ projectRoot: string;
668
+ label?: string;
669
+ startedAt?: number;
670
+ }): void {
671
+ if (!params.id.trim()) throw new Error('Canonical session ID is required');
672
+ const startedAt = params.startedAt ?? Date.now();
673
+ this.db.prepare(`
674
+ INSERT INTO sessions (id, project_root, label, started_at, status)
675
+ VALUES (?, ?, ?, ?, 'running')
676
+ ON CONFLICT(id) DO UPDATE SET
677
+ project_root = CASE
678
+ WHEN sessions.project_root = '' OR sessions.project_root = 'unknown' THEN excluded.project_root
679
+ ELSE sessions.project_root
680
+ END,
681
+ label = COALESCE(sessions.label, excluded.label),
682
+ started_at = MIN(sessions.started_at, excluded.started_at)
683
+ `).run(params.id, params.projectRoot || 'unknown', params.label ?? null, startedAt);
684
+ }
685
+
431
686
  getSession(id: string): any | null {
432
687
  const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as any;
433
688
  if (!row) return null;
@@ -443,6 +698,7 @@ export class DevFlowDatabase {
443
698
  directToolCalls: row.direct_tool_calls,
444
699
  subagentCount: row.subagent_count,
445
700
  status: row.status,
701
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
446
702
  };
447
703
  }
448
704
 
@@ -469,6 +725,7 @@ export class DevFlowDatabase {
469
725
  directToolCalls: row.direct_tool_calls,
470
726
  subagentCount: row.subagent_count,
471
727
  status: row.status,
728
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
472
729
  }));
473
730
  }
474
731
 
@@ -535,6 +792,8 @@ export class DevFlowDatabase {
535
792
  error: row.error,
536
793
  blocked: row.blocked === 1,
537
794
  blockReason: row.block_reason,
795
+ failureCategory: row.failure_category,
796
+ toolUseId: row.tool_use_id,
538
797
  tokensUsed: row.tokens_used ?? 0,
539
798
  }));
540
799
  }
@@ -570,6 +829,8 @@ export class DevFlowDatabase {
570
829
  error: row.error,
571
830
  blocked: row.blocked === 1,
572
831
  blockReason: row.block_reason,
832
+ failureCategory: row.failure_category,
833
+ toolUseId: row.tool_use_id,
573
834
  tokensUsed: row.tokens_used ?? 0,
574
835
  });
575
836
  }
@@ -588,7 +849,7 @@ export class DevFlowDatabase {
588
849
  availableMcpTools?: string[];
589
850
  }): void {
590
851
  this.db.prepare(`
591
- INSERT INTO skill_executions
852
+ INSERT OR IGNORE INTO skill_executions
592
853
  (execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
593
854
  VALUES (?, ?, ?, ?, ?, ?, ?)
594
855
  `).run(
@@ -622,6 +883,7 @@ export class DevFlowDatabase {
622
883
  mcpComplianceRate: row.mcp_compliance_rate,
623
884
  missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
624
885
  createdAt: row.created_at,
886
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
625
887
  };
626
888
  }
627
889
 
@@ -635,6 +897,7 @@ export class DevFlowDatabase {
635
897
  mcpToolCalls?: number;
636
898
  directToolCalls?: number;
637
899
  subagentCount?: number;
900
+ metadata?: Record<string, unknown>;
638
901
  }): void {
639
902
  const sets: string[] = [];
640
903
  const values: any[] = [];
@@ -648,6 +911,7 @@ export class DevFlowDatabase {
648
911
  if (updates.mcpToolCalls !== undefined) { sets.push('mcp_tool_calls = ?'); values.push(updates.mcpToolCalls); }
649
912
  if (updates.directToolCalls !== undefined) { sets.push('direct_tool_calls = ?'); values.push(updates.directToolCalls); }
650
913
  if (updates.subagentCount !== undefined) { sets.push('subagent_count = ?'); values.push(updates.subagentCount); }
914
+ if (updates.metadata !== undefined) { sets.push('metadata = ?'); values.push(JSON.stringify(updates.metadata)); }
651
915
 
652
916
  if (sets.length > 0) {
653
917
  values.push(executionId);
@@ -681,6 +945,7 @@ export class DevFlowDatabase {
681
945
  subagentCount: row.subagent_count,
682
946
  totalDuration: row.total_duration,
683
947
  mcpComplianceRate: row.mcp_compliance_rate,
948
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
684
949
  }));
685
950
  }
686
951
 
@@ -707,50 +972,84 @@ export class DevFlowDatabase {
707
972
  blocked: boolean;
708
973
  blockReason?: string;
709
974
  workflowRunId?: string;
710
- }): void {
711
- this.db.prepare(`
712
- INSERT INTO tool_call_events
713
- (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
714
- mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, duration,
715
- parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id)
716
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
717
- `).run(
718
- params.eventId,
719
- params.executionId || null,
720
- params.sessionId ?? null,
721
- params.timestamp,
722
- params.toolName,
723
- params.toolType,
724
- params.isMcpTool ? 1 : 0,
725
- params.mcpToolName || null,
726
- params.mcpEnforced ? 1 : 0,
727
- params.mcpFallback ? 1 : 0,
728
- params.kind ?? 'tool_use',
729
- JSON.stringify(params.input) ?? null,
730
- params.output ? JSON.stringify(params.output) : null,
731
- params.duration,
732
- params.parentToolCallId || null,
733
- params.subagentId || null,
734
- params.error || null,
735
- params.blocked ? 1 : 0,
736
- params.blockReason || null,
737
- params.workflowRunId || null,
738
- );
975
+ failureCategory?: string;
976
+ tokensUsed?: number;
977
+ toolUseId?: string;
978
+ }): boolean {
979
+ return this.db.transaction(() => {
980
+ const result = this.db.prepare(`
981
+ INSERT OR IGNORE INTO tool_call_events
982
+ (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
983
+ mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
984
+ parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
985
+ failure_category, tool_use_id)
986
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
987
+ `).run(
988
+ params.eventId,
989
+ params.executionId || '',
990
+ params.sessionId ?? null,
991
+ params.timestamp,
992
+ params.toolName,
993
+ params.toolType,
994
+ params.isMcpTool ? 1 : 0,
995
+ params.mcpToolName || null,
996
+ params.mcpEnforced ? 1 : 0,
997
+ params.mcpFallback ? 1 : 0,
998
+ params.kind ?? 'tool_use',
999
+ JSON.stringify(params.input) ?? null,
1000
+ params.output !== undefined ? JSON.stringify(params.output) : null,
1001
+ params.tokensUsed ?? 0,
1002
+ params.duration,
1003
+ params.parentToolCallId || null,
1004
+ params.subagentId || null,
1005
+ params.error || null,
1006
+ params.blocked ? 1 : 0,
1007
+ params.blockReason || null,
1008
+ params.workflowRunId || null,
1009
+ params.failureCategory || null,
1010
+ params.toolUseId || null,
1011
+ );
739
1012
 
740
- // Update execution summary counters
741
- this.db.prepare(`
742
- UPDATE skill_executions
743
- SET total_tool_calls = total_tool_calls + 1,
744
- mcp_tool_calls = mcp_tool_calls + ?,
745
- direct_tool_calls = direct_tool_calls + ?,
746
- subagent_count = subagent_count + ?
747
- WHERE execution_id = ?
748
- `).run(
749
- params.isMcpTool ? 1 : 0,
750
- params.isMcpTool ? 0 : 1,
751
- params.toolType === 'subagent' ? 1 : 0,
752
- params.executionId,
753
- );
1013
+ if (result.changes === 0) return false;
1014
+
1015
+ this.db.prepare(`
1016
+ UPDATE skill_executions
1017
+ SET total_tool_calls = total_tool_calls + 1,
1018
+ mcp_tool_calls = mcp_tool_calls + ?,
1019
+ direct_tool_calls = direct_tool_calls + ?,
1020
+ subagent_count = subagent_count + ?,
1021
+ total_duration = MAX(total_duration, ? - started_at),
1022
+ mcp_compliance_rate = ROUND(
1023
+ 100.0 * (mcp_tool_calls + ?) / NULLIF(total_tool_calls + 1, 0), 2
1024
+ )
1025
+ WHERE execution_id = ?
1026
+ `).run(
1027
+ params.isMcpTool ? 1 : 0,
1028
+ params.isMcpTool ? 0 : 1,
1029
+ params.toolType === 'subagent' ? 1 : 0,
1030
+ params.timestamp,
1031
+ params.isMcpTool ? 1 : 0,
1032
+ params.executionId,
1033
+ );
1034
+ if (params.sessionId) {
1035
+ this.db.prepare(`
1036
+ UPDATE sessions
1037
+ SET total_tool_calls = total_tool_calls + 1,
1038
+ mcp_tool_calls = mcp_tool_calls + ?,
1039
+ direct_tool_calls = direct_tool_calls + ?,
1040
+ subagent_count = subagent_count + ?,
1041
+ duration_ms = MAX(duration_ms, ? - started_at)
1042
+ WHERE id = ?
1043
+ `).run(
1044
+ params.isMcpTool ? 1 : 0,
1045
+ params.isMcpTool ? 0 : 1,
1046
+ params.toolType === 'subagent' ? 1 : 0,
1047
+ params.timestamp,
1048
+ params.sessionId,
1049
+ );
1050
+ }
1051
+ return true;
1052
+ });
754
1053
  }
755
1054
 
756
1055
  listToolCallEvents(executionId: string): any[] {
@@ -778,10 +1077,12 @@ export class DevFlowDatabase {
778
1077
  error: row.error,
779
1078
  blocked: row.blocked === 1,
780
1079
  blockReason: row.block_reason,
1080
+ failureCategory: row.failure_category,
1081
+ toolUseId: row.tool_use_id,
781
1082
  }));
782
1083
  }
783
1084
 
784
- updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }): void {
1085
+ updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }): boolean {
785
1086
  const sets: string[] = [];
786
1087
  const values: any[] = [];
787
1088
 
@@ -800,10 +1101,50 @@ export class DevFlowDatabase {
800
1101
 
801
1102
  if (sets.length > 0) {
802
1103
  values.push(eventId);
803
- this.db.prepare(
1104
+ const result = this.db.prepare(
804
1105
  `UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`
805
1106
  ).run(...values);
1107
+ return result.changes > 0;
806
1108
  }
1109
+ return false;
1110
+ }
1111
+
1112
+ markToolCallBlocked(eventId: string, reason: string): boolean {
1113
+ return this.db.prepare(`
1114
+ UPDATE tool_call_events
1115
+ SET blocked = 1, block_reason = ?, error = COALESCE(error, ?)
1116
+ WHERE event_id = ?
1117
+ `).run(reason, reason, eventId).changes > 0;
1118
+ }
1119
+
1120
+ reconcileSkillExecution(
1121
+ executionId: string,
1122
+ status: 'completed' | 'failed',
1123
+ finishedAt = Date.now(),
1124
+ metadata?: Record<string, unknown>,
1125
+ ): void {
1126
+ const row = this.db.prepare(`
1127
+ SELECT COUNT(*) AS total,
1128
+ SUM(CASE WHEN is_mcp_tool = 1 THEN 1 ELSE 0 END) AS mcp,
1129
+ SUM(CASE WHEN is_mcp_tool = 0 THEN 1 ELSE 0 END) AS direct,
1130
+ SUM(CASE WHEN tool_type = 'subagent' THEN 1 ELSE 0 END) AS subagents,
1131
+ MIN(timestamp) AS first_at,
1132
+ MAX(timestamp) AS last_at
1133
+ FROM tool_call_events WHERE execution_id = ?
1134
+ `).get(executionId) as any;
1135
+ const total = Number(row?.total ?? 0);
1136
+ const mcp = Number(row?.mcp ?? 0);
1137
+ this.updateSkillExecution(executionId, {
1138
+ status,
1139
+ finishedAt,
1140
+ totalToolCalls: total,
1141
+ mcpToolCalls: mcp,
1142
+ directToolCalls: Number(row?.direct ?? 0),
1143
+ subagentCount: Number(row?.subagents ?? 0),
1144
+ totalDuration: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
1145
+ mcpComplianceRate: total > 0 ? Math.round((mcp / total) * 10000) / 100 : 0,
1146
+ metadata,
1147
+ });
807
1148
  }
808
1149
 
809
1150
  // ---- MCP Compliance ----
@@ -860,11 +1201,13 @@ export class DevFlowDatabase {
860
1201
  const cutoff = Date.now() - 30 * 60 * 1000;
861
1202
  try {
862
1203
  this.db.prepare(
863
- `UPDATE skill_executions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`
864
- ).run(Date.now(), cutoff);
1204
+ `UPDATE skill_executions SET status = 'failed', finished_at = ?, metadata = ?
1205
+ WHERE status = 'running' AND started_at < ?`
1206
+ ).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
865
1207
  this.db.prepare(
866
- `UPDATE sessions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`
867
- ).run(Date.now(), cutoff);
1208
+ `UPDATE sessions SET status = 'failed', finished_at = ?, metadata = ?
1209
+ WHERE status = 'running' AND started_at < ?`
1210
+ ).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
868
1211
  } catch { /* best-effort */ }
869
1212
  }
870
1213
 
@@ -884,7 +1227,7 @@ export class DevFlowDatabase {
884
1227
  createdAt: number;
885
1228
  }): void {
886
1229
  this.db.prepare(`
887
- 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)
1230
+ 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)
888
1231
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
889
1232
  `).run(
890
1233
  metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName,
@@ -928,6 +1271,153 @@ export class DevFlowDatabase {
928
1271
  }));
929
1272
  }
930
1273
 
1274
+ aggregatePendingToolMetrics(limit = 1000): number {
1275
+ const rows = this.db.prepare(`
1276
+ SELECT e.*
1277
+ FROM tool_call_events e
1278
+ LEFT JOIN tool_metrics m ON m.tool_call_event_id = e.event_id
1279
+ WHERE m.id IS NULL AND e.output IS NOT NULL
1280
+ ORDER BY e.timestamp ASC
1281
+ LIMIT ?
1282
+ `).all(limit) as any[];
1283
+
1284
+ let inserted = 0;
1285
+ this.db.transaction(() => {
1286
+ for (const row of rows) {
1287
+ const input = parseJson(row.input);
1288
+ const output = parseJson(row.output);
1289
+ const toolName = row.mcp_tool_name || row.tool_name;
1290
+ const resultCount = countTelemetryResults(output);
1291
+ const resultSizeBytes = Buffer.byteLength(row.output ?? '', 'utf8');
1292
+ const status = row.error || row.blocked
1293
+ ? 'error'
1294
+ : resultCount === 0 ? 'empty' : 'success';
1295
+ const result = this.db.prepare(`
1296
+ INSERT OR IGNORE INTO tool_metrics
1297
+ (id, tool_call_event_id, session_id, tool_name, query, status, result_count,
1298
+ result_size_bytes, latency_ms, engine, metadata, created_at)
1299
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1300
+ `).run(
1301
+ `metric:${row.event_id}`,
1302
+ row.event_id,
1303
+ row.session_id || '',
1304
+ toolName,
1305
+ deriveQuery(input),
1306
+ status,
1307
+ resultCount,
1308
+ resultSizeBytes,
1309
+ row.duration ?? 0,
1310
+ deriveEngine(toolName),
1311
+ JSON.stringify({ executionId: row.execution_id, kind: row.kind }),
1312
+ row.timestamp,
1313
+ );
1314
+ inserted += result.changes;
1315
+ }
1316
+ });
1317
+ return inserted;
1318
+ }
1319
+
1320
+ // ---- Context Feedback ----
1321
+
1322
+ insertFeedback(feedback: FeedbackRecord): void {
1323
+ this.db.prepare(`
1324
+ INSERT OR IGNORE INTO feedback
1325
+ (id, project_root, query, rating, task_type, context_mode, run_id, token_count, created_at)
1326
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1327
+ `).run(
1328
+ feedback.id,
1329
+ feedback.projectRoot,
1330
+ feedback.query,
1331
+ feedback.rating,
1332
+ feedback.taskType ?? null,
1333
+ feedback.contextMode ?? null,
1334
+ feedback.runId ?? null,
1335
+ feedback.tokenCount ?? 0,
1336
+ feedback.createdAt,
1337
+ );
1338
+ }
1339
+
1340
+ listFeedback(projectRoot?: string, limit = 100): FeedbackRecord[] {
1341
+ const rows = projectRoot
1342
+ ? this.db.prepare('SELECT * FROM feedback WHERE project_root = ? ORDER BY created_at DESC LIMIT ?').all(projectRoot, limit)
1343
+ : this.db.prepare('SELECT * FROM feedback ORDER BY created_at DESC LIMIT ?').all(limit);
1344
+ return (rows as any[]).map(row => ({
1345
+ id: row.id,
1346
+ projectRoot: row.project_root,
1347
+ query: row.query,
1348
+ rating: row.rating,
1349
+ taskType: row.task_type ?? undefined,
1350
+ contextMode: row.context_mode ?? undefined,
1351
+ runId: row.run_id ?? undefined,
1352
+ tokenCount: row.token_count ?? 0,
1353
+ createdAt: row.created_at,
1354
+ }));
1355
+ }
1356
+
1357
+ getFeedbackSummary(projectRoot?: string): { total: number; hits: number; partials: number; misses: number; hitRate: number } {
1358
+ const row = (projectRoot
1359
+ ? 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)
1360
+ : this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback`).get()) as any;
1361
+ const total = Number(row?.total ?? 0);
1362
+ const hits = Number(row?.hits ?? 0);
1363
+ const partials = Number(row?.partials ?? 0);
1364
+ const misses = Number(row?.misses ?? 0);
1365
+ return { total, hits, partials, misses, hitRate: total > 0 ? hits / total : 0 };
1366
+ }
1367
+
1368
+ // ---- Telemetry Failures ----
1369
+
1370
+ insertTelemetryFailure(failure: TelemetryFailureRecord): void {
1371
+ this.db.prepare(`
1372
+ INSERT OR IGNORE INTO telemetry_failures
1373
+ (id, operation, payload, error, created_at, resolved_at)
1374
+ VALUES (?, ?, ?, ?, ?, ?)
1375
+ `).run(
1376
+ failure.id,
1377
+ failure.operation,
1378
+ JSON.stringify(failure.payload) ?? null,
1379
+ failure.error,
1380
+ failure.createdAt,
1381
+ failure.resolvedAt ?? null,
1382
+ );
1383
+ }
1384
+
1385
+ resolveTelemetryFailure(id: string, resolvedAt = Date.now()): void {
1386
+ this.db.prepare('UPDATE telemetry_failures SET resolved_at = ? WHERE id = ?').run(resolvedAt, id);
1387
+ }
1388
+
1389
+ listTelemetryFailures(options?: { unresolvedOnly?: boolean; limit?: number }): TelemetryFailureRecord[] {
1390
+ const where = options?.unresolvedOnly ? 'WHERE resolved_at IS NULL' : '';
1391
+ const rows = this.db.prepare(`SELECT * FROM telemetry_failures ${where} ORDER BY created_at DESC LIMIT ?`)
1392
+ .all(options?.limit ?? 100) as any[];
1393
+ return rows.map(row => ({
1394
+ id: row.id,
1395
+ operation: row.operation,
1396
+ payload: parseJson(row.payload),
1397
+ error: row.error,
1398
+ createdAt: row.created_at,
1399
+ resolvedAt: row.resolved_at ?? undefined,
1400
+ }));
1401
+ }
1402
+
1403
+ countTelemetryFailures(unresolvedOnly = false): number {
1404
+ const row = this.db.prepare(`SELECT COUNT(*) count FROM telemetry_failures ${unresolvedOnly ? 'WHERE resolved_at IS NULL' : ''}`).get() as any;
1405
+ return Number(row?.count ?? 0);
1406
+ }
1407
+
1408
+ trimTelemetryFailures(maxRows = 500): number {
1409
+ const limit = Math.max(1, Math.floor(maxRows));
1410
+ const result = this.db.prepare(`
1411
+ DELETE FROM telemetry_failures
1412
+ WHERE id IN (
1413
+ SELECT id FROM telemetry_failures
1414
+ ORDER BY created_at DESC, id DESC
1415
+ LIMIT -1 OFFSET ?
1416
+ )
1417
+ `).run(limit);
1418
+ return Number(result.changes);
1419
+ }
1420
+
931
1421
  // ---- Accuracy Queries ----
932
1422
 
933
1423
  insertAccuracyQuery(q: {
@@ -992,6 +1482,320 @@ export class DevFlowDatabase {
992
1482
  return statsByEngine;
993
1483
  }
994
1484
 
1485
+ // ---- Benchmark Reports ----
1486
+
1487
+ insertBenchmarkReport<T extends BenchmarkReportRecord>(report: T): void {
1488
+ this.db.prepare(`
1489
+ INSERT INTO benchmark_reports
1490
+ (run_id, suite_id, suite_version, project_root, commit_sha, task_count, status, report_json, created_at)
1491
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1492
+ ON CONFLICT(run_id) DO UPDATE SET
1493
+ suite_id = excluded.suite_id,
1494
+ suite_version = excluded.suite_version,
1495
+ project_root = excluded.project_root,
1496
+ commit_sha = excluded.commit_sha,
1497
+ task_count = excluded.task_count,
1498
+ status = excluded.status,
1499
+ report_json = excluded.report_json,
1500
+ created_at = excluded.created_at
1501
+ `).run(
1502
+ report.runId,
1503
+ report.suiteId,
1504
+ report.suiteVersion,
1505
+ report.projectRoot,
1506
+ report.commit,
1507
+ report.tasks.length,
1508
+ report.status,
1509
+ JSON.stringify(report),
1510
+ report.createdAt,
1511
+ );
1512
+ }
1513
+
1514
+ getBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(runId: string): T | null {
1515
+ const row = this.db.prepare(
1516
+ 'SELECT report_json FROM benchmark_reports WHERE run_id = ?',
1517
+ ).get(runId) as { report_json?: string } | undefined;
1518
+ return row?.report_json
1519
+ ? JSON.parse(row.report_json) as T
1520
+ : null;
1521
+ }
1522
+
1523
+ getLatestBenchmarkReport<T extends BenchmarkReportRecord = BenchmarkReportRecord>(suiteId?: string): T | null {
1524
+ const row = (suiteId
1525
+ ? this.db.prepare(
1526
+ 'SELECT report_json FROM benchmark_reports WHERE suite_id = ? ORDER BY created_at DESC LIMIT 1',
1527
+ ).get(suiteId)
1528
+ : this.db.prepare(
1529
+ 'SELECT report_json FROM benchmark_reports ORDER BY created_at DESC LIMIT 1',
1530
+ ).get()) as { report_json?: string } | undefined;
1531
+ return row?.report_json
1532
+ ? JSON.parse(row.report_json) as T
1533
+ : null;
1534
+ }
1535
+
1536
+ listBenchmarkReports(limit = 50): BenchmarkReportMetaRecord[] {
1537
+ const rows = this.db.prepare(`
1538
+ SELECT run_id, suite_id, suite_version, project_root, commit_sha,
1539
+ task_count, status, created_at
1540
+ FROM benchmark_reports
1541
+ ORDER BY created_at DESC
1542
+ LIMIT ?
1543
+ `).all(Math.max(1, Math.min(limit, 500))) as Array<{
1544
+ run_id: string;
1545
+ suite_id: string;
1546
+ suite_version: string;
1547
+ project_root: string;
1548
+ commit_sha: string | null;
1549
+ task_count: number;
1550
+ status: 'completed';
1551
+ created_at: number;
1552
+ }>;
1553
+ return rows.map((row) => ({
1554
+ runId: row.run_id,
1555
+ suiteId: row.suite_id,
1556
+ suiteVersion: row.suite_version,
1557
+ projectRoot: row.project_root,
1558
+ commit: row.commit_sha,
1559
+ taskCount: row.task_count,
1560
+ createdAt: row.created_at,
1561
+ status: row.status,
1562
+ }));
1563
+ }
1564
+
1565
+ // ---- Enforcer Governance ----
1566
+
1567
+ listGovernanceRules(includeDisabled = true): GovernanceRuleRecord[] {
1568
+ const where = includeDisabled ? '' : 'WHERE enabled = 1';
1569
+ const rows = this.db.prepare(`
1570
+ SELECT id, name, gate, level, condition_json, action, message,
1571
+ enabled, created_at, updated_at
1572
+ FROM devflow_rules ${where}
1573
+ ORDER BY gate, id
1574
+ `).all() as any[];
1575
+ return rows.map(row => ({
1576
+ id: row.id,
1577
+ name: row.name,
1578
+ gate: row.gate,
1579
+ level: row.level,
1580
+ condition: parseJson(row.condition_json) as Record<string, unknown>,
1581
+ action: row.action,
1582
+ message: row.message,
1583
+ enabled: row.enabled === 1,
1584
+ createdAt: row.created_at,
1585
+ updatedAt: row.updated_at,
1586
+ }));
1587
+ }
1588
+
1589
+ getGovernanceRule(id: string): GovernanceRuleRecord | null {
1590
+ return this.listGovernanceRules(true).find(rule => rule.id === id) ?? null;
1591
+ }
1592
+
1593
+ upsertGovernanceRule(rule: GovernanceRuleRecord): GovernanceRuleRecord {
1594
+ const now = Date.now();
1595
+ this.db.prepare(`
1596
+ INSERT INTO devflow_rules
1597
+ (id, name, gate, level, condition_json, action, message, enabled, created_at, updated_at)
1598
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1599
+ ON CONFLICT(id) DO UPDATE SET
1600
+ name = excluded.name,
1601
+ gate = excluded.gate,
1602
+ level = excluded.level,
1603
+ condition_json = excluded.condition_json,
1604
+ action = excluded.action,
1605
+ message = excluded.message,
1606
+ enabled = excluded.enabled,
1607
+ updated_at = excluded.updated_at
1608
+ `).run(
1609
+ rule.id,
1610
+ rule.name,
1611
+ rule.gate,
1612
+ rule.level,
1613
+ JSON.stringify(rule.condition),
1614
+ rule.action,
1615
+ rule.message,
1616
+ rule.enabled ? 1 : 0,
1617
+ rule.createdAt ?? now,
1618
+ now,
1619
+ );
1620
+ return this.getGovernanceRule(rule.id)!;
1621
+ }
1622
+
1623
+ deleteGovernanceRule(id: string): boolean {
1624
+ return this.db.prepare('DELETE FROM devflow_rules WHERE id = ?').run(id).changes > 0;
1625
+ }
1626
+
1627
+ listGovernanceAudit(limit = 100, offset = 0): { items: GovernanceAuditRecord[]; total: number } {
1628
+ const boundedLimit = Math.max(1, Math.min(limit, 500));
1629
+ const boundedOffset = Math.max(0, offset);
1630
+ const rows = this.db.prepare(`
1631
+ SELECT seq, ts, tool, args_hash, decision, rule_id, session_id, prev_hash, signature
1632
+ FROM devflow_audit_chain
1633
+ ORDER BY seq DESC
1634
+ LIMIT ? OFFSET ?
1635
+ `).all(boundedLimit, boundedOffset) as any[];
1636
+ const count = this.db.prepare('SELECT COUNT(*) AS total FROM devflow_audit_chain').get() as { total?: number } | undefined;
1637
+ return {
1638
+ items: rows.map(row => ({
1639
+ seq: row.seq,
1640
+ ts: row.ts,
1641
+ tool: row.tool,
1642
+ argsHash: row.args_hash,
1643
+ decision: row.decision,
1644
+ ruleId: row.rule_id,
1645
+ sessionId: row.session_id,
1646
+ prevHash: row.prev_hash,
1647
+ signature: row.signature,
1648
+ })),
1649
+ total: Number(count?.total ?? 0),
1650
+ };
1651
+ }
1652
+
1653
+ // ---- Hook Lifecycle ----
1654
+
1655
+ getHookReceipt(projectRoot: string): HookReceiptRecord | null {
1656
+ const row = this.db.prepare(`
1657
+ SELECT project_root, last_mcp_call, bypass_count, updated_at
1658
+ FROM devflow_hook_receipts WHERE project_root = ?
1659
+ `).get(projectRoot) as any;
1660
+ if (!row) return null;
1661
+ return {
1662
+ projectRoot: row.project_root,
1663
+ lastMcpCall: row.last_mcp_call ?? undefined,
1664
+ bypassCount: Number(row.bypass_count ?? 0),
1665
+ updatedAt: Number(row.updated_at),
1666
+ };
1667
+ }
1668
+
1669
+ updateHookReceipt(
1670
+ projectRoot: string,
1671
+ updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>,
1672
+ ): HookReceiptRecord {
1673
+ this.db.exec('BEGIN IMMEDIATE');
1674
+ try {
1675
+ const current = this.getHookReceipt(projectRoot);
1676
+ const next = updater(current);
1677
+ const updatedAt = Date.now();
1678
+ this.db.prepare(`
1679
+ INSERT INTO devflow_hook_receipts
1680
+ (project_root, last_mcp_call, bypass_count, updated_at)
1681
+ VALUES (?, ?, ?, ?)
1682
+ ON CONFLICT(project_root) DO UPDATE SET
1683
+ last_mcp_call = excluded.last_mcp_call,
1684
+ bypass_count = excluded.bypass_count,
1685
+ updated_at = excluded.updated_at
1686
+ `).run(
1687
+ projectRoot,
1688
+ next.lastMcpCall ?? null,
1689
+ next.bypassCount ?? 0,
1690
+ updatedAt,
1691
+ );
1692
+ this.db.exec('COMMIT');
1693
+ return {
1694
+ projectRoot,
1695
+ lastMcpCall: next.lastMcpCall,
1696
+ bypassCount: next.bypassCount ?? 0,
1697
+ updatedAt,
1698
+ };
1699
+ } catch (error) {
1700
+ try { this.db.exec('ROLLBACK'); } catch {}
1701
+ throw error;
1702
+ }
1703
+ }
1704
+
1705
+ deleteHookReceipt(projectRoot: string): boolean {
1706
+ return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
1707
+ .run(projectRoot).changes > 0;
1708
+ }
1709
+
1710
+ upsertContextReceipt(receipt: ContextReceiptRecord): void {
1711
+ this.db.prepare(`
1712
+ INSERT INTO devflow_context_receipts
1713
+ (project_root, session_id, execution_id, context_hash, issued_at, expires_at)
1714
+ VALUES (?, ?, ?, ?, ?, ?)
1715
+ ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
1716
+ context_hash = excluded.context_hash,
1717
+ issued_at = excluded.issued_at,
1718
+ expires_at = excluded.expires_at
1719
+ `).run(
1720
+ receipt.projectRoot,
1721
+ receipt.sessionId,
1722
+ receipt.executionId,
1723
+ receipt.contextHash,
1724
+ receipt.issuedAt,
1725
+ receipt.expiresAt,
1726
+ );
1727
+ }
1728
+
1729
+ getContextReceipt(
1730
+ projectRoot: string,
1731
+ sessionId: string,
1732
+ executionId: string,
1733
+ ): ContextReceiptRecord | null {
1734
+ const row = this.db.prepare(`
1735
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
1736
+ FROM devflow_context_receipts
1737
+ WHERE project_root = ? AND session_id = ? AND execution_id = ?
1738
+ `).get(projectRoot, sessionId, executionId) as any;
1739
+ return row ? {
1740
+ projectRoot: row.project_root,
1741
+ sessionId: row.session_id,
1742
+ executionId: row.execution_id,
1743
+ contextHash: row.context_hash,
1744
+ issuedAt: row.issued_at,
1745
+ expiresAt: row.expires_at,
1746
+ } : null;
1747
+ }
1748
+
1749
+ deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number {
1750
+ return executionId
1751
+ ? this.db.prepare(`DELETE FROM devflow_context_receipts
1752
+ WHERE project_root = ? AND session_id = ? AND execution_id = ?`)
1753
+ .run(projectRoot, sessionId, executionId).changes
1754
+ : this.db.prepare(`DELETE FROM devflow_context_receipts
1755
+ WHERE project_root = ? AND session_id = ?`)
1756
+ .run(projectRoot, sessionId).changes;
1757
+ }
1758
+
1759
+ purgeExpiredContextReceipts(now = Date.now()): number {
1760
+ return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
1761
+ .run(now).changes;
1762
+ }
1763
+
1764
+ recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void {
1765
+ this.db.prepare(`
1766
+ INSERT INTO devflow_memory_distill_checkpoints
1767
+ (id, project_root, session_id, trigger, pending_events, released_leases, created_at)
1768
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1769
+ `).run(
1770
+ checkpoint.id,
1771
+ checkpoint.projectRoot,
1772
+ checkpoint.sessionId ?? null,
1773
+ checkpoint.trigger,
1774
+ checkpoint.pendingEvents,
1775
+ checkpoint.releasedLeases,
1776
+ checkpoint.createdAt,
1777
+ );
1778
+ }
1779
+
1780
+ listMemoryDistillCheckpoints(projectRoot: string, limit = 50): MemoryDistillCheckpointRecord[] {
1781
+ const rows = this.db.prepare(`
1782
+ SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
1783
+ FROM devflow_memory_distill_checkpoints
1784
+ WHERE project_root = ?
1785
+ ORDER BY created_at DESC
1786
+ LIMIT ?
1787
+ `).all(projectRoot, Math.max(1, Math.min(limit, 500))) as any[];
1788
+ return rows.map(row => ({
1789
+ id: row.id,
1790
+ projectRoot: row.project_root,
1791
+ sessionId: row.session_id ?? undefined,
1792
+ trigger: row.trigger,
1793
+ pendingEvents: Number(row.pending_events),
1794
+ releasedLeases: Number(row.released_leases),
1795
+ createdAt: Number(row.created_at),
1796
+ }));
1797
+ }
1798
+
995
1799
  // Convenience methods for raw SQL queries (used by AutoChecker)
996
1800
  all(sql: string, ...params: unknown[]): unknown[] {
997
1801
  return this.db.prepare(sql).all(...params);
@@ -1005,3 +1809,42 @@ export class DevFlowDatabase {
1005
1809
  this.db.close();
1006
1810
  }
1007
1811
  }
1812
+
1813
+ function parseJson(value: unknown): unknown {
1814
+ if (typeof value !== 'string') return value;
1815
+ try { return JSON.parse(value); } catch { return value; }
1816
+ }
1817
+
1818
+ function deriveQuery(input: unknown): string | null {
1819
+ if (!input || typeof input !== 'object') return null;
1820
+ const record = input as Record<string, unknown>;
1821
+ for (const key of ['query', 'task', 'prompt', 'name', 'command']) {
1822
+ if (typeof record[key] === 'string' && record[key]) return record[key] as string;
1823
+ }
1824
+ return null;
1825
+ }
1826
+
1827
+ function deriveEngine(toolName: string): string | null {
1828
+ if (/project_context|symbol|dependency_graph|codegraph/.test(toolName)) return 'codegraph';
1829
+ if (/knowledge|docs/.test(toolName)) return 'knowledge';
1830
+ if (/memory/.test(toolName)) return 'memory';
1831
+ return null;
1832
+ }
1833
+
1834
+ function countTelemetryResults(value: unknown, depth = 0): number {
1835
+ if (value == null || depth > 2) return 0;
1836
+ if (Array.isArray(value)) return value.length;
1837
+ if (typeof value !== 'object') return value === '' ? 0 : 1;
1838
+
1839
+ const record = value as Record<string, unknown>;
1840
+ const unwrapped = record.data ?? record.structuredContent;
1841
+ if (unwrapped !== undefined && unwrapped !== value) return countTelemetryResults(unwrapped, depth + 1);
1842
+
1843
+ let count = 0;
1844
+ for (const key of ['files', 'results', 'result', 'memories', 'nodes', 'chunks', 'findings', 'symbols', 'keySymbols']) {
1845
+ if (Array.isArray(record[key])) count += record[key].length;
1846
+ }
1847
+ if (count > 0) return count;
1848
+ if (record.error) return 0;
1849
+ return Object.keys(record).length > 0 ? 1 : 0;
1850
+ }