claude-memory-layer 1.0.53 → 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 +922 -524
- 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 +577 -231
- package/dist/mcp/index.js.map +4 -4
- package/dist/server/api/index.js +691 -354
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +742 -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
package/dist/server/index.js
CHANGED
|
@@ -12,8 +12,8 @@ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
|
12
12
|
import { logger } from "hono/logger";
|
|
13
13
|
import { serve } from "@hono/node-server";
|
|
14
14
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
15
|
-
import * as
|
|
16
|
-
import * as
|
|
15
|
+
import * as path14 from "path";
|
|
16
|
+
import * as fs12 from "fs";
|
|
17
17
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
18
18
|
import { fileURLToPath as fileUrlToPath } from "url";
|
|
19
19
|
|
|
@@ -175,7 +175,13 @@ function toDate(value) {
|
|
|
175
175
|
return new Date(String(value));
|
|
176
176
|
}
|
|
177
177
|
function createDatabase(dbPath, options) {
|
|
178
|
-
|
|
178
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
179
|
+
db.pragma("busy_timeout = 5000");
|
|
180
|
+
if (!options?.readOnly) {
|
|
181
|
+
db.pragma("journal_mode = WAL");
|
|
182
|
+
db.pragma("synchronous = NORMAL");
|
|
183
|
+
}
|
|
184
|
+
return db;
|
|
179
185
|
}
|
|
180
186
|
function dbRun(db, sql, params = []) {
|
|
181
187
|
db.prepare(sql).run(...params);
|
|
@@ -408,19 +414,29 @@ var ConsolidatedStore = class {
|
|
|
408
414
|
*/
|
|
409
415
|
async create(input) {
|
|
410
416
|
const memoryId = randomUUID2();
|
|
411
|
-
|
|
412
|
-
|
|
417
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
418
|
+
const insertMemory = this.db.prepare(
|
|
413
419
|
`INSERT INTO consolidated_memories
|
|
414
420
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
415
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
416
|
-
|
|
421
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
422
|
+
);
|
|
423
|
+
const insertEvent = this.db.prepare(
|
|
424
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
425
|
+
);
|
|
426
|
+
this.db.transaction(() => {
|
|
427
|
+
insertMemory.run(
|
|
417
428
|
memoryId,
|
|
418
429
|
input.summary,
|
|
419
430
|
JSON.stringify(input.topics),
|
|
420
|
-
JSON.stringify(
|
|
431
|
+
JSON.stringify(sourceEvents),
|
|
421
432
|
input.confidence
|
|
422
|
-
|
|
423
|
-
|
|
433
|
+
);
|
|
434
|
+
for (const eventId of sourceEvents) {
|
|
435
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
436
|
+
insertEvent.run(memoryId, eventId);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
})();
|
|
424
440
|
return memoryId;
|
|
425
441
|
}
|
|
426
442
|
/**
|
|
@@ -526,11 +542,12 @@ var ConsolidatedStore = class {
|
|
|
526
542
|
* Delete a consolidated memory
|
|
527
543
|
*/
|
|
528
544
|
async delete(memoryId) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
545
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
546
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
547
|
+
this.db.transaction(() => {
|
|
548
|
+
deleteEvents.run(memoryId);
|
|
549
|
+
deleteMemory.run(memoryId);
|
|
550
|
+
})();
|
|
534
551
|
}
|
|
535
552
|
/**
|
|
536
553
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -643,16 +660,16 @@ var ConsolidatedStore = class {
|
|
|
643
660
|
* Check if source events are already consolidated
|
|
644
661
|
*/
|
|
645
662
|
async isAlreadyConsolidated(eventIds) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return
|
|
663
|
+
if (eventIds.length === 0) return false;
|
|
664
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
665
|
+
const rows = await dbAll(
|
|
666
|
+
this.db,
|
|
667
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
668
|
+
WHERE event_id IN (${placeholders})
|
|
669
|
+
LIMIT 1`,
|
|
670
|
+
eventIds
|
|
671
|
+
);
|
|
672
|
+
return rows.length > 0;
|
|
656
673
|
}
|
|
657
674
|
/**
|
|
658
675
|
* Get the last consolidation time
|
|
@@ -2188,6 +2205,23 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2188
2205
|
return `${sessionId}:${contentHash}`;
|
|
2189
2206
|
}
|
|
2190
2207
|
|
|
2208
|
+
// src/core/citation-generator.ts
|
|
2209
|
+
import { createHash as createHash2 } from "crypto";
|
|
2210
|
+
var ID_LENGTH = 6;
|
|
2211
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2212
|
+
function generateCitationId(eventId) {
|
|
2213
|
+
const hash = createHash2("sha256").update(eventId).digest();
|
|
2214
|
+
let id = "";
|
|
2215
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2216
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2217
|
+
}
|
|
2218
|
+
return id;
|
|
2219
|
+
}
|
|
2220
|
+
function parseCitationId(formatted) {
|
|
2221
|
+
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
2222
|
+
return match ? match[1] : null;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2191
2225
|
// src/core/sqlite-event-store.ts
|
|
2192
2226
|
import * as nodePath2 from "path";
|
|
2193
2227
|
|
|
@@ -2221,12 +2255,12 @@ function resolveProjectStoragePath(projectOrHash) {
|
|
|
2221
2255
|
import Database from "better-sqlite3";
|
|
2222
2256
|
import * as fs4 from "fs";
|
|
2223
2257
|
import * as nodePath from "path";
|
|
2224
|
-
function createSQLiteDatabase(
|
|
2225
|
-
const dir = nodePath.dirname(
|
|
2258
|
+
function createSQLiteDatabase(path15, options) {
|
|
2259
|
+
const dir = nodePath.dirname(path15);
|
|
2226
2260
|
if (!fs4.existsSync(dir)) {
|
|
2227
2261
|
fs4.mkdirSync(dir, { recursive: true });
|
|
2228
2262
|
}
|
|
2229
|
-
const db = new Database(
|
|
2263
|
+
const db = new Database(path15, {
|
|
2230
2264
|
readonly: options?.readonly ?? false
|
|
2231
2265
|
});
|
|
2232
2266
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -2236,17 +2270,28 @@ function createSQLiteDatabase(path14, options) {
|
|
|
2236
2270
|
}
|
|
2237
2271
|
return db;
|
|
2238
2272
|
}
|
|
2273
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2274
|
+
function prepareCached(db, sql) {
|
|
2275
|
+
let cache = statementCache.get(db);
|
|
2276
|
+
if (!cache) {
|
|
2277
|
+
cache = /* @__PURE__ */ new Map();
|
|
2278
|
+
statementCache.set(db, cache);
|
|
2279
|
+
}
|
|
2280
|
+
let stmt = cache.get(sql);
|
|
2281
|
+
if (!stmt) {
|
|
2282
|
+
stmt = db.prepare(sql);
|
|
2283
|
+
cache.set(sql, stmt);
|
|
2284
|
+
}
|
|
2285
|
+
return stmt;
|
|
2286
|
+
}
|
|
2239
2287
|
function sqliteRun(db, sql, params = []) {
|
|
2240
|
-
|
|
2241
|
-
return stmt.run(...params);
|
|
2288
|
+
return prepareCached(db, sql).run(...params);
|
|
2242
2289
|
}
|
|
2243
2290
|
function sqliteAll(db, sql, params = []) {
|
|
2244
|
-
|
|
2245
|
-
return stmt.all(...params);
|
|
2291
|
+
return prepareCached(db, sql).all(...params);
|
|
2246
2292
|
}
|
|
2247
2293
|
function sqliteGet(db, sql, params = []) {
|
|
2248
|
-
|
|
2249
|
-
return stmt.get(...params);
|
|
2294
|
+
return prepareCached(db, sql).get(...params);
|
|
2250
2295
|
}
|
|
2251
2296
|
function sqliteExec(db, sql) {
|
|
2252
2297
|
db.exec(sql);
|
|
@@ -2668,6 +2713,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2668
2713
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2669
2714
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2670
2715
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2716
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2671
2717
|
function emptyOutboxRecoveryResult() {
|
|
2672
2718
|
return {
|
|
2673
2719
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -2677,17 +2723,17 @@ function emptyOutboxRecoveryResult() {
|
|
|
2677
2723
|
function isRecord(value) {
|
|
2678
2724
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2679
2725
|
}
|
|
2680
|
-
function getNestedRecord(root,
|
|
2726
|
+
function getNestedRecord(root, path15) {
|
|
2681
2727
|
let cursor = root;
|
|
2682
|
-
for (const key of
|
|
2728
|
+
for (const key of path15) {
|
|
2683
2729
|
if (!isRecord(cursor)) return void 0;
|
|
2684
2730
|
cursor = cursor[key];
|
|
2685
2731
|
}
|
|
2686
2732
|
return isRecord(cursor) ? cursor : void 0;
|
|
2687
2733
|
}
|
|
2688
|
-
function getNestedString(root,
|
|
2734
|
+
function getNestedString(root, path15) {
|
|
2689
2735
|
let cursor = root;
|
|
2690
|
-
for (const key of
|
|
2736
|
+
for (const key of path15) {
|
|
2691
2737
|
if (!isRecord(cursor)) return void 0;
|
|
2692
2738
|
cursor = cursor[key];
|
|
2693
2739
|
}
|
|
@@ -3012,6 +3058,15 @@ var SQLiteEventStore = class {
|
|
|
3012
3058
|
access_count INTEGER DEFAULT 0
|
|
3013
3059
|
);
|
|
3014
3060
|
|
|
3061
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3062
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3063
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3064
|
+
memory_id TEXT NOT NULL,
|
|
3065
|
+
event_id TEXT NOT NULL,
|
|
3066
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3067
|
+
);
|
|
3068
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3069
|
+
|
|
3015
3070
|
-- Continuity Log table (tracks context transitions)
|
|
3016
3071
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
3017
3072
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3371,11 +3426,17 @@ var SQLiteEventStore = class {
|
|
|
3371
3426
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3372
3427
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3373
3428
|
END;
|
|
3429
|
+
|
|
3430
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3431
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3432
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3433
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3434
|
+
event_id TEXT PRIMARY KEY,
|
|
3435
|
+
citation_id TEXT NOT NULL
|
|
3436
|
+
);
|
|
3437
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3374
3438
|
`);
|
|
3375
|
-
|
|
3376
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3377
|
-
} catch {
|
|
3378
|
-
}
|
|
3439
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3379
3440
|
try {
|
|
3380
3441
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3381
3442
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3412,55 +3473,17 @@ var SQLiteEventStore = class {
|
|
|
3412
3473
|
}
|
|
3413
3474
|
} catch {
|
|
3414
3475
|
}
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
try {
|
|
3420
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3421
|
-
} catch {
|
|
3422
|
-
}
|
|
3423
|
-
try {
|
|
3424
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3425
|
-
} catch {
|
|
3426
|
-
}
|
|
3427
|
-
try {
|
|
3428
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3429
|
-
} catch {
|
|
3430
|
-
}
|
|
3476
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3477
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3478
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3479
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3431
3480
|
try {
|
|
3432
3481
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3433
3482
|
} catch {
|
|
3434
3483
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
try {
|
|
3439
|
-
sqliteExec(this.db, `
|
|
3440
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3441
|
-
`);
|
|
3442
|
-
} catch (err) {
|
|
3443
|
-
console.error("Error adding access_count column:", err);
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
3446
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3447
|
-
try {
|
|
3448
|
-
sqliteExec(this.db, `
|
|
3449
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3450
|
-
`);
|
|
3451
|
-
} catch (err) {
|
|
3452
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
if (!columnNames.includes("turn_id")) {
|
|
3456
|
-
try {
|
|
3457
|
-
sqliteExec(this.db, `
|
|
3458
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3459
|
-
`);
|
|
3460
|
-
} catch (err) {
|
|
3461
|
-
console.error("Error adding turn_id column:", err);
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3484
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3485
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3486
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3464
3487
|
try {
|
|
3465
3488
|
sqliteExec(this.db, `
|
|
3466
3489
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3477,8 +3500,18 @@ var SQLiteEventStore = class {
|
|
|
3477
3500
|
sqliteExec(this.db, `
|
|
3478
3501
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3479
3502
|
`);
|
|
3480
|
-
} catch
|
|
3503
|
+
} catch {
|
|
3504
|
+
}
|
|
3505
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3506
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3507
|
+
sqliteExec(this.db, `
|
|
3508
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3509
|
+
SELECT cm.memory_id, je.value
|
|
3510
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3511
|
+
WHERE json_valid(cm.source_events);
|
|
3512
|
+
`);
|
|
3481
3513
|
}
|
|
3514
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3482
3515
|
this.initialized = true;
|
|
3483
3516
|
}
|
|
3484
3517
|
/**
|
|
@@ -3610,6 +3643,67 @@ var SQLiteEventStore = class {
|
|
|
3610
3643
|
);
|
|
3611
3644
|
return rows.map(this.rowToEvent);
|
|
3612
3645
|
}
|
|
3646
|
+
/**
|
|
3647
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3648
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3649
|
+
* scan also under-counted stores larger than its cap).
|
|
3650
|
+
*/
|
|
3651
|
+
async getEventTypeCounts(options) {
|
|
3652
|
+
await this.initialize();
|
|
3653
|
+
const rows = sqliteAll(
|
|
3654
|
+
this.db,
|
|
3655
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3656
|
+
[]
|
|
3657
|
+
);
|
|
3658
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3662
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3663
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3664
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3665
|
+
*/
|
|
3666
|
+
async getEventsAfter(sinceIso, options) {
|
|
3667
|
+
await this.initialize();
|
|
3668
|
+
const rows = sqliteAll(
|
|
3669
|
+
this.db,
|
|
3670
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3671
|
+
[sinceIso]
|
|
3672
|
+
);
|
|
3673
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3674
|
+
}
|
|
3675
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3676
|
+
async getDistinctSessionCount(options) {
|
|
3677
|
+
await this.initialize();
|
|
3678
|
+
const row = sqliteGet(
|
|
3679
|
+
this.db,
|
|
3680
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3681
|
+
[]
|
|
3682
|
+
);
|
|
3683
|
+
return row?.count ?? 0;
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3687
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3688
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3689
|
+
* lexicographically.
|
|
3690
|
+
*/
|
|
3691
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3692
|
+
await this.initialize();
|
|
3693
|
+
return sqliteAll(
|
|
3694
|
+
this.db,
|
|
3695
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3696
|
+
COUNT(*) as total,
|
|
3697
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3698
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3699
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3700
|
+
FROM events
|
|
3701
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3702
|
+
GROUP BY day
|
|
3703
|
+
ORDER BY day ASC`,
|
|
3704
|
+
[sinceIso]
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3613
3707
|
/**
|
|
3614
3708
|
* Get event by ID
|
|
3615
3709
|
*/
|
|
@@ -3623,6 +3717,71 @@ var SQLiteEventStore = class {
|
|
|
3623
3717
|
if (!row) return null;
|
|
3624
3718
|
return this.rowToEvent(row);
|
|
3625
3719
|
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3722
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3723
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3724
|
+
*/
|
|
3725
|
+
async getEvents(ids, options) {
|
|
3726
|
+
await this.initialize();
|
|
3727
|
+
if (ids.length === 0) return [];
|
|
3728
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
3729
|
+
const rows = sqliteAll(
|
|
3730
|
+
this.db,
|
|
3731
|
+
`SELECT * FROM events WHERE id IN (${placeholders}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3732
|
+
ids
|
|
3733
|
+
);
|
|
3734
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3738
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3739
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3740
|
+
* freshly appended events) before retrying.
|
|
3741
|
+
*/
|
|
3742
|
+
async getEventByCitationId(citationId, options) {
|
|
3743
|
+
await this.initialize();
|
|
3744
|
+
if (!citationId) return null;
|
|
3745
|
+
let row = sqliteGet(
|
|
3746
|
+
this.db,
|
|
3747
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3748
|
+
[citationId]
|
|
3749
|
+
);
|
|
3750
|
+
if (!row) {
|
|
3751
|
+
this.indexMissingCitations();
|
|
3752
|
+
row = sqliteGet(
|
|
3753
|
+
this.db,
|
|
3754
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3755
|
+
[citationId]
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3758
|
+
if (!row) return null;
|
|
3759
|
+
return this.getEvent(row.event_id, options);
|
|
3760
|
+
}
|
|
3761
|
+
/**
|
|
3762
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3763
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3764
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3765
|
+
*/
|
|
3766
|
+
indexMissingCitations() {
|
|
3767
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3768
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3769
|
+
if (indexedCount >= eventCount) return;
|
|
3770
|
+
const unindexed = sqliteAll(
|
|
3771
|
+
this.db,
|
|
3772
|
+
`SELECT e.id FROM events e
|
|
3773
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3774
|
+
WHERE ec.event_id IS NULL`,
|
|
3775
|
+
[]
|
|
3776
|
+
);
|
|
3777
|
+
if (unindexed.length === 0) return;
|
|
3778
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3779
|
+
this.db.transaction(() => {
|
|
3780
|
+
for (const { id } of unindexed) {
|
|
3781
|
+
insert.run(id, generateCitationId(id));
|
|
3782
|
+
}
|
|
3783
|
+
})();
|
|
3784
|
+
}
|
|
3626
3785
|
/**
|
|
3627
3786
|
* Get events since a timestamp (for sync)
|
|
3628
3787
|
*/
|
|
@@ -3824,25 +3983,22 @@ var SQLiteEventStore = class {
|
|
|
3824
3983
|
*/
|
|
3825
3984
|
async getPendingOutboxItems(limit = 32) {
|
|
3826
3985
|
await this.initialize();
|
|
3827
|
-
const
|
|
3828
|
-
this.db,
|
|
3829
|
-
`SELECT * FROM embedding_outbox
|
|
3830
|
-
WHERE status = 'pending'
|
|
3831
|
-
ORDER BY created_at
|
|
3832
|
-
LIMIT ?`,
|
|
3833
|
-
[limit]
|
|
3834
|
-
);
|
|
3835
|
-
if (pending.length === 0) return [];
|
|
3836
|
-
const ids = pending.map((r) => r.id);
|
|
3837
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
3838
|
-
sqliteRun(
|
|
3986
|
+
const claimed = sqliteAll(
|
|
3839
3987
|
this.db,
|
|
3840
3988
|
`UPDATE embedding_outbox
|
|
3841
3989
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3842
|
-
WHERE id IN (
|
|
3843
|
-
|
|
3990
|
+
WHERE id IN (
|
|
3991
|
+
SELECT id FROM embedding_outbox
|
|
3992
|
+
WHERE status = 'pending'
|
|
3993
|
+
ORDER BY created_at
|
|
3994
|
+
LIMIT ?
|
|
3995
|
+
)
|
|
3996
|
+
RETURNING *`,
|
|
3997
|
+
[limit]
|
|
3844
3998
|
);
|
|
3845
|
-
|
|
3999
|
+
if (claimed.length === 0) return [];
|
|
4000
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
4001
|
+
return claimed.map((row) => ({
|
|
3846
4002
|
id: row.id,
|
|
3847
4003
|
eventId: row.event_id,
|
|
3848
4004
|
content: row.content,
|
|
@@ -4446,7 +4602,7 @@ var SQLiteEventStore = class {
|
|
|
4446
4602
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4447
4603
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4448
4604
|
let toolSuccessCount = 0;
|
|
4449
|
-
|
|
4605
|
+
const toolTotalCount = toolEvents.length;
|
|
4450
4606
|
for (const t of toolEvents) {
|
|
4451
4607
|
try {
|
|
4452
4608
|
const content = JSON.parse(t.content);
|
|
@@ -4609,10 +4765,14 @@ var SQLiteEventStore = class {
|
|
|
4609
4765
|
* Rebuild FTS index from existing events
|
|
4610
4766
|
* Call this once after upgrading to FTS5
|
|
4611
4767
|
*/
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4768
|
+
/**
|
|
4769
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4770
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4771
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4772
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4773
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4774
|
+
*/
|
|
4775
|
+
recreateEventsFtsTable() {
|
|
4616
4776
|
sqliteExec(this.db, `
|
|
4617
4777
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4618
4778
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4641,6 +4801,12 @@ var SQLiteEventStore = class {
|
|
|
4641
4801
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4642
4802
|
END;
|
|
4643
4803
|
`);
|
|
4804
|
+
}
|
|
4805
|
+
async rebuildFtsIndex() {
|
|
4806
|
+
await this.initialize();
|
|
4807
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4808
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4809
|
+
this.recreateEventsFtsTable();
|
|
4644
4810
|
return totalEvents;
|
|
4645
4811
|
}
|
|
4646
4812
|
/**
|
|
@@ -4658,6 +4824,35 @@ var SQLiteEventStore = class {
|
|
|
4658
4824
|
return false;
|
|
4659
4825
|
}
|
|
4660
4826
|
}
|
|
4827
|
+
hasTable(tableName) {
|
|
4828
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4829
|
+
try {
|
|
4830
|
+
const rows = sqliteAll(
|
|
4831
|
+
this.db,
|
|
4832
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4833
|
+
[tableName]
|
|
4834
|
+
);
|
|
4835
|
+
return rows.length > 0;
|
|
4836
|
+
} catch {
|
|
4837
|
+
return false;
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
/**
|
|
4841
|
+
* Add a column only when its table exists and the column is missing.
|
|
4842
|
+
*
|
|
4843
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4844
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4845
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4846
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4847
|
+
*/
|
|
4848
|
+
addColumnIfMissing(table, column, definition) {
|
|
4849
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4850
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4851
|
+
}
|
|
4852
|
+
if (!this.hasTable(table)) return;
|
|
4853
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4854
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4855
|
+
}
|
|
4661
4856
|
async recordRetrievalTrace(input) {
|
|
4662
4857
|
await this.initialize();
|
|
4663
4858
|
const traceId = randomUUID6();
|
|
@@ -4743,6 +4938,45 @@ var SQLiteEventStore = class {
|
|
|
4743
4938
|
FROM retrieval_traces`,
|
|
4744
4939
|
[]
|
|
4745
4940
|
);
|
|
4941
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
4942
|
+
const strategyRows = sqliteAll(
|
|
4943
|
+
this.db,
|
|
4944
|
+
`SELECT
|
|
4945
|
+
${strategyColumnSql} as strategy,
|
|
4946
|
+
COUNT(*) as total_queries,
|
|
4947
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
4948
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
4949
|
+
SUM(candidate_count) as total_candidate_count,
|
|
4950
|
+
SUM(selected_count) as total_selected_count,
|
|
4951
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
4952
|
+
AVG(selected_count) as avg_selected_count,
|
|
4953
|
+
CASE
|
|
4954
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
4955
|
+
ELSE 0
|
|
4956
|
+
END as selection_rate
|
|
4957
|
+
FROM retrieval_traces
|
|
4958
|
+
GROUP BY ${strategyColumnSql}
|
|
4959
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
4960
|
+
[]
|
|
4961
|
+
);
|
|
4962
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
4963
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
4964
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
4965
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
4966
|
+
return {
|
|
4967
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
4968
|
+
totalQueries: strategyTotalQueries,
|
|
4969
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
4970
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
4971
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
4972
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
4973
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
4974
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
4975
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
4976
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
4977
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
4978
|
+
};
|
|
4979
|
+
});
|
|
4746
4980
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4747
4981
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4748
4982
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4760,7 +4994,8 @@ var SQLiteEventStore = class {
|
|
|
4760
4994
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4761
4995
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4762
4996
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4763
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
4997
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
4998
|
+
strategyBreakdown
|
|
4764
4999
|
};
|
|
4765
5000
|
} catch (err) {
|
|
4766
5001
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4776,7 +5011,8 @@ var SQLiteEventStore = class {
|
|
|
4776
5011
|
rewrittenSelectionRate: 0,
|
|
4777
5012
|
rawSelectionRate: 0,
|
|
4778
5013
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4779
|
-
avgSelectedCountForRawQueries: 0
|
|
5014
|
+
avgSelectedCountForRawQueries: 0,
|
|
5015
|
+
strategyBreakdown: []
|
|
4780
5016
|
};
|
|
4781
5017
|
}
|
|
4782
5018
|
throw err;
|
|
@@ -4893,38 +5129,19 @@ var SQLiteEventStore = class {
|
|
|
4893
5129
|
if (events.length === 0) return 0;
|
|
4894
5130
|
const eventIds = events.map((e) => e.id);
|
|
4895
5131
|
const placeholders = eventIds.map(() => "?").join(",");
|
|
4896
|
-
const
|
|
4897
|
-
|
|
4898
|
-
|
|
5132
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5133
|
+
const runDelete = this.db.transaction(() => {
|
|
5134
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4899
5135
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4900
|
-
ftsTriggersDropped.push(triggerName);
|
|
4901
|
-
} catch {
|
|
4902
5136
|
}
|
|
4903
|
-
|
|
4904
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
4905
|
-
try {
|
|
5137
|
+
for (const table of relatedTables) {
|
|
4906
5138
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
4907
|
-
} catch {
|
|
4908
|
-
}
|
|
4909
|
-
}
|
|
4910
|
-
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
4911
|
-
if (ftsTriggersDropped.length > 0) {
|
|
4912
|
-
try {
|
|
4913
|
-
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
4914
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
4915
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4916
|
-
END`);
|
|
4917
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
4918
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4919
|
-
END`);
|
|
4920
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
4921
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4922
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4923
|
-
END`);
|
|
4924
|
-
} catch {
|
|
4925
5139
|
}
|
|
4926
|
-
|
|
4927
|
-
|
|
5140
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5141
|
+
this.recreateEventsFtsTable();
|
|
5142
|
+
return result.changes || 0;
|
|
5143
|
+
});
|
|
5144
|
+
return runDelete();
|
|
4928
5145
|
}
|
|
4929
5146
|
/**
|
|
4930
5147
|
* Convert database row to MemoryEvent
|
|
@@ -6479,12 +6696,12 @@ var GraphPathService = class {
|
|
|
6479
6696
|
}
|
|
6480
6697
|
}
|
|
6481
6698
|
}
|
|
6482
|
-
const paths = Array.from(bestByTarget.entries()).map(([key,
|
|
6699
|
+
const paths = Array.from(bestByTarget.entries()).map(([key, path15]) => ({
|
|
6483
6700
|
target: graph.node(nodeFromKey(key)),
|
|
6484
|
-
hops:
|
|
6485
|
-
totalCost:
|
|
6486
|
-
scoreContribution:
|
|
6487
|
-
steps:
|
|
6701
|
+
hops: path15.hops,
|
|
6702
|
+
totalCost: path15.totalCost,
|
|
6703
|
+
scoreContribution: path15.totalCost > 0 ? 1 / path15.totalCost : 0,
|
|
6704
|
+
steps: path15.steps
|
|
6488
6705
|
})).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
|
|
6489
6706
|
return { startNodes, effectiveMaxHops, paths };
|
|
6490
6707
|
}
|
|
@@ -7257,7 +7474,7 @@ var SessionActorRepository = class {
|
|
|
7257
7474
|
};
|
|
7258
7475
|
|
|
7259
7476
|
// src/core/operations/perspective-observation-repository.ts
|
|
7260
|
-
import { createHash as
|
|
7477
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
7261
7478
|
function projectHashToStorage4(projectHash) {
|
|
7262
7479
|
return projectHash ?? "";
|
|
7263
7480
|
}
|
|
@@ -7294,7 +7511,7 @@ function sanitizeStoredRecord(value) {
|
|
|
7294
7511
|
return sanitizeGovernanceAuditValue(value);
|
|
7295
7512
|
}
|
|
7296
7513
|
function stableHash(value) {
|
|
7297
|
-
return
|
|
7514
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7298
7515
|
}
|
|
7299
7516
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
7300
7517
|
return stableHash({
|
|
@@ -7802,23 +8019,6 @@ function createPerspectiveDeriver(options) {
|
|
|
7802
8019
|
return new PerspectiveDeriver(options);
|
|
7803
8020
|
}
|
|
7804
8021
|
|
|
7805
|
-
// src/core/citation-generator.ts
|
|
7806
|
-
import { createHash as createHash4 } from "crypto";
|
|
7807
|
-
var ID_LENGTH = 6;
|
|
7808
|
-
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
7809
|
-
function generateCitationId(eventId) {
|
|
7810
|
-
const hash = createHash4("sha256").update(eventId).digest();
|
|
7811
|
-
let id = "";
|
|
7812
|
-
for (let i = 0; i < ID_LENGTH; i++) {
|
|
7813
|
-
id += CHARSET[hash[i] % CHARSET.length];
|
|
7814
|
-
}
|
|
7815
|
-
return id;
|
|
7816
|
-
}
|
|
7817
|
-
function parseCitationId(formatted) {
|
|
7818
|
-
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
7819
|
-
return match ? match[1] : null;
|
|
7820
|
-
}
|
|
7821
|
-
|
|
7822
8022
|
// src/core/vector-store.ts
|
|
7823
8023
|
import * as lancedb from "@lancedb/lancedb";
|
|
7824
8024
|
var VectorStore = class {
|
|
@@ -8426,6 +8626,30 @@ var MemoryQueryService = class {
|
|
|
8426
8626
|
score: 1 - (r.rank - minRank) / rankRange
|
|
8427
8627
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
8428
8628
|
}
|
|
8629
|
+
async getEvent(id) {
|
|
8630
|
+
await this.initialize();
|
|
8631
|
+
return this.queryStore.getEvent(id);
|
|
8632
|
+
}
|
|
8633
|
+
async getEventByCitationId(citationId) {
|
|
8634
|
+
await this.initialize();
|
|
8635
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
8636
|
+
}
|
|
8637
|
+
async getEventsAfter(sinceIso) {
|
|
8638
|
+
await this.initialize();
|
|
8639
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
8640
|
+
}
|
|
8641
|
+
async getEventTypeCounts() {
|
|
8642
|
+
await this.initialize();
|
|
8643
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
8644
|
+
}
|
|
8645
|
+
async getDistinctSessionCount() {
|
|
8646
|
+
await this.initialize();
|
|
8647
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
8648
|
+
}
|
|
8649
|
+
async getDailyEventCounts(sinceIso) {
|
|
8650
|
+
await this.initialize();
|
|
8651
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
8652
|
+
}
|
|
8429
8653
|
async getSessionHistory(sessionId) {
|
|
8430
8654
|
await this.initialize();
|
|
8431
8655
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -9250,12 +9474,12 @@ var Retriever = class {
|
|
|
9250
9474
|
direction: "both"
|
|
9251
9475
|
});
|
|
9252
9476
|
const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
|
|
9253
|
-
for (const
|
|
9254
|
-
if (
|
|
9255
|
-
const target = await this.eventStore.getEvent(
|
|
9477
|
+
for (const path15 of expansion.paths) {
|
|
9478
|
+
if (path15.target.type !== "event") continue;
|
|
9479
|
+
const target = await this.eventStore.getEvent(path15.target.id);
|
|
9256
9480
|
if (!target) continue;
|
|
9257
|
-
const graphPath = toRetrievalGraphPathDebug(
|
|
9258
|
-
const score = graphPathScore(
|
|
9481
|
+
const graphPath = toRetrievalGraphPathDebug(path15, titleByEntityId);
|
|
9482
|
+
const score = graphPathScore(path15, opts.hopPenalty);
|
|
9259
9483
|
const existing = byId.get(target.id);
|
|
9260
9484
|
const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
|
|
9261
9485
|
const graphLane = {
|
|
@@ -9264,7 +9488,7 @@ var Retriever = class {
|
|
|
9264
9488
|
score
|
|
9265
9489
|
};
|
|
9266
9490
|
const row = {
|
|
9267
|
-
id: existing?.id ?? `graph-path-${
|
|
9491
|
+
id: existing?.id ?? `graph-path-${path15.hops}-${target.id}`,
|
|
9268
9492
|
eventId: target.id,
|
|
9269
9493
|
content: target.content,
|
|
9270
9494
|
score: Math.max(existing?.score ?? 0, score),
|
|
@@ -9394,11 +9618,12 @@ var Retriever = class {
|
|
|
9394
9618
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
9395
9619
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
9396
9620
|
const filtered = [];
|
|
9621
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9397
9622
|
for (const result of results) {
|
|
9398
9623
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
9399
9624
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
9400
9625
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
9401
|
-
const event =
|
|
9626
|
+
const event = eventMap.get(result.eventId);
|
|
9402
9627
|
if (!event) continue;
|
|
9403
9628
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
9404
9629
|
if (normalizedIncludes.length > 0) {
|
|
@@ -9507,10 +9732,30 @@ var Retriever = class {
|
|
|
9507
9732
|
async retrieveRecent(limit = 100) {
|
|
9508
9733
|
return this.eventStore.getRecentEvents(limit);
|
|
9509
9734
|
}
|
|
9735
|
+
/**
|
|
9736
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
9737
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
9738
|
+
*/
|
|
9739
|
+
async fetchEventsByIds(ids) {
|
|
9740
|
+
const map = /* @__PURE__ */ new Map();
|
|
9741
|
+
if (ids.length === 0) return map;
|
|
9742
|
+
const unique = [...new Set(ids)];
|
|
9743
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
9744
|
+
const events = await this.eventStore.getEvents(unique);
|
|
9745
|
+
for (const event of events) map.set(event.id, event);
|
|
9746
|
+
} else {
|
|
9747
|
+
const events = await Promise.all(unique.map((id) => this.eventStore.getEvent(id)));
|
|
9748
|
+
for (const event of events) {
|
|
9749
|
+
if (event) map.set(event.id, event);
|
|
9750
|
+
}
|
|
9751
|
+
}
|
|
9752
|
+
return map;
|
|
9753
|
+
}
|
|
9510
9754
|
async enrichResults(results, options, query) {
|
|
9511
9755
|
const memories = [];
|
|
9756
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9512
9757
|
for (const result of results) {
|
|
9513
|
-
const event =
|
|
9758
|
+
const event = eventMap.get(result.eventId);
|
|
9514
9759
|
if (!event) continue;
|
|
9515
9760
|
if (this.graduation) {
|
|
9516
9761
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -9583,8 +9828,8 @@ _Context:_ ${sessionContext}`;
|
|
|
9583
9828
|
}
|
|
9584
9829
|
matchesMetadataScope(metadata, expected) {
|
|
9585
9830
|
if (!metadata) return false;
|
|
9586
|
-
return Object.entries(expected).every(([
|
|
9587
|
-
const actual =
|
|
9831
|
+
return Object.entries(expected).every(([path15, value]) => {
|
|
9832
|
+
const actual = path15.split(".").reduce((acc, key) => {
|
|
9588
9833
|
if (typeof acc !== "object" || acc === null) return void 0;
|
|
9589
9834
|
return acc[key];
|
|
9590
9835
|
}, metadata);
|
|
@@ -9635,22 +9880,22 @@ function uniqueEntityStartNodes(candidates) {
|
|
|
9635
9880
|
}
|
|
9636
9881
|
return nodes;
|
|
9637
9882
|
}
|
|
9638
|
-
function toRetrievalGraphPathDebug(
|
|
9639
|
-
const firstStep =
|
|
9883
|
+
function toRetrievalGraphPathDebug(path15, titleByEntityId) {
|
|
9884
|
+
const firstStep = path15.steps[0];
|
|
9640
9885
|
const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
|
|
9641
9886
|
const startEntityId = startNode?.type === "entity" ? startNode.id : "";
|
|
9642
9887
|
return {
|
|
9643
9888
|
startEntityId,
|
|
9644
9889
|
startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
|
|
9645
|
-
targetId:
|
|
9646
|
-
targetType:
|
|
9647
|
-
hops:
|
|
9648
|
-
relationPath:
|
|
9890
|
+
targetId: path15.target.id,
|
|
9891
|
+
targetType: path15.target.type,
|
|
9892
|
+
hops: path15.hops,
|
|
9893
|
+
relationPath: path15.steps.map((step) => step.relationType)
|
|
9649
9894
|
};
|
|
9650
9895
|
}
|
|
9651
|
-
function graphPathScore(
|
|
9652
|
-
const base = Math.min(0.95, Math.max(0,
|
|
9653
|
-
return Math.max(0.05, base - hopPenalty * Math.max(0,
|
|
9896
|
+
function graphPathScore(path15, hopPenalty) {
|
|
9897
|
+
const base = Math.min(0.95, Math.max(0, path15.scoreContribution));
|
|
9898
|
+
return Math.max(0.05, base - hopPenalty * Math.max(0, path15.hops - 1));
|
|
9654
9899
|
}
|
|
9655
9900
|
function clampGraphHops(maxHops) {
|
|
9656
9901
|
if (!Number.isFinite(maxHops)) return 2;
|
|
@@ -9658,14 +9903,14 @@ function clampGraphHops(maxHops) {
|
|
|
9658
9903
|
}
|
|
9659
9904
|
function mergeGraphPaths(existing, incoming) {
|
|
9660
9905
|
const byKey = /* @__PURE__ */ new Map();
|
|
9661
|
-
for (const
|
|
9662
|
-
const key = [
|
|
9663
|
-
if (!byKey.has(key)) byKey.set(key,
|
|
9906
|
+
for (const path15 of [...existing, ...incoming]) {
|
|
9907
|
+
const key = [path15.startEntityId, path15.targetType, path15.targetId, path15.hops, ...path15.relationPath].join("\0");
|
|
9908
|
+
if (!byKey.has(key)) byKey.set(key, path15);
|
|
9664
9909
|
}
|
|
9665
9910
|
return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
|
|
9666
9911
|
}
|
|
9667
|
-
function graphPathSignature(
|
|
9668
|
-
return [
|
|
9912
|
+
function graphPathSignature(path15) {
|
|
9913
|
+
return [path15.startEntityId, path15.targetType, path15.targetId, path15.hops, ...path15.relationPath].join("|");
|
|
9669
9914
|
}
|
|
9670
9915
|
function compareStable(a, b) {
|
|
9671
9916
|
if (a < b) return -1;
|
|
@@ -12238,6 +12483,34 @@ var MemoryService = class {
|
|
|
12238
12483
|
async rebuildFtsIndex() {
|
|
12239
12484
|
return this.queryService.rebuildFtsIndex();
|
|
12240
12485
|
}
|
|
12486
|
+
/**
|
|
12487
|
+
* Get a single event by id (indexed lookup).
|
|
12488
|
+
*/
|
|
12489
|
+
async getEvent(id) {
|
|
12490
|
+
return this.queryService.getEvent(id);
|
|
12491
|
+
}
|
|
12492
|
+
/**
|
|
12493
|
+
* Resolve an event from its citation id via the reverse index.
|
|
12494
|
+
*/
|
|
12495
|
+
async getEventByCitationId(citationId) {
|
|
12496
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
12497
|
+
}
|
|
12498
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
12499
|
+
async getEventsAfter(sinceIso) {
|
|
12500
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
12501
|
+
}
|
|
12502
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
12503
|
+
async getEventTypeCounts() {
|
|
12504
|
+
return this.queryService.getEventTypeCounts();
|
|
12505
|
+
}
|
|
12506
|
+
/** Count distinct sessions (SQL). */
|
|
12507
|
+
async getDistinctSessionCount() {
|
|
12508
|
+
return this.queryService.getDistinctSessionCount();
|
|
12509
|
+
}
|
|
12510
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
12511
|
+
async getDailyEventCounts(sinceIso) {
|
|
12512
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
12513
|
+
}
|
|
12241
12514
|
/**
|
|
12242
12515
|
* Get session history
|
|
12243
12516
|
*/
|
|
@@ -12562,6 +12835,11 @@ var getLightweightMemoryServiceForProject = defaultRegistry.getLightweightMemory
|
|
|
12562
12835
|
var createMemoryService = defaultRegistry.createMemoryService;
|
|
12563
12836
|
|
|
12564
12837
|
// src/apps/server/api/utils.ts
|
|
12838
|
+
function jsonError(c, error, options = {}) {
|
|
12839
|
+
const status = options.status ?? 500;
|
|
12840
|
+
console.error(`[api] ${c.req.method} ${c.req.path} failed:`, error);
|
|
12841
|
+
return c.json({ error: options.message ?? "Internal server error" }, status);
|
|
12842
|
+
}
|
|
12565
12843
|
function getServiceFromQuery(c) {
|
|
12566
12844
|
const project = c.req.query("project") || c.req.query("projectId");
|
|
12567
12845
|
if (project) {
|
|
@@ -12673,7 +12951,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
12673
12951
|
hasMore: end < total
|
|
12674
12952
|
});
|
|
12675
12953
|
} catch (error) {
|
|
12676
|
-
return c
|
|
12954
|
+
return jsonError(c, error);
|
|
12677
12955
|
} finally {
|
|
12678
12956
|
await memoryService.shutdown();
|
|
12679
12957
|
}
|
|
@@ -12709,7 +12987,7 @@ sessionsRouter.get("/:id", async (c) => {
|
|
|
12709
12987
|
stats: eventsByType
|
|
12710
12988
|
});
|
|
12711
12989
|
} catch (error) {
|
|
12712
|
-
return c
|
|
12990
|
+
return jsonError(c, error);
|
|
12713
12991
|
} finally {
|
|
12714
12992
|
await memoryService.shutdown();
|
|
12715
12993
|
}
|
|
@@ -12775,7 +13053,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
12775
13053
|
hasMore: offset + limit < total
|
|
12776
13054
|
});
|
|
12777
13055
|
} catch (error) {
|
|
12778
|
-
return c
|
|
13056
|
+
return jsonError(c, error);
|
|
12779
13057
|
} finally {
|
|
12780
13058
|
await memoryService.shutdown();
|
|
12781
13059
|
}
|
|
@@ -12785,12 +13063,11 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
12785
13063
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
12786
13064
|
try {
|
|
12787
13065
|
await memoryService.initialize();
|
|
12788
|
-
const
|
|
12789
|
-
const event = recentEvents.find((e) => e.id === id);
|
|
13066
|
+
const event = await memoryService.getEvent(id);
|
|
12790
13067
|
if (!event) {
|
|
12791
13068
|
return c.json({ error: "Event not found" }, 404);
|
|
12792
13069
|
}
|
|
12793
|
-
const sessionEvents =
|
|
13070
|
+
const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
12794
13071
|
const eventIndex = sessionEvents.findIndex((e) => e.id === id);
|
|
12795
13072
|
const start = Math.max(0, eventIndex - 2);
|
|
12796
13073
|
const end = Math.min(sessionEvents.length, eventIndex + 3);
|
|
@@ -12812,7 +13089,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
12812
13089
|
}))
|
|
12813
13090
|
});
|
|
12814
13091
|
} catch (error) {
|
|
12815
|
-
return c
|
|
13092
|
+
return jsonError(c, error);
|
|
12816
13093
|
} finally {
|
|
12817
13094
|
await memoryService.shutdown();
|
|
12818
13095
|
}
|
|
@@ -12859,7 +13136,7 @@ searchRouter.post("/", async (c) => {
|
|
|
12859
13136
|
}
|
|
12860
13137
|
});
|
|
12861
13138
|
} catch (error) {
|
|
12862
|
-
return c
|
|
13139
|
+
return jsonError(c, error);
|
|
12863
13140
|
} finally {
|
|
12864
13141
|
await memoryService.shutdown();
|
|
12865
13142
|
}
|
|
@@ -12902,7 +13179,7 @@ searchRouter.post("/disclosure", async (c) => {
|
|
|
12902
13179
|
throw error;
|
|
12903
13180
|
}
|
|
12904
13181
|
} catch (error) {
|
|
12905
|
-
return c
|
|
13182
|
+
return jsonError(c, error);
|
|
12906
13183
|
} finally {
|
|
12907
13184
|
await memoryService?.shutdown();
|
|
12908
13185
|
}
|
|
@@ -12922,7 +13199,7 @@ searchRouter.get("/disclosure/:resultId/expand", async (c) => {
|
|
|
12922
13199
|
}
|
|
12923
13200
|
return c.json(result);
|
|
12924
13201
|
} catch (error) {
|
|
12925
|
-
return c
|
|
13202
|
+
return jsonError(c, error);
|
|
12926
13203
|
} finally {
|
|
12927
13204
|
await memoryService.shutdown();
|
|
12928
13205
|
}
|
|
@@ -12937,7 +13214,7 @@ searchRouter.get("/disclosure/:resultId/source", async (c) => {
|
|
|
12937
13214
|
}
|
|
12938
13215
|
return c.json(result);
|
|
12939
13216
|
} catch (error) {
|
|
12940
|
-
return c
|
|
13217
|
+
return jsonError(c, error);
|
|
12941
13218
|
} finally {
|
|
12942
13219
|
await memoryService.shutdown();
|
|
12943
13220
|
}
|
|
@@ -12966,7 +13243,7 @@ searchRouter.get("/", async (c) => {
|
|
|
12966
13243
|
}
|
|
12967
13244
|
});
|
|
12968
13245
|
} catch (error) {
|
|
12969
|
-
return c
|
|
13246
|
+
return jsonError(c, error);
|
|
12970
13247
|
} finally {
|
|
12971
13248
|
await memoryService.shutdown();
|
|
12972
13249
|
}
|
|
@@ -12974,9 +13251,89 @@ searchRouter.get("/", async (c) => {
|
|
|
12974
13251
|
|
|
12975
13252
|
// src/apps/server/api/stats.ts
|
|
12976
13253
|
import { Hono as Hono4 } from "hono";
|
|
12977
|
-
import * as
|
|
13254
|
+
import * as fs10 from "fs";
|
|
12978
13255
|
import * as os6 from "os";
|
|
13256
|
+
import * as path12 from "path";
|
|
13257
|
+
|
|
13258
|
+
// src/apps/server/api/stats-metrics.ts
|
|
13259
|
+
import * as fs9 from "fs";
|
|
12979
13260
|
import * as path11 from "path";
|
|
13261
|
+
var DEFAULT_KPI_THRESHOLDS = {
|
|
13262
|
+
usefulRecallRateMin: 0.45,
|
|
13263
|
+
reworkRateMax: 0.25,
|
|
13264
|
+
postChangeFailureRateMax: 0.2,
|
|
13265
|
+
avgCompletionTurnsMax: 12,
|
|
13266
|
+
memoryHitRateMin: 0.35
|
|
13267
|
+
};
|
|
13268
|
+
function loadKpiThresholds() {
|
|
13269
|
+
try {
|
|
13270
|
+
const filePath = path11.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
13271
|
+
if (!fs9.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
13272
|
+
const parsed = JSON.parse(fs9.readFileSync(filePath, "utf-8"));
|
|
13273
|
+
return {
|
|
13274
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
13275
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
13276
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
13277
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
13278
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
13279
|
+
};
|
|
13280
|
+
} catch {
|
|
13281
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
function windowToMs(window) {
|
|
13285
|
+
if (window === "24h") return 24 * 60 * 60 * 1e3;
|
|
13286
|
+
if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
|
|
13287
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
13288
|
+
}
|
|
13289
|
+
function inWindow(e, now, window) {
|
|
13290
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
13291
|
+
}
|
|
13292
|
+
function isEditToolName(name) {
|
|
13293
|
+
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
13294
|
+
}
|
|
13295
|
+
function parseToolPayload(e) {
|
|
13296
|
+
if (e.eventType !== "tool_observation") return null;
|
|
13297
|
+
try {
|
|
13298
|
+
const payload = JSON.parse(e.content);
|
|
13299
|
+
return {
|
|
13300
|
+
toolName: payload?.toolName,
|
|
13301
|
+
success: payload?.success,
|
|
13302
|
+
filePath: payload?.metadata?.filePath,
|
|
13303
|
+
command: payload?.metadata?.command
|
|
13304
|
+
};
|
|
13305
|
+
} catch {
|
|
13306
|
+
return {
|
|
13307
|
+
toolName: e.metadata?.toolName,
|
|
13308
|
+
success: e.metadata?.success,
|
|
13309
|
+
filePath: e.metadata?.filePath,
|
|
13310
|
+
command: e.metadata?.command
|
|
13311
|
+
};
|
|
13312
|
+
}
|
|
13313
|
+
}
|
|
13314
|
+
function isTestLikeCommand(command) {
|
|
13315
|
+
if (!command) return false;
|
|
13316
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
13317
|
+
}
|
|
13318
|
+
function safeRatio(num, den) {
|
|
13319
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
13320
|
+
return num / den;
|
|
13321
|
+
}
|
|
13322
|
+
function round(value, digits = 4) {
|
|
13323
|
+
const factor = 10 ** digits;
|
|
13324
|
+
return Math.round(value * factor) / factor;
|
|
13325
|
+
}
|
|
13326
|
+
function computeSessionTurnCount(sessionEvents) {
|
|
13327
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
13328
|
+
for (const e of sessionEvents) {
|
|
13329
|
+
const turnId = e.metadata?.turnId;
|
|
13330
|
+
if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
|
|
13331
|
+
}
|
|
13332
|
+
if (turnIds.size > 0) return turnIds.size;
|
|
13333
|
+
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
13334
|
+
}
|
|
13335
|
+
|
|
13336
|
+
// src/apps/server/api/stats.ts
|
|
12980
13337
|
var statsRouter = new Hono4();
|
|
12981
13338
|
var OPERATION_STATS_TABLES = [
|
|
12982
13339
|
"memory_facets",
|
|
@@ -13003,7 +13360,7 @@ function operationsStatsHomeDir() {
|
|
|
13003
13360
|
}
|
|
13004
13361
|
function projectStoragePathForOperationsStats(projectOrHash) {
|
|
13005
13362
|
const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
|
|
13006
|
-
return
|
|
13363
|
+
return path12.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
|
|
13007
13364
|
}
|
|
13008
13365
|
function getOperationsStatsContext(project) {
|
|
13009
13366
|
if (project && project.trim().length > 0) {
|
|
@@ -13013,13 +13370,13 @@ function getOperationsStatsContext(project) {
|
|
|
13013
13370
|
return {
|
|
13014
13371
|
projectHash,
|
|
13015
13372
|
storagePath: storagePath2,
|
|
13016
|
-
dbPath:
|
|
13373
|
+
dbPath: path12.join(storagePath2, "events.sqlite")
|
|
13017
13374
|
};
|
|
13018
13375
|
}
|
|
13019
|
-
const storagePath =
|
|
13376
|
+
const storagePath = path12.join(operationsStatsHomeDir(), ".claude-code", "memory");
|
|
13020
13377
|
return {
|
|
13021
13378
|
storagePath,
|
|
13022
|
-
dbPath:
|
|
13379
|
+
dbPath: path12.join(storagePath, "events.sqlite")
|
|
13023
13380
|
};
|
|
13024
13381
|
}
|
|
13025
13382
|
function countRowValue(db, sql, params = []) {
|
|
@@ -13037,6 +13394,23 @@ function sanitizeAggregateLabel(value) {
|
|
|
13037
13394
|
const trimmed = sanitized.trim();
|
|
13038
13395
|
return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
|
|
13039
13396
|
}
|
|
13397
|
+
var SAFE_RETRIEVAL_TRACE_STRATEGIES = /* @__PURE__ */ new Set([
|
|
13398
|
+
"auto",
|
|
13399
|
+
"deep",
|
|
13400
|
+
"fast",
|
|
13401
|
+
"hybrid",
|
|
13402
|
+
"keyword",
|
|
13403
|
+
"semantic",
|
|
13404
|
+
"mcp-context-pack",
|
|
13405
|
+
"session-start-hook",
|
|
13406
|
+
"unknown"
|
|
13407
|
+
]);
|
|
13408
|
+
function normalizeRetrievalTraceStrategy(value) {
|
|
13409
|
+
const label = sanitizeAggregateLabel(value);
|
|
13410
|
+
if (label === "[REDACTED]") return "unknown";
|
|
13411
|
+
const normalized = label.trim().toLowerCase();
|
|
13412
|
+
return SAFE_RETRIEVAL_TRACE_STRATEGIES.has(normalized) ? normalized : "unknown";
|
|
13413
|
+
}
|
|
13040
13414
|
function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
|
|
13041
13415
|
return {
|
|
13042
13416
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13450,80 +13824,6 @@ function buildLessonConfidenceBuckets(db, projectHash) {
|
|
|
13450
13824
|
}).length
|
|
13451
13825
|
}));
|
|
13452
13826
|
}
|
|
13453
|
-
var DEFAULT_KPI_THRESHOLDS = {
|
|
13454
|
-
usefulRecallRateMin: 0.45,
|
|
13455
|
-
reworkRateMax: 0.25,
|
|
13456
|
-
postChangeFailureRateMax: 0.2,
|
|
13457
|
-
avgCompletionTurnsMax: 12,
|
|
13458
|
-
memoryHitRateMin: 0.35
|
|
13459
|
-
};
|
|
13460
|
-
function loadKpiThresholds() {
|
|
13461
|
-
try {
|
|
13462
|
-
const filePath = path11.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
13463
|
-
if (!fs9.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
13464
|
-
const parsed = JSON.parse(fs9.readFileSync(filePath, "utf-8"));
|
|
13465
|
-
return {
|
|
13466
|
-
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
13467
|
-
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
13468
|
-
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
13469
|
-
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
13470
|
-
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
13471
|
-
};
|
|
13472
|
-
} catch {
|
|
13473
|
-
return DEFAULT_KPI_THRESHOLDS;
|
|
13474
|
-
}
|
|
13475
|
-
}
|
|
13476
|
-
function windowToMs(window) {
|
|
13477
|
-
if (window === "24h") return 24 * 60 * 60 * 1e3;
|
|
13478
|
-
if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
|
|
13479
|
-
return 30 * 24 * 60 * 60 * 1e3;
|
|
13480
|
-
}
|
|
13481
|
-
function inWindow(e, now, window) {
|
|
13482
|
-
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
13483
|
-
}
|
|
13484
|
-
function isEditToolName(name) {
|
|
13485
|
-
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
13486
|
-
}
|
|
13487
|
-
function parseToolPayload(e) {
|
|
13488
|
-
if (e.eventType !== "tool_observation") return null;
|
|
13489
|
-
try {
|
|
13490
|
-
const payload = JSON.parse(e.content);
|
|
13491
|
-
return {
|
|
13492
|
-
toolName: payload?.toolName,
|
|
13493
|
-
success: payload?.success,
|
|
13494
|
-
filePath: payload?.metadata?.filePath,
|
|
13495
|
-
command: payload?.metadata?.command
|
|
13496
|
-
};
|
|
13497
|
-
} catch {
|
|
13498
|
-
return {
|
|
13499
|
-
toolName: e.metadata?.toolName,
|
|
13500
|
-
success: e.metadata?.success,
|
|
13501
|
-
filePath: e.metadata?.filePath,
|
|
13502
|
-
command: e.metadata?.command
|
|
13503
|
-
};
|
|
13504
|
-
}
|
|
13505
|
-
}
|
|
13506
|
-
function isTestLikeCommand(command) {
|
|
13507
|
-
if (!command) return false;
|
|
13508
|
-
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
13509
|
-
}
|
|
13510
|
-
function safeRatio(num, den) {
|
|
13511
|
-
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
13512
|
-
return num / den;
|
|
13513
|
-
}
|
|
13514
|
-
function round(value, digits = 4) {
|
|
13515
|
-
const factor = 10 ** digits;
|
|
13516
|
-
return Math.round(value * factor) / factor;
|
|
13517
|
-
}
|
|
13518
|
-
function computeSessionTurnCount(sessionEvents) {
|
|
13519
|
-
const turnIds = /* @__PURE__ */ new Set();
|
|
13520
|
-
for (const e of sessionEvents) {
|
|
13521
|
-
const turnId = e.metadata?.turnId;
|
|
13522
|
-
if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
|
|
13523
|
-
}
|
|
13524
|
-
if (turnIds.size > 0) return turnIds.size;
|
|
13525
|
-
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
13526
|
-
}
|
|
13527
13827
|
function normalizeQueryRewriteKind2(value) {
|
|
13528
13828
|
const normalized = (value || "").trim().toLowerCase();
|
|
13529
13829
|
if (normalized === "follow-up-context" || normalized === "intent-rewrite") return normalized;
|
|
@@ -13551,6 +13851,63 @@ function getTraceSelectedCount(trace) {
|
|
|
13551
13851
|
function getTraceCandidateCount(trace) {
|
|
13552
13852
|
return Number(trace.candidateCount ?? trace.candidateEventIds?.length ?? trace.candidateDetails?.length ?? 0);
|
|
13553
13853
|
}
|
|
13854
|
+
function normalizeStatsNumber(value) {
|
|
13855
|
+
const numberValue = Number(value || 0);
|
|
13856
|
+
return Number.isFinite(numberValue) ? Math.max(0, numberValue) : 0;
|
|
13857
|
+
}
|
|
13858
|
+
function sanitizeRetrievalTraceStrategyBreakdown(value) {
|
|
13859
|
+
if (!Array.isArray(value)) return [];
|
|
13860
|
+
const byStrategy = /* @__PURE__ */ new Map();
|
|
13861
|
+
for (const item of value) {
|
|
13862
|
+
const row = item && typeof item === "object" ? item : {};
|
|
13863
|
+
const strategy = normalizeRetrievalTraceStrategy(row.strategy ?? "unknown");
|
|
13864
|
+
const bucket = byStrategy.get(strategy) ?? {
|
|
13865
|
+
strategy,
|
|
13866
|
+
totalQueries: 0,
|
|
13867
|
+
queriesWithSelection: 0,
|
|
13868
|
+
rewrittenQueries: 0,
|
|
13869
|
+
totalCandidateCount: 0,
|
|
13870
|
+
totalSelectedCount: 0
|
|
13871
|
+
};
|
|
13872
|
+
bucket.totalQueries += normalizeStatsNumber(row.totalQueries);
|
|
13873
|
+
bucket.queriesWithSelection += normalizeStatsNumber(row.queriesWithSelection);
|
|
13874
|
+
bucket.rewrittenQueries += normalizeStatsNumber(row.rewrittenQueries);
|
|
13875
|
+
bucket.totalCandidateCount += normalizeStatsNumber(row.totalCandidateCount);
|
|
13876
|
+
bucket.totalSelectedCount += normalizeStatsNumber(row.totalSelectedCount);
|
|
13877
|
+
byStrategy.set(strategy, bucket);
|
|
13878
|
+
}
|
|
13879
|
+
return Array.from(byStrategy.values()).map((row) => ({
|
|
13880
|
+
strategy: row.strategy,
|
|
13881
|
+
totalQueries: row.totalQueries,
|
|
13882
|
+
queriesWithSelection: row.queriesWithSelection,
|
|
13883
|
+
rewrittenQueries: row.rewrittenQueries,
|
|
13884
|
+
rewriteRate: normalizeMetric(safeRatio(row.rewrittenQueries, row.totalQueries)),
|
|
13885
|
+
totalCandidateCount: row.totalCandidateCount,
|
|
13886
|
+
totalSelectedCount: row.totalSelectedCount,
|
|
13887
|
+
avgCandidateCount: safeRatio(row.totalCandidateCount, row.totalQueries),
|
|
13888
|
+
avgSelectedCount: safeRatio(row.totalSelectedCount, row.totalQueries),
|
|
13889
|
+
selectionRate: normalizeMetric(safeRatio(row.totalSelectedCount, row.totalCandidateCount)),
|
|
13890
|
+
queryYieldRate: normalizeMetric(safeRatio(row.queriesWithSelection, row.totalQueries))
|
|
13891
|
+
}));
|
|
13892
|
+
}
|
|
13893
|
+
function sanitizeRetrievalTraceStats(stats) {
|
|
13894
|
+
const s = stats && typeof stats === "object" ? stats : {};
|
|
13895
|
+
return {
|
|
13896
|
+
totalQueries: Number(s.totalQueries || 0),
|
|
13897
|
+
avgCandidateCount: Number(s.avgCandidateCount || 0),
|
|
13898
|
+
avgSelectedCount: Number(s.avgSelectedCount || 0),
|
|
13899
|
+
selectionRate: normalizeMetric(s.selectionRate),
|
|
13900
|
+
rewrittenQueries: Number(s.rewrittenQueries || 0),
|
|
13901
|
+
rewriteRate: normalizeMetric(s.rewriteRate),
|
|
13902
|
+
rewrittenQueriesWithSelection: Number(s.rewrittenQueriesWithSelection || 0),
|
|
13903
|
+
rawQueriesWithSelection: Number(s.rawQueriesWithSelection || 0),
|
|
13904
|
+
rewrittenSelectionRate: normalizeMetric(s.rewrittenSelectionRate),
|
|
13905
|
+
rawSelectionRate: normalizeMetric(s.rawSelectionRate),
|
|
13906
|
+
avgSelectedCountForRewrittenQueries: Number(s.avgSelectedCountForRewrittenQueries || 0),
|
|
13907
|
+
avgSelectedCountForRawQueries: Number(s.avgSelectedCountForRawQueries || 0),
|
|
13908
|
+
strategyBreakdown: sanitizeRetrievalTraceStrategyBreakdown(s.strategyBreakdown)
|
|
13909
|
+
};
|
|
13910
|
+
}
|
|
13554
13911
|
function makeRetrievalReviewItem(trace) {
|
|
13555
13912
|
const candidateCount = getTraceCandidateCount(trace);
|
|
13556
13913
|
const selectedCount = getTraceSelectedCount(trace);
|
|
@@ -13604,7 +13961,7 @@ function makeRetrievalReviewItem(trace) {
|
|
|
13604
13961
|
action,
|
|
13605
13962
|
queryRewriteKind,
|
|
13606
13963
|
rewritten,
|
|
13607
|
-
strategy: trace.strategy
|
|
13964
|
+
strategy: normalizeRetrievalTraceStrategy(trace.strategy ?? "unknown"),
|
|
13608
13965
|
candidateCount,
|
|
13609
13966
|
selectedCount,
|
|
13610
13967
|
candidateEventIds: (trace.candidateEventIds || []).slice(0, 5),
|
|
@@ -14031,7 +14388,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
14031
14388
|
hasMore: events.length === limit
|
|
14032
14389
|
});
|
|
14033
14390
|
} catch (error) {
|
|
14034
|
-
return c
|
|
14391
|
+
return jsonError(c, error);
|
|
14035
14392
|
} finally {
|
|
14036
14393
|
await memoryService.shutdown();
|
|
14037
14394
|
}
|
|
@@ -14040,7 +14397,7 @@ statsRouter.get("/operations", async (c) => {
|
|
|
14040
14397
|
const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
|
|
14041
14398
|
const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
|
|
14042
14399
|
const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
|
|
14043
|
-
const databaseExists =
|
|
14400
|
+
const databaseExists = fs10.existsSync(context.dbPath);
|
|
14044
14401
|
if (!databaseExists) {
|
|
14045
14402
|
return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
|
|
14046
14403
|
}
|
|
@@ -14128,7 +14485,7 @@ statsRouter.get("/perspective", async (c) => {
|
|
|
14128
14485
|
const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
|
|
14129
14486
|
const contradictionLimit = parseStatsLimit(c.req.query("contradictionLimit"), 10, 50);
|
|
14130
14487
|
const graphLimit = parseStatsLimit(c.req.query("graphLimit"), 10, 50);
|
|
14131
|
-
const databaseExists =
|
|
14488
|
+
const databaseExists = fs10.existsSync(context.dbPath);
|
|
14132
14489
|
if (!databaseExists) {
|
|
14133
14490
|
return c.json(emptyPerspectiveStatsPayload(context, false, [...PERSPECTIVE_STATS_TABLES], windowDays));
|
|
14134
14491
|
}
|
|
@@ -14204,33 +14561,29 @@ statsRouter.get("/", async (c) => {
|
|
|
14204
14561
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
14205
14562
|
try {
|
|
14206
14563
|
await memoryService.initialize();
|
|
14207
|
-
const
|
|
14208
|
-
const
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
const
|
|
14216
|
-
const eventsByDay =
|
|
14217
|
-
|
|
14218
|
-
acc[day] = (acc[day] || 0) + 1;
|
|
14219
|
-
return acc;
|
|
14220
|
-
}, {});
|
|
14221
|
-
const retrievalTrace = await memoryService.getRetrievalTraceStats();
|
|
14564
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
|
|
14565
|
+
const [stats, typeCounts, sessionTotal, dailyCounts, rawTrace] = await Promise.all([
|
|
14566
|
+
memoryService.getStats(),
|
|
14567
|
+
memoryService.getEventTypeCounts(),
|
|
14568
|
+
memoryService.getDistinctSessionCount(),
|
|
14569
|
+
memoryService.getDailyEventCounts(sevenDaysAgo),
|
|
14570
|
+
memoryService.getRetrievalTraceStats()
|
|
14571
|
+
]);
|
|
14572
|
+
const eventsByType = Object.fromEntries(typeCounts.map((t) => [t.eventType, t.count]));
|
|
14573
|
+
const eventsByDay = Object.fromEntries(dailyCounts.map((d) => [d.day, d.total]));
|
|
14574
|
+
const retrievalTrace = sanitizeRetrievalTraceStats(rawTrace);
|
|
14222
14575
|
return c.json({
|
|
14223
14576
|
storage: {
|
|
14224
14577
|
eventCount: stats.totalEvents,
|
|
14225
14578
|
vectorCount: stats.vectorCount
|
|
14226
14579
|
},
|
|
14227
14580
|
sessions: {
|
|
14228
|
-
total:
|
|
14581
|
+
total: sessionTotal
|
|
14229
14582
|
},
|
|
14230
14583
|
eventsByType,
|
|
14231
14584
|
activity: {
|
|
14232
14585
|
daily: eventsByDay,
|
|
14233
|
-
total7Days:
|
|
14586
|
+
total7Days: dailyCounts.reduce((sum, d) => sum + d.total, 0)
|
|
14234
14587
|
},
|
|
14235
14588
|
memory: {
|
|
14236
14589
|
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
@@ -14240,7 +14593,7 @@ statsRouter.get("/", async (c) => {
|
|
|
14240
14593
|
retrievalTrace
|
|
14241
14594
|
});
|
|
14242
14595
|
} catch (error) {
|
|
14243
|
-
return c
|
|
14596
|
+
return jsonError(c, error);
|
|
14244
14597
|
} finally {
|
|
14245
14598
|
await memoryService.shutdown();
|
|
14246
14599
|
}
|
|
@@ -14277,30 +14630,22 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
14277
14630
|
}
|
|
14278
14631
|
});
|
|
14279
14632
|
statsRouter.get("/timeline", async (c) => {
|
|
14280
|
-
const
|
|
14633
|
+
const parsedDays = parseInt(c.req.query("days") || "7", 10);
|
|
14634
|
+
const days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 7;
|
|
14281
14635
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
14282
14636
|
try {
|
|
14283
14637
|
await memoryService.initialize();
|
|
14284
|
-
const
|
|
14285
|
-
const
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
if (e.eventType === "user_prompt") acc[day].prompts++;
|
|
14294
|
-
if (e.eventType === "agent_response") acc[day].responses++;
|
|
14295
|
-
if (e.eventType === "tool_observation") acc[day].tools++;
|
|
14296
|
-
return acc;
|
|
14297
|
-
}, {});
|
|
14298
|
-
return c.json({
|
|
14299
|
-
days,
|
|
14300
|
-
daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
|
|
14301
|
-
});
|
|
14638
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
14639
|
+
const daily = (await memoryService.getDailyEventCounts(cutoff)).map((d) => ({
|
|
14640
|
+
date: d.day,
|
|
14641
|
+
total: d.total,
|
|
14642
|
+
prompts: d.prompts,
|
|
14643
|
+
responses: d.responses,
|
|
14644
|
+
tools: d.tools
|
|
14645
|
+
}));
|
|
14646
|
+
return c.json({ days, daily });
|
|
14302
14647
|
} catch (error) {
|
|
14303
|
-
return c
|
|
14648
|
+
return jsonError(c, error);
|
|
14304
14649
|
} finally {
|
|
14305
14650
|
await memoryService.shutdown();
|
|
14306
14651
|
}
|
|
@@ -14343,16 +14688,14 @@ statsRouter.get("/usefulness", async (c) => {
|
|
|
14343
14688
|
try {
|
|
14344
14689
|
await memoryService.initialize();
|
|
14345
14690
|
const now = Date.now();
|
|
14346
|
-
const eventLimit = 2e4;
|
|
14347
14691
|
const traceLimit = 5e3;
|
|
14348
14692
|
const windowStart = new Date(now - windowToMs(window));
|
|
14349
14693
|
const [events, helpfulness, traces] = await Promise.all([
|
|
14350
|
-
memoryService.
|
|
14694
|
+
memoryService.getEventsAfter(windowStart.toISOString()),
|
|
14351
14695
|
memoryService.getHelpfulnessStats(windowStart),
|
|
14352
14696
|
memoryService.getRecentRetrievalTraces(traceLimit)
|
|
14353
14697
|
]);
|
|
14354
14698
|
return c.json(computeMemoryUsefulnessSummary(events, helpfulness, traces, now, window, {
|
|
14355
|
-
eventsLimit: eventLimit,
|
|
14356
14699
|
tracesLimit: traceLimit
|
|
14357
14700
|
}));
|
|
14358
14701
|
} catch (error) {
|
|
@@ -14370,7 +14713,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
14370
14713
|
const traces = await memoryService.getRecentRetrievalTraces(limit);
|
|
14371
14714
|
const traceStats = await memoryService.getRetrievalTraceStats();
|
|
14372
14715
|
return c.json({
|
|
14373
|
-
stats: traceStats,
|
|
14716
|
+
stats: sanitizeRetrievalTraceStats(traceStats),
|
|
14374
14717
|
traces: traces.map((t) => {
|
|
14375
14718
|
const queryRewriteKind = normalizeQueryRewriteKind2(t.queryRewriteKind);
|
|
14376
14719
|
return {
|
|
@@ -14379,7 +14722,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
14379
14722
|
projectHash: t.projectHash || null,
|
|
14380
14723
|
queryRewriteKind,
|
|
14381
14724
|
rewritten: queryRewriteKind !== "none",
|
|
14382
|
-
strategy: t.strategy
|
|
14725
|
+
strategy: normalizeRetrievalTraceStrategy(t.strategy ?? "unknown"),
|
|
14383
14726
|
candidateEventIds: t.candidateEventIds,
|
|
14384
14727
|
selectedEventIds: t.selectedEventIds,
|
|
14385
14728
|
candidateDetails: t.candidateDetails || [],
|
|
@@ -14406,7 +14749,8 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
14406
14749
|
rewrittenSelectionRate: 0,
|
|
14407
14750
|
rawSelectionRate: 0,
|
|
14408
14751
|
avgSelectedCountForRewrittenQueries: 0,
|
|
14409
|
-
avgSelectedCountForRawQueries: 0
|
|
14752
|
+
avgSelectedCountForRawQueries: 0,
|
|
14753
|
+
strategyBreakdown: []
|
|
14410
14754
|
},
|
|
14411
14755
|
traces: [],
|
|
14412
14756
|
error: error.message
|
|
@@ -14457,7 +14801,8 @@ statsRouter.get("/kpi", async (c) => {
|
|
|
14457
14801
|
await memoryService.initialize();
|
|
14458
14802
|
const now = Date.now();
|
|
14459
14803
|
const thresholds = loadKpiThresholds();
|
|
14460
|
-
const
|
|
14804
|
+
const lookbackMs = Math.max(2 * windowToMs(window), 30 * 24 * 60 * 60 * 1e3);
|
|
14805
|
+
const allEvents = await memoryService.getEventsAfter(new Date(now - lookbackMs).toISOString());
|
|
14461
14806
|
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
14462
14807
|
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
14463
14808
|
const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
|
|
@@ -14566,7 +14911,7 @@ statsRouter.get("/kpi", async (c) => {
|
|
|
14566
14911
|
alerts
|
|
14567
14912
|
});
|
|
14568
14913
|
} catch (error) {
|
|
14569
|
-
return c
|
|
14914
|
+
return jsonError(c, error);
|
|
14570
14915
|
} finally {
|
|
14571
14916
|
await memoryService.shutdown();
|
|
14572
14917
|
}
|
|
@@ -14617,11 +14962,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
14617
14962
|
const memoryService = getServiceFromQuery(c);
|
|
14618
14963
|
try {
|
|
14619
14964
|
await memoryService.initialize();
|
|
14620
|
-
const
|
|
14621
|
-
const event = recentEvents.find((e) => {
|
|
14622
|
-
const eventCitationId = generateCitationId(e.id);
|
|
14623
|
-
return eventCitationId === citationId;
|
|
14624
|
-
});
|
|
14965
|
+
const event = await memoryService.getEventByCitationId(citationId);
|
|
14625
14966
|
if (!event) {
|
|
14626
14967
|
return c.json({ error: "Citation not found" }, 404);
|
|
14627
14968
|
}
|
|
@@ -14640,7 +14981,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
14640
14981
|
}
|
|
14641
14982
|
});
|
|
14642
14983
|
} catch (error) {
|
|
14643
|
-
return c
|
|
14984
|
+
return jsonError(c, error);
|
|
14644
14985
|
} finally {
|
|
14645
14986
|
await memoryService.shutdown();
|
|
14646
14987
|
}
|
|
@@ -14651,15 +14992,11 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
14651
14992
|
const memoryService = getServiceFromQuery(c);
|
|
14652
14993
|
try {
|
|
14653
14994
|
await memoryService.initialize();
|
|
14654
|
-
const
|
|
14655
|
-
const event = recentEvents.find((e) => {
|
|
14656
|
-
const eventCitationId = generateCitationId(e.id);
|
|
14657
|
-
return eventCitationId === citationId;
|
|
14658
|
-
});
|
|
14995
|
+
const event = await memoryService.getEventByCitationId(citationId);
|
|
14659
14996
|
if (!event) {
|
|
14660
14997
|
return c.json({ error: "Citation not found" }, 404);
|
|
14661
14998
|
}
|
|
14662
|
-
const sessionEvents =
|
|
14999
|
+
const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
14663
15000
|
const eventIndex = sessionEvents.findIndex((e) => e.id === event.id);
|
|
14664
15001
|
const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
|
|
14665
15002
|
const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
|
|
@@ -14678,7 +15015,7 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
14678
15015
|
} : null
|
|
14679
15016
|
});
|
|
14680
15017
|
} catch (error) {
|
|
14681
|
-
return c
|
|
15018
|
+
return jsonError(c, error);
|
|
14682
15019
|
} finally {
|
|
14683
15020
|
await memoryService.shutdown();
|
|
14684
15021
|
}
|
|
@@ -14721,7 +15058,7 @@ turnsRouter.get("/", async (c) => {
|
|
|
14721
15058
|
hasMore: offset + limit < totalTurns
|
|
14722
15059
|
});
|
|
14723
15060
|
} catch (error) {
|
|
14724
|
-
return c
|
|
15061
|
+
return jsonError(c, error);
|
|
14725
15062
|
} finally {
|
|
14726
15063
|
await memoryService.shutdown();
|
|
14727
15064
|
}
|
|
@@ -14772,7 +15109,7 @@ turnsRouter.get("/:turnId", async (c) => {
|
|
|
14772
15109
|
totalEvents: events.length
|
|
14773
15110
|
});
|
|
14774
15111
|
} catch (error) {
|
|
14775
|
-
return c
|
|
15112
|
+
return jsonError(c, error);
|
|
14776
15113
|
} finally {
|
|
14777
15114
|
await memoryService.shutdown();
|
|
14778
15115
|
}
|
|
@@ -14799,8 +15136,8 @@ turnsRouter.post("/backfill", async (c) => {
|
|
|
14799
15136
|
|
|
14800
15137
|
// src/apps/server/api/projects.ts
|
|
14801
15138
|
import { Hono as Hono7 } from "hono";
|
|
14802
|
-
import * as
|
|
14803
|
-
import * as
|
|
15139
|
+
import * as fs11 from "fs";
|
|
15140
|
+
import * as path13 from "path";
|
|
14804
15141
|
import * as os7 from "os";
|
|
14805
15142
|
var projectsRouter = new Hono7();
|
|
14806
15143
|
projectsRouter.get("/:hash/detail", async (c) => {
|
|
@@ -14829,7 +15166,7 @@ projectsRouter.get("/:hash/detail", async (c) => {
|
|
|
14829
15166
|
return c.json({
|
|
14830
15167
|
project: {
|
|
14831
15168
|
hash,
|
|
14832
|
-
projectName: projectPath ?
|
|
15169
|
+
projectName: projectPath ? path13.basename(projectPath) : `unknown (${hash})`,
|
|
14833
15170
|
registered: Boolean(projectPath)
|
|
14834
15171
|
},
|
|
14835
15172
|
storage: {
|
|
@@ -14857,13 +15194,13 @@ projectsRouter.get("/:hash/detail", async (c) => {
|
|
|
14857
15194
|
});
|
|
14858
15195
|
projectsRouter.get("/", async (c) => {
|
|
14859
15196
|
try {
|
|
14860
|
-
const projectsDir =
|
|
14861
|
-
if (!
|
|
15197
|
+
const projectsDir = path13.join(os7.homedir(), ".claude-code", "memory", "projects");
|
|
15198
|
+
if (!fs11.existsSync(projectsDir)) {
|
|
14862
15199
|
return c.json({ projects: [] });
|
|
14863
15200
|
}
|
|
14864
|
-
const projectHashes =
|
|
14865
|
-
const fullPath =
|
|
14866
|
-
return
|
|
15201
|
+
const projectHashes = fs11.readdirSync(projectsDir).filter((name) => {
|
|
15202
|
+
const fullPath = path13.join(projectsDir, name);
|
|
15203
|
+
return fs11.statSync(fullPath).isDirectory();
|
|
14867
15204
|
});
|
|
14868
15205
|
const registry = loadSessionRegistry();
|
|
14869
15206
|
const hashToPath = /* @__PURE__ */ new Map();
|
|
@@ -14873,17 +15210,17 @@ projectsRouter.get("/", async (c) => {
|
|
|
14873
15210
|
}
|
|
14874
15211
|
}
|
|
14875
15212
|
const projects = projectHashes.map((hash) => {
|
|
14876
|
-
const dirPath =
|
|
14877
|
-
const dbPath =
|
|
15213
|
+
const dirPath = path13.join(projectsDir, hash);
|
|
15214
|
+
const dbPath = path13.join(dirPath, "events.sqlite");
|
|
14878
15215
|
let dbSize = 0;
|
|
14879
|
-
if (
|
|
14880
|
-
dbSize =
|
|
15216
|
+
if (fs11.existsSync(dbPath)) {
|
|
15217
|
+
dbSize = fs11.statSync(dbPath).size;
|
|
14881
15218
|
}
|
|
14882
15219
|
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
14883
15220
|
return {
|
|
14884
15221
|
hash,
|
|
14885
15222
|
projectPath,
|
|
14886
|
-
projectName:
|
|
15223
|
+
projectName: path13.basename(projectPath),
|
|
14887
15224
|
dbSize,
|
|
14888
15225
|
dbSizeHuman: formatBytes(dbSize)
|
|
14889
15226
|
};
|
|
@@ -15012,7 +15349,7 @@ chatRouter.post("/", async (c) => {
|
|
|
15012
15349
|
}
|
|
15013
15350
|
});
|
|
15014
15351
|
} catch (error) {
|
|
15015
|
-
return c
|
|
15352
|
+
return jsonError(c, error);
|
|
15016
15353
|
} finally {
|
|
15017
15354
|
await memoryService.shutdown();
|
|
15018
15355
|
}
|
|
@@ -15653,20 +15990,20 @@ playgroundRouter.post("/dry-run", async (c) => {
|
|
|
15653
15990
|
var apiRouter = new Hono11().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter).route("/health", healthRouter).route("/playground", playgroundRouter);
|
|
15654
15991
|
|
|
15655
15992
|
// src/apps/server/index.ts
|
|
15656
|
-
var moduleDir =
|
|
15993
|
+
var moduleDir = path14.dirname(fileUrlToPath(import.meta.url));
|
|
15657
15994
|
var DASHBOARD_SESSION_COOKIE = "cml_dashboard_session";
|
|
15658
15995
|
var DEFAULT_SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
|
|
15659
15996
|
function resolveUiPath() {
|
|
15660
15997
|
const candidates = [
|
|
15661
15998
|
// Built server: dist/server/index.js -> dist/ui
|
|
15662
|
-
|
|
15999
|
+
path14.resolve(moduleDir, "../ui"),
|
|
15663
16000
|
// Source/dev server: src/apps/server/index.ts -> src/apps/dashboard
|
|
15664
|
-
|
|
16001
|
+
path14.resolve(moduleDir, "../dashboard"),
|
|
15665
16002
|
// Fallback when running from a repository root.
|
|
15666
|
-
|
|
15667
|
-
|
|
16003
|
+
path14.resolve(process.cwd(), "dist/ui"),
|
|
16004
|
+
path14.resolve(process.cwd(), "src/apps/dashboard")
|
|
15668
16005
|
];
|
|
15669
|
-
return candidates.find((candidate) =>
|
|
16006
|
+
return candidates.find((candidate) => fs12.existsSync(path14.join(candidate, "index.html"))) ?? candidates[0];
|
|
15670
16007
|
}
|
|
15671
16008
|
function normalizeDashboardHost(host = "localhost") {
|
|
15672
16009
|
const normalized = host.trim().toLowerCase();
|
|
@@ -15807,14 +16144,51 @@ function createAuthOptions(options) {
|
|
|
15807
16144
|
return {
|
|
15808
16145
|
password: options.password ?? "",
|
|
15809
16146
|
cookieMaxAgeSeconds: options.cookieMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
15810
|
-
now: options.now ?? Date.now
|
|
16147
|
+
now: options.now ?? Date.now,
|
|
16148
|
+
allowedOrigins: options.allowedOrigins ?? []
|
|
15811
16149
|
};
|
|
15812
16150
|
}
|
|
16151
|
+
function resolveAllowedOrigins(explicit) {
|
|
16152
|
+
if (explicit && explicit.length > 0) return explicit;
|
|
16153
|
+
const raw = process.env.DASHBOARD_ALLOWED_ORIGINS;
|
|
16154
|
+
if (!raw) return [];
|
|
16155
|
+
return raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
16156
|
+
}
|
|
16157
|
+
function selfOrigins(c) {
|
|
16158
|
+
const origins = [];
|
|
16159
|
+
try {
|
|
16160
|
+
origins.push(new URL(c.req.url).origin);
|
|
16161
|
+
} catch {
|
|
16162
|
+
}
|
|
16163
|
+
const host = c.req.header("host");
|
|
16164
|
+
if (host) origins.push(`${isSecureRequest(c) ? "https" : "http"}://${host}`);
|
|
16165
|
+
return origins;
|
|
16166
|
+
}
|
|
16167
|
+
function isAllowedMutationOrigin(c, origin, allowedOrigins) {
|
|
16168
|
+
return allowedOrigins.includes(origin) || selfOrigins(c).includes(origin);
|
|
16169
|
+
}
|
|
15813
16170
|
function createDashboardApp(options = {}) {
|
|
15814
16171
|
const app2 = new Hono12();
|
|
15815
16172
|
const authOptions = createAuthOptions(options);
|
|
15816
|
-
|
|
16173
|
+
const allowedOrigins = resolveAllowedOrigins(options.allowedOrigins);
|
|
16174
|
+
if (allowedOrigins.length > 0) {
|
|
16175
|
+
app2.use("*", cors({
|
|
16176
|
+
origin: (origin) => allowedOrigins.includes(origin) ? origin : null,
|
|
16177
|
+
credentials: true
|
|
16178
|
+
}));
|
|
16179
|
+
}
|
|
15817
16180
|
app2.use("*", logger());
|
|
16181
|
+
app2.use("/api/*", async (c, next) => {
|
|
16182
|
+
const method = c.req.method;
|
|
16183
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
|
16184
|
+
return next();
|
|
16185
|
+
}
|
|
16186
|
+
const origin = c.req.header("origin");
|
|
16187
|
+
if (origin && !isAllowedMutationOrigin(c, origin, allowedOrigins)) {
|
|
16188
|
+
return c.json({ error: "Cross-origin request blocked" }, 403);
|
|
16189
|
+
}
|
|
16190
|
+
return next();
|
|
16191
|
+
});
|
|
15818
16192
|
app2.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
15819
16193
|
app2.get("/api/auth/status", (c) => c.json({
|
|
15820
16194
|
enabled: Boolean(authOptions.password),
|
|
@@ -15857,13 +16231,13 @@ function createDashboardApp(options = {}) {
|
|
|
15857
16231
|
}
|
|
15858
16232
|
app2.route("/api", apiRouter);
|
|
15859
16233
|
const uiPath = resolveUiPath();
|
|
15860
|
-
if (
|
|
16234
|
+
if (fs12.existsSync(uiPath)) {
|
|
15861
16235
|
app2.use("/*", serveStatic({ root: uiPath }));
|
|
15862
16236
|
}
|
|
15863
16237
|
app2.get("*", (c) => {
|
|
15864
|
-
const indexPath =
|
|
15865
|
-
if (
|
|
15866
|
-
return c.html(
|
|
16238
|
+
const indexPath = path14.join(uiPath, "index.html");
|
|
16239
|
+
if (fs12.existsSync(indexPath)) {
|
|
16240
|
+
return c.html(fs12.readFileSync(indexPath, "utf-8"));
|
|
15867
16241
|
}
|
|
15868
16242
|
return c.text('UI not built. Run "npm run build:ui" first.', 404);
|
|
15869
16243
|
});
|