@devflow-tools/database 0.16.12 → 0.16.13
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 +11 -0
- package/dist/database.d.ts +41 -0
- package/dist/database.js +322 -32
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/obligation-ledger.d.ts +19 -0
- package/dist/obligation-ledger.js +35 -0
- package/dist/work-queue.d.ts +1 -1
- package/package.json +2 -2
- package/src/database.ts +421 -30
- package/src/index.ts +11 -0
- package/src/obligation-ledger.ts +57 -0
- package/src/work-queue.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devflow-tools/database",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.13",
|
|
4
4
|
"description": "DevFlow SQLite database package",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
"typescript": "^5.5.0",
|
|
14
14
|
"vitest": "^2.0.0"
|
|
15
15
|
},
|
|
16
|
-
"gitHead": "
|
|
16
|
+
"gitHead": "67c9dfc3709bd0ce36b7c2a9341896eedcc8a725"
|
|
17
17
|
}
|
package/src/database.ts
CHANGED
|
@@ -13,6 +13,12 @@ import type {
|
|
|
13
13
|
WorkQueueHealth,
|
|
14
14
|
WorkState,
|
|
15
15
|
} from './work-queue';
|
|
16
|
+
import {
|
|
17
|
+
mapSessionObligationRow,
|
|
18
|
+
normalizeTurnId,
|
|
19
|
+
type SessionObligationRecord,
|
|
20
|
+
type SessionObligationState,
|
|
21
|
+
} from './obligation-ledger';
|
|
16
22
|
|
|
17
23
|
export interface BenchmarkReportRecord {
|
|
18
24
|
runId: string;
|
|
@@ -132,6 +138,23 @@ export interface ContextReceiptRecord {
|
|
|
132
138
|
contextHash: string;
|
|
133
139
|
issuedAt: number;
|
|
134
140
|
expiresAt: number;
|
|
141
|
+
selectedFiles?: string[];
|
|
142
|
+
memoryIds?: string[];
|
|
143
|
+
canonicalNextAction?: string;
|
|
144
|
+
requestId?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ContextSelectionEventRecord {
|
|
148
|
+
id: string;
|
|
149
|
+
projectRoot: string;
|
|
150
|
+
sessionId: string;
|
|
151
|
+
executionId: string;
|
|
152
|
+
requestId?: string;
|
|
153
|
+
selectionType: 'code' | 'action';
|
|
154
|
+
candidateId: string;
|
|
155
|
+
toolName: string;
|
|
156
|
+
toolUseId?: string;
|
|
157
|
+
selectedAt: number;
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
export interface MemoryDistillCheckpointRecord {
|
|
@@ -416,12 +439,32 @@ export class DevFlowDatabase {
|
|
|
416
439
|
context_hash TEXT NOT NULL,
|
|
417
440
|
issued_at INTEGER NOT NULL,
|
|
418
441
|
expires_at INTEGER NOT NULL,
|
|
442
|
+
selected_files TEXT NOT NULL DEFAULT '[]',
|
|
443
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
444
|
+
canonical_next_action TEXT,
|
|
445
|
+
request_id TEXT,
|
|
419
446
|
PRIMARY KEY (project_root, session_id, execution_id)
|
|
420
447
|
);
|
|
421
448
|
|
|
422
449
|
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
423
450
|
ON devflow_context_receipts(expires_at);
|
|
424
451
|
|
|
452
|
+
CREATE TABLE IF NOT EXISTS devflow_context_selection_events (
|
|
453
|
+
id TEXT PRIMARY KEY,
|
|
454
|
+
project_root TEXT NOT NULL,
|
|
455
|
+
session_id TEXT NOT NULL,
|
|
456
|
+
execution_id TEXT NOT NULL,
|
|
457
|
+
request_id TEXT,
|
|
458
|
+
selection_type TEXT NOT NULL CHECK(selection_type IN ('code', 'action')),
|
|
459
|
+
candidate_id TEXT NOT NULL,
|
|
460
|
+
tool_name TEXT NOT NULL,
|
|
461
|
+
tool_use_id TEXT,
|
|
462
|
+
selected_at INTEGER NOT NULL
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
CREATE INDEX IF NOT EXISTS idx_context_selection_identity
|
|
466
|
+
ON devflow_context_selection_events(project_root, session_id, execution_id, request_id);
|
|
467
|
+
|
|
425
468
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
426
469
|
id TEXT PRIMARY KEY,
|
|
427
470
|
project_root TEXT NOT NULL,
|
|
@@ -458,6 +501,32 @@ export class DevFlowDatabase {
|
|
|
458
501
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
|
|
459
502
|
ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
460
503
|
|
|
504
|
+
CREATE TABLE IF NOT EXISTS devflow_session_obligations (
|
|
505
|
+
obligation_id TEXT NOT NULL,
|
|
506
|
+
project_root TEXT NOT NULL,
|
|
507
|
+
session_id TEXT NOT NULL,
|
|
508
|
+
execution_id TEXT,
|
|
509
|
+
turn_id TEXT,
|
|
510
|
+
kind TEXT NOT NULL
|
|
511
|
+
CHECK(kind IN ('memory_decision', 'evidence_contract', 'session_finalize')),
|
|
512
|
+
state TEXT NOT NULL DEFAULT 'open'
|
|
513
|
+
CHECK(state IN ('open', 'satisfied', 'degraded', 'cancelled')),
|
|
514
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
515
|
+
receipt_id TEXT,
|
|
516
|
+
reason TEXT,
|
|
517
|
+
created_at INTEGER NOT NULL,
|
|
518
|
+
updated_at INTEGER NOT NULL,
|
|
519
|
+
resolved_at INTEGER,
|
|
520
|
+
PRIMARY KEY (project_root, session_id, obligation_id)
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
CREATE INDEX IF NOT EXISTS idx_session_obligations_state
|
|
524
|
+
ON devflow_session_obligations(project_root, session_id, state, created_at);
|
|
525
|
+
CREATE INDEX IF NOT EXISTS idx_session_obligations_turn
|
|
526
|
+
ON devflow_session_obligations(project_root, session_id, turn_id);
|
|
527
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_session_obligations_receipt
|
|
528
|
+
ON devflow_session_obligations(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
529
|
+
|
|
461
530
|
CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
|
|
462
531
|
id TEXT PRIMARY KEY,
|
|
463
532
|
project_root TEXT NOT NULL,
|
|
@@ -545,6 +614,10 @@ export class DevFlowDatabase {
|
|
|
545
614
|
} catch {}
|
|
546
615
|
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1'); } catch {}
|
|
547
616
|
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER'); } catch {}
|
|
617
|
+
try { this.db.exec("ALTER TABLE devflow_context_receipts ADD COLUMN selected_files TEXT NOT NULL DEFAULT '[]'"); } catch {}
|
|
618
|
+
try { this.db.exec("ALTER TABLE devflow_context_receipts ADD COLUMN memory_ids TEXT NOT NULL DEFAULT '[]'"); } catch {}
|
|
619
|
+
try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN canonical_next_action TEXT'); } catch {}
|
|
620
|
+
try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN request_id TEXT'); } catch {}
|
|
548
621
|
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
549
622
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
550
623
|
|
|
@@ -1311,7 +1384,12 @@ export class DevFlowDatabase {
|
|
|
1311
1384
|
}));
|
|
1312
1385
|
}
|
|
1313
1386
|
|
|
1314
|
-
updateToolCallEvent(eventId: string, updates: {
|
|
1387
|
+
updateToolCallEvent(eventId: string, updates: {
|
|
1388
|
+
output?: string;
|
|
1389
|
+
error?: string;
|
|
1390
|
+
failureCategory?: string;
|
|
1391
|
+
duration?: number;
|
|
1392
|
+
}): boolean {
|
|
1315
1393
|
const sets: string[] = [];
|
|
1316
1394
|
const values: any[] = [];
|
|
1317
1395
|
|
|
@@ -1323,6 +1401,10 @@ export class DevFlowDatabase {
|
|
|
1323
1401
|
sets.push('error = ?');
|
|
1324
1402
|
values.push(updates.error);
|
|
1325
1403
|
}
|
|
1404
|
+
if (updates.failureCategory !== undefined) {
|
|
1405
|
+
sets.push('failure_category = ?');
|
|
1406
|
+
values.push(updates.failureCategory);
|
|
1407
|
+
}
|
|
1326
1408
|
if (updates.duration !== undefined) {
|
|
1327
1409
|
sets.push('duration = ?');
|
|
1328
1410
|
values.push(updates.duration);
|
|
@@ -2164,14 +2246,11 @@ export class DevFlowDatabase {
|
|
|
2164
2246
|
excludingWorkItemId?: string,
|
|
2165
2247
|
closedAt = Date.now(),
|
|
2166
2248
|
): SessionClosureRecord {
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
AND (? IS NULL OR id <> ?)
|
|
2173
|
-
`).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null) as { count?: number };
|
|
2174
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2249
|
+
const pendingWorkCount = this.countSessionLifecyclePending(
|
|
2250
|
+
projectRoot,
|
|
2251
|
+
sessionId,
|
|
2252
|
+
excludingWorkItemId,
|
|
2253
|
+
);
|
|
2175
2254
|
const state = pendingWorkCount > 0 ? 'closed_with_pending_work' : 'closed';
|
|
2176
2255
|
this.db.prepare(`
|
|
2177
2256
|
UPDATE devflow_session_closures
|
|
@@ -2408,13 +2487,7 @@ export class DevFlowDatabase {
|
|
|
2408
2487
|
}
|
|
2409
2488
|
|
|
2410
2489
|
private refreshClosedSessionClosure(projectRoot: string, sessionId: string, now: number): void {
|
|
2411
|
-
const
|
|
2412
|
-
SELECT COUNT(*) AS count
|
|
2413
|
-
FROM devflow_work_items
|
|
2414
|
-
WHERE project_root = ? AND session_id = ?
|
|
2415
|
-
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
2416
|
-
`).get(projectRoot, sessionId) as { count?: number };
|
|
2417
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2490
|
+
const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId);
|
|
2418
2491
|
this.db.prepare(`
|
|
2419
2492
|
UPDATE devflow_session_closures
|
|
2420
2493
|
SET state = CASE WHEN ? = 0 THEN 'closed' ELSE 'closed_with_pending_work' END,
|
|
@@ -2424,6 +2497,44 @@ export class DevFlowDatabase {
|
|
|
2424
2497
|
`).run(pendingWorkCount, pendingWorkCount, now, projectRoot, sessionId);
|
|
2425
2498
|
}
|
|
2426
2499
|
|
|
2500
|
+
private countSessionLifecyclePending(
|
|
2501
|
+
projectRoot: string,
|
|
2502
|
+
sessionId: string,
|
|
2503
|
+
excludingWorkItemId?: string,
|
|
2504
|
+
): number {
|
|
2505
|
+
const work = this.db.prepare(`
|
|
2506
|
+
SELECT COUNT(*) AS count
|
|
2507
|
+
FROM devflow_work_items
|
|
2508
|
+
WHERE project_root = ? AND session_id = ?
|
|
2509
|
+
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
2510
|
+
AND (? IS NULL OR id <> ?)
|
|
2511
|
+
`).get(
|
|
2512
|
+
projectRoot,
|
|
2513
|
+
sessionId,
|
|
2514
|
+
excludingWorkItemId ?? null,
|
|
2515
|
+
excludingWorkItemId ?? null,
|
|
2516
|
+
) as { count?: number };
|
|
2517
|
+
const obligations = this.db.prepare(`
|
|
2518
|
+
SELECT COUNT(*) AS count
|
|
2519
|
+
FROM devflow_session_obligations
|
|
2520
|
+
WHERE project_root = ? AND session_id = ?
|
|
2521
|
+
AND state IN ('open', 'degraded')
|
|
2522
|
+
`).get(projectRoot, sessionId) as { count?: number };
|
|
2523
|
+
const legacyTurns = this.db.prepare(`
|
|
2524
|
+
SELECT COUNT(*) AS count
|
|
2525
|
+
FROM devflow_memory_turns t
|
|
2526
|
+
WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
|
|
2527
|
+
AND NOT EXISTS (
|
|
2528
|
+
SELECT 1 FROM devflow_session_obligations o
|
|
2529
|
+
WHERE o.project_root = t.project_root AND o.session_id = t.session_id
|
|
2530
|
+
AND o.obligation_id = 'memory:' || t.turn_id
|
|
2531
|
+
)
|
|
2532
|
+
`).get(projectRoot, sessionId) as { count?: number };
|
|
2533
|
+
return Number(work?.count ?? 0)
|
|
2534
|
+
+ Number(obligations?.count ?? 0)
|
|
2535
|
+
+ Number(legacyTurns?.count ?? 0);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2427
2538
|
// ---- Hook Lifecycle ----
|
|
2428
2539
|
|
|
2429
2540
|
getHookReceipt(projectRoot: string): HookReceiptRecord | null {
|
|
@@ -2484,12 +2595,17 @@ export class DevFlowDatabase {
|
|
|
2484
2595
|
upsertContextReceipt(receipt: ContextReceiptRecord): void {
|
|
2485
2596
|
this.db.prepare(`
|
|
2486
2597
|
INSERT INTO devflow_context_receipts
|
|
2487
|
-
(project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
2488
|
-
|
|
2598
|
+
(project_root, session_id, execution_id, context_hash, issued_at, expires_at,
|
|
2599
|
+
selected_files, memory_ids, canonical_next_action, request_id)
|
|
2600
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2489
2601
|
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
2490
2602
|
context_hash = excluded.context_hash,
|
|
2491
2603
|
issued_at = excluded.issued_at,
|
|
2492
|
-
expires_at = excluded.expires_at
|
|
2604
|
+
expires_at = excluded.expires_at,
|
|
2605
|
+
selected_files = excluded.selected_files,
|
|
2606
|
+
memory_ids = excluded.memory_ids,
|
|
2607
|
+
canonical_next_action = excluded.canonical_next_action,
|
|
2608
|
+
request_id = excluded.request_id
|
|
2493
2609
|
`).run(
|
|
2494
2610
|
receipt.projectRoot,
|
|
2495
2611
|
receipt.sessionId,
|
|
@@ -2497,6 +2613,10 @@ export class DevFlowDatabase {
|
|
|
2497
2613
|
receipt.contextHash,
|
|
2498
2614
|
receipt.issuedAt,
|
|
2499
2615
|
receipt.expiresAt,
|
|
2616
|
+
JSON.stringify(receipt.selectedFiles ?? []),
|
|
2617
|
+
JSON.stringify(receipt.memoryIds ?? []),
|
|
2618
|
+
receipt.canonicalNextAction ?? null,
|
|
2619
|
+
receipt.requestId ?? null,
|
|
2500
2620
|
);
|
|
2501
2621
|
}
|
|
2502
2622
|
|
|
@@ -2506,7 +2626,8 @@ export class DevFlowDatabase {
|
|
|
2506
2626
|
executionId: string,
|
|
2507
2627
|
): ContextReceiptRecord | null {
|
|
2508
2628
|
const row = this.db.prepare(`
|
|
2509
|
-
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
2629
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
|
|
2630
|
+
selected_files, memory_ids, canonical_next_action, request_id
|
|
2510
2631
|
FROM devflow_context_receipts
|
|
2511
2632
|
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
2512
2633
|
`).get(projectRoot, sessionId, executionId) as any;
|
|
@@ -2517,9 +2638,101 @@ export class DevFlowDatabase {
|
|
|
2517
2638
|
contextHash: row.context_hash,
|
|
2518
2639
|
issuedAt: row.issued_at,
|
|
2519
2640
|
expiresAt: row.expires_at,
|
|
2641
|
+
selectedFiles: parseJsonStringArray(row.selected_files),
|
|
2642
|
+
memoryIds: parseJsonStringArray(row.memory_ids),
|
|
2643
|
+
canonicalNextAction: row.canonical_next_action ?? undefined,
|
|
2644
|
+
requestId: row.request_id ?? undefined,
|
|
2520
2645
|
} : null;
|
|
2521
2646
|
}
|
|
2522
2647
|
|
|
2648
|
+
getActiveContextReceipt(
|
|
2649
|
+
projectRoot: string,
|
|
2650
|
+
sessionId: string,
|
|
2651
|
+
executionId?: string,
|
|
2652
|
+
now = Date.now(),
|
|
2653
|
+
): ContextReceiptRecord | null {
|
|
2654
|
+
const executionClause = executionId ? 'AND execution_id = ?' : '';
|
|
2655
|
+
const params = executionId
|
|
2656
|
+
? [projectRoot, sessionId, executionId, now]
|
|
2657
|
+
: [projectRoot, sessionId, now];
|
|
2658
|
+
const row = this.db.prepare(`
|
|
2659
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
|
|
2660
|
+
selected_files, memory_ids, canonical_next_action, request_id
|
|
2661
|
+
FROM devflow_context_receipts
|
|
2662
|
+
WHERE project_root = ? AND session_id = ? ${executionClause} AND expires_at > ?
|
|
2663
|
+
ORDER BY issued_at DESC
|
|
2664
|
+
LIMIT 1
|
|
2665
|
+
`).get(...params) as any;
|
|
2666
|
+
return row ? {
|
|
2667
|
+
projectRoot: row.project_root,
|
|
2668
|
+
sessionId: row.session_id,
|
|
2669
|
+
executionId: row.execution_id,
|
|
2670
|
+
contextHash: row.context_hash,
|
|
2671
|
+
issuedAt: row.issued_at,
|
|
2672
|
+
expiresAt: row.expires_at,
|
|
2673
|
+
selectedFiles: parseJsonStringArray(row.selected_files),
|
|
2674
|
+
memoryIds: parseJsonStringArray(row.memory_ids),
|
|
2675
|
+
canonicalNextAction: row.canonical_next_action ?? undefined,
|
|
2676
|
+
requestId: row.request_id ?? undefined,
|
|
2677
|
+
} : null;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean {
|
|
2681
|
+
return this.db.prepare(`
|
|
2682
|
+
INSERT OR IGNORE INTO devflow_context_selection_events
|
|
2683
|
+
(id, project_root, session_id, execution_id, request_id, selection_type,
|
|
2684
|
+
candidate_id, tool_name, tool_use_id, selected_at)
|
|
2685
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2686
|
+
`).run(
|
|
2687
|
+
event.id,
|
|
2688
|
+
event.projectRoot,
|
|
2689
|
+
event.sessionId,
|
|
2690
|
+
event.executionId,
|
|
2691
|
+
event.requestId ?? null,
|
|
2692
|
+
event.selectionType,
|
|
2693
|
+
event.candidateId,
|
|
2694
|
+
event.toolName,
|
|
2695
|
+
event.toolUseId ?? null,
|
|
2696
|
+
event.selectedAt,
|
|
2697
|
+
).changes > 0;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
listContextSelectionEvents(options: {
|
|
2701
|
+
projectRoot: string;
|
|
2702
|
+
sessionId?: string;
|
|
2703
|
+
executionId?: string;
|
|
2704
|
+
requestId?: string;
|
|
2705
|
+
}): ContextSelectionEventRecord[] {
|
|
2706
|
+
const predicates = ['project_root = ?'];
|
|
2707
|
+
const params: unknown[] = [options.projectRoot];
|
|
2708
|
+
for (const [column, value] of [
|
|
2709
|
+
['session_id', options.sessionId],
|
|
2710
|
+
['execution_id', options.executionId],
|
|
2711
|
+
['request_id', options.requestId],
|
|
2712
|
+
] as const) {
|
|
2713
|
+
if (!value) continue;
|
|
2714
|
+
predicates.push(`${column} = ?`);
|
|
2715
|
+
params.push(value);
|
|
2716
|
+
}
|
|
2717
|
+
const rows = this.db.prepare(`
|
|
2718
|
+
SELECT * FROM devflow_context_selection_events
|
|
2719
|
+
WHERE ${predicates.join(' AND ')}
|
|
2720
|
+
ORDER BY selected_at ASC, id ASC
|
|
2721
|
+
`).all(...params) as any[];
|
|
2722
|
+
return rows.map(row => ({
|
|
2723
|
+
id: row.id,
|
|
2724
|
+
projectRoot: row.project_root,
|
|
2725
|
+
sessionId: row.session_id,
|
|
2726
|
+
executionId: row.execution_id,
|
|
2727
|
+
requestId: row.request_id ?? undefined,
|
|
2728
|
+
selectionType: row.selection_type,
|
|
2729
|
+
candidateId: row.candidate_id,
|
|
2730
|
+
toolName: row.tool_name,
|
|
2731
|
+
toolUseId: row.tool_use_id ?? undefined,
|
|
2732
|
+
selectedAt: row.selected_at,
|
|
2733
|
+
}));
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2523
2736
|
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number {
|
|
2524
2737
|
return executionId
|
|
2525
2738
|
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
@@ -2536,25 +2749,158 @@ export class DevFlowDatabase {
|
|
|
2536
2749
|
}
|
|
2537
2750
|
|
|
2538
2751
|
beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord {
|
|
2752
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
2539
2753
|
this.db.prepare(`
|
|
2540
2754
|
INSERT OR IGNORE INTO devflow_memory_turns
|
|
2541
2755
|
(turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
|
|
2542
2756
|
VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
|
|
2543
2757
|
`).run(
|
|
2544
|
-
|
|
2758
|
+
turnId,
|
|
2545
2759
|
input.projectRoot,
|
|
2546
2760
|
input.sessionId,
|
|
2547
2761
|
input.promptHash,
|
|
2548
2762
|
input.eventId,
|
|
2549
2763
|
input.createdAt,
|
|
2550
2764
|
);
|
|
2551
|
-
|
|
2765
|
+
const turn = this.getMemoryTurn(turnId)!;
|
|
2766
|
+
this.upsertSessionObligation({
|
|
2767
|
+
obligationId: `memory:${turnId}`,
|
|
2768
|
+
projectRoot: input.projectRoot,
|
|
2769
|
+
sessionId: input.sessionId,
|
|
2770
|
+
turnId,
|
|
2771
|
+
kind: 'memory_decision',
|
|
2772
|
+
state: turn.status === 'pending' ? 'open' : 'satisfied',
|
|
2773
|
+
payload: { eventId: input.eventId, promptHash: input.promptHash },
|
|
2774
|
+
receiptId: turn.receiptId,
|
|
2775
|
+
createdAt: input.createdAt,
|
|
2776
|
+
updatedAt: Date.now(),
|
|
2777
|
+
resolvedAt: turn.decidedAt,
|
|
2778
|
+
});
|
|
2779
|
+
return turn;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
upsertSessionObligation(record: SessionObligationRecord): SessionObligationRecord {
|
|
2783
|
+
if (!record.obligationId.trim()) throw new Error('Session obligation requires an ID');
|
|
2784
|
+
if (!record.projectRoot.trim()) throw new Error('Session obligation requires a project root');
|
|
2785
|
+
if (!record.sessionId.trim()) throw new Error('Session obligation requires a session ID');
|
|
2786
|
+
const now = record.updatedAt || Date.now();
|
|
2787
|
+
this.db.prepare(`
|
|
2788
|
+
INSERT INTO devflow_session_obligations (
|
|
2789
|
+
obligation_id, project_root, session_id, execution_id, turn_id, kind,
|
|
2790
|
+
state, payload, receipt_id, reason, created_at, updated_at, resolved_at
|
|
2791
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2792
|
+
ON CONFLICT(project_root, session_id, obligation_id) DO UPDATE SET
|
|
2793
|
+
execution_id = COALESCE(excluded.execution_id, devflow_session_obligations.execution_id),
|
|
2794
|
+
turn_id = COALESCE(excluded.turn_id, devflow_session_obligations.turn_id),
|
|
2795
|
+
payload = excluded.payload,
|
|
2796
|
+
updated_at = excluded.updated_at
|
|
2797
|
+
WHERE devflow_session_obligations.state = 'open'
|
|
2798
|
+
`).run(
|
|
2799
|
+
record.obligationId,
|
|
2800
|
+
record.projectRoot,
|
|
2801
|
+
record.sessionId,
|
|
2802
|
+
record.executionId ?? null,
|
|
2803
|
+
record.turnId ?? null,
|
|
2804
|
+
record.kind,
|
|
2805
|
+
record.state,
|
|
2806
|
+
JSON.stringify(record.payload ?? {}),
|
|
2807
|
+
record.receiptId ?? null,
|
|
2808
|
+
record.reason ?? null,
|
|
2809
|
+
record.createdAt,
|
|
2810
|
+
now,
|
|
2811
|
+
record.resolvedAt ?? null,
|
|
2812
|
+
);
|
|
2813
|
+
return this.getSessionObligation(record.projectRoot, record.sessionId, record.obligationId)!;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
getSessionObligation(
|
|
2817
|
+
projectRoot: string,
|
|
2818
|
+
sessionId: string,
|
|
2819
|
+
obligationId: string,
|
|
2820
|
+
): SessionObligationRecord | null {
|
|
2821
|
+
const row = this.db.prepare(`
|
|
2822
|
+
SELECT * FROM devflow_session_obligations
|
|
2823
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ?
|
|
2824
|
+
`).get(projectRoot, sessionId, obligationId) as Record<string, unknown> | undefined;
|
|
2825
|
+
return row ? mapSessionObligationRow(row) : null;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
listSessionObligations(
|
|
2829
|
+
projectRoot: string,
|
|
2830
|
+
sessionId: string,
|
|
2831
|
+
states?: SessionObligationState[],
|
|
2832
|
+
): SessionObligationRecord[] {
|
|
2833
|
+
const allowed = [...new Set(states ?? [])].filter(state =>
|
|
2834
|
+
state === 'open' || state === 'satisfied' || state === 'degraded' || state === 'cancelled');
|
|
2835
|
+
const rows = (allowed.length > 0
|
|
2836
|
+
? this.db.prepare(`
|
|
2837
|
+
SELECT * FROM devflow_session_obligations
|
|
2838
|
+
WHERE project_root = ? AND session_id = ?
|
|
2839
|
+
AND state IN (${allowed.map(() => '?').join(',')})
|
|
2840
|
+
ORDER BY created_at ASC, obligation_id ASC
|
|
2841
|
+
`).all(projectRoot, sessionId, ...allowed)
|
|
2842
|
+
: this.db.prepare(`
|
|
2843
|
+
SELECT * FROM devflow_session_obligations
|
|
2844
|
+
WHERE project_root = ? AND session_id = ?
|
|
2845
|
+
ORDER BY created_at ASC, obligation_id ASC
|
|
2846
|
+
`).all(projectRoot, sessionId)) as Array<Record<string, unknown>>;
|
|
2847
|
+
return rows.map(mapSessionObligationRow);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
resolveSessionObligation(input: {
|
|
2851
|
+
projectRoot: string;
|
|
2852
|
+
sessionId: string;
|
|
2853
|
+
obligationId: string;
|
|
2854
|
+
state: 'satisfied' | 'degraded' | 'cancelled';
|
|
2855
|
+
receiptId?: string;
|
|
2856
|
+
reason?: string;
|
|
2857
|
+
resolvedAt?: number;
|
|
2858
|
+
}): SessionObligationRecord {
|
|
2859
|
+
return this.db.transaction(() => {
|
|
2860
|
+
const existing = this.getSessionObligation(
|
|
2861
|
+
input.projectRoot,
|
|
2862
|
+
input.sessionId,
|
|
2863
|
+
input.obligationId,
|
|
2864
|
+
);
|
|
2865
|
+
if (!existing) throw new Error(`Session obligation ${input.obligationId} does not exist`);
|
|
2866
|
+
if (existing.state !== 'open') {
|
|
2867
|
+
const sameResolution = existing.state === input.state
|
|
2868
|
+
&& (input.receiptId === undefined || existing.receiptId === input.receiptId);
|
|
2869
|
+
if (sameResolution) return existing;
|
|
2870
|
+
throw new Error(`OBLIGATION_TERMINAL_CONFLICT:${input.obligationId}`);
|
|
2871
|
+
}
|
|
2872
|
+
const resolvedAt = input.resolvedAt ?? Date.now();
|
|
2873
|
+
this.db.prepare(`
|
|
2874
|
+
UPDATE devflow_session_obligations
|
|
2875
|
+
SET state = ?, receipt_id = COALESCE(?, receipt_id), reason = ?,
|
|
2876
|
+
resolved_at = ?, updated_at = ?
|
|
2877
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'open'
|
|
2878
|
+
`).run(
|
|
2879
|
+
input.state,
|
|
2880
|
+
input.receiptId ?? null,
|
|
2881
|
+
input.reason ?? null,
|
|
2882
|
+
resolvedAt,
|
|
2883
|
+
resolvedAt,
|
|
2884
|
+
input.projectRoot,
|
|
2885
|
+
input.sessionId,
|
|
2886
|
+
input.obligationId,
|
|
2887
|
+
);
|
|
2888
|
+
return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId)!;
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
countOpenSessionObligations(projectRoot: string, sessionId: string): number {
|
|
2893
|
+
const row = this.db.prepare(`
|
|
2894
|
+
SELECT COUNT(*) AS count FROM devflow_session_obligations
|
|
2895
|
+
WHERE project_root = ? AND session_id = ? AND state = 'open'
|
|
2896
|
+
`).get(projectRoot, sessionId) as { count?: number };
|
|
2897
|
+
return Number(row?.count ?? 0);
|
|
2552
2898
|
}
|
|
2553
2899
|
|
|
2554
2900
|
getMemoryTurn(turnId: string): MemoryTurnRecord | null {
|
|
2555
2901
|
const row = this.db.prepare(
|
|
2556
2902
|
'SELECT * FROM devflow_memory_turns WHERE turn_id = ?',
|
|
2557
|
-
).get(turnId) as any;
|
|
2903
|
+
).get(normalizeTurnId(turnId)) as any;
|
|
2558
2904
|
return row ? this.mapMemoryTurn(row) : null;
|
|
2559
2905
|
}
|
|
2560
2906
|
|
|
@@ -2575,6 +2921,7 @@ export class DevFlowDatabase {
|
|
|
2575
2921
|
reason?: string;
|
|
2576
2922
|
decidedAt?: number;
|
|
2577
2923
|
}): MemoryTurnRecord {
|
|
2924
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
2578
2925
|
this.db.prepare(`
|
|
2579
2926
|
UPDATE devflow_memory_turns
|
|
2580
2927
|
SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
|
|
@@ -2585,12 +2932,22 @@ export class DevFlowDatabase {
|
|
|
2585
2932
|
input.source,
|
|
2586
2933
|
input.reason ?? null,
|
|
2587
2934
|
input.decidedAt ?? Date.now(),
|
|
2588
|
-
|
|
2935
|
+
turnId,
|
|
2589
2936
|
);
|
|
2590
|
-
const turn = this.getMemoryTurn(
|
|
2937
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2591
2938
|
if (!turn || turn.status !== 'committed') {
|
|
2592
|
-
throw new Error(`Memory turn ${
|
|
2939
|
+
throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
|
|
2593
2940
|
}
|
|
2941
|
+
this.ensureMemoryObligation(turn);
|
|
2942
|
+
this.resolveSessionObligation({
|
|
2943
|
+
projectRoot: turn.projectRoot,
|
|
2944
|
+
sessionId: turn.sessionId,
|
|
2945
|
+
obligationId: `memory:${turn.turnId}`,
|
|
2946
|
+
state: 'satisfied',
|
|
2947
|
+
receiptId: turn.receiptId,
|
|
2948
|
+
reason: turn.reason,
|
|
2949
|
+
resolvedAt: turn.decidedAt,
|
|
2950
|
+
});
|
|
2594
2951
|
return turn;
|
|
2595
2952
|
}
|
|
2596
2953
|
|
|
@@ -2600,6 +2957,7 @@ export class DevFlowDatabase {
|
|
|
2600
2957
|
reason: string;
|
|
2601
2958
|
decidedAt?: number;
|
|
2602
2959
|
}): MemoryTurnRecord {
|
|
2960
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
2603
2961
|
this.db.prepare(`
|
|
2604
2962
|
UPDATE devflow_memory_turns
|
|
2605
2963
|
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
|
|
@@ -2609,12 +2967,22 @@ export class DevFlowDatabase {
|
|
|
2609
2967
|
input.receiptId,
|
|
2610
2968
|
input.reason,
|
|
2611
2969
|
input.decidedAt ?? Date.now(),
|
|
2612
|
-
|
|
2970
|
+
turnId,
|
|
2613
2971
|
);
|
|
2614
|
-
const turn = this.getMemoryTurn(
|
|
2972
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2615
2973
|
if (!turn || turn.status !== 'skipped') {
|
|
2616
|
-
throw new Error(`Memory turn ${
|
|
2974
|
+
throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
|
|
2617
2975
|
}
|
|
2976
|
+
this.ensureMemoryObligation(turn);
|
|
2977
|
+
this.resolveSessionObligation({
|
|
2978
|
+
projectRoot: turn.projectRoot,
|
|
2979
|
+
sessionId: turn.sessionId,
|
|
2980
|
+
obligationId: `memory:${turn.turnId}`,
|
|
2981
|
+
state: 'satisfied',
|
|
2982
|
+
receiptId: turn.receiptId,
|
|
2983
|
+
reason: turn.reason,
|
|
2984
|
+
resolvedAt: turn.decidedAt,
|
|
2985
|
+
});
|
|
2618
2986
|
return turn;
|
|
2619
2987
|
}
|
|
2620
2988
|
|
|
@@ -2622,7 +2990,23 @@ export class DevFlowDatabase {
|
|
|
2622
2990
|
return this.db.prepare(`
|
|
2623
2991
|
UPDATE devflow_memory_turns SET stop_prompted_at = ?
|
|
2624
2992
|
WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
|
|
2625
|
-
`).run(promptedAt, turnId).changes === 1;
|
|
2993
|
+
`).run(promptedAt, normalizeTurnId(turnId)).changes === 1;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
private ensureMemoryObligation(turn: MemoryTurnRecord): void {
|
|
2997
|
+
const obligationId = `memory:${turn.turnId}`;
|
|
2998
|
+
if (this.getSessionObligation(turn.projectRoot, turn.sessionId, obligationId)) return;
|
|
2999
|
+
this.upsertSessionObligation({
|
|
3000
|
+
obligationId,
|
|
3001
|
+
projectRoot: turn.projectRoot,
|
|
3002
|
+
sessionId: turn.sessionId,
|
|
3003
|
+
turnId: turn.turnId,
|
|
3004
|
+
kind: 'memory_decision',
|
|
3005
|
+
state: 'open',
|
|
3006
|
+
payload: { eventId: turn.eventId, promptHash: turn.promptHash },
|
|
3007
|
+
createdAt: turn.createdAt,
|
|
3008
|
+
updatedAt: Date.now(),
|
|
3009
|
+
});
|
|
2626
3010
|
}
|
|
2627
3011
|
|
|
2628
3012
|
listMemoryTurns(projectRoot: string, sessionId?: string, limit = 50): MemoryTurnRecord[] {
|
|
@@ -2785,6 +3169,13 @@ function parseJson(value: unknown): unknown {
|
|
|
2785
3169
|
try { return JSON.parse(value); } catch { return value; }
|
|
2786
3170
|
}
|
|
2787
3171
|
|
|
3172
|
+
function parseJsonStringArray(value: unknown): string[] {
|
|
3173
|
+
const parsed = parseJson(value);
|
|
3174
|
+
return Array.isArray(parsed)
|
|
3175
|
+
? parsed.filter((item): item is string => typeof item === 'string')
|
|
3176
|
+
: [];
|
|
3177
|
+
}
|
|
3178
|
+
|
|
2788
3179
|
function deriveQuery(input: unknown): string | null {
|
|
2789
3180
|
if (!input || typeof input !== 'object') return null;
|
|
2790
3181
|
const record = input as Record<string, unknown>;
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type {
|
|
|
11
11
|
GovernanceRuleRecord,
|
|
12
12
|
HookFallbackRecord,
|
|
13
13
|
HookReceiptRecord,
|
|
14
|
+
ContextReceiptRecord,
|
|
15
|
+
ContextSelectionEventRecord,
|
|
14
16
|
MemoryDistillCheckpointRecord,
|
|
15
17
|
MemoryTurnRecord,
|
|
16
18
|
MemoryTurnStatus,
|
|
@@ -28,3 +30,12 @@ export type {
|
|
|
28
30
|
WorkQueueHealth,
|
|
29
31
|
WorkState,
|
|
30
32
|
} from './work-queue';
|
|
33
|
+
export {
|
|
34
|
+
mapSessionObligationRow,
|
|
35
|
+
normalizeTurnId,
|
|
36
|
+
} from './obligation-ledger';
|
|
37
|
+
export type {
|
|
38
|
+
SessionObligationKind,
|
|
39
|
+
SessionObligationRecord,
|
|
40
|
+
SessionObligationState,
|
|
41
|
+
} from './obligation-ledger';
|