@devflow-tools/database 0.16.11 → 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 +27 -0
- package/__tests__/database.memory-turn-receipts.test.ts +27 -0
- package/__tests__/database.skill-executions.test.ts +62 -1
- package/dist/database.d.ts +46 -0
- package/dist/database.js +438 -48
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/node-sqlite.js +1 -6
- 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 +3 -3
- package/src/database.ts +552 -45
- package/src/index.ts +11 -0
- package/src/node-sqlite.ts +1 -6
- package/src/obligation-ledger.ts +57 -0
- package/src/work-queue.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
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 {
|
|
@@ -141,6 +164,7 @@ export interface MemoryDistillCheckpointRecord {
|
|
|
141
164
|
trigger: 'pre_compact' | 'session_end';
|
|
142
165
|
pendingEvents: number;
|
|
143
166
|
releasedLeases: number;
|
|
167
|
+
details?: Record<string, unknown>;
|
|
144
168
|
createdAt: number;
|
|
145
169
|
}
|
|
146
170
|
|
|
@@ -415,12 +439,32 @@ export class DevFlowDatabase {
|
|
|
415
439
|
context_hash TEXT NOT NULL,
|
|
416
440
|
issued_at INTEGER NOT NULL,
|
|
417
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,
|
|
418
446
|
PRIMARY KEY (project_root, session_id, execution_id)
|
|
419
447
|
);
|
|
420
448
|
|
|
421
449
|
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
422
450
|
ON devflow_context_receipts(expires_at);
|
|
423
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
|
+
|
|
424
468
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
425
469
|
id TEXT PRIMARY KEY,
|
|
426
470
|
project_root TEXT NOT NULL,
|
|
@@ -428,6 +472,7 @@ export class DevFlowDatabase {
|
|
|
428
472
|
trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
|
|
429
473
|
pending_events INTEGER NOT NULL DEFAULT 0,
|
|
430
474
|
released_leases INTEGER NOT NULL DEFAULT 0,
|
|
475
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
431
476
|
created_at INTEGER NOT NULL
|
|
432
477
|
);
|
|
433
478
|
|
|
@@ -456,6 +501,32 @@ export class DevFlowDatabase {
|
|
|
456
501
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
|
|
457
502
|
ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
458
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
|
+
|
|
459
530
|
CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
|
|
460
531
|
id TEXT PRIMARY KEY,
|
|
461
532
|
project_root TEXT NOT NULL,
|
|
@@ -532,6 +603,7 @@ export class DevFlowDatabase {
|
|
|
532
603
|
try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT'); } catch {}
|
|
533
604
|
try { this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT'); } catch {}
|
|
534
605
|
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT'); } catch {}
|
|
606
|
+
try { this.db.exec("ALTER TABLE devflow_memory_distill_checkpoints ADD COLUMN details TEXT NOT NULL DEFAULT '{}'"); } catch {}
|
|
535
607
|
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN failed_tool_calls INTEGER DEFAULT 0'); } catch {}
|
|
536
608
|
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN blocked_tool_calls INTEGER DEFAULT 0'); } catch {}
|
|
537
609
|
try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN fallback_count INTEGER DEFAULT 0'); } catch {}
|
|
@@ -542,6 +614,10 @@ export class DevFlowDatabase {
|
|
|
542
614
|
} catch {}
|
|
543
615
|
try { this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1'); } catch {}
|
|
544
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 {}
|
|
545
621
|
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
546
622
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
547
623
|
|
|
@@ -1076,6 +1152,16 @@ export class DevFlowDatabase {
|
|
|
1076
1152
|
};
|
|
1077
1153
|
}
|
|
1078
1154
|
|
|
1155
|
+
getLatestSkillExecutionForSession(sessionId: string): any | null {
|
|
1156
|
+
const row = this.db.prepare(`
|
|
1157
|
+
SELECT execution_id FROM skill_executions
|
|
1158
|
+
WHERE session_id = ?
|
|
1159
|
+
ORDER BY started_at DESC, execution_id DESC
|
|
1160
|
+
LIMIT 1
|
|
1161
|
+
`).get(sessionId) as { execution_id?: string } | undefined;
|
|
1162
|
+
return row?.execution_id ? this.getSkillExecution(row.execution_id) : null;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1079
1165
|
updateSkillExecution(executionId: string, updates: {
|
|
1080
1166
|
status?: string;
|
|
1081
1167
|
finishedAt?: number;
|
|
@@ -1116,6 +1202,20 @@ export class DevFlowDatabase {
|
|
|
1116
1202
|
}
|
|
1117
1203
|
}
|
|
1118
1204
|
|
|
1205
|
+
mergeSkillExecutionMetadata(
|
|
1206
|
+
executionId: string,
|
|
1207
|
+
incoming: Record<string, unknown>,
|
|
1208
|
+
): Record<string, unknown> | null {
|
|
1209
|
+
const execution = this.getSkillExecution(executionId);
|
|
1210
|
+
if (!execution) return null;
|
|
1211
|
+
const existing = execution.metadata && typeof execution.metadata === 'object'
|
|
1212
|
+
? execution.metadata as Record<string, unknown>
|
|
1213
|
+
: {};
|
|
1214
|
+
const merged = mergeExecutionMetadata(existing, incoming);
|
|
1215
|
+
this.updateSkillExecution(executionId, { metadata: merged });
|
|
1216
|
+
return merged;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1119
1219
|
listSkillExecutions(limit: number, skillName?: string): any[] {
|
|
1120
1220
|
let query = 'SELECT * FROM skill_executions';
|
|
1121
1221
|
const params: any[] = [];
|
|
@@ -1284,7 +1384,12 @@ export class DevFlowDatabase {
|
|
|
1284
1384
|
}));
|
|
1285
1385
|
}
|
|
1286
1386
|
|
|
1287
|
-
updateToolCallEvent(eventId: string, updates: {
|
|
1387
|
+
updateToolCallEvent(eventId: string, updates: {
|
|
1388
|
+
output?: string;
|
|
1389
|
+
error?: string;
|
|
1390
|
+
failureCategory?: string;
|
|
1391
|
+
duration?: number;
|
|
1392
|
+
}): boolean {
|
|
1288
1393
|
const sets: string[] = [];
|
|
1289
1394
|
const values: any[] = [];
|
|
1290
1395
|
|
|
@@ -1296,6 +1401,10 @@ export class DevFlowDatabase {
|
|
|
1296
1401
|
sets.push('error = ?');
|
|
1297
1402
|
values.push(updates.error);
|
|
1298
1403
|
}
|
|
1404
|
+
if (updates.failureCategory !== undefined) {
|
|
1405
|
+
sets.push('failure_category = ?');
|
|
1406
|
+
values.push(updates.failureCategory);
|
|
1407
|
+
}
|
|
1299
1408
|
if (updates.duration !== undefined) {
|
|
1300
1409
|
sets.push('duration = ?');
|
|
1301
1410
|
values.push(updates.duration);
|
|
@@ -1343,23 +1452,38 @@ export class DevFlowDatabase {
|
|
|
1343
1452
|
const blocked = events.filter(event => event.blocked);
|
|
1344
1453
|
const directFallbacks = events.filter(event => event.mcpFallback);
|
|
1345
1454
|
const hookFallbacks = execution?.sessionId
|
|
1346
|
-
? this.
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1455
|
+
? (this.db.prepare(`
|
|
1456
|
+
SELECT * FROM devflow_hook_fallbacks
|
|
1457
|
+
WHERE session_id = ? AND created_at >= ? AND created_at <= ?
|
|
1458
|
+
ORDER BY created_at ASC
|
|
1459
|
+
`).all(execution.sessionId, execution.startedAt, finishedAt) as any[]).map(row => ({
|
|
1460
|
+
id: row.id,
|
|
1461
|
+
projectRoot: row.project_root,
|
|
1462
|
+
sessionId: row.session_id ?? undefined,
|
|
1463
|
+
toolUseId: row.tool_use_id ?? undefined,
|
|
1464
|
+
requestType: row.request_type,
|
|
1465
|
+
tool: row.tool,
|
|
1466
|
+
reason: row.reason,
|
|
1467
|
+
durationMs: row.duration_ms,
|
|
1468
|
+
attempts: row.attempts,
|
|
1469
|
+
createdAt: row.created_at,
|
|
1470
|
+
} satisfies HookFallbackRecord))
|
|
1351
1471
|
: [];
|
|
1352
1472
|
const fallbackReasons = [...new Set([
|
|
1353
1473
|
...directFallbacks.map(() => 'direct_tool_during_context'),
|
|
1354
1474
|
...hookFallbacks.map(fallback => `daemon_${fallback.reason}`),
|
|
1355
1475
|
])];
|
|
1356
1476
|
const eventReceiptIds = collectCanonicalReceiptIds(events.map(event => event.output));
|
|
1357
|
-
const
|
|
1358
|
-
? metadata
|
|
1359
|
-
:
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
: []
|
|
1477
|
+
const existingMetadata = execution?.metadata && typeof execution.metadata === 'object'
|
|
1478
|
+
? execution.metadata as Record<string, unknown>
|
|
1479
|
+
: {};
|
|
1480
|
+
const mergedMetadata = mergeExecutionMetadata(existingMetadata, metadata ?? {});
|
|
1481
|
+
const suppliedMemoryReceiptIds = [existingMetadata.memoryReceiptIds, metadata?.memoryReceiptIds]
|
|
1482
|
+
.flatMap(value => Array.isArray(value) ? value : [])
|
|
1483
|
+
.filter((id): id is string => typeof id === 'string');
|
|
1484
|
+
const suppliedDistillReceiptIds = [existingMetadata.distillReceiptIds, metadata?.distillReceiptIds]
|
|
1485
|
+
.flatMap(value => Array.isArray(value) ? value : [])
|
|
1486
|
+
.filter((id): id is string => typeof id === 'string');
|
|
1363
1487
|
const memoryReceiptIds = [...new Set([
|
|
1364
1488
|
...suppliedMemoryReceiptIds,
|
|
1365
1489
|
...eventReceiptIds.filter(id => id.startsWith('memory-receipt:')),
|
|
@@ -1385,7 +1509,7 @@ export class DevFlowDatabase {
|
|
|
1385
1509
|
fallbackCount: directFallbacks.length + hookFallbacks.length,
|
|
1386
1510
|
fallbackReasons,
|
|
1387
1511
|
metadata: {
|
|
1388
|
-
...
|
|
1512
|
+
...mergedMetadata,
|
|
1389
1513
|
applicableObligations: obligation.applicable,
|
|
1390
1514
|
satisfiedObligations: obligation.satisfied,
|
|
1391
1515
|
missedTools: obligation.missedTools,
|
|
@@ -2122,14 +2246,11 @@ export class DevFlowDatabase {
|
|
|
2122
2246
|
excludingWorkItemId?: string,
|
|
2123
2247
|
closedAt = Date.now(),
|
|
2124
2248
|
): SessionClosureRecord {
|
|
2125
|
-
const
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
AND (? IS NULL OR id <> ?)
|
|
2131
|
-
`).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null) as { count?: number };
|
|
2132
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2249
|
+
const pendingWorkCount = this.countSessionLifecyclePending(
|
|
2250
|
+
projectRoot,
|
|
2251
|
+
sessionId,
|
|
2252
|
+
excludingWorkItemId,
|
|
2253
|
+
);
|
|
2133
2254
|
const state = pendingWorkCount > 0 ? 'closed_with_pending_work' : 'closed';
|
|
2134
2255
|
this.db.prepare(`
|
|
2135
2256
|
UPDATE devflow_session_closures
|
|
@@ -2256,6 +2377,24 @@ export class DevFlowDatabase {
|
|
|
2256
2377
|
).changes === 1;
|
|
2257
2378
|
}
|
|
2258
2379
|
|
|
2380
|
+
deferWork(id: string, owner: string, error: WorkError, nextAttemptAt: number): boolean {
|
|
2381
|
+
const updatedAt = Date.now();
|
|
2382
|
+
return this.db.prepare(`
|
|
2383
|
+
UPDATE devflow_work_items
|
|
2384
|
+
SET state = 'pending', attempts = MAX(0, attempts - 1),
|
|
2385
|
+
lease_owner = NULL, lease_expires_at = NULL, next_attempt_at = ?,
|
|
2386
|
+
error_category = ?, error_message = ?, updated_at = ?
|
|
2387
|
+
WHERE id = ? AND state = 'leased' AND lease_owner = ?
|
|
2388
|
+
`).run(
|
|
2389
|
+
nextAttemptAt,
|
|
2390
|
+
error.category,
|
|
2391
|
+
error.message,
|
|
2392
|
+
updatedAt,
|
|
2393
|
+
id,
|
|
2394
|
+
owner,
|
|
2395
|
+
).changes === 1;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2259
2398
|
deadLetterWork(id: string, owner: string, error: WorkError): boolean {
|
|
2260
2399
|
return this.db.prepare(`
|
|
2261
2400
|
UPDATE devflow_work_items
|
|
@@ -2348,13 +2487,7 @@ export class DevFlowDatabase {
|
|
|
2348
2487
|
}
|
|
2349
2488
|
|
|
2350
2489
|
private refreshClosedSessionClosure(projectRoot: string, sessionId: string, now: number): void {
|
|
2351
|
-
const
|
|
2352
|
-
SELECT COUNT(*) AS count
|
|
2353
|
-
FROM devflow_work_items
|
|
2354
|
-
WHERE project_root = ? AND session_id = ?
|
|
2355
|
-
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
2356
|
-
`).get(projectRoot, sessionId) as { count?: number };
|
|
2357
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2490
|
+
const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId);
|
|
2358
2491
|
this.db.prepare(`
|
|
2359
2492
|
UPDATE devflow_session_closures
|
|
2360
2493
|
SET state = CASE WHEN ? = 0 THEN 'closed' ELSE 'closed_with_pending_work' END,
|
|
@@ -2364,6 +2497,44 @@ export class DevFlowDatabase {
|
|
|
2364
2497
|
`).run(pendingWorkCount, pendingWorkCount, now, projectRoot, sessionId);
|
|
2365
2498
|
}
|
|
2366
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
|
+
|
|
2367
2538
|
// ---- Hook Lifecycle ----
|
|
2368
2539
|
|
|
2369
2540
|
getHookReceipt(projectRoot: string): HookReceiptRecord | null {
|
|
@@ -2424,12 +2595,17 @@ export class DevFlowDatabase {
|
|
|
2424
2595
|
upsertContextReceipt(receipt: ContextReceiptRecord): void {
|
|
2425
2596
|
this.db.prepare(`
|
|
2426
2597
|
INSERT INTO devflow_context_receipts
|
|
2427
|
-
(project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
2428
|
-
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2429
2601
|
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
2430
2602
|
context_hash = excluded.context_hash,
|
|
2431
2603
|
issued_at = excluded.issued_at,
|
|
2432
|
-
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
|
|
2433
2609
|
`).run(
|
|
2434
2610
|
receipt.projectRoot,
|
|
2435
2611
|
receipt.sessionId,
|
|
@@ -2437,6 +2613,10 @@ export class DevFlowDatabase {
|
|
|
2437
2613
|
receipt.contextHash,
|
|
2438
2614
|
receipt.issuedAt,
|
|
2439
2615
|
receipt.expiresAt,
|
|
2616
|
+
JSON.stringify(receipt.selectedFiles ?? []),
|
|
2617
|
+
JSON.stringify(receipt.memoryIds ?? []),
|
|
2618
|
+
receipt.canonicalNextAction ?? null,
|
|
2619
|
+
receipt.requestId ?? null,
|
|
2440
2620
|
);
|
|
2441
2621
|
}
|
|
2442
2622
|
|
|
@@ -2446,7 +2626,8 @@ export class DevFlowDatabase {
|
|
|
2446
2626
|
executionId: string,
|
|
2447
2627
|
): ContextReceiptRecord | null {
|
|
2448
2628
|
const row = this.db.prepare(`
|
|
2449
|
-
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
|
|
2450
2631
|
FROM devflow_context_receipts
|
|
2451
2632
|
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
2452
2633
|
`).get(projectRoot, sessionId, executionId) as any;
|
|
@@ -2457,9 +2638,101 @@ export class DevFlowDatabase {
|
|
|
2457
2638
|
contextHash: row.context_hash,
|
|
2458
2639
|
issuedAt: row.issued_at,
|
|
2459
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,
|
|
2460
2645
|
} : null;
|
|
2461
2646
|
}
|
|
2462
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
|
+
|
|
2463
2736
|
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number {
|
|
2464
2737
|
return executionId
|
|
2465
2738
|
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
@@ -2476,25 +2749,158 @@ export class DevFlowDatabase {
|
|
|
2476
2749
|
}
|
|
2477
2750
|
|
|
2478
2751
|
beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord {
|
|
2752
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
2479
2753
|
this.db.prepare(`
|
|
2480
2754
|
INSERT OR IGNORE INTO devflow_memory_turns
|
|
2481
2755
|
(turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
|
|
2482
2756
|
VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
|
|
2483
2757
|
`).run(
|
|
2484
|
-
|
|
2758
|
+
turnId,
|
|
2485
2759
|
input.projectRoot,
|
|
2486
2760
|
input.sessionId,
|
|
2487
2761
|
input.promptHash,
|
|
2488
2762
|
input.eventId,
|
|
2489
2763
|
input.createdAt,
|
|
2490
2764
|
);
|
|
2491
|
-
|
|
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);
|
|
2492
2898
|
}
|
|
2493
2899
|
|
|
2494
2900
|
getMemoryTurn(turnId: string): MemoryTurnRecord | null {
|
|
2495
2901
|
const row = this.db.prepare(
|
|
2496
2902
|
'SELECT * FROM devflow_memory_turns WHERE turn_id = ?',
|
|
2497
|
-
).get(turnId) as any;
|
|
2903
|
+
).get(normalizeTurnId(turnId)) as any;
|
|
2498
2904
|
return row ? this.mapMemoryTurn(row) : null;
|
|
2499
2905
|
}
|
|
2500
2906
|
|
|
@@ -2515,6 +2921,7 @@ export class DevFlowDatabase {
|
|
|
2515
2921
|
reason?: string;
|
|
2516
2922
|
decidedAt?: number;
|
|
2517
2923
|
}): MemoryTurnRecord {
|
|
2924
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
2518
2925
|
this.db.prepare(`
|
|
2519
2926
|
UPDATE devflow_memory_turns
|
|
2520
2927
|
SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
|
|
@@ -2525,12 +2932,22 @@ export class DevFlowDatabase {
|
|
|
2525
2932
|
input.source,
|
|
2526
2933
|
input.reason ?? null,
|
|
2527
2934
|
input.decidedAt ?? Date.now(),
|
|
2528
|
-
|
|
2935
|
+
turnId,
|
|
2529
2936
|
);
|
|
2530
|
-
const turn = this.getMemoryTurn(
|
|
2937
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2531
2938
|
if (!turn || turn.status !== 'committed') {
|
|
2532
|
-
throw new Error(`Memory turn ${
|
|
2939
|
+
throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
|
|
2533
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
|
+
});
|
|
2534
2951
|
return turn;
|
|
2535
2952
|
}
|
|
2536
2953
|
|
|
@@ -2540,6 +2957,7 @@ export class DevFlowDatabase {
|
|
|
2540
2957
|
reason: string;
|
|
2541
2958
|
decidedAt?: number;
|
|
2542
2959
|
}): MemoryTurnRecord {
|
|
2960
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
2543
2961
|
this.db.prepare(`
|
|
2544
2962
|
UPDATE devflow_memory_turns
|
|
2545
2963
|
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
|
|
@@ -2549,12 +2967,22 @@ export class DevFlowDatabase {
|
|
|
2549
2967
|
input.receiptId,
|
|
2550
2968
|
input.reason,
|
|
2551
2969
|
input.decidedAt ?? Date.now(),
|
|
2552
|
-
|
|
2970
|
+
turnId,
|
|
2553
2971
|
);
|
|
2554
|
-
const turn = this.getMemoryTurn(
|
|
2972
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2555
2973
|
if (!turn || turn.status !== 'skipped') {
|
|
2556
|
-
throw new Error(`Memory turn ${
|
|
2974
|
+
throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
|
|
2557
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
|
+
});
|
|
2558
2986
|
return turn;
|
|
2559
2987
|
}
|
|
2560
2988
|
|
|
@@ -2562,7 +2990,23 @@ export class DevFlowDatabase {
|
|
|
2562
2990
|
return this.db.prepare(`
|
|
2563
2991
|
UPDATE devflow_memory_turns SET stop_prompted_at = ?
|
|
2564
2992
|
WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
|
|
2565
|
-
`).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
|
+
});
|
|
2566
3010
|
}
|
|
2567
3011
|
|
|
2568
3012
|
listMemoryTurns(projectRoot: string, sessionId?: string, limit = 50): MemoryTurnRecord[] {
|
|
@@ -2576,6 +3020,18 @@ export class DevFlowDatabase {
|
|
|
2576
3020
|
return rows.map(row => this.mapMemoryTurn(row));
|
|
2577
3021
|
}
|
|
2578
3022
|
|
|
3023
|
+
listSessionMemoryTurnsForReconciliation(
|
|
3024
|
+
projectRoot: string,
|
|
3025
|
+
sessionId: string,
|
|
3026
|
+
): MemoryTurnRecord[] {
|
|
3027
|
+
const rows = this.db.prepare(`
|
|
3028
|
+
SELECT * FROM devflow_memory_turns
|
|
3029
|
+
WHERE project_root = ? AND session_id = ?
|
|
3030
|
+
ORDER BY created_at ASC, turn_id ASC
|
|
3031
|
+
`).all(projectRoot, sessionId) as any[];
|
|
3032
|
+
return rows.map(row => this.mapMemoryTurn(row));
|
|
3033
|
+
}
|
|
3034
|
+
|
|
2579
3035
|
insertHookFallback(record: HookFallbackRecord): boolean {
|
|
2580
3036
|
return this.db.prepare(`
|
|
2581
3037
|
INSERT OR IGNORE INTO devflow_hook_fallbacks
|
|
@@ -2655,8 +3111,13 @@ export class DevFlowDatabase {
|
|
|
2655
3111
|
recordMemoryDistillCheckpoint(checkpoint: MemoryDistillCheckpointRecord): void {
|
|
2656
3112
|
this.db.prepare(`
|
|
2657
3113
|
INSERT INTO devflow_memory_distill_checkpoints
|
|
2658
|
-
(id, project_root, session_id, trigger, pending_events, released_leases, created_at)
|
|
2659
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3114
|
+
(id, project_root, session_id, trigger, pending_events, released_leases, details, created_at)
|
|
3115
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3116
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3117
|
+
pending_events = excluded.pending_events,
|
|
3118
|
+
released_leases = MAX(devflow_memory_distill_checkpoints.released_leases, excluded.released_leases),
|
|
3119
|
+
details = excluded.details,
|
|
3120
|
+
created_at = MIN(devflow_memory_distill_checkpoints.created_at, excluded.created_at)
|
|
2660
3121
|
`).run(
|
|
2661
3122
|
checkpoint.id,
|
|
2662
3123
|
checkpoint.projectRoot,
|
|
@@ -2664,13 +3125,14 @@ export class DevFlowDatabase {
|
|
|
2664
3125
|
checkpoint.trigger,
|
|
2665
3126
|
checkpoint.pendingEvents,
|
|
2666
3127
|
checkpoint.releasedLeases,
|
|
3128
|
+
JSON.stringify(checkpoint.details ?? {}),
|
|
2667
3129
|
checkpoint.createdAt,
|
|
2668
3130
|
);
|
|
2669
3131
|
}
|
|
2670
3132
|
|
|
2671
3133
|
listMemoryDistillCheckpoints(projectRoot: string, limit = 50): MemoryDistillCheckpointRecord[] {
|
|
2672
3134
|
const rows = this.db.prepare(`
|
|
2673
|
-
SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
|
|
3135
|
+
SELECT id, project_root, session_id, trigger, pending_events, released_leases, details, created_at
|
|
2674
3136
|
FROM devflow_memory_distill_checkpoints
|
|
2675
3137
|
WHERE project_root = ?
|
|
2676
3138
|
ORDER BY created_at DESC
|
|
@@ -2683,6 +3145,7 @@ export class DevFlowDatabase {
|
|
|
2683
3145
|
trigger: row.trigger,
|
|
2684
3146
|
pendingEvents: Number(row.pending_events),
|
|
2685
3147
|
releasedLeases: Number(row.released_leases),
|
|
3148
|
+
details: row.details ? JSON.parse(row.details) : {},
|
|
2686
3149
|
createdAt: Number(row.created_at),
|
|
2687
3150
|
}));
|
|
2688
3151
|
}
|
|
@@ -2706,6 +3169,13 @@ function parseJson(value: unknown): unknown {
|
|
|
2706
3169
|
try { return JSON.parse(value); } catch { return value; }
|
|
2707
3170
|
}
|
|
2708
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
|
+
|
|
2709
3179
|
function deriveQuery(input: unknown): string | null {
|
|
2710
3180
|
if (!input || typeof input !== 'object') return null;
|
|
2711
3181
|
const record = input as Record<string, unknown>;
|
|
@@ -2770,3 +3240,40 @@ function collectCanonicalReceiptIds(values: unknown[]): string[] {
|
|
|
2770
3240
|
values.forEach(value => visit(value, 0));
|
|
2771
3241
|
return [...receiptIds];
|
|
2772
3242
|
}
|
|
3243
|
+
|
|
3244
|
+
function stableMetadataKey(value: unknown): string {
|
|
3245
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? String(value);
|
|
3246
|
+
if (Array.isArray(value)) return `[${value.map(stableMetadataKey).join(',')}]`;
|
|
3247
|
+
const record = value as Record<string, unknown>;
|
|
3248
|
+
return `{${Object.keys(record).sort()
|
|
3249
|
+
.map(key => `${JSON.stringify(key)}:${stableMetadataKey(record[key])}`)
|
|
3250
|
+
.join(',')}}`;
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
function unionMetadataArrays(existing: unknown, incoming: unknown): unknown[] | undefined {
|
|
3254
|
+
const values = [
|
|
3255
|
+
...(Array.isArray(existing) ? existing : []),
|
|
3256
|
+
...(Array.isArray(incoming) ? incoming : []),
|
|
3257
|
+
];
|
|
3258
|
+
if (values.length === 0 && !Array.isArray(existing) && !Array.isArray(incoming)) return undefined;
|
|
3259
|
+
const unique = new Map<string, unknown>();
|
|
3260
|
+
for (const value of values) unique.set(stableMetadataKey(value), value);
|
|
3261
|
+
return [...unique.values()];
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
function mergeExecutionMetadata(
|
|
3265
|
+
existing: Record<string, unknown>,
|
|
3266
|
+
incoming: Record<string, unknown>,
|
|
3267
|
+
): Record<string, unknown> {
|
|
3268
|
+
const merged = { ...existing, ...incoming };
|
|
3269
|
+
for (const key of [
|
|
3270
|
+
'evidenceDegradations',
|
|
3271
|
+
'evidenceContractOutcomes',
|
|
3272
|
+
'pendingEvidenceContractIds',
|
|
3273
|
+
'sessionFinalizationDegradations',
|
|
3274
|
+
]) {
|
|
3275
|
+
const union = unionMetadataArrays(existing[key], incoming[key]);
|
|
3276
|
+
if (union !== undefined) merged[key] = union;
|
|
3277
|
+
}
|
|
3278
|
+
return merged;
|
|
3279
|
+
}
|