@devflow-tools/database 0.16.9 → 0.16.11

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 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',
@@ -305,4 +360,36 @@ describe('DevFlowDatabase - Skill Executions', () => {
305
360
  }),
306
361
  );
307
362
  });
363
+
364
+ it('reconciles every running execution owned by a closing session', () => {
365
+ for (const executionId of ['exec_implicit_a', 'exec_implicit_b']) {
366
+ db.insertSkillExecution({
367
+ executionId,
368
+ sessionId: 'session-closing',
369
+ skillName: 'devflow:implicit',
370
+ startedAt: 1_000,
371
+ status: 'running',
372
+ });
373
+ }
374
+ db.insertSkillExecution({
375
+ executionId: 'exec_other_session',
376
+ sessionId: 'session-other',
377
+ skillName: 'devflow:implicit',
378
+ startedAt: 1_000,
379
+ status: 'running',
380
+ });
381
+
382
+ const reconciled = db.reconcileRunningSkillExecutionsForSession(
383
+ 'session-closing',
384
+ 5_000,
385
+ { closureReason: 'session_finalize' },
386
+ );
387
+
388
+ expect(reconciled.map(execution => execution.executionId)).toEqual([
389
+ 'exec_implicit_a',
390
+ 'exec_implicit_b',
391
+ ]);
392
+ expect(reconciled.every(execution => execution.status === 'completed')).toBe(true);
393
+ expect(db.getSkillExecution('exec_other_session')).toMatchObject({ status: 'running' });
394
+ });
308
395
  });
