@devflow-tools/database 0.17.1 → 0.17.3

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/dist/database.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DevFlowDatabase = void 0;
4
4
  exports.getGlobalDevFlowDbPath = getGlobalDevFlowDbPath;
5
5
  exports.openGlobalDevFlowDatabase = openGlobalDevFlowDatabase;
6
+ exports.openGlobalDevFlowReadOnlyDatabase = openGlobalDevFlowReadOnlyDatabase;
6
7
  const node_sqlite_1 = require("./node-sqlite");
7
8
  const path_1 = require("path");
8
9
  const fs_1 = require("fs");
@@ -14,6 +15,7 @@ const retrieval_sessions_1 = require("./retrieval-sessions");
14
15
  const learning_candidates_1 = require("./learning-candidates");
15
16
  const workflow_workers_1 = require("./workflow-workers");
16
17
  const task_semantic_control_1 = require("./task-semantic-control");
18
+ const task_runtime_1 = require("./task-runtime");
17
19
  const CONTEXT_REQUIRED_SKILLS = new Set([
18
20
  'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
19
21
  ]);
@@ -62,28 +64,59 @@ function openGlobalDevFlowDatabase(home = (0, os_1.homedir)(), options) {
62
64
  return new DevFlowDatabase(home, {
63
65
  dbPath: getGlobalDevFlowDbPath(home),
64
66
  busyTimeoutMs: options?.busyTimeoutMs,
67
+ readonly: options?.readonly,
65
68
  });
66
69
  }
