@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 CHANGED
@@ -3,6 +3,17 @@
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.13](https://github.com/shilongfeicool/dev-flow/compare/v0.16.12...v0.16.13) (2026-07-26)
7
+
8
+
9
+ ### Features
10
+
11
+ * add retrieval quality control plane ([743bd12](https://github.com/shilongfeicool/dev-flow/commit/743bd12daa18ceec611dc82674b20b015d110f0c))
12
+
13
+
14
+
15
+
16
+
6
17
  ## [0.16.12](https://github.com/shilongfeicool/dev-flow/compare/v0.16.11...v0.16.12) (2026-07-26)
7
18
 
8
19
 
@@ -1,4 +1,5 @@
1
1
  import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, WorkError, WorkItemRecord, WorkQueueHealth } from './work-queue';
2
+ import { type SessionObligationRecord, type SessionObligationState } from './obligation-ledger';
2
3
  export interface BenchmarkReportRecord {
3
4
  runId: string;
4
5
  suiteId: string;
@@ -74,6 +75,22 @@ export interface ContextReceiptRecord {
74
75
  contextHash: string;
75
76
  issuedAt: number;
76
77
  expiresAt: number;
78
+ selectedFiles?: string[];
79
+ memoryIds?: string[];
80
+ canonicalNextAction?: string;
81
+ requestId?: string;
82
+ }
83
+ export interface ContextSelectionEventRecord {
84
+ id: string;
85
+ projectRoot: string;
86
+ sessionId: string;
87
+ executionId: string;
88
+ requestId?: string;
89
+ selectionType: 'code' | 'action';
90
+ candidateId: string;
91
+ toolName: string;
92
+ toolUseId?: string;
93
+ selectedAt: number;
77
94
  }
78
95
  export interface MemoryDistillCheckpointRecord {
79
96
  id: string;
@@ -217,6 +234,7 @@ export declare class DevFlowDatabase {
217
234
  updateToolCallEvent(eventId: string, updates: {
218
235
  output?: string;
219
236
  error?: string;
237
+ failureCategory?: string;
220
238
  duration?: number;
221
239
  }): boolean;
222
240
  markToolCallBlocked(eventId: string, reason: string): boolean;
@@ -321,14 +339,36 @@ export declare class DevFlowDatabase {
321
339
  private mapWorkItem;
322
340
  private mapSessionClosure;
323
341
  private refreshClosedSessionClosure;
342
+ private countSessionLifecyclePending;
324
343
  getHookReceipt(projectRoot: string): HookReceiptRecord | null;
325
344
  updateHookReceipt(projectRoot: string, updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>): HookReceiptRecord;
326
345
  deleteHookReceipt(projectRoot: string): boolean;
327
346
  upsertContextReceipt(receipt: ContextReceiptRecord): void;
328
347
  getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
348
+ getActiveContextReceipt(projectRoot: string, sessionId: string, executionId?: string, now?: number): ContextReceiptRecord | null;
349
+ recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean;
350
+ listContextSelectionEvents(options: {
351
+ projectRoot: string;
352
+ sessionId?: string;
353
+ executionId?: string;
354
+ requestId?: string;
355
+ }): ContextSelectionEventRecord[];
329
356
  deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number;
330
357
  purgeExpiredContextReceipts(now?: number): number;
331
358
  beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord;
359
+ upsertSessionObligation(record: SessionObligationRecord): SessionObligationRecord;
360
+ getSessionObligation(projectRoot: string, sessionId: string, obligationId: string): SessionObligationRecord | null;
361
+ listSessionObligations(projectRoot: string, sessionId: string, states?: SessionObligationState[]): SessionObligationRecord[];
362
+ resolveSessionObligation(input: {
363
+ projectRoot: string;
364
+ sessionId: string;
365
+ obligationId: string;
366
+ state: 'satisfied' | 'degraded' | 'cancelled';
367
+ receiptId?: string;
368
+ reason?: string;
369
+ resolvedAt?: number;
370
+ }): SessionObligationRecord;
371
+ countOpenSessionObligations(projectRoot: string, sessionId: string): number;
332
372
  getMemoryTurn(turnId: string): MemoryTurnRecord | null;
333
373
  getPendingMemoryTurn(projectRoot: string, sessionId: string): MemoryTurnRecord | null;
334
374
  commitMemoryTurn(input: {
@@ -346,6 +386,7 @@ export declare class DevFlowDatabase {
346
386
  decidedAt?: number;
347
387
  }): MemoryTurnRecord;
348
388
  markMemoryTurnStopPrompted(turnId: string, promptedAt?: number): boolean;
389
+ private ensureMemoryObligation;
349
390
  listMemoryTurns(projectRoot: string, sessionId?: string, limit?: number): MemoryTurnRecord[];
350
391
  listSessionMemoryTurnsForReconciliation(projectRoot: string, sessionId: string): MemoryTurnRecord[];
351
392
  insertHookFallback(record: HookFallbackRecord): boolean;
package/dist/database.js CHANGED
@@ -8,6 +8,7 @@ const path_1 = require("path");
8
8
  const fs_1 = require("fs");
9
9
  const os_1 = require("os");
10
10
  const crypto_1 = require("crypto");
11
+ const obligation_ledger_1 = require("./obligation-ledger");
11
12
  const CONTEXT_REQUIRED_SKILLS = new Set([
12
13
  'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
13
14
  ]);
@@ -281,12 +282,32 @@ class DevFlowDatabase {
281
282
  context_hash TEXT NOT NULL,
282
283
  issued_at INTEGER NOT NULL,
283
284
  expires_at INTEGER NOT NULL,
285
+ selected_files TEXT NOT NULL DEFAULT '[]',
286
+ memory_ids TEXT NOT NULL DEFAULT '[]',
287
+ canonical_next_action TEXT,
288
+ request_id TEXT,
284
289
  PRIMARY KEY (project_root, session_id, execution_id)
285
290
  );
286
291
 
287
292
  CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
288
293
  ON devflow_context_receipts(expires_at);
289
294
 
295
+ CREATE TABLE IF NOT EXISTS devflow_context_selection_events (
296
+ id TEXT PRIMARY KEY,
297
+ project_root TEXT NOT NULL,
298
+ session_id TEXT NOT NULL,
299
+ execution_id TEXT NOT NULL,
300
+ request_id TEXT,
301
+ selection_type TEXT NOT NULL CHECK(selection_type IN ('code', 'action')),
302
+ candidate_id TEXT NOT NULL,
303
+ tool_name TEXT NOT NULL,
304
+ tool_use_id TEXT,
305
+ selected_at INTEGER NOT NULL
306
+ );
307
+
308
+ CREATE INDEX IF NOT EXISTS idx_context_selection_identity
309
+ ON devflow_context_selection_events(project_root, session_id, execution_id, request_id);
310
+
290
311
  CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
291
312
  id TEXT PRIMARY KEY,
292
313
  project_root TEXT NOT NULL,
@@ -323,6 +344,32 @@ class DevFlowDatabase {
323
344
  CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
324
345
  ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
325
346
 
347
+ CREATE TABLE IF NOT EXISTS devflow_session_obligations (
348
+ obligation_id TEXT NOT NULL,
349
+ project_root TEXT NOT NULL,
350
+ session_id TEXT NOT NULL,
351
+ execution_id TEXT,
352
+ turn_id TEXT,
353
+ kind TEXT NOT NULL
354
+ CHECK(kind IN ('memory_decision', 'evidence_contract', 'session_finalize')),
355
+ state TEXT NOT NULL DEFAULT 'open'
356
+ CHECK(state IN ('open', 'satisfied', 'degraded', 'cancelled')),
357
+ payload TEXT NOT NULL DEFAULT '{}',
358
+ receipt_id TEXT,
359
+ reason TEXT,
360
+ created_at INTEGER NOT NULL,
361
+ updated_at INTEGER NOT NULL,
362
+ resolved_at INTEGER,
363
+ PRIMARY KEY (project_root, session_id, obligation_id)
364
+ );
365
+
366
+ CREATE INDEX IF NOT EXISTS idx_session_obligations_state
367
+ ON devflow_session_obligations(project_root, session_id, state, created_at);
368
+ CREATE INDEX IF NOT EXISTS idx_session_obligations_turn
369
+ ON devflow_session_obligations(project_root, session_id, turn_id);
370
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_session_obligations_receipt
371
+ ON devflow_session_obligations(receipt_id) WHERE receipt_id IS NOT NULL;
372
+
326
373
  CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
327
374
  id TEXT PRIMARY KEY,
328
375
  project_root TEXT NOT NULL,
@@ -458,6 +505,22 @@ class DevFlowDatabase {
458
505
  this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER');
459
506
  }
460
507
  catch { }
508
+ try {
509
+ this.db.exec("ALTER TABLE devflow_context_receipts ADD COLUMN selected_files TEXT NOT NULL DEFAULT '[]'");
510
+ }
511
+ catch { }
512
+ try {
513
+ this.db.exec("ALTER TABLE devflow_context_receipts ADD COLUMN memory_ids TEXT NOT NULL DEFAULT '[]'");
514
+ }
515
+ catch { }
516
+ try {
517
+ this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN canonical_next_action TEXT');
518
+ }
519
+ catch { }
520
+ try {
521
+ this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN request_id TEXT');
522
+ }
523
+ catch { }
461
524
  this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
462
525
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
463
526
  // Migration: tool_metrics table for per-tool metrics collection
@@ -1083,6 +1146,10 @@ class DevFlowDatabase {
1083
1146
  sets.push('error = ?');
1084
1147
  values.push(updates.error);
1085
1148
  }
1149
+ if (updates.failureCategory !== undefined) {
1150
+ sets.push('failure_category = ?');
1151
+ values.push(updates.failureCategory);
1152
+ }
1086
1153
  if (updates.duration !== undefined) {
1087
1154
  sets.push('duration = ?');
1088
1155
  values.push(updates.duration);
@@ -1748,14 +1815,7 @@ class DevFlowDatabase {
1748
1815
  return row ? this.mapSessionClosure(row) : null;
1749
1816
  }
1750
1817
  completeSessionClosure(projectRoot, sessionId, excludingWorkItemId, closedAt = Date.now()) {
1751
- const pending = this.db.prepare(`
1752
- SELECT COUNT(*) AS count
1753
- FROM devflow_work_items
1754
- WHERE project_root = ? AND session_id = ?
1755
- AND state IN ('pending', 'leased', 'failed', 'dead_letter')
1756
- AND (? IS NULL OR id <> ?)
1757
- `).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
1758
- const pendingWorkCount = Number(pending?.count ?? 0);
1818
+ const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId, excludingWorkItemId);
1759
1819
  const state = pendingWorkCount > 0 ? 'closed_with_pending_work' : 'closed';
1760
1820
  this.db.prepare(`
1761
1821
  UPDATE devflow_session_closures
@@ -1959,13 +2019,7 @@ class DevFlowDatabase {
1959
2019
  };
1960
2020
  }
1961
2021
  refreshClosedSessionClosure(projectRoot, sessionId, now) {
1962
- const pending = this.db.prepare(`
1963
- SELECT COUNT(*) AS count
1964
- FROM devflow_work_items
1965
- WHERE project_root = ? AND session_id = ?
1966
- AND state IN ('pending', 'leased', 'failed', 'dead_letter')
1967
- `).get(projectRoot, sessionId);
1968
- const pendingWorkCount = Number(pending?.count ?? 0);
2022
+ const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId);
1969
2023
  this.db.prepare(`
1970
2024
  UPDATE devflow_session_closures
1971
2025
  SET state = CASE WHEN ? = 0 THEN 'closed' ELSE 'closed_with_pending_work' END,
@@ -1974,6 +2028,34 @@ class DevFlowDatabase {
1974
2028
  AND state IN ('closed', 'closed_with_pending_work')
1975
2029
  `).run(pendingWorkCount, pendingWorkCount, now, projectRoot, sessionId);
1976
2030
  }
2031
+ countSessionLifecyclePending(projectRoot, sessionId, excludingWorkItemId) {
2032
+ const work = this.db.prepare(`
2033
+ SELECT COUNT(*) AS count
2034
+ FROM devflow_work_items
2035
+ WHERE project_root = ? AND session_id = ?
2036
+ AND state IN ('pending', 'leased', 'failed', 'dead_letter')
2037
+ AND (? IS NULL OR id <> ?)
2038
+ `).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
2039
+ const obligations = this.db.prepare(`
2040
+ SELECT COUNT(*) AS count
2041
+ FROM devflow_session_obligations
2042
+ WHERE project_root = ? AND session_id = ?
2043
+ AND state IN ('open', 'degraded')
2044
+ `).get(projectRoot, sessionId);
2045
+ const legacyTurns = this.db.prepare(`
2046
+ SELECT COUNT(*) AS count
2047
+ FROM devflow_memory_turns t
2048
+ WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
2049
+ AND NOT EXISTS (
2050
+ SELECT 1 FROM devflow_session_obligations o
2051
+ WHERE o.project_root = t.project_root AND o.session_id = t.session_id
2052
+ AND o.obligation_id = 'memory:' || t.turn_id
2053
+ )
2054
+ `).get(projectRoot, sessionId);
2055
+ return Number(work?.count ?? 0)
2056
+ + Number(obligations?.count ?? 0)
2057
+ + Number(legacyTurns?.count ?? 0);
2058
+ }
1977
2059
  // ---- Hook Lifecycle ----
1978
2060
  getHookReceipt(projectRoot) {
1979
2061
  const row = this.db.prepare(`
@@ -2027,17 +2109,23 @@ class DevFlowDatabase {
2027
2109
  upsertContextReceipt(receipt) {
2028
2110
  this.db.prepare(`
2029
2111
  INSERT INTO devflow_context_receipts
2030
- (project_root, session_id, execution_id, context_hash, issued_at, expires_at)
2031
- VALUES (?, ?, ?, ?, ?, ?)
2112
+ (project_root, session_id, execution_id, context_hash, issued_at, expires_at,
2113
+ selected_files, memory_ids, canonical_next_action, request_id)
2114
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2032
2115
  ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
2033
2116
  context_hash = excluded.context_hash,
2034
2117
  issued_at = excluded.issued_at,
2035
- expires_at = excluded.expires_at
2036
- `).run(receipt.projectRoot, receipt.sessionId, receipt.executionId, receipt.contextHash, receipt.issuedAt, receipt.expiresAt);
2118
+ expires_at = excluded.expires_at,
2119
+ selected_files = excluded.selected_files,
2120
+ memory_ids = excluded.memory_ids,
2121
+ canonical_next_action = excluded.canonical_next_action,
2122
+ request_id = excluded.request_id
2123
+ `).run(receipt.projectRoot, receipt.sessionId, receipt.executionId, receipt.contextHash, receipt.issuedAt, receipt.expiresAt, JSON.stringify(receipt.selectedFiles ?? []), JSON.stringify(receipt.memoryIds ?? []), receipt.canonicalNextAction ?? null, receipt.requestId ?? null);
2037
2124
  }
2038
2125
  getContextReceipt(projectRoot, sessionId, executionId) {
2039
2126
  const row = this.db.prepare(`
2040
- SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
2127
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
2128
+ selected_files, memory_ids, canonical_next_action, request_id
2041
2129
  FROM devflow_context_receipts
2042
2130
  WHERE project_root = ? AND session_id = ? AND execution_id = ?
2043
2131
  `).get(projectRoot, sessionId, executionId);
@@ -2048,8 +2136,77 @@ class DevFlowDatabase {
2048
2136
  contextHash: row.context_hash,
2049
2137
  issuedAt: row.issued_at,
2050
2138
  expiresAt: row.expires_at,
2139
+ selectedFiles: parseJsonStringArray(row.selected_files),
2140
+ memoryIds: parseJsonStringArray(row.memory_ids),
2141
+ canonicalNextAction: row.canonical_next_action ?? undefined,
2142
+ requestId: row.request_id ?? undefined,
2143
+ } : null;
2144
+ }
2145
+ getActiveContextReceipt(projectRoot, sessionId, executionId, now = Date.now()) {
2146
+ const executionClause = executionId ? 'AND execution_id = ?' : '';
2147
+ const params = executionId
2148
+ ? [projectRoot, sessionId, executionId, now]
2149
+ : [projectRoot, sessionId, now];
2150
+ const row = this.db.prepare(`
2151
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
2152
+ selected_files, memory_ids, canonical_next_action, request_id
2153
+ FROM devflow_context_receipts
2154
+ WHERE project_root = ? AND session_id = ? ${executionClause} AND expires_at > ?
2155
+ ORDER BY issued_at DESC
2156
+ LIMIT 1
2157
+ `).get(...params);
2158
+ return row ? {
2159
+ projectRoot: row.project_root,
2160
+ sessionId: row.session_id,
2161
+ executionId: row.execution_id,
2162
+ contextHash: row.context_hash,
2163
+ issuedAt: row.issued_at,
2164
+ expiresAt: row.expires_at,
2165
+ selectedFiles: parseJsonStringArray(row.selected_files),
2166
+ memoryIds: parseJsonStringArray(row.memory_ids),
2167
+ canonicalNextAction: row.canonical_next_action ?? undefined,
2168
+ requestId: row.request_id ?? undefined,
2051
2169
  } : null;
2052
2170
  }
2171
+ recordContextSelectionEvent(event) {
2172
+ return this.db.prepare(`
2173
+ INSERT OR IGNORE INTO devflow_context_selection_events
2174
+ (id, project_root, session_id, execution_id, request_id, selection_type,
2175
+ candidate_id, tool_name, tool_use_id, selected_at)
2176
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2177
+ `).run(event.id, event.projectRoot, event.sessionId, event.executionId, event.requestId ?? null, event.selectionType, event.candidateId, event.toolName, event.toolUseId ?? null, event.selectedAt).changes > 0;
2178
+ }
2179
+ listContextSelectionEvents(options) {
2180
+ const predicates = ['project_root = ?'];
2181
+ const params = [options.projectRoot];
2182
+ for (const [column, value] of [
2183
+ ['session_id', options.sessionId],
2184
+ ['execution_id', options.executionId],
2185
+ ['request_id', options.requestId],
2186
+ ]) {
2187
+ if (!value)
2188
+ continue;
2189
+ predicates.push(`${column} = ?`);
2190
+ params.push(value);
2191
+ }
2192
+ const rows = this.db.prepare(`
2193
+ SELECT * FROM devflow_context_selection_events
2194
+ WHERE ${predicates.join(' AND ')}
2195
+ ORDER BY selected_at ASC, id ASC
2196
+ `).all(...params);
2197
+ return rows.map(row => ({
2198
+ id: row.id,
2199
+ projectRoot: row.project_root,
2200
+ sessionId: row.session_id,
2201
+ executionId: row.execution_id,
2202
+ requestId: row.request_id ?? undefined,
2203
+ selectionType: row.selection_type,
2204
+ candidateId: row.candidate_id,
2205
+ toolName: row.tool_name,
2206
+ toolUseId: row.tool_use_id ?? undefined,
2207
+ selectedAt: row.selected_at,
2208
+ }));
2209
+ }
2053
2210
  deleteContextReceipt(projectRoot, sessionId, executionId) {
2054
2211
  return executionId
2055
2212
  ? this.db.prepare(`DELETE FROM devflow_context_receipts
@@ -2064,15 +2221,104 @@ class DevFlowDatabase {
2064
2221
  .run(now).changes;
2065
2222
  }
2066
2223
  beginMemoryTurn(input) {
2224
+ const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
2067
2225
  this.db.prepare(`
2068
2226
  INSERT OR IGNORE INTO devflow_memory_turns
2069
2227
  (turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
2070
2228
  VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
2071
- `).run(input.turnId, input.projectRoot, input.sessionId, input.promptHash, input.eventId, input.createdAt);
2072
- return this.getMemoryTurn(input.turnId);
2229
+ `).run(turnId, input.projectRoot, input.sessionId, input.promptHash, input.eventId, input.createdAt);
2230
+ const turn = this.getMemoryTurn(turnId);
2231
+ this.upsertSessionObligation({
2232
+ obligationId: `memory:${turnId}`,
2233
+ projectRoot: input.projectRoot,
2234
+ sessionId: input.sessionId,
2235
+ turnId,
2236
+ kind: 'memory_decision',
2237
+ state: turn.status === 'pending' ? 'open' : 'satisfied',
2238
+ payload: { eventId: input.eventId, promptHash: input.promptHash },
2239
+ receiptId: turn.receiptId,
2240
+ createdAt: input.createdAt,
2241
+ updatedAt: Date.now(),
2242
+ resolvedAt: turn.decidedAt,
2243
+ });
2244
+ return turn;
2245
+ }
2246
+ upsertSessionObligation(record) {
2247
+ if (!record.obligationId.trim())
2248
+ throw new Error('Session obligation requires an ID');
2249
+ if (!record.projectRoot.trim())
2250
+ throw new Error('Session obligation requires a project root');
2251
+ if (!record.sessionId.trim())
2252
+ throw new Error('Session obligation requires a session ID');
2253
+ const now = record.updatedAt || Date.now();
2254
+ this.db.prepare(`
2255
+ INSERT INTO devflow_session_obligations (
2256
+ obligation_id, project_root, session_id, execution_id, turn_id, kind,
2257
+ state, payload, receipt_id, reason, created_at, updated_at, resolved_at
2258
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2259
+ ON CONFLICT(project_root, session_id, obligation_id) DO UPDATE SET
2260
+ execution_id = COALESCE(excluded.execution_id, devflow_session_obligations.execution_id),
2261
+ turn_id = COALESCE(excluded.turn_id, devflow_session_obligations.turn_id),
2262
+ payload = excluded.payload,
2263
+ updated_at = excluded.updated_at
2264
+ WHERE devflow_session_obligations.state = 'open'
2265
+ `).run(record.obligationId, record.projectRoot, record.sessionId, record.executionId ?? null, record.turnId ?? null, record.kind, record.state, JSON.stringify(record.payload ?? {}), record.receiptId ?? null, record.reason ?? null, record.createdAt, now, record.resolvedAt ?? null);
2266
+ return this.getSessionObligation(record.projectRoot, record.sessionId, record.obligationId);
2267
+ }
2268
+ getSessionObligation(projectRoot, sessionId, obligationId) {
2269
+ const row = this.db.prepare(`
2270
+ SELECT * FROM devflow_session_obligations
2271
+ WHERE project_root = ? AND session_id = ? AND obligation_id = ?
2272
+ `).get(projectRoot, sessionId, obligationId);
2273
+ return row ? (0, obligation_ledger_1.mapSessionObligationRow)(row) : null;
2274
+ }
2275
+ listSessionObligations(projectRoot, sessionId, states) {
2276
+ const allowed = [...new Set(states ?? [])].filter(state => state === 'open' || state === 'satisfied' || state === 'degraded' || state === 'cancelled');
2277
+ const rows = (allowed.length > 0
2278
+ ? this.db.prepare(`
2279
+ SELECT * FROM devflow_session_obligations
2280
+ WHERE project_root = ? AND session_id = ?
2281
+ AND state IN (${allowed.map(() => '?').join(',')})
2282
+ ORDER BY created_at ASC, obligation_id ASC
2283
+ `).all(projectRoot, sessionId, ...allowed)
2284
+ : this.db.prepare(`
2285
+ SELECT * FROM devflow_session_obligations
2286
+ WHERE project_root = ? AND session_id = ?
2287
+ ORDER BY created_at ASC, obligation_id ASC
2288
+ `).all(projectRoot, sessionId));
2289
+ return rows.map(obligation_ledger_1.mapSessionObligationRow);
2290
+ }
2291
+ resolveSessionObligation(input) {
2292
+ return this.db.transaction(() => {
2293
+ const existing = this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
2294
+ if (!existing)
2295
+ throw new Error(`Session obligation ${input.obligationId} does not exist`);
2296
+ if (existing.state !== 'open') {
2297
+ const sameResolution = existing.state === input.state
2298
+ && (input.receiptId === undefined || existing.receiptId === input.receiptId);
2299
+ if (sameResolution)
2300
+ return existing;
2301
+ throw new Error(`OBLIGATION_TERMINAL_CONFLICT:${input.obligationId}`);
2302
+ }
2303
+ const resolvedAt = input.resolvedAt ?? Date.now();
2304
+ this.db.prepare(`
2305
+ UPDATE devflow_session_obligations
2306
+ SET state = ?, receipt_id = COALESCE(?, receipt_id), reason = ?,
2307
+ resolved_at = ?, updated_at = ?
2308
+ WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'open'
2309
+ `).run(input.state, input.receiptId ?? null, input.reason ?? null, resolvedAt, resolvedAt, input.projectRoot, input.sessionId, input.obligationId);
2310
+ return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
2311
+ });
2312
+ }
2313
+ countOpenSessionObligations(projectRoot, sessionId) {
2314
+ const row = this.db.prepare(`
2315
+ SELECT COUNT(*) AS count FROM devflow_session_obligations
2316
+ WHERE project_root = ? AND session_id = ? AND state = 'open'
2317
+ `).get(projectRoot, sessionId);
2318
+ return Number(row?.count ?? 0);
2073
2319
  }
2074
2320
  getMemoryTurn(turnId) {
2075
- const row = this.db.prepare('SELECT * FROM devflow_memory_turns WHERE turn_id = ?').get(turnId);
2321
+ const row = this.db.prepare('SELECT * FROM devflow_memory_turns WHERE turn_id = ?').get((0, obligation_ledger_1.normalizeTurnId)(turnId));
2076
2322
  return row ? this.mapMemoryTurn(row) : null;
2077
2323
  }
2078
2324
  getPendingMemoryTurn(projectRoot, sessionId) {
@@ -2084,35 +2330,73 @@ class DevFlowDatabase {
2084
2330
  return row ? this.mapMemoryTurn(row) : null;
2085
2331
  }
2086
2332
  commitMemoryTurn(input) {
2333
+ const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
2087
2334
  this.db.prepare(`
2088
2335
  UPDATE devflow_memory_turns
2089
2336
  SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
2090
2337
  WHERE turn_id = ? AND status = 'pending'
2091
- `).run(input.receiptId, JSON.stringify([...new Set(input.memoryIds)]), input.source, input.reason ?? null, input.decidedAt ?? Date.now(), input.turnId);
2092
- const turn = this.getMemoryTurn(input.turnId);
2338
+ `).run(input.receiptId, JSON.stringify([...new Set(input.memoryIds)]), input.source, input.reason ?? null, input.decidedAt ?? Date.now(), turnId);
2339
+ const turn = this.getMemoryTurn(turnId);
2093
2340
  if (!turn || turn.status !== 'committed') {
2094
- throw new Error(`Memory turn ${input.turnId} is not pending or does not exist`);
2095
- }
2341
+ throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
2342
+ }
2343
+ this.ensureMemoryObligation(turn);
2344
+ this.resolveSessionObligation({
2345
+ projectRoot: turn.projectRoot,
2346
+ sessionId: turn.sessionId,
2347
+ obligationId: `memory:${turn.turnId}`,
2348
+ state: 'satisfied',
2349
+ receiptId: turn.receiptId,
2350
+ reason: turn.reason,
2351
+ resolvedAt: turn.decidedAt,
2352
+ });
2096
2353
  return turn;
2097
2354
  }
2098
2355
  skipMemoryTurn(input) {
2356
+ const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
2099
2357
  this.db.prepare(`
2100
2358
  UPDATE devflow_memory_turns
2101
2359
  SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
2102
2360
  reason = ?, decided_at = ?
2103
2361
  WHERE turn_id = ? AND status = 'pending'
2104
- `).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(), input.turnId);
2105
- const turn = this.getMemoryTurn(input.turnId);
2362
+ `).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(), turnId);
2363
+ const turn = this.getMemoryTurn(turnId);
2106
2364
  if (!turn || turn.status !== 'skipped') {
2107
- throw new Error(`Memory turn ${input.turnId} is not pending or does not exist`);
2108
- }
2365
+ throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
2366
+ }
2367
+ this.ensureMemoryObligation(turn);
2368
+ this.resolveSessionObligation({
2369
+ projectRoot: turn.projectRoot,
2370
+ sessionId: turn.sessionId,
2371
+ obligationId: `memory:${turn.turnId}`,
2372
+ state: 'satisfied',
2373
+ receiptId: turn.receiptId,
2374
+ reason: turn.reason,
2375
+ resolvedAt: turn.decidedAt,
2376
+ });
2109
2377
  return turn;
2110
2378
  }
2111
2379
  markMemoryTurnStopPrompted(turnId, promptedAt = Date.now()) {
2112
2380
  return this.db.prepare(`
2113
2381
  UPDATE devflow_memory_turns SET stop_prompted_at = ?
2114
2382
  WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
2115
- `).run(promptedAt, turnId).changes === 1;
2383
+ `).run(promptedAt, (0, obligation_ledger_1.normalizeTurnId)(turnId)).changes === 1;
2384
+ }
2385
+ ensureMemoryObligation(turn) {
2386
+ const obligationId = `memory:${turn.turnId}`;
2387
+ if (this.getSessionObligation(turn.projectRoot, turn.sessionId, obligationId))
2388
+ return;
2389
+ this.upsertSessionObligation({
2390
+ obligationId,
2391
+ projectRoot: turn.projectRoot,
2392
+ sessionId: turn.sessionId,
2393
+ turnId: turn.turnId,
2394
+ kind: 'memory_decision',
2395
+ state: 'open',
2396
+ payload: { eventId: turn.eventId, promptHash: turn.promptHash },
2397
+ createdAt: turn.createdAt,
2398
+ updatedAt: Date.now(),
2399
+ });
2116
2400
  }
2117
2401
  listMemoryTurns(projectRoot, sessionId, limit = 50) {
2118
2402
  const rows = (sessionId
@@ -2255,6 +2539,12 @@ function parseJson(value) {
2255
2539
  return value;
2256
2540
  }
2257
2541
  }
2542
+ function parseJsonStringArray(value) {
2543
+ const parsed = parseJson(value);
2544
+ return Array.isArray(parsed)
2545
+ ? parsed.filter((item) => typeof item === 'string')
2546
+ : [];
2547
+ }
2258
2548
  function deriveQuery(input) {
2259
2549
  if (!input || typeof input !== 'object')
2260
2550
  return null;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
2
- export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
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
+ export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
5
+ export type { SessionObligationKind, SessionObligationRecord, SessionObligationState, } from './obligation-ledger';
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
3
+ exports.normalizeTurnId = exports.mapSessionObligationRow = 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; } });
7
7
  Object.defineProperty(exports, "openGlobalDevFlowDatabase", { enumerable: true, get: function () { return database_1.openGlobalDevFlowDatabase; } });
8
+ var obligation_ledger_1 = require("./obligation-ledger");
9
+ Object.defineProperty(exports, "mapSessionObligationRow", { enumerable: true, get: function () { return obligation_ledger_1.mapSessionObligationRow; } });
10
+ Object.defineProperty(exports, "normalizeTurnId", { enumerable: true, get: function () { return obligation_ledger_1.normalizeTurnId; } });
@@ -0,0 +1,19 @@
1
+ export type SessionObligationKind = 'memory_decision' | 'evidence_contract' | 'session_finalize';
2
+ export type SessionObligationState = 'open' | 'satisfied' | 'degraded' | 'cancelled';
3
+ export interface SessionObligationRecord {
4
+ obligationId: string;
5
+ projectRoot: string;
6
+ sessionId: string;
7
+ executionId?: string;
8
+ turnId?: string;
9
+ kind: SessionObligationKind;
10
+ state: SessionObligationState;
11
+ payload: Record<string, unknown>;
12
+ receiptId?: string;
13
+ reason?: string;
14
+ createdAt: number;
15
+ updatedAt: number;
16
+ resolvedAt?: number;
17
+ }
18
+ export declare function normalizeTurnId(value: string): string;
19
+ export declare function mapSessionObligationRow(row: Record<string, unknown>): SessionObligationRecord;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeTurnId = normalizeTurnId;
4
+ exports.mapSessionObligationRow = mapSessionObligationRow;
5
+ function normalizeTurnId(value) {
6
+ const trimmed = value.trim();
7
+ if (!trimmed)
8
+ throw new Error('Memory turn ID is required');
9
+ return trimmed.startsWith('turn:') ? trimmed : `turn:${trimmed}`;
10
+ }
11
+ function mapSessionObligationRow(row) {
12
+ let payload = {};
13
+ try {
14
+ const parsed = JSON.parse(String(row.payload ?? '{}'));
15
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
16
+ payload = parsed;
17
+ }
18
+ }
19
+ catch { }
20
+ return {
21
+ obligationId: String(row.obligation_id),
22
+ projectRoot: String(row.project_root),
23
+ sessionId: String(row.session_id),
24
+ executionId: typeof row.execution_id === 'string' ? row.execution_id : undefined,
25
+ turnId: typeof row.turn_id === 'string' ? row.turn_id : undefined,
26
+ kind: row.kind,
27
+ state: row.state,
28
+ payload,
29
+ receiptId: typeof row.receipt_id === 'string' ? row.receipt_id : undefined,
30
+ reason: typeof row.reason === 'string' ? row.reason : undefined,
31
+ createdAt: Number(row.created_at),
32
+ updatedAt: Number(row.updated_at),
33
+ resolvedAt: typeof row.resolved_at === 'number' ? row.resolved_at : undefined,
34
+ };
35
+ }
@@ -1,4 +1,4 @@
1
- export type WorkKind = 'memory.explicit_commit' | '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' | '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;