@devflow-tools/database 0.16.19 → 0.16.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/__tests__/database.retrieval-sessions.test.ts +79 -0
- package/dist/database.d.ts +17 -0
- package/dist/database.js +231 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/retrieval-sessions.d.ts +74 -0
- package/dist/retrieval-sessions.js +117 -0
- package/dist/work-queue.d.ts +1 -1
- package/package.json +1 -1
- package/src/database.ts +266 -0
- package/src/index.ts +10 -0
- package/src/retrieval-sessions.ts +196 -0
- package/src/work-queue.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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.21](https://github.com/shilongfeicool/dev-flow/compare/v0.16.20...v0.16.21) (2026-07-28)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Performance Improvements
|
|
10
|
+
|
|
11
|
+
* **runtime:** scope tools and context payloads ([7417eac](https://github.com/shilongfeicool/dev-flow/commit/7417eacd26f4d28097c8dfe7ab073efbe7a1404e))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.16.20](https://github.com/shilongfeicool/dev-flow/compare/v0.16.19...v0.16.20) (2026-07-28)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **retrieval:** close outcome-aware retrieval quality ([e4048a1](https://github.com/shilongfeicool/dev-flow/commit/e4048a1d921a85bd078de031b334f24cb8f4f393))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [0.16.19](https://github.com/shilongfeicool/dev-flow/compare/v0.16.18...v0.16.19) (2026-07-28)
|
|
7
29
|
|
|
8
30
|
|
|
@@ -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
|
+
});
|
package/dist/database.d.ts
CHANGED
|
@@ -1,6 +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
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';
|
|
4
5
|
export interface BenchmarkReportRecord {
|
|
5
6
|
runId: string;
|
|
6
7
|
suiteId: string;
|
|
@@ -344,6 +345,21 @@ export declare class DevFlowDatabase {
|
|
|
344
345
|
getHookReceipt(projectRoot: string): HookReceiptRecord | null;
|
|
345
346
|
updateHookReceipt(projectRoot: string, updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>): HookReceiptRecord;
|
|
346
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;
|
|
347
363
|
upsertContextReceipt(receipt: ContextReceiptRecord): void;
|
|
348
364
|
getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
|
|
349
365
|
getActiveContextReceipt(projectRoot: string, sessionId: string, executionId?: string, now?: number): ContextReceiptRecord | null;
|
|
@@ -408,6 +424,7 @@ export declare class DevFlowDatabase {
|
|
|
408
424
|
failHostAction(input: FailHostActionInput): HostActionRecord;
|
|
409
425
|
getHostAction(projectRoot: string, actionId: string): HostActionRecord | null;
|
|
410
426
|
listHostActionsForRun(projectRoot: string, runId: string): HostActionRecord[];
|
|
427
|
+
listHostActionsForSession(projectRoot: string, sessionId: string, executionId?: string): HostActionRecord[];
|
|
411
428
|
private transitionHostAction;
|
|
412
429
|
private appendHostActionEvent;
|
|
413
430
|
private validateHostActionIdentity;
|
package/dist/database.js
CHANGED
|
@@ -10,6 +10,7 @@ const os_1 = require("os");
|
|
|
10
10
|
const crypto_1 = require("crypto");
|
|
11
11
|
const obligation_ledger_1 = require("./obligation-ledger");
|
|
12
12
|
const host_actions_1 = require("./host-actions");
|
|
13
|
+
const retrieval_sessions_1 = require("./retrieval-sessions");
|
|
13
14
|
const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
14
15
|
'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
|
|
15
16
|
]);
|
|
@@ -309,6 +310,50 @@ class DevFlowDatabase {
|
|
|
309
310
|
CREATE INDEX IF NOT EXISTS idx_context_selection_identity
|
|
310
311
|
ON devflow_context_selection_events(project_root, session_id, execution_id, request_id);
|
|
311
312
|
|
|
313
|
+
CREATE TABLE IF NOT EXISTS devflow_retrieval_sessions (
|
|
314
|
+
id TEXT PRIMARY KEY,
|
|
315
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
316
|
+
project_root TEXT NOT NULL,
|
|
317
|
+
session_id TEXT NOT NULL,
|
|
318
|
+
execution_id TEXT,
|
|
319
|
+
query TEXT NOT NULL,
|
|
320
|
+
intent TEXT NOT NULL,
|
|
321
|
+
state TEXT NOT NULL DEFAULT 'open'
|
|
322
|
+
CHECK(state IN ('open', 'satisfied', 'exhausted', 'expired')),
|
|
323
|
+
cycle INTEGER NOT NULL DEFAULT 0 CHECK(cycle BETWEEN 0 AND 3),
|
|
324
|
+
max_cycles INTEGER NOT NULL DEFAULT 3 CHECK(max_cycles = 3),
|
|
325
|
+
initial_token_budget INTEGER NOT NULL CHECK(initial_token_budget >= 0),
|
|
326
|
+
remaining_token_budget INTEGER NOT NULL CHECK(remaining_token_budget >= 0),
|
|
327
|
+
baseline_receipt TEXT NOT NULL,
|
|
328
|
+
final_receipt TEXT,
|
|
329
|
+
created_at INTEGER NOT NULL,
|
|
330
|
+
updated_at INTEGER NOT NULL,
|
|
331
|
+
expires_at INTEGER NOT NULL
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_sessions_owner
|
|
335
|
+
ON devflow_retrieval_sessions(project_root, session_id, state, updated_at DESC);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_sessions_expiry
|
|
337
|
+
ON devflow_retrieval_sessions(state, expires_at);
|
|
338
|
+
|
|
339
|
+
CREATE TABLE IF NOT EXISTS devflow_retrieval_cycles (
|
|
340
|
+
retrieval_session_id TEXT NOT NULL,
|
|
341
|
+
cycle INTEGER NOT NULL CHECK(cycle BETWEEN 1 AND 3),
|
|
342
|
+
gaps_json TEXT NOT NULL DEFAULT '[]',
|
|
343
|
+
selected_ids TEXT NOT NULL DEFAULT '[]',
|
|
344
|
+
rejected_ids TEXT NOT NULL DEFAULT '[]',
|
|
345
|
+
token_cost INTEGER NOT NULL CHECK(token_cost >= 0),
|
|
346
|
+
remaining_token_budget INTEGER NOT NULL CHECK(remaining_token_budget >= 0),
|
|
347
|
+
quality_json TEXT NOT NULL DEFAULT '{}',
|
|
348
|
+
receipt TEXT NOT NULL,
|
|
349
|
+
evidence_hash TEXT NOT NULL,
|
|
350
|
+
created_at INTEGER NOT NULL,
|
|
351
|
+
PRIMARY KEY (retrieval_session_id, cycle)
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_retrieval_cycles_receipt
|
|
355
|
+
ON devflow_retrieval_cycles(receipt);
|
|
356
|
+
|
|
312
357
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
313
358
|
id TEXT PRIMARY KEY,
|
|
314
359
|
project_root TEXT NOT NULL,
|
|
@@ -2180,6 +2225,183 @@ class DevFlowDatabase {
|
|
|
2180
2225
|
return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
|
|
2181
2226
|
.run(projectRoot).changes > 0;
|
|
2182
2227
|
}
|
|
2228
|
+
createRetrievalSession(input) {
|
|
2229
|
+
this.validateRetrievalSessionInput(input);
|
|
2230
|
+
const now = Date.now();
|
|
2231
|
+
const id = input.id?.trim() || (0, crypto_1.randomUUID)();
|
|
2232
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
2233
|
+
try {
|
|
2234
|
+
const existing = this.getRetrievalSessionByRequest(input.requestId);
|
|
2235
|
+
if (existing) {
|
|
2236
|
+
this.assertRetrievalIdentity(existing, input);
|
|
2237
|
+
this.db.exec('COMMIT');
|
|
2238
|
+
return existing;
|
|
2239
|
+
}
|
|
2240
|
+
this.db.prepare(`
|
|
2241
|
+
INSERT INTO devflow_retrieval_sessions
|
|
2242
|
+
(id, request_id, project_root, session_id, execution_id, query, intent, state,
|
|
2243
|
+
cycle, max_cycles, initial_token_budget, remaining_token_budget,
|
|
2244
|
+
baseline_receipt, created_at, updated_at, expires_at)
|
|
2245
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'open', 0, ?, ?, ?, ?, ?, ?, ?)
|
|
2246
|
+
`).run(id, input.requestId, input.projectRoot, input.sessionId, input.executionId ?? null, input.query, input.intent, retrieval_sessions_1.RETRIEVAL_MAX_CYCLES, input.tokenBudget, input.tokenBudget, input.baselineReceipt, now, now, input.expiresAt);
|
|
2247
|
+
const created = this.getRetrievalSession(input.projectRoot, input.sessionId, id);
|
|
2248
|
+
this.db.exec('COMMIT');
|
|
2249
|
+
return created;
|
|
2250
|
+
}
|
|
2251
|
+
catch (error) {
|
|
2252
|
+
try {
|
|
2253
|
+
this.db.exec('ROLLBACK');
|
|
2254
|
+
}
|
|
2255
|
+
catch { }
|
|
2256
|
+
throw error;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
getRetrievalSession(projectRoot, sessionId, id) {
|
|
2260
|
+
const row = this.db.prepare(`
|
|
2261
|
+
SELECT * FROM devflow_retrieval_sessions
|
|
2262
|
+
WHERE id = ? AND project_root = ? AND session_id = ?
|
|
2263
|
+
`).get(id, projectRoot, sessionId);
|
|
2264
|
+
return row ? (0, retrieval_sessions_1.mapRetrievalSessionRow)(row) : null;
|
|
2265
|
+
}
|
|
2266
|
+
getRetrievalSessionByRequest(requestId) {
|
|
2267
|
+
const row = this.db.prepare(`
|
|
2268
|
+
SELECT * FROM devflow_retrieval_sessions WHERE request_id = ?
|
|
2269
|
+
`).get(requestId);
|
|
2270
|
+
return row ? (0, retrieval_sessions_1.mapRetrievalSessionRow)(row) : null;
|
|
2271
|
+
}
|
|
2272
|
+
listRetrievalCycles(retrievalSessionId) {
|
|
2273
|
+
return this.db.prepare(`
|
|
2274
|
+
SELECT * FROM devflow_retrieval_cycles
|
|
2275
|
+
WHERE retrieval_session_id = ? ORDER BY cycle ASC
|
|
2276
|
+
`).all(retrievalSessionId).map(retrieval_sessions_1.mapRetrievalCycleRow);
|
|
2277
|
+
}
|
|
2278
|
+
appendRetrievalCycle(input) {
|
|
2279
|
+
if (!Number.isInteger(input.cycle) || input.cycle < 1 || input.cycle > retrieval_sessions_1.RETRIEVAL_MAX_CYCLES) {
|
|
2280
|
+
throw new Error(`RETRIEVAL_INVALID_CYCLE:${input.cycle}`);
|
|
2281
|
+
}
|
|
2282
|
+
if (!Number.isSafeInteger(input.tokenCost) || input.tokenCost < 0
|
|
2283
|
+
|| !Number.isSafeInteger(input.remainingTokenBudget) || input.remainingTokenBudget < 0) {
|
|
2284
|
+
throw new Error('RETRIEVAL_INVALID_BUDGET');
|
|
2285
|
+
}
|
|
2286
|
+
const gapsJson = (0, retrieval_sessions_1.serializeRetrievalJson)(input.gaps);
|
|
2287
|
+
const selectedJson = (0, retrieval_sessions_1.serializeRetrievalJson)([...new Set(input.selectedIds)]);
|
|
2288
|
+
const rejectedJson = (0, retrieval_sessions_1.serializeRetrievalJson)([...new Set(input.rejectedIds)]);
|
|
2289
|
+
const qualityJson = (0, retrieval_sessions_1.serializeRetrievalJson)(input.quality);
|
|
2290
|
+
const now = Date.now();
|
|
2291
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
2292
|
+
try {
|
|
2293
|
+
const session = this.getRetrievalSession(input.projectRoot, input.sessionId, input.retrievalSessionId);
|
|
2294
|
+
if (!session)
|
|
2295
|
+
throw new Error(`RETRIEVAL_SESSION_NOT_FOUND:${input.retrievalSessionId}`);
|
|
2296
|
+
if (session.baselineReceipt !== input.baselineReceipt)
|
|
2297
|
+
throw new Error('RETRIEVAL_BASELINE_RECEIPT_MISMATCH');
|
|
2298
|
+
const existing = this.db.prepare(`
|
|
2299
|
+
SELECT * FROM devflow_retrieval_cycles WHERE retrieval_session_id = ? AND cycle = ?
|
|
2300
|
+
`).get(input.retrievalSessionId, input.cycle);
|
|
2301
|
+
if (existing) {
|
|
2302
|
+
const cycle = (0, retrieval_sessions_1.mapRetrievalCycleRow)(existing);
|
|
2303
|
+
if (cycle.evidenceHash !== input.evidenceHash || cycle.receipt !== input.receipt
|
|
2304
|
+
|| (0, retrieval_sessions_1.serializeRetrievalJson)(cycle.gaps) !== gapsJson
|
|
2305
|
+
|| (0, retrieval_sessions_1.serializeRetrievalJson)(cycle.selectedIds) !== selectedJson
|
|
2306
|
+
|| (0, retrieval_sessions_1.serializeRetrievalJson)(cycle.rejectedIds) !== rejectedJson
|
|
2307
|
+
|| (0, retrieval_sessions_1.serializeRetrievalJson)(cycle.quality) !== qualityJson) {
|
|
2308
|
+
throw new Error(`RETRIEVAL_CYCLE_CONFLICT:${input.cycle}`);
|
|
2309
|
+
}
|
|
2310
|
+
this.db.exec('COMMIT');
|
|
2311
|
+
return cycle;
|
|
2312
|
+
}
|
|
2313
|
+
if (session.state !== 'open')
|
|
2314
|
+
throw new Error(`RETRIEVAL_SESSION_TERMINAL:${session.state}`);
|
|
2315
|
+
if (session.expiresAt <= now)
|
|
2316
|
+
throw new Error('RETRIEVAL_SESSION_EXPIRED');
|
|
2317
|
+
if (input.cycle !== session.cycle + 1)
|
|
2318
|
+
throw new Error(`RETRIEVAL_CYCLE_SEQUENCE:${session.cycle}->${input.cycle}`);
|
|
2319
|
+
if (input.tokenCost > session.remainingTokenBudget
|
|
2320
|
+
|| input.remainingTokenBudget !== session.remainingTokenBudget - input.tokenCost) {
|
|
2321
|
+
throw new Error('RETRIEVAL_BUDGET_MISMATCH');
|
|
2322
|
+
}
|
|
2323
|
+
this.db.prepare(`
|
|
2324
|
+
INSERT INTO devflow_retrieval_cycles
|
|
2325
|
+
(retrieval_session_id, cycle, gaps_json, selected_ids, rejected_ids, token_cost,
|
|
2326
|
+
remaining_token_budget, quality_json, receipt, evidence_hash, created_at)
|
|
2327
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2328
|
+
`).run(input.retrievalSessionId, input.cycle, gapsJson, selectedJson, rejectedJson, input.tokenCost, input.remainingTokenBudget, qualityJson, input.receipt, input.evidenceHash, now);
|
|
2329
|
+
this.db.prepare(`
|
|
2330
|
+
UPDATE devflow_retrieval_sessions
|
|
2331
|
+
SET cycle = ?, remaining_token_budget = ?, updated_at = ?
|
|
2332
|
+
WHERE id = ? AND project_root = ? AND session_id = ?
|
|
2333
|
+
`).run(input.cycle, input.remainingTokenBudget, now, input.retrievalSessionId, input.projectRoot, input.sessionId);
|
|
2334
|
+
const created = (0, retrieval_sessions_1.mapRetrievalCycleRow)(this.db.prepare(`
|
|
2335
|
+
SELECT * FROM devflow_retrieval_cycles WHERE retrieval_session_id = ? AND cycle = ?
|
|
2336
|
+
`).get(input.retrievalSessionId, input.cycle));
|
|
2337
|
+
this.db.exec('COMMIT');
|
|
2338
|
+
return created;
|
|
2339
|
+
}
|
|
2340
|
+
catch (error) {
|
|
2341
|
+
try {
|
|
2342
|
+
this.db.exec('ROLLBACK');
|
|
2343
|
+
}
|
|
2344
|
+
catch { }
|
|
2345
|
+
throw error;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
finalizeRetrievalSession(input) {
|
|
2349
|
+
const now = Date.now();
|
|
2350
|
+
const current = this.getRetrievalSession(input.projectRoot, input.sessionId, input.id);
|
|
2351
|
+
if (!current)
|
|
2352
|
+
throw new Error(`RETRIEVAL_SESSION_NOT_FOUND:${input.id}`);
|
|
2353
|
+
if (current.state !== 'open') {
|
|
2354
|
+
if (current.state === input.state && current.finalReceipt === input.finalReceipt)
|
|
2355
|
+
return current;
|
|
2356
|
+
throw new Error(`RETRIEVAL_SESSION_TERMINAL:${current.state}`);
|
|
2357
|
+
}
|
|
2358
|
+
this.db.prepare(`
|
|
2359
|
+
UPDATE devflow_retrieval_sessions SET state = ?, final_receipt = ?, updated_at = ?
|
|
2360
|
+
WHERE id = ? AND project_root = ? AND session_id = ? AND state = 'open'
|
|
2361
|
+
`).run(input.state, input.finalReceipt, now, input.id, input.projectRoot, input.sessionId);
|
|
2362
|
+
return this.getRetrievalSession(input.projectRoot, input.sessionId, input.id);
|
|
2363
|
+
}
|
|
2364
|
+
expireRetrievalSessions(now = Date.now(), limit = 100) {
|
|
2365
|
+
const rows = this.db.prepare(`
|
|
2366
|
+
SELECT id FROM devflow_retrieval_sessions
|
|
2367
|
+
WHERE state = 'open' AND expires_at <= ? ORDER BY expires_at ASC LIMIT ?
|
|
2368
|
+
`).all(now, Math.max(1, Math.min(1000, Math.floor(limit))));
|
|
2369
|
+
if (rows.length === 0)
|
|
2370
|
+
return 0;
|
|
2371
|
+
const placeholders = rows.map(() => '?').join(',');
|
|
2372
|
+
return this.db.prepare(`
|
|
2373
|
+
UPDATE devflow_retrieval_sessions SET state = 'expired', updated_at = ?
|
|
2374
|
+
WHERE state = 'open' AND id IN (${placeholders})
|
|
2375
|
+
`).run(now, ...rows.map(row => row.id)).changes;
|
|
2376
|
+
}
|
|
2377
|
+
validateRetrievalSessionInput(input) {
|
|
2378
|
+
for (const [field, value] of Object.entries({
|
|
2379
|
+
requestId: input.requestId,
|
|
2380
|
+
projectRoot: input.projectRoot,
|
|
2381
|
+
sessionId: input.sessionId,
|
|
2382
|
+
query: input.query,
|
|
2383
|
+
intent: input.intent,
|
|
2384
|
+
baselineReceipt: input.baselineReceipt,
|
|
2385
|
+
})) {
|
|
2386
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
2387
|
+
throw new Error(`RETRIEVAL_INVALID_${field}`);
|
|
2388
|
+
}
|
|
2389
|
+
if (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget < 0)
|
|
2390
|
+
throw new Error('RETRIEVAL_INVALID_BUDGET');
|
|
2391
|
+
if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now())
|
|
2392
|
+
throw new Error('RETRIEVAL_INVALID_EXPIRY');
|
|
2393
|
+
}
|
|
2394
|
+
assertRetrievalIdentity(existing, input) {
|
|
2395
|
+
const matches = existing.projectRoot === input.projectRoot
|
|
2396
|
+
&& existing.sessionId === input.sessionId
|
|
2397
|
+
&& existing.executionId === input.executionId
|
|
2398
|
+
&& existing.query === input.query
|
|
2399
|
+
&& existing.intent === input.intent
|
|
2400
|
+
&& existing.initialTokenBudget === input.tokenBudget
|
|
2401
|
+
&& existing.baselineReceipt === input.baselineReceipt;
|
|
2402
|
+
if (!matches)
|
|
2403
|
+
throw new Error(`RETRIEVAL_REQUEST_CONFLICT:${input.requestId}`);
|
|
2404
|
+
}
|
|
2183
2405
|
upsertContextReceipt(receipt) {
|
|
2184
2406
|
this.db.prepare(`
|
|
2185
2407
|
INSERT INTO devflow_context_receipts
|
|
@@ -2665,6 +2887,15 @@ class DevFlowDatabase {
|
|
|
2665
2887
|
`).all(projectRoot, runId);
|
|
2666
2888
|
return rows.map(host_actions_1.mapHostActionRow);
|
|
2667
2889
|
}
|
|
2890
|
+
listHostActionsForSession(projectRoot, sessionId, executionId) {
|
|
2891
|
+
const rows = this.db.prepare(`
|
|
2892
|
+
SELECT * FROM devflow_host_actions
|
|
2893
|
+
WHERE project_root = ? AND session_id = ?
|
|
2894
|
+
AND (? IS NULL OR execution_id = ?)
|
|
2895
|
+
ORDER BY created_at ASC, action_id ASC
|
|
2896
|
+
`).all(projectRoot, sessionId, executionId ?? null, executionId ?? null);
|
|
2897
|
+
return rows.map(host_actions_1.mapHostActionRow);
|
|
2898
|
+
}
|
|
2668
2899
|
transitionHostAction(identity, targetState, report, evidenceHash, requireNonEmptyReport = false) {
|
|
2669
2900
|
if (!identity.actionId.trim())
|
|
2670
2901
|
throw new Error('Host action requires an action ID');
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } f
|
|
|
2
2
|
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, ContextReceiptRecord, ContextSelectionEventRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
|
|
3
3
|
export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, } from './work-queue';
|
|
4
4
|
export { hostActionReportsEqual, isHostActionState, mapHostActionRow, serializeHostActionReport, } from './host-actions';
|
|
5
|
+
export { RETRIEVAL_MAX_CYCLES, isRetrievalSessionState } from './retrieval-sessions';
|
|
6
|
+
export type { AppendRetrievalCycleInput, CreateRetrievalSessionInput, RetrievalCycleRecord, RetrievalGapKind, RetrievalGapRecord, RetrievalSessionRecord, RetrievalSessionState, } from './retrieval-sessions';
|
|
5
7
|
export type { FailHostActionInput, HostActionRecord, HostActionState, ReportHostActionInput, RequestHostActionInput, StartHostActionInput, VerifyHostActionInput, } from './host-actions';
|
|
6
8
|
export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
|
|
7
9
|
export type { SessionObligationKind, SessionObligationRecord, SessionObligationState, } from './obligation-ledger';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeTurnId = exports.mapSessionObligationRow = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
3
|
+
exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
4
4
|
var database_1 = require("./database");
|
|
5
5
|
Object.defineProperty(exports, "DevFlowDatabase", { enumerable: true, get: function () { return database_1.DevFlowDatabase; } });
|
|
6
6
|
Object.defineProperty(exports, "getGlobalDevFlowDbPath", { enumerable: true, get: function () { return database_1.getGlobalDevFlowDbPath; } });
|
|
@@ -10,6 +10,9 @@ Object.defineProperty(exports, "hostActionReportsEqual", { enumerable: true, get
|
|
|
10
10
|
Object.defineProperty(exports, "isHostActionState", { enumerable: true, get: function () { return host_actions_1.isHostActionState; } });
|
|
11
11
|
Object.defineProperty(exports, "mapHostActionRow", { enumerable: true, get: function () { return host_actions_1.mapHostActionRow; } });
|
|
12
12
|
Object.defineProperty(exports, "serializeHostActionReport", { enumerable: true, get: function () { return host_actions_1.serializeHostActionReport; } });
|
|
13
|
+
var retrieval_sessions_1 = require("./retrieval-sessions");
|
|
14
|
+
Object.defineProperty(exports, "RETRIEVAL_MAX_CYCLES", { enumerable: true, get: function () { return retrieval_sessions_1.RETRIEVAL_MAX_CYCLES; } });
|
|
15
|
+
Object.defineProperty(exports, "isRetrievalSessionState", { enumerable: true, get: function () { return retrieval_sessions_1.isRetrievalSessionState; } });
|
|
13
16
|
var obligation_ledger_1 = require("./obligation-ledger");
|
|
14
17
|
Object.defineProperty(exports, "mapSessionObligationRow", { enumerable: true, get: function () { return obligation_ledger_1.mapSessionObligationRow; } });
|
|
15
18
|
Object.defineProperty(exports, "normalizeTurnId", { enumerable: true, get: function () { return obligation_ledger_1.normalizeTurnId; } });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const RETRIEVAL_MAX_CYCLES: 3;
|
|
2
|
+
export type RetrievalSessionState = 'open' | 'satisfied' | 'exhausted' | 'expired';
|
|
3
|
+
export type RetrievalGapKind = 'target' | 'caller' | 'route' | 'resource' | 'test' | 'configuration' | 'symbol_ambiguity' | 'memory_applicability' | 'knowledge_version' | 'runtime_evidence' | 'unavailable_channel';
|
|
4
|
+
export interface RetrievalGapRecord {
|
|
5
|
+
kind: RetrievalGapKind;
|
|
6
|
+
target?: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
evidence: string[];
|
|
9
|
+
resolver: 'codegraph' | 'memory' | 'knowledge' | 'analyzer' | 'none';
|
|
10
|
+
priority: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RetrievalCycleRecord {
|
|
13
|
+
retrievalSessionId: string;
|
|
14
|
+
cycle: number;
|
|
15
|
+
gaps: RetrievalGapRecord[];
|
|
16
|
+
selectedIds: string[];
|
|
17
|
+
rejectedIds: string[];
|
|
18
|
+
tokenCost: number;
|
|
19
|
+
remainingTokenBudget: number;
|
|
20
|
+
quality: Record<string, unknown>;
|
|
21
|
+
receipt: string;
|
|
22
|
+
evidenceHash: string;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RetrievalSessionRecord {
|
|
26
|
+
id: string;
|
|
27
|
+
requestId: string;
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
executionId?: string;
|
|
31
|
+
query: string;
|
|
32
|
+
intent: string;
|
|
33
|
+
state: RetrievalSessionState;
|
|
34
|
+
cycle: number;
|
|
35
|
+
maxCycles: typeof RETRIEVAL_MAX_CYCLES;
|
|
36
|
+
initialTokenBudget: number;
|
|
37
|
+
remainingTokenBudget: number;
|
|
38
|
+
baselineReceipt: string;
|
|
39
|
+
finalReceipt?: string;
|
|
40
|
+
createdAt: number;
|
|
41
|
+
updatedAt: number;
|
|
42
|
+
expiresAt: number;
|
|
43
|
+
}
|
|
44
|
+
export interface CreateRetrievalSessionInput {
|
|
45
|
+
id?: string;
|
|
46
|
+
requestId: string;
|
|
47
|
+
projectRoot: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
executionId?: string;
|
|
50
|
+
query: string;
|
|
51
|
+
intent: string;
|
|
52
|
+
tokenBudget: number;
|
|
53
|
+
baselineReceipt: string;
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
}
|
|
56
|
+
export interface AppendRetrievalCycleInput {
|
|
57
|
+
retrievalSessionId: string;
|
|
58
|
+
projectRoot: string;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
baselineReceipt: string;
|
|
61
|
+
cycle: number;
|
|
62
|
+
gaps: RetrievalGapRecord[];
|
|
63
|
+
selectedIds: string[];
|
|
64
|
+
rejectedIds: string[];
|
|
65
|
+
tokenCost: number;
|
|
66
|
+
remainingTokenBudget: number;
|
|
67
|
+
quality: Record<string, unknown>;
|
|
68
|
+
receipt: string;
|
|
69
|
+
evidenceHash: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function mapRetrievalSessionRow(row: Record<string, unknown>): RetrievalSessionRecord;
|
|
72
|
+
export declare function mapRetrievalCycleRow(row: Record<string, unknown>): RetrievalCycleRecord;
|
|
73
|
+
export declare function serializeRetrievalJson(value: unknown): string;
|
|
74
|
+
export declare function isRetrievalSessionState(value: unknown): value is RetrievalSessionState;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RETRIEVAL_MAX_CYCLES = void 0;
|
|
4
|
+
exports.mapRetrievalSessionRow = mapRetrievalSessionRow;
|
|
5
|
+
exports.mapRetrievalCycleRow = mapRetrievalCycleRow;
|
|
6
|
+
exports.serializeRetrievalJson = serializeRetrievalJson;
|
|
7
|
+
exports.isRetrievalSessionState = isRetrievalSessionState;
|
|
8
|
+
exports.RETRIEVAL_MAX_CYCLES = 3;
|
|
9
|
+
function mapRetrievalSessionRow(row) {
|
|
10
|
+
const state = requiredString(row.state, 'state');
|
|
11
|
+
if (!isRetrievalSessionState(state)) {
|
|
12
|
+
throw new Error(`Invalid persisted retrieval session state: ${state}`);
|
|
13
|
+
}
|
|
14
|
+
const maxCycles = finiteInteger(row.max_cycles, 'max_cycles');
|
|
15
|
+
if (maxCycles !== exports.RETRIEVAL_MAX_CYCLES) {
|
|
16
|
+
throw new Error(`Invalid persisted retrieval max_cycles: ${maxCycles}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: requiredString(row.id, 'id'),
|
|
20
|
+
requestId: requiredString(row.request_id, 'request_id'),
|
|
21
|
+
projectRoot: requiredString(row.project_root, 'project_root'),
|
|
22
|
+
sessionId: requiredString(row.session_id, 'session_id'),
|
|
23
|
+
executionId: optionalString(row.execution_id),
|
|
24
|
+
query: requiredString(row.query, 'query'),
|
|
25
|
+
intent: requiredString(row.intent, 'intent'),
|
|
26
|
+
state,
|
|
27
|
+
cycle: finiteInteger(row.cycle, 'cycle'),
|
|
28
|
+
maxCycles: exports.RETRIEVAL_MAX_CYCLES,
|
|
29
|
+
initialTokenBudget: finiteInteger(row.initial_token_budget, 'initial_token_budget'),
|
|
30
|
+
remainingTokenBudget: finiteInteger(row.remaining_token_budget, 'remaining_token_budget'),
|
|
31
|
+
baselineReceipt: requiredString(row.baseline_receipt, 'baseline_receipt'),
|
|
32
|
+
finalReceipt: optionalString(row.final_receipt),
|
|
33
|
+
createdAt: finiteInteger(row.created_at, 'created_at'),
|
|
34
|
+
updatedAt: finiteInteger(row.updated_at, 'updated_at'),
|
|
35
|
+
expiresAt: finiteInteger(row.expires_at, 'expires_at'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function mapRetrievalCycleRow(row) {
|
|
39
|
+
return {
|
|
40
|
+
retrievalSessionId: requiredString(row.retrieval_session_id, 'retrieval_session_id'),
|
|
41
|
+
cycle: finiteInteger(row.cycle, 'cycle'),
|
|
42
|
+
gaps: parseJsonArray(row.gaps_json, 'gaps_json'),
|
|
43
|
+
selectedIds: parseStringArray(row.selected_ids, 'selected_ids'),
|
|
44
|
+
rejectedIds: parseStringArray(row.rejected_ids, 'rejected_ids'),
|
|
45
|
+
tokenCost: finiteInteger(row.token_cost, 'token_cost'),
|
|
46
|
+
remainingTokenBudget: finiteInteger(row.remaining_token_budget, 'remaining_token_budget'),
|
|
47
|
+
quality: parseJsonObject(row.quality_json, 'quality_json'),
|
|
48
|
+
receipt: requiredString(row.receipt, 'receipt'),
|
|
49
|
+
evidenceHash: requiredString(row.evidence_hash, 'evidence_hash'),
|
|
50
|
+
createdAt: finiteInteger(row.created_at, 'created_at'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function serializeRetrievalJson(value) {
|
|
54
|
+
return JSON.stringify(canonicalize(value));
|
|
55
|
+
}
|
|
56
|
+
function isRetrievalSessionState(value) {
|
|
57
|
+
return value === 'open' || value === 'satisfied' || value === 'exhausted' || value === 'expired';
|
|
58
|
+
}
|
|
59
|
+
function canonicalize(value) {
|
|
60
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
61
|
+
return value;
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
if (!Number.isFinite(value) || Object.is(value, -0))
|
|
64
|
+
throw new Error('Retrieval evidence must be lossless JSON');
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return value.map(canonicalize);
|
|
69
|
+
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
70
|
+
throw new Error('Retrieval evidence must be plain JSON');
|
|
71
|
+
}
|
|
72
|
+
const record = value;
|
|
73
|
+
return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonicalize(record[key])]));
|
|
74
|
+
}
|
|
75
|
+
function parseJson(value, field) {
|
|
76
|
+
if (typeof value !== 'string')
|
|
77
|
+
throw new Error(`Invalid persisted ${field}: expected JSON text`);
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(value);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new Error(`Invalid persisted ${field}: malformed JSON`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function parseJsonArray(value, field) {
|
|
86
|
+
const parsed = parseJson(value, field);
|
|
87
|
+
if (!Array.isArray(parsed))
|
|
88
|
+
throw new Error(`Invalid persisted ${field}: expected array`);
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
function parseStringArray(value, field) {
|
|
92
|
+
const parsed = parseJsonArray(value, field);
|
|
93
|
+
if (parsed.some(item => typeof item !== 'string'))
|
|
94
|
+
throw new Error(`Invalid persisted ${field}: expected strings`);
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
function parseJsonObject(value, field) {
|
|
98
|
+
const parsed = parseJson(value, field);
|
|
99
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
100
|
+
throw new Error(`Invalid persisted ${field}: expected object`);
|
|
101
|
+
}
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
function requiredString(value, field) {
|
|
105
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
106
|
+
throw new Error(`Invalid persisted ${field}`);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function optionalString(value) {
|
|
110
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
111
|
+
}
|
|
112
|
+
function finiteInteger(value, field) {
|
|
113
|
+
const result = Number(value);
|
|
114
|
+
if (!Number.isSafeInteger(result) || result < 0)
|
|
115
|
+
throw new Error(`Invalid persisted ${field}`);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
package/dist/work-queue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WorkKind = 'memory.explicit_commit' | 'memory.explicit_enrichment' | 'memory.turn_capture' | 'memory.turn_distill' | 'memory.vector_backfill' | 'session.bootstrap' | 'session.finalize' | 'telemetry.reconcile';
|
|
1
|
+
export type WorkKind = 'memory.explicit_commit' | 'memory.explicit_enrichment' | 'memory.turn_capture' | 'memory.turn_distill' | 'memory.vector_backfill' | 'context.prefetch' | 'session.bootstrap' | 'session.finalize' | 'telemetry.reconcile';
|
|
2
2
|
export type WorkState = 'pending' | 'leased' | 'completed' | 'failed' | 'dead_letter';
|
|
3
3
|
export interface WorkError {
|
|
4
4
|
category: string;
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -31,6 +31,17 @@ import {
|
|
|
31
31
|
type StartHostActionInput,
|
|
32
32
|
type VerifyHostActionInput,
|
|
33
33
|
} from './host-actions';
|
|
34
|
+
import {
|
|
35
|
+
RETRIEVAL_MAX_CYCLES,
|
|
36
|
+
mapRetrievalCycleRow,
|
|
37
|
+
mapRetrievalSessionRow,
|
|
38
|
+
serializeRetrievalJson,
|
|
39
|
+
type AppendRetrievalCycleInput,
|
|
40
|
+
type CreateRetrievalSessionInput,
|
|
41
|
+
type RetrievalCycleRecord,
|
|
42
|
+
type RetrievalSessionRecord,
|
|
43
|
+
type RetrievalSessionState,
|
|
44
|
+
} from './retrieval-sessions';
|
|
34
45
|
|
|
35
46
|
export interface BenchmarkReportRecord {
|
|
36
47
|
runId: string;
|
|
@@ -477,6 +488,50 @@ export class DevFlowDatabase {
|
|
|
477
488
|
CREATE INDEX IF NOT EXISTS idx_context_selection_identity
|
|
478
489
|
ON devflow_context_selection_events(project_root, session_id, execution_id, request_id);
|
|
479
490
|
|
|
491
|
+
CREATE TABLE IF NOT EXISTS devflow_retrieval_sessions (
|
|
492
|
+
id TEXT PRIMARY KEY,
|
|
493
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
494
|
+
project_root TEXT NOT NULL,
|
|
495
|
+
session_id TEXT NOT NULL,
|
|
496
|
+
execution_id TEXT,
|
|
497
|
+
query TEXT NOT NULL,
|
|
498
|
+
intent TEXT NOT NULL,
|
|
499
|
+
state TEXT NOT NULL DEFAULT 'open'
|
|
500
|
+
CHECK(state IN ('open', 'satisfied', 'exhausted', 'expired')),
|
|
501
|
+
cycle INTEGER NOT NULL DEFAULT 0 CHECK(cycle BETWEEN 0 AND 3),
|
|
502
|
+
max_cycles INTEGER NOT NULL DEFAULT 3 CHECK(max_cycles = 3),
|
|
503
|
+
initial_token_budget INTEGER NOT NULL CHECK(initial_token_budget >= 0),
|
|
504
|
+
remaining_token_budget INTEGER NOT NULL CHECK(remaining_token_budget >= 0),
|
|
505
|
+
baseline_receipt TEXT NOT NULL,
|
|
506
|
+
final_receipt TEXT,
|
|
507
|
+
created_at INTEGER NOT NULL,
|
|
508
|
+
updated_at INTEGER NOT NULL,
|
|
509
|
+
expires_at INTEGER NOT NULL
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_sessions_owner
|
|
513
|
+
ON devflow_retrieval_sessions(project_root, session_id, state, updated_at DESC);
|
|
514
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_sessions_expiry
|
|
515
|
+
ON devflow_retrieval_sessions(state, expires_at);
|
|
516
|
+
|
|
517
|
+
CREATE TABLE IF NOT EXISTS devflow_retrieval_cycles (
|
|
518
|
+
retrieval_session_id TEXT NOT NULL,
|
|
519
|
+
cycle INTEGER NOT NULL CHECK(cycle BETWEEN 1 AND 3),
|
|
520
|
+
gaps_json TEXT NOT NULL DEFAULT '[]',
|
|
521
|
+
selected_ids TEXT NOT NULL DEFAULT '[]',
|
|
522
|
+
rejected_ids TEXT NOT NULL DEFAULT '[]',
|
|
523
|
+
token_cost INTEGER NOT NULL CHECK(token_cost >= 0),
|
|
524
|
+
remaining_token_budget INTEGER NOT NULL CHECK(remaining_token_budget >= 0),
|
|
525
|
+
quality_json TEXT NOT NULL DEFAULT '{}',
|
|
526
|
+
receipt TEXT NOT NULL,
|
|
527
|
+
evidence_hash TEXT NOT NULL,
|
|
528
|
+
created_at INTEGER NOT NULL,
|
|
529
|
+
PRIMARY KEY (retrieval_session_id, cycle)
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_retrieval_cycles_receipt
|
|
533
|
+
ON devflow_retrieval_cycles(receipt);
|
|
534
|
+
|
|
480
535
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
481
536
|
id TEXT PRIMARY KEY,
|
|
482
537
|
project_root TEXT NOT NULL,
|
|
@@ -2677,6 +2732,207 @@ export class DevFlowDatabase {
|
|
|
2677
2732
|
.run(projectRoot).changes > 0;
|
|
2678
2733
|
}
|
|
2679
2734
|
|
|
2735
|
+
createRetrievalSession(input: CreateRetrievalSessionInput): RetrievalSessionRecord {
|
|
2736
|
+
this.validateRetrievalSessionInput(input);
|
|
2737
|
+
const now = Date.now();
|
|
2738
|
+
const id = input.id?.trim() || randomUUID();
|
|
2739
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
2740
|
+
try {
|
|
2741
|
+
const existing = this.getRetrievalSessionByRequest(input.requestId);
|
|
2742
|
+
if (existing) {
|
|
2743
|
+
this.assertRetrievalIdentity(existing, input);
|
|
2744
|
+
this.db.exec('COMMIT');
|
|
2745
|
+
return existing;
|
|
2746
|
+
}
|
|
2747
|
+
this.db.prepare(`
|
|
2748
|
+
INSERT INTO devflow_retrieval_sessions
|
|
2749
|
+
(id, request_id, project_root, session_id, execution_id, query, intent, state,
|
|
2750
|
+
cycle, max_cycles, initial_token_budget, remaining_token_budget,
|
|
2751
|
+
baseline_receipt, created_at, updated_at, expires_at)
|
|
2752
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'open', 0, ?, ?, ?, ?, ?, ?, ?)
|
|
2753
|
+
`).run(
|
|
2754
|
+
id,
|
|
2755
|
+
input.requestId,
|
|
2756
|
+
input.projectRoot,
|
|
2757
|
+
input.sessionId,
|
|
2758
|
+
input.executionId ?? null,
|
|
2759
|
+
input.query,
|
|
2760
|
+
input.intent,
|
|
2761
|
+
RETRIEVAL_MAX_CYCLES,
|
|
2762
|
+
input.tokenBudget,
|
|
2763
|
+
input.tokenBudget,
|
|
2764
|
+
input.baselineReceipt,
|
|
2765
|
+
now,
|
|
2766
|
+
now,
|
|
2767
|
+
input.expiresAt,
|
|
2768
|
+
);
|
|
2769
|
+
const created = this.getRetrievalSession(input.projectRoot, input.sessionId, id)!;
|
|
2770
|
+
this.db.exec('COMMIT');
|
|
2771
|
+
return created;
|
|
2772
|
+
} catch (error) {
|
|
2773
|
+
try { this.db.exec('ROLLBACK'); } catch {}
|
|
2774
|
+
throw error;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
getRetrievalSession(projectRoot: string, sessionId: string, id: string): RetrievalSessionRecord | null {
|
|
2779
|
+
const row = this.db.prepare(`
|
|
2780
|
+
SELECT * FROM devflow_retrieval_sessions
|
|
2781
|
+
WHERE id = ? AND project_root = ? AND session_id = ?
|
|
2782
|
+
`).get(id, projectRoot, sessionId) as Record<string, unknown> | undefined;
|
|
2783
|
+
return row ? mapRetrievalSessionRow(row) : null;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
getRetrievalSessionByRequest(requestId: string): RetrievalSessionRecord | null {
|
|
2787
|
+
const row = this.db.prepare(`
|
|
2788
|
+
SELECT * FROM devflow_retrieval_sessions WHERE request_id = ?
|
|
2789
|
+
`).get(requestId) as Record<string, unknown> | undefined;
|
|
2790
|
+
return row ? mapRetrievalSessionRow(row) : null;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
listRetrievalCycles(retrievalSessionId: string): RetrievalCycleRecord[] {
|
|
2794
|
+
return (this.db.prepare(`
|
|
2795
|
+
SELECT * FROM devflow_retrieval_cycles
|
|
2796
|
+
WHERE retrieval_session_id = ? ORDER BY cycle ASC
|
|
2797
|
+
`).all(retrievalSessionId) as Array<Record<string, unknown>>).map(mapRetrievalCycleRow);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
appendRetrievalCycle(input: AppendRetrievalCycleInput): RetrievalCycleRecord {
|
|
2801
|
+
if (!Number.isInteger(input.cycle) || input.cycle < 1 || input.cycle > RETRIEVAL_MAX_CYCLES) {
|
|
2802
|
+
throw new Error(`RETRIEVAL_INVALID_CYCLE:${input.cycle}`);
|
|
2803
|
+
}
|
|
2804
|
+
if (!Number.isSafeInteger(input.tokenCost) || input.tokenCost < 0
|
|
2805
|
+
|| !Number.isSafeInteger(input.remainingTokenBudget) || input.remainingTokenBudget < 0) {
|
|
2806
|
+
throw new Error('RETRIEVAL_INVALID_BUDGET');
|
|
2807
|
+
}
|
|
2808
|
+
const gapsJson = serializeRetrievalJson(input.gaps);
|
|
2809
|
+
const selectedJson = serializeRetrievalJson([...new Set(input.selectedIds)]);
|
|
2810
|
+
const rejectedJson = serializeRetrievalJson([...new Set(input.rejectedIds)]);
|
|
2811
|
+
const qualityJson = serializeRetrievalJson(input.quality);
|
|
2812
|
+
const now = Date.now();
|
|
2813
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
2814
|
+
try {
|
|
2815
|
+
const session = this.getRetrievalSession(input.projectRoot, input.sessionId, input.retrievalSessionId);
|
|
2816
|
+
if (!session) throw new Error(`RETRIEVAL_SESSION_NOT_FOUND:${input.retrievalSessionId}`);
|
|
2817
|
+
if (session.baselineReceipt !== input.baselineReceipt) throw new Error('RETRIEVAL_BASELINE_RECEIPT_MISMATCH');
|
|
2818
|
+
|
|
2819
|
+
const existing = this.db.prepare(`
|
|
2820
|
+
SELECT * FROM devflow_retrieval_cycles WHERE retrieval_session_id = ? AND cycle = ?
|
|
2821
|
+
`).get(input.retrievalSessionId, input.cycle) as Record<string, unknown> | undefined;
|
|
2822
|
+
if (existing) {
|
|
2823
|
+
const cycle = mapRetrievalCycleRow(existing);
|
|
2824
|
+
if (cycle.evidenceHash !== input.evidenceHash || cycle.receipt !== input.receipt
|
|
2825
|
+
|| serializeRetrievalJson(cycle.gaps) !== gapsJson
|
|
2826
|
+
|| serializeRetrievalJson(cycle.selectedIds) !== selectedJson
|
|
2827
|
+
|| serializeRetrievalJson(cycle.rejectedIds) !== rejectedJson
|
|
2828
|
+
|| serializeRetrievalJson(cycle.quality) !== qualityJson) {
|
|
2829
|
+
throw new Error(`RETRIEVAL_CYCLE_CONFLICT:${input.cycle}`);
|
|
2830
|
+
}
|
|
2831
|
+
this.db.exec('COMMIT');
|
|
2832
|
+
return cycle;
|
|
2833
|
+
}
|
|
2834
|
+
if (session.state !== 'open') throw new Error(`RETRIEVAL_SESSION_TERMINAL:${session.state}`);
|
|
2835
|
+
if (session.expiresAt <= now) throw new Error('RETRIEVAL_SESSION_EXPIRED');
|
|
2836
|
+
if (input.cycle !== session.cycle + 1) throw new Error(`RETRIEVAL_CYCLE_SEQUENCE:${session.cycle}->${input.cycle}`);
|
|
2837
|
+
if (input.tokenCost > session.remainingTokenBudget
|
|
2838
|
+
|| input.remainingTokenBudget !== session.remainingTokenBudget - input.tokenCost) {
|
|
2839
|
+
throw new Error('RETRIEVAL_BUDGET_MISMATCH');
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
this.db.prepare(`
|
|
2843
|
+
INSERT INTO devflow_retrieval_cycles
|
|
2844
|
+
(retrieval_session_id, cycle, gaps_json, selected_ids, rejected_ids, token_cost,
|
|
2845
|
+
remaining_token_budget, quality_json, receipt, evidence_hash, created_at)
|
|
2846
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2847
|
+
`).run(
|
|
2848
|
+
input.retrievalSessionId,
|
|
2849
|
+
input.cycle,
|
|
2850
|
+
gapsJson,
|
|
2851
|
+
selectedJson,
|
|
2852
|
+
rejectedJson,
|
|
2853
|
+
input.tokenCost,
|
|
2854
|
+
input.remainingTokenBudget,
|
|
2855
|
+
qualityJson,
|
|
2856
|
+
input.receipt,
|
|
2857
|
+
input.evidenceHash,
|
|
2858
|
+
now,
|
|
2859
|
+
);
|
|
2860
|
+
this.db.prepare(`
|
|
2861
|
+
UPDATE devflow_retrieval_sessions
|
|
2862
|
+
SET cycle = ?, remaining_token_budget = ?, updated_at = ?
|
|
2863
|
+
WHERE id = ? AND project_root = ? AND session_id = ?
|
|
2864
|
+
`).run(input.cycle, input.remainingTokenBudget, now, input.retrievalSessionId, input.projectRoot, input.sessionId);
|
|
2865
|
+
const created = mapRetrievalCycleRow(this.db.prepare(`
|
|
2866
|
+
SELECT * FROM devflow_retrieval_cycles WHERE retrieval_session_id = ? AND cycle = ?
|
|
2867
|
+
`).get(input.retrievalSessionId, input.cycle) as Record<string, unknown>);
|
|
2868
|
+
this.db.exec('COMMIT');
|
|
2869
|
+
return created;
|
|
2870
|
+
} catch (error) {
|
|
2871
|
+
try { this.db.exec('ROLLBACK'); } catch {}
|
|
2872
|
+
throw error;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
finalizeRetrievalSession(input: {
|
|
2877
|
+
id: string;
|
|
2878
|
+
projectRoot: string;
|
|
2879
|
+
sessionId: string;
|
|
2880
|
+
state: Extract<RetrievalSessionState, 'satisfied' | 'exhausted'>;
|
|
2881
|
+
finalReceipt: string;
|
|
2882
|
+
}): RetrievalSessionRecord {
|
|
2883
|
+
const now = Date.now();
|
|
2884
|
+
const current = this.getRetrievalSession(input.projectRoot, input.sessionId, input.id);
|
|
2885
|
+
if (!current) throw new Error(`RETRIEVAL_SESSION_NOT_FOUND:${input.id}`);
|
|
2886
|
+
if (current.state !== 'open') {
|
|
2887
|
+
if (current.state === input.state && current.finalReceipt === input.finalReceipt) return current;
|
|
2888
|
+
throw new Error(`RETRIEVAL_SESSION_TERMINAL:${current.state}`);
|
|
2889
|
+
}
|
|
2890
|
+
this.db.prepare(`
|
|
2891
|
+
UPDATE devflow_retrieval_sessions SET state = ?, final_receipt = ?, updated_at = ?
|
|
2892
|
+
WHERE id = ? AND project_root = ? AND session_id = ? AND state = 'open'
|
|
2893
|
+
`).run(input.state, input.finalReceipt, now, input.id, input.projectRoot, input.sessionId);
|
|
2894
|
+
return this.getRetrievalSession(input.projectRoot, input.sessionId, input.id)!;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
expireRetrievalSessions(now = Date.now(), limit = 100): number {
|
|
2898
|
+
const rows = this.db.prepare(`
|
|
2899
|
+
SELECT id FROM devflow_retrieval_sessions
|
|
2900
|
+
WHERE state = 'open' AND expires_at <= ? ORDER BY expires_at ASC LIMIT ?
|
|
2901
|
+
`).all(now, Math.max(1, Math.min(1000, Math.floor(limit)))) as Array<{ id: string }>;
|
|
2902
|
+
if (rows.length === 0) return 0;
|
|
2903
|
+
const placeholders = rows.map(() => '?').join(',');
|
|
2904
|
+
return this.db.prepare(`
|
|
2905
|
+
UPDATE devflow_retrieval_sessions SET state = 'expired', updated_at = ?
|
|
2906
|
+
WHERE state = 'open' AND id IN (${placeholders})
|
|
2907
|
+
`).run(now, ...rows.map(row => row.id)).changes;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
private validateRetrievalSessionInput(input: CreateRetrievalSessionInput): void {
|
|
2911
|
+
for (const [field, value] of Object.entries({
|
|
2912
|
+
requestId: input.requestId,
|
|
2913
|
+
projectRoot: input.projectRoot,
|
|
2914
|
+
sessionId: input.sessionId,
|
|
2915
|
+
query: input.query,
|
|
2916
|
+
intent: input.intent,
|
|
2917
|
+
baselineReceipt: input.baselineReceipt,
|
|
2918
|
+
})) {
|
|
2919
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`RETRIEVAL_INVALID_${field}`);
|
|
2920
|
+
}
|
|
2921
|
+
if (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget < 0) throw new Error('RETRIEVAL_INVALID_BUDGET');
|
|
2922
|
+
if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now()) throw new Error('RETRIEVAL_INVALID_EXPIRY');
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
private assertRetrievalIdentity(existing: RetrievalSessionRecord, input: CreateRetrievalSessionInput): void {
|
|
2926
|
+
const matches = existing.projectRoot === input.projectRoot
|
|
2927
|
+
&& existing.sessionId === input.sessionId
|
|
2928
|
+
&& existing.executionId === input.executionId
|
|
2929
|
+
&& existing.query === input.query
|
|
2930
|
+
&& existing.intent === input.intent
|
|
2931
|
+
&& existing.initialTokenBudget === input.tokenBudget
|
|
2932
|
+
&& existing.baselineReceipt === input.baselineReceipt;
|
|
2933
|
+
if (!matches) throw new Error(`RETRIEVAL_REQUEST_CONFLICT:${input.requestId}`);
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2680
2936
|
upsertContextReceipt(receipt: ContextReceiptRecord): void {
|
|
2681
2937
|
this.db.prepare(`
|
|
2682
2938
|
INSERT INTO devflow_context_receipts
|
|
@@ -3335,6 +3591,16 @@ export class DevFlowDatabase {
|
|
|
3335
3591
|
return rows.map(mapHostActionRow);
|
|
3336
3592
|
}
|
|
3337
3593
|
|
|
3594
|
+
listHostActionsForSession(projectRoot: string, sessionId: string, executionId?: string): HostActionRecord[] {
|
|
3595
|
+
const rows = this.db.prepare(`
|
|
3596
|
+
SELECT * FROM devflow_host_actions
|
|
3597
|
+
WHERE project_root = ? AND session_id = ?
|
|
3598
|
+
AND (? IS NULL OR execution_id = ?)
|
|
3599
|
+
ORDER BY created_at ASC, action_id ASC
|
|
3600
|
+
`).all(projectRoot, sessionId, executionId ?? null, executionId ?? null) as Array<Record<string, unknown>>;
|
|
3601
|
+
return rows.map(mapHostActionRow);
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3338
3604
|
private transitionHostAction(
|
|
3339
3605
|
identity: StartHostActionInput,
|
|
3340
3606
|
targetState: Exclude<HostActionState, 'waiting'>,
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,16 @@ export {
|
|
|
36
36
|
mapHostActionRow,
|
|
37
37
|
serializeHostActionReport,
|
|
38
38
|
} from './host-actions';
|
|
39
|
+
export { RETRIEVAL_MAX_CYCLES, isRetrievalSessionState } from './retrieval-sessions';
|
|
40
|
+
export type {
|
|
41
|
+
AppendRetrievalCycleInput,
|
|
42
|
+
CreateRetrievalSessionInput,
|
|
43
|
+
RetrievalCycleRecord,
|
|
44
|
+
RetrievalGapKind,
|
|
45
|
+
RetrievalGapRecord,
|
|
46
|
+
RetrievalSessionRecord,
|
|
47
|
+
RetrievalSessionState,
|
|
48
|
+
} from './retrieval-sessions';
|
|
39
49
|
export type {
|
|
40
50
|
FailHostActionInput,
|
|
41
51
|
HostActionRecord,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
export const RETRIEVAL_MAX_CYCLES = 3 as const;
|
|
2
|
+
|
|
3
|
+
export type RetrievalSessionState = 'open' | 'satisfied' | 'exhausted' | 'expired';
|
|
4
|
+
|
|
5
|
+
export type RetrievalGapKind =
|
|
6
|
+
| 'target'
|
|
7
|
+
| 'caller'
|
|
8
|
+
| 'route'
|
|
9
|
+
| 'resource'
|
|
10
|
+
| 'test'
|
|
11
|
+
| 'configuration'
|
|
12
|
+
| 'symbol_ambiguity'
|
|
13
|
+
| 'memory_applicability'
|
|
14
|
+
| 'knowledge_version'
|
|
15
|
+
| 'runtime_evidence'
|
|
16
|
+
| 'unavailable_channel';
|
|
17
|
+
|
|
18
|
+
export interface RetrievalGapRecord {
|
|
19
|
+
kind: RetrievalGapKind;
|
|
20
|
+
target?: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
evidence: string[];
|
|
23
|
+
resolver: 'codegraph' | 'memory' | 'knowledge' | 'analyzer' | 'none';
|
|
24
|
+
priority: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RetrievalCycleRecord {
|
|
28
|
+
retrievalSessionId: string;
|
|
29
|
+
cycle: number;
|
|
30
|
+
gaps: RetrievalGapRecord[];
|
|
31
|
+
selectedIds: string[];
|
|
32
|
+
rejectedIds: string[];
|
|
33
|
+
tokenCost: number;
|
|
34
|
+
remainingTokenBudget: number;
|
|
35
|
+
quality: Record<string, unknown>;
|
|
36
|
+
receipt: string;
|
|
37
|
+
evidenceHash: string;
|
|
38
|
+
createdAt: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RetrievalSessionRecord {
|
|
42
|
+
id: string;
|
|
43
|
+
requestId: string;
|
|
44
|
+
projectRoot: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
executionId?: string;
|
|
47
|
+
query: string;
|
|
48
|
+
intent: string;
|
|
49
|
+
state: RetrievalSessionState;
|
|
50
|
+
cycle: number;
|
|
51
|
+
maxCycles: typeof RETRIEVAL_MAX_CYCLES;
|
|
52
|
+
initialTokenBudget: number;
|
|
53
|
+
remainingTokenBudget: number;
|
|
54
|
+
baselineReceipt: string;
|
|
55
|
+
finalReceipt?: string;
|
|
56
|
+
createdAt: number;
|
|
57
|
+
updatedAt: number;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CreateRetrievalSessionInput {
|
|
62
|
+
id?: string;
|
|
63
|
+
requestId: string;
|
|
64
|
+
projectRoot: string;
|
|
65
|
+
sessionId: string;
|
|
66
|
+
executionId?: string;
|
|
67
|
+
query: string;
|
|
68
|
+
intent: string;
|
|
69
|
+
tokenBudget: number;
|
|
70
|
+
baselineReceipt: string;
|
|
71
|
+
expiresAt: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AppendRetrievalCycleInput {
|
|
75
|
+
retrievalSessionId: string;
|
|
76
|
+
projectRoot: string;
|
|
77
|
+
sessionId: string;
|
|
78
|
+
baselineReceipt: string;
|
|
79
|
+
cycle: number;
|
|
80
|
+
gaps: RetrievalGapRecord[];
|
|
81
|
+
selectedIds: string[];
|
|
82
|
+
rejectedIds: string[];
|
|
83
|
+
tokenCost: number;
|
|
84
|
+
remainingTokenBudget: number;
|
|
85
|
+
quality: Record<string, unknown>;
|
|
86
|
+
receipt: string;
|
|
87
|
+
evidenceHash: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function mapRetrievalSessionRow(row: Record<string, unknown>): RetrievalSessionRecord {
|
|
91
|
+
const state = requiredString(row.state, 'state');
|
|
92
|
+
if (!isRetrievalSessionState(state)) {
|
|
93
|
+
throw new Error(`Invalid persisted retrieval session state: ${state}`);
|
|
94
|
+
}
|
|
95
|
+
const maxCycles = finiteInteger(row.max_cycles, 'max_cycles');
|
|
96
|
+
if (maxCycles !== RETRIEVAL_MAX_CYCLES) {
|
|
97
|
+
throw new Error(`Invalid persisted retrieval max_cycles: ${maxCycles}`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
id: requiredString(row.id, 'id'),
|
|
101
|
+
requestId: requiredString(row.request_id, 'request_id'),
|
|
102
|
+
projectRoot: requiredString(row.project_root, 'project_root'),
|
|
103
|
+
sessionId: requiredString(row.session_id, 'session_id'),
|
|
104
|
+
executionId: optionalString(row.execution_id),
|
|
105
|
+
query: requiredString(row.query, 'query'),
|
|
106
|
+
intent: requiredString(row.intent, 'intent'),
|
|
107
|
+
state,
|
|
108
|
+
cycle: finiteInteger(row.cycle, 'cycle'),
|
|
109
|
+
maxCycles: RETRIEVAL_MAX_CYCLES,
|
|
110
|
+
initialTokenBudget: finiteInteger(row.initial_token_budget, 'initial_token_budget'),
|
|
111
|
+
remainingTokenBudget: finiteInteger(row.remaining_token_budget, 'remaining_token_budget'),
|
|
112
|
+
baselineReceipt: requiredString(row.baseline_receipt, 'baseline_receipt'),
|
|
113
|
+
finalReceipt: optionalString(row.final_receipt),
|
|
114
|
+
createdAt: finiteInteger(row.created_at, 'created_at'),
|
|
115
|
+
updatedAt: finiteInteger(row.updated_at, 'updated_at'),
|
|
116
|
+
expiresAt: finiteInteger(row.expires_at, 'expires_at'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mapRetrievalCycleRow(row: Record<string, unknown>): RetrievalCycleRecord {
|
|
121
|
+
return {
|
|
122
|
+
retrievalSessionId: requiredString(row.retrieval_session_id, 'retrieval_session_id'),
|
|
123
|
+
cycle: finiteInteger(row.cycle, 'cycle'),
|
|
124
|
+
gaps: parseJsonArray(row.gaps_json, 'gaps_json') as RetrievalGapRecord[],
|
|
125
|
+
selectedIds: parseStringArray(row.selected_ids, 'selected_ids'),
|
|
126
|
+
rejectedIds: parseStringArray(row.rejected_ids, 'rejected_ids'),
|
|
127
|
+
tokenCost: finiteInteger(row.token_cost, 'token_cost'),
|
|
128
|
+
remainingTokenBudget: finiteInteger(row.remaining_token_budget, 'remaining_token_budget'),
|
|
129
|
+
quality: parseJsonObject(row.quality_json, 'quality_json'),
|
|
130
|
+
receipt: requiredString(row.receipt, 'receipt'),
|
|
131
|
+
evidenceHash: requiredString(row.evidence_hash, 'evidence_hash'),
|
|
132
|
+
createdAt: finiteInteger(row.created_at, 'created_at'),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function serializeRetrievalJson(value: unknown): string {
|
|
137
|
+
return JSON.stringify(canonicalize(value));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isRetrievalSessionState(value: unknown): value is RetrievalSessionState {
|
|
141
|
+
return value === 'open' || value === 'satisfied' || value === 'exhausted' || value === 'expired';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function canonicalize(value: unknown): unknown {
|
|
145
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
146
|
+
if (typeof value === 'number') {
|
|
147
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) throw new Error('Retrieval evidence must be lossless JSON');
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
151
|
+
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
152
|
+
throw new Error('Retrieval evidence must be plain JSON');
|
|
153
|
+
}
|
|
154
|
+
const record = value as Record<string, unknown>;
|
|
155
|
+
return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonicalize(record[key])]));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseJson(value: unknown, field: string): unknown {
|
|
159
|
+
if (typeof value !== 'string') throw new Error(`Invalid persisted ${field}: expected JSON text`);
|
|
160
|
+
try { return JSON.parse(value) as unknown; } catch { throw new Error(`Invalid persisted ${field}: malformed JSON`); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseJsonArray(value: unknown, field: string): unknown[] {
|
|
164
|
+
const parsed = parseJson(value, field);
|
|
165
|
+
if (!Array.isArray(parsed)) throw new Error(`Invalid persisted ${field}: expected array`);
|
|
166
|
+
return parsed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseStringArray(value: unknown, field: string): string[] {
|
|
170
|
+
const parsed = parseJsonArray(value, field);
|
|
171
|
+
if (parsed.some(item => typeof item !== 'string')) throw new Error(`Invalid persisted ${field}: expected strings`);
|
|
172
|
+
return parsed as string[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseJsonObject(value: unknown, field: string): Record<string, unknown> {
|
|
176
|
+
const parsed = parseJson(value, field);
|
|
177
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
178
|
+
throw new Error(`Invalid persisted ${field}: expected object`);
|
|
179
|
+
}
|
|
180
|
+
return parsed as Record<string, unknown>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function requiredString(value: unknown, field: string): string {
|
|
184
|
+
if (typeof value !== 'string' || value.length === 0) throw new Error(`Invalid persisted ${field}`);
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function optionalString(value: unknown): string | undefined {
|
|
189
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function finiteInteger(value: unknown, field: string): number {
|
|
193
|
+
const result = Number(value);
|
|
194
|
+
if (!Number.isSafeInteger(result) || result < 0) throw new Error(`Invalid persisted ${field}`);
|
|
195
|
+
return result;
|
|
196
|
+
}
|