@devflow-tools/database 0.17.0 → 0.17.2

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");
@@ -13,6 +14,7 @@ const host_actions_1 = require("./host-actions");
13
14
  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");
17
+ const task_semantic_control_1 = require("./task-semantic-control");
16
18
  const CONTEXT_REQUIRED_SKILLS = new Set([
17
19
  'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
18
20
  ]);
@@ -61,28 +63,59 @@ function openGlobalDevFlowDatabase(home = (0, os_1.homedir)(), options) {
61
63
  return new DevFlowDatabase(home, {
62
64
  dbPath: getGlobalDevFlowDbPath(home),
63
65
  busyTimeoutMs: options?.busyTimeoutMs,
66
+ readonly: options?.readonly,
64
67
  });
65
68
  }
69
+ function openGlobalDevFlowReadOnlyDatabase(home = (0, os_1.homedir)(), options) {
70
+ return openGlobalDevFlowDatabase(home, { ...options, readonly: true });
71
+ }
72
+ // Schema setup is process-owned. Hook daemons open short-lived connections for
73
+ // bounded transactions, but replaying the full idempotent DDL on every open is
74
+ // still expensive and serializes concurrent host requests. An inode identity
75
+ // keeps the cache safe when a test, repair, or user replaces the database file.
76
+ const initializedDatabaseFiles = new Map();
77
+ function getDatabaseIdentity(path) {
78
+ try {
79
+ const stats = (0, fs_1.statSync)(path);
80
+ return { dev: stats.dev, ino: stats.ino };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
66
86
  class DevFlowDatabase {
67
87
  constructor(projectRoot, opts) {
68
88
  let dbPath;
69
89
  if (opts?.dbPath) {
70
90
  const dir = (0, path_1.dirname)(opts.dbPath);
71
- if (!(0, fs_1.existsSync)(dir))
91
+ if (!(0, fs_1.existsSync)(dir) && !opts.readonly)
72
92
  (0, fs_1.mkdirSync)(dir, { recursive: true });
73
93
  dbPath = opts.dbPath;
74
94
  }
75
95
  else {
76
96
  const devflowDir = (0, path_1.join)(projectRoot, '.devflow');
77
- if (!(0, fs_1.existsSync)(devflowDir))
97
+ if (!(0, fs_1.existsSync)(devflowDir) && !opts?.readonly)
78
98
  (0, fs_1.mkdirSync)(devflowDir, { recursive: true });
79
99
  dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
80
100
  }
81
- this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
82
- this.db.exec('PRAGMA journal_mode = WAL');
101
+ if (opts?.readonly && !(0, fs_1.existsSync)(dbPath))
102
+ throw new Error(`DEVFLOW_DATABASE_NOT_FOUND:${dbPath}`);
103
+ this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs, opts?.readonly === true);
83
104
  this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
105
+ if (opts?.readonly)
106
+ return;
84
107
  this.db.exec('PRAGMA foreign_keys = OFF');
85
- this.initializeSchema();
108
+ const databaseIdentity = getDatabaseIdentity(dbPath);
109
+ const initializedIdentity = initializedDatabaseFiles.get(dbPath);
110
+ const schemaInitialized = databaseIdentity !== null
111
+ && initializedIdentity?.dev === databaseIdentity.dev
112
+ && initializedIdentity.ino === databaseIdentity.ino;
113
+ if (!schemaInitialized) {
114
+ this.initializeSchema();
115
+ const currentIdentity = getDatabaseIdentity(dbPath);
116
+ if (currentIdentity)
117
+ initializedDatabaseFiles.set(dbPath, currentIdentity);
118
+ }
86
119
  }
87
120
  initializeSchema() {
88
121
  this.db.exec(`
@@ -329,6 +362,119 @@ class DevFlowDatabase {
329
362
  project_root, session_id, execution_id, request_id, context_receipt, source_type, source_id, created_at
330
363
  );
331
364
 
365
+ CREATE TABLE IF NOT EXISTS devflow_task_intent_artifacts (
366
+ artifact_hash TEXT PRIMARY KEY,
367
+ project_root TEXT NOT NULL,
368
+ project_id TEXT NOT NULL,
369
+ host_id TEXT NOT NULL,
370
+ session_id TEXT NOT NULL,
371
+ turn_id TEXT NOT NULL,
372
+ request_id TEXT NOT NULL,
373
+ execution_id TEXT,
374
+ version INTEGER NOT NULL CHECK(version > 0),
375
+ supersedes_hash TEXT,
376
+ raw_prompt TEXT NOT NULL,
377
+ normalized_prompt TEXT NOT NULL,
378
+ command TEXT,
379
+ slash_args_json TEXT NOT NULL DEFAULT '[]',
380
+ active_skill TEXT,
381
+ intent TEXT NOT NULL,
382
+ action TEXT NOT NULL,
383
+ entities_json TEXT NOT NULL DEFAULT '[]',
384
+ target_anchors_json TEXT NOT NULL DEFAULT '[]',
385
+ policy_constraints_json TEXT NOT NULL DEFAULT '[]',
386
+ classification_evidence_json TEXT NOT NULL DEFAULT '[]',
387
+ source_event_ids_json TEXT NOT NULL DEFAULT '[]',
388
+ source_hash TEXT NOT NULL,
389
+ created_at INTEGER NOT NULL,
390
+ UNIQUE(project_root, session_id, turn_id, version),
391
+ UNIQUE(project_root, session_id, request_id, version)
392
+ );
393
+ CREATE INDEX IF NOT EXISTS idx_task_intent_active
394
+ ON devflow_task_intent_artifacts(project_root, session_id, turn_id, version DESC);
395
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_task_intent_source
396
+ ON devflow_task_intent_artifacts(project_root, session_id, source_hash, version);
397
+
398
+ CREATE TABLE IF NOT EXISTS devflow_channel_query_plans (
399
+ plan_hash TEXT PRIMARY KEY,
400
+ source_intent_hash TEXT NOT NULL,
401
+ project_root TEXT NOT NULL,
402
+ project_id TEXT NOT NULL,
403
+ host_id TEXT NOT NULL,
404
+ session_id TEXT NOT NULL,
405
+ turn_id TEXT NOT NULL,
406
+ request_id TEXT NOT NULL,
407
+ execution_id TEXT,
408
+ code_queries_json TEXT NOT NULL DEFAULT '[]',
409
+ memory_queries_json TEXT NOT NULL DEFAULT '[]',
410
+ knowledge_queries_json TEXT NOT NULL DEFAULT '[]',
411
+ generated_by TEXT NOT NULL
412
+ CHECK(generated_by IN ('deterministic', 'host_semantic', 'hybrid')),
413
+ degradation_json TEXT,
414
+ created_at INTEGER NOT NULL,
415
+ UNIQUE(project_root, session_id, turn_id, source_intent_hash)
416
+ );
417
+ CREATE INDEX IF NOT EXISTS idx_channel_query_plan_request
418
+ ON devflow_channel_query_plans(project_root, session_id, request_id, created_at DESC);
419
+
420
+ CREATE TABLE IF NOT EXISTS devflow_tool_name_resolutions (
421
+ id TEXT PRIMARY KEY,
422
+ source_hash TEXT NOT NULL UNIQUE,
423
+ project_root TEXT NOT NULL,
424
+ project_id TEXT NOT NULL,
425
+ host_id TEXT NOT NULL,
426
+ session_id TEXT NOT NULL,
427
+ turn_id TEXT NOT NULL,
428
+ request_id TEXT NOT NULL,
429
+ execution_id TEXT,
430
+ requested_name TEXT NOT NULL,
431
+ canonical_name TEXT,
432
+ projected_name TEXT,
433
+ status TEXT NOT NULL CHECK(status IN ('canonical', 'alias', 'unsupported')),
434
+ attempt INTEGER NOT NULL CHECK(attempt >= 0),
435
+ capability_mechanism TEXT,
436
+ reason TEXT,
437
+ created_at INTEGER NOT NULL
438
+ );
439
+ CREATE INDEX IF NOT EXISTS idx_tool_resolution_request
440
+ ON devflow_tool_name_resolutions(project_root, session_id, request_id, created_at);
441
+
442
+ CREATE TABLE IF NOT EXISTS devflow_terminal_transitions (
443
+ receipt_id TEXT PRIMARY KEY,
444
+ project_root TEXT NOT NULL,
445
+ project_id TEXT NOT NULL,
446
+ host_id TEXT NOT NULL,
447
+ session_id TEXT NOT NULL,
448
+ turn_id TEXT NOT NULL,
449
+ request_id TEXT NOT NULL,
450
+ execution_id TEXT,
451
+ sequence INTEGER NOT NULL CHECK(sequence > 0),
452
+ from_state TEXT,
453
+ to_state TEXT NOT NULL,
454
+ source_receipt_id TEXT,
455
+ reason TEXT NOT NULL,
456
+ payload_json TEXT NOT NULL DEFAULT '{}',
457
+ created_at INTEGER NOT NULL,
458
+ UNIQUE(project_root, session_id, turn_id, sequence)
459
+ );
460
+ CREATE INDEX IF NOT EXISTS idx_terminal_transition_turn
461
+ ON devflow_terminal_transitions(project_root, session_id, turn_id, sequence);
462
+
463
+ CREATE TABLE IF NOT EXISTS devflow_transcript_checkpoints (
464
+ id TEXT PRIMARY KEY,
465
+ project_root TEXT NOT NULL,
466
+ host_id TEXT NOT NULL,
467
+ session_id TEXT NOT NULL,
468
+ source_path TEXT NOT NULL,
469
+ source_hash TEXT NOT NULL,
470
+ byte_offset INTEGER NOT NULL CHECK(byte_offset >= 0),
471
+ event_count INTEGER NOT NULL CHECK(event_count >= 0),
472
+ created_at INTEGER NOT NULL,
473
+ UNIQUE(project_root, host_id, session_id, source_path, source_hash)
474
+ );
475
+ CREATE INDEX IF NOT EXISTS idx_transcript_checkpoint_latest
476
+ ON devflow_transcript_checkpoints(project_root, host_id, session_id, source_path, created_at DESC);
477
+
332
478
  CREATE TABLE IF NOT EXISTS devflow_policy_verification_baselines (
333
479
  project_root TEXT NOT NULL,
334
480
  kind TEXT NOT NULL,
@@ -1180,14 +1326,20 @@ class DevFlowDatabase {
1180
1326
  }
1181
1327
  getToolCallEventByToolUseId(sessionId, toolUseId) {
1182
1328
  const row = this.db.prepare(`
1183
- SELECT event_id, timestamp, duration
1329
+ SELECT event_id, execution_id, timestamp, duration, input
1184
1330
  FROM tool_call_events
1185
1331
  WHERE session_id = ? AND tool_use_id = ?
1186
1332
  ORDER BY timestamp DESC
1187
1333
  LIMIT 1
1188
1334
  `).get(sessionId, toolUseId);
1189
1335
  return row
1190
- ? { eventId: row.event_id, timestamp: row.timestamp, duration: row.duration ?? 0 }
1336
+ ? {
1337
+ eventId: row.event_id,
1338
+ executionId: row.execution_id,
1339
+ timestamp: row.timestamp,
1340
+ duration: row.duration ?? 0,
1341
+ input: parseJsonObject(row.input) ?? {},
1342
+ }
1191
1343
  : null;
1192
1344
  }
1193
1345
  listToolCallEventsBySessions(sessionIds) {
@@ -2343,7 +2495,8 @@ class DevFlowDatabase {
2343
2495
  SELECT COUNT(*) AS count
2344
2496
  FROM devflow_work_items
2345
2497
  WHERE project_root = ? AND session_id = ?
2346
- AND state IN ('pending', 'leased', 'failed', 'dead_letter')
2498
+ AND state IN ('pending', 'leased', 'failed')
2499
+ AND attempts < max_attempts
2347
2500
  `).get(input.projectRoot, input.sessionId);
2348
2501
  const pendingWorkCount = Number(pending?.count ?? 0);
2349
2502
  this.db.prepare(`
@@ -2404,6 +2557,8 @@ class DevFlowDatabase {
2404
2557
  }
2405
2558
  if (input.kinds?.length === 0)
2406
2559
  return [];
2560
+ if (input.workItemIds?.length === 0)
2561
+ return [];
2407
2562
  const now = input.now ?? Date.now();
2408
2563
  const leaseExpiresAt = now + input.leaseMs;
2409
2564
  if (!Number.isSafeInteger(now) || !Number.isSafeInteger(leaseExpiresAt)) {
@@ -2411,9 +2566,13 @@ class DevFlowDatabase {
2411
2566
  }
2412
2567
  const limit = Math.min(Math.floor(input.limit), 1000);
2413
2568
  const kinds = input.kinds ? [...new Set(input.kinds)] : undefined;
2569
+ const workItemIds = input.workItemIds ? [...new Set(input.workItemIds)] : undefined;
2414
2570
  const kindClause = kinds
2415
2571
  ? `AND kind IN (${kinds.map(() => '?').join(', ')})`
2416
2572
  : '';
2573
+ const workItemClause = workItemIds
2574
+ ? `AND id IN (${workItemIds.map(() => '?').join(', ')})`
2575
+ : '';
2417
2576
  this.db.exec('BEGIN IMMEDIATE');
2418
2577
  try {
2419
2578
  const candidates = this.db.prepare(`
@@ -2426,9 +2585,10 @@ class DevFlowDatabase {
2426
2585
  AND next_attempt_at <= ?
2427
2586
  AND attempts < max_attempts
2428
2587
  ${kindClause}
2588
+ ${workItemClause}
2429
2589
  ORDER BY next_attempt_at ASC, created_at ASC, id ASC
2430
2590
  LIMIT ?
2431
- `).all(input.projectRoot, now, ...(kinds ?? []), limit);
2591
+ `).all(input.projectRoot, now, ...(kinds ?? []), ...(workItemIds ?? []), limit);
2432
2592
  const leased = [];
2433
2593
  for (const candidate of candidates) {
2434
2594
  const result = this.db.prepare(`
@@ -2600,28 +2760,11 @@ class DevFlowDatabase {
2600
2760
  SELECT COUNT(*) AS count
2601
2761
  FROM devflow_work_items
2602
2762
  WHERE project_root = ? AND session_id = ?
2603
- AND state IN ('pending', 'leased', 'failed', 'dead_letter')
2763
+ AND state IN ('pending', 'leased', 'failed')
2764
+ AND attempts < max_attempts
2604
2765
  AND (? IS NULL OR id <> ?)
2605
2766
  `).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
2606
- const obligations = this.db.prepare(`
2607
- SELECT COUNT(*) AS count
2608
- FROM devflow_session_obligations
2609
- WHERE project_root = ? AND session_id = ?
2610
- AND state IN ('open', 'degraded')
2611
- `).get(projectRoot, sessionId);
2612
- const legacyTurns = this.db.prepare(`
2613
- SELECT COUNT(*) AS count
2614
- FROM devflow_memory_turns t
2615
- WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
2616
- AND NOT EXISTS (
2617
- SELECT 1 FROM devflow_session_obligations o
2618
- WHERE o.project_root = t.project_root AND o.session_id = t.session_id
2619
- AND o.obligation_id = 'memory:' || t.turn_id
2620
- )
2621
- `).get(projectRoot, sessionId);
2622
- return Number(work?.count ?? 0)
2623
- + Number(obligations?.count ?? 0)
2624
- + Number(legacyTurns?.count ?? 0);
2767
+ return Number(work?.count ?? 0);
2625
2768
  }
2626
2769
  // ---- Hook Lifecycle ----
2627
2770
  getHookReceipt(projectRoot) {
@@ -2950,6 +3093,232 @@ class DevFlowDatabase {
2950
3093
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2951
3094
  `).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;
2952
3095
  }
3096
+ appendTaskIntentArtifact(record) {
3097
+ (0, task_semantic_control_1.assertTaskIdentity)(record);
3098
+ if (!Number.isSafeInteger(record.version) || record.version <= 0) {
3099
+ throw new Error('TASK_INTENT_INVALID_VERSION');
3100
+ }
3101
+ const existingByHash = this.getTaskIntentArtifact(record.artifactHash);
3102
+ if (existingByHash) {
3103
+ if ((0, task_semantic_control_1.stableSemanticJson)(existingByHash) !== (0, task_semantic_control_1.stableSemanticJson)(record)) {
3104
+ throw new Error(`TASK_INTENT_HASH_CONFLICT:${record.artifactHash}`);
3105
+ }
3106
+ return existingByHash;
3107
+ }
3108
+ this.db.exec('BEGIN IMMEDIATE');
3109
+ try {
3110
+ const conflicting = this.db.prepare(`
3111
+ SELECT * FROM devflow_task_intent_artifacts
3112
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND version = ?
3113
+ `).get(record.projectRoot, record.sessionId, record.turnId, record.version);
3114
+ if (conflicting)
3115
+ throw new Error(`TASK_INTENT_VERSION_CONFLICT:${record.turnId}:${record.version}`);
3116
+ if (record.supersedesHash && !this.getTaskIntentArtifact(record.supersedesHash)) {
3117
+ throw new Error(`TASK_INTENT_SUPERSEDED_NOT_FOUND:${record.supersedesHash}`);
3118
+ }
3119
+ this.db.prepare(`
3120
+ INSERT INTO devflow_task_intent_artifacts (
3121
+ artifact_hash, project_root, project_id, host_id, session_id, turn_id, request_id,
3122
+ execution_id, version, supersedes_hash, raw_prompt, normalized_prompt, command,
3123
+ slash_args_json, active_skill, intent, action, entities_json, target_anchors_json,
3124
+ policy_constraints_json, classification_evidence_json, source_event_ids_json,
3125
+ source_hash, created_at
3126
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3127
+ `).run(record.artifactHash, record.projectRoot, record.projectId, record.hostId, record.sessionId, record.turnId, record.requestId, record.executionId ?? null, record.version, record.supersedesHash ?? null, record.rawPrompt, record.normalizedPrompt, record.command ?? null, (0, task_semantic_control_1.stableSemanticJson)(record.slashArgs), record.activeSkill ?? null, record.intent, record.action, (0, task_semantic_control_1.stableSemanticJson)(record.entities), (0, task_semantic_control_1.stableSemanticJson)(record.targetAnchors), (0, task_semantic_control_1.stableSemanticJson)(record.policyConstraints), (0, task_semantic_control_1.stableSemanticJson)(record.classificationEvidence), (0, task_semantic_control_1.stableSemanticJson)(record.sourceEventIds), record.sourceHash, record.createdAt);
3128
+ const created = this.getTaskIntentArtifact(record.artifactHash);
3129
+ this.db.exec('COMMIT');
3130
+ return created;
3131
+ }
3132
+ catch (error) {
3133
+ try {
3134
+ this.db.exec('ROLLBACK');
3135
+ }
3136
+ catch { }
3137
+ throw error;
3138
+ }
3139
+ }
3140
+ getTaskIntentArtifact(artifactHash) {
3141
+ const row = this.db.prepare(`
3142
+ SELECT * FROM devflow_task_intent_artifacts WHERE artifact_hash = ?
3143
+ `).get(artifactHash);
3144
+ return row ? (0, task_semantic_control_1.mapTaskIntentArtifactRow)(row) : null;
3145
+ }
3146
+ getLatestTaskIntentArtifact(input) {
3147
+ if (!input.turnId && !input.requestId)
3148
+ throw new Error('TASK_INTENT_LOOKUP_IDENTITY_REQUIRED');
3149
+ const predicates = ['project_root = ?', 'session_id = ?'];
3150
+ const params = [input.projectRoot, input.sessionId];
3151
+ if (input.turnId) {
3152
+ predicates.push('turn_id = ?');
3153
+ params.push(input.turnId);
3154
+ }
3155
+ if (input.requestId) {
3156
+ predicates.push('request_id = ?');
3157
+ params.push(input.requestId);
3158
+ }
3159
+ const row = this.db.prepare(`
3160
+ SELECT * FROM devflow_task_intent_artifacts
3161
+ WHERE ${predicates.join(' AND ')} ORDER BY version DESC, created_at DESC LIMIT 1
3162
+ `).get(...params);
3163
+ return row ? (0, task_semantic_control_1.mapTaskIntentArtifactRow)(row) : null;
3164
+ }
3165
+ getTaskIntentArtifactBySource(input) {
3166
+ const row = this.db.prepare(`
3167
+ SELECT * FROM devflow_task_intent_artifacts
3168
+ WHERE project_root = ? AND session_id = ? AND source_hash = ?
3169
+ ORDER BY version DESC, created_at DESC LIMIT 1
3170
+ `).get(input.projectRoot, input.sessionId, input.sourceHash);
3171
+ return row ? (0, task_semantic_control_1.mapTaskIntentArtifactRow)(row) : null;
3172
+ }
3173
+ listLatestTaskIntentArtifacts(projectRoot, sessionId) {
3174
+ return this.db.prepare(`
3175
+ SELECT artifact.* FROM devflow_task_intent_artifacts artifact
3176
+ JOIN (
3177
+ SELECT turn_id, MAX(version) AS version
3178
+ FROM devflow_task_intent_artifacts
3179
+ WHERE project_root = ? AND session_id = ? GROUP BY turn_id
3180
+ ) latest ON latest.turn_id = artifact.turn_id AND latest.version = artifact.version
3181
+ WHERE artifact.project_root = ? AND artifact.session_id = ?
3182
+ ORDER BY artifact.created_at ASC, artifact.turn_id ASC
3183
+ `).all(projectRoot, sessionId, projectRoot, sessionId)
3184
+ .map(task_semantic_control_1.mapTaskIntentArtifactRow);
3185
+ }
3186
+ appendChannelQueryPlan(record) {
3187
+ (0, task_semantic_control_1.assertTaskIdentity)(record);
3188
+ const source = this.getTaskIntentArtifact(record.sourceIntentHash);
3189
+ if (!source)
3190
+ throw new Error(`QUERY_PLAN_INTENT_NOT_FOUND:${record.sourceIntentHash}`);
3191
+ if (!sameTaskIdentity(source, record))
3192
+ throw new Error('QUERY_PLAN_IDENTITY_MISMATCH');
3193
+ const existing = this.getChannelQueryPlan(record.planHash);
3194
+ if (existing) {
3195
+ if ((0, task_semantic_control_1.stableSemanticJson)(existing) !== (0, task_semantic_control_1.stableSemanticJson)(record)) {
3196
+ throw new Error(`QUERY_PLAN_HASH_CONFLICT:${record.planHash}`);
3197
+ }
3198
+ return existing;
3199
+ }
3200
+ try {
3201
+ this.db.prepare(`
3202
+ INSERT INTO devflow_channel_query_plans (
3203
+ plan_hash, source_intent_hash, project_root, project_id, host_id, session_id,
3204
+ turn_id, request_id, execution_id, code_queries_json, memory_queries_json,
3205
+ knowledge_queries_json, generated_by, degradation_json, created_at
3206
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3207
+ `).run(record.planHash, record.sourceIntentHash, record.projectRoot, record.projectId, record.hostId, record.sessionId, record.turnId, record.requestId, record.executionId ?? null, (0, task_semantic_control_1.stableSemanticJson)(record.code), (0, task_semantic_control_1.stableSemanticJson)(record.memory), (0, task_semantic_control_1.stableSemanticJson)(record.knowledge), record.generatedBy, record.degradation ? (0, task_semantic_control_1.stableSemanticJson)(record.degradation) : null, record.createdAt);
3208
+ }
3209
+ catch (error) {
3210
+ if (String(error).includes('UNIQUE constraint failed')) {
3211
+ throw new Error(`QUERY_PLAN_SOURCE_CONFLICT:${record.sourceIntentHash}`);
3212
+ }
3213
+ throw error;
3214
+ }
3215
+ return this.getChannelQueryPlan(record.planHash);
3216
+ }
3217
+ getChannelQueryPlan(planHash) {
3218
+ const row = this.db.prepare(`
3219
+ SELECT * FROM devflow_channel_query_plans WHERE plan_hash = ?
3220
+ `).get(planHash);
3221
+ return row ? (0, task_semantic_control_1.mapChannelQueryPlanRow)(row) : null;
3222
+ }
3223
+ getChannelQueryPlanForIntent(input) {
3224
+ const row = this.db.prepare(`
3225
+ SELECT * FROM devflow_channel_query_plans
3226
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND source_intent_hash = ?
3227
+ ORDER BY created_at DESC LIMIT 1
3228
+ `).get(input.projectRoot, input.sessionId, input.turnId, input.sourceIntentHash);
3229
+ return row ? (0, task_semantic_control_1.mapChannelQueryPlanRow)(row) : null;
3230
+ }
3231
+ appendToolNameResolution(record) {
3232
+ (0, task_semantic_control_1.assertTaskIdentity)(record);
3233
+ if (!Number.isSafeInteger(record.attempt) || record.attempt < 0) {
3234
+ throw new Error('TOOL_RESOLUTION_INVALID_ATTEMPT');
3235
+ }
3236
+ return this.db.prepare(`
3237
+ INSERT OR IGNORE INTO devflow_tool_name_resolutions (
3238
+ id, source_hash, project_root, project_id, host_id, session_id, turn_id,
3239
+ request_id, execution_id, requested_name, canonical_name, projected_name,
3240
+ status, attempt, capability_mechanism, reason, created_at
3241
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3242
+ `).run(record.id, record.sourceHash, record.projectRoot, record.projectId, record.hostId, record.sessionId, record.turnId, record.requestId, record.executionId ?? null, record.requestedName, record.canonicalName ?? null, record.projectedName ?? null, record.status, record.attempt, record.capabilityMechanism ?? null, record.reason ?? null, record.createdAt).changes > 0;
3243
+ }
3244
+ listToolNameResolutions(input) {
3245
+ const predicates = ['project_root = ?', 'session_id = ?'];
3246
+ const params = [input.projectRoot, input.sessionId];
3247
+ if (input.requestId) {
3248
+ predicates.push('request_id = ?');
3249
+ params.push(input.requestId);
3250
+ }
3251
+ return this.db.prepare(`
3252
+ SELECT * FROM devflow_tool_name_resolutions WHERE ${predicates.join(' AND ')}
3253
+ ORDER BY created_at ASC, attempt ASC, id ASC
3254
+ `).all(...params).map(task_semantic_control_1.mapToolNameResolutionRow);
3255
+ }
3256
+ appendTerminalTransition(record) {
3257
+ (0, task_semantic_control_1.assertTaskIdentity)(record);
3258
+ if (!Number.isSafeInteger(record.sequence) || record.sequence <= 0) {
3259
+ throw new Error('TERMINAL_TRANSITION_INVALID_SEQUENCE');
3260
+ }
3261
+ const existing = this.getTerminalTransition(record.receiptId);
3262
+ if (existing) {
3263
+ if ((0, task_semantic_control_1.stableSemanticJson)(existing) !== (0, task_semantic_control_1.stableSemanticJson)(record)) {
3264
+ throw new Error(`TERMINAL_RECEIPT_CONFLICT:${record.receiptId}`);
3265
+ }
3266
+ return existing;
3267
+ }
3268
+ const latest = this.getLatestTerminalTransition(record.projectRoot, record.sessionId, record.turnId);
3269
+ if (record.sequence !== (latest?.sequence ?? 0) + 1) {
3270
+ throw new Error(`TERMINAL_TRANSITION_SEQUENCE:${latest?.sequence ?? 0}->${record.sequence}`);
3271
+ }
3272
+ if ((latest?.toState ?? undefined) !== record.fromState) {
3273
+ throw new Error(`TERMINAL_TRANSITION_FROM_MISMATCH:${record.fromState ?? 'none'}`);
3274
+ }
3275
+ this.db.prepare(`
3276
+ INSERT INTO devflow_terminal_transitions (
3277
+ receipt_id, project_root, project_id, host_id, session_id, turn_id, request_id,
3278
+ execution_id, sequence, from_state, to_state, source_receipt_id, reason,
3279
+ payload_json, created_at
3280
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3281
+ `).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);
3282
+ return this.getTerminalTransition(record.receiptId);
3283
+ }
3284
+ getTerminalTransition(receiptId) {
3285
+ const row = this.db.prepare(`
3286
+ SELECT * FROM devflow_terminal_transitions WHERE receipt_id = ?
3287
+ `).get(receiptId);
3288
+ return row ? (0, task_semantic_control_1.mapTerminalTransitionRow)(row) : null;
3289
+ }
3290
+ getLatestTerminalTransition(projectRoot, sessionId, turnId) {
3291
+ const row = this.db.prepare(`
3292
+ SELECT * FROM devflow_terminal_transitions
3293
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
3294
+ ORDER BY sequence DESC LIMIT 1
3295
+ `).get(projectRoot, sessionId, turnId);
3296
+ return row ? (0, task_semantic_control_1.mapTerminalTransitionRow)(row) : null;
3297
+ }
3298
+ appendTranscriptCheckpoint(record) {
3299
+ if (!record.id || !record.projectRoot || !record.hostId || !record.sessionId
3300
+ || !record.sourcePath || !record.sourceHash) {
3301
+ throw new Error('TRANSCRIPT_CHECKPOINT_INVALID_IDENTITY');
3302
+ }
3303
+ if (!Number.isSafeInteger(record.byteOffset) || record.byteOffset < 0
3304
+ || !Number.isSafeInteger(record.eventCount) || record.eventCount < 0) {
3305
+ throw new Error('TRANSCRIPT_CHECKPOINT_INVALID_POSITION');
3306
+ }
3307
+ return this.db.prepare(`
3308
+ INSERT OR IGNORE INTO devflow_transcript_checkpoints (
3309
+ id, project_root, host_id, session_id, source_path, source_hash,
3310
+ byte_offset, event_count, created_at
3311
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3312
+ `).run(record.id, record.projectRoot, record.hostId, record.sessionId, record.sourcePath, record.sourceHash, record.byteOffset, record.eventCount, record.createdAt).changes > 0;
3313
+ }
3314
+ getLatestTranscriptCheckpoint(input) {
3315
+ const row = this.db.prepare(`
3316
+ SELECT * FROM devflow_transcript_checkpoints
3317
+ WHERE project_root = ? AND host_id = ? AND session_id = ? AND source_path = ?
3318
+ ORDER BY created_at DESC, byte_offset DESC LIMIT 1
3319
+ `).get(input.projectRoot, input.hostId, input.sessionId, input.sourcePath);
3320
+ return row ? (0, task_semantic_control_1.mapTranscriptCheckpointRow)(row) : null;
3321
+ }
2953
3322
  listRetrievalLedgerEvents(options) {
2954
3323
  const predicates = ['project_root = ?'];
2955
3324
  const params = [options.projectRoot];
@@ -3119,6 +3488,15 @@ class DevFlowDatabase {
3119
3488
  if (!existing)
3120
3489
  throw new Error(`Session obligation ${input.obligationId} does not exist`);
3121
3490
  if (existing.state !== 'open') {
3491
+ if (existing.state === 'degraded' && input.state === 'satisfied' && input.receiptId) {
3492
+ const resolvedAt = input.resolvedAt ?? Date.now();
3493
+ this.db.prepare(`
3494
+ UPDATE devflow_session_obligations
3495
+ SET state = 'satisfied', receipt_id = ?, reason = ?, resolved_at = ?, updated_at = ?
3496
+ WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'degraded'
3497
+ `).run(input.receiptId, input.reason ?? 'superseded_by_corrected_evidence', resolvedAt, resolvedAt, input.projectRoot, input.sessionId, input.obligationId);
3498
+ return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
3499
+ }
3122
3500
  const sameResolution = existing.state === input.state
3123
3501
  && (input.receiptId === undefined || existing.receiptId === input.receiptId);
3124
3502
  if (sameResolution)
@@ -3177,6 +3555,19 @@ class DevFlowDatabase {
3177
3555
  });
3178
3556
  return turn;
3179
3557
  }
3558
+ updateCommittedMemoryTurnProjection(input) {
3559
+ const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
3560
+ this.db.prepare(`
3561
+ UPDATE devflow_memory_turns
3562
+ SET memory_ids = ?, reason = COALESCE(?, reason)
3563
+ WHERE turn_id = ? AND status = 'committed' AND source = 'explicit_intent'
3564
+ `).run(JSON.stringify([...new Set(input.memoryIds)]), input.reason ?? null, turnId);
3565
+ const turn = this.getMemoryTurn(turnId);
3566
+ if (!turn || turn.status !== 'committed') {
3567
+ throw new Error(`Committed explicit memory turn ${turnId} does not exist`);
3568
+ }
3569
+ return turn;
3570
+ }
3180
3571
  skipMemoryTurn(input) {
3181
3572
  const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
3182
3573
  this.db.prepare(`
@@ -3922,3 +4313,12 @@ function canonicalWorkflowProjectRoot(projectRoot) {
3922
4313
  return resolved;
3923
4314
  }
3924
4315
  }
4316
+ function sameTaskIdentity(left, right) {
4317
+ return left.projectRoot === right.projectRoot
4318
+ && left.projectId === right.projectId
4319
+ && left.hostId === right.hostId
4320
+ && left.sessionId === right.sessionId
4321
+ && left.turnId === right.turnId
4322
+ && left.requestId === right.requestId
4323
+ && left.executionId === right.executionId;
4324
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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
3
  export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, } from './work-queue';
4
4
  export type { RetrievalLedgerEventRecord, RetrievalLedgerSourceType, RetrievalLedgerStage, } from './retrieval-ledger';
@@ -7,6 +7,8 @@ export { RETRIEVAL_MAX_CYCLES, isRetrievalSessionState } from './retrieval-sessi
7
7
  export type { AppendRetrievalCycleInput, CreateRetrievalSessionInput, RetrievalCycleRecord, RetrievalGapKind, RetrievalGapRecord, RetrievalSessionRecord, RetrievalSessionState, } from './retrieval-sessions';
8
8
  export type { FailHostActionInput, HostActionRecord, HostActionState, ReportHostActionInput, RequestHostActionInput, StartHostActionInput, VerifyHostActionInput, } from './host-actions';
9
9
  export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
10
+ export { assertTaskIdentity, mapChannelQueryPlanRow, mapTaskIntentArtifactRow, mapTerminalTransitionRow, mapToolNameResolutionRow, mapTranscriptCheckpointRow, stableSemanticJson, } from './task-semantic-control';
11
+ export type { ChannelQueryPlanRecord, TaskIdentityRecord, TaskIntentArtifactRecord, TerminalTransitionRecord, ToolNameResolutionRecord, ToolNameResolutionStatus, TranscriptCheckpointRecord, } from './task-semantic-control';
10
12
  export { LEGAL_WORKFLOW_WORKER_TRANSITIONS, TERMINAL_WORKFLOW_WORKER_STATES, mapWorkflowMergeRow, mapWorkflowWorkerEventRow, mapWorkflowWorkerRow, } from './workflow-workers';
11
13
  export type { CreateWorkflowWorkerInput, EnqueueWorkflowMergeInput, TransitionWorkflowWorkerInput, WorkflowMergeRecord, WorkflowMergeState, WorkflowWorkerEventRecord, WorkflowWorkerRecord, WorkflowWorkerState, } from './workflow-workers';
12
14
  export { mapLearningCandidateRow, mapLearningEvidenceRow, mapLearningVersionRow, stableLearningJson, } from './learning-candidates';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.stableLearningJson = exports.mapLearningVersionRow = exports.mapLearningEvidenceRow = exports.mapLearningCandidateRow = exports.mapWorkflowWorkerRow = exports.mapWorkflowWorkerEventRow = exports.mapWorkflowMergeRow = exports.TERMINAL_WORKFLOW_WORKER_STATES = exports.LEGAL_WORKFLOW_WORKER_TRANSITIONS = exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
3
+ exports.stableLearningJson = exports.mapLearningVersionRow = exports.mapLearningEvidenceRow = exports.mapLearningCandidateRow = exports.mapWorkflowWorkerRow = exports.mapWorkflowWorkerEventRow = exports.mapWorkflowMergeRow = exports.TERMINAL_WORKFLOW_WORKER_STATES = exports.LEGAL_WORKFLOW_WORKER_TRANSITIONS = exports.stableSemanticJson = exports.mapTranscriptCheckpointRow = exports.mapToolNameResolutionRow = exports.mapTerminalTransitionRow = exports.mapTaskIntentArtifactRow = exports.mapChannelQueryPlanRow = exports.assertTaskIdentity = exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowReadOnlyDatabase = 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
+ Object.defineProperty(exports, "openGlobalDevFlowReadOnlyDatabase", { enumerable: true, get: function () { return database_1.openGlobalDevFlowReadOnlyDatabase; } });
8
9
  var host_actions_1 = require("./host-actions");
9
10
  Object.defineProperty(exports, "hostActionReportsEqual", { enumerable: true, get: function () { return host_actions_1.hostActionReportsEqual; } });
10
11
  Object.defineProperty(exports, "isHostActionState", { enumerable: true, get: function () { return host_actions_1.isHostActionState; } });
@@ -16,6 +17,14 @@ Object.defineProperty(exports, "isRetrievalSessionState", { enumerable: true, ge
16
17
  var obligation_ledger_1 = require("./obligation-ledger");
17
18
  Object.defineProperty(exports, "mapSessionObligationRow", { enumerable: true, get: function () { return obligation_ledger_1.mapSessionObligationRow; } });
18
19
  Object.defineProperty(exports, "normalizeTurnId", { enumerable: true, get: function () { return obligation_ledger_1.normalizeTurnId; } });
20
+ var task_semantic_control_1 = require("./task-semantic-control");
21
+ Object.defineProperty(exports, "assertTaskIdentity", { enumerable: true, get: function () { return task_semantic_control_1.assertTaskIdentity; } });
22
+ Object.defineProperty(exports, "mapChannelQueryPlanRow", { enumerable: true, get: function () { return task_semantic_control_1.mapChannelQueryPlanRow; } });
23
+ Object.defineProperty(exports, "mapTaskIntentArtifactRow", { enumerable: true, get: function () { return task_semantic_control_1.mapTaskIntentArtifactRow; } });
24
+ Object.defineProperty(exports, "mapTerminalTransitionRow", { enumerable: true, get: function () { return task_semantic_control_1.mapTerminalTransitionRow; } });
25
+ Object.defineProperty(exports, "mapToolNameResolutionRow", { enumerable: true, get: function () { return task_semantic_control_1.mapToolNameResolutionRow; } });
26
+ Object.defineProperty(exports, "mapTranscriptCheckpointRow", { enumerable: true, get: function () { return task_semantic_control_1.mapTranscriptCheckpointRow; } });
27
+ Object.defineProperty(exports, "stableSemanticJson", { enumerable: true, get: function () { return task_semantic_control_1.stableSemanticJson; } });
19
28
  var workflow_workers_1 = require("./workflow-workers");
20
29
  Object.defineProperty(exports, "LEGAL_WORKFLOW_WORKER_TRANSITIONS", { enumerable: true, get: function () { return workflow_workers_1.LEGAL_WORKFLOW_WORKER_TRANSITIONS; } });
21
30
  Object.defineProperty(exports, "TERMINAL_WORKFLOW_WORKER_STATES", { enumerable: true, get: function () { return workflow_workers_1.TERMINAL_WORKFLOW_WORKER_STATES; } });
@@ -1,7 +1,7 @@
1
1
  import type { Database, Statement } from './types';
2
2
  export declare class NodeSqliteDatabase implements Database {
3
3
  private db;
4
- constructor(path: string, busyTimeoutMs?: number);
4
+ constructor(path: string, busyTimeoutMs?: number, readOnly?: boolean);
5
5
  exec(sql: string): void;
6
6
  prepare(sql: string): Statement;
7
7
  transaction<T>(fn: () => T): T;
@@ -3,11 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NodeSqliteDatabase = void 0;
4
4
  const node_sqlite_1 = require("node:sqlite");
5
5
  class NodeSqliteDatabase {
6
- constructor(path, busyTimeoutMs = 10000) {
7
- this.db = new node_sqlite_1.DatabaseSync(path);
6
+ constructor(path, busyTimeoutMs = 10000, readOnly = false) {
7
+ this.db = readOnly
8
+ ? new node_sqlite_1.DatabaseSync(path, { readOnly: true })
9
+ : new node_sqlite_1.DatabaseSync(path);
8
10
  this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
9
- this.db.exec('PRAGMA journal_mode = WAL');
10
- this.db.exec('PRAGMA synchronous = NORMAL');
11
+ if (!readOnly) {
12
+ this.db.exec('PRAGMA journal_mode = WAL');
13
+ this.db.exec('PRAGMA synchronous = NORMAL');
14
+ }
11
15
  }
12
16
  exec(sql) {
13
17
  this.db.exec(sql);