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/api/index.js
CHANGED
|
@@ -163,7 +163,13 @@ function toDate(value) {
|
|
|
163
163
|
return new Date(String(value));
|
|
164
164
|
}
|
|
165
165
|
function createDatabase(dbPath, options) {
|
|
166
|
-
|
|
166
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
167
|
+
db.pragma("busy_timeout = 5000");
|
|
168
|
+
if (!options?.readOnly) {
|
|
169
|
+
db.pragma("journal_mode = WAL");
|
|
170
|
+
db.pragma("synchronous = NORMAL");
|
|
171
|
+
}
|
|
172
|
+
return db;
|
|
167
173
|
}
|
|
168
174
|
function dbRun(db, sql, params = []) {
|
|
169
175
|
db.prepare(sql).run(...params);
|
|
@@ -396,19 +402,29 @@ var ConsolidatedStore = class {
|
|
|
396
402
|
*/
|
|
397
403
|
async create(input) {
|
|
398
404
|
const memoryId = randomUUID2();
|
|
399
|
-
|
|
400
|
-
|
|
405
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
406
|
+
const insertMemory = this.db.prepare(
|
|
401
407
|
`INSERT INTO consolidated_memories
|
|
402
408
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
403
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
404
|
-
|
|
409
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
410
|
+
);
|
|
411
|
+
const insertEvent = this.db.prepare(
|
|
412
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
413
|
+
);
|
|
414
|
+
this.db.transaction(() => {
|
|
415
|
+
insertMemory.run(
|
|
405
416
|
memoryId,
|
|
406
417
|
input.summary,
|
|
407
418
|
JSON.stringify(input.topics),
|
|
408
|
-
JSON.stringify(
|
|
419
|
+
JSON.stringify(sourceEvents),
|
|
409
420
|
input.confidence
|
|
410
|
-
|
|
411
|
-
|
|
421
|
+
);
|
|
422
|
+
for (const eventId of sourceEvents) {
|
|
423
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
424
|
+
insertEvent.run(memoryId, eventId);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
})();
|
|
412
428
|
return memoryId;
|
|
413
429
|
}
|
|
414
430
|
/**
|
|
@@ -514,11 +530,12 @@ var ConsolidatedStore = class {
|
|
|
514
530
|
* Delete a consolidated memory
|
|
515
531
|
*/
|
|
516
532
|
async delete(memoryId) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
533
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
534
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
535
|
+
this.db.transaction(() => {
|
|
536
|
+
deleteEvents.run(memoryId);
|
|
537
|
+
deleteMemory.run(memoryId);
|
|
538
|
+
})();
|
|
522
539
|
}
|
|
523
540
|
/**
|
|
524
541
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -631,16 +648,16 @@ var ConsolidatedStore = class {
|
|
|
631
648
|
* Check if source events are already consolidated
|
|
632
649
|
*/
|
|
633
650
|
async isAlreadyConsolidated(eventIds) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
return
|
|
651
|
+
if (eventIds.length === 0) return false;
|
|
652
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
653
|
+
const rows = await dbAll(
|
|
654
|
+
this.db,
|
|
655
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
656
|
+
WHERE event_id IN (${placeholders})
|
|
657
|
+
LIMIT 1`,
|
|
658
|
+
eventIds
|
|
659
|
+
);
|
|
660
|
+
return rows.length > 0;
|
|
644
661
|
}
|
|
645
662
|
/**
|
|
646
663
|
* Get the last consolidation time
|
|
@@ -2176,6 +2193,23 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2176
2193
|
return `${sessionId}:${contentHash}`;
|
|
2177
2194
|
}
|
|
2178
2195
|
|
|
2196
|
+
// src/core/citation-generator.ts
|
|
2197
|
+
import { createHash as createHash2 } from "crypto";
|
|
2198
|
+
var ID_LENGTH = 6;
|
|
2199
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2200
|
+
function generateCitationId(eventId) {
|
|
2201
|
+
const hash = createHash2("sha256").update(eventId).digest();
|
|
2202
|
+
let id = "";
|
|
2203
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2204
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2205
|
+
}
|
|
2206
|
+
return id;
|
|
2207
|
+
}
|
|
2208
|
+
function parseCitationId(formatted) {
|
|
2209
|
+
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
2210
|
+
return match ? match[1] : null;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2179
2213
|
// src/core/sqlite-event-store.ts
|
|
2180
2214
|
import * as nodePath2 from "path";
|
|
2181
2215
|
|
|
@@ -2209,12 +2243,12 @@ function resolveProjectStoragePath(projectOrHash) {
|
|
|
2209
2243
|
import Database from "better-sqlite3";
|
|
2210
2244
|
import * as fs4 from "fs";
|
|
2211
2245
|
import * as nodePath from "path";
|
|
2212
|
-
function createSQLiteDatabase(
|
|
2213
|
-
const dir = nodePath.dirname(
|
|
2246
|
+
function createSQLiteDatabase(path14, options) {
|
|
2247
|
+
const dir = nodePath.dirname(path14);
|
|
2214
2248
|
if (!fs4.existsSync(dir)) {
|
|
2215
2249
|
fs4.mkdirSync(dir, { recursive: true });
|
|
2216
2250
|
}
|
|
2217
|
-
const db = new Database(
|
|
2251
|
+
const db = new Database(path14, {
|
|
2218
2252
|
readonly: options?.readonly ?? false
|
|
2219
2253
|
});
|
|
2220
2254
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -2224,17 +2258,28 @@ function createSQLiteDatabase(path13, options) {
|
|
|
2224
2258
|
}
|
|
2225
2259
|
return db;
|
|
2226
2260
|
}
|
|
2261
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2262
|
+
function prepareCached(db, sql) {
|
|
2263
|
+
let cache = statementCache.get(db);
|
|
2264
|
+
if (!cache) {
|
|
2265
|
+
cache = /* @__PURE__ */ new Map();
|
|
2266
|
+
statementCache.set(db, cache);
|
|
2267
|
+
}
|
|
2268
|
+
let stmt = cache.get(sql);
|
|
2269
|
+
if (!stmt) {
|
|
2270
|
+
stmt = db.prepare(sql);
|
|
2271
|
+
cache.set(sql, stmt);
|
|
2272
|
+
}
|
|
2273
|
+
return stmt;
|
|
2274
|
+
}
|
|
2227
2275
|
function sqliteRun(db, sql, params = []) {
|
|
2228
|
-
|
|
2229
|
-
return stmt.run(...params);
|
|
2276
|
+
return prepareCached(db, sql).run(...params);
|
|
2230
2277
|
}
|
|
2231
2278
|
function sqliteAll(db, sql, params = []) {
|
|
2232
|
-
|
|
2233
|
-
return stmt.all(...params);
|
|
2279
|
+
return prepareCached(db, sql).all(...params);
|
|
2234
2280
|
}
|
|
2235
2281
|
function sqliteGet(db, sql, params = []) {
|
|
2236
|
-
|
|
2237
|
-
return stmt.get(...params);
|
|
2282
|
+
return prepareCached(db, sql).get(...params);
|
|
2238
2283
|
}
|
|
2239
2284
|
function sqliteExec(db, sql) {
|
|
2240
2285
|
db.exec(sql);
|
|
@@ -2656,6 +2701,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2656
2701
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2657
2702
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2658
2703
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2704
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2659
2705
|
function emptyOutboxRecoveryResult() {
|
|
2660
2706
|
return {
|
|
2661
2707
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -2665,17 +2711,17 @@ function emptyOutboxRecoveryResult() {
|
|
|
2665
2711
|
function isRecord(value) {
|
|
2666
2712
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2667
2713
|
}
|
|
2668
|
-
function getNestedRecord(root,
|
|
2714
|
+
function getNestedRecord(root, path14) {
|
|
2669
2715
|
let cursor = root;
|
|
2670
|
-
for (const key of
|
|
2716
|
+
for (const key of path14) {
|
|
2671
2717
|
if (!isRecord(cursor)) return void 0;
|
|
2672
2718
|
cursor = cursor[key];
|
|
2673
2719
|
}
|
|
2674
2720
|
return isRecord(cursor) ? cursor : void 0;
|
|
2675
2721
|
}
|
|
2676
|
-
function getNestedString(root,
|
|
2722
|
+
function getNestedString(root, path14) {
|
|
2677
2723
|
let cursor = root;
|
|
2678
|
-
for (const key of
|
|
2724
|
+
for (const key of path14) {
|
|
2679
2725
|
if (!isRecord(cursor)) return void 0;
|
|
2680
2726
|
cursor = cursor[key];
|
|
2681
2727
|
}
|
|
@@ -3000,6 +3046,15 @@ var SQLiteEventStore = class {
|
|
|
3000
3046
|
access_count INTEGER DEFAULT 0
|
|
3001
3047
|
);
|
|
3002
3048
|
|
|
3049
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3050
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3051
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3052
|
+
memory_id TEXT NOT NULL,
|
|
3053
|
+
event_id TEXT NOT NULL,
|
|
3054
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3055
|
+
);
|
|
3056
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3057
|
+
|
|
3003
3058
|
-- Continuity Log table (tracks context transitions)
|
|
3004
3059
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
3005
3060
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3359,11 +3414,17 @@ var SQLiteEventStore = class {
|
|
|
3359
3414
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3360
3415
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3361
3416
|
END;
|
|
3417
|
+
|
|
3418
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3419
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3420
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3421
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3422
|
+
event_id TEXT PRIMARY KEY,
|
|
3423
|
+
citation_id TEXT NOT NULL
|
|
3424
|
+
);
|
|
3425
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3362
3426
|
`);
|
|
3363
|
-
|
|
3364
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3365
|
-
} catch {
|
|
3366
|
-
}
|
|
3427
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3367
3428
|
try {
|
|
3368
3429
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3369
3430
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3400,55 +3461,17 @@ var SQLiteEventStore = class {
|
|
|
3400
3461
|
}
|
|
3401
3462
|
} catch {
|
|
3402
3463
|
}
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
try {
|
|
3408
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3409
|
-
} catch {
|
|
3410
|
-
}
|
|
3411
|
-
try {
|
|
3412
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3413
|
-
} catch {
|
|
3414
|
-
}
|
|
3415
|
-
try {
|
|
3416
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3417
|
-
} catch {
|
|
3418
|
-
}
|
|
3464
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3465
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3466
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3467
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3419
3468
|
try {
|
|
3420
3469
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3421
3470
|
} catch {
|
|
3422
3471
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
try {
|
|
3427
|
-
sqliteExec(this.db, `
|
|
3428
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3429
|
-
`);
|
|
3430
|
-
} catch (err) {
|
|
3431
|
-
console.error("Error adding access_count column:", err);
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3435
|
-
try {
|
|
3436
|
-
sqliteExec(this.db, `
|
|
3437
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3438
|
-
`);
|
|
3439
|
-
} catch (err) {
|
|
3440
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
if (!columnNames.includes("turn_id")) {
|
|
3444
|
-
try {
|
|
3445
|
-
sqliteExec(this.db, `
|
|
3446
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3447
|
-
`);
|
|
3448
|
-
} catch (err) {
|
|
3449
|
-
console.error("Error adding turn_id column:", err);
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3472
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3473
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3474
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3452
3475
|
try {
|
|
3453
3476
|
sqliteExec(this.db, `
|
|
3454
3477
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3465,8 +3488,18 @@ var SQLiteEventStore = class {
|
|
|
3465
3488
|
sqliteExec(this.db, `
|
|
3466
3489
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3467
3490
|
`);
|
|
3468
|
-
} catch
|
|
3491
|
+
} catch {
|
|
3492
|
+
}
|
|
3493
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3494
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3495
|
+
sqliteExec(this.db, `
|
|
3496
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3497
|
+
SELECT cm.memory_id, je.value
|
|
3498
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3499
|
+
WHERE json_valid(cm.source_events);
|
|
3500
|
+
`);
|
|
3469
3501
|
}
|
|
3502
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3470
3503
|
this.initialized = true;
|
|
3471
3504
|
}
|
|
3472
3505
|
/**
|
|
@@ -3598,6 +3631,67 @@ var SQLiteEventStore = class {
|
|
|
3598
3631
|
);
|
|
3599
3632
|
return rows.map(this.rowToEvent);
|
|
3600
3633
|
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3636
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3637
|
+
* scan also under-counted stores larger than its cap).
|
|
3638
|
+
*/
|
|
3639
|
+
async getEventTypeCounts(options) {
|
|
3640
|
+
await this.initialize();
|
|
3641
|
+
const rows = sqliteAll(
|
|
3642
|
+
this.db,
|
|
3643
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3644
|
+
[]
|
|
3645
|
+
);
|
|
3646
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3650
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3651
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3652
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3653
|
+
*/
|
|
3654
|
+
async getEventsAfter(sinceIso, options) {
|
|
3655
|
+
await this.initialize();
|
|
3656
|
+
const rows = sqliteAll(
|
|
3657
|
+
this.db,
|
|
3658
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3659
|
+
[sinceIso]
|
|
3660
|
+
);
|
|
3661
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3662
|
+
}
|
|
3663
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3664
|
+
async getDistinctSessionCount(options) {
|
|
3665
|
+
await this.initialize();
|
|
3666
|
+
const row = sqliteGet(
|
|
3667
|
+
this.db,
|
|
3668
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3669
|
+
[]
|
|
3670
|
+
);
|
|
3671
|
+
return row?.count ?? 0;
|
|
3672
|
+
}
|
|
3673
|
+
/**
|
|
3674
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3675
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3676
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3677
|
+
* lexicographically.
|
|
3678
|
+
*/
|
|
3679
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3680
|
+
await this.initialize();
|
|
3681
|
+
return sqliteAll(
|
|
3682
|
+
this.db,
|
|
3683
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3684
|
+
COUNT(*) as total,
|
|
3685
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3686
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3687
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3688
|
+
FROM events
|
|
3689
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3690
|
+
GROUP BY day
|
|
3691
|
+
ORDER BY day ASC`,
|
|
3692
|
+
[sinceIso]
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3601
3695
|
/**
|
|
3602
3696
|
* Get event by ID
|
|
3603
3697
|
*/
|
|
@@ -3611,6 +3705,71 @@ var SQLiteEventStore = class {
|
|
|
3611
3705
|
if (!row) return null;
|
|
3612
3706
|
return this.rowToEvent(row);
|
|
3613
3707
|
}
|
|
3708
|
+
/**
|
|
3709
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3710
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3711
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3712
|
+
*/
|
|
3713
|
+
async getEvents(ids, options) {
|
|
3714
|
+
await this.initialize();
|
|
3715
|
+
if (ids.length === 0) return [];
|
|
3716
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
3717
|
+
const rows = sqliteAll(
|
|
3718
|
+
this.db,
|
|
3719
|
+
`SELECT * FROM events WHERE id IN (${placeholders}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3720
|
+
ids
|
|
3721
|
+
);
|
|
3722
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3726
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3727
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3728
|
+
* freshly appended events) before retrying.
|
|
3729
|
+
*/
|
|
3730
|
+
async getEventByCitationId(citationId, options) {
|
|
3731
|
+
await this.initialize();
|
|
3732
|
+
if (!citationId) return null;
|
|
3733
|
+
let row = sqliteGet(
|
|
3734
|
+
this.db,
|
|
3735
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3736
|
+
[citationId]
|
|
3737
|
+
);
|
|
3738
|
+
if (!row) {
|
|
3739
|
+
this.indexMissingCitations();
|
|
3740
|
+
row = sqliteGet(
|
|
3741
|
+
this.db,
|
|
3742
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3743
|
+
[citationId]
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3746
|
+
if (!row) return null;
|
|
3747
|
+
return this.getEvent(row.event_id, options);
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3751
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3752
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3753
|
+
*/
|
|
3754
|
+
indexMissingCitations() {
|
|
3755
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3756
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3757
|
+
if (indexedCount >= eventCount) return;
|
|
3758
|
+
const unindexed = sqliteAll(
|
|
3759
|
+
this.db,
|
|
3760
|
+
`SELECT e.id FROM events e
|
|
3761
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3762
|
+
WHERE ec.event_id IS NULL`,
|
|
3763
|
+
[]
|
|
3764
|
+
);
|
|
3765
|
+
if (unindexed.length === 0) return;
|
|
3766
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3767
|
+
this.db.transaction(() => {
|
|
3768
|
+
for (const { id } of unindexed) {
|
|
3769
|
+
insert.run(id, generateCitationId(id));
|
|
3770
|
+
}
|
|
3771
|
+
})();
|
|
3772
|
+
}
|
|
3614
3773
|
/**
|
|
3615
3774
|
* Get events since a timestamp (for sync)
|
|
3616
3775
|
*/
|
|
@@ -3812,25 +3971,22 @@ var SQLiteEventStore = class {
|
|
|
3812
3971
|
*/
|
|
3813
3972
|
async getPendingOutboxItems(limit = 32) {
|
|
3814
3973
|
await this.initialize();
|
|
3815
|
-
const
|
|
3816
|
-
this.db,
|
|
3817
|
-
`SELECT * FROM embedding_outbox
|
|
3818
|
-
WHERE status = 'pending'
|
|
3819
|
-
ORDER BY created_at
|
|
3820
|
-
LIMIT ?`,
|
|
3821
|
-
[limit]
|
|
3822
|
-
);
|
|
3823
|
-
if (pending.length === 0) return [];
|
|
3824
|
-
const ids = pending.map((r) => r.id);
|
|
3825
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
3826
|
-
sqliteRun(
|
|
3974
|
+
const claimed = sqliteAll(
|
|
3827
3975
|
this.db,
|
|
3828
3976
|
`UPDATE embedding_outbox
|
|
3829
3977
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3830
|
-
WHERE id IN (
|
|
3831
|
-
|
|
3978
|
+
WHERE id IN (
|
|
3979
|
+
SELECT id FROM embedding_outbox
|
|
3980
|
+
WHERE status = 'pending'
|
|
3981
|
+
ORDER BY created_at
|
|
3982
|
+
LIMIT ?
|
|
3983
|
+
)
|
|
3984
|
+
RETURNING *`,
|
|
3985
|
+
[limit]
|
|
3832
3986
|
);
|
|
3833
|
-
|
|
3987
|
+
if (claimed.length === 0) return [];
|
|
3988
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
3989
|
+
return claimed.map((row) => ({
|
|
3834
3990
|
id: row.id,
|
|
3835
3991
|
eventId: row.event_id,
|
|
3836
3992
|
content: row.content,
|
|
@@ -4434,7 +4590,7 @@ var SQLiteEventStore = class {
|
|
|
4434
4590
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4435
4591
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4436
4592
|
let toolSuccessCount = 0;
|
|
4437
|
-
|
|
4593
|
+
const toolTotalCount = toolEvents.length;
|
|
4438
4594
|
for (const t of toolEvents) {
|
|
4439
4595
|
try {
|
|
4440
4596
|
const content = JSON.parse(t.content);
|
|
@@ -4597,10 +4753,14 @@ var SQLiteEventStore = class {
|
|
|
4597
4753
|
* Rebuild FTS index from existing events
|
|
4598
4754
|
* Call this once after upgrading to FTS5
|
|
4599
4755
|
*/
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4756
|
+
/**
|
|
4757
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4758
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4759
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4760
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4761
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4762
|
+
*/
|
|
4763
|
+
recreateEventsFtsTable() {
|
|
4604
4764
|
sqliteExec(this.db, `
|
|
4605
4765
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4606
4766
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4629,6 +4789,12 @@ var SQLiteEventStore = class {
|
|
|
4629
4789
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4630
4790
|
END;
|
|
4631
4791
|
`);
|
|
4792
|
+
}
|
|
4793
|
+
async rebuildFtsIndex() {
|
|
4794
|
+
await this.initialize();
|
|
4795
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4796
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4797
|
+
this.recreateEventsFtsTable();
|
|
4632
4798
|
return totalEvents;
|
|
4633
4799
|
}
|
|
4634
4800
|
/**
|
|
@@ -4646,6 +4812,35 @@ var SQLiteEventStore = class {
|
|
|
4646
4812
|
return false;
|
|
4647
4813
|
}
|
|
4648
4814
|
}
|
|
4815
|
+
hasTable(tableName) {
|
|
4816
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4817
|
+
try {
|
|
4818
|
+
const rows = sqliteAll(
|
|
4819
|
+
this.db,
|
|
4820
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4821
|
+
[tableName]
|
|
4822
|
+
);
|
|
4823
|
+
return rows.length > 0;
|
|
4824
|
+
} catch {
|
|
4825
|
+
return false;
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4829
|
+
* Add a column only when its table exists and the column is missing.
|
|
4830
|
+
*
|
|
4831
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4832
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4833
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4834
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4835
|
+
*/
|
|
4836
|
+
addColumnIfMissing(table, column, definition) {
|
|
4837
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4838
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4839
|
+
}
|
|
4840
|
+
if (!this.hasTable(table)) return;
|
|
4841
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4842
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4843
|
+
}
|
|
4649
4844
|
async recordRetrievalTrace(input) {
|
|
4650
4845
|
await this.initialize();
|
|
4651
4846
|
const traceId = randomUUID6();
|
|
@@ -4731,6 +4926,45 @@ var SQLiteEventStore = class {
|
|
|
4731
4926
|
FROM retrieval_traces`,
|
|
4732
4927
|
[]
|
|
4733
4928
|
);
|
|
4929
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
4930
|
+
const strategyRows = sqliteAll(
|
|
4931
|
+
this.db,
|
|
4932
|
+
`SELECT
|
|
4933
|
+
${strategyColumnSql} as strategy,
|
|
4934
|
+
COUNT(*) as total_queries,
|
|
4935
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
4936
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
4937
|
+
SUM(candidate_count) as total_candidate_count,
|
|
4938
|
+
SUM(selected_count) as total_selected_count,
|
|
4939
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
4940
|
+
AVG(selected_count) as avg_selected_count,
|
|
4941
|
+
CASE
|
|
4942
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
4943
|
+
ELSE 0
|
|
4944
|
+
END as selection_rate
|
|
4945
|
+
FROM retrieval_traces
|
|
4946
|
+
GROUP BY ${strategyColumnSql}
|
|
4947
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
4948
|
+
[]
|
|
4949
|
+
);
|
|
4950
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
4951
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
4952
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
4953
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
4954
|
+
return {
|
|
4955
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
4956
|
+
totalQueries: strategyTotalQueries,
|
|
4957
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
4958
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
4959
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
4960
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
4961
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
4962
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
4963
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
4964
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
4965
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
4966
|
+
};
|
|
4967
|
+
});
|
|
4734
4968
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4735
4969
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4736
4970
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4748,7 +4982,8 @@ var SQLiteEventStore = class {
|
|
|
4748
4982
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4749
4983
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4750
4984
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4751
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
4985
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
4986
|
+
strategyBreakdown
|
|
4752
4987
|
};
|
|
4753
4988
|
} catch (err) {
|
|
4754
4989
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4764,7 +4999,8 @@ var SQLiteEventStore = class {
|
|
|
4764
4999
|
rewrittenSelectionRate: 0,
|
|
4765
5000
|
rawSelectionRate: 0,
|
|
4766
5001
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4767
|
-
avgSelectedCountForRawQueries: 0
|
|
5002
|
+
avgSelectedCountForRawQueries: 0,
|
|
5003
|
+
strategyBreakdown: []
|
|
4768
5004
|
};
|
|
4769
5005
|
}
|
|
4770
5006
|
throw err;
|
|
@@ -4881,38 +5117,19 @@ var SQLiteEventStore = class {
|
|
|
4881
5117
|
if (events.length === 0) return 0;
|
|
4882
5118
|
const eventIds = events.map((e) => e.id);
|
|
4883
5119
|
const placeholders = eventIds.map(() => "?").join(",");
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
|
|
5120
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5121
|
+
const runDelete = this.db.transaction(() => {
|
|
5122
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4887
5123
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4888
|
-
ftsTriggersDropped.push(triggerName);
|
|
4889
|
-
} catch {
|
|
4890
5124
|
}
|
|
4891
|
-
|
|
4892
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
4893
|
-
try {
|
|
5125
|
+
for (const table of relatedTables) {
|
|
4894
5126
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
4895
|
-
} catch {
|
|
4896
5127
|
}
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
4903
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4904
|
-
END`);
|
|
4905
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
4906
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4907
|
-
END`);
|
|
4908
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
4909
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4910
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4911
|
-
END`);
|
|
4912
|
-
} catch {
|
|
4913
|
-
}
|
|
4914
|
-
}
|
|
4915
|
-
return result.changes || 0;
|
|
5128
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5129
|
+
this.recreateEventsFtsTable();
|
|
5130
|
+
return result.changes || 0;
|
|
5131
|
+
});
|
|
5132
|
+
return runDelete();
|
|
4916
5133
|
}
|
|
4917
5134
|
/**
|
|
4918
5135
|
* Convert database row to MemoryEvent
|
|
@@ -6467,12 +6684,12 @@ var GraphPathService = class {
|
|
|
6467
6684
|
}
|
|
6468
6685
|
}
|
|
6469
6686
|
}
|
|
6470
|
-
const paths = Array.from(bestByTarget.entries()).map(([key,
|
|
6687
|
+
const paths = Array.from(bestByTarget.entries()).map(([key, path14]) => ({
|
|
6471
6688
|
target: graph.node(nodeFromKey(key)),
|
|
6472
|
-
hops:
|
|
6473
|
-
totalCost:
|
|
6474
|
-
scoreContribution:
|
|
6475
|
-
steps:
|
|
6689
|
+
hops: path14.hops,
|
|
6690
|
+
totalCost: path14.totalCost,
|
|
6691
|
+
scoreContribution: path14.totalCost > 0 ? 1 / path14.totalCost : 0,
|
|
6692
|
+
steps: path14.steps
|
|
6476
6693
|
})).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
|
|
6477
6694
|
return { startNodes, effectiveMaxHops, paths };
|
|
6478
6695
|
}
|
|
@@ -7245,7 +7462,7 @@ var SessionActorRepository = class {
|
|
|
7245
7462
|
};
|
|
7246
7463
|
|
|
7247
7464
|
// src/core/operations/perspective-observation-repository.ts
|
|
7248
|
-
import { createHash as
|
|
7465
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
7249
7466
|
function projectHashToStorage4(projectHash) {
|
|
7250
7467
|
return projectHash ?? "";
|
|
7251
7468
|
}
|
|
@@ -7282,7 +7499,7 @@ function sanitizeStoredRecord(value) {
|
|
|
7282
7499
|
return sanitizeGovernanceAuditValue(value);
|
|
7283
7500
|
}
|
|
7284
7501
|
function stableHash(value) {
|
|
7285
|
-
return
|
|
7502
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7286
7503
|
}
|
|
7287
7504
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
7288
7505
|
return stableHash({
|
|
@@ -7790,23 +8007,6 @@ function createPerspectiveDeriver(options) {
|
|
|
7790
8007
|
return new PerspectiveDeriver(options);
|
|
7791
8008
|
}
|
|
7792
8009
|
|
|
7793
|
-
// src/core/citation-generator.ts
|
|
7794
|
-
import { createHash as createHash4 } from "crypto";
|
|
7795
|
-
var ID_LENGTH = 6;
|
|
7796
|
-
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
7797
|
-
function generateCitationId(eventId) {
|
|
7798
|
-
const hash = createHash4("sha256").update(eventId).digest();
|
|
7799
|
-
let id = "";
|
|
7800
|
-
for (let i = 0; i < ID_LENGTH; i++) {
|
|
7801
|
-
id += CHARSET[hash[i] % CHARSET.length];
|
|
7802
|
-
}
|
|
7803
|
-
return id;
|
|
7804
|
-
}
|
|
7805
|
-
function parseCitationId(formatted) {
|
|
7806
|
-
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
7807
|
-
return match ? match[1] : null;
|
|
7808
|
-
}
|
|
7809
|
-
|
|
7810
8010
|
// src/core/vector-store.ts
|
|
7811
8011
|
import * as lancedb from "@lancedb/lancedb";
|
|
7812
8012
|
var VectorStore = class {
|
|
@@ -8414,6 +8614,30 @@ var MemoryQueryService = class {
|
|
|
8414
8614
|
score: 1 - (r.rank - minRank) / rankRange
|
|
8415
8615
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
8416
8616
|
}
|
|
8617
|
+
async getEvent(id) {
|
|
8618
|
+
await this.initialize();
|
|
8619
|
+
return this.queryStore.getEvent(id);
|
|
8620
|
+
}
|
|
8621
|
+
async getEventByCitationId(citationId) {
|
|
8622
|
+
await this.initialize();
|
|
8623
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
8624
|
+
}
|
|
8625
|
+
async getEventsAfter(sinceIso) {
|
|
8626
|
+
await this.initialize();
|
|
8627
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
8628
|
+
}
|
|
8629
|
+
async getEventTypeCounts() {
|
|
8630
|
+
await this.initialize();
|
|
8631
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
8632
|
+
}
|
|
8633
|
+
async getDistinctSessionCount() {
|
|
8634
|
+
await this.initialize();
|
|
8635
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
8636
|
+
}
|
|
8637
|
+
async getDailyEventCounts(sinceIso) {
|
|
8638
|
+
await this.initialize();
|
|
8639
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
8640
|
+
}
|
|
8417
8641
|
async getSessionHistory(sessionId) {
|
|
8418
8642
|
await this.initialize();
|
|
8419
8643
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -9238,12 +9462,12 @@ var Retriever = class {
|
|
|
9238
9462
|
direction: "both"
|
|
9239
9463
|
});
|
|
9240
9464
|
const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
|
|
9241
|
-
for (const
|
|
9242
|
-
if (
|
|
9243
|
-
const target = await this.eventStore.getEvent(
|
|
9465
|
+
for (const path14 of expansion.paths) {
|
|
9466
|
+
if (path14.target.type !== "event") continue;
|
|
9467
|
+
const target = await this.eventStore.getEvent(path14.target.id);
|
|
9244
9468
|
if (!target) continue;
|
|
9245
|
-
const graphPath = toRetrievalGraphPathDebug(
|
|
9246
|
-
const score = graphPathScore(
|
|
9469
|
+
const graphPath = toRetrievalGraphPathDebug(path14, titleByEntityId);
|
|
9470
|
+
const score = graphPathScore(path14, opts.hopPenalty);
|
|
9247
9471
|
const existing = byId.get(target.id);
|
|
9248
9472
|
const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
|
|
9249
9473
|
const graphLane = {
|
|
@@ -9252,7 +9476,7 @@ var Retriever = class {
|
|
|
9252
9476
|
score
|
|
9253
9477
|
};
|
|
9254
9478
|
const row = {
|
|
9255
|
-
id: existing?.id ?? `graph-path-${
|
|
9479
|
+
id: existing?.id ?? `graph-path-${path14.hops}-${target.id}`,
|
|
9256
9480
|
eventId: target.id,
|
|
9257
9481
|
content: target.content,
|
|
9258
9482
|
score: Math.max(existing?.score ?? 0, score),
|
|
@@ -9382,11 +9606,12 @@ var Retriever = class {
|
|
|
9382
9606
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
9383
9607
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
9384
9608
|
const filtered = [];
|
|
9609
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9385
9610
|
for (const result of results) {
|
|
9386
9611
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
9387
9612
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
9388
9613
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
9389
|
-
const event =
|
|
9614
|
+
const event = eventMap.get(result.eventId);
|
|
9390
9615
|
if (!event) continue;
|
|
9391
9616
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
9392
9617
|
if (normalizedIncludes.length > 0) {
|
|
@@ -9495,10 +9720,30 @@ var Retriever = class {
|
|
|
9495
9720
|
async retrieveRecent(limit = 100) {
|
|
9496
9721
|
return this.eventStore.getRecentEvents(limit);
|
|
9497
9722
|
}
|
|
9723
|
+
/**
|
|
9724
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
9725
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
9726
|
+
*/
|
|
9727
|
+
async fetchEventsByIds(ids) {
|
|
9728
|
+
const map = /* @__PURE__ */ new Map();
|
|
9729
|
+
if (ids.length === 0) return map;
|
|
9730
|
+
const unique = [...new Set(ids)];
|
|
9731
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
9732
|
+
const events = await this.eventStore.getEvents(unique);
|
|
9733
|
+
for (const event of events) map.set(event.id, event);
|
|
9734
|
+
} else {
|
|
9735
|
+
const events = await Promise.all(unique.map((id) => this.eventStore.getEvent(id)));
|
|
9736
|
+
for (const event of events) {
|
|
9737
|
+
if (event) map.set(event.id, event);
|
|
9738
|
+
}
|
|
9739
|
+
}
|
|
9740
|
+
return map;
|
|
9741
|
+
}
|
|
9498
9742
|
async enrichResults(results, options, query) {
|
|
9499
9743
|
const memories = [];
|
|
9744
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9500
9745
|
for (const result of results) {
|
|
9501
|
-
const event =
|
|
9746
|
+
const event = eventMap.get(result.eventId);
|
|
9502
9747
|
if (!event) continue;
|
|
9503
9748
|
if (this.graduation) {
|
|
9504
9749
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -9571,8 +9816,8 @@ _Context:_ ${sessionContext}`;
|
|
|
9571
9816
|
}
|
|
9572
9817
|
matchesMetadataScope(metadata, expected) {
|
|
9573
9818
|
if (!metadata) return false;
|
|
9574
|
-
return Object.entries(expected).every(([
|
|
9575
|
-
const actual =
|
|
9819
|
+
return Object.entries(expected).every(([path14, value]) => {
|
|
9820
|
+
const actual = path14.split(".").reduce((acc, key) => {
|
|
9576
9821
|
if (typeof acc !== "object" || acc === null) return void 0;
|
|
9577
9822
|
return acc[key];
|
|
9578
9823
|
}, metadata);
|
|
@@ -9623,22 +9868,22 @@ function uniqueEntityStartNodes(candidates) {
|
|
|
9623
9868
|
}
|
|
9624
9869
|
return nodes;
|
|
9625
9870
|
}
|
|
9626
|
-
function toRetrievalGraphPathDebug(
|
|
9627
|
-
const firstStep =
|
|
9871
|
+
function toRetrievalGraphPathDebug(path14, titleByEntityId) {
|
|
9872
|
+
const firstStep = path14.steps[0];
|
|
9628
9873
|
const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
|
|
9629
9874
|
const startEntityId = startNode?.type === "entity" ? startNode.id : "";
|
|
9630
9875
|
return {
|
|
9631
9876
|
startEntityId,
|
|
9632
9877
|
startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
|
|
9633
|
-
targetId:
|
|
9634
|
-
targetType:
|
|
9635
|
-
hops:
|
|
9636
|
-
relationPath:
|
|
9878
|
+
targetId: path14.target.id,
|
|
9879
|
+
targetType: path14.target.type,
|
|
9880
|
+
hops: path14.hops,
|
|
9881
|
+
relationPath: path14.steps.map((step) => step.relationType)
|
|
9637
9882
|
};
|
|
9638
9883
|
}
|
|
9639
|
-
function graphPathScore(
|
|
9640
|
-
const base = Math.min(0.95, Math.max(0,
|
|
9641
|
-
return Math.max(0.05, base - hopPenalty * Math.max(0,
|
|
9884
|
+
function graphPathScore(path14, hopPenalty) {
|
|
9885
|
+
const base = Math.min(0.95, Math.max(0, path14.scoreContribution));
|
|
9886
|
+
return Math.max(0.05, base - hopPenalty * Math.max(0, path14.hops - 1));
|
|
9642
9887
|
}
|
|
9643
9888
|
function clampGraphHops(maxHops) {
|
|
9644
9889
|
if (!Number.isFinite(maxHops)) return 2;
|
|
@@ -9646,14 +9891,14 @@ function clampGraphHops(maxHops) {
|
|
|
9646
9891
|
}
|
|
9647
9892
|
function mergeGraphPaths(existing, incoming) {
|
|
9648
9893
|
const byKey = /* @__PURE__ */ new Map();
|
|
9649
|
-
for (const
|
|
9650
|
-
const key = [
|
|
9651
|
-
if (!byKey.has(key)) byKey.set(key,
|
|
9894
|
+
for (const path14 of [...existing, ...incoming]) {
|
|
9895
|
+
const key = [path14.startEntityId, path14.targetType, path14.targetId, path14.hops, ...path14.relationPath].join("\0");
|
|
9896
|
+
if (!byKey.has(key)) byKey.set(key, path14);
|
|
9652
9897
|
}
|
|
9653
9898
|
return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
|
|
9654
9899
|
}
|
|
9655
|
-
function graphPathSignature(
|
|
9656
|
-
return [
|
|
9900
|
+
function graphPathSignature(path14) {
|
|
9901
|
+
return [path14.startEntityId, path14.targetType, path14.targetId, path14.hops, ...path14.relationPath].join("|");
|
|
9657
9902
|
}
|
|
9658
9903
|
function compareStable(a, b) {
|
|
9659
9904
|
if (a < b) return -1;
|
|
@@ -12226,6 +12471,34 @@ var MemoryService = class {
|
|
|
12226
12471
|
async rebuildFtsIndex() {
|
|
12227
12472
|
return this.queryService.rebuildFtsIndex();
|
|
12228
12473
|
}
|
|
12474
|
+
/**
|
|
12475
|
+
* Get a single event by id (indexed lookup).
|
|
12476
|
+
*/
|
|
12477
|
+
async getEvent(id) {
|
|
12478
|
+
return this.queryService.getEvent(id);
|
|
12479
|
+
}
|
|
12480
|
+
/**
|
|
12481
|
+
* Resolve an event from its citation id via the reverse index.
|
|
12482
|
+
*/
|
|
12483
|
+
async getEventByCitationId(citationId) {
|
|
12484
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
12485
|
+
}
|
|
12486
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
12487
|
+
async getEventsAfter(sinceIso) {
|
|
12488
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
12489
|
+
}
|
|
12490
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
12491
|
+
async getEventTypeCounts() {
|
|
12492
|
+
return this.queryService.getEventTypeCounts();
|
|
12493
|
+
}
|
|
12494
|
+
/** Count distinct sessions (SQL). */
|
|
12495
|
+
async getDistinctSessionCount() {
|
|
12496
|
+
return this.queryService.getDistinctSessionCount();
|
|
12497
|
+
}
|
|
12498
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
12499
|
+
async getDailyEventCounts(sinceIso) {
|
|
12500
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
12501
|
+
}
|
|
12229
12502
|
/**
|
|
12230
12503
|
* Get session history
|
|
12231
12504
|
*/
|
|
@@ -12550,6 +12823,11 @@ var getLightweightMemoryServiceForProject = defaultRegistry.getLightweightMemory
|
|
|
12550
12823
|
var createMemoryService = defaultRegistry.createMemoryService;
|
|
12551
12824
|
|
|
12552
12825
|
// src/apps/server/api/utils.ts
|
|
12826
|
+
function jsonError(c, error, options = {}) {
|
|
12827
|
+
const status = options.status ?? 500;
|
|
12828
|
+
console.error(`[api] ${c.req.method} ${c.req.path} failed:`, error);
|
|
12829
|
+
return c.json({ error: options.message ?? "Internal server error" }, status);
|
|
12830
|
+
}
|
|
12553
12831
|
function getServiceFromQuery(c) {
|
|
12554
12832
|
const project = c.req.query("project") || c.req.query("projectId");
|
|
12555
12833
|
if (project) {
|
|
@@ -12661,7 +12939,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
12661
12939
|
hasMore: end < total
|
|
12662
12940
|
});
|
|
12663
12941
|
} catch (error) {
|
|
12664
|
-
return c
|
|
12942
|
+
return jsonError(c, error);
|
|
12665
12943
|
} finally {
|
|
12666
12944
|
await memoryService.shutdown();
|
|
12667
12945
|
}
|
|
@@ -12697,7 +12975,7 @@ sessionsRouter.get("/:id", async (c) => {
|
|
|
12697
12975
|
stats: eventsByType
|
|
12698
12976
|
});
|
|
12699
12977
|
} catch (error) {
|
|
12700
|
-
return c
|
|
12978
|
+
return jsonError(c, error);
|
|
12701
12979
|
} finally {
|
|
12702
12980
|
await memoryService.shutdown();
|
|
12703
12981
|
}
|
|
@@ -12763,7 +13041,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
12763
13041
|
hasMore: offset + limit < total
|
|
12764
13042
|
});
|
|
12765
13043
|
} catch (error) {
|
|
12766
|
-
return c
|
|
13044
|
+
return jsonError(c, error);
|
|
12767
13045
|
} finally {
|
|
12768
13046
|
await memoryService.shutdown();
|
|
12769
13047
|
}
|
|
@@ -12773,12 +13051,11 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
12773
13051
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
12774
13052
|
try {
|
|
12775
13053
|
await memoryService.initialize();
|
|
12776
|
-
const
|
|
12777
|
-
const event = recentEvents.find((e) => e.id === id);
|
|
13054
|
+
const event = await memoryService.getEvent(id);
|
|
12778
13055
|
if (!event) {
|
|
12779
13056
|
return c.json({ error: "Event not found" }, 404);
|
|
12780
13057
|
}
|
|
12781
|
-
const sessionEvents =
|
|
13058
|
+
const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
12782
13059
|
const eventIndex = sessionEvents.findIndex((e) => e.id === id);
|
|
12783
13060
|
const start = Math.max(0, eventIndex - 2);
|
|
12784
13061
|
const end = Math.min(sessionEvents.length, eventIndex + 3);
|
|
@@ -12800,7 +13077,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
12800
13077
|
}))
|
|
12801
13078
|
});
|
|
12802
13079
|
} catch (error) {
|
|
12803
|
-
return c
|
|
13080
|
+
return jsonError(c, error);
|
|
12804
13081
|
} finally {
|
|
12805
13082
|
await memoryService.shutdown();
|
|
12806
13083
|
}
|
|
@@ -12847,7 +13124,7 @@ searchRouter.post("/", async (c) => {
|
|
|
12847
13124
|
}
|
|
12848
13125
|
});
|
|
12849
13126
|
} catch (error) {
|
|
12850
|
-
return c
|
|
13127
|
+
return jsonError(c, error);
|
|
12851
13128
|
} finally {
|
|
12852
13129
|
await memoryService.shutdown();
|
|
12853
13130
|
}
|
|
@@ -12890,7 +13167,7 @@ searchRouter.post("/disclosure", async (c) => {
|
|
|
12890
13167
|
throw error;
|
|
12891
13168
|
}
|
|
12892
13169
|
} catch (error) {
|
|
12893
|
-
return c
|
|
13170
|
+
return jsonError(c, error);
|
|
12894
13171
|
} finally {
|
|
12895
13172
|
await memoryService?.shutdown();
|
|
12896
13173
|
}
|
|
@@ -12910,7 +13187,7 @@ searchRouter.get("/disclosure/:resultId/expand", async (c) => {
|
|
|
12910
13187
|
}
|
|
12911
13188
|
return c.json(result);
|
|
12912
13189
|
} catch (error) {
|
|
12913
|
-
return c
|
|
13190
|
+
return jsonError(c, error);
|
|
12914
13191
|
} finally {
|
|
12915
13192
|
await memoryService.shutdown();
|
|
12916
13193
|
}
|
|
@@ -12925,7 +13202,7 @@ searchRouter.get("/disclosure/:resultId/source", async (c) => {
|
|
|
12925
13202
|
}
|
|
12926
13203
|
return c.json(result);
|
|
12927
13204
|
} catch (error) {
|
|
12928
|
-
return c
|
|
13205
|
+
return jsonError(c, error);
|
|
12929
13206
|
} finally {
|
|
12930
13207
|
await memoryService.shutdown();
|
|
12931
13208
|
}
|
|
@@ -12954,7 +13231,7 @@ searchRouter.get("/", async (c) => {
|
|
|
12954
13231
|
}
|
|
12955
13232
|
});
|
|
12956
13233
|
} catch (error) {
|
|
12957
|
-
return c
|
|
13234
|
+
return jsonError(c, error);
|
|
12958
13235
|
} finally {
|
|
12959
13236
|
await memoryService.shutdown();
|
|
12960
13237
|
}
|
|
@@ -12962,9 +13239,89 @@ searchRouter.get("/", async (c) => {
|
|
|
12962
13239
|
|
|
12963
13240
|
// src/apps/server/api/stats.ts
|
|
12964
13241
|
import { Hono as Hono4 } from "hono";
|
|
12965
|
-
import * as
|
|
13242
|
+
import * as fs10 from "fs";
|
|
12966
13243
|
import * as os6 from "os";
|
|
13244
|
+
import * as path12 from "path";
|
|
13245
|
+
|
|
13246
|
+
// src/apps/server/api/stats-metrics.ts
|
|
13247
|
+
import * as fs9 from "fs";
|
|
12967
13248
|
import * as path11 from "path";
|
|
13249
|
+
var DEFAULT_KPI_THRESHOLDS = {
|
|
13250
|
+
usefulRecallRateMin: 0.45,
|
|
13251
|
+
reworkRateMax: 0.25,
|
|
13252
|
+
postChangeFailureRateMax: 0.2,
|
|
13253
|
+
avgCompletionTurnsMax: 12,
|
|
13254
|
+
memoryHitRateMin: 0.35
|
|
13255
|
+
};
|
|
13256
|
+
function loadKpiThresholds() {
|
|
13257
|
+
try {
|
|
13258
|
+
const filePath = path11.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
13259
|
+
if (!fs9.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
13260
|
+
const parsed = JSON.parse(fs9.readFileSync(filePath, "utf-8"));
|
|
13261
|
+
return {
|
|
13262
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
13263
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
13264
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
13265
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
13266
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
13267
|
+
};
|
|
13268
|
+
} catch {
|
|
13269
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
13270
|
+
}
|
|
13271
|
+
}
|
|
13272
|
+
function windowToMs(window) {
|
|
13273
|
+
if (window === "24h") return 24 * 60 * 60 * 1e3;
|
|
13274
|
+
if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
|
|
13275
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
13276
|
+
}
|
|
13277
|
+
function inWindow(e, now, window) {
|
|
13278
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
13279
|
+
}
|
|
13280
|
+
function isEditToolName(name) {
|
|
13281
|
+
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
13282
|
+
}
|
|
13283
|
+
function parseToolPayload(e) {
|
|
13284
|
+
if (e.eventType !== "tool_observation") return null;
|
|
13285
|
+
try {
|
|
13286
|
+
const payload = JSON.parse(e.content);
|
|
13287
|
+
return {
|
|
13288
|
+
toolName: payload?.toolName,
|
|
13289
|
+
success: payload?.success,
|
|
13290
|
+
filePath: payload?.metadata?.filePath,
|
|
13291
|
+
command: payload?.metadata?.command
|
|
13292
|
+
};
|
|
13293
|
+
} catch {
|
|
13294
|
+
return {
|
|
13295
|
+
toolName: e.metadata?.toolName,
|
|
13296
|
+
success: e.metadata?.success,
|
|
13297
|
+
filePath: e.metadata?.filePath,
|
|
13298
|
+
command: e.metadata?.command
|
|
13299
|
+
};
|
|
13300
|
+
}
|
|
13301
|
+
}
|
|
13302
|
+
function isTestLikeCommand(command) {
|
|
13303
|
+
if (!command) return false;
|
|
13304
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
13305
|
+
}
|
|
13306
|
+
function safeRatio(num, den) {
|
|
13307
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
13308
|
+
return num / den;
|
|
13309
|
+
}
|
|
13310
|
+
function round(value, digits = 4) {
|
|
13311
|
+
const factor = 10 ** digits;
|
|
13312
|
+
return Math.round(value * factor) / factor;
|
|
13313
|
+
}
|
|
13314
|
+
function computeSessionTurnCount(sessionEvents) {
|
|
13315
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
13316
|
+
for (const e of sessionEvents) {
|
|
13317
|
+
const turnId = e.metadata?.turnId;
|
|
13318
|
+
if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
|
|
13319
|
+
}
|
|
13320
|
+
if (turnIds.size > 0) return turnIds.size;
|
|
13321
|
+
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
13322
|
+
}
|
|
13323
|
+
|
|
13324
|
+
// src/apps/server/api/stats.ts
|
|
12968
13325
|
var statsRouter = new Hono4();
|
|
12969
13326
|
var OPERATION_STATS_TABLES = [
|
|
12970
13327
|
"memory_facets",
|
|
@@ -12991,7 +13348,7 @@ function operationsStatsHomeDir() {
|
|
|
12991
13348
|
}
|
|
12992
13349
|
function projectStoragePathForOperationsStats(projectOrHash) {
|
|
12993
13350
|
const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
|
|
12994
|
-
return
|
|
13351
|
+
return path12.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
|
|
12995
13352
|
}
|
|
12996
13353
|
function getOperationsStatsContext(project) {
|
|
12997
13354
|
if (project && project.trim().length > 0) {
|
|
@@ -13001,13 +13358,13 @@ function getOperationsStatsContext(project) {
|
|
|
13001
13358
|
return {
|
|
13002
13359
|
projectHash,
|
|
13003
13360
|
storagePath: storagePath2,
|
|
13004
|
-
dbPath:
|
|
13361
|
+
dbPath: path12.join(storagePath2, "events.sqlite")
|
|
13005
13362
|
};
|
|
13006
13363
|
}
|
|
13007
|
-
const storagePath =
|
|
13364
|
+
const storagePath = path12.join(operationsStatsHomeDir(), ".claude-code", "memory");
|
|
13008
13365
|
return {
|
|
13009
13366
|
storagePath,
|
|
13010
|
-
dbPath:
|
|
13367
|
+
dbPath: path12.join(storagePath, "events.sqlite")
|
|
13011
13368
|
};
|
|
13012
13369
|
}
|
|
13013
13370
|
function countRowValue(db, sql, params = []) {
|
|
@@ -13025,6 +13382,23 @@ function sanitizeAggregateLabel(value) {
|
|
|
13025
13382
|
const trimmed = sanitized.trim();
|
|
13026
13383
|
return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
|
|
13027
13384
|
}
|
|
13385
|
+
var SAFE_RETRIEVAL_TRACE_STRATEGIES = /* @__PURE__ */ new Set([
|
|
13386
|
+
"auto",
|
|
13387
|
+
"deep",
|
|
13388
|
+
"fast",
|
|
13389
|
+
"hybrid",
|
|
13390
|
+
"keyword",
|
|
13391
|
+
"semantic",
|
|
13392
|
+
"mcp-context-pack",
|
|
13393
|
+
"session-start-hook",
|
|
13394
|
+
"unknown"
|
|
13395
|
+
]);
|
|
13396
|
+
function normalizeRetrievalTraceStrategy(value) {
|
|
13397
|
+
const label = sanitizeAggregateLabel(value);
|
|
13398
|
+
if (label === "[REDACTED]") return "unknown";
|
|
13399
|
+
const normalized = label.trim().toLowerCase();
|
|
13400
|
+
return SAFE_RETRIEVAL_TRACE_STRATEGIES.has(normalized) ? normalized : "unknown";
|
|
13401
|
+
}
|
|
13028
13402
|
function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
|
|
13029
13403
|
return {
|
|
13030
13404
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13438,80 +13812,6 @@ function buildLessonConfidenceBuckets(db, projectHash) {
|
|
|
13438
13812
|
}).length
|
|
13439
13813
|
}));
|
|
13440
13814
|
}
|
|
13441
|
-
var DEFAULT_KPI_THRESHOLDS = {
|
|
13442
|
-
usefulRecallRateMin: 0.45,
|
|
13443
|
-
reworkRateMax: 0.25,
|
|
13444
|
-
postChangeFailureRateMax: 0.2,
|
|
13445
|
-
avgCompletionTurnsMax: 12,
|
|
13446
|
-
memoryHitRateMin: 0.35
|
|
13447
|
-
};
|
|
13448
|
-
function loadKpiThresholds() {
|
|
13449
|
-
try {
|
|
13450
|
-
const filePath = path11.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
13451
|
-
if (!fs9.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
13452
|
-
const parsed = JSON.parse(fs9.readFileSync(filePath, "utf-8"));
|
|
13453
|
-
return {
|
|
13454
|
-
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
13455
|
-
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
13456
|
-
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
13457
|
-
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
13458
|
-
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
13459
|
-
};
|
|
13460
|
-
} catch {
|
|
13461
|
-
return DEFAULT_KPI_THRESHOLDS;
|
|
13462
|
-
}
|
|
13463
|
-
}
|
|
13464
|
-
function windowToMs(window) {
|
|
13465
|
-
if (window === "24h") return 24 * 60 * 60 * 1e3;
|
|
13466
|
-
if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
|
|
13467
|
-
return 30 * 24 * 60 * 60 * 1e3;
|
|
13468
|
-
}
|
|
13469
|
-
function inWindow(e, now, window) {
|
|
13470
|
-
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
13471
|
-
}
|
|
13472
|
-
function isEditToolName(name) {
|
|
13473
|
-
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
13474
|
-
}
|
|
13475
|
-
function parseToolPayload(e) {
|
|
13476
|
-
if (e.eventType !== "tool_observation") return null;
|
|
13477
|
-
try {
|
|
13478
|
-
const payload = JSON.parse(e.content);
|
|
13479
|
-
return {
|
|
13480
|
-
toolName: payload?.toolName,
|
|
13481
|
-
success: payload?.success,
|
|
13482
|
-
filePath: payload?.metadata?.filePath,
|
|
13483
|
-
command: payload?.metadata?.command
|
|
13484
|
-
};
|
|
13485
|
-
} catch {
|
|
13486
|
-
return {
|
|
13487
|
-
toolName: e.metadata?.toolName,
|
|
13488
|
-
success: e.metadata?.success,
|
|
13489
|
-
filePath: e.metadata?.filePath,
|
|
13490
|
-
command: e.metadata?.command
|
|
13491
|
-
};
|
|
13492
|
-
}
|
|
13493
|
-
}
|
|
13494
|
-
function isTestLikeCommand(command) {
|
|
13495
|
-
if (!command) return false;
|
|
13496
|
-
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
13497
|
-
}
|
|
13498
|
-
function safeRatio(num, den) {
|
|
13499
|
-
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
13500
|
-
return num / den;
|
|
13501
|
-
}
|
|
13502
|
-
function round(value, digits = 4) {
|
|
13503
|
-
const factor = 10 ** digits;
|
|
13504
|
-
return Math.round(value * factor) / factor;
|
|
13505
|
-
}
|
|
13506
|
-
function computeSessionTurnCount(sessionEvents) {
|
|
13507
|
-
const turnIds = /* @__PURE__ */ new Set();
|
|
13508
|
-
for (const e of sessionEvents) {
|
|
13509
|
-
const turnId = e.metadata?.turnId;
|
|
13510
|
-
if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
|
|
13511
|
-
}
|
|
13512
|
-
if (turnIds.size > 0) return turnIds.size;
|
|
13513
|
-
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
13514
|
-
}
|
|
13515
13815
|
function normalizeQueryRewriteKind2(value) {
|
|
13516
13816
|
const normalized = (value || "").trim().toLowerCase();
|
|
13517
13817
|
if (normalized === "follow-up-context" || normalized === "intent-rewrite") return normalized;
|
|
@@ -13539,6 +13839,63 @@ function getTraceSelectedCount(trace) {
|
|
|
13539
13839
|
function getTraceCandidateCount(trace) {
|
|
13540
13840
|
return Number(trace.candidateCount ?? trace.candidateEventIds?.length ?? trace.candidateDetails?.length ?? 0);
|
|
13541
13841
|
}
|
|
13842
|
+
function normalizeStatsNumber(value) {
|
|
13843
|
+
const numberValue = Number(value || 0);
|
|
13844
|
+
return Number.isFinite(numberValue) ? Math.max(0, numberValue) : 0;
|
|
13845
|
+
}
|
|
13846
|
+
function sanitizeRetrievalTraceStrategyBreakdown(value) {
|
|
13847
|
+
if (!Array.isArray(value)) return [];
|
|
13848
|
+
const byStrategy = /* @__PURE__ */ new Map();
|
|
13849
|
+
for (const item of value) {
|
|
13850
|
+
const row = item && typeof item === "object" ? item : {};
|
|
13851
|
+
const strategy = normalizeRetrievalTraceStrategy(row.strategy ?? "unknown");
|
|
13852
|
+
const bucket = byStrategy.get(strategy) ?? {
|
|
13853
|
+
strategy,
|
|
13854
|
+
totalQueries: 0,
|
|
13855
|
+
queriesWithSelection: 0,
|
|
13856
|
+
rewrittenQueries: 0,
|
|
13857
|
+
totalCandidateCount: 0,
|
|
13858
|
+
totalSelectedCount: 0
|
|
13859
|
+
};
|
|
13860
|
+
bucket.totalQueries += normalizeStatsNumber(row.totalQueries);
|
|
13861
|
+
bucket.queriesWithSelection += normalizeStatsNumber(row.queriesWithSelection);
|
|
13862
|
+
bucket.rewrittenQueries += normalizeStatsNumber(row.rewrittenQueries);
|
|
13863
|
+
bucket.totalCandidateCount += normalizeStatsNumber(row.totalCandidateCount);
|
|
13864
|
+
bucket.totalSelectedCount += normalizeStatsNumber(row.totalSelectedCount);
|
|
13865
|
+
byStrategy.set(strategy, bucket);
|
|
13866
|
+
}
|
|
13867
|
+
return Array.from(byStrategy.values()).map((row) => ({
|
|
13868
|
+
strategy: row.strategy,
|
|
13869
|
+
totalQueries: row.totalQueries,
|
|
13870
|
+
queriesWithSelection: row.queriesWithSelection,
|
|
13871
|
+
rewrittenQueries: row.rewrittenQueries,
|
|
13872
|
+
rewriteRate: normalizeMetric(safeRatio(row.rewrittenQueries, row.totalQueries)),
|
|
13873
|
+
totalCandidateCount: row.totalCandidateCount,
|
|
13874
|
+
totalSelectedCount: row.totalSelectedCount,
|
|
13875
|
+
avgCandidateCount: safeRatio(row.totalCandidateCount, row.totalQueries),
|
|
13876
|
+
avgSelectedCount: safeRatio(row.totalSelectedCount, row.totalQueries),
|
|
13877
|
+
selectionRate: normalizeMetric(safeRatio(row.totalSelectedCount, row.totalCandidateCount)),
|
|
13878
|
+
queryYieldRate: normalizeMetric(safeRatio(row.queriesWithSelection, row.totalQueries))
|
|
13879
|
+
}));
|
|
13880
|
+
}
|
|
13881
|
+
function sanitizeRetrievalTraceStats(stats) {
|
|
13882
|
+
const s = stats && typeof stats === "object" ? stats : {};
|
|
13883
|
+
return {
|
|
13884
|
+
totalQueries: Number(s.totalQueries || 0),
|
|
13885
|
+
avgCandidateCount: Number(s.avgCandidateCount || 0),
|
|
13886
|
+
avgSelectedCount: Number(s.avgSelectedCount || 0),
|
|
13887
|
+
selectionRate: normalizeMetric(s.selectionRate),
|
|
13888
|
+
rewrittenQueries: Number(s.rewrittenQueries || 0),
|
|
13889
|
+
rewriteRate: normalizeMetric(s.rewriteRate),
|
|
13890
|
+
rewrittenQueriesWithSelection: Number(s.rewrittenQueriesWithSelection || 0),
|
|
13891
|
+
rawQueriesWithSelection: Number(s.rawQueriesWithSelection || 0),
|
|
13892
|
+
rewrittenSelectionRate: normalizeMetric(s.rewrittenSelectionRate),
|
|
13893
|
+
rawSelectionRate: normalizeMetric(s.rawSelectionRate),
|
|
13894
|
+
avgSelectedCountForRewrittenQueries: Number(s.avgSelectedCountForRewrittenQueries || 0),
|
|
13895
|
+
avgSelectedCountForRawQueries: Number(s.avgSelectedCountForRawQueries || 0),
|
|
13896
|
+
strategyBreakdown: sanitizeRetrievalTraceStrategyBreakdown(s.strategyBreakdown)
|
|
13897
|
+
};
|
|
13898
|
+
}
|
|
13542
13899
|
function makeRetrievalReviewItem(trace) {
|
|
13543
13900
|
const candidateCount = getTraceCandidateCount(trace);
|
|
13544
13901
|
const selectedCount = getTraceSelectedCount(trace);
|
|
@@ -13592,7 +13949,7 @@ function makeRetrievalReviewItem(trace) {
|
|
|
13592
13949
|
action,
|
|
13593
13950
|
queryRewriteKind,
|
|
13594
13951
|
rewritten,
|
|
13595
|
-
strategy: trace.strategy
|
|
13952
|
+
strategy: normalizeRetrievalTraceStrategy(trace.strategy ?? "unknown"),
|
|
13596
13953
|
candidateCount,
|
|
13597
13954
|
selectedCount,
|
|
13598
13955
|
candidateEventIds: (trace.candidateEventIds || []).slice(0, 5),
|
|
@@ -14019,7 +14376,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
14019
14376
|
hasMore: events.length === limit
|
|
14020
14377
|
});
|
|
14021
14378
|
} catch (error) {
|
|
14022
|
-
return c
|
|
14379
|
+
return jsonError(c, error);
|
|
14023
14380
|
} finally {
|
|
14024
14381
|
await memoryService.shutdown();
|
|
14025
14382
|
}
|
|
@@ -14028,7 +14385,7 @@ statsRouter.get("/operations", async (c) => {
|
|
|
14028
14385
|
const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
|
|
14029
14386
|
const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
|
|
14030
14387
|
const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
|
|
14031
|
-
const databaseExists =
|
|
14388
|
+
const databaseExists = fs10.existsSync(context.dbPath);
|
|
14032
14389
|
if (!databaseExists) {
|
|
14033
14390
|
return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
|
|
14034
14391
|
}
|
|
@@ -14116,7 +14473,7 @@ statsRouter.get("/perspective", async (c) => {
|
|
|
14116
14473
|
const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
|
|
14117
14474
|
const contradictionLimit = parseStatsLimit(c.req.query("contradictionLimit"), 10, 50);
|
|
14118
14475
|
const graphLimit = parseStatsLimit(c.req.query("graphLimit"), 10, 50);
|
|
14119
|
-
const databaseExists =
|
|
14476
|
+
const databaseExists = fs10.existsSync(context.dbPath);
|
|
14120
14477
|
if (!databaseExists) {
|
|
14121
14478
|
return c.json(emptyPerspectiveStatsPayload(context, false, [...PERSPECTIVE_STATS_TABLES], windowDays));
|
|
14122
14479
|
}
|
|
@@ -14192,33 +14549,29 @@ statsRouter.get("/", async (c) => {
|
|
|
14192
14549
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
14193
14550
|
try {
|
|
14194
14551
|
await memoryService.initialize();
|
|
14195
|
-
const
|
|
14196
|
-
const
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
const
|
|
14204
|
-
const eventsByDay =
|
|
14205
|
-
|
|
14206
|
-
acc[day] = (acc[day] || 0) + 1;
|
|
14207
|
-
return acc;
|
|
14208
|
-
}, {});
|
|
14209
|
-
const retrievalTrace = await memoryService.getRetrievalTraceStats();
|
|
14552
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
|
|
14553
|
+
const [stats, typeCounts, sessionTotal, dailyCounts, rawTrace] = await Promise.all([
|
|
14554
|
+
memoryService.getStats(),
|
|
14555
|
+
memoryService.getEventTypeCounts(),
|
|
14556
|
+
memoryService.getDistinctSessionCount(),
|
|
14557
|
+
memoryService.getDailyEventCounts(sevenDaysAgo),
|
|
14558
|
+
memoryService.getRetrievalTraceStats()
|
|
14559
|
+
]);
|
|
14560
|
+
const eventsByType = Object.fromEntries(typeCounts.map((t) => [t.eventType, t.count]));
|
|
14561
|
+
const eventsByDay = Object.fromEntries(dailyCounts.map((d) => [d.day, d.total]));
|
|
14562
|
+
const retrievalTrace = sanitizeRetrievalTraceStats(rawTrace);
|
|
14210
14563
|
return c.json({
|
|
14211
14564
|
storage: {
|
|
14212
14565
|
eventCount: stats.totalEvents,
|
|
14213
14566
|
vectorCount: stats.vectorCount
|
|
14214
14567
|
},
|
|
14215
14568
|
sessions: {
|
|
14216
|
-
total:
|
|
14569
|
+
total: sessionTotal
|
|
14217
14570
|
},
|
|
14218
14571
|
eventsByType,
|
|
14219
14572
|
activity: {
|
|
14220
14573
|
daily: eventsByDay,
|
|
14221
|
-
total7Days:
|
|
14574
|
+
total7Days: dailyCounts.reduce((sum, d) => sum + d.total, 0)
|
|
14222
14575
|
},
|
|
14223
14576
|
memory: {
|
|
14224
14577
|
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
@@ -14228,7 +14581,7 @@ statsRouter.get("/", async (c) => {
|
|
|
14228
14581
|
retrievalTrace
|
|
14229
14582
|
});
|
|
14230
14583
|
} catch (error) {
|
|
14231
|
-
return c
|
|
14584
|
+
return jsonError(c, error);
|
|
14232
14585
|
} finally {
|
|
14233
14586
|
await memoryService.shutdown();
|
|
14234
14587
|
}
|
|
@@ -14265,30 +14618,22 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
14265
14618
|
}
|
|
14266
14619
|
});
|
|
14267
14620
|
statsRouter.get("/timeline", async (c) => {
|
|
14268
|
-
const
|
|
14621
|
+
const parsedDays = parseInt(c.req.query("days") || "7", 10);
|
|
14622
|
+
const days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 7;
|
|
14269
14623
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
14270
14624
|
try {
|
|
14271
14625
|
await memoryService.initialize();
|
|
14272
|
-
const
|
|
14273
|
-
const
|
|
14274
|
-
|
|
14275
|
-
|
|
14276
|
-
|
|
14277
|
-
|
|
14278
|
-
|
|
14279
|
-
|
|
14280
|
-
|
|
14281
|
-
if (e.eventType === "user_prompt") acc[day].prompts++;
|
|
14282
|
-
if (e.eventType === "agent_response") acc[day].responses++;
|
|
14283
|
-
if (e.eventType === "tool_observation") acc[day].tools++;
|
|
14284
|
-
return acc;
|
|
14285
|
-
}, {});
|
|
14286
|
-
return c.json({
|
|
14287
|
-
days,
|
|
14288
|
-
daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
|
|
14289
|
-
});
|
|
14626
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
14627
|
+
const daily = (await memoryService.getDailyEventCounts(cutoff)).map((d) => ({
|
|
14628
|
+
date: d.day,
|
|
14629
|
+
total: d.total,
|
|
14630
|
+
prompts: d.prompts,
|
|
14631
|
+
responses: d.responses,
|
|
14632
|
+
tools: d.tools
|
|
14633
|
+
}));
|
|
14634
|
+
return c.json({ days, daily });
|
|
14290
14635
|
} catch (error) {
|
|
14291
|
-
return c
|
|
14636
|
+
return jsonError(c, error);
|
|
14292
14637
|
} finally {
|
|
14293
14638
|
await memoryService.shutdown();
|
|
14294
14639
|
}
|
|
@@ -14331,16 +14676,14 @@ statsRouter.get("/usefulness", async (c) => {
|
|
|
14331
14676
|
try {
|
|
14332
14677
|
await memoryService.initialize();
|
|
14333
14678
|
const now = Date.now();
|
|
14334
|
-
const eventLimit = 2e4;
|
|
14335
14679
|
const traceLimit = 5e3;
|
|
14336
14680
|
const windowStart = new Date(now - windowToMs(window));
|
|
14337
14681
|
const [events, helpfulness, traces] = await Promise.all([
|
|
14338
|
-
memoryService.
|
|
14682
|
+
memoryService.getEventsAfter(windowStart.toISOString()),
|
|
14339
14683
|
memoryService.getHelpfulnessStats(windowStart),
|
|
14340
14684
|
memoryService.getRecentRetrievalTraces(traceLimit)
|
|
14341
14685
|
]);
|
|
14342
14686
|
return c.json(computeMemoryUsefulnessSummary(events, helpfulness, traces, now, window, {
|
|
14343
|
-
eventsLimit: eventLimit,
|
|
14344
14687
|
tracesLimit: traceLimit
|
|
14345
14688
|
}));
|
|
14346
14689
|
} catch (error) {
|
|
@@ -14358,7 +14701,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
14358
14701
|
const traces = await memoryService.getRecentRetrievalTraces(limit);
|
|
14359
14702
|
const traceStats = await memoryService.getRetrievalTraceStats();
|
|
14360
14703
|
return c.json({
|
|
14361
|
-
stats: traceStats,
|
|
14704
|
+
stats: sanitizeRetrievalTraceStats(traceStats),
|
|
14362
14705
|
traces: traces.map((t) => {
|
|
14363
14706
|
const queryRewriteKind = normalizeQueryRewriteKind2(t.queryRewriteKind);
|
|
14364
14707
|
return {
|
|
@@ -14367,7 +14710,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
14367
14710
|
projectHash: t.projectHash || null,
|
|
14368
14711
|
queryRewriteKind,
|
|
14369
14712
|
rewritten: queryRewriteKind !== "none",
|
|
14370
|
-
strategy: t.strategy
|
|
14713
|
+
strategy: normalizeRetrievalTraceStrategy(t.strategy ?? "unknown"),
|
|
14371
14714
|
candidateEventIds: t.candidateEventIds,
|
|
14372
14715
|
selectedEventIds: t.selectedEventIds,
|
|
14373
14716
|
candidateDetails: t.candidateDetails || [],
|
|
@@ -14394,7 +14737,8 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
14394
14737
|
rewrittenSelectionRate: 0,
|
|
14395
14738
|
rawSelectionRate: 0,
|
|
14396
14739
|
avgSelectedCountForRewrittenQueries: 0,
|
|
14397
|
-
avgSelectedCountForRawQueries: 0
|
|
14740
|
+
avgSelectedCountForRawQueries: 0,
|
|
14741
|
+
strategyBreakdown: []
|
|
14398
14742
|
},
|
|
14399
14743
|
traces: [],
|
|
14400
14744
|
error: error.message
|
|
@@ -14445,7 +14789,8 @@ statsRouter.get("/kpi", async (c) => {
|
|
|
14445
14789
|
await memoryService.initialize();
|
|
14446
14790
|
const now = Date.now();
|
|
14447
14791
|
const thresholds = loadKpiThresholds();
|
|
14448
|
-
const
|
|
14792
|
+
const lookbackMs = Math.max(2 * windowToMs(window), 30 * 24 * 60 * 60 * 1e3);
|
|
14793
|
+
const allEvents = await memoryService.getEventsAfter(new Date(now - lookbackMs).toISOString());
|
|
14449
14794
|
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
14450
14795
|
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
14451
14796
|
const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
|
|
@@ -14554,7 +14899,7 @@ statsRouter.get("/kpi", async (c) => {
|
|
|
14554
14899
|
alerts
|
|
14555
14900
|
});
|
|
14556
14901
|
} catch (error) {
|
|
14557
|
-
return c
|
|
14902
|
+
return jsonError(c, error);
|
|
14558
14903
|
} finally {
|
|
14559
14904
|
await memoryService.shutdown();
|
|
14560
14905
|
}
|
|
@@ -14605,11 +14950,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
14605
14950
|
const memoryService = getServiceFromQuery(c);
|
|
14606
14951
|
try {
|
|
14607
14952
|
await memoryService.initialize();
|
|
14608
|
-
const
|
|
14609
|
-
const event = recentEvents.find((e) => {
|
|
14610
|
-
const eventCitationId = generateCitationId(e.id);
|
|
14611
|
-
return eventCitationId === citationId;
|
|
14612
|
-
});
|
|
14953
|
+
const event = await memoryService.getEventByCitationId(citationId);
|
|
14613
14954
|
if (!event) {
|
|
14614
14955
|
return c.json({ error: "Citation not found" }, 404);
|
|
14615
14956
|
}
|
|
@@ -14628,7 +14969,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
14628
14969
|
}
|
|
14629
14970
|
});
|
|
14630
14971
|
} catch (error) {
|
|
14631
|
-
return c
|
|
14972
|
+
return jsonError(c, error);
|
|
14632
14973
|
} finally {
|
|
14633
14974
|
await memoryService.shutdown();
|
|
14634
14975
|
}
|
|
@@ -14639,15 +14980,11 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
14639
14980
|
const memoryService = getServiceFromQuery(c);
|
|
14640
14981
|
try {
|
|
14641
14982
|
await memoryService.initialize();
|
|
14642
|
-
const
|
|
14643
|
-
const event = recentEvents.find((e) => {
|
|
14644
|
-
const eventCitationId = generateCitationId(e.id);
|
|
14645
|
-
return eventCitationId === citationId;
|
|
14646
|
-
});
|
|
14983
|
+
const event = await memoryService.getEventByCitationId(citationId);
|
|
14647
14984
|
if (!event) {
|
|
14648
14985
|
return c.json({ error: "Citation not found" }, 404);
|
|
14649
14986
|
}
|
|
14650
|
-
const sessionEvents =
|
|
14987
|
+
const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
14651
14988
|
const eventIndex = sessionEvents.findIndex((e) => e.id === event.id);
|
|
14652
14989
|
const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
|
|
14653
14990
|
const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
|
|
@@ -14666,7 +15003,7 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
14666
15003
|
} : null
|
|
14667
15004
|
});
|
|
14668
15005
|
} catch (error) {
|
|
14669
|
-
return c
|
|
15006
|
+
return jsonError(c, error);
|
|
14670
15007
|
} finally {
|
|
14671
15008
|
await memoryService.shutdown();
|
|
14672
15009
|
}
|
|
@@ -14709,7 +15046,7 @@ turnsRouter.get("/", async (c) => {
|
|
|
14709
15046
|
hasMore: offset + limit < totalTurns
|
|
14710
15047
|
});
|
|
14711
15048
|
} catch (error) {
|
|
14712
|
-
return c
|
|
15049
|
+
return jsonError(c, error);
|
|
14713
15050
|
} finally {
|
|
14714
15051
|
await memoryService.shutdown();
|
|
14715
15052
|
}
|
|
@@ -14760,7 +15097,7 @@ turnsRouter.get("/:turnId", async (c) => {
|
|
|
14760
15097
|
totalEvents: events.length
|
|
14761
15098
|
});
|
|
14762
15099
|
} catch (error) {
|
|
14763
|
-
return c
|
|
15100
|
+
return jsonError(c, error);
|
|
14764
15101
|
} finally {
|
|
14765
15102
|
await memoryService.shutdown();
|
|
14766
15103
|
}
|
|
@@ -14787,8 +15124,8 @@ turnsRouter.post("/backfill", async (c) => {
|
|
|
14787
15124
|
|
|
14788
15125
|
// src/apps/server/api/projects.ts
|
|
14789
15126
|
import { Hono as Hono7 } from "hono";
|
|
14790
|
-
import * as
|
|
14791
|
-
import * as
|
|
15127
|
+
import * as fs11 from "fs";
|
|
15128
|
+
import * as path13 from "path";
|
|
14792
15129
|
import * as os7 from "os";
|
|
14793
15130
|
var projectsRouter = new Hono7();
|
|
14794
15131
|
projectsRouter.get("/:hash/detail", async (c) => {
|
|
@@ -14817,7 +15154,7 @@ projectsRouter.get("/:hash/detail", async (c) => {
|
|
|
14817
15154
|
return c.json({
|
|
14818
15155
|
project: {
|
|
14819
15156
|
hash,
|
|
14820
|
-
projectName: projectPath ?
|
|
15157
|
+
projectName: projectPath ? path13.basename(projectPath) : `unknown (${hash})`,
|
|
14821
15158
|
registered: Boolean(projectPath)
|
|
14822
15159
|
},
|
|
14823
15160
|
storage: {
|
|
@@ -14845,13 +15182,13 @@ projectsRouter.get("/:hash/detail", async (c) => {
|
|
|
14845
15182
|
});
|
|
14846
15183
|
projectsRouter.get("/", async (c) => {
|
|
14847
15184
|
try {
|
|
14848
|
-
const projectsDir =
|
|
14849
|
-
if (!
|
|
15185
|
+
const projectsDir = path13.join(os7.homedir(), ".claude-code", "memory", "projects");
|
|
15186
|
+
if (!fs11.existsSync(projectsDir)) {
|
|
14850
15187
|
return c.json({ projects: [] });
|
|
14851
15188
|
}
|
|
14852
|
-
const projectHashes =
|
|
14853
|
-
const fullPath =
|
|
14854
|
-
return
|
|
15189
|
+
const projectHashes = fs11.readdirSync(projectsDir).filter((name) => {
|
|
15190
|
+
const fullPath = path13.join(projectsDir, name);
|
|
15191
|
+
return fs11.statSync(fullPath).isDirectory();
|
|
14855
15192
|
});
|
|
14856
15193
|
const registry = loadSessionRegistry();
|
|
14857
15194
|
const hashToPath = /* @__PURE__ */ new Map();
|
|
@@ -14861,17 +15198,17 @@ projectsRouter.get("/", async (c) => {
|
|
|
14861
15198
|
}
|
|
14862
15199
|
}
|
|
14863
15200
|
const projects = projectHashes.map((hash) => {
|
|
14864
|
-
const dirPath =
|
|
14865
|
-
const dbPath =
|
|
15201
|
+
const dirPath = path13.join(projectsDir, hash);
|
|
15202
|
+
const dbPath = path13.join(dirPath, "events.sqlite");
|
|
14866
15203
|
let dbSize = 0;
|
|
14867
|
-
if (
|
|
14868
|
-
dbSize =
|
|
15204
|
+
if (fs11.existsSync(dbPath)) {
|
|
15205
|
+
dbSize = fs11.statSync(dbPath).size;
|
|
14869
15206
|
}
|
|
14870
15207
|
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
14871
15208
|
return {
|
|
14872
15209
|
hash,
|
|
14873
15210
|
projectPath,
|
|
14874
|
-
projectName:
|
|
15211
|
+
projectName: path13.basename(projectPath),
|
|
14875
15212
|
dbSize,
|
|
14876
15213
|
dbSizeHuman: formatBytes(dbSize)
|
|
14877
15214
|
};
|
|
@@ -15000,7 +15337,7 @@ chatRouter.post("/", async (c) => {
|
|
|
15000
15337
|
}
|
|
15001
15338
|
});
|
|
15002
15339
|
} catch (error) {
|
|
15003
|
-
return c
|
|
15340
|
+
return jsonError(c, error);
|
|
15004
15341
|
} finally {
|
|
15005
15342
|
await memoryService.shutdown();
|
|
15006
15343
|
}
|