@@ -0,0 +1,240 @@
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 durable work queue', () => {
8
+ let directory: string;
9
+ let database: DevFlowDatabase;
10
+ const projectRoot = '/project';
11
+
12
+ beforeEach(() => {
13
+ directory = mkdtempSync(join(tmpdir(), 'devflow-work-queue-'));
14
+ database = new DevFlowDatabase(directory);
15
+ });
16
+
17
+ afterEach(() => {
18
+ database.close();
19
+ rmSync(directory, { recursive: true, force: true });
20
+ });
21
+
22
+ it('returns the existing item for an idempotency conflict without resetting completed work', () => {
23
+ const first = database.enqueueWork({
24
+ idempotencyKey: 'memory:session-1:turn-1',
25
+ kind: 'memory.explicit_commit',
26
+ projectRoot,
27
+ sessionId: 'session-1',
28
+ turnId: 'turn-1',
29
+ payload: { text: 'remember this' },
30
+ nextAttemptAt: 10,
31
+ });
32
+ const [leased] = database.leaseWork({
33
+ projectRoot,
34
+ owner: 'worker-a',
35
+ limit: 1,
36
+ leaseMs: 100,
37
+ now: 10,
38
+ });
39
+ expect(leased?.id).toBe(first.id);
40
+ expect(database.completeWork(first.id, 'worker-a', 20)).toBe(true);
41
+
42
+ const duplicate = database.enqueueWork({
43
+ idempotencyKey: 'memory:session-1:turn-1',
44
+ kind: 'telemetry.reconcile',
45
+ projectRoot: '/different-project',
46
+ payload: { replacement: true },
47
+ maxAttempts: 1,
48
+ nextAttemptAt: 999,
49
+ });
50
+
51
+ expect(duplicate).toMatchObject({
52
+ id: first.id,
53
+ kind: 'memory.explicit_commit',
54
+ projectRoot,
55
+ payload: { text: 'remember this' },
56
+ state: 'completed',
57
+ attempts: 1,
58
+ maxAttempts: 5,
59
+ completedAt: 20,
60
+ });
61
+ expect(database.all('SELECT id FROM devflow_work_items')).toHaveLength(1);
62
+ });
63
+
64
+ it('leases atomically across database connections and requires the lease owner to complete', () => {
65
+ database.enqueueWork({
66
+ idempotencyKey: 'session:finalize:1',
67
+ kind: 'session.finalize',
68
+ projectRoot,
69
+ payload: { sessionId: 'session-1' },
70
+ nextAttemptAt: 100,
71
+ });
72
+ const secondConnection = new DevFlowDatabase(directory);
73
+ try {
74
+ const firstLease = database.leaseWork({
75
+ projectRoot,
76
+ owner: 'worker-a',
77
+ kinds: ['session.finalize'],
78
+ limit: 1,
79
+ leaseMs: 1_000,
80
+ now: 100,
81
+ });
82
+ const competingLease = secondConnection.leaseWork({
83
+ projectRoot,
84
+ owner: 'worker-b',
85
+ limit: 1,
86
+ leaseMs: 1_000,
87
+ now: 100,
88
+ });
89
+
90
+ expect(firstLease).toHaveLength(1);
91
+ expect(firstLease[0]).toMatchObject({
92
+ state: 'leased',
93
+ attempts: 1,
94
+ leaseOwner: 'worker-a',
95
+ leaseExpiresAt: 1_100,
96
+ });
97
+ expect(competingLease).toEqual([]);
98
+ expect(secondConnection.completeWork(firstLease[0]!.id, 'worker-b', 200)).toBe(false);
99
+ expect(database.completeWork(firstLease[0]!.id, 'worker-a', 200)).toBe(true);
100
+ expect(database.completeWork(firstLease[0]!.id, 'worker-a', 201)).toBe(false);
101
+ } finally {
102
+ secondConnection.close();
103
+ }
104
+ });
105
+
106
+ it('applies owner and state predicates to retries and dead letters', () => {
107
+ const item = database.enqueueWork({
108
+ idempotencyKey: 'telemetry:reconcile:1',
109
+ kind: 'telemetry.reconcile',
110
+ projectRoot,
111
+ payload: {},
112
+ nextAttemptAt: 50,
113
+ });
114
+ database.leaseWork({
115
+ projectRoot,
116
+ owner: 'worker-a',
117
+ limit: 1,
118
+ leaseMs: 100,
119
+ now: 50,
120
+ });
121
+
122
+ expect(database.retryWork(
123
+ item.id,
124
+ 'worker-b',
125
+ { category: 'temporary', message: 'try again' },
126
+ 75,
127
+ )).toBe(false);
128
+ expect(database.retryWork(
129
+ item.id,
130
+ 'worker-a',
131
+ { category: 'temporary', message: 'try again' },
132
+ 75,
133
+ )).toBe(true);
134
+ expect(database.getWorkQueueHealth(projectRoot, 75)).toMatchObject({
135
+ queueDepth: 1,
136
+ failed: 1,
137
+ deadLetters: 0,
138
+ });
139
+
140
+ const [retried] = database.leaseWork({
141
+ projectRoot,
142
+ owner: 'worker-b',
143
+ limit: 1,
144
+ leaseMs: 100,
145
+ now: 75,
146
+ });
147
+ expect(retried).toMatchObject({ attempts: 2, errorCategory: 'temporary' });
148
+ expect(database.deadLetterWork(
149
+ item.id,
150
+ 'worker-a',
151
+ { category: 'invalid_schema', message: 'payload is invalid' },
152
+ )).toBe(false);
153
+ expect(database.deadLetterWork(
154
+ item.id,
155
+ 'worker-b',
156
+ { category: 'invalid_schema', message: 'payload is invalid' },
157
+ )).toBe(true);
158
+ expect(database.deadLetterWork(
159
+ item.id,
160
+ 'worker-b',
161
+ { category: 'invalid_schema', message: 'payload is invalid' },
162
+ )).toBe(false);
163
+ expect(database.getWorkQueueHealth(projectRoot, 80)).toMatchObject({
164
+ queueDepth: 0,
165
+ deadLetters: 1,
166
+ });
167
+ });
168
+
169
+ it('recovers expired leases and dead-letters exhausted work', () => {
170
+ database.enqueueWork({
171
+ idempotencyKey: 'memory:distill:retryable',
172
+ kind: 'memory.turn_distill',
173
+ projectRoot,
174
+ payload: {},
175
+ maxAttempts: 2,
176
+ nextAttemptAt: 100,
177
+ });
178
+ database.enqueueWork({
179
+ idempotencyKey: 'memory:vector:exhausted',
180
+ kind: 'memory.vector_backfill',
181
+ projectRoot,
182
+ payload: {},
183
+ maxAttempts: 1,
184
+ nextAttemptAt: 100,
185
+ });
186
+ database.leaseWork({
187
+ projectRoot,
188
+ owner: 'worker-a',
189
+ limit: 2,
190
+ leaseMs: 10,
191
+ now: 100,
192
+ });
193
+
194
+ expect(database.getWorkQueueHealth(projectRoot, 110)).toMatchObject({
195
+ queueDepth: 2,
196
+ leased: 2,
197
+ expiredLeases: 2,
198
+ });
199
+ expect(database.recoverExpiredWork(projectRoot, 110)).toBe(2);
200
+ expect(database.getWorkQueueHealth(projectRoot, 110)).toMatchObject({
201
+ queueDepth: 1,
202
+ leased: 0,
203
+ failed: 1,
204
+ expiredLeases: 0,
205
+ deadLetters: 1,
206
+ });
207
+
208
+ const recovered = database.leaseWork({
209
+ projectRoot,
210
+ owner: 'worker-b',
211
+ limit: 2,
212
+ leaseMs: 20,
213
+ now: 110,
214
+ });
215
+ expect(recovered).toHaveLength(1);
216
+ expect(recovered[0]).toMatchObject({
217
+ idempotencyKey: 'memory:distill:retryable',
218
+ state: 'leased',
219
+ attempts: 2,
220
+ leaseOwner: 'worker-b',
221
+ errorCategory: 'lease_expired',
222
+ });
223
+
224
+ const revived = database.enqueueWork({
225
+ idempotencyKey: 'memory:vector:exhausted',
226
+ kind: 'memory.vector_backfill',
227
+ projectRoot,
228
+ payload: {},
229
+ maxAttempts: 20,
230
+ nextAttemptAt: 120,
231
+ });
232
+ expect(revived).toMatchObject({
233
+ state: 'failed',
234
+ attempts: 1,
235
+ maxAttempts: 20,
236
+ nextAttemptAt: 120,
237
+ errorCategory: undefined,
238
+ });
239
+ });
240
+ });
@@ -1,3 +1,4 @@
1
+ import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, WorkError, WorkItemRecord, WorkQueueHealth } from './work-queue';
1
2
  export interface BenchmarkReportRecord {
2
3
  runId: string;
3
4
  suiteId: string;
@@ -83,6 +84,34 @@ export interface MemoryDistillCheckpointRecord {
83
84
  releasedLeases: number;
84
85
  createdAt: number;
85
86
  }
87
+ export type MemoryTurnStatus = 'pending' | 'committed' | 'skipped';
88
+ export interface MemoryTurnRecord {
89
+ turnId: string;
90
+ projectRoot: string;
91
+ sessionId: string;
92
+ promptHash: string;
93
+ eventId: string;
94
+ status: MemoryTurnStatus;
95
+ receiptId?: string;
96
+ memoryIds: string[];
97
+ source?: string;
98
+ reason?: string;
99
+ stopPromptedAt?: number;
100
+ createdAt: number;
101
+ decidedAt?: number;
102
+ }
103
+ export interface HookFallbackRecord {
104
+ id: string;
105
+ projectRoot: string;
106
+ sessionId?: string;
107
+ toolUseId?: string;
108
+ requestType: string;
109
+ tool: string;
110
+ reason: 'timeout' | 'unreachable' | 'protocol' | 'unknown';
111
+ durationMs: number;
112
+ attempts: number;
113
+ createdAt: number;
114
+ }
86
115
  export declare function getGlobalDevFlowDbPath(home?: string): string;
87
116
  export declare function openGlobalDevFlowDatabase(home?: string, options?: {
88
117
  busyTimeoutMs?: number;
@@ -145,6 +174,10 @@ export declare class DevFlowDatabase {
145
174
  totalDuration?: number;
146
175
  mcpComplianceRate?: number;
147
176
  missedMcpTools?: string[];
177
+ failedToolCalls?: number;
178
+ blockedToolCalls?: number;
179
+ fallbackCount?: number;
180
+ fallbackReasons?: string[];
148
181
  totalToolCalls?: number;
149
182
  mcpToolCalls?: number;
150
183
  directToolCalls?: number;
@@ -185,6 +218,7 @@ export declare class DevFlowDatabase {
185
218
  }): boolean;
186
219
  markToolCallBlocked(eventId: string, reason: string): boolean;
187
220
  reconcileSkillExecution(executionId: string, status: 'completed' | 'failed', finishedAt?: number, metadata?: Record<string, unknown>): void;
221
+ reconcileRunningSkillExecutionsForSession(sessionId: string, finishedAt?: number, metadata?: Record<string, unknown>): any[];
188
222
  getExecutionObligationCompliance(executionId: string): {
189
223
  rate: number;
190
224
  applicable: number;
@@ -268,6 +302,21 @@ export declare class DevFlowDatabase {
268
302
  items: GovernanceAuditRecord[];
269
303
  total: number;
270
304
  };
305
+ enqueueWork(input: EnqueueWorkInput): WorkItemRecord;
306
+ getWorkByIdempotencyKey(idempotencyKey: string): WorkItemRecord | null;
307
+ requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord;
308
+ getSessionClosure(projectRoot: string, sessionId: string): SessionClosureRecord | null;
309
+ completeSessionClosure(projectRoot: string, sessionId: string, excludingWorkItemId?: string, closedAt?: number): SessionClosureRecord;
310
+ listSessionClosures(projectRoot: string, limit?: number): SessionClosureRecord[];
311
+ leaseWork(input: LeaseWorkInput): WorkItemRecord[];
312
+ completeWork(id: string, owner: string, now?: number): boolean;
313
+ retryWork(id: string, owner: string, error: WorkError, nextAttemptAt: number): boolean;
314
+ deadLetterWork(id: string, owner: string, error: WorkError): boolean;
315
+ recoverExpiredWork(projectRoot: string, now?: number): number;
316
+ getWorkQueueHealth(projectRoot: string, now?: number): WorkQueueHealth;
317
+ private mapWorkItem;
318
+ private mapSessionClosure;
319
+ private refreshClosedSessionClosure;
271
320
  getHookReceipt(projectRoot: string): HookReceiptRecord | null;
272
321
  updateHookReceipt(projectRoot: string, updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>): HookReceiptRecord;
273
322
  deleteHookReceipt(projectRoot: string): boolean;
@@ -275,6 +324,34 @@ export declare class DevFlowDatabase {
275
324
  getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
276
325
  deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number;
277
326
  purgeExpiredContextReceipts(now?: number): number;
327
+ beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord;
328
+ getMemoryTurn(turnId: string): MemoryTurnRecord | null;
329
+ getPendingMemoryTurn(projectRoot: string, sessionId: string): MemoryTurnRecord | null;
330
+ commitMemoryTurn(input: {
331
+ turnId: string;
332
+ receiptId: string;
333
+ memoryIds: string[];
334
+ source: string;
335
+ reason?: string;
336
+ decidedAt?: number;
337
+ }): MemoryTurnRecord;
338
+ skipMemoryTurn(input: {
339
+ turnId: string;
340
+ receiptId: string;
341
+ reason: string;
342
+ decidedAt?: number;
343
+ }): MemoryTurnRecord;
344
+ markMemoryTurnStopPrompted(turnId: string, promptedAt?: number): boolean;
345
+ listMemoryTurns(projectRoot: string, sessionId?: string, limit?: number): MemoryTurnRecord[];
346
+ insertHookFallback(record: HookFallbackRecord): boolean;
347
+ listHookFallbacks(filter?: {
348
+ projectRoot?: string;
349
+ sessionId?: string;
350
+ from?: number;
351
+ to?: number;
352
+ limit?: number;
353
+ }): HookFallbackRecord[];
354
+ private mapMemoryTurn;
278
355
  recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void;
279
356
  listMemoryDistillCheckpoints(projectRoot: string, limit?: number): MemoryDistillCheckpointRecord[];
280
357
  all(sql: string, ...params: unknown[]): unknown[];