@devflow-tools/database 0.17.2 → 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.
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stableTaskRuntimeJson = stableTaskRuntimeJson;
4
+ exports.stableTaskRuntimeHash = stableTaskRuntimeHash;
5
+ exports.mapTaskRuntimeEventRow = mapTaskRuntimeEventRow;
6
+ exports.mapTaskRuntimeSnapshotRow = mapTaskRuntimeSnapshotRow;
7
+ const node_crypto_1 = require("node:crypto");
8
+ function stableTaskRuntimeJson(value) {
9
+ return JSON.stringify(canonicalize(value));
10
+ }
11
+ function stableTaskRuntimeHash(value) {
12
+ return (0, node_crypto_1.createHash)('sha256').update(stableTaskRuntimeJson(value)).digest('hex');
13
+ }
14
+ function mapTaskRuntimeEventRow(row) {
15
+ return {
16
+ schemaVersion: requiredLiteral(row.schema_version, 'task-runtime-event.v1'),
17
+ producer: requiredString(row.producer, 'producer'),
18
+ producerVersion: requiredString(row.producer_version, 'producer_version'),
19
+ eventId: requiredString(row.event_id, 'event_id'),
20
+ sequence: requiredPositiveInteger(row.sequence, 'sequence'),
21
+ identity: mapIdentity(row),
22
+ taskSpecHash: requiredString(row.task_spec_hash, 'task_spec_hash'),
23
+ kind: requiredString(row.kind, 'kind'),
24
+ payload: parseObject(row.payload_json, 'payload_json'),
25
+ createdAt: requiredNonNegativeInteger(row.created_at, 'created_at'),
26
+ };
27
+ }
28
+ function mapTaskRuntimeSnapshotRow(row) {
29
+ const snapshot = parseObject(row.snapshot_json, 'snapshot_json');
30
+ if (snapshot.schemaVersion !== 'task-runtime-snapshot.v1') {
31
+ throw new Error(`TASK_RUNTIME_SCHEMA_UNSUPPORTED:${String(snapshot.schemaVersion)}`);
32
+ }
33
+ if (snapshot.snapshotHash !== requiredString(row.snapshot_hash, 'snapshot_hash')) {
34
+ throw new Error('TASK_RUNTIME_SNAPSHOT_HASH_MISMATCH');
35
+ }
36
+ const { snapshotHash: _snapshotHash, ...body } = snapshot;
37
+ if (stableTaskRuntimeHash(body) !== snapshot.snapshotHash) {
38
+ throw new Error('TASK_RUNTIME_SNAPSHOT_HASH_MISMATCH');
39
+ }
40
+ return snapshot;
41
+ }
42
+ function mapIdentity(row) {
43
+ return {
44
+ projectRoot: requiredString(row.project_root, 'project_root'),
45
+ projectId: requiredString(row.project_id, 'project_id'),
46
+ hostId: requiredString(row.host_id, 'host_id'),
47
+ sessionId: requiredString(row.session_id, 'session_id'),
48
+ turnId: requiredString(row.turn_id, 'turn_id'),
49
+ requestId: requiredString(row.request_id, 'request_id'),
50
+ ...(optionalString(row.execution_id) ? { executionId: optionalString(row.execution_id) } : {}),
51
+ };
52
+ }
53
+ function requiredLiteral(value, literal) {
54
+ if (value !== literal)
55
+ throw new Error(`TASK_RUNTIME_SCHEMA_UNSUPPORTED:${String(value)}`);
56
+ return literal;
57
+ }
58
+ function requiredString(value, field) {
59
+ if (typeof value !== 'string' || !value.trim())
60
+ throw new Error(`TASK_RUNTIME_ROW_INVALID:${field}`);
61
+ return value;
62
+ }
63
+ function optionalString(value) {
64
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
65
+ }
66
+ function requiredPositiveInteger(value, field) {
67
+ const result = Number(value);
68
+ if (!Number.isSafeInteger(result) || result < 1)
69
+ throw new Error(`TASK_RUNTIME_ROW_INVALID:${field}`);
70
+ return result;
71
+ }
72
+ function requiredNonNegativeInteger(value, field) {
73
+ const result = Number(value);
74
+ if (!Number.isSafeInteger(result) || result < 0)
75
+ throw new Error(`TASK_RUNTIME_ROW_INVALID:${field}`);
76
+ return result;
77
+ }
78
+ function parseObject(value, field) {
79
+ if (typeof value !== 'string')
80
+ throw new Error(`TASK_RUNTIME_ROW_INVALID:${field}`);
81
+ try {
82
+ const parsed = JSON.parse(value);
83
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
84
+ throw new Error('not object');
85
+ return parsed;
86
+ }
87
+ catch {
88
+ throw new Error(`TASK_RUNTIME_ROW_INVALID:${field}`);
89
+ }
90
+ }
91
+ function canonicalize(value) {
92
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
93
+ return value;
94
+ if (typeof value === 'number') {
95
+ if (!Number.isFinite(value) || Object.is(value, -0))
96
+ throw new Error('TASK_RUNTIME_JSON_INVALID');
97
+ return value;
98
+ }
99
+ if (Array.isArray(value))
100
+ return value.map(canonicalize);
101
+ if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) {
102
+ throw new Error('TASK_RUNTIME_JSON_INVALID');
103
+ }
104
+ return Object.fromEntries(Object.keys(value).sort()
105
+ .map(key => [key, canonicalize(value[key])]));
106
+ }
@@ -1,5 +1,5 @@
1
- export type WorkKind = 'memory.explicit_commit' | 'memory.explicit_enrichment' | 'memory.turn_capture' | 'memory.turn_distill' | 'memory.vector_backfill' | 'context.prefetch' | 'verification.policy' | 'learning.analyze_session' | 'workflow.worker_start' | 'workflow.worker_monitor' | 'workflow.worker_verify' | 'workflow.worker_merge' | 'workflow.worker_cleanup' | 'session.bootstrap' | 'session.finalize' | 'telemetry.reconcile';
2
- export type WorkState = 'pending' | 'leased' | 'completed' | 'failed' | 'dead_letter';
1
+ export type WorkKind = 'memory.explicit_commit' | 'memory.explicit_enrichment' | 'memory.turn_capture' | 'memory.turn_distill' | 'memory.vector_backfill' | 'knowledge.ingest' | 'knowledge.index_refresh' | 'context.prefetch' | 'verification.policy' | 'learning.analyze_session' | 'workflow.worker_start' | 'workflow.worker_monitor' | 'workflow.worker_verify' | 'workflow.worker_merge' | 'workflow.worker_cleanup' | 'session.bootstrap' | 'session.finalize' | 'telemetry.reconcile';
2
+ export type WorkState = 'pending' | 'leased' | 'completed' | 'failed' | 'dead_letter' | 'cancelled';
3
3
  export interface WorkError {
4
4
  category: string;
5
5
  message: string;
@@ -11,6 +11,8 @@ export interface WorkItemRecord {
11
11
  projectRoot: string;
12
12
  sessionId?: string;
13
13
  turnId?: string;
14
+ sourceHash?: string;
15
+ taskSpecHash?: string;
14
16
  payload: unknown;
15
17
  state: WorkState;
16
18
  attempts: number;
@@ -23,6 +25,10 @@ export interface WorkItemRecord {
23
25
  createdAt: number;
24
26
  updatedAt: number;
25
27
  completedAt?: number;
28
+ cancelledAt?: number;
29
+ cancellationReason?: string;
30
+ replayOfId?: string;
31
+ replayCount: number;
26
32
  }
27
33
  export interface EnqueueWorkInput {
28
34
  idempotencyKey: string;
@@ -30,10 +36,20 @@ export interface EnqueueWorkInput {
30
36
  projectRoot: string;
31
37
  sessionId?: string;
32
38
  turnId?: string;
39
+ sourceHash?: string;
40
+ taskSpecHash?: string;
33
41
  payload: unknown;
34
42
  maxAttempts?: number;
35
43
  nextAttemptAt?: number;
36
44
  }
45
+ export interface ProjectMutationLeaseRecord {
46
+ projectRoot: string;
47
+ mutationKind: string;
48
+ owner: string;
49
+ leaseExpiresAt: number;
50
+ sourceHash?: string;
51
+ updatedAt: number;
52
+ }
37
53
  export interface LeaseWorkInput {
38
54
  projectRoot: string;
39
55
  owner: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-tools/database",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "DevFlow SQLite database package",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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,13 @@ 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';
87
95
 
88
96
  export interface BenchmarkReportRecord {
89
97
  runId: string;
@@ -612,6 +620,9 @@ export class DevFlowDatabase {
612
620
  source_id TEXT NOT NULL, stage TEXT NOT NULL, rank INTEGER, raw_score REAL,
613
621
  normalized_score REAL, final_score REAL, applicability TEXT NOT NULL DEFAULT '[]',
614
622
  reason TEXT, tool_evidence TEXT NOT NULL DEFAULT '[]', verification_receipt TEXT,
623
+ schema_version TEXT NOT NULL DEFAULT 'retrieval-ledger-event.v1', task_spec_hash TEXT,
624
+ actor TEXT, source_version TEXT, source_content_hash TEXT,
625
+ evidence_ids TEXT NOT NULL DEFAULT '[]', reason_code TEXT,
615
626
  payload TEXT NOT NULL DEFAULT '{}', created_at INTEGER NOT NULL
616
627
  );
617
628
  CREATE INDEX IF NOT EXISTS idx_retrieval_ledger_identity ON devflow_retrieval_ledger(
@@ -716,6 +727,40 @@ export class DevFlowDatabase {
716
727
  CREATE INDEX IF NOT EXISTS idx_terminal_transition_turn
717
728
  ON devflow_terminal_transitions(project_root, session_id, turn_id, sequence);
718
729
 
730
+ CREATE TABLE IF NOT EXISTS devflow_task_runtime_events (
731
+ event_id TEXT PRIMARY KEY,
732
+ schema_version TEXT NOT NULL,
733
+ producer TEXT NOT NULL,
734
+ producer_version TEXT NOT NULL,
735
+ project_root TEXT NOT NULL,
736
+ project_id TEXT NOT NULL,
737
+ host_id TEXT NOT NULL,
738
+ session_id TEXT NOT NULL,
739
+ turn_id TEXT NOT NULL,
740
+ request_id TEXT NOT NULL,
741
+ execution_id TEXT,
742
+ task_spec_hash TEXT NOT NULL,
743
+ sequence INTEGER NOT NULL CHECK(sequence > 0),
744
+ kind TEXT NOT NULL,
745
+ payload_json TEXT NOT NULL DEFAULT '{}',
746
+ created_at INTEGER NOT NULL,
747
+ UNIQUE(project_root, session_id, turn_id, sequence)
748
+ );
749
+ CREATE INDEX IF NOT EXISTS idx_task_runtime_event_identity
750
+ ON devflow_task_runtime_events(project_root, session_id, turn_id, sequence);
751
+
752
+ CREATE TABLE IF NOT EXISTS devflow_task_runtime_snapshots (
753
+ project_root TEXT NOT NULL,
754
+ session_id TEXT NOT NULL,
755
+ turn_id TEXT NOT NULL,
756
+ last_event_sequence INTEGER NOT NULL,
757
+ schema_version TEXT NOT NULL,
758
+ snapshot_json TEXT NOT NULL,
759
+ snapshot_hash TEXT NOT NULL,
760
+ updated_at INTEGER NOT NULL,
761
+ PRIMARY KEY(project_root, session_id, turn_id)
762
+ );
763
+
719
764
  CREATE TABLE IF NOT EXISTS devflow_transcript_checkpoints (
720
765
  id TEXT PRIMARY KEY,
721
766
  project_root TEXT NOT NULL,
@@ -869,9 +914,11 @@ export class DevFlowDatabase {
869
914
  project_root TEXT NOT NULL,
870
915
  session_id TEXT,
871
916
  turn_id TEXT,
917
+ source_hash TEXT,
918
+ task_spec_hash TEXT,
872
919
  payload TEXT NOT NULL,
873
920
  state TEXT NOT NULL DEFAULT 'pending'
874
- CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter')),
921
+ CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter', 'cancelled')),
875
922
  attempts INTEGER NOT NULL DEFAULT 0 CHECK(attempts >= 0),
876
923
  max_attempts INTEGER NOT NULL DEFAULT 5 CHECK(max_attempts > 0),
877
924
  lease_owner TEXT,
@@ -881,7 +928,11 @@ export class DevFlowDatabase {
881
928
  error_message TEXT,
882
929
  created_at INTEGER NOT NULL,
883
930
  updated_at INTEGER NOT NULL,
884
- completed_at INTEGER
931
+ completed_at INTEGER,
932
+ cancelled_at INTEGER,
933
+ cancellation_reason TEXT,
934
+ replay_of_id TEXT,
935
+ replay_count INTEGER NOT NULL DEFAULT 0
885
936
  );
886
937
 
887
938
  CREATE INDEX IF NOT EXISTS idx_work_items_ready
@@ -891,6 +942,16 @@ export class DevFlowDatabase {
891
942
  CREATE INDEX IF NOT EXISTS idx_work_items_completed
892
943
  ON devflow_work_items(project_root, completed_at DESC);
893
944
 
945
+ CREATE TABLE IF NOT EXISTS devflow_project_mutation_leases (
946
+ project_root TEXT NOT NULL,
947
+ mutation_kind TEXT NOT NULL,
948
+ owner TEXT NOT NULL,
949
+ lease_expires_at INTEGER NOT NULL,
950
+ source_hash TEXT,
951
+ updated_at INTEGER NOT NULL,
952
+ PRIMARY KEY(project_root, mutation_kind)
953
+ );
954
+
894
955
  CREATE TABLE IF NOT EXISTS devflow_session_closures (
895
956
  session_id TEXT NOT NULL,
896
957
  project_root TEXT NOT NULL,
@@ -1162,6 +1223,19 @@ export class DevFlowDatabase {
1162
1223
  try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN action_satisfied_at INTEGER'); } catch {}
1163
1224
  try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN action_degradation_json TEXT'); } catch {}
1164
1225
  try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN request_id TEXT'); } catch {}
1226
+ try { this.db.exec("ALTER TABLE devflow_retrieval_ledger ADD COLUMN schema_version TEXT NOT NULL DEFAULT 'retrieval-ledger-event.v1'"); } catch {}
1227
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN task_spec_hash TEXT'); } catch {}
1228
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN actor TEXT'); } catch {}
1229
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN source_version TEXT'); } catch {}
1230
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN source_content_hash TEXT'); } catch {}
1231
+ try { this.db.exec("ALTER TABLE devflow_retrieval_ledger ADD COLUMN evidence_ids TEXT NOT NULL DEFAULT '[]'"); } catch {}
1232
+ try { this.db.exec('ALTER TABLE devflow_retrieval_ledger ADD COLUMN reason_code TEXT'); } catch {}
1233
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN source_hash TEXT'); } catch {}
1234
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN task_spec_hash TEXT'); } catch {}
1235
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN cancelled_at INTEGER'); } catch {}
1236
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN cancellation_reason TEXT'); } catch {}
1237
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN replay_of_id TEXT'); } catch {}
1238
+ try { this.db.exec('ALTER TABLE devflow_work_items ADD COLUMN replay_count INTEGER NOT NULL DEFAULT 0'); } catch {}
1165
1239
  this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
1166
1240
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
1167
1241
 
@@ -3010,7 +3084,8 @@ export class DevFlowDatabase {
3010
3084
  enqueueWork(input: EnqueueWorkInput): WorkItemRecord {
3011
3085
  if (!input.idempotencyKey.trim()) throw new Error('Work idempotency key is required');
3012
3086
  if (!input.projectRoot.trim()) throw new Error('Work project root is required');
3013
- const maxAttempts = input.maxAttempts ?? 5;
3087
+ const maxAttempts = input.maxAttempts
3088
+ ?? (input.kind === 'knowledge.ingest' || input.kind === 'knowledge.index_refresh' ? 3 : 5);
3014
3089
  if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) {
3015
3090
  throw new Error('Work maxAttempts must be a positive safe integer');
3016
3091
  }
@@ -3023,10 +3098,10 @@ export class DevFlowDatabase {
3023
3098
 
3024
3099
  const row = this.db.prepare(`
3025
3100
  INSERT INTO devflow_work_items (
3026
- id, idempotency_key, kind, project_root, session_id, turn_id, payload,
3101
+ id, idempotency_key, kind, project_root, session_id, turn_id, source_hash, task_spec_hash, payload,
3027
3102
  state, attempts, max_attempts, lease_owner, lease_expires_at,
3028
3103
  next_attempt_at, error_category, error_message, created_at, updated_at, completed_at
3029
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
3104
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
3030
3105
  ON CONFLICT(idempotency_key) DO UPDATE SET
3031
3106
  state = CASE
3032
3107
  WHEN devflow_work_items.state = 'dead_letter'
@@ -3067,6 +3142,8 @@ export class DevFlowDatabase {
3067
3142
  input.projectRoot,
3068
3143
  input.sessionId ?? null,
3069
3144
  input.turnId ?? null,
3145
+ input.sourceHash ?? null,
3146
+ input.taskSpecHash ?? null,
3070
3147
  JSON.stringify(input.payload ?? null),
3071
3148
  maxAttempts,
3072
3149
  nextAttemptAt,
@@ -3084,6 +3161,53 @@ export class DevFlowDatabase {
3084
3161
  return row ? this.mapWorkItem(row) : null;
3085
3162
  }
3086
3163
 
3164
+ getWorkById(id: string): WorkItemRecord | null {
3165
+ const row = this.db.prepare('SELECT * FROM devflow_work_items WHERE id = ?').get(id) as any;
3166
+ return row ? this.mapWorkItem(row) : null;
3167
+ }
3168
+
3169
+ acquireProjectMutationLease(input: { projectRoot: string; mutationKind: string; owner: string; leaseMs: number; sourceHash?: string; now?: number }): boolean {
3170
+ const now = input.now ?? Date.now();
3171
+ return this.db.prepare(`
3172
+ INSERT INTO devflow_project_mutation_leases
3173
+ (project_root, mutation_kind, owner, lease_expires_at, source_hash, updated_at)
3174
+ VALUES (?, ?, ?, ?, ?, ?)
3175
+ ON CONFLICT(project_root, mutation_kind) DO UPDATE SET
3176
+ owner = excluded.owner, lease_expires_at = excluded.lease_expires_at,
3177
+ source_hash = excluded.source_hash, updated_at = excluded.updated_at
3178
+ WHERE devflow_project_mutation_leases.lease_expires_at <= ? OR devflow_project_mutation_leases.owner = excluded.owner
3179
+ `).run(input.projectRoot, input.mutationKind, input.owner, now + input.leaseMs, input.sourceHash ?? null, now, now).changes > 0;
3180
+ }
3181
+
3182
+ releaseProjectMutationLease(projectRoot: string, mutationKind: string, owner: string): boolean {
3183
+ return this.db.prepare('DELETE FROM devflow_project_mutation_leases WHERE project_root = ? AND mutation_kind = ? AND owner = ?')
3184
+ .run(projectRoot, mutationKind, owner).changes > 0;
3185
+ }
3186
+
3187
+ cancelStaleWork(input: { id: string; owner: string; sourceHash?: string; taskSpecHash?: string; reason?: string; now?: number }): boolean {
3188
+ const current = this.getWorkById(input.id);
3189
+ const stale = current && current.state === 'leased' && current.leaseOwner === input.owner
3190
+ && ((input.sourceHash !== undefined && current.sourceHash !== input.sourceHash)
3191
+ || (input.taskSpecHash !== undefined && current.taskSpecHash !== input.taskSpecHash));
3192
+ if (!stale) return false;
3193
+ const now = input.now ?? Date.now();
3194
+ try {
3195
+ 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 = ?`)
3196
+ .run(now, input.reason ?? 'stale_source_or_task_spec', now, input.id, input.owner).changes === 1;
3197
+ } catch {
3198
+ 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 = ?`)
3199
+ .run(now, input.reason ?? 'stale_source_or_task_spec', now, input.id, input.owner).changes === 1;
3200
+ }
3201
+ }
3202
+
3203
+ replayDeadLetterWork(id: string, input: { idempotencyKey: string; sourceHash?: string; taskSpecHash?: string; now?: number }): WorkItemRecord | null {
3204
+ const original = this.getWorkById(id);
3205
+ if (!original || original.state !== 'dead_letter') return null;
3206
+ 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() });
3207
+ this.db.prepare('UPDATE devflow_work_items SET replay_of_id = ?, replay_count = replay_count + 1 WHERE id = ?').run(original.id, replay.id);
3208
+ return this.getWorkById(replay.id);
3209
+ }
3210
+
3087
3211
  requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord {
3088
3212
  if (!input.sessionId.trim()) throw new Error('Session closure requires a session ID');
3089
3213
  if (!input.projectRoot.trim()) throw new Error('Session closure requires a project root');
@@ -3387,8 +3511,10 @@ export class DevFlowDatabase {
3387
3511
  projectRoot: row.project_root,
3388
3512
  sessionId: row.session_id ?? undefined,
3389
3513
  turnId: row.turn_id ?? undefined,
3514
+ sourceHash: row.source_hash ?? undefined,
3515
+ taskSpecHash: row.task_spec_hash ?? undefined,
3390
3516
  payload: parseJson(row.payload),
3391
- state: row.state,
3517
+ state: row.cancelled_at != null ? 'cancelled' : row.state,
3392
3518
  attempts: Number(row.attempts),
3393
3519
  maxAttempts: Number(row.max_attempts),
3394
3520
  leaseOwner: row.lease_owner ?? undefined,
@@ -3399,6 +3525,10 @@ export class DevFlowDatabase {
3399
3525
  createdAt: Number(row.created_at),
3400
3526
  updatedAt: Number(row.updated_at),
3401
3527
  completedAt: row.completed_at == null ? undefined : Number(row.completed_at),
3528
+ cancelledAt: row.cancelled_at == null ? undefined : Number(row.cancelled_at),
3529
+ cancellationReason: row.cancellation_reason ?? undefined,
3530
+ replayOfId: row.replay_of_id ?? undefined,
3531
+ replayCount: Number(row.replay_count ?? 0),
3402
3532
  };
3403
3533
  }
3404
3534
 
@@ -3810,6 +3940,38 @@ export class DevFlowDatabase {
3810
3940
  } : null;
3811
3941
  }
3812
3942
 
3943
+ getContextReceiptForRequest(
3944
+ projectRoot: string,
3945
+ sessionId: string,
3946
+ requestId: string,
3947
+ ): ContextReceiptRecord | null {
3948
+ const row = this.db.prepare(`
3949
+ SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
3950
+ selected_files, memory_ids, canonical_next_action, canonical_action_json,
3951
+ action_attempts, action_satisfied_at, action_degradation_json, request_id
3952
+ FROM devflow_context_receipts
3953
+ WHERE project_root = ? AND session_id = ? AND request_id = ?
3954
+ ORDER BY issued_at DESC
3955
+ LIMIT 1
3956
+ `).get(projectRoot, sessionId, requestId) as any;
3957
+ return row ? {
3958
+ projectRoot: row.project_root,
3959
+ sessionId: row.session_id,
3960
+ executionId: row.execution_id,
3961
+ contextHash: row.context_hash,
3962
+ issuedAt: row.issued_at,
3963
+ expiresAt: row.expires_at,
3964
+ selectedFiles: parseJsonStringArray(row.selected_files),
3965
+ memoryIds: parseJsonStringArray(row.memory_ids),
3966
+ canonicalNextAction: row.canonical_next_action ?? undefined,
3967
+ canonicalAction: parseJsonObject(row.canonical_action_json),
3968
+ actionAttempts: parseJsonArray(row.action_attempts),
3969
+ actionSatisfiedAt: row.action_satisfied_at ?? undefined,
3970
+ actionDegradation: parseJsonObject(row.action_degradation_json),
3971
+ requestId: row.request_id ?? undefined,
3972
+ } : null;
3973
+ }
3974
+
3813
3975
  recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean {
3814
3976
  return this.db.prepare(`
3815
3977
  INSERT OR IGNORE INTO devflow_context_selection_events
@@ -3842,7 +4004,8 @@ export class DevFlowDatabase {
3842
4004
  id, project_root, session_id, execution_id, turn_id, request_id, context_receipt,
3843
4005
  source_type, source_id, stage, rank, raw_score, normalized_score, final_score,
3844
4006
  applicability, reason, tool_evidence, verification_receipt, payload, created_at
3845
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4007
+ , schema_version, task_spec_hash, actor, source_version, source_content_hash, evidence_ids, reason_code
4008
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3846
4009
  `).run(
3847
4010
  event.id, event.projectRoot, event.sessionId, event.executionId ?? null, event.turnId ?? null,
3848
4011
  event.requestId, event.contextReceipt, event.sourceType, event.sourceId, event.stage,
@@ -3850,6 +4013,9 @@ export class DevFlowDatabase {
3850
4013
  event.finalScore ?? null, JSON.stringify(event.applicability), event.reason ?? null,
3851
4014
  JSON.stringify(event.toolEvidence), event.verificationReceipt ?? null,
3852
4015
  JSON.stringify(event.payload), event.createdAt,
4016
+ event.schemaVersion ?? 'retrieval-ledger-event.v1', event.taskSpecHash ?? null,
4017
+ event.actor ?? null, event.sourceVersion ?? null, event.sourceContentHash ?? null,
4018
+ JSON.stringify(event.evidenceIds ?? []), event.reasonCode ?? null,
3853
4019
  ).changes > 0;
3854
4020
  }
3855
4021
 
@@ -4085,6 +4251,123 @@ export class DevFlowDatabase {
4085
4251
  return this.getTerminalTransition(record.receiptId)!;
4086
4252
  }
4087
4253
 
4254
+ appendTaskRuntimeEvent(event: TaskRuntimeEventRecord): TaskRuntimeEventRecord {
4255
+ const existing = this.getTaskRuntimeEvent(event.eventId);
4256
+ if (existing) {
4257
+ if (stableTaskRuntimeJson(existing) !== stableTaskRuntimeJson(event)) {
4258
+ throw new Error(`TASK_RUNTIME_EVENT_CONFLICT:${event.eventId}`);
4259
+ }
4260
+ return existing;
4261
+ }
4262
+ const atSequence = this.db.prepare(`
4263
+ SELECT event_id FROM devflow_task_runtime_events
4264
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND sequence = ?
4265
+ `).get(event.identity.projectRoot, event.identity.sessionId, event.identity.turnId, event.sequence) as
4266
+ { event_id?: string } | undefined;
4267
+ if (atSequence) throw new Error(`TASK_RUNTIME_SEQUENCE_CONFLICT:${event.sequence}`);
4268
+ try {
4269
+ this.db.prepare(`
4270
+ INSERT INTO devflow_task_runtime_events (
4271
+ event_id, schema_version, producer, producer_version, project_root, project_id,
4272
+ host_id, session_id, turn_id, request_id, execution_id, task_spec_hash,
4273
+ sequence, kind, payload_json, created_at
4274
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4275
+ `).run(
4276
+ event.eventId, event.schemaVersion, event.producer, event.producerVersion,
4277
+ event.identity.projectRoot, event.identity.projectId, event.identity.hostId,
4278
+ event.identity.sessionId, event.identity.turnId, event.identity.requestId,
4279
+ event.identity.executionId ?? null, event.taskSpecHash, event.sequence,
4280
+ event.kind, stableTaskRuntimeJson(event.payload), event.createdAt,
4281
+ );
4282
+ } catch (error) {
4283
+ const concurrentEvent = this.getTaskRuntimeEvent(event.eventId);
4284
+ if (concurrentEvent) {
4285
+ if (stableTaskRuntimeJson(concurrentEvent) === stableTaskRuntimeJson(event)) return concurrentEvent;
4286
+ throw new Error(`TASK_RUNTIME_EVENT_CONFLICT:${event.eventId}`);
4287
+ }
4288
+ const concurrentSequence = this.db.prepare(`
4289
+ SELECT event_id FROM devflow_task_runtime_events
4290
+ WHERE project_root = ? AND session_id = ? AND turn_id = ? AND sequence = ?
4291
+ `).get(event.identity.projectRoot, event.identity.sessionId, event.identity.turnId, event.sequence);
4292
+ if (concurrentSequence) throw new Error(`TASK_RUNTIME_SEQUENCE_CONFLICT:${event.sequence}`);
4293
+ throw error;
4294
+ }
4295
+ return this.getTaskRuntimeEvent(event.eventId)!;
4296
+ }
4297
+
4298
+ getTaskRuntimeEvent(eventId: string): TaskRuntimeEventRecord | null {
4299
+ const row = this.db.prepare('SELECT * FROM devflow_task_runtime_events WHERE event_id = ?')
4300
+ .get(eventId) as Record<string, unknown> | undefined;
4301
+ return row ? mapTaskRuntimeEventRow(row) : null;
4302
+ }
4303
+
4304
+ listTaskRuntimeEvents(projectRoot: string, sessionId: string, turnId: string): TaskRuntimeEventRecord[] {
4305
+ return (this.db.prepare(`
4306
+ SELECT * FROM devflow_task_runtime_events
4307
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4308
+ ORDER BY sequence ASC
4309
+ `).all(projectRoot, sessionId, turnId) as Array<Record<string, unknown>>)
4310
+ .map(mapTaskRuntimeEventRow);
4311
+ }
4312
+
4313
+ putTaskRuntimeSnapshot(snapshot: TaskRuntimeSnapshotRecord): TaskRuntimeSnapshotRecord {
4314
+ const current = this.getTaskRuntimeSnapshot(
4315
+ snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId,
4316
+ );
4317
+ if (current && current.lastEventSequence > snapshot.lastEventSequence) {
4318
+ throw new Error('TASK_RUNTIME_SNAPSHOT_REGRESSION');
4319
+ }
4320
+ this.db.prepare(`
4321
+ INSERT INTO devflow_task_runtime_snapshots (
4322
+ project_root, session_id, turn_id, last_event_sequence, schema_version,
4323
+ snapshot_json, snapshot_hash, updated_at
4324
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4325
+ ON CONFLICT(project_root, session_id, turn_id) DO UPDATE SET
4326
+ last_event_sequence = excluded.last_event_sequence,
4327
+ schema_version = excluded.schema_version,
4328
+ snapshot_json = excluded.snapshot_json,
4329
+ snapshot_hash = excluded.snapshot_hash,
4330
+ updated_at = excluded.updated_at
4331
+ WHERE excluded.last_event_sequence >= devflow_task_runtime_snapshots.last_event_sequence
4332
+ `).run(
4333
+ snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId,
4334
+ snapshot.lastEventSequence, snapshot.schemaVersion, stableTaskRuntimeJson(snapshot),
4335
+ snapshot.snapshotHash, snapshot.updatedAt,
4336
+ );
4337
+ return this.getTaskRuntimeSnapshot(
4338
+ snapshot.identity.projectRoot, snapshot.identity.sessionId, snapshot.identity.turnId,
4339
+ )!;
4340
+ }
4341
+
4342
+ appendTaskRuntimeEventAndSnapshot(
4343
+ event: TaskRuntimeEventRecord,
4344
+ snapshot: TaskRuntimeSnapshotRecord,
4345
+ ): TaskRuntimeSnapshotRecord {
4346
+ return this.db.transaction(() => {
4347
+ this.appendTaskRuntimeEvent(event);
4348
+ return this.putTaskRuntimeSnapshot(snapshot);
4349
+ });
4350
+ }
4351
+
4352
+ getTaskRuntimeSnapshot(
4353
+ projectRoot: string,
4354
+ sessionId: string,
4355
+ turnId: string,
4356
+ ): TaskRuntimeSnapshotRecord | null {
4357
+ const row = this.db.prepare(`
4358
+ SELECT * FROM devflow_task_runtime_snapshots
4359
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4360
+ `).get(projectRoot, sessionId, turnId) as Record<string, unknown> | undefined;
4361
+ return row ? mapTaskRuntimeSnapshotRow(row) : null;
4362
+ }
4363
+
4364
+ deleteTaskRuntimeSnapshot(projectRoot: string, sessionId: string, turnId: string): boolean {
4365
+ return this.db.prepare(`
4366
+ DELETE FROM devflow_task_runtime_snapshots
4367
+ WHERE project_root = ? AND session_id = ? AND turn_id = ?
4368
+ `).run(projectRoot, sessionId, turnId).changes > 0;
4369
+ }
4370
+
4088
4371
  getTerminalTransition(receiptId: string): TerminalTransitionRecord | null {
4089
4372
  const row = this.db.prepare(`
4090
4373
  SELECT * FROM devflow_terminal_transitions WHERE receipt_id = ?
@@ -4156,7 +4439,11 @@ export class DevFlowDatabase {
4156
4439
  id: row.id, projectRoot: row.project_root, sessionId: row.session_id,
4157
4440
  executionId: row.execution_id ?? undefined, turnId: row.turn_id ?? undefined,
4158
4441
  requestId: row.request_id, contextReceipt: row.context_receipt,
4159
- sourceType: row.source_type, sourceId: row.source_id, stage: row.stage,
4442
+ schemaVersion: row.schema_version ?? 'retrieval-ledger-event.v1',
4443
+ sourceType: row.source_type, sourceId: row.source_id, stage: row.stage === 'adopted' ? 'consumed' : row.stage,
4444
+ taskSpecHash: row.task_spec_hash ?? undefined, actor: row.actor ?? undefined,
4445
+ sourceVersion: row.source_version ?? undefined, sourceContentHash: row.source_content_hash ?? undefined,
4446
+ evidenceIds: parseJsonStringArray(row.evidence_ids), reasonCode: row.reason_code ?? undefined,
4160
4447
  rank: row.rank ?? undefined, rawScore: row.raw_score ?? undefined,
4161
4448
  normalizedScore: row.normalized_score ?? undefined, finalScore: row.final_score ?? undefined,
4162
4449
  applicability: parseJsonStringArray(row.applicability), reason: row.reason ?? undefined,
@@ -4501,16 +4788,18 @@ export class DevFlowDatabase {
4501
4788
  turnId: string;
4502
4789
  receiptId: string;
4503
4790
  reason: string;
4791
+ source?: string;
4504
4792
  decidedAt?: number;
4505
4793
  }): MemoryTurnRecord {
4506
4794
  const turnId = normalizeTurnId(input.turnId);
4507
4795
  this.db.prepare(`
4508
4796
  UPDATE devflow_memory_turns
4509
- SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
4797
+ SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = ?,
4510
4798
  reason = ?, decided_at = ?
4511
4799
  WHERE turn_id = ? AND status = 'pending'
4512
4800
  `).run(
4513
4801
  input.receiptId,
4802
+ input.source ?? 'host_skip',
4514
4803
  input.reason,
4515
4804
  input.decidedAt ?? Date.now(),
4516
4805
  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,
@@ -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;