claude-memory-layer 1.0.52 → 1.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  };
@@ -18435,6 +18796,8 @@ function summarizeProjectOutbox(outbox) {
18435
18796
  pending: (embedding.pending || 0) + (vector.pending || 0),
18436
18797
  processing: (embedding.processing || 0) + (vector.processing || 0),
18437
18798
  failed: (embedding.failed || 0) + (vector.failed || 0),
18799
+ retryableFailed: (embedding.retryableFailed || 0) + (vector.retryableFailed || 0),
18800
+ quarantinedFailed: (embedding.quarantinedFailed || 0) + (vector.quarantinedFailed || 0),
18438
18801
  stuckProcessing: (embedding.stuckProcessing || 0) + (vector.stuckProcessing || 0)
18439
18802
  };
18440
18803
  }
@@ -18513,7 +18876,7 @@ chatRouter.post("/", async (c) => {
18513
18876
  }
18514
18877
  });
18515
18878
  } catch (error) {
18516
- return c.json({ error: error.message }, 500);
18879
+ return jsonError(c, error);
18517
18880
  } finally {
18518
18881
  await memoryService.shutdown();
18519
18882
  }
@@ -19154,20 +19517,20 @@ playgroundRouter.post("/dry-run", async (c) => {
19154
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);
19155
19518
 
19156
19519
  // src/apps/server/index.ts
19157
- var moduleDir = path17.dirname(fileUrlToPath(import.meta.url));
19520
+ var moduleDir = path18.dirname(fileUrlToPath(import.meta.url));
19158
19521
  var DASHBOARD_SESSION_COOKIE = "cml_dashboard_session";
19159
19522
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
19160
19523
  function resolveUiPath() {
19161
19524
  const candidates = [
19162
19525
  // Built server: dist/server/index.js -> dist/ui
19163
- path17.resolve(moduleDir, "../ui"),
19526
+ path18.resolve(moduleDir, "../ui"),
19164
19527
  // Source/dev server: src/apps/server/index.ts -> src/apps/dashboard
19165
- path17.resolve(moduleDir, "../dashboard"),
19528
+ path18.resolve(moduleDir, "../dashboard"),
19166
19529
  // Fallback when running from a repository root.
19167
- path17.resolve(process.cwd(), "dist/ui"),
19168
- path17.resolve(process.cwd(), "src/apps/dashboard")
19530
+ path18.resolve(process.cwd(), "dist/ui"),
19531
+ path18.resolve(process.cwd(), "src/apps/dashboard")
19169
19532
  ];
19170
- 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];
19171
19534
  }
19172
19535
  function normalizeDashboardHost(host = "localhost") {
19173
19536
  const normalized = host.trim().toLowerCase();
@@ -19308,14 +19671,51 @@ function createAuthOptions(options) {
19308
19671
  return {
19309
19672
  password: options.password ?? "",
19310
19673
  cookieMaxAgeSeconds: options.cookieMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
19311
- now: options.now ?? Date.now
19674
+ now: options.now ?? Date.now,
19675
+ allowedOrigins: options.allowedOrigins ?? []
19312
19676
  };
19313
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
+ }
19314
19697
  function createDashboardApp(options = {}) {
19315
19698
  const app2 = new Hono12();
19316
19699
  const authOptions = createAuthOptions(options);
19317
- 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
+ }
19318
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
+ });
19319
19719
  app2.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
