@devflow-tools/database 0.17.1 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/database.d.ts +12 -0
- package/dist/database.js +80 -29
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/node-sqlite.d.ts +1 -1
- package/dist/node-sqlite.js +8 -4
- package/dist/work-queue.d.ts +1 -0
- package/package.json +1 -1
- package/src/database.ts +115 -35
- package/src/index.ts +1 -0
- package/src/node-sqlite.ts +8 -4
- package/src/work-queue.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.17.2](https://github.com/shilongfeicool/dev-flow/compare/v0.17.1...v0.17.2) (2026-08-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **reliability:** close semantic lifecycle gaps ([08bae87](https://github.com/shilongfeicool/dev-flow/commit/08bae8742c5907dffdd476679e70c86cc373a7e8))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **reliability:** strengthen semantic retrieval closure ([b979ab3](https://github.com/shilongfeicool/dev-flow/commit/b979ab3ec790e3d7119db512902db90a9567667d))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [0.17.1](https://github.com/shilongfeicool/dev-flow/compare/v0.16.28...v0.17.1) (2026-07-31)
|
|
7
23
|
|
|
8
24
|
|
package/dist/database.d.ts
CHANGED
|
@@ -174,12 +174,17 @@ export interface HookFallbackRecord {
|
|
|
174
174
|
export declare function getGlobalDevFlowDbPath(home?: string): string;
|
|
175
175
|
export declare function openGlobalDevFlowDatabase(home?: string, options?: {
|
|
176
176
|
busyTimeoutMs?: number;
|
|
177
|
+
readonly?: boolean;
|
|
178
|
+
}): DevFlowDatabase;
|
|
179
|
+
export declare function openGlobalDevFlowReadOnlyDatabase(home?: string, options?: {
|
|
180
|
+
busyTimeoutMs?: number;
|
|
177
181
|
}): DevFlowDatabase;
|
|
178
182
|
export declare class DevFlowDatabase {
|
|
179
183
|
private db;
|
|
180
184
|
constructor(projectRoot: string, opts?: {
|
|
181
185
|
dbPath?: string;
|
|
182
186
|
busyTimeoutMs?: number;
|
|
187
|
+
readonly?: boolean;
|
|
183
188
|
});
|
|
184
189
|
private initializeSchema;
|
|
185
190
|
insertRun(run: any): void;
|
|
@@ -213,8 +218,10 @@ export declare class DevFlowDatabase {
|
|
|
213
218
|
listToolCallEventsBySession(sessionId: string): any[];
|
|
214
219
|
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
215
220
|
eventId: string;
|
|
221
|
+
executionId: string;
|
|
216
222
|
timestamp: number;
|
|
217
223
|
duration: number;
|
|
224
|
+
input: Record<string, unknown>;
|
|
218
225
|
} | null;
|
|
219
226
|
listToolCallEventsBySessions(sessionIds: string[]): Record<string, any[]>;
|
|
220
227
|
insertSkillExecution(params: {
|
|
@@ -499,6 +506,11 @@ export declare class DevFlowDatabase {
|
|
|
499
506
|
reason?: string;
|
|
500
507
|
decidedAt?: number;
|
|
501
508
|
}): MemoryTurnRecord;
|
|
509
|
+
updateCommittedMemoryTurnProjection(input: {
|
|
510
|
+
turnId: string;
|
|
511
|
+
memoryIds: string[];
|
|
512
|
+
reason?: string;
|
|
513
|
+
}): MemoryTurnRecord;
|
|
502
514
|
skipMemoryTurn(input: {
|
|
503
515
|
turnId: string;
|
|
504
516
|
receiptId: string;
|
package/dist/database.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DevFlowDatabase = void 0;
|
|
4
4
|
exports.getGlobalDevFlowDbPath = getGlobalDevFlowDbPath;
|
|
5
5
|
exports.openGlobalDevFlowDatabase = openGlobalDevFlowDatabase;
|
|
6
|
+
exports.openGlobalDevFlowReadOnlyDatabase = openGlobalDevFlowReadOnlyDatabase;
|
|
6
7
|
const node_sqlite_1 = require("./node-sqlite");
|
|
7
8
|
const path_1 = require("path");
|
|
8
9
|
const fs_1 = require("fs");
|
|
@@ -62,28 +63,59 @@ function openGlobalDevFlowDatabase(home = (0, os_1.homedir)(), options) {
|
|
|
62
63
|
return new DevFlowDatabase(home, {
|
|
63
64
|
dbPath: getGlobalDevFlowDbPath(home),
|
|
64
65
|
busyTimeoutMs: options?.busyTimeoutMs,
|
|
66
|
+
readonly: options?.readonly,
|
|
65
67
|
});
|
|
66
68
|
}
|
|
69
|
+
function openGlobalDevFlowReadOnlyDatabase(home = (0, os_1.homedir)(), options) {
|
|
70
|
+
return openGlobalDevFlowDatabase(home, { ...options, readonly: true });
|
|
71
|
+
}
|
|
72
|
+
// Schema setup is process-owned. Hook daemons open short-lived connections for
|
|
73
|
+
// bounded transactions, but replaying the full idempotent DDL on every open is
|
|
74
|
+
// still expensive and serializes concurrent host requests. An inode identity
|
|
75
|
+
// keeps the cache safe when a test, repair, or user replaces the database file.
|
|
76
|
+
const initializedDatabaseFiles = new Map();
|
|
77
|
+
function getDatabaseIdentity(path) {
|
|
78
|
+
try {
|
|
79
|
+
const stats = (0, fs_1.statSync)(path);
|
|
80
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
class DevFlowDatabase {
|
|
68
87
|
constructor(projectRoot, opts) {
|
|
69
88
|
let dbPath;
|
|
70
89
|
if (opts?.dbPath) {
|
|
71
90
|
const dir = (0, path_1.dirname)(opts.dbPath);
|
|
72
|
-
if (!(0, fs_1.existsSync)(dir))
|
|
91
|
+
if (!(0, fs_1.existsSync)(dir) && !opts.readonly)
|
|
73
92
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
74
93
|
dbPath = opts.dbPath;
|
|
75
94
|
}
|
|
76
95
|
else {
|
|
77
96
|
const devflowDir = (0, path_1.join)(projectRoot, '.devflow');
|
|
78
|
-
if (!(0, fs_1.existsSync)(devflowDir))
|
|
97
|
+
if (!(0, fs_1.existsSync)(devflowDir) && !opts?.readonly)
|
|
79
98
|
(0, fs_1.mkdirSync)(devflowDir, { recursive: true });
|
|
80
99
|
dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
|
|
81
100
|
}
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
if (opts?.readonly && !(0, fs_1.existsSync)(dbPath))
|
|
102
|
+
throw new Error(`DEVFLOW_DATABASE_NOT_FOUND:${dbPath}`);
|
|
103
|
+
this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs, opts?.readonly === true);
|
|
84
104
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
105
|
+
if (opts?.readonly)
|
|
106
|
+
return;
|
|
85
107
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
86
|
-
|
|
108
|
+
const databaseIdentity = getDatabaseIdentity(dbPath);
|
|
109
|
+
const initializedIdentity = initializedDatabaseFiles.get(dbPath);
|
|
110
|
+
const schemaInitialized = databaseIdentity !== null
|
|
111
|
+
&& initializedIdentity?.dev === databaseIdentity.dev
|
|
112
|
+
&& initializedIdentity.ino === databaseIdentity.ino;
|
|
113
|
+
if (!schemaInitialized) {
|
|
114
|
+
this.initializeSchema();
|
|
115
|
+
const currentIdentity = getDatabaseIdentity(dbPath);
|
|
116
|
+
if (currentIdentity)
|
|
117
|
+
initializedDatabaseFiles.set(dbPath, currentIdentity);
|
|
118
|
+
}
|
|
87
119
|
}
|
|
88
120
|
initializeSchema() {
|
|
89
121
|
this.db.exec(`
|
|
@@ -1294,14 +1326,20 @@ class DevFlowDatabase {
|
|
|
1294
1326
|
}
|
|
1295
1327
|
getToolCallEventByToolUseId(sessionId, toolUseId) {
|
|
1296
1328
|
const row = this.db.prepare(`
|
|
1297
|
-
SELECT event_id, timestamp, duration
|
|
1329
|
+
SELECT event_id, execution_id, timestamp, duration, input
|
|
1298
1330
|
FROM tool_call_events
|
|
1299
1331
|
WHERE session_id = ? AND tool_use_id = ?
|
|
1300
1332
|
ORDER BY timestamp DESC
|
|
1301
1333
|
LIMIT 1
|
|
1302
1334
|
`).get(sessionId, toolUseId);
|
|
1303
1335
|
return row
|
|
1304
|
-
? {
|
|
1336
|
+
? {
|
|
1337
|
+
eventId: row.event_id,
|
|
1338
|
+
executionId: row.execution_id,
|
|
1339
|
+
timestamp: row.timestamp,
|
|
1340
|
+
duration: row.duration ?? 0,
|
|
1341
|
+
input: parseJsonObject(row.input) ?? {},
|
|
1342
|
+
}
|
|
1305
1343
|
: null;
|
|
1306
1344
|
}
|
|
1307
1345
|
listToolCallEventsBySessions(sessionIds) {
|
|
@@ -2457,7 +2495,8 @@ class DevFlowDatabase {
|
|
|
2457
2495
|
SELECT COUNT(*) AS count
|
|
2458
2496
|
FROM devflow_work_items
|
|
2459
2497
|
WHERE project_root = ? AND session_id = ?
|
|
2460
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
2498
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
2499
|
+
AND attempts < max_attempts
|
|
2461
2500
|
`).get(input.projectRoot, input.sessionId);
|
|
2462
2501
|
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2463
2502
|
this.db.prepare(`
|
|
@@ -2518,6 +2557,8 @@ class DevFlowDatabase {
|
|
|
2518
2557
|
}
|
|
2519
2558
|
if (input.kinds?.length === 0)
|
|
2520
2559
|
return [];
|
|
2560
|
+
if (input.workItemIds?.length === 0)
|
|
2561
|
+
return [];
|
|
2521
2562
|
const now = input.now ?? Date.now();
|
|
2522
2563
|
const leaseExpiresAt = now + input.leaseMs;
|
|
2523
2564
|
if (!Number.isSafeInteger(now) || !Number.isSafeInteger(leaseExpiresAt)) {
|
|
@@ -2525,9 +2566,13 @@ class DevFlowDatabase {
|
|
|
2525
2566
|
}
|
|
2526
2567
|
const limit = Math.min(Math.floor(input.limit), 1000);
|
|
2527
2568
|
const kinds = input.kinds ? [...new Set(input.kinds)] : undefined;
|
|
2569
|
+
const workItemIds = input.workItemIds ? [...new Set(input.workItemIds)] : undefined;
|
|
2528
2570
|
const kindClause = kinds
|
|
2529
2571
|
? `AND kind IN (${kinds.map(() => '?').join(', ')})`
|
|
2530
2572
|
: '';
|
|
2573
|
+
const workItemClause = workItemIds
|
|
2574
|
+
? `AND id IN (${workItemIds.map(() => '?').join(', ')})`
|
|
2575
|
+
: '';
|
|
2531
2576
|
this.db.exec('BEGIN IMMEDIATE');
|
|
2532
2577
|
try {
|
|
2533
2578
|
const candidates = this.db.prepare(`
|
|
@@ -2540,9 +2585,10 @@ class DevFlowDatabase {
|
|
|
2540
2585
|
AND next_attempt_at <= ?
|
|
2541
2586
|
AND attempts < max_attempts
|
|
2542
2587
|
${kindClause}
|
|
2588
|
+
${workItemClause}
|
|
2543
2589
|
ORDER BY next_attempt_at ASC, created_at ASC, id ASC
|
|
2544
2590
|
LIMIT ?
|
|
2545
|
-
`).all(input.projectRoot, now, ...(kinds ?? []), limit);
|
|
2591
|
+
`).all(input.projectRoot, now, ...(kinds ?? []), ...(workItemIds ?? []), limit);
|
|
2546
2592
|
const leased = [];
|
|
2547
2593
|
for (const candidate of candidates) {
|
|
2548
2594
|
const result = this.db.prepare(`
|
|
@@ -2714,28 +2760,11 @@ class DevFlowDatabase {
|
|
|
2714
2760
|
SELECT COUNT(*) AS count
|
|
2715
2761
|
FROM devflow_work_items
|
|
2716
2762
|
WHERE project_root = ? AND session_id = ?
|
|
2717
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
2763
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
2764
|
+
AND attempts < max_attempts
|
|
2718
2765
|
AND (? IS NULL OR id <> ?)
|
|
2719
2766
|
`).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
|
|
2720
|
-
|
|
2721
|
-
SELECT COUNT(*) AS count
|
|
2722
|
-
FROM devflow_session_obligations
|
|
2723
|
-
WHERE project_root = ? AND session_id = ?
|
|
2724
|
-
AND state IN ('open', 'degraded')
|
|
2725
|
-
`).get(projectRoot, sessionId);
|
|
2726
|
-
const legacyTurns = this.db.prepare(`
|
|
2727
|
-
SELECT COUNT(*) AS count
|
|
2728
|
-
FROM devflow_memory_turns t
|
|
2729
|
-
WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
|
|
2730
|
-
AND NOT EXISTS (
|
|
2731
|
-
SELECT 1 FROM devflow_session_obligations o
|
|
2732
|
-
WHERE o.project_root = t.project_root AND o.session_id = t.session_id
|
|
2733
|
-
AND o.obligation_id = 'memory:' || t.turn_id
|
|
2734
|
-
)
|
|
2735
|
-
`).get(projectRoot, sessionId);
|
|
2736
|
-
return Number(work?.count ?? 0)
|
|
2737
|
-
+ Number(obligations?.count ?? 0)
|
|
2738
|
-
+ Number(legacyTurns?.count ?? 0);
|
|
2767
|
+
return Number(work?.count ?? 0);
|
|
2739
2768
|
}
|
|
2740
2769
|
// ---- Hook Lifecycle ----
|
|
2741
2770
|
getHookReceipt(projectRoot) {
|
|
@@ -3459,6 +3488,15 @@ class DevFlowDatabase {
|
|
|
3459
3488
|
if (!existing)
|
|
3460
3489
|
throw new Error(`Session obligation ${input.obligationId} does not exist`);
|
|
3461
3490
|
if (existing.state !== 'open') {
|
|
3491
|
+
if (existing.state === 'degraded' && input.state === 'satisfied' && input.receiptId) {
|
|
3492
|
+
const resolvedAt = input.resolvedAt ?? Date.now();
|
|
3493
|
+
this.db.prepare(`
|
|
3494
|
+
UPDATE devflow_session_obligations
|
|
3495
|
+
SET state = 'satisfied', receipt_id = ?, reason = ?, resolved_at = ?, updated_at = ?
|
|
3496
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'degraded'
|
|
3497
|
+
`).run(input.receiptId, input.reason ?? 'superseded_by_corrected_evidence', resolvedAt, resolvedAt, input.projectRoot, input.sessionId, input.obligationId);
|
|
3498
|
+
return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
|
|
3499
|
+
}
|
|
3462
3500
|
const sameResolution = existing.state === input.state
|
|
3463
3501
|
&& (input.receiptId === undefined || existing.receiptId === input.receiptId);
|
|
3464
3502
|
if (sameResolution)
|
|
@@ -3517,6 +3555,19 @@ class DevFlowDatabase {
|
|
|
3517
3555
|
});
|
|
3518
3556
|
return turn;
|
|
3519
3557
|
}
|
|
3558
|
+
updateCommittedMemoryTurnProjection(input) {
|
|
3559
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
3560
|
+
this.db.prepare(`
|
|
3561
|
+
UPDATE devflow_memory_turns
|
|
3562
|
+
SET memory_ids = ?, reason = COALESCE(?, reason)
|
|
3563
|
+
WHERE turn_id = ? AND status = 'committed' AND source = 'explicit_intent'
|
|
3564
|
+
`).run(JSON.stringify([...new Set(input.memoryIds)]), input.reason ?? null, turnId);
|
|
3565
|
+
const turn = this.getMemoryTurn(turnId);
|
|
3566
|
+
if (!turn || turn.status !== 'committed') {
|
|
3567
|
+
throw new Error(`Committed explicit memory turn ${turnId} does not exist`);
|
|
3568
|
+
}
|
|
3569
|
+
return turn;
|
|
3570
|
+
}
|
|
3520
3571
|
skipMemoryTurn(input) {
|
|
3521
3572
|
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
3522
3573
|
this.db.prepare(`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
|
|
1
|
+
export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, openGlobalDevFlowReadOnlyDatabase, } from './database';
|
|
2
2
|
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, ContextReceiptRecord, ContextSelectionEventRecord, PolicyVerificationBaselineRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
|
|
3
3
|
export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, } from './work-queue';
|
|
4
4
|
export type { RetrievalLedgerEventRecord, RetrievalLedgerSourceType, RetrievalLedgerStage, } from './retrieval-ledger';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.stableLearningJson = exports.mapLearningVersionRow = exports.mapLearningEvidenceRow = exports.mapLearningCandidateRow = exports.mapWorkflowWorkerRow = exports.mapWorkflowWorkerEventRow = exports.mapWorkflowMergeRow = exports.TERMINAL_WORKFLOW_WORKER_STATES = exports.LEGAL_WORKFLOW_WORKER_TRANSITIONS = exports.stableSemanticJson = exports.mapTranscriptCheckpointRow = exports.mapToolNameResolutionRow = exports.mapTerminalTransitionRow = exports.mapTaskIntentArtifactRow = exports.mapChannelQueryPlanRow = exports.assertTaskIdentity = exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
3
|
+
exports.stableLearningJson = exports.mapLearningVersionRow = exports.mapLearningEvidenceRow = exports.mapLearningCandidateRow = exports.mapWorkflowWorkerRow = exports.mapWorkflowWorkerEventRow = exports.mapWorkflowMergeRow = exports.TERMINAL_WORKFLOW_WORKER_STATES = exports.LEGAL_WORKFLOW_WORKER_TRANSITIONS = exports.stableSemanticJson = exports.mapTranscriptCheckpointRow = exports.mapToolNameResolutionRow = exports.mapTerminalTransitionRow = exports.mapTaskIntentArtifactRow = exports.mapChannelQueryPlanRow = exports.assertTaskIdentity = exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowReadOnlyDatabase = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
4
4
|
var database_1 = require("./database");
|
|
5
5
|
Object.defineProperty(exports, "DevFlowDatabase", { enumerable: true, get: function () { return database_1.DevFlowDatabase; } });
|
|
6
6
|
Object.defineProperty(exports, "getGlobalDevFlowDbPath", { enumerable: true, get: function () { return database_1.getGlobalDevFlowDbPath; } });
|
|
7
7
|
Object.defineProperty(exports, "openGlobalDevFlowDatabase", { enumerable: true, get: function () { return database_1.openGlobalDevFlowDatabase; } });
|
|
8
|
+
Object.defineProperty(exports, "openGlobalDevFlowReadOnlyDatabase", { enumerable: true, get: function () { return database_1.openGlobalDevFlowReadOnlyDatabase; } });
|
|
8
9
|
var host_actions_1 = require("./host-actions");
|
|
9
10
|
Object.defineProperty(exports, "hostActionReportsEqual", { enumerable: true, get: function () { return host_actions_1.hostActionReportsEqual; } });
|
|
10
11
|
Object.defineProperty(exports, "isHostActionState", { enumerable: true, get: function () { return host_actions_1.isHostActionState; } });
|
package/dist/node-sqlite.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Database, Statement } from './types';
|
|
2
2
|
export declare class NodeSqliteDatabase implements Database {
|
|
3
3
|
private db;
|
|
4
|
-
constructor(path: string, busyTimeoutMs?: number);
|
|
4
|
+
constructor(path: string, busyTimeoutMs?: number, readOnly?: boolean);
|
|
5
5
|
exec(sql: string): void;
|
|
6
6
|
prepare(sql: string): Statement;
|
|
7
7
|
transaction<T>(fn: () => T): T;
|
package/dist/node-sqlite.js
CHANGED
|
@@ -3,11 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.NodeSqliteDatabase = void 0;
|
|
4
4
|
const node_sqlite_1 = require("node:sqlite");
|
|
5
5
|
class NodeSqliteDatabase {
|
|
6
|
-
constructor(path, busyTimeoutMs = 10000) {
|
|
7
|
-
this.db =
|
|
6
|
+
constructor(path, busyTimeoutMs = 10000, readOnly = false) {
|
|
7
|
+
this.db = readOnly
|
|
8
|
+
? new node_sqlite_1.DatabaseSync(path, { readOnly: true })
|
|
9
|
+
: new node_sqlite_1.DatabaseSync(path);
|
|
8
10
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
if (!readOnly) {
|
|
12
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
13
|
+
this.db.exec('PRAGMA synchronous = NORMAL');
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
exec(sql) {
|
|
13
17
|
this.db.exec(sql);
|
package/dist/work-queue.d.ts
CHANGED
package/package.json
CHANGED
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 {
|
|
@@ -305,35 +305,72 @@ export function getGlobalDevFlowDbPath(home = homedir()): string {
|
|
|
305
305
|
|
|
306
306
|
export function openGlobalDevFlowDatabase(
|
|
307
307
|
home = homedir(),
|
|
308
|
-
options?: { busyTimeoutMs?: number },
|
|
308
|
+
options?: { busyTimeoutMs?: number; readonly?: boolean },
|
|
309
309
|
): DevFlowDatabase {
|
|
310
310
|
return new DevFlowDatabase(home, {
|
|
311
311
|
dbPath: getGlobalDevFlowDbPath(home),
|
|
312
312
|
busyTimeoutMs: options?.busyTimeoutMs,
|
|
313
|
+
readonly: options?.readonly,
|
|
313
314
|
});
|
|
314
315
|
}
|
|
315
316
|
|
|
317
|
+
export function openGlobalDevFlowReadOnlyDatabase(
|
|
318
|
+
home = homedir(),
|
|
319
|
+
options?: { busyTimeoutMs?: number },
|
|
320
|
+
): DevFlowDatabase {
|
|
321
|
+
return openGlobalDevFlowDatabase(home, { ...options, readonly: true });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface InitializedDatabaseIdentity {
|
|
325
|
+
dev: number;
|
|
326
|
+
ino: number;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Schema setup is process-owned. Hook daemons open short-lived connections for
|
|
330
|
+
// bounded transactions, but replaying the full idempotent DDL on every open is
|
|
331
|
+
// still expensive and serializes concurrent host requests. An inode identity
|
|
332
|
+
// keeps the cache safe when a test, repair, or user replaces the database file.
|
|
333
|
+
const initializedDatabaseFiles = new Map<string, InitializedDatabaseIdentity>();
|
|
334
|
+
|
|
335
|
+
function getDatabaseIdentity(path: string): InitializedDatabaseIdentity | null {
|
|
336
|
+
try {
|
|
337
|
+
const stats = statSync(path);
|
|
338
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
316
344
|
export class DevFlowDatabase {
|
|
317
345
|
private db: NodeSqliteDatabase;
|
|
318
346
|
|
|
319
|
-
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number }) {
|
|
347
|
+
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number; readonly?: boolean }) {
|
|
320
348
|
let dbPath: string;
|
|
321
349
|
if (opts?.dbPath) {
|
|
322
350
|
const dir = dirname(opts.dbPath);
|
|
323
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
351
|
+
if (!existsSync(dir) && !opts.readonly) mkdirSync(dir, { recursive: true });
|
|
324
352
|
dbPath = opts.dbPath;
|
|
325
353
|
} else {
|
|
326
354
|
const devflowDir = join(projectRoot, '.devflow');
|
|
327
|
-
if (!existsSync(devflowDir)) mkdirSync(devflowDir, { recursive: true });
|
|
355
|
+
if (!existsSync(devflowDir) && !opts?.readonly) mkdirSync(devflowDir, { recursive: true });
|
|
328
356
|
dbPath = join(devflowDir, 'devflow.db');
|
|
329
357
|
}
|
|
330
|
-
|
|
358
|
+
if (opts?.readonly && !existsSync(dbPath)) throw new Error(`DEVFLOW_DATABASE_NOT_FOUND:${dbPath}`);
|
|
359
|
+
this.db = new NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs, opts?.readonly === true);
|
|
331
360
|
|
|
332
|
-
this.db.exec('PRAGMA journal_mode = WAL');
|
|
333
361
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
362
|
+
if (opts?.readonly) return;
|
|
334
363
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
335
|
-
|
|
336
|
-
|
|
364
|
+
const databaseIdentity = getDatabaseIdentity(dbPath);
|
|
365
|
+
const initializedIdentity = initializedDatabaseFiles.get(dbPath);
|
|
366
|
+
const schemaInitialized = databaseIdentity !== null
|
|
367
|
+
&& initializedIdentity?.dev === databaseIdentity.dev
|
|
368
|
+
&& initializedIdentity.ino === databaseIdentity.ino;
|
|
369
|
+
if (!schemaInitialized) {
|
|
370
|
+
this.initializeSchema();
|
|
371
|
+
const currentIdentity = getDatabaseIdentity(dbPath);
|
|
372
|
+
if (currentIdentity) initializedDatabaseFiles.set(dbPath, currentIdentity);
|
|
373
|
+
}
|
|
337
374
|
}
|
|
338
375
|
|
|
339
376
|
private initializeSchema() {
|
|
@@ -1550,16 +1587,30 @@ export class DevFlowDatabase {
|
|
|
1550
1587
|
}));
|
|
1551
1588
|
}
|
|
1552
1589
|
|
|
1553
|
-
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
1590
|
+
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
1591
|
+
eventId: string;
|
|
1592
|
+
executionId: string;
|
|
1593
|
+
timestamp: number;
|
|
1594
|
+
duration: number;
|
|
1595
|
+
input: Record<string, unknown>;
|
|
1596
|
+
} | null {
|
|
1554
1597
|
const row = this.db.prepare(`
|
|
1555
|
-
SELECT event_id, timestamp, duration
|
|
1598
|
+
SELECT event_id, execution_id, timestamp, duration, input
|
|
1556
1599
|
FROM tool_call_events
|
|
1557
1600
|
WHERE session_id = ? AND tool_use_id = ?
|
|
1558
1601
|
ORDER BY timestamp DESC
|
|
1559
1602
|
LIMIT 1
|
|
1560
|
-
`).get(sessionId, toolUseId) as {
|
|
1603
|
+
`).get(sessionId, toolUseId) as {
|
|
1604
|
+
event_id: string; execution_id: string; timestamp: number; duration: number; input: string | null;
|
|
1605
|
+
} | undefined;
|
|
1561
1606
|
return row
|
|
1562
|
-
? {
|
|
1607
|
+
? {
|
|
1608
|
+
eventId: row.event_id,
|
|
1609
|
+
executionId: row.execution_id,
|
|
1610
|
+
timestamp: row.timestamp,
|
|
1611
|
+
duration: row.duration ?? 0,
|
|
1612
|
+
input: parseJsonObject(row.input) ?? {},
|
|
1613
|
+
}
|
|
1563
1614
|
: null;
|
|
1564
1615
|
}
|
|
1565
1616
|
|
|
@@ -3060,7 +3111,8 @@ export class DevFlowDatabase {
|
|
|
3060
3111
|
SELECT COUNT(*) AS count
|
|
3061
3112
|
FROM devflow_work_items
|
|
3062
3113
|
WHERE project_root = ? AND session_id = ?
|
|
3063
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
3114
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
3115
|
+
AND attempts < max_attempts
|
|
3064
3116
|
`).get(input.projectRoot, input.sessionId) as { count?: number };
|
|
3065
3117
|
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
3066
3118
|
this.db.prepare(`
|
|
@@ -3142,6 +3194,7 @@ export class DevFlowDatabase {
|
|
|
3142
3194
|
throw new Error('Work leaseMs must be a positive safe integer');
|
|
3143
3195
|
}
|
|
3144
3196
|
if (input.kinds?.length === 0) return [];
|
|
3197
|
+
if (input.workItemIds?.length === 0) return [];
|
|
3145
3198
|
|
|
3146
3199
|
const now = input.now ?? Date.now();
|
|
3147
3200
|
const leaseExpiresAt = now + input.leaseMs;
|
|
@@ -3150,9 +3203,13 @@ export class DevFlowDatabase {
|
|
|
3150
3203
|
}
|
|
3151
3204
|
const limit = Math.min(Math.floor(input.limit), 1_000);
|
|
3152
3205
|
const kinds = input.kinds ? [...new Set(input.kinds)] : undefined;
|
|
3206
|
+
const workItemIds = input.workItemIds ? [...new Set(input.workItemIds)] : undefined;
|
|
3153
3207
|
const kindClause = kinds
|
|
3154
3208
|
? `AND kind IN (${kinds.map(() => '?').join(', ')})`
|
|
3155
3209
|
: '';
|
|
3210
|
+
const workItemClause = workItemIds
|
|
3211
|
+
? `AND id IN (${workItemIds.map(() => '?').join(', ')})`
|
|
3212
|
+
: '';
|
|
3156
3213
|
|
|
3157
3214
|
this.db.exec('BEGIN IMMEDIATE');
|
|
3158
3215
|
try {
|
|
@@ -3166,9 +3223,10 @@ export class DevFlowDatabase {
|
|
|
3166
3223
|
AND next_attempt_at <= ?
|
|
3167
3224
|
AND attempts < max_attempts
|
|
3168
3225
|
${kindClause}
|
|
3226
|
+
${workItemClause}
|
|
3169
3227
|
ORDER BY next_attempt_at ASC, created_at ASC, id ASC
|
|
3170
3228
|
LIMIT ?
|
|
3171
|
-
`).all(input.projectRoot, now, ...(kinds ?? []), limit) as Array<{
|
|
3229
|
+
`).all(input.projectRoot, now, ...(kinds ?? []), ...(workItemIds ?? []), limit) as Array<{
|
|
3172
3230
|
id: string;
|
|
3173
3231
|
state: WorkState;
|
|
3174
3232
|
}>;
|
|
@@ -3378,7 +3436,8 @@ export class DevFlowDatabase {
|
|
|
3378
3436
|
SELECT COUNT(*) AS count
|
|
3379
3437
|
FROM devflow_work_items
|
|
3380
3438
|
WHERE project_root = ? AND session_id = ?
|
|
3381
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
3439
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
3440
|
+
AND attempts < max_attempts
|
|
3382
3441
|
AND (? IS NULL OR id <> ?)
|
|
3383
3442
|
`).get(
|
|
3384
3443
|
projectRoot,
|
|
@@ -3386,25 +3445,7 @@ export class DevFlowDatabase {
|
|
|
3386
3445
|
excludingWorkItemId ?? null,
|
|
3387
3446
|
excludingWorkItemId ?? null,
|
|
3388
3447
|
) 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);
|
|
3448
|
+
return Number(work?.count ?? 0);
|
|
3408
3449
|
}
|
|
3409
3450
|
|
|
3410
3451
|
// ---- Hook Lifecycle ----
|
|
@@ -4330,6 +4371,23 @@ export class DevFlowDatabase {
|
|
|
4330
4371
|
);
|
|
4331
4372
|
if (!existing) throw new Error(`Session obligation ${input.obligationId} does not exist`);
|
|
4332
4373
|
if (existing.state !== 'open') {
|
|
4374
|
+
if (existing.state === 'degraded' && input.state === 'satisfied' && input.receiptId) {
|
|
4375
|
+
const resolvedAt = input.resolvedAt ?? Date.now();
|
|
4376
|
+
this.db.prepare(`
|
|
4377
|
+
UPDATE devflow_session_obligations
|
|
4378
|
+
SET state = 'satisfied', receipt_id = ?, reason = ?, resolved_at = ?, updated_at = ?
|
|
4379
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'degraded'
|
|
4380
|
+
`).run(
|
|
4381
|
+
input.receiptId,
|
|
4382
|
+
input.reason ?? 'superseded_by_corrected_evidence',
|
|
4383
|
+
resolvedAt,
|
|
4384
|
+
resolvedAt,
|
|
4385
|
+
input.projectRoot,
|
|
4386
|
+
input.sessionId,
|
|
4387
|
+
input.obligationId,
|
|
4388
|
+
);
|
|
4389
|
+
return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId)!;
|
|
4390
|
+
}
|
|
4333
4391
|
const sameResolution = existing.state === input.state
|
|
4334
4392
|
&& (input.receiptId === undefined || existing.receiptId === input.receiptId);
|
|
4335
4393
|
if (sameResolution) return existing;
|
|
@@ -4417,6 +4475,28 @@ export class DevFlowDatabase {
|
|
|
4417
4475
|
return turn;
|
|
4418
4476
|
}
|
|
4419
4477
|
|
|
4478
|
+
updateCommittedMemoryTurnProjection(input: {
|
|
4479
|
+
turnId: string;
|
|
4480
|
+
memoryIds: string[];
|
|
4481
|
+
reason?: string;
|
|
4482
|
+
}): MemoryTurnRecord {
|
|
4483
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
4484
|
+
this.db.prepare(`
|
|
4485
|
+
UPDATE devflow_memory_turns
|
|
4486
|
+
SET memory_ids = ?, reason = COALESCE(?, reason)
|
|
4487
|
+
WHERE turn_id = ? AND status = 'committed' AND source = 'explicit_intent'
|
|
4488
|
+
`).run(
|
|
4489
|
+
JSON.stringify([...new Set(input.memoryIds)]),
|
|
4490
|
+
input.reason ?? null,
|
|
4491
|
+
turnId,
|
|
4492
|
+
);
|
|
4493
|
+
const turn = this.getMemoryTurn(turnId);
|
|
4494
|
+
if (!turn || turn.status !== 'committed') {
|
|
4495
|
+
throw new Error(`Committed explicit memory turn ${turnId} does not exist`);
|
|
4496
|
+
}
|
|
4497
|
+
return turn;
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4420
4500
|
skipMemoryTurn(input: {
|
|
4421
4501
|
turnId: string;
|
|
4422
4502
|
receiptId: string;
|
package/src/index.ts
CHANGED
package/src/node-sqlite.ts
CHANGED
|
@@ -4,11 +4,15 @@ import type { Database, Statement, RunResult } from './types';
|
|
|
4
4
|
export class NodeSqliteDatabase implements Database {
|
|
5
5
|
private db: DatabaseSync;
|
|
6
6
|
|
|
7
|
-
constructor(path: string, busyTimeoutMs = 10000) {
|
|
8
|
-
this.db =
|
|
7
|
+
constructor(path: string, busyTimeoutMs = 10000, readOnly = false) {
|
|
8
|
+
this.db = readOnly
|
|
9
|
+
? new DatabaseSync(path, { readOnly: true })
|
|
10
|
+
: new DatabaseSync(path);
|
|
9
11
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
if (!readOnly) {
|
|
13
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
14
|
+
this.db.exec('PRAGMA synchronous = NORMAL');
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
exec(sql: string): void {
|