70
+ function openGlobalDevFlowReadOnlyDatabase(home = (0, os_1.homedir)(), options) {
71
+ return openGlobalDevFlowDatabase(home, { ...options, readonly: true });
72
+ }
73
+ // Schema setup is process-owned. Hook daemons open short-lived connections for
74
+ // bounded transactions, but replaying the full idempotent DDL on every open is
75
+ // still expensive and serializes concurrent host requests. An inode identity
76
+ // keeps the cache safe when a test, repair, or user replaces the database file.
77
+ const initializedDatabaseFiles = new Map();
78
+ function getDatabaseIdentity(path) {
79
+ try {
80
+ const stats = (0, fs_1.statSync)(path);
81
+ return { dev: stats.dev, ino: stats.ino };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
67
87
  class DevFlowDatabase {
68
88
  constructor(projectRoot, opts) {
69
89
  let dbPath;
70
90
  if (opts?.dbPath) {
71
91
  const dir = (0, path_1.dirname)(opts.dbPath);
72
- if (!(0, fs_1.existsSync)(dir))
92
+ if (!(0, fs_1.existsSync)(dir) && !opts.readonly)
73
93
  (0, fs_1.mkdirSync)(dir, { recursive: true });
74
94
  dbPath = opts.dbPath;
75
95
  }
76
96
  else {
77
97
  const devflowDir = (0, path_1.join)(projectRoot, '.devflow');
78
- if (!(0, fs_1.existsSync)(devflowDir))
98
+ if (!(0, fs_1.existsSync)(devflowDir) && !opts?.readonly)
79
99
  (0, fs_1.mkdirSync)(devflowDir, { recursive: true });
80
100
  dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
81
101
  }
82
- this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
83
- this.db.exec('PRAGMA journal_mode = WAL');
102
+ if (opts?.readonly && !(0, fs_1.existsSync)(dbPath))
103
+ throw new Error(`DEVFLOW_DATABASE_NOT_FOUND:${dbPath}`);
104
+ this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs, opts?.readonly === true);
84
105
  this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
106
+ if (opts?.readonly)
107
+ return;
85
108
  this.db.exec('PRAGMA foreign_keys = OFF');
86
- this.initializeSchema();
109
+ const databaseIdentity = getDatabaseIdentity(dbPath);
110
+ const initializedIdentity = initializedDatabaseFiles.get(dbPath);
111
+ const schemaInitialized = databaseIdentity !== null
112
+ && initializedIdentity?.dev === databaseIdentity.dev
113
+ && initializedIdentity.ino === databaseIdentity.ino;
114
+ if (!schemaInitialized) {
115
+ this.initializeSchema();
116
+ const currentIdentity = getDatabaseIdentity(dbPath);
117
+ if (currentIdentity)
118
+ initializedDatabaseFiles.set(dbPath, currentIdentity);
119
+ }
87
120
  }
88
121
  initializeSchema() {
89
122
  this.db.exec(`
@@ -324,6 +357,9 @@ class DevFlowDatabase {
324
357
  source_id TEXT NOT NULL, stage TEXT NOT NULL, rank INTEGER, raw_score REAL,
325
358
  normalized_score REAL, final_score REAL, applicability TEXT NOT NULL DEFAULT '[]',
326
359
  reason TEXT, tool_evidence TEXT NOT NULL DEFAULT '[]', verification_receipt TEXT,
360
+ schema_version TEXT NOT NULL DEFAULT 'retrieval-ledger-event.v1', task_spec_hash TEXT,
361
+ actor TEXT, source_version TEXT, source_content_hash TEXT,
362
+ evidence_ids TEXT NOT NULL DEFAULT '[]', reason_code TEXT,
327
363
  payload TEXT NOT NULL DEFAULT '{}', created_at INTEGER NOT NULL
328
364
  );
329
365
  CREATE INDEX IF NOT EXISTS idx_retrieval_ledger_identity ON devflow_retrieval_ledger(
@@ -428,6 +464,40 @@ class DevFlowDatabase {
428
464
  CREATE INDEX IF NOT EXISTS idx_terminal_transition_turn
429
465
  ON devflow_terminal_transitions(project_root, session_id, turn_id, sequence);
430
466
 
467
+ CREATE TABLE IF NOT EXISTS devflow_task_runtime_events (
468
+ event_id TEXT PRIMARY KEY,
469
+ schema_version TEXT NOT NULL,
470
+ producer TEXT NOT NULL,
471
+ producer_version TEXT NOT NULL,
472
+ project_root TEXT NOT NULL,
473
+ project_id TEXT NOT NULL,
474
+ host_id TEXT NOT NULL,
475
+ session_id TEXT NOT NULL,
476
+ turn_id TEXT NOT NULL,
477
+ request_id TEXT NOT NULL,
478
+ execution_id TEXT,
479
+ task_spec_hash TEXT NOT NULL,
480
+ sequence INTEGER NOT NULL CHECK(sequence > 0),
481
+ kind TEXT NOT NULL,
482
+ payload_json TEXT NOT NULL DEFAULT '{}',
483
+ created_at INTEGER NOT NULL,
484
+ UNIQUE(project_root, session_id, turn_id, sequence)
485
+ );
486
+ CREATE INDEX IF NOT EXISTS idx_task_runtime_event_identity
487
+ ON devflow_task_runtime_events(project_root, session_id, turn_id, sequence);
488
+
489
+ CREATE TABLE IF NOT EXISTS devflow_task_runtime_snapshots (
490
+ project_root TEXT NOT NULL,
491
+ session_id TEXT NOT NULL,
492
+ turn_id TEXT NOT NULL,
493
+ last_event_sequence INTEGER NOT NULL,
494
+ schema_version TEXT NOT NULL,
495
+ snapshot_json TEXT NOT NULL,
496
+ snapshot_hash TEXT NOT NULL,
497
+ updated_at INTEGER NOT NULL,
498
+ PRIMARY KEY(project_root, session_id, turn_id)
499
+ );
500
+
431
501
  CREATE TABLE IF NOT EXISTS devflow_transcript_checkpoints (
432
502
  id TEXT PRIMARY KEY,
433
503
  project_root TEXT NOT NULL,
@@ -581,9 +651,11 @@ class DevFlowDatabase {
581
651
  project_root TEXT NOT NULL,
582
652
  session_id TEXT,
583
653
  turn_id TEXT,
654
+ source_hash TEXT,
655
+ task_spec_hash TEXT,
584
656
  payload TEXT NOT NULL,
585
657
  state TEXT NOT NULL DEFAULT 'pending'
586
- CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter')),
658
+ CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter', 'cancelled')),
587
659
  attempts INTEGER NOT NULL DEFAULT 0 CHECK(attempts >= 0),
588
660
  max_attempts INTEGER NOT NULL DEFAULT 5 CHECK(max_attempts > 0),
589
661
  lease_owner TEXT,
@@ -593,7 +665,11 @@ class DevFlowDatabase {
593
665
  error_message TEXT,
594
666
  created_at INTEGER NOT NULL,
595
667
  updated_at INTEGER NOT NULL,
596
- completed_at INTEGER
668
+ completed_at INTEGER,
669
+ cancelled_at INTEGER,
670
+ cancellation_reason TEXT,
671
+ replay_of_id TEXT,
672
+ replay_count INTEGER NOT NULL DEFAULT 0
597
673
  );
598
674
 
599
675
  CREATE INDEX IF NOT EXISTS idx_work_items_ready
@@ -603,6 +679,16 @@ class DevFlowDatabase {
603
679
  CREATE INDEX IF NOT EXISTS idx_work_items_completed
604
680
  ON devflow_work_items(project_root, completed_at DESC);
605
681
 
682
+ CREATE TABLE IF NOT EXISTS devflow_project_mutation_leases (
683
+ project_root TEXT NOT NULL,
684
+ mutation_kind TEXT NOT NULL,
685
+ owner TEXT NOT NULL,
686
+ lease_expires_at INTEGER NOT NULL,
687
+ source_hash TEXT,
688
+ updated_at INTEGER NOT NULL,
689
+ PRIMARY KEY(project_root, mutation_kind)
690
+ );
691
+
606
692
  CREATE TABLE IF NOT EXISTS devflow_session_closures (
607
693
  session_id TEXT NOT NULL,
608
694
  project_root TEXT NOT NULL,
@@ -946,6 +1032,58 @@ class DevFlowDatabase {
946
1032
  this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN request_id TEXT');
947
1033
  }
948
1034
  catch { }
1035
+ try {
1036
+ this.db.exec("ALTER TABLE devflow_retrieval_ledger ADD COLUMN schema_version TEXT NOT NULL DEFAULT 'retrieval-ledger-event.v1'");
1037
+ }
1038
+ catch { }
1039
+ try {
1040
+ this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN task_spec_hash TEXT');
1041
+ }
1042
+ catch { }
1043
+ try {
1044
+ this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN actor TEXT');
1045
+ }
1046
+ catch { }
1047
+ try {
1048
+ this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN source_version TEXT');
1049
+ }
1050
+ catch { }
1051
+ try {
1052
+ this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN source_content_hash TEXT');
1053
+ }
1054
+ catch { }
1055
+ try {
1056
+ this.db.exec("ALTER TABLE devflow_retrieval_ledger ADD COLUMN evidence_ids TEXT NOT NULL DEFAULT '[]'");
1057
+ }
1058
+ catch { }
1059
+ try {
1060
+ this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN reason_code TEXT');
1061
+ }
1062
+ catch { }
1063
+ try {
1064
+ this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN source_hash TEXT');
1065
+ }
1066
+ catch { }
1067
+ try {
1068
+ this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN task_spec_hash TEXT');
1069
+ }
1070
+ catch { }
1071
+ try {
1072
+ this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN cancelled_at INTEGER');
1073
+ }
1074
+ catch { }
1075
+ try {
1076
+ this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN cancellation_reason TEXT');
1077
+ }
1078
+ catch { }
1079
+ try {
1080
+ this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN replay_of_id TEXT');
1081
+ }
1082
+ catch { }
1083
+ try {
1084
+ this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN replay_count INTEGER NOT NULL DEFAULT 0');
1085
+ }
1086
+ catch { }
949
1087
  this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
950
1088
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
951
1089
  // Migration: tool_metrics table for per-tool metrics collection
@@ -1294,14 +1432,20 @@ class DevFlowDatabase {
1294
1432
  }
1295
1433
  getToolCallEventByToolUseId(sessionId, toolUseId) {
1296
1434
  const row = this.db.prepare(`
1297
- SELECT event_id, timestamp, duration
1435
+ SELECT event_id, execution_id, timestamp, duration, input
1298
1436
  FROM tool_call_events
1299
1437
  WHERE session_id = ? AND tool_use_id = ?
1300
1438
  ORDER BY timestamp DESC
1301
1439
  LIMIT 1
1302
1440
  `).get(sessionId, toolUseId);
1303
1441
  return row
1304
- ? { eventId: row.event_id, timestamp: row.timestamp, duration: row.duration ?? 0 }
1442
+ ? {
1443
+ eventId: row.event_id,
1444
+ executionId: row.execution_id,
1445
+ timestamp: row.timestamp,
1446
+ duration: row.duration ?? 0,
1447
+ input: parseJsonObject(row.input) ?? {},
1448
+ }
1305
1449
  : null;
1306
1450
  }
1307
1451
  listToolCallEventsBySessions(sessionIds) {
@@ -2374,7 +2518,8 @@ class DevFlowDatabase {
2374
2518
  throw new Error('Work idempotency key is required');
2375
2519
  if (!input.projectRoot.trim())
2376
2520
  throw new Error('Work project root is required');
2377
- const maxAttempts = input.maxAttempts ?? 5;
2521
+ const maxAttempts = input.maxAttempts
2522
+ ?? (input.kind === 'knowledge.ingest' || input.kind === 'knowledge.index_refresh' ? 3 : 5);
2378
2523
  if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) {
2379
2524
  throw new Error('Work maxAttempts must be a positive safe integer');
2380
2525
  }
@@ -2385,10 +2530,10 @@ class DevFlowDatabase {
2385
2530
  }
2386
2531
  const row = this.db.prepare(`
2387
2532
  INSERT INTO devflow_work_items (
2388
- id, idempotency_key, kind, project_root, session_id, turn_id, payload,
2533
+ id, idempotency_key, kind, project_root, session_id, turn_id, source_hash, task_spec_hash, payload,
2389
2534
  state, attempts, max_attempts, lease_owner, lease_expires_at,
2390
2535
  next_attempt_at, error_category, error_message, created_at, updated_at, completed_at
2391
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
2536
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
2392
2537
  ON CONFLICT(idempotency_key) DO UPDATE SET
2393
2538
  state = CASE
2394
2539
  WHEN devflow_work_items.state = 'dead_letter'
@@ -2422,13 +2567,58 @@ class DevFlowDatabase {
2422
2567
  ELSE devflow_work_items.updated_at
2423
2568
  END
2424
2569
  RETURNING *
2425
- `).get((0, crypto_1.randomUUID)(), input.idempotencyKey, input.kind, input.projectRoot, input.sessionId ?? null, input.turnId ?? null, JSON.stringify(input.payload ?? null), maxAttempts, nextAttemptAt, createdAt, createdAt);
2570
+ `).get((0, crypto_1.randomUUID)(), input.idempotencyKey, input.kind, input.projectRoot, input.sessionId ?? null, input.turnId ?? null, input.sourceHash ?? null, input.taskSpecHash ?? null, JSON.stringify(input.payload ?? null), maxAttempts, nextAttemptAt, createdAt, createdAt);
2426
2571
  return this.mapWorkItem(row);
2427
2572
  }
2428
2573
  getWorkByIdempotencyKey(idempotencyKey) {
2429
2574
  const row = this.db.prepare('SELECT * FROM devflow_work_items WHERE idempotency_key = ?').get(idempotencyKey);
2430
2575
  return row ? this.mapWorkItem(row) : null;
2431
2576
  }
2577
+ getWorkById(id) {
2578
+ const row = this.db.prepare('SELECT * FROM devflow_work_items WHERE id = ?').get(id);
2579
+ return row ? this.mapWorkItem(row) : null;
2580
+ }
2581
+ acquireProjectMutationLease(input) {
2582
+ const now = input.now ?? Date.now();
2583
+ return this.db.prepare(`
2584
+ INSERT INTO devflow_project_mutation_leases
2585
+ (project_root, mutation_kind, owner, lease_expires_at, source_hash, updated_at)
2586
+ VALUES (?, ?, ?, ?, ?, ?)
2587
+ ON CONFLICT(project_root, mutation_kind) DO UPDATE SET
2588
+ owner = excluded.owner, lease_expires_at = excluded.lease_expires_at,
2589
+ source_hash = excluded.source_hash, updated_at = excluded.updated_at
2590
+ WHERE devflow_project_mutation_leases.lease_expires_at <= ? OR devflow_project_mutation_leases.owner = excluded.owner
2591
+ `).run(input.projectRoot, input.mutationKind, input.owner, now + input.leaseMs, input.sourceHash ?? null, now, now).changes > 0;
2592
+ }
2593
+ releaseProjectMutationLease(projectRoot, mutationKind, owner) {
2594
+ return this.db.prepare('DELETE FROM devflow_project_mutation_leases WHERE project_root = ? AND mutation_kind = ? AND owner = ?')
2595
+ .run(projectRoot, mutationKind, owner).changes > 0;
2596
+ }
2597
+ cancelStaleWork(input) {
2598
+ const current = this.getWorkById(input.id);
2599
+ const stale = current && current.state === 'leased' && current.leaseOwner === input.owner
2600
+ && ((input.sourceHash !== undefined && current.sourceHash !== input.sourceHash)
2601
+ || (input.taskSpecHash !== undefined && current.taskSpecHash !== input.taskSpecHash));
2602
+ if (!stale)
2603
+ return false;
2604
+ const now = input.now ?? Date.now();
2605
+ try {
2606
+ return this.db.prepare(`UPDATE devflow_work_items SET state = 'cancelled', lease_owner = NULL, lease_expires_at = NULL, cancelled_at = ?, cancellation_reason = ?, updated_at = ? WHERE id = ? AND state = 'leased' AND lease_owner = ?`)
2607
+ .run(now, input.reason ?? 'stale_source_or_task_spec', now, input.id, input.owner).changes === 1;
2608
+ }
2609
+ catch {
2610
+ return this.db.prepare(`UPDATE devflow_work_items SET state = 'completed', lease_owner = NULL, lease_expires_at = NULL, cancelled_at = ?, cancellation_reason = ?, updated_at = ? WHERE id = ? AND state = 'leased' AND lease_owner = ?`)
2611
+ .run(now, input.reason ?? 'stale_source_or_task_spec', now, input.id, input.owner).changes === 1;
2612
+ }
2613
+ }
2614
+ replayDeadLetterWork(id, input) {
2615
+ const original = this.getWorkById(id);
2616
+ if (!original || original.state !== 'dead_letter')
2617
+ return null;
2618
+ const replay = this.enqueueWork({ idempotencyKey: input.idempotencyKey, kind: original.kind, projectRoot: original.projectRoot, sessionId: original.sessionId, turnId: original.turnId, payload: original.payload, sourceHash: input.sourceHash ?? original.sourceHash, taskSpecHash: input.taskSpecHash ?? original.taskSpecHash, maxAttempts: original.maxAttempts, nextAttemptAt: input.now ?? Date.now() });
2619
+ this.db.prepare('UPDATE devflow_work_items SET replay_of_id = ?, replay_count = replay_count + 1 WHERE id = ?').run(original.id, replay.id);
2620
+ return this.getWorkById(replay.id);
2621
+ }
2432
2622
  requestSessionClosure(input) {
2433
2623
  if (!input.sessionId.trim())
2434
2624
  throw new Error('Session closure requires a session ID');
@@ -2457,7 +2647,8 @@ class DevFlowDatabase {
2457
2647
  SELECT COUNT(*) AS count
2458
2648
  FROM devflow_work_items
2459
2649
  WHERE project_root = ? AND session_id = ?
2460
- AND state IN ('pending', 'leased', 'failed', 'dead_letter')
2650
+ AND state IN ('pending', 'leased', 'failed')
2651
+ AND attempts < max_attempts
2461
2652
  `).get(input.projectRoot, input.sessionId);
2462
2653
  const pendingWorkCount = Number(pending?.count ?? 0);
2463
2654
  this.db.prepare(`
@@ -2518,6 +2709,8 @@ class DevFlowDatabase {
2518
2709
  }
2519
2710
  if (input.kinds?.length === 0)
2520
2711
  return [];
2712
+ if (input.workItemIds?.length === 0)
2713
+ return [];
2521
2714
  const now = input.now ?? Date.now();
2522
2715
  const leaseExpiresAt = now + input.leaseMs;
2523
2716
  if (!Number.isSafeInteger(now) || !Number.isSafeInteger(leaseExpiresAt)) {
@@ -2525,9 +2718,13 @@ class DevFlowDatabase {
2525
2718
  }
2526
2719
  const limit = Math.min(Math.floor(input.limit), 1000);
2527
2720
  const kinds = input.kinds ? [...new Set(input.kinds)] : undefined;
2721
+ const workItemIds = input.workItemIds ? [...new Set(input.workItemIds)] : undefined;
2528
2722
  const kindClause = kinds
2529
2723
  ? `AND kind IN (${kinds.map(() => '?').join(', ')})`
2530
2724
  : '';
2725
+ const workItemClause = workItemIds
2726
+ ? `AND id IN (${workItemIds.map(() => '?').join(', ')})`
2727
+ : '';
2531
2728
  this.db.exec('BEGIN IMMEDIATE');
2532
2729
  try {
2533
2730
  const candidates = this.db.prepare(`
@@ -2540,9 +2737,10 @@ class DevFlowDatabase {
2540
2737
  AND next_attempt_at <= ?
2541
2738
  AND attempts < max_attempts
2542
2739
  ${kindClause}
2740
+ ${workItemClause}
2543
2741
  ORDER BY next_attempt_at ASC, created_at ASC, id ASC
2544
2742
  LIMIT ?
2545
- `).all(input.projectRoot, now, ...(kinds ?? []), limit);
2743
+ `).all(input.projectRoot, now, ...(kinds ?? []), ...(workItemIds ?? []), limit);
2546
2744
  const leased = [];
2547
2745
  for (const candidate of candidates) {
2548
2746
  const result = this.db.prepare(`
@@ -2672,8 +2870,10 @@ class DevFlowDatabase {
2672
2870
  projectRoot: row.project_root,
2673
2871
  sessionId: row.session_id ?? undefined,
2674
2872
  turnId: row.turn_id ?? undefined,
2873
+ sourceHash: row.source_hash ?? undefined,
2874
+ taskSpecHash: row.task_spec_hash ?? undefined,
2675
2875
  payload: parseJson(row.payload),
2676
- state: row.state,
2876
+ state: row.cancelled_at != null ? 'cancelled' : row.state,
2677
2877
  attempts: Number(row.attempts),
2678
2878
  maxAttempts: Number(row.max_attempts),
2679
2879
  leaseOwner: row.lease_owner ?? undefined,
@@ -2684,6 +2884,10 @@ class DevFlowDatabase {
2684
2884
  createdAt: Number(row.created_at),
2685
2885
  updatedAt: Number(row.updated_at),
2686
2886
  completedAt: row.completed_at == null ? undefined : Number(row.completed_at),
2887
+ cancelledAt: row.cancelled_at == null ? undefined : Number(row.cancelled_at),
2888
+ cancellationReason: row.cancellation_reason ?? undefined,
2889
+ replayOfId: row.replay_of_id ?? undefined,
2890
+ replayCount: Number(row.replay_count ?? 0),
2687
2891
  };
2688
2892
  }
2689
2893
  mapSessionClosure(row) {
@@ -2714,28 +2918,11 @@ class DevFlowDatabase {
2714
2918
  SELECT COUNT(*) AS count
2715
2919
  FROM devflow_work_items
2716
2920
  WHERE project_root = ? AND session_id = ?
2717
- AND state IN ('pending', 'leased', 'failed', 'dead_letter')
2921
+ AND state IN ('pending', 'leased', 'failed')
2922
+ AND attempts < max_attempts
2718
2923
  AND (? IS NULL OR id <> ?)
2719
2924
  `).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
2720
- const obligations = this.db.prepare(`
2721
- SELECT COUNT(*) AS count
2722
- FROM devflow_session_obligations
2723
- WHERE project_root = ? AND session_id = ?
2724
- AND state IN ('open', 'degraded')
2725
- `).get(projectRoot, sessionId);
2726
- const legacyTurns = this.db.prepare(`
2727
- SELECT COUNT(*) AS count
2728
- FROM devflow_memory_turns t
2729
- WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
2730
- AND NOT EXISTS (
2731
- SELECT 1 FROM devflow_session_obligations o
2732
- WHERE o.project_root = t.project_root AND o.session_id = t.session_id
2733
- AND o.obligation_id = 'memory:' || t.turn_id
2734
- )
2735
- `).get(projectRoot, sessionId);
2736
- return Number(work?.count ?? 0)
2737
- + Number(obligations?.count ?? 0)
2738
- + Number(legacyTurns?.count ?? 0);
2925
+ return Number(work?.count ?? 0);
2739
2926
  }
2740
2927
  // ---- Hook Lifecycle ----
2741
2928
  getHookReceipt(projectRoot) {
@@ -3041,6 +3228,33 @@ class DevFlowDatabase {
3041
3228
  requestId: row.request_id ?? undefined,
3042
3229
  } : null;
3043
3230
  }
3231
+ getContextReceiptForRequest(projectRoot, sessionId, requestId) {
3232
+ const row = this.db.prepare(`
3233
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
3234
+ selected_files, memory_ids, canonical_next_action, canonical_action_json,
3235
+ action_attempts, action_satisfied_at, action_degradation_json, request_id
3236
+ FROM devflow_context_receipts
3237
+ WHERE project_root = ? AND session_id = ? AND request_id = ?
3238
+ ORDER BY issued_at DESC
3239
+ LIMIT 1
3240
+ `).get(projectRoot, sessionId, requestId);
3241
+ return row ? {
3242
+ projectRoot: row.project_root,
3243
+ sessionId: row.session_id,
3244
+ executionId: row.execution_id,
3245
+ contextHash: row.context_hash,
3246
+ issuedAt: row.issued_at,
3247
+ expiresAt: row.expires_at,
3248
+ selectedFiles: parseJsonStringArray(row.selected_files),
3249
+ memoryIds: parseJsonStringArray(row.memory_ids),
3250
+ canonicalNextAction: row.canonical_next_action ?? undefined,
3251
+ canonicalAction: parseJsonObject(row.canonical_action_json),
3252
+ actionAttempts: parseJsonArray(row.action_attempts),
3253
+ actionSatisfiedAt: row.action_satisfied_at ?? undefined,
3254
+ actionDegradation: parseJsonObject(row.action_degradation_json),
3255
+ requestId: row.request_id ?? undefined,
3256
+ } : null;
3257
+ }
3044
3258
  recordContextSelectionEvent(event) {
3045
3259
  return this.db.prepare(`
3046
3260
  INSERT OR IGNORE INTO devflow_context_selection_events
@@ -3061,8 +3275,9 @@ class DevFlowDatabase {
3061
3275
  id, project_root, session_id, execution_id, turn_id, request_id, context_receipt,
3062
3276
  source_type, source_id, stage, rank, raw_score, normalized_score, final_score,
3063
3277
  applicability, reason, tool_evidence, verification_receipt, payload, created_at
3064
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3065
- `).run(event.id, event.projectRoot, event.sessionId, event.executionId ?? null, event.turnId ?? null, event.requestId, event.contextReceipt, event.sourceType, event.sourceId, event.stage, event.rank ?? null, event.rawScore ?? null, event.normalizedScore ?? null, event.finalScore ?? null, JSON.stringify(event.applicability), event.reason ?? null, JSON.stringify(event.toolEvidence), event.verificationReceipt ?? null, JSON.stringify(event.payload), event.createdAt).changes > 0;
3278
+ , schema_version, task_spec_hash, actor, source_version, source_content_hash, evidence_ids, reason_code
3279
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3280
+ `).run(event.id, event.projectRoot, event.sessionId, event.executionId ?? null, event.turnId ?? null, event.requestId, event.contextReceipt, event.sourceType, event.sourceId, event.stage, event.rank ?? null, event.rawScore ?? null, event.normalizedScore ?? null, event.finalScore ?? null, JSON.stringify(event.applicability), event.reason ?? null, JSON.stringify(event.toolEvidence), event.verificationReceipt ?? null, JSON.stringify(event.payload), event.createdAt, event.schemaVersion ?? 'retrieval-ledger-event.v1', event.taskSpecHash ?? null, event.actor ?? null, event.sourceVersion ?? null, event.sourceContentHash ?? null, JSON.stringify(event.evidenceIds ?? []), event.reasonCode ?? null).changes > 0;
3066
3281
  }
3067
3282
  appendTaskIntentArtifact(record) {
3068
3283
  (0, task_semantic_control_1.assertTaskIdentity)(record);
@@ -3252,6 +3467,98 @@ class DevFlowDatabase {
3252
3467
  `).run(record.receiptId, record.projectRoot, record.projectId, record.hostId, record.sessionId, record.turnId, record.requestId, record.executionId ?? null, record.sequence, record.fromState ?? null, record.toState, record.sourceReceiptId ?? null, record.reason, (0, task_semantic_control_1.stableSemanticJson)(record.payload), record.createdAt);
3253
3468
  return this.getTerminalTransition(record.receiptId);
3254
3469
  }
3470
+ appendTaskRuntimeEvent(event) {
3471
+ const existing = this.getTaskRuntimeEvent(event.eventId);
3472
+ if (existing) {
3473
+ if ((0, task_runtime_1.stableTaskRuntimeJson)(existing) !== (0, task_runtime_1.stableTaskRuntimeJson)(event)) {
3474
+ throw new Error(`TASK_RUNTIME_EVENT_CONFLICT:${event.eventId}`);
3475
+ }
3476
+ return existing;
3477
+ }
3478
+ const atSequence = this.db.prepare(`
3479
+ SELECT event_id FROM devflow_task_runtime_events
3480
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND sequence = ?
3481
+ `).get(event.identity.projectRoot, event.identity.sessionId, event.identity.turnId, event.sequence);
3482
+ if (atSequence)
3483
+ throw new Error(`TASK_RUNTIME_SEQUENCE_CONFLICT:${event.sequence}`);
3484
+ try {
3485
+ this.db.prepare(`
3486
+ INSERT INTO devflow_task_runtime_events (
3487
+ event_id, schema_version, producer, producer_version, project_root, project_id,
3488
+ host_id, session_id, turn_id, request_id, execution_id, task_spec_hash,
3489
+ sequence, kind, payload_json, created_at
3490
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3491
+ `).run(event.eventId, event.schemaVersion, event.producer, event.producerVersion, event.identity.projectRoot, event.identity.projectId, event.identity.hostId, event.identity.sessionId, event.identity.turnId, event.identity.requestId, event.identity.executionId ?? null, event.taskSpecHash, event.sequence, event.kind, (0, task_runtime_1.stableTaskRuntimeJson)(event.payload), event.createdAt);
3492
+ }
3493
+ catch (error) {
3494
+ const concurrentEvent = this.getTaskRuntimeEvent(event.eventId);
3495
+ if (concurrentEvent) {
3496
+ if ((0, task_runtime_1.stableTaskRuntimeJson)(concurrentEvent) === (0, task_runtime_1.stableTaskRuntimeJson)(event))
3497
+ return concurrentEvent;
3498
+ throw new Error(`TASK_RUNTIME_EVENT_CONFLICT:${event.eventId}`);
3499
+ }
3500
+ const concurrentSequence = this.db.prepare(`
3501
+ SELECT event_id FROM devflow_task_runtime_events
3502
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND sequence = ?
3503
+ `).get(event.identity.projectRoot, event.identity.sessionId, event.identity.turnId, event.sequence);
3504
+ if (concurrentSequence)
3505
+ throw new Error(`TASK_RUNTIME_SEQUENCE_CONFLICT:${event.sequence}`);
3506
+ throw error;
3507
+ }
3508
+ return this.getTaskRuntimeEvent(event.eventId);
3509
+ }
3510
+ getTaskRuntimeEvent(eventId) {
3511
+ const row = this.db.prepare('SELECT * FROM devflow_task_runtime_events WHERE event_id = ?')
3512
+ .get(eventId);
3513
+ return row ? (0, task_runtime_1.mapTaskRuntimeEventRow)(row) : null;
3514
+ }
3515
+ listTaskRuntimeEvents(projectRoot, sessionId, turnId) {
3516
+ return this.db.prepare(`
3517
+ SELECT * FROM devflow_task_runtime_events
3518
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
3519
+ ORDER BY sequence ASC
3520
+ `).all(projectRoot, sessionId, turnId)
3521
+ .map(task_runtime_1.mapTaskRuntimeEventRow);
3522
+ }
3523
+ putTaskRuntimeSnapshot(snapshot) {
3524
+ const current = this.getTaskRuntimeSnapshot(snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId);
3525
+ if (current && current.lastEventSequence > snapshot.lastEventSequence) {
3526
+ throw new Error('TASK_RUNTIME_SNAPSHOT_REGRESSION');
3527
+ }
3528
+ this.db.prepare(`
3529
+ INSERT INTO devflow_task_runtime_snapshots (
3530
+ project_root, session_id, turn_id, last_event_sequence, schema_version,
3531
+ snapshot_json, snapshot_hash, updated_at
3532
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3533
+ ON CONFLICT(project_root, session_id, turn_id) DO UPDATE SET
3534
+ last_event_sequence = excluded.last_event_sequence,
3535
+ schema_version = excluded.schema_version,
3536
+ snapshot_json = excluded.snapshot_json,
3537
+ snapshot_hash = excluded.snapshot_hash,
3538
+ updated_at = excluded.updated_at
3539
+ WHERE excluded.last_event_sequence >= devflow_task_runtime_snapshots.last_event_sequence
3540
+ `).run(snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId, snapshot.lastEventSequence, snapshot.schemaVersion, (0, task_runtime_1.stableTaskRuntimeJson)(snapshot), snapshot.snapshotHash, snapshot.updatedAt);
3541
+ return this.getTaskRuntimeSnapshot(snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId);
3542
+ }
3543
+ appendTaskRuntimeEventAndSnapshot(event, snapshot) {
3544
+ return this.db.transaction(() => {
3545
+ this.appendTaskRuntimeEvent(event);
3546
+ return this.putTaskRuntimeSnapshot(snapshot);
3547
+ });
3548
+ }
3549
+ getTaskRuntimeSnapshot(projectRoot, sessionId, turnId) {
3550
+ const row = this.db.prepare(`
3551
+ SELECT * FROM devflow_task_runtime_snapshots
3552
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
3553
+ `).get(projectRoot, sessionId, turnId);
3554
+ return row ? (0, task_runtime_1.mapTaskRuntimeSnapshotRow)(row) : null;
3555
+ }
3556
+ deleteTaskRuntimeSnapshot(projectRoot, sessionId, turnId) {
3557
+ return this.db.prepare(`
3558
+ DELETE FROM devflow_task_runtime_snapshots
3559
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
3560
+ `).run(projectRoot, sessionId, turnId).changes > 0;
3561
+ }
3255
3562
  getTerminalTransition(receiptId) {
3256
3563
  const row = this.db.prepare(`
3257
3564
  SELECT * FROM devflow_terminal_transitions WHERE receipt_id = ?
@@ -3307,7 +3614,11 @@ class DevFlowDatabase {
3307
3614
  id: row.id, projectRoot: row.project_root, sessionId: row.session_id,
3308
3615
  executionId: row.execution_id ?? undefined, turnId: row.turn_id ?? undefined,
3309
3616
  requestId: row.request_id, contextReceipt: row.context_receipt,
3310
- sourceType: row.source_type, sourceId: row.source_id, stage: row.stage,
3617
+ schemaVersion: row.schema_version ?? 'retrieval-ledger-event.v1',
3618
+ sourceType: row.source_type, sourceId: row.source_id, stage: row.stage === 'adopted' ? 'consumed' : row.stage,
3619
+ taskSpecHash: row.task_spec_hash ?? undefined, actor: row.actor ?? undefined,
3620
+ sourceVersion: row.source_version ?? undefined, sourceContentHash: row.source_content_hash ?? undefined,
3621
+ evidenceIds: parseJsonStringArray(row.evidence_ids), reasonCode: row.reason_code ?? undefined,
3311
3622
  rank: row.rank ?? undefined, rawScore: row.raw_score ?? undefined,
3312
3623
  normalizedScore: row.normalized_score ?? undefined, finalScore: row.final_score ?? undefined,
3313
3624
  applicability: parseJsonStringArray(row.applicability), reason: row.reason ?? undefined,
@@ -3459,6 +3770,15 @@ class DevFlowDatabase {
3459
3770
  if (!existing)
3460
3771
  throw new Error(`Session obligation ${input.obligationId} does not exist`);
3461
3772
  if (existing.state !== 'open') {
3773
+ if (existing.state === 'degraded' && input.state === 'satisfied' && input.receiptId) {
3774
+ const resolvedAt = input.resolvedAt ?? Date.now();
3775
+ this.db.prepare(`
3776
+ UPDATE devflow_session_obligations
3777
+ SET state = 'satisfied', receipt_id = ?, reason = ?, resolved_at = ?, updated_at = ?
3778
+ WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'degraded'
3779
+ `).run(input.receiptId, input.reason ?? 'superseded_by_corrected_evidence', resolvedAt, resolvedAt, input.projectRoot, input.sessionId, input.obligationId);
3780
+ return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
3781
+ }
3462
3782
  const sameResolution = existing.state === input.state
3463
3783
  && (input.receiptId === undefined || existing.receiptId === input.receiptId);
3464
3784
  if (sameResolution)
@@ -3517,14 +3837,27 @@ class DevFlowDatabase {
3517
3837
  });
3518
3838
  return turn;
3519
3839
  }
3840
+ updateCommittedMemoryTurnProjection(input) {
3841
+ const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
3842
+ this.db.prepare(`
3843
+ UPDATE devflow_memory_turns
3844
+ SET memory_ids = ?, reason = COALESCE(?, reason)
3845
+ WHERE turn_id = ? AND status = 'committed' AND source = 'explicit_intent'
3846
+ `).run(JSON.stringify([...new Set(input.memoryIds)]), input.reason ?? null, turnId);
3847
+ const turn = this.getMemoryTurn(turnId);
3848
+ if (!turn || turn.status !== 'committed') {
3849
+ throw new Error(`Committed explicit memory turn ${turnId} does not exist`);
3850
+ }
3851
+ return turn;
3852
+ }
3520
3853
  skipMemoryTurn(input) {
3521
3854
  const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
3522
3855
  this.db.prepare(`
3523
3856
  UPDATE devflow_memory_turns
3524
- SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
3857
+ SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = ?,
3525
3858
  reason = ?, decided_at = ?
3526
3859
  WHERE turn_id = ? AND status = 'pending'
3527
- `).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(), turnId);
3860
+ `).run(input.receiptId, input.source ?? 'host_skip', input.reason, input.decidedAt ?? Date.now(), turnId);
3528
3861
  const turn = this.getMemoryTurn(turnId);
3529
3862
  if (!turn || turn.status !== 'skipped') {
3530
3863
  throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
1
+ export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, openGlobalDevFlowReadOnlyDatabase, } from './database';
2
2
  export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, ContextReceiptRecord, ContextSelectionEventRecord, PolicyVerificationBaselineRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
3
- export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, } from './work-queue';
3
+ export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, ProjectMutationLeaseRecord, } from './work-queue';
4
4
  export type { RetrievalLedgerEventRecord, RetrievalLedgerSourceType, RetrievalLedgerStage, } from './retrieval-ledger';
5
5
  export { hostActionReportsEqual, isHostActionState, mapHostActionRow, serializeHostActionReport, } from './host-actions';
6
6
  export { RETRIEVAL_MAX_CYCLES, isRetrievalSessionState } from './retrieval-sessions';
@@ -8,6 +8,8 @@ export type { AppendRetrievalCycleInput, CreateRetrievalSessionInput, RetrievalC
8
8
  export type { FailHostActionInput, HostActionRecord, HostActionState, ReportHostActionInput, RequestHostActionInput, StartHostActionInput, VerifyHostActionInput, } from './host-actions';
9
9
  export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
10
10
  export { assertTaskIdentity, mapChannelQueryPlanRow, mapTaskIntentArtifactRow, mapTerminalTransitionRow, mapToolNameResolutionRow, mapTranscriptCheckpointRow, stableSemanticJson, } from './task-semantic-control';
11
+ export { mapTaskRuntimeEventRow, mapTaskRuntimeSnapshotRow, stableTaskRuntimeHash, stableTaskRuntimeJson, } from './task-runtime';
12
+ export type { TaskRuntimeChannelRecord, TaskRuntimeChannelRequirement, TaskRuntimeChannelStatus, TaskRuntimeEventRecord, TaskRuntimeIdentityRecord, TaskRuntimeObligationRecord, TaskRuntimeObligationState, TaskRuntimeSnapshotRecord, } from './task-runtime';
11
13
  export type { ChannelQueryPlanRecord, TaskIdentityRecord, TaskIntentArtifactRecord, TerminalTransitionRecord, ToolNameResolutionRecord, ToolNameResolutionStatus, TranscriptCheckpointRecord, } from './task-semantic-control';
12
14
  export { LEGAL_WORKFLOW_WORKER_TRANSITIONS, TERMINAL_WORKFLOW_WORKER_STATES, mapWorkflowMergeRow, mapWorkflowWorkerEventRow, mapWorkflowWorkerRow, } from './workflow-workers';
13
15
  export type { CreateWorkflowWorkerInput, EnqueueWorkflowMergeInput, TransitionWorkflowWorkerInput, WorkflowMergeRecord, WorkflowMergeState, WorkflowWorkerEventRecord, WorkflowWorkerRecord, WorkflowWorkerState, } from './workflow-workers';