19320
19720
  app2.get("/api/auth/status", (c) => c.json({
19321
19721
  enabled: Boolean(authOptions.password),
@@ -19358,13 +19758,13 @@ function createDashboardApp(options = {}) {
19358
19758
  }
19359
19759
  app2.route("/api", apiRouter);
19360
19760
  const uiPath = resolveUiPath();
19361
- if (fs15.existsSync(uiPath)) {
19761
+ if (fs16.existsSync(uiPath)) {
19362
19762
  app2.use("/*", serveStatic({ root: uiPath }));
19363
19763
  }
19364
19764
  app2.get("*", (c) => {
19365
- const indexPath = path17.join(uiPath, "index.html");
19366
- if (fs15.existsSync(indexPath)) {
19367
- 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"));
19368
19768
  }
19369
19769
  return c.text('UI not built. Run "npm run build:ui" first.', 404);
19370
19770
  });
@@ -19822,7 +20222,7 @@ function formatMetadataValue(value) {
19822
20222
  }
19823
20223
 
19824
20224
  // src/apps/cli/mcp-install.ts
19825
- 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";
19826
20226
  import { dirname as dirname7, join as join18 } from "node:path";
19827
20227
  import { homedir as homedir11 } from "node:os";
19828
20228
  function getDefaultClaudeDesktopConfigPath(platform = process.platform, env = process.env, home = homedir11()) {
@@ -19835,7 +20235,7 @@ function getDefaultClaudeDesktopConfigPath(platform = process.platform, env = pr
19835
20235
  return join18(env.XDG_CONFIG_HOME ?? join18(home, ".config"), "claude", "claude_desktop_config.json");
19836
20236
  }
19837
20237
  function readJsonConfig(configPath) {
19838
- if (!existsSync13(configPath)) {
20238
+ if (!existsSync14(configPath)) {
19839
20239
  return {};
19840
20240
  }
19841
20241
  const content = readFileSync6(configPath, "utf8").trim();
@@ -19884,7 +20284,7 @@ function installMcpServer(options = {}) {
19884
20284
  }
19885
20285
 
19886
20286
  // src/apps/cli/claude-settings-hooks.ts
19887
- import * as path18 from "path";
20287
+ import * as path19 from "path";
19888
20288
  var REQUIRED_HOOK_FILES = [
19889
20289
  "user-prompt-submit.js",
19890
20290
  "post-tool-use.js",
@@ -19903,7 +20303,7 @@ function shellQuotePathForNode(filePath) {
19903
20303
  return `'${filePath.replace(/'/g, `'\\''`)}'`;
19904
20304
  }
19905
20305
  function buildHookCommand(pluginPath, fileName) {
19906
- return `node ${shellQuotePathForNode(path18.join(pluginPath, "hooks", fileName))}`;
20306
+ return `node ${shellQuotePathForNode(path19.join(pluginPath, "hooks", fileName))}`;
19907
20307
  }
19908
20308
  function getHooksConfig(pluginPath) {
19909
20309
  const makeHook = (fileName) => [
@@ -19974,8 +20374,8 @@ function mergePluginHooksIntoSettings(settings, pluginPath) {
19974
20374
  }
19975
20375
 
19976
20376
  // src/apps/cli/codex-validation-output.ts
19977
- import * as fs16 from "fs";
19978
- import * as path19 from "path";
20377
+ import * as fs17 from "fs";
20378
+ import * as path20 from "path";
19979
20379
  function formatNumber(value) {
19980
20380
  return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
19981
20381
  }
@@ -20049,14 +20449,14 @@ function formatCodexValidationReport(report, format = "markdown") {
20049
20449
  return lines.join("\n");
20050
20450
  }
20051
20451
  function writeCodexValidationReport(outputPath, report, format = "markdown") {
20052
- const dir = path19.dirname(outputPath);
20053
- fs16.mkdirSync(dir, { recursive: true });
20054
- 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");
20055
20455
  }
20056
20456
 
20057
20457
  // src/apps/cli/hermes-validation-output.ts
20058
- import * as fs17 from "fs";
20059
- import * as path20 from "path";
20458
+ import * as fs18 from "fs";
20459
+ import * as path21 from "path";
20060
20460
  function formatNumber2(value) {
20061
20461
  return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
20062
20462
  }
@@ -20126,9 +20526,9 @@ function formatHermesValidationReport(report, format = "markdown") {
20126
20526
  return lines.join("\n");
20127
20527
  }
20128
20528
  function writeHermesValidationReport(outputPath, report, format = "markdown") {
20129
- const dir = path20.dirname(outputPath);
20130
- fs17.mkdirSync(dir, { recursive: true });
20131
- 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");
20132
20532
  }
20133
20533
 
20134
20534
  // src/apps/cli/codex-import-runner.ts
@@ -20815,7 +21215,7 @@ function redactSecrets(input) {
20815
21215
  }
20816
21216
 
20817
21217
  // src/apps/cli/process-command.ts
20818
- import * as path21 from "node:path";
21218
+ import * as path22 from "node:path";
20819
21219
  function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
20820
21220
  const explicitProject = options.project;
20821
21221
  if (explicitProject !== void 0 && explicitProject.trim().length === 0) {
@@ -20836,7 +21236,7 @@ function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
20836
21236
  projectPath,
20837
21237
  recoverStuck,
20838
21238
  dryRunRecovery,
20839
- lockPath: explicitLockPath ?? path21.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21239
+ lockPath: explicitLockPath ?? path22.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
20840
21240
  };
20841
21241
  }
20842
21242
  function formatProcessLockBusy(input) {
@@ -20900,14 +21300,12 @@ function resolveVectorStatusCommandOptions(options, cwd = process.cwd()) {
20900
21300
  if (options.project !== void 0 && options.project.trim().length === 0) {
20901
21301
  throw new Error("--project must not be empty");
20902
21302
  }
20903
- return { projectPath: options.project ?? cwd };
21303
+ return { projectPath: options.project ?? cwd, json: options.json === true };
20904
21304
  }
20905
21305
  function formatVectorStatusReport(input) {
20906
- const embedding = normalizeQueue(input.outbox.embedding);
20907
- const vector = normalizeQueue(input.outbox.vector);
20908
- const totals = sumQueues(embedding, vector);
20909
- const status = totals.failed > 0 || totals.stuckProcessing > 0 ? "needs-attention" : "ok";
20910
- const oldestProcessingAge = maxNullable2(embedding.oldestProcessingAgeMs, vector.oldestProcessingAgeMs);
21306
+ const report = buildVectorStatusReport(input);
21307
+ const { embedding, vector, totals } = report.outbox;
21308
+ const oldestProcessingAge = report.oldestProcessingAgeMs;
20911
21309
  const lines = [
20912
21310
  "Vector Outbox Status",
20913
21311
  `Vector count: ${input.stats.vectorCount}`,
@@ -20920,13 +21318,40 @@ function formatVectorStatusReport(input) {
20920
21318
  "",
20921
21319
  `Totals: pending=${totals.pending}, processing=${totals.processing}, failed=${totals.failed}, retryableFailed=${totals.retryableFailed ?? 0}, quarantinedFailed=${totals.quarantinedFailed ?? 0}, stuck=${totals.stuckProcessing}, total=${totals.total}`,
20922
21320
  `Oldest processing age: ${formatDuration2(oldestProcessingAge)}`,
20923
- `Status: ${status}`
21321
+ `Status: ${report.status}`
20924
21322
  ];
20925
- if (status === "needs-attention") {
21323
+ if (report.recommendedAction === "run-recovery") {
20926
21324
  lines.push("", "Next step: claude-memory-layer process --dry-run-recovery");
21325
+ } else if (report.recommendedAction === "inspect-quarantined") {
21326
+ lines.push("", "Next step: inspect quarantined outbox failures; recovery has no retryable failed rows.");
20927
21327
  }
20928
21328
  return lines.join("\n");
20929
21329
  }
21330
+ function formatVectorStatusJsonReport(input) {
21331
+ return JSON.stringify(buildVectorStatusReport(input), null, 2);
21332
+ }
21333
+ function buildVectorStatusReport(input) {
21334
+ const embedding = normalizeQueue(input.outbox.embedding);
21335
+ const vector = normalizeQueue(input.outbox.vector);
21336
+ const totals = sumQueues(embedding, vector);
21337
+ const status = totals.failed > 0 || totals.stuckProcessing > 0 ? "needs-attention" : "ok";
21338
+ const oldestProcessingAgeMs = maxNullable2(embedding.oldestProcessingAgeMs, vector.oldestProcessingAgeMs);
21339
+ return {
21340
+ storage: {
21341
+ totalEvents: numberOrZero(input.stats.totalEvents),
21342
+ vectorCount: numberOrZero(input.stats.vectorCount)
21343
+ },
21344
+ outbox: { embedding, vector, totals },
21345
+ status,
21346
+ recommendedAction: selectRecommendedAction(totals),
21347
+ oldestProcessingAgeMs
21348
+ };
21349
+ }
21350
+ function selectRecommendedAction(totals) {
21351
+ if ((totals.retryableFailed ?? 0) > 0 || totals.stuckProcessing > 0) return "run-recovery";
21352
+ if ((totals.quarantinedFailed ?? 0) > 0) return "inspect-quarantined";
21353
+ return "none";
21354
+ }
20930
21355
  function normalizeQueue(queue) {
20931
21356
  return {
20932
21357
  pending: numberOrZero(queue.pending),
@@ -20989,12 +21414,12 @@ function formatDuration2(ms) {
20989
21414
  }
20990
21415
 
20991
21416
  // src/apps/cli/mongo-sync-command.ts
20992
- import * as path23 from "node:path";
21417
+ import * as path24 from "node:path";
20993
21418
 
20994
21419
  // src/core/worker-lock.ts
20995
- 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";
20996
21421
  import * as os12 from "node:os";
20997
- import * as path22 from "node:path";
21422
+ import * as path23 from "node:path";
20998
21423
  import { randomUUID as randomUUID18 } from "node:crypto";
20999
21424
  var WorkerLock = class {
21000
21425
  lockPath;
@@ -21003,7 +21428,7 @@ var WorkerLock = class {
21003
21428
  now;
21004
21429
  isProcessRunning;
21005
21430
  acquired = false;
21006
- 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 = {}) {
21007
21432
  this.lockPath = lockPath;
21008
21433
  this.pid = options.pid ?? process.pid;
21009
21434
  this.ownerId = options.ownerId ?? randomUUID18();
@@ -21016,7 +21441,7 @@ var WorkerLock = class {
21016
21441
  acquire() {
21017
21442
  let staleRecovered = false;
21018
21443
  for (; ; ) {
21019
- mkdirSync10(path22.dirname(this.lockPath), { recursive: true });
21444
+ mkdirSync10(path23.dirname(this.lockPath), { recursive: true });
21020
21445
  const createResult = this.tryCreateLockFile();
21021
21446
  if (createResult === "created") {
21022
21447
  this.acquired = true;
@@ -21050,7 +21475,7 @@ var WorkerLock = class {
21050
21475
  }
21051
21476
  }
21052
21477
  release() {
21053
- if (!this.acquired || !existsSync14(this.lockPath)) return false;
21478
+ if (!this.acquired || !existsSync15(this.lockPath)) return false;
21054
21479
  const existing = this.readPayload();
21055
21480
  if (!this.isOwnedPayload(existing)) return false;
21056
21481
  try {
@@ -21148,7 +21573,7 @@ function resolveMongoSyncProcessOptions(options, cwd = process.cwd(), deps = {})
21148
21573
  projectPath,
21149
21574
  processAfterSync: options.processAfterSync === true,
21150
21575
  processIntervalMs,
21151
- lockPath: explicitLockPath ?? path23.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21576
+ lockPath: explicitLockPath ?? path24.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21152
21577
  };
21153
21578
  }
21154
21579
  function createMongoSyncPostProcessor(options, deps = {}) {
@@ -21231,28 +21656,28 @@ function parsePositiveIntegerOption2(raw, flagName) {
21231
21656
  }
21232
21657
 
21233
21658
  // src/apps/cli/index.ts
21234
- var CLAUDE_SETTINGS_PATH = path24.join(os13.homedir(), ".claude", "settings.json");
21659
+ var CLAUDE_SETTINGS_PATH = path25.join(os13.homedir(), ".claude", "settings.json");
21235
21660
  function getPluginPath() {
21236
21661
  const possiblePaths = [
21237
- path24.join(__dirname, ".."),
21662
+ path25.join(__dirname, ".."),
21238
21663
  // When running from dist/cli
21239
- path24.join(__dirname, "../..", "dist"),
21664
+ path25.join(__dirname, "../..", "dist"),
21240
21665
  // When running from src
21241
- path24.join(process.cwd(), "dist")
21666
+ path25.join(process.cwd(), "dist")
21242
21667
  // Current working directory
21243
21668
  ];
21244
21669
  for (const p of possiblePaths) {
21245
- const hooksPath = path24.join(p, "hooks", "user-prompt-submit.js");
21246
- if (fs18.existsSync(hooksPath)) {
21670
+ const hooksPath = path25.join(p, "hooks", "user-prompt-submit.js");
21671
+ if (fs19.existsSync(hooksPath)) {
21247
21672
  return p;
21248
21673
  }
21249
21674
  }
21250
- 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");
21251
21676
  }
21252
21677
  function loadClaudeSettings() {
21253
21678
  try {
21254
- if (fs18.existsSync(CLAUDE_SETTINGS_PATH)) {
21255
- 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");
21256
21681
  return JSON.parse(content);
21257
21682
  }
21258
21683
  } catch (error) {
@@ -21261,13 +21686,13 @@ function loadClaudeSettings() {
21261
21686
  return {};
21262
21687
  }
21263
21688
  function saveClaudeSettings(settings) {
21264
- const dir = path24.dirname(CLAUDE_SETTINGS_PATH);
21265
- if (!fs18.existsSync(dir)) {
21266
- fs18.mkdirSync(dir, { recursive: true });
21689
+ const dir = path25.dirname(CLAUDE_SETTINGS_PATH);
21690
+ if (!fs19.existsSync(dir)) {
21691
+ fs19.mkdirSync(dir, { recursive: true });
21267
21692
  }
21268
21693
  const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
21269
- fs18.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
21270
- fs18.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
21694
+ fs19.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
21695
+ fs19.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
21271
21696
  }
21272
21697
  function parseCommaList(value) {
21273
21698
  if (!value) return void 0;
@@ -21327,7 +21752,7 @@ async function runCodexValidationCommand(options) {
21327
21752
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
21328
21753
  `);
21329
21754
  if (options.output) {
21330
- const outputPath = path24.resolve(options.output);
21755
+ const outputPath = path25.resolve(options.output);
21331
21756
  writeCodexValidationReport(outputPath, report, format);
21332
21757
  console.log(`
21333
21758
  Report written to ${outputPath}`);
@@ -21347,7 +21772,7 @@ async function runHermesValidationCommand(options) {
21347
21772
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
21348
21773
  `);
21349
21774
  if (options.output) {
21350
- const outputPath = path24.resolve(options.output);
21775
+ const outputPath = path25.resolve(options.output);
21351
21776
  writeHermesValidationReport(outputPath, report, format);
21352
21777
  console.log(`
21353
21778
  Report written to ${outputPath}`);
@@ -21381,18 +21806,18 @@ var OPERATION_PRIVACY_CONFIG = {
21381
21806
  }
21382
21807
  };
21383
21808
  function resolveOperationProject(project) {
21384
- const projectPath = path24.resolve(project ?? process.cwd());
21809
+ const projectPath = path25.resolve(project ?? process.cwd());
21385
21810
  const projectHash = hashProjectPath(projectPath);
21386
21811
  const storagePath = getProjectStoragePath(projectPath);
21387
21812
  return {
21388
21813
  projectPath,
21389
21814
  projectHash,
21390
21815
  storagePath,
21391
- dbPath: path24.join(storagePath, "events.sqlite")
21816
+ dbPath: path25.join(storagePath, "events.sqlite")
21392
21817
  };
21393
21818
  }
21394
21819
  function openOperationReadDatabase(context) {
21395
- if (!fs18.existsSync(context.dbPath)) return null;
21820
+ if (!fs19.existsSync(context.dbPath)) return null;
21396
21821
  return createSQLiteDatabase(context.dbPath, { readonly: true, walMode: false });
21397
21822
  }
21398
21823
  async function withOperationWriteDatabase(context, callback) {
@@ -21602,7 +22027,7 @@ async function runOperationCli(action, label) {
21602
22027
  }
21603
22028
  }
21604
22029
  var program = new Command();
21605
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.52");
22030
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.54");
21606
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) => {
21607
22032
  try {
21608
22033
  await runMarketContextCommand(options);
@@ -21615,7 +22040,7 @@ program.command("install").description("Install hooks into Claude Code settings"
21615
22040
  try {
21616
22041
  const pluginPath = options.path || getPluginPath();
21617
22042
  const missingHooks = REQUIRED_HOOK_FILES.filter(
21618
- (file) => !fs18.existsSync(path24.join(pluginPath, "hooks", file))
22043
+ (file) => !fs19.existsSync(path25.join(pluginPath, "hooks", file))
21619
22044
  );
21620
22045
  if (missingHooks.length > 0) {
21621
22046
  console.error(`
@@ -21681,7 +22106,7 @@ program.command("status").description("Check plugin installation status").action
21681
22106
  console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21682
22107
  console.log(` Stop: ${hasStopHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21683
22108
  console.log(` SessionEnd: ${hasSessionEndHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21684
- 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)));
21685
22110
  console.log(`
21686
22111
  Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
21687
22112
  console.log(` Path: ${pluginPath}`);
@@ -21816,7 +22241,7 @@ program.command("stats").description("View memory statistics").option("-p, --pro
21816
22241
  process.exit(1);
21817
22242
  }
21818
22243
  });
21819
- program.command("vector-status").description("Show aggregate vector outbox status").option("-p, --project <path>", "Project path (defaults to cwd)").action(async (options) => {
22244
+ program.command("vector-status").description("Show aggregate vector outbox status").option("-p, --project <path>", "Project path (defaults to cwd)").option("--json", "Print aggregate vector outbox status as JSON for automation").action(async (options) => {
21820
22245
  let service;
21821
22246
  try {
21822
22247
  const vectorOptions = resolveVectorStatusCommandOptions(options);
@@ -21825,7 +22250,8 @@ program.command("vector-status").description("Show aggregate vector outbox statu
21825
22250
  service.getStats(),
21826
22251
  service.getOutboxStats()
21827
22252
  ]);
21828
- console.log(formatVectorStatusReport({ stats, outbox }));
22253
+ const rendered = vectorOptions.json ? formatVectorStatusJsonReport({ stats, outbox }) : formatVectorStatusReport({ stats, outbox });
22254
+ console.log(rendered);
21829
22255
  } catch (error) {
21830
22256
  const message = error instanceof Error && error.message.startsWith("--") ? error.message : "Vector status failed";
21831
22257
  console.error(message);
@@ -21915,8 +22341,8 @@ repairCommand.command("legacy-project-scope").description("Dry-run or apply proj
21915
22341
  apply: options.apply
21916
22342
  });
21917
22343
  const storagePath = projectPath ? getProjectStoragePath(projectPath) : resolveProjectStoragePath(repairOptions.projectHash);
21918
- const dbPath = path24.join(storagePath, "events.sqlite");
21919
- if (repairOptions.dryRun && !fs18.existsSync(dbPath)) {
22344
+ const dbPath = path25.join(storagePath, "events.sqlite");
22345
+ if (repairOptions.dryRun && !fs19.existsSync(dbPath)) {
21920
22346
  const projectHash = repairOptions.projectHash || hashProjectPath(repairOptions.projectPath);
21921
22347
  console.log(formatLegacyProjectScopeRepairResult({
21922
22348
  dryRun: true,
@@ -22163,8 +22589,8 @@ retentionCommand.command("audit").description("Run a dry-run retention audit for
22163
22589
  });
22164
22590
  const projectHash = auditOptions.projectHash ?? hashProjectPath(auditOptions.projectPath);
22165
22591
  const storagePath = auditOptions.projectPath ? getProjectStoragePath(auditOptions.projectPath) : resolveProjectStoragePath(projectHash);
22166
- const dbPath = path24.join(storagePath, "events.sqlite");
22167
- if (!fs18.existsSync(dbPath)) {
22592
+ const dbPath = path25.join(storagePath, "events.sqlite");
22593
+ if (!fs19.existsSync(dbPath)) {
22168
22594
  const emptyReport = emptyRetentionAuditReport(projectHash, auditOptions.limit);
22169
22595
  console.log(formatRetentionAuditReport(emptyReport, { json: auditOptions.json }));
22170
22596
  return;
@@ -22191,7 +22617,7 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
22191
22617
  const projectPath = options.project || process.cwd();
22192
22618
  const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
22193
22619
  const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
22194
- 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);
22195
22621
  const direction = String(options.direction || "both").toLowerCase();
22196
22622
  if (!mongoUri || !mongoDb) {
22197
22623
  console.error("\n\u274C MongoDB sync is not configured.");
@@ -22221,14 +22647,14 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
22221
22647
  }
22222
22648
  })();
22223
22649
  const storagePath = getProjectStoragePath(projectPath);
22224
- if (!fs18.existsSync(storagePath)) {
22225
- fs18.mkdirSync(storagePath, { recursive: true });
22650
+ if (!fs19.existsSync(storagePath)) {
22651
+ fs19.mkdirSync(storagePath, { recursive: true });
22226
22652
  }
22227
22653
  const postProcessor = createMongoSyncPostProcessor(processOptions, {
22228
22654
  log: (message) => process.stdout.write(`${message}
22229
22655
  `)
22230
22656
  });
22231
- const sqliteStore = new SQLiteEventStore(path24.join(storagePath, "events.sqlite"));
22657
+ const sqliteStore = new SQLiteEventStore(path25.join(storagePath, "events.sqlite"));
22232
22658
  const worker = new MongoSyncWorker(sqliteStore, {
22233
22659
  uri: mongoUri,
22234
22660
  dbName: mongoDb,
@@ -22288,7 +22714,7 @@ function renderProgress(event) {
22288
22714
  break;
22289
22715
  case "session-start": {
22290
22716
  const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
22291
- const sessionName = path24.basename(event.filePath, ".jsonl").slice(0, 8);
22717
+ const sessionName = path25.basename(event.filePath, ".jsonl").slice(0, 8);
22292
22718
  process.stdout.write(
22293
22719
  `\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
22294
22720
  );
@@ -22363,9 +22789,9 @@ async function listMarkdownFiles(root) {
22363
22789
  const stack = [root];
22364
22790
  while (stack.length > 0) {
22365
22791
  const dir = stack.pop();
22366
- const entries = await fs18.promises.readdir(dir, { withFileTypes: true });
22792
+ const entries = await fs19.promises.readdir(dir, { withFileTypes: true });
22367
22793
  for (const e of entries) {
22368
- const full = path24.join(dir, e.name);
22794
+ const full = path25.join(dir, e.name);
22369
22795
  if (e.isDirectory()) stack.push(full);
22370
22796
  else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md") out.push(full);
22371
22797
  }
@@ -22373,8 +22799,8 @@ async function listMarkdownFiles(root) {
22373
22799
  return out.sort();
22374
22800
  }
22375
22801
  function deriveNamespaceCategory(sourceRoot, filePath) {
22376
- const rel = path24.relative(sourceRoot, filePath);
22377
- 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);
22378
22804
  if (dirSeg.length >= 2) {
22379
22805
  const namespace = sanitizeSegment3(dirSeg[0], "default");
22380
22806
  const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
@@ -22393,10 +22819,10 @@ function extractImportEvidence(markdown) {
22393
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) => {
22394
22820
  const projectPath = options.project || process.cwd();
22395
22821
  const sessionId = options.session || "import:organized";
22396
- const sourceRoot = path24.resolve(sourceDir || options.out || projectPath);
22397
- const repoPath = path24.resolve(options.repo || projectPath);
22398
- if (!fs18.existsSync(sourceRoot)) {
22399
- 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 });
22400
22826
  }
22401
22827
  const service = getMemoryServiceForProject(projectPath);
22402
22828
  try {
@@ -22406,7 +22832,7 @@ program.command("organize-import [sourceDir]").description("Import existing mark
22406
22832
  const hasMarkdown = files.length > 0;
22407
22833
  const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
22408
22834
  if (shouldBootstrap) {
22409
- const outDir = path24.resolve(options.out || path24.join(sourceRoot, "bootstrap-kb"));
22835
+ const outDir = path25.resolve(options.out || path25.join(sourceRoot, "bootstrap-kb"));
22410
22836
  const since = options.since || "180 days ago";
22411
22837
  const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
22412
22838
  console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
@@ -22445,13 +22871,13 @@ program.command("organize-import [sourceDir]").description("Import existing mark
22445
22871
  let imported = 0;
22446
22872
  let skipped = 0;
22447
22873
  for (const file of targets) {
22448
- const text = await fs18.promises.readFile(file, "utf8");
22874
+ const text = await fs19.promises.readFile(file, "utf8");
22449
22875
  if (!text.trim()) {
22450
22876
  skipped += 1;
22451
22877
  continue;
22452
22878
  }
22453
22879
  const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
22454
- const rel = path24.relative(activeSourceRoot, file);
22880
+ const rel = path25.relative(activeSourceRoot, file);
22455
22881
  const evidence = extractImportEvidence(text);
22456
22882
  if (options.dryRun) {
22457
22883
  console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);