@devflow-tools/database 0.16.12 → 0.16.13
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 +11 -0
- package/dist/database.d.ts +41 -0
- package/dist/database.js +322 -32
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/obligation-ledger.d.ts +19 -0
- package/dist/obligation-ledger.js +35 -0
- package/dist/work-queue.d.ts +1 -1
- package/package.json +2 -2
- package/src/database.ts +421 -30
- package/src/index.ts +11 -0
- package/src/obligation-ledger.ts +57 -0
- package/src/work-queue.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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.16.13](https://github.com/shilongfeicool/dev-flow/compare/v0.16.12...v0.16.13) (2026-07-26)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add retrieval quality control plane ([743bd12](https://github.com/shilongfeicool/dev-flow/commit/743bd12daa18ceec611dc82674b20b015d110f0c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [0.16.12](https://github.com/shilongfeicool/dev-flow/compare/v0.16.11...v0.16.12) (2026-07-26)
|
|
7
18
|
|
|
8
19
|
|
package/dist/database.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, WorkError, WorkItemRecord, WorkQueueHealth } from './work-queue';
|
|
2
|
+
import { type SessionObligationRecord, type SessionObligationState } from './obligation-ledger';
|
|
2
3
|
export interface BenchmarkReportRecord {
|
|
3
4
|
runId: string;
|
|
4
5
|
suiteId: string;
|
|
@@ -74,6 +75,22 @@ export interface ContextReceiptRecord {
|
|
|
74
75
|
contextHash: string;
|
|
75
76
|
issuedAt: number;
|
|
76
77
|
expiresAt: number;
|
|
78
|
+
selectedFiles?: string[];
|
|
79
|
+
memoryIds?: string[];
|
|
80
|
+
canonicalNextAction?: string;
|
|
81
|
+
requestId?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface ContextSelectionEventRecord {
|
|
84
|
+
id: string;
|
|
85
|
+
projectRoot: string;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
executionId: string;
|
|
88
|
+
requestId?: string;
|
|
89
|
+
selectionType: 'code' | 'action';
|
|
90
|
+
candidateId: string;
|
|
91
|
+
toolName: string;
|
|
92
|
+
toolUseId?: string;
|
|
93
|
+
selectedAt: number;
|
|
77
94
|
}
|
|
78
95
|
export interface MemoryDistillCheckpointRecord {
|
|
79
96
|
id: string;
|
|
@@ -217,6 +234,7 @@ export declare class DevFlowDatabase {
|
|
|
217
234
|
updateToolCallEvent(eventId: string, updates: {
|
|
218
235
|
output?: string;
|
|
219
236
|
error?: string;
|
|
237
|
+
failureCategory?: string;
|
|
220
238
|
duration?: number;
|
|
221
239
|
}): boolean;
|
|
222
240
|
markToolCallBlocked(eventId: string, reason: string): boolean;
|
|
@@ -321,14 +339,36 @@ export declare class DevFlowDatabase {
|
|
|
321
339
|
private mapWorkItem;
|
|
322
340
|
private mapSessionClosure;
|
|
323
341
|
private refreshClosedSessionClosure;
|
|
342
|
+
private countSessionLifecyclePending;
|
|
324
343
|
getHookReceipt(projectRoot: string): HookReceiptRecord | null;
|
|
325
344
|
updateHookReceipt(projectRoot: string, updater: (current: HookReceiptRecord | null) => Omit<HookReceiptRecord, 'projectRoot' | 'updatedAt'>): HookReceiptRecord;
|
|
326
345
|
deleteHookReceipt(projectRoot: string): boolean;
|
|
327
346
|
upsertContextReceipt(receipt: ContextReceiptRecord): void;
|
|
328
347
|
getContextReceipt(projectRoot: string, sessionId: string, executionId: string): ContextReceiptRecord | null;
|
|
348
|
+
getActiveContextReceipt(projectRoot: string, sessionId: string, executionId?: string, now?: number): ContextReceiptRecord | null;
|
|
349
|
+
recordContextSelectionEvent(event: ContextSelectionEventRecord): boolean;
|
|
350
|
+
listContextSelectionEvents(options: {
|
|
351
|
+
projectRoot: string;
|
|
352
|
+
sessionId?: string;
|
|
353
|
+
executionId?: string;
|
|
354
|
+
requestId?: string;
|
|
355
|
+
}): ContextSelectionEventRecord[];
|
|
329
356
|
deleteContextReceipt(projectRoot: string, sessionId: string, executionId?: string): number;
|
|
330
357
|
purgeExpiredContextReceipts(now?: number): number;
|
|
331
358
|
beginMemoryTurn(input: Omit<MemoryTurnRecord, 'status' | 'memoryIds'>): MemoryTurnRecord;
|
|
359
|
+
upsertSessionObligation(record: SessionObligationRecord): SessionObligationRecord;
|
|
360
|
+
getSessionObligation(projectRoot: string, sessionId: string, obligationId: string): SessionObligationRecord | null;
|
|
361
|
+
listSessionObligations(projectRoot: string, sessionId: string, states?: SessionObligationState[]): SessionObligationRecord[];
|
|
362
|
+
resolveSessionObligation(input: {
|
|
363
|
+
projectRoot: string;
|
|
364
|
+
sessionId: string;
|
|
365
|
+
obligationId: string;
|
|
366
|
+
state: 'satisfied' | 'degraded' | 'cancelled';
|
|
367
|
+
receiptId?: string;
|
|
368
|
+
reason?: string;
|
|
369
|
+
resolvedAt?: number;
|
|
370
|
+
}): SessionObligationRecord;
|
|
371
|
+
countOpenSessionObligations(projectRoot: string, sessionId: string): number;
|
|
332
372
|
getMemoryTurn(turnId: string): MemoryTurnRecord | null;
|
|
333
373
|
getPendingMemoryTurn(projectRoot: string, sessionId: string): MemoryTurnRecord | null;
|
|
334
374
|
commitMemoryTurn(input: {
|
|
@@ -346,6 +386,7 @@ export declare class DevFlowDatabase {
|
|
|
346
386
|
decidedAt?: number;
|
|
347
387
|
}): MemoryTurnRecord;
|
|
348
388
|
markMemoryTurnStopPrompted(turnId: string, promptedAt?: number): boolean;
|
|
389
|
+
private ensureMemoryObligation;
|
|
349
390
|
listMemoryTurns(projectRoot: string, sessionId?: string, limit?: number): MemoryTurnRecord[];
|
|
350
391
|
listSessionMemoryTurnsForReconciliation(projectRoot: string, sessionId: string): MemoryTurnRecord[];
|
|
351
392
|
insertHookFallback(record: HookFallbackRecord): boolean;
|
package/dist/database.js
CHANGED
|
@@ -8,6 +8,7 @@ const path_1 = require("path");
|
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const os_1 = require("os");
|
|
10
10
|
const crypto_1 = require("crypto");
|
|
11
|
+
const obligation_ledger_1 = require("./obligation-ledger");
|
|
11
12
|
const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
12
13
|
'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
|
|
13
14
|
]);
|
|
@@ -281,12 +282,32 @@ class DevFlowDatabase {
|
|
|
281
282
|
context_hash TEXT NOT NULL,
|
|
282
283
|
issued_at INTEGER NOT NULL,
|
|
283
284
|
expires_at INTEGER NOT NULL,
|
|
285
|
+
selected_files TEXT NOT NULL DEFAULT '[]',
|
|
286
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
287
|
+
canonical_next_action TEXT,
|
|
288
|
+
request_id TEXT,
|
|
284
289
|
PRIMARY KEY (project_root, session_id, execution_id)
|
|
285
290
|
);
|
|
286
291
|
|
|
287
292
|
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
288
293
|
ON devflow_context_receipts(expires_at);
|
|
289
294
|
|
|
295
|
+
CREATE TABLE IF NOT EXISTS devflow_context_selection_events (
|
|
296
|
+
id TEXT PRIMARY KEY,
|
|
297
|
+
project_root TEXT NOT NULL,
|
|
298
|
+
session_id TEXT NOT NULL,
|
|
299
|
+
execution_id TEXT NOT NULL,
|
|
300
|
+
request_id TEXT,
|
|
301
|
+
selection_type TEXT NOT NULL CHECK(selection_type IN ('code', 'action')),
|
|
302
|
+
candidate_id TEXT NOT NULL,
|
|
303
|
+
tool_name TEXT NOT NULL,
|
|
304
|
+
tool_use_id TEXT,
|
|
305
|
+
selected_at INTEGER NOT NULL
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_context_selection_identity
|
|
309
|
+
ON devflow_context_selection_events(project_root, session_id, execution_id, request_id);
|
|
310
|
+
|
|
290
311
|
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
291
312
|
id TEXT PRIMARY KEY,
|
|
292
313
|
project_root TEXT NOT NULL,
|
|
@@ -323,6 +344,32 @@ class DevFlowDatabase {
|
|
|
323
344
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
|
|
324
345
|
ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
325
346
|
|
|
347
|
+
CREATE TABLE IF NOT EXISTS devflow_session_obligations (
|
|
348
|
+
obligation_id TEXT NOT NULL,
|
|
349
|
+
project_root TEXT NOT NULL,
|
|
350
|
+
session_id TEXT NOT NULL,
|
|
351
|
+
execution_id TEXT,
|
|
352
|
+
turn_id TEXT,
|
|
353
|
+
kind TEXT NOT NULL
|
|
354
|
+
CHECK(kind IN ('memory_decision', 'evidence_contract', 'session_finalize')),
|
|
355
|
+
state TEXT NOT NULL DEFAULT 'open'
|
|
356
|
+
CHECK(state IN ('open', 'satisfied', 'degraded', 'cancelled')),
|
|
357
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
358
|
+
receipt_id TEXT,
|
|
359
|
+
reason TEXT,
|
|
360
|
+
created_at INTEGER NOT NULL,
|
|
361
|
+
updated_at INTEGER NOT NULL,
|
|
362
|
+
resolved_at INTEGER,
|
|
363
|
+
PRIMARY KEY (project_root, session_id, obligation_id)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_session_obligations_state
|
|
367
|
+
ON devflow_session_obligations(project_root, session_id, state, created_at);
|
|
368
|
+
CREATE INDEX IF NOT EXISTS idx_session_obligations_turn
|
|
369
|
+
ON devflow_session_obligations(project_root, session_id, turn_id);
|
|
370
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_session_obligations_receipt
|
|
371
|
+
ON devflow_session_obligations(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
372
|
+
|
|
326
373
|
CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
|
|
327
374
|
id TEXT PRIMARY KEY,
|
|
328
375
|
project_root TEXT NOT NULL,
|
|
@@ -458,6 +505,22 @@ class DevFlowDatabase {
|
|
|
458
505
|
this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER');
|
|
459
506
|
}
|
|
460
507
|
catch { }
|
|
508
|
+
try {
|
|
509
|
+
this.db.exec("ALTER TABLE devflow_context_receipts ADD COLUMN selected_files TEXT NOT NULL DEFAULT '[]'");
|
|
510
|
+
}
|
|
511
|
+
catch { }
|
|
512
|
+
try {
|
|
513
|
+
this.db.exec("ALTER TABLE devflow_context_receipts ADD COLUMN memory_ids TEXT NOT NULL DEFAULT '[]'");
|
|
514
|
+
}
|
|
515
|
+
catch { }
|
|
516
|
+
try {
|
|
517
|
+
this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN canonical_next_action TEXT');
|
|
518
|
+
}
|
|
519
|
+
catch { }
|
|
520
|
+
try {
|
|
521
|
+
this.db.exec('ALTER TABLE devflow_context_receipts ADD COLUMN request_id TEXT');
|
|
522
|
+
}
|
|
523
|
+
catch { }
|
|
461
524
|
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
462
525
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
463
526
|
// Migration: tool_metrics table for per-tool metrics collection
|
|
@@ -1083,6 +1146,10 @@ class DevFlowDatabase {
|
|
|
1083
1146
|
sets.push('error = ?');
|
|
1084
1147
|
values.push(updates.error);
|
|
1085
1148
|
}
|
|
1149
|
+
if (updates.failureCategory !== undefined) {
|
|
1150
|
+
sets.push('failure_category = ?');
|
|
1151
|
+
values.push(updates.failureCategory);
|
|
1152
|
+
}
|
|
1086
1153
|
if (updates.duration !== undefined) {
|
|
1087
1154
|
sets.push('duration = ?');
|
|
1088
1155
|
values.push(updates.duration);
|
|
@@ -1748,14 +1815,7 @@ class DevFlowDatabase {
|
|
|
1748
1815
|
return row ? this.mapSessionClosure(row) : null;
|
|
1749
1816
|
}
|
|
1750
1817
|
completeSessionClosure(projectRoot, sessionId, excludingWorkItemId, closedAt = Date.now()) {
|
|
1751
|
-
const
|
|
1752
|
-
SELECT COUNT(*) AS count
|
|
1753
|
-
FROM devflow_work_items
|
|
1754
|
-
WHERE project_root = ? AND session_id = ?
|
|
1755
|
-
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
1756
|
-
AND (? IS NULL OR id <> ?)
|
|
1757
|
-
`).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
|
|
1758
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
1818
|
+
const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId, excludingWorkItemId);
|
|
1759
1819
|
const state = pendingWorkCount > 0 ? 'closed_with_pending_work' : 'closed';
|
|
1760
1820
|
this.db.prepare(`
|
|
1761
1821
|
UPDATE devflow_session_closures
|
|
@@ -1959,13 +2019,7 @@ class DevFlowDatabase {
|
|
|
1959
2019
|
};
|
|
1960
2020
|
}
|
|
1961
2021
|
refreshClosedSessionClosure(projectRoot, sessionId, now) {
|
|
1962
|
-
const
|
|
1963
|
-
SELECT COUNT(*) AS count
|
|
1964
|
-
FROM devflow_work_items
|
|
1965
|
-
WHERE project_root = ? AND session_id = ?
|
|
1966
|
-
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
1967
|
-
`).get(projectRoot, sessionId);
|
|
1968
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2022
|
+
const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId);
|
|
1969
2023
|
this.db.prepare(`
|
|
1970
2024
|
UPDATE devflow_session_closures
|
|
1971
2025
|
SET state = CASE WHEN ? = 0 THEN 'closed' ELSE 'closed_with_pending_work' END,
|
|
@@ -1974,6 +2028,34 @@ class DevFlowDatabase {
|
|
|
1974
2028
|
AND state IN ('closed', 'closed_with_pending_work')
|
|
1975
2029
|
`).run(pendingWorkCount, pendingWorkCount, now, projectRoot, sessionId);
|
|
1976
2030
|
}
|
|
2031
|
+
countSessionLifecyclePending(projectRoot, sessionId, excludingWorkItemId) {
|
|
2032
|
+
const work = this.db.prepare(`
|
|
2033
|
+
SELECT COUNT(*) AS count
|
|
2034
|
+
FROM devflow_work_items
|
|
2035
|
+
WHERE project_root = ? AND session_id = ?
|
|
2036
|
+
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
2037
|
+
AND (? IS NULL OR id <> ?)
|
|
2038
|
+
`).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
|
|
2039
|
+
const obligations = this.db.prepare(`
|
|
2040
|
+
SELECT COUNT(*) AS count
|
|
2041
|
+
FROM devflow_session_obligations
|
|
2042
|
+
WHERE project_root = ? AND session_id = ?
|
|
2043
|
+
AND state IN ('open', 'degraded')
|
|
2044
|
+
`).get(projectRoot, sessionId);
|
|
2045
|
+
const legacyTurns = this.db.prepare(`
|
|
2046
|
+
SELECT COUNT(*) AS count
|
|
2047
|
+
FROM devflow_memory_turns t
|
|
2048
|
+
WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
|
|
2049
|
+
AND NOT EXISTS (
|
|
2050
|
+
SELECT 1 FROM devflow_session_obligations o
|
|
2051
|
+
WHERE o.project_root = t.project_root AND o.session_id = t.session_id
|
|
2052
|
+
AND o.obligation_id = 'memory:' || t.turn_id
|
|
2053
|
+
)
|
|
2054
|
+
`).get(projectRoot, sessionId);
|
|
2055
|
+
return Number(work?.count ?? 0)
|
|
2056
|
+
+ Number(obligations?.count ?? 0)
|
|
2057
|
+
+ Number(legacyTurns?.count ?? 0);
|
|
2058
|
+
}
|
|
1977
2059
|
// ---- Hook Lifecycle ----
|
|
1978
2060
|
getHookReceipt(projectRoot) {
|
|
1979
2061
|
const row = this.db.prepare(`
|
|
@@ -2027,17 +2109,23 @@ class DevFlowDatabase {
|
|
|
2027
2109
|
upsertContextReceipt(receipt) {
|
|
2028
2110
|
this.db.prepare(`
|
|
2029
2111
|
INSERT INTO devflow_context_receipts
|
|
2030
|
-
(project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
2031
|
-
|
|
2112
|
+
(project_root, session_id, execution_id, context_hash, issued_at, expires_at,
|
|
2113
|
+
selected_files, memory_ids, canonical_next_action, request_id)
|
|
2114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2032
2115
|
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
2033
2116
|
context_hash = excluded.context_hash,
|
|
2034
2117
|
issued_at = excluded.issued_at,
|
|
2035
|
-
expires_at = excluded.expires_at
|
|
2036
|
-
|
|
2118
|
+
expires_at = excluded.expires_at,
|
|
2119
|
+
selected_files = excluded.selected_files,
|
|
2120
|
+
memory_ids = excluded.memory_ids,
|
|
2121
|
+
canonical_next_action = excluded.canonical_next_action,
|
|
2122
|
+
request_id = excluded.request_id
|
|
2123
|
+
`).run(receipt.projectRoot, receipt.sessionId, receipt.executionId, receipt.contextHash, receipt.issuedAt, receipt.expiresAt, JSON.stringify(receipt.selectedFiles ?? []), JSON.stringify(receipt.memoryIds ?? []), receipt.canonicalNextAction ?? null, receipt.requestId ?? null);
|
|
2037
2124
|
}
|
|
2038
2125
|
getContextReceipt(projectRoot, sessionId, executionId) {
|
|
2039
2126
|
const row = this.db.prepare(`
|
|
2040
|
-
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
2127
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
|
|
2128
|
+
selected_files, memory_ids, canonical_next_action, request_id
|
|
2041
2129
|
FROM devflow_context_receipts
|
|
2042
2130
|
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
2043
2131
|
`).get(projectRoot, sessionId, executionId);
|
|
@@ -2048,8 +2136,77 @@ class DevFlowDatabase {
|
|
|
2048
2136
|
contextHash: row.context_hash,
|
|
2049
2137
|
issuedAt: row.issued_at,
|
|
2050
2138
|
expiresAt: row.expires_at,
|
|
2139
|
+
selectedFiles: parseJsonStringArray(row.selected_files),
|
|
2140
|
+
memoryIds: parseJsonStringArray(row.memory_ids),
|
|
2141
|
+
canonicalNextAction: row.canonical_next_action ?? undefined,
|
|
2142
|
+
requestId: row.request_id ?? undefined,
|
|
2143
|
+
} : null;
|
|
2144
|
+
}
|
|
2145
|
+
getActiveContextReceipt(projectRoot, sessionId, executionId, now = Date.now()) {
|
|
2146
|
+
const executionClause = executionId ? 'AND execution_id = ?' : '';
|
|
2147
|
+
const params = executionId
|
|
2148
|
+
? [projectRoot, sessionId, executionId, now]
|
|
2149
|
+
: [projectRoot, sessionId, now];
|
|
2150
|
+
const row = this.db.prepare(`
|
|
2151
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at,
|
|
2152
|
+
selected_files, memory_ids, canonical_next_action, request_id
|
|
2153
|
+
FROM devflow_context_receipts
|
|
2154
|
+
WHERE project_root = ? AND session_id = ? ${executionClause} AND expires_at > ?
|
|
2155
|
+
ORDER BY issued_at DESC
|
|
2156
|
+
LIMIT 1
|
|
2157
|
+
`).get(...params);
|
|
2158
|
+
return row ? {
|
|
2159
|
+
projectRoot: row.project_root,
|
|
2160
|
+
sessionId: row.session_id,
|
|
2161
|
+
executionId: row.execution_id,
|
|
2162
|
+
contextHash: row.context_hash,
|
|
2163
|
+
issuedAt: row.issued_at,
|
|
2164
|
+
expiresAt: row.expires_at,
|
|
2165
|
+
selectedFiles: parseJsonStringArray(row.selected_files),
|
|
2166
|
+
memoryIds: parseJsonStringArray(row.memory_ids),
|
|
2167
|
+
canonicalNextAction: row.canonical_next_action ?? undefined,
|
|
2168
|
+
requestId: row.request_id ?? undefined,
|
|
2051
2169
|
} : null;
|
|
2052
2170
|
}
|
|
2171
|
+
recordContextSelectionEvent(event) {
|
|
2172
|
+
return this.db.prepare(`
|
|
2173
|
+
INSERT OR IGNORE INTO devflow_context_selection_events
|
|
2174
|
+
(id, project_root, session_id, execution_id, request_id, selection_type,
|
|
2175
|
+
candidate_id, tool_name, tool_use_id, selected_at)
|
|
2176
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2177
|
+
`).run(event.id, event.projectRoot, event.sessionId, event.executionId, event.requestId ?? null, event.selectionType, event.candidateId, event.toolName, event.toolUseId ?? null, event.selectedAt).changes > 0;
|
|
2178
|
+
}
|
|
2179
|
+
listContextSelectionEvents(options) {
|
|
2180
|
+
const predicates = ['project_root = ?'];
|
|
2181
|
+
const params = [options.projectRoot];
|
|
2182
|
+
for (const [column, value] of [
|
|
2183
|
+
['session_id', options.sessionId],
|
|
2184
|
+
['execution_id', options.executionId],
|
|
2185
|
+
['request_id', options.requestId],
|
|
2186
|
+
]) {
|
|
2187
|
+
if (!value)
|
|
2188
|
+
continue;
|
|
2189
|
+
predicates.push(`${column} = ?`);
|
|
2190
|
+
params.push(value);
|
|
2191
|
+
}
|
|
2192
|
+
const rows = this.db.prepare(`
|
|
2193
|
+
SELECT * FROM devflow_context_selection_events
|
|
2194
|
+
WHERE ${predicates.join(' AND ')}
|
|
2195
|
+
ORDER BY selected_at ASC, id ASC
|
|
2196
|
+
`).all(...params);
|
|
2197
|
+
return rows.map(row => ({
|
|
2198
|
+
id: row.id,
|
|
2199
|
+
projectRoot: row.project_root,
|
|
2200
|
+
sessionId: row.session_id,
|
|
2201
|
+
executionId: row.execution_id,
|
|
2202
|
+
requestId: row.request_id ?? undefined,
|
|
2203
|
+
selectionType: row.selection_type,
|
|
2204
|
+
candidateId: row.candidate_id,
|
|
2205
|
+
toolName: row.tool_name,
|
|
2206
|
+
toolUseId: row.tool_use_id ?? undefined,
|
|
2207
|
+
selectedAt: row.selected_at,
|
|
2208
|
+
}));
|
|
2209
|
+
}
|
|
2053
2210
|
deleteContextReceipt(projectRoot, sessionId, executionId) {
|
|
2054
2211
|
return executionId
|
|
2055
2212
|
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
@@ -2064,15 +2221,104 @@ class DevFlowDatabase {
|
|
|
2064
2221
|
.run(now).changes;
|
|
2065
2222
|
}
|
|
2066
2223
|
beginMemoryTurn(input) {
|
|
2224
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
2067
2225
|
this.db.prepare(`
|
|
2068
2226
|
INSERT OR IGNORE INTO devflow_memory_turns
|
|
2069
2227
|
(turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
|
|
2070
2228
|
VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
|
|
2071
|
-
`).run(
|
|
2072
|
-
|
|
2229
|
+
`).run(turnId, input.projectRoot, input.sessionId, input.promptHash, input.eventId, input.createdAt);
|
|
2230
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2231
|
+
this.upsertSessionObligation({
|
|
2232
|
+
obligationId: `memory:${turnId}`,
|
|
2233
|
+
projectRoot: input.projectRoot,
|
|
2234
|
+
sessionId: input.sessionId,
|
|
2235
|
+
turnId,
|
|
2236
|
+
kind: 'memory_decision',
|
|
2237
|
+
state: turn.status === 'pending' ? 'open' : 'satisfied',
|
|
2238
|
+
payload: { eventId: input.eventId, promptHash: input.promptHash },
|
|
2239
|
+
receiptId: turn.receiptId,
|
|
2240
|
+
createdAt: input.createdAt,
|
|
2241
|
+
updatedAt: Date.now(),
|
|
2242
|
+
resolvedAt: turn.decidedAt,
|
|
2243
|
+
});
|
|
2244
|
+
return turn;
|
|
2245
|
+
}
|
|
2246
|
+
upsertSessionObligation(record) {
|
|
2247
|
+
if (!record.obligationId.trim())
|
|
2248
|
+
throw new Error('Session obligation requires an ID');
|
|
2249
|
+
if (!record.projectRoot.trim())
|
|
2250
|
+
throw new Error('Session obligation requires a project root');
|
|
2251
|
+
if (!record.sessionId.trim())
|
|
2252
|
+
throw new Error('Session obligation requires a session ID');
|
|
2253
|
+
const now = record.updatedAt || Date.now();
|
|
2254
|
+
this.db.prepare(`
|
|
2255
|
+
INSERT INTO devflow_session_obligations (
|
|
2256
|
+
obligation_id, project_root, session_id, execution_id, turn_id, kind,
|
|
2257
|
+
state, payload, receipt_id, reason, created_at, updated_at, resolved_at
|
|
2258
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2259
|
+
ON CONFLICT(project_root, session_id, obligation_id) DO UPDATE SET
|
|
2260
|
+
execution_id = COALESCE(excluded.execution_id, devflow_session_obligations.execution_id),
|
|
2261
|
+
turn_id = COALESCE(excluded.turn_id, devflow_session_obligations.turn_id),
|
|
2262
|
+
payload = excluded.payload,
|
|
2263
|
+
updated_at = excluded.updated_at
|
|
2264
|
+
WHERE devflow_session_obligations.state = 'open'
|
|
2265
|
+
`).run(record.obligationId, record.projectRoot, record.sessionId, record.executionId ?? null, record.turnId ?? null, record.kind, record.state, JSON.stringify(record.payload ?? {}), record.receiptId ?? null, record.reason ?? null, record.createdAt, now, record.resolvedAt ?? null);
|
|
2266
|
+
return this.getSessionObligation(record.projectRoot, record.sessionId, record.obligationId);
|
|
2267
|
+
}
|
|
2268
|
+
getSessionObligation(projectRoot, sessionId, obligationId) {
|
|
2269
|
+
const row = this.db.prepare(`
|
|
2270
|
+
SELECT * FROM devflow_session_obligations
|
|
2271
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ?
|
|
2272
|
+
`).get(projectRoot, sessionId, obligationId);
|
|
2273
|
+
return row ? (0, obligation_ledger_1.mapSessionObligationRow)(row) : null;
|
|
2274
|
+
}
|
|
2275
|
+
listSessionObligations(projectRoot, sessionId, states) {
|
|
2276
|
+
const allowed = [...new Set(states ?? [])].filter(state => state === 'open' || state === 'satisfied' || state === 'degraded' || state === 'cancelled');
|
|
2277
|
+
const rows = (allowed.length > 0
|
|
2278
|
+
? this.db.prepare(`
|
|
2279
|
+
SELECT * FROM devflow_session_obligations
|
|
2280
|
+
WHERE project_root = ? AND session_id = ?
|
|
2281
|
+
AND state IN (${allowed.map(() => '?').join(',')})
|
|
2282
|
+
ORDER BY created_at ASC, obligation_id ASC
|
|
2283
|
+
`).all(projectRoot, sessionId, ...allowed)
|
|
2284
|
+
: this.db.prepare(`
|
|
2285
|
+
SELECT * FROM devflow_session_obligations
|
|
2286
|
+
WHERE project_root = ? AND session_id = ?
|
|
2287
|
+
ORDER BY created_at ASC, obligation_id ASC
|
|
2288
|
+
`).all(projectRoot, sessionId));
|
|
2289
|
+
return rows.map(obligation_ledger_1.mapSessionObligationRow);
|
|
2290
|
+
}
|
|
2291
|
+
resolveSessionObligation(input) {
|
|
2292
|
+
return this.db.transaction(() => {
|
|
2293
|
+
const existing = this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
|
|
2294
|
+
if (!existing)
|
|
2295
|
+
throw new Error(`Session obligation ${input.obligationId} does not exist`);
|
|
2296
|
+
if (existing.state !== 'open') {
|
|
2297
|
+
const sameResolution = existing.state === input.state
|
|
2298
|
+
&& (input.receiptId === undefined || existing.receiptId === input.receiptId);
|
|
2299
|
+
if (sameResolution)
|
|
2300
|
+
return existing;
|
|
2301
|
+
throw new Error(`OBLIGATION_TERMINAL_CONFLICT:${input.obligationId}`);
|
|
2302
|
+
}
|
|
2303
|
+
const resolvedAt = input.resolvedAt ?? Date.now();
|
|
2304
|
+
this.db.prepare(`
|
|
2305
|
+
UPDATE devflow_session_obligations
|
|
2306
|
+
SET state = ?, receipt_id = COALESCE(?, receipt_id), reason = ?,
|
|
2307
|
+
resolved_at = ?, updated_at = ?
|
|
2308
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'open'
|
|
2309
|
+
`).run(input.state, input.receiptId ?? null, input.reason ?? null, resolvedAt, resolvedAt, input.projectRoot, input.sessionId, input.obligationId);
|
|
2310
|
+
return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId);
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
countOpenSessionObligations(projectRoot, sessionId) {
|
|
2314
|
+
const row = this.db.prepare(`
|
|
2315
|
+
SELECT COUNT(*) AS count FROM devflow_session_obligations
|
|
2316
|
+
WHERE project_root = ? AND session_id = ? AND state = 'open'
|
|
2317
|
+
`).get(projectRoot, sessionId);
|
|
2318
|
+
return Number(row?.count ?? 0);
|
|
2073
2319
|
}
|
|
2074
2320
|
getMemoryTurn(turnId) {
|
|
2075
|
-
const row = this.db.prepare('SELECT * FROM devflow_memory_turns WHERE turn_id = ?').get(turnId);
|
|
2321
|
+
const row = this.db.prepare('SELECT * FROM devflow_memory_turns WHERE turn_id = ?').get((0, obligation_ledger_1.normalizeTurnId)(turnId));
|
|
2076
2322
|
return row ? this.mapMemoryTurn(row) : null;
|
|
2077
2323
|
}
|
|
2078
2324
|
getPendingMemoryTurn(projectRoot, sessionId) {
|
|
@@ -2084,35 +2330,73 @@ class DevFlowDatabase {
|
|
|
2084
2330
|
return row ? this.mapMemoryTurn(row) : null;
|
|
2085
2331
|
}
|
|
2086
2332
|
commitMemoryTurn(input) {
|
|
2333
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
2087
2334
|
this.db.prepare(`
|
|
2088
2335
|
UPDATE devflow_memory_turns
|
|
2089
2336
|
SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
|
|
2090
2337
|
WHERE turn_id = ? AND status = 'pending'
|
|
2091
|
-
`).run(input.receiptId, JSON.stringify([...new Set(input.memoryIds)]), input.source, input.reason ?? null, input.decidedAt ?? Date.now(),
|
|
2092
|
-
const turn = this.getMemoryTurn(
|
|
2338
|
+
`).run(input.receiptId, JSON.stringify([...new Set(input.memoryIds)]), input.source, input.reason ?? null, input.decidedAt ?? Date.now(), turnId);
|
|
2339
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2093
2340
|
if (!turn || turn.status !== 'committed') {
|
|
2094
|
-
throw new Error(`Memory turn ${
|
|
2095
|
-
}
|
|
2341
|
+
throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
|
|
2342
|
+
}
|
|
2343
|
+
this.ensureMemoryObligation(turn);
|
|
2344
|
+
this.resolveSessionObligation({
|
|
2345
|
+
projectRoot: turn.projectRoot,
|
|
2346
|
+
sessionId: turn.sessionId,
|
|
2347
|
+
obligationId: `memory:${turn.turnId}`,
|
|
2348
|
+
state: 'satisfied',
|
|
2349
|
+
receiptId: turn.receiptId,
|
|
2350
|
+
reason: turn.reason,
|
|
2351
|
+
resolvedAt: turn.decidedAt,
|
|
2352
|
+
});
|
|
2096
2353
|
return turn;
|
|
2097
2354
|
}
|
|
2098
2355
|
skipMemoryTurn(input) {
|
|
2356
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
2099
2357
|
this.db.prepare(`
|
|
2100
2358
|
UPDATE devflow_memory_turns
|
|
2101
2359
|
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
|
|
2102
2360
|
reason = ?, decided_at = ?
|
|
2103
2361
|
WHERE turn_id = ? AND status = 'pending'
|
|
2104
|
-
`).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(),
|
|
2105
|
-
const turn = this.getMemoryTurn(
|
|
2362
|
+
`).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(), turnId);
|
|
2363
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2106
2364
|
if (!turn || turn.status !== 'skipped') {
|
|
2107
|
-
throw new Error(`Memory turn ${
|
|
2108
|
-
}
|
|
2365
|
+
throw new Error(`Memory turn ${turnId} is not pending or does not exist`);
|
|
2366
|
+
}
|
|
2367
|
+
this.ensureMemoryObligation(turn);
|
|
2368
|
+
this.resolveSessionObligation({
|
|
2369
|
+
projectRoot: turn.projectRoot,
|
|
2370
|
+
sessionId: turn.sessionId,
|
|
2371
|
+
obligationId: `memory:${turn.turnId}`,
|
|
2372
|
+
state: 'satisfied',
|
|
2373
|
+
receiptId: turn.receiptId,
|
|
2374
|
+
reason: turn.reason,
|
|
2375
|
+
resolvedAt: turn.decidedAt,
|
|
2376
|
+
});
|
|
2109
2377
|
return turn;
|
|
2110
2378
|
}
|
|
2111
2379
|
markMemoryTurnStopPrompted(turnId, promptedAt = Date.now()) {
|
|
2112
2380
|
return this.db.prepare(`
|
|
2113
2381
|
UPDATE devflow_memory_turns SET stop_prompted_at = ?
|
|
2114
2382
|
WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
|
|
2115
|
-
`).run(promptedAt, turnId).changes === 1;
|
|
2383
|
+
`).run(promptedAt, (0, obligation_ledger_1.normalizeTurnId)(turnId)).changes === 1;
|
|
2384
|
+
}
|
|
2385
|
+
ensureMemoryObligation(turn) {
|
|
2386
|
+
const obligationId = `memory:${turn.turnId}`;
|
|
2387
|
+
if (this.getSessionObligation(turn.projectRoot, turn.sessionId, obligationId))
|
|
2388
|
+
return;
|
|
2389
|
+
this.upsertSessionObligation({
|
|
2390
|
+
obligationId,
|
|
2391
|
+
projectRoot: turn.projectRoot,
|
|
2392
|
+
sessionId: turn.sessionId,
|
|
2393
|
+
turnId: turn.turnId,
|
|
2394
|
+
kind: 'memory_decision',
|
|
2395
|
+
state: 'open',
|
|
2396
|
+
payload: { eventId: turn.eventId, promptHash: turn.promptHash },
|
|
2397
|
+
createdAt: turn.createdAt,
|
|
2398
|
+
updatedAt: Date.now(),
|
|
2399
|
+
});
|
|
2116
2400
|
}
|
|
2117
2401
|
listMemoryTurns(projectRoot, sessionId, limit = 50) {
|
|
2118
2402
|
const rows = (sessionId
|
|
@@ -2255,6 +2539,12 @@ function parseJson(value) {
|
|
|
2255
2539
|
return value;
|
|
2256
2540
|
}
|
|
2257
2541
|
}
|
|
2542
|
+
function parseJsonStringArray(value) {
|
|
2543
|
+
const parsed = parseJson(value);
|
|
2544
|
+
return Array.isArray(parsed)
|
|
2545
|
+
? parsed.filter((item) => typeof item === 'string')
|
|
2546
|
+
: [];
|
|
2547
|
+
}
|
|
2258
2548
|
function deriveQuery(input) {
|
|
2259
2549
|
if (!input || typeof input !== 'object')
|
|
2260
2550
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
|
|
2
|
-
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
|
|
2
|
+
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, ContextReceiptRecord, ContextSelectionEventRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
|
|
3
3
|
export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, } from './work-queue';
|
|
4
|
+
export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
|
|
5
|
+
export type { SessionObligationKind, SessionObligationRecord, SessionObligationState, } from './obligation-ledger';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
3
|
+
exports.normalizeTurnId = exports.mapSessionObligationRow = 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
|
+
var obligation_ledger_1 = require("./obligation-ledger");
|
|
9
|
+
Object.defineProperty(exports, "mapSessionObligationRow", { enumerable: true, get: function () { return obligation_ledger_1.mapSessionObligationRow; } });
|
|
10
|
+
Object.defineProperty(exports, "normalizeTurnId", { enumerable: true, get: function () { return obligation_ledger_1.normalizeTurnId; } });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type SessionObligationKind = 'memory_decision' | 'evidence_contract' | 'session_finalize';
|
|
2
|
+
export type SessionObligationState = 'open' | 'satisfied' | 'degraded' | 'cancelled';
|
|
3
|
+
export interface SessionObligationRecord {
|
|
4
|
+
obligationId: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
executionId?: string;
|
|
8
|
+
turnId?: string;
|
|
9
|
+
kind: SessionObligationKind;
|
|
10
|
+
state: SessionObligationState;
|
|
11
|
+
payload: Record<string, unknown>;
|
|
12
|
+
receiptId?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
resolvedAt?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function normalizeTurnId(value: string): string;
|
|
19
|
+
export declare function mapSessionObligationRow(row: Record<string, unknown>): SessionObligationRecord;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeTurnId = normalizeTurnId;
|
|
4
|
+
exports.mapSessionObligationRow = mapSessionObligationRow;
|
|
5
|
+
function normalizeTurnId(value) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
throw new Error('Memory turn ID is required');
|
|
9
|
+
return trimmed.startsWith('turn:') ? trimmed : `turn:${trimmed}`;
|
|
10
|
+
}
|
|
11
|
+
function mapSessionObligationRow(row) {
|
|
12
|
+
let payload = {};
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(String(row.payload ?? '{}'));
|
|
15
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
16
|
+
payload = parsed;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return {
|
|
21
|
+
obligationId: String(row.obligation_id),
|
|
22
|
+
projectRoot: String(row.project_root),
|
|
23
|
+
sessionId: String(row.session_id),
|
|
24
|
+
executionId: typeof row.execution_id === 'string' ? row.execution_id : undefined,
|
|
25
|
+
turnId: typeof row.turn_id === 'string' ? row.turn_id : undefined,
|
|
26
|
+
kind: row.kind,
|
|
27
|
+
state: row.state,
|
|
28
|
+
payload,
|
|
29
|
+
receiptId: typeof row.receipt_id === 'string' ? row.receipt_id : undefined,
|
|
30
|
+
reason: typeof row.reason === 'string' ? row.reason : undefined,
|
|
31
|
+
createdAt: Number(row.created_at),
|
|
32
|
+
updatedAt: Number(row.updated_at),
|
|
33
|
+
resolvedAt: typeof row.resolved_at === 'number' ? row.resolved_at : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/work-queue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WorkKind = 'memory.explicit_commit' | 'memory.turn_capture' | 'memory.turn_distill' | 'memory.vector_backfill' | 'session.bootstrap' | 'session.finalize' | 'telemetry.reconcile';
|
|
1
|
+
export type WorkKind = 'memory.explicit_commit' | 'memory.explicit_enrichment' | 'memory.turn_capture' | 'memory.turn_distill' | 'memory.vector_backfill' | 'session.bootstrap' | 'session.finalize' | 'telemetry.reconcile';
|
|
2
2
|
export type WorkState = 'pending' | 'leased' | 'completed' | 'failed' | 'dead_letter';
|
|
3
3
|
export interface WorkError {
|
|
4
4
|
category: string;
|