@devflow-tools/database 0.17.2 → 0.17.4

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/src/database.ts CHANGED
@@ -12,6 +12,7 @@ import type {
12
12
  WorkItemRecord,
13
13
  WorkQueueHealth,
14
14
  WorkState,
15
+ ProjectMutationLeaseRecord,
15
16
  } from './work-queue';
16
17
  import {
17
18
  mapSessionObligationRow,
@@ -84,6 +85,19 @@ import {
84
85
  type ToolNameResolutionRecord,
85
86
  type TranscriptCheckpointRecord,
86
87
  } from './task-semantic-control';
88
+ import {
89
+ mapTaskRuntimeEventRow,
90
+ mapTaskRuntimeSnapshotRow,
91
+ stableTaskRuntimeJson,
92
+ type TaskRuntimeEventRecord,
93
+ type TaskRuntimeSnapshotRecord,
94
+ } from './task-runtime';
95
+ import {
96
+ assertSemanticResolutionRecord,
97
+ mapSemanticResolutionRow,
98
+ stableSemanticResolutionJson,
99
+ type SemanticResolutionRecord,
100
+ } from './semantic-resolution';
87
101
 
88
102
  export interface BenchmarkReportRecord {
89
103
  runId: string;
@@ -612,6 +626,9 @@ export class DevFlowDatabase {
612
626
  source_id TEXT NOT NULL, stage TEXT NOT NULL, rank INTEGER, raw_score REAL,
613
627
  normalized_score REAL, final_score REAL, applicability TEXT NOT NULL DEFAULT '[]',
614
628
  reason TEXT, tool_evidence TEXT NOT NULL DEFAULT '[]', verification_receipt TEXT,
629
+ schema_version TEXT NOT NULL DEFAULT 'retrieval-ledger-event.v1', task_spec_hash TEXT,
630
+ actor TEXT, source_version TEXT, source_content_hash TEXT,
631
+ evidence_ids TEXT NOT NULL DEFAULT '[]', reason_code TEXT,
615
632
  payload TEXT NOT NULL DEFAULT '{}', created_at INTEGER NOT NULL
616
633
  );
617
634
  CREATE INDEX IF NOT EXISTS idx_retrieval_ledger_identity ON devflow_retrieval_ledger(
@@ -651,6 +668,35 @@ export class DevFlowDatabase {
651
668
  CREATE UNIQUE INDEX IF NOT EXISTS idx_task_intent_source
652
669
  ON devflow_task_intent_artifacts(project_root, session_id, source_hash, version);
653
670
 
671
+ CREATE TABLE IF NOT EXISTS devflow_semantic_resolutions (
672
+ frame_hash TEXT PRIMARY KEY,
673
+ project_root TEXT NOT NULL,
674
+ project_id TEXT NOT NULL,
675
+ host_id TEXT NOT NULL,
676
+ session_id TEXT NOT NULL,
677
+ turn_id TEXT NOT NULL,
678
+ request_id TEXT NOT NULL,
679
+ source_hash TEXT NOT NULL,
680
+ artifact_hash TEXT NOT NULL,
681
+ artifact_revision INTEGER NOT NULL CHECK(artifact_revision > 0),
682
+ supersedes_frame_hash TEXT,
683
+ state TEXT NOT NULL CHECK(state IN ('provisional', 'resolved', 'abstained', 'unavailable')),
684
+ generated_by TEXT NOT NULL CHECK(generated_by IN ('deterministic', 'embedding_router', 'host_semantic', 'hybrid')),
685
+ model_id TEXT,
686
+ model_revision TEXT,
687
+ route_catalog_version TEXT NOT NULL,
688
+ threshold_version TEXT NOT NULL,
689
+ route_json TEXT NOT NULL,
690
+ sampling_json TEXT NOT NULL,
691
+ conflicts_json TEXT NOT NULL,
692
+ frame_json TEXT NOT NULL,
693
+ duration_ms INTEGER NOT NULL CHECK(duration_ms >= 0),
694
+ created_at INTEGER NOT NULL,
695
+ UNIQUE(project_root, session_id, turn_id, artifact_revision)
696
+ );
697
+ CREATE INDEX IF NOT EXISTS idx_semantic_resolution_latest
698
+ ON devflow_semantic_resolutions(project_root, session_id, turn_id, artifact_revision DESC);
699
+
654
700
  CREATE TABLE IF NOT EXISTS devflow_channel_query_plans (
655
701
  plan_hash TEXT PRIMARY KEY,
656
702
  source_intent_hash TEXT NOT NULL,
@@ -716,6 +762,40 @@ export class DevFlowDatabase {
716
762
  CREATE INDEX IF NOT EXISTS idx_terminal_transition_turn
717
763
  ON devflow_terminal_transitions(project_root, session_id, turn_id, sequence);
718
764
 
765
+ CREATE TABLE IF NOT EXISTS devflow_task_runtime_events (
766
+ event_id TEXT PRIMARY KEY,
767
+ schema_version TEXT NOT NULL,
768
+ producer TEXT NOT NULL,
769
+ producer_version TEXT NOT NULL,
770
+ project_root TEXT NOT NULL,
771
+ project_id TEXT NOT NULL,
772
+ host_id TEXT NOT NULL,
773
+ session_id TEXT NOT NULL,
774
+ turn_id TEXT NOT NULL,
775
+ request_id TEXT NOT NULL,
776
+ execution_id TEXT,
777
+ task_spec_hash TEXT NOT NULL,
778
+ sequence INTEGER NOT NULL CHECK(sequence > 0),
779
+ kind TEXT NOT NULL,
780
+ payload_json TEXT NOT NULL DEFAULT '{}',
781
+ created_at INTEGER NOT NULL,
782
+ UNIQUE(project_root, session_id, turn_id, sequence)
783
+ );
784
+ CREATE INDEX IF NOT EXISTS idx_task_runtime_event_identity
785
+ ON devflow_task_runtime_events(project_root, session_id, turn_id, sequence);
786
+
787
+ CREATE TABLE IF NOT EXISTS devflow_task_runtime_snapshots (
788
+ project_root TEXT NOT NULL,
789
+ session_id TEXT NOT NULL,
790
+ turn_id TEXT NOT NULL,
791
+ last_event_sequence INTEGER NOT NULL,
792
+ schema_version TEXT NOT NULL,
793
+ snapshot_json TEXT NOT NULL,
794
+ snapshot_hash TEXT NOT NULL,
795
+ updated_at INTEGER NOT NULL,
796
+ PRIMARY KEY(project_root, session_id, turn_id)
797
+ );
798
+
719
799
  CREATE TABLE IF NOT EXISTS devflow_transcript_checkpoints (
720
800
  id TEXT PRIMARY KEY,
721
801
  project_root TEXT NOT NULL,
@@ -869,9 +949,11 @@ export class DevFlowDatabase {
869
949
  project_root TEXT NOT NULL,
870
950
  session_id TEXT,
871
951
  turn_id TEXT,
952
+ source_hash TEXT,
953
+ task_spec_hash TEXT,
872
954
  payload TEXT NOT NULL,
873
955
  state TEXT NOT NULL DEFAULT 'pending'
874
- CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter')),
956
+ CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter', 'cancelled')),
875
957
  attempts INTEGER NOT NULL DEFAULT 0 CHECK(attempts >= 0),
876
958
  max_attempts INTEGER NOT NULL DEFAULT 5 CHECK(max_attempts > 0),
877
959
  lease_owner TEXT,
@@ -881,7 +963,11 @@ export class DevFlowDatabase {
881
963
  error_message TEXT,
882
964
  created_at INTEGER NOT NULL,
883
965
  updated_at INTEGER NOT NULL,
884
- completed_at INTEGER
966
+ completed_at INTEGER,
967
+ cancelled_at INTEGER,
968
+ cancellation_reason TEXT,
969
+ replay_of_id TEXT,
970
+ replay_count INTEGER NOT NULL DEFAULT 0
885
971
  );
886
972
 
887
973
  CREATE INDEX IF NOT EXISTS idx_work_items_ready
@@ -891,6 +977,16 @@ export class DevFlowDatabase {
891
977
  CREATE INDEX IF NOT EXISTS idx_work_items_completed
892
978
  ON devflow_work_items(project_root, completed_at DESC);
893
979
 
980
+ CREATE TABLE IF NOT EXISTS devflow_project_mutation_leases (
981
+ project_root TEXT NOT NULL,
982
+ mutation_kind TEXT NOT NULL,
983
+ owner TEXT NOT NULL,
984
+ lease_expires_at INTEGER NOT NULL,
985
+ source_hash TEXT,
986
+ updated_at INTEGER NOT NULL,
987
+ PRIMARY KEY(project_root, mutation_kind)
988
+ );
989
+
894
990
  CREATE TABLE IF NOT EXISTS devflow_session_closures (
895
991
  session_id TEXT NOT NULL,
896
992
  project_root TEXT NOT NULL,
@@ -1162,6 +1258,19 @@ export class DevFlowDatabase {
1162
1258
  try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN action_satisfied_at INTEGER'); } catch {}
1163
1259
  try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN action_degradation_json TEXT'); } catch {}
1164
1260
  try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN request_id TEXT'); } catch {}
1261
+ try { this.db.exec("ALTER TABLE devflow_retrieval_ledger ADD COLUMN schema_version TEXT NOT NULL DEFAULT 'retrieval-ledger-event.v1'"); } catch {}
1262
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN task_spec_hash TEXT'); } catch {}
1263
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN actor TEXT'); } catch {}
1264
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN source_version TEXT'); } catch {}
1265
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN source_content_hash TEXT'); } catch {}
1266
+ try { this.db.exec("ALTER TABLE devflow_retrieval_ledger ADD COLUMN evidence_ids TEXT NOT NULL DEFAULT '[]'"); } catch {}
1267
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN reason_code TEXT'); } catch {}
1268
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN source_hash TEXT'); } catch {}
1269
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN task_spec_hash TEXT'); } catch {}
1270
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN cancelled_at INTEGER'); } catch {}
1271
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN cancellation_reason TEXT'); } catch {}
1272
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN replay_of_id TEXT'); } catch {}
1273
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN replay_count INTEGER NOT NULL DEFAULT 0'); } catch {}
1165
1274
  this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
1166
1275
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
1167
1276
 
@@ -3010,7 +3119,8 @@ export class DevFlowDatabase {
3010
3119
  enqueueWork(input: EnqueueWorkInput): WorkItemRecord {
3011
3120
  if (!input.idempotencyKey.trim()) throw new Error('Work idempotency key is required');
3012
3121
  if (!input.projectRoot.trim()) throw new Error('Work project root is required');
3013
- const maxAttempts = input.maxAttempts ?? 5;
3122
+ const maxAttempts = input.maxAttempts
3123
+ ?? (input.kind === 'knowledge.ingest' || input.kind === 'knowledge.index_refresh' ? 3 : 5);
3014
3124
  if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) {
3015
3125
  throw new Error('Work maxAttempts must be a positive safe integer');
3016
3126
  }
@@ -3023,10 +3133,10 @@ export class DevFlowDatabase {
3023
3133
 
3024
3134
  const row = this.db.prepare(`
3025
3135
  INSERT INTO devflow_work_items (
3026
- id, idempotency_key, kind, project_root, session_id, turn_id, payload,
3136
+ id, idempotency_key, kind, project_root, session_id, turn_id, source_hash, task_spec_hash, payload,
3027
3137
  state, attempts, max_attempts, lease_owner, lease_expires_at,
3028
3138
  next_attempt_at, error_category, error_message, created_at, updated_at, completed_at
3029
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
3139
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
3030
3140
  ON CONFLICT(idempotency_key) DO UPDATE SET
3031
3141
  state = CASE
3032
3142
  WHEN devflow_work_items.state = 'dead_letter'
@@ -3067,6 +3177,8 @@ export class DevFlowDatabase {
3067
3177
  input.projectRoot,
3068
3178
  input.sessionId ?? null,
3069
3179
  input.turnId ?? null,
3180
+ input.sourceHash ?? null,
3181
+ input.taskSpecHash ?? null,
3070
3182
  JSON.stringify(input.payload ?? null),
3071
3183
  maxAttempts,
3072
3184
  nextAttemptAt,
@@ -3084,6 +3196,53 @@ export class DevFlowDatabase {
3084
3196
  return row ? this.mapWorkItem(row) : null;
3085
3197
  }
3086
3198
 
3199
+ getWorkById(id: string): WorkItemRecord | null {
3200
+ const row = this.db.prepare('SELECT * FROM devflow_work_items WHERE id = ?').get(id) as any;
3201
+ return row ? this.mapWorkItem(row) : null;
3202
+ }
3203
+
3204
+ acquireProjectMutationLease(input: { projectRoot: string; mutationKind: string; owner: string; leaseMs: number; sourceHash?: string; now?: number }): boolean {
3205
+ const now = input.now ?? Date.now();
3206
+ return this.db.prepare(`
3207
+ INSERT INTO devflow_project_mutation_leases
3208
+ (project_root, mutation_kind, owner, lease_expires_at, source_hash, updated_at)
3209
+ VALUES (?, ?, ?, ?, ?, ?)
3210
+ ON CONFLICT(project_root, mutation_kind) DO UPDATE SET
3211
+ owner = excluded.owner, lease_expires_at = excluded.lease_expires_at,
3212
+ source_hash = excluded.source_hash, updated_at = excluded.updated_at
3213
+ WHERE devflow_project_mutation_leases.lease_expires_at <= ? OR devflow_project_mutation_leases.owner = excluded.owner
3214
+ `).run(input.projectRoot, input.mutationKind, input.owner, now + input.leaseMs, input.sourceHash ?? null, now, now).changes > 0;
3215
+ }
3216
+
3217
+ releaseProjectMutationLease(projectRoot: string, mutationKind: string, owner: string): boolean {
3218
+ return this.db.prepare('DELETE FROM devflow_project_mutation_leases WHERE project_root = ? AND mutation_kind = ? AND owner = ?')
3219
+ .run(projectRoot, mutationKind, owner).changes > 0;
3220
+ }
3221
+
3222
+ cancelStaleWork(input: { id: string; owner: string; sourceHash?: string; taskSpecHash?: string; reason?: string; now?: number }): boolean {
3223
+ const current = this.getWorkById(input.id);
3224
+ const stale = current && current.state === 'leased' && current.leaseOwner === input.owner
3225
+ && ((input.sourceHash !== undefined && current.sourceHash !== input.sourceHash)
3226
+ || (input.taskSpecHash !== undefined && current.taskSpecHash !== input.taskSpecHash));
3227
+ if (!stale) return false;
3228
+ const now = input.now ?? Date.now();
3229
+ try {
3230
+ 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 = ?`)
3231
+ .run(now, input.reason ?? 'stale_source_or_task_spec', now, input.id, input.owner).changes === 1;
3232
+ } catch {
3233
+ 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 = ?`)
3234
+ .run(now, input.reason ?? 'stale_source_or_task_spec', now, input.id, input.owner).changes === 1;
3235
+ }
3236
+ }
3237
+
3238
+ replayDeadLetterWork(id: string, input: { idempotencyKey: string; sourceHash?: string; taskSpecHash?: string; now?: number }): WorkItemRecord | null {
3239
+ const original = this.getWorkById(id);
3240
+ if (!original || original.state !== 'dead_letter') return null;
3241
+ 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() });
3242
+ this.db.prepare('UPDATE devflow_work_items SET replay_of_id = ?, replay_count = replay_count + 1 WHERE id = ?').run(original.id, replay.id);
3243
+ return this.getWorkById(replay.id);
3244
+ }
3245
+
3087
3246
  requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord {
3088
3247
  if (!input.sessionId.trim()) throw new Error('Session closure requires a session ID');
3089
3248
  if (!input.projectRoot.trim()) throw new Error('Session closure requires a project root');
@@ -3387,8 +3546,10 @@ export class DevFlowDatabase {
3387
3546
  projectRoot: row.project_root,
3388
3547
  sessionId: row.session_id ?? undefined,
3389
3548
  turnId: row.turn_id ?? undefined,
3549
+ sourceHash: row.source_hash ?? undefined,
3550
+ taskSpecHash: row.task_spec_hash ?? undefined,
3390
3551
  payload: parseJson(row.payload),
3391
- state: row.state,
3552
+ state: row.cancelled_at != null ? 'cancelled' : row.state,
3392
3553
  attempts: Number(row.attempts),
3393
3554
  maxAttempts: Number(row.max_attempts),
3394
3555
  leaseOwner: row.lease_owner ?? undefined,
@@ -3399,6 +3560,10 @@ export class DevFlowDatabase {
3399
3560
  createdAt: Number(row.created_at),
3400
3561
  updatedAt: Number(row.updated_at),
3401
3562
  completedAt: row.completed_at == null ? undefined : Number(row.completed_at),
3563
+ cancelledAt: row.cancelled_at == null ? undefined : Number(row.cancelled_at),
3564
+ cancellationReason: row.cancellation_reason ?? undefined,
3565
+ replayOfId: row.replay_of_id ?? undefined,
3566
+ replayCount: Number(row.replay_count ?? 0),
3402
3567
  };
3403
3568
  }
3404
3569
 
@@ -3810,6 +3975,38 @@ export class DevFlowDatabase {
3810
3975
  } : null;
3811
3976
  }
3812
3977
 
3978
+ getContextReceiptForRequest(
3979
+ projectRoot: string,
3980
+ sessionId: string,
3981
+ requestId: string,
3982
+ ): ContextReceiptRecord | null {
3983
+ const row = this.db.prepare(`
3984
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
3985
+ selected_files, memory_ids, canonical_next_action, canonical_action_json,
3986
+ action_attempts, action_satisfied_at, action_degradation_json, request_id
3987
+ FROM devflow_context_receipts
3988
+ WHERE project_root = ? AND session_id = ? AND request_id = ?
3989
+ ORDER BY issued_at DESC
3990
+ LIMIT 1
3991
+ `).get(projectRoot, sessionId, requestId) as any;
3992
+ return row ? {
3993
+ projectRoot: row.project_root,
3994
+ sessionId: row.session_id,
3995
+ executionId: row.execution_id,
3996
+ contextHash: row.context_hash,
3997
+ issuedAt: row.issued_at,
3998
+ expiresAt: row.expires_at,
3999
+ selectedFiles: parseJsonStringArray(row.selected_files),
4000
+ memoryIds: parseJsonStringArray(row.memory_ids),
4001
+ canonicalNextAction: row.canonical_next_action ?? undefined,
4002
+ canonicalAction: parseJsonObject(row.canonical_action_json),
4003
+ actionAttempts: parseJsonArray(row.action_attempts),
4004
+ actionSatisfiedAt: row.action_satisfied_at ?? undefined,
4005
+ actionDegradation: parseJsonObject(row.action_degradation_json),
4006
+ requestId: row.request_id ?? undefined,
4007
+ } : null;
4008
+ }
4009
+
3813
4010
  recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean {
3814
4011
  return this.db.prepare(`
3815
4012
  INSERT OR IGNORE INTO devflow_context_selection_events
@@ -3842,7 +4039,8 @@ export class DevFlowDatabase {
3842
4039
  id, project_root, session_id, execution_id, turn_id, request_id, context_receipt,
3843
4040
  source_type, source_id, stage, rank, raw_score, normalized_score, final_score,
3844
4041
  applicability, reason, tool_evidence, verification_receipt, payload, created_at
3845
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4042
+ , schema_version, task_spec_hash, actor, source_version, source_content_hash, evidence_ids, reason_code
4043
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3846
4044
  `).run(
3847
4045
  event.id, event.projectRoot, event.sessionId, event.executionId ?? null, event.turnId ?? null,
3848
4046
  event.requestId, event.contextReceipt, event.sourceType, event.sourceId, event.stage,
@@ -3850,6 +4048,9 @@ export class DevFlowDatabase {
3850
4048
  event.finalScore ?? null, JSON.stringify(event.applicability), event.reason ?? null,
3851
4049
  JSON.stringify(event.toolEvidence), event.verificationReceipt ?? null,
3852
4050
  JSON.stringify(event.payload), event.createdAt,
4051
+ event.schemaVersion ?? 'retrieval-ledger-event.v1', event.taskSpecHash ?? null,
4052
+ event.actor ?? null, event.sourceVersion ?? null, event.sourceContentHash ?? null,
4053
+ JSON.stringify(event.evidenceIds ?? []), event.reasonCode ?? null,
3853
4054
  ).changes > 0;
3854
4055
  }
3855
4056
 
@@ -3940,6 +4141,96 @@ export class DevFlowDatabase {
3940
4141
  return row ? mapTaskIntentArtifactRow(row) : null;
3941
4142
  }
3942
4143
 
4144
+ appendSemanticResolution(record: SemanticResolutionRecord): boolean {
4145
+ assertSemanticResolutionRecord(record);
4146
+ const existing = this.getSemanticResolution(record.frameHash);
4147
+ if (existing) {
4148
+ if (stableSemanticResolutionJson(existing) !== stableSemanticResolutionJson(record)) {
4149
+ throw new Error(`SEMANTIC_RESOLUTION_HASH_CONFLICT:${record.frameHash}`);
4150
+ }
4151
+ return false;
4152
+ }
4153
+ this.db.exec('BEGIN IMMEDIATE');
4154
+ try {
4155
+ const conflicting = this.db.prepare(`
4156
+ SELECT frame_hash FROM devflow_semantic_resolutions
4157
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND artifact_revision = ?
4158
+ `).get(
4159
+ record.projectRoot, record.sessionId, record.turnId, record.artifactRevision,
4160
+ ) as Record<string, unknown> | undefined;
4161
+ if (conflicting) {
4162
+ throw new Error(`SEMANTIC_RESOLUTION_REVISION_CONFLICT:${record.turnId}:${record.artifactRevision}`);
4163
+ }
4164
+ if (record.supersedesFrameHash) {
4165
+ const superseded = this.getSemanticResolution(record.supersedesFrameHash);
4166
+ if (!superseded) {
4167
+ throw new Error(`SEMANTIC_RESOLUTION_SUPERSEDED_NOT_FOUND:${record.supersedesFrameHash}`);
4168
+ }
4169
+ if (superseded.projectRoot !== record.projectRoot
4170
+ || superseded.sessionId !== record.sessionId
4171
+ || superseded.turnId !== record.turnId
4172
+ || superseded.artifactRevision >= record.artifactRevision) {
4173
+ throw new Error('SEMANTIC_RESOLUTION_SUPERSEDES_INVALID');
4174
+ }
4175
+ }
4176
+ this.db.prepare(`
4177
+ INSERT INTO devflow_semantic_resolutions (
4178
+ frame_hash, project_root, project_id, host_id, session_id, turn_id, request_id,
4179
+ source_hash, artifact_hash, artifact_revision, supersedes_frame_hash, state,
4180
+ generated_by, model_id, model_revision, route_catalog_version, threshold_version,
4181
+ route_json, sampling_json, conflicts_json, frame_json, duration_ms, created_at
4182
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4183
+ `).run(
4184
+ record.frameHash, record.projectRoot, record.projectId, record.hostId,
4185
+ record.sessionId, record.turnId, record.requestId, record.sourceHash,
4186
+ record.artifactHash, record.artifactRevision, record.supersedesFrameHash ?? null,
4187
+ record.state, record.generatedBy, record.modelId ?? null, record.modelRevision ?? null,
4188
+ record.routeCatalogVersion, record.thresholdVersion,
4189
+ stableSemanticResolutionJson(record.route), stableSemanticResolutionJson(record.sampling),
4190
+ stableSemanticResolutionJson(record.conflicts), stableSemanticResolutionJson(record.frame),
4191
+ record.durationMs, record.createdAt,
4192
+ );
4193
+ this.db.exec('COMMIT');
4194
+ return true;
4195
+ } catch (error) {
4196
+ try { this.db.exec('ROLLBACK'); } catch {}
4197
+ throw error;
4198
+ }
4199
+ }
4200
+
4201
+ getSemanticResolution(frameHash: string): SemanticResolutionRecord | null {
4202
+ const row = this.db.prepare(`
4203
+ SELECT * FROM devflow_semantic_resolutions WHERE frame_hash = ?
4204
+ `).get(frameHash) as Record<string, unknown> | undefined;
4205
+ return row ? mapSemanticResolutionRow(row) : null;
4206
+ }
4207
+
4208
+ getLatestSemanticResolution(
4209
+ projectRoot: string,
4210
+ sessionId: string,
4211
+ turnId: string,
4212
+ ): SemanticResolutionRecord | null {
4213
+ const row = this.db.prepare(`
4214
+ SELECT * FROM devflow_semantic_resolutions
4215
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4216
+ ORDER BY artifact_revision DESC, created_at DESC LIMIT 1
4217
+ `).get(projectRoot, sessionId, turnId) as Record<string, unknown> | undefined;
4218
+ return row ? mapSemanticResolutionRow(row) : null;
4219
+ }
4220
+
4221
+ listSemanticResolutions(
4222
+ projectRoot: string,
4223
+ sessionId: string,
4224
+ turnId: string,
4225
+ ): SemanticResolutionRecord[] {
4226
+ return (this.db.prepare(`
4227
+ SELECT * FROM devflow_semantic_resolutions
4228
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4229
+ ORDER BY artifact_revision ASC, created_at ASC
4230
+ `).all(projectRoot, sessionId, turnId) as Array<Record<string, unknown>>)
4231
+ .map(mapSemanticResolutionRow);
4232
+ }
4233
+
3943
4234
  listLatestTaskIntentArtifacts(projectRoot: string, sessionId: string): TaskIntentArtifactRecord[] {
3944
4235
  return (this.db.prepare(`
3945
4236
  SELECT artifact.* FROM devflow_task_intent_artifacts artifact
@@ -4085,6 +4376,123 @@ export class DevFlowDatabase {
4085
4376
  return this.getTerminalTransition(record.receiptId)!;
4086
4377
  }
4087
4378
 
4379
+ appendTaskRuntimeEvent(event: TaskRuntimeEventRecord): TaskRuntimeEventRecord {
4380
+ const existing = this.getTaskRuntimeEvent(event.eventId);
4381
+ if (existing) {
4382
+ if (stableTaskRuntimeJson(existing) !== stableTaskRuntimeJson(event)) {
4383
+ throw new Error(`TASK_RUNTIME_EVENT_CONFLICT:${event.eventId}`);
4384
+ }
4385
+ return existing;
4386
+ }
4387
+ const atSequence = this.db.prepare(`
4388
+ SELECT event_id FROM devflow_task_runtime_events
4389
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND sequence = ?
4390
+ `).get(event.identity.projectRoot, event.identity.sessionId, event.identity.turnId, event.sequence) as
4391
+ { event_id?: string } | undefined;
4392
+ if (atSequence) throw new Error(`TASK_RUNTIME_SEQUENCE_CONFLICT:${event.sequence}`);
4393
+ try {
4394
+ this.db.prepare(`
4395
+ INSERT INTO devflow_task_runtime_events (
4396
+ event_id, schema_version, producer, producer_version, project_root, project_id,
4397
+ host_id, session_id, turn_id, request_id, execution_id, task_spec_hash,
4398
+ sequence, kind, payload_json, created_at
4399
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4400
+ `).run(
4401
+ event.eventId, event.schemaVersion, event.producer, event.producerVersion,
4402
+ event.identity.projectRoot, event.identity.projectId, event.identity.hostId,
4403
+ event.identity.sessionId, event.identity.turnId, event.identity.requestId,
4404
+ event.identity.executionId ?? null, event.taskSpecHash, event.sequence,
4405
+ event.kind, stableTaskRuntimeJson(event.payload), event.createdAt,
4406
+ );
4407
+ } catch (error) {
4408
+ const concurrentEvent = this.getTaskRuntimeEvent(event.eventId);
4409
+ if (concurrentEvent) {
4410
+ if (stableTaskRuntimeJson(concurrentEvent) === stableTaskRuntimeJson(event)) return concurrentEvent;
4411
+ throw new Error(`TASK_RUNTIME_EVENT_CONFLICT:${event.eventId}`);
4412
+ }
4413
+ const concurrentSequence = this.db.prepare(`
4414
+ SELECT event_id FROM devflow_task_runtime_events
4415
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND sequence = ?
4416
+ `).get(event.identity.projectRoot, event.identity.sessionId, event.identity.turnId, event.sequence);
4417
+ if (concurrentSequence) throw new Error(`TASK_RUNTIME_SEQUENCE_CONFLICT:${event.sequence}`);
4418
+ throw error;
4419
+ }
4420
+ return this.getTaskRuntimeEvent(event.eventId)!;
4421
+ }
4422
+
4423
+ getTaskRuntimeEvent(eventId: string): TaskRuntimeEventRecord | null {
4424
+ const row = this.db.prepare('SELECT * FROM devflow_task_runtime_events WHERE event_id = ?')
4425
+ .get(eventId) as Record<string, unknown> | undefined;
4426
+ return row ? mapTaskRuntimeEventRow(row) : null;
4427
+ }
4428
+
4429
+ listTaskRuntimeEvents(projectRoot: string, sessionId: string, turnId: string): TaskRuntimeEventRecord[] {
4430
+ return (this.db.prepare(`
4431
+ SELECT * FROM devflow_task_runtime_events
4432
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4433
+ ORDER BY sequence ASC
4434
+ `).all(projectRoot, sessionId, turnId) as Array<Record<string, unknown>>)
4435
+ .map(mapTaskRuntimeEventRow);
4436
+ }
4437
+
4438
+ putTaskRuntimeSnapshot(snapshot: TaskRuntimeSnapshotRecord): TaskRuntimeSnapshotRecord {
4439
+ const current = this.getTaskRuntimeSnapshot(
4440
+ snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId,
4441
+ );
4442
+ if (current && current.lastEventSequence > snapshot.lastEventSequence) {
4443
+ throw new Error('TASK_RUNTIME_SNAPSHOT_REGRESSION');
4444
+ }
4445
+ this.db.prepare(`
4446
+ INSERT INTO devflow_task_runtime_snapshots (
4447
+ project_root, session_id, turn_id, last_event_sequence, schema_version,
4448
+ snapshot_json, snapshot_hash, updated_at
4449
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4450
+ ON CONFLICT(project_root, session_id, turn_id) DO UPDATE SET
4451
+ last_event_sequence = excluded.last_event_sequence,
4452
+ schema_version = excluded.schema_version,
4453
+ snapshot_json = excluded.snapshot_json,
4454
+ snapshot_hash = excluded.snapshot_hash,
4455
+ updated_at = excluded.updated_at
4456
+ WHERE excluded.last_event_sequence >= devflow_task_runtime_snapshots.last_event_sequence
4457
+ `).run(
4458
+ snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId,
4459
+ snapshot.lastEventSequence, snapshot.schemaVersion, stableTaskRuntimeJson(snapshot),
4460
+ snapshot.snapshotHash, snapshot.updatedAt,
4461
+ );
4462
+ return this.getTaskRuntimeSnapshot(
4463
+ snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId,
4464
+ )!;
4465
+ }
4466
+
4467
+ appendTaskRuntimeEventAndSnapshot(
4468
+ event: TaskRuntimeEventRecord,
4469
+ snapshot: TaskRuntimeSnapshotRecord,
4470
+ ): TaskRuntimeSnapshotRecord {
4471
+ return this.db.transaction(() => {
4472
+ this.appendTaskRuntimeEvent(event);
4473
+ return this.putTaskRuntimeSnapshot(snapshot);
4474
+ });
4475
+ }
4476
+
4477
+ getTaskRuntimeSnapshot(
4478
+ projectRoot: string,
4479
+ sessionId: string,
4480
+ turnId: string,
4481
+ ): TaskRuntimeSnapshotRecord | null {
4482
+ const row = this.db.prepare(`
4483
+ SELECT * FROM devflow_task_runtime_snapshots
4484
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4485
+ `).get(projectRoot, sessionId, turnId) as Record<string, unknown> | undefined;
4486
+ return row ? mapTaskRuntimeSnapshotRow(row) : null;
4487
+ }
4488
+
4489
+ deleteTaskRuntimeSnapshot(projectRoot: string, sessionId: string, turnId: string): boolean {
4490
+ return this.db.prepare(`
4491
+ DELETE FROM devflow_task_runtime_snapshots
4492
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4493
+ `).run(projectRoot, sessionId, turnId).changes > 0;
4494
+ }
4495
+
4088
4496
  getTerminalTransition(receiptId: string): TerminalTransitionRecord | null {
4089
4497
  const row = this.db.prepare(`
4090
4498
  SELECT * FROM devflow_terminal_transitions WHERE receipt_id = ?
@@ -4156,7 +4564,11 @@ export class DevFlowDatabase {
4156
4564
  id: row.id, projectRoot: row.project_root, sessionId: row.session_id,
4157
4565
  executionId: row.execution_id ?? undefined, turnId: row.turn_id ?? undefined,
4158
4566
  requestId: row.request_id, contextReceipt: row.context_receipt,
4159
- sourceType: row.source_type, sourceId: row.source_id, stage: row.stage,
4567
+ schemaVersion: row.schema_version ?? 'retrieval-ledger-event.v1',
4568
+ sourceType: row.source_type, sourceId: row.source_id, stage: row.stage === 'adopted' ? 'consumed' : row.stage,
4569
+ taskSpecHash: row.task_spec_hash ?? undefined, actor: row.actor ?? undefined,
4570
+ sourceVersion: row.source_version ?? undefined, sourceContentHash: row.source_content_hash ?? undefined,
4571
+ evidenceIds: parseJsonStringArray(row.evidence_ids), reasonCode: row.reason_code ?? undefined,
4160
4572
  rank: row.rank ?? undefined, rawScore: row.raw_score ?? undefined,
4161
4573
  normalizedScore: row.normalized_score ?? undefined, finalScore: row.final_score ?? undefined,
4162
4574
  applicability: parseJsonStringArray(row.applicability), reason: row.reason ?? undefined,
@@ -4501,16 +4913,18 @@ export class DevFlowDatabase {
4501
4913
  turnId: string;
4502
4914
  receiptId: string;
4503
4915
  reason: string;
4916
+ source?: string;
4504
4917
  decidedAt?: number;
4505
4918
  }): MemoryTurnRecord {
4506
4919
  const turnId = normalizeTurnId(input.turnId);
4507
4920
  this.db.prepare(`
4508
4921
  UPDATE devflow_memory_turns
4509
- SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
4922
+ SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = ?,
4510
4923
  reason = ?, decided_at = ?
4511
4924
  WHERE turn_id = ? AND status = 'pending'
4512
4925
  `).run(
4513
4926
  input.receiptId,
4927
+ input.source ?? 'host_skip',
4514
4928
  input.reason,
4515
4929
  input.decidedAt ?? Date.now(),
4516
4930
  turnId,
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export type {
31
31
  WorkKind,
32
32
  WorkQueueHealth,
33
33
  WorkState,
34
+ ProjectMutationLeaseRecord,
34
35
  } from './work-queue';
35
36
  export type {
36
37
  RetrievalLedgerEventRecord,
@@ -75,6 +76,22 @@ export {
75
76
  mapTranscriptCheckpointRow,
76
77
  stableSemanticJson,
77
78
  } from './task-semantic-control';
79
+ export {
80
+ mapTaskRuntimeEventRow,
81
+ mapTaskRuntimeSnapshotRow,
82
+ stableTaskRuntimeHash,
83
+ stableTaskRuntimeJson,
84
+ } from './task-runtime';
85
+ export type {
86
+ TaskRuntimeChannelRecord,
87
+ TaskRuntimeChannelRequirement,
88
+ TaskRuntimeChannelStatus,
89
+ TaskRuntimeEventRecord,
90
+ TaskRuntimeIdentityRecord,
91
+ TaskRuntimeObligationRecord,
92
+ TaskRuntimeObligationState,
93
+ TaskRuntimeSnapshotRecord,
94
+ } from './task-runtime';
78
95
  export type {
79
96
  ChannelQueryPlanRecord,
80
97
  TaskIdentityRecord,
@@ -124,3 +141,13 @@ export type {
124
141
  SessionObligationRecord,
125
142
  SessionObligationState,
126
143
  } from './obligation-ledger';
144
+ export {
145
+ assertSemanticResolutionRecord,
146
+ mapSemanticResolutionRow,
147
+ stableSemanticResolutionJson,
148
+ } from './semantic-resolution';
149
+ export type {
150
+ PersistedSemanticGeneratedBy,
151
+ PersistedSemanticResolutionState,
152
+ SemanticResolutionRecord,
153
+ } from './semantic-resolution';
@@ -1,10 +1,11 @@
1
1
  export type RetrievalLedgerStage =
2
- | 'eligible' | 'candidate' | 'selected' | 'exposed' | 'adopted'
3
- | 'positive_outcome' | 'negative_outcome' | 'unknown' | 'rejected';
2
+ | 'eligible' | 'candidate' | 'selected' | 'exposed' | 'adopted' | 'consumed' | 'verified'
3
+ | 'positive_outcome' | 'negative_outcome' | 'unknown' | 'unknown_outcome' | 'rejected';
4
4
 
5
5
  export type RetrievalLedgerSourceType = 'code' | 'memory' | 'knowledge' | 'action';
6
6
 
7
7
  export interface RetrievalLedgerEventRecord {
8
+ schemaVersion?: 'retrieval-ledger-event.v1';
8
9
  id: string;
9
10
  projectRoot: string;
10
11
  sessionId: string;
@@ -14,6 +15,12 @@ export interface RetrievalLedgerEventRecord {
14
15
  contextReceipt: string;
15
16
  sourceType: RetrievalLedgerSourceType;
16
17
  sourceId: string;
18
+ taskSpecHash?: string;
19
+ actor?: string;
20
+ sourceVersion?: string;
21
+ sourceContentHash?: string;
22
+ evidenceIds?: string[];
23
+ reasonCode?: string;
17
24
  stage: RetrievalLedgerStage;
18
25
  rank?: number;
19
26
  rawScore?: number;