@devflow-tools/database 0.16.11 → 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 +27 -0
- package/__tests__/database.memory-turn-receipts.test.ts +27 -0
- package/__tests__/database.skill-executions.test.ts +62 -1
- package/dist/database.d.ts +46 -0
- package/dist/database.js +438 -48
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/node-sqlite.js +1 -6
- 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 +3 -3
- package/src/database.ts +552 -45
- package/src/index.ts +11 -0
- package/src/node-sqlite.ts +1 -6
- package/src/obligation-ledger.ts +57 -0
- package/src/work-queue.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
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,
|
|
@@ -294,6 +315,7 @@ class DevFlowDatabase {
|
|
|
294
315
|
trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
|
|
295
316
|
pending_events INTEGER NOT NULL DEFAULT 0,
|
|
296
317
|
released_leases INTEGER NOT NULL DEFAULT 0,
|
|
318
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
297
319
|
created_at INTEGER NOT NULL
|
|
298
320
|
);
|
|
299
321
|
|
|
@@ -322,6 +344,32 @@ class DevFlowDatabase {
|
|
|
322
344
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_turns_receipt
|
|
323
345
|
ON devflow_memory_turns(receipt_id) WHERE receipt_id IS NOT NULL;
|
|
324
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
|
+
|
|
325
373
|
CREATE TABLE IF NOT EXISTS devflow_hook_fallbacks (
|
|
326
374
|
id TEXT PRIMARY KEY,
|
|
327
375
|
project_root TEXT NOT NULL,
|
|
@@ -424,6 +472,10 @@ class DevFlowDatabase {
|
|
|
424
472
|
this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT');
|
|
425
473
|
}
|
|
426
474
|
catch { }
|
|
475
|
+
try {
|
|
476
|
+
this.db.exec("ALTER TABLE devflow_memory_distill_checkpoints ADD COLUMN details TEXT NOT NULL DEFAULT '{}'");
|
|
477
|
+
}
|
|
478
|
+
catch { }
|
|
427
479
|
try {
|
|
428
480
|
this.db.exec('ALTER TABLE skill_executions ADD COLUMN failed_tool_calls INTEGER DEFAULT 0');
|
|
429
481
|
}
|
|
@@ -453,6 +505,22 @@ class DevFlowDatabase {
|
|
|
453
505
|
this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER');
|
|
454
506
|
}
|
|
455
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 { }
|
|
456
524
|
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
457
525
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
458
526
|
// Migration: tool_metrics table for per-tool metrics collection
|
|
@@ -887,6 +955,15 @@ class DevFlowDatabase {
|
|
|
887
955
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
888
956
|
};
|
|
889
957
|
}
|
|
958
|
+
getLatestSkillExecutionForSession(sessionId) {
|
|
959
|
+
const row = this.db.prepare(`
|
|
960
|
+
SELECT execution_id FROM skill_executions
|
|
961
|
+
WHERE session_id = ?
|
|
962
|
+
ORDER BY started_at DESC, execution_id DESC
|
|
963
|
+
LIMIT 1
|
|
964
|
+
`).get(sessionId);
|
|
965
|
+
return row?.execution_id ? this.getSkillExecution(row.execution_id) : null;
|
|
966
|
+
}
|
|
890
967
|
updateSkillExecution(executionId, updates) {
|
|
891
968
|
const sets = [];
|
|
892
969
|
const values = [];
|
|
@@ -951,6 +1028,17 @@ class DevFlowDatabase {
|
|
|
951
1028
|
this.db.prepare(`UPDATE skill_executions SET ${sets.join(', ')} WHERE execution_id = ?`).run(...values);
|
|
952
1029
|
}
|
|
953
1030
|
}
|
|
1031
|
+
mergeSkillExecutionMetadata(executionId, incoming) {
|
|
1032
|
+
const execution = this.getSkillExecution(executionId);
|
|
1033
|
+
if (!execution)
|
|
1034
|
+
return null;
|
|
1035
|
+
const existing = execution.metadata && typeof execution.metadata === 'object'
|
|
1036
|
+
? execution.metadata
|
|
1037
|
+
: {};
|
|
1038
|
+
const merged = mergeExecutionMetadata(existing, incoming);
|
|
1039
|
+
this.updateSkillExecution(executionId, { metadata: merged });
|
|
1040
|
+
return merged;
|
|
1041
|
+
}
|
|
954
1042
|
listSkillExecutions(limit, skillName) {
|
|
955
1043
|
let query = 'SELECT * FROM skill_executions';
|
|
956
1044
|
const params = [];
|
|
@@ -1058,6 +1146,10 @@ class DevFlowDatabase {
|
|
|
1058
1146
|
sets.push('error = ?');
|
|
1059
1147
|
values.push(updates.error);
|
|
1060
1148
|
}
|
|
1149
|
+
if (updates.failureCategory !== undefined) {
|
|
1150
|
+
sets.push('failure_category = ?');
|
|
1151
|
+
values.push(updates.failureCategory);
|
|
1152
|
+
}
|
|
1061
1153
|
if (updates.duration !== undefined) {
|
|
1062
1154
|
sets.push('duration = ?');
|
|
1063
1155
|
values.push(updates.duration);
|
|
@@ -1095,23 +1187,38 @@ class DevFlowDatabase {
|
|
|
1095
1187
|
const blocked = events.filter(event => event.blocked);
|
|
1096
1188
|
const directFallbacks = events.filter(event => event.mcpFallback);
|
|
1097
1189
|
const hookFallbacks = execution?.sessionId
|
|
1098
|
-
? this.
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1190
|
+
? this.db.prepare(`
|
|
1191
|
+
SELECT * FROM devflow_hook_fallbacks
|
|
1192
|
+
WHERE session_id = ? AND created_at >= ? AND created_at <= ?
|
|
1193
|
+
ORDER BY created_at ASC
|
|
1194
|
+
`).all(execution.sessionId, execution.startedAt, finishedAt).map(row => ({
|
|
1195
|
+
id: row.id,
|
|
1196
|
+
projectRoot: row.project_root,
|
|
1197
|
+
sessionId: row.session_id ?? undefined,
|
|
1198
|
+
toolUseId: row.tool_use_id ?? undefined,
|
|
1199
|
+
requestType: row.request_type,
|
|
1200
|
+
tool: row.tool,
|
|
1201
|
+
reason: row.reason,
|
|
1202
|
+
durationMs: row.duration_ms,
|
|
1203
|
+
attempts: row.attempts,
|
|
1204
|
+
createdAt: row.created_at,
|
|
1205
|
+
}))
|
|
1103
1206
|
: [];
|
|
1104
1207
|
const fallbackReasons = [...new Set([
|
|
1105
1208
|
...directFallbacks.map(() => 'direct_tool_during_context'),
|
|
1106
1209
|
...hookFallbacks.map(fallback => `daemon_${fallback.reason}`),
|
|
1107
1210
|
])];
|
|
1108
1211
|
const eventReceiptIds = collectCanonicalReceiptIds(events.map(event => event.output));
|
|
1109
|
-
const
|
|
1110
|
-
? metadata
|
|
1111
|
-
:
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
: []
|
|
1212
|
+
const existingMetadata = execution?.metadata && typeof execution.metadata === 'object'
|
|
1213
|
+
? execution.metadata
|
|
1214
|
+
: {};
|
|
1215
|
+
const mergedMetadata = mergeExecutionMetadata(existingMetadata, metadata ?? {});
|
|
1216
|
+
const suppliedMemoryReceiptIds = [existingMetadata.memoryReceiptIds, metadata?.memoryReceiptIds]
|
|
1217
|
+
.flatMap(value => Array.isArray(value) ? value : [])
|
|
1218
|
+
.filter((id) => typeof id === 'string');
|
|
1219
|
+
const suppliedDistillReceiptIds = [existingMetadata.distillReceiptIds, metadata?.distillReceiptIds]
|
|
1220
|
+
.flatMap(value => Array.isArray(value) ? value : [])
|
|
1221
|
+
.filter((id) => typeof id === 'string');
|
|
1115
1222
|
const memoryReceiptIds = [...new Set([
|
|
1116
1223
|
...suppliedMemoryReceiptIds,
|
|
1117
1224
|
...eventReceiptIds.filter(id => id.startsWith('memory-receipt:')),
|
|
@@ -1137,7 +1244,7 @@ class DevFlowDatabase {
|
|
|
1137
1244
|
fallbackCount: directFallbacks.length + hookFallbacks.length,
|
|
1138
1245
|
fallbackReasons,
|
|
1139
1246
|
metadata: {
|
|
1140
|
-
...
|
|
1247
|
+
...mergedMetadata,
|
|
1141
1248
|
applicableObligations: obligation.applicable,
|
|
1142
1249
|
satisfiedObligations: obligation.satisfied,
|
|
1143
1250
|
missedTools: obligation.missedTools,
|
|
@@ -1708,14 +1815,7 @@ class DevFlowDatabase {
|
|
|
1708
1815
|
return row ? this.mapSessionClosure(row) : null;
|
|
1709
1816
|
}
|
|
1710
1817
|
completeSessionClosure(projectRoot, sessionId, excludingWorkItemId, closedAt = Date.now()) {
|
|
1711
|
-
const
|
|
1712
|
-
SELECT COUNT(*) AS count
|
|
1713
|
-
FROM devflow_work_items
|
|
1714
|
-
WHERE project_root = ? AND session_id = ?
|
|
1715
|
-
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
1716
|
-
AND (? IS NULL OR id <> ?)
|
|
1717
|
-
`).get(projectRoot, sessionId, excludingWorkItemId ?? null, excludingWorkItemId ?? null);
|
|
1718
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
1818
|
+
const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId, excludingWorkItemId);
|
|
1719
1819
|
const state = pendingWorkCount > 0 ? 'closed_with_pending_work' : 'closed';
|
|
1720
1820
|
this.db.prepare(`
|
|
1721
1821
|
UPDATE devflow_session_closures
|
|
@@ -1823,6 +1923,16 @@ class DevFlowDatabase {
|
|
|
1823
1923
|
WHERE id = ? AND state = 'leased' AND lease_owner = ?
|
|
1824
1924
|
`).run(nextAttemptAt, error.category, error.message, updatedAt, id, owner).changes === 1;
|
|
1825
1925
|
}
|
|
1926
|
+
deferWork(id, owner, error, nextAttemptAt) {
|
|
1927
|
+
const updatedAt = Date.now();
|
|
1928
|
+
return this.db.prepare(`
|
|
1929
|
+
UPDATE devflow_work_items
|
|
1930
|
+
SET state = 'pending', attempts = MAX(0, attempts - 1),
|
|
1931
|
+
lease_owner = NULL, lease_expires_at = NULL, next_attempt_at = ?,
|
|
1932
|
+
error_category = ?, error_message = ?, updated_at = ?
|
|
1933
|
+
WHERE id = ? AND state = 'leased' AND lease_owner = ?
|
|
1934
|
+
`).run(nextAttemptAt, error.category, error.message, updatedAt, id, owner).changes === 1;
|
|
1935
|
+
}
|
|
1826
1936
|
deadLetterWork(id, owner, error) {
|
|
1827
1937
|
return this.db.prepare(`
|
|
1828
1938
|
UPDATE devflow_work_items
|
|
@@ -1909,13 +2019,7 @@ class DevFlowDatabase {
|
|
|
1909
2019
|
};
|
|
1910
2020
|
}
|
|
1911
2021
|
refreshClosedSessionClosure(projectRoot, sessionId, now) {
|
|
1912
|
-
const
|
|
1913
|
-
SELECT COUNT(*) AS count
|
|
1914
|
-
FROM devflow_work_items
|
|
1915
|
-
WHERE project_root = ? AND session_id = ?
|
|
1916
|
-
AND state IN ('pending', 'leased', 'failed', 'dead_letter')
|
|
1917
|
-
`).get(projectRoot, sessionId);
|
|
1918
|
-
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2022
|
+
const pendingWorkCount = this.countSessionLifecyclePending(projectRoot, sessionId);
|
|
1919
2023
|
this.db.prepare(`
|
|
1920
2024
|
UPDATE devflow_session_closures
|
|
1921
2025
|
SET state = CASE WHEN ? = 0 THEN 'closed' ELSE 'closed_with_pending_work' END,
|
|
@@ -1924,6 +2028,34 @@ class DevFlowDatabase {
|
|
|
1924
2028
|
AND state IN ('closed', 'closed_with_pending_work')
|
|
1925
2029
|
`).run(pendingWorkCount, pendingWorkCount, now, projectRoot, sessionId);
|
|
1926
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
|
+
}
|
|
1927
2059
|
// ---- Hook Lifecycle ----
|
|
1928
2060
|
getHookReceipt(projectRoot) {
|
|
1929
2061
|
const row = this.db.prepare(`
|
|
@@ -1977,17 +2109,23 @@ class DevFlowDatabase {
|
|
|
1977
2109
|
upsertContextReceipt(receipt) {
|
|
1978
2110
|
this.db.prepare(`
|
|
1979
2111
|
INSERT INTO devflow_context_receipts
|
|
1980
|
-
(project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
1981
|
-
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1982
2115
|
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
1983
2116
|
context_hash = excluded.context_hash,
|
|
1984
2117
|
issued_at = excluded.issued_at,
|
|
1985
|
-
expires_at = excluded.expires_at
|
|
1986
|
-
|
|
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);
|
|
1987
2124
|
}
|
|
1988
2125
|
getContextReceipt(projectRoot, sessionId, executionId) {
|
|
1989
2126
|
const row = this.db.prepare(`
|
|
1990
|
-
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
|
|
1991
2129
|
FROM devflow_context_receipts
|
|
1992
2130
|
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
1993
2131
|
`).get(projectRoot, sessionId, executionId);
|
|
@@ -1998,8 +2136,77 @@ class DevFlowDatabase {
|
|
|
1998
2136
|
contextHash: row.context_hash,
|
|
1999
2137
|
issuedAt: row.issued_at,
|
|
2000
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,
|
|
2001
2169
|
} : null;
|
|
2002
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
|
+
}
|
|
2003
2210
|
deleteContextReceipt(projectRoot, sessionId, executionId) {
|
|
2004
2211
|
return executionId
|
|
2005
2212
|
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
@@ -2014,15 +2221,104 @@ class DevFlowDatabase {
|
|
|
2014
2221
|
.run(now).changes;
|
|
2015
2222
|
}
|
|
2016
2223
|
beginMemoryTurn(input) {
|
|
2224
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
2017
2225
|
this.db.prepare(`
|
|
2018
2226
|
INSERT OR IGNORE INTO devflow_memory_turns
|
|
2019
2227
|
(turn_id, project_root, session_id, prompt_hash, event_id, status, memory_ids, created_at)
|
|
2020
2228
|
VALUES (?, ?, ?, ?, ?, 'pending', '[]', ?)
|
|
2021
|
-
`).run(
|
|
2022
|
-
|
|
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);
|
|
2023
2319
|
}
|
|
2024
2320
|
getMemoryTurn(turnId) {
|
|
2025
|
-
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));
|
|
2026
2322
|
return row ? this.mapMemoryTurn(row) : null;
|
|
2027
2323
|
}
|
|
2028
2324
|
getPendingMemoryTurn(projectRoot, sessionId) {
|
|
@@ -2034,35 +2330,73 @@ class DevFlowDatabase {
|
|
|
2034
2330
|
return row ? this.mapMemoryTurn(row) : null;
|
|
2035
2331
|
}
|
|
2036
2332
|
commitMemoryTurn(input) {
|
|
2333
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
2037
2334
|
this.db.prepare(`
|
|
2038
2335
|
UPDATE devflow_memory_turns
|
|
2039
2336
|
SET status = 'committed', receipt_id = ?, memory_ids = ?, source = ?, reason = ?, decided_at = ?
|
|
2040
2337
|
WHERE turn_id = ? AND status = 'pending'
|
|
2041
|
-
`).run(input.receiptId, JSON.stringify([...new Set(input.memoryIds)]), input.source, input.reason ?? null, input.decidedAt ?? Date.now(),
|
|
2042
|
-
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);
|
|
2043
2340
|
if (!turn || turn.status !== 'committed') {
|
|
2044
|
-
throw new Error(`Memory turn ${
|
|
2045
|
-
}
|
|
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
|
+
});
|
|
2046
2353
|
return turn;
|
|
2047
2354
|
}
|
|
2048
2355
|
skipMemoryTurn(input) {
|
|
2356
|
+
const turnId = (0, obligation_ledger_1.normalizeTurnId)(input.turnId);
|
|
2049
2357
|
this.db.prepare(`
|
|
2050
2358
|
UPDATE devflow_memory_turns
|
|
2051
2359
|
SET status = 'skipped', receipt_id = ?, memory_ids = '[]', source = 'host_skip',
|
|
2052
2360
|
reason = ?, decided_at = ?
|
|
2053
2361
|
WHERE turn_id = ? AND status = 'pending'
|
|
2054
|
-
`).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(),
|
|
2055
|
-
const turn = this.getMemoryTurn(
|
|
2362
|
+
`).run(input.receiptId, input.reason, input.decidedAt ?? Date.now(), turnId);
|
|
2363
|
+
const turn = this.getMemoryTurn(turnId);
|
|
2056
2364
|
if (!turn || turn.status !== 'skipped') {
|
|
2057
|
-
throw new Error(`Memory turn ${
|
|
2058
|
-
}
|
|
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
|
+
});
|
|
2059
2377
|
return turn;
|
|
2060
2378
|
}
|
|
2061
2379
|
markMemoryTurnStopPrompted(turnId, promptedAt = Date.now()) {
|
|
2062
2380
|
return this.db.prepare(`
|
|
2063
2381
|
UPDATE devflow_memory_turns SET stop_prompted_at = ?
|
|
2064
2382
|
WHERE turn_id = ? AND status = 'pending' AND stop_prompted_at IS NULL
|
|
2065
|
-
`).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
|
+
});
|
|
2066
2400
|
}
|
|
2067
2401
|
listMemoryTurns(projectRoot, sessionId, limit = 50) {
|
|
2068
2402
|
const rows = (sessionId
|
|
@@ -2074,6 +2408,14 @@ class DevFlowDatabase {
|
|
|
2074
2408
|
.all(projectRoot, limit));
|
|
2075
2409
|
return rows.map(row => this.mapMemoryTurn(row));
|
|
2076
2410
|
}
|
|
2411
|
+
listSessionMemoryTurnsForReconciliation(projectRoot, sessionId) {
|
|
2412
|
+
const rows = this.db.prepare(`
|
|
2413
|
+
SELECT * FROM devflow_memory_turns
|
|
2414
|
+
WHERE project_root = ? AND session_id = ?
|
|
2415
|
+
ORDER BY created_at ASC, turn_id ASC
|
|
2416
|
+
`).all(projectRoot, sessionId);
|
|
2417
|
+
return rows.map(row => this.mapMemoryTurn(row));
|
|
2418
|
+
}
|
|
2077
2419
|
insertHookFallback(record) {
|
|
2078
2420
|
return this.db.prepare(`
|
|
2079
2421
|
INSERT OR IGNORE INTO devflow_hook_fallbacks
|
|
@@ -2147,13 +2489,18 @@ class DevFlowDatabase {
|
|
|
2147
2489
|
recordMemoryDistillCheckpoint(checkpoint) {
|
|
2148
2490
|
this.db.prepare(`
|
|
2149
2491
|
INSERT INTO devflow_memory_distill_checkpoints
|
|
2150
|
-
(id, project_root, session_id, trigger, pending_events, released_leases, created_at)
|
|
2151
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2152
|
-
|
|
2492
|
+
(id, project_root, session_id, trigger, pending_events, released_leases, details, created_at)
|
|
2493
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2494
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2495
|
+
pending_events = excluded.pending_events,
|
|
2496
|
+
released_leases = MAX(devflow_memory_distill_checkpoints.released_leases, excluded.released_leases),
|
|
2497
|
+
details = excluded.details,
|
|
2498
|
+
created_at = MIN(devflow_memory_distill_checkpoints.created_at, excluded.created_at)
|
|
2499
|
+
`).run(checkpoint.id, checkpoint.projectRoot, checkpoint.sessionId ?? null, checkpoint.trigger, checkpoint.pendingEvents, checkpoint.releasedLeases, JSON.stringify(checkpoint.details ?? {}), checkpoint.createdAt);
|
|
2153
2500
|
}
|
|
2154
2501
|
listMemoryDistillCheckpoints(projectRoot, limit = 50) {
|
|
2155
2502
|
const rows = this.db.prepare(`
|
|
2156
|
-
SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
|
|
2503
|
+
SELECT id, project_root, session_id, trigger, pending_events, released_leases, details, created_at
|
|
2157
2504
|
FROM devflow_memory_distill_checkpoints
|
|
2158
2505
|
WHERE project_root = ?
|
|
2159
2506
|
ORDER BY created_at DESC
|
|
@@ -2166,6 +2513,7 @@ class DevFlowDatabase {
|
|
|
2166
2513
|
trigger: row.trigger,
|
|
2167
2514
|
pendingEvents: Number(row.pending_events),
|
|
2168
2515
|
releasedLeases: Number(row.released_leases),
|
|
2516
|
+
details: row.details ? JSON.parse(row.details) : {},
|
|
2169
2517
|
createdAt: Number(row.created_at),
|
|
2170
2518
|
}));
|
|
2171
2519
|
}
|
|
@@ -2191,6 +2539,12 @@ function parseJson(value) {
|
|
|
2191
2539
|
return value;
|
|
2192
2540
|
}
|
|
2193
2541
|
}
|
|
2542
|
+
function parseJsonStringArray(value) {
|
|
2543
|
+
const parsed = parseJson(value);
|
|
2544
|
+
return Array.isArray(parsed)
|
|
2545
|
+
? parsed.filter((item) => typeof item === 'string')
|
|
2546
|
+
: [];
|
|
2547
|
+
}
|
|
2194
2548
|
function deriveQuery(input) {
|
|
2195
2549
|
if (!input || typeof input !== 'object')
|
|
2196
2550
|
return null;
|
|
@@ -2266,3 +2620,39 @@ function collectCanonicalReceiptIds(values) {
|
|
|
2266
2620
|
values.forEach(value => visit(value, 0));
|
|
2267
2621
|
return [...receiptIds];
|
|
2268
2622
|
}
|
|
2623
|
+
function stableMetadataKey(value) {
|
|
2624
|
+
if (value === null || typeof value !== 'object')
|
|
2625
|
+
return JSON.stringify(value) ?? String(value);
|
|
2626
|
+
if (Array.isArray(value))
|
|
2627
|
+
return `[${value.map(stableMetadataKey).join(',')}]`;
|
|
2628
|
+
const record = value;
|
|
2629
|
+
return `{${Object.keys(record).sort()
|
|
2630
|
+
.map(key => `${JSON.stringify(key)}:${stableMetadataKey(record[key])}`)
|
|
2631
|
+
.join(',')}}`;
|
|
2632
|
+
}
|
|
2633
|
+
function unionMetadataArrays(existing, incoming) {
|
|
2634
|
+
const values = [
|
|
2635
|
+
...(Array.isArray(existing) ? existing : []),
|
|
2636
|
+
...(Array.isArray(incoming) ? incoming : []),
|
|
2637
|
+
];
|
|
2638
|
+
if (values.length === 0 && !Array.isArray(existing) && !Array.isArray(incoming))
|
|
2639
|
+
return undefined;
|
|
2640
|
+
const unique = new Map();
|
|
2641
|
+
for (const value of values)
|
|
2642
|
+
unique.set(stableMetadataKey(value), value);
|
|
2643
|
+
return [...unique.values()];
|
|
2644
|
+
}
|
|
2645
|
+
function mergeExecutionMetadata(existing, incoming) {
|
|
2646
|
+
const merged = { ...existing, ...incoming };
|
|
2647
|
+
for (const key of [
|
|
2648
|
+
'evidenceDegradations',
|
|
2649
|
+
'evidenceContractOutcomes',
|
|
2650
|
+
'pendingEvidenceContractIds',
|
|
2651
|
+
'sessionFinalizationDegradations',
|
|
2652
|
+
]) {
|
|
2653
|
+
const union = unionMetadataArrays(existing[key], incoming[key]);
|
|
2654
|
+
if (union !== undefined)
|
|
2655
|
+
merged[key] = union;
|
|
2656
|
+
}
|
|
2657
|
+
return merged;
|
|
2658
|
+
}
|