crosscheck-mcp 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-ext.cjs +47 -11
- package/dist/browser-ext.cjs.map +1 -1
- package/dist/browser-ext.js +47 -11
- package/dist/browser-ext.js.map +1 -1
- package/dist/node-stdio.cjs +76 -11
- package/dist/node-stdio.cjs.map +1 -1
- package/dist/node-stdio.js +76 -11
- package/dist/node-stdio.js.map +1 -1
- package/dist/self-check.cjs +1000 -0
- package/dist/self-check.cjs.map +1 -0
- package/dist/self-check.d.cts +30 -0
- package/dist/self-check.d.ts +30 -0
- package/dist/self-check.js +977 -0
- package/dist/self-check.js.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
35
|
+
var init_cjs_shims = __esm({
|
|
36
|
+
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// src/adapters/storage/migrations/0001_init.ts
|
|
42
|
+
var m0001_init;
|
|
43
|
+
var init_init = __esm({
|
|
44
|
+
"src/adapters/storage/migrations/0001_init.ts"() {
|
|
45
|
+
"use strict";
|
|
46
|
+
init_cjs_shims();
|
|
47
|
+
m0001_init = {
|
|
48
|
+
id: "0001_init",
|
|
49
|
+
name: "initial schema (sessions, usage_log, claims, ...)",
|
|
50
|
+
up: [
|
|
51
|
+
// sessions — includes the totals columns that Python adds via ALTER.
|
|
52
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
53
|
+
session_id TEXT PRIMARY KEY,
|
|
54
|
+
started_at INTEGER NOT NULL,
|
|
55
|
+
last_at INTEGER,
|
|
56
|
+
calls INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
wall_ms INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
cache_hits INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
total_prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
total_completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
total_cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
total_cost_usd REAL NOT NULL DEFAULT 0.0,
|
|
64
|
+
total_cpu_ms INTEGER NOT NULL DEFAULT 0
|
|
65
|
+
)`,
|
|
66
|
+
// usage_log — per-call ledger.
|
|
67
|
+
`CREATE TABLE IF NOT EXISTS usage_log (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
session_id TEXT NOT NULL,
|
|
70
|
+
ts INTEGER NOT NULL,
|
|
71
|
+
tool TEXT,
|
|
72
|
+
purpose TEXT NOT NULL,
|
|
73
|
+
provider TEXT NOT NULL,
|
|
74
|
+
model TEXT NOT NULL,
|
|
75
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
cost_usd REAL NOT NULL DEFAULT 0.0,
|
|
80
|
+
estimated INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
wall_ms INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
cpu_ms INTEGER NOT NULL DEFAULT 0
|
|
83
|
+
)`,
|
|
84
|
+
`CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_log(session_id)`,
|
|
85
|
+
`CREATE INDEX IF NOT EXISTS idx_usage_provider ON usage_log(provider)`,
|
|
86
|
+
// claims (FK to sessions, ON DELETE CASCADE).
|
|
87
|
+
`CREATE TABLE IF NOT EXISTS claims (
|
|
88
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
|
+
session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
90
|
+
text TEXT NOT NULL,
|
|
91
|
+
provider TEXT,
|
|
92
|
+
confidence REAL,
|
|
93
|
+
citations_json TEXT,
|
|
94
|
+
kind TEXT,
|
|
95
|
+
created_at INTEGER NOT NULL
|
|
96
|
+
)`,
|
|
97
|
+
`CREATE INDEX IF NOT EXISTS idx_claims_session ON claims(session_id)`,
|
|
98
|
+
// claim_links (FK to claims). Kind enum widened in Python's
|
|
99
|
+
// _migrate_claim_links_check; mirrored here as the CHECK clause.
|
|
100
|
+
`CREATE TABLE IF NOT EXISTS claim_links (
|
|
101
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
102
|
+
src_id INTEGER NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
|
103
|
+
dst_id INTEGER NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
|
104
|
+
kind TEXT NOT NULL CHECK (kind IN ('supports','attacks','derives_from','merges_with')),
|
|
105
|
+
created_at INTEGER NOT NULL,
|
|
106
|
+
UNIQUE(src_id, dst_id, kind)
|
|
107
|
+
)`,
|
|
108
|
+
`CREATE INDEX IF NOT EXISTS idx_links_src ON claim_links(src_id)`,
|
|
109
|
+
`CREATE INDEX IF NOT EXISTS idx_links_dst ON claim_links(dst_id)`,
|
|
110
|
+
// provider_stats — ballots accumulator for the smart router.
|
|
111
|
+
`CREATE TABLE IF NOT EXISTS provider_stats (
|
|
112
|
+
provider TEXT PRIMARY KEY,
|
|
113
|
+
wins INTEGER NOT NULL DEFAULT 0,
|
|
114
|
+
losses INTEGER NOT NULL DEFAULT 0,
|
|
115
|
+
abstains INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
last_at INTEGER
|
|
117
|
+
)`,
|
|
118
|
+
// delegations — cross-model handshake ledger.
|
|
119
|
+
`CREATE TABLE IF NOT EXISTS delegations (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
session_id TEXT,
|
|
122
|
+
requester TEXT,
|
|
123
|
+
tool_call TEXT NOT NULL,
|
|
124
|
+
via TEXT NOT NULL,
|
|
125
|
+
accepted INTEGER NOT NULL,
|
|
126
|
+
created_at INTEGER NOT NULL
|
|
127
|
+
)`,
|
|
128
|
+
`CREATE INDEX IF NOT EXISTS idx_deleg_session ON delegations(session_id)`,
|
|
129
|
+
`CREATE INDEX IF NOT EXISTS idx_deleg_req ON delegations(requester)`,
|
|
130
|
+
// session_memory — facts / open_questions / decisions ledger.
|
|
131
|
+
`CREATE TABLE IF NOT EXISTS session_memory (
|
|
132
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
133
|
+
session_id TEXT NOT NULL,
|
|
134
|
+
kind TEXT NOT NULL CHECK (kind IN ('fact','open_question','decision')),
|
|
135
|
+
content TEXT NOT NULL,
|
|
136
|
+
source_tool TEXT,
|
|
137
|
+
source_call_id TEXT,
|
|
138
|
+
confidence REAL,
|
|
139
|
+
created_at INTEGER NOT NULL,
|
|
140
|
+
stale_at INTEGER,
|
|
141
|
+
stale_reason TEXT
|
|
142
|
+
)`,
|
|
143
|
+
`CREATE INDEX IF NOT EXISTS idx_session_memory_session ON session_memory(session_id)`,
|
|
144
|
+
`CREATE INDEX IF NOT EXISTS idx_session_memory_kind ON session_memory(kind)`,
|
|
145
|
+
// fetch_egress — per-session per-host byte ledger.
|
|
146
|
+
`CREATE TABLE IF NOT EXISTS fetch_egress (
|
|
147
|
+
session_id TEXT NOT NULL,
|
|
148
|
+
host TEXT NOT NULL,
|
|
149
|
+
total_bytes INTEGER NOT NULL DEFAULT 0,
|
|
150
|
+
last_at INTEGER NOT NULL,
|
|
151
|
+
PRIMARY KEY (session_id, host)
|
|
152
|
+
)`,
|
|
153
|
+
// transcripts_fts — FTS5 virtual table for the recall tool. Python uses
|
|
154
|
+
// `tokenize='unicode61 remove_diacritics 2'`; mirrored exactly.
|
|
155
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS transcripts_fts USING fts5(
|
|
156
|
+
session_id, tool, ts UNINDEXED, path UNINDEXED, content,
|
|
157
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
158
|
+
)`
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// src/adapters/storage/migrations/index.ts
|
|
165
|
+
var MIGRATIONS;
|
|
166
|
+
var init_migrations = __esm({
|
|
167
|
+
"src/adapters/storage/migrations/index.ts"() {
|
|
168
|
+
"use strict";
|
|
169
|
+
init_cjs_shims();
|
|
170
|
+
init_init();
|
|
171
|
+
MIGRATIONS = [m0001_init];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// src/adapters/storage/schema.ts
|
|
176
|
+
function canonicalSchema(reader) {
|
|
177
|
+
const tables = listTables(reader);
|
|
178
|
+
const lines = [];
|
|
179
|
+
for (const table of tables) {
|
|
180
|
+
if (isFtsShadowTable(table)) continue;
|
|
181
|
+
if (isInternal(table)) continue;
|
|
182
|
+
if (table === "schema_migrations") continue;
|
|
183
|
+
if (isFtsVirtualTable(reader, table)) {
|
|
184
|
+
lines.push(`fts:${table}:${ftsTokenize(reader, table)}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
lines.push(`table:${table}`);
|
|
188
|
+
for (const c of tableInfo(reader, table)) {
|
|
189
|
+
lines.push(
|
|
190
|
+
` col:${c.cid}:${c.name}:${c.type}:${c.notnull}:${formatDefault(c.dflt_value)}:${c.pk}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
for (const idx of listIndexes(reader, table)) {
|
|
194
|
+
lines.push(
|
|
195
|
+
` idx:${idx.name}:${idx.unique}:${idx.origin}:${idx.partial}`
|
|
196
|
+
);
|
|
197
|
+
const cols = indexInfo(reader, idx.name);
|
|
198
|
+
lines.push(` cols:${cols.map((c) => c.name).join(",")}`);
|
|
199
|
+
}
|
|
200
|
+
for (const fk of listFks(reader, table)) {
|
|
201
|
+
lines.push(
|
|
202
|
+
` fk:${fk.id}:${fk.seq}:${fk.table}:${fk.from}:${fk.to}:${fk.on_update}:${fk.on_delete}:${fk.match}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return lines.join("\n") + "\n";
|
|
207
|
+
}
|
|
208
|
+
function listTables(reader) {
|
|
209
|
+
const rows = reader.list(
|
|
210
|
+
"SELECT name FROM sqlite_master WHERE type IN ('table') AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
211
|
+
);
|
|
212
|
+
return rows.map((r) => String(r["name"]));
|
|
213
|
+
}
|
|
214
|
+
function isInternal(name) {
|
|
215
|
+
return name.startsWith("sqlite_");
|
|
216
|
+
}
|
|
217
|
+
function isFtsShadowTable(name) {
|
|
218
|
+
return /_(data|idx|docsize|content|config)$/.test(name);
|
|
219
|
+
}
|
|
220
|
+
function isFtsVirtualTable(reader, name) {
|
|
221
|
+
const r = reader.list(
|
|
222
|
+
`SELECT sql FROM sqlite_master WHERE type='table' AND name='${name.replace(/'/g, "''")}'`
|
|
223
|
+
);
|
|
224
|
+
if (r.length === 0) return false;
|
|
225
|
+
const sql = String(r[0]?.["sql"] ?? "").toLowerCase();
|
|
226
|
+
return sql.includes("using fts");
|
|
227
|
+
}
|
|
228
|
+
function ftsTokenize(reader, name) {
|
|
229
|
+
const r = reader.list(
|
|
230
|
+
`SELECT sql FROM sqlite_master WHERE type='table' AND name='${name.replace(/'/g, "''")}'`
|
|
231
|
+
);
|
|
232
|
+
if (r.length === 0) return "";
|
|
233
|
+
const sql = String(r[0]?.["sql"] ?? "");
|
|
234
|
+
const m = sql.match(/tokenize\s*=\s*['"]([^'"]+)['"]/i);
|
|
235
|
+
return m?.[1] ?? "";
|
|
236
|
+
}
|
|
237
|
+
function tableInfo(reader, name) {
|
|
238
|
+
const rows = reader.pragma("table_info", name);
|
|
239
|
+
return rows.map(
|
|
240
|
+
(r) => ({
|
|
241
|
+
cid: Number(r["cid"] ?? 0),
|
|
242
|
+
name: String(r["name"] ?? ""),
|
|
243
|
+
type: String(r["type"] ?? ""),
|
|
244
|
+
notnull: Number(r["notnull"] ?? 0),
|
|
245
|
+
dflt_value: r["dflt_value"],
|
|
246
|
+
pk: Number(r["pk"] ?? 0)
|
|
247
|
+
})
|
|
248
|
+
).sort((a, b) => a.cid - b.cid);
|
|
249
|
+
}
|
|
250
|
+
function formatDefault(v) {
|
|
251
|
+
if (v === null || v === void 0) return "<null>";
|
|
252
|
+
return String(v);
|
|
253
|
+
}
|
|
254
|
+
function listIndexes(reader, table) {
|
|
255
|
+
const rows = reader.pragma("index_list", table);
|
|
256
|
+
return rows.map(
|
|
257
|
+
(r) => ({
|
|
258
|
+
seq: Number(r["seq"] ?? 0),
|
|
259
|
+
name: String(r["name"] ?? ""),
|
|
260
|
+
unique: Number(r["unique"] ?? 0),
|
|
261
|
+
origin: String(r["origin"] ?? ""),
|
|
262
|
+
partial: Number(r["partial"] ?? 0)
|
|
263
|
+
})
|
|
264
|
+
).filter((r) => !r.name.startsWith("sqlite_autoindex_")).sort((a, b) => a.name.localeCompare(b.name));
|
|
265
|
+
}
|
|
266
|
+
function indexInfo(reader, indexName) {
|
|
267
|
+
const rows = reader.pragma("index_info", indexName);
|
|
268
|
+
return rows.map(
|
|
269
|
+
(r) => ({
|
|
270
|
+
seqno: Number(r["seqno"] ?? 0),
|
|
271
|
+
name: String(r["name"] ?? "")
|
|
272
|
+
})
|
|
273
|
+
).sort((a, b) => a.seqno - b.seqno);
|
|
274
|
+
}
|
|
275
|
+
function listFks(reader, table) {
|
|
276
|
+
const rows = reader.pragma("foreign_key_list", table);
|
|
277
|
+
return rows.map(
|
|
278
|
+
(r) => ({
|
|
279
|
+
id: Number(r["id"] ?? 0),
|
|
280
|
+
seq: Number(r["seq"] ?? 0),
|
|
281
|
+
table: String(r["table"] ?? ""),
|
|
282
|
+
from: String(r["from"] ?? ""),
|
|
283
|
+
to: String(r["to"] ?? ""),
|
|
284
|
+
on_update: String(r["on_update"] ?? ""),
|
|
285
|
+
on_delete: String(r["on_delete"] ?? ""),
|
|
286
|
+
match: String(r["match"] ?? "")
|
|
287
|
+
})
|
|
288
|
+
).sort((a, b) => a.id - b.id || a.seq - b.seq);
|
|
289
|
+
}
|
|
290
|
+
var init_schema = __esm({
|
|
291
|
+
"src/adapters/storage/schema.ts"() {
|
|
292
|
+
"use strict";
|
|
293
|
+
init_cjs_shims();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// src/adapters/storage/better-sqlite3.ts
|
|
298
|
+
var better_sqlite3_exports = {};
|
|
299
|
+
__export(better_sqlite3_exports, {
|
|
300
|
+
openBetterSqliteStorage: () => openBetterSqliteStorage
|
|
301
|
+
});
|
|
302
|
+
function openBetterSqliteStorage(opts) {
|
|
303
|
+
const db = new import_better_sqlite3.default(opts.path);
|
|
304
|
+
const wantWal = opts.wal ?? opts.path !== ":memory:";
|
|
305
|
+
if (wantWal) db.pragma("journal_mode = WAL");
|
|
306
|
+
db.pragma("synchronous = NORMAL");
|
|
307
|
+
db.pragma(`busy_timeout = ${opts.busyTimeoutMs ?? 5e3}`);
|
|
308
|
+
db.pragma("foreign_keys = ON");
|
|
309
|
+
return new BetterSqliteStorage(db);
|
|
310
|
+
}
|
|
311
|
+
var import_better_sqlite3, ALLOWED_LINK_KINDS, ALLOWED_MEMORY_KINDS, BetterSqliteStorage;
|
|
312
|
+
var init_better_sqlite3 = __esm({
|
|
313
|
+
"src/adapters/storage/better-sqlite3.ts"() {
|
|
314
|
+
"use strict";
|
|
315
|
+
init_cjs_shims();
|
|
316
|
+
import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
317
|
+
init_migrations();
|
|
318
|
+
init_schema();
|
|
319
|
+
ALLOWED_LINK_KINDS = /* @__PURE__ */ new Set([
|
|
320
|
+
"supports",
|
|
321
|
+
"attacks",
|
|
322
|
+
"derives_from",
|
|
323
|
+
"merges_with"
|
|
324
|
+
]);
|
|
325
|
+
ALLOWED_MEMORY_KINDS = /* @__PURE__ */ new Set([
|
|
326
|
+
"fact",
|
|
327
|
+
"open_question",
|
|
328
|
+
"decision"
|
|
329
|
+
]);
|
|
330
|
+
BetterSqliteStorage = class {
|
|
331
|
+
constructor(db) {
|
|
332
|
+
this.db = db;
|
|
333
|
+
}
|
|
334
|
+
db;
|
|
335
|
+
stmts = /* @__PURE__ */ new Map();
|
|
336
|
+
// ------------------------------------------------------------------
|
|
337
|
+
// Lifecycle / lifetime
|
|
338
|
+
// ------------------------------------------------------------------
|
|
339
|
+
async migrate() {
|
|
340
|
+
this.db.exec(
|
|
341
|
+
"CREATE TABLE IF NOT EXISTS schema_migrations (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
342
|
+
);
|
|
343
|
+
const existing = new Set(
|
|
344
|
+
this.db.prepare("SELECT id FROM schema_migrations").all().map((r) => r.id)
|
|
345
|
+
);
|
|
346
|
+
const applied = [];
|
|
347
|
+
const ordered = [...MIGRATIONS].sort((a, b) => a.id.localeCompare(b.id));
|
|
348
|
+
for (const m of ordered) {
|
|
349
|
+
if (existing.has(m.id)) continue;
|
|
350
|
+
this.applyMigration(m);
|
|
351
|
+
applied.push(m.id);
|
|
352
|
+
}
|
|
353
|
+
return { applied };
|
|
354
|
+
}
|
|
355
|
+
applyMigration(m) {
|
|
356
|
+
const txn = this.db.transaction(() => {
|
|
357
|
+
for (const stmt of m.up) this.db.exec(stmt);
|
|
358
|
+
this.db.prepare(
|
|
359
|
+
"INSERT INTO schema_migrations(id, applied_at) VALUES (?, ?)"
|
|
360
|
+
).run(m.id, Date.now());
|
|
361
|
+
});
|
|
362
|
+
txn();
|
|
363
|
+
}
|
|
364
|
+
async canonicalSchema() {
|
|
365
|
+
const reader = {
|
|
366
|
+
pragma: (name, arg) => arg === void 0 ? this.db.pragma(name) : this.db.pragma(`${name}('${arg.replace(/'/g, "''")}')`),
|
|
367
|
+
list: (sql) => this.db.prepare(sql).all()
|
|
368
|
+
};
|
|
369
|
+
return canonicalSchema(reader);
|
|
370
|
+
}
|
|
371
|
+
unsafe() {
|
|
372
|
+
return {
|
|
373
|
+
exec: async (sql, params) => {
|
|
374
|
+
const stmt = this.db.prepare(sql);
|
|
375
|
+
const info = stmt.run(...params ?? []);
|
|
376
|
+
return Number(info.changes);
|
|
377
|
+
},
|
|
378
|
+
query: async (sql, params) => this.db.prepare(sql).all(...params ?? [])
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async close() {
|
|
382
|
+
this.db.close();
|
|
383
|
+
}
|
|
384
|
+
// ------------------------------------------------------------------
|
|
385
|
+
// Transactions. better-sqlite3 supports nested transactions via
|
|
386
|
+
// SAVEPOINTs automatically when the outer caller is already inside
|
|
387
|
+
// db.transaction(). We expose a Txn that mirrors Storage's surface;
|
|
388
|
+
// every method call inside the callback runs inside the open txn.
|
|
389
|
+
// ------------------------------------------------------------------
|
|
390
|
+
async txn(fn) {
|
|
391
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
392
|
+
try {
|
|
393
|
+
const result = await fn(this);
|
|
394
|
+
this.db.exec("COMMIT");
|
|
395
|
+
return result;
|
|
396
|
+
} catch (e) {
|
|
397
|
+
try {
|
|
398
|
+
this.db.exec("ROLLBACK");
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
throw e;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// ==================================================================
|
|
405
|
+
// sessions
|
|
406
|
+
// ==================================================================
|
|
407
|
+
async getSession(sessionId) {
|
|
408
|
+
const row = this.cached(
|
|
409
|
+
"session-get",
|
|
410
|
+
"SELECT * FROM sessions WHERE session_id = ?"
|
|
411
|
+
).get(sessionId);
|
|
412
|
+
return row ?? null;
|
|
413
|
+
}
|
|
414
|
+
async listSessions(opts) {
|
|
415
|
+
const limit = opts?.limit ?? 100;
|
|
416
|
+
return this.cached(
|
|
417
|
+
"session-list",
|
|
418
|
+
"SELECT * FROM sessions ORDER BY last_at DESC LIMIT ?"
|
|
419
|
+
).all(limit);
|
|
420
|
+
}
|
|
421
|
+
async upsertSession(row) {
|
|
422
|
+
this.cached(
|
|
423
|
+
"session-upsert",
|
|
424
|
+
`INSERT INTO sessions
|
|
425
|
+
(session_id, started_at, last_at, calls, wall_ms, cache_hits,
|
|
426
|
+
total_prompt_tokens, total_completion_tokens, total_cached_tokens,
|
|
427
|
+
total_tokens, total_cost_usd, total_cpu_ms)
|
|
428
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
429
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
430
|
+
last_at = excluded.last_at,
|
|
431
|
+
calls = excluded.calls,
|
|
432
|
+
wall_ms = excluded.wall_ms,
|
|
433
|
+
cache_hits = excluded.cache_hits,
|
|
434
|
+
total_prompt_tokens = excluded.total_prompt_tokens,
|
|
435
|
+
total_completion_tokens = excluded.total_completion_tokens,
|
|
436
|
+
total_cached_tokens = excluded.total_cached_tokens,
|
|
437
|
+
total_tokens = excluded.total_tokens,
|
|
438
|
+
total_cost_usd = excluded.total_cost_usd,
|
|
439
|
+
total_cpu_ms = excluded.total_cpu_ms`
|
|
440
|
+
).run(
|
|
441
|
+
row.session_id,
|
|
442
|
+
row.started_at,
|
|
443
|
+
row.last_at,
|
|
444
|
+
row.calls,
|
|
445
|
+
row.wall_ms,
|
|
446
|
+
row.cache_hits,
|
|
447
|
+
row.total_prompt_tokens,
|
|
448
|
+
row.total_completion_tokens,
|
|
449
|
+
row.total_cached_tokens,
|
|
450
|
+
row.total_tokens,
|
|
451
|
+
row.total_cost_usd,
|
|
452
|
+
row.total_cpu_ms
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
async accumulateSessionTotals(sessionId, delta) {
|
|
456
|
+
this.cached(
|
|
457
|
+
"session-touch",
|
|
458
|
+
`INSERT OR IGNORE INTO sessions
|
|
459
|
+
(session_id, started_at, last_at, calls, wall_ms, cache_hits)
|
|
460
|
+
VALUES (?, ?, ?, 0, 0, 0)`
|
|
461
|
+
).run(sessionId, delta.last_at ?? 0, delta.last_at ?? null);
|
|
462
|
+
this.cached(
|
|
463
|
+
"session-acc",
|
|
464
|
+
`UPDATE sessions SET
|
|
465
|
+
calls = calls + ?,
|
|
466
|
+
wall_ms = wall_ms + ?,
|
|
467
|
+
cache_hits = cache_hits + ?,
|
|
468
|
+
total_prompt_tokens = total_prompt_tokens + ?,
|
|
469
|
+
total_completion_tokens = total_completion_tokens + ?,
|
|
470
|
+
total_cached_tokens = total_cached_tokens + ?,
|
|
471
|
+
total_tokens = total_tokens + ?,
|
|
472
|
+
total_cost_usd = total_cost_usd + ?,
|
|
473
|
+
total_cpu_ms = total_cpu_ms + ?,
|
|
474
|
+
last_at = COALESCE(?, last_at)
|
|
475
|
+
WHERE session_id = ?`
|
|
476
|
+
).run(
|
|
477
|
+
delta.calls ?? 0,
|
|
478
|
+
delta.wall_ms ?? 0,
|
|
479
|
+
delta.cache_hits ?? 0,
|
|
480
|
+
delta.total_prompt_tokens ?? 0,
|
|
481
|
+
delta.total_completion_tokens ?? 0,
|
|
482
|
+
delta.total_cached_tokens ?? 0,
|
|
483
|
+
delta.total_tokens ?? 0,
|
|
484
|
+
delta.total_cost_usd ?? 0,
|
|
485
|
+
delta.total_cpu_ms ?? 0,
|
|
486
|
+
delta.last_at ?? null,
|
|
487
|
+
sessionId
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
// ==================================================================
|
|
491
|
+
// usage_log
|
|
492
|
+
// ==================================================================
|
|
493
|
+
async insertUsage(rows) {
|
|
494
|
+
if (rows.length === 0) return;
|
|
495
|
+
const stmt = this.cached(
|
|
496
|
+
"usage-insert",
|
|
497
|
+
`INSERT INTO usage_log
|
|
498
|
+
(session_id, ts, tool, purpose, provider, model,
|
|
499
|
+
prompt_tokens, completion_tokens, cached_tokens, total_tokens,
|
|
500
|
+
cost_usd, estimated, wall_ms, cpu_ms)
|
|
501
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
502
|
+
);
|
|
503
|
+
const tx = this.db.transaction((items) => {
|
|
504
|
+
for (const r of items) {
|
|
505
|
+
stmt.run(
|
|
506
|
+
r.session_id,
|
|
507
|
+
r.ts,
|
|
508
|
+
r.tool,
|
|
509
|
+
r.purpose,
|
|
510
|
+
r.provider,
|
|
511
|
+
r.model,
|
|
512
|
+
r.prompt_tokens ?? 0,
|
|
513
|
+
r.completion_tokens ?? 0,
|
|
514
|
+
r.cached_tokens ?? 0,
|
|
515
|
+
r.total_tokens ?? 0,
|
|
516
|
+
r.cost_usd ?? 0,
|
|
517
|
+
r.estimated ?? 0,
|
|
518
|
+
r.wall_ms ?? 0,
|
|
519
|
+
r.cpu_ms ?? 0
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
tx(rows);
|
|
524
|
+
}
|
|
525
|
+
async listUsageForSession(sessionId, opts) {
|
|
526
|
+
const where = ["session_id = ?"];
|
|
527
|
+
const params = [sessionId];
|
|
528
|
+
if (opts?.only_purpose && opts.only_purpose.length > 0) {
|
|
529
|
+
where.push(`purpose IN (${opts.only_purpose.map(() => "?").join(",")})`);
|
|
530
|
+
params.push(...opts.only_purpose);
|
|
531
|
+
}
|
|
532
|
+
if (opts?.only_provider && opts.only_provider.length > 0) {
|
|
533
|
+
where.push(`provider IN (${opts.only_provider.map(() => "?").join(",")})`);
|
|
534
|
+
params.push(...opts.only_provider);
|
|
535
|
+
}
|
|
536
|
+
const limit = opts?.limit ?? 1e3;
|
|
537
|
+
params.push(limit);
|
|
538
|
+
const sql = `SELECT * FROM usage_log WHERE ${where.join(" AND ")} ORDER BY id ASC LIMIT ?`;
|
|
539
|
+
return this.db.prepare(sql).all(...params);
|
|
540
|
+
}
|
|
541
|
+
async listUsageGroupedByPurpose(sessionId) {
|
|
542
|
+
return this.cached(
|
|
543
|
+
"usage-grp-purpose",
|
|
544
|
+
`SELECT purpose,
|
|
545
|
+
COUNT(*) AS calls,
|
|
546
|
+
COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,
|
|
547
|
+
COALESCE(SUM(completion_tokens), 0) AS completion_tokens,
|
|
548
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
549
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
550
|
+
COALESCE(SUM(wall_ms), 0) AS wall_ms,
|
|
551
|
+
COALESCE(SUM(cpu_ms), 0) AS cpu_ms
|
|
552
|
+
FROM usage_log
|
|
553
|
+
WHERE session_id = ?
|
|
554
|
+
GROUP BY purpose
|
|
555
|
+
ORDER BY purpose`
|
|
556
|
+
).all(sessionId);
|
|
557
|
+
}
|
|
558
|
+
async listUsageGroupedByModel(sessionId) {
|
|
559
|
+
return this.cached(
|
|
560
|
+
"usage-grp-model",
|
|
561
|
+
`SELECT provider,
|
|
562
|
+
model,
|
|
563
|
+
COUNT(*) AS calls,
|
|
564
|
+
COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,
|
|
565
|
+
COALESCE(SUM(completion_tokens), 0) AS completion_tokens,
|
|
566
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
567
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
568
|
+
FROM usage_log
|
|
569
|
+
WHERE session_id = ?
|
|
570
|
+
GROUP BY provider, model
|
|
571
|
+
ORDER BY cost_usd DESC, provider, model`
|
|
572
|
+
).all(sessionId);
|
|
573
|
+
}
|
|
574
|
+
async listUsageGroupedByProvider(purpose) {
|
|
575
|
+
if (purpose !== void 0) {
|
|
576
|
+
return this.cached(
|
|
577
|
+
"usage-grp-provider-p",
|
|
578
|
+
`SELECT provider,
|
|
579
|
+
COUNT(*) AS calls,
|
|
580
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
581
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
582
|
+
0 AS errors
|
|
583
|
+
FROM usage_log
|
|
584
|
+
WHERE purpose = ?
|
|
585
|
+
GROUP BY provider
|
|
586
|
+
ORDER BY provider`
|
|
587
|
+
).all(purpose);
|
|
588
|
+
}
|
|
589
|
+
return this.cached(
|
|
590
|
+
"usage-grp-provider",
|
|
591
|
+
`SELECT provider,
|
|
592
|
+
COUNT(*) AS calls,
|
|
593
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
594
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
595
|
+
0 AS errors
|
|
596
|
+
FROM usage_log
|
|
597
|
+
GROUP BY provider
|
|
598
|
+
ORDER BY provider`
|
|
599
|
+
).all();
|
|
600
|
+
}
|
|
601
|
+
async listRouterStatsByPurpose(purpose, sinceMs) {
|
|
602
|
+
if (sinceMs !== void 0) {
|
|
603
|
+
return this.cached(
|
|
604
|
+
"router-stats-purpose-since",
|
|
605
|
+
`SELECT LOWER(provider) AS provider,
|
|
606
|
+
COUNT(*) AS calls,
|
|
607
|
+
COALESCE(SUM(total_tokens), 0) AS tokens_sum,
|
|
608
|
+
COALESCE(AVG(total_tokens), 0) AS avg_total_tokens,
|
|
609
|
+
COALESCE(AVG(cost_usd), 0) AS avg_cost_usd,
|
|
610
|
+
COALESCE(AVG(wall_ms), 0) AS avg_wall_ms
|
|
611
|
+
FROM usage_log
|
|
612
|
+
WHERE purpose = ? AND ts >= ? AND provider IS NOT NULL
|
|
613
|
+
GROUP BY LOWER(provider)
|
|
614
|
+
ORDER BY provider`
|
|
615
|
+
).all(purpose, sinceMs);
|
|
616
|
+
}
|
|
617
|
+
return this.cached(
|
|
618
|
+
"router-stats-purpose",
|
|
619
|
+
`SELECT LOWER(provider) AS provider,
|
|
620
|
+
COUNT(*) AS calls,
|
|
621
|
+
COALESCE(SUM(total_tokens), 0) AS tokens_sum,
|
|
622
|
+
COALESCE(AVG(total_tokens), 0) AS avg_total_tokens,
|
|
623
|
+
COALESCE(AVG(cost_usd), 0) AS avg_cost_usd,
|
|
624
|
+
COALESCE(AVG(wall_ms), 0) AS avg_wall_ms
|
|
625
|
+
FROM usage_log
|
|
626
|
+
WHERE purpose = ? AND provider IS NOT NULL
|
|
627
|
+
GROUP BY LOWER(provider)
|
|
628
|
+
ORDER BY provider`
|
|
629
|
+
).all(purpose);
|
|
630
|
+
}
|
|
631
|
+
// ==================================================================
|
|
632
|
+
// claims
|
|
633
|
+
// ==================================================================
|
|
634
|
+
async insertClaim(claim) {
|
|
635
|
+
const info = this.cached(
|
|
636
|
+
"claim-insert",
|
|
637
|
+
`INSERT INTO claims
|
|
638
|
+
(session_id, text, provider, confidence, citations_json, kind, created_at)
|
|
639
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
640
|
+
).run(
|
|
641
|
+
claim.session_id,
|
|
642
|
+
claim.text,
|
|
643
|
+
claim.provider ?? null,
|
|
644
|
+
claim.confidence ?? null,
|
|
645
|
+
claim.citations ? JSON.stringify(claim.citations) : null,
|
|
646
|
+
claim.kind ?? null,
|
|
647
|
+
Date.now()
|
|
648
|
+
);
|
|
649
|
+
return Number(info.lastInsertRowid);
|
|
650
|
+
}
|
|
651
|
+
async insertClaimLink(srcId, dstId, kind) {
|
|
652
|
+
if (!ALLOWED_LINK_KINDS.has(kind)) {
|
|
653
|
+
throw new RangeError(`invalid claim_link kind: ${kind}`);
|
|
654
|
+
}
|
|
655
|
+
this.cached(
|
|
656
|
+
"claim-link-insert",
|
|
657
|
+
`INSERT OR IGNORE INTO claim_links (src_id, dst_id, kind, created_at)
|
|
658
|
+
VALUES (?, ?, ?, ?)`
|
|
659
|
+
).run(srcId, dstId, kind, Date.now());
|
|
660
|
+
}
|
|
661
|
+
async listClaimsForSession(sessionId) {
|
|
662
|
+
return this.cached(
|
|
663
|
+
"claim-list-session",
|
|
664
|
+
"SELECT * FROM claims WHERE session_id = ? ORDER BY id"
|
|
665
|
+
).all(sessionId);
|
|
666
|
+
}
|
|
667
|
+
async getClaim(claimId) {
|
|
668
|
+
const row = this.cached(
|
|
669
|
+
"claim-get",
|
|
670
|
+
"SELECT * FROM claims WHERE id = ?"
|
|
671
|
+
).get(claimId);
|
|
672
|
+
return row ?? null;
|
|
673
|
+
}
|
|
674
|
+
async listClaimLinksForSession(sessionId) {
|
|
675
|
+
return this.cached(
|
|
676
|
+
"claim-link-list-session",
|
|
677
|
+
`SELECT cl.*
|
|
678
|
+
FROM claim_links cl
|
|
679
|
+
JOIN claims c ON c.id = cl.src_id OR c.id = cl.dst_id
|
|
680
|
+
WHERE c.session_id = ?
|
|
681
|
+
GROUP BY cl.id
|
|
682
|
+
ORDER BY cl.id`
|
|
683
|
+
).all(sessionId);
|
|
684
|
+
}
|
|
685
|
+
async deleteClaimsForSession(sessionId) {
|
|
686
|
+
const info = this.cached(
|
|
687
|
+
"claim-delete-session",
|
|
688
|
+
"DELETE FROM claims WHERE session_id = ?"
|
|
689
|
+
).run(sessionId);
|
|
690
|
+
return Number(info.changes);
|
|
691
|
+
}
|
|
692
|
+
// ==================================================================
|
|
693
|
+
// provider_stats
|
|
694
|
+
// ==================================================================
|
|
695
|
+
async listProviderStats(opts) {
|
|
696
|
+
const limit = opts?.limit ?? 100;
|
|
697
|
+
return this.cached(
|
|
698
|
+
"provider-stats-list",
|
|
699
|
+
"SELECT * FROM provider_stats ORDER BY (wins + losses + abstains) DESC, provider ASC LIMIT ?"
|
|
700
|
+
).all(limit);
|
|
701
|
+
}
|
|
702
|
+
async getProviderStats(provider) {
|
|
703
|
+
const row = this.cached(
|
|
704
|
+
"provider-stats-get",
|
|
705
|
+
"SELECT * FROM provider_stats WHERE provider = ?"
|
|
706
|
+
).get(provider);
|
|
707
|
+
return row ?? null;
|
|
708
|
+
}
|
|
709
|
+
async bumpProviderBallot(provider, ballot, at) {
|
|
710
|
+
const column = ballot === "agree" ? "wins" : ballot === "disagree" ? "losses" : "abstains";
|
|
711
|
+
this.db.prepare(
|
|
712
|
+
`INSERT INTO provider_stats(provider, wins, losses, abstains, last_at)
|
|
713
|
+
VALUES (?, 0, 0, 0, ?)
|
|
714
|
+
ON CONFLICT(provider) DO NOTHING`
|
|
715
|
+
).run(provider, at);
|
|
716
|
+
this.db.prepare(
|
|
717
|
+
`UPDATE provider_stats SET ${column} = ${column} + 1, last_at = ? WHERE provider = ?`
|
|
718
|
+
).run(at, provider);
|
|
719
|
+
}
|
|
720
|
+
// ==================================================================
|
|
721
|
+
// delegations
|
|
722
|
+
// ==================================================================
|
|
723
|
+
async insertDelegation(row) {
|
|
724
|
+
this.cached(
|
|
725
|
+
"delegation-insert",
|
|
726
|
+
`INSERT INTO delegations
|
|
727
|
+
(session_id, requester, tool_call, via, accepted, created_at)
|
|
728
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
729
|
+
).run(
|
|
730
|
+
row.session_id,
|
|
731
|
+
row.requester,
|
|
732
|
+
row.tool_call,
|
|
733
|
+
row.via,
|
|
734
|
+
row.accepted,
|
|
735
|
+
row.created_at
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
async listDelegationsForSession(sessionId) {
|
|
739
|
+
return this.cached(
|
|
740
|
+
"delegation-list-session",
|
|
741
|
+
"SELECT * FROM delegations WHERE session_id = ? ORDER BY id"
|
|
742
|
+
).all(sessionId);
|
|
743
|
+
}
|
|
744
|
+
async countDelegationsByRequester(requester) {
|
|
745
|
+
const r = this.cached(
|
|
746
|
+
"delegation-count-req",
|
|
747
|
+
"SELECT COUNT(*) AS n FROM delegations WHERE requester = ?"
|
|
748
|
+
).get(requester);
|
|
749
|
+
return Number(r.n);
|
|
750
|
+
}
|
|
751
|
+
async countDelegationsBySession(sessionId) {
|
|
752
|
+
const r = this.cached(
|
|
753
|
+
"delegation-count-session",
|
|
754
|
+
"SELECT COUNT(*) AS n FROM delegations WHERE session_id = ?"
|
|
755
|
+
).get(sessionId);
|
|
756
|
+
return Number(r.n);
|
|
757
|
+
}
|
|
758
|
+
async countAcceptedDelegationsBySession(sessionId) {
|
|
759
|
+
const r = this.cached(
|
|
760
|
+
"delegation-count-accepted-session",
|
|
761
|
+
"SELECT COUNT(*) AS n FROM delegations WHERE session_id = ? AND accepted = 1"
|
|
762
|
+
).get(sessionId);
|
|
763
|
+
return Number(r.n);
|
|
764
|
+
}
|
|
765
|
+
async countAcceptedDelegationsByRequester(requester) {
|
|
766
|
+
const r = this.cached(
|
|
767
|
+
"delegation-count-accepted-requester",
|
|
768
|
+
"SELECT COUNT(*) AS n FROM delegations WHERE requester = ? AND accepted = 1"
|
|
769
|
+
).get(requester);
|
|
770
|
+
return Number(r.n);
|
|
771
|
+
}
|
|
772
|
+
async listDelegationAggregatesByRequester() {
|
|
773
|
+
const rows = this.cached(
|
|
774
|
+
"delegation-agg-requester",
|
|
775
|
+
"SELECT requester, accepted, COUNT(*) AS n FROM delegations WHERE requester IS NOT NULL GROUP BY requester, accepted"
|
|
776
|
+
).all();
|
|
777
|
+
return rows.map((r) => ({
|
|
778
|
+
requester: String(r.requester),
|
|
779
|
+
accepted: r.accepted === 1 ? 1 : 0,
|
|
780
|
+
count: Number(r.n)
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
// ==================================================================
|
|
784
|
+
// global counts (scoreboard / observability)
|
|
785
|
+
// ==================================================================
|
|
786
|
+
async countScoreboardTotals() {
|
|
787
|
+
const countOne = (table) => {
|
|
788
|
+
try {
|
|
789
|
+
const r = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
790
|
+
return r ? Number(r.n) : 0;
|
|
791
|
+
} catch {
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
return {
|
|
796
|
+
sessions: countOne("sessions"),
|
|
797
|
+
claims: countOne("claims"),
|
|
798
|
+
claim_links: countOne("claim_links"),
|
|
799
|
+
delegations: countOne("delegations")
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
// ==================================================================
|
|
803
|
+
// session_memory
|
|
804
|
+
// ==================================================================
|
|
805
|
+
async insertSessionMemory(row) {
|
|
806
|
+
if (!ALLOWED_MEMORY_KINDS.has(row.kind)) {
|
|
807
|
+
throw new RangeError(`invalid session_memory kind: ${row.kind}`);
|
|
808
|
+
}
|
|
809
|
+
const info = this.cached(
|
|
810
|
+
"memory-insert",
|
|
811
|
+
`INSERT INTO session_memory
|
|
812
|
+
(session_id, kind, content, source_tool, source_call_id, confidence, created_at)
|
|
813
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
814
|
+
).run(
|
|
815
|
+
row.session_id,
|
|
816
|
+
row.kind,
|
|
817
|
+
row.content,
|
|
818
|
+
row.source_tool ?? null,
|
|
819
|
+
row.source_call_id ?? null,
|
|
820
|
+
row.confidence ?? null,
|
|
821
|
+
row.created_at
|
|
822
|
+
);
|
|
823
|
+
return Number(info.lastInsertRowid);
|
|
824
|
+
}
|
|
825
|
+
async listSessionMemory(sessionId, opts) {
|
|
826
|
+
const where = ["session_id = ?"];
|
|
827
|
+
const params = [sessionId];
|
|
828
|
+
if (!opts?.include_stale) where.push("stale_at IS NULL");
|
|
829
|
+
if (opts?.kinds && opts.kinds.length > 0) {
|
|
830
|
+
where.push(`kind IN (${opts.kinds.map(() => "?").join(",")})`);
|
|
831
|
+
params.push(...opts.kinds);
|
|
832
|
+
}
|
|
833
|
+
const limit = opts?.limit ?? 50;
|
|
834
|
+
params.push(limit);
|
|
835
|
+
const sql = `SELECT * FROM session_memory WHERE ${where.join(" AND ")} ORDER BY id DESC LIMIT ?`;
|
|
836
|
+
return this.db.prepare(sql).all(...params);
|
|
837
|
+
}
|
|
838
|
+
async markSessionMemoryStale(sessionId, at, opts) {
|
|
839
|
+
const where = ["session_id = ?", "stale_at IS NULL"];
|
|
840
|
+
const params = [at, opts?.reason ?? "manual", sessionId];
|
|
841
|
+
if (opts?.ids && opts.ids.length > 0) {
|
|
842
|
+
where.push(`id IN (${opts.ids.map(() => "?").join(",")})`);
|
|
843
|
+
params.push(...opts.ids);
|
|
844
|
+
}
|
|
845
|
+
if (opts?.kinds && opts.kinds.length > 0) {
|
|
846
|
+
where.push(`kind IN (${opts.kinds.map(() => "?").join(",")})`);
|
|
847
|
+
params.push(...opts.kinds);
|
|
848
|
+
}
|
|
849
|
+
const sql = `UPDATE session_memory SET stale_at = ?, stale_reason = ? WHERE ${where.join(" AND ")}`;
|
|
850
|
+
const info = this.db.prepare(sql).run(...params);
|
|
851
|
+
return Number(info.changes);
|
|
852
|
+
}
|
|
853
|
+
async clearSessionMemory(sessionId) {
|
|
854
|
+
const info = this.cached(
|
|
855
|
+
"memory-clear",
|
|
856
|
+
"DELETE FROM session_memory WHERE session_id = ?"
|
|
857
|
+
).run(sessionId);
|
|
858
|
+
return Number(info.changes);
|
|
859
|
+
}
|
|
860
|
+
// ==================================================================
|
|
861
|
+
// fetch_egress
|
|
862
|
+
// ==================================================================
|
|
863
|
+
async recordFetchEgress(sessionId, host, bytes, at) {
|
|
864
|
+
this.cached(
|
|
865
|
+
"fetch-egress-upsert",
|
|
866
|
+
`INSERT INTO fetch_egress (session_id, host, total_bytes, last_at)
|
|
867
|
+
VALUES (?, ?, ?, ?)
|
|
868
|
+
ON CONFLICT(session_id, host) DO UPDATE SET
|
|
869
|
+
total_bytes = total_bytes + excluded.total_bytes,
|
|
870
|
+
last_at = excluded.last_at`
|
|
871
|
+
).run(sessionId, host, bytes, at);
|
|
872
|
+
}
|
|
873
|
+
async getFetchEgressTotals(sessionId) {
|
|
874
|
+
const r = this.cached(
|
|
875
|
+
"fetch-egress-totals",
|
|
876
|
+
"SELECT COALESCE(SUM(total_bytes), 0) AS total_bytes, COUNT(DISTINCT host) AS unique_hosts FROM fetch_egress WHERE session_id = ?"
|
|
877
|
+
).get(sessionId);
|
|
878
|
+
return {
|
|
879
|
+
total_bytes: Number(r.total_bytes ?? 0),
|
|
880
|
+
unique_hosts: Number(r.unique_hosts ?? 0)
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
async hasFetchEgressHost(sessionId, host) {
|
|
884
|
+
const r = this.cached(
|
|
885
|
+
"fetch-egress-has-host",
|
|
886
|
+
"SELECT 1 AS one FROM fetch_egress WHERE session_id = ? AND host = ? LIMIT 1"
|
|
887
|
+
).get(sessionId, host);
|
|
888
|
+
return r !== void 0;
|
|
889
|
+
}
|
|
890
|
+
// ==================================================================
|
|
891
|
+
// transcripts_fts — the encapsulated FTS5 surface.
|
|
892
|
+
// ==================================================================
|
|
893
|
+
async indexTranscript(row) {
|
|
894
|
+
this.cached(
|
|
895
|
+
"fts-insert",
|
|
896
|
+
`INSERT INTO transcripts_fts (session_id, tool, ts, path, content)
|
|
897
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
898
|
+
).run(row.session_id ?? "", row.tool, String(row.ts), row.path, row.content);
|
|
899
|
+
}
|
|
900
|
+
async recallSearch(query, k, opts) {
|
|
901
|
+
const where = ["transcripts_fts MATCH ?"];
|
|
902
|
+
const params = [query];
|
|
903
|
+
if (opts?.session_id) {
|
|
904
|
+
where.push("session_id = ?");
|
|
905
|
+
params.push(opts.session_id);
|
|
906
|
+
}
|
|
907
|
+
if (opts?.tool) {
|
|
908
|
+
where.push("tool = ?");
|
|
909
|
+
params.push(opts.tool);
|
|
910
|
+
}
|
|
911
|
+
if (opts?.since_ms !== void 0) {
|
|
912
|
+
where.push("CAST(ts AS INTEGER) >= ?");
|
|
913
|
+
params.push(opts.since_ms);
|
|
914
|
+
}
|
|
915
|
+
params.push(k);
|
|
916
|
+
const sql = `SELECT path, session_id, tool, ts, snippet(transcripts_fts, -1, '[[', ']]', '...', 16) AS snippet, bm25(transcripts_fts) AS rank FROM transcripts_fts WHERE ${where.join(" AND ")} ORDER BY rank LIMIT ?`;
|
|
917
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
918
|
+
return rows.map((r) => ({
|
|
919
|
+
path: r.path,
|
|
920
|
+
session_id: r.session_id ?? null,
|
|
921
|
+
tool: r.tool ?? null,
|
|
922
|
+
ts: Number(r.ts),
|
|
923
|
+
snippet: r.snippet,
|
|
924
|
+
score: 1 / (1 + Math.max(0, r.rank ?? 0))
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
927
|
+
// ------------------------------------------------------------------
|
|
928
|
+
// Prepared-statement cache. better-sqlite3 prepares statements lazily
|
|
929
|
+
// and caches them internally, but the JS-side reference still costs
|
|
930
|
+
// a hashmap lookup on every call. Keeping our own map by key lets
|
|
931
|
+
// hot-path queries hit a single Map.get().
|
|
932
|
+
// ------------------------------------------------------------------
|
|
933
|
+
cached(key, sql) {
|
|
934
|
+
let s = this.stmts.get(key);
|
|
935
|
+
if (!s) {
|
|
936
|
+
s = this.db.prepare(sql);
|
|
937
|
+
this.stmts.set(key, s);
|
|
938
|
+
}
|
|
939
|
+
return s;
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// src/self-check.ts
|
|
946
|
+
var self_check_exports = {};
|
|
947
|
+
__export(self_check_exports, {
|
|
948
|
+
checkLocalStorage: () => checkLocalStorage,
|
|
949
|
+
parseAbiMismatch: () => parseAbiMismatch
|
|
950
|
+
});
|
|
951
|
+
module.exports = __toCommonJS(self_check_exports);
|
|
952
|
+
init_cjs_shims();
|
|
953
|
+
var import_node_fs = require("fs");
|
|
954
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
955
|
+
var ABI_MISMATCH_RE = /NODE_MODULE_VERSION (\d+)\..*requires\s+NODE_MODULE_VERSION (\d+)/s;
|
|
956
|
+
function resolveRepoFile(rel) {
|
|
957
|
+
let dir = __dirname;
|
|
958
|
+
for (let i = 0; i < 8; i++) {
|
|
959
|
+
const candidate = import_node_path.default.join(dir, rel);
|
|
960
|
+
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
961
|
+
const parent = import_node_path.default.dirname(dir);
|
|
962
|
+
if (parent === dir) break;
|
|
963
|
+
dir = parent;
|
|
964
|
+
}
|
|
965
|
+
return void 0;
|
|
966
|
+
}
|
|
967
|
+
function parseAbiMismatch(message) {
|
|
968
|
+
const m = ABI_MISMATCH_RE.exec(message);
|
|
969
|
+
return m ? { loaded: Number(m[1]), required: Number(m[2]) } : void 0;
|
|
970
|
+
}
|
|
971
|
+
function defaultDbPath() {
|
|
972
|
+
const dataDir = process.env["CROSSCHECK_DATA_DIR"] || import_node_path.default.join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "", ".crosscheck");
|
|
973
|
+
return process.env["CROSSCHECK_DB_PATH"] ?? resolveRepoFile(".crosscheck/db.sqlite") ?? import_node_path.default.join(dataDir, "db.sqlite");
|
|
974
|
+
}
|
|
975
|
+
async function checkLocalStorage(dbPath) {
|
|
976
|
+
const resolvedPath = dbPath ?? defaultDbPath();
|
|
977
|
+
try {
|
|
978
|
+
(0, import_node_fs.mkdirSync)(import_node_path.default.dirname(resolvedPath), { recursive: true });
|
|
979
|
+
const { openBetterSqliteStorage: openBetterSqliteStorage2 } = await Promise.resolve().then(() => (init_better_sqlite3(), better_sqlite3_exports));
|
|
980
|
+
const storage = openBetterSqliteStorage2({ path: resolvedPath });
|
|
981
|
+
await storage.migrate();
|
|
982
|
+
await storage.close();
|
|
983
|
+
return { ok: true, path: resolvedPath };
|
|
984
|
+
} catch (e) {
|
|
985
|
+
const message = e.message ?? String(e);
|
|
986
|
+
const abiMismatch = parseAbiMismatch(message);
|
|
987
|
+
return {
|
|
988
|
+
ok: false,
|
|
989
|
+
path: resolvedPath,
|
|
990
|
+
error: message,
|
|
991
|
+
...abiMismatch ? { abiMismatch } : {}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
996
|
+
0 && (module.exports = {
|
|
997
|
+
checkLocalStorage,
|
|
998
|
+
parseAbiMismatch
|
|
999
|
+
});
|
|
1000
|
+
//# sourceMappingURL=self-check.cjs.map
|