@origintrail-official/dkg-node-ui 10.0.11 → 10.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +50 -50
  3. package/dist/chain-cursor-stores.js +6 -6
  4. package/dist/chat-memory.js +209 -209
  5. package/dist/db.js +955 -955
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/telemetry.d.ts +56 -0
  11. package/dist/telemetry.d.ts.map +1 -1
  12. package/dist/telemetry.js +96 -6
  13. package/dist/telemetry.js.map +1 -1
  14. package/dist-ui/assets/{3d-force-graph-Bl6v-uII.js → 3d-force-graph-C9KowW4u.js} +1 -1
  15. package/dist-ui/assets/{AgentHub-D2vaQxlw.js → AgentHub-BVrcom9_.js} +1 -1
  16. package/dist-ui/assets/{AgentProfilePage-BgSTdsjD.js → AgentProfilePage-C9XaTt7j.js} +1 -1
  17. package/dist-ui/assets/{ApproveWalletsModal-D0Ub_eiD.js → ApproveWalletsModal-BdR2mshx.js} +3 -3
  18. package/dist-ui/assets/{ConvictionDetailView-CieVypna.js → ConvictionDetailView-cao48GL1.js} +1 -1
  19. package/dist-ui/assets/{Network-DKw_XEGi.js → Network-B3jCYzr5.js} +1 -1
  20. package/dist-ui/assets/{OnChainProvenanceCard-Dn_IEP7Q.js → OnChainProvenanceCard-DboMX4Aq.js} +1 -1
  21. package/dist-ui/assets/{Operations-BO2C3UWB.js → Operations-Dg1gfLXv.js} +1 -1
  22. package/dist-ui/assets/{PublishingConviction-xKRNHbzk.js → PublishingConviction-Byp6t69e.js} +1 -1
  23. package/dist-ui/assets/{Settings-DC_d9STV.js → Settings-CxI0SKeV.js} +1 -1
  24. package/dist-ui/assets/{ccip-zWfeQL8Q.js → ccip-wJ3OMRuz.js} +1 -1
  25. package/dist-ui/assets/{index-CwRQnNYh.js → index-5DS6xjw0.js} +3 -3
  26. package/dist-ui/assets/index-DOjsv0r7.css +1 -0
  27. package/dist-ui/assets/index-KoAnx9JE.js +1661 -0
  28. package/dist-ui/assets/{jsonld-B67IUHNU-C3ydusi_.js → jsonld-32FQRO67-CDirPi8u.js} +2 -2
  29. package/dist-ui/assets/{jsonld-G-CM-8JR.js → jsonld-CwHW9UDN.js} +1 -1
  30. package/dist-ui/assets/{renderer-3d-OSESAPET-ConXqyK6.js → renderer-3d-2EVDZII7-BNXV6iSu.js} +2 -2
  31. package/dist-ui/assets/{shikiHighlighter-C6lRYd1O.js → shikiHighlighter-C-LWPPSu.js} +1 -1
  32. package/dist-ui/index.html +14 -14
  33. package/package.json +4 -3
  34. package/dist-ui/assets/index-DEtfq9qv.css +0 -1
  35. package/dist-ui/assets/index-hrI-Ir4l.js +0 -1661
  36. /package/dist-ui/assets/{ntriples-SF2LFNMC-DH7OofPp.js → ntriples-ZWBY2WET-DH7OofPp.js} +0 -0
  37. /package/dist-ui/assets/{turtle-BXN4L2BY-BzTu0283.js → turtle-JJPK7LJ5-BzTu0283.js} +0 -0
package/dist/db.js CHANGED
@@ -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
175
  const ensureSyncCheckpointResponderSessionColumns = () => {
176
- const table = this.db.prepare(`
177
- SELECT 1 AS found FROM sqlite_master
178
- WHERE type = 'table' AND name = 'sync_checkpoints'
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;
@@ -199,117 +199,117 @@ export class DashboardDB {
199
199
  return;
200
200
  }
201
201
  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
- );
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
+ );
292
292
  `);
293
293
  }
294
294
  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;
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;
313
313
  `);
314
314
  }
315
315
  if (version < 3) {
@@ -319,42 +319,42 @@ export class DashboardDB {
319
319
  // column + the partial unique index in one step; the V11 block
320
320
  // below idempotently adds the column to nodes that upgraded
321
321
  // 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);
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);
335
335
  `);
336
336
  }
337
337
  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);
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);
358
358
  `);
359
359
  }
360
360
  if (version < 5) {
@@ -363,77 +363,77 @@ export class DashboardDB {
363
363
  // fresh-install CREATE so new nodes get the column + its index in one
364
364
  // step; the V16 block below idempotently adds it to nodes that upgraded
365
365
  // 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);
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);
382
382
  `);
383
383
  }
384
384
  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
- );
385
+ this.db.exec(`
386
+ CREATE TABLE IF NOT EXISTS settings (
387
+ key TEXT PRIMARY KEY,
388
+ value TEXT NOT NULL
389
+ );
390
390
  `);
391
391
  }
392
392
  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);
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);
406
406
  `);
407
407
  }
408
408
  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);
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);
429
429
  `);
430
430
  }
