claude-memory-layer 1.0.52 → 1.0.54
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/README.md +5 -5
- package/dist/cli/index.js +960 -534
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +437 -164
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +502 -159
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +432 -134
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +496 -152
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +503 -152
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +494 -154
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +503 -159
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +484 -193
- package/dist/index.js.map +4 -4
- package/dist/mcp/index.js +580 -234
- package/dist/mcp/index.js.map +4 -4
- package/dist/server/api/index.js +693 -354
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +744 -368
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +419 -133
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/assets/js/overview.js +34 -2
- package/package.json +5 -2
|
@@ -157,7 +157,13 @@ function toDate(value) {
|
|
|
157
157
|
return new Date(String(value));
|
|
158
158
|
}
|
|
159
159
|
function createDatabase(dbPath, options) {
|
|
160
|
-
|
|
160
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
161
|
+
db.pragma("busy_timeout = 5000");
|
|
162
|
+
if (!options?.readOnly) {
|
|
163
|
+
db.pragma("journal_mode = WAL");
|
|
164
|
+
db.pragma("synchronous = NORMAL");
|
|
165
|
+
}
|
|
166
|
+
return db;
|
|
161
167
|
}
|
|
162
168
|
function dbRun(db, sql, params = []) {
|
|
163
169
|
db.prepare(sql).run(...params);
|
|
@@ -390,19 +396,29 @@ var ConsolidatedStore = class {
|
|
|
390
396
|
*/
|
|
391
397
|
async create(input) {
|
|
392
398
|
const memoryId = randomUUID2();
|
|
393
|
-
|
|
394
|
-
|
|
399
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
400
|
+
const insertMemory = this.db.prepare(
|
|
395
401
|
`INSERT INTO consolidated_memories
|
|
396
402
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
397
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
398
|
-
|
|
403
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
404
|
+
);
|
|
405
|
+
const insertEvent = this.db.prepare(
|
|
406
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
407
|
+
);
|
|
408
|
+
this.db.transaction(() => {
|
|
409
|
+
insertMemory.run(
|
|
399
410
|
memoryId,
|
|
400
411
|
input.summary,
|
|
401
412
|
JSON.stringify(input.topics),
|
|
402
|
-
JSON.stringify(
|
|
413
|
+
JSON.stringify(sourceEvents),
|
|
403
414
|
input.confidence
|
|
404
|
-
|
|
405
|
-
|
|
415
|
+
);
|
|
416
|
+
for (const eventId of sourceEvents) {
|
|
417
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
418
|
+
insertEvent.run(memoryId, eventId);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
})();
|
|
406
422
|
return memoryId;
|
|
407
423
|
}
|
|
408
424
|
/**
|
|
@@ -508,11 +524,12 @@ var ConsolidatedStore = class {
|
|
|
508
524
|
* Delete a consolidated memory
|
|
509
525
|
*/
|
|
510
526
|
async delete(memoryId) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
527
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
528
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
529
|
+
this.db.transaction(() => {
|
|
530
|
+
deleteEvents.run(memoryId);
|
|
531
|
+
deleteMemory.run(memoryId);
|
|
532
|
+
})();
|
|
516
533
|
}
|
|
517
534
|
/**
|
|
518
535
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -625,16 +642,16 @@ var ConsolidatedStore = class {
|
|
|
625
642
|
* Check if source events are already consolidated
|
|
626
643
|
*/
|
|
627
644
|
async isAlreadyConsolidated(eventIds) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
return
|
|
645
|
+
if (eventIds.length === 0) return false;
|
|
646
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
647
|
+
const rows = await dbAll(
|
|
648
|
+
this.db,
|
|
649
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
650
|
+
WHERE event_id IN (${placeholders})
|
|
651
|
+
LIMIT 1`,
|
|
652
|
+
eventIds
|
|
653
|
+
);
|
|
654
|
+
return rows.length > 0;
|
|
638
655
|
}
|
|
639
656
|
/**
|
|
640
657
|
* Get the last consolidation time
|
|
@@ -2170,6 +2187,19 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2170
2187
|
return `${sessionId}:${contentHash}`;
|
|
2171
2188
|
}
|
|
2172
2189
|
|
|
2190
|
+
// src/core/citation-generator.ts
|
|
2191
|
+
import { createHash as createHash2 } from "crypto";
|
|
2192
|
+
var ID_LENGTH = 6;
|
|
2193
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2194
|
+
function generateCitationId(eventId) {
|
|
2195
|
+
const hash = createHash2("sha256").update(eventId).digest();
|
|
2196
|
+
let id = "";
|
|
2197
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2198
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2199
|
+
}
|
|
2200
|
+
return id;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2173
2203
|
// src/core/sqlite-event-store.ts
|
|
2174
2204
|
import * as nodePath2 from "path";
|
|
2175
2205
|
|
|
@@ -2214,17 +2244,28 @@ function createSQLiteDatabase(path11, options) {
|
|
|
2214
2244
|
}
|
|
2215
2245
|
return db;
|
|
2216
2246
|
}
|
|
2247
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2248
|
+
function prepareCached(db, sql) {
|
|
2249
|
+
let cache = statementCache.get(db);
|
|
2250
|
+
if (!cache) {
|
|
2251
|
+
cache = /* @__PURE__ */ new Map();
|
|
2252
|
+
statementCache.set(db, cache);
|
|
2253
|
+
}
|
|
2254
|
+
let stmt = cache.get(sql);
|
|
2255
|
+
if (!stmt) {
|
|
2256
|
+
stmt = db.prepare(sql);
|
|
2257
|
+
cache.set(sql, stmt);
|
|
2258
|
+
}
|
|
2259
|
+
return stmt;
|
|
2260
|
+
}
|
|
2217
2261
|
function sqliteRun(db, sql, params = []) {
|
|
2218
|
-
|
|
2219
|
-
return stmt.run(...params);
|
|
2262
|
+
return prepareCached(db, sql).run(...params);
|
|
2220
2263
|
}
|
|
2221
2264
|
function sqliteAll(db, sql, params = []) {
|
|
2222
|
-
|
|
2223
|
-
return stmt.all(...params);
|
|
2265
|
+
return prepareCached(db, sql).all(...params);
|
|
2224
2266
|
}
|
|
2225
2267
|
function sqliteGet(db, sql, params = []) {
|
|
2226
|
-
|
|
2227
|
-
return stmt.get(...params);
|
|
2268
|
+
return prepareCached(db, sql).get(...params);
|
|
2228
2269
|
}
|
|
2229
2270
|
function sqliteExec(db, sql) {
|
|
2230
2271
|
db.exec(sql);
|
|
@@ -2646,6 +2687,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2646
2687
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2647
2688
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2648
2689
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2690
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2649
2691
|
function emptyOutboxRecoveryResult() {
|
|
2650
2692
|
return {
|
|
2651
2693
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -2990,6 +3032,15 @@ var SQLiteEventStore = class {
|
|
|
2990
3032
|
access_count INTEGER DEFAULT 0
|
|
2991
3033
|
);
|
|
2992
3034
|
|
|
3035
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3036
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3037
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3038
|
+
memory_id TEXT NOT NULL,
|
|
3039
|
+
event_id TEXT NOT NULL,
|
|
3040
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3041
|
+
);
|
|
3042
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3043
|
+
|
|
2993
3044
|
-- Continuity Log table (tracks context transitions)
|
|
2994
3045
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
2995
3046
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3349,11 +3400,17 @@ var SQLiteEventStore = class {
|
|
|
3349
3400
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3350
3401
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3351
3402
|
END;
|
|
3403
|
+
|
|
3404
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3405
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3406
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3407
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3408
|
+
event_id TEXT PRIMARY KEY,
|
|
3409
|
+
citation_id TEXT NOT NULL
|
|
3410
|
+
);
|
|
3411
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3352
3412
|
`);
|
|
3353
|
-
|
|
3354
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3355
|
-
} catch {
|
|
3356
|
-
}
|
|
3413
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3357
3414
|
try {
|
|
3358
3415
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3359
3416
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3390,55 +3447,17 @@ var SQLiteEventStore = class {
|
|
|
3390
3447
|
}
|
|
3391
3448
|
} catch {
|
|
3392
3449
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
try {
|
|
3398
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3399
|
-
} catch {
|
|
3400
|
-
}
|
|
3401
|
-
try {
|
|
3402
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3403
|
-
} catch {
|
|
3404
|
-
}
|
|
3405
|
-
try {
|
|
3406
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3407
|
-
} catch {
|
|
3408
|
-
}
|
|
3450
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3451
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3452
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3453
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3409
3454
|
try {
|
|
3410
3455
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3411
3456
|
} catch {
|
|
3412
3457
|
}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
try {
|
|
3417
|
-
sqliteExec(this.db, `
|
|
3418
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3419
|
-
`);
|
|
3420
|
-
} catch (err) {
|
|
3421
|
-
console.error("Error adding access_count column:", err);
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3425
|
-
try {
|
|
3426
|
-
sqliteExec(this.db, `
|
|
3427
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3428
|
-
`);
|
|
3429
|
-
} catch (err) {
|
|
3430
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
if (!columnNames.includes("turn_id")) {
|
|
3434
|
-
try {
|
|
3435
|
-
sqliteExec(this.db, `
|
|
3436
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3437
|
-
`);
|
|
3438
|
-
} catch (err) {
|
|
3439
|
-
console.error("Error adding turn_id column:", err);
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3458
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3459
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3460
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3442
3461
|
try {
|
|
3443
3462
|
sqliteExec(this.db, `
|
|
3444
3463
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3455,8 +3474,18 @@ var SQLiteEventStore = class {
|
|
|
3455
3474
|
sqliteExec(this.db, `
|
|
3456
3475
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3457
3476
|
`);
|
|
3458
|
-
} catch
|
|
3477
|
+
} catch {
|
|
3478
|
+
}
|
|
3479
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3480
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3481
|
+
sqliteExec(this.db, `
|
|
3482
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3483
|
+
SELECT cm.memory_id, je.value
|
|
3484
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3485
|
+
WHERE json_valid(cm.source_events);
|
|
3486
|
+
`);
|
|
3459
3487
|
}
|
|
3488
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3460
3489
|
this.initialized = true;
|
|
3461
3490
|
}
|
|
3462
3491
|
/**
|
|
@@ -3588,6 +3617,67 @@ var SQLiteEventStore = class {
|
|
|
3588
3617
|
);
|
|
3589
3618
|
return rows.map(this.rowToEvent);
|
|
3590
3619
|
}
|
|
3620
|
+
/**
|
|
3621
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3622
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3623
|
+
* scan also under-counted stores larger than its cap).
|
|
3624
|
+
*/
|
|
3625
|
+
async getEventTypeCounts(options) {
|
|
3626
|
+
await this.initialize();
|
|
3627
|
+
const rows = sqliteAll(
|
|
3628
|
+
this.db,
|
|
3629
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3630
|
+
[]
|
|
3631
|
+
);
|
|
3632
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3636
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3637
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3638
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3639
|
+
*/
|
|
3640
|
+
async getEventsAfter(sinceIso, options) {
|
|
3641
|
+
await this.initialize();
|
|
3642
|
+
const rows = sqliteAll(
|
|
3643
|
+
this.db,
|
|
3644
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3645
|
+
[sinceIso]
|
|
3646
|
+
);
|
|
3647
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3648
|
+
}
|
|
3649
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3650
|
+
async getDistinctSessionCount(options) {
|
|
3651
|
+
await this.initialize();
|
|
3652
|
+
const row = sqliteGet(
|
|
3653
|
+
this.db,
|
|
3654
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3655
|
+
[]
|
|
3656
|
+
);
|
|
3657
|
+
return row?.count ?? 0;
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3661
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3662
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3663
|
+
* lexicographically.
|
|
3664
|
+
*/
|
|
3665
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3666
|
+
await this.initialize();
|
|
3667
|
+
return sqliteAll(
|
|
3668
|
+
this.db,
|
|
3669
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3670
|
+
COUNT(*) as total,
|
|
3671
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3672
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3673
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3674
|
+
FROM events
|
|
3675
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3676
|
+
GROUP BY day
|
|
3677
|
+
ORDER BY day ASC`,
|
|
3678
|
+
[sinceIso]
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
3591
3681
|
/**
|
|
3592
3682
|
* Get event by ID
|
|
3593
3683
|
*/
|
|
@@ -3601,6 +3691,71 @@ var SQLiteEventStore = class {
|
|
|
3601
3691
|
if (!row) return null;
|
|
3602
3692
|
return this.rowToEvent(row);
|
|
3603
3693
|
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3696
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3697
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3698
|
+
*/
|
|
3699
|
+
async getEvents(ids, options) {
|
|
3700
|
+
await this.initialize();
|
|
3701
|
+
if (ids.length === 0) return [];
|
|
3702
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
3703
|
+
const rows = sqliteAll(
|
|
3704
|
+
this.db,
|
|
3705
|
+
`SELECT * FROM events WHERE id IN (${placeholders}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3706
|
+
ids
|
|
3707
|
+
);
|
|
3708
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3712
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3713
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3714
|
+
* freshly appended events) before retrying.
|
|
3715
|
+
*/
|
|
3716
|
+
async getEventByCitationId(citationId, options) {
|
|
3717
|
+
await this.initialize();
|
|
3718
|
+
if (!citationId) return null;
|
|
3719
|
+
let row = sqliteGet(
|
|
3720
|
+
this.db,
|
|
3721
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3722
|
+
[citationId]
|
|
3723
|
+
);
|
|
3724
|
+
if (!row) {
|
|
3725
|
+
this.indexMissingCitations();
|
|
3726
|
+
row = sqliteGet(
|
|
3727
|
+
this.db,
|
|
3728
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3729
|
+
[citationId]
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
if (!row) return null;
|
|
3733
|
+
return this.getEvent(row.event_id, options);
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3737
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3738
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3739
|
+
*/
|
|
3740
|
+
indexMissingCitations() {
|
|
3741
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3742
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3743
|
+
if (indexedCount >= eventCount) return;
|
|
3744
|
+
const unindexed = sqliteAll(
|
|
3745
|
+
this.db,
|
|
3746
|
+
`SELECT e.id FROM events e
|
|
3747
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3748
|
+
WHERE ec.event_id IS NULL`,
|
|
3749
|
+
[]
|
|
3750
|
+
);
|
|
3751
|
+
if (unindexed.length === 0) return;
|
|
3752
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3753
|
+
this.db.transaction(() => {
|
|
3754
|
+
for (const { id } of unindexed) {
|
|
3755
|
+
insert.run(id, generateCitationId(id));
|
|
3756
|
+
}
|
|
3757
|
+
})();
|
|
3758
|
+
}
|
|
3604
3759
|
/**
|
|
3605
3760
|
* Get events since a timestamp (for sync)
|
|
3606
3761
|
*/
|
|
@@ -3802,25 +3957,22 @@ var SQLiteEventStore = class {
|
|
|
3802
3957
|
*/
|
|
3803
3958
|
async getPendingOutboxItems(limit = 32) {
|
|
3804
3959
|
await this.initialize();
|
|
3805
|
-
const
|
|
3806
|
-
this.db,
|
|
3807
|
-
`SELECT * FROM embedding_outbox
|
|
3808
|
-
WHERE status = 'pending'
|
|
3809
|
-
ORDER BY created_at
|
|
3810
|
-
LIMIT ?`,
|
|
3811
|
-
[limit]
|
|
3812
|
-
);
|
|
3813
|
-
if (pending.length === 0) return [];
|
|
3814
|
-
const ids = pending.map((r) => r.id);
|
|
3815
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
3816
|
-
sqliteRun(
|
|
3960
|
+
const claimed = sqliteAll(
|
|
3817
3961
|
this.db,
|
|
3818
3962
|
`UPDATE embedding_outbox
|
|
3819
3963
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3820
|
-
WHERE id IN (
|
|
3821
|
-
|
|
3964
|
+
WHERE id IN (
|
|
3965
|
+
SELECT id FROM embedding_outbox
|
|
3966
|
+
WHERE status = 'pending'
|
|
3967
|
+
ORDER BY created_at
|
|
3968
|
+
LIMIT ?
|
|
3969
|
+
)
|
|
3970
|
+
RETURNING *`,
|
|
3971
|
+
[limit]
|
|
3822
3972
|
);
|
|
3823
|
-
|
|
3973
|
+
if (claimed.length === 0) return [];
|
|
3974
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
3975
|
+
return claimed.map((row) => ({
|
|
3824
3976
|
id: row.id,
|
|
3825
3977
|
eventId: row.event_id,
|
|
3826
3978
|
content: row.content,
|
|
@@ -4424,7 +4576,7 @@ var SQLiteEventStore = class {
|
|
|
4424
4576
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4425
4577
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4426
4578
|
let toolSuccessCount = 0;
|
|
4427
|
-
|
|
4579
|
+
const toolTotalCount = toolEvents.length;
|
|
4428
4580
|
for (const t of toolEvents) {
|
|
4429
4581
|
try {
|
|
4430
4582
|
const content = JSON.parse(t.content);
|
|
@@ -4587,10 +4739,14 @@ var SQLiteEventStore = class {
|
|
|
4587
4739
|
* Rebuild FTS index from existing events
|
|
4588
4740
|
* Call this once after upgrading to FTS5
|
|
4589
4741
|
*/
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4742
|
+
/**
|
|
4743
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4744
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4745
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4746
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4747
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4748
|
+
*/
|
|
4749
|
+
recreateEventsFtsTable() {
|
|
4594
4750
|
sqliteExec(this.db, `
|
|
4595
4751
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4596
4752
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4619,6 +4775,12 @@ var SQLiteEventStore = class {
|
|
|
4619
4775
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4620
4776
|
END;
|
|
4621
4777
|
`);
|
|
4778
|
+
}
|
|
4779
|
+
async rebuildFtsIndex() {
|
|
4780
|
+
await this.initialize();
|
|
4781
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4782
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4783
|
+
this.recreateEventsFtsTable();
|
|
4622
4784
|
return totalEvents;
|
|
4623
4785
|
}
|
|
4624
4786
|
/**
|
|
@@ -4636,6 +4798,35 @@ var SQLiteEventStore = class {
|
|
|
4636
4798
|
return false;
|
|
4637
4799
|
}
|
|
4638
4800
|
}
|
|
4801
|
+
hasTable(tableName) {
|
|
4802
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4803
|
+
try {
|
|
4804
|
+
const rows = sqliteAll(
|
|
4805
|
+
this.db,
|
|
4806
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4807
|
+
[tableName]
|
|
4808
|
+
);
|
|
4809
|
+
return rows.length > 0;
|
|
4810
|
+
} catch {
|
|
4811
|
+
return false;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4815
|
+
* Add a column only when its table exists and the column is missing.
|
|
4816
|
+
*
|
|
4817
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4818
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4819
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4820
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4821
|
+
*/
|
|
4822
|
+
addColumnIfMissing(table, column, definition) {
|
|
4823
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4824
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4825
|
+
}
|
|
4826
|
+
if (!this.hasTable(table)) return;
|
|
4827
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4828
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4829
|
+
}
|
|
4639
4830
|
async recordRetrievalTrace(input) {
|
|
4640
4831
|
await this.initialize();
|
|
4641
4832
|
const traceId = randomUUID6();
|
|
@@ -4721,6 +4912,45 @@ var SQLiteEventStore = class {
|
|
|
4721
4912
|
FROM retrieval_traces`,
|
|
4722
4913
|
[]
|
|
4723
4914
|
);
|
|
4915
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
4916
|
+
const strategyRows = sqliteAll(
|
|
4917
|
+
this.db,
|
|
4918
|
+
`SELECT
|
|
4919
|
+
${strategyColumnSql} as strategy,
|
|
4920
|
+
COUNT(*) as total_queries,
|
|
4921
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
4922
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
4923
|
+
SUM(candidate_count) as total_candidate_count,
|
|
4924
|
+
SUM(selected_count) as total_selected_count,
|
|
4925
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
4926
|
+
AVG(selected_count) as avg_selected_count,
|
|
4927
|
+
CASE
|
|
4928
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
4929
|
+
ELSE 0
|
|
4930
|
+
END as selection_rate
|
|
4931
|
+
FROM retrieval_traces
|
|
4932
|
+
GROUP BY ${strategyColumnSql}
|
|
4933
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
4934
|
+
[]
|
|
4935
|
+
);
|
|
4936
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
4937
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
4938
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
4939
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
4940
|
+
return {
|
|
4941
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
4942
|
+
totalQueries: strategyTotalQueries,
|
|
4943
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
4944
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
4945
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
4946
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
4947
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
4948
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
4949
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
4950
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
4951
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
4952
|
+
};
|
|
4953
|
+
});
|
|
4724
4954
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4725
4955
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4726
4956
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4738,7 +4968,8 @@ var SQLiteEventStore = class {
|
|
|
4738
4968
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4739
4969
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4740
4970
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4741
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
4971
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
4972
|
+
strategyBreakdown
|
|
4742
4973
|
};
|
|
4743
4974
|
} catch (err) {
|
|
4744
4975
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4754,7 +4985,8 @@ var SQLiteEventStore = class {
|
|
|
4754
4985
|
rewrittenSelectionRate: 0,
|
|
4755
4986
|
rawSelectionRate: 0,
|
|
4756
4987
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4757
|
-
avgSelectedCountForRawQueries: 0
|
|
4988
|
+
avgSelectedCountForRawQueries: 0,
|
|
4989
|
+
strategyBreakdown: []
|
|
4758
4990
|
};
|
|
4759
4991
|
}
|
|
4760
4992
|
throw err;
|
|
@@ -4871,38 +5103,19 @@ var SQLiteEventStore = class {
|
|
|
4871
5103
|
if (events.length === 0) return 0;
|
|
4872
5104
|
const eventIds = events.map((e) => e.id);
|
|
4873
5105
|
const placeholders = eventIds.map(() => "?").join(",");
|
|
4874
|
-
const
|
|
4875
|
-
|
|
4876
|
-
|
|
5106
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5107
|
+
const runDelete = this.db.transaction(() => {
|
|
5108
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4877
5109
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4878
|
-
ftsTriggersDropped.push(triggerName);
|
|
4879
|
-
} catch {
|
|
4880
5110
|
}
|
|
4881
|
-
|
|
4882
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
4883
|
-
try {
|
|
5111
|
+
for (const table of relatedTables) {
|
|
4884
5112
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
4885
|
-
} catch {
|
|
4886
5113
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
4893
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4894
|
-
END`);
|
|
4895
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
4896
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4897
|
-
END`);
|
|
4898
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
4899
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4900
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4901
|
-
END`);
|
|
4902
|
-
} catch {
|
|
4903
|
-
}
|
|
4904
|
-
}
|
|
4905
|
-
return result.changes || 0;
|
|
5114
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5115
|
+
this.recreateEventsFtsTable();
|
|
5116
|
+
return result.changes || 0;
|
|
5117
|
+
});
|
|
5118
|
+
return runDelete();
|
|
4906
5119
|
}
|
|
4907
5120
|
/**
|
|
4908
5121
|
* Convert database row to MemoryEvent
|
|
@@ -7235,7 +7448,7 @@ var SessionActorRepository = class {
|
|
|
7235
7448
|
};
|
|
7236
7449
|
|
|
7237
7450
|
// src/core/operations/perspective-observation-repository.ts
|
|
7238
|
-
import { createHash as
|
|
7451
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
7239
7452
|
function projectHashToStorage4(projectHash) {
|
|
7240
7453
|
return projectHash ?? "";
|
|
7241
7454
|
}
|
|
@@ -7272,7 +7485,7 @@ function sanitizeStoredRecord(value) {
|
|
|
7272
7485
|
return sanitizeGovernanceAuditValue(value);
|
|
7273
7486
|
}
|
|
7274
7487
|
function stableHash(value) {
|
|
7275
|
-
return
|
|
7488
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7276
7489
|
}
|
|
7277
7490
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
7278
7491
|
return stableHash({
|
|
@@ -8387,6 +8600,30 @@ var MemoryQueryService = class {
|
|
|
8387
8600
|
score: 1 - (r.rank - minRank) / rankRange
|
|
8388
8601
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
8389
8602
|
}
|
|
8603
|
+
async getEvent(id) {
|
|
8604
|
+
await this.initialize();
|
|
8605
|
+
return this.queryStore.getEvent(id);
|
|
8606
|
+
}
|
|
8607
|
+
async getEventByCitationId(citationId) {
|
|
8608
|
+
await this.initialize();
|
|
8609
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
8610
|
+
}
|
|
8611
|
+
async getEventsAfter(sinceIso) {
|
|
8612
|
+
await this.initialize();
|
|
8613
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
8614
|
+
}
|
|
8615
|
+
async getEventTypeCounts() {
|
|
8616
|
+
await this.initialize();
|
|
8617
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
8618
|
+
}
|
|
8619
|
+
async getDistinctSessionCount() {
|
|
8620
|
+
await this.initialize();
|
|
8621
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
8622
|
+
}
|
|
8623
|
+
async getDailyEventCounts(sinceIso) {
|
|
8624
|
+
await this.initialize();
|
|
8625
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
8626
|
+
}
|
|
8390
8627
|
async getSessionHistory(sessionId) {
|
|
8391
8628
|
await this.initialize();
|
|
8392
8629
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -9355,11 +9592,12 @@ var Retriever = class {
|
|
|
9355
9592
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
9356
9593
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
9357
9594
|
const filtered = [];
|
|
9595
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9358
9596
|
for (const result of results) {
|
|
9359
9597
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
9360
9598
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
9361
9599
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
9362
|
-
const event =
|
|
9600
|
+
const event = eventMap.get(result.eventId);
|
|
9363
9601
|
if (!event) continue;
|
|
9364
9602
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
9365
9603
|
if (normalizedIncludes.length > 0) {
|
|
@@ -9468,10 +9706,30 @@ var Retriever = class {
|
|
|
9468
9706
|
async retrieveRecent(limit = 100) {
|
|
9469
9707
|
return this.eventStore.getRecentEvents(limit);
|
|
9470
9708
|
}
|
|
9709
|
+
/**
|
|
9710
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
9711
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
9712
|
+
*/
|
|
9713
|
+
async fetchEventsByIds(ids) {
|
|
9714
|
+
const map = /* @__PURE__ */ new Map();
|
|
9715
|
+
if (ids.length === 0) return map;
|
|
9716
|
+
const unique = [...new Set(ids)];
|
|
9717
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
9718
|
+
const events = await this.eventStore.getEvents(unique);
|
|
9719
|
+
for (const event of events) map.set(event.id, event);
|
|
9720
|
+
} else {
|
|
9721
|
+
const events = await Promise.all(unique.map((id) => this.eventStore.getEvent(id)));
|
|
9722
|
+
for (const event of events) {
|
|
9723
|
+
if (event) map.set(event.id, event);
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
return map;
|
|
9727
|
+
}
|
|
9471
9728
|
async enrichResults(results, options, query) {
|
|
9472
9729
|
const memories = [];
|
|
9730
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9473
9731
|
for (const result of results) {
|
|
9474
|
-
const event =
|
|
9732
|
+
const event = eventMap.get(result.eventId);
|
|
9475
9733
|
if (!event) continue;
|
|
9476
9734
|
if (this.graduation) {
|
|
9477
9735
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -12224,6 +12482,34 @@ var MemoryService = class {
|
|
|
12224
12482
|
async rebuildFtsIndex() {
|
|
12225
12483
|
return this.queryService.rebuildFtsIndex();
|
|
12226
12484
|
}
|
|
12485
|
+
/**
|
|
12486
|
+
* Get a single event by id (indexed lookup).
|
|
12487
|
+
*/
|
|
12488
|
+
async getEvent(id) {
|
|
12489
|
+
return this.queryService.getEvent(id);
|
|
12490
|
+
}
|
|
12491
|
+
/**
|
|
12492
|
+
* Resolve an event from its citation id via the reverse index.
|
|
12493
|
+
*/
|
|
12494
|
+
async getEventByCitationId(citationId) {
|
|
12495
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
12496
|
+
}
|
|
12497
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
12498
|
+
async getEventsAfter(sinceIso) {
|
|
12499
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
12500
|
+
}
|
|
12501
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
12502
|
+
async getEventTypeCounts() {
|
|
12503
|
+
return this.queryService.getEventTypeCounts();
|
|
12504
|
+
}
|
|
12505
|
+
/** Count distinct sessions (SQL). */
|
|
12506
|
+
async getDistinctSessionCount() {
|
|
12507
|
+
return this.queryService.getDistinctSessionCount();
|
|
12508
|
+
}
|
|
12509
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
12510
|
+
async getDailyEventCounts(sinceIso) {
|
|
12511
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
12512
|
+
}
|
|
12227
12513
|
/**
|
|
12228
12514
|
* Get session history
|
|
12229
12515
|
*/
|