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/dist/cli/index.js CHANGED
@@ -9,8 +9,8 @@ const __dirname = dirname(__filename);
9
9
  // src/apps/cli/index.ts
10
10
  import { Command } from "commander";
11
11
  import { exec } from "child_process";
12
- import * as fs18 from "fs";
13
- import * as path24 from "path";
12
+ import * as fs19 from "fs";
13
+ import * as path25 from "path";
14
14
  import * as os13 from "os";
15
15
 
16
16
  // src/services/memory-service.ts
@@ -165,7 +165,13 @@ function toDate(value) {
165
165
  return new Date(String(value));
166
166
  }
167
167
  function createDatabase(dbPath, options) {
168
- return new BetterSqlite3(dbPath, { readonly: options?.readOnly });
168
+ const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
169
+ db.pragma("busy_timeout = 5000");
170
+ if (!options?.readOnly) {
171
+ db.pragma("journal_mode = WAL");
172
+ db.pragma("synchronous = NORMAL");
173
+ }
174
+ return db;
169
175
  }
170
176
  function dbRun(db, sql, params = []) {
171
177
  db.prepare(sql).run(...params);
@@ -398,19 +404,29 @@ var ConsolidatedStore = class {
398
404
  */
399
405
  async create(input) {
400
406
  const memoryId = randomUUID2();
401
- await dbRun(
402
- this.db,
407
+ const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
408
+ const insertMemory = this.db.prepare(
403
409
  `INSERT INTO consolidated_memories
404
410
  (memory_id, summary, topics, source_events, confidence, created_at)
405
- VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
406
- [
411
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
412
+ );
413
+ const insertEvent = this.db.prepare(
414
+ `INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
415
+ );
416
+ this.db.transaction(() => {
417
+ insertMemory.run(
407
418
  memoryId,
408
419
  input.summary,
409
420
  JSON.stringify(input.topics),
410
- JSON.stringify(input.sourceEvents),
421
+ JSON.stringify(sourceEvents),
411
422
  input.confidence
412
- ]
413
- );
423
+ );
424
+ for (const eventId of sourceEvents) {
425
+ if (typeof eventId === "string" && eventId.length > 0) {
426
+ insertEvent.run(memoryId, eventId);
427
+ }
428
+ }
429
+ })();
414
430
  return memoryId;
415
431
  }
416
432
  /**
@@ -516,11 +532,12 @@ var ConsolidatedStore = class {
516
532
  * Delete a consolidated memory
517
533
  */
518
534
  async delete(memoryId) {
519
- await dbRun(
520
- this.db,
521
- `DELETE FROM consolidated_memories WHERE memory_id = ?`,
522
- [memoryId]
523
- );
535
+ const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
536
+ const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
537
+ this.db.transaction(() => {
538
+ deleteEvents.run(memoryId);
539
+ deleteMemory.run(memoryId);
540
+ })();
524
541
  }
525
542
  /**
526
543
  * Create a long-term rule promoted from stable summaries
@@ -633,16 +650,16 @@ var ConsolidatedStore = class {
633
650
  * Check if source events are already consolidated
634
651
  */
635
652
  async isAlreadyConsolidated(eventIds) {
636
- for (const eventId of eventIds) {
637
- const result = await dbAll(
638
- this.db,
639
- `SELECT COUNT(*) as count FROM consolidated_memories
640
- WHERE source_events LIKE ?`,
641
- [`%"${eventId}"%`]
642
- );
643
- if ((result[0]?.count || 0) > 0) return true;
644
- }
645
- return false;
653
+ if (eventIds.length === 0) return false;
654
+ const placeholders2 = eventIds.map(() => "?").join(",");
655
+ const rows = await dbAll(
656
+ this.db,
657
+ `SELECT memory_id FROM consolidated_memory_events
658
+ WHERE event_id IN (${placeholders2})
659
+ LIMIT 1`,
660
+ eventIds
661
+ );
662
+ return rows.length > 0;
646
663
  }
647
664
  /**
648
665
  * Get the last consolidation time
@@ -2178,6 +2195,23 @@ function makeDedupeKey(content, sessionId) {
2178
2195
  return `${sessionId}:${contentHash}`;
2179
2196
  }
2180
2197
 
2198
+ // src/core/citation-generator.ts
2199
+ import { createHash as createHash2 } from "crypto";
2200
+ var ID_LENGTH = 6;
2201
+ var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2202
+ function generateCitationId(eventId) {
2203
+ const hash = createHash2("sha256").update(eventId).digest();
2204
+ let id = "";
2205
+ for (let i = 0; i < ID_LENGTH; i++) {
2206
+ id += CHARSET[hash[i] % CHARSET.length];
2207
+ }
2208
+ return id;
2209
+ }
2210
+ function parseCitationId(formatted) {
2211
+ const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
2212
+ return match ? match[1] : null;
2213
+ }
2214
+
2181
2215
  // src/core/sqlite-event-store.ts
2182
2216
  import * as nodePath2 from "path";
2183
2217
 
@@ -2211,12 +2245,12 @@ function resolveProjectStoragePath(projectOrHash) {
2211
2245
  import Database from "better-sqlite3";
2212
2246
  import * as fs4 from "fs";
2213
2247
  import * as nodePath from "path";
2214
- function createSQLiteDatabase(path25, options) {
2215
- const dir = nodePath.dirname(path25);
2248
+ function createSQLiteDatabase(path26, options) {
2249
+ const dir = nodePath.dirname(path26);
2216
2250
  if (!fs4.existsSync(dir)) {
2217
2251
  fs4.mkdirSync(dir, { recursive: true });
2218
2252
  }
2219
- const db = new Database(path25, {
2253
+ const db = new Database(path26, {
2220
2254
  readonly: options?.readonly ?? false
2221
2255
  });
2222
2256
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -2226,17 +2260,28 @@ function createSQLiteDatabase(path25, options) {
2226
2260
  }
2227
2261
  return db;
2228
2262
  }
2263
+ var statementCache = /* @__PURE__ */ new WeakMap();
2264
+ function prepareCached(db, sql) {
2265
+ let cache = statementCache.get(db);
2266
+ if (!cache) {
2267
+ cache = /* @__PURE__ */ new Map();
2268
+ statementCache.set(db, cache);
2269
+ }
2270
+ let stmt = cache.get(sql);
2271
+ if (!stmt) {
2272
+ stmt = db.prepare(sql);
2273
+ cache.set(sql, stmt);
2274
+ }
2275
+ return stmt;
2276
+ }
2229
2277
  function sqliteRun(db, sql, params = []) {
2230
- const stmt = db.prepare(sql);
2231
- return stmt.run(...params);
2278
+ return prepareCached(db, sql).run(...params);
2232
2279
  }
2233
2280
  function sqliteAll(db, sql, params = []) {
2234
- const stmt = db.prepare(sql);
2235
- return stmt.all(...params);
2281
+ return prepareCached(db, sql).all(...params);
2236
2282
  }
2237
2283
  function sqliteGet(db, sql, params = []) {
2238
- const stmt = db.prepare(sql);
2239
- return stmt.get(...params);
2284
+ return prepareCached(db, sql).get(...params);
2240
2285
  }
2241
2286
  function sqliteExec(db, sql) {
2242
2287
  db.exec(sql);
@@ -2658,6 +2703,7 @@ function normalizeQueryRewriteKind(value) {
2658
2703
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2659
2704
  var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2660
2705
  var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2706
+ var SQLITE_SCHEMA_VERSION = 1;
2661
2707
  function emptyOutboxRecoveryResult() {
2662
2708
  return {
2663
2709
  embedding: { recoveredProcessing: 0, retriedFailed: 0 },
@@ -2667,17 +2713,17 @@ function emptyOutboxRecoveryResult() {
2667
2713
  function isRecord(value) {
2668
2714
  return typeof value === "object" && value !== null && !Array.isArray(value);
2669
2715
  }
2670
- function getNestedRecord(root, path25) {
2716
+ function getNestedRecord(root, path26) {
2671
2717
  let cursor = root;
2672
- for (const key of path25) {
2718
+ for (const key of path26) {
2673
2719
  if (!isRecord(cursor)) return void 0;
2674
2720
  cursor = cursor[key];
2675
2721
  }
2676
2722
  return isRecord(cursor) ? cursor : void 0;
2677
2723
  }
2678
- function getNestedString(root, path25) {
2724
+ function getNestedString(root, path26) {
2679
2725
  let cursor = root;
2680
- for (const key of path25) {
2726
+ for (const key of path26) {
2681
2727
  if (!isRecord(cursor)) return void 0;
2682
2728
  cursor = cursor[key];
2683
2729
  }
@@ -3002,6 +3048,15 @@ var SQLiteEventStore = class {
3002
3048
  access_count INTEGER DEFAULT 0
3003
3049
  );
3004
3050
 
3051
+ -- Junction: consolidated memory -> source event id (indexed lookup that
3052
+ -- replaces per-event source_events LIKE scans during consolidation).
3053
+ CREATE TABLE IF NOT EXISTS consolidated_memory_events (
3054
+ memory_id TEXT NOT NULL,
3055
+ event_id TEXT NOT NULL,
3056
+ PRIMARY KEY (memory_id, event_id)
3057
+ );
3058
+ CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
3059
+
3005
3060
  -- Continuity Log table (tracks context transitions)
3006
3061
  CREATE TABLE IF NOT EXISTS continuity_log (
3007
3062
  log_id TEXT PRIMARY KEY,
@@ -3361,11 +3416,17 @@ var SQLiteEventStore = class {
3361
3416
  DELETE FROM events_fts WHERE rowid = OLD.rowid;
3362
3417
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
3363
3418
  END;
3419
+
3420
+ -- Reverse index for citation ids (a non-reversible hash of event id), so
3421
+ -- citation lookups are O(1) instead of scanning recent events and hashing
3422
+ -- each one. Populated lazily/self-healing by getEventByCitationId.
3423
+ CREATE TABLE IF NOT EXISTS event_citations (
3424
+ event_id TEXT PRIMARY KEY,
3425
+ citation_id TEXT NOT NULL
3426
+ );
3427
+ CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
3364
3428
  `);
3365
- try {
3366
- sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
3367
- } catch {
3368
- }
3429
+ this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
3369
3430
  try {
3370
3431
  const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
3371
3432
  const hasSourceAwareUnique = edgeIndexes.some((index) => {
@@ -3402,55 +3463,17 @@ var SQLiteEventStore = class {
3402
3463
  }
3403
3464
  } catch {
3404
3465
  }
3405
- try {
3406
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
3407
- } catch {
3408
- }
3409
- try {
3410
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
3411
- } catch {
3412
- }
3413
- try {
3414
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
3415
- } catch {
3416
- }
3417
- try {
3418
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
3419
- } catch {
3420
- }
3466
+ this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
3467
+ this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
3468
+ this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
3469
+ this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
3421
3470
  try {
3422
3471
  sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
3423
3472
  } catch {
3424
3473
  }
3425
- const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
3426
- const columnNames = tableInfo.map((col) => col.name);
3427
- if (!columnNames.includes("access_count")) {
3428
- try {
3429
- sqliteExec(this.db, `
3430
- ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
3431
- `);
3432
- } catch (err) {
3433
- console.error("Error adding access_count column:", err);
3434
- }
3435
- }
3436
- if (!columnNames.includes("last_accessed_at")) {
3437
- try {
3438
- sqliteExec(this.db, `
3439
- ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
3440
- `);
3441
- } catch (err) {
3442
- console.error("Error adding last_accessed_at column:", err);
3443
- }
3444
- }
3445
- if (!columnNames.includes("turn_id")) {
3446
- try {
3447
- sqliteExec(this.db, `
3448
- ALTER TABLE events ADD COLUMN turn_id TEXT;
3449
- `);
3450
- } catch (err) {
3451
- console.error("Error adding turn_id column:", err);
3452
- }
3453
- }
3474
+ this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
3475
+ this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
3476
+ this.addColumnIfMissing("events", "turn_id", "TEXT");
3454
3477
  try {
3455
3478
  sqliteExec(this.db, `
3456
3479
  CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
@@ -3467,8 +3490,18 @@ var SQLiteEventStore = class {
3467
3490
  sqliteExec(this.db, `
3468
3491
  CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
3469
3492
  `);
3470
- } catch (err) {
3493
+ } catch {
3494
+ }
3495
+ const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
3496
+ if ((cmeCount?.c ?? 0) === 0) {
3497
+ sqliteExec(this.db, `
3498
+ INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
3499
+ SELECT cm.memory_id, je.value
3500
+ FROM consolidated_memories cm, json_each(cm.source_events) je
3501
+ WHERE json_valid(cm.source_events);
3502
+ `);
3471
3503
  }
3504
+ sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
3472
3505
  this.initialized = true;
3473
3506
  }
3474
3507
  /**
@@ -3600,6 +3633,67 @@ var SQLiteEventStore = class {
3600
3633
  );
3601
3634
  return rows.map(this.rowToEvent);
3602
3635
  }
3636
+ /**
3637
+ * Aggregate event counts by type via SQL GROUP BY, instead of loading rows
3638
+ * into memory and counting in JS. Counts all events (the old recent-window
3639
+ * scan also under-counted stores larger than its cap).
3640
+ */
3641
+ async getEventTypeCounts(options) {
3642
+ await this.initialize();
3643
+ const rows = sqliteAll(
3644
+ this.db,
3645
+ `SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
3646
+ []
3647
+ );
3648
+ return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
3649
+ }
3650
+ /**
3651
+ * Fetch every event at/after an ISO timestamp (ascending), with no row cap.
3652
+ * For window-scoped analytics (KPI/usefulness) this fetches exactly the events
3653
+ * the window needs instead of an arbitrary "most recent N" slice that both
3654
+ * over-fetches sparse windows and truncates very active ones.
3655
+ */
3656
+ async getEventsAfter(sinceIso, options) {
3657
+ await this.initialize();
3658
+ const rows = sqliteAll(
3659
+ this.db,
3660
+ `SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3661
+ [sinceIso]
3662
+ );
3663
+ return rows.map((row) => this.rowToEvent(row));
3664
+ }
3665
+ /** Count distinct sessions via SQL instead of materializing a Set in JS. */
3666
+ async getDistinctSessionCount(options) {
3667
+ await this.initialize();
3668
+ const row = sqliteGet(
3669
+ this.db,
3670
+ `SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
3671
+ []
3672
+ );
3673
+ return row?.count ?? 0;
3674
+ }
3675
+ /**
3676
+ * Per-day event counts (with type breakdown) since an ISO timestamp, computed
3677
+ * by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
3678
+ * strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
3679
+ * lexicographically.
3680
+ */
3681
+ async getDailyEventCounts(sinceIso, options) {
3682
+ await this.initialize();
3683
+ return sqliteAll(
3684
+ this.db,
3685
+ `SELECT substr(timestamp, 1, 10) as day,
3686
+ COUNT(*) as total,
3687
+ SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
3688
+ SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
3689
+ SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
3690
+ FROM events
3691
+ WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
3692
+ GROUP BY day
3693
+ ORDER BY day ASC`,
3694
+ [sinceIso]
3695
+ );
3696
+ }
3603
3697
  /**
3604
3698
  * Get event by ID
3605
3699
  */
@@ -3613,6 +3707,71 @@ var SQLiteEventStore = class {
3613
3707
  if (!row) return null;
3614
3708
  return this.rowToEvent(row);
3615
3709
  }
3710
+ /**
3711
+ * Batch-fetch events by id in a single query, applying the same quarantine
3712
+ * predicate as getEvent. Lets hot paths replace N sequential round-trips with
3713
+ * one. Missing ids are simply absent from the result (order not guaranteed).
3714
+ */
3715
+ async getEvents(ids, options) {
3716
+ await this.initialize();
3717
+ if (ids.length === 0) return [];
3718
+ const placeholders2 = ids.map(() => "?").join(",");
3719
+ const rows = sqliteAll(
3720
+ this.db,
3721
+ `SELECT * FROM events WHERE id IN (${placeholders2}) AND ${maybeQuarantinePredicate(options)}`,
3722
+ ids
3723
+ );
3724
+ return rows.map((row) => this.rowToEvent(row));
3725
+ }
3726
+ /**
3727
+ * Resolve an event from its citation id via the reverse index, instead of
3728
+ * scanning recent events and hashing each one. The index is self-healing: a
3729
+ * miss triggers a one-time backfill of any events not yet indexed (covering
3730
+ * freshly appended events) before retrying.
3731
+ */
3732
+ async getEventByCitationId(citationId, options) {
3733
+ await this.initialize();
3734
+ if (!citationId) return null;
3735
+ let row = sqliteGet(
3736
+ this.db,
3737
+ `SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
3738
+ [citationId]
3739
+ );
3740
+ if (!row) {
3741
+ this.indexMissingCitations();
3742
+ row = sqliteGet(
3743
+ this.db,
3744
+ `SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
3745
+ [citationId]
3746
+ );
3747
+ }
3748
+ if (!row) return null;
3749
+ return this.getEvent(row.event_id, options);
3750
+ }
3751
+ /**
3752
+ * Backfill citation ids for any events not yet present in event_citations.
3753
+ * Gated on a cheap count comparison so the common (fully-indexed) path does no
3754
+ * work, and the scan only runs when there are genuinely new events to index.
3755
+ */
3756
+ indexMissingCitations() {
3757
+ const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
3758
+ const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
3759
+ if (indexedCount >= eventCount) return;
3760
+ const unindexed = sqliteAll(
3761
+ this.db,
3762
+ `SELECT e.id FROM events e
3763
+ LEFT JOIN event_citations ec ON ec.event_id = e.id
3764
+ WHERE ec.event_id IS NULL`,
3765
+ []
3766
+ );
3767
+ if (unindexed.length === 0) return;
3768
+ const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
3769
+ this.db.transaction(() => {
3770
+ for (const { id } of unindexed) {
3771
+ insert.run(id, generateCitationId(id));
3772
+ }
3773
+ })();
3774
+ }
3616
3775
  /**
3617
3776
  * Get events since a timestamp (for sync)
3618
3777
  */
@@ -3814,25 +3973,22 @@ var SQLiteEventStore = class {
3814
3973
  */
3815
3974
  async getPendingOutboxItems(limit = 32) {
3816
3975
  await this.initialize();
3817
- const pending = sqliteAll(
3818
- this.db,
3819
- `SELECT * FROM embedding_outbox
3820
- WHERE status = 'pending'
3821
- ORDER BY created_at
3822
- LIMIT ?`,
3823
- [limit]
3824
- );
3825
- if (pending.length === 0) return [];
3826
- const ids = pending.map((r) => r.id);
3827
- const placeholders2 = ids.map(() => "?").join(",");
3828
- sqliteRun(
3976
+ const claimed = sqliteAll(
3829
3977
  this.db,
3830
3978
  `UPDATE embedding_outbox
3831
3979
  SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3832
- WHERE id IN (${placeholders2})`,
3833
- ids
3980
+ WHERE id IN (
3981
+ SELECT id FROM embedding_outbox
3982
+ WHERE status = 'pending'
3983
+ ORDER BY created_at
3984
+ LIMIT ?
3985
+ )
3986
+ RETURNING *`,
3987
+ [limit]
3834
3988
  );
3835
- return pending.map((row) => ({
3989
+ if (claimed.length === 0) return [];
3990
+ claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
3991
+ return claimed.map((row) => ({
3836
3992
  id: row.id,
3837
3993
  eventId: row.event_id,
3838
3994
  content: row.content,
@@ -4436,7 +4592,7 @@ var SQLiteEventStore = class {
4436
4592
  const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
4437
4593
  const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
4438
4594
  let toolSuccessCount = 0;
4439
- let toolTotalCount = toolEvents.length;
4595
+ const toolTotalCount = toolEvents.length;
4440
4596
  for (const t of toolEvents) {
4441
4597
  try {
4442
4598
  const content = JSON.parse(t.content);
@@ -4599,10 +4755,14 @@ var SQLiteEventStore = class {
4599
4755
  * Rebuild FTS index from existing events
4600
4756
  * Call this once after upgrading to FTS5
4601
4757
  */
4602
- async rebuildFtsIndex() {
4603
- await this.initialize();
4604
- const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
4605
- const totalEvents = countRow?.count ?? 0;
4758
+ /**
4759
+ * Drop and recreate the events_fts virtual table + sync triggers, repopulating
4760
+ * from the current events table. Recreating the table (instead of issuing
4761
+ * DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
4762
+ * with `no such column: T.event_id` on synthetic deletes. Safe to call inside
4763
+ * a transaction (pure DDL + INSERT...SELECT, no transaction control).
4764
+ */
4765
+ recreateEventsFtsTable() {
4606
4766
  sqliteExec(this.db, `
4607
4767
  DROP TRIGGER IF EXISTS events_fts_insert;
4608
4768
  DROP TRIGGER IF EXISTS events_fts_delete;
@@ -4631,6 +4791,12 @@ var SQLiteEventStore = class {
4631
4791
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
4632
4792
  END;
4633
4793
  `);
4794
+ }
4795
+ async rebuildFtsIndex() {
4796
+ await this.initialize();
4797
+ const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
4798
+ const totalEvents = countRow?.count ?? 0;
4799
+ this.recreateEventsFtsTable();
4634
4800
  return totalEvents;
4635
4801
  }
4636
4802
  /**
@@ -4648,6 +4814,35 @@ var SQLiteEventStore = class {
4648
4814
  return false;
4649
4815
  }
4650
4816
  }
4817
+ hasTable(tableName) {
4818
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
4819
+ try {
4820
+ const rows = sqliteAll(
4821
+ this.db,
4822
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
4823
+ [tableName]
4824
+ );
4825
+ return rows.length > 0;
4826
+ } catch {
4827
+ return false;
4828
+ }
4829
+ }
4830
+ /**
4831
+ * Add a column only when its table exists and the column is missing.
4832
+ *
4833
+ * Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
4834
+ * migrated" / "table not present yet" (both safely skipped) from a genuine
4835
+ * failure (disk full, corruption), which is allowed to surface instead of
4836
+ * being silently swallowed as if the migration had succeeded.
4837
+ */
4838
+ addColumnIfMissing(table, column, definition) {
4839
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
4840
+ throw new Error(`Invalid identifier in migration: ${table}.${column}`);
4841
+ }
4842
+ if (!this.hasTable(table)) return;
4843
+ if (this.hasTableColumn(table, column)) return;
4844
+ sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
4845
+ }
4651
4846
  async recordRetrievalTrace(input) {
4652
4847
  await this.initialize();
4653
4848
  const traceId = randomUUID6();
@@ -4733,6 +4928,45 @@ var SQLiteEventStore = class {
4733
4928
  FROM retrieval_traces`,
4734
4929
  []
4735
4930
  );
4931
+ const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
4932
+ const strategyRows = sqliteAll(
4933
+ this.db,
4934
+ `SELECT
4935
+ ${strategyColumnSql} as strategy,
4936
+ COUNT(*) as total_queries,
4937
+ SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
4938
+ SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
4939
+ SUM(candidate_count) as total_candidate_count,
4940
+ SUM(selected_count) as total_selected_count,
4941
+ AVG(candidate_count) as avg_candidate_count,
4942
+ AVG(selected_count) as avg_selected_count,
4943
+ CASE
4944
+ WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
4945
+ ELSE 0
4946
+ END as selection_rate
4947
+ FROM retrieval_traces
4948
+ GROUP BY ${strategyColumnSql}
4949
+ ORDER BY total_queries DESC, strategy ASC`,
4950
+ []
4951
+ );
4952
+ const strategyBreakdown = strategyRows.map((strategyRow) => {
4953
+ const strategyTotalQueries = Number(strategyRow.total_queries || 0);
4954
+ const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
4955
+ const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
4956
+ return {
4957
+ strategy: String(strategyRow.strategy || "unknown"),
4958
+ totalQueries: strategyTotalQueries,
4959
+ queriesWithSelection: strategyQueriesWithSelection,
4960
+ rewrittenQueries: strategyRewrittenQueries,
4961
+ rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
4962
+ totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
4963
+ totalSelectedCount: Number(strategyRow.total_selected_count || 0),
4964
+ avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
4965
+ avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
4966
+ selectionRate: Number(strategyRow.selection_rate || 0),
4967
+ queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
4968
+ };
4969
+ });
4736
4970
  const totalQueries = Number(row?.total_queries || 0);
4737
4971
  const rewrittenQueries = Number(row?.rewritten_queries || 0);
4738
4972
  const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
@@ -4750,7 +4984,8 @@ var SQLiteEventStore = class {
4750
4984
  rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
4751
4985
  rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
4752
4986
  avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
4753
- avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
4987
+ avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
4988
+ strategyBreakdown
4754
4989
  };
4755
4990
  } catch (err) {
4756
4991
  if (err?.message?.includes("no such table")) {
@@ -4766,7 +5001,8 @@ var SQLiteEventStore = class {
4766
5001
  rewrittenSelectionRate: 0,
4767
5002
  rawSelectionRate: 0,
4768
5003
  avgSelectedCountForRewrittenQueries: 0,
4769
- avgSelectedCountForRawQueries: 0
5004
+ avgSelectedCountForRawQueries: 0,
5005
+ strategyBreakdown: []
4770
5006
  };
4771
5007
  }
4772
5008
  throw err;
@@ -4883,38 +5119,19 @@ var SQLiteEventStore = class {
4883
5119
  if (events.length === 0) return 0;
4884
5120
  const eventIds = events.map((e) => e.id);
4885
5121
  const placeholders2 = eventIds.map(() => "?").join(",");
4886
- const ftsTriggersDropped = [];
4887
- for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
4888
- try {
5122
+ const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
5123
+ const runDelete = this.db.transaction(() => {
5124
+ for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
4889
5125
  sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
4890
- ftsTriggersDropped.push(triggerName);
4891
- } catch {
4892
5126
  }
4893
- }
4894
- for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
4895
- try {
5127
+ for (const table of relatedTables) {
4896
5128
  sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders2})`, eventIds);
4897
- } catch {
4898
5129
  }
4899
- }
4900
- const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
4901
- if (ftsTriggersDropped.length > 0) {
4902
- try {
4903
- sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
4904
- sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
4905
- INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
4906
- END`);
4907
- sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
4908
- DELETE FROM events_fts WHERE rowid = OLD.rowid;
4909
- END`);
4910
- sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
4911
- DELETE FROM events_fts WHERE rowid = OLD.rowid;
4912
- INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
4913
- END`);
4914
- } catch {
4915
- }
4916
- }
4917
- return result.changes || 0;
5130
+ const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
5131
+ this.recreateEventsFtsTable();
5132
+ return result.changes || 0;
5133
+ });
5134
+ return runDelete();
4918
5135
  }
4919
5136
  /**
4920
5137
  * Convert database row to MemoryEvent
@@ -7678,9 +7895,9 @@ function safeParseObject(value) {
7678
7895
  return void 0;
7679
7896
  }
7680
7897
  }
7681
- function nestedString(root, path25) {
7898
+ function nestedString(root, path26) {
7682
7899
  let cursor = root;
7683
- for (const key of path25) {
7900
+ for (const key of path26) {
7684
7901
  if (!isRecord2(cursor)) return void 0;
7685
7902
  cursor = cursor[key];
7686
7903
  }
@@ -7755,12 +7972,12 @@ var GraphPathService = class {
7755
7972
  }
7756
7973
  }
7757
7974
  }
7758
- const paths = Array.from(bestByTarget.entries()).map(([key, path25]) => ({
7975
+ const paths = Array.from(bestByTarget.entries()).map(([key, path26]) => ({
7759
7976
  target: graph.node(nodeFromKey(key)),
7760
- hops: path25.hops,
7761
- totalCost: path25.totalCost,
7762
- scoreContribution: path25.totalCost > 0 ? 1 / path25.totalCost : 0,
7763
- steps: path25.steps
7977
+ hops: path26.hops,
7978
+ totalCost: path26.totalCost,
7979
+ scoreContribution: path26.totalCost > 0 ? 1 / path26.totalCost : 0,
7980
+ steps: path26.steps
7764
7981
  })).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
7765
7982
  return { startNodes, effectiveMaxHops, paths };
7766
7983
  }
@@ -8537,7 +8754,7 @@ var SessionActorRepository = class {
8537
8754
  };
8538
8755
 
8539
8756
  // src/core/operations/perspective-observation-repository.ts
8540
- import { createHash as createHash3, randomUUID as randomUUID11 } from "crypto";
8757
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
8541
8758
  function projectHashToStorage4(projectHash) {
8542
8759
  return projectHash ?? "";
8543
8760
  }
@@ -8574,7 +8791,7 @@ function sanitizeStoredRecord(value) {
8574
8791
  return sanitizeGovernanceAuditValue(value);
8575
8792
  }
8576
8793
  function stableHash(value) {
8577
- return createHash3("sha256").update(JSON.stringify(value)).digest("hex");
8794
+ return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
8578
8795
  }
8579
8796
  function evidenceHash(sourceEventIds, sourceObservationIds) {
8580
8797
  return stableHash({
@@ -9082,23 +9299,6 @@ function createPerspectiveDeriver(options) {
9082
9299
  return new PerspectiveDeriver(options);
9083
9300
  }
9084
9301
 
9085
- // src/core/citation-generator.ts
9086
- import { createHash as createHash4 } from "crypto";
9087
- var ID_LENGTH = 6;
9088
- var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
9089
- function generateCitationId(eventId) {
9090
- const hash = createHash4("sha256").update(eventId).digest();
9091
- let id = "";
9092
- for (let i = 0; i < ID_LENGTH; i++) {
9093
- id += CHARSET[hash[i] % CHARSET.length];
9094
- }
9095
- return id;
9096
- }
9097
- function parseCitationId(formatted) {
9098
- const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
9099
- return match ? match[1] : null;
9100
- }
9101
-
9102
9302
  // src/core/operations/perspective-session-actor-backfill.ts
9103
9303
  var DEFAULT_EVENT_LIMIT = 1e4;
9104
9304
  var DEFAULT_SAMPLE_LIMIT2 = 10;
@@ -9892,6 +10092,30 @@ var MemoryQueryService = class {
9892
10092
  score: 1 - (r.rank - minRank) / rankRange
9893
10093
  })).filter((r) => !options?.minScore || r.score >= options.minScore);
9894
10094
  }
10095
+ async getEvent(id) {
10096
+ await this.initialize();
10097
+ return this.queryStore.getEvent(id);
10098
+ }
10099
+ async getEventByCitationId(citationId) {
10100
+ await this.initialize();
10101
+ return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
10102
+ }
10103
+ async getEventsAfter(sinceIso) {
10104
+ await this.initialize();
10105
+ return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
10106
+ }
10107
+ async getEventTypeCounts() {
10108
+ await this.initialize();
10109
+ return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
10110
+ }
10111
+ async getDistinctSessionCount() {
10112
+ await this.initialize();
10113
+ return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
10114
+ }
10115
+ async getDailyEventCounts(sinceIso) {
10116
+ await this.initialize();
10117
+ return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
10118
+ }
9895
10119
  async getSessionHistory(sessionId) {
9896
10120
  await this.initialize();
9897
10121
  return this.queryStore.getSessionEvents(sessionId);
@@ -10716,12 +10940,12 @@ var Retriever = class {
10716
10940
  direction: "both"
10717
10941
  });
10718
10942
  const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
10719
- for (const path25 of expansion.paths) {
10720
- if (path25.target.type !== "event") continue;
10721
- const target = await this.eventStore.getEvent(path25.target.id);
10943
+ for (const path26 of expansion.paths) {
10944
+ if (path26.target.type !== "event") continue;
10945
+ const target = await this.eventStore.getEvent(path26.target.id);
10722
10946
  if (!target) continue;
10723
- const graphPath = toRetrievalGraphPathDebug(path25, titleByEntityId);
10724
- const score = graphPathScore(path25, opts.hopPenalty);
10947
+ const graphPath = toRetrievalGraphPathDebug(path26, titleByEntityId);
10948
+ const score = graphPathScore(path26, opts.hopPenalty);
10725
10949
  const existing = byId.get(target.id);
10726
10950
  const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
10727
10951
  const graphLane = {
@@ -10730,7 +10954,7 @@ var Retriever = class {
10730
10954
  score
10731
10955
  };
10732
10956
  const row = {
10733
- id: existing?.id ?? `graph-path-${path25.hops}-${target.id}`,
10957
+ id: existing?.id ?? `graph-path-${path26.hops}-${target.id}`,
10734
10958
  eventId: target.id,
10735
10959
  content: target.content,
10736
10960
  score: Math.max(existing?.score ?? 0, score),
@@ -10860,11 +11084,12 @@ var Retriever = class {
10860
11084
  if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
10861
11085
  const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
10862
11086
  const filtered = [];
11087
+ const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
10863
11088
  for (const result of results) {
10864
11089
  if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
10865
11090
  if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
10866
11091
  if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
10867
- const event = await this.eventStore.getEvent(result.eventId);
11092
+ const event = eventMap.get(result.eventId);
10868
11093
  if (!event) continue;
10869
11094
  if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
10870
11095
  if (normalizedIncludes.length > 0) {
@@ -10973,10 +11198,30 @@ var Retriever = class {
10973
11198
  async retrieveRecent(limit = 100) {
10974
11199
  return this.eventStore.getRecentEvents(limit);
10975
11200
  }
11201
+ /**
11202
+ * Batch-load events by id into a Map in a single query, de-duplicating ids.
11203
+ * Replaces per-result getEvent round-trips in the scope/enrich hot paths.
11204
+ */
11205
+ async fetchEventsByIds(ids) {
11206
+ const map = /* @__PURE__ */ new Map();
11207
+ if (ids.length === 0) return map;
11208
+ const unique2 = [...new Set(ids)];
11209
+ if (typeof this.eventStore.getEvents === "function") {
11210
+ const events = await this.eventStore.getEvents(unique2);
11211
+ for (const event of events) map.set(event.id, event);
11212
+ } else {
11213
+ const events = await Promise.all(unique2.map((id) => this.eventStore.getEvent(id)));
11214
+ for (const event of events) {
11215
+ if (event) map.set(event.id, event);
11216
+ }
11217
+ }
11218
+ return map;
11219
+ }
10976
11220
  async enrichResults(results, options, query) {
10977
11221
  const memories = [];
11222
+ const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
10978
11223
  for (const result of results) {
10979
- const event = await this.eventStore.getEvent(result.eventId);
11224
+ const event = eventMap.get(result.eventId);
10980
11225
  if (!event) continue;
10981
11226
  if (this.graduation) {
10982
11227
  this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
@@ -11049,8 +11294,8 @@ _Context:_ ${sessionContext}`;
11049
11294
  }
11050
11295
  matchesMetadataScope(metadata, expected) {
11051
11296
  if (!metadata) return false;
11052
- return Object.entries(expected).every(([path25, value]) => {
11053
- const actual = path25.split(".").reduce((acc, key) => {
11297
+ return Object.entries(expected).every(([path26, value]) => {
11298
+ const actual = path26.split(".").reduce((acc, key) => {
11054
11299
  if (typeof acc !== "object" || acc === null) return void 0;
11055
11300
  return acc[key];
11056
11301
  }, metadata);
@@ -11101,22 +11346,22 @@ function uniqueEntityStartNodes(candidates) {
11101
11346
  }
11102
11347
  return nodes;
11103
11348
  }
11104
- function toRetrievalGraphPathDebug(path25, titleByEntityId) {
11105
- const firstStep = path25.steps[0];
11349
+ function toRetrievalGraphPathDebug(path26, titleByEntityId) {
11350
+ const firstStep = path26.steps[0];
11106
11351
  const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
11107
11352
  const startEntityId = startNode?.type === "entity" ? startNode.id : "";
11108
11353
  return {
11109
11354
  startEntityId,
11110
11355
  startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
11111
- targetId: path25.target.id,
11112
- targetType: path25.target.type,
11113
- hops: path25.hops,
11114
- relationPath: path25.steps.map((step) => step.relationType)
11356
+ targetId: path26.target.id,
11357
+ targetType: path26.target.type,
11358
+ hops: path26.hops,
11359
+ relationPath: path26.steps.map((step) => step.relationType)
11115
11360
  };
11116
11361
  }
11117
- function graphPathScore(path25, hopPenalty) {
11118
- const base = Math.min(0.95, Math.max(0, path25.scoreContribution));
11119
- return Math.max(0.05, base - hopPenalty * Math.max(0, path25.hops - 1));
11362
+ function graphPathScore(path26, hopPenalty) {
11363
+ const base = Math.min(0.95, Math.max(0, path26.scoreContribution));
11364
+ return Math.max(0.05, base - hopPenalty * Math.max(0, path26.hops - 1));
11120
11365
  }
11121
11366
  function clampGraphHops(maxHops) {
11122
11367
  if (!Number.isFinite(maxHops)) return 2;
@@ -11124,14 +11369,14 @@ function clampGraphHops(maxHops) {
11124
11369
  }
11125
11370
  function mergeGraphPaths(existing, incoming) {
11126
11371
  const byKey = /* @__PURE__ */ new Map();
11127
- for (const path25 of [...existing, ...incoming]) {
11128
- const key = [path25.startEntityId, path25.targetType, path25.targetId, path25.hops, ...path25.relationPath].join("\0");
11129
- if (!byKey.has(key)) byKey.set(key, path25);
11372
+ for (const path26 of [...existing, ...incoming]) {
11373
+ const key = [path26.startEntityId, path26.targetType, path26.targetId, path26.hops, ...path26.relationPath].join("\0");
11374
+ if (!byKey.has(key)) byKey.set(key, path26);
11130
11375
  }
11131
11376
  return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
11132
11377
  }
11133
- function graphPathSignature(path25) {
11134
- return [path25.startEntityId, path25.targetType, path25.targetId, path25.hops, ...path25.relationPath].join("|");
11378
+ function graphPathSignature(path26) {
11379
+ return [path26.startEntityId, path26.targetType, path26.targetId, path26.hops, ...path26.relationPath].join("|");
11135
11380
  }
11136
11381
  function compareStable(a, b) {
11137
11382
  if (a < b) return -1;
@@ -13730,19 +13975,47 @@ var MemoryService = class {
13730
13975
  return this.queryService.rebuildFtsIndex();
13731
13976
  }
13732
13977
  /**
13733
- * Get session history
13978
+ * Get a single event by id (indexed lookup).
13734
13979
  */
13735
- async getSessionHistory(sessionId) {
13736
- return this.queryService.getSessionHistory(sessionId);
13980
+ async getEvent(id) {
13981
+ return this.queryService.getEvent(id);
13737
13982
  }
13738
13983
  /**
13739
- * Get recent events
13984
+ * Resolve an event from its citation id via the reverse index.
13740
13985
  */
13741
- async getRecentEvents(limit = 100) {
13742
- return this.queryService.getRecentEvents(limit);
13986
+ async getEventByCitationId(citationId) {
13987
+ return this.queryService.getEventByCitationId(citationId);
13743
13988
  }
13744
- /**
13745
- * Get memory statistics
13989
+ /** All events at/after an ISO timestamp (ascending, uncapped). */
13990
+ async getEventsAfter(sinceIso) {
13991
+ return this.queryService.getEventsAfter(sinceIso);
13992
+ }
13993
+ /** Aggregate event counts by type (SQL GROUP BY). */
13994
+ async getEventTypeCounts() {
13995
+ return this.queryService.getEventTypeCounts();
13996
+ }
13997
+ /** Count distinct sessions (SQL). */
13998
+ async getDistinctSessionCount() {
13999
+ return this.queryService.getDistinctSessionCount();
14000
+ }
14001
+ /** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
14002
+ async getDailyEventCounts(sinceIso) {
14003
+ return this.queryService.getDailyEventCounts(sinceIso);
14004
+ }
14005
+ /**
14006
+ * Get session history
14007
+ */
14008
+ async getSessionHistory(sessionId) {
14009
+ return this.queryService.getSessionHistory(sessionId);
14010
+ }
14011
+ /**
14012
+ * Get recent events
14013
+ */
14014
+ async getRecentEvents(limit = 100) {
14015
+ return this.queryService.getRecentEvents(limit);
14016
+ }
14017
+ /**
14018
+ * Get memory statistics
13746
14019
  */
13747
14020
  async getOutboxStats(options) {
13748
14021
  return this.queryService.getOutboxStats(options);
@@ -14058,6 +14331,20 @@ import * as path11 from "path";
14058
14331
  import * as os6 from "os";
14059
14332
  import * as readline from "readline";
14060
14333
  import { randomUUID as randomUUID14 } from "crypto";
14334
+
14335
+ // src/services/turn-buffering.ts
14336
+ var MIN_SUBSTANTIVE_RESPONSE_LEN = 100;
14337
+ var MAX_AGENT_RESPONSE_LEN = 1e4;
14338
+ function mergeAgentResponseBlocks(blocks) {
14339
+ const substantive = blocks.filter((block) => block.length >= MIN_SUBSTANTIVE_RESPONSE_LEN);
14340
+ if (substantive.length > 0) return substantive.join("\n\n");
14341
+ return blocks.reduce((longest, block) => longest.length >= block.length ? longest : block, "");
14342
+ }
14343
+ function truncateAgentResponse(text) {
14344
+ return text.length > MAX_AGENT_RESPONSE_LEN ? `${text.slice(0, MAX_AGENT_RESPONSE_LEN)}...[truncated]` : text;
14345
+ }
14346
+
14347
+ // src/services/session-history-importer.ts
14061
14348
  function isClaudeLocalCommandArtifact(content) {
14062
14349
  const trimmed = content.trim();
14063
14350
  return /^<local-command-(stdout|stderr)>/.test(trimmed) || /^<command-(name|message)>/.test(trimmed) || trimmed.includes("<command-name>") && trimmed.includes("<local-command-stdout>");
@@ -14189,6 +14476,34 @@ var SessionHistoryImporter = class {
14189
14476
  /**
14190
14477
  * Import a specific session file
14191
14478
  */
14479
+ /**
14480
+ * Pre-flight check used before a destructive force-reimport: returns true only
14481
+ * if the file can be opened and contains at least one line that parses as JSON.
14482
+ * Streams the file and short-circuits on the first valid record.
14483
+ */
14484
+ async hasParseableContent(filePath) {
14485
+ let stream;
14486
+ let rl;
14487
+ try {
14488
+ stream = fs9.createReadStream(filePath);
14489
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
14490
+ for await (const line of rl) {
14491
+ const trimmed = line.trim();
14492
+ if (!trimmed) continue;
14493
+ try {
14494
+ JSON.parse(trimmed);
14495
+ return true;
14496
+ } catch {
14497
+ }
14498
+ }
14499
+ return false;
14500
+ } catch {
14501
+ return false;
14502
+ } finally {
14503
+ rl?.close();
14504
+ stream?.close();
14505
+ }
14506
+ }
14192
14507
  async importSessionFile(filePath, options = {}) {
14193
14508
  const result = {
14194
14509
  totalSessions: 1,
@@ -14204,6 +14519,10 @@ var SessionHistoryImporter = class {
14204
14519
  }
14205
14520
  const sessionId = path11.basename(filePath, ".jsonl");
14206
14521
  if (options.force) {
14522
+ if (!await this.hasParseableContent(filePath)) {
14523
+ result.errors.push(`Skipped force reimport (no parseable records): ${filePath}`);
14524
+ return result;
14525
+ }
14207
14526
  const deleted = await this.memoryService.deleteSessionEvents(sessionId);
14208
14527
  if (options.verbose && deleted > 0) {
14209
14528
  console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
@@ -14225,13 +14544,12 @@ var SessionHistoryImporter = class {
14225
14544
  let lastTimestamp;
14226
14545
  const flushTextBuffer = async () => {
14227
14546
  if (textBuffer.length === 0 || !currentTurnId) return;
14228
- const substantive = textBuffer.filter((t) => t.length >= 100);
14229
- const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
14547
+ const merged = mergeAgentResponseBlocks(textBuffer);
14230
14548
  if (!merged) {
14231
14549
  textBuffer = [];
14232
14550
  return;
14233
14551
  }
14234
- const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
14552
+ const truncated = truncateAgentResponse(merged);
14235
14553
  const appendResult = await this.memoryService.storeAgentResponse(
14236
14554
  sessionId,
14237
14555
  truncated,
@@ -14245,55 +14563,60 @@ var SessionHistoryImporter = class {
14245
14563
  lineCount++;
14246
14564
  textBuffer = [];
14247
14565
  };
14248
- for await (const line of rl) {
14249
- if (lineCount >= limit) break;
14250
- try {
14251
- const entry = JSON.parse(line);
14252
- result.totalMessages++;
14253
- const msgClass = classifyEntry(entry);
14254
- if (msgClass === "user_prompt") {
14255
- await flushTextBuffer();
14256
- const content = this.extractContent(entry);
14257
- if (!content) continue;
14258
- if (!isWorthStoringPrompt(content)) {
14259
- result.skippedDuplicates++;
14260
- continue;
14261
- }
14262
- currentTurnId = randomUUID14();
14263
- const appendResult = await this.memoryService.storeUserPrompt(
14264
- sessionId,
14265
- content,
14266
- { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
14267
- );
14268
- if (appendResult.success && appendResult.isDuplicate) {
14269
- result.skippedDuplicates++;
14270
- } else {
14271
- result.importedPrompts++;
14566
+ try {
14567
+ for await (const line of rl) {
14568
+ if (lineCount >= limit) break;
14569
+ try {
14570
+ const entry = JSON.parse(line);
14571
+ result.totalMessages++;
14572
+ const msgClass = classifyEntry(entry);
14573
+ if (msgClass === "user_prompt") {
14574
+ await flushTextBuffer();
14575
+ const content = this.extractContent(entry);
14576
+ if (!content) continue;
14577
+ if (!isWorthStoringPrompt(content)) {
14578
+ result.skippedDuplicates++;
14579
+ continue;
14580
+ }
14581
+ currentTurnId = randomUUID14();
14582
+ const appendResult = await this.memoryService.storeUserPrompt(
14583
+ sessionId,
14584
+ content,
14585
+ { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
14586
+ );
14587
+ if (appendResult.success && appendResult.isDuplicate) {
14588
+ result.skippedDuplicates++;
14589
+ } else {
14590
+ result.importedPrompts++;
14591
+ }
14592
+ lineCount++;
14593
+ } else if (msgClass === "agent_text") {
14594
+ const content = this.extractContent(entry);
14595
+ if (content) {
14596
+ textBuffer.push(content);
14597
+ lastTimestamp = entry.timestamp;
14598
+ }
14272
14599
  }
14273
- lineCount++;
14274
- } else if (msgClass === "agent_text") {
14275
- const content = this.extractContent(entry);
14276
- if (content) {
14277
- textBuffer.push(content);
14278
- lastTimestamp = entry.timestamp;
14600
+ const now = Date.now();
14601
+ if (now - lastProgressAt > 200) {
14602
+ lastProgressAt = now;
14603
+ onProgress?.({
14604
+ phase: "session-progress",
14605
+ sessionIndex,
14606
+ messagesProcessed: result.totalMessages,
14607
+ imported: result.importedPrompts + result.importedResponses,
14608
+ skipped: result.skippedDuplicates
14609
+ });
14279
14610
  }
14611
+ } catch (parseError) {
14612
+ result.errors.push(`Parse error on line: ${parseError}`);
14280
14613
  }
14281
- const now = Date.now();
14282
- if (now - lastProgressAt > 200) {
14283
- lastProgressAt = now;
14284
- onProgress?.({
14285
- phase: "session-progress",
14286
- sessionIndex,
14287
- messagesProcessed: result.totalMessages,
14288
- imported: result.importedPrompts + result.importedResponses,
14289
- skipped: result.skippedDuplicates
14290
- });
14291
- }
14292
- } catch (parseError) {
14293
- result.errors.push(`Parse error on line: ${parseError}`);
14294
14614
  }
14615
+ await flushTextBuffer();
14616
+ } finally {
14617
+ rl.close();
14618
+ fileStream.close();
14295
14619
  }
14296
- await flushTextBuffer();
14297
14620
  await this.memoryService.endSession(sessionId);
14298
14621
  if (options.projectPath) {
14299
14622
  registerSession(sessionId, options.projectPath);
@@ -14804,29 +15127,6 @@ var CodexSessionHistoryImporter = class {
14804
15127
  getSessionsRoot() {
14805
15128
  return this.sessionsRoot;
14806
15129
  }
14807
- listSessionFilesRecursive(rootDir) {
14808
- if (!fs10.existsSync(rootDir)) return [];
14809
- const out = [];
14810
- const stack = [rootDir];
14811
- while (stack.length > 0) {
14812
- const dir = stack.pop();
14813
- let entries;
14814
- try {
14815
- entries = fs10.readdirSync(dir, { withFileTypes: true });
14816
- } catch {
14817
- continue;
14818
- }
14819
- for (const ent of entries) {
14820
- const fullPath = path12.join(dir, ent.name);
14821
- if (ent.isDirectory()) {
14822
- stack.push(fullPath);
14823
- } else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
14824
- out.push(fullPath);
14825
- }
14826
- }
14827
- }
14828
- return out;
14829
- }
14830
15130
  async readSessionMeta(filePath) {
14831
15131
  const fileStream = fs10.createReadStream(filePath, { encoding: "utf-8" });
14832
15132
  const rl = readline2.createInterface({ input: fileStream, crlfDelay: Infinity });
@@ -14876,7 +15176,7 @@ var CodexSessionHistoryImporter = class {
14876
15176
  }
14877
15177
  const normalizedTarget = normalizeMaybeRealpath(projectPath);
14878
15178
  onProgress?.({ phase: "scan", message: "Scanning Codex session files..." });
14879
- const sessionFiles = this.listSessionFilesRecursive(sessionsRoot);
15179
+ const sessionFiles = listCodexSessionFilesRecursive(sessionsRoot);
14880
15180
  const matchingFiles = [];
14881
15181
  for (const filePath of sessionFiles) {
14882
15182
  try {
@@ -14941,7 +15241,7 @@ var CodexSessionHistoryImporter = class {
14941
15241
  return result;
14942
15242
  }
14943
15243
  onProgress?.({ phase: "scan", message: "Scanning all Codex sessions..." });
14944
- const sessionFiles = this.listSessionFilesRecursive(sessionsRoot);
15244
+ const sessionFiles = listCodexSessionFilesRecursive(sessionsRoot);
14945
15245
  const selectedFiles = selectRecentCodexSessionFiles(sessionFiles, options.sessionLimit);
14946
15246
  onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} Codex session file(s)` });
14947
15247
  const totalLimit = normalizePositiveImportLimit(options.limit);
@@ -15020,13 +15320,12 @@ var CodexSessionHistoryImporter = class {
15020
15320
  return;
15021
15321
  }
15022
15322
  if (textBuffer.length === 0 || !currentTurnId) return;
15023
- const substantive = textBuffer.filter((t) => t.length >= 100);
15024
- const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
15323
+ const merged = mergeAgentResponseBlocks(textBuffer);
15025
15324
  if (!merged) {
15026
15325
  textBuffer = [];
15027
15326
  return;
15028
15327
  }
15029
- const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
15328
+ const truncated = truncateAgentResponse(merged);
15030
15329
  const appendResult = await this.memoryService.storeAgentResponse(
15031
15330
  sessionId,
15032
15331
  truncated,
@@ -15110,7 +15409,7 @@ var CodexSessionHistoryImporter = class {
15110
15409
  async listAvailableSessions(projectPath) {
15111
15410
  const sessionsRoot = this.getSessionsRoot();
15112
15411
  if (!fs10.existsSync(sessionsRoot)) return [];
15113
- const files = this.listSessionFilesRecursive(sessionsRoot);
15412
+ const files = listCodexSessionFilesRecursive(sessionsRoot);
15114
15413
  const sessions = [];
15115
15414
  const normalizedTarget = projectPath ? normalizeMaybeRealpath(projectPath) : null;
15116
15415
  for (const filePath of files) {
@@ -15551,9 +15850,7 @@ var HermesSessionHistoryImporter = class {
15551
15850
  return;
15552
15851
  }
15553
15852
  if (textBuffer.length === 0 || !currentTurnId) return;
15554
- const contentBlocks = textBuffer.map((item) => item.content);
15555
- const substantive = contentBlocks.filter((content) => content.length >= 100);
15556
- const merged = substantive.length > 0 ? substantive.join("\n\n") : contentBlocks.reduce((a, b) => a.length >= b.length ? a : b, "");
15853
+ const merged = mergeAgentResponseBlocks(textBuffer.map((item) => item.content));
15557
15854
  if (!merged) {
15558
15855
  textBuffer = [];
15559
15856
  return;
@@ -16053,8 +16350,8 @@ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
16053
16350
  import { logger } from "hono/logger";
16054
16351
  import { serve } from "@hono/node-server";
16055
16352
  import { serveStatic } from "@hono/node-server/serve-static";
16056
- import * as path17 from "path";
16057
- import * as fs15 from "fs";
16353
+ import * as path18 from "path";
16354
+ import * as fs16 from "fs";
16058
16355
  import { createHmac, timingSafeEqual } from "crypto";
16059
16356
  import { fileURLToPath as fileUrlToPath } from "url";
16060
16357
 
@@ -16065,6 +16362,11 @@ import { Hono as Hono11 } from "hono";
16065
16362
  import { Hono } from "hono";
16066
16363
 
16067
16364
  // src/apps/server/api/utils.ts
16365
+ function jsonError(c, error, options = {}) {
16366
+ const status = options.status ?? 500;
16367
+ console.error(`[api] ${c.req.method} ${c.req.path} failed:`, error);
16368
+ return c.json({ error: options.message ?? "Internal server error" }, status);
16369
+ }
16068
16370
  function getServiceFromQuery(c) {
16069
16371
  const project = c.req.query("project") || c.req.query("projectId");
16070
16372
  if (project) {
@@ -16176,7 +16478,7 @@ sessionsRouter.get("/", async (c) => {
16176
16478
  hasMore: end < total
16177
16479
  });
16178
16480
  } catch (error) {
16179
- return c.json({ error: error.message }, 500);
16481
+ return jsonError(c, error);
16180
16482
  } finally {
16181
16483
  await memoryService.shutdown();
16182
16484
  }
@@ -16212,7 +16514,7 @@ sessionsRouter.get("/:id", async (c) => {
16212
16514
  stats: eventsByType
16213
16515
  });
16214
16516
  } catch (error) {
16215
- return c.json({ error: error.message }, 500);
16517
+ return jsonError(c, error);
16216
16518
  } finally {
16217
16519
  await memoryService.shutdown();
16218
16520
  }
@@ -16278,7 +16580,7 @@ eventsRouter.get("/", async (c) => {
16278
16580
  hasMore: offset + limit < total
16279
16581
  });
16280
16582
  } catch (error) {
16281
- return c.json({ error: error.message }, 500);
16583
+ return jsonError(c, error);
16282
16584
  } finally {
16283
16585
  await memoryService.shutdown();
16284
16586
  }
@@ -16288,12 +16590,11 @@ eventsRouter.get("/:id", async (c) => {
16288
16590
  const memoryService = getLightweightServiceFromQuery(c);
16289
16591
  try {
16290
16592
  await memoryService.initialize();
16291
- const recentEvents = await memoryService.getRecentEvents(1e4);
16292
- const event = recentEvents.find((e) => e.id === id);
16593
+ const event = await memoryService.getEvent(id);
16293
16594
  if (!event) {
16294
16595
  return c.json({ error: "Event not found" }, 404);
16295
16596
  }
16296
- const sessionEvents = recentEvents.filter((e) => e.sessionId === event.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
16597
+ const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
16297
16598
  const eventIndex = sessionEvents.findIndex((e) => e.id === id);
16298
16599
  const start = Math.max(0, eventIndex - 2);
16299
16600
  const end = Math.min(sessionEvents.length, eventIndex + 3);
@@ -16315,7 +16616,7 @@ eventsRouter.get("/:id", async (c) => {
16315
16616
  }))
16316
16617
  });
16317
16618
  } catch (error) {
16318
- return c.json({ error: error.message }, 500);
16619
+ return jsonError(c, error);
16319
16620
  } finally {
16320
16621
  await memoryService.shutdown();
16321
16622
  }
@@ -16362,7 +16663,7 @@ searchRouter.post("/", async (c) => {
16362
16663
  }
16363
16664
  });
16364
16665
  } catch (error) {
16365
- return c.json({ error: error.message }, 500);
16666
+ return jsonError(c, error);
16366
16667
  } finally {
16367
16668
  await memoryService.shutdown();
16368
16669
  }
@@ -16405,7 +16706,7 @@ searchRouter.post("/disclosure", async (c) => {
16405
16706
  throw error;
16406
16707
  }
16407
16708
  } catch (error) {
16408
- return c.json({ error: error.message }, 500);
16709
+ return jsonError(c, error);
16409
16710
  } finally {
16410
16711
  await memoryService?.shutdown();
16411
16712
  }
@@ -16425,7 +16726,7 @@ searchRouter.get("/disclosure/:resultId/expand", async (c) => {
16425
16726
  }
16426
16727
  return c.json(result);
16427
16728
  } catch (error) {
16428
- return c.json({ error: error.message }, 500);
16729
+ return jsonError(c, error);
16429
16730
  } finally {
16430
16731
  await memoryService.shutdown();
16431
16732
  }
@@ -16440,7 +16741,7 @@ searchRouter.get("/disclosure/:resultId/source", async (c) => {
16440
16741
  }
16441
16742
  return c.json(result);
16442
16743
  } catch (error) {
16443
- return c.json({ error: error.message }, 500);
16744
+ return jsonError(c, error);
16444
16745
  } finally {
16445
16746
  await memoryService.shutdown();
16446
16747
  }
@@ -16469,7 +16770,7 @@ searchRouter.get("/", async (c) => {
16469
16770
  }
16470
16771
  });
16471
16772
  } catch (error) {
16472
- return c.json({ error: error.message }, 500);
16773
+ return jsonError(c, error);
16473
16774
  } finally {
16474
16775
  await memoryService.shutdown();
16475
16776
  }
@@ -16477,9 +16778,89 @@ searchRouter.get("/", async (c) => {
16477
16778
 
16478
16779
  // src/apps/server/api/stats.ts
16479
16780
  import { Hono as Hono4 } from "hono";
16480
- import * as fs13 from "fs";
16781
+ import * as fs14 from "fs";
16481
16782
  import * as os9 from "os";
16783
+ import * as path16 from "path";
16784
+
16785
+ // src/apps/server/api/stats-metrics.ts
16786
+ import * as fs13 from "fs";
16482
16787
  import * as path15 from "path";
16788
+ var DEFAULT_KPI_THRESHOLDS = {
16789
+ usefulRecallRateMin: 0.45,
16790
+ reworkRateMax: 0.25,
16791
+ postChangeFailureRateMax: 0.2,
16792
+ avgCompletionTurnsMax: 12,
16793
+ memoryHitRateMin: 0.35
16794
+ };
16795
+ function loadKpiThresholds() {
16796
+ try {
16797
+ const filePath = path15.resolve(process.cwd(), "config", "kpi-thresholds.json");
16798
+ if (!fs13.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
16799
+ const parsed = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
16800
+ return {
16801
+ usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
16802
+ reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
16803
+ postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
16804
+ avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
16805
+ memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
16806
+ };
16807
+ } catch {
16808
+ return DEFAULT_KPI_THRESHOLDS;
16809
+ }
16810
+ }
16811
+ function windowToMs(window) {
16812
+ if (window === "24h") return 24 * 60 * 60 * 1e3;
16813
+ if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
16814
+ return 30 * 24 * 60 * 60 * 1e3;
16815
+ }
16816
+ function inWindow(e, now, window) {
16817
+ return now - e.timestamp.getTime() <= windowToMs(window);
16818
+ }
16819
+ function isEditToolName(name) {
16820
+ return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
16821
+ }
16822
+ function parseToolPayload(e) {
16823
+ if (e.eventType !== "tool_observation") return null;
16824
+ try {
16825
+ const payload = JSON.parse(e.content);
16826
+ return {
16827
+ toolName: payload?.toolName,
16828
+ success: payload?.success,
16829
+ filePath: payload?.metadata?.filePath,
16830
+ command: payload?.metadata?.command
16831
+ };
16832
+ } catch {
16833
+ return {
16834
+ toolName: e.metadata?.toolName,
16835
+ success: e.metadata?.success,
16836
+ filePath: e.metadata?.filePath,
16837
+ command: e.metadata?.command
16838
+ };
16839
+ }
16840
+ }
16841
+ function isTestLikeCommand(command) {
16842
+ if (!command) return false;
16843
+ return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
16844
+ }
16845
+ function safeRatio(num, den) {
16846
+ if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
16847
+ return num / den;
16848
+ }
16849
+ function round2(value, digits = 4) {
16850
+ const factor = 10 ** digits;
16851
+ return Math.round(value * factor) / factor;
16852
+ }
16853
+ function computeSessionTurnCount(sessionEvents) {
16854
+ const turnIds = /* @__PURE__ */ new Set();
16855
+ for (const e of sessionEvents) {
16856
+ const turnId = e.metadata?.turnId;
16857
+ if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
16858
+ }
16859
+ if (turnIds.size > 0) return turnIds.size;
16860
+ return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
16861
+ }
16862
+
16863
+ // src/apps/server/api/stats.ts
16483
16864
  var statsRouter = new Hono4();
16484
16865
  var OPERATION_STATS_TABLES = [
16485
16866
  "memory_facets",
@@ -16506,7 +16887,7 @@ function operationsStatsHomeDir() {
16506
16887
  }
16507
16888
  function projectStoragePathForOperationsStats(projectOrHash) {
16508
16889
  const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
16509
- return path15.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
16890
+ return path16.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
16510
16891
  }
16511
16892
  function getOperationsStatsContext(project) {
16512
16893
  if (project && project.trim().length > 0) {
@@ -16516,13 +16897,13 @@ function getOperationsStatsContext(project) {
16516
16897
  return {
16517
16898
  projectHash,
16518
16899
  storagePath: storagePath2,
16519
- dbPath: path15.join(storagePath2, "events.sqlite")
16900
+ dbPath: path16.join(storagePath2, "events.sqlite")
16520
16901
  };
16521
16902
  }
16522
- const storagePath = path15.join(operationsStatsHomeDir(), ".claude-code", "memory");
16903
+ const storagePath = path16.join(operationsStatsHomeDir(), ".claude-code", "memory");
16523
16904
  return {
16524
16905
  storagePath,
16525
- dbPath: path15.join(storagePath, "events.sqlite")
16906
+ dbPath: path16.join(storagePath, "events.sqlite")
16526
16907
  };
16527
16908
  }
16528
16909
  function countRowValue(db, sql, params = []) {
@@ -16540,6 +16921,23 @@ function sanitizeAggregateLabel(value) {
16540
16921
  const trimmed = sanitized.trim();
16541
16922
  return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
16542
16923
  }
16924
+ var SAFE_RETRIEVAL_TRACE_STRATEGIES = /* @__PURE__ */ new Set([
16925
+ "auto",
16926
+ "deep",
16927
+ "fast",
16928
+ "hybrid",
16929
+ "keyword",
16930
+ "semantic",
16931
+ "mcp-context-pack",
16932
+ "session-start-hook",
16933
+ "unknown"
16934
+ ]);
16935
+ function normalizeRetrievalTraceStrategy(value) {
16936
+ const label = sanitizeAggregateLabel(value);
16937
+ if (label === "[REDACTED]") return "unknown";
16938
+ const normalized = label.trim().toLowerCase();
16939
+ return SAFE_RETRIEVAL_TRACE_STRATEGIES.has(normalized) ? normalized : "unknown";
16940
+ }
16543
16941
  function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
16544
16942
  return {
16545
16943
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16953,80 +17351,6 @@ function buildLessonConfidenceBuckets(db, projectHash) {
16953
17351
  }).length
16954
17352
  }));
16955
17353
  }
16956
- var DEFAULT_KPI_THRESHOLDS = {
16957
- usefulRecallRateMin: 0.45,
16958
- reworkRateMax: 0.25,
16959
- postChangeFailureRateMax: 0.2,
16960
- avgCompletionTurnsMax: 12,
16961
- memoryHitRateMin: 0.35
16962
- };
16963
- function loadKpiThresholds() {
16964
- try {
16965
- const filePath = path15.resolve(process.cwd(), "config", "kpi-thresholds.json");
16966
- if (!fs13.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
16967
- const parsed = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
16968
- return {
16969
- usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
16970
- reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
16971
- postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
16972
- avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
16973
- memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
16974
- };
16975
- } catch {
16976
- return DEFAULT_KPI_THRESHOLDS;
16977
- }
16978
- }
16979
- function windowToMs(window) {
16980
- if (window === "24h") return 24 * 60 * 60 * 1e3;
16981
- if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
16982
- return 30 * 24 * 60 * 60 * 1e3;
16983
- }
16984
- function inWindow(e, now, window) {
16985
- return now - e.timestamp.getTime() <= windowToMs(window);
16986
- }
16987
- function isEditToolName(name) {
16988
- return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
16989
- }
16990
- function parseToolPayload(e) {
16991
- if (e.eventType !== "tool_observation") return null;
16992
- try {
16993
- const payload = JSON.parse(e.content);
16994
- return {
16995
- toolName: payload?.toolName,
16996
- success: payload?.success,
16997
- filePath: payload?.metadata?.filePath,
16998
- command: payload?.metadata?.command
16999
- };
17000
- } catch {
17001
- return {
17002
- toolName: e.metadata?.toolName,
17003
- success: e.metadata?.success,
17004
- filePath: e.metadata?.filePath,
17005
- command: e.metadata?.command
17006
- };
17007
- }
17008
- }
17009
- function isTestLikeCommand(command) {
17010
- if (!command) return false;
17011
- return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
17012
- }
17013
- function safeRatio(num, den) {
17014
- if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
17015
- return num / den;
17016
- }
17017
- function round2(value, digits = 4) {
17018
- const factor = 10 ** digits;
17019
- return Math.round(value * factor) / factor;
17020
- }
17021
- function computeSessionTurnCount(sessionEvents) {
17022
- const turnIds = /* @__PURE__ */ new Set();
17023
- for (const e of sessionEvents) {
17024
- const turnId = e.metadata?.turnId;
17025
- if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
17026
- }
17027
- if (turnIds.size > 0) return turnIds.size;
17028
- return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
17029
- }
17030
17354
  function normalizeQueryRewriteKind2(value) {
17031
17355
  const normalized = (value || "").trim().toLowerCase();
17032
17356
  if (normalized === "follow-up-context" || normalized === "intent-rewrite") return normalized;
@@ -17054,6 +17378,63 @@ function getTraceSelectedCount(trace) {
17054
17378
  function getTraceCandidateCount(trace) {
17055
17379
  return Number(trace.candidateCount ?? trace.candidateEventIds?.length ?? trace.candidateDetails?.length ?? 0);
17056
17380
  }
17381
+ function normalizeStatsNumber(value) {
17382
+ const numberValue = Number(value || 0);
17383
+ return Number.isFinite(numberValue) ? Math.max(0, numberValue) : 0;
17384
+ }
17385
+ function sanitizeRetrievalTraceStrategyBreakdown(value) {
17386
+ if (!Array.isArray(value)) return [];
17387
+ const byStrategy = /* @__PURE__ */ new Map();
17388
+ for (const item of value) {
17389
+ const row = item && typeof item === "object" ? item : {};
17390
+ const strategy = normalizeRetrievalTraceStrategy(row.strategy ?? "unknown");
17391
+ const bucket = byStrategy.get(strategy) ?? {
17392
+ strategy,
17393
+ totalQueries: 0,
17394
+ queriesWithSelection: 0,
17395
+ rewrittenQueries: 0,
17396
+ totalCandidateCount: 0,
17397
+ totalSelectedCount: 0
17398
+ };
17399
+ bucket.totalQueries += normalizeStatsNumber(row.totalQueries);
17400
+ bucket.queriesWithSelection += normalizeStatsNumber(row.queriesWithSelection);
17401
+ bucket.rewrittenQueries += normalizeStatsNumber(row.rewrittenQueries);
17402
+ bucket.totalCandidateCount += normalizeStatsNumber(row.totalCandidateCount);
17403
+ bucket.totalSelectedCount += normalizeStatsNumber(row.totalSelectedCount);
17404
+ byStrategy.set(strategy, bucket);
17405
+ }
17406
+ return Array.from(byStrategy.values()).map((row) => ({
17407
+ strategy: row.strategy,
17408
+ totalQueries: row.totalQueries,
17409
+ queriesWithSelection: row.queriesWithSelection,
17410
+ rewrittenQueries: row.rewrittenQueries,
17411
+ rewriteRate: normalizeMetric(safeRatio(row.rewrittenQueries, row.totalQueries)),
17412
+ totalCandidateCount: row.totalCandidateCount,
17413
+ totalSelectedCount: row.totalSelectedCount,
17414
+ avgCandidateCount: safeRatio(row.totalCandidateCount, row.totalQueries),
17415
+ avgSelectedCount: safeRatio(row.totalSelectedCount, row.totalQueries),
17416
+ selectionRate: normalizeMetric(safeRatio(row.totalSelectedCount, row.totalCandidateCount)),
17417
+ queryYieldRate: normalizeMetric(safeRatio(row.queriesWithSelection, row.totalQueries))
17418
+ }));
17419
+ }
17420
+ function sanitizeRetrievalTraceStats(stats) {
17421
+ const s = stats && typeof stats === "object" ? stats : {};
17422
+ return {
17423
+ totalQueries: Number(s.totalQueries || 0),
17424
+ avgCandidateCount: Number(s.avgCandidateCount || 0),
17425
+ avgSelectedCount: Number(s.avgSelectedCount || 0),
17426
+ selectionRate: normalizeMetric(s.selectionRate),
17427
+ rewrittenQueries: Number(s.rewrittenQueries || 0),
17428
+ rewriteRate: normalizeMetric(s.rewriteRate),
17429
+ rewrittenQueriesWithSelection: Number(s.rewrittenQueriesWithSelection || 0),
17430
+ rawQueriesWithSelection: Number(s.rawQueriesWithSelection || 0),
17431
+ rewrittenSelectionRate: normalizeMetric(s.rewrittenSelectionRate),
17432
+ rawSelectionRate: normalizeMetric(s.rawSelectionRate),
17433
+ avgSelectedCountForRewrittenQueries: Number(s.avgSelectedCountForRewrittenQueries || 0),
17434
+ avgSelectedCountForRawQueries: Number(s.avgSelectedCountForRawQueries || 0),
17435
+ strategyBreakdown: sanitizeRetrievalTraceStrategyBreakdown(s.strategyBreakdown)
17436
+ };
17437
+ }
17057
17438
  function makeRetrievalReviewItem(trace) {
17058
17439
  const candidateCount = getTraceCandidateCount(trace);
17059
17440
  const selectedCount = getTraceSelectedCount(trace);
@@ -17107,7 +17488,7 @@ function makeRetrievalReviewItem(trace) {
17107
17488
  action,
17108
17489
  queryRewriteKind,
17109
17490
  rewritten,
17110
- strategy: trace.strategy || null,
17491
+ strategy: normalizeRetrievalTraceStrategy(trace.strategy ?? "unknown"),
17111
17492
  candidateCount,
17112
17493
  selectedCount,
17113
17494
  candidateEventIds: (trace.candidateEventIds || []).slice(0, 5),
@@ -17534,7 +17915,7 @@ statsRouter.get("/levels/:level", async (c) => {
17534
17915
  hasMore: events.length === limit
17535
17916
  });
17536
17917
  } catch (error) {
17537
- return c.json({ error: error.message }, 500);
17918
+ return jsonError(c, error);
17538
17919
  } finally {
17539
17920
  await memoryService.shutdown();
17540
17921
  }
@@ -17543,7 +17924,7 @@ statsRouter.get("/operations", async (c) => {
17543
17924
  const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
17544
17925
  const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
17545
17926
  const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
17546
- const databaseExists = fs13.existsSync(context.dbPath);
17927
+ const databaseExists = fs14.existsSync(context.dbPath);
17547
17928
  if (!databaseExists) {
17548
17929
  return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
17549
17930
  }
@@ -17631,7 +18012,7 @@ statsRouter.get("/perspective", async (c) => {
17631
18012
  const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
17632
18013
  const contradictionLimit = parseStatsLimit(c.req.query("contradictionLimit"), 10, 50);
17633
18014
  const graphLimit = parseStatsLimit(c.req.query("graphLimit"), 10, 50);
17634
- const databaseExists = fs13.existsSync(context.dbPath);
18015
+ const databaseExists = fs14.existsSync(context.dbPath);
17635
18016
  if (!databaseExists) {
17636
18017
  return c.json(emptyPerspectiveStatsPayload(context, false, [...PERSPECTIVE_STATS_TABLES], windowDays));
17637
18018
  }
@@ -17707,33 +18088,29 @@ statsRouter.get("/", async (c) => {
17707
18088
  const memoryService = getLightweightServiceFromQuery(c);
17708
18089
  try {
17709
18090
  await memoryService.initialize();
17710
- const stats = await memoryService.getStats();
17711
- const recentEvents = await memoryService.getRecentEvents(1e4);
17712
- const eventsByType = recentEvents.reduce((acc, e) => {
17713
- acc[e.eventType] = (acc[e.eventType] || 0) + 1;
17714
- return acc;
17715
- }, {});
17716
- const uniqueSessions = new Set(recentEvents.map((e) => e.sessionId));
17717
- const now = /* @__PURE__ */ new Date();
17718
- const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
17719
- const eventsByDay = recentEvents.filter((e) => e.timestamp >= sevenDaysAgo).reduce((acc, e) => {
17720
- const day = e.timestamp.toISOString().split("T")[0];
17721
- acc[day] = (acc[day] || 0) + 1;
17722
- return acc;
17723
- }, {});
17724
- const retrievalTrace = await memoryService.getRetrievalTraceStats();
18091
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
18092
+ const [stats, typeCounts, sessionTotal, dailyCounts, rawTrace] = await Promise.all([
18093
+ memoryService.getStats(),
18094
+ memoryService.getEventTypeCounts(),
18095
+ memoryService.getDistinctSessionCount(),
18096
+ memoryService.getDailyEventCounts(sevenDaysAgo),
18097
+ memoryService.getRetrievalTraceStats()
18098
+ ]);
18099
+ const eventsByType = Object.fromEntries(typeCounts.map((t) => [t.eventType, t.count]));
18100
+ const eventsByDay = Object.fromEntries(dailyCounts.map((d) => [d.day, d.total]));
18101
+ const retrievalTrace = sanitizeRetrievalTraceStats(rawTrace);
17725
18102
  return c.json({
17726
18103
  storage: {
17727
18104
  eventCount: stats.totalEvents,
17728
18105
  vectorCount: stats.vectorCount
17729
18106
  },
17730
18107
  sessions: {
17731
- total: uniqueSessions.size
18108
+ total: sessionTotal
17732
18109
  },
17733
18110
  eventsByType,
17734
18111
  activity: {
17735
18112
  daily: eventsByDay,
17736
- total7Days: recentEvents.filter((e) => e.timestamp >= sevenDaysAgo).length
18113
+ total7Days: dailyCounts.reduce((sum, d) => sum + d.total, 0)
17737
18114
  },
17738
18115
  memory: {
17739
18116
  heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
@@ -17743,7 +18120,7 @@ statsRouter.get("/", async (c) => {
17743
18120
  retrievalTrace
17744
18121
  });
17745
18122
  } catch (error) {
17746
- return c.json({ error: error.message }, 500);
18123
+ return jsonError(c, error);
17747
18124
  } finally {
17748
18125
  await memoryService.shutdown();
17749
18126
  }
@@ -17780,30 +18157,22 @@ statsRouter.get("/most-accessed", async (c) => {
17780
18157
  }
17781
18158
  });
17782
18159
  statsRouter.get("/timeline", async (c) => {
17783
- const days = parseInt(c.req.query("days") || "7", 10);
18160
+ const parsedDays = parseInt(c.req.query("days") || "7", 10);
18161
+ const days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 7;
17784
18162
  const memoryService = getLightweightServiceFromQuery(c);
17785
18163
  try {
17786
18164
  await memoryService.initialize();
17787
- const recentEvents = await memoryService.getRecentEvents(1e4);
17788
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
17789
- const filteredEvents = recentEvents.filter((e) => e.timestamp >= cutoff);
17790
- const daily = filteredEvents.reduce((acc, e) => {
17791
- const day = e.timestamp.toISOString().split("T")[0];
17792
- if (!acc[day]) {
17793
- acc[day] = { date: day, total: 0, prompts: 0, responses: 0, tools: 0 };
17794
- }
17795
- acc[day].total++;
17796
- if (e.eventType === "user_prompt") acc[day].prompts++;
17797
- if (e.eventType === "agent_response") acc[day].responses++;
17798
- if (e.eventType === "tool_observation") acc[day].tools++;
17799
- return acc;
17800
- }, {});
17801
- return c.json({
17802
- days,
17803
- daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
17804
- });
18165
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
18166
+ const daily = (await memoryService.getDailyEventCounts(cutoff)).map((d) => ({
18167
+ date: d.day,
18168
+ total: d.total,
18169
+ prompts: d.prompts,
18170
+ responses: d.responses,
18171
+ tools: d.tools
18172
+ }));
18173
+ return c.json({ days, daily });
17805
18174
  } catch (error) {
17806
- return c.json({ error: error.message }, 500);
18175
+ return jsonError(c, error);
17807
18176
  } finally {
17808
18177
  await memoryService.shutdown();
17809
18178
  }
@@ -17846,16 +18215,14 @@ statsRouter.get("/usefulness", async (c) => {
17846
18215
  try {
17847
18216
  await memoryService.initialize();
17848
18217
  const now = Date.now();
17849
- const eventLimit = 2e4;
17850
18218
  const traceLimit = 5e3;
17851
18219
  const windowStart = new Date(now - windowToMs(window));
17852
18220
  const [events, helpfulness, traces] = await Promise.all([
17853
- memoryService.getRecentEvents(eventLimit),
18221
+ memoryService.getEventsAfter(windowStart.toISOString()),
17854
18222
  memoryService.getHelpfulnessStats(windowStart),
17855
18223
  memoryService.getRecentRetrievalTraces(traceLimit)
17856
18224
  ]);
17857
18225
  return c.json(computeMemoryUsefulnessSummary(events, helpfulness, traces, now, window, {
17858
- eventsLimit: eventLimit,
17859
18226
  tracesLimit: traceLimit
17860
18227
  }));
17861
18228
  } catch (error) {
@@ -17873,7 +18240,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
17873
18240
  const traces = await memoryService.getRecentRetrievalTraces(limit);
17874
18241
  const traceStats = await memoryService.getRetrievalTraceStats();
17875
18242
  return c.json({
17876
- stats: traceStats,
18243
+ stats: sanitizeRetrievalTraceStats(traceStats),
17877
18244
  traces: traces.map((t) => {
17878
18245
  const queryRewriteKind = normalizeQueryRewriteKind2(t.queryRewriteKind);
17879
18246
  return {
@@ -17882,7 +18249,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
17882
18249
  projectHash: t.projectHash || null,
17883
18250
  queryRewriteKind,
17884
18251
  rewritten: queryRewriteKind !== "none",
17885
- strategy: t.strategy || null,
18252
+ strategy: normalizeRetrievalTraceStrategy(t.strategy ?? "unknown"),
17886
18253
  candidateEventIds: t.candidateEventIds,
17887
18254
  selectedEventIds: t.selectedEventIds,
17888
18255
  candidateDetails: t.candidateDetails || [],
@@ -17909,7 +18276,8 @@ statsRouter.get("/retrieval-traces", async (c) => {
17909
18276
  rewrittenSelectionRate: 0,
17910
18277
  rawSelectionRate: 0,
17911
18278
  avgSelectedCountForRewrittenQueries: 0,
17912
- avgSelectedCountForRawQueries: 0
18279
+ avgSelectedCountForRawQueries: 0,
18280
+ strategyBreakdown: []
17913
18281
  },
17914
18282
  traces: [],
17915
18283
  error: error.message
@@ -17960,7 +18328,8 @@ statsRouter.get("/kpi", async (c) => {
17960
18328
  await memoryService.initialize();
17961
18329
  const now = Date.now();
17962
18330
  const thresholds = loadKpiThresholds();
17963
- const allEvents = await memoryService.getRecentEvents(2e4);
18331
+ const lookbackMs = Math.max(2 * windowToMs(window), 30 * 24 * 60 * 60 * 1e3);
18332
+ const allEvents = await memoryService.getEventsAfter(new Date(now - lookbackMs).toISOString());
17964
18333
  const events = allEvents.filter((e) => inWindow(e, now, window));
17965
18334
  const helpfulness = await memoryService.getHelpfulnessStats();
17966
18335
  const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round2(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
@@ -18069,7 +18438,7 @@ statsRouter.get("/kpi", async (c) => {
18069
18438
  alerts
18070
18439
  });
18071
18440
  } catch (error) {
18072
- return c.json({ error: error.message }, 500);
18441
+ return jsonError(c, error);
18073
18442
  } finally {
18074
18443
  await memoryService.shutdown();
18075
18444
  }
@@ -18120,11 +18489,7 @@ citationsRouter.get("/:id", async (c) => {
18120
18489
  const memoryService = getServiceFromQuery(c);
18121
18490
  try {
18122
18491
  await memoryService.initialize();
18123
- const recentEvents = await memoryService.getRecentEvents(1e4);
18124
- const event = recentEvents.find((e) => {
18125
- const eventCitationId = generateCitationId(e.id);
18126
- return eventCitationId === citationId;
18127
- });
18492
+ const event = await memoryService.getEventByCitationId(citationId);
18128
18493
  if (!event) {
18129
18494
  return c.json({ error: "Citation not found" }, 404);
18130
18495
  }
@@ -18143,7 +18508,7 @@ citationsRouter.get("/:id", async (c) => {
18143
18508
  }
18144
18509
  });
18145
18510
  } catch (error) {
18146
- return c.json({ error: error.message }, 500);
18511
+ return jsonError(c, error);
18147
18512
  } finally {
18148
18513
  await memoryService.shutdown();
18149
18514
  }
@@ -18154,15 +18519,11 @@ citationsRouter.get("/:id/related", async (c) => {
18154
18519
  const memoryService = getServiceFromQuery(c);
18155
18520
  try {
18156
18521
  await memoryService.initialize();
18157
- const recentEvents = await memoryService.getRecentEvents(1e4);
18158
- const event = recentEvents.find((e) => {
18159
- const eventCitationId = generateCitationId(e.id);
18160
- return eventCitationId === citationId;
18161
- });
18522
+ const event = await memoryService.getEventByCitationId(citationId);
18162
18523
  if (!event) {
18163
18524
  return c.json({ error: "Citation not found" }, 404);
18164
18525
  }
18165
- const sessionEvents = recentEvents.filter((e) => e.sessionId === event.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
18526
+ const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
18166
18527
  const eventIndex = sessionEvents.findIndex((e) => e.id === event.id);
18167
18528
  const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
18168
18529
  const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
@@ -18181,7 +18542,7 @@ citationsRouter.get("/:id/related", async (c) => {
18181
18542
  } : null
18182
18543
  });
18183
18544
  } catch (error) {
18184
- return c.json({ error: error.message }, 500);
18545
+ return jsonError(c, error);
18185
18546
  } finally {
18186
18547
  await memoryService.shutdown();
18187
18548
  }
@@ -18224,7 +18585,7 @@ turnsRouter.get("/", async (c) => {
18224
18585
  hasMore: offset + limit < totalTurns
18225
18586
  });
18226
18587
  } catch (error) {
18227
- return c.json({ error: error.message }, 500);
18588
+ return jsonError(c, error);
18228
18589
  } finally {
18229
18590
  await memoryService.shutdown();
18230
18591
  }
@@ -18275,7 +18636,7 @@ turnsRouter.get("/:turnId", async (c) => {
18275
18636
  totalEvents: events.length
18276
18637
  });
18277
18638
  } catch (error) {
18278
- return c.json({ error: error.message }, 500);
18639
+ return jsonError(c, error);
18279
18640
  } finally {
18280
18641
  await memoryService.shutdown();
18281
18642
  }
@@ -18302,8 +18663,8 @@ turnsRouter.post("/backfill", async (c) => {
18302
18663
 
18303
18664
  // src/apps/server/api/projects.ts
18304
18665
  import { Hono as Hono7 } from "hono";
18305
- import * as fs14 from "fs";
18306
- import * as path16 from "path";
18666
+ import * as fs15 from "fs";
18667
+ import * as path17 from "path";
18307
18668
  import * as os10 from "os";
18308
18669
  var projectsRouter = new Hono7();
18309
18670
  projectsRouter.get("/:hash/detail", async (c) => {
@@ -18332,7 +18693,7 @@ projectsRouter.get("/:hash/detail", async (c) => {
18332
18693
  return c.json({
18333
18694
  project: {
18334
18695
  hash,
18335
- projectName: projectPath ? path16.basename(projectPath) : `unknown (${hash})`,
18696
+ projectName: projectPath ? path17.basename(projectPath) : `unknown (${hash})`,
18336
18697
  registered: Boolean(projectPath)
18337
18698
  },
18338
18699
  storage: {
@@ -18360,13 +18721,13 @@ projectsRouter.get("/:hash/detail", async (c) => {
18360
18721
  });
18361
18722
  projectsRouter.get("/", async (c) => {
18362
18723
  try {
18363
- const projectsDir = path16.join(os10.homedir(), ".claude-code", "memory", "projects");
18364
- if (!fs14.existsSync(projectsDir)) {
18724
+ const projectsDir = path17.join(os10.homedir(), ".claude-code", "memory", "projects");
18725
+ if (!fs15.existsSync(projectsDir)) {
18365
18726
  return c.json({ projects: [] });
18366
18727
  }
18367
- const projectHashes = fs14.readdirSync(projectsDir).filter((name) => {
18368
- const fullPath = path16.join(projectsDir, name);
18369
- return fs14.statSync(fullPath).isDirectory();
18728
+ const projectHashes = fs15.readdirSync(projectsDir).filter((name) => {
18729
+ const fullPath = path17.join(projectsDir, name);
18730
+ return fs15.statSync(fullPath).isDirectory();
18370
18731
  });
18371
18732
  const registry = loadSessionRegistry();
18372
18733
  const hashToPath = /* @__PURE__ */ new Map();
@@ -18376,17 +18737,17 @@ projectsRouter.get("/", async (c) => {
18376
18737
  }
18377
18738
  }
18378
18739
  const projects = projectHashes.map((hash) => {
18379
- const dirPath = path16.join(projectsDir, hash);
18380
- const dbPath = path16.join(dirPath, "events.sqlite");
18740
+ const dirPath = path17.join(projectsDir, hash);
18741
+ const dbPath = path17.join(dirPath, "events.sqlite");
18381
18742
  let dbSize = 0;
18382
- if (fs14.existsSync(dbPath)) {
18383
- dbSize = fs14.statSync(dbPath).size;
18743
+ if (fs15.existsSync(dbPath)) {
18744
+ dbSize = fs15.statSync(dbPath).size;
18384
18745
  }
18385
18746
  const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
18386
18747
  return {
18387
18748
  hash,
18388
18749
  projectPath,
18389
- projectName: path16.basename(projectPath),
18750
+ projectName: path17.basename(projectPath),
18390
18751
  dbSize,
18391
18752
  dbSizeHuman: formatBytes(dbSize)
18392
18753
  };
@@ -18515,7 +18876,7 @@ chatRouter.post("/", async (c) => {
18515
18876
  }
18516
18877
  });
18517
18878
  } catch (error) {
18518
- return c.json({ error: error.message }, 500);
18879
+ return jsonError(c, error);
18519
18880
  } finally {
18520
18881
  await memoryService.shutdown();
18521
18882
  }
@@ -19156,20 +19517,20 @@ playgroundRouter.post("/dry-run", async (c) => {
19156
19517
  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);
19157
19518
 
19158
19519
  // src/apps/server/index.ts
19159
- var moduleDir = path17.dirname(fileUrlToPath(import.meta.url));
19520
+ var moduleDir = path18.dirname(fileUrlToPath(import.meta.url));
19160
19521
  var DASHBOARD_SESSION_COOKIE = "cml_dashboard_session";
19161
19522
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
19162
19523
  function resolveUiPath() {
19163
19524
  const candidates = [
19164
19525
  // Built server: dist/server/index.js -> dist/ui
19165
- path17.resolve(moduleDir, "../ui"),
19526
+ path18.resolve(moduleDir, "../ui"),
19166
19527
  // Source/dev server: src/apps/server/index.ts -> src/apps/dashboard
19167
- path17.resolve(moduleDir, "../dashboard"),
19528
+ path18.resolve(moduleDir, "../dashboard"),
19168
19529
  // Fallback when running from a repository root.
19169
- path17.resolve(process.cwd(), "dist/ui"),
19170
- path17.resolve(process.cwd(), "src/apps/dashboard")
19530
+ path18.resolve(process.cwd(), "dist/ui"),
19531
+ path18.resolve(process.cwd(), "src/apps/dashboard")
19171
19532
  ];
19172
- return candidates.find((candidate) => fs15.existsSync(path17.join(candidate, "index.html"))) ?? candidates[0];
19533
+ return candidates.find((candidate) => fs16.existsSync(path18.join(candidate, "index.html"))) ?? candidates[0];
19173
19534
  }
19174
19535
  function normalizeDashboardHost(host = "localhost") {
19175
19536
  const normalized = host.trim().toLowerCase();
@@ -19310,14 +19671,51 @@ function createAuthOptions(options) {
19310
19671
  return {
19311
19672
  password: options.password ?? "",
19312
19673
  cookieMaxAgeSeconds: options.cookieMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
19313
- now: options.now ?? Date.now
19674
+ now: options.now ?? Date.now,
19675
+ allowedOrigins: options.allowedOrigins ?? []
19314
19676
  };
19315
19677
  }
19678
+ function resolveAllowedOrigins(explicit) {
19679
+ if (explicit && explicit.length > 0) return explicit;
19680
+ const raw = process.env.DASHBOARD_ALLOWED_ORIGINS;
19681
+ if (!raw) return [];
19682
+ return raw.split(",").map((value) => value.trim()).filter(Boolean);
19683
+ }
19684
+ function selfOrigins(c) {
19685
+ const origins = [];
19686
+ try {
19687
+ origins.push(new URL(c.req.url).origin);
19688
+ } catch {
19689
+ }
19690
+ const host = c.req.header("host");
19691
+ if (host) origins.push(`${isSecureRequest(c) ? "https" : "http"}://${host}`);
19692
+ return origins;
19693
+ }
19694
+ function isAllowedMutationOrigin(c, origin, allowedOrigins) {
19695
+ return allowedOrigins.includes(origin) || selfOrigins(c).includes(origin);
19696
+ }
19316
19697
  function createDashboardApp(options = {}) {
19317
19698
  const app2 = new Hono12();
19318
19699
  const authOptions = createAuthOptions(options);
19319
- app2.use("*", cors());
19700
+ const allowedOrigins = resolveAllowedOrigins(options.allowedOrigins);
19701
+ if (allowedOrigins.length > 0) {
19702
+ app2.use("*", cors({
19703
+ origin: (origin) => allowedOrigins.includes(origin) ? origin : null,
19704
+ credentials: true
19705
+ }));
19706
+ }
19320
19707
  app2.use("*", logger());
19708
+ app2.use("/api/*", async (c, next) => {
19709
+ const method = c.req.method;
19710
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
19711
+ return next();
19712
+ }
19713
+ const origin = c.req.header("origin");
19714
+ if (origin && !isAllowedMutationOrigin(c, origin, allowedOrigins)) {
19715
+ return c.json({ error: "Cross-origin request blocked" }, 403);
19716
+ }
19717
+ return next();
19718
+ });
19321
19719
  app2.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
19322
19720
  app2.get("/api/auth/status", (c) => c.json({
19323
19721
  enabled: Boolean(authOptions.password),
@@ -19360,13 +19758,13 @@ function createDashboardApp(options = {}) {
19360
19758
  }
19361
19759
  app2.route("/api", apiRouter);
19362
19760
  const uiPath = resolveUiPath();
19363
- if (fs15.existsSync(uiPath)) {
19761
+ if (fs16.existsSync(uiPath)) {
19364
19762
  app2.use("/*", serveStatic({ root: uiPath }));
19365
19763
  }
19366
19764
  app2.get("*", (c) => {
19367
- const indexPath = path17.join(uiPath, "index.html");
19368
- if (fs15.existsSync(indexPath)) {
19369
- return c.html(fs15.readFileSync(indexPath, "utf-8"));
19765
+ const indexPath = path18.join(uiPath, "index.html");
19766
+ if (fs16.existsSync(indexPath)) {
19767
+ return c.html(fs16.readFileSync(indexPath, "utf-8"));
19370
19768
  }
19371
19769
  return c.text('UI not built. Run "npm run build:ui" first.', 404);
19372
19770
  });
@@ -19824,7 +20222,7 @@ function formatMetadataValue(value) {
19824
20222
  }
19825
20223
 
19826
20224
  // src/apps/cli/mcp-install.ts
19827
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
20225
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
19828
20226
  import { dirname as dirname7, join as join18 } from "node:path";
19829
20227
  import { homedir as homedir11 } from "node:os";
19830
20228
  function getDefaultClaudeDesktopConfigPath(platform = process.platform, env = process.env, home = homedir11()) {
@@ -19837,7 +20235,7 @@ function getDefaultClaudeDesktopConfigPath(platform = process.platform, env = pr
19837
20235
  return join18(env.XDG_CONFIG_HOME ?? join18(home, ".config"), "claude", "claude_desktop_config.json");
19838
20236
  }
19839
20237
  function readJsonConfig(configPath) {
19840
- if (!existsSync13(configPath)) {
20238
+ if (!existsSync14(configPath)) {
19841
20239
  return {};
19842
20240
  }
19843
20241
  const content = readFileSync6(configPath, "utf8").trim();
@@ -19886,7 +20284,7 @@ function installMcpServer(options = {}) {
19886
20284
  }
19887
20285
 
19888
20286
  // src/apps/cli/claude-settings-hooks.ts
19889
- import * as path18 from "path";
20287
+ import * as path19 from "path";
19890
20288
  var REQUIRED_HOOK_FILES = [
19891
20289
  "user-prompt-submit.js",
19892
20290
  "post-tool-use.js",
@@ -19905,7 +20303,7 @@ function shellQuotePathForNode(filePath) {
19905
20303
  return `'${filePath.replace(/'/g, `'\\''`)}'`;
19906
20304
  }
19907
20305
  function buildHookCommand(pluginPath, fileName) {
19908
- return `node ${shellQuotePathForNode(path18.join(pluginPath, "hooks", fileName))}`;
20306
+ return `node ${shellQuotePathForNode(path19.join(pluginPath, "hooks", fileName))}`;
19909
20307
  }
19910
20308
  function getHooksConfig(pluginPath) {
19911
20309
  const makeHook = (fileName) => [
@@ -19976,8 +20374,8 @@ function mergePluginHooksIntoSettings(settings, pluginPath) {
19976
20374
  }
19977
20375
 
19978
20376
  // src/apps/cli/codex-validation-output.ts
19979
- import * as fs16 from "fs";
19980
- import * as path19 from "path";
20377
+ import * as fs17 from "fs";
20378
+ import * as path20 from "path";
19981
20379
  function formatNumber(value) {
19982
20380
  return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
19983
20381
  }
@@ -20051,14 +20449,14 @@ function formatCodexValidationReport(report, format = "markdown") {
20051
20449
  return lines.join("\n");
20052
20450
  }
20053
20451
  function writeCodexValidationReport(outputPath, report, format = "markdown") {
20054
- const dir = path19.dirname(outputPath);
20055
- fs16.mkdirSync(dir, { recursive: true });
20056
- fs16.writeFileSync(outputPath, formatCodexValidationReport(report, format), "utf8");
20452
+ const dir = path20.dirname(outputPath);
20453
+ fs17.mkdirSync(dir, { recursive: true });
20454
+ fs17.writeFileSync(outputPath, formatCodexValidationReport(report, format), "utf8");
20057
20455
  }
20058
20456
 
20059
20457
  // src/apps/cli/hermes-validation-output.ts
20060
- import * as fs17 from "fs";
20061
- import * as path20 from "path";
20458
+ import * as fs18 from "fs";
20459
+ import * as path21 from "path";
20062
20460
  function formatNumber2(value) {
20063
20461
  return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
20064
20462
  }
@@ -20128,9 +20526,9 @@ function formatHermesValidationReport(report, format = "markdown") {
20128
20526
  return lines.join("\n");
20129
20527
  }
20130
20528
  function writeHermesValidationReport(outputPath, report, format = "markdown") {
20131
- const dir = path20.dirname(outputPath);
20132
- fs17.mkdirSync(dir, { recursive: true });
20133
- fs17.writeFileSync(outputPath, formatHermesValidationReport(report, format), "utf8");
20529
+ const dir = path21.dirname(outputPath);
20530
+ fs18.mkdirSync(dir, { recursive: true });
20531
+ fs18.writeFileSync(outputPath, formatHermesValidationReport(report, format), "utf8");
20134
20532
  }
20135
20533
 
20136
20534
  // src/apps/cli/codex-import-runner.ts
@@ -20817,7 +21215,7 @@ function redactSecrets(input) {
20817
21215
  }
20818
21216
 
20819
21217
  // src/apps/cli/process-command.ts
20820
- import * as path21 from "node:path";
21218
+ import * as path22 from "node:path";
20821
21219
  function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
20822
21220
  const explicitProject = options.project;
20823
21221
  if (explicitProject !== void 0 && explicitProject.trim().length === 0) {
@@ -20838,7 +21236,7 @@ function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
20838
21236
  projectPath,
20839
21237
  recoverStuck,
20840
21238
  dryRunRecovery,
20841
- lockPath: explicitLockPath ?? path21.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21239
+ lockPath: explicitLockPath ?? path22.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
20842
21240
  };
20843
21241
  }
20844
21242
  function formatProcessLockBusy(input) {
@@ -21016,12 +21414,12 @@ function formatDuration2(ms) {
21016
21414
  }
21017
21415
 
21018
21416
  // src/apps/cli/mongo-sync-command.ts
21019
- import * as path23 from "node:path";
21417
+ import * as path24 from "node:path";
21020
21418
 
21021
21419
  // src/core/worker-lock.ts
21022
- import { closeSync, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
21420
+ import { closeSync, existsSync as existsSync15, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
21023
21421
  import * as os12 from "node:os";
21024
- import * as path22 from "node:path";
21422
+ import * as path23 from "node:path";
21025
21423
  import { randomUUID as randomUUID18 } from "node:crypto";
21026
21424
  var WorkerLock = class {
21027
21425
  lockPath;
@@ -21030,7 +21428,7 @@ var WorkerLock = class {
21030
21428
  now;
21031
21429
  isProcessRunning;
21032
21430
  acquired = false;
21033
- constructor(lockPath = path22.join(os12.tmpdir(), "claude-memory-layer", "vector-worker.lock"), options = {}) {
21431
+ constructor(lockPath = path23.join(os12.tmpdir(), "claude-memory-layer", "vector-worker.lock"), options = {}) {
21034
21432
  this.lockPath = lockPath;
21035
21433
  this.pid = options.pid ?? process.pid;
21036
21434
  this.ownerId = options.ownerId ?? randomUUID18();
@@ -21043,7 +21441,7 @@ var WorkerLock = class {
21043
21441
  acquire() {
21044
21442
  let staleRecovered = false;
21045
21443
  for (; ; ) {
21046
- mkdirSync10(path22.dirname(this.lockPath), { recursive: true });
21444
+ mkdirSync10(path23.dirname(this.lockPath), { recursive: true });
21047
21445
  const createResult = this.tryCreateLockFile();
21048
21446
  if (createResult === "created") {
21049
21447
  this.acquired = true;
@@ -21077,7 +21475,7 @@ var WorkerLock = class {
21077
21475
  }
21078
21476
  }
21079
21477
  release() {
21080
- if (!this.acquired || !existsSync14(this.lockPath)) return false;
21478
+ if (!this.acquired || !existsSync15(this.lockPath)) return false;
21081
21479
  const existing = this.readPayload();
21082
21480
  if (!this.isOwnedPayload(existing)) return false;
21083
21481
  try {
@@ -21175,7 +21573,7 @@ function resolveMongoSyncProcessOptions(options, cwd = process.cwd(), deps = {})
21175
21573
  projectPath,
21176
21574
  processAfterSync: options.processAfterSync === true,
21177
21575
  processIntervalMs,
21178
- lockPath: explicitLockPath ?? path23.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21576
+ lockPath: explicitLockPath ?? path24.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21179
21577
  };
21180
21578
  }
21181
21579
  function createMongoSyncPostProcessor(options, deps = {}) {
@@ -21258,28 +21656,28 @@ function parsePositiveIntegerOption2(raw, flagName) {
21258
21656
  }
21259
21657
 
21260
21658
  // src/apps/cli/index.ts
21261
- var CLAUDE_SETTINGS_PATH = path24.join(os13.homedir(), ".claude", "settings.json");
21659
+ var CLAUDE_SETTINGS_PATH = path25.join(os13.homedir(), ".claude", "settings.json");
21262
21660
  function getPluginPath() {
21263
21661
  const possiblePaths = [
21264
- path24.join(__dirname, ".."),
21662
+ path25.join(__dirname, ".."),
21265
21663
  // When running from dist/cli
21266
- path24.join(__dirname, "../..", "dist"),
21664
+ path25.join(__dirname, "../..", "dist"),
21267
21665
  // When running from src
21268
- path24.join(process.cwd(), "dist")
21666
+ path25.join(process.cwd(), "dist")
21269
21667
  // Current working directory
21270
21668
  ];
21271
21669
  for (const p of possiblePaths) {
21272
- const hooksPath = path24.join(p, "hooks", "user-prompt-submit.js");
21273
- if (fs18.existsSync(hooksPath)) {
21670
+ const hooksPath = path25.join(p, "hooks", "user-prompt-submit.js");
21671
+ if (fs19.existsSync(hooksPath)) {
21274
21672
  return p;
21275
21673
  }
21276
21674
  }
21277
- return path24.join(os13.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
21675
+ return path25.join(os13.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
21278
21676
  }
21279
21677
  function loadClaudeSettings() {
21280
21678
  try {
21281
- if (fs18.existsSync(CLAUDE_SETTINGS_PATH)) {
21282
- const content = fs18.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
21679
+ if (fs19.existsSync(CLAUDE_SETTINGS_PATH)) {
21680
+ const content = fs19.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
21283
21681
  return JSON.parse(content);
21284
21682
  }
21285
21683
  } catch (error) {
@@ -21288,13 +21686,13 @@ function loadClaudeSettings() {
21288
21686
  return {};
21289
21687
  }
21290
21688
  function saveClaudeSettings(settings) {
21291
- const dir = path24.dirname(CLAUDE_SETTINGS_PATH);
21292
- if (!fs18.existsSync(dir)) {
21293
- fs18.mkdirSync(dir, { recursive: true });
21689
+ const dir = path25.dirname(CLAUDE_SETTINGS_PATH);
21690
+ if (!fs19.existsSync(dir)) {
21691
+ fs19.mkdirSync(dir, { recursive: true });
21294
21692
  }
21295
21693
  const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
21296
- fs18.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
21297
- fs18.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
21694
+ fs19.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
21695
+ fs19.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
21298
21696
  }
21299
21697
  function parseCommaList(value) {
21300
21698
  if (!value) return void 0;
@@ -21354,7 +21752,7 @@ async function runCodexValidationCommand(options) {
21354
21752
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
21355
21753
  `);
21356
21754
  if (options.output) {
21357
- const outputPath = path24.resolve(options.output);
21755
+ const outputPath = path25.resolve(options.output);
21358
21756
  writeCodexValidationReport(outputPath, report, format);
21359
21757
  console.log(`
21360
21758
  Report written to ${outputPath}`);
@@ -21374,7 +21772,7 @@ async function runHermesValidationCommand(options) {
21374
21772
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
21375
21773
  `);
21376
21774
  if (options.output) {
21377
- const outputPath = path24.resolve(options.output);
21775
+ const outputPath = path25.resolve(options.output);
21378
21776
  writeHermesValidationReport(outputPath, report, format);
21379
21777
  console.log(`
21380
21778
  Report written to ${outputPath}`);
@@ -21408,18 +21806,18 @@ var OPERATION_PRIVACY_CONFIG = {
21408
21806
  }
21409
21807
  };
21410
21808
  function resolveOperationProject(project) {
21411
- const projectPath = path24.resolve(project ?? process.cwd());
21809
+ const projectPath = path25.resolve(project ?? process.cwd());
21412
21810
  const projectHash = hashProjectPath(projectPath);
21413
21811
  const storagePath = getProjectStoragePath(projectPath);
21414
21812
  return {
21415
21813
  projectPath,
21416
21814
  projectHash,
21417
21815
  storagePath,
21418
- dbPath: path24.join(storagePath, "events.sqlite")
21816
+ dbPath: path25.join(storagePath, "events.sqlite")
21419
21817
  };
21420
21818
  }
21421
21819
  function openOperationReadDatabase(context) {
21422
- if (!fs18.existsSync(context.dbPath)) return null;
21820
+ if (!fs19.existsSync(context.dbPath)) return null;
21423
21821
  return createSQLiteDatabase(context.dbPath, { readonly: true, walMode: false });
21424
21822
  }
21425
21823
  async function withOperationWriteDatabase(context, callback) {
@@ -21629,7 +22027,7 @@ async function runOperationCli(action, label) {
21629
22027
  }
21630
22028
  }
21631
22029
  var program = new Command();
21632
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.53");
22030
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.54");
21633
22031
  program.command("market-context").description("Fetch read-only DART/FRED/Finnhub context with structured MarketContextSnapshot bull/bear/risk/catalyst analysis").option("--company <name>", "Company name for DART fallback search and report subject").option("--dart-corp-code <code>", "Exact DART corp_code for issuer-specific filings").option("--symbol <ticker>", "Listed ticker for Finnhub company profile").option("--providers <list>", "Comma-separated providers: dart,fred,finnhub").option("--fred-series <list>", "Comma-separated FRED series IDs").option("--json", "Print structured JSON including analysis.marketSnapshot").option("--no-snapshot", "Disable MarketContextSnapshot and DART company snapshot analysis").action(async (options) => {
21634
22032
  try {
21635
22033
  await runMarketContextCommand(options);
@@ -21642,7 +22040,7 @@ program.command("install").description("Install hooks into Claude Code settings"
21642
22040
  try {
21643
22041
  const pluginPath = options.path || getPluginPath();
21644
22042
  const missingHooks = REQUIRED_HOOK_FILES.filter(
21645
- (file) => !fs18.existsSync(path24.join(pluginPath, "hooks", file))
22043
+ (file) => !fs19.existsSync(path25.join(pluginPath, "hooks", file))
21646
22044
  );
21647
22045
  if (missingHooks.length > 0) {
21648
22046
  console.error(`
@@ -21708,7 +22106,7 @@ program.command("status").description("Check plugin installation status").action
21708
22106
  console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21709
22107
  console.log(` Stop: ${hasStopHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21710
22108
  console.log(` SessionEnd: ${hasSessionEndHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21711
- const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs18.existsSync(path24.join(pluginPath, "hooks", file)));
22109
+ const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs19.existsSync(path25.join(pluginPath, "hooks", file)));
21712
22110
  console.log(`
21713
22111
  Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
21714
22112
  console.log(` Path: ${pluginPath}`);
@@ -21943,8 +22341,8 @@ repairCommand.command("legacy-project-scope").description("Dry-run or apply proj
21943
22341
  apply: options.apply
21944
22342
  });
21945
22343
  const storagePath = projectPath ? getProjectStoragePath(projectPath) : resolveProjectStoragePath(repairOptions.projectHash);
21946
- const dbPath = path24.join(storagePath, "events.sqlite");
21947
- if (repairOptions.dryRun && !fs18.existsSync(dbPath)) {
22344
+ const dbPath = path25.join(storagePath, "events.sqlite");
22345
+ if (repairOptions.dryRun && !fs19.existsSync(dbPath)) {
21948
22346
  const projectHash = repairOptions.projectHash || hashProjectPath(repairOptions.projectPath);
21949
22347
  console.log(formatLegacyProjectScopeRepairResult({
21950
22348
  dryRun: true,
@@ -22191,8 +22589,8 @@ retentionCommand.command("audit").description("Run a dry-run retention audit for
22191
22589
  });
22192
22590
  const projectHash = auditOptions.projectHash ?? hashProjectPath(auditOptions.projectPath);
22193
22591
  const storagePath = auditOptions.projectPath ? getProjectStoragePath(auditOptions.projectPath) : resolveProjectStoragePath(projectHash);
22194
- const dbPath = path24.join(storagePath, "events.sqlite");
22195
- if (!fs18.existsSync(dbPath)) {
22592
+ const dbPath = path25.join(storagePath, "events.sqlite");
22593
+ if (!fs19.existsSync(dbPath)) {
22196
22594
  const emptyReport = emptyRetentionAuditReport(projectHash, auditOptions.limit);
22197
22595
  console.log(formatRetentionAuditReport(emptyReport, { json: auditOptions.json }));
22198
22596
  return;
@@ -22219,7 +22617,7 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
22219
22617
  const projectPath = options.project || process.cwd();
22220
22618
  const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
22221
22619
  const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
22222
- const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path24.basename(projectPath);
22620
+ const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path25.basename(projectPath);
22223
22621
  const direction = String(options.direction || "both").toLowerCase();
22224
22622
  if (!mongoUri || !mongoDb) {
22225
22623
  console.error("\n\u274C MongoDB sync is not configured.");
@@ -22249,14 +22647,14 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
22249
22647
  }
22250
22648
  })();
22251
22649
  const storagePath = getProjectStoragePath(projectPath);
22252
- if (!fs18.existsSync(storagePath)) {
22253
- fs18.mkdirSync(storagePath, { recursive: true });
22650
+ if (!fs19.existsSync(storagePath)) {
22651
+ fs19.mkdirSync(storagePath, { recursive: true });
22254
22652
  }
22255
22653
  const postProcessor = createMongoSyncPostProcessor(processOptions, {
22256
22654
  log: (message) => process.stdout.write(`${message}
22257
22655
  `)
22258
22656
  });
22259
- const sqliteStore = new SQLiteEventStore(path24.join(storagePath, "events.sqlite"));
22657
+ const sqliteStore = new SQLiteEventStore(path25.join(storagePath, "events.sqlite"));
22260
22658
  const worker = new MongoSyncWorker(sqliteStore, {
22261
22659
  uri: mongoUri,
22262
22660
  dbName: mongoDb,
@@ -22316,7 +22714,7 @@ function renderProgress(event) {
22316
22714
  break;
22317
22715
  case "session-start": {
22318
22716
  const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
22319
- const sessionName = path24.basename(event.filePath, ".jsonl").slice(0, 8);
22717
+ const sessionName = path25.basename(event.filePath, ".jsonl").slice(0, 8);
22320
22718
  process.stdout.write(
22321
22719
  `\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
22322
22720
  );
@@ -22391,9 +22789,9 @@ async function listMarkdownFiles(root) {
22391
22789
  const stack = [root];
22392
22790
  while (stack.length > 0) {
22393
22791
  const dir = stack.pop();
22394
- const entries = await fs18.promises.readdir(dir, { withFileTypes: true });
22792
+ const entries = await fs19.promises.readdir(dir, { withFileTypes: true });
22395
22793
  for (const e of entries) {
22396
- const full = path24.join(dir, e.name);
22794
+ const full = path25.join(dir, e.name);
22397
22795
  if (e.isDirectory()) stack.push(full);
22398
22796
  else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md") out.push(full);
22399
22797
  }
@@ -22401,8 +22799,8 @@ async function listMarkdownFiles(root) {
22401
22799
  return out.sort();
22402
22800
  }
22403
22801
  function deriveNamespaceCategory(sourceRoot, filePath) {
22404
- const rel = path24.relative(sourceRoot, filePath);
22405
- const dirSeg = path24.dirname(rel).split(path24.sep).filter(Boolean);
22802
+ const rel = path25.relative(sourceRoot, filePath);
22803
+ const dirSeg = path25.dirname(rel).split(path25.sep).filter(Boolean);
22406
22804
  if (dirSeg.length >= 2) {
22407
22805
  const namespace = sanitizeSegment3(dirSeg[0], "default");
22408
22806
  const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
@@ -22421,10 +22819,10 @@ function extractImportEvidence(markdown) {
22421
22819
  program.command("organize-import [sourceDir]").description("Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing").option("-p, --project <path>", "Project path (defaults to cwd)").option("--session <id>", "Session id for imported events (default: import:organized)").option("--limit <n>", "Limit number of files to import").option("--dry-run", "Preview mapping without writing").option("--bootstrap", "Force-generate structured markdown from codebase + git history before import").option("--bootstrap-if-empty", "Auto-bootstrap when source has no markdown files (default: true)", true).option("--no-bootstrap-if-empty", "Disable auto-bootstrap when source has no markdown files").option("--force-bootstrap", "Run bootstrap even when markdown files exist").option("--repo <path>", "Repository root for bootstrap analysis (default: project path)").option("--out <path>", "Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)").option("--since <range>", 'Git history range for bootstrap (default: "180 days ago")').option("--max-commits <n>", "Max commits to analyze for bootstrap (default: 1000)").option("--incremental", "Use previous bootstrap manifest as baseline for incremental updates (default: true)", true).option("--no-incremental", "Disable incremental bootstrap; regenerate full snapshot").action(async (sourceDir, options) => {
22422
22820
  const projectPath = options.project || process.cwd();
22423
22821
  const sessionId = options.session || "import:organized";
22424
- const sourceRoot = path24.resolve(sourceDir || options.out || projectPath);
22425
- const repoPath = path24.resolve(options.repo || projectPath);
22426
- if (!fs18.existsSync(sourceRoot)) {
22427
- fs18.mkdirSync(sourceRoot, { recursive: true });
22822
+ const sourceRoot = path25.resolve(sourceDir || options.out || projectPath);
22823
+ const repoPath = path25.resolve(options.repo || projectPath);
22824
+ if (!fs19.existsSync(sourceRoot)) {
22825
+ fs19.mkdirSync(sourceRoot, { recursive: true });
22428
22826
  }
22429
22827
  const service = getMemoryServiceForProject(projectPath);
22430
22828
  try {
@@ -22434,7 +22832,7 @@ program.command("organize-import [sourceDir]").description("Import existing mark
22434
22832
  const hasMarkdown = files.length > 0;
22435
22833
  const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
22436
22834
  if (shouldBootstrap) {
22437
- const outDir = path24.resolve(options.out || path24.join(sourceRoot, "bootstrap-kb"));
22835
+ const outDir = path25.resolve(options.out || path25.join(sourceRoot, "bootstrap-kb"));
22438
22836
  const since = options.since || "180 days ago";
22439
22837
  const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
22440
22838
  console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
@@ -22473,13 +22871,13 @@ program.command("organize-import [sourceDir]").description("Import existing mark
22473
22871
  let imported = 0;
22474
22872
  let skipped = 0;
22475
22873
  for (const file of targets) {
22476
- const text = await fs18.promises.readFile(file, "utf8");
22874
+ const text = await fs19.promises.readFile(file, "utf8");
22477
22875
  if (!text.trim()) {
22478
22876
  skipped += 1;
22479
22877
  continue;
22480
22878
  }
22481
22879
  const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
22482
- const rel = path24.relative(activeSourceRoot, file);
22880
+ const rel = path25.relative(activeSourceRoot, file);
22483
22881
  const evidence = extractImportEvidence(text);
22484
22882
  if (options.dryRun) {
22485
22883
  console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);