431
431
  if (version < 9) {
432
432
  const columns = this.db.prepare('PRAGMA table_info(context_graph_subscriptions)').all();
433
433
  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;
434
+ this.db.exec(`
435
+ ALTER TABLE context_graph_subscriptions
436
+ ADD COLUMN shared_memory_synced INTEGER;
437
437
  `);
438
438
  }
439
439
  }
@@ -502,10 +502,10 @@ export class DashboardDB {
502
502
  if (!chatCols.has('message_id')) {
503
503
  this.db.exec(`ALTER TABLE chat_messages ADD COLUMN message_id TEXT;`);
504
504
  }
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;
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;
509
509
  `);
510
510
  }
511
511
  if (version < 12) {
@@ -561,33 +561,33 @@ export class DashboardDB {
561
561
  // `message_id` is enforced via `message_idempotency` instead.
562
562
  // V13 will also preserve `chat_messages.message_id` as
563
563
  // 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);
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);
591
591
  `);
592
592
  }
593
593
  if (version < 13) {
@@ -612,8 +612,8 @@ export class DashboardDB {
612
612
  // table rebuild on downgrade.
613
613
  //
614
614
  // No data migration. No new tables. Pure DROP INDEX.
615
- this.db.exec(`
616
- DROP INDEX IF EXISTS idx_chat_msgid;
615
+ this.db.exec(`
616
+ DROP INDEX IF EXISTS idx_chat_msgid;
617
617
  `);
618
618
  }
619
619
  if (version < 14) {
@@ -654,10 +654,10 @@ export class DashboardDB {
654
654
  // operation-correlated log views (`getOperation`,
655
655
  // `getFailedOperations`). Substring/text search moves to
656
656
  // /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;
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;
661
661
  `);
662
662
  // VACUUM cannot run inside a transaction; better-sqlite3 wraps
663
663
  // multi-statement exec() in implicit BEGIN/COMMIT, so we issue
@@ -721,23 +721,23 @@ export class DashboardDB {
721
721
  // re-parsing the daemon log. One row per decision point (sweep / fetch /
722
722
  // promote / already / defer / cursor-advance / core-fill). Pruned by `ts`
723
723
  // 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
- );
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
+ );
741
741
  `);
742
742
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_repl_ts ON replication_events(ts);`);
743
743
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_repl_cg ON replication_events(context_graph_id, ts);`);
@@ -776,54 +776,54 @@ export class DashboardDB {
776
776
  // the 2^63 INTEGER max and under the uint96 on-chain field. Like
777
777
  // `protocol_outbox`, this state is durable and is NEVER pruned —
778
778
  // 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
- );
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
+ );
784
784
  `);
785
785
  }
786
786
  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
- CREATE INDEX IF NOT EXISTS idx_sync_checkpoints_expires_at
795
- ON sync_checkpoints(expires_at);
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
+ CREATE INDEX IF NOT EXISTS idx_sync_checkpoints_expires_at
795
+ ON sync_checkpoints(expires_at);
796
796
  `);
797
797
  }
798
798
  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
- );
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
+ );
808
808
  `);
809
- this.db.exec(`
810
- CREATE INDEX IF NOT EXISTS idx_runtime_cursors_namespace_scope
811
- ON runtime_cursors(namespace, scope);
809
+ this.db.exec(`
810
+ CREATE INDEX IF NOT EXISTS idx_runtime_cursors_namespace_scope
811
+ ON runtime_cursors(namespace, scope);
812
812
  `);
813
813
  }
814
814
  if (version < 23) {
815
815
  // OT-RFC-59 SC5: durable per-(peer, contextGraph) changelog cursor. NEVER
816
816
  // pruned (see SqliteChangelogCursorStore) — a TTL would defeat the O(delta)
817
817
  // 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
- );
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
+ );
827
827
  `);
828
828
  }
829
829
  if (version < 24) {
@@ -832,39 +832,39 @@ export class DashboardDB {
832
832
  // `store.nq` restore does NOT roll back with the RDF store. node-ui.db
833
833
  // survives such a restore, so a rollback under the same era rotates the era
834
834
  // (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
- );
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
+ );
842
842
  `);
843
843
  }
844
844
  if (version < 25) {
845
845
  // Durable security audit for private-CG open enrollment. V25/V26 also
846
846
  // used this table as admission state; V27 projects those legacy rows into
847
847
  // 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);
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);
868
868
  `);
869
869
  }
870
870
  if (version < 26) {
@@ -883,108 +883,108 @@ export class DashboardDB {
883
883
  // Operational admission state is deliberately separate from the audit
884
884
  // stream. Audit retention and event naming must never alter rate-limit,
885
885
  // 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);
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);
917
917
  `);
918
918
  // Best-effort one-time projection for V25/V26 databases. Invalid legacy
919
919
  // JSON cannot abort startup; only rows with a typed numeric epoch can be
920
920
  // authoritative operational state. For duplicate audit reservations,
921
921
  // 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;
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;
988
988
  `);
989
989
  // V26's trigger protected live reservation audit rows because they were
990
990
  // operational state. The ledger makes that coupling obsolete, so replace
