@devflow-tools/database 0.16.0 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/__tests__/database.skill-executions.test.ts +21 -6
- package/dist/database.d.ts +32 -0
- package/dist/database.js +256 -41
- package/package.json +2 -2
- package/src/database.ts +296 -47
- package/tsconfig.tsbuildinfo +1 -1
package/src/database.ts
CHANGED
|
@@ -71,6 +71,33 @@ export interface GovernanceAuditRecord {
|
|
|
71
71
|
signature: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
75
|
+
'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function toolNameMatches(event: any, expected: string): boolean {
|
|
79
|
+
if (event.blocked || event.error) return false;
|
|
80
|
+
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
81
|
+
.replace(/^mcp__[^_]+__/, '');
|
|
82
|
+
return name === expected;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function obligationsForSkill(skillName: string, configured: unknown): string[] {
|
|
86
|
+
const configuredTools = Array.isArray(configured)
|
|
87
|
+
? configured.filter((tool): tool is string => typeof tool === 'string' && tool.length > 0)
|
|
88
|
+
: [];
|
|
89
|
+
const family = skillName.split(':').pop() ?? skillName;
|
|
90
|
+
if (CONTEXT_REQUIRED_SKILLS.has(family)) {
|
|
91
|
+
return [`${family}:context`, `${family}:domain`];
|
|
92
|
+
}
|
|
93
|
+
if (family === 'context') return ['get_project_context'];
|
|
94
|
+
if (family === 'graph') return ['get_dependency_graph'];
|
|
95
|
+
if (family === 'knowledge') return ['get_knowledge'];
|
|
96
|
+
if (family === 'memory') return ['get_memory'];
|
|
97
|
+
if (family === 'workflow') return ['run_workflow'];
|
|
98
|
+
return configuredTools.slice(0, 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
export interface HookReceiptRecord {
|
|
75
102
|
projectRoot: string;
|
|
76
103
|
lastMcpCall?: number;
|
|
@@ -78,6 +105,15 @@ export interface HookReceiptRecord {
|
|
|
78
105
|
updatedAt: number;
|
|
79
106
|
}
|
|
80
107
|
|
|
108
|
+
export interface ContextReceiptRecord {
|
|
109
|
+
projectRoot: string;
|
|
110
|
+
sessionId: string;
|
|
111
|
+
executionId: string;
|
|
112
|
+
contextHash: string;
|
|
113
|
+
issuedAt: number;
|
|
114
|
+
expiresAt: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
export interface MemoryDistillCheckpointRecord {
|
|
82
118
|
id: string;
|
|
83
119
|
projectRoot: string;
|
|
@@ -225,7 +261,8 @@ export class DevFlowDatabase {
|
|
|
225
261
|
error TEXT,
|
|
226
262
|
blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
227
263
|
block_reason TEXT,
|
|
228
|
-
failure_category TEXT
|
|
264
|
+
failure_category TEXT,
|
|
265
|
+
tool_use_id TEXT
|
|
229
266
|
);
|
|
230
267
|
|
|
231
268
|
CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
|
|
@@ -248,7 +285,8 @@ export class DevFlowDatabase {
|
|
|
248
285
|
mcp_tool_calls INTEGER DEFAULT 0,
|
|
249
286
|
direct_tool_calls INTEGER DEFAULT 0,
|
|
250
287
|
subagent_count INTEGER DEFAULT 0,
|
|
251
|
-
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
|
|
288
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
|
|
289
|
+
metadata TEXT
|
|
252
290
|
);
|
|
253
291
|
|
|
254
292
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
@@ -315,6 +353,19 @@ export class DevFlowDatabase {
|
|
|
315
353
|
updated_at INTEGER NOT NULL
|
|
316
354
|
);
|
|
317
355
|
|
|
356
|
+
CREATE TABLE IF NOT EXISTS devflow_context_receipts (
|
|
357
|
+
project_root TEXT NOT NULL,
|
|
358
|
+
session_id TEXT NOT NULL,
|
|
359
|
+
execution_id TEXT NOT NULL,
|
|
360
|
+
context_hash TEXT NOT NULL,
|
|
361
|
+
issued_at INTEGER NOT NULL,
|
|
362
|
+
expires_at INTEGER NOT NULL,
|
|
363
|
+
PRIMARY KEY (project_root, session_id, execution_id)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
367
|
+
ON devflow_context_receipts(expires_at);
|
|
368
|
+
|
|
318
369
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
319
370
|
id TEXT PRIMARY KEY,
|
|
320
371
|
project_root TEXT NOT NULL,
|
|
@@ -340,6 +391,13 @@ export class DevFlowDatabase {
|
|
|
340
391
|
// Migration: workflow run persistence
|
|
341
392
|
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT'); } catch {}
|
|
342
393
|
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT'); } catch {}
|
|
394
|
+
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT'); } catch {}
|
|
395
|
+
try { this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT'); } catch {}
|
|
396
|
+
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT'); } catch {}
|
|
397
|
+
try {
|
|
398
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
|
|
399
|
+
ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
|
|
400
|
+
} catch {}
|
|
343
401
|
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1'); } catch {}
|
|
344
402
|
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER'); } catch {}
|
|
345
403
|
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
@@ -631,6 +689,27 @@ export class DevFlowDatabase {
|
|
|
631
689
|
`).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
|
|
632
690
|
}
|
|
633
691
|
|
|
692
|
+
ensureSession(params: {
|
|
693
|
+
id: string;
|
|
694
|
+
projectRoot: string;
|
|
695
|
+
label?: string;
|
|
696
|
+
startedAt?: number;
|
|
697
|
+
}): void {
|
|
698
|
+
if (!params.id.trim()) throw new Error('Canonical session ID is required');
|
|
699
|
+
const startedAt = params.startedAt ?? Date.now();
|
|
700
|
+
this.db.prepare(`
|
|
701
|
+
INSERT INTO sessions (id, project_root, label, started_at, status)
|
|
702
|
+
VALUES (?, ?, ?, ?, 'running')
|
|
703
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
704
|
+
project_root = CASE
|
|
705
|
+
WHEN sessions.project_root = '' OR sessions.project_root = 'unknown' THEN excluded.project_root
|
|
706
|
+
ELSE sessions.project_root
|
|
707
|
+
END,
|
|
708
|
+
label = COALESCE(sessions.label, excluded.label),
|
|
709
|
+
started_at = MIN(sessions.started_at, excluded.started_at)
|
|
710
|
+
`).run(params.id, params.projectRoot || 'unknown', params.label ?? null, startedAt);
|
|
711
|
+
}
|
|
712
|
+
|
|
634
713
|
getSession(id: string): any | null {
|
|
635
714
|
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as any;
|
|
636
715
|
if (!row) return null;
|
|
@@ -646,6 +725,7 @@ export class DevFlowDatabase {
|
|
|
646
725
|
directToolCalls: row.direct_tool_calls,
|
|
647
726
|
subagentCount: row.subagent_count,
|
|
648
727
|
status: row.status,
|
|
728
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
649
729
|
};
|
|
650
730
|
}
|
|
651
731
|
|
|
@@ -672,6 +752,7 @@ export class DevFlowDatabase {
|
|
|
672
752
|
directToolCalls: row.direct_tool_calls,
|
|
673
753
|
subagentCount: row.subagent_count,
|
|
674
754
|
status: row.status,
|
|
755
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
675
756
|
}));
|
|
676
757
|
}
|
|
677
758
|
|
|
@@ -739,6 +820,7 @@ export class DevFlowDatabase {
|
|
|
739
820
|
blocked: row.blocked === 1,
|
|
740
821
|
blockReason: row.block_reason,
|
|
741
822
|
failureCategory: row.failure_category,
|
|
823
|
+
toolUseId: row.tool_use_id,
|
|
742
824
|
tokensUsed: row.tokens_used ?? 0,
|
|
743
825
|
}));
|
|
744
826
|
}
|
|
@@ -775,6 +857,7 @@ export class DevFlowDatabase {
|
|
|
775
857
|
blocked: row.blocked === 1,
|
|
776
858
|
blockReason: row.block_reason,
|
|
777
859
|
failureCategory: row.failure_category,
|
|
860
|
+
toolUseId: row.tool_use_id,
|
|
778
861
|
tokensUsed: row.tokens_used ?? 0,
|
|
779
862
|
});
|
|
780
863
|
}
|
|
@@ -827,6 +910,7 @@ export class DevFlowDatabase {
|
|
|
827
910
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
828
911
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
829
912
|
createdAt: row.created_at,
|
|
913
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
830
914
|
};
|
|
831
915
|
}
|
|
832
916
|
|
|
@@ -840,6 +924,7 @@ export class DevFlowDatabase {
|
|
|
840
924
|
mcpToolCalls?: number;
|
|
841
925
|
directToolCalls?: number;
|
|
842
926
|
subagentCount?: number;
|
|
927
|
+
metadata?: Record<string, unknown>;
|
|
843
928
|
}): void {
|
|
844
929
|
const sets: string[] = [];
|
|
845
930
|
const values: any[] = [];
|
|
@@ -853,6 +938,7 @@ export class DevFlowDatabase {
|
|
|
853
938
|
if (updates.mcpToolCalls !== undefined) { sets.push('mcp_tool_calls = ?'); values.push(updates.mcpToolCalls); }
|
|
854
939
|
if (updates.directToolCalls !== undefined) { sets.push('direct_tool_calls = ?'); values.push(updates.directToolCalls); }
|
|
855
940
|
if (updates.subagentCount !== undefined) { sets.push('subagent_count = ?'); values.push(updates.subagentCount); }
|
|
941
|
+
if (updates.metadata !== undefined) { sets.push('metadata = ?'); values.push(JSON.stringify(updates.metadata)); }
|
|
856
942
|
|
|
857
943
|
if (sets.length > 0) {
|
|
858
944
|
values.push(executionId);
|
|
@@ -886,6 +972,7 @@ export class DevFlowDatabase {
|
|
|
886
972
|
subagentCount: row.subagent_count,
|
|
887
973
|
totalDuration: row.total_duration,
|
|
888
974
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
975
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
889
976
|
}));
|
|
890
977
|
}
|
|
891
978
|
|
|
@@ -914,6 +1001,7 @@ export class DevFlowDatabase {
|
|
|
914
1001
|
workflowRunId?: string;
|
|
915
1002
|
failureCategory?: string;
|
|
916
1003
|
tokensUsed?: number;
|
|
1004
|
+
toolUseId?: string;
|
|
917
1005
|
}): boolean {
|
|
918
1006
|
return this.db.transaction(() => {
|
|
919
1007
|
const result = this.db.prepare(`
|
|
@@ -921,8 +1009,8 @@ export class DevFlowDatabase {
|
|
|
921
1009
|
(event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
922
1010
|
mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
|
|
923
1011
|
parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
|
|
924
|
-
failure_category)
|
|
925
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1012
|
+
failure_category, tool_use_id)
|
|
1013
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
926
1014
|
`).run(
|
|
927
1015
|
params.eventId,
|
|
928
1016
|
params.executionId || '',
|
|
@@ -946,6 +1034,7 @@ export class DevFlowDatabase {
|
|
|
946
1034
|
params.blockReason || null,
|
|
947
1035
|
params.workflowRunId || null,
|
|
948
1036
|
params.failureCategory || null,
|
|
1037
|
+
params.toolUseId || null,
|
|
949
1038
|
);
|
|
950
1039
|
|
|
951
1040
|
if (result.changes === 0) return false;
|
|
@@ -955,14 +1044,37 @@ export class DevFlowDatabase {
|
|
|
955
1044
|
SET total_tool_calls = total_tool_calls + 1,
|
|
956
1045
|
mcp_tool_calls = mcp_tool_calls + ?,
|
|
957
1046
|
direct_tool_calls = direct_tool_calls + ?,
|
|
958
|
-
subagent_count = subagent_count +
|
|
1047
|
+
subagent_count = subagent_count + ?,
|
|
1048
|
+
total_duration = MAX(total_duration, ? - started_at),
|
|
1049
|
+
mcp_compliance_rate = ROUND(
|
|
1050
|
+
100.0 * (mcp_tool_calls + ?) / NULLIF(total_tool_calls + 1, 0), 2
|
|
1051
|
+
)
|
|
959
1052
|
WHERE execution_id = ?
|
|
960
1053
|
`).run(
|
|
961
1054
|
params.isMcpTool ? 1 : 0,
|
|
962
1055
|
params.isMcpTool ? 0 : 1,
|
|
963
1056
|
params.toolType === 'subagent' ? 1 : 0,
|
|
1057
|
+
params.timestamp,
|
|
1058
|
+
params.isMcpTool ? 1 : 0,
|
|
964
1059
|
params.executionId,
|
|
965
1060
|
);
|
|
1061
|
+
if (params.sessionId) {
|
|
1062
|
+
this.db.prepare(`
|
|
1063
|
+
UPDATE sessions
|
|
1064
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
1065
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
1066
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
1067
|
+
subagent_count = subagent_count + ?,
|
|
1068
|
+
duration_ms = MAX(duration_ms, ? - started_at)
|
|
1069
|
+
WHERE id = ?
|
|
1070
|
+
`).run(
|
|
1071
|
+
params.isMcpTool ? 1 : 0,
|
|
1072
|
+
params.isMcpTool ? 0 : 1,
|
|
1073
|
+
params.toolType === 'subagent' ? 1 : 0,
|
|
1074
|
+
params.timestamp,
|
|
1075
|
+
params.sessionId,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
966
1078
|
return true;
|
|
967
1079
|
});
|
|
968
1080
|
}
|
|
@@ -993,6 +1105,7 @@ export class DevFlowDatabase {
|
|
|
993
1105
|
blocked: row.blocked === 1,
|
|
994
1106
|
blockReason: row.block_reason,
|
|
995
1107
|
failureCategory: row.failure_category,
|
|
1108
|
+
toolUseId: row.tool_use_id,
|
|
996
1109
|
}));
|
|
997
1110
|
}
|
|
998
1111
|
|
|
@@ -1023,65 +1136,147 @@ export class DevFlowDatabase {
|
|
|
1023
1136
|
return false;
|
|
1024
1137
|
}
|
|
1025
1138
|
|
|
1139
|
+
markToolCallBlocked(eventId: string, reason: string): boolean {
|
|
1140
|
+
return this.db.prepare(`
|
|
1141
|
+
UPDATE tool_call_events
|
|
1142
|
+
SET blocked = 1, block_reason = ?, error = COALESCE(error, ?)
|
|
1143
|
+
WHERE event_id = ?
|
|
1144
|
+
`).run(reason, reason, eventId).changes > 0;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
reconcileSkillExecution(
|
|
1148
|
+
executionId: string,
|
|
1149
|
+
status: 'completed' | 'failed',
|
|
1150
|
+
finishedAt = Date.now(),
|
|
1151
|
+
metadata?: Record<string, unknown>,
|
|
1152
|
+
): void {
|
|
1153
|
+
const row = this.db.prepare(`
|
|
1154
|
+
SELECT COUNT(*) AS total,
|
|
1155
|
+
SUM(CASE WHEN is_mcp_tool = 1 THEN 1 ELSE 0 END) AS mcp,
|
|
1156
|
+
SUM(CASE WHEN is_mcp_tool = 0 THEN 1 ELSE 0 END) AS direct,
|
|
1157
|
+
SUM(CASE WHEN tool_type = 'subagent' THEN 1 ELSE 0 END) AS subagents,
|
|
1158
|
+
MIN(timestamp) AS first_at,
|
|
1159
|
+
MAX(timestamp) AS last_at
|
|
1160
|
+
FROM tool_call_events WHERE execution_id = ?
|
|
1161
|
+
`).get(executionId) as any;
|
|
1162
|
+
const total = Number(row?.total ?? 0);
|
|
1163
|
+
const mcp = Number(row?.mcp ?? 0);
|
|
1164
|
+
const obligation = this.getExecutionObligationCompliance(executionId);
|
|
1165
|
+
this.updateSkillExecution(executionId, {
|
|
1166
|
+
status,
|
|
1167
|
+
finishedAt,
|
|
1168
|
+
totalToolCalls: total,
|
|
1169
|
+
mcpToolCalls: mcp,
|
|
1170
|
+
directToolCalls: Number(row?.direct ?? 0),
|
|
1171
|
+
subagentCount: Number(row?.subagents ?? 0),
|
|
1172
|
+
totalDuration: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
|
|
1173
|
+
mcpComplianceRate: obligation.rate,
|
|
1174
|
+
metadata,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
getExecutionObligationCompliance(executionId: string): {
|
|
1179
|
+
rate: number;
|
|
1180
|
+
applicable: number;
|
|
1181
|
+
satisfied: number;
|
|
1182
|
+
missedTools: string[];
|
|
1183
|
+
mcpCallShare: number;
|
|
1184
|
+
} {
|
|
1185
|
+
const execution = this.getSkillExecution(executionId);
|
|
1186
|
+
const events = this.listToolCallEvents(executionId);
|
|
1187
|
+
if (!execution) {
|
|
1188
|
+
return { rate: 0, applicable: 0, satisfied: 0, missedTools: [], mcpCallShare: 0 };
|
|
1189
|
+
}
|
|
1190
|
+
const obligations = obligationsForSkill(execution.skillName, execution.requiredMcpTools);
|
|
1191
|
+
const satisfied = obligations.filter((obligation) => {
|
|
1192
|
+
if (obligation.endsWith(':context')) {
|
|
1193
|
+
return events.some((event) => toolNameMatches(event, 'get_project_context'));
|
|
1194
|
+
}
|
|
1195
|
+
if (obligation.endsWith(':domain')) {
|
|
1196
|
+
const family = obligation.slice(0, -':domain'.length);
|
|
1197
|
+
return events.some((event) => {
|
|
1198
|
+
if (event.blocked || event.error || !event.isMcpTool) return false;
|
|
1199
|
+
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
1200
|
+
.replace(/^mcp__[^_]+__/, '');
|
|
1201
|
+
return name.startsWith(`${family}_`);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
return events.some((event) => toolNameMatches(event, obligation));
|
|
1205
|
+
});
|
|
1206
|
+
const missedTools = obligations.filter((obligation) => !satisfied.includes(obligation));
|
|
1207
|
+
const total = events.length;
|
|
1208
|
+
const mcp = events.filter((event) => event.isMcpTool).length;
|
|
1209
|
+
return {
|
|
1210
|
+
rate: obligations.length > 0
|
|
1211
|
+
? Math.round((satisfied.length / obligations.length) * 10000) / 100
|
|
1212
|
+
: 0,
|
|
1213
|
+
applicable: obligations.length,
|
|
1214
|
+
satisfied: satisfied.length,
|
|
1215
|
+
missedTools,
|
|
1216
|
+
mcpCallShare: total > 0 ? Math.round((mcp / total) * 10000) / 100 : 0,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1026
1220
|
// ---- MCP Compliance ----
|
|
1027
1221
|
|
|
1028
1222
|
getMcpCompliance(): {
|
|
1029
1223
|
overall: number;
|
|
1030
1224
|
bySkill: Record<string, number>;
|
|
1031
1225
|
missedTools: string[];
|
|
1226
|
+
applicableObligations: number;
|
|
1227
|
+
satisfiedObligations: number;
|
|
1228
|
+
mcpCallShare: number;
|
|
1032
1229
|
} {
|
|
1033
|
-
|
|
1034
|
-
const overallRow = this.db.prepare(`
|
|
1035
|
-
SELECT
|
|
1036
|
-
SUM(mcp_tool_calls) as mcp_calls,
|
|
1037
|
-
SUM(total_tool_calls) as total_calls
|
|
1038
|
-
FROM skill_executions
|
|
1039
|
-
`).get() as any;
|
|
1040
|
-
|
|
1041
|
-
const overall = overallRow?.total_calls > 0
|
|
1042
|
-
? Math.round((overallRow.mcp_calls / overallRow.total_calls) * 10000) / 100
|
|
1043
|
-
: 0;
|
|
1044
|
-
|
|
1045
|
-
// By skill
|
|
1046
|
-
const bySkillRows = this.db.prepare(`
|
|
1047
|
-
SELECT
|
|
1048
|
-
skill_name,
|
|
1049
|
-
SUM(mcp_tool_calls) as mcp_calls,
|
|
1050
|
-
SUM(total_tool_calls) as total_calls
|
|
1051
|
-
FROM skill_executions
|
|
1052
|
-
GROUP BY skill_name
|
|
1053
|
-
`).all() as any[];
|
|
1054
|
-
|
|
1230
|
+
const executions = this.db.prepare('SELECT execution_id, skill_name FROM skill_executions').all() as any[];
|
|
1055
1231
|
const bySkill: Record<string, number> = {};
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1232
|
+
const bySkillCounts = new Map<string, { applicable: number; satisfied: number }>();
|
|
1233
|
+
let applicableObligations = 0;
|
|
1234
|
+
let satisfiedObligations = 0;
|
|
1235
|
+
let totalCalls = 0;
|
|
1236
|
+
let mcpCalls = 0;
|
|
1237
|
+
const missedTools = new Set<string>();
|
|
1238
|
+
for (const execution of executions) {
|
|
1239
|
+
const result = this.getExecutionObligationCompliance(execution.execution_id);
|
|
1240
|
+
const skill = String(execution.skill_name ?? 'unknown');
|
|
1241
|
+
const skillCounts = bySkillCounts.get(skill) ?? { applicable: 0, satisfied: 0 };
|
|
1242
|
+
skillCounts.applicable += result.applicable;
|
|
1243
|
+
skillCounts.satisfied += result.satisfied;
|
|
1244
|
+
bySkillCounts.set(skill, skillCounts);
|
|
1245
|
+
applicableObligations += result.applicable;
|
|
1246
|
+
satisfiedObligations += result.satisfied;
|
|
1247
|
+
result.missedTools.forEach((tool) => missedTools.add(tool));
|
|
1248
|
+
const events = this.listToolCallEvents(execution.execution_id);
|
|
1249
|
+
totalCalls += events.length;
|
|
1250
|
+
mcpCalls += events.filter((event) => event.isMcpTool).length;
|
|
1251
|
+
}
|
|
1252
|
+
for (const [skill, counts] of bySkillCounts) {
|
|
1253
|
+
bySkill[skill] = counts.applicable > 0
|
|
1254
|
+
? Math.round((counts.satisfied / counts.applicable) * 10000) / 100
|
|
1059
1255
|
: 0;
|
|
1060
1256
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const missedTools = missedRows.map((row: any) => row.mcp_tool_name);
|
|
1072
|
-
|
|
1073
|
-
return { overall, bySkill, missedTools };
|
|
1257
|
+
return {
|
|
1258
|
+
overall: applicableObligations > 0
|
|
1259
|
+
? Math.round((satisfiedObligations / applicableObligations) * 10000) / 100
|
|
1260
|
+
: 0,
|
|
1261
|
+
bySkill,
|
|
1262
|
+
missedTools: [...missedTools].slice(0, 10),
|
|
1263
|
+
applicableObligations,
|
|
1264
|
+
satisfiedObligations,
|
|
1265
|
+
mcpCallShare: totalCalls > 0 ? Math.round((mcpCalls / totalCalls) * 10000) / 100 : 0,
|
|
1266
|
+
};
|
|
1074
1267
|
}
|
|
1075
1268
|
|
|
1076
1269
|
cleanupStaleRecords(): void {
|
|
1077
1270
|
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
1078
1271
|
try {
|
|
1079
1272
|
this.db.prepare(
|
|
1080
|
-
`UPDATE skill_executions SET status = 'failed', finished_at =
|
|
1081
|
-
|
|
1273
|
+
`UPDATE skill_executions SET status = 'failed', finished_at = ?, metadata = ?
|
|
1274
|
+
WHERE status = 'running' AND started_at < ?`
|
|
1275
|
+
).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
1082
1276
|
this.db.prepare(
|
|
1083
|
-
`UPDATE sessions SET status = 'failed', finished_at =
|
|
1084
|
-
|
|
1277
|
+
`UPDATE sessions SET status = 'failed', finished_at = ?, metadata = ?
|
|
1278
|
+
WHERE status = 'running' AND started_at < ?`
|
|
1279
|
+
).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
1085
1280
|
} catch { /* best-effort */ }
|
|
1086
1281
|
}
|
|
1087
1282
|
|
|
@@ -1581,6 +1776,60 @@ export class DevFlowDatabase {
|
|
|
1581
1776
|
.run(projectRoot).changes > 0;
|
|
1582
1777
|
}
|
|
1583
1778
|
|
|
1779
|
+
upsertContextReceipt(receipt: ContextReceiptRecord): void {
|
|
1780
|
+
this.db.prepare(`
|
|
1781
|
+
INSERT INTO devflow_context_receipts
|
|
1782
|
+
(project_root, session_id, execution_id, context_hash, issued_at, expires_at)
|
|
1783
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1784
|
+
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
1785
|
+
context_hash = excluded.context_hash,
|
|
1786
|
+
issued_at = excluded.issued_at,
|
|
1787
|
+
expires_at = excluded.expires_at
|
|
1788
|
+
`).run(
|
|
1789
|
+
receipt.projectRoot,
|
|
1790
|
+
receipt.sessionId,
|
|
1791
|
+
receipt.executionId,
|
|
1792
|
+
receipt.contextHash,
|
|
1793
|
+
receipt.issuedAt,
|
|
1794
|
+
receipt.expiresAt,
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
getContextReceipt(
|
|
1799
|
+
projectRoot: string,
|
|
1800
|
+
sessionId: string,
|
|
1801
|
+
executionId: string,
|
|
1802
|
+
): ContextReceiptRecord | null {
|
|
1803
|
+
const row = this.db.prepare(`
|
|
1804
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
1805
|
+
FROM devflow_context_receipts
|
|
1806
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
1807
|
+
`).get(projectRoot, sessionId, executionId) as any;
|
|
1808
|
+
return row ? {
|
|
1809
|
+
projectRoot: row.project_root,
|
|
1810
|
+
sessionId: row.session_id,
|
|
1811
|
+
executionId: row.execution_id,
|
|
1812
|
+
contextHash: row.context_hash,
|
|
1813
|
+
issuedAt: row.issued_at,
|
|
1814
|
+
expiresAt: row.expires_at,
|
|
1815
|
+
} : null;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number {
|
|
1819
|
+
return executionId
|
|
1820
|
+
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1821
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?`)
|
|
1822
|
+
.run(projectRoot, sessionId, executionId).changes
|
|
1823
|
+
: this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1824
|
+
WHERE project_root = ? AND session_id = ?`)
|
|
1825
|
+
.run(projectRoot, sessionId).changes;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
purgeExpiredContextReceipts(now = Date.now()): number {
|
|
1829
|
+
return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
|
|
1830
|
+
.run(now).changes;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1584
1833
|
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void {
|
|
1585
1834
|
this.db.prepare(`
|
|
1586
1835
|
INSERT INTO devflow_memory_distill_checkpoints
|