@origintrail-official/dkg-node-ui 10.0.12 → 10.0.14
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/LICENSE +201 -201
- package/README.md +50 -50
- package/dist/chain-cursor-stores.js +6 -6
- package/dist/chat-memory.d.ts +43 -2
- package/dist/chat-memory.d.ts.map +1 -1
- package/dist/chat-memory.js +240 -216
- package/dist/chat-memory.js.map +1 -1
- package/dist/db.d.ts +18 -10
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1118 -988
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist-ui/assets/{3d-force-graph-Bl6v-uII.js → 3d-force-graph-C9KowW4u.js} +1 -1
- package/dist-ui/assets/{AgentHub-D2vaQxlw.js → AgentHub-BVrcom9_.js} +1 -1
- package/dist-ui/assets/{AgentProfilePage-BgSTdsjD.js → AgentProfilePage-C9XaTt7j.js} +1 -1
- package/dist-ui/assets/{ApproveWalletsModal-D0Ub_eiD.js → ApproveWalletsModal-BdR2mshx.js} +3 -3
- package/dist-ui/assets/{ConvictionDetailView-CieVypna.js → ConvictionDetailView-cao48GL1.js} +1 -1
- package/dist-ui/assets/{Network-DKw_XEGi.js → Network-B3jCYzr5.js} +1 -1
- package/dist-ui/assets/{OnChainProvenanceCard-Dn_IEP7Q.js → OnChainProvenanceCard-DboMX4Aq.js} +1 -1
- package/dist-ui/assets/{Operations-BO2C3UWB.js → Operations-Dg1gfLXv.js} +1 -1
- package/dist-ui/assets/{PublishingConviction-xKRNHbzk.js → PublishingConviction-Byp6t69e.js} +1 -1
- package/dist-ui/assets/{Settings-DC_d9STV.js → Settings-CxI0SKeV.js} +1 -1
- package/dist-ui/assets/{ccip-zWfeQL8Q.js → ccip-wJ3OMRuz.js} +1 -1
- package/dist-ui/assets/{index-CwRQnNYh.js → index-5DS6xjw0.js} +3 -3
- package/dist-ui/assets/index-DOjsv0r7.css +1 -0
- package/dist-ui/assets/index-KoAnx9JE.js +1661 -0
- package/dist-ui/assets/{jsonld-B67IUHNU-C3ydusi_.js → jsonld-32FQRO67-CDirPi8u.js} +2 -2
- package/dist-ui/assets/{jsonld-G-CM-8JR.js → jsonld-CwHW9UDN.js} +1 -1
- package/dist-ui/assets/{renderer-3d-OSESAPET-ConXqyK6.js → renderer-3d-2EVDZII7-BNXV6iSu.js} +2 -2
- package/dist-ui/assets/{shikiHighlighter-C6lRYd1O.js → shikiHighlighter-C-LWPPSu.js} +1 -1
- package/dist-ui/index.html +14 -14
- package/package.json +4 -3
- package/dist-ui/assets/index-DEtfq9qv.css +0 -1
- package/dist-ui/assets/index-hrI-Ir4l.js +0 -1661
- /package/dist-ui/assets/{ntriples-SF2LFNMC-DH7OofPp.js → ntriples-ZWBY2WET-DH7OofPp.js} +0 -0
- /package/dist-ui/assets/{turtle-BXN4L2BY-BzTu0283.js → turtle-JJPK7LJ5-BzTu0283.js} +0 -0
package/dist/db.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { RESPONSE_CACHE_BYTES, parseContextGraphJoinPolicyRecord, } from '@origintrail-official/dkg-core';
|
|
3
|
+
import { RESPONSE_CACHE_BYTES, parseContextGraphJoinPolicyRecord, DEFAULT_SYNC_CHECKPOINT_TTL_MS, isValidSyncCheckpointEntry, transitionSyncCheckpointManifestOffset, transitionSyncCheckpointOffset, transitionSyncCheckpointResponderSession, withoutSyncCheckpointResponderSession, } from '@origintrail-official/dkg-core';
|
|
4
4
|
export { SqliteChainEventCursorStore, SqliteContextGraphRegistryScanCursorStore, } from './chain-cursor-stores.js';
|
|
5
|
-
const SCHEMA_VERSION =
|
|
5
|
+
export const SCHEMA_VERSION = 34;
|
|
6
6
|
// Default operator retention. Lowered from 90 → 14 days on V15 (2026-05) after
|
|
7
7
|
// a production incident in which the `logs` table + its FTS5 shadow tables
|
|
8
8
|
// grew to ~9 GB on a 12-day-old node and corrupted the SQLite page (header
|
|
@@ -138,44 +138,44 @@ export class DashboardDB {
|
|
|
138
138
|
migrate() {
|
|
139
139
|
const version = this.db.pragma('user_version', { simple: true });
|
|
140
140
|
const upgradedExistingDb = version > 0 && version < SCHEMA_VERSION;
|
|
141
|
-
const ensureJoinPolicyAuditCapTrigger = () => this.db.exec(`
|
|
142
|
-
CREATE TRIGGER IF NOT EXISTS cap_cg_join_policy_audit_rows
|
|
143
|
-
AFTER INSERT ON context_graph_join_policy_audit
|
|
144
|
-
BEGIN
|
|
145
|
-
DELETE FROM context_graph_join_policy_audit
|
|
146
|
-
WHERE id <= NEW.id - 100000
|
|
147
|
-
AND event_type NOT IN (
|
|
148
|
-
'join_admission_committed',
|
|
149
|
-
'join_policy_changed'
|
|
150
|
-
);
|
|
151
|
-
END;
|
|
141
|
+
const ensureJoinPolicyAuditCapTrigger = () => this.db.exec(`
|
|
142
|
+
CREATE TRIGGER IF NOT EXISTS cap_cg_join_policy_audit_rows
|
|
143
|
+
AFTER INSERT ON context_graph_join_policy_audit
|
|
144
|
+
BEGIN
|
|
145
|
+
DELETE FROM context_graph_join_policy_audit
|
|
146
|
+
WHERE id <= NEW.id - 100000
|
|
147
|
+
AND event_type NOT IN (
|
|
148
|
+
'join_admission_committed',
|
|
149
|
+
'join_policy_changed'
|
|
150
|
+
);
|
|
151
|
+
END;
|
|
152
152
|
`);
|
|
153
153
|
const ensureJoinApprovalRepairMarker = () => {
|
|
154
|
-
const table = this.db.prepare(`
|
|
155
|
-
SELECT 1 AS found FROM sqlite_master
|
|
156
|
-
WHERE type = 'table' AND name = 'context_graph_join_approval_ledger'
|
|
154
|
+
const table = this.db.prepare(`
|
|
155
|
+
SELECT 1 AS found FROM sqlite_master
|
|
156
|
+
WHERE type = 'table' AND name = 'context_graph_join_approval_ledger'
|
|
157
157
|
`).get();
|
|
158
158
|
if (!table)
|
|
159
159
|
return;
|
|
160
160
|
const columns = this.db.pragma('table_info(context_graph_join_approval_ledger)');
|
|
161
161
|
if (!columns.some((column) => column.name === 'repair_pending')) {
|
|
162
|
-
this.db.exec(`
|
|
163
|
-
ALTER TABLE context_graph_join_approval_ledger
|
|
164
|
-
ADD COLUMN repair_pending INTEGER NOT NULL DEFAULT 0
|
|
165
|
-
CHECK (repair_pending IN (0, 1));
|
|
162
|
+
this.db.exec(`
|
|
163
|
+
ALTER TABLE context_graph_join_approval_ledger
|
|
164
|
+
ADD COLUMN repair_pending INTEGER NOT NULL DEFAULT 0
|
|
165
|
+
CHECK (repair_pending IN (0, 1));
|
|
166
166
|
`);
|
|
167
167
|
}
|
|
168
|
-
this.db.exec(`
|
|
169
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_repair
|
|
170
|
-
ON context_graph_join_approval_ledger(
|
|
171
|
-
context_graph_id, request_digest, repair_pending, reserved_at DESC
|
|
172
|
-
);
|
|
168
|
+
this.db.exec(`
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_repair
|
|
170
|
+
ON context_graph_join_approval_ledger(
|
|
171
|
+
context_graph_id, request_digest, repair_pending, reserved_at DESC
|
|
172
|
+
);
|
|
173
173
|
`);
|
|
174
174
|
};
|
|
175
|
-
const
|
|
176
|
-
const table = this.db.prepare(`
|
|
177
|
-
SELECT 1 AS found FROM sqlite_master
|
|
178
|
-
WHERE type = 'table' AND name = 'sync_checkpoints'
|
|
175
|
+
const ensureSyncCheckpointResumeColumns = () => {
|
|
176
|
+
const table = this.db.prepare(`
|
|
177
|
+
SELECT 1 AS found FROM sqlite_master
|
|
178
|
+
WHERE type = 'table' AND name = 'sync_checkpoints'
|
|
179
179
|
`).get();
|
|
180
180
|
if (!table)
|
|
181
181
|
return;
|
|
@@ -187,6 +187,18 @@ export class DashboardDB {
|
|
|
187
187
|
if (!columns.has('responder_session_expires_at')) {
|
|
188
188
|
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN responder_session_expires_at INTEGER;`);
|
|
189
189
|
}
|
|
190
|
+
if (!columns.has('responder_session_offset')) {
|
|
191
|
+
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN responder_session_offset INTEGER;`);
|
|
192
|
+
}
|
|
193
|
+
if (!columns.has('manifest_digest')) {
|
|
194
|
+
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN manifest_digest TEXT;`);
|
|
195
|
+
}
|
|
196
|
+
if (!columns.has('manifest_prefix_digest')) {
|
|
197
|
+
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN manifest_prefix_digest TEXT;`);
|
|
198
|
+
}
|
|
199
|
+
if (!columns.has('terminal')) {
|
|
200
|
+
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN terminal INTEGER NOT NULL DEFAULT 0 CHECK (terminal IN (0, 1));`);
|
|
201
|
+
}
|
|
190
202
|
};
|
|
191
203
|
if (version > SCHEMA_VERSION)
|
|
192
204
|
return;
|
|
@@ -194,122 +206,122 @@ export class DashboardDB {
|
|
|
194
206
|
// Repair restored/development databases that carry the current version
|
|
195
207
|
// but lost an idempotent schema adjunct.
|
|
196
208
|
ensureJoinApprovalRepairMarker();
|
|
197
|
-
|
|
209
|
+
ensureSyncCheckpointResumeColumns();
|
|
198
210
|
ensureJoinPolicyAuditCapTrigger();
|
|
199
211
|
return;
|
|
200
212
|
}
|
|
201
213
|
if (version < 1) {
|
|
202
|
-
this.db.exec(`
|
|
203
|
-
CREATE TABLE IF NOT EXISTS metric_snapshots (
|
|
204
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
205
|
-
ts INTEGER NOT NULL,
|
|
206
|
-
cpu_percent REAL,
|
|
207
|
-
mem_used_bytes INTEGER,
|
|
208
|
-
mem_total_bytes INTEGER,
|
|
209
|
-
disk_used_bytes INTEGER,
|
|
210
|
-
disk_total_bytes INTEGER,
|
|
211
|
-
heap_used_bytes INTEGER,
|
|
212
|
-
uptime_seconds INTEGER,
|
|
213
|
-
peer_count INTEGER,
|
|
214
|
-
direct_peers INTEGER,
|
|
215
|
-
relayed_peers INTEGER,
|
|
216
|
-
mesh_peers INTEGER,
|
|
217
|
-
contextGraph_count INTEGER,
|
|
218
|
-
total_triples INTEGER,
|
|
219
|
-
total_kcs INTEGER,
|
|
220
|
-
total_kas INTEGER,
|
|
221
|
-
store_bytes INTEGER,
|
|
222
|
-
confirmed_kcs INTEGER,
|
|
223
|
-
tentative_kcs INTEGER,
|
|
224
|
-
rpc_latency_ms INTEGER,
|
|
225
|
-
rpc_healthy INTEGER,
|
|
226
|
-
relay_capacity INTEGER,
|
|
227
|
-
relay_reservation_count INTEGER,
|
|
228
|
-
relay_active_circuits INTEGER,
|
|
229
|
-
relay_bytes_in INTEGER,
|
|
230
|
-
relay_bytes_out INTEGER
|
|
231
|
-
);
|
|
232
|
-
CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON metric_snapshots(ts);
|
|
233
|
-
|
|
234
|
-
CREATE TABLE IF NOT EXISTS operations (
|
|
235
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
236
|
-
operation_id TEXT NOT NULL,
|
|
237
|
-
operation_name TEXT NOT NULL,
|
|
238
|
-
started_at INTEGER NOT NULL,
|
|
239
|
-
duration_ms INTEGER,
|
|
240
|
-
status TEXT DEFAULT 'in_progress',
|
|
241
|
-
peer_id TEXT,
|
|
242
|
-
contextGraph_id TEXT,
|
|
243
|
-
triple_count INTEGER,
|
|
244
|
-
error_message TEXT,
|
|
245
|
-
details TEXT
|
|
246
|
-
);
|
|
247
|
-
CREATE INDEX IF NOT EXISTS idx_ops_operation_id ON operations(operation_id);
|
|
248
|
-
CREATE INDEX IF NOT EXISTS idx_ops_started_at ON operations(started_at);
|
|
249
|
-
CREATE INDEX IF NOT EXISTS idx_ops_name ON operations(operation_name);
|
|
250
|
-
|
|
251
|
-
CREATE TABLE IF NOT EXISTS logs (
|
|
252
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
253
|
-
ts INTEGER NOT NULL,
|
|
254
|
-
level TEXT NOT NULL,
|
|
255
|
-
operation_name TEXT,
|
|
256
|
-
operation_id TEXT,
|
|
257
|
-
module TEXT,
|
|
258
|
-
message TEXT NOT NULL
|
|
259
|
-
);
|
|
260
|
-
CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
|
|
261
|
-
CREATE INDEX IF NOT EXISTS idx_logs_operation_id ON logs(operation_id);
|
|
262
|
-
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
263
|
-
|
|
264
|
-
-- NOTE: Earlier schema versions (V1..V14) also created an FTS5
|
|
265
|
-
-- virtual table "logs_fts" plus AFTER INSERT/DELETE triggers to
|
|
266
|
-
-- keep it in sync with "logs". That fed a free-text search path
|
|
267
|
-
-- behind /api/logs?q=... The FTS5 index turned out to dominate
|
|
268
|
-
-- on-disk size (multi-GB shadow tables on long-lived nodes) and
|
|
269
|
-
-- the corresponding HTTP route had no production consumer
|
|
270
|
-
-- (the dashboard log viewer is file-backed via /api/node-log).
|
|
271
|
-
-- V15 drops it for fresh installs; the migration below cleans
|
|
272
|
-
-- it up for in-place upgrades.
|
|
273
|
-
|
|
274
|
-
CREATE TABLE IF NOT EXISTS query_history (
|
|
275
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
276
|
-
ts INTEGER NOT NULL,
|
|
277
|
-
sparql TEXT NOT NULL,
|
|
278
|
-
duration_ms INTEGER,
|
|
279
|
-
result_count INTEGER,
|
|
280
|
-
error TEXT
|
|
281
|
-
);
|
|
282
|
-
CREATE INDEX IF NOT EXISTS idx_qhist_ts ON query_history(ts);
|
|
283
|
-
|
|
284
|
-
CREATE TABLE IF NOT EXISTS saved_queries (
|
|
285
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
286
|
-
name TEXT NOT NULL,
|
|
287
|
-
description TEXT,
|
|
288
|
-
sparql TEXT NOT NULL,
|
|
289
|
-
created_at INTEGER NOT NULL,
|
|
290
|
-
updated_at INTEGER NOT NULL
|
|
291
|
-
);
|
|
214
|
+
this.db.exec(`
|
|
215
|
+
CREATE TABLE IF NOT EXISTS metric_snapshots (
|
|
216
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
217
|
+
ts INTEGER NOT NULL,
|
|
218
|
+
cpu_percent REAL,
|
|
219
|
+
mem_used_bytes INTEGER,
|
|
220
|
+
mem_total_bytes INTEGER,
|
|
221
|
+
disk_used_bytes INTEGER,
|
|
222
|
+
disk_total_bytes INTEGER,
|
|
223
|
+
heap_used_bytes INTEGER,
|
|
224
|
+
uptime_seconds INTEGER,
|
|
225
|
+
peer_count INTEGER,
|
|
226
|
+
direct_peers INTEGER,
|
|
227
|
+
relayed_peers INTEGER,
|
|
228
|
+
mesh_peers INTEGER,
|
|
229
|
+
contextGraph_count INTEGER,
|
|
230
|
+
total_triples INTEGER,
|
|
231
|
+
total_kcs INTEGER,
|
|
232
|
+
total_kas INTEGER,
|
|
233
|
+
store_bytes INTEGER,
|
|
234
|
+
confirmed_kcs INTEGER,
|
|
235
|
+
tentative_kcs INTEGER,
|
|
236
|
+
rpc_latency_ms INTEGER,
|
|
237
|
+
rpc_healthy INTEGER,
|
|
238
|
+
relay_capacity INTEGER,
|
|
239
|
+
relay_reservation_count INTEGER,
|
|
240
|
+
relay_active_circuits INTEGER,
|
|
241
|
+
relay_bytes_in INTEGER,
|
|
242
|
+
relay_bytes_out INTEGER
|
|
243
|
+
);
|
|
244
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON metric_snapshots(ts);
|
|
245
|
+
|
|
246
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
247
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
248
|
+
operation_id TEXT NOT NULL,
|
|
249
|
+
operation_name TEXT NOT NULL,
|
|
250
|
+
started_at INTEGER NOT NULL,
|
|
251
|
+
duration_ms INTEGER,
|
|
252
|
+
status TEXT DEFAULT 'in_progress',
|
|
253
|
+
peer_id TEXT,
|
|
254
|
+
contextGraph_id TEXT,
|
|
255
|
+
triple_count INTEGER,
|
|
256
|
+
error_message TEXT,
|
|
257
|
+
details TEXT
|
|
258
|
+
);
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_ops_operation_id ON operations(operation_id);
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_ops_started_at ON operations(started_at);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_ops_name ON operations(operation_name);
|
|
262
|
+
|
|
263
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
264
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
265
|
+
ts INTEGER NOT NULL,
|
|
266
|
+
level TEXT NOT NULL,
|
|
267
|
+
operation_name TEXT,
|
|
268
|
+
operation_id TEXT,
|
|
269
|
+
module TEXT,
|
|
270
|
+
message TEXT NOT NULL
|
|
271
|
+
);
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
|
|
273
|
+
CREATE INDEX IF NOT EXISTS idx_logs_operation_id ON logs(operation_id);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
275
|
+
|
|
276
|
+
-- NOTE: Earlier schema versions (V1..V14) also created an FTS5
|
|
277
|
+
-- virtual table "logs_fts" plus AFTER INSERT/DELETE triggers to
|
|
278
|
+
-- keep it in sync with "logs". That fed a free-text search path
|
|
279
|
+
-- behind /api/logs?q=... The FTS5 index turned out to dominate
|
|
280
|
+
-- on-disk size (multi-GB shadow tables on long-lived nodes) and
|
|
281
|
+
-- the corresponding HTTP route had no production consumer
|
|
282
|
+
-- (the dashboard log viewer is file-backed via /api/node-log).
|
|
283
|
+
-- V15 drops it for fresh installs; the migration below cleans
|
|
284
|
+
-- it up for in-place upgrades.
|
|
285
|
+
|
|
286
|
+
CREATE TABLE IF NOT EXISTS query_history (
|
|
287
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
288
|
+
ts INTEGER NOT NULL,
|
|
289
|
+
sparql TEXT NOT NULL,
|
|
290
|
+
duration_ms INTEGER,
|
|
291
|
+
result_count INTEGER,
|
|
292
|
+
error TEXT
|
|
293
|
+
);
|
|
294
|
+
CREATE INDEX IF NOT EXISTS idx_qhist_ts ON query_history(ts);
|
|
295
|
+
|
|
296
|
+
CREATE TABLE IF NOT EXISTS saved_queries (
|
|
297
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
298
|
+
name TEXT NOT NULL,
|
|
299
|
+
description TEXT,
|
|
300
|
+
sparql TEXT NOT NULL,
|
|
301
|
+
created_at INTEGER NOT NULL,
|
|
302
|
+
updated_at INTEGER NOT NULL
|
|
303
|
+
);
|
|
292
304
|
`);
|
|
293
305
|
}
|
|
294
306
|
if (version < 2) {
|
|
295
|
-
this.db.exec(`
|
|
296
|
-
CREATE TABLE IF NOT EXISTS operation_phases (
|
|
297
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
298
|
-
operation_id TEXT NOT NULL,
|
|
299
|
-
phase TEXT NOT NULL,
|
|
300
|
-
started_at INTEGER NOT NULL,
|
|
301
|
-
duration_ms INTEGER,
|
|
302
|
-
status TEXT DEFAULT 'in_progress',
|
|
303
|
-
details TEXT
|
|
304
|
-
);
|
|
305
|
-
CREATE INDEX IF NOT EXISTS idx_phases_op ON operation_phases(operation_id);
|
|
306
|
-
|
|
307
|
-
ALTER TABLE operations ADD COLUMN gas_used INTEGER;
|
|
308
|
-
ALTER TABLE operations ADD COLUMN gas_price_gwei REAL;
|
|
309
|
-
ALTER TABLE operations ADD COLUMN gas_cost_eth REAL;
|
|
310
|
-
ALTER TABLE operations ADD COLUMN trac_cost REAL;
|
|
311
|
-
ALTER TABLE operations ADD COLUMN tx_hash TEXT;
|
|
312
|
-
ALTER TABLE operations ADD COLUMN chain_id INTEGER;
|
|
307
|
+
this.db.exec(`
|
|
308
|
+
CREATE TABLE IF NOT EXISTS operation_phases (
|
|
309
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
310
|
+
operation_id TEXT NOT NULL,
|
|
311
|
+
phase TEXT NOT NULL,
|
|
312
|
+
started_at INTEGER NOT NULL,
|
|
313
|
+
duration_ms INTEGER,
|
|
314
|
+
status TEXT DEFAULT 'in_progress',
|
|
315
|
+
details TEXT
|
|
316
|
+
);
|
|
317
|
+
CREATE INDEX IF NOT EXISTS idx_phases_op ON operation_phases(operation_id);
|
|
318
|
+
|
|
319
|
+
ALTER TABLE operations ADD COLUMN gas_used INTEGER;
|
|
320
|
+
ALTER TABLE operations ADD COLUMN gas_price_gwei REAL;
|
|
321
|
+
ALTER TABLE operations ADD COLUMN gas_cost_eth REAL;
|
|
322
|
+
ALTER TABLE operations ADD COLUMN trac_cost REAL;
|
|
323
|
+
ALTER TABLE operations ADD COLUMN tx_hash TEXT;
|
|
324
|
+
ALTER TABLE operations ADD COLUMN chain_id INTEGER;
|
|
313
325
|
`);
|
|
314
326
|
}
|
|
315
327
|
if (version < 3) {
|
|
@@ -319,42 +331,42 @@ export class DashboardDB {
|
|
|
319
331
|
// column + the partial unique index in one step; the V11 block
|
|
320
332
|
// below idempotently adds the column to nodes that upgraded
|
|
321
333
|
// through versions 3..10 first.
|
|
322
|
-
this.db.exec(`
|
|
323
|
-
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
324
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
325
|
-
ts INTEGER NOT NULL,
|
|
326
|
-
direction TEXT NOT NULL,
|
|
327
|
-
peer TEXT NOT NULL,
|
|
328
|
-
peer_name TEXT,
|
|
329
|
-
text TEXT NOT NULL,
|
|
330
|
-
delivered INTEGER,
|
|
331
|
-
message_id TEXT
|
|
332
|
-
);
|
|
333
|
-
CREATE INDEX IF NOT EXISTS idx_chat_ts ON chat_messages(ts);
|
|
334
|
-
CREATE INDEX IF NOT EXISTS idx_chat_peer ON chat_messages(peer);
|
|
334
|
+
this.db.exec(`
|
|
335
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
336
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
337
|
+
ts INTEGER NOT NULL,
|
|
338
|
+
direction TEXT NOT NULL,
|
|
339
|
+
peer TEXT NOT NULL,
|
|
340
|
+
peer_name TEXT,
|
|
341
|
+
text TEXT NOT NULL,
|
|
342
|
+
delivered INTEGER,
|
|
343
|
+
message_id TEXT
|
|
344
|
+
);
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_chat_ts ON chat_messages(ts);
|
|
346
|
+
CREATE INDEX IF NOT EXISTS idx_chat_peer ON chat_messages(peer);
|
|
335
347
|
`);
|
|
336
348
|
}
|
|
337
349
|
if (version < 4) {
|
|
338
|
-
this.db.exec(`
|
|
339
|
-
CREATE TABLE IF NOT EXISTS chat_persistence_jobs (
|
|
340
|
-
turn_id TEXT PRIMARY KEY,
|
|
341
|
-
session_id TEXT NOT NULL,
|
|
342
|
-
user_message TEXT NOT NULL,
|
|
343
|
-
assistant_reply TEXT NOT NULL,
|
|
344
|
-
tool_calls_json TEXT,
|
|
345
|
-
status TEXT NOT NULL,
|
|
346
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
347
|
-
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
348
|
-
next_attempt_at INTEGER NOT NULL,
|
|
349
|
-
queued_at INTEGER NOT NULL,
|
|
350
|
-
updated_at INTEGER NOT NULL,
|
|
351
|
-
store_ms INTEGER,
|
|
352
|
-
error_message TEXT
|
|
353
|
-
);
|
|
354
|
-
CREATE INDEX IF NOT EXISTS idx_chat_persist_status_next
|
|
355
|
-
ON chat_persistence_jobs(status, next_attempt_at);
|
|
356
|
-
CREATE INDEX IF NOT EXISTS idx_chat_persist_session
|
|
357
|
-
ON chat_persistence_jobs(session_id);
|
|
350
|
+
this.db.exec(`
|
|
351
|
+
CREATE TABLE IF NOT EXISTS chat_persistence_jobs (
|
|
352
|
+
turn_id TEXT PRIMARY KEY,
|
|
353
|
+
session_id TEXT NOT NULL,
|
|
354
|
+
user_message TEXT NOT NULL,
|
|
355
|
+
assistant_reply TEXT NOT NULL,
|
|
356
|
+
tool_calls_json TEXT,
|
|
357
|
+
status TEXT NOT NULL,
|
|
358
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
359
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
360
|
+
next_attempt_at INTEGER NOT NULL,
|
|
361
|
+
queued_at INTEGER NOT NULL,
|
|
362
|
+
updated_at INTEGER NOT NULL,
|
|
363
|
+
store_ms INTEGER,
|
|
364
|
+
error_message TEXT
|
|
365
|
+
);
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_chat_persist_status_next
|
|
367
|
+
ON chat_persistence_jobs(status, next_attempt_at);
|
|
368
|
+
CREATE INDEX IF NOT EXISTS idx_chat_persist_session
|
|
369
|
+
ON chat_persistence_jobs(session_id);
|
|
358
370
|
`);
|
|
359
371
|
}
|
|
360
372
|
if (version < 5) {
|
|
@@ -363,77 +375,77 @@ export class DashboardDB {
|
|
|
363
375
|
// fresh-install CREATE so new nodes get the column + its index in one
|
|
364
376
|
// step; the V16 block below idempotently adds it to nodes that upgraded
|
|
365
377
|
// through versions 5..15 first (same pattern as `message_id` in V3/V11).
|
|
366
|
-
this.db.exec(`
|
|
367
|
-
CREATE TABLE IF NOT EXISTS notifications (
|
|
368
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
369
|
-
ts INTEGER NOT NULL,
|
|
370
|
-
type TEXT NOT NULL,
|
|
371
|
-
title TEXT NOT NULL,
|
|
372
|
-
message TEXT NOT NULL,
|
|
373
|
-
source TEXT,
|
|
374
|
-
peer TEXT,
|
|
375
|
-
read INTEGER NOT NULL DEFAULT 0,
|
|
376
|
-
meta TEXT,
|
|
377
|
-
context_graph_id TEXT
|
|
378
|
-
);
|
|
379
|
-
CREATE INDEX IF NOT EXISTS idx_notif_ts ON notifications(ts);
|
|
380
|
-
CREATE INDEX IF NOT EXISTS idx_notif_read ON notifications(read);
|
|
381
|
-
CREATE INDEX IF NOT EXISTS idx_notif_cg ON notifications(context_graph_id);
|
|
378
|
+
this.db.exec(`
|
|
379
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
380
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
381
|
+
ts INTEGER NOT NULL,
|
|
382
|
+
type TEXT NOT NULL,
|
|
383
|
+
title TEXT NOT NULL,
|
|
384
|
+
message TEXT NOT NULL,
|
|
385
|
+
source TEXT,
|
|
386
|
+
peer TEXT,
|
|
387
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
388
|
+
meta TEXT,
|
|
389
|
+
context_graph_id TEXT
|
|
390
|
+
);
|
|
391
|
+
CREATE INDEX IF NOT EXISTS idx_notif_ts ON notifications(ts);
|
|
392
|
+
CREATE INDEX IF NOT EXISTS idx_notif_read ON notifications(read);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS idx_notif_cg ON notifications(context_graph_id);
|
|
382
394
|
`);
|
|
383
395
|
}
|
|
384
396
|
if (version < 6) {
|
|
385
|
-
this.db.exec(`
|
|
386
|
-
CREATE TABLE IF NOT EXISTS settings (
|
|
387
|
-
key TEXT PRIMARY KEY,
|
|
388
|
-
value TEXT NOT NULL
|
|
389
|
-
);
|
|
397
|
+
this.db.exec(`
|
|
398
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
399
|
+
key TEXT PRIMARY KEY,
|
|
400
|
+
value TEXT NOT NULL
|
|
401
|
+
);
|
|
390
402
|
`);
|
|
391
403
|
}
|
|
392
404
|
if (version < 7) {
|
|
393
|
-
this.db.exec(`
|
|
394
|
-
CREATE TABLE IF NOT EXISTS context_graph_subscriptions (
|
|
395
|
-
context_graph_id TEXT PRIMARY KEY,
|
|
396
|
-
name TEXT,
|
|
397
|
-
subscribed INTEGER NOT NULL,
|
|
398
|
-
synced INTEGER NOT NULL,
|
|
399
|
-
meta_synced INTEGER,
|
|
400
|
-
on_chain_id TEXT,
|
|
401
|
-
sync_scoped INTEGER NOT NULL DEFAULT 1,
|
|
402
|
-
updated_at INTEGER NOT NULL
|
|
403
|
-
);
|
|
404
|
-
CREATE INDEX IF NOT EXISTS idx_cg_subs_sync_scoped
|
|
405
|
-
ON context_graph_subscriptions(sync_scoped);
|
|
405
|
+
this.db.exec(`
|
|
406
|
+
CREATE TABLE IF NOT EXISTS context_graph_subscriptions (
|
|
407
|
+
context_graph_id TEXT PRIMARY KEY,
|
|
408
|
+
name TEXT,
|
|
409
|
+
subscribed INTEGER NOT NULL,
|
|
410
|
+
synced INTEGER NOT NULL,
|
|
411
|
+
meta_synced INTEGER,
|
|
412
|
+
on_chain_id TEXT,
|
|
413
|
+
sync_scoped INTEGER NOT NULL DEFAULT 1,
|
|
414
|
+
updated_at INTEGER NOT NULL
|
|
415
|
+
);
|
|
416
|
+
CREATE INDEX IF NOT EXISTS idx_cg_subs_sync_scoped
|
|
417
|
+
ON context_graph_subscriptions(sync_scoped);
|
|
406
418
|
`);
|
|
407
419
|
}
|
|
408
420
|
if (version < 8) {
|
|
409
|
-
this.db.exec(`
|
|
410
|
-
CREATE TABLE IF NOT EXISTS context_graph_memberships (
|
|
411
|
-
context_graph_id TEXT NOT NULL,
|
|
412
|
-
principal_type TEXT NOT NULL,
|
|
413
|
-
principal_id TEXT NOT NULL,
|
|
414
|
-
role TEXT,
|
|
415
|
-
status TEXT NOT NULL,
|
|
416
|
-
source TEXT,
|
|
417
|
-
display_name TEXT,
|
|
418
|
-
metadata TEXT,
|
|
419
|
-
first_seen_at INTEGER NOT NULL,
|
|
420
|
-
updated_at INTEGER NOT NULL,
|
|
421
|
-
PRIMARY KEY (context_graph_id, principal_type, principal_id)
|
|
422
|
-
);
|
|
423
|
-
CREATE INDEX IF NOT EXISTS idx_cg_members_context
|
|
424
|
-
ON context_graph_memberships(context_graph_id);
|
|
425
|
-
CREATE INDEX IF NOT EXISTS idx_cg_members_principal
|
|
426
|
-
ON context_graph_memberships(principal_type, principal_id);
|
|
427
|
-
CREATE INDEX IF NOT EXISTS idx_cg_members_status
|
|
428
|
-
ON context_graph_memberships(status);
|
|
421
|
+
this.db.exec(`
|
|
422
|
+
CREATE TABLE IF NOT EXISTS context_graph_memberships (
|
|
423
|
+
context_graph_id TEXT NOT NULL,
|
|
424
|
+
principal_type TEXT NOT NULL,
|
|
425
|
+
principal_id TEXT NOT NULL,
|
|
426
|
+
role TEXT,
|
|
427
|
+
status TEXT NOT NULL,
|
|
428
|
+
source TEXT,
|
|
429
|
+
display_name TEXT,
|
|
430
|
+
metadata TEXT,
|
|
431
|
+
first_seen_at INTEGER NOT NULL,
|
|
432
|
+
updated_at INTEGER NOT NULL,
|
|
433
|
+
PRIMARY KEY (context_graph_id, principal_type, principal_id)
|
|
434
|
+
);
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_cg_members_context
|
|
436
|
+
ON context_graph_memberships(context_graph_id);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_cg_members_principal
|
|
438
|
+
ON context_graph_memberships(principal_type, principal_id);
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_cg_members_status
|
|
440
|
+
ON context_graph_memberships(status);
|
|
429
441
|
`);
|
|
430
442
|
}
|
|
431
443
|
if (version < 9) {
|
|
432
444
|
const columns = this.db.prepare('PRAGMA table_info(context_graph_subscriptions)').all();
|
|
433
445
|
if (!columns.some((column) => column.name === 'shared_memory_synced')) {
|
|
434
|
-
this.db.exec(`
|
|
435
|
-
ALTER TABLE context_graph_subscriptions
|
|
436
|
-
ADD COLUMN shared_memory_synced INTEGER;
|
|
446
|
+
this.db.exec(`
|
|
447
|
+
ALTER TABLE context_graph_subscriptions
|
|
448
|
+
ADD COLUMN shared_memory_synced INTEGER;
|
|
437
449
|
`);
|
|
438
450
|
}
|
|
439
451
|
}
|
|
@@ -502,10 +514,10 @@ export class DashboardDB {
|
|
|
502
514
|
if (!chatCols.has('message_id')) {
|
|
503
515
|
this.db.exec(`ALTER TABLE chat_messages ADD COLUMN message_id TEXT;`);
|
|
504
516
|
}
|
|
505
|
-
this.db.exec(`
|
|
506
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_msgid
|
|
507
|
-
ON chat_messages(peer, direction, message_id)
|
|
508
|
-
WHERE message_id IS NOT NULL;
|
|
517
|
+
this.db.exec(`
|
|
518
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_msgid
|
|
519
|
+
ON chat_messages(peer, direction, message_id)
|
|
520
|
+
WHERE message_id IS NOT NULL;
|
|
509
521
|
`);
|
|
510
522
|
}
|
|
511
523
|
if (version < 12) {
|
|
@@ -561,33 +573,33 @@ export class DashboardDB {
|
|
|
561
573
|
// `message_id` is enforced via `message_idempotency` instead.
|
|
562
574
|
// V13 will also preserve `chat_messages.message_id` as
|
|
563
575
|
// nullable + unwritten for rollback safety.
|
|
564
|
-
this.db.exec(`
|
|
565
|
-
CREATE TABLE IF NOT EXISTS message_idempotency (
|
|
566
|
-
peer_id TEXT NOT NULL,
|
|
567
|
-
protocol TEXT NOT NULL,
|
|
568
|
-
message_id TEXT NOT NULL,
|
|
569
|
-
direction TEXT NOT NULL CHECK (direction IN ('in', 'out')),
|
|
570
|
-
response_blob BLOB,
|
|
571
|
-
response_size INTEGER NOT NULL DEFAULT 0,
|
|
572
|
-
ts INTEGER NOT NULL,
|
|
573
|
-
PRIMARY KEY (peer_id, protocol, message_id, direction)
|
|
574
|
-
);
|
|
575
|
-
CREATE INDEX IF NOT EXISTS idx_idem_ts ON message_idempotency(ts);
|
|
576
|
-
|
|
577
|
-
CREATE TABLE IF NOT EXISTS protocol_outbox (
|
|
578
|
-
peer_id TEXT NOT NULL,
|
|
579
|
-
protocol TEXT NOT NULL,
|
|
580
|
-
message_id TEXT NOT NULL,
|
|
581
|
-
payload BLOB NOT NULL,
|
|
582
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
583
|
-
first_failure_at INTEGER NOT NULL,
|
|
584
|
-
last_attempt_at INTEGER NOT NULL,
|
|
585
|
-
next_attempt_at INTEGER NOT NULL,
|
|
586
|
-
last_error TEXT,
|
|
587
|
-
PRIMARY KEY (peer_id, protocol, message_id)
|
|
588
|
-
);
|
|
589
|
-
CREATE INDEX IF NOT EXISTS idx_outbox_next_attempt
|
|
590
|
-
ON protocol_outbox(next_attempt_at);
|
|
576
|
+
this.db.exec(`
|
|
577
|
+
CREATE TABLE IF NOT EXISTS message_idempotency (
|
|
578
|
+
peer_id TEXT NOT NULL,
|
|
579
|
+
protocol TEXT NOT NULL,
|
|
580
|
+
message_id TEXT NOT NULL,
|
|
581
|
+
direction TEXT NOT NULL CHECK (direction IN ('in', 'out')),
|
|
582
|
+
response_blob BLOB,
|
|
583
|
+
response_size INTEGER NOT NULL DEFAULT 0,
|
|
584
|
+
ts INTEGER NOT NULL,
|
|
585
|
+
PRIMARY KEY (peer_id, protocol, message_id, direction)
|
|
586
|
+
);
|
|
587
|
+
CREATE INDEX IF NOT EXISTS idx_idem_ts ON message_idempotency(ts);
|
|
588
|
+
|
|
589
|
+
CREATE TABLE IF NOT EXISTS protocol_outbox (
|
|
590
|
+
peer_id TEXT NOT NULL,
|
|
591
|
+
protocol TEXT NOT NULL,
|
|
592
|
+
message_id TEXT NOT NULL,
|
|
593
|
+
payload BLOB NOT NULL,
|
|
594
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
595
|
+
first_failure_at INTEGER NOT NULL,
|
|
596
|
+
last_attempt_at INTEGER NOT NULL,
|
|
597
|
+
next_attempt_at INTEGER NOT NULL,
|
|
598
|
+
last_error TEXT,
|
|
599
|
+
PRIMARY KEY (peer_id, protocol, message_id)
|
|
600
|
+
);
|
|
601
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_next_attempt
|
|
602
|
+
ON protocol_outbox(next_attempt_at);
|
|
591
603
|
`);
|
|
592
604
|
}
|
|
593
605
|
if (version < 13) {
|
|
@@ -612,8 +624,8 @@ export class DashboardDB {
|
|
|
612
624
|
// table rebuild on downgrade.
|
|
613
625
|
//
|
|
614
626
|
// No data migration. No new tables. Pure DROP INDEX.
|
|
615
|
-
this.db.exec(`
|
|
616
|
-
DROP INDEX IF EXISTS idx_chat_msgid;
|
|
627
|
+
this.db.exec(`
|
|
628
|
+
DROP INDEX IF EXISTS idx_chat_msgid;
|
|
617
629
|
`);
|
|
618
630
|
}
|
|
619
631
|
if (version < 14) {
|
|
@@ -654,10 +666,10 @@ export class DashboardDB {
|
|
|
654
666
|
// operation-correlated log views (`getOperation`,
|
|
655
667
|
// `getFailedOperations`). Substring/text search moves to
|
|
656
668
|
// /api/node-log, which already supports `?q=`.
|
|
657
|
-
this.db.exec(`
|
|
658
|
-
DROP TRIGGER IF EXISTS logs_ai;
|
|
659
|
-
DROP TRIGGER IF EXISTS logs_ad;
|
|
660
|
-
DROP TABLE IF EXISTS logs_fts;
|
|
669
|
+
this.db.exec(`
|
|
670
|
+
DROP TRIGGER IF EXISTS logs_ai;
|
|
671
|
+
DROP TRIGGER IF EXISTS logs_ad;
|
|
672
|
+
DROP TABLE IF EXISTS logs_fts;
|
|
661
673
|
`);
|
|
662
674
|
// VACUUM cannot run inside a transaction; better-sqlite3 wraps
|
|
663
675
|
// multi-statement exec() in implicit BEGIN/COMMIT, so we issue
|
|
@@ -721,23 +733,23 @@ export class DashboardDB {
|
|
|
721
733
|
// re-parsing the daemon log. One row per decision point (sweep / fetch /
|
|
722
734
|
// promote / already / defer / cursor-advance / core-fill). Pruned by `ts`
|
|
723
735
|
// like the other event tables.
|
|
724
|
-
this.db.exec(`
|
|
725
|
-
CREATE TABLE IF NOT EXISTS replication_events (
|
|
726
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
727
|
-
ts INTEGER NOT NULL,
|
|
728
|
-
context_graph_id TEXT NOT NULL,
|
|
729
|
-
on_chain_cg_id TEXT,
|
|
730
|
-
action TEXT NOT NULL,
|
|
731
|
-
ual TEXT,
|
|
732
|
-
ordinal INTEGER,
|
|
733
|
-
ka_id TEXT,
|
|
734
|
-
from_watermark INTEGER,
|
|
735
|
-
to_watermark INTEGER,
|
|
736
|
-
head INTEGER,
|
|
737
|
-
reconciled INTEGER,
|
|
738
|
-
pending INTEGER,
|
|
739
|
-
detail TEXT
|
|
740
|
-
);
|
|
736
|
+
this.db.exec(`
|
|
737
|
+
CREATE TABLE IF NOT EXISTS replication_events (
|
|
738
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
739
|
+
ts INTEGER NOT NULL,
|
|
740
|
+
context_graph_id TEXT NOT NULL,
|
|
741
|
+
on_chain_cg_id TEXT,
|
|
742
|
+
action TEXT NOT NULL,
|
|
743
|
+
ual TEXT,
|
|
744
|
+
ordinal INTEGER,
|
|
745
|
+
ka_id TEXT,
|
|
746
|
+
from_watermark INTEGER,
|
|
747
|
+
to_watermark INTEGER,
|
|
748
|
+
head INTEGER,
|
|
749
|
+
reconciled INTEGER,
|
|
750
|
+
pending INTEGER,
|
|
751
|
+
detail TEXT
|
|
752
|
+
);
|
|
741
753
|
`);
|
|
742
754
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_repl_ts ON replication_events(ts);`);
|
|
743
755
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_repl_cg ON replication_events(context_graph_id, ts);`);
|
|
@@ -776,54 +788,60 @@ export class DashboardDB {
|
|
|
776
788
|
// the 2^63 INTEGER max and under the uint96 on-chain field. Like
|
|
777
789
|
// `protocol_outbox`, this state is durable and is NEVER pruned —
|
|
778
790
|
// reclaiming a number could mint a duplicate kaId.
|
|
779
|
-
this.db.exec(`
|
|
780
|
-
CREATE TABLE IF NOT EXISTS ka_numbers (
|
|
781
|
-
author_address TEXT PRIMARY KEY,
|
|
782
|
-
next_number INTEGER NOT NULL
|
|
783
|
-
);
|
|
791
|
+
this.db.exec(`
|
|
792
|
+
CREATE TABLE IF NOT EXISTS ka_numbers (
|
|
793
|
+
author_address TEXT PRIMARY KEY,
|
|
794
|
+
next_number INTEGER NOT NULL
|
|
795
|
+
);
|
|
784
796
|
`);
|
|
785
797
|
}
|
|
786
798
|
if (version < 21) {
|
|
787
|
-
this.db.exec(`
|
|
788
|
-
CREATE TABLE IF NOT EXISTS sync_checkpoints (
|
|
789
|
-
key TEXT PRIMARY KEY,
|
|
790
|
-
offset INTEGER NOT NULL CHECK (offset >= 0),
|
|
791
|
-
updated_at INTEGER NOT NULL,
|
|
792
|
-
expires_at INTEGER NOT NULL
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
799
|
+
this.db.exec(`
|
|
800
|
+
CREATE TABLE IF NOT EXISTS sync_checkpoints (
|
|
801
|
+
key TEXT PRIMARY KEY,
|
|
802
|
+
offset INTEGER NOT NULL CHECK (offset >= 0),
|
|
803
|
+
updated_at INTEGER NOT NULL,
|
|
804
|
+
expires_at INTEGER NOT NULL,
|
|
805
|
+
responder_session_id TEXT,
|
|
806
|
+
responder_session_expires_at INTEGER,
|
|
807
|
+
responder_session_offset INTEGER CHECK (responder_session_offset >= 0),
|
|
808
|
+
manifest_digest TEXT,
|
|
809
|
+
manifest_prefix_digest TEXT,
|
|
810
|
+
terminal INTEGER NOT NULL DEFAULT 0 CHECK (terminal IN (0, 1))
|
|
811
|
+
);
|
|
812
|
+
CREATE INDEX IF NOT EXISTS idx_sync_checkpoints_expires_at
|
|
813
|
+
ON sync_checkpoints(expires_at);
|
|
796
814
|
`);
|
|
797
815
|
}
|
|
798
816
|
if (version < 22) {
|
|
799
|
-
this.db.exec(`
|
|
800
|
-
CREATE TABLE IF NOT EXISTS runtime_cursors (
|
|
801
|
-
namespace TEXT NOT NULL,
|
|
802
|
-
scope TEXT NOT NULL,
|
|
803
|
-
key TEXT NOT NULL,
|
|
804
|
-
value INTEGER NOT NULL CHECK (value > 0),
|
|
805
|
-
updated_at INTEGER NOT NULL,
|
|
806
|
-
PRIMARY KEY (namespace, scope, key)
|
|
807
|
-
);
|
|
817
|
+
this.db.exec(`
|
|
818
|
+
CREATE TABLE IF NOT EXISTS runtime_cursors (
|
|
819
|
+
namespace TEXT NOT NULL,
|
|
820
|
+
scope TEXT NOT NULL,
|
|
821
|
+
key TEXT NOT NULL,
|
|
822
|
+
value INTEGER NOT NULL CHECK (value > 0),
|
|
823
|
+
updated_at INTEGER NOT NULL,
|
|
824
|
+
PRIMARY KEY (namespace, scope, key)
|
|
825
|
+
);
|
|
808
826
|
`);
|
|
809
|
-
this.db.exec(`
|
|
810
|
-
CREATE INDEX IF NOT EXISTS idx_runtime_cursors_namespace_scope
|
|
811
|
-
ON runtime_cursors(namespace, scope);
|
|
827
|
+
this.db.exec(`
|
|
828
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_cursors_namespace_scope
|
|
829
|
+
ON runtime_cursors(namespace, scope);
|
|
812
830
|
`);
|
|
813
831
|
}
|
|
814
832
|
if (version < 23) {
|
|
815
833
|
// OT-RFC-59 SC5: durable per-(peer, contextGraph) changelog cursor. NEVER
|
|
816
834
|
// pruned (see SqliteChangelogCursorStore) — a TTL would defeat the O(delta)
|
|
817
835
|
// cross-restart catch-up this table exists for.
|
|
818
|
-
this.db.exec(`
|
|
819
|
-
CREATE TABLE IF NOT EXISTS changelog_cursors (
|
|
820
|
-
peer_id TEXT NOT NULL,
|
|
821
|
-
context_graph_id TEXT NOT NULL,
|
|
822
|
-
era TEXT NOT NULL,
|
|
823
|
-
seq INTEGER NOT NULL CHECK (seq >= 0),
|
|
824
|
-
updated_at INTEGER NOT NULL,
|
|
825
|
-
PRIMARY KEY (peer_id, context_graph_id)
|
|
826
|
-
);
|
|
836
|
+
this.db.exec(`
|
|
837
|
+
CREATE TABLE IF NOT EXISTS changelog_cursors (
|
|
838
|
+
peer_id TEXT NOT NULL,
|
|
839
|
+
context_graph_id TEXT NOT NULL,
|
|
840
|
+
era TEXT NOT NULL,
|
|
841
|
+
seq INTEGER NOT NULL CHECK (seq >= 0),
|
|
842
|
+
updated_at INTEGER NOT NULL,
|
|
843
|
+
PRIMARY KEY (peer_id, context_graph_id)
|
|
844
|
+
);
|
|
827
845
|
`);
|
|
828
846
|
}
|
|
829
847
|
if (version < 24) {
|
|
@@ -832,39 +850,39 @@ export class DashboardDB {
|
|
|
832
850
|
// `store.nq` restore does NOT roll back with the RDF store. node-ui.db
|
|
833
851
|
// survives such a restore, so a rollback under the same era rotates the era
|
|
834
852
|
// (forcing peers to full-resync instead of silently skipping). Single row.
|
|
835
|
-
this.db.exec(`
|
|
836
|
-
CREATE TABLE IF NOT EXISTS changelog_era (
|
|
837
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
838
|
-
era TEXT NOT NULL,
|
|
839
|
-
high_seq INTEGER NOT NULL CHECK (high_seq >= 0),
|
|
840
|
-
updated_at INTEGER NOT NULL
|
|
841
|
-
);
|
|
853
|
+
this.db.exec(`
|
|
854
|
+
CREATE TABLE IF NOT EXISTS changelog_era (
|
|
855
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
856
|
+
era TEXT NOT NULL,
|
|
857
|
+
high_seq INTEGER NOT NULL CHECK (high_seq >= 0),
|
|
858
|
+
updated_at INTEGER NOT NULL
|
|
859
|
+
);
|
|
842
860
|
`);
|
|
843
861
|
}
|
|
844
862
|
if (version < 25) {
|
|
845
863
|
// Durable security audit for private-CG open enrollment. V25/V26 also
|
|
846
864
|
// used this table as admission state; V27 projects those legacy rows into
|
|
847
865
|
// the typed operational ledger below.
|
|
848
|
-
this.db.exec(`
|
|
849
|
-
CREATE TABLE IF NOT EXISTS context_graph_join_policy_audit (
|
|
850
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
851
|
-
ts INTEGER NOT NULL,
|
|
852
|
-
context_graph_id TEXT NOT NULL,
|
|
853
|
-
event_type TEXT NOT NULL,
|
|
854
|
-
actor TEXT,
|
|
855
|
-
agent_address TEXT,
|
|
856
|
-
outcome TEXT NOT NULL,
|
|
857
|
-
reason TEXT,
|
|
858
|
-
request_digest TEXT,
|
|
859
|
-
policy_version INTEGER,
|
|
860
|
-
details TEXT
|
|
861
|
-
);
|
|
862
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_policy_audit_ts
|
|
863
|
-
ON context_graph_join_policy_audit(ts);
|
|
864
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_policy_audit_cg_ts
|
|
865
|
-
ON context_graph_join_policy_audit(context_graph_id, ts);
|
|
866
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_policy_audit_event_ts
|
|
867
|
-
ON context_graph_join_policy_audit(event_type, ts);
|
|
866
|
+
this.db.exec(`
|
|
867
|
+
CREATE TABLE IF NOT EXISTS context_graph_join_policy_audit (
|
|
868
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
869
|
+
ts INTEGER NOT NULL,
|
|
870
|
+
context_graph_id TEXT NOT NULL,
|
|
871
|
+
event_type TEXT NOT NULL,
|
|
872
|
+
actor TEXT,
|
|
873
|
+
agent_address TEXT,
|
|
874
|
+
outcome TEXT NOT NULL,
|
|
875
|
+
reason TEXT,
|
|
876
|
+
request_digest TEXT,
|
|
877
|
+
policy_version INTEGER,
|
|
878
|
+
details TEXT
|
|
879
|
+
);
|
|
880
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_policy_audit_ts
|
|
881
|
+
ON context_graph_join_policy_audit(ts);
|
|
882
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_policy_audit_cg_ts
|
|
883
|
+
ON context_graph_join_policy_audit(context_graph_id, ts);
|
|
884
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_policy_audit_event_ts
|
|
885
|
+
ON context_graph_join_policy_audit(event_type, ts);
|
|
868
886
|
`);
|
|
869
887
|
}
|
|
870
888
|
if (version < 26) {
|
|
@@ -883,108 +901,108 @@ export class DashboardDB {
|
|
|
883
901
|
// Operational admission state is deliberately separate from the audit
|
|
884
902
|
// stream. Audit retention and event naming must never alter rate-limit,
|
|
885
903
|
// epoch, or commit-idempotency behaviour.
|
|
886
|
-
this.db.exec(`
|
|
887
|
-
CREATE TABLE IF NOT EXISTS context_graph_join_approval_ledger (
|
|
888
|
-
context_graph_id TEXT NOT NULL,
|
|
889
|
-
request_digest TEXT NOT NULL,
|
|
890
|
-
policy_epoch INTEGER NOT NULL,
|
|
891
|
-
reserved_at INTEGER NOT NULL,
|
|
892
|
-
state TEXT NOT NULL CHECK (state IN ('reserved', 'committed')),
|
|
893
|
-
committed_at INTEGER,
|
|
894
|
-
actor TEXT NOT NULL,
|
|
895
|
-
agent_address TEXT NOT NULL,
|
|
896
|
-
policy_version INTEGER NOT NULL,
|
|
897
|
-
PRIMARY KEY (context_graph_id, request_digest, policy_epoch),
|
|
898
|
-
CHECK (length(context_graph_id) > 0),
|
|
899
|
-
CHECK (length(request_digest) > 0),
|
|
900
|
-
CHECK (typeof(policy_epoch) = 'integer'),
|
|
901
|
-
CHECK (typeof(reserved_at) = 'integer'),
|
|
902
|
-
CHECK (committed_at IS NULL OR typeof(committed_at) = 'integer'),
|
|
903
|
-
CHECK (typeof(policy_version) = 'integer'),
|
|
904
|
-
CHECK (
|
|
905
|
-
(state = 'reserved' AND committed_at IS NULL)
|
|
906
|
-
OR (state = 'committed' AND committed_at IS NOT NULL)
|
|
907
|
-
)
|
|
908
|
-
);
|
|
909
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_reserved_at
|
|
910
|
-
ON context_graph_join_approval_ledger(reserved_at);
|
|
911
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_cg_reserved_at
|
|
912
|
-
ON context_graph_join_approval_ledger(context_graph_id, reserved_at);
|
|
913
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_digest_reserved_at
|
|
914
|
-
ON context_graph_join_approval_ledger(context_graph_id, request_digest, reserved_at DESC);
|
|
915
|
-
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_committed_at
|
|
916
|
-
ON context_graph_join_approval_ledger(committed_at);
|
|
904
|
+
this.db.exec(`
|
|
905
|
+
CREATE TABLE IF NOT EXISTS context_graph_join_approval_ledger (
|
|
906
|
+
context_graph_id TEXT NOT NULL,
|
|
907
|
+
request_digest TEXT NOT NULL,
|
|
908
|
+
policy_epoch INTEGER NOT NULL,
|
|
909
|
+
reserved_at INTEGER NOT NULL,
|
|
910
|
+
state TEXT NOT NULL CHECK (state IN ('reserved', 'committed')),
|
|
911
|
+
committed_at INTEGER,
|
|
912
|
+
actor TEXT NOT NULL,
|
|
913
|
+
agent_address TEXT NOT NULL,
|
|
914
|
+
policy_version INTEGER NOT NULL,
|
|
915
|
+
PRIMARY KEY (context_graph_id, request_digest, policy_epoch),
|
|
916
|
+
CHECK (length(context_graph_id) > 0),
|
|
917
|
+
CHECK (length(request_digest) > 0),
|
|
918
|
+
CHECK (typeof(policy_epoch) = 'integer'),
|
|
919
|
+
CHECK (typeof(reserved_at) = 'integer'),
|
|
920
|
+
CHECK (committed_at IS NULL OR typeof(committed_at) = 'integer'),
|
|
921
|
+
CHECK (typeof(policy_version) = 'integer'),
|
|
922
|
+
CHECK (
|
|
923
|
+
(state = 'reserved' AND committed_at IS NULL)
|
|
924
|
+
OR (state = 'committed' AND committed_at IS NOT NULL)
|
|
925
|
+
)
|
|
926
|
+
);
|
|
927
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_reserved_at
|
|
928
|
+
ON context_graph_join_approval_ledger(reserved_at);
|
|
929
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_cg_reserved_at
|
|
930
|
+
ON context_graph_join_approval_ledger(context_graph_id, reserved_at);
|
|
931
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_digest_reserved_at
|
|
932
|
+
ON context_graph_join_approval_ledger(context_graph_id, request_digest, reserved_at DESC);
|
|
933
|
+
CREATE INDEX IF NOT EXISTS idx_cg_join_approval_ledger_committed_at
|
|
934
|
+
ON context_graph_join_approval_ledger(committed_at);
|
|
917
935
|
`);
|
|
918
936
|
// Best-effort one-time projection for V25/V26 databases. Invalid legacy
|
|
919
937
|
// JSON cannot abort startup; only rows with a typed numeric epoch can be
|
|
920
938
|
// authoritative operational state. For duplicate audit reservations,
|
|
921
939
|
// retain the newest row for each (CG, digest, epoch).
|
|
922
|
-
this.db.exec(`
|
|
923
|
-
WITH raw_reservations AS (
|
|
924
|
-
SELECT
|
|
925
|
-
id,
|
|
926
|
-
context_graph_id,
|
|
927
|
-
request_digest,
|
|
928
|
-
CASE
|
|
929
|
-
WHEN json_valid(details)
|
|
930
|
-
AND json_type(details, '$.policyEpoch') IN ('integer', 'real')
|
|
931
|
-
THEN CAST(json_extract(details, '$.policyEpoch') AS INTEGER)
|
|
932
|
-
ELSE NULL
|
|
933
|
-
END AS policy_epoch,
|
|
934
|
-
ts AS reserved_at,
|
|
935
|
-
COALESCE(actor, '') AS actor,
|
|
936
|
-
COALESCE(agent_address, '') AS agent_address,
|
|
937
|
-
COALESCE(policy_version, 1) AS policy_version
|
|
938
|
-
FROM context_graph_join_policy_audit
|
|
939
|
-
WHERE event_type = 'join_auto_reservation'
|
|
940
|
-
AND outcome = 'reserved'
|
|
941
|
-
AND request_digest IS NOT NULL
|
|
942
|
-
),
|
|
943
|
-
ranked_reservations AS (
|
|
944
|
-
SELECT *, ROW_NUMBER() OVER (
|
|
945
|
-
PARTITION BY context_graph_id, request_digest, policy_epoch
|
|
946
|
-
ORDER BY reserved_at DESC, id DESC
|
|
947
|
-
) AS rank
|
|
948
|
-
FROM raw_reservations
|
|
949
|
-
WHERE policy_epoch IS NOT NULL
|
|
950
|
-
),
|
|
951
|
-
raw_commits AS (
|
|
952
|
-
SELECT
|
|
953
|
-
context_graph_id,
|
|
954
|
-
request_digest,
|
|
955
|
-
CASE
|
|
956
|
-
WHEN json_valid(details)
|
|
957
|
-
AND json_type(details, '$.policyEpoch') IN ('integer', 'real')
|
|
958
|
-
THEN CAST(json_extract(details, '$.policyEpoch') AS INTEGER)
|
|
959
|
-
ELSE NULL
|
|
960
|
-
END AS policy_epoch,
|
|
961
|
-
MIN(ts) AS committed_at
|
|
962
|
-
FROM context_graph_join_policy_audit
|
|
963
|
-
WHERE event_type = 'join_admission_committed'
|
|
964
|
-
AND outcome = 'approved'
|
|
965
|
-
AND request_digest IS NOT NULL
|
|
966
|
-
GROUP BY context_graph_id, request_digest, policy_epoch
|
|
967
|
-
)
|
|
968
|
-
INSERT OR IGNORE INTO context_graph_join_approval_ledger (
|
|
969
|
-
context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
970
|
-
state, committed_at, actor, agent_address, policy_version
|
|
971
|
-
)
|
|
972
|
-
SELECT
|
|
973
|
-
reservation.context_graph_id,
|
|
974
|
-
reservation.request_digest,
|
|
975
|
-
reservation.policy_epoch,
|
|
976
|
-
reservation.reserved_at,
|
|
977
|
-
CASE WHEN commit_row.committed_at IS NULL THEN 'reserved' ELSE 'committed' END,
|
|
978
|
-
commit_row.committed_at,
|
|
979
|
-
reservation.actor,
|
|
980
|
-
reservation.agent_address,
|
|
981
|
-
reservation.policy_version
|
|
982
|
-
FROM ranked_reservations AS reservation
|
|
983
|
-
LEFT JOIN raw_commits AS commit_row
|
|
984
|
-
ON commit_row.context_graph_id = reservation.context_graph_id
|
|
985
|
-
AND commit_row.request_digest = reservation.request_digest
|
|
986
|
-
AND commit_row.policy_epoch = reservation.policy_epoch
|
|
987
|
-
WHERE reservation.rank = 1;
|
|
940
|
+
this.db.exec(`
|
|
941
|
+
WITH raw_reservations AS (
|
|
942
|
+
SELECT
|
|
943
|
+
id,
|
|
944
|
+
context_graph_id,
|
|
945
|
+
request_digest,
|
|
946
|
+
CASE
|
|
947
|
+
WHEN json_valid(details)
|
|
948
|
+
AND json_type(details, '$.policyEpoch') IN ('integer', 'real')
|
|
949
|
+
THEN CAST(json_extract(details, '$.policyEpoch') AS INTEGER)
|
|
950
|
+
ELSE NULL
|
|
951
|
+
END AS policy_epoch,
|
|
952
|
+
ts AS reserved_at,
|
|
953
|
+
COALESCE(actor, '') AS actor,
|
|
954
|
+
COALESCE(agent_address, '') AS agent_address,
|
|
955
|
+
COALESCE(policy_version, 1) AS policy_version
|
|
956
|
+
FROM context_graph_join_policy_audit
|
|
957
|
+
WHERE event_type = 'join_auto_reservation'
|
|
958
|
+
AND outcome = 'reserved'
|
|
959
|
+
AND request_digest IS NOT NULL
|
|
960
|
+
),
|
|
961
|
+
ranked_reservations AS (
|
|
962
|
+
SELECT *, ROW_NUMBER() OVER (
|
|
963
|
+
PARTITION BY context_graph_id, request_digest, policy_epoch
|
|
964
|
+
ORDER BY reserved_at DESC, id DESC
|
|
965
|
+
) AS rank
|
|
966
|
+
FROM raw_reservations
|
|
967
|
+
WHERE policy_epoch IS NOT NULL
|
|
968
|
+
),
|
|
969
|
+
raw_commits AS (
|
|
970
|
+
SELECT
|
|
971
|
+
context_graph_id,
|
|
972
|
+
request_digest,
|
|
973
|
+
CASE
|
|
974
|
+
WHEN json_valid(details)
|
|
975
|
+
AND json_type(details, '$.policyEpoch') IN ('integer', 'real')
|
|
976
|
+
THEN CAST(json_extract(details, '$.policyEpoch') AS INTEGER)
|
|
977
|
+
ELSE NULL
|
|
978
|
+
END AS policy_epoch,
|
|
979
|
+
MIN(ts) AS committed_at
|
|
980
|
+
FROM context_graph_join_policy_audit
|
|
981
|
+
WHERE event_type = 'join_admission_committed'
|
|
982
|
+
AND outcome = 'approved'
|
|
983
|
+
AND request_digest IS NOT NULL
|
|
984
|
+
GROUP BY context_graph_id, request_digest, policy_epoch
|
|
985
|
+
)
|
|
986
|
+
INSERT OR IGNORE INTO context_graph_join_approval_ledger (
|
|
987
|
+
context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
988
|
+
state, committed_at, actor, agent_address, policy_version
|
|
989
|
+
)
|
|
990
|
+
SELECT
|
|
991
|
+
reservation.context_graph_id,
|
|
992
|
+
reservation.request_digest,
|
|
993
|
+
reservation.policy_epoch,
|
|
994
|
+
reservation.reserved_at,
|
|
995
|
+
CASE WHEN commit_row.committed_at IS NULL THEN 'reserved' ELSE 'committed' END,
|
|
996
|
+
commit_row.committed_at,
|
|
997
|
+
reservation.actor,
|
|
998
|
+
reservation.agent_address,
|
|
999
|
+
reservation.policy_version
|
|
1000
|
+
FROM ranked_reservations AS reservation
|
|
1001
|
+
LEFT JOIN raw_commits AS commit_row
|
|
1002
|
+
ON commit_row.context_graph_id = reservation.context_graph_id
|
|
1003
|
+
AND commit_row.request_digest = reservation.request_digest
|
|
1004
|
+
AND commit_row.policy_epoch = reservation.policy_epoch
|
|
1005
|
+
WHERE reservation.rank = 1;
|
|
988
1006
|
`);
|
|
989
1007
|
// V26's trigger protected live reservation audit rows because they were
|
|
990
1008
|
// operational state. The ledger makes that coupling obsolete, so replace
|
|
@@ -1002,46 +1020,81 @@ export class DashboardDB {
|
|
|
1002
1020
|
// idempotent and keeps the audit bound fail-closed on every open.
|
|
1003
1021
|
ensureJoinPolicyAuditCapTrigger();
|
|
1004
1022
|
if (version < 29) {
|
|
1005
|
-
this.db.exec(`
|
|
1006
|
-
CREATE TABLE IF NOT EXISTS vm_reconcile_negative_cache (
|
|
1007
|
-
cache_key TEXT PRIMARY KEY,
|
|
1008
|
-
context_graph_id TEXT NOT NULL,
|
|
1009
|
-
failures INTEGER NOT NULL,
|
|
1010
|
-
next_retry_at INTEGER NOT NULL,
|
|
1011
|
-
swm_gen TEXT NOT NULL,
|
|
1012
|
-
candidate_namespaces TEXT NOT NULL,
|
|
1013
|
-
peer_topology_key TEXT NOT NULL,
|
|
1014
|
-
updated_at INTEGER NOT NULL
|
|
1015
|
-
);
|
|
1016
|
-
CREATE INDEX IF NOT EXISTS idx_vm_reconcile_negative_cg
|
|
1017
|
-
ON vm_reconcile_negative_cache(context_graph_id);
|
|
1018
|
-
CREATE INDEX IF NOT EXISTS idx_vm_reconcile_negative_retry
|
|
1019
|
-
ON vm_reconcile_negative_cache(next_retry_at);
|
|
1023
|
+
this.db.exec(`
|
|
1024
|
+
CREATE TABLE IF NOT EXISTS vm_reconcile_negative_cache (
|
|
1025
|
+
cache_key TEXT PRIMARY KEY,
|
|
1026
|
+
context_graph_id TEXT NOT NULL,
|
|
1027
|
+
failures INTEGER NOT NULL,
|
|
1028
|
+
next_retry_at INTEGER NOT NULL,
|
|
1029
|
+
swm_gen TEXT NOT NULL,
|
|
1030
|
+
candidate_namespaces TEXT NOT NULL,
|
|
1031
|
+
peer_topology_key TEXT NOT NULL,
|
|
1032
|
+
updated_at INTEGER NOT NULL
|
|
1033
|
+
);
|
|
1034
|
+
CREATE INDEX IF NOT EXISTS idx_vm_reconcile_negative_cg
|
|
1035
|
+
ON vm_reconcile_negative_cache(context_graph_id);
|
|
1036
|
+
CREATE INDEX IF NOT EXISTS idx_vm_reconcile_negative_retry
|
|
1037
|
+
ON vm_reconcile_negative_cache(next_retry_at);
|
|
1020
1038
|
`);
|
|
1021
1039
|
}
|
|
1022
1040
|
if (version < 30) {
|
|
1023
1041
|
// OFFSET pagination is scoped to an immutable responder row list. Keep
|
|
1024
1042
|
// the opaque responder token beside the verified offset so a requester
|
|
1025
1043
|
// restart can resume that same list instead of discarding durable work.
|
|
1026
|
-
|
|
1044
|
+
ensureSyncCheckpointResumeColumns();
|
|
1027
1045
|
}
|
|
1028
1046
|
if (version < 31) {
|
|
1029
|
-
this.db.exec(`
|
|
1030
|
-
CREATE TABLE IF NOT EXISTS snapshot_page_indexes (
|
|
1031
|
-
snapshot_digest TEXT PRIMARY KEY,
|
|
1032
|
-
format_version INTEGER NOT NULL CHECK (format_version > 0),
|
|
1033
|
-
stride INTEGER NOT NULL CHECK (stride > 0),
|
|
1034
|
-
snapshot_file_size INTEGER NOT NULL CHECK (snapshot_file_size >= 0),
|
|
1035
|
-
modification_fingerprint TEXT NOT NULL,
|
|
1036
|
-
offset_count INTEGER NOT NULL CHECK (offset_count > 0),
|
|
1037
|
-
offsets BLOB NOT NULL,
|
|
1038
|
-
checksum TEXT NOT NULL,
|
|
1039
|
-
CHECK (length(snapshot_digest) > 0),
|
|
1040
|
-
CHECK (length(modification_fingerprint) > 0),
|
|
1041
|
-
CHECK (length(offsets) = offset_count * 8),
|
|
1042
|
-
CHECK (length(checksum) > 0)
|
|
1043
|
-
);
|
|
1047
|
+
this.db.exec(`
|
|
1048
|
+
CREATE TABLE IF NOT EXISTS snapshot_page_indexes (
|
|
1049
|
+
snapshot_digest TEXT PRIMARY KEY,
|
|
1050
|
+
format_version INTEGER NOT NULL CHECK (format_version > 0),
|
|
1051
|
+
stride INTEGER NOT NULL CHECK (stride > 0),
|
|
1052
|
+
snapshot_file_size INTEGER NOT NULL CHECK (snapshot_file_size >= 0),
|
|
1053
|
+
modification_fingerprint TEXT NOT NULL,
|
|
1054
|
+
offset_count INTEGER NOT NULL CHECK (offset_count > 0),
|
|
1055
|
+
offsets BLOB NOT NULL,
|
|
1056
|
+
checksum TEXT NOT NULL,
|
|
1057
|
+
CHECK (length(snapshot_digest) > 0),
|
|
1058
|
+
CHECK (length(modification_fingerprint) > 0),
|
|
1059
|
+
CHECK (length(offsets) = offset_count * 8),
|
|
1060
|
+
CHECK (length(checksum) > 0)
|
|
1061
|
+
);
|
|
1044
1062
|
`);
|
|
1063
|
+
}
|
|
1064
|
+
if (version < 32) {
|
|
1065
|
+
this.db.exec(`
|
|
1066
|
+
CREATE TABLE IF NOT EXISTS selected_vm_reconcile_cursors (
|
|
1067
|
+
deployment_id TEXT NOT NULL CHECK (length(trim(deployment_id)) > 0),
|
|
1068
|
+
context_graph_id TEXT NOT NULL,
|
|
1069
|
+
on_chain_context_graph_id TEXT NOT NULL,
|
|
1070
|
+
name_hash TEXT NOT NULL,
|
|
1071
|
+
watermark INTEGER NOT NULL CHECK (watermark >= 0),
|
|
1072
|
+
updated_at INTEGER NOT NULL,
|
|
1073
|
+
PRIMARY KEY (deployment_id, context_graph_id, on_chain_context_graph_id)
|
|
1074
|
+
);
|
|
1075
|
+
CREATE INDEX IF NOT EXISTS idx_selected_vm_reconcile_cursor_cg
|
|
1076
|
+
ON selected_vm_reconcile_cursors(deployment_id, context_graph_id);
|
|
1077
|
+
`);
|
|
1078
|
+
}
|
|
1079
|
+
if (version < 33) {
|
|
1080
|
+
// A durable DATA offset is reusable only with cryptographic proof of the
|
|
1081
|
+
// canonical META generation and verified descriptor prefix that gave it
|
|
1082
|
+
// meaning. Persist both digests beside the offset/session so production
|
|
1083
|
+
// SQLite checkpoints have the same safety contract as the in-memory
|
|
1084
|
+
// requester store and survive daemon restarts.
|
|
1085
|
+
ensureSyncCheckpointResumeColumns();
|
|
1086
|
+
}
|
|
1087
|
+
if (version < 34) {
|
|
1088
|
+
// The V33 manifest-bound continuation must not share a key with V32,
|
|
1089
|
+
// which interpreted OFFSET as an unverified raw responder coordinate.
|
|
1090
|
+
// Drop every pre-versioned durable DATA row before new code starts using
|
|
1091
|
+
// the `|checkpoint:v2` namespace, so a later rollback also fails closed.
|
|
1092
|
+
ensureSyncCheckpointResumeColumns();
|
|
1093
|
+
this.db.prepare(`
|
|
1094
|
+
DELETE FROM sync_checkpoints
|
|
1095
|
+
WHERE key LIKE '%|durable|data%'
|
|
1096
|
+
AND key NOT LIKE '%|durable|data|checkpoint:v2%'
|
|
1097
|
+
`).run();
|
|
1045
1098
|
}
|
|
1046
1099
|
this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
1047
1100
|
if (upgradedExistingDb && !this.explicitRetentionDays) {
|
|
@@ -1080,10 +1133,10 @@ export class DashboardDB {
|
|
|
1080
1133
|
// not depend on whether the corresponding audit rows still exist. Preserve
|
|
1081
1134
|
// the historical retention envelope for durable commit idempotency while
|
|
1082
1135
|
// allowing fully expired reservation/commit state to age out.
|
|
1083
|
-
this.db.prepare(`
|
|
1084
|
-
DELETE FROM context_graph_join_approval_ledger
|
|
1085
|
-
WHERE reserved_at < ?
|
|
1086
|
-
AND (committed_at IS NULL OR committed_at < ?)
|
|
1136
|
+
this.db.prepare(`
|
|
1137
|
+
DELETE FROM context_graph_join_approval_ledger
|
|
1138
|
+
WHERE reserved_at < ?
|
|
1139
|
+
AND (committed_at IS NULL OR committed_at < ?)
|
|
1087
1140
|
`).run(cutoff, cutoff);
|
|
1088
1141
|
this.db.prepare(`DELETE FROM sync_checkpoints WHERE expires_at < ?`).run(Date.now());
|
|
1089
1142
|
// Reconcile negatives are accelerators, never historical records. Once the
|
|
@@ -1136,16 +1189,16 @@ export class DashboardDB {
|
|
|
1136
1189
|
: 'done',
|
|
1137
1190
|
};
|
|
1138
1191
|
}
|
|
1139
|
-
const deleted = this.db.prepare(`
|
|
1140
|
-
DELETE FROM logs
|
|
1141
|
-
WHERE id IN (
|
|
1142
|
-
SELECT id
|
|
1143
|
-
FROM logs
|
|
1144
|
-
WHERE id <= @cutoff
|
|
1145
|
-
AND level NOT IN ('warn', 'error')
|
|
1146
|
-
ORDER BY id ASC
|
|
1147
|
-
LIMIT @batchRows
|
|
1148
|
-
)
|
|
1192
|
+
const deleted = this.db.prepare(`
|
|
1193
|
+
DELETE FROM logs
|
|
1194
|
+
WHERE id IN (
|
|
1195
|
+
SELECT id
|
|
1196
|
+
FROM logs
|
|
1197
|
+
WHERE id <= @cutoff
|
|
1198
|
+
AND level NOT IN ('warn', 'error')
|
|
1199
|
+
ORDER BY id ASC
|
|
1200
|
+
LIMIT @batchRows
|
|
1201
|
+
)
|
|
1149
1202
|
`).run({
|
|
1150
1203
|
cutoff: overflowCutoff,
|
|
1151
1204
|
batchRows: this.logVolumePruneBatchRows,
|
|
@@ -1171,12 +1224,12 @@ export class DashboardDB {
|
|
|
1171
1224
|
};
|
|
1172
1225
|
}
|
|
1173
1226
|
routineLogOverflowCutoff() {
|
|
1174
|
-
const row = this.db.prepare(`
|
|
1175
|
-
SELECT id
|
|
1176
|
-
FROM logs
|
|
1177
|
-
WHERE level NOT IN ('warn', 'error')
|
|
1178
|
-
ORDER BY id DESC
|
|
1179
|
-
LIMIT 1 OFFSET ?
|
|
1227
|
+
const row = this.db.prepare(`
|
|
1228
|
+
SELECT id
|
|
1229
|
+
FROM logs
|
|
1230
|
+
WHERE level NOT IN ('warn', 'error')
|
|
1231
|
+
ORDER BY id DESC
|
|
1232
|
+
LIMIT 1 OFFSET ?
|
|
1180
1233
|
`).get(this.routineLogRowCap);
|
|
1181
1234
|
return row?.id ?? null;
|
|
1182
1235
|
}
|
|
@@ -1277,50 +1330,50 @@ export class DashboardDB {
|
|
|
1277
1330
|
}
|
|
1278
1331
|
// --- Metric snapshots ---
|
|
1279
1332
|
insertSnapshot(snap) {
|
|
1280
|
-
this.stmt('insertSnapshot', `
|
|
1281
|
-
INSERT INTO metric_snapshots (
|
|
1282
|
-
ts, cpu_percent, mem_used_bytes, mem_total_bytes,
|
|
1283
|
-
disk_used_bytes, disk_total_bytes, heap_used_bytes, uptime_seconds,
|
|
1284
|
-
peer_count, direct_peers, relayed_peers, mesh_peers, contextGraph_count,
|
|
1285
|
-
total_triples, total_kcs, total_kas, store_bytes,
|
|
1286
|
-
confirmed_kcs, tentative_kcs, rpc_latency_ms, rpc_healthy,
|
|
1287
|
-
relay_capacity, relay_reservation_count, relay_active_circuits,
|
|
1288
|
-
relay_bytes_in, relay_bytes_out
|
|
1289
|
-
) VALUES (
|
|
1290
|
-
@ts, @cpu_percent, @mem_used_bytes, @mem_total_bytes,
|
|
1291
|
-
@disk_used_bytes, @disk_total_bytes, @heap_used_bytes, @uptime_seconds,
|
|
1292
|
-
@peer_count, @direct_peers, @relayed_peers, @mesh_peers, @contextGraph_count,
|
|
1293
|
-
@total_triples, @total_kcs, @total_kas, @store_bytes,
|
|
1294
|
-
@confirmed_kcs, @tentative_kcs, @rpc_latency_ms, @rpc_healthy,
|
|
1295
|
-
@relay_capacity, @relay_reservation_count, @relay_active_circuits,
|
|
1296
|
-
@relay_bytes_in, @relay_bytes_out
|
|
1297
|
-
)
|
|
1333
|
+
this.stmt('insertSnapshot', `
|
|
1334
|
+
INSERT INTO metric_snapshots (
|
|
1335
|
+
ts, cpu_percent, mem_used_bytes, mem_total_bytes,
|
|
1336
|
+
disk_used_bytes, disk_total_bytes, heap_used_bytes, uptime_seconds,
|
|
1337
|
+
peer_count, direct_peers, relayed_peers, mesh_peers, contextGraph_count,
|
|
1338
|
+
total_triples, total_kcs, total_kas, store_bytes,
|
|
1339
|
+
confirmed_kcs, tentative_kcs, rpc_latency_ms, rpc_healthy,
|
|
1340
|
+
relay_capacity, relay_reservation_count, relay_active_circuits,
|
|
1341
|
+
relay_bytes_in, relay_bytes_out
|
|
1342
|
+
) VALUES (
|
|
1343
|
+
@ts, @cpu_percent, @mem_used_bytes, @mem_total_bytes,
|
|
1344
|
+
@disk_used_bytes, @disk_total_bytes, @heap_used_bytes, @uptime_seconds,
|
|
1345
|
+
@peer_count, @direct_peers, @relayed_peers, @mesh_peers, @contextGraph_count,
|
|
1346
|
+
@total_triples, @total_kcs, @total_kas, @store_bytes,
|
|
1347
|
+
@confirmed_kcs, @tentative_kcs, @rpc_latency_ms, @rpc_healthy,
|
|
1348
|
+
@relay_capacity, @relay_reservation_count, @relay_active_circuits,
|
|
1349
|
+
@relay_bytes_in, @relay_bytes_out
|
|
1350
|
+
)
|
|
1298
1351
|
`).run(snap);
|
|
1299
1352
|
}
|
|
1300
1353
|
getLatestSnapshot() {
|
|
1301
1354
|
return this.db.prepare('SELECT * FROM metric_snapshots ORDER BY ts DESC LIMIT 1').get();
|
|
1302
1355
|
}
|
|
1303
1356
|
upsertContextGraphSubscription(record) {
|
|
1304
|
-
this.stmt('upsertContextGraphSubscription', `
|
|
1305
|
-
INSERT INTO context_graph_subscriptions (
|
|
1306
|
-
context_graph_id, name, subscribed, synced, shared_memory_synced, meta_synced,
|
|
1307
|
-
on_chain_id, on_chain_hash, last_reconciled_ordinal, core_hosted, sync_scoped, updated_at
|
|
1308
|
-
) VALUES (
|
|
1309
|
-
@context_graph_id, @name, @subscribed, @synced, @shared_memory_synced, @meta_synced,
|
|
1310
|
-
@on_chain_id, @on_chain_hash, @last_reconciled_ordinal, @core_hosted, @sync_scoped, @updated_at
|
|
1311
|
-
)
|
|
1312
|
-
ON CONFLICT(context_graph_id) DO UPDATE SET
|
|
1313
|
-
name = excluded.name,
|
|
1314
|
-
subscribed = excluded.subscribed,
|
|
1315
|
-
synced = excluded.synced,
|
|
1316
|
-
shared_memory_synced = excluded.shared_memory_synced,
|
|
1317
|
-
meta_synced = excluded.meta_synced,
|
|
1318
|
-
on_chain_id = excluded.on_chain_id,
|
|
1319
|
-
on_chain_hash = excluded.on_chain_hash,
|
|
1320
|
-
last_reconciled_ordinal = excluded.last_reconciled_ordinal,
|
|
1321
|
-
core_hosted = excluded.core_hosted,
|
|
1322
|
-
sync_scoped = excluded.sync_scoped,
|
|
1323
|
-
updated_at = excluded.updated_at
|
|
1357
|
+
this.stmt('upsertContextGraphSubscription', `
|
|
1358
|
+
INSERT INTO context_graph_subscriptions (
|
|
1359
|
+
context_graph_id, name, subscribed, synced, shared_memory_synced, meta_synced,
|
|
1360
|
+
on_chain_id, on_chain_hash, last_reconciled_ordinal, core_hosted, sync_scoped, updated_at
|
|
1361
|
+
) VALUES (
|
|
1362
|
+
@context_graph_id, @name, @subscribed, @synced, @shared_memory_synced, @meta_synced,
|
|
1363
|
+
@on_chain_id, @on_chain_hash, @last_reconciled_ordinal, @core_hosted, @sync_scoped, @updated_at
|
|
1364
|
+
)
|
|
1365
|
+
ON CONFLICT(context_graph_id) DO UPDATE SET
|
|
1366
|
+
name = excluded.name,
|
|
1367
|
+
subscribed = excluded.subscribed,
|
|
1368
|
+
synced = excluded.synced,
|
|
1369
|
+
shared_memory_synced = excluded.shared_memory_synced,
|
|
1370
|
+
meta_synced = excluded.meta_synced,
|
|
1371
|
+
on_chain_id = excluded.on_chain_id,
|
|
1372
|
+
on_chain_hash = excluded.on_chain_hash,
|
|
1373
|
+
last_reconciled_ordinal = excluded.last_reconciled_ordinal,
|
|
1374
|
+
core_hosted = excluded.core_hosted,
|
|
1375
|
+
sync_scoped = excluded.sync_scoped,
|
|
1376
|
+
updated_at = excluded.updated_at
|
|
1324
1377
|
`).run({
|
|
1325
1378
|
context_graph_id: record.context_graph_id,
|
|
1326
1379
|
name: record.name ?? null,
|
|
@@ -1418,37 +1471,37 @@ export class DashboardDB {
|
|
|
1418
1471
|
transition();
|
|
1419
1472
|
}
|
|
1420
1473
|
appendContextGraphJoinPolicyAudit(event) {
|
|
1421
|
-
this.stmt('appendContextGraphJoinPolicyAudit', `
|
|
1422
|
-
INSERT INTO context_graph_join_policy_audit (
|
|
1423
|
-
ts, context_graph_id, event_type, actor, agent_address, outcome,
|
|
1424
|
-
reason, request_digest, policy_version, details
|
|
1425
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1474
|
+
this.stmt('appendContextGraphJoinPolicyAudit', `
|
|
1475
|
+
INSERT INTO context_graph_join_policy_audit (
|
|
1476
|
+
ts, context_graph_id, event_type, actor, agent_address, outcome,
|
|
1477
|
+
reason, request_digest, policy_version, details
|
|
1478
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1426
1479
|
`).run(event.timestamp, event.contextGraphId, event.eventType, event.actor ?? null, event.agentAddress ?? null, event.outcome, event.reason ?? null, event.requestDigest ?? null, event.policyVersion ?? null, event.details ? JSON.stringify(event.details) : null);
|
|
1427
1480
|
}
|
|
1428
1481
|
reserveContextGraphAutomaticApproval(input) {
|
|
1429
1482
|
const reserve = this.db.transaction(() => {
|
|
1430
1483
|
const since = input.timestamp - 60 * 60 * 1000;
|
|
1431
|
-
const cgRow = this.stmt('countContextGraphAutomaticApprovalReservations', `
|
|
1432
|
-
SELECT COUNT(*) AS count
|
|
1433
|
-
FROM context_graph_join_approval_ledger
|
|
1434
|
-
WHERE context_graph_id = ?
|
|
1435
|
-
AND reserved_at >= ?
|
|
1484
|
+
const cgRow = this.stmt('countContextGraphAutomaticApprovalReservations', `
|
|
1485
|
+
SELECT COUNT(*) AS count
|
|
1486
|
+
FROM context_graph_join_approval_ledger
|
|
1487
|
+
WHERE context_graph_id = ?
|
|
1488
|
+
AND reserved_at >= ?
|
|
1436
1489
|
`).get(input.contextGraphId, since);
|
|
1437
|
-
const nodeRow = this.stmt('countNodeAutomaticApprovalReservations', `
|
|
1438
|
-
SELECT COUNT(*) AS count
|
|
1439
|
-
FROM context_graph_join_approval_ledger
|
|
1440
|
-
WHERE reserved_at >= ?
|
|
1490
|
+
const nodeRow = this.stmt('countNodeAutomaticApprovalReservations', `
|
|
1491
|
+
SELECT COUNT(*) AS count
|
|
1492
|
+
FROM context_graph_join_approval_ledger
|
|
1493
|
+
WHERE reserved_at >= ?
|
|
1441
1494
|
`).get(since);
|
|
1442
1495
|
const contextGraphCount = Number(cgRow?.count ?? 0);
|
|
1443
1496
|
const nodeCount = Number(nodeRow?.count ?? 0);
|
|
1444
|
-
const existing = this.stmt('findContextGraphAutomaticApprovalReservation', `
|
|
1445
|
-
SELECT 1 AS found
|
|
1446
|
-
FROM context_graph_join_approval_ledger
|
|
1447
|
-
WHERE context_graph_id = ?
|
|
1448
|
-
AND request_digest = ?
|
|
1449
|
-
AND policy_epoch = ?
|
|
1450
|
-
AND reserved_at >= ?
|
|
1451
|
-
LIMIT 1
|
|
1497
|
+
const existing = this.stmt('findContextGraphAutomaticApprovalReservation', `
|
|
1498
|
+
SELECT 1 AS found
|
|
1499
|
+
FROM context_graph_join_approval_ledger
|
|
1500
|
+
WHERE context_graph_id = ?
|
|
1501
|
+
AND request_digest = ?
|
|
1502
|
+
AND policy_epoch = ?
|
|
1503
|
+
AND reserved_at >= ?
|
|
1504
|
+
LIMIT 1
|
|
1452
1505
|
`).get(input.contextGraphId, input.requestDigest, input.policyEpoch, since);
|
|
1453
1506
|
if (existing) {
|
|
1454
1507
|
return {
|
|
@@ -1478,16 +1531,16 @@ export class DashboardDB {
|
|
|
1478
1531
|
// reservation without durable operational state. An expired replay of
|
|
1479
1532
|
// the same digest+epoch refreshes its reservation timestamp and consumes
|
|
1480
1533
|
// the rolling-window quota again, matching the former append-only model.
|
|
1481
|
-
this.stmt('upsertContextGraphAutomaticApprovalReservation', `
|
|
1482
|
-
INSERT INTO context_graph_join_approval_ledger (
|
|
1483
|
-
context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
1484
|
-
state, committed_at, actor, agent_address, policy_version
|
|
1485
|
-
) VALUES (?, ?, ?, ?, 'reserved', NULL, ?, ?, ?)
|
|
1486
|
-
ON CONFLICT(context_graph_id, request_digest, policy_epoch) DO UPDATE SET
|
|
1487
|
-
reserved_at = excluded.reserved_at,
|
|
1488
|
-
actor = excluded.actor,
|
|
1489
|
-
agent_address = excluded.agent_address,
|
|
1490
|
-
policy_version = excluded.policy_version
|
|
1534
|
+
this.stmt('upsertContextGraphAutomaticApprovalReservation', `
|
|
1535
|
+
INSERT INTO context_graph_join_approval_ledger (
|
|
1536
|
+
context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
1537
|
+
state, committed_at, actor, agent_address, policy_version
|
|
1538
|
+
) VALUES (?, ?, ?, ?, 'reserved', NULL, ?, ?, ?)
|
|
1539
|
+
ON CONFLICT(context_graph_id, request_digest, policy_epoch) DO UPDATE SET
|
|
1540
|
+
reserved_at = excluded.reserved_at,
|
|
1541
|
+
actor = excluded.actor,
|
|
1542
|
+
agent_address = excluded.agent_address,
|
|
1543
|
+
policy_version = excluded.policy_version
|
|
1491
1544
|
`).run(input.contextGraphId, input.requestDigest, input.policyEpoch, input.timestamp, input.actor, input.agentAddress, input.policyVersion);
|
|
1492
1545
|
this.appendContextGraphJoinPolicyAudit({
|
|
1493
1546
|
timestamp: input.timestamp,
|
|
@@ -1513,26 +1566,26 @@ export class DashboardDB {
|
|
|
1513
1566
|
return reserve();
|
|
1514
1567
|
}
|
|
1515
1568
|
markContextGraphAutomaticApprovalRepairPending(input) {
|
|
1516
|
-
const marked = this.stmt('markContextGraphAutomaticApprovalRepairPending', `
|
|
1517
|
-
UPDATE context_graph_join_approval_ledger
|
|
1518
|
-
SET repair_pending = 1
|
|
1519
|
-
WHERE context_graph_id = ?
|
|
1520
|
-
AND request_digest = ?
|
|
1521
|
-
AND policy_epoch = ?
|
|
1522
|
-
AND state = 'reserved'
|
|
1569
|
+
const marked = this.stmt('markContextGraphAutomaticApprovalRepairPending', `
|
|
1570
|
+
UPDATE context_graph_join_approval_ledger
|
|
1571
|
+
SET repair_pending = 1
|
|
1572
|
+
WHERE context_graph_id = ?
|
|
1573
|
+
AND request_digest = ?
|
|
1574
|
+
AND policy_epoch = ?
|
|
1575
|
+
AND state = 'reserved'
|
|
1523
1576
|
`).run(input.contextGraphId, input.requestDigest, input.policyEpoch);
|
|
1524
1577
|
return marked.changes === 1;
|
|
1525
1578
|
}
|
|
1526
1579
|
getContextGraphAutomaticApprovalRepair(contextGraphId, requestDigest) {
|
|
1527
|
-
const repair = this.stmt('getContextGraphAutomaticApprovalRepair', `
|
|
1528
|
-
SELECT policy_epoch, actor, agent_address
|
|
1529
|
-
FROM context_graph_join_approval_ledger
|
|
1530
|
-
WHERE context_graph_id = ?
|
|
1531
|
-
AND request_digest = ?
|
|
1532
|
-
AND state = 'reserved'
|
|
1533
|
-
AND repair_pending = 1
|
|
1534
|
-
ORDER BY reserved_at DESC
|
|
1535
|
-
LIMIT 1
|
|
1580
|
+
const repair = this.stmt('getContextGraphAutomaticApprovalRepair', `
|
|
1581
|
+
SELECT policy_epoch, actor, agent_address
|
|
1582
|
+
FROM context_graph_join_approval_ledger
|
|
1583
|
+
WHERE context_graph_id = ?
|
|
1584
|
+
AND request_digest = ?
|
|
1585
|
+
AND state = 'reserved'
|
|
1586
|
+
AND repair_pending = 1
|
|
1587
|
+
ORDER BY reserved_at DESC
|
|
1588
|
+
LIMIT 1
|
|
1536
1589
|
`).get(contextGraphId, requestDigest);
|
|
1537
1590
|
return repair
|
|
1538
1591
|
? {
|
|
@@ -1544,26 +1597,26 @@ export class DashboardDB {
|
|
|
1544
1597
|
}
|
|
1545
1598
|
commitContextGraphAutomaticApproval(input) {
|
|
1546
1599
|
const commit = this.db.transaction(() => {
|
|
1547
|
-
const reservation = this.stmt('findExactContextGraphAutomaticApprovalReservation', `
|
|
1548
|
-
SELECT context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
1549
|
-
state, committed_at, actor, agent_address, policy_version
|
|
1550
|
-
FROM context_graph_join_approval_ledger
|
|
1551
|
-
WHERE context_graph_id = ?
|
|
1552
|
-
AND request_digest = ?
|
|
1553
|
-
AND policy_epoch = ?
|
|
1554
|
-
LIMIT 1
|
|
1600
|
+
const reservation = this.stmt('findExactContextGraphAutomaticApprovalReservation', `
|
|
1601
|
+
SELECT context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
1602
|
+
state, committed_at, actor, agent_address, policy_version
|
|
1603
|
+
FROM context_graph_join_approval_ledger
|
|
1604
|
+
WHERE context_graph_id = ?
|
|
1605
|
+
AND request_digest = ?
|
|
1606
|
+
AND policy_epoch = ?
|
|
1607
|
+
LIMIT 1
|
|
1555
1608
|
`).get(input.contextGraphId, input.requestDigest, input.policyEpoch);
|
|
1556
1609
|
if (!reservation)
|
|
1557
1610
|
return false;
|
|
1558
1611
|
if (reservation.state === 'committed')
|
|
1559
1612
|
return true;
|
|
1560
|
-
const updated = this.stmt('commitContextGraphAutomaticApprovalReservation', `
|
|
1561
|
-
UPDATE context_graph_join_approval_ledger
|
|
1562
|
-
SET state = 'committed', committed_at = ?, repair_pending = 0
|
|
1563
|
-
WHERE context_graph_id = ?
|
|
1564
|
-
AND request_digest = ?
|
|
1565
|
-
AND policy_epoch = ?
|
|
1566
|
-
AND state = 'reserved'
|
|
1613
|
+
const updated = this.stmt('commitContextGraphAutomaticApprovalReservation', `
|
|
1614
|
+
UPDATE context_graph_join_approval_ledger
|
|
1615
|
+
SET state = 'committed', committed_at = ?, repair_pending = 0
|
|
1616
|
+
WHERE context_graph_id = ?
|
|
1617
|
+
AND request_digest = ?
|
|
1618
|
+
AND policy_epoch = ?
|
|
1619
|
+
AND state = 'reserved'
|
|
1567
1620
|
`).run(input.timestamp, reservation.context_graph_id, reservation.request_digest, reservation.policy_epoch);
|
|
1568
1621
|
if (updated.changes !== 1)
|
|
1569
1622
|
return false;
|
|
@@ -1587,48 +1640,48 @@ export class DashboardDB {
|
|
|
1587
1640
|
}
|
|
1588
1641
|
getContextGraphAutomaticApprovalUsage(contextGraphId, timestamp) {
|
|
1589
1642
|
const since = timestamp - 60 * 60 * 1000;
|
|
1590
|
-
const contextGraphApprovalsLastHour = Number(this.stmt('getContextGraphAutomaticApprovalUsage', `
|
|
1591
|
-
SELECT COUNT(*) AS count
|
|
1592
|
-
FROM context_graph_join_approval_ledger
|
|
1593
|
-
WHERE context_graph_id = ?
|
|
1594
|
-
AND reserved_at >= ?
|
|
1643
|
+
const contextGraphApprovalsLastHour = Number(this.stmt('getContextGraphAutomaticApprovalUsage', `
|
|
1644
|
+
SELECT COUNT(*) AS count
|
|
1645
|
+
FROM context_graph_join_approval_ledger
|
|
1646
|
+
WHERE context_graph_id = ?
|
|
1647
|
+
AND reserved_at >= ?
|
|
1595
1648
|
`).get(contextGraphId, since)?.count ?? 0);
|
|
1596
|
-
const nodeApprovalsLastHour = Number(this.stmt('getNodeAutomaticApprovalUsage', `
|
|
1597
|
-
SELECT COUNT(*) AS count
|
|
1598
|
-
FROM context_graph_join_approval_ledger
|
|
1599
|
-
WHERE reserved_at >= ?
|
|
1649
|
+
const nodeApprovalsLastHour = Number(this.stmt('getNodeAutomaticApprovalUsage', `
|
|
1650
|
+
SELECT COUNT(*) AS count
|
|
1651
|
+
FROM context_graph_join_approval_ledger
|
|
1652
|
+
WHERE reserved_at >= ?
|
|
1600
1653
|
`).get(since)?.count ?? 0);
|
|
1601
1654
|
return { contextGraphApprovalsLastHour, nodeApprovalsLastHour };
|
|
1602
1655
|
}
|
|
1603
1656
|
listContextGraphJoinPolicyAudit(contextGraphId) {
|
|
1604
|
-
return this.stmt('listContextGraphJoinPolicyAudit', `
|
|
1605
|
-
SELECT id, ts, context_graph_id, event_type, actor, agent_address,
|
|
1606
|
-
outcome, reason, request_digest, policy_version, details
|
|
1607
|
-
FROM context_graph_join_policy_audit
|
|
1608
|
-
WHERE context_graph_id = ?
|
|
1609
|
-
ORDER BY id ASC
|
|
1657
|
+
return this.stmt('listContextGraphJoinPolicyAudit', `
|
|
1658
|
+
SELECT id, ts, context_graph_id, event_type, actor, agent_address,
|
|
1659
|
+
outcome, reason, request_digest, policy_version, details
|
|
1660
|
+
FROM context_graph_join_policy_audit
|
|
1661
|
+
WHERE context_graph_id = ?
|
|
1662
|
+
ORDER BY id ASC
|
|
1610
1663
|
`).all(contextGraphId);
|
|
1611
1664
|
}
|
|
1612
1665
|
contextGraphJoinPolicyKey(contextGraphId) {
|
|
1613
1666
|
return `contextGraphJoinPolicy:${contextGraphId}`;
|
|
1614
1667
|
}
|
|
1615
1668
|
upsertVmReconcileNegative(record) {
|
|
1616
|
-
this.stmt('upsertVmReconcileNegative', `
|
|
1617
|
-
INSERT INTO vm_reconcile_negative_cache (
|
|
1618
|
-
cache_key, context_graph_id, failures, next_retry_at, swm_gen,
|
|
1619
|
-
candidate_namespaces, peer_topology_key, updated_at
|
|
1620
|
-
) VALUES (
|
|
1621
|
-
@cache_key, @context_graph_id, @failures, @next_retry_at, @swm_gen,
|
|
1622
|
-
@candidate_namespaces, @peer_topology_key, @updated_at
|
|
1623
|
-
)
|
|
1624
|
-
ON CONFLICT(cache_key) DO UPDATE SET
|
|
1625
|
-
context_graph_id = excluded.context_graph_id,
|
|
1626
|
-
failures = excluded.failures,
|
|
1627
|
-
next_retry_at = excluded.next_retry_at,
|
|
1628
|
-
swm_gen = excluded.swm_gen,
|
|
1629
|
-
candidate_namespaces = excluded.candidate_namespaces,
|
|
1630
|
-
peer_topology_key = excluded.peer_topology_key,
|
|
1631
|
-
updated_at = excluded.updated_at
|
|
1669
|
+
this.stmt('upsertVmReconcileNegative', `
|
|
1670
|
+
INSERT INTO vm_reconcile_negative_cache (
|
|
1671
|
+
cache_key, context_graph_id, failures, next_retry_at, swm_gen,
|
|
1672
|
+
candidate_namespaces, peer_topology_key, updated_at
|
|
1673
|
+
) VALUES (
|
|
1674
|
+
@cache_key, @context_graph_id, @failures, @next_retry_at, @swm_gen,
|
|
1675
|
+
@candidate_namespaces, @peer_topology_key, @updated_at
|
|
1676
|
+
)
|
|
1677
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
1678
|
+
context_graph_id = excluded.context_graph_id,
|
|
1679
|
+
failures = excluded.failures,
|
|
1680
|
+
next_retry_at = excluded.next_retry_at,
|
|
1681
|
+
swm_gen = excluded.swm_gen,
|
|
1682
|
+
candidate_namespaces = excluded.candidate_namespaces,
|
|
1683
|
+
peer_topology_key = excluded.peer_topology_key,
|
|
1684
|
+
updated_at = excluded.updated_at
|
|
1632
1685
|
`).run(record);
|
|
1633
1686
|
}
|
|
1634
1687
|
getVmReconcileNegative(cacheKey) {
|
|
@@ -1640,6 +1693,29 @@ export class DashboardDB {
|
|
|
1640
1693
|
deleteVmReconcileNegativesForContextGraph(contextGraphId) {
|
|
1641
1694
|
this.stmt('deleteVmReconcileNegativesForContextGraph', 'DELETE FROM vm_reconcile_negative_cache WHERE context_graph_id = ?').run(contextGraphId);
|
|
1642
1695
|
}
|
|
1696
|
+
upsertSelectedVmReconcileCursor(record) {
|
|
1697
|
+
this.stmt('upsertSelectedVmReconcileCursor', `
|
|
1698
|
+
INSERT INTO selected_vm_reconcile_cursors (
|
|
1699
|
+
deployment_id, context_graph_id, on_chain_context_graph_id,
|
|
1700
|
+
name_hash, watermark, updated_at
|
|
1701
|
+
) VALUES (
|
|
1702
|
+
@deployment_id, @context_graph_id, @on_chain_context_graph_id,
|
|
1703
|
+
@name_hash, @watermark, @updated_at
|
|
1704
|
+
)
|
|
1705
|
+
ON CONFLICT(deployment_id, context_graph_id, on_chain_context_graph_id) DO UPDATE SET
|
|
1706
|
+
name_hash = excluded.name_hash,
|
|
1707
|
+
watermark = excluded.watermark,
|
|
1708
|
+
updated_at = excluded.updated_at
|
|
1709
|
+
`).run(record);
|
|
1710
|
+
}
|
|
1711
|
+
getSelectedVmReconcileCursor(deploymentId, contextGraphId, onChainContextGraphId) {
|
|
1712
|
+
return this.stmt('getSelectedVmReconcileCursor', `
|
|
1713
|
+
SELECT * FROM selected_vm_reconcile_cursors
|
|
1714
|
+
WHERE deployment_id = ?
|
|
1715
|
+
AND context_graph_id = ?
|
|
1716
|
+
AND on_chain_context_graph_id = ?
|
|
1717
|
+
`).get(deploymentId, contextGraphId, onChainContextGraphId);
|
|
1718
|
+
}
|
|
1643
1719
|
// --- Phase F: chain-driven VM reconciliation telemetry ---
|
|
1644
1720
|
/**
|
|
1645
1721
|
* Persist one {@link ReplicationEvent} (best-effort sink target). `ordinal`,
|
|
@@ -1647,14 +1723,14 @@ export class DashboardDB {
|
|
|
1647
1723
|
* coerced defensively so a malformed event can never abort the insert.
|
|
1648
1724
|
*/
|
|
1649
1725
|
insertReplicationEvent(ev) {
|
|
1650
|
-
this.stmt('insertReplicationEvent', `
|
|
1651
|
-
INSERT INTO replication_events (
|
|
1652
|
-
ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1653
|
-
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1654
|
-
) VALUES (
|
|
1655
|
-
@ts, @context_graph_id, @on_chain_cg_id, @action, @ual, @ordinal, @ka_id,
|
|
1656
|
-
@from_watermark, @to_watermark, @head, @reconciled, @pending, @detail
|
|
1657
|
-
)
|
|
1726
|
+
this.stmt('insertReplicationEvent', `
|
|
1727
|
+
INSERT INTO replication_events (
|
|
1728
|
+
ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1729
|
+
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1730
|
+
) VALUES (
|
|
1731
|
+
@ts, @context_graph_id, @on_chain_cg_id, @action, @ual, @ordinal, @ka_id,
|
|
1732
|
+
@from_watermark, @to_watermark, @head, @reconciled, @pending, @detail
|
|
1733
|
+
)
|
|
1658
1734
|
`).run({
|
|
1659
1735
|
ts: ev.ts,
|
|
1660
1736
|
context_graph_id: ev.context_graph_id,
|
|
@@ -1701,15 +1777,15 @@ export class DashboardDB {
|
|
|
1701
1777
|
// and inflate the reported latency. fetch/promote for one version always
|
|
1702
1778
|
// share the same ordinal, so this pairs them exactly; a fast-path promote
|
|
1703
1779
|
// with no fetch for its ordinal correctly contributes a 0ms latency.
|
|
1704
|
-
const latencyRows = this.db.prepare(`
|
|
1705
|
-
SELECT p.ts AS promote_ts,
|
|
1706
|
-
(SELECT MAX(f.ts) FROM replication_events f
|
|
1707
|
-
WHERE f.action = 'fetch'
|
|
1708
|
-
AND f.context_graph_id = p.context_graph_id
|
|
1709
|
-
AND f.ordinal = p.ordinal
|
|
1710
|
-
AND f.ts <= p.ts AND f.ts >= ?) AS fetch_ts
|
|
1711
|
-
FROM replication_events p
|
|
1712
|
-
WHERE p.action = 'promote' AND p.ts >= ? AND p.ordinal IS NOT NULL
|
|
1780
|
+
const latencyRows = this.db.prepare(`
|
|
1781
|
+
SELECT p.ts AS promote_ts,
|
|
1782
|
+
(SELECT MAX(f.ts) FROM replication_events f
|
|
1783
|
+
WHERE f.action = 'fetch'
|
|
1784
|
+
AND f.context_graph_id = p.context_graph_id
|
|
1785
|
+
AND f.ordinal = p.ordinal
|
|
1786
|
+
AND f.ts <= p.ts AND f.ts >= ?) AS fetch_ts
|
|
1787
|
+
FROM replication_events p
|
|
1788
|
+
WHERE p.action = 'promote' AND p.ts >= ? AND p.ordinal IS NOT NULL
|
|
1713
1789
|
`).all(cutoff, cutoff);
|
|
1714
1790
|
const latencies = latencyRows
|
|
1715
1791
|
.map((r) => (r.fetch_ts != null ? r.promote_ts - r.fetch_ts : 0))
|
|
@@ -1739,20 +1815,20 @@ export class DashboardDB {
|
|
|
1739
1815
|
}
|
|
1740
1816
|
_computeReplicationPerCg(periodMs) {
|
|
1741
1817
|
const cutoff = Date.now() - periodMs;
|
|
1742
|
-
return this.db.prepare(`
|
|
1743
|
-
SELECT context_graph_id,
|
|
1744
|
-
on_chain_cg_id,
|
|
1745
|
-
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1746
|
-
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1747
|
-
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1748
|
-
SUM(CASE WHEN action = 'cursor-advance' THEN 1 ELSE 0 END) AS cursor_advances,
|
|
1749
|
-
MAX(to_watermark) AS last_watermark,
|
|
1750
|
-
MAX(head) AS last_head,
|
|
1751
|
-
MAX(ts) AS last_event_ts
|
|
1752
|
-
FROM replication_events
|
|
1753
|
-
WHERE ts >= ?
|
|
1754
|
-
GROUP BY context_graph_id, on_chain_cg_id
|
|
1755
|
-
ORDER BY last_event_ts DESC
|
|
1818
|
+
return this.db.prepare(`
|
|
1819
|
+
SELECT context_graph_id,
|
|
1820
|
+
on_chain_cg_id,
|
|
1821
|
+
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1822
|
+
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1823
|
+
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1824
|
+
SUM(CASE WHEN action = 'cursor-advance' THEN 1 ELSE 0 END) AS cursor_advances,
|
|
1825
|
+
MAX(to_watermark) AS last_watermark,
|
|
1826
|
+
MAX(head) AS last_head,
|
|
1827
|
+
MAX(ts) AS last_event_ts
|
|
1828
|
+
FROM replication_events
|
|
1829
|
+
WHERE ts >= ?
|
|
1830
|
+
GROUP BY context_graph_id, on_chain_cg_id
|
|
1831
|
+
ORDER BY last_event_ts DESC
|
|
1756
1832
|
`).all(cutoff);
|
|
1757
1833
|
}
|
|
1758
1834
|
/**
|
|
@@ -1771,16 +1847,16 @@ export class DashboardDB {
|
|
|
1771
1847
|
const cutoff = Date.now() - opts.periodMs;
|
|
1772
1848
|
const bucket = Math.max(1, Math.floor(opts.bucketMs));
|
|
1773
1849
|
const where = opts.contextGraphId ? 'AND context_graph_id = @cg' : '';
|
|
1774
|
-
return this.db.prepare(`
|
|
1775
|
-
SELECT (ts / @bucket) * @bucket AS bucket,
|
|
1776
|
-
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1777
|
-
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1778
|
-
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1779
|
-
COUNT(*) AS total
|
|
1780
|
-
FROM replication_events
|
|
1781
|
-
WHERE ts >= @cutoff ${where}
|
|
1782
|
-
GROUP BY bucket
|
|
1783
|
-
ORDER BY bucket ASC
|
|
1850
|
+
return this.db.prepare(`
|
|
1851
|
+
SELECT (ts / @bucket) * @bucket AS bucket,
|
|
1852
|
+
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1853
|
+
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1854
|
+
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1855
|
+
COUNT(*) AS total
|
|
1856
|
+
FROM replication_events
|
|
1857
|
+
WHERE ts >= @cutoff ${where}
|
|
1858
|
+
GROUP BY bucket
|
|
1859
|
+
ORDER BY bucket ASC
|
|
1784
1860
|
`).all({ cutoff, bucket, cg: opts.contextGraphId ?? null });
|
|
1785
1861
|
}
|
|
1786
1862
|
/**
|
|
@@ -1792,13 +1868,13 @@ export class DashboardDB {
|
|
|
1792
1868
|
const safeLimit = Number.isFinite(limit)
|
|
1793
1869
|
? Math.max(1, Math.min(1000, Math.trunc(limit)))
|
|
1794
1870
|
: 100;
|
|
1795
|
-
return this.db.prepare(`
|
|
1796
|
-
SELECT ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1797
|
-
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1798
|
-
FROM replication_events
|
|
1799
|
-
WHERE context_graph_id = ?
|
|
1800
|
-
ORDER BY ts DESC
|
|
1801
|
-
LIMIT ?
|
|
1871
|
+
return this.db.prepare(`
|
|
1872
|
+
SELECT ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1873
|
+
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1874
|
+
FROM replication_events
|
|
1875
|
+
WHERE context_graph_id = ?
|
|
1876
|
+
ORDER BY ts DESC
|
|
1877
|
+
LIMIT ?
|
|
1802
1878
|
`).all(contextGraphId, safeLimit);
|
|
1803
1879
|
}
|
|
1804
1880
|
/**
|
|
@@ -1807,38 +1883,38 @@ export class DashboardDB {
|
|
|
1807
1883
|
* on-disk truth the Replication tab's cursor inspector renders.
|
|
1808
1884
|
*/
|
|
1809
1885
|
getReplicationCursors() {
|
|
1810
|
-
return this.db.prepare(`
|
|
1811
|
-
SELECT s.context_graph_id AS context_graph_id,
|
|
1812
|
-
s.on_chain_id AS on_chain_id,
|
|
1813
|
-
s.last_reconciled_ordinal AS last_reconciled_ordinal,
|
|
1814
|
-
s.core_hosted AS core_hosted,
|
|
1815
|
-
s.subscribed AS subscribed,
|
|
1816
|
-
(SELECT MAX(head) FROM replication_events e
|
|
1817
|
-
WHERE e.context_graph_id = s.context_graph_id) AS last_head,
|
|
1818
|
-
(SELECT MAX(ts) FROM replication_events e
|
|
1819
|
-
WHERE e.context_graph_id = s.context_graph_id) AS last_event_ts
|
|
1820
|
-
FROM context_graph_subscriptions s
|
|
1821
|
-
ORDER BY s.context_graph_id ASC
|
|
1886
|
+
return this.db.prepare(`
|
|
1887
|
+
SELECT s.context_graph_id AS context_graph_id,
|
|
1888
|
+
s.on_chain_id AS on_chain_id,
|
|
1889
|
+
s.last_reconciled_ordinal AS last_reconciled_ordinal,
|
|
1890
|
+
s.core_hosted AS core_hosted,
|
|
1891
|
+
s.subscribed AS subscribed,
|
|
1892
|
+
(SELECT MAX(head) FROM replication_events e
|
|
1893
|
+
WHERE e.context_graph_id = s.context_graph_id) AS last_head,
|
|
1894
|
+
(SELECT MAX(ts) FROM replication_events e
|
|
1895
|
+
WHERE e.context_graph_id = s.context_graph_id) AS last_event_ts
|
|
1896
|
+
FROM context_graph_subscriptions s
|
|
1897
|
+
ORDER BY s.context_graph_id ASC
|
|
1822
1898
|
`).all();
|
|
1823
1899
|
}
|
|
1824
1900
|
upsertContextGraphMember(record) {
|
|
1825
1901
|
const firstSeenAt = record.first_seen_at ?? record.updated_at;
|
|
1826
|
-
this.stmt('upsertContextGraphMember', `
|
|
1827
|
-
INSERT INTO context_graph_memberships (
|
|
1828
|
-
context_graph_id, principal_type, principal_id, role, status, source,
|
|
1829
|
-
display_name, metadata, first_seen_at, updated_at
|
|
1830
|
-
) VALUES (
|
|
1831
|
-
@context_graph_id, @principal_type, @principal_id, @role, @status, @source,
|
|
1832
|
-
@display_name, @metadata, @first_seen_at, @updated_at
|
|
1833
|
-
)
|
|
1834
|
-
ON CONFLICT(context_graph_id, principal_type, principal_id) DO UPDATE SET
|
|
1835
|
-
role = excluded.role,
|
|
1836
|
-
status = excluded.status,
|
|
1837
|
-
source = excluded.source,
|
|
1838
|
-
display_name = excluded.display_name,
|
|
1839
|
-
metadata = excluded.metadata,
|
|
1840
|
-
first_seen_at = context_graph_memberships.first_seen_at,
|
|
1841
|
-
updated_at = excluded.updated_at
|
|
1902
|
+
this.stmt('upsertContextGraphMember', `
|
|
1903
|
+
INSERT INTO context_graph_memberships (
|
|
1904
|
+
context_graph_id, principal_type, principal_id, role, status, source,
|
|
1905
|
+
display_name, metadata, first_seen_at, updated_at
|
|
1906
|
+
) VALUES (
|
|
1907
|
+
@context_graph_id, @principal_type, @principal_id, @role, @status, @source,
|
|
1908
|
+
@display_name, @metadata, @first_seen_at, @updated_at
|
|
1909
|
+
)
|
|
1910
|
+
ON CONFLICT(context_graph_id, principal_type, principal_id) DO UPDATE SET
|
|
1911
|
+
role = excluded.role,
|
|
1912
|
+
status = excluded.status,
|
|
1913
|
+
source = excluded.source,
|
|
1914
|
+
display_name = excluded.display_name,
|
|
1915
|
+
metadata = excluded.metadata,
|
|
1916
|
+
first_seen_at = context_graph_memberships.first_seen_at,
|
|
1917
|
+
updated_at = excluded.updated_at
|
|
1842
1918
|
`).run({
|
|
1843
1919
|
context_graph_id: record.context_graph_id,
|
|
1844
1920
|
principal_type: record.principal_type,
|
|
@@ -1854,15 +1930,15 @@ export class DashboardDB {
|
|
|
1854
1930
|
}
|
|
1855
1931
|
listContextGraphMembers(contextGraphId) {
|
|
1856
1932
|
if (contextGraphId) {
|
|
1857
|
-
return this.db.prepare(`
|
|
1858
|
-
SELECT * FROM context_graph_memberships
|
|
1859
|
-
WHERE context_graph_id = ?
|
|
1860
|
-
ORDER BY principal_type ASC, principal_id ASC
|
|
1933
|
+
return this.db.prepare(`
|
|
1934
|
+
SELECT * FROM context_graph_memberships
|
|
1935
|
+
WHERE context_graph_id = ?
|
|
1936
|
+
ORDER BY principal_type ASC, principal_id ASC
|
|
1861
1937
|
`).all(contextGraphId);
|
|
1862
1938
|
}
|
|
1863
|
-
return this.db.prepare(`
|
|
1864
|
-
SELECT * FROM context_graph_memberships
|
|
1865
|
-
ORDER BY context_graph_id ASC, principal_type ASC, principal_id ASC
|
|
1939
|
+
return this.db.prepare(`
|
|
1940
|
+
SELECT * FROM context_graph_memberships
|
|
1941
|
+
ORDER BY context_graph_id ASC, principal_type ASC, principal_id ASC
|
|
1866
1942
|
`).all();
|
|
1867
1943
|
}
|
|
1868
1944
|
deleteContextGraphMember(contextGraphId, principalType, principalId) {
|
|
@@ -1874,18 +1950,18 @@ export class DashboardDB {
|
|
|
1874
1950
|
return this.db.prepare('SELECT * FROM metric_snapshots WHERE ts >= ? AND ts <= ? ORDER BY ts').all(from, to);
|
|
1875
1951
|
}
|
|
1876
1952
|
const step = Math.ceil(total.c / maxPoints);
|
|
1877
|
-
return this.db.prepare(`
|
|
1878
|
-
SELECT * FROM (
|
|
1879
|
-
SELECT *, ROW_NUMBER() OVER (ORDER BY ts) as rn
|
|
1880
|
-
FROM metric_snapshots WHERE ts >= ? AND ts <= ?
|
|
1881
|
-
) WHERE rn % ? = 0 ORDER BY ts
|
|
1953
|
+
return this.db.prepare(`
|
|
1954
|
+
SELECT * FROM (
|
|
1955
|
+
SELECT *, ROW_NUMBER() OVER (ORDER BY ts) as rn
|
|
1956
|
+
FROM metric_snapshots WHERE ts >= ? AND ts <= ?
|
|
1957
|
+
) WHERE rn % ? = 0 ORDER BY ts
|
|
1882
1958
|
`).all(from, to, step);
|
|
1883
1959
|
}
|
|
1884
1960
|
// --- Operations ---
|
|
1885
1961
|
insertOperation(op) {
|
|
1886
|
-
this.stmt('insertOp', `
|
|
1887
|
-
INSERT INTO operations (operation_id, operation_name, started_at, status, peer_id, contextGraph_id, details)
|
|
1888
|
-
VALUES (@operation_id, @operation_name, @started_at, 'in_progress', @peer_id, @contextGraph_id, @details)
|
|
1962
|
+
this.stmt('insertOp', `
|
|
1963
|
+
INSERT INTO operations (operation_id, operation_name, started_at, status, peer_id, contextGraph_id, details)
|
|
1964
|
+
VALUES (@operation_id, @operation_name, @started_at, 'in_progress', @peer_id, @contextGraph_id, @details)
|
|
1889
1965
|
`).run({
|
|
1890
1966
|
operation_id: op.operation_id,
|
|
1891
1967
|
operation_name: op.operation_name,
|
|
@@ -1896,10 +1972,10 @@ export class DashboardDB {
|
|
|
1896
1972
|
});
|
|
1897
1973
|
}
|
|
1898
1974
|
completeOperation(op) {
|
|
1899
|
-
this.stmt('completeOp', `
|
|
1900
|
-
UPDATE operations SET status = 'success', duration_ms = @duration_ms,
|
|
1901
|
-
triple_count = @triple_count, details = COALESCE(@details, details)
|
|
1902
|
-
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1975
|
+
this.stmt('completeOp', `
|
|
1976
|
+
UPDATE operations SET status = 'success', duration_ms = @duration_ms,
|
|
1977
|
+
triple_count = @triple_count, details = COALESCE(@details, details)
|
|
1978
|
+
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1903
1979
|
`).run({
|
|
1904
1980
|
operation_id: op.operation_id,
|
|
1905
1981
|
duration_ms: op.duration_ms,
|
|
@@ -1914,10 +1990,10 @@ export class DashboardDB {
|
|
|
1914
1990
|
this.finishOperation({ ...op, status: 'cancelled' });
|
|
1915
1991
|
}
|
|
1916
1992
|
finishOperation(op) {
|
|
1917
|
-
this.stmt('finishOperation', `
|
|
1918
|
-
UPDATE operations SET status = @status, duration_ms = @duration_ms,
|
|
1919
|
-
error_message = @error_message
|
|
1920
|
-
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1993
|
+
this.stmt('finishOperation', `
|
|
1994
|
+
UPDATE operations SET status = @status, duration_ms = @duration_ms,
|
|
1995
|
+
error_message = @error_message
|
|
1996
|
+
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1921
1997
|
`).run(op);
|
|
1922
1998
|
}
|
|
1923
1999
|
getOperations(opts = {}) {
|
|
@@ -1974,22 +2050,22 @@ export class DashboardDB {
|
|
|
1974
2050
|
}
|
|
1975
2051
|
getErrorHotspots(periodMs = 7 * 86_400_000) {
|
|
1976
2052
|
const cutoff = Date.now() - periodMs;
|
|
1977
|
-
return this.db.prepare(`
|
|
1978
|
-
SELECT
|
|
1979
|
-
p.phase,
|
|
1980
|
-
o.operation_name,
|
|
1981
|
-
COUNT(*) as error_count,
|
|
1982
|
-
(SELECT p2.details FROM operation_phases p2
|
|
1983
|
-
JOIN operations o2 ON o2.operation_id = p2.operation_id
|
|
1984
|
-
WHERE p2.phase = p.phase AND o2.operation_name = o.operation_name
|
|
1985
|
-
AND p2.status = 'error' AND p2.started_at >= ?
|
|
1986
|
-
ORDER BY p2.started_at DESC LIMIT 1) as last_error,
|
|
1987
|
-
MAX(p.started_at) as last_occurred
|
|
1988
|
-
FROM operation_phases p
|
|
1989
|
-
JOIN operations o ON o.operation_id = p.operation_id
|
|
1990
|
-
WHERE p.status = 'error' AND p.started_at >= ?
|
|
1991
|
-
GROUP BY p.phase, o.operation_name
|
|
1992
|
-
ORDER BY error_count DESC
|
|
2053
|
+
return this.db.prepare(`
|
|
2054
|
+
SELECT
|
|
2055
|
+
p.phase,
|
|
2056
|
+
o.operation_name,
|
|
2057
|
+
COUNT(*) as error_count,
|
|
2058
|
+
(SELECT p2.details FROM operation_phases p2
|
|
2059
|
+
JOIN operations o2 ON o2.operation_id = p2.operation_id
|
|
2060
|
+
WHERE p2.phase = p.phase AND o2.operation_name = o.operation_name
|
|
2061
|
+
AND p2.status = 'error' AND p2.started_at >= ?
|
|
2062
|
+
ORDER BY p2.started_at DESC LIMIT 1) as last_error,
|
|
2063
|
+
MAX(p.started_at) as last_occurred
|
|
2064
|
+
FROM operation_phases p
|
|
2065
|
+
JOIN operations o ON o.operation_id = p.operation_id
|
|
2066
|
+
WHERE p.status = 'error' AND p.started_at >= ?
|
|
2067
|
+
GROUP BY p.phase, o.operation_name
|
|
2068
|
+
ORDER BY error_count DESC
|
|
1993
2069
|
`).all(cutoff, cutoff);
|
|
1994
2070
|
}
|
|
1995
2071
|
getFailedOperations(opts = {}) {
|
|
@@ -2011,17 +2087,17 @@ export class DashboardDB {
|
|
|
2011
2087
|
params.push(like, like, like);
|
|
2012
2088
|
}
|
|
2013
2089
|
params.push(limit);
|
|
2014
|
-
const rows = this.db.prepare(`
|
|
2015
|
-
SELECT
|
|
2016
|
-
o.*,
|
|
2017
|
-
p.phase AS phase,
|
|
2018
|
-
p.details AS phase_error,
|
|
2019
|
-
p.started_at AS phase_started_at
|
|
2020
|
-
FROM operation_phases p
|
|
2021
|
-
JOIN operations o ON o.operation_id = p.operation_id
|
|
2022
|
-
WHERE ${where}
|
|
2023
|
-
ORDER BY p.started_at DESC
|
|
2024
|
-
LIMIT ?
|
|
2090
|
+
const rows = this.db.prepare(`
|
|
2091
|
+
SELECT
|
|
2092
|
+
o.*,
|
|
2093
|
+
p.phase AS phase,
|
|
2094
|
+
p.details AS phase_error,
|
|
2095
|
+
p.started_at AS phase_started_at
|
|
2096
|
+
FROM operation_phases p
|
|
2097
|
+
JOIN operations o ON o.operation_id = p.operation_id
|
|
2098
|
+
WHERE ${where}
|
|
2099
|
+
ORDER BY p.started_at DESC
|
|
2100
|
+
LIMIT ?
|
|
2025
2101
|
`).all(...params);
|
|
2026
2102
|
const operations = rows.map(row => {
|
|
2027
2103
|
const logs = this.db.prepare('SELECT * FROM logs WHERE operation_id = ? ORDER BY ts DESC LIMIT 20').all(row.operation_id);
|
|
@@ -2038,15 +2114,15 @@ export class DashboardDB {
|
|
|
2038
2114
|
}
|
|
2039
2115
|
// --- Operation phases ---
|
|
2040
2116
|
insertPhase(op) {
|
|
2041
|
-
this.stmt('insertPhase', `
|
|
2042
|
-
INSERT INTO operation_phases (operation_id, phase, started_at, status)
|
|
2043
|
-
VALUES (@operation_id, @phase, @started_at, 'in_progress')
|
|
2117
|
+
this.stmt('insertPhase', `
|
|
2118
|
+
INSERT INTO operation_phases (operation_id, phase, started_at, status)
|
|
2119
|
+
VALUES (@operation_id, @phase, @started_at, 'in_progress')
|
|
2044
2120
|
`).run(op);
|
|
2045
2121
|
}
|
|
2046
2122
|
completePhase(op) {
|
|
2047
|
-
this.stmt('completePhase', `
|
|
2048
|
-
UPDATE operation_phases SET status = 'success', duration_ms = @duration_ms
|
|
2049
|
-
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
2123
|
+
this.stmt('completePhase', `
|
|
2124
|
+
UPDATE operation_phases SET status = 'success', duration_ms = @duration_ms
|
|
2125
|
+
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
2050
2126
|
`).run(op);
|
|
2051
2127
|
}
|
|
2052
2128
|
failPhase(op) {
|
|
@@ -2056,30 +2132,30 @@ export class DashboardDB {
|
|
|
2056
2132
|
this.finishPhase({ ...op, status: 'cancelled' });
|
|
2057
2133
|
}
|
|
2058
2134
|
finishPhase(op) {
|
|
2059
|
-
this.stmt('finishPhase', `
|
|
2060
|
-
UPDATE operation_phases SET status = @status, duration_ms = @duration_ms,
|
|
2061
|
-
details = @error_message
|
|
2062
|
-
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
2135
|
+
this.stmt('finishPhase', `
|
|
2136
|
+
UPDATE operation_phases SET status = @status, duration_ms = @duration_ms,
|
|
2137
|
+
details = @error_message
|
|
2138
|
+
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
2063
2139
|
`).run(op);
|
|
2064
2140
|
}
|
|
2065
2141
|
failAllPhases(op) {
|
|
2066
|
-
this.stmt('failAllPhases', `
|
|
2067
|
-
UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
|
|
2068
|
-
details = @error_message
|
|
2069
|
-
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
2142
|
+
this.stmt('failAllPhases', `
|
|
2143
|
+
UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
|
|
2144
|
+
details = @error_message
|
|
2145
|
+
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
2070
2146
|
`).run(op);
|
|
2071
2147
|
}
|
|
2072
2148
|
// --- Operation cost & tx ---
|
|
2073
2149
|
setOperationCost(op) {
|
|
2074
|
-
this.stmt('setCost', `
|
|
2075
|
-
UPDATE operations SET
|
|
2076
|
-
gas_used = COALESCE(@gas_used, gas_used),
|
|
2077
|
-
gas_price_gwei = COALESCE(@gas_price_gwei, gas_price_gwei),
|
|
2078
|
-
gas_cost_eth = COALESCE(@gas_cost_eth, gas_cost_eth),
|
|
2079
|
-
trac_cost = COALESCE(@trac_cost, trac_cost),
|
|
2080
|
-
tx_hash = COALESCE(@tx_hash, tx_hash),
|
|
2081
|
-
chain_id = COALESCE(@chain_id, chain_id)
|
|
2082
|
-
WHERE operation_id = @operation_id
|
|
2150
|
+
this.stmt('setCost', `
|
|
2151
|
+
UPDATE operations SET
|
|
2152
|
+
gas_used = COALESCE(@gas_used, gas_used),
|
|
2153
|
+
gas_price_gwei = COALESCE(@gas_price_gwei, gas_price_gwei),
|
|
2154
|
+
gas_cost_eth = COALESCE(@gas_cost_eth, gas_cost_eth),
|
|
2155
|
+
trac_cost = COALESCE(@trac_cost, trac_cost),
|
|
2156
|
+
tx_hash = COALESCE(@tx_hash, tx_hash),
|
|
2157
|
+
chain_id = COALESCE(@chain_id, chain_id)
|
|
2158
|
+
WHERE operation_id = @operation_id
|
|
2083
2159
|
`).run({
|
|
2084
2160
|
operation_id: op.operation_id,
|
|
2085
2161
|
gas_used: op.gas_used ?? null,
|
|
@@ -2097,18 +2173,18 @@ export class DashboardDB {
|
|
|
2097
2173
|
const params = [cutoff];
|
|
2098
2174
|
if (opts.name)
|
|
2099
2175
|
params.push(opts.name);
|
|
2100
|
-
const summaryRow = this.db.prepare(`
|
|
2101
|
-
SELECT
|
|
2102
|
-
COUNT(*) as totalCount,
|
|
2103
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2104
|
-
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errorCount,
|
|
2105
|
-
SUM(CASE WHEN status IN ('success', 'error') THEN 1 ELSE 0 END) as healthCount,
|
|
2106
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
2107
|
-
AVG(gas_cost_eth) as avgGasCostEth,
|
|
2108
|
-
SUM(gas_cost_eth) as totalGasCostEth,
|
|
2109
|
-
AVG(trac_cost) as avgTracCost,
|
|
2110
|
-
SUM(trac_cost) as totalTracCost
|
|
2111
|
-
FROM operations WHERE started_at >= ? ${nameFilter}
|
|
2176
|
+
const summaryRow = this.db.prepare(`
|
|
2177
|
+
SELECT
|
|
2178
|
+
COUNT(*) as totalCount,
|
|
2179
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2180
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errorCount,
|
|
2181
|
+
SUM(CASE WHEN status IN ('success', 'error') THEN 1 ELSE 0 END) as healthCount,
|
|
2182
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
2183
|
+
AVG(gas_cost_eth) as avgGasCostEth,
|
|
2184
|
+
SUM(gas_cost_eth) as totalGasCostEth,
|
|
2185
|
+
AVG(trac_cost) as avgTracCost,
|
|
2186
|
+
SUM(trac_cost) as totalTracCost
|
|
2187
|
+
FROM operations WHERE started_at >= ? ${nameFilter}
|
|
2112
2188
|
`).get(...params);
|
|
2113
2189
|
const summary = {
|
|
2114
2190
|
totalCount: summaryRow.totalCount ?? 0,
|
|
@@ -2127,18 +2203,18 @@ export class DashboardDB {
|
|
|
2127
2203
|
const tsParams = [bucketSize, cutoff];
|
|
2128
2204
|
if (opts.name)
|
|
2129
2205
|
tsParams.push(opts.name);
|
|
2130
|
-
const timeSeries = this.db.prepare(`
|
|
2131
|
-
SELECT
|
|
2132
|
-
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
2133
|
-
COUNT(*) as count,
|
|
2134
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2135
|
-
SUM(CASE WHEN status IN ('success', 'error') THEN 1 ELSE 0 END) as healthCount,
|
|
2136
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
2137
|
-
AVG(gas_cost_eth) as avgGasCostEth,
|
|
2138
|
-
SUM(gas_cost_eth) as totalGasCostEth
|
|
2139
|
-
FROM operations
|
|
2140
|
-
WHERE started_at >= ? ${nameFilter}
|
|
2141
|
-
GROUP BY bucket ORDER BY bucket
|
|
2206
|
+
const timeSeries = this.db.prepare(`
|
|
2207
|
+
SELECT
|
|
2208
|
+
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
2209
|
+
COUNT(*) as count,
|
|
2210
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2211
|
+
SUM(CASE WHEN status IN ('success', 'error') THEN 1 ELSE 0 END) as healthCount,
|
|
2212
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
2213
|
+
AVG(gas_cost_eth) as avgGasCostEth,
|
|
2214
|
+
SUM(gas_cost_eth) as totalGasCostEth
|
|
2215
|
+
FROM operations
|
|
2216
|
+
WHERE started_at >= ? ${nameFilter}
|
|
2217
|
+
GROUP BY bucket ORDER BY bucket
|
|
2142
2218
|
`).all(bucketSize, bucketSize, ...params);
|
|
2143
2219
|
return {
|
|
2144
2220
|
summary,
|
|
@@ -2155,17 +2231,17 @@ export class DashboardDB {
|
|
|
2155
2231
|
// --- Per-type time series ---
|
|
2156
2232
|
getPerTypeTimeSeries(opts) {
|
|
2157
2233
|
const cutoff = Date.now() - opts.periodMs;
|
|
2158
|
-
const rows = this.db.prepare(`
|
|
2159
|
-
SELECT
|
|
2160
|
-
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
2161
|
-
operation_name as type,
|
|
2162
|
-
COUNT(*) as count,
|
|
2163
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs,
|
|
2164
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2165
|
-
SUM(CASE WHEN status IN ('success', 'error') THEN 1 ELSE 0 END) as healthCount,
|
|
2166
|
-
SUM(gas_cost_eth) as gasCostEth
|
|
2167
|
-
FROM operations WHERE started_at >= ?
|
|
2168
|
-
GROUP BY bucket, operation_name ORDER BY bucket
|
|
2234
|
+
const rows = this.db.prepare(`
|
|
2235
|
+
SELECT
|
|
2236
|
+
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
2237
|
+
operation_name as type,
|
|
2238
|
+
COUNT(*) as count,
|
|
2239
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs,
|
|
2240
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2241
|
+
SUM(CASE WHEN status IN ('success', 'error') THEN 1 ELSE 0 END) as healthCount,
|
|
2242
|
+
SUM(gas_cost_eth) as gasCostEth
|
|
2243
|
+
FROM operations WHERE started_at >= ?
|
|
2244
|
+
GROUP BY bucket, operation_name ORDER BY bucket
|
|
2169
2245
|
`).all(opts.bucketMs, opts.bucketMs, cutoff);
|
|
2170
2246
|
const bucketSet = new Set();
|
|
2171
2247
|
const typeSet = new Set();
|
|
@@ -2195,15 +2271,15 @@ export class DashboardDB {
|
|
|
2195
2271
|
// --- Success rates by operation type ---
|
|
2196
2272
|
getSuccessRatesByType(periodMs) {
|
|
2197
2273
|
const cutoff = Date.now() - periodMs;
|
|
2198
|
-
return this.db.prepare(`
|
|
2199
|
-
SELECT
|
|
2200
|
-
operation_name as type,
|
|
2201
|
-
COUNT(*) as total,
|
|
2202
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
|
|
2203
|
-
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
2204
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs
|
|
2205
|
-
FROM operations WHERE started_at >= ?
|
|
2206
|
-
GROUP BY operation_name ORDER BY total DESC
|
|
2274
|
+
return this.db.prepare(`
|
|
2275
|
+
SELECT
|
|
2276
|
+
operation_name as type,
|
|
2277
|
+
COUNT(*) as total,
|
|
2278
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
|
|
2279
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
2280
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs
|
|
2281
|
+
FROM operations WHERE started_at >= ?
|
|
2282
|
+
GROUP BY operation_name ORDER BY total DESC
|
|
2207
2283
|
`).all(cutoff).map(r => ({
|
|
2208
2284
|
...r,
|
|
2209
2285
|
rate: r.success + r.error > 0 ? r.success / (r.success + r.error) : 0,
|
|
@@ -2226,16 +2302,16 @@ export class DashboardDB {
|
|
|
2226
2302
|
// on-chain (Verifiable Memory commits) are counted, so the publish
|
|
2227
2303
|
// count and the TRAC total are consistent. Free SWM/local/testnet
|
|
2228
2304
|
// publishes record trac_cost = 0 and are intentionally excluded.
|
|
2229
|
-
const row = this.db.prepare(`
|
|
2230
|
-
SELECT
|
|
2231
|
-
SUM(CASE WHEN trac_cost > 0 THEN 1 ELSE 0 END) as publishCount,
|
|
2232
|
-
SUM(CASE WHEN status = 'success' AND trac_cost > 0 THEN 1 ELSE 0 END) as successCount,
|
|
2233
|
-
COALESCE(SUM(gas_cost_eth), 0) as totalGasEth,
|
|
2234
|
-
COALESCE(SUM(trac_cost), 0) as totalTrac,
|
|
2235
|
-
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN gas_cost_eth END), 0) as avgGasEth,
|
|
2236
|
-
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN trac_cost END), 0) as avgTrac
|
|
2237
|
-
FROM operations
|
|
2238
|
-
WHERE operation_name = 'publish' AND trac_cost > 0 AND started_at >= ?
|
|
2305
|
+
const row = this.db.prepare(`
|
|
2306
|
+
SELECT
|
|
2307
|
+
SUM(CASE WHEN trac_cost > 0 THEN 1 ELSE 0 END) as publishCount,
|
|
2308
|
+
SUM(CASE WHEN status = 'success' AND trac_cost > 0 THEN 1 ELSE 0 END) as successCount,
|
|
2309
|
+
COALESCE(SUM(gas_cost_eth), 0) as totalGasEth,
|
|
2310
|
+
COALESCE(SUM(trac_cost), 0) as totalTrac,
|
|
2311
|
+
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN gas_cost_eth END), 0) as avgGasEth,
|
|
2312
|
+
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN trac_cost END), 0) as avgTrac
|
|
2313
|
+
FROM operations
|
|
2314
|
+
WHERE operation_name = 'publish' AND trac_cost > 0 AND started_at >= ?
|
|
2239
2315
|
`).get(cutoff);
|
|
2240
2316
|
results.periods.push({
|
|
2241
2317
|
label: p.label,
|
|
@@ -2270,9 +2346,9 @@ export class DashboardDB {
|
|
|
2270
2346
|
* still throw — pinned by db.test.ts.
|
|
2271
2347
|
*/
|
|
2272
2348
|
insertChatMessage(msg) {
|
|
2273
|
-
const info = this.stmt('insertChat', `
|
|
2274
|
-
INSERT INTO chat_messages (ts, direction, peer, peer_name, text, delivered, message_id)
|
|
2275
|
-
VALUES (@ts, @direction, @peer, @peer_name, @text, @delivered, @message_id)
|
|
2349
|
+
const info = this.stmt('insertChat', `
|
|
2350
|
+
INSERT INTO chat_messages (ts, direction, peer, peer_name, text, delivered, message_id)
|
|
2351
|
+
VALUES (@ts, @direction, @peer, @peer_name, @text, @delivered, @message_id)
|
|
2276
2352
|
`).run({
|
|
2277
2353
|
ts: msg.ts,
|
|
2278
2354
|
direction: msg.direction,
|
|
@@ -2363,16 +2439,16 @@ export class DashboardDB {
|
|
|
2363
2439
|
return this.db.prepare('SELECT * FROM chat_persistence_jobs WHERE turn_id = ?').get(turnId);
|
|
2364
2440
|
}
|
|
2365
2441
|
insertChatPersistenceJob(job) {
|
|
2366
|
-
this.stmt('insertChatPersistenceJob', `
|
|
2367
|
-
INSERT INTO chat_persistence_jobs (
|
|
2368
|
-
turn_id, session_id, user_message, assistant_reply, tool_calls_json,
|
|
2369
|
-
status, attempts, max_attempts, next_attempt_at, queued_at, updated_at,
|
|
2370
|
-
store_ms, error_message
|
|
2371
|
-
) VALUES (
|
|
2372
|
-
@turn_id, @session_id, @user_message, @assistant_reply, @tool_calls_json,
|
|
2373
|
-
@status, @attempts, @max_attempts, @next_attempt_at, @queued_at, @updated_at,
|
|
2374
|
-
@store_ms, @error_message
|
|
2375
|
-
)
|
|
2442
|
+
this.stmt('insertChatPersistenceJob', `
|
|
2443
|
+
INSERT INTO chat_persistence_jobs (
|
|
2444
|
+
turn_id, session_id, user_message, assistant_reply, tool_calls_json,
|
|
2445
|
+
status, attempts, max_attempts, next_attempt_at, queued_at, updated_at,
|
|
2446
|
+
store_ms, error_message
|
|
2447
|
+
) VALUES (
|
|
2448
|
+
@turn_id, @session_id, @user_message, @assistant_reply, @tool_calls_json,
|
|
2449
|
+
@status, @attempts, @max_attempts, @next_attempt_at, @queued_at, @updated_at,
|
|
2450
|
+
@store_ms, @error_message
|
|
2451
|
+
)
|
|
2376
2452
|
`).run({
|
|
2377
2453
|
...job,
|
|
2378
2454
|
tool_calls_json: job.tool_calls_json ?? null,
|
|
@@ -2381,46 +2457,46 @@ export class DashboardDB {
|
|
|
2381
2457
|
});
|
|
2382
2458
|
}
|
|
2383
2459
|
markChatPersistenceInProgress(turnId, attempts, updatedAt) {
|
|
2384
|
-
this.stmt('markChatPersistenceInProgress', `
|
|
2385
|
-
UPDATE chat_persistence_jobs
|
|
2386
|
-
SET status = 'in_progress', attempts = ?, updated_at = ?, error_message = NULL
|
|
2387
|
-
WHERE turn_id = ?
|
|
2460
|
+
this.stmt('markChatPersistenceInProgress', `
|
|
2461
|
+
UPDATE chat_persistence_jobs
|
|
2462
|
+
SET status = 'in_progress', attempts = ?, updated_at = ?, error_message = NULL
|
|
2463
|
+
WHERE turn_id = ?
|
|
2388
2464
|
`).run(attempts, updatedAt, turnId);
|
|
2389
2465
|
}
|
|
2390
2466
|
markChatPersistenceStored(turnId, storeMs, updatedAt) {
|
|
2391
|
-
this.stmt('markChatPersistenceStored', `
|
|
2392
|
-
UPDATE chat_persistence_jobs
|
|
2393
|
-
SET status = 'stored', store_ms = ?, updated_at = ?, error_message = NULL
|
|
2394
|
-
WHERE turn_id = ?
|
|
2467
|
+
this.stmt('markChatPersistenceStored', `
|
|
2468
|
+
UPDATE chat_persistence_jobs
|
|
2469
|
+
SET status = 'stored', store_ms = ?, updated_at = ?, error_message = NULL
|
|
2470
|
+
WHERE turn_id = ?
|
|
2395
2471
|
`).run(storeMs, updatedAt, turnId);
|
|
2396
2472
|
}
|
|
2397
2473
|
markChatPersistencePendingRetry(turnId, attempts, nextAttemptAt, updatedAt, errorMessage) {
|
|
2398
|
-
this.stmt('markChatPersistencePendingRetry', `
|
|
2399
|
-
UPDATE chat_persistence_jobs
|
|
2400
|
-
SET status = 'pending', attempts = ?, next_attempt_at = ?, updated_at = ?, error_message = ?
|
|
2401
|
-
WHERE turn_id = ?
|
|
2474
|
+
this.stmt('markChatPersistencePendingRetry', `
|
|
2475
|
+
UPDATE chat_persistence_jobs
|
|
2476
|
+
SET status = 'pending', attempts = ?, next_attempt_at = ?, updated_at = ?, error_message = ?
|
|
2477
|
+
WHERE turn_id = ?
|
|
2402
2478
|
`).run(attempts, nextAttemptAt, updatedAt, errorMessage, turnId);
|
|
2403
2479
|
}
|
|
2404
2480
|
markChatPersistenceFailed(turnId, attempts, updatedAt, errorMessage) {
|
|
2405
|
-
this.stmt('markChatPersistenceFailed', `
|
|
2406
|
-
UPDATE chat_persistence_jobs
|
|
2407
|
-
SET status = 'failed', attempts = ?, updated_at = ?, error_message = ?
|
|
2408
|
-
WHERE turn_id = ?
|
|
2481
|
+
this.stmt('markChatPersistenceFailed', `
|
|
2482
|
+
UPDATE chat_persistence_jobs
|
|
2483
|
+
SET status = 'failed', attempts = ?, updated_at = ?, error_message = ?
|
|
2484
|
+
WHERE turn_id = ?
|
|
2409
2485
|
`).run(attempts, updatedAt, errorMessage, turnId);
|
|
2410
2486
|
}
|
|
2411
2487
|
recoverInProgressChatPersistenceJobs(now) {
|
|
2412
|
-
this.stmt('recoverInProgressChatPersistenceJobs', `
|
|
2413
|
-
UPDATE chat_persistence_jobs
|
|
2414
|
-
SET status = 'pending', next_attempt_at = ?, updated_at = ?
|
|
2415
|
-
WHERE status = 'in_progress'
|
|
2488
|
+
this.stmt('recoverInProgressChatPersistenceJobs', `
|
|
2489
|
+
UPDATE chat_persistence_jobs
|
|
2490
|
+
SET status = 'pending', next_attempt_at = ?, updated_at = ?
|
|
2491
|
+
WHERE status = 'in_progress'
|
|
2416
2492
|
`).run(now, now);
|
|
2417
2493
|
}
|
|
2418
2494
|
getRunnableChatPersistenceJobs(now, limit = 10) {
|
|
2419
|
-
return this.db.prepare(`
|
|
2420
|
-
SELECT * FROM chat_persistence_jobs
|
|
2421
|
-
WHERE status = 'pending' AND next_attempt_at <= ?
|
|
2422
|
-
ORDER BY next_attempt_at ASC, queued_at ASC
|
|
2423
|
-
LIMIT ?
|
|
2495
|
+
return this.db.prepare(`
|
|
2496
|
+
SELECT * FROM chat_persistence_jobs
|
|
2497
|
+
WHERE status = 'pending' AND next_attempt_at <= ?
|
|
2498
|
+
ORDER BY next_attempt_at ASC, queued_at ASC
|
|
2499
|
+
LIMIT ?
|
|
2424
2500
|
`).all(now, limit);
|
|
2425
2501
|
}
|
|
2426
2502
|
getNextPendingChatPersistenceAt() {
|
|
@@ -2428,19 +2504,19 @@ export class DashboardDB {
|
|
|
2428
2504
|
return row?.next_at ?? null;
|
|
2429
2505
|
}
|
|
2430
2506
|
getChatPersistenceHealth(now) {
|
|
2431
|
-
const counts = this.db.prepare(`
|
|
2432
|
-
SELECT
|
|
2433
|
-
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
|
|
2434
|
-
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_count,
|
|
2435
|
-
SUM(CASE WHEN status = 'stored' THEN 1 ELSE 0 END) AS stored_count,
|
|
2436
|
-
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_count,
|
|
2437
|
-
SUM(CASE WHEN status = 'pending' AND next_attempt_at < ? THEN 1 ELSE 0 END) AS overdue_pending_count
|
|
2438
|
-
FROM chat_persistence_jobs
|
|
2507
|
+
const counts = this.db.prepare(`
|
|
2508
|
+
SELECT
|
|
2509
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
|
|
2510
|
+
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_count,
|
|
2511
|
+
SUM(CASE WHEN status = 'stored' THEN 1 ELSE 0 END) AS stored_count,
|
|
2512
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_count,
|
|
2513
|
+
SUM(CASE WHEN status = 'pending' AND next_attempt_at < ? THEN 1 ELSE 0 END) AS overdue_pending_count
|
|
2514
|
+
FROM chat_persistence_jobs
|
|
2439
2515
|
`).get(now);
|
|
2440
|
-
const oldest = this.db.prepare(`
|
|
2441
|
-
SELECT MIN(queued_at) AS oldest_pending_queued_at
|
|
2442
|
-
FROM chat_persistence_jobs
|
|
2443
|
-
WHERE status = 'pending'
|
|
2516
|
+
const oldest = this.db.prepare(`
|
|
2517
|
+
SELECT MIN(queued_at) AS oldest_pending_queued_at
|
|
2518
|
+
FROM chat_persistence_jobs
|
|
2519
|
+
WHERE status = 'pending'
|
|
2444
2520
|
`).get();
|
|
2445
2521
|
return {
|
|
2446
2522
|
pending_count: counts?.pending_count ?? 0,
|
|
@@ -2453,9 +2529,9 @@ export class DashboardDB {
|
|
|
2453
2529
|
}
|
|
2454
2530
|
// --- Logs ---
|
|
2455
2531
|
insertLog(entry) {
|
|
2456
|
-
this.stmt('insertLog', `
|
|
2457
|
-
INSERT INTO logs (ts, level, operation_name, operation_id, module, message)
|
|
2458
|
-
VALUES (@ts, @level, @operation_name, @operation_id, @module, @message)
|
|
2532
|
+
this.stmt('insertLog', `
|
|
2533
|
+
INSERT INTO logs (ts, level, operation_name, operation_id, module, message)
|
|
2534
|
+
VALUES (@ts, @level, @operation_name, @operation_id, @module, @message)
|
|
2459
2535
|
`).run({
|
|
2460
2536
|
ts: entry.ts,
|
|
2461
2537
|
level: entry.level,
|
|
@@ -2510,9 +2586,9 @@ export class DashboardDB {
|
|
|
2510
2586
|
}
|
|
2511
2587
|
// --- Query history ---
|
|
2512
2588
|
insertQueryHistory(entry) {
|
|
2513
|
-
this.stmt('insertQueryHistory', `
|
|
2514
|
-
INSERT INTO query_history (ts, sparql, duration_ms, result_count, error)
|
|
2515
|
-
VALUES (@ts, @sparql, @duration_ms, @result_count, @error)
|
|
2589
|
+
this.stmt('insertQueryHistory', `
|
|
2590
|
+
INSERT INTO query_history (ts, sparql, duration_ms, result_count, error)
|
|
2591
|
+
VALUES (@ts, @sparql, @duration_ms, @result_count, @error)
|
|
2516
2592
|
`).run({
|
|
2517
2593
|
ts: Date.now(),
|
|
2518
2594
|
sparql: entry.sparql,
|
|
@@ -2556,9 +2632,9 @@ export class DashboardDB {
|
|
|
2556
2632
|
}
|
|
2557
2633
|
// --- Notifications ---
|
|
2558
2634
|
insertNotification(n) {
|
|
2559
|
-
const result = this.stmt('insertNotif', `
|
|
2560
|
-
INSERT INTO notifications (ts, type, title, message, source, peer, read, meta, context_graph_id)
|
|
2561
|
-
VALUES (@ts, @type, @title, @message, @source, @peer, 0, @meta, @contextGraphId)
|
|
2635
|
+
const result = this.stmt('insertNotif', `
|
|
2636
|
+
INSERT INTO notifications (ts, type, title, message, source, peer, read, meta, context_graph_id)
|
|
2637
|
+
VALUES (@ts, @type, @title, @message, @source, @peer, 0, @meta, @contextGraphId)
|
|
2562
2638
|
`).run({
|
|
2563
2639
|
ts: n.ts,
|
|
2564
2640
|
type: n.type,
|
|
@@ -2594,8 +2670,8 @@ export class DashboardDB {
|
|
|
2594
2670
|
if (cgIds.length === 0)
|
|
2595
2671
|
return [];
|
|
2596
2672
|
const placeholders = cgIds.map(() => '?').join(',');
|
|
2597
|
-
return this.db.prepare(`SELECT * FROM notifications
|
|
2598
|
-
WHERE context_graph_id IN (${placeholders})
|
|
2673
|
+
return this.db.prepare(`SELECT * FROM notifications
|
|
2674
|
+
WHERE context_graph_id IN (${placeholders})
|
|
2599
2675
|
ORDER BY ts DESC LIMIT ?`).all(...cgIds, limit);
|
|
2600
2676
|
}
|
|
2601
2677
|
/**
|
|
@@ -2626,8 +2702,8 @@ export class DashboardDB {
|
|
|
2626
2702
|
if (types.length === 0)
|
|
2627
2703
|
return [];
|
|
2628
2704
|
const placeholders = types.map(() => '?').join(',');
|
|
2629
|
-
return this.db.prepare(`SELECT * FROM notifications
|
|
2630
|
-
WHERE type IN (${placeholders})
|
|
2705
|
+
return this.db.prepare(`SELECT * FROM notifications
|
|
2706
|
+
WHERE type IN (${placeholders})
|
|
2631
2707
|
ORDER BY ts DESC LIMIT ?`).all(...types, limit);
|
|
2632
2708
|
}
|
|
2633
2709
|
/**
|
|
@@ -2647,9 +2723,9 @@ export class DashboardDB {
|
|
|
2647
2723
|
const wallet = walletAddress.trim().toLowerCase();
|
|
2648
2724
|
if (!wallet)
|
|
2649
2725
|
return [];
|
|
2650
|
-
return this.db.prepare(`SELECT * FROM notifications
|
|
2651
|
-
WHERE type = 'pca_cost_covered'
|
|
2652
|
-
AND lower(json_extract(meta, '$.publisherAddress')) = ?
|
|
2726
|
+
return this.db.prepare(`SELECT * FROM notifications
|
|
2727
|
+
WHERE type = 'pca_cost_covered'
|
|
2728
|
+
AND lower(json_extract(meta, '$.publisherAddress')) = ?
|
|
2653
2729
|
ORDER BY ts DESC LIMIT ?`).all(wallet, limit);
|
|
2654
2730
|
}
|
|
2655
2731
|
markNotificationsRead(ids) {
|
|
@@ -2679,10 +2755,10 @@ export class DashboardDB {
|
|
|
2679
2755
|
return [];
|
|
2680
2756
|
const windowStart = parsed.windowBucket * ACTIVITY_DIGEST_WINDOW_MS;
|
|
2681
2757
|
const windowEnd = windowStart + ACTIVITY_DIGEST_WINDOW_MS;
|
|
2682
|
-
const rows = this.db.prepare(`SELECT id FROM notifications
|
|
2683
|
-
WHERE context_graph_id = ?
|
|
2684
|
-
AND type = ?
|
|
2685
|
-
AND ts >= ? AND ts < ?
|
|
2758
|
+
const rows = this.db.prepare(`SELECT id FROM notifications
|
|
2759
|
+
WHERE context_graph_id = ?
|
|
2760
|
+
AND type = ?
|
|
2761
|
+
AND ts >= ? AND ts < ?
|
|
2686
2762
|
AND json_extract(meta, '$.kind') = ?`).all(parsed.contextGraphId, ASSERTION_ACTIVITY_TYPE, windowStart, windowEnd, parsed.kind);
|
|
2687
2763
|
return rows.map((r) => r.id);
|
|
2688
2764
|
}
|
|
@@ -2691,7 +2767,6 @@ export class DashboardDB {
|
|
|
2691
2767
|
}
|
|
2692
2768
|
}
|
|
2693
2769
|
// --- Sync requester checkpoints (issue #1138 A3) ---
|
|
2694
|
-
const DEFAULT_SYNC_CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2695
2770
|
export class SqliteSyncCheckpointStore {
|
|
2696
2771
|
db;
|
|
2697
2772
|
clock;
|
|
@@ -2701,73 +2776,128 @@ export class SqliteSyncCheckpointStore {
|
|
|
2701
2776
|
this.clock = options.clock ?? (() => Date.now());
|
|
2702
2777
|
this.ttlMs = options.ttlMs ?? DEFAULT_SYNC_CHECKPOINT_TTL_MS;
|
|
2703
2778
|
}
|
|
2704
|
-
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2707
|
-
|
|
2779
|
+
readRow(key) {
|
|
2780
|
+
const row = this.db.prepare(`SELECT offset, updated_at, expires_at,
|
|
2781
|
+
responder_session_id, responder_session_expires_at, responder_session_offset,
|
|
2782
|
+
manifest_digest, manifest_prefix_digest, terminal
|
|
2708
2783
|
FROM sync_checkpoints WHERE key = ?`).get(key);
|
|
2709
2784
|
if (!row)
|
|
2710
2785
|
return undefined;
|
|
2711
|
-
if (row.expires_at < now) {
|
|
2712
|
-
this.delete(key);
|
|
2713
|
-
return undefined;
|
|
2714
|
-
}
|
|
2715
|
-
const hasFreshResponderSession = Boolean(row.responder_session_id)
|
|
2716
|
-
&& Number.isSafeInteger(row.responder_session_expires_at)
|
|
2717
|
-
&& (row.responder_session_expires_at ?? 0) > now;
|
|
2718
|
-
if (row.responder_session_id && !hasFreshResponderSession) {
|
|
2719
|
-
this.clearResponderSession(key);
|
|
2720
|
-
}
|
|
2721
2786
|
return {
|
|
2722
2787
|
offset: row.offset,
|
|
2723
2788
|
updatedAtMs: row.updated_at,
|
|
2724
2789
|
expiresAtMs: row.expires_at,
|
|
2725
|
-
...(
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
}
|
|
2790
|
+
...(row.terminal === 1 ? { terminal: true } : {}),
|
|
2791
|
+
...(row.manifest_digest ? { manifestDigest: row.manifest_digest } : {}),
|
|
2792
|
+
...(row.manifest_prefix_digest
|
|
2793
|
+
? { manifestPrefixDigest: row.manifest_prefix_digest }
|
|
2794
|
+
: {}),
|
|
2795
|
+
// Preserve each nullable session column independently so the shared
|
|
2796
|
+
// validator can distinguish an absent session from a torn/malformed
|
|
2797
|
+
// persisted session. Collapsing a partial row to no session fields would
|
|
2798
|
+
// turn corrupt durable state into an apparently valid ordinary offset.
|
|
2799
|
+
...(row.responder_session_id !== null
|
|
2800
|
+
? { responderSessionId: row.responder_session_id }
|
|
2801
|
+
: {}),
|
|
2802
|
+
...(row.responder_session_expires_at !== null
|
|
2803
|
+
? { responderSessionExpiresAtMs: row.responder_session_expires_at }
|
|
2804
|
+
: {}),
|
|
2805
|
+
...(row.responder_session_offset !== null
|
|
2806
|
+
? { responderSessionOffset: row.responder_session_offset }
|
|
2730
2807
|
: {}),
|
|
2731
2808
|
};
|
|
2732
2809
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2810
|
+
get(key, now = this.clock()) {
|
|
2811
|
+
const entry = this.readRow(key);
|
|
2812
|
+
if (!entry)
|
|
2813
|
+
return undefined;
|
|
2814
|
+
if (!isValidSyncCheckpointEntry(entry) || entry.expiresAtMs < now) {
|
|
2815
|
+
this.delete(key);
|
|
2816
|
+
return undefined;
|
|
2736
2817
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2818
|
+
if (entry.responderSessionId
|
|
2819
|
+
&& (entry.responderSessionExpiresAtMs ?? 0) <= now) {
|
|
2820
|
+
const withoutExpiredSession = withoutSyncCheckpointResponderSession(entry);
|
|
2821
|
+
this.writeEntry(key, withoutExpiredSession);
|
|
2822
|
+
return withoutExpiredSession;
|
|
2823
|
+
}
|
|
2824
|
+
return entry;
|
|
2825
|
+
}
|
|
2826
|
+
writeEntry(key, entry) {
|
|
2827
|
+
this.db.prepare(`
|
|
2828
|
+
INSERT INTO sync_checkpoints (
|
|
2829
|
+
key, offset, updated_at, expires_at,
|
|
2830
|
+
responder_session_id, responder_session_expires_at, responder_session_offset,
|
|
2831
|
+
manifest_digest, manifest_prefix_digest, terminal
|
|
2832
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2833
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
2834
|
+
offset = excluded.offset,
|
|
2835
|
+
updated_at = excluded.updated_at,
|
|
2836
|
+
expires_at = excluded.expires_at,
|
|
2837
|
+
responder_session_id = excluded.responder_session_id,
|
|
2838
|
+
responder_session_expires_at = excluded.responder_session_expires_at,
|
|
2839
|
+
responder_session_offset = excluded.responder_session_offset,
|
|
2840
|
+
manifest_digest = excluded.manifest_digest,
|
|
2841
|
+
manifest_prefix_digest = excluded.manifest_prefix_digest,
|
|
2842
|
+
terminal = excluded.terminal
|
|
2843
|
+
`).run(key, entry.offset, entry.updatedAtMs, entry.expiresAtMs, entry.responderSessionId ?? null, entry.responderSessionExpiresAtMs ?? null, entry.responderSessionOffset ?? null, entry.manifestDigest ?? null, entry.manifestPrefixDigest ?? null, entry.terminal ? 1 : 0);
|
|
2844
|
+
}
|
|
2845
|
+
set(key, value, nowMs = this.clock(), responderSessionOffset) {
|
|
2846
|
+
const transition = this.db.transaction(() => {
|
|
2847
|
+
this.writeEntry(key, transitionSyncCheckpointOffset({
|
|
2848
|
+
key,
|
|
2849
|
+
existing: this.get(key, nowMs),
|
|
2850
|
+
value,
|
|
2851
|
+
nowMs,
|
|
2852
|
+
ttlMs: this.ttlMs,
|
|
2853
|
+
responderSessionOffset,
|
|
2854
|
+
}));
|
|
2855
|
+
});
|
|
2856
|
+
transition();
|
|
2745
2857
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2858
|
+
setManifestBoundOffset(key, value, manifestDigest, nowMs = this.clock(), manifestPrefixDigest, terminal = false, responderSessionOffset) {
|
|
2859
|
+
const transition = this.db.transaction(() => {
|
|
2860
|
+
this.writeEntry(key, transitionSyncCheckpointManifestOffset({
|
|
2861
|
+
key,
|
|
2862
|
+
existing: this.get(key, nowMs),
|
|
2863
|
+
value,
|
|
2864
|
+
manifestDigest,
|
|
2865
|
+
nowMs,
|
|
2866
|
+
ttlMs: this.ttlMs,
|
|
2867
|
+
manifestPrefixDigest,
|
|
2868
|
+
terminal,
|
|
2869
|
+
responderSessionOffset,
|
|
2870
|
+
}));
|
|
2871
|
+
});
|
|
2872
|
+
transition();
|
|
2873
|
+
}
|
|
2874
|
+
setResponderSession(key, sessionId, expiresAtMs, nowMs = this.clock(), manifestDigest, manifestPrefixDigest, responderSessionOffset) {
|
|
2750
2875
|
if (expiresAtMs <= nowMs) {
|
|
2751
2876
|
this.clearResponderSession(key);
|
|
2752
2877
|
return;
|
|
2753
2878
|
}
|
|
2754
|
-
this.db.
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2879
|
+
const transition = this.db.transaction(() => {
|
|
2880
|
+
this.writeEntry(key, transitionSyncCheckpointResponderSession({
|
|
2881
|
+
key,
|
|
2882
|
+
existing: this.get(key, nowMs),
|
|
2883
|
+
sessionId,
|
|
2884
|
+
expiresAtMs,
|
|
2885
|
+
nowMs,
|
|
2886
|
+
ttlMs: this.ttlMs,
|
|
2887
|
+
manifestDigest,
|
|
2888
|
+
manifestPrefixDigest,
|
|
2889
|
+
responderSessionOffset,
|
|
2890
|
+
}));
|
|
2891
|
+
});
|
|
2892
|
+
transition();
|
|
2763
2893
|
}
|
|
2764
2894
|
clearResponderSession(key) {
|
|
2765
|
-
this.db.
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2895
|
+
const transition = this.db.transaction(() => {
|
|
2896
|
+
const existing = this.readRow(key);
|
|
2897
|
+
if (existing)
|
|
2898
|
+
this.writeEntry(key, withoutSyncCheckpointResponderSession(existing));
|
|
2899
|
+
});
|
|
2900
|
+
transition();
|
|
2771
2901
|
}
|
|
2772
2902
|
delete(key) {
|
|
2773
2903
|
this.db.prepare(`DELETE FROM sync_checkpoints WHERE key = ?`).run(key);
|
|
@@ -2800,13 +2930,13 @@ export class SqliteChangelogCursorStore {
|
|
|
2800
2930
|
if (!Number.isSafeInteger(seq) || seq < 0) {
|
|
2801
2931
|
throw new Error(`Invalid changelog cursor seq for ${peerId}/${contextGraphId}: ${seq}`);
|
|
2802
2932
|
}
|
|
2803
|
-
this.db.prepare(`
|
|
2804
|
-
INSERT INTO changelog_cursors (peer_id, context_graph_id, era, seq, updated_at)
|
|
2805
|
-
VALUES (?, ?, ?, ?, ?)
|
|
2806
|
-
ON CONFLICT(peer_id, context_graph_id) DO UPDATE SET
|
|
2807
|
-
era = excluded.era,
|
|
2808
|
-
seq = excluded.seq,
|
|
2809
|
-
updated_at = excluded.updated_at
|
|
2933
|
+
this.db.prepare(`
|
|
2934
|
+
INSERT INTO changelog_cursors (peer_id, context_graph_id, era, seq, updated_at)
|
|
2935
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2936
|
+
ON CONFLICT(peer_id, context_graph_id) DO UPDATE SET
|
|
2937
|
+
era = excluded.era,
|
|
2938
|
+
seq = excluded.seq,
|
|
2939
|
+
updated_at = excluded.updated_at
|
|
2810
2940
|
`).run(peerId, contextGraphId, era, seq, nowMs);
|
|
2811
2941
|
}
|
|
2812
2942
|
}
|
|
@@ -2834,13 +2964,13 @@ export class SqliteChangelogEraGuard {
|
|
|
2834
2964
|
if (!Number.isSafeInteger(highSeq) || highSeq < 0) {
|
|
2835
2965
|
throw new Error(`Invalid changelog era high_seq: ${highSeq}`);
|
|
2836
2966
|
}
|
|
2837
|
-
this.db.prepare(`
|
|
2838
|
-
INSERT INTO changelog_era (id, era, high_seq, updated_at)
|
|
2839
|
-
VALUES (1, ?, ?, ?)
|
|
2840
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
2841
|
-
era = excluded.era,
|
|
2842
|
-
high_seq = excluded.high_seq,
|
|
2843
|
-
updated_at = excluded.updated_at
|
|
2967
|
+
this.db.prepare(`
|
|
2968
|
+
INSERT INTO changelog_era (id, era, high_seq, updated_at)
|
|
2969
|
+
VALUES (1, ?, ?, ?)
|
|
2970
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2971
|
+
era = excluded.era,
|
|
2972
|
+
high_seq = excluded.high_seq,
|
|
2973
|
+
updated_at = excluded.updated_at
|
|
2844
2974
|
`).run(era, highSeq, this.clock());
|
|
2845
2975
|
}
|
|
2846
2976
|
}
|
|
@@ -2874,7 +3004,7 @@ export class SqliteMessageIdempotencyStore {
|
|
|
2874
3004
|
}
|
|
2875
3005
|
check(peer, protocol, messageId, direction) {
|
|
2876
3006
|
const row = this.db
|
|
2877
|
-
.prepare(`SELECT response_blob FROM message_idempotency
|
|
3007
|
+
.prepare(`SELECT response_blob FROM message_idempotency
|
|
2878
3008
|
WHERE peer_id = ? AND protocol = ? AND message_id = ? AND direction = ?`)
|
|
2879
3009
|
.get(peer, protocol, messageId, direction);
|
|
2880
3010
|
if (!row)
|
|
@@ -2904,9 +3034,9 @@ export class SqliteMessageIdempotencyStore {
|
|
|
2904
3034
|
// SqliteError so the substrate's bug doesn't disguise itself as
|
|
2905
3035
|
// a normal duplicate.
|
|
2906
3036
|
this.db
|
|
2907
|
-
.prepare(`INSERT INTO message_idempotency
|
|
2908
|
-
(peer_id, protocol, message_id, direction, response_blob, response_size, ts)
|
|
2909
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3037
|
+
.prepare(`INSERT INTO message_idempotency
|
|
3038
|
+
(peer_id, protocol, message_id, direction, response_blob, response_size, ts)
|
|
3039
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2910
3040
|
ON CONFLICT (peer_id, protocol, message_id, direction) DO NOTHING`)
|
|
2911
3041
|
.run(peer, protocol, messageId, direction, blob, responseSize, this.clock());
|
|
2912
3042
|
}
|
|
@@ -2931,15 +3061,15 @@ export class SqliteProtocolOutboxStore {
|
|
|
2931
3061
|
}
|
|
2932
3062
|
enqueue(peer, protocol, messageId, payload, error, now) {
|
|
2933
3063
|
const existing = this.db
|
|
2934
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
3064
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
2935
3065
|
WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
|
|
2936
3066
|
.get(peer, protocol, messageId);
|
|
2937
3067
|
if (existing) {
|
|
2938
3068
|
const newAttempts = existing.attempts + 1;
|
|
2939
3069
|
const nextAttemptAt = now + this.backoffFor(newAttempts);
|
|
2940
3070
|
this.db
|
|
2941
|
-
.prepare(`UPDATE protocol_outbox
|
|
2942
|
-
SET attempts = ?, last_attempt_at = ?, next_attempt_at = ?, last_error = ?
|
|
3071
|
+
.prepare(`UPDATE protocol_outbox
|
|
3072
|
+
SET attempts = ?, last_attempt_at = ?, next_attempt_at = ?, last_error = ?
|
|
2943
3073
|
WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
|
|
2944
3074
|
.run(newAttempts, now, nextAttemptAt, error, peer, protocol, messageId);
|
|
2945
3075
|
return {
|
|
@@ -2958,9 +3088,9 @@ export class SqliteProtocolOutboxStore {
|
|
|
2958
3088
|
const nextAttemptAt = now + this.backoffFor(attempts);
|
|
2959
3089
|
const blob = Buffer.from(payload);
|
|
2960
3090
|
this.db
|
|
2961
|
-
.prepare(`INSERT INTO protocol_outbox
|
|
2962
|
-
(peer_id, protocol, message_id, payload, attempts,
|
|
2963
|
-
first_failure_at, last_attempt_at, next_attempt_at, last_error)
|
|
3091
|
+
.prepare(`INSERT INTO protocol_outbox
|
|
3092
|
+
(peer_id, protocol, message_id, payload, attempts,
|
|
3093
|
+
first_failure_at, last_attempt_at, next_attempt_at, last_error)
|
|
2964
3094
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
2965
3095
|
.run(peer, protocol, messageId, blob, attempts, now, now, nextAttemptAt, error);
|
|
2966
3096
|
return {
|
|
@@ -2977,14 +3107,14 @@ export class SqliteProtocolOutboxStore {
|
|
|
2977
3107
|
}
|
|
2978
3108
|
markDelivered(peer, protocol, messageId) {
|
|
2979
3109
|
const result = this.db
|
|
2980
|
-
.prepare(`DELETE FROM protocol_outbox
|
|
3110
|
+
.prepare(`DELETE FROM protocol_outbox
|
|
2981
3111
|
WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
|
|
2982
3112
|
.run(peer, protocol, messageId);
|
|
2983
3113
|
return result.changes > 0;
|
|
2984
3114
|
}
|
|
2985
3115
|
hasEntry(peer, protocol, messageId) {
|
|
2986
3116
|
const row = this.db
|
|
2987
|
-
.prepare(`SELECT 1 FROM protocol_outbox
|
|
3117
|
+
.prepare(`SELECT 1 FROM protocol_outbox
|
|
2988
3118
|
WHERE peer_id = ? AND protocol = ? AND message_id = ? LIMIT 1`)
|
|
2989
3119
|
.get(peer, protocol, messageId);
|
|
2990
3120
|
return row !== undefined;
|
|
@@ -2994,27 +3124,27 @@ export class SqliteProtocolOutboxStore {
|
|
|
2994
3124
|
}
|
|
2995
3125
|
pendingFor(peer) {
|
|
2996
3126
|
const rows = this.db
|
|
2997
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
2998
|
-
WHERE peer_id = ?
|
|
3127
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
3128
|
+
WHERE peer_id = ?
|
|
2999
3129
|
ORDER BY first_failure_at ASC, protocol ASC, message_id ASC`)
|
|
3000
3130
|
.all(peer);
|
|
3001
3131
|
return rows.map(SqliteProtocolOutboxStore.rowToEntry);
|
|
3002
3132
|
}
|
|
3003
3133
|
due(now) {
|
|
3004
3134
|
const rows = this.db
|
|
3005
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
3006
|
-
WHERE next_attempt_at <= ?
|
|
3007
|
-
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
3135
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
3136
|
+
WHERE next_attempt_at <= ?
|
|
3137
|
+
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
3008
3138
|
peer_id ASC, protocol ASC, message_id ASC`)
|
|
3009
3139
|
.all(now);
|
|
3010
3140
|
return rows.map(SqliteProtocolOutboxStore.rowToEntry);
|
|
3011
3141
|
}
|
|
3012
3142
|
duePage(now, limit) {
|
|
3013
3143
|
const rows = this.db
|
|
3014
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
3015
|
-
WHERE next_attempt_at <= ?
|
|
3016
|
-
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
3017
|
-
peer_id ASC, protocol ASC, message_id ASC
|
|
3144
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
3145
|
+
WHERE next_attempt_at <= ?
|
|
3146
|
+
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
3147
|
+
peer_id ASC, protocol ASC, message_id ASC
|
|
3018
3148
|
LIMIT ?`)
|
|
3019
3149
|
.all(now, limit);
|
|
3020
3150
|
return rows.map(SqliteProtocolOutboxStore.rowToEntry);
|
|
@@ -3104,9 +3234,9 @@ export class SqliteKaNumberStore {
|
|
|
3104
3234
|
// `safeIntegers(true)` opts INTO bigint returns so the counter is
|
|
3105
3235
|
// exact past `Number.MAX_SAFE_INTEGER` (codex PR #976 F6).
|
|
3106
3236
|
const row = this.db
|
|
3107
|
-
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
3108
|
-
VALUES (?, 1)
|
|
3109
|
-
ON CONFLICT(author_address) DO UPDATE SET next_number = next_number + 1
|
|
3237
|
+
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
3238
|
+
VALUES (?, 1)
|
|
3239
|
+
ON CONFLICT(author_address) DO UPDATE SET next_number = next_number + 1
|
|
3110
3240
|
RETURNING next_number - 1 AS number`)
|
|
3111
3241
|
.safeIntegers(true)
|
|
3112
3242
|
.get(author);
|
|
@@ -3118,9 +3248,9 @@ export class SqliteKaNumberStore {
|
|
|
3118
3248
|
// `excluded.next_number` is the proposed floor from the VALUES row.
|
|
3119
3249
|
// `better-sqlite3` accepts bigint statement parameters natively.
|
|
3120
3250
|
this.db
|
|
3121
|
-
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
3122
|
-
VALUES (?, ?)
|
|
3123
|
-
ON CONFLICT(author_address) DO UPDATE SET
|
|
3251
|
+
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
3252
|
+
VALUES (?, ?)
|
|
3253
|
+
ON CONFLICT(author_address) DO UPDATE SET
|
|
3124
3254
|
next_number = MAX(next_number, excluded.next_number)`)
|
|
3125
3255
|
.run(author, nextNumberFloor);
|
|
3126
3256
|
}
|