@@ -1002,21 +1002,21 @@ export class DashboardDB {
1002
1002
  // idempotent and keeps the audit bound fail-closed on every open.
1003
1003
  ensureJoinPolicyAuditCapTrigger();
1004
1004
  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);
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);
1020
1020
  `);
1021
1021
  }
1022
1022
  if (version < 30) {
@@ -1026,21 +1026,21 @@ export class DashboardDB {
1026
1026
  ensureSyncCheckpointResponderSessionColumns();
1027
1027
  }
1028
1028
  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
- );
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
+ );
1044
1044
  `);
1045
1045
  }
1046
1046
  this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
@@ -1080,10 +1080,10 @@ export class DashboardDB {
1080
1080
  // not depend on whether the corresponding audit rows still exist. Preserve
1081
1081
  // the historical retention envelope for durable commit idempotency while
1082
1082
  // 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 < ?)
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 < ?)
1087
1087
  `).run(cutoff, cutoff);
1088
1088
  this.db.prepare(`DELETE FROM sync_checkpoints WHERE expires_at < ?`).run(Date.now());
1089
1089
  // Reconcile negatives are accelerators, never historical records. Once the
@@ -1136,16 +1136,16 @@ export class DashboardDB {
1136
1136
  : 'done',
1137
1137
  };
1138
1138
  }
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
- )
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
+ )
1149
1149
  `).run({
1150
1150
  cutoff: overflowCutoff,
1151
1151
  batchRows: this.logVolumePruneBatchRows,
@@ -1171,12 +1171,12 @@ export class DashboardDB {
1171
1171
  };
1172
1172
  }
1173
1173
  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 ?
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 ?
1180
1180
  `).get(this.routineLogRowCap);
1181
1181
  return row?.id ?? null;
1182
1182
  }
@@ -1277,50 +1277,50 @@ export class DashboardDB {
1277
1277
  }
1278
1278
  // --- Metric snapshots ---
1279
1279
  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
- )
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
+ )
1298
1298
  `).run(snap);
1299
1299
  }
1300
1300
  getLatestSnapshot() {
1301
1301
  return this.db.prepare('SELECT * FROM metric_snapshots ORDER BY ts DESC LIMIT 1').get();
1302
1302
  }
1303
1303
  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
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
1324
1324
  `).run({
1325
1325
  context_graph_id: record.context_graph_id,
1326
1326
  name: record.name ?? null,
@@ -1418,37 +1418,37 @@ export class DashboardDB {
1418
1418
  transition();
1419
1419
  }
1420
1420
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1426
1426
  `).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
1427
  }
1428
1428
  reserveContextGraphAutomaticApproval(input) {
1429
1429
  const reserve = this.db.transaction(() => {
1430
1430
  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 >= ?
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 >= ?
1436
1436
  `).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 >= ?
1437
+ const nodeRow = this.stmt('countNodeAutomaticApprovalReservations', `
1438
+ SELECT COUNT(*) AS count
1439
+ FROM context_graph_join_approval_ledger
1440
+ WHERE reserved_at >= ?
1441
1441
  `).get(since);
1442
1442
  const contextGraphCount = Number(cgRow?.count ?? 0);
1443
1443
  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
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
1452
1452
  `).get(input.contextGraphId, input.requestDigest, input.policyEpoch, since);
1453
1453
  if (existing) {
1454
1454
  return {
@@ -1478,16 +1478,16 @@ export class DashboardDB {
1478
1478
  // reservation without durable operational state. An expired replay of
1479
1479
  // the same digest+epoch refreshes its reservation timestamp and consumes
1480
1480
  // 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
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
1491
1491
  `).run(input.contextGraphId, input.requestDigest, input.policyEpoch, input.timestamp, input.actor, input.agentAddress, input.policyVersion);
1492
1492
  this.appendContextGraphJoinPolicyAudit({
1493
1493
  timestamp: input.timestamp,
@@ -1513,26 +1513,26 @@ export class DashboardDB {
1513
1513
  return reserve();
1514
1514
  }
1515
1515
  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'
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'
1523
1523
  `).run(input.contextGraphId, input.requestDigest, input.policyEpoch);
1524
1524
  return marked.changes === 1;
1525
1525
  }
1526
1526
  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
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
1536
1536
  `).get(contextGraphId, requestDigest);
1537
1537
  return repair
1538
1538
  ? {
@@ -1544,26 +1544,26 @@ export class DashboardDB {
1544
1544
  }
1545
1545
  commitContextGraphAutomaticApproval(input) {
1546
1546
  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
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
1555
1555
  `).get(input.contextGraphId, input.requestDigest, input.policyEpoch);
1556
1556
  if (!reservation)
1557
1557
  return false;
1558
1558
  if (reservation.state === 'committed')
1559
1559
  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'
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'
1567
1567
  `).run(input.timestamp, reservation.context_graph_id, reservation.request_digest, reservation.policy_epoch);
1568
1568
  if (updated.changes !== 1)
1569
1569
  return false;
@@ -1587,48 +1587,48 @@ export class DashboardDB {
1587
1587
  }
1588
1588
  getContextGraphAutomaticApprovalUsage(contextGraphId, timestamp) {
1589
1589
  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 >= ?
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 >= ?
1595
1595
  `).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 >= ?
1596
+ const nodeApprovalsLastHour = Number(this.stmt('getNodeAutomaticApprovalUsage', `
1597
+ SELECT COUNT(*) AS count
1598
+ FROM context_graph_join_approval_ledger
1599
+ WHERE reserved_at >= ?
1600
1600
  `).get(since)?.count ?? 0);
1601
1601
  return { contextGraphApprovalsLastHour, nodeApprovalsLastHour };
1602
1602
  }
