@devflow-tools/database 0.16.9 → 0.16.10
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 +16 -0
- package/__tests__/database.memory-turn-receipts.test.ts +71 -0
- package/__tests__/database.skill-executions.test.ts +60 -5
- package/dist/database.d.ts +60 -0
- package/dist/database.js +301 -3
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/database.ts +354 -3
- package/src/index.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.16.10](https://github.com/shilongfeicool/dev-flow/compare/v0.16.9...v0.16.10) (2026-07-24)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **telemetry:** persist compliance failure facts ([fcb5a8b](https://github.com/shilongfeicool/dev-flow/commit/fcb5a8b4db37fc545927262ae841669395491b06))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **memory:** add canonical turn and explicit receipts ([eb3e178](https://github.com/shilongfeicool/dev-flow/commit/eb3e1786e17f5dbc2c4e12e4fa788af7095d335e))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [0.16.9](https://github.com/shilongfeicool/dev-flow/compare/v0.16.8...v0.16.9) (2026-07-23)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @devflow-tools/database
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { DevFlowDatabase } from '../src/database';
|
|
6
|
+
|
|
7
|
+
describe('DevFlowDatabase memory turn receipts', () => {
|
|
8
|
+
let directory: string;
|
|
9
|
+
let database: DevFlowDatabase;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
directory = mkdtempSync(join(tmpdir(), 'devflow-memory-turn-'));
|
|
13
|
+
database = new DevFlowDatabase(directory);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
database.close();
|
|
18
|
+
rmSync(directory, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('commits a pending turn with one canonical receipt', () => {
|
|
22
|
+
database.beginMemoryTurn({
|
|
23
|
+
turnId: 'turn:1',
|
|
24
|
+
projectRoot: '/project',
|
|
25
|
+
sessionId: 'session-1',
|
|
26
|
+
promptHash: 'hash',
|
|
27
|
+
eventId: 'event-1',
|
|
28
|
+
createdAt: 100,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const committed = database.commitMemoryTurn({
|
|
32
|
+
turnId: 'turn:1',
|
|
33
|
+
receiptId: 'memory-receipt:1',
|
|
34
|
+
memoryIds: ['memory-1'],
|
|
35
|
+
source: 'explicit_intent',
|
|
36
|
+
decidedAt: 200,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(committed).toMatchObject({
|
|
40
|
+
status: 'committed',
|
|
41
|
+
receiptId: 'memory-receipt:1',
|
|
42
|
+
memoryIds: ['memory-1'],
|
|
43
|
+
stopPromptedAt: undefined,
|
|
44
|
+
});
|
|
45
|
+
expect(database.getPendingMemoryTurn('/project', 'session-1')).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('marks a pending Stop prompt only once and supports skip receipts', () => {
|
|
49
|
+
database.beginMemoryTurn({
|
|
50
|
+
turnId: 'turn:2',
|
|
51
|
+
projectRoot: '/project',
|
|
52
|
+
sessionId: 'session-1',
|
|
53
|
+
promptHash: 'hash-2',
|
|
54
|
+
eventId: 'event-2',
|
|
55
|
+
createdAt: 300,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(database.markMemoryTurnStopPrompted('turn:2', 350)).toBe(true);
|
|
59
|
+
expect(database.markMemoryTurnStopPrompted('turn:2', 360)).toBe(false);
|
|
60
|
+
expect(database.skipMemoryTurn({
|
|
61
|
+
turnId: 'turn:2',
|
|
62
|
+
receiptId: 'memory-receipt:2',
|
|
63
|
+
reason: 'transient request',
|
|
64
|
+
decidedAt: 400,
|
|
65
|
+
})).toMatchObject({
|
|
66
|
+
status: 'skipped',
|
|
67
|
+
reason: 'transient request',
|
|
68
|
+
stopPromptedAt: 350,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -154,19 +154,19 @@ describe('DevFlowDatabase - Skill Executions', () => {
|
|
|
154
154
|
eventId: 'evt_context_1', executionId: 'exec_001', timestamp: Date.now(),
|
|
155
155
|
toolName: 'mcp__devflow__get_project_context', toolType: 'mcp',
|
|
156
156
|
isMcpTool: true, mcpToolName: 'get_project_context', mcpEnforced: true,
|
|
157
|
-
mcpFallback: false, input: {}, duration: 20, blocked: false,
|
|
157
|
+
mcpFallback: false, input: {}, output: { files: ['src/App.tsx'] }, duration: 20, blocked: false,
|
|
158
158
|
});
|
|
159
159
|
db.insertToolCallEvent({
|
|
160
160
|
eventId: 'evt_001', executionId: 'exec_001', timestamp: Date.now(),
|
|
161
161
|
toolName: 'mcp__devflow__react_diagnose_bug', toolType: 'mcp',
|
|
162
162
|
isMcpTool: true, mcpEnforced: true, mcpFallback: false,
|
|
163
|
-
input: {}, tokensUsed: 100, duration: 500, blocked: false,
|
|
163
|
+
input: {}, output: { findings: [{ id: 'finding-1' }] }, tokensUsed: 100, duration: 500, blocked: false,
|
|
164
164
|
});
|
|
165
165
|
db.insertToolCallEvent({
|
|
166
166
|
eventId: 'evt_002', executionId: 'exec_001', timestamp: Date.now(),
|
|
167
167
|
toolName: 'mcp__devflow__react_review_hooks', toolType: 'mcp',
|
|
168
168
|
isMcpTool: true, mcpEnforced: true, mcpFallback: false,
|
|
169
|
-
input: {}, tokensUsed: 100, duration: 500, blocked: false,
|
|
169
|
+
input: {}, output: { findings: [{ id: 'finding-2' }] }, tokensUsed: 100, duration: 500, blocked: false,
|
|
170
170
|
});
|
|
171
171
|
db.insertToolCallEvent({
|
|
172
172
|
eventId: 'evt_003', executionId: 'exec_001', timestamp: Date.now(),
|
|
@@ -186,13 +186,13 @@ describe('DevFlowDatabase - Skill Executions', () => {
|
|
|
186
186
|
eventId: 'evt_context_2', executionId: 'exec_002', timestamp: Date.now(),
|
|
187
187
|
toolName: 'mcp__devflow__get_project_context', toolType: 'mcp',
|
|
188
188
|
isMcpTool: true, mcpToolName: 'get_project_context', mcpEnforced: true,
|
|
189
|
-
mcpFallback: false, input: {}, duration: 20, blocked: false,
|
|
189
|
+
mcpFallback: false, input: {}, output: { files: ['src/App.vue'] }, duration: 20, blocked: false,
|
|
190
190
|
});
|
|
191
191
|
db.insertToolCallEvent({
|
|
192
192
|
eventId: 'evt_004', executionId: 'exec_002', timestamp: Date.now(),
|
|
193
193
|
toolName: 'mcp__devflow__vue_diagnose_bug', toolType: 'mcp',
|
|
194
194
|
isMcpTool: true, mcpEnforced: true, mcpFallback: false,
|
|
195
|
-
input: {}, tokensUsed: 100, duration: 500, blocked: false,
|
|
195
|
+
input: {}, output: { findings: [{ id: 'finding-vue' }] }, tokensUsed: 100, duration: 500, blocked: false,
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
const compliance = db.getMcpCompliance();
|
|
@@ -204,6 +204,61 @@ describe('DevFlowDatabase - Skill Executions', () => {
|
|
|
204
204
|
expect(compliance.mcpCallShare).toBeCloseTo(83.33, 1);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
it('persists missed tools, actual failures, and fallback reasons on reconciliation', () => {
|
|
208
|
+
db.ensureSession({ id: 'session-facts', projectRoot: '/project', startedAt: 100 });
|
|
209
|
+
db.insertSkillExecution({
|
|
210
|
+
executionId: 'exec-facts',
|
|
211
|
+
sessionId: 'session-facts',
|
|
212
|
+
skillName: 'devflow:react',
|
|
213
|
+
startedAt: 100,
|
|
214
|
+
status: 'running',
|
|
215
|
+
});
|
|
216
|
+
db.insertToolCallEvent({
|
|
217
|
+
eventId: 'event-failed', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 150,
|
|
218
|
+
toolName: 'mcp__devflow__get_project_context', toolType: 'mcp', isMcpTool: true,
|
|
219
|
+
mcpToolName: 'get_project_context', mcpEnforced: true, mcpFallback: false,
|
|
220
|
+
input: {}, error: 'route failed', duration: 20, blocked: false,
|
|
221
|
+
});
|
|
222
|
+
db.insertToolCallEvent({
|
|
223
|
+
eventId: 'event-fallback', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 160,
|
|
224
|
+
toolName: 'Read', toolType: 'direct', isMcpTool: false, mcpEnforced: false,
|
|
225
|
+
mcpFallback: true, input: {}, output: { content: 'fallback' }, duration: 10, blocked: false,
|
|
226
|
+
});
|
|
227
|
+
db.insertToolCallEvent({
|
|
228
|
+
eventId: 'event-memory-receipt', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 165,
|
|
229
|
+
toolName: 'mcp__devflow__memory_commit_turn', toolType: 'mcp', isMcpTool: true,
|
|
230
|
+
mcpToolName: 'memory_commit_turn', mcpEnforced: true, mcpFallback: false,
|
|
231
|
+
input: {}, output: { receiptId: 'memory-receipt:host-commit' }, duration: 5, blocked: false,
|
|
232
|
+
});
|
|
233
|
+
db.insertToolCallEvent({
|
|
234
|
+
eventId: 'event-distill-receipt', executionId: 'exec-facts', sessionId: 'session-facts', timestamp: 166,
|
|
235
|
+
toolName: 'mcp__devflow__memory_save_distilled', toolType: 'mcp', isMcpTool: true,
|
|
236
|
+
mcpToolName: 'memory_save_distilled', mcpEnforced: true, mcpFallback: false,
|
|
237
|
+
input: {}, output: { receipt: { receiptId: 'distill-receipt:batch-1' } }, duration: 5, blocked: false,
|
|
238
|
+
});
|
|
239
|
+
db.insertHookFallback({
|
|
240
|
+
id: 'hook-fallback-1', projectRoot: '/project', sessionId: 'session-facts',
|
|
241
|
+
requestType: 'post-tool-use', tool: 'Read', reason: 'timeout', durationMs: 1_000,
|
|
242
|
+
attempts: 1, createdAt: 170,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
db.reconcileSkillExecution('exec-facts', 'completed', 200);
|
|
246
|
+
|
|
247
|
+
expect(db.getSkillExecution('exec-facts')).toMatchObject({
|
|
248
|
+
missedMcpTools: ['react:context', 'react:domain'],
|
|
249
|
+
failedToolCalls: 1,
|
|
250
|
+
blockedToolCalls: 0,
|
|
251
|
+
fallbackCount: 2,
|
|
252
|
+
fallbackReasons: ['direct_tool_during_context', 'daemon_timeout'],
|
|
253
|
+
metadata: expect.objectContaining({
|
|
254
|
+
actualFailureCount: 1,
|
|
255
|
+
fallbackCount: 2,
|
|
256
|
+
memoryReceiptIds: ['memory-receipt:host-commit'],
|
|
257
|
+
distillReceiptIds: ['distill-receipt:batch-1'],
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
207
262
|
it('aggregates an empty findings collection as an empty result', () => {
|
|
208
263
|
db.insertSkillExecution({
|
|
209
264
|
executionId: 'exec_empty_findings',
|
package/dist/database.d.ts
CHANGED
|
@@ -83,6 +83,34 @@ export interface MemoryDistillCheckpointRecord {
|
|
|
83
83
|
releasedLeases: number;
|
|
84
84
|
createdAt: number;
|
|
85
85
|
}
|
|
86
|
+
export type MemoryTurnStatus = 'pending' | 'committed' | 'skipped';
|
|
87
|
+
export interface MemoryTurnRecord {
|
|
88
|
+
turnId: string;
|
|
89
|
+
projectRoot: string;
|
|
90
|
+
sessionId: string;
|
|
91
|
+
promptHash: string;
|
|
92
|
+
eventId: string;
|
|
93
|
+
status: MemoryTurnStatus;
|
|
94
|
+
receiptId?: string;
|
|
95
|
+
memoryIds: string[];
|
|
96
|
+
source?: string;
|
|
97
|
+
reason?: string;
|
|
98
|
+
stopPromptedAt?: number;
|
|
99
|
+
createdAt: number;
|
|
100
|
+
decidedAt?: number;
|
|
101
|
+
}
|
|
102
|
+
export interface HookFallbackRecord {
|
|
103
|
+
id: string;
|
|
104
|
+
projectRoot: string;
|
|
105
|
+
sessionId?: string;
|
|
106
|
+
toolUseId?: string;
|
|
107
|
+
requestType: string;
|
|
108
|
+
tool: string;
|
|
109
|
+
reason: 'timeout' | 'unreachable' | 'protocol' | 'unknown';
|
|
110
|
+
durationMs: number;
|
|
111
|
+
attempts: number;
|
|
112
|
+
createdAt: number;
|
|
113
|
+
}
|
|
86
114
|
export declare function getGlobalDevFlowDbPath(home?: string): string;
|
|
87
115
|
export declare function openGlobalDevFlowDatabase(home?: string, options?: {
|
|
88
116
|
busyTimeoutMs?: number;
|
|
@@ -145,6 +173,10 @@ export declare class DevFlowDatabase {
|
|
|
145
173
|
totalDuration?: number;
|
|
146
174
|
mcpComplianceRate?: number;
|
|
147
175
|
missedMcpTools?: string[];
|
|
176
|
+
failedToolCalls?: number;
|
|
177
|
+
blockedToolCalls?: number;
|
|
178
|
+
fallbackCount?: number;
|
|
179
|
+
fallbackReasons?: string[];
|
|
148
180
|
totalToolCalls?: number;
|
|
149
181
|
mcpToolCalls?: number;
|
|
150
182
|
directToolCalls?: number;
|
|
@@ -275,6 +307,34 @@ export declare class DevFlowDatabase {
|
|
|
275
307
|
getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
|
|
276
308
|
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number;
|
|
277
309
|
purgeExpiredContextReceipts(now?: number): number;
|
|
310
|
+
beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord;
|
|
311
|
+
getMemoryTurn(turnId: string): MemoryTurnRecord | null;
|
|
312
|
+
getPendingMemoryTurn(projectRoot: string, sessionId: string): MemoryTurnRecord | null;
|
|
313
|
+
commitMemoryTurn(input: {
|
|
314
|
+
turnId: string;
|
|
315
|
+
receiptId: string;
|
|
316
|
+
memoryIds: string[];
|
|
317
|
+
source: string;
|
|
318
|
+
reason?: string;
|
|
319
|
+
decidedAt?: number;
|
|
320
|
+
}): MemoryTurnRecord;
|
|
321
|
+
skipMemoryTurn(input: {
|
|
322
|
+
turnId: string;
|
|
323
|
+
receiptId: string;
|
|
324
|
+
reason: string;
|
|
325
|
+
decidedAt?: number;
|
|
326
|
+
}): MemoryTurnRecord;
|
|
327
|
+
markMemoryTurnStopPrompted(turnId: string, promptedAt?: number): boolean;
|
|
328
|
+
listMemoryTurns(projectRoot: string, sessionId?: string, limit?: number): MemoryTurnRecord[];
|
|
329
|
+
insertHookFallback(record: HookFallbackRecord): boolean;
|
|
330
|
+
listHookFallbacks(filter?: {
|
|
331
|
+
projectRoot?: string;
|
|
332
|
+
sessionId?: string;
|
|
333
|
+
from?: number;
|
|
334
|
+
to?: number;
|
|
335
|
+
limit?: number;
|
|
336
|
+
}): HookFallbackRecord[];
|
|
337
|
+
private mapMemoryTurn;
|
|
278
338
|
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void;
|
|
279
339
|
listMemoryDistillCheckpoints(projectRoot: string, limit?: number): MemoryDistillCheckpointRecord[];
|
|
280
340
|
all(sql: string, ...params: unknown[]): unknown[];
|
package/dist/database.js
CHANGED
|
@@ -11,12 +11,22 @@ const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
|
11
11
|
'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
|
|
12
12
|
]);
|
|
13
13
|
function toolNameMatches(event, expected) {
|
|
14
|
-
if (event
|
|
14
|
+
if (!isEffectiveToolEvent(event))
|
|
15
15
|
return false;
|
|
16
16
|
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
17
17
|
.replace(/^mcp__[^_]+__/, '');
|
|
18
18
|
return name === expected;
|
|
19
19
|
}
|
|
20
|
+
function isEffectiveToolEvent(event) {
|
|
21
|
+
if (event.blocked || event.error || event.output == null)
|
|
22
|
+
return false;
|
|
23
|
+
if (typeof event.output === 'object') {
|
|
24
|
+
const output = event.output;
|
|
25
|
+
if (output.error || output.isError === true || output.success === false || output.degraded === true)
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return countTelemetryResults(event.output) > 0;
|
|
29
|
+
}
|
|
20
30
|
function obligationsForSkill(skillName, configured) {
|
|
21
31
|
const configuredTools = Array.isArray(configured)
|
|
22
32
|
? configured.filter((tool) => typeof tool === 'string' && tool.length > 0)
|
|
@@ -145,6 +155,10 @@ class DevFlowDatabase {
|
|
|
145
155
|
total_duration INTEGER DEFAULT 0,
|
|
146
156
|
mcp_compliance_rate REAL DEFAULT 0,
|
|
147
157
|
missed_mcp_tools TEXT,
|
|
158
|
+
failed_tool_calls INTEGER DEFAULT 0,
|
|
159
|
+
blocked_tool_calls INTEGER DEFAULT 0,
|
|
160
|
+
fallback_count INTEGER DEFAULT 0,
|
|
161
|
+
fallback_reasons TEXT,
|
|
148
162
|
created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)
|
|
149
163
|
);
|
|
150
164
|
|
|
@@ -284,6 +298,43 @@ class DevFlowDatabase {
|
|
|
284
298
|
|
|
285
299
|
CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
|
|
286
300
|
ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
|
|
301
|
+
|
|
302
|
+
CREATE TABLE IF NOT EXISTS devflow_memory_turns (
|
|
303
|
+
turn_id TEXT PRIMARY KEY,
|
|
304
|
+
project_root TEXT NOT NULL,
|
|
305
|
+
session_id TEXT NOT NULL,
|
|
306
|
+
prompt_hash TEXT NOT NULL,
|
|
307
|
+
event_id TEXT NOT NULL,
|
|
308
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
309
|
+
CHECK(status IN ('pending', 'committed', 'skipped')),
|
|
310
|
+
receipt_id TEXT,
|
|
311
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
312
|
+
source TEXT,
|
|
313
|
+
reason TEXT,
|
|
314
|
+
stop_prompted_at INTEGER,
|
|
315
|
+
created_at INTEGER NOT NULL,
|
|
316
|
+
decided_at INTEGER
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
CREATE INDEX IF NOT EXISTS idx_memory_turns_pending
|
|
320
|
+
ON devflow_memory_turns(project_root, session_id, status, created_at DESC);
|
|
321
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
|
|
322
|
+
ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
323
|
+
|
|
324
|
+
CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
|
|
325
|
+
id TEXT PRIMARY KEY,
|
|
326
|
+
project_root TEXT NOT NULL,
|
|
327
|
+
session_id TEXT,
|
|
328
|
+
tool_use_id TEXT,
|
|
329
|
+
request_type TEXT NOT NULL,
|
|
330
|
+
tool TEXT NOT NULL,
|
|
331
|
+
reason TEXT NOT NULL CHECK(reason IN ('timeout', 'unreachable', 'protocol', 'unknown')),
|
|
332
|
+
duration_ms INTEGER NOT NULL,
|
|
333
|
+
attempts INTEGER NOT NULL,
|
|
334
|
+
created_at INTEGER NOT NULL
|
|
335
|
+
);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_hook_fallbacks_session
|
|
337
|
+
ON devflow_hook_fallbacks(project_root, session_id, created_at DESC);
|
|
287
338
|
`);
|
|
288
339
|
// Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
|
|
289
340
|
try {
|
|
@@ -326,6 +377,22 @@ class DevFlowDatabase {
|
|
|
326
377
|
this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT');
|
|
327
378
|
}
|
|
328
379
|
catch { }
|
|
380
|
+
try {
|
|
381
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN failed_tool_calls INTEGER DEFAULT 0');
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
try {
|
|
385
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN blocked_tool_calls INTEGER DEFAULT 0');
|
|
386
|
+
}
|
|
387
|
+
catch { }
|
|
388
|
+
try {
|
|
389
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN fallback_count INTEGER DEFAULT 0');
|
|
390
|
+
}
|
|
391
|
+
catch { }
|
|
392
|
+
try {
|
|
393
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN fallback_reasons TEXT');
|
|
394
|
+
}
|
|
395
|
+
catch { }
|
|
329
396
|
try {
|
|
330
397
|
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
|
|
331
398
|
ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
|
|
@@ -765,6 +832,10 @@ class DevFlowDatabase {
|
|
|
765
832
|
totalDuration: row.total_duration,
|
|
766
833
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
767
834
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
835
|
+
failedToolCalls: row.failed_tool_calls ?? 0,
|
|
836
|
+
blockedToolCalls: row.blocked_tool_calls ?? 0,
|
|
837
|
+
fallbackCount: row.fallback_count ?? 0,
|
|
838
|
+
fallbackReasons: row.fallback_reasons ? JSON.parse(row.fallback_reasons) : [],
|
|
768
839
|
createdAt: row.created_at,
|
|
769
840
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
770
841
|
};
|
|
@@ -792,6 +863,22 @@ class DevFlowDatabase {
|
|
|
792
863
|
sets.push('missed_mcp_tools = ?');
|
|
793
864
|
values.push(JSON.stringify(updates.missedMcpTools));
|
|
794
865
|
}
|
|
866
|
+
if (updates.failedToolCalls !== undefined) {
|
|
867
|
+
sets.push('failed_tool_calls = ?');
|
|
868
|
+
values.push(updates.failedToolCalls);
|
|
869
|
+
}
|
|
870
|
+
if (updates.blockedToolCalls !== undefined) {
|
|
871
|
+
sets.push('blocked_tool_calls = ?');
|
|
872
|
+
values.push(updates.blockedToolCalls);
|
|
873
|
+
}
|
|
874
|
+
if (updates.fallbackCount !== undefined) {
|
|
875
|
+
sets.push('fallback_count = ?');
|
|
876
|
+
values.push(updates.fallbackCount);
|
|
877
|
+
}
|
|
878
|
+
if (updates.fallbackReasons !== undefined) {
|
|
879
|
+
sets.push('fallback_reasons = ?');
|
|
880
|
+
values.push(JSON.stringify(updates.fallbackReasons));
|
|
881
|
+
}
|
|
795
882
|
if (updates.totalToolCalls !== undefined) {
|
|
796
883
|
sets.push('total_tool_calls = ?');
|
|
797
884
|
values.push(updates.totalToolCalls);
|
|
@@ -840,6 +927,11 @@ class DevFlowDatabase {
|
|
|
840
927
|
subagentCount: row.subagent_count,
|
|
841
928
|
totalDuration: row.total_duration,
|
|
842
929
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
930
|
+
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : [],
|
|
931
|
+
failedToolCalls: row.failed_tool_calls ?? 0,
|
|
932
|
+
blockedToolCalls: row.blocked_tool_calls ?? 0,
|
|
933
|
+
fallbackCount: row.fallback_count ?? 0,
|
|
934
|
+
fallbackReasons: row.fallback_reasons ? JSON.parse(row.fallback_reasons) : [],
|
|
843
935
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
844
936
|
}));
|
|
845
937
|
}
|
|
@@ -948,9 +1040,39 @@ class DevFlowDatabase {
|
|
|
948
1040
|
FROM tool_call_events WHERE execution_id = ?
|
|
949
1041
|
`).get(executionId);
|
|
950
1042
|
const execution = this.getSkillExecution(executionId);
|
|
1043
|
+
const events = this.listToolCallEvents(executionId);
|
|
951
1044
|
const total = Number(row?.total ?? 0);
|
|
952
1045
|
const mcp = Number(row?.mcp ?? 0);
|
|
953
1046
|
const obligation = this.getExecutionObligationCompliance(executionId);
|
|
1047
|
+
const failures = events.filter(event => Boolean(event.error) && !event.blocked);
|
|
1048
|
+
const blocked = events.filter(event => event.blocked);
|
|
1049
|
+
const directFallbacks = events.filter(event => event.mcpFallback);
|
|
1050
|
+
const hookFallbacks = execution?.sessionId
|
|
1051
|
+
? this.listHookFallbacks({
|
|
1052
|
+
sessionId: execution.sessionId,
|
|
1053
|
+
from: execution.startedAt,
|
|
1054
|
+
to: finishedAt,
|
|
1055
|
+
})
|
|
1056
|
+
: [];
|
|
1057
|
+
const fallbackReasons = [...new Set([
|
|
1058
|
+
...directFallbacks.map(() => 'direct_tool_during_context'),
|
|
1059
|
+
...hookFallbacks.map(fallback => `daemon_${fallback.reason}`),
|
|
1060
|
+
])];
|
|
1061
|
+
const eventReceiptIds = collectCanonicalReceiptIds(events.map(event => event.output));
|
|
1062
|
+
const suppliedMemoryReceiptIds = Array.isArray(metadata?.memoryReceiptIds)
|
|
1063
|
+
? metadata.memoryReceiptIds.filter((id) => typeof id === 'string')
|
|
1064
|
+
: [];
|
|
1065
|
+
const suppliedDistillReceiptIds = Array.isArray(metadata?.distillReceiptIds)
|
|
1066
|
+
? metadata.distillReceiptIds.filter((id) => typeof id === 'string')
|
|
1067
|
+
: [];
|
|
1068
|
+
const memoryReceiptIds = [...new Set([
|
|
1069
|
+
...suppliedMemoryReceiptIds,
|
|
1070
|
+
...eventReceiptIds.filter(id => id.startsWith('memory-receipt:')),
|
|
1071
|
+
])];
|
|
1072
|
+
const distillReceiptIds = [...new Set([
|
|
1073
|
+
...suppliedDistillReceiptIds,
|
|
1074
|
+
...eventReceiptIds.filter(id => id.startsWith('distill-receipt:')),
|
|
1075
|
+
])];
|
|
954
1076
|
this.updateSkillExecution(executionId, {
|
|
955
1077
|
status,
|
|
956
1078
|
finishedAt,
|
|
@@ -962,7 +1084,23 @@ class DevFlowDatabase {
|
|
|
962
1084
|
? Math.max(0, finishedAt - execution.startedAt)
|
|
963
1085
|
: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
|
|
964
1086
|
mcpComplianceRate: obligation.rate,
|
|
965
|
-
|
|
1087
|
+
missedMcpTools: obligation.missedTools,
|
|
1088
|
+
failedToolCalls: failures.length,
|
|
1089
|
+
blockedToolCalls: blocked.length,
|
|
1090
|
+
fallbackCount: directFallbacks.length + hookFallbacks.length,
|
|
1091
|
+
fallbackReasons,
|
|
1092
|
+
metadata: {
|
|
1093
|
+
...(metadata ?? {}),
|
|
1094
|
+
applicableObligations: obligation.applicable,
|
|
1095
|
+
satisfiedObligations: obligation.satisfied,
|
|
1096
|
+
missedTools: obligation.missedTools,
|
|
1097
|
+
actualFailureCount: failures.length,
|
|
1098
|
+
blockedCount: blocked.length,
|
|
1099
|
+
fallbackCount: directFallbacks.length + hookFallbacks.length,
|
|
1100
|
+
fallbackReasons,
|
|
1101
|
+
memoryReceiptIds,
|
|
1102
|
+
distillReceiptIds,
|
|
1103
|
+
},
|
|
966
1104
|
});
|
|
967
1105
|
}
|
|
968
1106
|
getExecutionObligationCompliance(executionId) {
|
|
@@ -979,7 +1117,7 @@ class DevFlowDatabase {
|
|
|
979
1117
|
if (obligation.endsWith(':domain')) {
|
|
980
1118
|
const family = obligation.slice(0, -':domain'.length);
|
|
981
1119
|
return events.some((event) => {
|
|
982
|
-
if (event.
|
|
1120
|
+
if (!event.isMcpTool || !isEffectiveToolEvent(event))
|
|
983
1121
|
return false;
|
|
984
1122
|
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
985
1123
|
.replace(/^mcp__[^_]+__/, '');
|
|
@@ -1477,6 +1615,137 @@ class DevFlowDatabase {
|
|
|
1477
1615
|
return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
|
|
1478
1616
|
.run(now).changes;
|
|
1479
1617
|
}
|
|
1618
|
+
beginMemoryTurn(input) {
|
|
1619
|
+
this.db.prepare(`
|
|
1620
|
+
INSERT OR IGNORE INTO devflow_memory_turns
|
|
1621
|
+
(turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
|
|
1622
|
+
VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
|
|
1623
|
+
`).run(input.turnId, input.projectRoot, input.sessionId, input.promptHash, input.eventId, input.createdAt);
|
|
1624
|
+
return this.getMemoryTurn(input.turnId);
|
|
1625
|
+
}
|
|
1626
|
+
getMemoryTurn(turnId) {
|
|
1627
|
+
const row = this.db.prepare('SELECT * FROM devflow_memory_turns WHERE turn_id = ?').get(turnId);
|
|
1628
|
+
return row ? this.mapMemoryTurn(row) : null;
|
|
1629
|
+
}
|
|
1630
|
+
getPendingMemoryTurn(projectRoot, sessionId) {
|
|
1631
|
+
const row = this.db.prepare(`
|
|
1632
|
+
SELECT * FROM devflow_memory_turns
|
|
1633
|
+
WHERE project_root = ? AND session_id = ? AND status = 'pending'
|
|
1634
|
+
ORDER BY created_at DESC LIMIT 1
|
|
1635
|
+
`).get(projectRoot, sessionId);
|
|
1636
|
+
return row ? this.mapMemoryTurn(row) : null;
|
|
1637
|
+
}
|
|
1638
|
+
commitMemoryTurn(input) {
|
|
1639
|
+
this.db.prepare(`
|
|
1640
|
+
UPDATE devflow_memory_turns
|
|
1641
|
+
SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
|
|
1642
|
+
WHERE turn_id = ? AND status = 'pending'
|
|
1643
|
+
`).run(input.receiptId, JSON.stringify([...new Set(input.memoryIds)]), input.source, input.reason ?? null, input.decidedAt ?? Date.now(), input.turnId);
|
|
1644
|
+
const turn = this.getMemoryTurn(input.turnId);
|
|
1645
|
+
if (!turn || turn.status !== 'committed') {
|
|
1646
|
+
throw new Error(`Memory turn ${input.turnId} is not pending or does not exist`);
|
|
1647
|
+
}
|
|
1648
|
+
return turn;
|
|
1649
|
+
}
|
|
1650
|
+
skipMemoryTurn(input) {
|
|
1651
|
+
this.db.prepare(`
|
|
1652
|
+
UPDATE devflow_memory_turns
|
|
1653
|
+
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
|
|
1654
|
+
reason = ?, decided_at = ?
|
|
1655
|
+
WHERE turn_id = ? AND status = 'pending'
|
|
1656
|
+
`).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(), input.turnId);
|
|
1657
|
+
const turn = this.getMemoryTurn(input.turnId);
|
|
1658
|
+
if (!turn || turn.status !== 'skipped') {
|
|
1659
|
+
throw new Error(`Memory turn ${input.turnId} is not pending or does not exist`);
|
|
1660
|
+
}
|
|
1661
|
+
return turn;
|
|
1662
|
+
}
|
|
1663
|
+
markMemoryTurnStopPrompted(turnId, promptedAt = Date.now()) {
|
|
1664
|
+
return this.db.prepare(`
|
|
1665
|
+
UPDATE devflow_memory_turns SET stop_prompted_at = ?
|
|
1666
|
+
WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
|
|
1667
|
+
`).run(promptedAt, turnId).changes === 1;
|
|
1668
|
+
}
|
|
1669
|
+
listMemoryTurns(projectRoot, sessionId, limit = 50) {
|
|
1670
|
+
const rows = (sessionId
|
|
1671
|
+
? this.db.prepare(`SELECT * FROM devflow_memory_turns
|
|
1672
|
+
WHERE project_root = ? AND session_id = ? ORDER BY created_at DESC LIMIT ?`)
|
|
1673
|
+
.all(projectRoot, sessionId, limit)
|
|
1674
|
+
: this.db.prepare(`SELECT * FROM devflow_memory_turns
|
|
1675
|
+
WHERE project_root = ? ORDER BY created_at DESC LIMIT ?`)
|
|
1676
|
+
.all(projectRoot, limit));
|
|
1677
|
+
return rows.map(row => this.mapMemoryTurn(row));
|
|
1678
|
+
}
|
|
1679
|
+
insertHookFallback(record) {
|
|
1680
|
+
return this.db.prepare(`
|
|
1681
|
+
INSERT OR IGNORE INTO devflow_hook_fallbacks
|
|
1682
|
+
(id, project_root, session_id, tool_use_id, request_type, tool, reason,
|
|
1683
|
+
duration_ms, attempts, created_at)
|
|
1684
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1685
|
+
`).run(record.id, record.projectRoot, record.sessionId ?? null, record.toolUseId ?? null, record.requestType, record.tool, record.reason, record.durationMs, record.attempts, record.createdAt).changes === 1;
|
|
1686
|
+
}
|
|
1687
|
+
listHookFallbacks(filter = {}) {
|
|
1688
|
+
const conditions = [];
|
|
1689
|
+
const values = [];
|
|
1690
|
+
if (filter.projectRoot) {
|
|
1691
|
+
conditions.push('project_root = ?');
|
|
1692
|
+
values.push(filter.projectRoot);
|
|
1693
|
+
}
|
|
1694
|
+
if (filter.sessionId) {
|
|
1695
|
+
conditions.push('session_id = ?');
|
|
1696
|
+
values.push(filter.sessionId);
|
|
1697
|
+
}
|
|
1698
|
+
if (filter.from !== undefined) {
|
|
1699
|
+
conditions.push('created_at >= ?');
|
|
1700
|
+
values.push(filter.from);
|
|
1701
|
+
}
|
|
1702
|
+
if (filter.to !== undefined) {
|
|
1703
|
+
conditions.push('created_at <= ?');
|
|
1704
|
+
values.push(filter.to);
|
|
1705
|
+
}
|
|
1706
|
+
values.push(Math.max(1, Math.min(filter.limit ?? 500, 5000)));
|
|
1707
|
+
const rows = this.db.prepare(`
|
|
1708
|
+
SELECT * FROM devflow_hook_fallbacks
|
|
1709
|
+
${conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''}
|
|
1710
|
+
ORDER BY created_at DESC LIMIT ?
|
|
1711
|
+
`).all(...values);
|
|
1712
|
+
return rows.map(row => ({
|
|
1713
|
+
id: row.id,
|
|
1714
|
+
projectRoot: row.project_root,
|
|
1715
|
+
sessionId: row.session_id ?? undefined,
|
|
1716
|
+
toolUseId: row.tool_use_id ?? undefined,
|
|
1717
|
+
requestType: row.request_type,
|
|
1718
|
+
tool: row.tool,
|
|
1719
|
+
reason: row.reason,
|
|
1720
|
+
durationMs: row.duration_ms,
|
|
1721
|
+
attempts: row.attempts,
|
|
1722
|
+
createdAt: row.created_at,
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
mapMemoryTurn(row) {
|
|
1726
|
+
let memoryIds = [];
|
|
1727
|
+
try {
|
|
1728
|
+
const parsed = JSON.parse(row.memory_ids ?? '[]');
|
|
1729
|
+
if (Array.isArray(parsed))
|
|
1730
|
+
memoryIds = parsed.filter((id) => typeof id === 'string');
|
|
1731
|
+
}
|
|
1732
|
+
catch { }
|
|
1733
|
+
return {
|
|
1734
|
+
turnId: row.turn_id,
|
|
1735
|
+
projectRoot: row.project_root,
|
|
1736
|
+
sessionId: row.session_id,
|
|
1737
|
+
promptHash: row.prompt_hash,
|
|
1738
|
+
eventId: row.event_id,
|
|
1739
|
+
status: row.status,
|
|
1740
|
+
receiptId: row.receipt_id ?? undefined,
|
|
1741
|
+
memoryIds,
|
|
1742
|
+
source: row.source ?? undefined,
|
|
1743
|
+
reason: row.reason ?? undefined,
|
|
1744
|
+
stopPromptedAt: row.stop_prompted_at ?? undefined,
|
|
1745
|
+
createdAt: row.created_at,
|
|
1746
|
+
decidedAt: row.decided_at ?? undefined,
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1480
1749
|
recordMemoryDistillCheckpoint(checkpoint) {
|
|
1481
1750
|
this.db.prepare(`
|
|
1482
1751
|
INSERT INTO devflow_memory_distill_checkpoints
|
|
@@ -1570,3 +1839,32 @@ function countTelemetryResults(value, depth = 0) {
|
|
|
1570
1839
|
return 0;
|
|
1571
1840
|
return Object.keys(record).length > 0 ? 1 : 0;
|
|
1572
1841
|
}
|
|
1842
|
+
function collectCanonicalReceiptIds(values) {
|
|
1843
|
+
const receiptIds = new Set();
|
|
1844
|
+
const visit = (value, depth) => {
|
|
1845
|
+
if (depth > 5 || value == null)
|
|
1846
|
+
return;
|
|
1847
|
+
if (typeof value === 'string') {
|
|
1848
|
+
if (value.startsWith('memory-receipt:') || value.startsWith('distill-receipt:')) {
|
|
1849
|
+
receiptIds.add(value);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if ((value.startsWith('{') || value.startsWith('[')) && value.length < 1000000) {
|
|
1853
|
+
try {
|
|
1854
|
+
visit(JSON.parse(value), depth + 1);
|
|
1855
|
+
}
|
|
1856
|
+
catch { }
|
|
1857
|
+
}
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (Array.isArray(value)) {
|
|
1861
|
+
value.forEach(item => visit(item, depth + 1));
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (typeof value === 'object') {
|
|
1865
|
+
Object.values(value).forEach(item => visit(item, depth + 1));
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
values.forEach(value => visit(value, 0));
|
|
1869
|
+
return [...receiptIds];
|
|
1870
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
|
|
2
|
-
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookReceiptRecord, MemoryDistillCheckpointRecord, TelemetryFailureRecord, } from './database';
|
|
2
|
+
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -76,12 +76,21 @@ const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
|
76
76
|
]);
|
|
77
77
|
|
|
78
78
|
function toolNameMatches(event: any, expected: string): boolean {
|
|
79
|
-
if (event
|
|
79
|
+
if (!isEffectiveToolEvent(event)) return false;
|
|
80
80
|
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
81
81
|
.replace(/^mcp__[^_]+__/, '');
|
|
82
82
|
return name === expected;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function isEffectiveToolEvent(event: any): boolean {
|
|
86
|
+
if (event.blocked || event.error || event.output == null) return false;
|
|
87
|
+
if (typeof event.output === 'object') {
|
|
88
|
+
const output = event.output as Record<string, unknown>;
|
|
89
|
+
if (output.error || output.isError === true || output.success === false || output.degraded === true) return false;
|
|
90
|
+
}
|
|
91
|
+
return countTelemetryResults(event.output) > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
function obligationsForSkill(skillName: string, configured: unknown): string[] {
|
|
86
95
|
const configuredTools = Array.isArray(configured)
|
|
87
96
|
? configured.filter((tool): tool is string => typeof tool === 'string' && tool.length > 0)
|
|
@@ -124,6 +133,37 @@ export interface MemoryDistillCheckpointRecord {
|
|
|
124
133
|
createdAt: number;
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
export type MemoryTurnStatus = 'pending' | 'committed' | 'skipped';
|
|
137
|
+
|
|
138
|
+
export interface MemoryTurnRecord {
|
|
139
|
+
turnId: string;
|
|
140
|
+
projectRoot: string;
|
|
141
|
+
sessionId: string;
|
|
142
|
+
promptHash: string;
|
|
143
|
+
eventId: string;
|
|
144
|
+
status: MemoryTurnStatus;
|
|
145
|
+
receiptId?: string;
|
|
146
|
+
memoryIds: string[];
|
|
147
|
+
source?: string;
|
|
148
|
+
reason?: string;
|
|
149
|
+
stopPromptedAt?: number;
|
|
150
|
+
createdAt: number;
|
|
151
|
+
decidedAt?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface HookFallbackRecord {
|
|
155
|
+
id: string;
|
|
156
|
+
projectRoot: string;
|
|
157
|
+
sessionId?: string;
|
|
158
|
+
toolUseId?: string;
|
|
159
|
+
requestType: string;
|
|
160
|
+
tool: string;
|
|
161
|
+
reason: 'timeout' | 'unreachable' | 'protocol' | 'unknown';
|
|
162
|
+
durationMs: number;
|
|
163
|
+
attempts: number;
|
|
164
|
+
createdAt: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
127
167
|
export function getGlobalDevFlowDbPath(home = homedir()): string {
|
|
128
168
|
const stateDir = process.env.DEVFLOW_STATE_DIR ?? join(home, '.devflow', 'global');
|
|
129
169
|
return join(stateDir, 'devflow.db');
|
|
@@ -239,6 +279,10 @@ export class DevFlowDatabase {
|
|
|
239
279
|
total_duration INTEGER DEFAULT 0,
|
|
240
280
|
mcp_compliance_rate REAL DEFAULT 0,
|
|
241
281
|
missed_mcp_tools TEXT,
|
|
282
|
+
failed_tool_calls INTEGER DEFAULT 0,
|
|
283
|
+
blocked_tool_calls INTEGER DEFAULT 0,
|
|
284
|
+
fallback_count INTEGER DEFAULT 0,
|
|
285
|
+
fallback_reasons TEXT,
|
|
242
286
|
created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)
|
|
243
287
|
);
|
|
244
288
|
|
|
@@ -378,6 +422,43 @@ export class DevFlowDatabase {
|
|
|
378
422
|
|
|
379
423
|
CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
|
|
380
424
|
ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
|
|
425
|
+
|
|
426
|
+
CREATE TABLE IF NOT EXISTS devflow_memory_turns (
|
|
427
|
+
turn_id TEXT PRIMARY KEY,
|
|
428
|
+
project_root TEXT NOT NULL,
|
|
429
|
+
session_id TEXT NOT NULL,
|
|
430
|
+
prompt_hash TEXT NOT NULL,
|
|
431
|
+
event_id TEXT NOT NULL,
|
|
432
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
433
|
+
CHECK(status IN ('pending', 'committed', 'skipped')),
|
|
434
|
+
receipt_id TEXT,
|
|
435
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
436
|
+
source TEXT,
|
|
437
|
+
reason TEXT,
|
|
438
|
+
stop_prompted_at INTEGER,
|
|
439
|
+
created_at INTEGER NOT NULL,
|
|
440
|
+
decided_at INTEGER
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
CREATE INDEX IF NOT EXISTS idx_memory_turns_pending
|
|
444
|
+
ON devflow_memory_turns(project_root, session_id, status, created_at DESC);
|
|
445
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
|
|
446
|
+
ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
447
|
+
|
|
448
|
+
CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
|
|
449
|
+
id TEXT PRIMARY KEY,
|
|
450
|
+
project_root TEXT NOT NULL,
|
|
451
|
+
session_id TEXT,
|
|
452
|
+
tool_use_id TEXT,
|
|
453
|
+
request_type TEXT NOT NULL,
|
|
454
|
+
tool TEXT NOT NULL,
|
|
455
|
+
reason TEXT NOT NULL CHECK(reason IN ('timeout', 'unreachable', 'protocol', 'unknown')),
|
|
456
|
+
duration_ms INTEGER NOT NULL,
|
|
457
|
+
attempts INTEGER NOT NULL,
|
|
458
|
+
created_at INTEGER NOT NULL
|
|
459
|
+
);
|
|
460
|
+
CREATE INDEX IF NOT EXISTS idx_hook_fallbacks_session
|
|
461
|
+
ON devflow_hook_fallbacks(project_root, session_id, created_at DESC);
|
|
381
462
|
`);
|
|
382
463
|
|
|
383
464
|
// Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
|
|
@@ -394,6 +475,10 @@ export class DevFlowDatabase {
|
|
|
394
475
|
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT'); } catch {}
|
|
395
476
|
try { this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT'); } catch {}
|
|
396
477
|
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT'); } catch {}
|
|
478
|
+
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN failed_tool_calls INTEGER DEFAULT 0'); } catch {}
|
|
479
|
+
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN blocked_tool_calls INTEGER DEFAULT 0'); } catch {}
|
|
480
|
+
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN fallback_count INTEGER DEFAULT 0'); } catch {}
|
|
481
|
+
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN fallback_reasons TEXT'); } catch {}
|
|
397
482
|
try {
|
|
398
483
|
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
|
|
399
484
|
ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
|
|
@@ -925,6 +1010,10 @@ export class DevFlowDatabase {
|
|
|
925
1010
|
totalDuration: row.total_duration,
|
|
926
1011
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
927
1012
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
1013
|
+
failedToolCalls: row.failed_tool_calls ?? 0,
|
|
1014
|
+
blockedToolCalls: row.blocked_tool_calls ?? 0,
|
|
1015
|
+
fallbackCount: row.fallback_count ?? 0,
|
|
1016
|
+
fallbackReasons: row.fallback_reasons ? JSON.parse(row.fallback_reasons) : [],
|
|
928
1017
|
createdAt: row.created_at,
|
|
929
1018
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
930
1019
|
};
|
|
@@ -936,6 +1025,10 @@ export class DevFlowDatabase {
|
|
|
936
1025
|
totalDuration?: number;
|
|
937
1026
|
mcpComplianceRate?: number;
|
|
938
1027
|
missedMcpTools?: string[];
|
|
1028
|
+
failedToolCalls?: number;
|
|
1029
|
+
blockedToolCalls?: number;
|
|
1030
|
+
fallbackCount?: number;
|
|
1031
|
+
fallbackReasons?: string[];
|
|
939
1032
|
totalToolCalls?: number;
|
|
940
1033
|
mcpToolCalls?: number;
|
|
941
1034
|
directToolCalls?: number;
|
|
@@ -950,6 +1043,10 @@ export class DevFlowDatabase {
|
|
|
950
1043
|
if (updates.totalDuration !== undefined) { sets.push('total_duration = ?'); values.push(updates.totalDuration); }
|
|
951
1044
|
if (updates.mcpComplianceRate !== undefined) { sets.push('mcp_compliance_rate = ?'); values.push(updates.mcpComplianceRate); }
|
|
952
1045
|
if (updates.missedMcpTools !== undefined) { sets.push('missed_mcp_tools = ?'); values.push(JSON.stringify(updates.missedMcpTools)); }
|
|
1046
|
+
if (updates.failedToolCalls !== undefined) { sets.push('failed_tool_calls = ?'); values.push(updates.failedToolCalls); }
|
|
1047
|
+
if (updates.blockedToolCalls !== undefined) { sets.push('blocked_tool_calls = ?'); values.push(updates.blockedToolCalls); }
|
|
1048
|
+
if (updates.fallbackCount !== undefined) { sets.push('fallback_count = ?'); values.push(updates.fallbackCount); }
|
|
1049
|
+
if (updates.fallbackReasons !== undefined) { sets.push('fallback_reasons = ?'); values.push(JSON.stringify(updates.fallbackReasons)); }
|
|
953
1050
|
if (updates.totalToolCalls !== undefined) { sets.push('total_tool_calls = ?'); values.push(updates.totalToolCalls); }
|
|
954
1051
|
if (updates.mcpToolCalls !== undefined) { sets.push('mcp_tool_calls = ?'); values.push(updates.mcpToolCalls); }
|
|
955
1052
|
if (updates.directToolCalls !== undefined) { sets.push('direct_tool_calls = ?'); values.push(updates.directToolCalls); }
|
|
@@ -988,6 +1085,11 @@ export class DevFlowDatabase {
|
|
|
988
1085
|
subagentCount: row.subagent_count,
|
|
989
1086
|
totalDuration: row.total_duration,
|
|
990
1087
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
1088
|
+
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : [],
|
|
1089
|
+
failedToolCalls: row.failed_tool_calls ?? 0,
|
|
1090
|
+
blockedToolCalls: row.blocked_tool_calls ?? 0,
|
|
1091
|
+
fallbackCount: row.fallback_count ?? 0,
|
|
1092
|
+
fallbackReasons: row.fallback_reasons ? JSON.parse(row.fallback_reasons) : [],
|
|
991
1093
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
992
1094
|
}));
|
|
993
1095
|
}
|
|
@@ -1176,9 +1278,39 @@ export class DevFlowDatabase {
|
|
|
1176
1278
|
FROM tool_call_events WHERE execution_id = ?
|
|
1177
1279
|
`).get(executionId) as any;
|
|
1178
1280
|
const execution = this.getSkillExecution(executionId);
|
|
1281
|
+
const events = this.listToolCallEvents(executionId);
|
|
1179
1282
|
const total = Number(row?.total ?? 0);
|
|
1180
1283
|
const mcp = Number(row?.mcp ?? 0);
|
|
1181
1284
|
const obligation = this.getExecutionObligationCompliance(executionId);
|
|
1285
|
+
const failures = events.filter(event => Boolean(event.error) && !event.blocked);
|
|
1286
|
+
const blocked = events.filter(event => event.blocked);
|
|
1287
|
+
const directFallbacks = events.filter(event => event.mcpFallback);
|
|
1288
|
+
const hookFallbacks = execution?.sessionId
|
|
1289
|
+
? this.listHookFallbacks({
|
|
1290
|
+
sessionId: execution.sessionId,
|
|
1291
|
+
from: execution.startedAt,
|
|
1292
|
+
to: finishedAt,
|
|
1293
|
+
})
|
|
1294
|
+
: [];
|
|
1295
|
+
const fallbackReasons = [...new Set([
|
|
1296
|
+
...directFallbacks.map(() => 'direct_tool_during_context'),
|
|
1297
|
+
...hookFallbacks.map(fallback => `daemon_${fallback.reason}`),
|
|
1298
|
+
])];
|
|
1299
|
+
const eventReceiptIds = collectCanonicalReceiptIds(events.map(event => event.output));
|
|
1300
|
+
const suppliedMemoryReceiptIds = Array.isArray(metadata?.memoryReceiptIds)
|
|
1301
|
+
? metadata.memoryReceiptIds.filter((id): id is string => typeof id === 'string')
|
|
1302
|
+
: [];
|
|
1303
|
+
const suppliedDistillReceiptIds = Array.isArray(metadata?.distillReceiptIds)
|
|
1304
|
+
? metadata.distillReceiptIds.filter((id): id is string => typeof id === 'string')
|
|
1305
|
+
: [];
|
|
1306
|
+
const memoryReceiptIds = [...new Set([
|
|
1307
|
+
...suppliedMemoryReceiptIds,
|
|
1308
|
+
...eventReceiptIds.filter(id => id.startsWith('memory-receipt:')),
|
|
1309
|
+
])];
|
|
1310
|
+
const distillReceiptIds = [...new Set([
|
|
1311
|
+
...suppliedDistillReceiptIds,
|
|
1312
|
+
...eventReceiptIds.filter(id => id.startsWith('distill-receipt:')),
|
|
1313
|
+
])];
|
|
1182
1314
|
this.updateSkillExecution(executionId, {
|
|
1183
1315
|
status,
|
|
1184
1316
|
finishedAt,
|
|
@@ -1190,7 +1322,23 @@ export class DevFlowDatabase {
|
|
|
1190
1322
|
? Math.max(0, finishedAt - execution.startedAt)
|
|
1191
1323
|
: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
|
|
1192
1324
|
mcpComplianceRate: obligation.rate,
|
|
1193
|
-
|
|
1325
|
+
missedMcpTools: obligation.missedTools,
|
|
1326
|
+
failedToolCalls: failures.length,
|
|
1327
|
+
blockedToolCalls: blocked.length,
|
|
1328
|
+
fallbackCount: directFallbacks.length + hookFallbacks.length,
|
|
1329
|
+
fallbackReasons,
|
|
1330
|
+
metadata: {
|
|
1331
|
+
...(metadata ?? {}),
|
|
1332
|
+
applicableObligations: obligation.applicable,
|
|
1333
|
+
satisfiedObligations: obligation.satisfied,
|
|
1334
|
+
missedTools: obligation.missedTools,
|
|
1335
|
+
actualFailureCount: failures.length,
|
|
1336
|
+
blockedCount: blocked.length,
|
|
1337
|
+
fallbackCount: directFallbacks.length + hookFallbacks.length,
|
|
1338
|
+
fallbackReasons,
|
|
1339
|
+
memoryReceiptIds,
|
|
1340
|
+
distillReceiptIds,
|
|
1341
|
+
},
|
|
1194
1342
|
});
|
|
1195
1343
|
}
|
|
1196
1344
|
|
|
@@ -1214,7 +1362,7 @@ export class DevFlowDatabase {
|
|
|
1214
1362
|
if (obligation.endsWith(':domain')) {
|
|
1215
1363
|
const family = obligation.slice(0, -':domain'.length);
|
|
1216
1364
|
return events.some((event) => {
|
|
1217
|
-
if (event.
|
|
1365
|
+
if (!event.isMcpTool || !isEffectiveToolEvent(event)) return false;
|
|
1218
1366
|
const name = String(event.mcpToolName ?? event.toolName ?? '')
|
|
1219
1367
|
.replace(/^mcp__[^_]+__/, '');
|
|
1220
1368
|
return name.startsWith(`${family}_`);
|
|
@@ -1853,6 +2001,183 @@ export class DevFlowDatabase {
|
|
|
1853
2001
|
.run(now).changes;
|
|
1854
2002
|
}
|
|
1855
2003
|
|
|
2004
|
+
beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord {
|
|
2005
|
+
this.db.prepare(`
|
|
2006
|
+
INSERT OR IGNORE INTO devflow_memory_turns
|
|
2007
|
+
(turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
|
|
2008
|
+
VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
|
|
2009
|
+
`).run(
|
|
2010
|
+
input.turnId,
|
|
2011
|
+
input.projectRoot,
|
|
2012
|
+
input.sessionId,
|
|
2013
|
+
input.promptHash,
|
|
2014
|
+
input.eventId,
|
|
2015
|
+
input.createdAt,
|
|
2016
|
+
);
|
|
2017
|
+
return this.getMemoryTurn(input.turnId)!;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
getMemoryTurn(turnId: string): MemoryTurnRecord | null {
|
|
2021
|
+
const row = this.db.prepare(
|
|
2022
|
+
'SELECT * FROM devflow_memory_turns WHERE turn_id = ?',
|
|
2023
|
+
).get(turnId) as any;
|
|
2024
|
+
return row ? this.mapMemoryTurn(row) : null;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
getPendingMemoryTurn(projectRoot: string, sessionId: string): MemoryTurnRecord | null {
|
|
2028
|
+
const row = this.db.prepare(`
|
|
2029
|
+
SELECT * FROM devflow_memory_turns
|
|
2030
|
+
WHERE project_root = ? AND session_id = ? AND status = 'pending'
|
|
2031
|
+
ORDER BY created_at DESC LIMIT 1
|
|
2032
|
+
`).get(projectRoot, sessionId) as any;
|
|
2033
|
+
return row ? this.mapMemoryTurn(row) : null;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
commitMemoryTurn(input: {
|
|
2037
|
+
turnId: string;
|
|
2038
|
+
receiptId: string;
|
|
2039
|
+
memoryIds: string[];
|
|
2040
|
+
source: string;
|
|
2041
|
+
reason?: string;
|
|
2042
|
+
decidedAt?: number;
|
|
2043
|
+
}): MemoryTurnRecord {
|
|
2044
|
+
this.db.prepare(`
|
|
2045
|
+
UPDATE devflow_memory_turns
|
|
2046
|
+
SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
|
|
2047
|
+
WHERE turn_id = ? AND status = 'pending'
|
|
2048
|
+
`).run(
|
|
2049
|
+
input.receiptId,
|
|
2050
|
+
JSON.stringify([...new Set(input.memoryIds)]),
|
|
2051
|
+
input.source,
|
|
2052
|
+
input.reason ?? null,
|
|
2053
|
+
input.decidedAt ?? Date.now(),
|
|
2054
|
+
input.turnId,
|
|
2055
|
+
);
|
|
2056
|
+
const turn = this.getMemoryTurn(input.turnId);
|
|
2057
|
+
if (!turn || turn.status !== 'committed') {
|
|
2058
|
+
throw new Error(`Memory turn ${input.turnId} is not pending or does not exist`);
|
|
2059
|
+
}
|
|
2060
|
+
return turn;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
skipMemoryTurn(input: {
|
|
2064
|
+
turnId: string;
|
|
2065
|
+
receiptId: string;
|
|
2066
|
+
reason: string;
|
|
2067
|
+
decidedAt?: number;
|
|
2068
|
+
}): MemoryTurnRecord {
|
|
2069
|
+
this.db.prepare(`
|
|
2070
|
+
UPDATE devflow_memory_turns
|
|
2071
|
+
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
|
|
2072
|
+
reason = ?, decided_at = ?
|
|
2073
|
+
WHERE turn_id = ? AND status = 'pending'
|
|
2074
|
+
`).run(
|
|
2075
|
+
input.receiptId,
|
|
2076
|
+
input.reason,
|
|
2077
|
+
input.decidedAt ?? Date.now(),
|
|
2078
|
+
input.turnId,
|
|
2079
|
+
);
|
|
2080
|
+
const turn = this.getMemoryTurn(input.turnId);
|
|
2081
|
+
if (!turn || turn.status !== 'skipped') {
|
|
2082
|
+
throw new Error(`Memory turn ${input.turnId} is not pending or does not exist`);
|
|
2083
|
+
}
|
|
2084
|
+
return turn;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
markMemoryTurnStopPrompted(turnId: string, promptedAt = Date.now()): boolean {
|
|
2088
|
+
return this.db.prepare(`
|
|
2089
|
+
UPDATE devflow_memory_turns SET stop_prompted_at = ?
|
|
2090
|
+
WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
|
|
2091
|
+
`).run(promptedAt, turnId).changes === 1;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
listMemoryTurns(projectRoot: string, sessionId?: string, limit = 50): MemoryTurnRecord[] {
|
|
2095
|
+
const rows = (sessionId
|
|
2096
|
+
? this.db.prepare(`SELECT * FROM devflow_memory_turns
|
|
2097
|
+
WHERE project_root = ? AND session_id = ? ORDER BY created_at DESC LIMIT ?`)
|
|
2098
|
+
.all(projectRoot, sessionId, limit)
|
|
2099
|
+
: this.db.prepare(`SELECT * FROM devflow_memory_turns
|
|
2100
|
+
WHERE project_root = ? ORDER BY created_at DESC LIMIT ?`)
|
|
2101
|
+
.all(projectRoot, limit)) as any[];
|
|
2102
|
+
return rows.map(row => this.mapMemoryTurn(row));
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
insertHookFallback(record: HookFallbackRecord): boolean {
|
|
2106
|
+
return this.db.prepare(`
|
|
2107
|
+
INSERT OR IGNORE INTO devflow_hook_fallbacks
|
|
2108
|
+
(id, project_root, session_id, tool_use_id, request_type, tool, reason,
|
|
2109
|
+
duration_ms, attempts, created_at)
|
|
2110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2111
|
+
`).run(
|
|
2112
|
+
record.id,
|
|
2113
|
+
record.projectRoot,
|
|
2114
|
+
record.sessionId ?? null,
|
|
2115
|
+
record.toolUseId ?? null,
|
|
2116
|
+
record.requestType,
|
|
2117
|
+
record.tool,
|
|
2118
|
+
record.reason,
|
|
2119
|
+
record.durationMs,
|
|
2120
|
+
record.attempts,
|
|
2121
|
+
record.createdAt,
|
|
2122
|
+
).changes === 1;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
listHookFallbacks(filter: {
|
|
2126
|
+
projectRoot?: string;
|
|
2127
|
+
sessionId?: string;
|
|
2128
|
+
from?: number;
|
|
2129
|
+
to?: number;
|
|
2130
|
+
limit?: number;
|
|
2131
|
+
} = {}): HookFallbackRecord[] {
|
|
2132
|
+
const conditions: string[] = [];
|
|
2133
|
+
const values: unknown[] = [];
|
|
2134
|
+
if (filter.projectRoot) { conditions.push('project_root = ?'); values.push(filter.projectRoot); }
|
|
2135
|
+
if (filter.sessionId) { conditions.push('session_id = ?'); values.push(filter.sessionId); }
|
|
2136
|
+
if (filter.from !== undefined) { conditions.push('created_at >= ?'); values.push(filter.from); }
|
|
2137
|
+
if (filter.to !== undefined) { conditions.push('created_at <= ?'); values.push(filter.to); }
|
|
2138
|
+
values.push(Math.max(1, Math.min(filter.limit ?? 500, 5_000)));
|
|
2139
|
+
const rows = this.db.prepare(`
|
|
2140
|
+
SELECT * FROM devflow_hook_fallbacks
|
|
2141
|
+
${conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''}
|
|
2142
|
+
ORDER BY created_at DESC LIMIT ?
|
|
2143
|
+
`).all(...values) as any[];
|
|
2144
|
+
return rows.map(row => ({
|
|
2145
|
+
id: row.id,
|
|
2146
|
+
projectRoot: row.project_root,
|
|
2147
|
+
sessionId: row.session_id ?? undefined,
|
|
2148
|
+
toolUseId: row.tool_use_id ?? undefined,
|
|
2149
|
+
requestType: row.request_type,
|
|
2150
|
+
tool: row.tool,
|
|
2151
|
+
reason: row.reason,
|
|
2152
|
+
durationMs: row.duration_ms,
|
|
2153
|
+
attempts: row.attempts,
|
|
2154
|
+
createdAt: row.created_at,
|
|
2155
|
+
}));
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
private mapMemoryTurn(row: any): MemoryTurnRecord {
|
|
2159
|
+
let memoryIds: string[] = [];
|
|
2160
|
+
try {
|
|
2161
|
+
const parsed: unknown = JSON.parse(row.memory_ids ?? '[]');
|
|
2162
|
+
if (Array.isArray(parsed)) memoryIds = parsed.filter((id): id is string => typeof id === 'string');
|
|
2163
|
+
} catch {}
|
|
2164
|
+
return {
|
|
2165
|
+
turnId: row.turn_id,
|
|
2166
|
+
projectRoot: row.project_root,
|
|
2167
|
+
sessionId: row.session_id,
|
|
2168
|
+
promptHash: row.prompt_hash,
|
|
2169
|
+
eventId: row.event_id,
|
|
2170
|
+
status: row.status,
|
|
2171
|
+
receiptId: row.receipt_id ?? undefined,
|
|
2172
|
+
memoryIds,
|
|
2173
|
+
source: row.source ?? undefined,
|
|
2174
|
+
reason: row.reason ?? undefined,
|
|
2175
|
+
stopPromptedAt: row.stop_prompted_at ?? undefined,
|
|
2176
|
+
createdAt: row.created_at,
|
|
2177
|
+
decidedAt: row.decided_at ?? undefined,
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1856
2181
|
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void {
|
|
1857
2182
|
this.db.prepare(`
|
|
1858
2183
|
INSERT INTO devflow_memory_distill_checkpoints
|
|
@@ -1945,3 +2270,29 @@ function countTelemetryResults(value: unknown, depth = 0): number {
|
|
|
1945
2270
|
if (record.error) return 0;
|
|
1946
2271
|
return Object.keys(record).length > 0 ? 1 : 0;
|
|
1947
2272
|
}
|
|
2273
|
+
|
|
2274
|
+
function collectCanonicalReceiptIds(values: unknown[]): string[] {
|
|
2275
|
+
const receiptIds = new Set<string>();
|
|
2276
|
+
const visit = (value: unknown, depth: number): void => {
|
|
2277
|
+
if (depth > 5 || value == null) return;
|
|
2278
|
+
if (typeof value === 'string') {
|
|
2279
|
+
if (value.startsWith('memory-receipt:') || value.startsWith('distill-receipt:')) {
|
|
2280
|
+
receiptIds.add(value);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if ((value.startsWith('{') || value.startsWith('[')) && value.length < 1_000_000) {
|
|
2284
|
+
try { visit(JSON.parse(value), depth + 1); } catch {}
|
|
2285
|
+
}
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
if (Array.isArray(value)) {
|
|
2289
|
+
value.forEach(item => visit(item, depth + 1));
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (typeof value === 'object') {
|
|
2293
|
+
Object.values(value as Record<string, unknown>).forEach(item => visit(item, depth + 1));
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
values.forEach(value => visit(value, 0));
|
|
2297
|
+
return [...receiptIds];
|
|
2298
|
+
}
|
package/src/index.ts
CHANGED