@origintrail-official/dkg-node-ui 10.0.6 → 10.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +50 -50
- package/dist/chain-cursor-stores.js +6 -6
- package/dist/chat-memory.js +209 -209
- package/dist/db.d.ts +127 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1356 -675
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist-ui/assets/{3d-force-graph-Beyt80PS.js → 3d-force-graph-BOxtBjdk.js} +1 -1
- package/dist-ui/assets/{AgentHub-BGlM0TkX.js → AgentHub-BwBMYiaM.js} +1 -1
- package/dist-ui/assets/{AgentProfilePage-BK2nv-IH.js → AgentProfilePage-Bufz0Md_.js} +1 -1
- package/dist-ui/assets/{ApproveWalletsModal-CZrl51Kn.js → ApproveWalletsModal-B8bQTpiR.js} +3 -3
- package/dist-ui/assets/{ConvictionDetailView-CP2aS_DI.js → ConvictionDetailView-CiwcbT2W.js} +1 -1
- package/dist-ui/assets/{Network-DiXYe0aB.js → Network-C8AvmIfL.js} +1 -1
- package/dist-ui/assets/{OnChainProvenanceCard-BykYhleb.js → OnChainProvenanceCard-Dga51ZJL.js} +1 -1
- package/dist-ui/assets/{Operations-Cgg0jP9m.js → Operations-BhRo3Qzg.js} +1 -1
- package/dist-ui/assets/{PublishingConviction-DGtPr9Cs.js → PublishingConviction-DzvXFq9z.js} +1 -1
- package/dist-ui/assets/{Settings-dSId2mSF.js → Settings-CwO6V6er.js} +1 -1
- package/dist-ui/assets/{ccip-DUvYIt92.js → ccip-BxPhmAn8.js} +1 -1
- package/dist-ui/assets/{index-DOIKXU1s.js → index-BTD4QwAm.js} +3 -3
- package/dist-ui/assets/index-Btc8omeP.js +1661 -0
- package/dist-ui/assets/{jsonld-32FQRO67-XkBT-pPz.js → jsonld-B67IUHNU-Xa0S85WB.js} +2 -2
- package/dist-ui/assets/{jsonld-BQ18nHYe.js → jsonld-DzfKKFxR.js} +1 -1
- package/dist-ui/assets/{renderer-3d-2EVDZII7-DlPF04Rl.js → renderer-3d-OSESAPET-xkehb8k9.js} +2 -2
- package/dist-ui/assets/{shikiHighlighter-BhEVT05C.js → shikiHighlighter-DRJC8bc5.js} +1 -1
- package/dist-ui/index.html +13 -13
- package/package.json +3 -3
- package/dist-ui/assets/index-DJH2KcPG.js +0 -1661
- /package/dist-ui/assets/{ntriples-ZWBY2WET-DH7OofPp.js → ntriples-SF2LFNMC-DH7OofPp.js} +0 -0
- /package/dist-ui/assets/{turtle-JJPK7LJ5-BzTu0283.js → turtle-BXN4L2BY-BzTu0283.js} +0 -0
package/dist/db.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { RESPONSE_CACHE_BYTES, } from '@origintrail-official/dkg-core';
|
|
3
|
+
import { RESPONSE_CACHE_BYTES, parseContextGraphJoinPolicyRecord, } from '@origintrail-official/dkg-core';
|
|
4
4
|
export { SqliteChainEventCursorStore, SqliteContextGraphRegistryScanCursorStore, } from './chain-cursor-stores.js';
|
|
5
|
-
const SCHEMA_VERSION =
|
|
5
|
+
const SCHEMA_VERSION = 30;
|
|
6
6
|
// Default operator retention. Lowered from 90 → 14 days on V15 (2026-05) after
|
|
7
7
|
// a production incident in which the `logs` table + its FTS5 shadow tables
|
|
8
8
|
// grew to ~9 GB on a 12-day-old node and corrupted the SQLite page (header
|
|
9
9
|
// hash mismatch on boot). 90 days had been chosen for "metrics history",
|
|
10
10
|
// but logs were the dominant grower (~1M rows/12d) and pruning was a no-op
|
|
11
|
-
// on any DB younger than 90 days. 14 days is still long enough for
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// on any DB younger than 90 days. 14 days is still long enough for a useful
|
|
12
|
+
// operator-driven post-mortem, but the mainnet sync storm proved that time
|
|
13
|
+
// retention alone cannot bound worst-case growth. Operators who want longer
|
|
14
14
|
// retention can override via `setRetentionDays()`; the setting is persisted
|
|
15
|
-
// in the `settings` table and re-read on next boot.
|
|
15
|
+
// in the `settings` table and re-read on next boot. Time alone is not a hard
|
|
16
|
+
// size bound during a log storm, so routine info/debug rows also have a count
|
|
17
|
+
// ceiling. Warning/error rows keep the full operator-selected time window.
|
|
16
18
|
const DEFAULT_RETENTION_DAYS = 14;
|
|
17
19
|
const LEGACY_IMPLICIT_RETENTION_DAYS = 90;
|
|
20
|
+
const DEFAULT_ROUTINE_LOG_ROW_CAP = 1_000_000;
|
|
21
|
+
const DEFAULT_LOG_VOLUME_PRUNE_BATCH_ROWS = 25_000;
|
|
18
22
|
const LOGS_VACUUM_DELETE_THRESHOLD = 10_000;
|
|
19
23
|
// SQLite reports reusable-but-not-yet-reclaimed pages via freelist_count.
|
|
20
24
|
// With the default 4 KiB page size this is roughly 4 MiB, large enough
|
|
@@ -100,10 +104,14 @@ export class DashboardDB {
|
|
|
100
104
|
dataDir;
|
|
101
105
|
retentionDays;
|
|
102
106
|
explicitRetentionDays;
|
|
107
|
+
routineLogRowCap;
|
|
108
|
+
logVolumePruneBatchRows;
|
|
103
109
|
constructor(opts) {
|
|
104
110
|
this.dataDir = opts.dataDir;
|
|
105
111
|
this.explicitRetentionDays = opts.retentionDays !== undefined;
|
|
106
112
|
this.retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
113
|
+
this.routineLogRowCap = Math.max(0, Math.floor(opts.routineLogRowCap ?? DEFAULT_ROUTINE_LOG_ROW_CAP));
|
|
114
|
+
this.logVolumePruneBatchRows = Math.max(1, Math.floor(opts.logVolumePruneBatchRows ?? DEFAULT_LOG_VOLUME_PRUNE_BATCH_ROWS));
|
|
107
115
|
this._memoTtlMs = resolveCacheTtlMs(opts.cacheTtlMs);
|
|
108
116
|
const dbPath = join(opts.dataDir, 'node-ui.db');
|
|
109
117
|
this.db = new Database(dbPath);
|
|
@@ -130,120 +138,178 @@ export class DashboardDB {
|
|
|
130
138
|
migrate() {
|
|
131
139
|
const version = this.db.pragma('user_version', { simple: true });
|
|
132
140
|
const upgradedExistingDb = version > 0 && version < SCHEMA_VERSION;
|
|
133
|
-
|
|
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
|
+
`);
|
|
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'
|
|
157
|
+
`).get();
|
|
158
|
+
if (!table)
|
|
159
|
+
return;
|
|
160
|
+
const columns = this.db.pragma('table_info(context_graph_join_approval_ledger)');
|
|
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));
|
|
166
|
+
`);
|
|
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
|
+
);
|
|
173
|
+
`);
|
|
174
|
+
};
|
|
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'
|
|
179
|
+
`).get();
|
|
180
|
+
if (!table)
|
|
181
|
+
return;
|
|
182
|
+
const columns = new Set(this.db.prepare('PRAGMA table_info(sync_checkpoints)').all()
|
|
183
|
+
.map((column) => column.name));
|
|
184
|
+
if (!columns.has('responder_session_id')) {
|
|
185
|
+
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN responder_session_id TEXT;`);
|
|
186
|
+
}
|
|
187
|
+
if (!columns.has('responder_session_expires_at')) {
|
|
188
|
+
this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN responder_session_expires_at INTEGER;`);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
if (version > SCHEMA_VERSION)
|
|
192
|
+
return;
|
|
193
|
+
if (version === SCHEMA_VERSION) {
|
|
194
|
+
// Repair restored/development databases that carry the current version
|
|
195
|
+
// but lost an idempotent schema adjunct.
|
|
196
|
+
ensureJoinApprovalRepairMarker();
|
|
197
|
+
ensureSyncCheckpointResponderSessionColumns();
|
|
198
|
+
ensureJoinPolicyAuditCapTrigger();
|
|
134
199
|
return;
|
|
200
|
+
}
|
|
135
201
|
if (version < 1) {
|
|
136
|
-
this.db.exec(`
|
|
137
|
-
CREATE TABLE IF NOT EXISTS metric_snapshots (
|
|
138
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
-
ts INTEGER NOT NULL,
|
|
140
|
-
cpu_percent REAL,
|
|
141
|
-
mem_used_bytes INTEGER,
|
|
142
|
-
mem_total_bytes INTEGER,
|
|
143
|
-
disk_used_bytes INTEGER,
|
|
144
|
-
disk_total_bytes INTEGER,
|
|
145
|
-
heap_used_bytes INTEGER,
|
|
146
|
-
uptime_seconds INTEGER,
|
|
147
|
-
peer_count INTEGER,
|
|
148
|
-
direct_peers INTEGER,
|
|
149
|
-
relayed_peers INTEGER,
|
|
150
|
-
mesh_peers INTEGER,
|
|
151
|
-
contextGraph_count INTEGER,
|
|
152
|
-
total_triples INTEGER,
|
|
153
|
-
total_kcs INTEGER,
|
|
154
|
-
total_kas INTEGER,
|
|
155
|
-
store_bytes INTEGER,
|
|
156
|
-
confirmed_kcs INTEGER,
|
|
157
|
-
tentative_kcs INTEGER,
|
|
158
|
-
rpc_latency_ms INTEGER,
|
|
159
|
-
rpc_healthy INTEGER,
|
|
160
|
-
relay_capacity INTEGER,
|
|
161
|
-
relay_reservation_count INTEGER,
|
|
162
|
-
relay_active_circuits INTEGER,
|
|
163
|
-
relay_bytes_in INTEGER,
|
|
164
|
-
relay_bytes_out INTEGER
|
|
165
|
-
);
|
|
166
|
-
CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON metric_snapshots(ts);
|
|
167
|
-
|
|
168
|
-
CREATE TABLE IF NOT EXISTS operations (
|
|
169
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
170
|
-
operation_id TEXT NOT NULL,
|
|
171
|
-
operation_name TEXT NOT NULL,
|
|
172
|
-
started_at INTEGER NOT NULL,
|
|
173
|
-
duration_ms INTEGER,
|
|
174
|
-
status TEXT DEFAULT 'in_progress',
|
|
175
|
-
peer_id TEXT,
|
|
176
|
-
contextGraph_id TEXT,
|
|
177
|
-
triple_count INTEGER,
|
|
178
|
-
error_message TEXT,
|
|
179
|
-
details TEXT
|
|
180
|
-
);
|
|
181
|
-
CREATE INDEX IF NOT EXISTS idx_ops_operation_id ON operations(operation_id);
|
|
182
|
-
CREATE INDEX IF NOT EXISTS idx_ops_started_at ON operations(started_at);
|
|
183
|
-
CREATE INDEX IF NOT EXISTS idx_ops_name ON operations(operation_name);
|
|
184
|
-
|
|
185
|
-
CREATE TABLE IF NOT EXISTS logs (
|
|
186
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
187
|
-
ts INTEGER NOT NULL,
|
|
188
|
-
level TEXT NOT NULL,
|
|
189
|
-
operation_name TEXT,
|
|
190
|
-
operation_id TEXT,
|
|
191
|
-
module TEXT,
|
|
192
|
-
message TEXT NOT NULL
|
|
193
|
-
);
|
|
194
|
-
CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
|
|
195
|
-
CREATE INDEX IF NOT EXISTS idx_logs_operation_id ON logs(operation_id);
|
|
196
|
-
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
197
|
-
|
|
198
|
-
-- NOTE: Earlier schema versions (V1..V14) also created an FTS5
|
|
199
|
-
-- virtual table "logs_fts" plus AFTER INSERT/DELETE triggers to
|
|
200
|
-
-- keep it in sync with "logs". That fed a free-text search path
|
|
201
|
-
-- behind /api/logs?q=... The FTS5 index turned out to dominate
|
|
202
|
-
-- on-disk size (multi-GB shadow tables on long-lived nodes) and
|
|
203
|
-
-- the corresponding HTTP route had no production consumer
|
|
204
|
-
-- (the dashboard log viewer is file-backed via /api/node-log).
|
|
205
|
-
-- V15 drops it for fresh installs; the migration below cleans
|
|
206
|
-
-- it up for in-place upgrades.
|
|
207
|
-
|
|
208
|
-
CREATE TABLE IF NOT EXISTS query_history (
|
|
209
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
210
|
-
ts INTEGER NOT NULL,
|
|
211
|
-
sparql TEXT NOT NULL,
|
|
212
|
-
duration_ms INTEGER,
|
|
213
|
-
result_count INTEGER,
|
|
214
|
-
error TEXT
|
|
215
|
-
);
|
|
216
|
-
CREATE INDEX IF NOT EXISTS idx_qhist_ts ON query_history(ts);
|
|
217
|
-
|
|
218
|
-
CREATE TABLE IF NOT EXISTS saved_queries (
|
|
219
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
220
|
-
name TEXT NOT NULL,
|
|
221
|
-
description TEXT,
|
|
222
|
-
sparql TEXT NOT NULL,
|
|
223
|
-
created_at INTEGER NOT NULL,
|
|
224
|
-
updated_at INTEGER NOT NULL
|
|
225
|
-
);
|
|
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
|
+
);
|
|
226
292
|
`);
|
|
227
293
|
}
|
|
228
294
|
if (version < 2) {
|
|
229
|
-
this.db.exec(`
|
|
230
|
-
CREATE TABLE IF NOT EXISTS operation_phases (
|
|
231
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
232
|
-
operation_id TEXT NOT NULL,
|
|
233
|
-
phase TEXT NOT NULL,
|
|
234
|
-
started_at INTEGER NOT NULL,
|
|
235
|
-
duration_ms INTEGER,
|
|
236
|
-
status TEXT DEFAULT 'in_progress',
|
|
237
|
-
details TEXT
|
|
238
|
-
);
|
|
239
|
-
CREATE INDEX IF NOT EXISTS idx_phases_op ON operation_phases(operation_id);
|
|
240
|
-
|
|
241
|
-
ALTER TABLE operations ADD COLUMN gas_used INTEGER;
|
|
242
|
-
ALTER TABLE operations ADD COLUMN gas_price_gwei REAL;
|
|
243
|
-
ALTER TABLE operations ADD COLUMN gas_cost_eth REAL;
|
|
244
|
-
ALTER TABLE operations ADD COLUMN trac_cost REAL;
|
|
245
|
-
ALTER TABLE operations ADD COLUMN tx_hash TEXT;
|
|
246
|
-
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;
|
|
247
313
|
`);
|
|
248
314
|
}
|
|
249
315
|
if (version < 3) {
|
|
@@ -253,42 +319,42 @@ export class DashboardDB {
|
|
|
253
319
|
// column + the partial unique index in one step; the V11 block
|
|
254
320
|
// below idempotently adds the column to nodes that upgraded
|
|
255
321
|
// through versions 3..10 first.
|
|
256
|
-
this.db.exec(`
|
|
257
|
-
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
258
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
259
|
-
ts INTEGER NOT NULL,
|
|
260
|
-
direction TEXT NOT NULL,
|
|
261
|
-
peer TEXT NOT NULL,
|
|
262
|
-
peer_name TEXT,
|
|
263
|
-
text TEXT NOT NULL,
|
|
264
|
-
delivered INTEGER,
|
|
265
|
-
message_id TEXT
|
|
266
|
-
);
|
|
267
|
-
CREATE INDEX IF NOT EXISTS idx_chat_ts ON chat_messages(ts);
|
|
268
|
-
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);
|
|
269
335
|
`);
|
|
270
336
|
}
|
|
271
337
|
if (version < 4) {
|
|
272
|
-
this.db.exec(`
|
|
273
|
-
CREATE TABLE IF NOT EXISTS chat_persistence_jobs (
|
|
274
|
-
turn_id TEXT PRIMARY KEY,
|
|
275
|
-
session_id TEXT NOT NULL,
|
|
276
|
-
user_message TEXT NOT NULL,
|
|
277
|
-
assistant_reply TEXT NOT NULL,
|
|
278
|
-
tool_calls_json TEXT,
|
|
279
|
-
status TEXT NOT NULL,
|
|
280
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
281
|
-
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
282
|
-
next_attempt_at INTEGER NOT NULL,
|
|
283
|
-
queued_at INTEGER NOT NULL,
|
|
284
|
-
updated_at INTEGER NOT NULL,
|
|
285
|
-
store_ms INTEGER,
|
|
286
|
-
error_message TEXT
|
|
287
|
-
);
|
|
288
|
-
CREATE INDEX IF NOT EXISTS idx_chat_persist_status_next
|
|
289
|
-
ON chat_persistence_jobs(status, next_attempt_at);
|
|
290
|
-
CREATE INDEX IF NOT EXISTS idx_chat_persist_session
|
|
291
|
-
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);
|
|
292
358
|
`);
|
|
293
359
|
}
|
|
294
360
|
if (version < 5) {
|
|
@@ -297,77 +363,77 @@ export class DashboardDB {
|
|
|
297
363
|
// fresh-install CREATE so new nodes get the column + its index in one
|
|
298
364
|
// step; the V16 block below idempotently adds it to nodes that upgraded
|
|
299
365
|
// through versions 5..15 first (same pattern as `message_id` in V3/V11).
|
|
300
|
-
this.db.exec(`
|
|
301
|
-
CREATE TABLE IF NOT EXISTS notifications (
|
|
302
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
303
|
-
ts INTEGER NOT NULL,
|
|
304
|
-
type TEXT NOT NULL,
|
|
305
|
-
title TEXT NOT NULL,
|
|
306
|
-
message TEXT NOT NULL,
|
|
307
|
-
source TEXT,
|
|
308
|
-
peer TEXT,
|
|
309
|
-
read INTEGER NOT NULL DEFAULT 0,
|
|
310
|
-
meta TEXT,
|
|
311
|
-
context_graph_id TEXT
|
|
312
|
-
);
|
|
313
|
-
CREATE INDEX IF NOT EXISTS idx_notif_ts ON notifications(ts);
|
|
314
|
-
CREATE INDEX IF NOT EXISTS idx_notif_read ON notifications(read);
|
|
315
|
-
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);
|
|
316
382
|
`);
|
|
317
383
|
}
|
|
318
384
|
if (version < 6) {
|
|
319
|
-
this.db.exec(`
|
|
320
|
-
CREATE TABLE IF NOT EXISTS settings (
|
|
321
|
-
key TEXT PRIMARY KEY,
|
|
322
|
-
value TEXT NOT NULL
|
|
323
|
-
);
|
|
385
|
+
this.db.exec(`
|
|
386
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
387
|
+
key TEXT PRIMARY KEY,
|
|
388
|
+
value TEXT NOT NULL
|
|
389
|
+
);
|
|
324
390
|
`);
|
|
325
391
|
}
|
|
326
392
|
if (version < 7) {
|
|
327
|
-
this.db.exec(`
|
|
328
|
-
CREATE TABLE IF NOT EXISTS context_graph_subscriptions (
|
|
329
|
-
context_graph_id TEXT PRIMARY KEY,
|
|
330
|
-
name TEXT,
|
|
331
|
-
subscribed INTEGER NOT NULL,
|
|
332
|
-
synced INTEGER NOT NULL,
|
|
333
|
-
meta_synced INTEGER,
|
|
334
|
-
on_chain_id TEXT,
|
|
335
|
-
sync_scoped INTEGER NOT NULL DEFAULT 1,
|
|
336
|
-
updated_at INTEGER NOT NULL
|
|
337
|
-
);
|
|
338
|
-
CREATE INDEX IF NOT EXISTS idx_cg_subs_sync_scoped
|
|
339
|
-
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);
|
|
340
406
|
`);
|
|
341
407
|
}
|
|
342
408
|
if (version < 8) {
|
|
343
|
-
this.db.exec(`
|
|
344
|
-
CREATE TABLE IF NOT EXISTS context_graph_memberships (
|
|
345
|
-
context_graph_id TEXT NOT NULL,
|
|
346
|
-
principal_type TEXT NOT NULL,
|
|
347
|
-
principal_id TEXT NOT NULL,
|
|
348
|
-
role TEXT,
|
|
349
|
-
status TEXT NOT NULL,
|
|
350
|
-
source TEXT,
|
|
351
|
-
display_name TEXT,
|
|
352
|
-
metadata TEXT,
|
|
353
|
-
first_seen_at INTEGER NOT NULL,
|
|
354
|
-
updated_at INTEGER NOT NULL,
|
|
355
|
-
PRIMARY KEY (context_graph_id, principal_type, principal_id)
|
|
356
|
-
);
|
|
357
|
-
CREATE INDEX IF NOT EXISTS idx_cg_members_context
|
|
358
|
-
ON context_graph_memberships(context_graph_id);
|
|
359
|
-
CREATE INDEX IF NOT EXISTS idx_cg_members_principal
|
|
360
|
-
ON context_graph_memberships(principal_type, principal_id);
|
|
361
|
-
CREATE INDEX IF NOT EXISTS idx_cg_members_status
|
|
362
|
-
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);
|
|
363
429
|
`);
|
|
364
430
|
}
|
|
365
431
|
if (version < 9) {
|
|
366
432
|
const columns = this.db.prepare('PRAGMA table_info(context_graph_subscriptions)').all();
|
|
367
433
|
if (!columns.some((column) => column.name === 'shared_memory_synced')) {
|
|
368
|
-
this.db.exec(`
|
|
369
|
-
ALTER TABLE context_graph_subscriptions
|
|
370
|
-
ADD COLUMN shared_memory_synced INTEGER;
|
|
434
|
+
this.db.exec(`
|
|
435
|
+
ALTER TABLE context_graph_subscriptions
|
|
436
|
+
ADD COLUMN shared_memory_synced INTEGER;
|
|
371
437
|
`);
|
|
372
438
|
}
|
|
373
439
|
}
|
|
@@ -436,10 +502,10 @@ export class DashboardDB {
|
|
|
436
502
|
if (!chatCols.has('message_id')) {
|
|
437
503
|
this.db.exec(`ALTER TABLE chat_messages ADD COLUMN message_id TEXT;`);
|
|
438
504
|
}
|
|
439
|
-
this.db.exec(`
|
|
440
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_msgid
|
|
441
|
-
ON chat_messages(peer, direction, message_id)
|
|
442
|
-
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;
|
|
443
509
|
`);
|
|
444
510
|
}
|
|
445
511
|
if (version < 12) {
|
|
@@ -495,33 +561,33 @@ export class DashboardDB {
|
|
|
495
561
|
// `message_id` is enforced via `message_idempotency` instead.
|
|
496
562
|
// V13 will also preserve `chat_messages.message_id` as
|
|
497
563
|
// nullable + unwritten for rollback safety.
|
|
498
|
-
this.db.exec(`
|
|
499
|
-
CREATE TABLE IF NOT EXISTS message_idempotency (
|
|
500
|
-
peer_id TEXT NOT NULL,
|
|
501
|
-
protocol TEXT NOT NULL,
|
|
502
|
-
message_id TEXT NOT NULL,
|
|
503
|
-
direction TEXT NOT NULL CHECK (direction IN ('in', 'out')),
|
|
504
|
-
response_blob BLOB,
|
|
505
|
-
response_size INTEGER NOT NULL DEFAULT 0,
|
|
506
|
-
ts INTEGER NOT NULL,
|
|
507
|
-
PRIMARY KEY (peer_id, protocol, message_id, direction)
|
|
508
|
-
);
|
|
509
|
-
CREATE INDEX IF NOT EXISTS idx_idem_ts ON message_idempotency(ts);
|
|
510
|
-
|
|
511
|
-
CREATE TABLE IF NOT EXISTS protocol_outbox (
|
|
512
|
-
peer_id TEXT NOT NULL,
|
|
513
|
-
protocol TEXT NOT NULL,
|
|
514
|
-
message_id TEXT NOT NULL,
|
|
515
|
-
payload BLOB NOT NULL,
|
|
516
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
517
|
-
first_failure_at INTEGER NOT NULL,
|
|
518
|
-
last_attempt_at INTEGER NOT NULL,
|
|
519
|
-
next_attempt_at INTEGER NOT NULL,
|
|
520
|
-
last_error TEXT,
|
|
521
|
-
PRIMARY KEY (peer_id, protocol, message_id)
|
|
522
|
-
);
|
|
523
|
-
CREATE INDEX IF NOT EXISTS idx_outbox_next_attempt
|
|
524
|
-
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);
|
|
525
591
|
`);
|
|
526
592
|
}
|
|
527
593
|
if (version < 13) {
|
|
@@ -546,8 +612,8 @@ export class DashboardDB {
|
|
|
546
612
|
// table rebuild on downgrade.
|
|
547
613
|
//
|
|
548
614
|
// No data migration. No new tables. Pure DROP INDEX.
|
|
549
|
-
this.db.exec(`
|
|
550
|
-
DROP INDEX IF EXISTS idx_chat_msgid;
|
|
615
|
+
this.db.exec(`
|
|
616
|
+
DROP INDEX IF EXISTS idx_chat_msgid;
|
|
551
617
|
`);
|
|
552
618
|
}
|
|
553
619
|
if (version < 14) {
|
|
@@ -588,10 +654,10 @@ export class DashboardDB {
|
|
|
588
654
|
// operation-correlated log views (`getOperation`,
|
|
589
655
|
// `getFailedOperations`). Substring/text search moves to
|
|
590
656
|
// /api/node-log, which already supports `?q=`.
|
|
591
|
-
this.db.exec(`
|
|
592
|
-
DROP TRIGGER IF EXISTS logs_ai;
|
|
593
|
-
DROP TRIGGER IF EXISTS logs_ad;
|
|
594
|
-
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;
|
|
595
661
|
`);
|
|
596
662
|
// VACUUM cannot run inside a transaction; better-sqlite3 wraps
|
|
597
663
|
// multi-statement exec() in implicit BEGIN/COMMIT, so we issue
|
|
@@ -655,23 +721,23 @@ export class DashboardDB {
|
|
|
655
721
|
// re-parsing the daemon log. One row per decision point (sweep / fetch /
|
|
656
722
|
// promote / already / defer / cursor-advance / core-fill). Pruned by `ts`
|
|
657
723
|
// like the other event tables.
|
|
658
|
-
this.db.exec(`
|
|
659
|
-
CREATE TABLE IF NOT EXISTS replication_events (
|
|
660
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
661
|
-
ts INTEGER NOT NULL,
|
|
662
|
-
context_graph_id TEXT NOT NULL,
|
|
663
|
-
on_chain_cg_id TEXT,
|
|
664
|
-
action TEXT NOT NULL,
|
|
665
|
-
ual TEXT,
|
|
666
|
-
ordinal INTEGER,
|
|
667
|
-
ka_id TEXT,
|
|
668
|
-
from_watermark INTEGER,
|
|
669
|
-
to_watermark INTEGER,
|
|
670
|
-
head INTEGER,
|
|
671
|
-
reconciled INTEGER,
|
|
672
|
-
pending INTEGER,
|
|
673
|
-
detail TEXT
|
|
674
|
-
);
|
|
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
|
+
);
|
|
675
741
|
`);
|
|
676
742
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_repl_ts ON replication_events(ts);`);
|
|
677
743
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_repl_cg ON replication_events(context_graph_id, ts);`);
|
|
@@ -710,54 +776,54 @@ export class DashboardDB {
|
|
|
710
776
|
// the 2^63 INTEGER max and under the uint96 on-chain field. Like
|
|
711
777
|
// `protocol_outbox`, this state is durable and is NEVER pruned —
|
|
712
778
|
// reclaiming a number could mint a duplicate kaId.
|
|
713
|
-
this.db.exec(`
|
|
714
|
-
CREATE TABLE IF NOT EXISTS ka_numbers (
|
|
715
|
-
author_address TEXT PRIMARY KEY,
|
|
716
|
-
next_number INTEGER NOT NULL
|
|
717
|
-
);
|
|
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
|
+
);
|
|
718
784
|
`);
|
|
719
785
|
}
|
|
720
786
|
if (version < 21) {
|
|
721
|
-
this.db.exec(`
|
|
722
|
-
CREATE TABLE IF NOT EXISTS sync_checkpoints (
|
|
723
|
-
key TEXT PRIMARY KEY,
|
|
724
|
-
offset INTEGER NOT NULL CHECK (offset >= 0),
|
|
725
|
-
updated_at INTEGER NOT NULL,
|
|
726
|
-
expires_at INTEGER NOT NULL
|
|
727
|
-
);
|
|
728
|
-
CREATE INDEX IF NOT EXISTS idx_sync_checkpoints_expires_at
|
|
729
|
-
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);
|
|
730
796
|
`);
|
|
731
797
|
}
|
|
732
798
|
if (version < 22) {
|
|
733
|
-
this.db.exec(`
|
|
734
|
-
CREATE TABLE IF NOT EXISTS runtime_cursors (
|
|
735
|
-
namespace TEXT NOT NULL,
|
|
736
|
-
scope TEXT NOT NULL,
|
|
737
|
-
key TEXT NOT NULL,
|
|
738
|
-
value INTEGER NOT NULL CHECK (value > 0),
|
|
739
|
-
updated_at INTEGER NOT NULL,
|
|
740
|
-
PRIMARY KEY (namespace, scope, key)
|
|
741
|
-
);
|
|
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
|
+
);
|
|
742
808
|
`);
|
|
743
|
-
this.db.exec(`
|
|
744
|
-
CREATE INDEX IF NOT EXISTS idx_runtime_cursors_namespace_scope
|
|
745
|
-
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);
|
|
746
812
|
`);
|
|
747
813
|
}
|
|
748
814
|
if (version < 23) {
|
|
749
815
|
// OT-RFC-59 SC5: durable per-(peer, contextGraph) changelog cursor. NEVER
|
|
750
816
|
// pruned (see SqliteChangelogCursorStore) — a TTL would defeat the O(delta)
|
|
751
817
|
// cross-restart catch-up this table exists for.
|
|
752
|
-
this.db.exec(`
|
|
753
|
-
CREATE TABLE IF NOT EXISTS changelog_cursors (
|
|
754
|
-
peer_id TEXT NOT NULL,
|
|
755
|
-
context_graph_id TEXT NOT NULL,
|
|
756
|
-
era TEXT NOT NULL,
|
|
757
|
-
seq INTEGER NOT NULL CHECK (seq >= 0),
|
|
758
|
-
updated_at INTEGER NOT NULL,
|
|
759
|
-
PRIMARY KEY (peer_id, context_graph_id)
|
|
760
|
-
);
|
|
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
|
+
);
|
|
761
827
|
`);
|
|
762
828
|
}
|
|
763
829
|
if (version < 24) {
|
|
@@ -766,15 +832,199 @@ export class DashboardDB {
|
|
|
766
832
|
// `store.nq` restore does NOT roll back with the RDF store. node-ui.db
|
|
767
833
|
// survives such a restore, so a rollback under the same era rotates the era
|
|
768
834
|
// (forcing peers to full-resync instead of silently skipping). Single row.
|
|
769
|
-
this.db.exec(`
|
|
770
|
-
CREATE TABLE IF NOT EXISTS changelog_era (
|
|
771
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
772
|
-
era TEXT NOT NULL,
|
|
773
|
-
high_seq INTEGER NOT NULL CHECK (high_seq >= 0),
|
|
774
|
-
updated_at INTEGER NOT NULL
|
|
775
|
-
);
|
|
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
|
+
`);
|
|
843
|
+
}
|
|
844
|
+
if (version < 25) {
|
|
845
|
+
// Durable security audit for private-CG open enrollment. V25/V26 also
|
|
846
|
+
// used this table as admission state; V27 projects those legacy rows into
|
|
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);
|
|
776
868
|
`);
|
|
777
869
|
}
|
|
870
|
+
if (version < 26) {
|
|
871
|
+
// Bound flood-generated decision noise without letting that same flood
|
|
872
|
+
// erase the two records needed to reconstruct a private-CG admission:
|
|
873
|
+
// who changed the policy, and who was automatically admitted under it.
|
|
874
|
+
// V26 additionally retained live reservations because they still enforced
|
|
875
|
+
// the rolling one-hour ceilings. V27 removes that dependency below.
|
|
876
|
+
//
|
|
877
|
+
// V25 already installed this trigger, so DROP before CREATE is required:
|
|
878
|
+
// CREATE TRIGGER IF NOT EXISTS would silently preserve the old predicate
|
|
879
|
+
// on an upgraded database.
|
|
880
|
+
this.db.exec('DROP TRIGGER IF EXISTS cap_cg_join_policy_audit_rows;');
|
|
881
|
+
}
|
|
882
|
+
if (version < 27) {
|
|
883
|
+
// Operational admission state is deliberately separate from the audit
|
|
884
|
+
// stream. Audit retention and event naming must never alter rate-limit,
|
|
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);
|
|
917
|
+
`);
|
|
918
|
+
// Best-effort one-time projection for V25/V26 databases. Invalid legacy
|
|
919
|
+
// JSON cannot abort startup; only rows with a typed numeric epoch can be
|
|
920
|
+
// authoritative operational state. For duplicate audit reservations,
|
|
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;
|
|
988
|
+
`);
|
|
989
|
+
// V26's trigger protected live reservation audit rows because they were
|
|
990
|
+
// operational state. The ledger makes that coupling obsolete, so replace
|
|
991
|
+
// the trigger while preserving the flood-resistant proof exemptions.
|
|
992
|
+
this.db.exec('DROP TRIGGER IF EXISTS cap_cg_join_policy_audit_rows;');
|
|
993
|
+
}
|
|
994
|
+
if (version < 28) {
|
|
995
|
+
// A reservation alone does not prove that admission crossed its
|
|
996
|
+
// membership boundary. Persist that distinction so an exact retry can
|
|
997
|
+
// finish moderation/audit after delegation expiry or daemon restart.
|
|
998
|
+
ensureJoinApprovalRepairMarker();
|
|
999
|
+
}
|
|
1000
|
+
// Keep this repair outside the version gate. Restored/development DBs can
|
|
1001
|
+
// carry the current user_version while missing a trigger; recreating it is
|
|
1002
|
+
// idempotent and keeps the audit bound fail-closed on every open.
|
|
1003
|
+
ensureJoinPolicyAuditCapTrigger();
|
|
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);
|
|
1020
|
+
`);
|
|
1021
|
+
}
|
|
1022
|
+
if (version < 30) {
|
|
1023
|
+
// OFFSET pagination is scoped to an immutable responder row list. Keep
|
|
1024
|
+
// the opaque responder token beside the verified offset so a requester
|
|
1025
|
+
// restart can resume that same list instead of discarding durable work.
|
|
1026
|
+
ensureSyncCheckpointResponderSessionColumns();
|
|
1027
|
+
}
|
|
778
1028
|
this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
779
1029
|
if (upgradedExistingDb && !this.explicitRetentionDays) {
|
|
780
1030
|
this.retentionDays = LEGACY_IMPLICIT_RETENTION_DAYS;
|
|
@@ -807,7 +1057,21 @@ export class DashboardDB {
|
|
|
807
1057
|
this.db.exec(`DELETE FROM chat_persistence_jobs WHERE updated_at < ${cutoff} AND status IN ('stored', 'failed')`);
|
|
808
1058
|
this.db.exec(`DELETE FROM notifications WHERE ts < ${cutoff}`);
|
|
809
1059
|
this.db.exec(`DELETE FROM replication_events WHERE ts < ${cutoff}`);
|
|
1060
|
+
this.db.exec(`DELETE FROM context_graph_join_policy_audit WHERE ts < ${cutoff}`);
|
|
1061
|
+
// The admission ledger has its own typed lifetime. It intentionally does
|
|
1062
|
+
// not depend on whether the corresponding audit rows still exist. Preserve
|
|
1063
|
+
// the historical retention envelope for durable commit idempotency while
|
|
1064
|
+
// allowing fully expired reservation/commit state to age out.
|
|
1065
|
+
this.db.prepare(`
|
|
1066
|
+
DELETE FROM context_graph_join_approval_ledger
|
|
1067
|
+
WHERE reserved_at < ?
|
|
1068
|
+
AND (committed_at IS NULL OR committed_at < ?)
|
|
1069
|
+
`).run(cutoff, cutoff);
|
|
810
1070
|
this.db.prepare(`DELETE FROM sync_checkpoints WHERE expires_at < ?`).run(Date.now());
|
|
1071
|
+
// Reconcile negatives are accelerators, never historical records. Once the
|
|
1072
|
+
// retry window elapses they must not survive indefinitely or accumulate one
|
|
1073
|
+
// row per previously-seen KA across restarts.
|
|
1074
|
+
this.db.prepare(`DELETE FROM vm_reconcile_negative_cache WHERE next_retry_at < ?`).run(Date.now());
|
|
811
1075
|
// Universal Messenger idempotency table. Shorter TTL than the
|
|
812
1076
|
// operator retention: no realistic dedup window extends beyond
|
|
813
1077
|
// a day. The protocol_outbox table is intentionally not pruned
|
|
@@ -815,20 +1079,12 @@ export class DashboardDB {
|
|
|
815
1079
|
// SqliteProtocolOutboxStore.dropExpired().
|
|
816
1080
|
const messengerCutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
817
1081
|
this.db.exec(`DELETE FROM message_idempotency WHERE ts < ${messengerCutoff}`);
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (logsDeleted > LOGS_VACUUM_DELETE_THRESHOLD || freePages > VACUUM_FREE_PAGE_THRESHOLD) {
|
|
825
|
-
try {
|
|
826
|
-
this.db.exec(`VACUUM`);
|
|
827
|
-
}
|
|
828
|
-
catch {
|
|
829
|
-
// VACUUM requires an exclusive lock. If another connection is
|
|
830
|
-
// holding the DB open we skip and retry on the next prune.
|
|
831
|
-
}
|
|
1082
|
+
// Avoid rebuilding an oversized DB at startup while millions of routine
|
|
1083
|
+
// rows are still live. The daemon trims that backlog in bounded batches;
|
|
1084
|
+
// the final batch performs one compacting VACUUM. Databases already under
|
|
1085
|
+
// the cap retain the established time-prune reclamation behaviour.
|
|
1086
|
+
if (!this.hasRoutineLogOverflow()) {
|
|
1087
|
+
this.reclaimFreePagesIfNeeded(logsDeleted > LOGS_VACUUM_DELETE_THRESHOLD);
|
|
832
1088
|
}
|
|
833
1089
|
// Return the WAL file itself to the OS. journal_size_limit bounds it
|
|
834
1090
|
// in steady state, but a TRUNCATE checkpoint here shrinks it promptly
|
|
@@ -836,6 +1092,96 @@ export class DashboardDB {
|
|
|
836
1092
|
// which rewrites the whole DB through the WAL and momentarily grows
|
|
837
1093
|
// it. Runs unconditionally — independent of the VACUUM gate — because
|
|
838
1094
|
// an idle node still wants its -wal reclaimed.
|
|
1095
|
+
this.truncateWal('prune');
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Remove one bounded batch of the oldest routine (non-warning/error) logs.
|
|
1099
|
+
* A count cap complements time retention: a high-rate sync storm can create
|
|
1100
|
+
* millions of rows inside a single day, long before a 14-day cutoff applies.
|
|
1101
|
+
*
|
|
1102
|
+
* Deletion is deliberately incremental so an upgrade does not block node
|
|
1103
|
+
* startup on a multi-GB transaction. Once the backlog reaches the cap, one
|
|
1104
|
+
* VACUUM returns the accumulated free pages to the OS and the file shrinks.
|
|
1105
|
+
*/
|
|
1106
|
+
pruneLogVolumeBatch() {
|
|
1107
|
+
const overflowCutoff = this.routineLogOverflowCutoff();
|
|
1108
|
+
if (overflowCutoff === null) {
|
|
1109
|
+
const reclaim = this.reclaimFreePagesIfNeeded(false);
|
|
1110
|
+
if (!reclaim.reclaimPending)
|
|
1111
|
+
this.truncateWal('log-volume prune');
|
|
1112
|
+
return {
|
|
1113
|
+
deleted: 0,
|
|
1114
|
+
status: reclaim.reclaimPending
|
|
1115
|
+
? 'reclaim-pending'
|
|
1116
|
+
: reclaim.compacted
|
|
1117
|
+
? 'done-compacted'
|
|
1118
|
+
: 'done',
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
const deleted = this.db.prepare(`
|
|
1122
|
+
DELETE FROM logs
|
|
1123
|
+
WHERE id IN (
|
|
1124
|
+
SELECT id
|
|
1125
|
+
FROM logs
|
|
1126
|
+
WHERE id <= @cutoff
|
|
1127
|
+
AND level NOT IN ('warn', 'error')
|
|
1128
|
+
ORDER BY id ASC
|
|
1129
|
+
LIMIT @batchRows
|
|
1130
|
+
)
|
|
1131
|
+
`).run({
|
|
1132
|
+
cutoff: overflowCutoff,
|
|
1133
|
+
batchRows: this.logVolumePruneBatchRows,
|
|
1134
|
+
}).changes;
|
|
1135
|
+
// If the batch filled, conservatively schedule another tick. An exact-size
|
|
1136
|
+
// final batch costs one extra cheap probe before compaction, which is safer
|
|
1137
|
+
// than running a second million-row count after every deletion.
|
|
1138
|
+
const hasMore = deleted === this.logVolumePruneBatchRows;
|
|
1139
|
+
const reclaim = hasMore
|
|
1140
|
+
? { compacted: false, reclaimPending: false }
|
|
1141
|
+
: this.reclaimFreePagesIfNeeded(deleted > LOGS_VACUUM_DELETE_THRESHOLD);
|
|
1142
|
+
if (!hasMore && !reclaim.reclaimPending)
|
|
1143
|
+
this.truncateWal('log-volume prune');
|
|
1144
|
+
return {
|
|
1145
|
+
deleted,
|
|
1146
|
+
status: hasMore
|
|
1147
|
+
? 'more'
|
|
1148
|
+
: reclaim.reclaimPending
|
|
1149
|
+
? 'reclaim-pending'
|
|
1150
|
+
: reclaim.compacted
|
|
1151
|
+
? 'done-compacted'
|
|
1152
|
+
: 'done',
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
routineLogOverflowCutoff() {
|
|
1156
|
+
const row = this.db.prepare(`
|
|
1157
|
+
SELECT id
|
|
1158
|
+
FROM logs
|
|
1159
|
+
WHERE level NOT IN ('warn', 'error')
|
|
1160
|
+
ORDER BY id DESC
|
|
1161
|
+
LIMIT 1 OFFSET ?
|
|
1162
|
+
`).get(this.routineLogRowCap);
|
|
1163
|
+
return row?.id ?? null;
|
|
1164
|
+
}
|
|
1165
|
+
hasRoutineLogOverflow() {
|
|
1166
|
+
return this.routineLogOverflowCutoff() !== null;
|
|
1167
|
+
}
|
|
1168
|
+
reclaimFreePagesIfNeeded(force) {
|
|
1169
|
+
// DELETE / DROP only marks pages reusable; VACUUM is what returns them to
|
|
1170
|
+
// the OS. A failed exclusive-lock/disk attempt remains pending for a later
|
|
1171
|
+
// background retry and never prevents the node from running.
|
|
1172
|
+
const freePages = Number(this.db.pragma('freelist_count', { simple: true }) ?? 0);
|
|
1173
|
+
if (!force && freePages <= VACUUM_FREE_PAGE_THRESHOLD) {
|
|
1174
|
+
return { compacted: false, reclaimPending: false };
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
this.db.exec(`VACUUM`);
|
|
1178
|
+
return { compacted: true, reclaimPending: false };
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return { compacted: false, reclaimPending: true };
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
truncateWal(reason) {
|
|
839
1185
|
try {
|
|
840
1186
|
// wal_checkpoint signals reader contention through its result row
|
|
841
1187
|
// (`busy = 1`), NOT by throwing — so a busy checkpoint leaves the
|
|
@@ -845,7 +1191,7 @@ export class DashboardDB {
|
|
|
845
1191
|
// rather than indistinguishable from a real one.
|
|
846
1192
|
const [checkpoint] = this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
847
1193
|
if (checkpoint?.busy) {
|
|
848
|
-
console.warn(`[DashboardDB] wal_checkpoint(TRUNCATE) busy — WAL not reclaimed
|
|
1194
|
+
console.warn(`[DashboardDB] wal_checkpoint(TRUNCATE) busy — WAL not reclaimed during ${reason} ` +
|
|
849
1195
|
`(log=${checkpoint.log}, checkpointed=${checkpoint.checkpointed}); retried next prune`);
|
|
850
1196
|
}
|
|
851
1197
|
}
|
|
@@ -856,7 +1202,7 @@ export class DashboardDB {
|
|
|
856
1202
|
// better-sqlite3 change turns lock or I/O errors into throws, the
|
|
857
1203
|
// stalled WAL reclaim is visible in the daemon log instead of only
|
|
858
1204
|
// showing up later as unexplained disk growth. Never block prune.
|
|
859
|
-
console.warn(`[DashboardDB] wal_checkpoint(TRUNCATE) failed — WAL not reclaimed
|
|
1205
|
+
console.warn(`[DashboardDB] wal_checkpoint(TRUNCATE) failed — WAL not reclaimed during ${reason}: ` +
|
|
860
1206
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
861
1207
|
}
|
|
862
1208
|
}
|
|
@@ -913,50 +1259,50 @@ export class DashboardDB {
|
|
|
913
1259
|
}
|
|
914
1260
|
// --- Metric snapshots ---
|
|
915
1261
|
insertSnapshot(snap) {
|
|
916
|
-
this.stmt('insertSnapshot', `
|
|
917
|
-
INSERT INTO metric_snapshots (
|
|
918
|
-
ts, cpu_percent, mem_used_bytes, mem_total_bytes,
|
|
919
|
-
disk_used_bytes, disk_total_bytes, heap_used_bytes, uptime_seconds,
|
|
920
|
-
peer_count, direct_peers, relayed_peers, mesh_peers, contextGraph_count,
|
|
921
|
-
total_triples, total_kcs, total_kas, store_bytes,
|
|
922
|
-
confirmed_kcs, tentative_kcs, rpc_latency_ms, rpc_healthy,
|
|
923
|
-
relay_capacity, relay_reservation_count, relay_active_circuits,
|
|
924
|
-
relay_bytes_in, relay_bytes_out
|
|
925
|
-
) VALUES (
|
|
926
|
-
@ts, @cpu_percent, @mem_used_bytes, @mem_total_bytes,
|
|
927
|
-
@disk_used_bytes, @disk_total_bytes, @heap_used_bytes, @uptime_seconds,
|
|
928
|
-
@peer_count, @direct_peers, @relayed_peers, @mesh_peers, @contextGraph_count,
|
|
929
|
-
@total_triples, @total_kcs, @total_kas, @store_bytes,
|
|
930
|
-
@confirmed_kcs, @tentative_kcs, @rpc_latency_ms, @rpc_healthy,
|
|
931
|
-
@relay_capacity, @relay_reservation_count, @relay_active_circuits,
|
|
932
|
-
@relay_bytes_in, @relay_bytes_out
|
|
933
|
-
)
|
|
1262
|
+
this.stmt('insertSnapshot', `
|
|
1263
|
+
INSERT INTO metric_snapshots (
|
|
1264
|
+
ts, cpu_percent, mem_used_bytes, mem_total_bytes,
|
|
1265
|
+
disk_used_bytes, disk_total_bytes, heap_used_bytes, uptime_seconds,
|
|
1266
|
+
peer_count, direct_peers, relayed_peers, mesh_peers, contextGraph_count,
|
|
1267
|
+
total_triples, total_kcs, total_kas, store_bytes,
|
|
1268
|
+
confirmed_kcs, tentative_kcs, rpc_latency_ms, rpc_healthy,
|
|
1269
|
+
relay_capacity, relay_reservation_count, relay_active_circuits,
|
|
1270
|
+
relay_bytes_in, relay_bytes_out
|
|
1271
|
+
) VALUES (
|
|
1272
|
+
@ts, @cpu_percent, @mem_used_bytes, @mem_total_bytes,
|
|
1273
|
+
@disk_used_bytes, @disk_total_bytes, @heap_used_bytes, @uptime_seconds,
|
|
1274
|
+
@peer_count, @direct_peers, @relayed_peers, @mesh_peers, @contextGraph_count,
|
|
1275
|
+
@total_triples, @total_kcs, @total_kas, @store_bytes,
|
|
1276
|
+
@confirmed_kcs, @tentative_kcs, @rpc_latency_ms, @rpc_healthy,
|
|
1277
|
+
@relay_capacity, @relay_reservation_count, @relay_active_circuits,
|
|
1278
|
+
@relay_bytes_in, @relay_bytes_out
|
|
1279
|
+
)
|
|
934
1280
|
`).run(snap);
|
|
935
1281
|
}
|
|
936
1282
|
getLatestSnapshot() {
|
|
937
1283
|
return this.db.prepare('SELECT * FROM metric_snapshots ORDER BY ts DESC LIMIT 1').get();
|
|
938
1284
|
}
|
|
939
1285
|
upsertContextGraphSubscription(record) {
|
|
940
|
-
this.stmt('upsertContextGraphSubscription', `
|
|
941
|
-
INSERT INTO context_graph_subscriptions (
|
|
942
|
-
context_graph_id, name, subscribed, synced, shared_memory_synced, meta_synced,
|
|
943
|
-
on_chain_id, on_chain_hash, last_reconciled_ordinal, core_hosted, sync_scoped, updated_at
|
|
944
|
-
) VALUES (
|
|
945
|
-
@context_graph_id, @name, @subscribed, @synced, @shared_memory_synced, @meta_synced,
|
|
946
|
-
@on_chain_id, @on_chain_hash, @last_reconciled_ordinal, @core_hosted, @sync_scoped, @updated_at
|
|
947
|
-
)
|
|
948
|
-
ON CONFLICT(context_graph_id) DO UPDATE SET
|
|
949
|
-
name = excluded.name,
|
|
950
|
-
subscribed = excluded.subscribed,
|
|
951
|
-
synced = excluded.synced,
|
|
952
|
-
shared_memory_synced = excluded.shared_memory_synced,
|
|
953
|
-
meta_synced = excluded.meta_synced,
|
|
954
|
-
on_chain_id = excluded.on_chain_id,
|
|
955
|
-
on_chain_hash = excluded.on_chain_hash,
|
|
956
|
-
last_reconciled_ordinal = excluded.last_reconciled_ordinal,
|
|
957
|
-
core_hosted = excluded.core_hosted,
|
|
958
|
-
sync_scoped = excluded.sync_scoped,
|
|
959
|
-
updated_at = excluded.updated_at
|
|
1286
|
+
this.stmt('upsertContextGraphSubscription', `
|
|
1287
|
+
INSERT INTO context_graph_subscriptions (
|
|
1288
|
+
context_graph_id, name, subscribed, synced, shared_memory_synced, meta_synced,
|
|
1289
|
+
on_chain_id, on_chain_hash, last_reconciled_ordinal, core_hosted, sync_scoped, updated_at
|
|
1290
|
+
) VALUES (
|
|
1291
|
+
@context_graph_id, @name, @subscribed, @synced, @shared_memory_synced, @meta_synced,
|
|
1292
|
+
@on_chain_id, @on_chain_hash, @last_reconciled_ordinal, @core_hosted, @sync_scoped, @updated_at
|
|
1293
|
+
)
|
|
1294
|
+
ON CONFLICT(context_graph_id) DO UPDATE SET
|
|
1295
|
+
name = excluded.name,
|
|
1296
|
+
subscribed = excluded.subscribed,
|
|
1297
|
+
synced = excluded.synced,
|
|
1298
|
+
shared_memory_synced = excluded.shared_memory_synced,
|
|
1299
|
+
meta_synced = excluded.meta_synced,
|
|
1300
|
+
on_chain_id = excluded.on_chain_id,
|
|
1301
|
+
on_chain_hash = excluded.on_chain_hash,
|
|
1302
|
+
last_reconciled_ordinal = excluded.last_reconciled_ordinal,
|
|
1303
|
+
core_hosted = excluded.core_hosted,
|
|
1304
|
+
sync_scoped = excluded.sync_scoped,
|
|
1305
|
+
updated_at = excluded.updated_at
|
|
960
1306
|
`).run({
|
|
961
1307
|
context_graph_id: record.context_graph_id,
|
|
962
1308
|
name: record.name ?? null,
|
|
@@ -979,7 +1325,302 @@ export class DashboardDB {
|
|
|
979
1325
|
return this.stmt('getContextGraphSubscription', 'SELECT * FROM context_graph_subscriptions WHERE context_graph_id = ?').get(contextGraphId);
|
|
980
1326
|
}
|
|
981
1327
|
deleteContextGraphSubscription(contextGraphId) {
|
|
982
|
-
|
|
1328
|
+
const remove = this.db.transaction(() => {
|
|
1329
|
+
this.stmt('deleteContextGraphSubscription', 'DELETE FROM context_graph_subscriptions WHERE context_graph_id = ?').run(contextGraphId);
|
|
1330
|
+
this.stmt('deleteContextGraphReadinessProvenance', 'DELETE FROM settings WHERE key = ?')
|
|
1331
|
+
.run(this.contextGraphReadinessProvenanceKey(contextGraphId));
|
|
1332
|
+
});
|
|
1333
|
+
remove();
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* CLI-owned, durable provenance for context-graph readiness flags.
|
|
1337
|
+
*
|
|
1338
|
+
* Subscription rows predate per-plane proof and can therefore contain the
|
|
1339
|
+
* v10.0.6 false-ready shape (`synced=1`, `shared_memory_synced=1`) after an
|
|
1340
|
+
* unrelated peer returned an empty response. Keep the proof out of the
|
|
1341
|
+
* agent-owned subscription upsert so older/custom agent stores remain
|
|
1342
|
+
* source-compatible and routine subscription persistence cannot overwrite a
|
|
1343
|
+
* proof established by the daemon's catch-up classifier.
|
|
1344
|
+
*/
|
|
1345
|
+
getContextGraphReadinessProvenance(contextGraphId) {
|
|
1346
|
+
const row = this.stmt('getContextGraphReadinessProvenance', 'SELECT value FROM settings WHERE key = ?').get(this.contextGraphReadinessProvenanceKey(contextGraphId));
|
|
1347
|
+
if (!row)
|
|
1348
|
+
return null;
|
|
1349
|
+
try {
|
|
1350
|
+
const parsed = JSON.parse(row.value);
|
|
1351
|
+
if (!Number.isInteger(parsed.version) || (parsed.version ?? 0) < 1)
|
|
1352
|
+
return null;
|
|
1353
|
+
return {
|
|
1354
|
+
version: parsed.version,
|
|
1355
|
+
durableVerified: parsed.durableVerified === true,
|
|
1356
|
+
sharedMemoryVerified: parsed.sharedMemoryVerified === true,
|
|
1357
|
+
updatedAt: typeof parsed.updatedAt === 'number' && Number.isFinite(parsed.updatedAt)
|
|
1358
|
+
? parsed.updatedAt
|
|
1359
|
+
: 0,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
setContextGraphReadinessProvenance(contextGraphId, provenance) {
|
|
1367
|
+
const record = {
|
|
1368
|
+
version: provenance.version,
|
|
1369
|
+
durableVerified: provenance.durableVerified,
|
|
1370
|
+
sharedMemoryVerified: provenance.sharedMemoryVerified,
|
|
1371
|
+
updatedAt: provenance.updatedAt ?? Date.now(),
|
|
1372
|
+
};
|
|
1373
|
+
this.stmt('setContextGraphReadinessProvenance', 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(this.contextGraphReadinessProvenanceKey(contextGraphId), JSON.stringify(record));
|
|
1374
|
+
}
|
|
1375
|
+
contextGraphReadinessProvenanceKey(contextGraphId) {
|
|
1376
|
+
return `contextGraphReadiness:${contextGraphId}`;
|
|
1377
|
+
}
|
|
1378
|
+
// --- Private context-graph join policy + audit ---
|
|
1379
|
+
getContextGraphJoinPolicy(contextGraphId) {
|
|
1380
|
+
const row = this.stmt('getContextGraphJoinPolicy', 'SELECT value FROM settings WHERE key = ?').get(this.contextGraphJoinPolicyKey(contextGraphId));
|
|
1381
|
+
if (!row)
|
|
1382
|
+
return null;
|
|
1383
|
+
try {
|
|
1384
|
+
return parseContextGraphJoinPolicyRecord(JSON.parse(row.value), contextGraphId);
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
// Corrupt policy state is indistinguishable from no policy. The agent's
|
|
1388
|
+
// default is manual, so this is intentionally fail-closed.
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
setContextGraphJoinPolicy(record) {
|
|
1393
|
+
this.stmt('setContextGraphJoinPolicy', 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(this.contextGraphJoinPolicyKey(record.contextGraphId), JSON.stringify(record));
|
|
1394
|
+
}
|
|
1395
|
+
setContextGraphJoinPolicyWithAudit(record, event) {
|
|
1396
|
+
const transition = this.db.transaction(() => {
|
|
1397
|
+
this.setContextGraphJoinPolicy(record);
|
|
1398
|
+
this.appendContextGraphJoinPolicyAudit(event);
|
|
1399
|
+
});
|
|
1400
|
+
transition();
|
|
1401
|
+
}
|
|
1402
|
+
appendContextGraphJoinPolicyAudit(event) {
|
|
1403
|
+
this.stmt('appendContextGraphJoinPolicyAudit', `
|
|
1404
|
+
INSERT INTO context_graph_join_policy_audit (
|
|
1405
|
+
ts, context_graph_id, event_type, actor, agent_address, outcome,
|
|
1406
|
+
reason, request_digest, policy_version, details
|
|
1407
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1408
|
+
`).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);
|
|
1409
|
+
}
|
|
1410
|
+
reserveContextGraphAutomaticApproval(input) {
|
|
1411
|
+
const reserve = this.db.transaction(() => {
|
|
1412
|
+
const since = input.timestamp - 60 * 60 * 1000;
|
|
1413
|
+
const cgRow = this.stmt('countContextGraphAutomaticApprovalReservations', `
|
|
1414
|
+
SELECT COUNT(*) AS count
|
|
1415
|
+
FROM context_graph_join_approval_ledger
|
|
1416
|
+
WHERE context_graph_id = ?
|
|
1417
|
+
AND reserved_at >= ?
|
|
1418
|
+
`).get(input.contextGraphId, since);
|
|
1419
|
+
const nodeRow = this.stmt('countNodeAutomaticApprovalReservations', `
|
|
1420
|
+
SELECT COUNT(*) AS count
|
|
1421
|
+
FROM context_graph_join_approval_ledger
|
|
1422
|
+
WHERE reserved_at >= ?
|
|
1423
|
+
`).get(since);
|
|
1424
|
+
const contextGraphCount = Number(cgRow?.count ?? 0);
|
|
1425
|
+
const nodeCount = Number(nodeRow?.count ?? 0);
|
|
1426
|
+
const existing = this.stmt('findContextGraphAutomaticApprovalReservation', `
|
|
1427
|
+
SELECT 1 AS found
|
|
1428
|
+
FROM context_graph_join_approval_ledger
|
|
1429
|
+
WHERE context_graph_id = ?
|
|
1430
|
+
AND request_digest = ?
|
|
1431
|
+
AND policy_epoch = ?
|
|
1432
|
+
AND reserved_at >= ?
|
|
1433
|
+
LIMIT 1
|
|
1434
|
+
`).get(input.contextGraphId, input.requestDigest, input.policyEpoch, since);
|
|
1435
|
+
if (existing) {
|
|
1436
|
+
return {
|
|
1437
|
+
allowed: true,
|
|
1438
|
+
contextGraphApprovalsLastHour: contextGraphCount,
|
|
1439
|
+
nodeApprovalsLastHour: nodeCount,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
if (contextGraphCount >= input.contextGraphLimit) {
|
|
1443
|
+
return {
|
|
1444
|
+
allowed: false,
|
|
1445
|
+
contextGraphApprovalsLastHour: contextGraphCount,
|
|
1446
|
+
nodeApprovalsLastHour: nodeCount,
|
|
1447
|
+
reason: 'context-graph-rate-limit',
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
if (nodeCount >= input.nodeLimit) {
|
|
1451
|
+
return {
|
|
1452
|
+
allowed: false,
|
|
1453
|
+
contextGraphApprovalsLastHour: contextGraphCount,
|
|
1454
|
+
nodeApprovalsLastHour: nodeCount,
|
|
1455
|
+
reason: 'node-rate-limit',
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
// The typed ledger row and its audit projection share one transaction:
|
|
1459
|
+
// failures cannot consume quota without an audit event or emit an audit
|
|
1460
|
+
// reservation without durable operational state. An expired replay of
|
|
1461
|
+
// the same digest+epoch refreshes its reservation timestamp and consumes
|
|
1462
|
+
// the rolling-window quota again, matching the former append-only model.
|
|
1463
|
+
this.stmt('upsertContextGraphAutomaticApprovalReservation', `
|
|
1464
|
+
INSERT INTO context_graph_join_approval_ledger (
|
|
1465
|
+
context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
1466
|
+
state, committed_at, actor, agent_address, policy_version
|
|
1467
|
+
) VALUES (?, ?, ?, ?, 'reserved', NULL, ?, ?, ?)
|
|
1468
|
+
ON CONFLICT(context_graph_id, request_digest, policy_epoch) DO UPDATE SET
|
|
1469
|
+
reserved_at = excluded.reserved_at,
|
|
1470
|
+
actor = excluded.actor,
|
|
1471
|
+
agent_address = excluded.agent_address,
|
|
1472
|
+
policy_version = excluded.policy_version
|
|
1473
|
+
`).run(input.contextGraphId, input.requestDigest, input.policyEpoch, input.timestamp, input.actor, input.agentAddress, input.policyVersion);
|
|
1474
|
+
this.appendContextGraphJoinPolicyAudit({
|
|
1475
|
+
timestamp: input.timestamp,
|
|
1476
|
+
contextGraphId: input.contextGraphId,
|
|
1477
|
+
eventType: 'join_auto_reservation',
|
|
1478
|
+
actor: input.actor,
|
|
1479
|
+
agentAddress: input.agentAddress,
|
|
1480
|
+
outcome: 'reserved',
|
|
1481
|
+
requestDigest: input.requestDigest,
|
|
1482
|
+
policyVersion: input.policyVersion,
|
|
1483
|
+
details: {
|
|
1484
|
+
contextGraphLimit: input.contextGraphLimit,
|
|
1485
|
+
nodeLimit: input.nodeLimit,
|
|
1486
|
+
policyEpoch: input.policyEpoch,
|
|
1487
|
+
},
|
|
1488
|
+
});
|
|
1489
|
+
return {
|
|
1490
|
+
allowed: true,
|
|
1491
|
+
contextGraphApprovalsLastHour: contextGraphCount + 1,
|
|
1492
|
+
nodeApprovalsLastHour: nodeCount + 1,
|
|
1493
|
+
};
|
|
1494
|
+
});
|
|
1495
|
+
return reserve();
|
|
1496
|
+
}
|
|
1497
|
+
markContextGraphAutomaticApprovalRepairPending(input) {
|
|
1498
|
+
const marked = this.stmt('markContextGraphAutomaticApprovalRepairPending', `
|
|
1499
|
+
UPDATE context_graph_join_approval_ledger
|
|
1500
|
+
SET repair_pending = 1
|
|
1501
|
+
WHERE context_graph_id = ?
|
|
1502
|
+
AND request_digest = ?
|
|
1503
|
+
AND policy_epoch = ?
|
|
1504
|
+
AND state = 'reserved'
|
|
1505
|
+
`).run(input.contextGraphId, input.requestDigest, input.policyEpoch);
|
|
1506
|
+
return marked.changes === 1;
|
|
1507
|
+
}
|
|
1508
|
+
getContextGraphAutomaticApprovalRepair(contextGraphId, requestDigest) {
|
|
1509
|
+
const repair = this.stmt('getContextGraphAutomaticApprovalRepair', `
|
|
1510
|
+
SELECT policy_epoch, actor, agent_address
|
|
1511
|
+
FROM context_graph_join_approval_ledger
|
|
1512
|
+
WHERE context_graph_id = ?
|
|
1513
|
+
AND request_digest = ?
|
|
1514
|
+
AND state = 'reserved'
|
|
1515
|
+
AND repair_pending = 1
|
|
1516
|
+
ORDER BY reserved_at DESC
|
|
1517
|
+
LIMIT 1
|
|
1518
|
+
`).get(contextGraphId, requestDigest);
|
|
1519
|
+
return repair
|
|
1520
|
+
? {
|
|
1521
|
+
policyEpoch: repair.policy_epoch,
|
|
1522
|
+
actor: repair.actor,
|
|
1523
|
+
agentAddress: repair.agent_address,
|
|
1524
|
+
}
|
|
1525
|
+
: null;
|
|
1526
|
+
}
|
|
1527
|
+
commitContextGraphAutomaticApproval(input) {
|
|
1528
|
+
const commit = this.db.transaction(() => {
|
|
1529
|
+
const reservation = this.stmt('findExactContextGraphAutomaticApprovalReservation', `
|
|
1530
|
+
SELECT context_graph_id, request_digest, policy_epoch, reserved_at,
|
|
1531
|
+
state, committed_at, actor, agent_address, policy_version
|
|
1532
|
+
FROM context_graph_join_approval_ledger
|
|
1533
|
+
WHERE context_graph_id = ?
|
|
1534
|
+
AND request_digest = ?
|
|
1535
|
+
AND policy_epoch = ?
|
|
1536
|
+
LIMIT 1
|
|
1537
|
+
`).get(input.contextGraphId, input.requestDigest, input.policyEpoch);
|
|
1538
|
+
if (!reservation)
|
|
1539
|
+
return false;
|
|
1540
|
+
if (reservation.state === 'committed')
|
|
1541
|
+
return true;
|
|
1542
|
+
const updated = this.stmt('commitContextGraphAutomaticApprovalReservation', `
|
|
1543
|
+
UPDATE context_graph_join_approval_ledger
|
|
1544
|
+
SET state = 'committed', committed_at = ?, repair_pending = 0
|
|
1545
|
+
WHERE context_graph_id = ?
|
|
1546
|
+
AND request_digest = ?
|
|
1547
|
+
AND policy_epoch = ?
|
|
1548
|
+
AND state = 'reserved'
|
|
1549
|
+
`).run(input.timestamp, reservation.context_graph_id, reservation.request_digest, reservation.policy_epoch);
|
|
1550
|
+
if (updated.changes !== 1)
|
|
1551
|
+
return false;
|
|
1552
|
+
this.appendContextGraphJoinPolicyAudit({
|
|
1553
|
+
timestamp: input.timestamp,
|
|
1554
|
+
contextGraphId: input.contextGraphId,
|
|
1555
|
+
eventType: 'join_admission_committed',
|
|
1556
|
+
actor: reservation.actor || input.actor,
|
|
1557
|
+
agentAddress: reservation.agent_address || input.agentAddress,
|
|
1558
|
+
outcome: 'approved',
|
|
1559
|
+
requestDigest: input.requestDigest,
|
|
1560
|
+
policyVersion: reservation.policy_version,
|
|
1561
|
+
details: {
|
|
1562
|
+
...input.details,
|
|
1563
|
+
policyEpoch: reservation.policy_epoch,
|
|
1564
|
+
},
|
|
1565
|
+
});
|
|
1566
|
+
return true;
|
|
1567
|
+
});
|
|
1568
|
+
return commit();
|
|
1569
|
+
}
|
|
1570
|
+
getContextGraphAutomaticApprovalUsage(contextGraphId, timestamp) {
|
|
1571
|
+
const since = timestamp - 60 * 60 * 1000;
|
|
1572
|
+
const contextGraphApprovalsLastHour = Number(this.stmt('getContextGraphAutomaticApprovalUsage', `
|
|
1573
|
+
SELECT COUNT(*) AS count
|
|
1574
|
+
FROM context_graph_join_approval_ledger
|
|
1575
|
+
WHERE context_graph_id = ?
|
|
1576
|
+
AND reserved_at >= ?
|
|
1577
|
+
`).get(contextGraphId, since)?.count ?? 0);
|
|
1578
|
+
const nodeApprovalsLastHour = Number(this.stmt('getNodeAutomaticApprovalUsage', `
|
|
1579
|
+
SELECT COUNT(*) AS count
|
|
1580
|
+
FROM context_graph_join_approval_ledger
|
|
1581
|
+
WHERE reserved_at >= ?
|
|
1582
|
+
`).get(since)?.count ?? 0);
|
|
1583
|
+
return { contextGraphApprovalsLastHour, nodeApprovalsLastHour };
|
|
1584
|
+
}
|
|
1585
|
+
listContextGraphJoinPolicyAudit(contextGraphId) {
|
|
1586
|
+
return this.stmt('listContextGraphJoinPolicyAudit', `
|
|
1587
|
+
SELECT id, ts, context_graph_id, event_type, actor, agent_address,
|
|
1588
|
+
outcome, reason, request_digest, policy_version, details
|
|
1589
|
+
FROM context_graph_join_policy_audit
|
|
1590
|
+
WHERE context_graph_id = ?
|
|
1591
|
+
ORDER BY id ASC
|
|
1592
|
+
`).all(contextGraphId);
|
|
1593
|
+
}
|
|
1594
|
+
contextGraphJoinPolicyKey(contextGraphId) {
|
|
1595
|
+
return `contextGraphJoinPolicy:${contextGraphId}`;
|
|
1596
|
+
}
|
|
1597
|
+
upsertVmReconcileNegative(record) {
|
|
1598
|
+
this.stmt('upsertVmReconcileNegative', `
|
|
1599
|
+
INSERT INTO vm_reconcile_negative_cache (
|
|
1600
|
+
cache_key, context_graph_id, failures, next_retry_at, swm_gen,
|
|
1601
|
+
candidate_namespaces, peer_topology_key, updated_at
|
|
1602
|
+
) VALUES (
|
|
1603
|
+
@cache_key, @context_graph_id, @failures, @next_retry_at, @swm_gen,
|
|
1604
|
+
@candidate_namespaces, @peer_topology_key, @updated_at
|
|
1605
|
+
)
|
|
1606
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
1607
|
+
context_graph_id = excluded.context_graph_id,
|
|
1608
|
+
failures = excluded.failures,
|
|
1609
|
+
next_retry_at = excluded.next_retry_at,
|
|
1610
|
+
swm_gen = excluded.swm_gen,
|
|
1611
|
+
candidate_namespaces = excluded.candidate_namespaces,
|
|
1612
|
+
peer_topology_key = excluded.peer_topology_key,
|
|
1613
|
+
updated_at = excluded.updated_at
|
|
1614
|
+
`).run(record);
|
|
1615
|
+
}
|
|
1616
|
+
getVmReconcileNegative(cacheKey) {
|
|
1617
|
+
return this.stmt('getVmReconcileNegative', 'SELECT * FROM vm_reconcile_negative_cache WHERE cache_key = ?').get(cacheKey);
|
|
1618
|
+
}
|
|
1619
|
+
deleteVmReconcileNegative(cacheKey) {
|
|
1620
|
+
this.stmt('deleteVmReconcileNegative', 'DELETE FROM vm_reconcile_negative_cache WHERE cache_key = ?').run(cacheKey);
|
|
1621
|
+
}
|
|
1622
|
+
deleteVmReconcileNegativesForContextGraph(contextGraphId) {
|
|
1623
|
+
this.stmt('deleteVmReconcileNegativesForContextGraph', 'DELETE FROM vm_reconcile_negative_cache WHERE context_graph_id = ?').run(contextGraphId);
|
|
983
1624
|
}
|
|
984
1625
|
// --- Phase F: chain-driven VM reconciliation telemetry ---
|
|
985
1626
|
/**
|
|
@@ -988,14 +1629,14 @@ export class DashboardDB {
|
|
|
988
1629
|
* coerced defensively so a malformed event can never abort the insert.
|
|
989
1630
|
*/
|
|
990
1631
|
insertReplicationEvent(ev) {
|
|
991
|
-
this.stmt('insertReplicationEvent', `
|
|
992
|
-
INSERT INTO replication_events (
|
|
993
|
-
ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
994
|
-
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
995
|
-
) VALUES (
|
|
996
|
-
@ts, @context_graph_id, @on_chain_cg_id, @action, @ual, @ordinal, @ka_id,
|
|
997
|
-
@from_watermark, @to_watermark, @head, @reconciled, @pending, @detail
|
|
998
|
-
)
|
|
1632
|
+
this.stmt('insertReplicationEvent', `
|
|
1633
|
+
INSERT INTO replication_events (
|
|
1634
|
+
ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1635
|
+
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1636
|
+
) VALUES (
|
|
1637
|
+
@ts, @context_graph_id, @on_chain_cg_id, @action, @ual, @ordinal, @ka_id,
|
|
1638
|
+
@from_watermark, @to_watermark, @head, @reconciled, @pending, @detail
|
|
1639
|
+
)
|
|
999
1640
|
`).run({
|
|
1000
1641
|
ts: ev.ts,
|
|
1001
1642
|
context_graph_id: ev.context_graph_id,
|
|
@@ -1042,15 +1683,15 @@ export class DashboardDB {
|
|
|
1042
1683
|
// and inflate the reported latency. fetch/promote for one version always
|
|
1043
1684
|
// share the same ordinal, so this pairs them exactly; a fast-path promote
|
|
1044
1685
|
// with no fetch for its ordinal correctly contributes a 0ms latency.
|
|
1045
|
-
const latencyRows = this.db.prepare(`
|
|
1046
|
-
SELECT p.ts AS promote_ts,
|
|
1047
|
-
(SELECT MAX(f.ts) FROM replication_events f
|
|
1048
|
-
WHERE f.action = 'fetch'
|
|
1049
|
-
AND f.context_graph_id = p.context_graph_id
|
|
1050
|
-
AND f.ordinal = p.ordinal
|
|
1051
|
-
AND f.ts <= p.ts AND f.ts >= ?) AS fetch_ts
|
|
1052
|
-
FROM replication_events p
|
|
1053
|
-
WHERE p.action = 'promote' AND p.ts >= ? AND p.ordinal IS NOT NULL
|
|
1686
|
+
const latencyRows = this.db.prepare(`
|
|
1687
|
+
SELECT p.ts AS promote_ts,
|
|
1688
|
+
(SELECT MAX(f.ts) FROM replication_events f
|
|
1689
|
+
WHERE f.action = 'fetch'
|
|
1690
|
+
AND f.context_graph_id = p.context_graph_id
|
|
1691
|
+
AND f.ordinal = p.ordinal
|
|
1692
|
+
AND f.ts <= p.ts AND f.ts >= ?) AS fetch_ts
|
|
1693
|
+
FROM replication_events p
|
|
1694
|
+
WHERE p.action = 'promote' AND p.ts >= ? AND p.ordinal IS NOT NULL
|
|
1054
1695
|
`).all(cutoff, cutoff);
|
|
1055
1696
|
const latencies = latencyRows
|
|
1056
1697
|
.map((r) => (r.fetch_ts != null ? r.promote_ts - r.fetch_ts : 0))
|
|
@@ -1080,20 +1721,20 @@ export class DashboardDB {
|
|
|
1080
1721
|
}
|
|
1081
1722
|
_computeReplicationPerCg(periodMs) {
|
|
1082
1723
|
const cutoff = Date.now() - periodMs;
|
|
1083
|
-
return this.db.prepare(`
|
|
1084
|
-
SELECT context_graph_id,
|
|
1085
|
-
on_chain_cg_id,
|
|
1086
|
-
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1087
|
-
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1088
|
-
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1089
|
-
SUM(CASE WHEN action = 'cursor-advance' THEN 1 ELSE 0 END) AS cursor_advances,
|
|
1090
|
-
MAX(to_watermark) AS last_watermark,
|
|
1091
|
-
MAX(head) AS last_head,
|
|
1092
|
-
MAX(ts) AS last_event_ts
|
|
1093
|
-
FROM replication_events
|
|
1094
|
-
WHERE ts >= ?
|
|
1095
|
-
GROUP BY context_graph_id, on_chain_cg_id
|
|
1096
|
-
ORDER BY last_event_ts DESC
|
|
1724
|
+
return this.db.prepare(`
|
|
1725
|
+
SELECT context_graph_id,
|
|
1726
|
+
on_chain_cg_id,
|
|
1727
|
+
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1728
|
+
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1729
|
+
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1730
|
+
SUM(CASE WHEN action = 'cursor-advance' THEN 1 ELSE 0 END) AS cursor_advances,
|
|
1731
|
+
MAX(to_watermark) AS last_watermark,
|
|
1732
|
+
MAX(head) AS last_head,
|
|
1733
|
+
MAX(ts) AS last_event_ts
|
|
1734
|
+
FROM replication_events
|
|
1735
|
+
WHERE ts >= ?
|
|
1736
|
+
GROUP BY context_graph_id, on_chain_cg_id
|
|
1737
|
+
ORDER BY last_event_ts DESC
|
|
1097
1738
|
`).all(cutoff);
|
|
1098
1739
|
}
|
|
1099
1740
|
/**
|
|
@@ -1112,16 +1753,16 @@ export class DashboardDB {
|
|
|
1112
1753
|
const cutoff = Date.now() - opts.periodMs;
|
|
1113
1754
|
const bucket = Math.max(1, Math.floor(opts.bucketMs));
|
|
1114
1755
|
const where = opts.contextGraphId ? 'AND context_graph_id = @cg' : '';
|
|
1115
|
-
return this.db.prepare(`
|
|
1116
|
-
SELECT (ts / @bucket) * @bucket AS bucket,
|
|
1117
|
-
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1118
|
-
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1119
|
-
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1120
|
-
COUNT(*) AS total
|
|
1121
|
-
FROM replication_events
|
|
1122
|
-
WHERE ts >= @cutoff ${where}
|
|
1123
|
-
GROUP BY bucket
|
|
1124
|
-
ORDER BY bucket ASC
|
|
1756
|
+
return this.db.prepare(`
|
|
1757
|
+
SELECT (ts / @bucket) * @bucket AS bucket,
|
|
1758
|
+
SUM(CASE WHEN action = 'promote' THEN 1 ELSE 0 END) AS promotes,
|
|
1759
|
+
SUM(CASE WHEN action = 'fetch' THEN 1 ELSE 0 END) AS fetches,
|
|
1760
|
+
SUM(CASE WHEN action = 'defer' THEN 1 ELSE 0 END) AS defers,
|
|
1761
|
+
COUNT(*) AS total
|
|
1762
|
+
FROM replication_events
|
|
1763
|
+
WHERE ts >= @cutoff ${where}
|
|
1764
|
+
GROUP BY bucket
|
|
1765
|
+
ORDER BY bucket ASC
|
|
1125
1766
|
`).all({ cutoff, bucket, cg: opts.contextGraphId ?? null });
|
|
1126
1767
|
}
|
|
1127
1768
|
/**
|
|
@@ -1133,13 +1774,13 @@ export class DashboardDB {
|
|
|
1133
1774
|
const safeLimit = Number.isFinite(limit)
|
|
1134
1775
|
? Math.max(1, Math.min(1000, Math.trunc(limit)))
|
|
1135
1776
|
: 100;
|
|
1136
|
-
return this.db.prepare(`
|
|
1137
|
-
SELECT ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1138
|
-
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1139
|
-
FROM replication_events
|
|
1140
|
-
WHERE context_graph_id = ?
|
|
1141
|
-
ORDER BY ts DESC
|
|
1142
|
-
LIMIT ?
|
|
1777
|
+
return this.db.prepare(`
|
|
1778
|
+
SELECT ts, context_graph_id, on_chain_cg_id, action, ual, ordinal, ka_id,
|
|
1779
|
+
from_watermark, to_watermark, head, reconciled, pending, detail
|
|
1780
|
+
FROM replication_events
|
|
1781
|
+
WHERE context_graph_id = ?
|
|
1782
|
+
ORDER BY ts DESC
|
|
1783
|
+
LIMIT ?
|
|
1143
1784
|
`).all(contextGraphId, safeLimit);
|
|
1144
1785
|
}
|
|
1145
1786
|
/**
|
|
@@ -1148,38 +1789,38 @@ export class DashboardDB {
|
|
|
1148
1789
|
* on-disk truth the Replication tab's cursor inspector renders.
|
|
1149
1790
|
*/
|
|
1150
1791
|
getReplicationCursors() {
|
|
1151
|
-
return this.db.prepare(`
|
|
1152
|
-
SELECT s.context_graph_id AS context_graph_id,
|
|
1153
|
-
s.on_chain_id AS on_chain_id,
|
|
1154
|
-
s.last_reconciled_ordinal AS last_reconciled_ordinal,
|
|
1155
|
-
s.core_hosted AS core_hosted,
|
|
1156
|
-
s.subscribed AS subscribed,
|
|
1157
|
-
(SELECT MAX(head) FROM replication_events e
|
|
1158
|
-
WHERE e.context_graph_id = s.context_graph_id) AS last_head,
|
|
1159
|
-
(SELECT MAX(ts) FROM replication_events e
|
|
1160
|
-
WHERE e.context_graph_id = s.context_graph_id) AS last_event_ts
|
|
1161
|
-
FROM context_graph_subscriptions s
|
|
1162
|
-
ORDER BY s.context_graph_id ASC
|
|
1792
|
+
return this.db.prepare(`
|
|
1793
|
+
SELECT s.context_graph_id AS context_graph_id,
|
|
1794
|
+
s.on_chain_id AS on_chain_id,
|
|
1795
|
+
s.last_reconciled_ordinal AS last_reconciled_ordinal,
|
|
1796
|
+
s.core_hosted AS core_hosted,
|
|
1797
|
+
s.subscribed AS subscribed,
|
|
1798
|
+
(SELECT MAX(head) FROM replication_events e
|
|
1799
|
+
WHERE e.context_graph_id = s.context_graph_id) AS last_head,
|
|
1800
|
+
(SELECT MAX(ts) FROM replication_events e
|
|
1801
|
+
WHERE e.context_graph_id = s.context_graph_id) AS last_event_ts
|
|
1802
|
+
FROM context_graph_subscriptions s
|
|
1803
|
+
ORDER BY s.context_graph_id ASC
|
|
1163
1804
|
`).all();
|
|
1164
1805
|
}
|
|
1165
1806
|
upsertContextGraphMember(record) {
|
|
1166
1807
|
const firstSeenAt = record.first_seen_at ?? record.updated_at;
|
|
1167
|
-
this.stmt('upsertContextGraphMember', `
|
|
1168
|
-
INSERT INTO context_graph_memberships (
|
|
1169
|
-
context_graph_id, principal_type, principal_id, role, status, source,
|
|
1170
|
-
display_name, metadata, first_seen_at, updated_at
|
|
1171
|
-
) VALUES (
|
|
1172
|
-
@context_graph_id, @principal_type, @principal_id, @role, @status, @source,
|
|
1173
|
-
@display_name, @metadata, @first_seen_at, @updated_at
|
|
1174
|
-
)
|
|
1175
|
-
ON CONFLICT(context_graph_id, principal_type, principal_id) DO UPDATE SET
|
|
1176
|
-
role = excluded.role,
|
|
1177
|
-
status = excluded.status,
|
|
1178
|
-
source = excluded.source,
|
|
1179
|
-
display_name = excluded.display_name,
|
|
1180
|
-
metadata = excluded.metadata,
|
|
1181
|
-
first_seen_at = context_graph_memberships.first_seen_at,
|
|
1182
|
-
updated_at = excluded.updated_at
|
|
1808
|
+
this.stmt('upsertContextGraphMember', `
|
|
1809
|
+
INSERT INTO context_graph_memberships (
|
|
1810
|
+
context_graph_id, principal_type, principal_id, role, status, source,
|
|
1811
|
+
display_name, metadata, first_seen_at, updated_at
|
|
1812
|
+
) VALUES (
|
|
1813
|
+
@context_graph_id, @principal_type, @principal_id, @role, @status, @source,
|
|
1814
|
+
@display_name, @metadata, @first_seen_at, @updated_at
|
|
1815
|
+
)
|
|
1816
|
+
ON CONFLICT(context_graph_id, principal_type, principal_id) DO UPDATE SET
|
|
1817
|
+
role = excluded.role,
|
|
1818
|
+
status = excluded.status,
|
|
1819
|
+
source = excluded.source,
|
|
1820
|
+
display_name = excluded.display_name,
|
|
1821
|
+
metadata = excluded.metadata,
|
|
1822
|
+
first_seen_at = context_graph_memberships.first_seen_at,
|
|
1823
|
+
updated_at = excluded.updated_at
|
|
1183
1824
|
`).run({
|
|
1184
1825
|
context_graph_id: record.context_graph_id,
|
|
1185
1826
|
principal_type: record.principal_type,
|
|
@@ -1195,15 +1836,15 @@ export class DashboardDB {
|
|
|
1195
1836
|
}
|
|
1196
1837
|
listContextGraphMembers(contextGraphId) {
|
|
1197
1838
|
if (contextGraphId) {
|
|
1198
|
-
return this.db.prepare(`
|
|
1199
|
-
SELECT * FROM context_graph_memberships
|
|
1200
|
-
WHERE context_graph_id = ?
|
|
1201
|
-
ORDER BY principal_type ASC, principal_id ASC
|
|
1839
|
+
return this.db.prepare(`
|
|
1840
|
+
SELECT * FROM context_graph_memberships
|
|
1841
|
+
WHERE context_graph_id = ?
|
|
1842
|
+
ORDER BY principal_type ASC, principal_id ASC
|
|
1202
1843
|
`).all(contextGraphId);
|
|
1203
1844
|
}
|
|
1204
|
-
return this.db.prepare(`
|
|
1205
|
-
SELECT * FROM context_graph_memberships
|
|
1206
|
-
ORDER BY context_graph_id ASC, principal_type ASC, principal_id ASC
|
|
1845
|
+
return this.db.prepare(`
|
|
1846
|
+
SELECT * FROM context_graph_memberships
|
|
1847
|
+
ORDER BY context_graph_id ASC, principal_type ASC, principal_id ASC
|
|
1207
1848
|
`).all();
|
|
1208
1849
|
}
|
|
1209
1850
|
deleteContextGraphMember(contextGraphId, principalType, principalId) {
|
|
@@ -1215,18 +1856,18 @@ export class DashboardDB {
|
|
|
1215
1856
|
return this.db.prepare('SELECT * FROM metric_snapshots WHERE ts >= ? AND ts <= ? ORDER BY ts').all(from, to);
|
|
1216
1857
|
}
|
|
1217
1858
|
const step = Math.ceil(total.c / maxPoints);
|
|
1218
|
-
return this.db.prepare(`
|
|
1219
|
-
SELECT * FROM (
|
|
1220
|
-
SELECT *, ROW_NUMBER() OVER (ORDER BY ts) as rn
|
|
1221
|
-
FROM metric_snapshots WHERE ts >= ? AND ts <= ?
|
|
1222
|
-
) WHERE rn % ? = 0 ORDER BY ts
|
|
1859
|
+
return this.db.prepare(`
|
|
1860
|
+
SELECT * FROM (
|
|
1861
|
+
SELECT *, ROW_NUMBER() OVER (ORDER BY ts) as rn
|
|
1862
|
+
FROM metric_snapshots WHERE ts >= ? AND ts <= ?
|
|
1863
|
+
) WHERE rn % ? = 0 ORDER BY ts
|
|
1223
1864
|
`).all(from, to, step);
|
|
1224
1865
|
}
|
|
1225
1866
|
// --- Operations ---
|
|
1226
1867
|
insertOperation(op) {
|
|
1227
|
-
this.stmt('insertOp', `
|
|
1228
|
-
INSERT INTO operations (operation_id, operation_name, started_at, status, peer_id, contextGraph_id, details)
|
|
1229
|
-
VALUES (@operation_id, @operation_name, @started_at, 'in_progress', @peer_id, @contextGraph_id, @details)
|
|
1868
|
+
this.stmt('insertOp', `
|
|
1869
|
+
INSERT INTO operations (operation_id, operation_name, started_at, status, peer_id, contextGraph_id, details)
|
|
1870
|
+
VALUES (@operation_id, @operation_name, @started_at, 'in_progress', @peer_id, @contextGraph_id, @details)
|
|
1230
1871
|
`).run({
|
|
1231
1872
|
operation_id: op.operation_id,
|
|
1232
1873
|
operation_name: op.operation_name,
|
|
@@ -1237,10 +1878,10 @@ export class DashboardDB {
|
|
|
1237
1878
|
});
|
|
1238
1879
|
}
|
|
1239
1880
|
completeOperation(op) {
|
|
1240
|
-
this.stmt('completeOp', `
|
|
1241
|
-
UPDATE operations SET status = 'success', duration_ms = @duration_ms,
|
|
1242
|
-
triple_count = @triple_count, details = COALESCE(@details, details)
|
|
1243
|
-
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1881
|
+
this.stmt('completeOp', `
|
|
1882
|
+
UPDATE operations SET status = 'success', duration_ms = @duration_ms,
|
|
1883
|
+
triple_count = @triple_count, details = COALESCE(@details, details)
|
|
1884
|
+
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1244
1885
|
`).run({
|
|
1245
1886
|
operation_id: op.operation_id,
|
|
1246
1887
|
duration_ms: op.duration_ms,
|
|
@@ -1249,10 +1890,10 @@ export class DashboardDB {
|
|
|
1249
1890
|
});
|
|
1250
1891
|
}
|
|
1251
1892
|
failOperation(op) {
|
|
1252
|
-
this.stmt('failOp', `
|
|
1253
|
-
UPDATE operations SET status = 'error', duration_ms = @duration_ms,
|
|
1254
|
-
error_message = @error_message
|
|
1255
|
-
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1893
|
+
this.stmt('failOp', `
|
|
1894
|
+
UPDATE operations SET status = 'error', duration_ms = @duration_ms,
|
|
1895
|
+
error_message = @error_message
|
|
1896
|
+
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1256
1897
|
`).run(op);
|
|
1257
1898
|
}
|
|
1258
1899
|
getOperations(opts = {}) {
|
|
@@ -1309,22 +1950,22 @@ export class DashboardDB {
|
|
|
1309
1950
|
}
|
|
1310
1951
|
getErrorHotspots(periodMs = 7 * 86_400_000) {
|
|
1311
1952
|
const cutoff = Date.now() - periodMs;
|
|
1312
|
-
return this.db.prepare(`
|
|
1313
|
-
SELECT
|
|
1314
|
-
p.phase,
|
|
1315
|
-
o.operation_name,
|
|
1316
|
-
COUNT(*) as error_count,
|
|
1317
|
-
(SELECT p2.details FROM operation_phases p2
|
|
1318
|
-
JOIN operations o2 ON o2.operation_id = p2.operation_id
|
|
1319
|
-
WHERE p2.phase = p.phase AND o2.operation_name = o.operation_name
|
|
1320
|
-
AND p2.status = 'error' AND p2.started_at >= ?
|
|
1321
|
-
ORDER BY p2.started_at DESC LIMIT 1) as last_error,
|
|
1322
|
-
MAX(p.started_at) as last_occurred
|
|
1323
|
-
FROM operation_phases p
|
|
1324
|
-
JOIN operations o ON o.operation_id = p.operation_id
|
|
1325
|
-
WHERE p.status = 'error' AND p.started_at >= ?
|
|
1326
|
-
GROUP BY p.phase, o.operation_name
|
|
1327
|
-
ORDER BY error_count DESC
|
|
1953
|
+
return this.db.prepare(`
|
|
1954
|
+
SELECT
|
|
1955
|
+
p.phase,
|
|
1956
|
+
o.operation_name,
|
|
1957
|
+
COUNT(*) as error_count,
|
|
1958
|
+
(SELECT p2.details FROM operation_phases p2
|
|
1959
|
+
JOIN operations o2 ON o2.operation_id = p2.operation_id
|
|
1960
|
+
WHERE p2.phase = p.phase AND o2.operation_name = o.operation_name
|
|
1961
|
+
AND p2.status = 'error' AND p2.started_at >= ?
|
|
1962
|
+
ORDER BY p2.started_at DESC LIMIT 1) as last_error,
|
|
1963
|
+
MAX(p.started_at) as last_occurred
|
|
1964
|
+
FROM operation_phases p
|
|
1965
|
+
JOIN operations o ON o.operation_id = p.operation_id
|
|
1966
|
+
WHERE p.status = 'error' AND p.started_at >= ?
|
|
1967
|
+
GROUP BY p.phase, o.operation_name
|
|
1968
|
+
ORDER BY error_count DESC
|
|
1328
1969
|
`).all(cutoff, cutoff);
|
|
1329
1970
|
}
|
|
1330
1971
|
getFailedOperations(opts = {}) {
|
|
@@ -1346,17 +1987,17 @@ export class DashboardDB {
|
|
|
1346
1987
|
params.push(like, like, like);
|
|
1347
1988
|
}
|
|
1348
1989
|
params.push(limit);
|
|
1349
|
-
const rows = this.db.prepare(`
|
|
1350
|
-
SELECT
|
|
1351
|
-
o.*,
|
|
1352
|
-
p.phase AS phase,
|
|
1353
|
-
p.details AS phase_error,
|
|
1354
|
-
p.started_at AS phase_started_at
|
|
1355
|
-
FROM operation_phases p
|
|
1356
|
-
JOIN operations o ON o.operation_id = p.operation_id
|
|
1357
|
-
WHERE ${where}
|
|
1358
|
-
ORDER BY p.started_at DESC
|
|
1359
|
-
LIMIT ?
|
|
1990
|
+
const rows = this.db.prepare(`
|
|
1991
|
+
SELECT
|
|
1992
|
+
o.*,
|
|
1993
|
+
p.phase AS phase,
|
|
1994
|
+
p.details AS phase_error,
|
|
1995
|
+
p.started_at AS phase_started_at
|
|
1996
|
+
FROM operation_phases p
|
|
1997
|
+
JOIN operations o ON o.operation_id = p.operation_id
|
|
1998
|
+
WHERE ${where}
|
|
1999
|
+
ORDER BY p.started_at DESC
|
|
2000
|
+
LIMIT ?
|
|
1360
2001
|
`).all(...params);
|
|
1361
2002
|
const operations = rows.map(row => {
|
|
1362
2003
|
const logs = this.db.prepare('SELECT * FROM logs WHERE operation_id = ? ORDER BY ts DESC LIMIT 20').all(row.operation_id);
|
|
@@ -1373,42 +2014,42 @@ export class DashboardDB {
|
|
|
1373
2014
|
}
|
|
1374
2015
|
// --- Operation phases ---
|
|
1375
2016
|
insertPhase(op) {
|
|
1376
|
-
this.stmt('insertPhase', `
|
|
1377
|
-
INSERT INTO operation_phases (operation_id, phase, started_at, status)
|
|
1378
|
-
VALUES (@operation_id, @phase, @started_at, 'in_progress')
|
|
2017
|
+
this.stmt('insertPhase', `
|
|
2018
|
+
INSERT INTO operation_phases (operation_id, phase, started_at, status)
|
|
2019
|
+
VALUES (@operation_id, @phase, @started_at, 'in_progress')
|
|
1379
2020
|
`).run(op);
|
|
1380
2021
|
}
|
|
1381
2022
|
completePhase(op) {
|
|
1382
|
-
this.stmt('completePhase', `
|
|
1383
|
-
UPDATE operation_phases SET status = 'success', duration_ms = @duration_ms
|
|
1384
|
-
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
2023
|
+
this.stmt('completePhase', `
|
|
2024
|
+
UPDATE operation_phases SET status = 'success', duration_ms = @duration_ms
|
|
2025
|
+
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
1385
2026
|
`).run(op);
|
|
1386
2027
|
}
|
|
1387
2028
|
failPhase(op) {
|
|
1388
|
-
this.stmt('failPhase', `
|
|
1389
|
-
UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
|
|
1390
|
-
details = @error_message
|
|
1391
|
-
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
2029
|
+
this.stmt('failPhase', `
|
|
2030
|
+
UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
|
|
2031
|
+
details = @error_message
|
|
2032
|
+
WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
|
|
1392
2033
|
`).run(op);
|
|
1393
2034
|
}
|
|
1394
2035
|
failAllPhases(op) {
|
|
1395
|
-
this.stmt('failAllPhases', `
|
|
1396
|
-
UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
|
|
1397
|
-
details = @error_message
|
|
1398
|
-
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
2036
|
+
this.stmt('failAllPhases', `
|
|
2037
|
+
UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
|
|
2038
|
+
details = @error_message
|
|
2039
|
+
WHERE operation_id = @operation_id AND status = 'in_progress'
|
|
1399
2040
|
`).run(op);
|
|
1400
2041
|
}
|
|
1401
2042
|
// --- Operation cost & tx ---
|
|
1402
2043
|
setOperationCost(op) {
|
|
1403
|
-
this.stmt('setCost', `
|
|
1404
|
-
UPDATE operations SET
|
|
1405
|
-
gas_used = COALESCE(@gas_used, gas_used),
|
|
1406
|
-
gas_price_gwei = COALESCE(@gas_price_gwei, gas_price_gwei),
|
|
1407
|
-
gas_cost_eth = COALESCE(@gas_cost_eth, gas_cost_eth),
|
|
1408
|
-
trac_cost = COALESCE(@trac_cost, trac_cost),
|
|
1409
|
-
tx_hash = COALESCE(@tx_hash, tx_hash),
|
|
1410
|
-
chain_id = COALESCE(@chain_id, chain_id)
|
|
1411
|
-
WHERE operation_id = @operation_id
|
|
2044
|
+
this.stmt('setCost', `
|
|
2045
|
+
UPDATE operations SET
|
|
2046
|
+
gas_used = COALESCE(@gas_used, gas_used),
|
|
2047
|
+
gas_price_gwei = COALESCE(@gas_price_gwei, gas_price_gwei),
|
|
2048
|
+
gas_cost_eth = COALESCE(@gas_cost_eth, gas_cost_eth),
|
|
2049
|
+
trac_cost = COALESCE(@trac_cost, trac_cost),
|
|
2050
|
+
tx_hash = COALESCE(@tx_hash, tx_hash),
|
|
2051
|
+
chain_id = COALESCE(@chain_id, chain_id)
|
|
2052
|
+
WHERE operation_id = @operation_id
|
|
1412
2053
|
`).run({
|
|
1413
2054
|
operation_id: op.operation_id,
|
|
1414
2055
|
gas_used: op.gas_used ?? null,
|
|
@@ -1426,17 +2067,17 @@ export class DashboardDB {
|
|
|
1426
2067
|
const params = [cutoff];
|
|
1427
2068
|
if (opts.name)
|
|
1428
2069
|
params.push(opts.name);
|
|
1429
|
-
const summaryRow = this.db.prepare(`
|
|
1430
|
-
SELECT
|
|
1431
|
-
COUNT(*) as totalCount,
|
|
1432
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
1433
|
-
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errorCount,
|
|
1434
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
1435
|
-
AVG(gas_cost_eth) as avgGasCostEth,
|
|
1436
|
-
SUM(gas_cost_eth) as totalGasCostEth,
|
|
1437
|
-
AVG(trac_cost) as avgTracCost,
|
|
1438
|
-
SUM(trac_cost) as totalTracCost
|
|
1439
|
-
FROM operations WHERE started_at >= ? ${nameFilter}
|
|
2070
|
+
const summaryRow = this.db.prepare(`
|
|
2071
|
+
SELECT
|
|
2072
|
+
COUNT(*) as totalCount,
|
|
2073
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2074
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errorCount,
|
|
2075
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
2076
|
+
AVG(gas_cost_eth) as avgGasCostEth,
|
|
2077
|
+
SUM(gas_cost_eth) as totalGasCostEth,
|
|
2078
|
+
AVG(trac_cost) as avgTracCost,
|
|
2079
|
+
SUM(trac_cost) as totalTracCost
|
|
2080
|
+
FROM operations WHERE started_at >= ? ${nameFilter}
|
|
1440
2081
|
`).get(...params);
|
|
1441
2082
|
const summary = {
|
|
1442
2083
|
totalCount: summaryRow.totalCount ?? 0,
|
|
@@ -1453,17 +2094,17 @@ export class DashboardDB {
|
|
|
1453
2094
|
const tsParams = [bucketSize, cutoff];
|
|
1454
2095
|
if (opts.name)
|
|
1455
2096
|
tsParams.push(opts.name);
|
|
1456
|
-
const timeSeries = this.db.prepare(`
|
|
1457
|
-
SELECT
|
|
1458
|
-
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
1459
|
-
COUNT(*) as count,
|
|
1460
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
1461
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
1462
|
-
AVG(gas_cost_eth) as avgGasCostEth,
|
|
1463
|
-
SUM(gas_cost_eth) as totalGasCostEth
|
|
1464
|
-
FROM operations
|
|
1465
|
-
WHERE started_at >= ? ${nameFilter}
|
|
1466
|
-
GROUP BY bucket ORDER BY bucket
|
|
2097
|
+
const timeSeries = this.db.prepare(`
|
|
2098
|
+
SELECT
|
|
2099
|
+
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
2100
|
+
COUNT(*) as count,
|
|
2101
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2102
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
|
|
2103
|
+
AVG(gas_cost_eth) as avgGasCostEth,
|
|
2104
|
+
SUM(gas_cost_eth) as totalGasCostEth
|
|
2105
|
+
FROM operations
|
|
2106
|
+
WHERE started_at >= ? ${nameFilter}
|
|
2107
|
+
GROUP BY bucket ORDER BY bucket
|
|
1467
2108
|
`).all(bucketSize, bucketSize, ...params);
|
|
1468
2109
|
return {
|
|
1469
2110
|
summary,
|
|
@@ -1480,16 +2121,16 @@ export class DashboardDB {
|
|
|
1480
2121
|
// --- Per-type time series ---
|
|
1481
2122
|
getPerTypeTimeSeries(opts) {
|
|
1482
2123
|
const cutoff = Date.now() - opts.periodMs;
|
|
1483
|
-
const rows = this.db.prepare(`
|
|
1484
|
-
SELECT
|
|
1485
|
-
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
1486
|
-
operation_name as type,
|
|
1487
|
-
COUNT(*) as count,
|
|
1488
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs,
|
|
1489
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
1490
|
-
SUM(gas_cost_eth) as gasCostEth
|
|
1491
|
-
FROM operations WHERE started_at >= ?
|
|
1492
|
-
GROUP BY bucket, operation_name ORDER BY bucket
|
|
2124
|
+
const rows = this.db.prepare(`
|
|
2125
|
+
SELECT
|
|
2126
|
+
(CAST(started_at / ? AS INTEGER) * ?) as bucket,
|
|
2127
|
+
operation_name as type,
|
|
2128
|
+
COUNT(*) as count,
|
|
2129
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs,
|
|
2130
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
|
|
2131
|
+
SUM(gas_cost_eth) as gasCostEth
|
|
2132
|
+
FROM operations WHERE started_at >= ?
|
|
2133
|
+
GROUP BY bucket, operation_name ORDER BY bucket
|
|
1493
2134
|
`).all(opts.bucketMs, opts.bucketMs, cutoff);
|
|
1494
2135
|
const bucketSet = new Set();
|
|
1495
2136
|
const typeSet = new Set();
|
|
@@ -1519,15 +2160,15 @@ export class DashboardDB {
|
|
|
1519
2160
|
// --- Success rates by operation type ---
|
|
1520
2161
|
getSuccessRatesByType(periodMs) {
|
|
1521
2162
|
const cutoff = Date.now() - periodMs;
|
|
1522
|
-
return this.db.prepare(`
|
|
1523
|
-
SELECT
|
|
1524
|
-
operation_name as type,
|
|
1525
|
-
COUNT(*) as total,
|
|
1526
|
-
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
|
|
1527
|
-
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
1528
|
-
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs
|
|
1529
|
-
FROM operations WHERE started_at >= ?
|
|
1530
|
-
GROUP BY operation_name ORDER BY total DESC
|
|
2163
|
+
return this.db.prepare(`
|
|
2164
|
+
SELECT
|
|
2165
|
+
operation_name as type,
|
|
2166
|
+
COUNT(*) as total,
|
|
2167
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
|
|
2168
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
2169
|
+
AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs
|
|
2170
|
+
FROM operations WHERE started_at >= ?
|
|
2171
|
+
GROUP BY operation_name ORDER BY total DESC
|
|
1531
2172
|
`).all(cutoff).map(r => ({
|
|
1532
2173
|
...r,
|
|
1533
2174
|
rate: r.total > 0 ? r.success / r.total : 0,
|
|
@@ -1550,16 +2191,16 @@ export class DashboardDB {
|
|
|
1550
2191
|
// on-chain (Verifiable Memory commits) are counted, so the publish
|
|
1551
2192
|
// count and the TRAC total are consistent. Free SWM/local/testnet
|
|
1552
2193
|
// publishes record trac_cost = 0 and are intentionally excluded.
|
|
1553
|
-
const row = this.db.prepare(`
|
|
1554
|
-
SELECT
|
|
1555
|
-
SUM(CASE WHEN trac_cost > 0 THEN 1 ELSE 0 END) as publishCount,
|
|
1556
|
-
SUM(CASE WHEN status = 'success' AND trac_cost > 0 THEN 1 ELSE 0 END) as successCount,
|
|
1557
|
-
COALESCE(SUM(gas_cost_eth), 0) as totalGasEth,
|
|
1558
|
-
COALESCE(SUM(trac_cost), 0) as totalTrac,
|
|
1559
|
-
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN gas_cost_eth END), 0) as avgGasEth,
|
|
1560
|
-
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN trac_cost END), 0) as avgTrac
|
|
1561
|
-
FROM operations
|
|
1562
|
-
WHERE operation_name = 'publish' AND trac_cost > 0 AND started_at >= ?
|
|
2194
|
+
const row = this.db.prepare(`
|
|
2195
|
+
SELECT
|
|
2196
|
+
SUM(CASE WHEN trac_cost > 0 THEN 1 ELSE 0 END) as publishCount,
|
|
2197
|
+
SUM(CASE WHEN status = 'success' AND trac_cost > 0 THEN 1 ELSE 0 END) as successCount,
|
|
2198
|
+
COALESCE(SUM(gas_cost_eth), 0) as totalGasEth,
|
|
2199
|
+
COALESCE(SUM(trac_cost), 0) as totalTrac,
|
|
2200
|
+
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN gas_cost_eth END), 0) as avgGasEth,
|
|
2201
|
+
COALESCE(AVG(CASE WHEN trac_cost > 0 THEN trac_cost END), 0) as avgTrac
|
|
2202
|
+
FROM operations
|
|
2203
|
+
WHERE operation_name = 'publish' AND trac_cost > 0 AND started_at >= ?
|
|
1563
2204
|
`).get(cutoff);
|
|
1564
2205
|
results.periods.push({
|
|
1565
2206
|
label: p.label,
|
|
@@ -1594,9 +2235,9 @@ export class DashboardDB {
|
|
|
1594
2235
|
* still throw — pinned by db.test.ts.
|
|
1595
2236
|
*/
|
|
1596
2237
|
insertChatMessage(msg) {
|
|
1597
|
-
const info = this.stmt('insertChat', `
|
|
1598
|
-
INSERT INTO chat_messages (ts, direction, peer, peer_name, text, delivered, message_id)
|
|
1599
|
-
VALUES (@ts, @direction, @peer, @peer_name, @text, @delivered, @message_id)
|
|
2238
|
+
const info = this.stmt('insertChat', `
|
|
2239
|
+
INSERT INTO chat_messages (ts, direction, peer, peer_name, text, delivered, message_id)
|
|
2240
|
+
VALUES (@ts, @direction, @peer, @peer_name, @text, @delivered, @message_id)
|
|
1600
2241
|
`).run({
|
|
1601
2242
|
ts: msg.ts,
|
|
1602
2243
|
direction: msg.direction,
|
|
@@ -1687,16 +2328,16 @@ export class DashboardDB {
|
|
|
1687
2328
|
return this.db.prepare('SELECT * FROM chat_persistence_jobs WHERE turn_id = ?').get(turnId);
|
|
1688
2329
|
}
|
|
1689
2330
|
insertChatPersistenceJob(job) {
|
|
1690
|
-
this.stmt('insertChatPersistenceJob', `
|
|
1691
|
-
INSERT INTO chat_persistence_jobs (
|
|
1692
|
-
turn_id, session_id, user_message, assistant_reply, tool_calls_json,
|
|
1693
|
-
status, attempts, max_attempts, next_attempt_at, queued_at, updated_at,
|
|
1694
|
-
store_ms, error_message
|
|
1695
|
-
) VALUES (
|
|
1696
|
-
@turn_id, @session_id, @user_message, @assistant_reply, @tool_calls_json,
|
|
1697
|
-
@status, @attempts, @max_attempts, @next_attempt_at, @queued_at, @updated_at,
|
|
1698
|
-
@store_ms, @error_message
|
|
1699
|
-
)
|
|
2331
|
+
this.stmt('insertChatPersistenceJob', `
|
|
2332
|
+
INSERT INTO chat_persistence_jobs (
|
|
2333
|
+
turn_id, session_id, user_message, assistant_reply, tool_calls_json,
|
|
2334
|
+
status, attempts, max_attempts, next_attempt_at, queued_at, updated_at,
|
|
2335
|
+
store_ms, error_message
|
|
2336
|
+
) VALUES (
|
|
2337
|
+
@turn_id, @session_id, @user_message, @assistant_reply, @tool_calls_json,
|
|
2338
|
+
@status, @attempts, @max_attempts, @next_attempt_at, @queued_at, @updated_at,
|
|
2339
|
+
@store_ms, @error_message
|
|
2340
|
+
)
|
|
1700
2341
|
`).run({
|
|
1701
2342
|
...job,
|
|
1702
2343
|
tool_calls_json: job.tool_calls_json ?? null,
|
|
@@ -1705,46 +2346,46 @@ export class DashboardDB {
|
|
|
1705
2346
|
});
|
|
1706
2347
|
}
|
|
1707
2348
|
markChatPersistenceInProgress(turnId, attempts, updatedAt) {
|
|
1708
|
-
this.stmt('markChatPersistenceInProgress', `
|
|
1709
|
-
UPDATE chat_persistence_jobs
|
|
1710
|
-
SET status = 'in_progress', attempts = ?, updated_at = ?, error_message = NULL
|
|
1711
|
-
WHERE turn_id = ?
|
|
2349
|
+
this.stmt('markChatPersistenceInProgress', `
|
|
2350
|
+
UPDATE chat_persistence_jobs
|
|
2351
|
+
SET status = 'in_progress', attempts = ?, updated_at = ?, error_message = NULL
|
|
2352
|
+
WHERE turn_id = ?
|
|
1712
2353
|
`).run(attempts, updatedAt, turnId);
|
|
1713
2354
|
}
|
|
1714
2355
|
markChatPersistenceStored(turnId, storeMs, updatedAt) {
|
|
1715
|
-
this.stmt('markChatPersistenceStored', `
|
|
1716
|
-
UPDATE chat_persistence_jobs
|
|
1717
|
-
SET status = 'stored', store_ms = ?, updated_at = ?, error_message = NULL
|
|
1718
|
-
WHERE turn_id = ?
|
|
2356
|
+
this.stmt('markChatPersistenceStored', `
|
|
2357
|
+
UPDATE chat_persistence_jobs
|
|
2358
|
+
SET status = 'stored', store_ms = ?, updated_at = ?, error_message = NULL
|
|
2359
|
+
WHERE turn_id = ?
|
|
1719
2360
|
`).run(storeMs, updatedAt, turnId);
|
|
1720
2361
|
}
|
|
1721
2362
|
markChatPersistencePendingRetry(turnId, attempts, nextAttemptAt, updatedAt, errorMessage) {
|
|
1722
|
-
this.stmt('markChatPersistencePendingRetry', `
|
|
1723
|
-
UPDATE chat_persistence_jobs
|
|
1724
|
-
SET status = 'pending', attempts = ?, next_attempt_at = ?, updated_at = ?, error_message = ?
|
|
1725
|
-
WHERE turn_id = ?
|
|
2363
|
+
this.stmt('markChatPersistencePendingRetry', `
|
|
2364
|
+
UPDATE chat_persistence_jobs
|
|
2365
|
+
SET status = 'pending', attempts = ?, next_attempt_at = ?, updated_at = ?, error_message = ?
|
|
2366
|
+
WHERE turn_id = ?
|
|
1726
2367
|
`).run(attempts, nextAttemptAt, updatedAt, errorMessage, turnId);
|
|
1727
2368
|
}
|
|
1728
2369
|
markChatPersistenceFailed(turnId, attempts, updatedAt, errorMessage) {
|
|
1729
|
-
this.stmt('markChatPersistenceFailed', `
|
|
1730
|
-
UPDATE chat_persistence_jobs
|
|
1731
|
-
SET status = 'failed', attempts = ?, updated_at = ?, error_message = ?
|
|
1732
|
-
WHERE turn_id = ?
|
|
2370
|
+
this.stmt('markChatPersistenceFailed', `
|
|
2371
|
+
UPDATE chat_persistence_jobs
|
|
2372
|
+
SET status = 'failed', attempts = ?, updated_at = ?, error_message = ?
|
|
2373
|
+
WHERE turn_id = ?
|
|
1733
2374
|
`).run(attempts, updatedAt, errorMessage, turnId);
|
|
1734
2375
|
}
|
|
1735
2376
|
recoverInProgressChatPersistenceJobs(now) {
|
|
1736
|
-
this.stmt('recoverInProgressChatPersistenceJobs', `
|
|
1737
|
-
UPDATE chat_persistence_jobs
|
|
1738
|
-
SET status = 'pending', next_attempt_at = ?, updated_at = ?
|
|
1739
|
-
WHERE status = 'in_progress'
|
|
2377
|
+
this.stmt('recoverInProgressChatPersistenceJobs', `
|
|
2378
|
+
UPDATE chat_persistence_jobs
|
|
2379
|
+
SET status = 'pending', next_attempt_at = ?, updated_at = ?
|
|
2380
|
+
WHERE status = 'in_progress'
|
|
1740
2381
|
`).run(now, now);
|
|
1741
2382
|
}
|
|
1742
2383
|
getRunnableChatPersistenceJobs(now, limit = 10) {
|
|
1743
|
-
return this.db.prepare(`
|
|
1744
|
-
SELECT * FROM chat_persistence_jobs
|
|
1745
|
-
WHERE status = 'pending' AND next_attempt_at <= ?
|
|
1746
|
-
ORDER BY next_attempt_at ASC, queued_at ASC
|
|
1747
|
-
LIMIT ?
|
|
2384
|
+
return this.db.prepare(`
|
|
2385
|
+
SELECT * FROM chat_persistence_jobs
|
|
2386
|
+
WHERE status = 'pending' AND next_attempt_at <= ?
|
|
2387
|
+
ORDER BY next_attempt_at ASC, queued_at ASC
|
|
2388
|
+
LIMIT ?
|
|
1748
2389
|
`).all(now, limit);
|
|
1749
2390
|
}
|
|
1750
2391
|
getNextPendingChatPersistenceAt() {
|
|
@@ -1752,19 +2393,19 @@ export class DashboardDB {
|
|
|
1752
2393
|
return row?.next_at ?? null;
|
|
1753
2394
|
}
|
|
1754
2395
|
getChatPersistenceHealth(now) {
|
|
1755
|
-
const counts = this.db.prepare(`
|
|
1756
|
-
SELECT
|
|
1757
|
-
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
|
|
1758
|
-
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_count,
|
|
1759
|
-
SUM(CASE WHEN status = 'stored' THEN 1 ELSE 0 END) AS stored_count,
|
|
1760
|
-
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_count,
|
|
1761
|
-
SUM(CASE WHEN status = 'pending' AND next_attempt_at < ? THEN 1 ELSE 0 END) AS overdue_pending_count
|
|
1762
|
-
FROM chat_persistence_jobs
|
|
2396
|
+
const counts = this.db.prepare(`
|
|
2397
|
+
SELECT
|
|
2398
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
|
|
2399
|
+
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_count,
|
|
2400
|
+
SUM(CASE WHEN status = 'stored' THEN 1 ELSE 0 END) AS stored_count,
|
|
2401
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_count,
|
|
2402
|
+
SUM(CASE WHEN status = 'pending' AND next_attempt_at < ? THEN 1 ELSE 0 END) AS overdue_pending_count
|
|
2403
|
+
FROM chat_persistence_jobs
|
|
1763
2404
|
`).get(now);
|
|
1764
|
-
const oldest = this.db.prepare(`
|
|
1765
|
-
SELECT MIN(queued_at) AS oldest_pending_queued_at
|
|
1766
|
-
FROM chat_persistence_jobs
|
|
1767
|
-
WHERE status = 'pending'
|
|
2405
|
+
const oldest = this.db.prepare(`
|
|
2406
|
+
SELECT MIN(queued_at) AS oldest_pending_queued_at
|
|
2407
|
+
FROM chat_persistence_jobs
|
|
2408
|
+
WHERE status = 'pending'
|
|
1768
2409
|
`).get();
|
|
1769
2410
|
return {
|
|
1770
2411
|
pending_count: counts?.pending_count ?? 0,
|
|
@@ -1777,9 +2418,9 @@ export class DashboardDB {
|
|
|
1777
2418
|
}
|
|
1778
2419
|
// --- Logs ---
|
|
1779
2420
|
insertLog(entry) {
|
|
1780
|
-
this.stmt('insertLog', `
|
|
1781
|
-
INSERT INTO logs (ts, level, operation_name, operation_id, module, message)
|
|
1782
|
-
VALUES (@ts, @level, @operation_name, @operation_id, @module, @message)
|
|
2421
|
+
this.stmt('insertLog', `
|
|
2422
|
+
INSERT INTO logs (ts, level, operation_name, operation_id, module, message)
|
|
2423
|
+
VALUES (@ts, @level, @operation_name, @operation_id, @module, @message)
|
|
1783
2424
|
`).run({
|
|
1784
2425
|
ts: entry.ts,
|
|
1785
2426
|
level: entry.level,
|
|
@@ -1834,9 +2475,9 @@ export class DashboardDB {
|
|
|
1834
2475
|
}
|
|
1835
2476
|
// --- Query history ---
|
|
1836
2477
|
insertQueryHistory(entry) {
|
|
1837
|
-
this.stmt('insertQueryHistory', `
|
|
1838
|
-
INSERT INTO query_history (ts, sparql, duration_ms, result_count, error)
|
|
1839
|
-
VALUES (@ts, @sparql, @duration_ms, @result_count, @error)
|
|
2478
|
+
this.stmt('insertQueryHistory', `
|
|
2479
|
+
INSERT INTO query_history (ts, sparql, duration_ms, result_count, error)
|
|
2480
|
+
VALUES (@ts, @sparql, @duration_ms, @result_count, @error)
|
|
1840
2481
|
`).run({
|
|
1841
2482
|
ts: Date.now(),
|
|
1842
2483
|
sparql: entry.sparql,
|
|
@@ -1880,9 +2521,9 @@ export class DashboardDB {
|
|
|
1880
2521
|
}
|
|
1881
2522
|
// --- Notifications ---
|
|
1882
2523
|
insertNotification(n) {
|
|
1883
|
-
const result = this.stmt('insertNotif', `
|
|
1884
|
-
INSERT INTO notifications (ts, type, title, message, source, peer, read, meta, context_graph_id)
|
|
1885
|
-
VALUES (@ts, @type, @title, @message, @source, @peer, 0, @meta, @contextGraphId)
|
|
2524
|
+
const result = this.stmt('insertNotif', `
|
|
2525
|
+
INSERT INTO notifications (ts, type, title, message, source, peer, read, meta, context_graph_id)
|
|
2526
|
+
VALUES (@ts, @type, @title, @message, @source, @peer, 0, @meta, @contextGraphId)
|
|
1886
2527
|
`).run({
|
|
1887
2528
|
ts: n.ts,
|
|
1888
2529
|
type: n.type,
|
|
@@ -1918,8 +2559,8 @@ export class DashboardDB {
|
|
|
1918
2559
|
if (cgIds.length === 0)
|
|
1919
2560
|
return [];
|
|
1920
2561
|
const placeholders = cgIds.map(() => '?').join(',');
|
|
1921
|
-
return this.db.prepare(`SELECT * FROM notifications
|
|
1922
|
-
WHERE context_graph_id IN (${placeholders})
|
|
2562
|
+
return this.db.prepare(`SELECT * FROM notifications
|
|
2563
|
+
WHERE context_graph_id IN (${placeholders})
|
|
1923
2564
|
ORDER BY ts DESC LIMIT ?`).all(...cgIds, limit);
|
|
1924
2565
|
}
|
|
1925
2566
|
/**
|
|
@@ -1950,8 +2591,8 @@ export class DashboardDB {
|
|
|
1950
2591
|
if (types.length === 0)
|
|
1951
2592
|
return [];
|
|
1952
2593
|
const placeholders = types.map(() => '?').join(',');
|
|
1953
|
-
return this.db.prepare(`SELECT * FROM notifications
|
|
1954
|
-
WHERE type IN (${placeholders})
|
|
2594
|
+
return this.db.prepare(`SELECT * FROM notifications
|
|
2595
|
+
WHERE type IN (${placeholders})
|
|
1955
2596
|
ORDER BY ts DESC LIMIT ?`).all(...types, limit);
|
|
1956
2597
|
}
|
|
1957
2598
|
/**
|
|
@@ -1971,9 +2612,9 @@ export class DashboardDB {
|
|
|
1971
2612
|
const wallet = walletAddress.trim().toLowerCase();
|
|
1972
2613
|
if (!wallet)
|
|
1973
2614
|
return [];
|
|
1974
|
-
return this.db.prepare(`SELECT * FROM notifications
|
|
1975
|
-
WHERE type = 'pca_cost_covered'
|
|
1976
|
-
AND lower(json_extract(meta, '$.publisherAddress')) = ?
|
|
2615
|
+
return this.db.prepare(`SELECT * FROM notifications
|
|
2616
|
+
WHERE type = 'pca_cost_covered'
|
|
2617
|
+
AND lower(json_extract(meta, '$.publisherAddress')) = ?
|
|
1977
2618
|
ORDER BY ts DESC LIMIT ?`).all(wallet, limit);
|
|
1978
2619
|
}
|
|
1979
2620
|
markNotificationsRead(ids) {
|
|
@@ -2003,10 +2644,10 @@ export class DashboardDB {
|
|
|
2003
2644
|
return [];
|
|
2004
2645
|
const windowStart = parsed.windowBucket * ACTIVITY_DIGEST_WINDOW_MS;
|
|
2005
2646
|
const windowEnd = windowStart + ACTIVITY_DIGEST_WINDOW_MS;
|
|
2006
|
-
const rows = this.db.prepare(`SELECT id FROM notifications
|
|
2007
|
-
WHERE context_graph_id = ?
|
|
2008
|
-
AND type = ?
|
|
2009
|
-
AND ts >= ? AND ts < ?
|
|
2647
|
+
const rows = this.db.prepare(`SELECT id FROM notifications
|
|
2648
|
+
WHERE context_graph_id = ?
|
|
2649
|
+
AND type = ?
|
|
2650
|
+
AND ts >= ? AND ts < ?
|
|
2010
2651
|
AND json_extract(meta, '$.kind') = ?`).all(parsed.contextGraphId, ASSERTION_ACTIVITY_TYPE, windowStart, windowEnd, parsed.kind);
|
|
2011
2652
|
return rows.map((r) => r.id);
|
|
2012
2653
|
}
|
|
@@ -2027,32 +2668,72 @@ export class SqliteSyncCheckpointStore {
|
|
|
2027
2668
|
}
|
|
2028
2669
|
get(key) {
|
|
2029
2670
|
const now = this.clock();
|
|
2030
|
-
const row = this.db.prepare(`SELECT offset, updated_at, expires_at
|
|
2671
|
+
const row = this.db.prepare(`SELECT offset, updated_at, expires_at,
|
|
2672
|
+
responder_session_id, responder_session_expires_at
|
|
2673
|
+
FROM sync_checkpoints WHERE key = ?`).get(key);
|
|
2031
2674
|
if (!row)
|
|
2032
2675
|
return undefined;
|
|
2033
2676
|
if (row.expires_at < now) {
|
|
2034
2677
|
this.delete(key);
|
|
2035
2678
|
return undefined;
|
|
2036
2679
|
}
|
|
2680
|
+
const hasFreshResponderSession = Boolean(row.responder_session_id)
|
|
2681
|
+
&& Number.isSafeInteger(row.responder_session_expires_at)
|
|
2682
|
+
&& (row.responder_session_expires_at ?? 0) > now;
|
|
2683
|
+
if (row.responder_session_id && !hasFreshResponderSession) {
|
|
2684
|
+
this.clearResponderSession(key);
|
|
2685
|
+
}
|
|
2037
2686
|
return {
|
|
2038
2687
|
offset: row.offset,
|
|
2039
2688
|
updatedAtMs: row.updated_at,
|
|
2040
2689
|
expiresAtMs: row.expires_at,
|
|
2690
|
+
...(hasFreshResponderSession
|
|
2691
|
+
? {
|
|
2692
|
+
responderSessionId: row.responder_session_id,
|
|
2693
|
+
responderSessionExpiresAtMs: row.responder_session_expires_at,
|
|
2694
|
+
}
|
|
2695
|
+
: {}),
|
|
2041
2696
|
};
|
|
2042
2697
|
}
|
|
2043
2698
|
set(key, value, nowMs = this.clock()) {
|
|
2044
2699
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2045
2700
|
throw new Error(`Invalid sync checkpoint offset for ${key}: ${value}`);
|
|
2046
2701
|
}
|
|
2047
|
-
this.db.prepare(`
|
|
2048
|
-
INSERT INTO sync_checkpoints (key, offset, updated_at, expires_at)
|
|
2049
|
-
VALUES (?, ?, ?, ?)
|
|
2050
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
2051
|
-
offset = excluded.offset,
|
|
2052
|
-
updated_at = excluded.updated_at,
|
|
2053
|
-
expires_at = excluded.expires_at
|
|
2702
|
+
this.db.prepare(`
|
|
2703
|
+
INSERT INTO sync_checkpoints (key, offset, updated_at, expires_at)
|
|
2704
|
+
VALUES (?, ?, ?, ?)
|
|
2705
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
2706
|
+
offset = excluded.offset,
|
|
2707
|
+
updated_at = excluded.updated_at,
|
|
2708
|
+
expires_at = excluded.expires_at
|
|
2054
2709
|
`).run(key, value, nowMs, nowMs + this.ttlMs);
|
|
2055
2710
|
}
|
|
2711
|
+
setResponderSession(key, sessionId, expiresAtMs, nowMs = this.clock()) {
|
|
2712
|
+
if (!sessionId || !Number.isSafeInteger(expiresAtMs)) {
|
|
2713
|
+
throw new Error(`Invalid sync responder session for ${key}`);
|
|
2714
|
+
}
|
|
2715
|
+
if (expiresAtMs <= nowMs) {
|
|
2716
|
+
this.clearResponderSession(key);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
this.db.prepare(`
|
|
2720
|
+
INSERT INTO sync_checkpoints (
|
|
2721
|
+
key, offset, updated_at, expires_at,
|
|
2722
|
+
responder_session_id, responder_session_expires_at
|
|
2723
|
+
) VALUES (?, 0, ?, ?, ?, ?)
|
|
2724
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
2725
|
+
responder_session_id = excluded.responder_session_id,
|
|
2726
|
+
responder_session_expires_at = excluded.responder_session_expires_at
|
|
2727
|
+
`).run(key, nowMs, nowMs + this.ttlMs, sessionId, expiresAtMs);
|
|
2728
|
+
}
|
|
2729
|
+
clearResponderSession(key) {
|
|
2730
|
+
this.db.prepare(`
|
|
2731
|
+
UPDATE sync_checkpoints
|
|
2732
|
+
SET responder_session_id = NULL,
|
|
2733
|
+
responder_session_expires_at = NULL
|
|
2734
|
+
WHERE key = ?
|
|
2735
|
+
`).run(key);
|
|
2736
|
+
}
|
|
2056
2737
|
delete(key) {
|
|
2057
2738
|
this.db.prepare(`DELETE FROM sync_checkpoints WHERE key = ?`).run(key);
|
|
2058
2739
|
}
|
|
@@ -2084,13 +2765,13 @@ export class SqliteChangelogCursorStore {
|
|
|
2084
2765
|
if (!Number.isSafeInteger(seq) || seq < 0) {
|
|
2085
2766
|
throw new Error(`Invalid changelog cursor seq for ${peerId}/${contextGraphId}: ${seq}`);
|
|
2086
2767
|
}
|
|
2087
|
-
this.db.prepare(`
|
|
2088
|
-
INSERT INTO changelog_cursors (peer_id, context_graph_id, era, seq, updated_at)
|
|
2089
|
-
VALUES (?, ?, ?, ?, ?)
|
|
2090
|
-
ON CONFLICT(peer_id, context_graph_id) DO UPDATE SET
|
|
2091
|
-
era = excluded.era,
|
|
2092
|
-
seq = excluded.seq,
|
|
2093
|
-
updated_at = excluded.updated_at
|
|
2768
|
+
this.db.prepare(`
|
|
2769
|
+
INSERT INTO changelog_cursors (peer_id, context_graph_id, era, seq, updated_at)
|
|
2770
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2771
|
+
ON CONFLICT(peer_id, context_graph_id) DO UPDATE SET
|
|
2772
|
+
era = excluded.era,
|
|
2773
|
+
seq = excluded.seq,
|
|
2774
|
+
updated_at = excluded.updated_at
|
|
2094
2775
|
`).run(peerId, contextGraphId, era, seq, nowMs);
|
|
2095
2776
|
}
|
|
2096
2777
|
}
|
|
@@ -2118,13 +2799,13 @@ export class SqliteChangelogEraGuard {
|
|
|
2118
2799
|
if (!Number.isSafeInteger(highSeq) || highSeq < 0) {
|
|
2119
2800
|
throw new Error(`Invalid changelog era high_seq: ${highSeq}`);
|
|
2120
2801
|
}
|
|
2121
|
-
this.db.prepare(`
|
|
2122
|
-
INSERT INTO changelog_era (id, era, high_seq, updated_at)
|
|
2123
|
-
VALUES (1, ?, ?, ?)
|
|
2124
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
2125
|
-
era = excluded.era,
|
|
2126
|
-
high_seq = excluded.high_seq,
|
|
2127
|
-
updated_at = excluded.updated_at
|
|
2802
|
+
this.db.prepare(`
|
|
2803
|
+
INSERT INTO changelog_era (id, era, high_seq, updated_at)
|
|
2804
|
+
VALUES (1, ?, ?, ?)
|
|
2805
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2806
|
+
era = excluded.era,
|
|
2807
|
+
high_seq = excluded.high_seq,
|
|
2808
|
+
updated_at = excluded.updated_at
|
|
2128
2809
|
`).run(era, highSeq, this.clock());
|
|
2129
2810
|
}
|
|
2130
2811
|
}
|
|
@@ -2158,7 +2839,7 @@ export class SqliteMessageIdempotencyStore {
|
|
|
2158
2839
|
}
|
|
2159
2840
|
check(peer, protocol, messageId, direction) {
|
|
2160
2841
|
const row = this.db
|
|
2161
|
-
.prepare(`SELECT response_blob FROM message_idempotency
|
|
2842
|
+
.prepare(`SELECT response_blob FROM message_idempotency
|
|
2162
2843
|
WHERE peer_id = ? AND protocol = ? AND message_id = ? AND direction = ?`)
|
|
2163
2844
|
.get(peer, protocol, messageId, direction);
|
|
2164
2845
|
if (!row)
|
|
@@ -2188,9 +2869,9 @@ export class SqliteMessageIdempotencyStore {
|
|
|
2188
2869
|
// SqliteError so the substrate's bug doesn't disguise itself as
|
|
2189
2870
|
// a normal duplicate.
|
|
2190
2871
|
this.db
|
|
2191
|
-
.prepare(`INSERT INTO message_idempotency
|
|
2192
|
-
(peer_id, protocol, message_id, direction, response_blob, response_size, ts)
|
|
2193
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2872
|
+
.prepare(`INSERT INTO message_idempotency
|
|
2873
|
+
(peer_id, protocol, message_id, direction, response_blob, response_size, ts)
|
|
2874
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2194
2875
|
ON CONFLICT (peer_id, protocol, message_id, direction) DO NOTHING`)
|
|
2195
2876
|
.run(peer, protocol, messageId, direction, blob, responseSize, this.clock());
|
|
2196
2877
|
}
|
|
@@ -2215,15 +2896,15 @@ export class SqliteProtocolOutboxStore {
|
|
|
2215
2896
|
}
|
|
2216
2897
|
enqueue(peer, protocol, messageId, payload, error, now) {
|
|
2217
2898
|
const existing = this.db
|
|
2218
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
2899
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
2219
2900
|
WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
|
|
2220
2901
|
.get(peer, protocol, messageId);
|
|
2221
2902
|
if (existing) {
|
|
2222
2903
|
const newAttempts = existing.attempts + 1;
|
|
2223
2904
|
const nextAttemptAt = now + this.backoffFor(newAttempts);
|
|
2224
2905
|
this.db
|
|
2225
|
-
.prepare(`UPDATE protocol_outbox
|
|
2226
|
-
SET attempts = ?, last_attempt_at = ?, next_attempt_at = ?, last_error = ?
|
|
2906
|
+
.prepare(`UPDATE protocol_outbox
|
|
2907
|
+
SET attempts = ?, last_attempt_at = ?, next_attempt_at = ?, last_error = ?
|
|
2227
2908
|
WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
|
|
2228
2909
|
.run(newAttempts, now, nextAttemptAt, error, peer, protocol, messageId);
|
|
2229
2910
|
return {
|
|
@@ -2242,9 +2923,9 @@ export class SqliteProtocolOutboxStore {
|
|
|
2242
2923
|
const nextAttemptAt = now + this.backoffFor(attempts);
|
|
2243
2924
|
const blob = Buffer.from(payload);
|
|
2244
2925
|
this.db
|
|
2245
|
-
.prepare(`INSERT INTO protocol_outbox
|
|
2246
|
-
(peer_id, protocol, message_id, payload, attempts,
|
|
2247
|
-
first_failure_at, last_attempt_at, next_attempt_at, last_error)
|
|
2926
|
+
.prepare(`INSERT INTO protocol_outbox
|
|
2927
|
+
(peer_id, protocol, message_id, payload, attempts,
|
|
2928
|
+
first_failure_at, last_attempt_at, next_attempt_at, last_error)
|
|
2248
2929
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
2249
2930
|
.run(peer, protocol, messageId, blob, attempts, now, now, nextAttemptAt, error);
|
|
2250
2931
|
return {
|
|
@@ -2261,14 +2942,14 @@ export class SqliteProtocolOutboxStore {
|
|
|
2261
2942
|
}
|
|
2262
2943
|
markDelivered(peer, protocol, messageId) {
|
|
2263
2944
|
const result = this.db
|
|
2264
|
-
.prepare(`DELETE FROM protocol_outbox
|
|
2945
|
+
.prepare(`DELETE FROM protocol_outbox
|
|
2265
2946
|
WHERE peer_id = ? AND protocol = ? AND message_id = ?`)
|
|
2266
2947
|
.run(peer, protocol, messageId);
|
|
2267
2948
|
return result.changes > 0;
|
|
2268
2949
|
}
|
|
2269
2950
|
hasEntry(peer, protocol, messageId) {
|
|
2270
2951
|
const row = this.db
|
|
2271
|
-
.prepare(`SELECT 1 FROM protocol_outbox
|
|
2952
|
+
.prepare(`SELECT 1 FROM protocol_outbox
|
|
2272
2953
|
WHERE peer_id = ? AND protocol = ? AND message_id = ? LIMIT 1`)
|
|
2273
2954
|
.get(peer, protocol, messageId);
|
|
2274
2955
|
return row !== undefined;
|
|
@@ -2278,27 +2959,27 @@ export class SqliteProtocolOutboxStore {
|
|
|
2278
2959
|
}
|
|
2279
2960
|
pendingFor(peer) {
|
|
2280
2961
|
const rows = this.db
|
|
2281
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
2282
|
-
WHERE peer_id = ?
|
|
2962
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
2963
|
+
WHERE peer_id = ?
|
|
2283
2964
|
ORDER BY first_failure_at ASC, protocol ASC, message_id ASC`)
|
|
2284
2965
|
.all(peer);
|
|
2285
2966
|
return rows.map(SqliteProtocolOutboxStore.rowToEntry);
|
|
2286
2967
|
}
|
|
2287
2968
|
due(now) {
|
|
2288
2969
|
const rows = this.db
|
|
2289
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
2290
|
-
WHERE next_attempt_at <= ?
|
|
2291
|
-
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
2970
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
2971
|
+
WHERE next_attempt_at <= ?
|
|
2972
|
+
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
2292
2973
|
peer_id ASC, protocol ASC, message_id ASC`)
|
|
2293
2974
|
.all(now);
|
|
2294
2975
|
return rows.map(SqliteProtocolOutboxStore.rowToEntry);
|
|
2295
2976
|
}
|
|
2296
2977
|
duePage(now, limit) {
|
|
2297
2978
|
const rows = this.db
|
|
2298
|
-
.prepare(`SELECT * FROM protocol_outbox
|
|
2299
|
-
WHERE next_attempt_at <= ?
|
|
2300
|
-
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
2301
|
-
peer_id ASC, protocol ASC, message_id ASC
|
|
2979
|
+
.prepare(`SELECT * FROM protocol_outbox
|
|
2980
|
+
WHERE next_attempt_at <= ?
|
|
2981
|
+
ORDER BY next_attempt_at ASC, first_failure_at ASC,
|
|
2982
|
+
peer_id ASC, protocol ASC, message_id ASC
|
|
2302
2983
|
LIMIT ?`)
|
|
2303
2984
|
.all(now, limit);
|
|
2304
2985
|
return rows.map(SqliteProtocolOutboxStore.rowToEntry);
|
|
@@ -2388,9 +3069,9 @@ export class SqliteKaNumberStore {
|
|
|
2388
3069
|
// `safeIntegers(true)` opts INTO bigint returns so the counter is
|
|
2389
3070
|
// exact past `Number.MAX_SAFE_INTEGER` (codex PR #976 F6).
|
|
2390
3071
|
const row = this.db
|
|
2391
|
-
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
2392
|
-
VALUES (?, 1)
|
|
2393
|
-
ON CONFLICT(author_address) DO UPDATE SET next_number = next_number + 1
|
|
3072
|
+
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
3073
|
+
VALUES (?, 1)
|
|
3074
|
+
ON CONFLICT(author_address) DO UPDATE SET next_number = next_number + 1
|
|
2394
3075
|
RETURNING next_number - 1 AS number`)
|
|
2395
3076
|
.safeIntegers(true)
|
|
2396
3077
|
.get(author);
|
|
@@ -2402,9 +3083,9 @@ export class SqliteKaNumberStore {
|
|
|
2402
3083
|
// `excluded.next_number` is the proposed floor from the VALUES row.
|
|
2403
3084
|
// `better-sqlite3` accepts bigint statement parameters natively.
|
|
2404
3085
|
this.db
|
|
2405
|
-
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
2406
|
-
VALUES (?, ?)
|
|
2407
|
-
ON CONFLICT(author_address) DO UPDATE SET
|
|
3086
|
+
.prepare(`INSERT INTO ka_numbers (author_address, next_number)
|
|
3087
|
+
VALUES (?, ?)
|
|
3088
|
+
ON CONFLICT(author_address) DO UPDATE SET
|
|
2408
3089
|
next_number = MAX(next_number, excluded.next_number)`)
|
|
2409
3090
|
.run(author, nextNumberFloor);
|
|
2410
3091
|
}
|