1603
1603
  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
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
1610
1610
  `).all(contextGraphId);
1611
1611
  }
1612
1612
  contextGraphJoinPolicyKey(contextGraphId) {
1613
1613
  return `contextGraphJoinPolicy:${contextGraphId}`;
1614
1614
  }
1615
1615
  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
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
1632
1632
  `).run(record);
1633
1633
  }
1634
1634
  getVmReconcileNegative(cacheKey) {
@@ -1647,14 +1647,14 @@ export class DashboardDB {
1647
1647
  * coerced defensively so a malformed event can never abort the insert.
1648
1648
  */
1649
1649
  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
- )
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
+ )
1658
1658
  `).run({
1659
1659
  ts: ev.ts,
1660
1660
  context_graph_id: ev.context_graph_id,
@@ -1701,15 +1701,15 @@ export class DashboardDB {
1701
1701
  // and inflate the reported latency. fetch/promote for one version always
1702
1702
  // share the same ordinal, so this pairs them exactly; a fast-path promote
1703
1703
  // 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
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
1713
1713
  `).all(cutoff, cutoff);
1714
1714
  const latencies = latencyRows
1715
1715
  .map((r) => (r.fetch_ts != null ? r.promote_ts - r.fetch_ts : 0))
@@ -1739,20 +1739,20 @@ export class DashboardDB {
1739
1739
  }
1740
1740
  _computeReplicationPerCg(periodMs) {
1741
1741
  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
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
1756
1756
  `).all(cutoff);
1757
1757
  }
1758
1758
  /**
@@ -1771,16 +1771,16 @@ export class DashboardDB {
1771
1771
  const cutoff = Date.now() - opts.periodMs;
1772
1772
  const bucket = Math.max(1, Math.floor(opts.bucketMs));
1773
1773
  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
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
1784
1784
  `).all({ cutoff, bucket, cg: opts.contextGraphId ?? null });
1785
1785
  }
1786
1786
  /**
@@ -1792,13 +1792,13 @@ export class DashboardDB {
1792
1792
  const safeLimit = Number.isFinite(limit)
1793
1793
  ? Math.max(1, Math.min(1000, Math.trunc(limit)))
1794
1794
  : 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 ?
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 ?
1802
1802
  `).all(contextGraphId, safeLimit);
1803
1803
  }
1804
1804
  /**
@@ -1807,38 +1807,38 @@ export class DashboardDB {
1807
1807
  * on-disk truth the Replication tab's cursor inspector renders.
1808
1808
  */
1809
1809
  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
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
1822
1822
  `).all();
1823
1823
  }
1824
1824
  upsertContextGraphMember(record) {
1825
1825
  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
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
1842
1842
  `).run({
1843
1843
  context_graph_id: record.context_graph_id,
1844
1844
  principal_type: record.principal_type,
@@ -1854,15 +1854,15 @@ export class DashboardDB {
1854
1854
  }
1855
1855
  listContextGraphMembers(contextGraphId) {
1856
1856
  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
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
1861
1861
  `).all(contextGraphId);
1862
1862
  }
1863
- return this.db.prepare(`
1864
- SELECT * FROM context_graph_memberships
1865
- ORDER BY context_graph_id ASC, principal_type ASC, principal_id ASC
1863
+ return this.db.prepare(`
1864
+ SELECT * FROM context_graph_memberships
1865
+ ORDER BY context_graph_id ASC, principal_type ASC, principal_id ASC
1866
1866
  `).all();
1867
1867
  }
1868
1868
  deleteContextGraphMember(contextGraphId, principalType, principalId) {
@@ -1874,18 +1874,18 @@ export class DashboardDB {
1874
1874
  return this.db.prepare('SELECT * FROM metric_snapshots WHERE ts >= ? AND ts <= ? ORDER BY ts').all(from, to);
1875
1875
  }
1876
1876
  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
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
1882
1882
  `).all(from, to, step);
1883
1883
  }
1884
1884
  // --- Operations ---
1885
1885
  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)
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)
1889
1889
  `).run({
1890
1890
  operation_id: op.operation_id,
1891
1891
  operation_name: op.operation_name,
@@ -1896,10 +1896,10 @@ export class DashboardDB {
1896
1896
  });
1897
1897
  }
1898
1898
  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'
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'
1903
1903
  `).run({
1904
1904
  operation_id: op.operation_id,
1905
1905
  duration_ms: op.duration_ms,
@@ -1914,10 +1914,10 @@ export class DashboardDB {
1914
1914
  this.finishOperation({ ...op, status: 'cancelled' });
1915
1915
  }
1916
1916
  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'
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'
1921
1921
  `).run(op);
1922
1922
  }
