@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 CHANGED
@@ -3,6 +3,41 @@
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.3](https://github.com/shilongfeicool/dev-flow/compare/v0.17.2...v0.17.3) (2026-08-03)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **runtime:** enforce evidence-driven task truth ([6a8f84a](https://github.com/shilongfeicool/dev-flow/commit/6a8f84abf62666dfb7b8643fa88a5e9489940fce))
12
+
13
+
14
+ ### Features
15
+
16
+ * **evidence:** attribute consumed and verified retrieval ([65e5828](https://github.com/shilongfeicool/dev-flow/commit/65e5828de6d8455a2712ff78ea411aa2bf8be829))
17
+ * **runtime:** persist and reduce task events ([f1966c5](https://github.com/shilongfeicool/dev-flow/commit/f1966c5eaa6c023b88c4c52b3109657b7ad8cca1))
18
+ * **runtime:** shadow online task reduction ([c15088c](https://github.com/shilongfeicool/dev-flow/commit/c15088c9fc9434bba86bcbceeb107da36f2d9606))
19
+ * **semantic:** harden durable memory and knowledge work ([69eab16](https://github.com/shilongfeicool/dev-flow/commit/69eab16012ad7b3cb6a7a38ddcb1c7c1fc30054a))
20
+
21
+
22
+
23
+
24
+
25
+ ## [0.17.2](https://github.com/shilongfeicool/dev-flow/compare/v0.17.1...v0.17.2) (2026-08-01)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **reliability:** close semantic lifecycle gaps ([08bae87](https://github.com/shilongfeicool/dev-flow/commit/08bae8742c5907dffdd476679e70c86cc373a7e8))
31
+
32
+
33
+ ### Features
34
+
35
+ * **reliability:** strengthen semantic retrieval closure ([b979ab3](https://github.com/shilongfeicool/dev-flow/commit/b979ab3ec790e3d7119db512902db90a9567667d))
36
+
37
+
38
+
39
+
40
+
6
41
  ## [0.17.1](https://github.com/shilongfeicool/dev-flow/compare/v0.16.28...v0.17.1) (2026-07-31)
7
42
 
8
43
 
@@ -35,6 +35,21 @@ describe('retrieval ledger', () => {
35
35
  database.close();
36
36
  });
37
37
 
38
+ it('persists v1 provenance and reads legacy adopted rows as consumed', () => {
39
+ const database = createDatabase();
40
+ database.appendRetrievalLedgerEvent(event({
41
+ id: 'legacy-adopted', stage: 'adopted', taskSpecHash: 'spec:1', actor: 'tool:read',
42
+ sourceVersion: 'v1', sourceContentHash: 'sha256:1', evidenceIds: ['memory-a'],
43
+ reasonCode: 'structural_evidence_ref',
44
+ }));
45
+ expect(database.listRetrievalLedgerEvents({ projectRoot: '/project' })[0]).toMatchObject({
46
+ schemaVersion: 'retrieval-ledger-event.v1', stage: 'consumed', taskSpecHash: 'spec:1',
47
+ actor: 'tool:read', sourceVersion: 'v1', sourceContentHash: 'sha256:1',
48
+ evidenceIds: ['memory-a'], reasonCode: 'structural_evidence_ref',
49
+ });
50
+ database.close();
51
+ });
52
+
38
53
  function createDatabase(): DevFlowDatabase {
39
54
  const directory = mkdtempSync(join(tmpdir(), 'devflow-retrieval-ledger-'));
40
55
  directories.push(directory);
@@ -0,0 +1,73 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { DevFlowDatabase } from '../src/database';
6
+ import { stableTaskRuntimeHash, type TaskRuntimeEventRecord, type TaskRuntimeSnapshotRecord } from '../src/task-runtime';
7
+
8
+ const identity = {
9
+ projectRoot: '/project', projectId: 'project:1', hostId: 'claude-code',
10
+ sessionId: 'session:1', turnId: 'turn:1', requestId: 'request:1',
11
+ };
12
+
13
+ function event(overrides: Partial<TaskRuntimeEventRecord> = {}): TaskRuntimeEventRecord {
14
+ return {
15
+ schemaVersion: 'task-runtime-event.v1', producer: 'test', producerVersion: '1.0.0',
16
+ eventId: 'event:1', sequence: 1, identity, taskSpecHash: 'spec:1',
17
+ kind: 'task.spec.bound', payload: {}, createdAt: 1, ...overrides,
18
+ };
19
+ }
20
+
21
+ function snapshot(lastEventSequence = 1): TaskRuntimeSnapshotRecord {
22
+ const body = {
23
+ schemaVersion: 'task-runtime-snapshot.v1' as const,
24
+ producer: 'test', producerVersion: '1.0.0', identity, taskSpecHash: 'spec:1',
25
+ lastEventSequence,
26
+ artifacts: { requiredIdentityIds: [], resolvedIdentityIds: [], ambiguousIdentityIds: [] },
27
+ channels: {},
28
+ action: { requirement: 'not_required' as const, state: 'satisfied' as const },
29
+ verification: { requirement: 'not_required' as const, state: 'satisfied' as const },
30
+ memory: { requirement: 'required' as const, state: 'satisfied' as const },
31
+ durableWorkIds: [], terminal: 'completed' as const, runtimeHealth: 'healthy' as const,
32
+ updatedAt: lastEventSequence,
33
+ };
34
+ return { ...body, snapshotHash: stableTaskRuntimeHash(body) };
35
+ }
36
+
37
+ describe('DevFlowDatabase task runtime persistence', () => {
38
+ let directory: string;
39
+ let database: DevFlowDatabase;
40
+
41
+ beforeEach(() => {
42
+ directory = mkdtempSync(join(tmpdir(), 'devflow-task-runtime-'));
43
+ database = new DevFlowDatabase(directory);
44
+ });
45
+
46
+ afterEach(() => {
47
+ database.close();
48
+ rmSync(directory, { recursive: true, force: true });
49
+ });
50
+
51
+ it('makes identical event replay idempotent and rejects conflicting IDs or sequences', () => {
52
+ const first = database.appendTaskRuntimeEvent(event());
53
+ expect(database.appendTaskRuntimeEvent(event())).toEqual(first);
54
+ expect(database.listTaskRuntimeEvents('/project', 'session:1', 'turn:1')).toEqual([first]);
55
+
56
+ expect(() => database.appendTaskRuntimeEvent(event({ payload: { changed: true } })))
57
+ .toThrow('TASK_RUNTIME_EVENT_CONFLICT:event:1');
58
+ expect(() => database.appendTaskRuntimeEvent(event({ eventId: 'event:2' })))
59
+ .toThrow('TASK_RUNTIME_SEQUENCE_CONFLICT:1');
60
+ });
61
+
62
+ it('round-trips snapshots, rejects regression, and supports deterministic rebuild deletion', () => {
63
+ database.appendTaskRuntimeEvent(event());
64
+ const current = database.putTaskRuntimeSnapshot(snapshot(1));
65
+ expect(database.getTaskRuntimeSnapshot('/project', 'session:1', 'turn:1')).toEqual(current);
66
+ expect(() => database.putTaskRuntimeSnapshot(snapshot(0))).toThrow('TASK_RUNTIME_SNAPSHOT_REGRESSION');
67
+
68
+ expect(database.deleteTaskRuntimeSnapshot('/project', 'session:1', 'turn:1')).toBe(true);
69
+ expect(database.getTaskRuntimeSnapshot('/project', 'session:1', 'turn:1')).toBeNull();
70
+ expect(database.listTaskRuntimeEvents('/project', 'session:1', 'turn:1')).toEqual([event()]);
71
+ expect(database.putTaskRuntimeSnapshot(snapshot(1))).toEqual(current);
72
+ });
73
+ });
@@ -166,6 +166,40 @@ describe('DevFlowDatabase durable work queue', () => {
166
166
  });
167
167
  });
168
168
 
169
+ it('serializes project mutations, cancels stale work, and preserves dead-letter replay lineage', () => {
170
+ expect(database.acquireProjectMutationLease({
171
+ projectRoot, mutationKind: 'knowledge_publish', owner: 'worker-a', leaseMs: 100, now: 10,
172
+ })).toBe(true);
173
+ expect(database.acquireProjectMutationLease({
174
+ projectRoot, mutationKind: 'knowledge_publish', owner: 'worker-b', leaseMs: 100, now: 50,
175
+ })).toBe(false);
176
+ expect(database.acquireProjectMutationLease({
177
+ projectRoot, mutationKind: 'knowledge_publish', owner: 'worker-b', leaseMs: 100, now: 111,
178
+ })).toBe(true);
179
+
180
+ const stale = database.enqueueWork({
181
+ idempotencyKey: 'knowledge:stale', kind: 'knowledge.ingest', projectRoot,
182
+ sourceHash: 'source:v1', taskSpecHash: 'spec:v1', payload: {}, nextAttemptAt: 200,
183
+ });
184
+ expect(stale.maxAttempts).toBe(3);
185
+ database.leaseWork({ projectRoot, owner: 'worker-b', workItemIds: [stale.id], limit: 1, leaseMs: 100, now: 200 });
186
+ expect(database.cancelStaleWork({
187
+ id: stale.id, owner: 'worker-b', sourceHash: 'source:v2', taskSpecHash: 'spec:v1', now: 210,
188
+ })).toBe(true);
189
+ expect(database.getWorkById(stale.id)).toMatchObject({ state: 'cancelled', sourceHash: 'source:v1' });
190
+
191
+ const failed = database.enqueueWork({
192
+ idempotencyKey: 'knowledge:dead', kind: 'knowledge.ingest', projectRoot,
193
+ sourceHash: 'source:dead', payload: {}, maxAttempts: 1, nextAttemptAt: 300,
194
+ });
195
+ database.leaseWork({ projectRoot, owner: 'worker-b', workItemIds: [failed.id], limit: 1, leaseMs: 100, now: 300 });
196
+ database.retryWork(failed.id, 'worker-b', { category: 'semantic_invalid', message: 'empty' }, 310);
197
+ const replay = database.replayDeadLetterWork(failed.id, {
198
+ idempotencyKey: 'knowledge:dead:replay:1', sourceHash: 'source:fixed', now: 320,
199
+ });
200
+ expect(replay).toMatchObject({ state: 'pending', replayOfId: failed.id, replayCount: 1, sourceHash: 'source:fixed' });
201
+ });
202
+
169
203
  it('recovers expired leases and dead-letters exhausted work', () => {
170
204
  database.enqueueWork({
171
205
  idempotencyKey: 'memory:distill:retryable',
@@ -6,6 +6,7 @@ import { type AddLearningCandidateEvidenceInput, type LearningCandidateEvidenceR
6
6
  import { type CreateWorkflowWorkerInput, type EnqueueWorkflowMergeInput, type TransitionWorkflowWorkerInput, type WorkflowMergeRecord, type WorkflowMergeState, type WorkflowWorkerEventRecord, type WorkflowWorkerRecord } from './workflow-workers';
7
7
  import type { RetrievalLedgerEventRecord } from './retrieval-ledger';
8
8
  import { type ChannelQueryPlanRecord, type TaskIntentArtifactRecord, type TerminalTransitionRecord, type ToolNameResolutionRecord, type TranscriptCheckpointRecord } from './task-semantic-control';
9
+ import { type TaskRuntimeEventRecord, type TaskRuntimeSnapshotRecord } from './task-runtime';
9
10
  export interface BenchmarkReportRecord {
10
11
  runId: string;
11
12
  suiteId: string;
@@ -174,12 +175,17 @@ export interface HookFallbackRecord {
174
175
  export declare function getGlobalDevFlowDbPath(home?: string): string;
175
176
  export declare function openGlobalDevFlowDatabase(home?: string, options?: {
176
177
  busyTimeoutMs?: number;
178
+ readonly?: boolean;
179
+ }): DevFlowDatabase;
180
+ export declare function openGlobalDevFlowReadOnlyDatabase(home?: string, options?: {
181
+ busyTimeoutMs?: number;
177
182
  }): DevFlowDatabase;
178
183
  export declare class DevFlowDatabase {
179
184
  private db;
180
185
  constructor(projectRoot: string, opts?: {
181
186
  dbPath?: string;
182
187
  busyTimeoutMs?: number;
188
+ readonly?: boolean;
183
189
  });
184
190
  private initializeSchema;
185
191
  insertRun(run: any): void;
@@ -213,8 +219,10 @@ export declare class DevFlowDatabase {
213
219
  listToolCallEventsBySession(sessionId: string): any[];
214
220
  getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
215
221
  eventId: string;
222
+ executionId: string;
216
223
  timestamp: number;
217
224
  duration: number;
225
+ input: Record<string, unknown>;
218
226
  } | null;
219
227
  listToolCallEventsBySessions(sessionIds: string[]): Record<string, any[]>;
220
228
  insertSkillExecution(params: {
@@ -382,6 +390,30 @@ export declare class DevFlowDatabase {
382
390
  private insertLearningActivation;
383
391
  enqueueWork(input: EnqueueWorkInput): WorkItemRecord;
384
392
  getWorkByIdempotencyKey(idempotencyKey: string): WorkItemRecord | null;
393
+ getWorkById(id: string): WorkItemRecord | null;
394
+ acquireProjectMutationLease(input: {
395
+ projectRoot: string;
396
+ mutationKind: string;
397
+ owner: string;
398
+ leaseMs: number;
399
+ sourceHash?: string;
400
+ now?: number;
401
+ }): boolean;
402
+ releaseProjectMutationLease(projectRoot: string, mutationKind: string, owner: string): boolean;
403
+ cancelStaleWork(input: {
404
+ id: string;
405
+ owner: string;
406
+ sourceHash?: string;
407
+ taskSpecHash?: string;
408
+ reason?: string;
409
+ now?: number;
410
+ }): boolean;
411
+ replayDeadLetterWork(id: string, input: {
412
+ idempotencyKey: string;
413
+ sourceHash?: string;
414
+ taskSpecHash?: string;
415
+ now?: number;
416
+ }): WorkItemRecord | null;
385
417
  requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord;
386
418
  getSessionClosure(projectRoot: string, sessionId: string): SessionClosureRecord | null;
387
419
  completeSessionClosure(projectRoot: string, sessionId: string, excludingWorkItemId?: string, closedAt?: number): SessionClosureRecord;
@@ -419,6 +451,7 @@ export declare class DevFlowDatabase {
419
451
  upsertContextReceipt(receipt: ContextReceiptRecord): void;
420
452
  getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
421
453
  getActiveContextReceipt(projectRoot: string, sessionId: string, executionId?: string, now?: number): ContextReceiptRecord | null;
454
+ getContextReceiptForRequest(projectRoot: string, sessionId: string, requestId: string): ContextReceiptRecord | null;
422
455
  recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean;
423
456
  appendRetrievalLedgerEvent(event: RetrievalLedgerEventRecord): boolean;
424
457
  appendTaskIntentArtifact(record: TaskIntentArtifactRecord): TaskIntentArtifactRecord;
@@ -450,6 +483,13 @@ export declare class DevFlowDatabase {
450
483
  requestId?: string;
451
484
  }): ToolNameResolutionRecord[];
452
485
  appendTerminalTransition(record: TerminalTransitionRecord): TerminalTransitionRecord;
486
+ appendTaskRuntimeEvent(event: TaskRuntimeEventRecord): TaskRuntimeEventRecord;
487
+ getTaskRuntimeEvent(eventId: string): TaskRuntimeEventRecord | null;
488
+ listTaskRuntimeEvents(projectRoot: string, sessionId: string, turnId: string): TaskRuntimeEventRecord[];
489
+ putTaskRuntimeSnapshot(snapshot: TaskRuntimeSnapshotRecord): TaskRuntimeSnapshotRecord;
490
+ appendTaskRuntimeEventAndSnapshot(event: TaskRuntimeEventRecord, snapshot: TaskRuntimeSnapshotRecord): TaskRuntimeSnapshotRecord;
491
+ getTaskRuntimeSnapshot(projectRoot: string, sessionId: string, turnId: string): TaskRuntimeSnapshotRecord | null;
492
+ deleteTaskRuntimeSnapshot(projectRoot: string, sessionId: string, turnId: string): boolean;
453
493
  getTerminalTransition(receiptId: string): TerminalTransitionRecord | null;
454
494
  getLatestTerminalTransition(projectRoot: string, sessionId: string, turnId: string): TerminalTransitionRecord | null;
455
495
  appendTranscriptCheckpoint(record: TranscriptCheckpointRecord): boolean;
@@ -499,10 +539,16 @@ export declare class DevFlowDatabase {
499
539
  reason?: string;
500
540
  decidedAt?: number;
501
541
  }): MemoryTurnRecord;
542
+ updateCommittedMemoryTurnProjection(input: {
543
+ turnId: string;
544
+ memoryIds: string[];
545
+ reason?: string;
546
+ }): MemoryTurnRecord;
502
547
  skipMemoryTurn(input: {
503
548
  turnId: string;
504
549
  receiptId: string;
505
550
  reason: string;
551
+ source?: string;
506
552
  decidedAt?: number;
507
553
  }): MemoryTurnRecord;
508
554
  markMemoryTurnStopPrompted(turnId: string, promptedAt?: number): boolean;