@devflow-tools/database 0.16.18 → 0.16.20

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,33 @@
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.20](https://github.com/shilongfeicool/dev-flow/compare/v0.16.19...v0.16.20) (2026-07-28)
7
+
8
+
9
+ ### Features
10
+
11
+ * **retrieval:** close outcome-aware retrieval quality ([e4048a1](https://github.com/shilongfeicool/dev-flow/commit/e4048a1d921a85bd078de031b334f24cb8f4f393))
12
+
13
+
14
+
15
+
16
+
17
+ ## [0.16.19](https://github.com/shilongfeicool/dev-flow/compare/v0.16.18...v0.16.19) (2026-07-28)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **database:** harden host action evidence ([b74dc11](https://github.com/shilongfeicool/dev-flow/commit/b74dc110e0b96583dd741d2772c286e414928e45))
23
+
24
+
25
+ ### Features
26
+
27
+ * **database:** persist workflow host actions ([6e42606](https://github.com/shilongfeicool/dev-flow/commit/6e42606ff028a591878ade9378c5277257a35961))
28
+
29
+
30
+
31
+
32
+
6
33
  ## [0.16.18](https://github.com/shilongfeicool/dev-flow/compare/v0.16.17...v0.16.18) (2026-07-27)
7
34
 
8
35
  **Note:** Version bump only for package @devflow-tools/database
@@ -0,0 +1,405 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { Worker } from 'node:worker_threads';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { DevFlowDatabase } from '../src/database';
7
+ import type { FailHostActionInput, VerifyHostActionInput } from '../src/host-actions';
8
+
9
+ describe('DevFlowDatabase durable host actions', () => {
10
+ let directory: string;
11
+ let database: DevFlowDatabase;
12
+ const projectRoot = '/project-a';
13
+
14
+ beforeEach(() => {
15
+ directory = mkdtempSync(join(tmpdir(), 'devflow-host-actions-'));
16
+ database = new DevFlowDatabase(directory);
17
+ });
18
+
19
+ afterEach(() => {
20
+ database.close();
21
+ rmSync(directory, { recursive: true, force: true });
22
+ });
23
+
24
+ function request(actionId: string, stepId = 'execute') {
25
+ return database.requestHostAction({
26
+ actionId,
27
+ runId: 'telemetry-run-1',
28
+ engineRunId: 'engine-run-1',
29
+ stepId,
30
+ projectRoot,
31
+ sessionId: 'session-1',
32
+ executionId: 'execution-1',
33
+ contextReceipt: 'context-1',
34
+ });
35
+ }
36
+
37
+ function eventCount(actionId: string): number {
38
+ return Number((database.get(
39
+ 'SELECT COUNT(*) AS count FROM devflow_host_action_events WHERE action_id = ?',
40
+ actionId,
41
+ ) as { count: number }).count);
42
+ }
43
+
44
+ it('persists the full waiting to running to reported to verified flow', () => {
45
+ expect(request('action-full')).toMatchObject({
46
+ actionId: 'action-full',
47
+ runId: 'telemetry-run-1',
48
+ engineRunId: 'engine-run-1',
49
+ state: 'waiting',
50
+ report: {},
51
+ finishedAt: undefined,
52
+ });
53
+ expect(database.startHostAction({ actionId: 'action-full', projectRoot })).toMatchObject({
54
+ state: 'running',
55
+ });
56
+ expect(database.reportHostAction({
57
+ actionId: 'action-full',
58
+ projectRoot,
59
+ report: { files: ['src/index.ts'], exitCode: 0 },
60
+ evidenceHash: 'execution-hash',
61
+ })).toMatchObject({ state: 'reported', evidenceHash: 'execution-hash' });
62
+ const verified = database.verifyHostAction({
63
+ actionId: 'action-full',
64
+ projectRoot,
65
+ report: { command: 'npm test', exitCode: 0 },
66
+ evidenceHash: 'verification-hash',
67
+ });
68
+
69
+ expect(verified).toMatchObject({
70
+ state: 'verified',
71
+ report: { command: 'npm test', exitCode: 0 },
72
+ evidenceHash: 'verification-hash',
73
+ });
74
+ expect(verified.finishedAt).toEqual(expect.any(Number));
75
+ expect(eventCount('action-full')).toBe(4);
76
+ });
77
+
78
+ it('supports a direct waiting to reported transition for MCP reports', () => {
79
+ request('action-direct');
80
+ expect(database.reportHostAction({
81
+ actionId: 'action-direct',
82
+ projectRoot,
83
+ report: { toolCalls: ['write_file'] },
84
+ evidenceHash: 'direct-hash',
85
+ })).toMatchObject({ state: 'reported', finishedAt: undefined });
86
+ expect(eventCount('action-direct')).toBe(2);
87
+ });
88
+
89
+ it('rolls back invalid transitions without appending an event', () => {
90
+ request('action-invalid');
91
+ const before = eventCount('action-invalid');
92
+
93
+ expect(() => database.verifyHostAction({
94
+ actionId: 'action-invalid',
95
+ projectRoot,
96
+ report: { command: 'npm test', exitCode: 0 },
97
+ evidenceHash: 'verify-hash',
98
+ })).toThrow('HOST_ACTION_INVALID_TRANSITION:waiting->verified');
99
+ expect(database.getHostAction(projectRoot, 'action-invalid')?.state).toBe('waiting');
100
+ expect(eventCount('action-invalid')).toBe(before);
101
+ });
102
+
103
+ it('makes identical terminal reports idempotent and rejects changed duplicates', () => {
104
+ request('action-terminal');
105
+ database.reportHostAction({
106
+ actionId: 'action-terminal',
107
+ projectRoot,
108
+ report: { exitCode: 0 },
109
+ evidenceHash: 'execution-hash',
110
+ });
111
+ const input = {
112
+ actionId: 'action-terminal',
113
+ projectRoot,
114
+ report: { summary: 'passed', exitCode: 0 },
115
+ evidenceHash: 'terminal-hash',
116
+ };
117
+ const terminal = database.verifyHostAction(input);
118
+ const before = eventCount('action-terminal');
119
+
120
+ expect(database.verifyHostAction({
121
+ ...input,
122
+ report: { exitCode: 0, summary: 'passed' },
123
+ })).toEqual(terminal);
124
+ expect(eventCount('action-terminal')).toBe(before);
125
+ expect(() => database.verifyHostAction({
126
+ ...input,
127
+ evidenceHash: 'changed-hash',
128
+ })).toThrow('HOST_ACTION_TERMINAL_CONFLICT:action-terminal');
129
+ expect(() => database.verifyHostAction({
130
+ ...input,
131
+ report: { summary: 'different', exitCode: 0 },
132
+ })).toThrow('HOST_ACTION_TERMINAL_CONFLICT:action-terminal');
133
+ expect(eventCount('action-terminal')).toBe(before);
134
+ });
135
+
136
+ it.each(['failed', 'cancelled', 'degraded'] as const)(
137
+ 'reaches %s with a completion timestamp',
138
+ outcome => {
139
+ const actionId = `action-${outcome}`;
140
+ request(actionId, outcome);
141
+ const terminal = database.failHostAction({
142
+ actionId,
143
+ projectRoot,
144
+ outcome,
145
+ report: { reason: `${outcome} by host` },
146
+ evidenceHash: `${outcome}-hash`,
147
+ });
148
+
149
+ expect(terminal.state).toBe(outcome);
150
+ expect(terminal.finishedAt).toEqual(expect.any(Number));
151
+ expect(eventCount(actionId)).toBe(2);
152
+ },
153
+ );
154
+
155
+ it('rejects request identity conflicts instead of merging metadata', () => {
156
+ request('action-identity');
157
+ const identical = request('action-identity');
158
+ expect(identical.actionId).toBe('action-identity');
159
+ expect(eventCount('action-identity')).toBe(1);
160
+
161
+ expect(() => database.requestHostAction({
162
+ actionId: 'different-action',
163
+ runId: 'telemetry-run-1',
164
+ engineRunId: 'different-engine-run',
165
+ stepId: 'execute',
166
+ projectRoot,
167
+ sessionId: 'different-session',
168
+ executionId: 'execution-1',
169
+ contextReceipt: 'context-1',
170
+ })).toThrow('HOST_ACTION_IDENTITY_CONFLICT:telemetry-run-1:execute');
171
+ expect(database.all('SELECT action_id FROM devflow_host_actions')).toHaveLength(1);
172
+ });
173
+
174
+ it('scopes action and run reads by project root', () => {
175
+ request('action-isolated');
176
+
177
+ expect(database.getHostAction('/project-b', 'action-isolated')).toBeNull();
178
+ expect(database.listHostActionsForRun('/project-b', 'telemetry-run-1')).toEqual([]);
179
+ expect(database.listHostActionsForRun(projectRoot, 'telemetry-run-1')).toHaveLength(1);
180
+ expect(() => database.startHostAction({
181
+ actionId: 'action-isolated',
182
+ projectRoot: '/project-b',
183
+ })).toThrow('HOST_ACTION_NOT_FOUND:action-isolated');
184
+ });
185
+
186
+ it('keeps rows and schema stable across close and reopen', () => {
187
+ request('action-durable');
188
+ database.reportHostAction({
189
+ actionId: 'action-durable',
190
+ projectRoot,
191
+ report: { result: 'observed' },
192
+ evidenceHash: 'durable-hash',
193
+ });
194
+ const schemaBefore = database.all(`
195
+ SELECT type, name, sql FROM sqlite_master
196
+ WHERE name LIKE '%host_action%'
197
+ ORDER BY type, name
198
+ `);
199
+ database.close();
200
+ database = new DevFlowDatabase(directory);
201
+
202
+ expect(database.getHostAction(projectRoot, 'action-durable')).toMatchObject({
203
+ state: 'reported',
204
+ report: { result: 'observed' },
205
+ evidenceHash: 'durable-hash',
206
+ });
207
+ expect(eventCount('action-durable')).toBe(2);
208
+ expect(database.all(`
209
+ SELECT type, name, sql FROM sqlite_master
210
+ WHERE name LIKE '%host_action%'
211
+ ORDER BY type, name
212
+ `)).toEqual(schemaBefore);
213
+ });
214
+
215
+ it('uses UUID text event IDs and enforces append-only event rows', () => {
216
+ request('action-events');
217
+ const [event] = database.all(`
218
+ SELECT event_id FROM devflow_host_action_events WHERE action_id = ?
219
+ `, 'action-events') as Array<{ event_id: string }>;
220
+
221
+ expect(event?.event_id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
222
+ expect(() => database.all(
223
+ 'UPDATE devflow_host_action_events SET evidence_hash = ? WHERE event_id = ?',
224
+ 'changed',
225
+ event!.event_id,
226
+ )).toThrow('append-only');
227
+ });
228
+
229
+ it('rejects INSERT OR REPLACE attempts without mutating an event', () => {
230
+ request('action-replace');
231
+ const [event] = database.all(`
232
+ SELECT * FROM devflow_host_action_events WHERE action_id = ?
233
+ `, 'action-replace') as Array<Record<string, unknown>>;
234
+
235
+ expect(() => database.all(`
236
+ INSERT OR REPLACE INTO devflow_host_action_events (
237
+ event_id, action_id, project_root, run_id, step_id, from_state,
238
+ to_state, report_json, evidence_hash, created_at
239
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
240
+ `,
241
+ event!.event_id,
242
+ event!.action_id,
243
+ event!.project_root,
244
+ event!.run_id,
245
+ event!.step_id,
246
+ event!.from_state,
247
+ event!.to_state,
248
+ event!.report_json,
249
+ 'replacement-hash',
250
+ event!.created_at,
251
+ )).toThrow('append-only');
252
+
253
+ expect(database.get(
254
+ 'SELECT evidence_hash FROM devflow_host_action_events WHERE event_id = ?',
255
+ event!.event_id,
256
+ )).toMatchObject({ evidence_hash: null });
257
+ });
258
+
259
+ it.each([
260
+ ['undefined', { value: undefined }],
261
+ ['NaN', { value: Number.NaN }],
262
+ ['Infinity', { value: Number.POSITIVE_INFINITY }],
263
+ ['negative zero', { value: -0 }],
264
+ ['function', { value: () => undefined }],
265
+ ['bigint', { value: 1n }],
266
+ ['symbol', { value: Symbol('value') }],
267
+ ['non-plain object', { value: new Date(0) }],
268
+ ['sparse array', { value: Array(1) }],
269
+ ])('rejects lossy %s report values', (_name, report) => {
270
+ request(`action-json-${_name}`);
271
+ expect(() => database.reportHostAction({
272
+ actionId: `action-json-${_name}`,
273
+ projectRoot,
274
+ report,
275
+ evidenceHash: 'report-hash',
276
+ })).toThrow('Host action report contains a non-JSON value');
277
+ });
278
+
279
+ it('rejects cyclic reports', () => {
280
+ request('action-json-cycle');
281
+ const report: Record<string, unknown> = {};
282
+ report.self = report;
283
+
284
+ expect(() => database.reportHostAction({
285
+ actionId: 'action-json-cycle',
286
+ projectRoot,
287
+ report,
288
+ evidenceHash: 'report-hash',
289
+ })).toThrow('Host action report contains a cycle');
290
+ });
291
+
292
+ it.each(['not-json', '[]'])(
293
+ 'fails closed when persisted report JSON is corrupt: %s',
294
+ reportJson => {
295
+ request('action-corrupt');
296
+ database.all(
297
+ 'UPDATE devflow_host_actions SET report_json = ? WHERE action_id = ?',
298
+ reportJson,
299
+ 'action-corrupt',
300
+ );
301
+
302
+ expect(() => database.getHostAction(projectRoot, 'action-corrupt'))
303
+ .toThrow('Invalid persisted host action report_json');
304
+ },
305
+ );
306
+
307
+ it('requires non-empty verification evidence and report content', () => {
308
+ request('action-verification-input');
309
+ database.reportHostAction({
310
+ actionId: 'action-verification-input',
311
+ projectRoot,
312
+ report: { output: 'observed' },
313
+ evidenceHash: 'execution-hash',
314
+ });
315
+ const baseInput = {
316
+ actionId: 'action-verification-input',
317
+ projectRoot,
318
+ report: { command: 'npm test', exitCode: 0 },
319
+ };
320
+
321
+ expect(() => database.verifyHostAction({
322
+ ...baseInput,
323
+ evidenceHash: ' ',
324
+ })).toThrow('Host action verification requires a non-empty evidence hash');
325
+ expect(() => database.verifyHostAction(baseInput as VerifyHostActionInput))
326
+ .toThrow('Host action verification requires a non-empty evidence hash');
327
+ expect(() => database.verifyHostAction({
328
+ ...baseInput,
329
+ report: {},
330
+ evidenceHash: 'verification-hash',
331
+ })).toThrow('Host action verification requires a non-empty report');
332
+ expect(() => database.failHostAction({
333
+ ...baseInput,
334
+ outcome: 'verified',
335
+ } as unknown as FailHostActionInput)).toThrow('Invalid host action failure outcome');
336
+ expect(database.getHostAction(projectRoot, 'action-verification-input')?.state).toBe('reported');
337
+ expect(eventCount('action-verification-input')).toBe(2);
338
+ });
339
+
340
+ it('waits for a competing writer and resolves the committed request idempotently', async () => {
341
+ const worker = new Worker(`
342
+ const { parentPort, workerData } = require('node:worker_threads');
343
+ const { DatabaseSync } = require('node:sqlite');
344
+ const db = new DatabaseSync(workerData.dbPath);
345
+ db.exec('PRAGMA busy_timeout = 5000');
346
+ db.exec('BEGIN IMMEDIATE');
347
+ const now = Date.now();
348
+ db.prepare(
349
+ 'INSERT INTO devflow_host_actions (' +
350
+ 'action_id, run_id, engine_run_id, step_id, project_root, session_id, ' +
351
+ 'execution_id, context_receipt, state, report_json, evidence_hash, ' +
352
+ 'created_at, updated_at, finished_at' +
353
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'waiting', '{}', NULL, ?, ?, NULL)",
354
+ ).run(
355
+ 'action-contended', 'telemetry-run-1', 'engine-run-1', 'execute', '/project-a',
356
+ 'session-1', 'execution-1', 'context-1', now, now,
357
+ );
358
+ db.prepare(
359
+ 'INSERT INTO devflow_host_action_events (' +
360
+ 'event_id, action_id, project_root, run_id, step_id, from_state, ' +
361
+ 'to_state, report_json, evidence_hash, created_at' +
362
+ ") VALUES (?, ?, ?, ?, ?, NULL, 'waiting', '{}', NULL, ?)",
363
+ ).run(
364
+ 'worker-event-contended', 'action-contended', '/project-a',
365
+ 'telemetry-run-1', 'execute', now,
366
+ );
367
+ parentPort.postMessage('locked');
368
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
369
+ db.exec('COMMIT');
370
+ db.close();
371
+ parentPort.postMessage('committed');
372
+ `, {
373
+ eval: true,
374
+ workerData: { dbPath: join(directory, '.devflow', 'devflow.db') },
375
+ });
376
+ let duplicate: ReturnType<typeof request> | undefined;
377
+
378
+ try {
379
+ await new Promise<void>((resolve, reject) => {
380
+ worker.on('message', message => {
381
+ if (message === 'locked') {
382
+ try {
383
+ duplicate = request('action-contended');
384
+ } catch (error) {
385
+ reject(error);
386
+ }
387
+ }
388
+ if (message === 'committed') resolve();
389
+ });
390
+ worker.on('error', reject);
391
+ worker.on('exit', code => {
392
+ if (code !== 0) reject(new Error(`Host-action lock worker exited with code ${code}`));
393
+ });
394
+ });
395
+
396
+ expect(duplicate).toMatchObject({
397
+ actionId: 'action-contended',
398
+ state: 'waiting',
399
+ });
400
+ expect(eventCount('action-contended')).toBe(1);
401
+ } finally {
402
+ await worker.terminate();
403
+ }
404
+ });
405
+ });
@@ -0,0 +1,79 @@
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 retrieval sessions', () => {
8
+ let root: string;
9
+ let database: DevFlowDatabase;
10
+
11
+ beforeEach(() => {
12
+ root = mkdtempSync(join(tmpdir(), 'devflow-retrieval-session-'));
13
+ database = new DevFlowDatabase(root);
14
+ });
15
+
16
+ afterEach(() => {
17
+ database.close();
18
+ rmSync(root, { recursive: true, force: true });
19
+ });
20
+
21
+ const create = () => database.createRetrievalSession({
22
+ id: 'retrieval-1', requestId: 'request-1', projectRoot: '/project-a',
23
+ sessionId: 'session-1', executionId: 'execution-1', query: 'review player',
24
+ intent: 'performance', tokenBudget: 1000, baselineReceipt: 'baseline-1',
25
+ expiresAt: Date.now() + 60_000,
26
+ });
27
+
28
+ it('persists idempotent cycles and rejects changed evidence', () => {
29
+ expect(create()).toMatchObject({ id: 'retrieval-1', cycle: 0, remainingTokenBudget: 1000 });
30
+ expect(create()).toMatchObject({ id: 'retrieval-1' });
31
+ const input = {
32
+ retrievalSessionId: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
33
+ baselineReceipt: 'baseline-1', cycle: 1,
34
+ gaps: [{ kind: 'caller' as const, reason: 'missing caller', evidence: [], resolver: 'codegraph' as const, priority: 80 }],
35
+ selectedIds: ['code:src/player.tsx'], rejectedIds: [], tokenCost: 200,
36
+ remainingTokenBudget: 800, quality: { status: 'healthy' }, receipt: 'cycle-1', evidenceHash: 'hash-1',
37
+ };
38
+ expect(database.appendRetrievalCycle(input)).toMatchObject({ cycle: 1, tokenCost: 200 });
39
+ expect(database.appendRetrievalCycle(input)).toMatchObject({ evidenceHash: 'hash-1' });
40
+ expect(() => database.appendRetrievalCycle({ ...input, evidenceHash: 'changed' }))
41
+ .toThrow('RETRIEVAL_CYCLE_CONFLICT:1');
42
+ expect(database.getRetrievalSession('/project-a', 'session-1', 'retrieval-1'))
43
+ .toMatchObject({ cycle: 1, remainingTokenBudget: 800 });
44
+ });
45
+
46
+ it('enforces ownership, sequence, terminal state, and expiration', () => {
47
+ create();
48
+ expect(database.getRetrievalSession('/project-b', 'session-1', 'retrieval-1')).toBeNull();
49
+ expect(() => database.appendRetrievalCycle({
50
+ retrievalSessionId: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
51
+ baselineReceipt: 'baseline-1', cycle: 2, gaps: [], selectedIds: [], rejectedIds: [],
52
+ tokenCost: 1, remainingTokenBudget: 999, quality: {}, receipt: 'cycle-2', evidenceHash: 'hash-2',
53
+ })).toThrow('RETRIEVAL_CYCLE_SEQUENCE:0->2');
54
+ database.finalizeRetrievalSession({
55
+ id: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
56
+ state: 'exhausted', finalReceipt: 'final-1',
57
+ });
58
+ expect(() => database.finalizeRetrievalSession({
59
+ id: 'retrieval-1', projectRoot: '/project-a', sessionId: 'session-1',
60
+ state: 'satisfied', finalReceipt: 'different',
61
+ })).toThrow('RETRIEVAL_SESSION_TERMINAL:exhausted');
62
+
63
+ database.createRetrievalSession({
64
+ id: 'retrieval-expire', requestId: 'request-expire', projectRoot: '/project-a',
65
+ sessionId: 'session-1', query: 'x', intent: 'debug', tokenBudget: 1,
66
+ baselineReceipt: 'baseline-expire', expiresAt: Date.now() + 10,
67
+ });
68
+ expect(database.expireRetrievalSessions(Date.now() + 20)).toBe(1);
69
+ expect(database.getRetrievalSession('/project-a', 'session-1', 'retrieval-expire')?.state).toBe('expired');
70
+ });
71
+
72
+ it('keeps retrieval rows stable across reopen', () => {
73
+ create();
74
+ database.close();
75
+ database = new DevFlowDatabase(root);
76
+ expect(database.getRetrievalSession('/project-a', 'session-1', 'retrieval-1'))
77
+ .toMatchObject({ requestId: 'request-1', baselineReceipt: 'baseline-1' });
78
+ });
79
+ });
@@ -1,5 +1,7 @@
1
1
  import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, WorkError, WorkItemRecord, WorkQueueHealth } from './work-queue';
2
2
  import { type SessionObligationRecord, type SessionObligationState } from './obligation-ledger';
3
+ import { type FailHostActionInput, type HostActionRecord, type ReportHostActionInput, type RequestHostActionInput, type StartHostActionInput, type VerifyHostActionInput } from './host-actions';
4
+ import { type AppendRetrievalCycleInput, type CreateRetrievalSessionInput, type RetrievalCycleRecord, type RetrievalSessionRecord, type RetrievalSessionState } from './retrieval-sessions';
3
5
  export interface BenchmarkReportRecord {
4
6
  runId: string;
5
7
  suiteId: string;
@@ -343,6 +345,21 @@ export declare class DevFlowDatabase {
343
345
  getHookReceipt(projectRoot: string): HookReceiptRecord | null;
344
346
  updateHookReceipt(projectRoot: string, updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>): HookReceiptRecord;
345
347
  deleteHookReceipt(projectRoot: string): boolean;
348
+ createRetrievalSession(input: CreateRetrievalSessionInput): RetrievalSessionRecord;
349
+ getRetrievalSession(projectRoot: string, sessionId: string, id: string): RetrievalSessionRecord | null;
350
+ getRetrievalSessionByRequest(requestId: string): RetrievalSessionRecord | null;
351
+ listRetrievalCycles(retrievalSessionId: string): RetrievalCycleRecord[];
352
+ appendRetrievalCycle(input: AppendRetrievalCycleInput): RetrievalCycleRecord;
353
+ finalizeRetrievalSession(input: {
354
+ id: string;
355
+ projectRoot: string;
356
+ sessionId: string;
357
+ state: Extract<RetrievalSessionState, 'satisfied' | 'exhausted'>;
358
+ finalReceipt: string;
359
+ }): RetrievalSessionRecord;
360
+ expireRetrievalSessions(now?: number, limit?: number): number;
361
+ private validateRetrievalSessionInput;
362
+ private assertRetrievalIdentity;
346
363
  upsertContextReceipt(receipt: ContextReceiptRecord): void;
347
364
  getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
348
365
  getActiveContextReceipt(projectRoot: string, sessionId: string, executionId?: string, now?: number): ContextReceiptRecord | null;
@@ -400,6 +417,20 @@ export declare class DevFlowDatabase {
400
417
  private mapMemoryTurn;
401
418
  recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void;
402
419
  listMemoryDistillCheckpoints(projectRoot: string, limit?: number): MemoryDistillCheckpointRecord[];
420
+ requestHostAction(input: RequestHostActionInput): HostActionRecord;
421
+ startHostAction(input: StartHostActionInput): HostActionRecord;
422
+ reportHostAction(input: ReportHostActionInput): HostActionRecord;
423
+ verifyHostAction(input: VerifyHostActionInput): HostActionRecord;
424
+ failHostAction(input: FailHostActionInput): HostActionRecord;
425
+ getHostAction(projectRoot: string, actionId: string): HostActionRecord | null;
426
+ listHostActionsForRun(projectRoot: string, runId: string): HostActionRecord[];
427
+ listHostActionsForSession(projectRoot: string, sessionId: string, executionId?: string): HostActionRecord[];
428
+ private transitionHostAction;
429
+ private appendHostActionEvent;
430
+ private validateHostActionIdentity;
431
+ private withImmediateTransaction;
432
+ private isTerminalHostActionState;
433
+ private isLegalHostActionTransition;
403
434
  all(sql: string, ...params: unknown[]): unknown[];
404
435
  get(sql: string, ...params: unknown[]): unknown;
405
436
  close(): void;