1923
1923
  getOperations(opts = {}) {
@@ -1974,22 +1974,22 @@ export class DashboardDB {
1974
1974
  }
1975
1975
  getErrorHotspots(periodMs = 7 * 86_400_000) {
1976
1976
  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
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
1993
1993
  `).all(cutoff, cutoff);
1994
1994
  }
1995
1995
  getFailedOperations(opts = {}) {
@@ -2011,17 +2011,17 @@ export class DashboardDB {
2011
2011
  params.push(like, like, like);
2012
2012
  }
2013
2013
  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 ?
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 ?
2025
2025
  `).all(...params);
2026
2026
  const operations = rows.map(row => {
2027
2027
  const logs = this.db.prepare('SELECT * FROM logs WHERE operation_id = ? ORDER BY ts DESC LIMIT 20').all(row.operation_id);
@@ -2038,15 +2038,15 @@ export class DashboardDB {
2038
2038
  }
2039
2039
  // --- Operation phases ---
2040
2040
  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')
2041
+ this.stmt('insertPhase', `
2042
+ INSERT INTO operation_phases (operation_id, phase, started_at, status)
2043
+ VALUES (@operation_id, @phase, @started_at, 'in_progress')
2044
2044
  `).run(op);
2045
2045
  }
2046
2046
  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'
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'
2050
2050
  `).run(op);
2051
2051
  }
2052
2052
  failPhase(op) {
@@ -2056,30 +2056,30 @@ export class DashboardDB {
2056
2056
  this.finishPhase({ ...op, status: 'cancelled' });
2057
2057
  }
2058
2058
  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'
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'
2063
2063
  `).run(op);
2064
2064
  }
2065
2065
  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'
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'
2070
2070
  `).run(op);
2071
2071
  }
2072
2072
  // --- Operation cost & tx ---
2073
2073
  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
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
2083
2083
  `).run({
2084
2084
  operation_id: op.operation_id,
2085
2085
  gas_used: op.gas_used ?? null,
@@ -2097,18 +2097,18 @@ export class DashboardDB {
2097
2097
  const params = [cutoff];
2098
2098
  if (opts.name)
2099
2099
  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}
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}
2112
2112
  `).get(...params);
2113
2113
  const summary = {
2114
2114
  totalCount: summaryRow.totalCount ?? 0,
@@ -2127,18 +2127,18 @@ export class DashboardDB {
2127
2127
  const tsParams = [bucketSize, cutoff];
2128
2128
  if (opts.name)
2129
2129
  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
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
2142
2142
  `).all(bucketSize, bucketSize, ...params);
