@devflow-tools/database 0.17.1 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/__tests__/database.retrieval-ledger.test.ts +15 -0
- package/__tests__/database.task-runtime.test.ts +73 -0
- package/__tests__/database.work-queue.test.ts +34 -0
- package/dist/database.d.ts +46 -0
- package/dist/database.js +374 -41
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -1
- package/dist/node-sqlite.d.ts +1 -1
- package/dist/node-sqlite.js +8 -4
- package/dist/retrieval-ledger.d.ts +8 -1
- package/dist/task-runtime.d.ts +64 -0
- package/dist/task-runtime.js +106 -0
- package/dist/work-queue.d.ts +19 -2
- package/package.json +1 -1
- package/src/database.ts +413 -44
- package/src/index.ts +18 -0
- package/src/node-sqlite.ts +8 -4
- package/src/retrieval-ledger.ts +9 -2
- package/src/task-runtime.ts +169 -0
- package/src/work-queue.ts +21 -1
package/src/database.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeSqliteDatabase } from './node-sqlite';
|
|
2
2
|
import { join, dirname, resolve } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, realpathSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, realpathSync, statSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import type {
|
|
@@ -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;
|
|
@@ -305,35 +313,72 @@ export function getGlobalDevFlowDbPath(home = homedir()): string {
|
|
|
305
313
|
|
|
306
314
|
export function openGlobalDevFlowDatabase(
|
|
307
315
|
home = homedir(),
|
|
308
|
-
options?: { busyTimeoutMs?: number },
|
|
316
|
+
options?: { busyTimeoutMs?: number; readonly?: boolean },
|
|
309
317
|
): DevFlowDatabase {
|
|
310
318
|
return new DevFlowDatabase(home, {
|
|
311
319
|
dbPath: getGlobalDevFlowDbPath(home),
|
|
312
320
|
busyTimeoutMs: options?.busyTimeoutMs,
|
|
321
|
+
readonly: options?.readonly,
|
|
313
322
|
});
|
|
314
323
|
}
|
|
315
324
|
|
|
325
|
+
export function openGlobalDevFlowReadOnlyDatabase(
|
|
326
|
+
home = homedir(),
|
|
327
|
+
options?: { busyTimeoutMs?: number },
|
|
328
|
+
): DevFlowDatabase {
|
|
329
|
+
return openGlobalDevFlowDatabase(home, { ...options, readonly: true });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
interface InitializedDatabaseIdentity {
|
|
333
|
+
dev: number;
|
|
334
|
+
ino: number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Schema setup is process-owned. Hook daemons open short-lived connections for
|
|
338
|
+
// bounded transactions, but replaying the full idempotent DDL on every open is
|
|
339
|
+
// still expensive and serializes concurrent host requests. An inode identity
|
|
340
|
+
// keeps the cache safe when a test, repair, or user replaces the database file.
|
|
341
|
+
const initializedDatabaseFiles = new Map<string, InitializedDatabaseIdentity>();
|
|
342
|
+
|
|
343
|
+
function getDatabaseIdentity(path: string): InitializedDatabaseIdentity | null {
|
|
344
|
+
try {
|
|
345
|
+
const stats = statSync(path);
|
|
346
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
316
352
|
export class DevFlowDatabase {
|
|
317
353
|
private db: NodeSqliteDatabase;
|
|
318
354
|
|
|
319
|
-
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number }) {
|
|
355
|
+
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number; readonly?: boolean }) {
|
|
320
356
|
let dbPath: string;
|
|
321
357
|
if (opts?.dbPath) {
|
|
322
358
|
const dir = dirname(opts.dbPath);
|
|
323
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
359
|
+
if (!existsSync(dir) && !opts.readonly) mkdirSync(dir, { recursive: true });
|
|
324
360
|
dbPath = opts.dbPath;
|
|
325
361
|
} else {
|
|
326
362
|
const devflowDir = join(projectRoot, '.devflow');
|
|
327
|
-
if (!existsSync(devflowDir)) mkdirSync(devflowDir, { recursive: true });
|
|
363
|
+
if (!existsSync(devflowDir) && !opts?.readonly) mkdirSync(devflowDir, { recursive: true });
|
|
328
364
|
dbPath = join(devflowDir, 'devflow.db');
|
|
329
365
|
}
|
|
330
|
-
|
|
366
|
+
if (opts?.readonly && !existsSync(dbPath)) throw new Error(`DEVFLOW_DATABASE_NOT_FOUND:${dbPath}`);
|
|
367
|
+
this.db = new NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs, opts?.readonly === true);
|
|
331
368
|
|
|
332
|
-
this.db.exec('PRAGMA journal_mode = WAL');
|
|
333
369
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
370
|
+
if (opts?.readonly) return;
|
|
334
371
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
335
|
-
|
|
336
|
-
|
|
372
|
+
const databaseIdentity = getDatabaseIdentity(dbPath);
|
|
373
|
+
const initializedIdentity = initializedDatabaseFiles.get(dbPath);
|
|
374
|
+
const schemaInitialized = databaseIdentity !== null
|
|
375
|
+
&& initializedIdentity?.dev === databaseIdentity.dev
|
|
376
|
+
&& initializedIdentity.ino === databaseIdentity.ino;
|
|
377
|
+
if (!schemaInitialized) {
|
|
378
|
+
this.initializeSchema();
|
|
379
|
+
const currentIdentity = getDatabaseIdentity(dbPath);
|
|
380
|
+
if (currentIdentity) initializedDatabaseFiles.set(dbPath, currentIdentity);
|
|
381
|
+
}
|
|
337
382
|
}
|
|
338
383
|
|
|
339
384
|
private initializeSchema() {
|
|
@@ -575,6 +620,9 @@ export class DevFlowDatabase {
|
|
|
575
620
|
source_id TEXT NOT NULL, stage TEXT NOT NULL, rank INTEGER, raw_score REAL,
|
|
576
621
|
normalized_score REAL, final_score REAL, applicability TEXT NOT NULL DEFAULT '[]',
|
|
577
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,
|
|
578
626
|
payload TEXT NOT NULL DEFAULT '{}', created_at INTEGER NOT NULL
|
|
579
627
|
);
|
|
580
628
|
CREATE INDEX IF NOT EXISTS idx_retrieval_ledger_identity ON devflow_retrieval_ledger(
|
|
@@ -679,6 +727,40 @@ export class DevFlowDatabase {
|
|
|
679
727
|
CREATE INDEX IF NOT EXISTS idx_terminal_transition_turn
|
|
680
728
|
ON devflow_terminal_transitions(project_root, session_id, turn_id, sequence);
|
|
681
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
|
+
|
|
682
764
|
CREATE TABLE IF NOT EXISTS devflow_transcript_checkpoints (
|
|
683
765
|
id TEXT PRIMARY KEY,
|
|
684
766
|
project_root TEXT NOT NULL,
|
|
@@ -832,9 +914,11 @@ export class DevFlowDatabase {
|
|
|
832
914
|
project_root TEXT NOT NULL,
|
|
833
915
|
session_id TEXT,
|
|
834
916
|
turn_id TEXT,
|
|
917
|
+
source_hash TEXT,
|
|
918
|
+
task_spec_hash TEXT,
|
|
835
919
|
payload TEXT NOT NULL,
|
|
836
920
|
state TEXT NOT NULL DEFAULT 'pending'
|
|
837
|
-
CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter')),
|
|
921
|
+
CHECK(state IN ('pending', 'leased', 'completed', 'failed', 'dead_letter', 'cancelled')),
|
|
838
922
|
attempts INTEGER NOT NULL DEFAULT 0 CHECK(attempts >= 0),
|
|
839
923
|
max_attempts INTEGER NOT NULL DEFAULT 5 CHECK(max_attempts > 0),
|
|
840
924
|
lease_owner TEXT,
|
|
@@ -844,7 +928,11 @@ export class DevFlowDatabase {
|
|
|
844
928
|
error_message TEXT,
|
|
845
929
|
created_at INTEGER NOT NULL,
|
|
846
930
|
updated_at INTEGER NOT NULL,
|
|
847
|
-
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
|
|
848
936
|
);
|
|
849
937
|
|
|
850
938
|
CREATE INDEX IF NOT EXISTS idx_work_items_ready
|
|
@@ -854,6 +942,16 @@ export class DevFlowDatabase {
|
|
|
854
942
|
CREATE INDEX IF NOT EXISTS idx_work_items_completed
|
|
855
943
|
ON devflow_work_items(project_root, completed_at DESC);
|
|
856
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
|
+
|
|
857
955
|
CREATE TABLE IF NOT EXISTS devflow_session_closures (
|
|
858
956
|
session_id TEXT NOT NULL,
|
|
859
957
|
project_root TEXT NOT NULL,
|
|
@@ -1125,6 +1223,19 @@ export class DevFlowDatabase {
|
|
|
1125
1223
|
try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN action_satisfied_at INTEGER'); } catch {}
|
|
1126
1224
|
try { this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN action_degradation_json TEXT'); } catch {}
|
|
1127
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 {}
|
|
1128
1239
|
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
1129
1240
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
1130
1241
|
|
|
@@ -1550,16 +1661,30 @@ export class DevFlowDatabase {
|
|
|
1550
1661
|
}));
|
|
1551
1662
|
}
|
|
1552
1663
|
|
|
1553
|
-
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
1664
|
+
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
1665
|
+
eventId: string;
|
|
1666
|
+
executionId: string;
|
|
1667
|
+
timestamp: number;
|
|
1668
|
+
duration: number;
|
|
1669
|
+
input: Record<string, unknown>;
|
|
1670
|
+
} | null {
|
|
1554
1671
|
const row = this.db.prepare(`
|
|
1555
|
-
SELECT event_id, timestamp, duration
|
|
1672
|
+
SELECT event_id, execution_id, timestamp, duration, input
|
|
1556
1673
|
FROM tool_call_events
|
|
1557
1674
|
WHERE session_id = ? AND tool_use_id = ?
|
|
1558
1675
|
ORDER BY timestamp DESC
|
|
1559
1676
|
LIMIT 1
|
|
1560
|
-
`).get(sessionId, toolUseId) as {
|
|
1677
|
+
`).get(sessionId, toolUseId) as {
|
|
1678
|
+
event_id: string; execution_id: string; timestamp: number; duration: number; input: string | null;
|
|
1679
|
+
} | undefined;
|
|
1561
1680
|
return row
|
|
1562
|
-
? {
|
|
1681
|
+
? {
|
|
1682
|
+
eventId: row.event_id,
|
|
1683
|
+
executionId: row.execution_id,
|
|
1684
|
+
timestamp: row.timestamp,
|
|
1685
|
+
duration: row.duration ?? 0,
|
|
1686
|
+
input: parseJsonObject(row.input) ?? {},
|
|
1687
|
+
}
|
|
1563
1688
|
: null;
|
|
1564
1689
|
}
|
|
1565
1690
|
|
|
@@ -2959,7 +3084,8 @@ export class DevFlowDatabase {
|
|
|
2959
3084
|
enqueueWork(input: EnqueueWorkInput): WorkItemRecord {
|
|
2960
3085
|
if (!input.idempotencyKey.trim()) throw new Error('Work idempotency key is required');
|
|
2961
3086
|
if (!input.projectRoot.trim()) throw new Error('Work project root is required');
|
|
2962
|
-
const maxAttempts = input.maxAttempts
|
|
3087
|
+
const maxAttempts = input.maxAttempts
|
|
3088
|
+
?? (input.kind === 'knowledge.ingest' || input.kind === 'knowledge.index_refresh' ? 3 : 5);
|
|
2963
3089
|
if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) {
|
|
2964
3090
|
throw new Error('Work maxAttempts must be a positive safe integer');
|
|
2965
3091
|
}
|
|
@@ -2972,10 +3098,10 @@ export class DevFlowDatabase {
|
|
|
2972
3098
|
|
|
2973
3099
|
const row = this.db.prepare(`
|
|
2974
3100
|
INSERT INTO devflow_work_items (
|
|
2975
|
-
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,
|
|
2976
3102
|
state, attempts, max_attempts, lease_owner, lease_expires_at,
|
|
2977
3103
|
next_attempt_at, error_category, error_message, created_at, updated_at, completed_at
|
|
2978
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
|
|
3104
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, ?, NULL, NULL, ?, ?, NULL)
|
|
2979
3105
|
ON CONFLICT(idempotency_key) DO UPDATE SET
|
|
2980
3106
|
state = CASE
|
|
2981
3107
|
WHEN devflow_work_items.state = 'dead_letter'
|
|
@@ -3016,6 +3142,8 @@ export class DevFlowDatabase {
|
|
|
3016
3142
|
input.projectRoot,
|
|
3017
3143
|
input.sessionId ?? null,
|
|
3018
3144
|
input.turnId ?? null,
|
|
3145
|
+
input.sourceHash ?? null,
|
|
3146
|
+
input.taskSpecHash ?? null,
|
|
3019
3147
|
JSON.stringify(input.payload ?? null),
|
|
3020
3148
|
maxAttempts,
|
|
3021
3149
|
nextAttemptAt,
|
|
@@ -3033,6 +3161,53 @@ export class DevFlowDatabase {
|
|
|
3033
3161
|
return row ? this.mapWorkItem(row) : null;
|
|
3034
3162
|
}
|
|
3035
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
|
+
|
|
3036
3211
|
requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord {
|
|
3037
3212
|
if (!input.sessionId.trim()) throw new Error('Session closure requires a session ID');
|
|
3038
3213
|
if (!input.projectRoot.trim()) throw new Error('Session closure requires a project root');
|
|
@@ -3060,7 +3235,8 @@ export class DevFlowDatabase {
|
|
|
3060
3235
|
SELECT COUNT(*) AS count
|
|
3061
3236
|
FROM devflow_work_items
|
|
3062
3237
|
WHERE project_root = ? AND session_id = ?
|
|
3063
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
3238
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
3239
|
+
AND attempts < max_attempts
|
|
3064
3240
|
`).get(input.projectRoot, input.sessionId) as { count?: number };
|
|
3065
3241
|
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
3066
3242
|
this.db.prepare(`
|
|
@@ -3142,6 +3318,7 @@ export class DevFlowDatabase {
|
|
|
3142
3318
|
throw new Error('Work leaseMs must be a positive safe integer');
|
|
3143
3319
|
}
|
|
3144
3320
|
if (input.kinds?.length === 0) return [];
|
|
3321
|
+
if (input.workItemIds?.length === 0) return [];
|
|
3145
3322
|
|
|
3146
3323
|
const now = input.now ?? Date.now();
|
|
3147
3324
|
const leaseExpiresAt = now + input.leaseMs;
|
|
@@ -3150,9 +3327,13 @@ export class DevFlowDatabase {
|
|
|
3150
3327
|
}
|
|
3151
3328
|
const limit = Math.min(Math.floor(input.limit), 1_000);
|
|
3152
3329
|
const kinds = input.kinds ? [...new Set(input.kinds)] : undefined;
|
|
3330
|
+
const workItemIds = input.workItemIds ? [...new Set(input.workItemIds)] : undefined;
|
|
3153
3331
|
const kindClause = kinds
|
|
3154
3332
|
? `AND kind IN (${kinds.map(() => '?').join(', ')})`
|
|
3155
3333
|
: '';
|
|
3334
|
+
const workItemClause = workItemIds
|
|
3335
|
+
? `AND id IN (${workItemIds.map(() => '?').join(', ')})`
|
|
3336
|
+
: '';
|
|
3156
3337
|
|
|
3157
3338
|
this.db.exec('BEGIN IMMEDIATE');
|
|
3158
3339
|
try {
|
|
@@ -3166,9 +3347,10 @@ export class DevFlowDatabase {
|
|
|
3166
3347
|
AND next_attempt_at <= ?
|
|
3167
3348
|
AND attempts < max_attempts
|
|
3168
3349
|
${kindClause}
|
|
3350
|
+
${workItemClause}
|
|
3169
3351
|
ORDER BY next_attempt_at ASC, created_at ASC, id ASC
|
|
3170
3352
|
LIMIT ?
|
|
3171
|
-
`).all(input.projectRoot, now, ...(kinds ?? []), limit) as Array<{
|
|
3353
|
+
`).all(input.projectRoot, now, ...(kinds ?? []), ...(workItemIds ?? []), limit) as Array<{
|
|
3172
3354
|
id: string;
|
|
3173
3355
|
state: WorkState;
|
|
3174
3356
|
}>;
|
|
@@ -3329,8 +3511,10 @@ export class DevFlowDatabase {
|
|
|
3329
3511
|
projectRoot: row.project_root,
|
|
3330
3512
|
sessionId: row.session_id ?? undefined,
|
|
3331
3513
|
turnId: row.turn_id ?? undefined,
|
|
3514
|
+
sourceHash: row.source_hash ?? undefined,
|
|
3515
|
+
taskSpecHash: row.task_spec_hash ?? undefined,
|
|
3332
3516
|
payload: parseJson(row.payload),
|
|
3333
|
-
state: row.state,
|
|
3517
|
+
state: row.cancelled_at != null ? 'cancelled' : row.state,
|
|
3334
3518
|
attempts: Number(row.attempts),
|
|
3335
3519
|
maxAttempts: Number(row.max_attempts),
|
|
3336
3520
|
leaseOwner: row.lease_owner ?? undefined,
|
|
@@ -3341,6 +3525,10 @@ export class DevFlowDatabase {
|
|
|
3341
3525
|
createdAt: Number(row.created_at),
|
|
3342
3526
|
updatedAt: Number(row.updated_at),
|
|
3343
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),
|
|
3344
3532
|
};
|
|
3345
3533
|
}
|
|
3346
3534
|
|
|
@@ -3378,7 +3566,8 @@ export class DevFlowDatabase {
|
|
|
3378
3566
|
SELECT COUNT(*) AS count
|
|
3379
3567
|
FROM devflow_work_items
|
|
3380
3568
|
WHERE project_root = ? AND session_id = ?
|
|
3381
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
3569
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
3570
|
+
AND attempts < max_attempts
|
|
3382
3571
|
AND (? IS NULL OR id <> ?)
|
|
3383
3572
|
`).get(
|
|
3384
3573
|
projectRoot,
|
|
@@ -3386,25 +3575,7 @@ export class DevFlowDatabase {
|
|
|
3386
3575
|
excludingWorkItemId ?? null,
|
|
3387
3576
|
excludingWorkItemId ?? null,
|
|
3388
3577
|
) as { count?: number };
|
|
3389
|
-
|
|
3390
|
-
SELECT COUNT(*) AS count
|
|
3391
|
-
FROM devflow_session_obligations
|
|
3392
|
-
WHERE project_root = ? AND session_id = ?
|
|
3393
|
-
AND state IN ('open', 'degraded')
|
|
3394
|
-
`).get(projectRoot, sessionId) as { count?: number };
|
|
3395
|
-
const legacyTurns = this.db.prepare(`
|
|
3396
|
-
SELECT COUNT(*) AS count
|
|
3397
|
-
FROM devflow_memory_turns t
|
|
3398
|
-
WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
|
|
3399
|
-
AND NOT EXISTS (
|
|
3400
|
-
SELECT 1 FROM devflow_session_obligations o
|
|
3401
|
-
WHERE o.project_root = t.project_root AND o.session_id = t.session_id
|
|
3402
|
-
AND o.obligation_id = 'memory:' || t.turn_id
|
|
3403
|
-
)
|
|
3404
|
-
`).get(projectRoot, sessionId) as { count?: number };
|
|
3405
|
-
return Number(work?.count ?? 0)
|
|
3406
|
-
+ Number(obligations?.count ?? 0)
|
|
3407
|
-
+ Number(legacyTurns?.count ?? 0);
|
|
3578
|
+
return Number(work?.count ?? 0);
|
|
3408
3579
|
}
|
|
3409
3580
|
|
|
3410
3581
|
// ---- Hook Lifecycle ----
|
|
@@ -3769,6 +3940,38 @@ export class DevFlowDatabase {
|
|
|
3769
3940
|
} : null;
|
|
3770
3941
|
}
|
|
3771
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
|
+
|
|
3772
3975
|
recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean {
|
|
3773
3976
|
return this.db.prepare(`
|
|
3774
3977
|
INSERT OR IGNORE INTO devflow_context_selection_events
|
|
@@ -3801,7 +4004,8 @@ export class DevFlowDatabase {
|
|
|
3801
4004
|
id, project_root, session_id, execution_id, turn_id, request_id, context_receipt,
|
|
3802
4005
|
source_type, source_id, stage, rank, raw_score, normalized_score, final_score,
|
|
3803
4006
|
applicability, reason, tool_evidence, verification_receipt, payload, created_at
|
|
3804
|
-
|
|
4007
|
+
, schema_version, task_spec_hash, actor, source_version, source_content_hash, evidence_ids, reason_code
|
|
4008
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3805
4009
|
`).run(
|
|
3806
4010
|
event.id, event.projectRoot, event.sessionId, event.executionId ?? null, event.turnId ?? null,
|
|
3807
4011
|
event.requestId, event.contextReceipt, event.sourceType, event.sourceId, event.stage,
|
|
@@ -3809,6 +4013,9 @@ export class DevFlowDatabase {
|
|
|
3809
4013
|
event.finalScore ?? null, JSON.stringify(event.applicability), event.reason ?? null,
|
|
3810
4014
|
JSON.stringify(event.toolEvidence), event.verificationReceipt ?? null,
|
|
3811
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,
|
|
3812
4019
|
).changes > 0;
|
|
3813
4020
|
}
|
|
3814
4021
|
|
|
@@ -4044,6 +4251,123 @@ export class DevFlowDatabase {
|
|
|
4044
4251
|
return this.getTerminalTransition(record.receiptId)!;
|
|
4045
4252
|
}
|
|
4046
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
|
+
|
|
4047
4371
|
getTerminalTransition(receiptId: string): TerminalTransitionRecord | null {
|
|
4048
4372
|
const row = this.db.prepare(`
|
|
4049
4373
|
SELECT * FROM devflow_terminal_transitions WHERE receipt_id = ?
|
|
@@ -4115,7 +4439,11 @@ export class DevFlowDatabase {
|
|
|
4115
4439
|
id: row.id, projectRoot: row.project_root, sessionId: row.session_id,
|
|
4116
4440
|
executionId: row.execution_id ?? undefined, turnId: row.turn_id ?? undefined,
|
|
4117
4441
|
requestId: row.request_id, contextReceipt: row.context_receipt,
|
|
4118
|
-
|
|
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,
|
|
4119
4447
|
rank: row.rank ?? undefined, rawScore: row.raw_score ?? undefined,
|
|
4120
4448
|
normalizedScore: row.normalized_score ?? undefined, finalScore: row.final_score ?? undefined,
|
|
4121
4449
|
applicability: parseJsonStringArray(row.applicability), reason: row.reason ?? undefined,
|
|
@@ -4330,6 +4658,23 @@ export class DevFlowDatabase {
|
|
|
4330
4658
|
);
|
|
4331
4659
|
if (!existing) throw new Error(`Session obligation ${input.obligationId} does not exist`);
|
|
4332
4660
|
if (existing.state !== 'open') {
|
|
4661
|
+
if (existing.state === 'degraded' && input.state === 'satisfied' && input.receiptId) {
|
|
4662
|
+
const resolvedAt = input.resolvedAt ?? Date.now();
|
|
4663
|
+
this.db.prepare(`
|
|
4664
|
+
UPDATE devflow_session_obligations
|
|
4665
|
+
SET state = 'satisfied', receipt_id = ?, reason = ?, resolved_at = ?, updated_at = ?
|
|
4666
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'degraded'
|
|
4667
|
+
`).run(
|
|
4668
|
+
input.receiptId,
|
|
4669
|
+
input.reason ?? 'superseded_by_corrected_evidence',
|
|
4670
|
+
resolvedAt,
|
|
4671
|
+
resolvedAt,
|
|
4672
|
+
input.projectRoot,
|
|
4673
|
+
input.sessionId,
|
|
4674
|
+
input.obligationId,
|
|
4675
|
+
);
|
|
4676
|
+
return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId)!;
|
|
4677
|
+
}
|
|
4333
4678
|
const sameResolution = existing.state === input.state
|
|
4334
4679
|
&& (input.receiptId === undefined || existing.receiptId === input.receiptId);
|
|
4335
4680
|
if (sameResolution) return existing;
|
|
@@ -4417,20 +4762,44 @@ export class DevFlowDatabase {
|
|
|
4417
4762
|
return turn;
|
|
4418
4763
|
}
|
|
4419
4764
|
|
|
4765
|
+
updateCommittedMemoryTurnProjection(input: {
|
|
4766
|
+
turnId: string;
|
|
4767
|
+
memoryIds: string[];
|
|
4768
|
+
reason?: string;
|
|
4769
|
+
}): MemoryTurnRecord {
|
|
4770
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
4771
|
+
this.db.prepare(`
|
|
4772
|
+
UPDATE devflow_memory_turns
|
|
4773
|
+
SET memory_ids = ?, reason = COALESCE(?, reason)
|
|
4774
|
+
WHERE turn_id = ? AND status = 'committed' AND source = 'explicit_intent'
|
|
4775
|
+
`).run(
|
|
4776
|
+
JSON.stringify([...new Set(input.memoryIds)]),
|
|
4777
|
+
input.reason ?? null,
|
|
4778
|
+
turnId,
|
|
4779
|
+
);
|
|
4780
|
+
const turn = this.getMemoryTurn(turnId);
|
|
4781
|
+
if (!turn || turn.status !== 'committed') {
|
|
4782
|
+
throw new Error(`Committed explicit memory turn ${turnId} does not exist`);
|
|
4783
|
+
}
|
|
4784
|
+
return turn;
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4420
4787
|
skipMemoryTurn(input: {
|
|
4421
4788
|
turnId: string;
|
|
4422
4789
|
receiptId: string;
|
|
4423
4790
|
reason: string;
|
|
4791
|
+
source?: string;
|
|
4424
4792
|
decidedAt?: number;
|
|
4425
4793
|
}): MemoryTurnRecord {
|
|
4426
4794
|
const turnId = normalizeTurnId(input.turnId);
|
|
4427
4795
|
this.db.prepare(`
|
|
4428
4796
|
UPDATE devflow_memory_turns
|
|
4429
|
-
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source =
|
|
4797
|
+
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = ?,
|
|
4430
4798
|
reason = ?, decided_at = ?
|
|
4431
4799
|
WHERE turn_id = ? AND status = 'pending'
|
|
4432
4800
|
`).run(
|
|
4433
4801
|
input.receiptId,
|
|
4802
|
+
input.source ?? 'host_skip',
|
|
4434
4803
|
input.reason,
|
|
4435
4804
|
input.decidedAt ?? Date.now(),
|
|
4436
4805
|
turnId,
|