2143
2143
  return {
2144
2144
  summary,
@@ -2155,17 +2155,17 @@ export class DashboardDB {
2155
2155
  // --- Per-type time series ---
2156
2156
  getPerTypeTimeSeries(opts) {
2157
2157
  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
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
2169
2169
  `).all(opts.bucketMs, opts.bucketMs, cutoff);
2170
2170
  const bucketSet = new Set();
2171
2171
  const typeSet = new Set();
@@ -2195,15 +2195,15 @@ export class DashboardDB {
2195
2195
  // --- Success rates by operation type ---
2196
2196
  getSuccessRatesByType(periodMs) {
2197
2197
  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
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
2207
2207
  `).all(cutoff).map(r => ({
2208
2208
  ...r,
2209
2209
  rate: r.success + r.error > 0 ? r.success / (r.success + r.error) : 0,
@@ -2226,16 +2226,16 @@ export class DashboardDB {
2226
2226
  // on-chain (Verifiable Memory commits) are counted, so the publish
2227
2227
  // count and the TRAC total are consistent. Free SWM/local/testnet
2228
2228
  // 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 >= ?
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 >= ?
2239
2239
  `).get(cutoff);
2240
2240
  results.periods.push({
2241
2241
  label: p.label,
@@ -2270,9 +2270,9 @@ export class DashboardDB {
2270
2270
  * still throw — pinned by db.test.ts.
2271
2271
  */
2272
2272
  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)
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)
2276
2276
  `).run({
2277
2277
  ts: msg.ts,
2278
2278
  direction: msg.direction,
@@ -2363,16 +2363,16 @@ export class DashboardDB {
2363
2363
  return this.db.prepare('SELECT * FROM chat_persistence_jobs WHERE turn_id = ?').get(turnId);
2364
2364
  }
2365
2365
  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
- )
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
+ )
2376
2376
  `).run({
2377
2377
  ...job,
2378
2378
  tool_calls_json: job.tool_calls_json ?? null,
@@ -2381,46 +2381,46 @@ export class DashboardDB {
2381
2381
  });
2382
2382
  }
2383
2383
  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 = ?
2384
+ this.stmt('markChatPersistenceInProgress', `
2385
+ UPDATE chat_persistence_jobs
2386
+ SET status = 'in_progress', attempts = ?, updated_at = ?, error_message = NULL
2387
+ WHERE turn_id = ?
2388
2388
  `).run(attempts, updatedAt, turnId);
2389
2389
  }
2390
2390
  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 = ?
2391
+ this.stmt('markChatPersistenceStored', `
2392
+ UPDATE chat_persistence_jobs
2393
+ SET status = 'stored', store_ms = ?, updated_at = ?, error_message = NULL
2394
+ WHERE turn_id = ?
2395
2395
  `).run(storeMs, updatedAt, turnId);
2396
2396
  }
2397
2397
  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 = ?
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 = ?
2402
2402
  `).run(attempts, nextAttemptAt, updatedAt, errorMessage, turnId);
2403
2403
  }
2404
2404
  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 = ?
2405
+ this.stmt('markChatPersistenceFailed', `
2406
+ UPDATE chat_persistence_jobs
2407
+ SET status = 'failed', attempts = ?, updated_at = ?, error_message = ?
2408
+ WHERE turn_id = ?
2409
2409
  `).run(attempts, updatedAt, errorMessage, turnId);
2410
2410
  }
2411
2411
  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'
2412
+ this.stmt('recoverInProgressChatPersistenceJobs', `
2413
+ UPDATE chat_persistence_jobs
2414
+ SET status = 'pending', next_attempt_at = ?, updated_at = ?
2415
+ WHERE status = 'in_progress'
2416
2416
  `).run(now, now);
2417
2417
  }
2418
2418
  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 ?
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 ?
2424
2424
  `).all(now, limit);
2425
2425
  }
2426
2426
  getNextPendingChatPersistenceAt() {
@@ -2428,19 +2428,19 @@ export class DashboardDB {
2428
2428
  return row?.next_at ?? null;
2429
2429
  }
2430
2430
  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
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
2439
2439
  `).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'
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'
2444
2444
  `).get();
2445
2445
  return {
2446
2446
  pending_count: counts?.pending_count ?? 0,
@@ -2453,9 +2453,9 @@ export class DashboardDB {
2453
2453
  }
2454
2454
  // --- Logs ---
2455
2455
  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)
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)
2459
2459
  `).run({
2460
2460
  ts: entry.ts,
2461
2461
  level: entry.level,
@@ -2510,9 +2510,9 @@ export class DashboardDB {
2510
2510
  }
2511
2511
  // --- Query history ---
2512
2512
  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)
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)
2516
2516
  `).run({
2517
2517
  ts: Date.now(),
2518
2518
  sparql: entry.sparql,
@@ -2556,9 +2556,9 @@ export class DashboardDB {
2556
2556
  }
2557
2557
  // --- Notifications ---
2558
2558
  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)
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)
2562
2562
  `).run({
2563
2563
  ts: n.ts,
2564
2564
  type: n.type,
@@ -2594,8 +2594,8 @@ export class DashboardDB {
2594
2594
  if (cgIds.length === 0)
2595
2595
  return [];
2596
2596
  const placeholders = cgIds.map(() => '?').join(',');
2597
- return this.db.prepare(`SELECT * FROM notifications
2598
- WHERE context_graph_id IN (${placeholders})
2597
+ return this.db.prepare(`SELECT * FROM notifications
2598
+ WHERE context_graph_id IN (${placeholders})
2599
2599
  ORDER BY ts DESC LIMIT ?`).all(...cgIds, limit);
2600
2600
  }
2601
2601
  /**
@@ -2626,8 +2626,8 @@ export class DashboardDB {
2626
2626
  if (types.length === 0)
2627
2627
  return [];
2628
2628
  const placeholders = types.map(() => '?').join(',');
2629
- return this.db.prepare(`SELECT * FROM notifications
2630
- WHERE type IN (${placeholders})
2629
+ return this.db.prepare(`SELECT * FROM notifications
2630
+ WHERE type IN (${placeholders})
2631
2631
  ORDER BY ts DESC LIMIT ?`).all(...types, limit);
2632
2632
  }
2633
2633
  /**
@@ -2647,9 +2647,9 @@ export class DashboardDB {
2647
2647
  const wallet = walletAddress.trim().toLowerCase();
2648
2648
  if (!wallet)
2649
2649
  return [];
2650
- return this.db.prepare(`SELECT * FROM notifications
2651
- WHERE type = 'pca_cost_covered'
2652
- AND lower(json_extract(meta, '$.publisherAddress')) = ?
2650
+ return this.db.prepare(`SELECT * FROM notifications
2651
+ WHERE type = 'pca_cost_covered'
2652
+ AND lower(json_extract(meta, '$.publisherAddress')) = ?
2653
2653
  ORDER BY ts DESC LIMIT ?`).all(wallet, limit);
2654
2654
  }
2655
2655
  markNotificationsRead(ids) {
@@ -2679,10 +2679,10 @@ export class DashboardDB {
2679
2679
  return [];
2680
2680
  const windowStart = parsed.windowBucket * ACTIVITY_DIGEST_WINDOW_MS;
2681
2681
  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 < ?
2682
+ const rows = this.db.prepare(`SELECT id FROM notifications
2683
+ WHERE context_graph_id = ?
2684
+ AND type = ?
2685
+ AND ts >= ? AND ts < ?
2686
2686
  AND json_extract(meta, '$.kind') = ?`).all(parsed.contextGraphId, ASSERTION_ACTIVITY_TYPE, windowStart, windowEnd, parsed.kind);
2687
2687
  return rows.map((r) => r.id);
2688
2688
  }
@@ -2703,8 +2703,8 @@ export class SqliteSyncCheckpointStore {
2703
2703
  }
2704
2704
  get(key) {
2705
2705
  const now = this.clock();
2706
- const row = this.db.prepare(`SELECT offset, updated_at, expires_at,
2707
- responder_session_id, responder_session_expires_at
2706
+ const row = this.db.prepare(`SELECT offset, updated_at, expires_at,
2707
+ responder_session_id, responder_session_expires_at
2708
2708
  FROM sync_checkpoints WHERE key = ?`).get(key);
2709
2709
  if (!row)
2710
2710
  return undefined;
@@ -2734,13 +2734,13 @@ export class SqliteSyncCheckpointStore {
2734
2734
  if (!Number.isSafeInteger(value) || value < 0) {
2735
2735
  throw new Error(`Invalid sync checkpoint offset for ${key}: ${value}`);
2736
2736
  }
2737
- this.db.prepare(`
2738
- INSERT INTO sync_checkpoints (key, offset, updated_at, expires_at)
2739
- VALUES (?, ?, ?, ?)
2740
- ON CONFLICT(key) DO UPDATE SET
2741
- offset = excluded.offset,
2742
- updated_at = excluded.updated_at,
2743
- expires_at = excluded.expires_at
2737
+ this.db.prepare(`
2738
+ INSERT INTO sync_checkpoints (key, offset, updated_at, expires_at)
2739
+ VALUES (?, ?, ?, ?)
2740
+ ON CONFLICT(key) DO UPDATE SET
2741
+ offset = excluded.offset,
2742
+ updated_at = excluded.updated_at,
2743
+ expires_at = excluded.expires_at
2744
2744
  `).run(key, value, nowMs, nowMs + this.ttlMs);
2745
2745
  }
2746
2746
  setResponderSession(key, sessionId, expiresAtMs, nowMs = this.clock()) {
@@ -2751,22 +2751,22 @@ export class SqliteSyncCheckpointStore {
2751
2751
  this.clearResponderSession(key);
2752
2752
  return;
2753
2753
  }
2754
- this.db.prepare(`
2755
- INSERT INTO sync_checkpoints (
2756
- key, offset, updated_at, expires_at,
2757
- responder_session_id, responder_session_expires_at
2758
- ) VALUES (?, 0, ?, ?, ?, ?)
2759
- ON CONFLICT(key) DO UPDATE SET
2760
- responder_session_id = excluded.responder_session_id,
2761
- responder_session_expires_at = excluded.responder_session_expires_at
2754
+ this.db.prepare(`
2755
+ INSERT INTO sync_checkpoints (
2756
+ key, offset, updated_at, expires_at,
2757
+ responder_session_id, responder_session_expires_at
2758
+ ) VALUES (?, 0, ?, ?, ?, ?)
2759
+ ON CONFLICT(key) DO UPDATE SET
2760
+ responder_session_id = excluded.responder_session_id,
2761
+ responder_session_expires_at = excluded.responder_session_expires_at
2762
2762
  `).run(key, nowMs, nowMs + this.ttlMs, sessionId, expiresAtMs);
2763
2763
  }
2764
2764
  clearResponderSession(key) {
2765
- this.db.prepare(`
2766
- UPDATE sync_checkpoints
2767
- SET responder_session_id = NULL,
2768
- responder_session_expires_at = NULL
2769
- WHERE key = ?
2765
+ this.db.prepare(`
2766
+ UPDATE sync_checkpoints
2767
+ SET responder_session_id = NULL,
2768
+ responder_session_expires_at = NULL
2769
+ WHERE key = ?
2770
2770
  `).run(key);
2771
2771
  }
2772
2772
  delete(key) {
@@ -2800,13 +2800,13 @@ export class SqliteChangelogCursorStore {
2800
2800
  if (!Number.isSafeInteger(seq) || seq < 0) {
2801
2801
  throw new Error(`Invalid changelog cursor seq for ${peerId}/${contextGraphId}: ${seq}`);
2802
2802
  }
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
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
2810
2810
  `).run(peerId, contextGraphId, era, seq, nowMs);
2811
2811
  }
2812
2812
  }
@@ -2834,13 +2834,13 @@ export class SqliteChangelogEraGuard {
2834
2834
  if (!Number.isSafeInteger(highSeq) || highSeq < 0) {
2835
2835
  throw new Error(`Invalid changelog era high_seq: ${highSeq}`);
2836
2836
  }
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
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
2844
2844
  `).run(era, highSeq, this.clock());
2845
2845
  }
2846
2846
  }
@@ -2874,7 +2874,7 @@ export class SqliteMessageIdempotencyStore {
2874
2874
  }
2875
2875
  check(peer, protocol, messageId, direction) {
2876
2876
  const row = this.db
2877
- .prepare(`SELECT response_blob FROM message_idempotency
2877
+ .prepare(`SELECT response_blob FROM message_idempotency
2878
2878
  WHERE peer_id = ? AND protocol = ? AND message_id = ? AND direction = ?`)
2879
2879
  .get(peer, protocol, messageId, direction);
2880
2880
  if (!row)
@@ -2904,9 +2904,9 @@ export class SqliteMessageIdempotencyStore {
2904
2904
  // SqliteError so the substrate's bug doesn't disguise itself as
2905
2905
  // a normal duplicate.
2906
2906
  this.db
2907
- .prepare(`INSERT INTO message_idempotency
2908
- (peer_id, protocol, message_id, direction, response_blob, response_size, ts)
2909
- VALUES (?, ?, ?, ?, ?, ?, ?)
2907
+ .prepare(`INSERT INTO message_idempotency
2908
+ (peer_id, protocol, message_id, direction, response_blob, response_size, ts)
2909
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2910
2910
  ON CONFLICT (peer_id, protocol, message_id, direction) DO NOTHING`)
2911
2911
  .run(peer, protocol, messageId, direction, blob, responseSize, this.clock());
2912
2912
  }
@@ -2931,15 +2931,15 @@ export class SqliteProtocolOutboxStore {
2931
2931
  }
2932
2932
  enqueue(peer, protocol, messageId, payload, error, now) {
2933
2933
  const existing = this.db
2934
- .prepare(`SELECT * FROM protocol_outbox
2934
+ .prepare(`SELECT * FROM protocol_outbox
2935
2935
  WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
2936
2936
  .get(peer, protocol, messageId);
2937
2937
  if (existing) {
2938
2938
  const newAttempts = existing.attempts + 1;
2939
2939
  const nextAttemptAt = now + this.backoffFor(newAttempts);
2940
2940
  this.db
2941
- .prepare(`UPDATE protocol_outbox
2942
- SET attempts = ?, last_attempt_at = ?, next_attempt_at = ?, last_error = ?
2941
+ .prepare(`UPDATE protocol_outbox
2942
+ SET attempts = ?, last_attempt_at = ?, next_attempt_at = ?, last_error = ?
2943
2943
  WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
2944
2944
  .run(newAttempts, now, nextAttemptAt, error, peer, protocol, messageId);
2945
2945
  return {
@@ -2958,9 +2958,9 @@ export class SqliteProtocolOutboxStore {
2958
2958
  const nextAttemptAt = now + this.backoffFor(attempts);
2959
2959
  const blob = Buffer.from(payload);
2960
2960
  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)
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)
2964
2964
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
2965
2965
  .run(peer, protocol, messageId, blob, attempts, now, now, nextAttemptAt, error);
2966
2966
  return {
@@ -2977,14 +2977,14 @@ export class SqliteProtocolOutboxStore {
2977
2977
  }
2978
2978
  markDelivered(peer, protocol, messageId) {
2979
2979
  const result = this.db
2980
- .prepare(`DELETE FROM protocol_outbox
2980
+ .prepare(`DELETE FROM protocol_outbox
2981
2981
  WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
2982
2982
  .run(peer, protocol, messageId);
2983
2983
  return result.changes > 0;
2984
2984
  }
2985
2985
  hasEntry(peer, protocol, messageId) {
2986
2986
  const row = this.db
2987
- .prepare(`SELECT 1 FROM protocol_outbox
2987
+ .prepare(`SELECT 1 FROM protocol_outbox
2988
2988
  WHERE peer_id = ? AND protocol = ? AND message_id = ? LIMIT 1`)
2989
2989
  .get(peer, protocol, messageId);
2990
2990
  return row !== undefined;
@@ -2994,27 +2994,27 @@ export class SqliteProtocolOutboxStore {
2994
2994
  }
2995
2995
  pendingFor(peer) {
2996
2996
  const rows = this.db
2997
- .prepare(`SELECT * FROM protocol_outbox
2998
- WHERE peer_id = ?
2997
+ .prepare(`SELECT * FROM protocol_outbox
2998
+ WHERE peer_id = ?
2999
2999
  ORDER BY first_failure_at ASC, protocol ASC, message_id ASC`)
3000
3000
  .all(peer);
3001
3001
  return rows.map(SqliteProtocolOutboxStore.rowToEntry);
3002
3002
  }
3003
3003
  due(now) {
3004
3004
  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,
3005
+ .prepare(`SELECT * FROM protocol_outbox
3006
+ WHERE next_attempt_at <= ?
3007
+ ORDER BY next_attempt_at ASC, first_failure_at ASC,
3008
3008
  peer_id ASC, protocol ASC, message_id ASC`)
3009
3009
  .all(now);
3010
3010
  return rows.map(SqliteProtocolOutboxStore.rowToEntry);
3011
3011
  }
3012
3012
  duePage(now, limit) {
3013
3013
  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
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
3018
3018
  LIMIT ?`)
3019
3019
  .all(now, limit);
3020
3020
  return rows.map(SqliteProtocolOutboxStore.rowToEntry);
@@ -3104,9 +3104,9 @@ export class SqliteKaNumberStore {
3104
3104
  // `safeIntegers(true)` opts INTO bigint returns so the counter is
3105
3105
  // exact past `Number.MAX_SAFE_INTEGER` (codex PR #976 F6).
3106
3106
  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
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
3110
3110
  RETURNING next_number - 1 AS number`)
3111
3111
  .safeIntegers(true)
3112
3112
  .get(author);
@@ -3118,9 +3118,9 @@ export class SqliteKaNumberStore {
3118
3118
  // `excluded.next_number` is the proposed floor from the VALUES row.
3119
3119
  // `better-sqlite3` accepts bigint statement parameters natively.
3120
3120
  this.db
3121
- .prepare(`INSERT INTO ka_numbers (author_address, next_number)
3122
- VALUES (?, ?)
3123
- ON CONFLICT(author_address) DO UPDATE SET
3121
+ .prepare(`INSERT INTO ka_numbers (author_address, next_number)
3122
+ VALUES (?, ?)
3123
+ ON CONFLICT(author_address) DO UPDATE SET
3124
3124
  next_number = MAX(next_number, excluded.next_number)`)
3125
3125
  .run(author, nextNumberFloor);
3126
3126
  }