claude-memory-layer 1.0.53 → 1.0.55

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 fs20 from "fs";
13
+ import * as path26 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(path27, options) {
2249
+ const dir = nodePath.dirname(path27);
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(path27, {
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, path27) {
2671
2717
  let cursor = root;
2672
- for (const key of path25) {
2718
+ for (const key of path27) {
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, path27) {
2679
2725
  let cursor = root;
2680
- for (const key of path25) {
2726
+ for (const key of path27) {
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, path27) {
7682
7899
  let cursor = root;
7683
- for (const key of path25) {
7900
+ for (const key of path27) {
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, path27]) => ({
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: path27.hops,
7978
+ totalCost: path27.totalCost,
7979
+ scoreContribution: path27.totalCost > 0 ? 1 / path27.totalCost : 0,
7980
+ steps: path27.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 path27 of expansion.paths) {
10944
+ if (path27.target.type !== "event") continue;
10945
+ const target = await this.eventStore.getEvent(path27.target.id);
10722
10946
  if (!target) continue;
10723
- const graphPath = toRetrievalGraphPathDebug(path25, titleByEntityId);
10724
- const score = graphPathScore(path25, opts.hopPenalty);
10947
+ const graphPath = toRetrievalGraphPathDebug(path27, titleByEntityId);
10948
+ const score = graphPathScore(path27, 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-${path27.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(([path27, value]) => {
11298
+ const actual = path27.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(path27, titleByEntityId) {
11350
+ const firstStep = path27.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: path27.target.id,
11357
+ targetType: path27.target.type,
11358
+ hops: path27.hops,
11359
+ relationPath: path27.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(path27, hopPenalty) {
11363
+ const base = Math.min(0.95, Math.max(0, path27.scoreContribution));
11364
+ return Math.max(0.05, base - hopPenalty * Math.max(0, path27.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 path27 of [...existing, ...incoming]) {
11373
+ const key = [path27.startEntityId, path27.targetType, path27.targetId, path27.hops, ...path27.relationPath].join("\0");
11374
+ if (!byKey.has(key)) byKey.set(key, path27);
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(path27) {
11379
+ return [path27.startEntityId, path27.targetType, path27.targetId, path27.hops, ...path27.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 path19 from "path";
16354
+ import * as fs17 from "fs";
16058
16355
  import { createHmac, timingSafeEqual } from "crypto";
16059
16356
  import { fileURLToPath as fileUrlToPath } from "url";
16060
16357
 
@@ -16065,6 +16362,11 @@ import { Hono as Hono11 } from "hono";
16065
16362
  import { Hono } from "hono";
16066
16363
 
16067
16364
  // src/apps/server/api/utils.ts
16365
+ function jsonError(c, error, options = {}) {
16366
+ const status = options.status ?? 500;
16367
+ console.error(`[api] ${c.req.method} ${c.req.path} failed:`, error);
16368
+ return c.json({ error: options.message ?? "Internal server error" }, status);
16369
+ }
16068
16370
  function getServiceFromQuery(c) {
16069
16371
  const project = c.req.query("project") || c.req.query("projectId");
16070
16372
  if (project) {
@@ -16176,7 +16478,7 @@ sessionsRouter.get("/", async (c) => {
16176
16478
  hasMore: end < total
16177
16479
  });
16178
16480
  } catch (error) {
16179
- return c.json({ error: error.message }, 500);
16481
+ return jsonError(c, error);
16180
16482
  } finally {
16181
16483
  await memoryService.shutdown();
16182
16484
  }
@@ -16212,7 +16514,7 @@ sessionsRouter.get("/:id", async (c) => {
16212
16514
  stats: eventsByType
16213
16515
  });
16214
16516
  } catch (error) {
16215
- return c.json({ error: error.message }, 500);
16517
+ return jsonError(c, error);
16216
16518
  } finally {
16217
16519
  await memoryService.shutdown();
16218
16520
  }
@@ -16278,7 +16580,7 @@ eventsRouter.get("/", async (c) => {
16278
16580
  hasMore: offset + limit < total
16279
16581
  });
16280
16582
  } catch (error) {
16281
- return c.json({ error: error.message }, 500);
16583
+ return jsonError(c, error);
16282
16584
  } finally {
16283
16585
  await memoryService.shutdown();
16284
16586
  }
@@ -16288,12 +16590,11 @@ eventsRouter.get("/:id", async (c) => {
16288
16590
  const memoryService = getLightweightServiceFromQuery(c);
16289
16591
  try {
16290
16592
  await memoryService.initialize();
16291
- const recentEvents = await memoryService.getRecentEvents(1e4);
16292
- const event = recentEvents.find((e) => e.id === id);
16593
+ const event = await memoryService.getEvent(id);
16293
16594
  if (!event) {
16294
16595
  return c.json({ error: "Event not found" }, 404);
16295
16596
  }
16296
- const sessionEvents = recentEvents.filter((e) => e.sessionId === event.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
16597
+ const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
16297
16598
  const eventIndex = sessionEvents.findIndex((e) => e.id === id);
16298
16599
  const start = Math.max(0, eventIndex - 2);
16299
16600
  const end = Math.min(sessionEvents.length, eventIndex + 3);
@@ -16315,7 +16616,7 @@ eventsRouter.get("/:id", async (c) => {
16315
16616
  }))
16316
16617
  });
16317
16618
  } catch (error) {
16318
- return c.json({ error: error.message }, 500);
16619
+ return jsonError(c, error);
16319
16620
  } finally {
16320
16621
  await memoryService.shutdown();
16321
16622
  }
@@ -16362,7 +16663,7 @@ searchRouter.post("/", async (c) => {
16362
16663
  }
16363
16664
  });
16364
16665
  } catch (error) {
16365
- return c.json({ error: error.message }, 500);
16666
+ return jsonError(c, error);
16366
16667
  } finally {
16367
16668
  await memoryService.shutdown();
16368
16669
  }
@@ -16405,7 +16706,7 @@ searchRouter.post("/disclosure", async (c) => {
16405
16706
  throw error;
16406
16707
  }
16407
16708
  } catch (error) {
16408
- return c.json({ error: error.message }, 500);
16709
+ return jsonError(c, error);
16409
16710
  } finally {
16410
16711
  await memoryService?.shutdown();
16411
16712
  }
@@ -16425,7 +16726,7 @@ searchRouter.get("/disclosure/:resultId/expand", async (c) => {
16425
16726
  }
16426
16727
  return c.json(result);
16427
16728
  } catch (error) {
16428
- return c.json({ error: error.message }, 500);
16729
+ return jsonError(c, error);
16429
16730
  } finally {
16430
16731
  await memoryService.shutdown();
16431
16732
  }
@@ -16440,7 +16741,7 @@ searchRouter.get("/disclosure/:resultId/source", async (c) => {
16440
16741
  }
16441
16742
  return c.json(result);
16442
16743
  } catch (error) {
16443
- return c.json({ error: error.message }, 500);
16744
+ return jsonError(c, error);
16444
16745
  } finally {
16445
16746
  await memoryService.shutdown();
16446
16747
  }
@@ -16469,7 +16770,7 @@ searchRouter.get("/", async (c) => {
16469
16770
  }
16470
16771
  });
16471
16772
  } catch (error) {
16472
- return c.json({ error: error.message }, 500);
16773
+ return jsonError(c, error);
16473
16774
  } finally {
16474
16775
  await memoryService.shutdown();
16475
16776
  }
@@ -16477,9 +16778,89 @@ searchRouter.get("/", async (c) => {
16477
16778
 
16478
16779
  // src/apps/server/api/stats.ts
16479
16780
  import { Hono as Hono4 } from "hono";
16480
- import * as fs13 from "fs";
16781
+ import * as fs14 from "fs";
16481
16782
  import * as os9 from "os";
16783
+ import * as path16 from "path";
16784
+
16785
+ // src/apps/server/api/stats-metrics.ts
16786
+ import * as fs13 from "fs";
16482
16787
  import * as path15 from "path";
16788
+ var DEFAULT_KPI_THRESHOLDS = {
16789
+ usefulRecallRateMin: 0.45,
16790
+ reworkRateMax: 0.25,
16791
+ postChangeFailureRateMax: 0.2,
16792
+ avgCompletionTurnsMax: 12,
16793
+ memoryHitRateMin: 0.35
16794
+ };
16795
+ function loadKpiThresholds() {
16796
+ try {
16797
+ const filePath = path15.resolve(process.cwd(), "config", "kpi-thresholds.json");
16798
+ if (!fs13.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
16799
+ const parsed = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
16800
+ return {
16801
+ usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
16802
+ reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
16803
+ postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
16804
+ avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
16805
+ memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
16806
+ };
16807
+ } catch {
16808
+ return DEFAULT_KPI_THRESHOLDS;
16809
+ }
16810
+ }
16811
+ function windowToMs(window) {
16812
+ if (window === "24h") return 24 * 60 * 60 * 1e3;
16813
+ if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
16814
+ return 30 * 24 * 60 * 60 * 1e3;
16815
+ }
16816
+ function inWindow(e, now, window) {
16817
+ return now - e.timestamp.getTime() <= windowToMs(window);
16818
+ }
16819
+ function isEditToolName(name) {
16820
+ return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
16821
+ }
16822
+ function parseToolPayload(e) {
16823
+ if (e.eventType !== "tool_observation") return null;
16824
+ try {
16825
+ const payload = JSON.parse(e.content);
16826
+ return {
16827
+ toolName: payload?.toolName,
16828
+ success: payload?.success,
16829
+ filePath: payload?.metadata?.filePath,
16830
+ command: payload?.metadata?.command
16831
+ };
16832
+ } catch {
16833
+ return {
16834
+ toolName: e.metadata?.toolName,
16835
+ success: e.metadata?.success,
16836
+ filePath: e.metadata?.filePath,
16837
+ command: e.metadata?.command
16838
+ };
16839
+ }
16840
+ }
16841
+ function isTestLikeCommand(command) {
16842
+ if (!command) return false;
16843
+ return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
16844
+ }
16845
+ function safeRatio(num, den) {
16846
+ if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
16847
+ return num / den;
16848
+ }
16849
+ function round2(value, digits = 4) {
16850
+ const factor = 10 ** digits;
16851
+ return Math.round(value * factor) / factor;
16852
+ }
16853
+ function computeSessionTurnCount(sessionEvents) {
16854
+ const turnIds = /* @__PURE__ */ new Set();
16855
+ for (const e of sessionEvents) {
16856
+ const turnId = e.metadata?.turnId;
16857
+ if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
16858
+ }
16859
+ if (turnIds.size > 0) return turnIds.size;
16860
+ return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
16861
+ }
16862
+
16863
+ // src/apps/server/api/stats.ts
16483
16864
  var statsRouter = new Hono4();
16484
16865
  var OPERATION_STATS_TABLES = [
16485
16866
  "memory_facets",
@@ -16506,7 +16887,7 @@ function operationsStatsHomeDir() {
16506
16887
  }
16507
16888
  function projectStoragePathForOperationsStats(projectOrHash) {
16508
16889
  const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
16509
- return path15.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
16890
+ return path16.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
16510
16891
  }
16511
16892
  function getOperationsStatsContext(project) {
16512
16893
  if (project && project.trim().length > 0) {
@@ -16516,13 +16897,13 @@ function getOperationsStatsContext(project) {
16516
16897
  return {
16517
16898
  projectHash,
16518
16899
  storagePath: storagePath2,
16519
- dbPath: path15.join(storagePath2, "events.sqlite")
16900
+ dbPath: path16.join(storagePath2, "events.sqlite")
16520
16901
  };
16521
16902
  }
16522
- const storagePath = path15.join(operationsStatsHomeDir(), ".claude-code", "memory");
16903
+ const storagePath = path16.join(operationsStatsHomeDir(), ".claude-code", "memory");
16523
16904
  return {
16524
16905
  storagePath,
16525
- dbPath: path15.join(storagePath, "events.sqlite")
16906
+ dbPath: path16.join(storagePath, "events.sqlite")
16526
16907
  };
16527
16908
  }
16528
16909
  function countRowValue(db, sql, params = []) {
@@ -16540,6 +16921,23 @@ function sanitizeAggregateLabel(value) {
16540
16921
  const trimmed = sanitized.trim();
16541
16922
  return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
16542
16923
  }
16924
+ var SAFE_RETRIEVAL_TRACE_STRATEGIES = /* @__PURE__ */ new Set([
16925
+ "auto",
16926
+ "deep",
16927
+ "fast",
16928
+ "hybrid",
16929
+ "keyword",
16930
+ "semantic",
16931
+ "mcp-context-pack",
16932
+ "session-start-hook",
16933
+ "unknown"
16934
+ ]);
16935
+ function normalizeRetrievalTraceStrategy(value) {
16936
+ const label = sanitizeAggregateLabel(value);
16937
+ if (label === "[REDACTED]") return "unknown";
16938
+ const normalized = label.trim().toLowerCase();
16939
+ return SAFE_RETRIEVAL_TRACE_STRATEGIES.has(normalized) ? normalized : "unknown";
16940
+ }
16543
16941
  function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
16544
16942
  return {
16545
16943
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16953,80 +17351,6 @@ function buildLessonConfidenceBuckets(db, projectHash) {
16953
17351
  }).length
16954
17352
  }));
16955
17353
  }
16956
- var DEFAULT_KPI_THRESHOLDS = {
16957
- usefulRecallRateMin: 0.45,
16958
- reworkRateMax: 0.25,
16959
- postChangeFailureRateMax: 0.2,
16960
- avgCompletionTurnsMax: 12,
16961
- memoryHitRateMin: 0.35
16962
- };
16963
- function loadKpiThresholds() {
16964
- try {
16965
- const filePath = path15.resolve(process.cwd(), "config", "kpi-thresholds.json");
16966
- if (!fs13.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
16967
- const parsed = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
16968
- return {
16969
- usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
16970
- reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
16971
- postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
16972
- avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
16973
- memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
16974
- };
16975
- } catch {
16976
- return DEFAULT_KPI_THRESHOLDS;
16977
- }
16978
- }
16979
- function windowToMs(window) {
16980
- if (window === "24h") return 24 * 60 * 60 * 1e3;
16981
- if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
16982
- return 30 * 24 * 60 * 60 * 1e3;
16983
- }
16984
- function inWindow(e, now, window) {
16985
- return now - e.timestamp.getTime() <= windowToMs(window);
16986
- }
16987
- function isEditToolName(name) {
16988
- return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
16989
- }
16990
- function parseToolPayload(e) {
16991
- if (e.eventType !== "tool_observation") return null;
16992
- try {
16993
- const payload = JSON.parse(e.content);
16994
- return {
16995
- toolName: payload?.toolName,
16996
- success: payload?.success,
16997
- filePath: payload?.metadata?.filePath,
16998
- command: payload?.metadata?.command
16999
- };
17000
- } catch {
17001
- return {
17002
- toolName: e.metadata?.toolName,
17003
- success: e.metadata?.success,
17004
- filePath: e.metadata?.filePath,
17005
- command: e.metadata?.command
17006
- };
17007
- }
17008
- }
17009
- function isTestLikeCommand(command) {
17010
- if (!command) return false;
17011
- return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
17012
- }
17013
- function safeRatio(num, den) {
17014
- if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
17015
- return num / den;
17016
- }
17017
- function round2(value, digits = 4) {
17018
- const factor = 10 ** digits;
17019
- return Math.round(value * factor) / factor;
17020
- }
17021
- function computeSessionTurnCount(sessionEvents) {
17022
- const turnIds = /* @__PURE__ */ new Set();
17023
- for (const e of sessionEvents) {
17024
- const turnId = e.metadata?.turnId;
17025
- if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
17026
- }
17027
- if (turnIds.size > 0) return turnIds.size;
17028
- return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
17029
- }
17030
17354
  function normalizeQueryRewriteKind2(value) {
17031
17355
  const normalized = (value || "").trim().toLowerCase();
17032
17356
  if (normalized === "follow-up-context" || normalized === "intent-rewrite") return normalized;
@@ -17054,6 +17378,63 @@ function getTraceSelectedCount(trace) {
17054
17378
  function getTraceCandidateCount(trace) {
17055
17379
  return Number(trace.candidateCount ?? trace.candidateEventIds?.length ?? trace.candidateDetails?.length ?? 0);
17056
17380
  }
17381
+ function normalizeStatsNumber(value) {
17382
+ const numberValue = Number(value || 0);
17383
+ return Number.isFinite(numberValue) ? Math.max(0, numberValue) : 0;
17384
+ }
17385
+ function sanitizeRetrievalTraceStrategyBreakdown(value) {
17386
+ if (!Array.isArray(value)) return [];
17387
+ const byStrategy = /* @__PURE__ */ new Map();
17388
+ for (const item of value) {
17389
+ const row = item && typeof item === "object" ? item : {};
17390
+ const strategy = normalizeRetrievalTraceStrategy(row.strategy ?? "unknown");
17391
+ const bucket = byStrategy.get(strategy) ?? {
17392
+ strategy,
17393
+ totalQueries: 0,
17394
+ queriesWithSelection: 0,
17395
+ rewrittenQueries: 0,
17396
+ totalCandidateCount: 0,
17397
+ totalSelectedCount: 0
17398
+ };
17399
+ bucket.totalQueries += normalizeStatsNumber(row.totalQueries);
17400
+ bucket.queriesWithSelection += normalizeStatsNumber(row.queriesWithSelection);
17401
+ bucket.rewrittenQueries += normalizeStatsNumber(row.rewrittenQueries);
17402
+ bucket.totalCandidateCount += normalizeStatsNumber(row.totalCandidateCount);
17403
+ bucket.totalSelectedCount += normalizeStatsNumber(row.totalSelectedCount);
17404
+ byStrategy.set(strategy, bucket);
17405
+ }
17406
+ return Array.from(byStrategy.values()).map((row) => ({
17407
+ strategy: row.strategy,
17408
+ totalQueries: row.totalQueries,
17409
+ queriesWithSelection: row.queriesWithSelection,
17410
+ rewrittenQueries: row.rewrittenQueries,
17411
+ rewriteRate: normalizeMetric(safeRatio(row.rewrittenQueries, row.totalQueries)),
17412
+ totalCandidateCount: row.totalCandidateCount,
17413
+ totalSelectedCount: row.totalSelectedCount,
17414
+ avgCandidateCount: safeRatio(row.totalCandidateCount, row.totalQueries),
17415
+ avgSelectedCount: safeRatio(row.totalSelectedCount, row.totalQueries),
17416
+ selectionRate: normalizeMetric(safeRatio(row.totalSelectedCount, row.totalCandidateCount)),
17417
+ queryYieldRate: normalizeMetric(safeRatio(row.queriesWithSelection, row.totalQueries))
17418
+ }));
17419
+ }
17420
+ function sanitizeRetrievalTraceStats(stats) {
17421
+ const s = stats && typeof stats === "object" ? stats : {};
17422
+ return {
17423
+ totalQueries: Number(s.totalQueries || 0),
17424
+ avgCandidateCount: Number(s.avgCandidateCount || 0),
17425
+ avgSelectedCount: Number(s.avgSelectedCount || 0),
17426
+ selectionRate: normalizeMetric(s.selectionRate),
17427
+ rewrittenQueries: Number(s.rewrittenQueries || 0),
17428
+ rewriteRate: normalizeMetric(s.rewriteRate),
17429
+ rewrittenQueriesWithSelection: Number(s.rewrittenQueriesWithSelection || 0),
17430
+ rawQueriesWithSelection: Number(s.rawQueriesWithSelection || 0),
17431
+ rewrittenSelectionRate: normalizeMetric(s.rewrittenSelectionRate),
17432
+ rawSelectionRate: normalizeMetric(s.rawSelectionRate),
17433
+ avgSelectedCountForRewrittenQueries: Number(s.avgSelectedCountForRewrittenQueries || 0),
17434
+ avgSelectedCountForRawQueries: Number(s.avgSelectedCountForRawQueries || 0),
17435
+ strategyBreakdown: sanitizeRetrievalTraceStrategyBreakdown(s.strategyBreakdown)
17436
+ };
17437
+ }
17057
17438
  function makeRetrievalReviewItem(trace) {
17058
17439
  const candidateCount = getTraceCandidateCount(trace);
17059
17440
  const selectedCount = getTraceSelectedCount(trace);
@@ -17107,7 +17488,7 @@ function makeRetrievalReviewItem(trace) {
17107
17488
  action,
17108
17489
  queryRewriteKind,
17109
17490
  rewritten,
17110
- strategy: trace.strategy || null,
17491
+ strategy: normalizeRetrievalTraceStrategy(trace.strategy ?? "unknown"),
17111
17492
  candidateCount,
17112
17493
  selectedCount,
17113
17494
  candidateEventIds: (trace.candidateEventIds || []).slice(0, 5),
@@ -17534,7 +17915,7 @@ statsRouter.get("/levels/:level", async (c) => {
17534
17915
  hasMore: events.length === limit
17535
17916
  });
17536
17917
  } catch (error) {
17537
- return c.json({ error: error.message }, 500);
17918
+ return jsonError(c, error);
17538
17919
  } finally {
17539
17920
  await memoryService.shutdown();
17540
17921
  }
@@ -17543,7 +17924,7 @@ statsRouter.get("/operations", async (c) => {
17543
17924
  const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
17544
17925
  const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
17545
17926
  const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
17546
- const databaseExists = fs13.existsSync(context.dbPath);
17927
+ const databaseExists = fs14.existsSync(context.dbPath);
17547
17928
  if (!databaseExists) {
17548
17929
  return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
17549
17930
  }
@@ -17631,7 +18012,7 @@ statsRouter.get("/perspective", async (c) => {
17631
18012
  const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
17632
18013
  const contradictionLimit = parseStatsLimit(c.req.query("contradictionLimit"), 10, 50);
17633
18014
  const graphLimit = parseStatsLimit(c.req.query("graphLimit"), 10, 50);
17634
- const databaseExists = fs13.existsSync(context.dbPath);
18015
+ const databaseExists = fs14.existsSync(context.dbPath);
17635
18016
  if (!databaseExists) {
17636
18017
  return c.json(emptyPerspectiveStatsPayload(context, false, [...PERSPECTIVE_STATS_TABLES], windowDays));
17637
18018
  }
@@ -17707,33 +18088,29 @@ statsRouter.get("/", async (c) => {
17707
18088
  const memoryService = getLightweightServiceFromQuery(c);
17708
18089
  try {
17709
18090
  await memoryService.initialize();
17710
- const stats = await memoryService.getStats();
17711
- const recentEvents = await memoryService.getRecentEvents(1e4);
17712
- const eventsByType = recentEvents.reduce((acc, e) => {
17713
- acc[e.eventType] = (acc[e.eventType] || 0) + 1;
17714
- return acc;
17715
- }, {});
17716
- const uniqueSessions = new Set(recentEvents.map((e) => e.sessionId));
17717
- const now = /* @__PURE__ */ new Date();
17718
- const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
17719
- const eventsByDay = recentEvents.filter((e) => e.timestamp >= sevenDaysAgo).reduce((acc, e) => {
17720
- const day = e.timestamp.toISOString().split("T")[0];
17721
- acc[day] = (acc[day] || 0) + 1;
17722
- return acc;
17723
- }, {});
17724
- const retrievalTrace = await memoryService.getRetrievalTraceStats();
18091
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
18092
+ const [stats, typeCounts, sessionTotal, dailyCounts, rawTrace] = await Promise.all([
18093
+ memoryService.getStats(),
18094
+ memoryService.getEventTypeCounts(),
18095
+ memoryService.getDistinctSessionCount(),
18096
+ memoryService.getDailyEventCounts(sevenDaysAgo),
18097
+ memoryService.getRetrievalTraceStats()
18098
+ ]);
18099
+ const eventsByType = Object.fromEntries(typeCounts.map((t) => [t.eventType, t.count]));
18100
+ const eventsByDay = Object.fromEntries(dailyCounts.map((d) => [d.day, d.total]));
18101
+ const retrievalTrace = sanitizeRetrievalTraceStats(rawTrace);
17725
18102
  return c.json({
17726
18103
  storage: {
17727
18104
  eventCount: stats.totalEvents,
17728
18105
  vectorCount: stats.vectorCount
17729
18106
  },
17730
18107
  sessions: {
17731
- total: uniqueSessions.size
18108
+ total: sessionTotal
17732
18109
  },
17733
18110
  eventsByType,
17734
18111
  activity: {
17735
18112
  daily: eventsByDay,
17736
- total7Days: recentEvents.filter((e) => e.timestamp >= sevenDaysAgo).length
18113
+ total7Days: dailyCounts.reduce((sum, d) => sum + d.total, 0)
17737
18114
  },
17738
18115
  memory: {
17739
18116
  heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
@@ -17743,7 +18120,7 @@ statsRouter.get("/", async (c) => {
17743
18120
  retrievalTrace
17744
18121
  });
17745
18122
  } catch (error) {
17746
- return c.json({ error: error.message }, 500);
18123
+ return jsonError(c, error);
17747
18124
  } finally {
17748
18125
  await memoryService.shutdown();
17749
18126
  }
@@ -17780,30 +18157,22 @@ statsRouter.get("/most-accessed", async (c) => {
17780
18157
  }
17781
18158
  });
17782
18159
  statsRouter.get("/timeline", async (c) => {
17783
- const days = parseInt(c.req.query("days") || "7", 10);
18160
+ const parsedDays = parseInt(c.req.query("days") || "7", 10);
18161
+ const days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 7;
17784
18162
  const memoryService = getLightweightServiceFromQuery(c);
17785
18163
  try {
17786
18164
  await memoryService.initialize();
17787
- const recentEvents = await memoryService.getRecentEvents(1e4);
17788
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
17789
- const filteredEvents = recentEvents.filter((e) => e.timestamp >= cutoff);
17790
- const daily = filteredEvents.reduce((acc, e) => {
17791
- const day = e.timestamp.toISOString().split("T")[0];
17792
- if (!acc[day]) {
17793
- acc[day] = { date: day, total: 0, prompts: 0, responses: 0, tools: 0 };
17794
- }
17795
- acc[day].total++;
17796
- if (e.eventType === "user_prompt") acc[day].prompts++;
17797
- if (e.eventType === "agent_response") acc[day].responses++;
17798
- if (e.eventType === "tool_observation") acc[day].tools++;
17799
- return acc;
17800
- }, {});
17801
- return c.json({
17802
- days,
17803
- daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
17804
- });
18165
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
18166
+ const daily = (await memoryService.getDailyEventCounts(cutoff)).map((d) => ({
18167
+ date: d.day,
18168
+ total: d.total,
18169
+ prompts: d.prompts,
18170
+ responses: d.responses,
18171
+ tools: d.tools
18172
+ }));
18173
+ return c.json({ days, daily });
17805
18174
  } catch (error) {
17806
- return c.json({ error: error.message }, 500);
18175
+ return jsonError(c, error);
17807
18176
  } finally {
17808
18177
  await memoryService.shutdown();
17809
18178
  }
@@ -17846,16 +18215,14 @@ statsRouter.get("/usefulness", async (c) => {
17846
18215
  try {
17847
18216
  await memoryService.initialize();
17848
18217
  const now = Date.now();
17849
- const eventLimit = 2e4;
17850
18218
  const traceLimit = 5e3;
17851
18219
  const windowStart = new Date(now - windowToMs(window));
17852
18220
  const [events, helpfulness, traces] = await Promise.all([
17853
- memoryService.getRecentEvents(eventLimit),
18221
+ memoryService.getEventsAfter(windowStart.toISOString()),
17854
18222
  memoryService.getHelpfulnessStats(windowStart),
17855
18223
  memoryService.getRecentRetrievalTraces(traceLimit)
17856
18224
  ]);
17857
18225
  return c.json(computeMemoryUsefulnessSummary(events, helpfulness, traces, now, window, {
17858
- eventsLimit: eventLimit,
17859
18226
  tracesLimit: traceLimit
17860
18227
  }));
17861
18228
  } catch (error) {
@@ -17873,7 +18240,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
17873
18240
  const traces = await memoryService.getRecentRetrievalTraces(limit);
17874
18241
  const traceStats = await memoryService.getRetrievalTraceStats();
17875
18242
  return c.json({
17876
- stats: traceStats,
18243
+ stats: sanitizeRetrievalTraceStats(traceStats),
17877
18244
  traces: traces.map((t) => {
17878
18245
  const queryRewriteKind = normalizeQueryRewriteKind2(t.queryRewriteKind);
17879
18246
  return {
@@ -17882,7 +18249,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
17882
18249
  projectHash: t.projectHash || null,
17883
18250
  queryRewriteKind,
17884
18251
  rewritten: queryRewriteKind !== "none",
17885
- strategy: t.strategy || null,
18252
+ strategy: normalizeRetrievalTraceStrategy(t.strategy ?? "unknown"),
17886
18253
  candidateEventIds: t.candidateEventIds,
17887
18254
  selectedEventIds: t.selectedEventIds,
17888
18255
  candidateDetails: t.candidateDetails || [],
@@ -17909,7 +18276,8 @@ statsRouter.get("/retrieval-traces", async (c) => {
17909
18276
  rewrittenSelectionRate: 0,
17910
18277
  rawSelectionRate: 0,
17911
18278
  avgSelectedCountForRewrittenQueries: 0,
17912
- avgSelectedCountForRawQueries: 0
18279
+ avgSelectedCountForRawQueries: 0,
18280
+ strategyBreakdown: []
17913
18281
  },
17914
18282
  traces: [],
17915
18283
  error: error.message
@@ -17960,7 +18328,8 @@ statsRouter.get("/kpi", async (c) => {
17960
18328
  await memoryService.initialize();
17961
18329
  const now = Date.now();
17962
18330
  const thresholds = loadKpiThresholds();
17963
- const allEvents = await memoryService.getRecentEvents(2e4);
18331
+ const lookbackMs = Math.max(2 * windowToMs(window), 30 * 24 * 60 * 60 * 1e3);
18332
+ const allEvents = await memoryService.getEventsAfter(new Date(now - lookbackMs).toISOString());
17964
18333
  const events = allEvents.filter((e) => inWindow(e, now, window));
17965
18334
  const helpfulness = await memoryService.getHelpfulnessStats();
17966
18335
  const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round2(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
@@ -18069,7 +18438,7 @@ statsRouter.get("/kpi", async (c) => {
18069
18438
  alerts
18070
18439
  });
18071
18440
  } catch (error) {
18072
- return c.json({ error: error.message }, 500);
18441
+ return jsonError(c, error);
18073
18442
  } finally {
18074
18443
  await memoryService.shutdown();
18075
18444
  }
@@ -18120,11 +18489,7 @@ citationsRouter.get("/:id", async (c) => {
18120
18489
  const memoryService = getServiceFromQuery(c);
18121
18490
  try {
18122
18491
  await memoryService.initialize();
18123
- const recentEvents = await memoryService.getRecentEvents(1e4);
18124
- const event = recentEvents.find((e) => {
18125
- const eventCitationId = generateCitationId(e.id);
18126
- return eventCitationId === citationId;
18127
- });
18492
+ const event = await memoryService.getEventByCitationId(citationId);
18128
18493
  if (!event) {
18129
18494
  return c.json({ error: "Citation not found" }, 404);
18130
18495
  }
@@ -18143,7 +18508,7 @@ citationsRouter.get("/:id", async (c) => {
18143
18508
  }
18144
18509
  });
18145
18510
  } catch (error) {
18146
- return c.json({ error: error.message }, 500);
18511
+ return jsonError(c, error);
18147
18512
  } finally {
18148
18513
  await memoryService.shutdown();
18149
18514
  }
@@ -18154,15 +18519,11 @@ citationsRouter.get("/:id/related", async (c) => {
18154
18519
  const memoryService = getServiceFromQuery(c);
18155
18520
  try {
18156
18521
  await memoryService.initialize();
18157
- const recentEvents = await memoryService.getRecentEvents(1e4);
18158
- const event = recentEvents.find((e) => {
18159
- const eventCitationId = generateCitationId(e.id);
18160
- return eventCitationId === citationId;
18161
- });
18522
+ const event = await memoryService.getEventByCitationId(citationId);
18162
18523
  if (!event) {
18163
18524
  return c.json({ error: "Citation not found" }, 404);
18164
18525
  }
18165
- const sessionEvents = recentEvents.filter((e) => e.sessionId === event.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
18526
+ const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
18166
18527
  const eventIndex = sessionEvents.findIndex((e) => e.id === event.id);
18167
18528
  const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
18168
18529
  const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
@@ -18181,7 +18542,7 @@ citationsRouter.get("/:id/related", async (c) => {
18181
18542
  } : null
18182
18543
  });
18183
18544
  } catch (error) {
18184
- return c.json({ error: error.message }, 500);
18545
+ return jsonError(c, error);
18185
18546
  } finally {
18186
18547
  await memoryService.shutdown();
18187
18548
  }
@@ -18224,7 +18585,7 @@ turnsRouter.get("/", async (c) => {
18224
18585
  hasMore: offset + limit < totalTurns
18225
18586
  });
18226
18587
  } catch (error) {
18227
- return c.json({ error: error.message }, 500);
18588
+ return jsonError(c, error);
18228
18589
  } finally {
18229
18590
  await memoryService.shutdown();
18230
18591
  }
@@ -18275,7 +18636,7 @@ turnsRouter.get("/:turnId", async (c) => {
18275
18636
  totalEvents: events.length
18276
18637
  });
18277
18638
  } catch (error) {
18278
- return c.json({ error: error.message }, 500);
18639
+ return jsonError(c, error);
18279
18640
  } finally {
18280
18641
  await memoryService.shutdown();
18281
18642
  }
@@ -18302,8 +18663,8 @@ turnsRouter.post("/backfill", async (c) => {
18302
18663
 
18303
18664
  // src/apps/server/api/projects.ts
18304
18665
  import { Hono as Hono7 } from "hono";
18305
- import * as fs14 from "fs";
18306
- import * as path16 from "path";
18666
+ import * as fs15 from "fs";
18667
+ import * as path17 from "path";
18307
18668
  import * as os10 from "os";
18308
18669
  var projectsRouter = new Hono7();
18309
18670
  projectsRouter.get("/:hash/detail", async (c) => {
@@ -18332,7 +18693,7 @@ projectsRouter.get("/:hash/detail", async (c) => {
18332
18693
  return c.json({
18333
18694
  project: {
18334
18695
  hash,
18335
- projectName: projectPath ? path16.basename(projectPath) : `unknown (${hash})`,
18696
+ projectName: projectPath ? path17.basename(projectPath) : `unknown (${hash})`,
18336
18697
  registered: Boolean(projectPath)
18337
18698
  },
18338
18699
  storage: {
@@ -18360,13 +18721,13 @@ projectsRouter.get("/:hash/detail", async (c) => {
18360
18721
  });
18361
18722
  projectsRouter.get("/", async (c) => {
18362
18723
  try {
18363
- const projectsDir = path16.join(os10.homedir(), ".claude-code", "memory", "projects");
18364
- if (!fs14.existsSync(projectsDir)) {
18724
+ const projectsDir = path17.join(os10.homedir(), ".claude-code", "memory", "projects");
18725
+ if (!fs15.existsSync(projectsDir)) {
18365
18726
  return c.json({ projects: [] });
18366
18727
  }
18367
- const projectHashes = fs14.readdirSync(projectsDir).filter((name) => {
18368
- const fullPath = path16.join(projectsDir, name);
18369
- return fs14.statSync(fullPath).isDirectory();
18728
+ const projectHashes = fs15.readdirSync(projectsDir).filter((name) => {
18729
+ const fullPath = path17.join(projectsDir, name);
18730
+ return fs15.statSync(fullPath).isDirectory();
18370
18731
  });
18371
18732
  const registry = loadSessionRegistry();
18372
18733
  const hashToPath = /* @__PURE__ */ new Map();
@@ -18376,17 +18737,17 @@ projectsRouter.get("/", async (c) => {
18376
18737
  }
18377
18738
  }
18378
18739
  const projects = projectHashes.map((hash) => {
18379
- const dirPath = path16.join(projectsDir, hash);
18380
- const dbPath = path16.join(dirPath, "events.sqlite");
18740
+ const dirPath = path17.join(projectsDir, hash);
18741
+ const dbPath = path17.join(dirPath, "events.sqlite");
18381
18742
  let dbSize = 0;
18382
- if (fs14.existsSync(dbPath)) {
18383
- dbSize = fs14.statSync(dbPath).size;
18743
+ if (fs15.existsSync(dbPath)) {
18744
+ dbSize = fs15.statSync(dbPath).size;
18384
18745
  }
18385
18746
  const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
18386
18747
  return {
18387
18748
  hash,
18388
18749
  projectPath,
18389
- projectName: path16.basename(projectPath),
18750
+ projectName: path17.basename(projectPath),
18390
18751
  dbSize,
18391
18752
  dbSizeHuman: formatBytes(dbSize)
18392
18753
  };
@@ -18515,7 +18876,7 @@ chatRouter.post("/", async (c) => {
18515
18876
  }
18516
18877
  });
18517
18878
  } catch (error) {
18518
- return c.json({ error: error.message }, 500);
18879
+ return jsonError(c, error);
18519
18880
  } finally {
18520
18881
  await memoryService.shutdown();
18521
18882
  }
@@ -18739,6 +19100,150 @@ function streamClaudeResponse(prompt, stream) {
18739
19100
  // src/apps/server/api/health.ts
18740
19101
  import { Hono as Hono9 } from "hono";
18741
19102
  import { spawnSync } from "child_process";
19103
+ import * as fs16 from "fs";
19104
+ import * as path18 from "path";
19105
+
19106
+ // src/core/productivity-health-report.ts
19107
+ var PRODUCTIVITY_HEALTH_SCHEMA_VERSION = "agent-productivity-health-v1";
19108
+ var PRODUCTIVITY_HEALTH_PROFILES = ["coder", "reviewer", "pm", "support", "researcher", "team"];
19109
+ var PRODUCTIVITY_HEALTH_MODES = ["observe", "preview", "enforce"];
19110
+ function parseProductivityHealthProfile(value) {
19111
+ const normalized = typeof value === "string" && value.trim().length > 0 ? value.trim() : "coder";
19112
+ if (PRODUCTIVITY_HEALTH_PROFILES.includes(normalized)) {
19113
+ return normalized;
19114
+ }
19115
+ throw new Error(`Invalid --profile: expected one of ${PRODUCTIVITY_HEALTH_PROFILES.join(", ")}`);
19116
+ }
19117
+ function parseProductivityHealthMode(value) {
19118
+ const normalized = typeof value === "string" && value.trim().length > 0 ? value.trim() : "preview";
19119
+ if (PRODUCTIVITY_HEALTH_MODES.includes(normalized)) {
19120
+ return normalized;
19121
+ }
19122
+ throw new Error(`Invalid --mode: expected one of ${PRODUCTIVITY_HEALTH_MODES.join(", ")}`);
19123
+ }
19124
+ function buildProductivityHealthReport(input) {
19125
+ const profile = input.profile ?? "coder";
19126
+ const mode = input.mode ?? "preview";
19127
+ const embedding = normalizeQueue(input.outbox.embedding);
19128
+ const vector = normalizeQueue(input.outbox.vector);
19129
+ const totals = sumQueues(embedding, vector);
19130
+ const storage = {
19131
+ totalEvents: numberOrZero(input.stats.totalEvents),
19132
+ vectorCount: numberOrZero(input.stats.vectorCount),
19133
+ levelStats: normalizeLevelStats(input.stats.levelStats)
19134
+ };
19135
+ const warningReasons = [];
19136
+ const projectGate = buildProjectScopeGate(input.project);
19137
+ const outboxGate = buildOutboxGate(totals);
19138
+ const densityGate = buildMemoryDensityGate(storage.totalEvents);
19139
+ const riskGates = [projectGate, outboxGate, densityGate];
19140
+ if (projectGate.status === "warn") warningReasons.push("project_scope_unknown");
19141
+ if (outboxGate.status === "warn") warningReasons.push("outbox_requires_attention");
19142
+ if (densityGate.status === "warn") warningReasons.push("memory_density_low");
19143
+ const status = riskGates.some((gate) => gate.status === "warn") ? "needs-attention" : "ok";
19144
+ return {
19145
+ schemaVersion: PRODUCTIVITY_HEALTH_SCHEMA_VERSION,
19146
+ generatedAt: (input.generatedAt ?? /* @__PURE__ */ new Date()).toISOString(),
19147
+ status,
19148
+ profile,
19149
+ mode,
19150
+ project: input.project,
19151
+ summary: { warningReasons },
19152
+ signals: {
19153
+ storage,
19154
+ outbox: { embedding, vector, totals }
19155
+ },
19156
+ riskGates,
19157
+ nextBestAction: selectNextBestAction({ projectGate, outboxGate, densityGate })
19158
+ };
19159
+ }
19160
+ function buildProjectScopeGate(project) {
19161
+ if (project.scope === "project") {
19162
+ return { id: "project-scope-known", severity: "blocker", status: "pass" };
19163
+ }
19164
+ return {
19165
+ id: "project-scope-known",
19166
+ severity: "blocker",
19167
+ status: "warn",
19168
+ message: "No project parameter supplied; report uses global memory scope."
19169
+ };
19170
+ }
19171
+ function buildOutboxGate(totals) {
19172
+ if (totals.failed > 0 || totals.stuckProcessing > 0) {
19173
+ return {
19174
+ id: "outbox-healthy",
19175
+ severity: "warning",
19176
+ status: "warn",
19177
+ message: "Outbox has failed or stuck processing items; run recovery before trusting retrieval freshness."
19178
+ };
19179
+ }
19180
+ return { id: "outbox-healthy", severity: "warning", status: "pass" };
19181
+ }
19182
+ function buildMemoryDensityGate(totalEvents) {
19183
+ if (totalEvents <= 0) {
19184
+ return {
19185
+ id: "memory-density",
19186
+ severity: "warning",
19187
+ status: "warn",
19188
+ message: "No memories are available for this scope yet."
19189
+ };
19190
+ }
19191
+ return { id: "memory-density", severity: "warning", status: "pass" };
19192
+ }
19193
+ function selectNextBestAction(input) {
19194
+ if (input.outboxGate.status === "warn") {
19195
+ return "Run claude-memory-layer process --dry-run-recovery, then process pending embeddings.";
19196
+ }
19197
+ if (input.densityGate.status === "warn") {
19198
+ return "Import or capture project context before relying on productivity memory guidance.";
19199
+ }
19200
+ if (input.projectGate.status === "warn") {
19201
+ return "Provide a project path or project hash before relying on project-scoped productivity guidance.";
19202
+ }
19203
+ return "No immediate maintenance action required.";
19204
+ }
19205
+ function normalizeLevelStats(levelStats) {
19206
+ if (!Array.isArray(levelStats)) return [];
19207
+ return levelStats.filter((entry) => typeof entry.level === "string" && entry.level.trim().length > 0).map((entry) => ({ level: entry.level.trim().slice(0, 20), count: numberOrZero(entry.count) }));
19208
+ }
19209
+ function normalizeQueue(queue) {
19210
+ return {
19211
+ pending: numberOrZero(queue.pending),
19212
+ processing: numberOrZero(queue.processing),
19213
+ failed: numberOrZero(queue.failed),
19214
+ retryableFailed: numberOrZero(queue.retryableFailed),
19215
+ quarantinedFailed: numberOrZero(queue.quarantinedFailed),
19216
+ total: numberOrZero(queue.total),
19217
+ stuckProcessing: numberOrZero(queue.stuckProcessing),
19218
+ oldestProcessingAgeMs: normalizeNullableAge(queue.oldestProcessingAgeMs)
19219
+ };
19220
+ }
19221
+ function sumQueues(a, b) {
19222
+ return {
19223
+ pending: a.pending + b.pending,
19224
+ processing: a.processing + b.processing,
19225
+ failed: a.failed + b.failed,
19226
+ retryableFailed: (a.retryableFailed ?? 0) + (b.retryableFailed ?? 0),
19227
+ quarantinedFailed: (a.quarantinedFailed ?? 0) + (b.quarantinedFailed ?? 0),
19228
+ total: a.total + b.total,
19229
+ stuckProcessing: a.stuckProcessing + b.stuckProcessing,
19230
+ oldestProcessingAgeMs: maxNullable(a.oldestProcessingAgeMs, b.oldestProcessingAgeMs)
19231
+ };
19232
+ }
19233
+ function normalizeNullableAge(value) {
19234
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
19235
+ }
19236
+ function maxNullable(a, b) {
19237
+ if (a === null) return b;
19238
+ if (b === null) return a;
19239
+ return Math.max(a, b);
19240
+ }
19241
+ function numberOrZero(value) {
19242
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
19243
+ return Math.floor(value);
19244
+ }
19245
+
19246
+ // src/apps/server/api/health.ts
18742
19247
  var healthRouter = new Hono9();
18743
19248
  function hasEnvSignal(name) {
18744
19249
  const value = process.env[name];
@@ -18779,6 +19284,33 @@ function aggregateOutbox(outbox) {
18779
19284
  const stuckProcessing = (outbox.embedding?.stuckProcessing || 0) + (outbox.vector?.stuckProcessing || 0);
18780
19285
  return { pending, processing, failed, retryableFailed, quarantinedFailed, stuckProcessing };
18781
19286
  }
19287
+ function resolveProductivityProjectIdentity(project) {
19288
+ if (!project || project.trim().length === 0) return { scope: "global", id: "global" };
19289
+ const normalized = project.trim();
19290
+ return { scope: "project", id: /^[a-f0-9]{8}$/.test(normalized) ? normalized : hashProjectPath(normalized) };
19291
+ }
19292
+ function explicitProjectStoreExists(project) {
19293
+ if (!project || project.trim().length === 0) return true;
19294
+ return fs16.existsSync(path18.join(resolveProjectStoragePath(project.trim()), "events.sqlite"));
19295
+ }
19296
+ function emptyProductivityStats() {
19297
+ return { totalEvents: 0, vectorCount: 0, levelStats: [] };
19298
+ }
19299
+ function emptyProductivityQueue() {
19300
+ return {
19301
+ pending: 0,
19302
+ processing: 0,
19303
+ failed: 0,
19304
+ retryableFailed: 0,
19305
+ quarantinedFailed: 0,
19306
+ total: 0,
19307
+ stuckProcessing: 0,
19308
+ oldestProcessingAgeMs: null
19309
+ };
19310
+ }
19311
+ function emptyProductivityOutbox() {
19312
+ return { embedding: emptyProductivityQueue(), vector: emptyProductivityQueue() };
19313
+ }
18782
19314
  healthRouter.get("/setup", async (c) => {
18783
19315
  const memoryService = getLightweightServiceFromQuery(c);
18784
19316
  try {
@@ -18835,6 +19367,40 @@ healthRouter.get("/setup", async (c) => {
18835
19367
  await memoryService.shutdown();
18836
19368
  }
18837
19369
  });
19370
+ healthRouter.get("/productivity", async (c) => {
19371
+ let memoryService;
19372
+ try {
19373
+ const profile = parseProductivityHealthProfile(c.req.query("profile"));
19374
+ const mode = parseProductivityHealthMode(c.req.query("mode"));
19375
+ const projectQuery = c.req.query("project") || c.req.query("projectId");
19376
+ const project = resolveProductivityProjectIdentity(projectQuery);
19377
+ if (!explicitProjectStoreExists(projectQuery)) {
19378
+ return c.json(buildProductivityHealthReport({
19379
+ stats: emptyProductivityStats(),
19380
+ outbox: emptyProductivityOutbox(),
19381
+ project,
19382
+ profile,
19383
+ mode
19384
+ }));
19385
+ }
19386
+ memoryService = getLightweightServiceFromQuery(c);
19387
+ await memoryService.initialize();
19388
+ const [stats, outbox] = await Promise.all([
19389
+ memoryService.getStats(),
19390
+ memoryService.getOutboxStats()
19391
+ ]);
19392
+ return c.json(buildProductivityHealthReport({ stats, outbox, project, profile, mode }));
19393
+ } catch (error) {
19394
+ const status = error instanceof Error && error.message.startsWith("Invalid --") ? 400 : 500;
19395
+ return c.json({
19396
+ status: "error",
19397
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19398
+ error: status === 400 ? "Invalid productivity health option" : "Productivity health check failed"
19399
+ }, status);
19400
+ } finally {
19401
+ if (memoryService) await memoryService.shutdown();
19402
+ }
19403
+ });
18838
19404
  healthRouter.get("/", async (c) => {
18839
19405
  const memoryService = getLightweightServiceFromQuery(c);
18840
19406
  try {
@@ -19156,20 +19722,20 @@ playgroundRouter.post("/dry-run", async (c) => {
19156
19722
  var apiRouter = new Hono11().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter).route("/health", healthRouter).route("/playground", playgroundRouter);
19157
19723
 
19158
19724
  // src/apps/server/index.ts
19159
- var moduleDir = path17.dirname(fileUrlToPath(import.meta.url));
19725
+ var moduleDir = path19.dirname(fileUrlToPath(import.meta.url));
19160
19726
  var DASHBOARD_SESSION_COOKIE = "cml_dashboard_session";
19161
19727
  var DEFAULT_SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
19162
19728
  function resolveUiPath() {
19163
19729
  const candidates = [
19164
19730
  // Built server: dist/server/index.js -> dist/ui
19165
- path17.resolve(moduleDir, "../ui"),
19731
+ path19.resolve(moduleDir, "../ui"),
19166
19732
  // Source/dev server: src/apps/server/index.ts -> src/apps/dashboard
19167
- path17.resolve(moduleDir, "../dashboard"),
19733
+ path19.resolve(moduleDir, "../dashboard"),
19168
19734
  // Fallback when running from a repository root.
19169
- path17.resolve(process.cwd(), "dist/ui"),
19170
- path17.resolve(process.cwd(), "src/apps/dashboard")
19735
+ path19.resolve(process.cwd(), "dist/ui"),
19736
+ path19.resolve(process.cwd(), "src/apps/dashboard")
19171
19737
  ];
19172
- return candidates.find((candidate) => fs15.existsSync(path17.join(candidate, "index.html"))) ?? candidates[0];
19738
+ return candidates.find((candidate) => fs17.existsSync(path19.join(candidate, "index.html"))) ?? candidates[0];
19173
19739
  }
19174
19740
  function normalizeDashboardHost(host = "localhost") {
19175
19741
  const normalized = host.trim().toLowerCase();
@@ -19310,14 +19876,51 @@ function createAuthOptions(options) {
19310
19876
  return {
19311
19877
  password: options.password ?? "",
19312
19878
  cookieMaxAgeSeconds: options.cookieMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
19313
- now: options.now ?? Date.now
19879
+ now: options.now ?? Date.now,
19880
+ allowedOrigins: options.allowedOrigins ?? []
19314
19881
  };
19315
19882
  }
19883
+ function resolveAllowedOrigins(explicit) {
19884
+ if (explicit && explicit.length > 0) return explicit;
19885
+ const raw = process.env.DASHBOARD_ALLOWED_ORIGINS;
19886
+ if (!raw) return [];
19887
+ return raw.split(",").map((value) => value.trim()).filter(Boolean);
19888
+ }
19889
+ function selfOrigins(c) {
19890
+ const origins = [];
19891
+ try {
19892
+ origins.push(new URL(c.req.url).origin);
19893
+ } catch {
19894
+ }
19895
+ const host = c.req.header("host");
19896
+ if (host) origins.push(`${isSecureRequest(c) ? "https" : "http"}://${host}`);
19897
+ return origins;
19898
+ }
19899
+ function isAllowedMutationOrigin(c, origin, allowedOrigins) {
19900
+ return allowedOrigins.includes(origin) || selfOrigins(c).includes(origin);
19901
+ }
19316
19902
  function createDashboardApp(options = {}) {
19317
19903
  const app2 = new Hono12();
19318
19904
  const authOptions = createAuthOptions(options);
19319
- app2.use("*", cors());
19905
+ const allowedOrigins = resolveAllowedOrigins(options.allowedOrigins);
19906
+ if (allowedOrigins.length > 0) {
19907
+ app2.use("*", cors({
19908
+ origin: (origin) => allowedOrigins.includes(origin) ? origin : null,
19909
+ credentials: true
19910
+ }));
19911
+ }
19320
19912
  app2.use("*", logger());
19913
+ app2.use("/api/*", async (c, next) => {
19914
+ const method = c.req.method;
19915
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
19916
+ return next();
19917
+ }
19918
+ const origin = c.req.header("origin");
19919
+ if (origin && !isAllowedMutationOrigin(c, origin, allowedOrigins)) {
19920
+ return c.json({ error: "Cross-origin request blocked" }, 403);
19921
+ }
19922
+ return next();
19923
+ });
19321
19924
  app2.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
19322
19925
  app2.get("/api/auth/status", (c) => c.json({
19323
19926
  enabled: Boolean(authOptions.password),
@@ -19360,13 +19963,13 @@ function createDashboardApp(options = {}) {
19360
19963
  }
19361
19964
  app2.route("/api", apiRouter);
19362
19965
  const uiPath = resolveUiPath();
19363
- if (fs15.existsSync(uiPath)) {
19966
+ if (fs17.existsSync(uiPath)) {
19364
19967
  app2.use("/*", serveStatic({ root: uiPath }));
19365
19968
  }
19366
19969
  app2.get("*", (c) => {
19367
- const indexPath = path17.join(uiPath, "index.html");
19368
- if (fs15.existsSync(indexPath)) {
19369
- return c.html(fs15.readFileSync(indexPath, "utf-8"));
19970
+ const indexPath = path19.join(uiPath, "index.html");
19971
+ if (fs17.existsSync(indexPath)) {
19972
+ return c.html(fs17.readFileSync(indexPath, "utf-8"));
19370
19973
  }
19371
19974
  return c.text('UI not built. Run "npm run build:ui" first.', 404);
19372
19975
  });
@@ -19824,20 +20427,20 @@ function formatMetadataValue(value) {
19824
20427
  }
19825
20428
 
19826
20429
  // src/apps/cli/mcp-install.ts
19827
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
19828
- import { dirname as dirname7, join as join18 } from "node:path";
20430
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
20431
+ import { dirname as dirname7, join as join19 } from "node:path";
19829
20432
  import { homedir as homedir11 } from "node:os";
19830
20433
  function getDefaultClaudeDesktopConfigPath(platform = process.platform, env = process.env, home = homedir11()) {
19831
20434
  if (platform === "darwin") {
19832
- return join18(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
20435
+ return join19(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
19833
20436
  }
19834
20437
  if (platform === "win32") {
19835
- return join18(env.APPDATA ?? join18(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
20438
+ return join19(env.APPDATA ?? join19(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
19836
20439
  }
19837
- return join18(env.XDG_CONFIG_HOME ?? join18(home, ".config"), "claude", "claude_desktop_config.json");
20440
+ return join19(env.XDG_CONFIG_HOME ?? join19(home, ".config"), "claude", "claude_desktop_config.json");
19838
20441
  }
19839
20442
  function readJsonConfig(configPath) {
19840
- if (!existsSync13(configPath)) {
20443
+ if (!existsSync15(configPath)) {
19841
20444
  return {};
19842
20445
  }
19843
20446
  const content = readFileSync6(configPath, "utf8").trim();
@@ -19886,7 +20489,7 @@ function installMcpServer(options = {}) {
19886
20489
  }
19887
20490
 
19888
20491
  // src/apps/cli/claude-settings-hooks.ts
19889
- import * as path18 from "path";
20492
+ import * as path20 from "path";
19890
20493
  var REQUIRED_HOOK_FILES = [
19891
20494
  "user-prompt-submit.js",
19892
20495
  "post-tool-use.js",
@@ -19905,7 +20508,7 @@ function shellQuotePathForNode(filePath) {
19905
20508
  return `'${filePath.replace(/'/g, `'\\''`)}'`;
19906
20509
  }
19907
20510
  function buildHookCommand(pluginPath, fileName) {
19908
- return `node ${shellQuotePathForNode(path18.join(pluginPath, "hooks", fileName))}`;
20511
+ return `node ${shellQuotePathForNode(path20.join(pluginPath, "hooks", fileName))}`;
19909
20512
  }
19910
20513
  function getHooksConfig(pluginPath) {
19911
20514
  const makeHook = (fileName) => [
@@ -19976,8 +20579,8 @@ function mergePluginHooksIntoSettings(settings, pluginPath) {
19976
20579
  }
19977
20580
 
19978
20581
  // src/apps/cli/codex-validation-output.ts
19979
- import * as fs16 from "fs";
19980
- import * as path19 from "path";
20582
+ import * as fs18 from "fs";
20583
+ import * as path21 from "path";
19981
20584
  function formatNumber(value) {
19982
20585
  return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
19983
20586
  }
@@ -20051,14 +20654,14 @@ function formatCodexValidationReport(report, format = "markdown") {
20051
20654
  return lines.join("\n");
20052
20655
  }
20053
20656
  function writeCodexValidationReport(outputPath, report, format = "markdown") {
20054
- const dir = path19.dirname(outputPath);
20055
- fs16.mkdirSync(dir, { recursive: true });
20056
- fs16.writeFileSync(outputPath, formatCodexValidationReport(report, format), "utf8");
20657
+ const dir = path21.dirname(outputPath);
20658
+ fs18.mkdirSync(dir, { recursive: true });
20659
+ fs18.writeFileSync(outputPath, formatCodexValidationReport(report, format), "utf8");
20057
20660
  }
20058
20661
 
20059
20662
  // src/apps/cli/hermes-validation-output.ts
20060
- import * as fs17 from "fs";
20061
- import * as path20 from "path";
20663
+ import * as fs19 from "fs";
20664
+ import * as path22 from "path";
20062
20665
  function formatNumber2(value) {
20063
20666
  return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
20064
20667
  }
@@ -20128,9 +20731,9 @@ function formatHermesValidationReport(report, format = "markdown") {
20128
20731
  return lines.join("\n");
20129
20732
  }
20130
20733
  function writeHermesValidationReport(outputPath, report, format = "markdown") {
20131
- const dir = path20.dirname(outputPath);
20132
- fs17.mkdirSync(dir, { recursive: true });
20133
- fs17.writeFileSync(outputPath, formatHermesValidationReport(report, format), "utf8");
20734
+ const dir = path22.dirname(outputPath);
20735
+ fs19.mkdirSync(dir, { recursive: true });
20736
+ fs19.writeFileSync(outputPath, formatHermesValidationReport(report, format), "utf8");
20134
20737
  }
20135
20738
 
20136
20739
  // src/apps/cli/codex-import-runner.ts
@@ -20817,7 +21420,7 @@ function redactSecrets(input) {
20817
21420
  }
20818
21421
 
20819
21422
  // src/apps/cli/process-command.ts
20820
- import * as path21 from "node:path";
21423
+ import * as path23 from "node:path";
20821
21424
  function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
20822
21425
  const explicitProject = options.project;
20823
21426
  if (explicitProject !== void 0 && explicitProject.trim().length === 0) {
@@ -20838,7 +21441,7 @@ function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
20838
21441
  projectPath,
20839
21442
  recoverStuck,
20840
21443
  dryRunRecovery,
20841
- lockPath: explicitLockPath ?? path21.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21444
+ lockPath: explicitLockPath ?? path23.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
20842
21445
  };
20843
21446
  }
20844
21447
  function formatProcessLockBusy(input) {
@@ -20858,7 +21461,7 @@ function formatProcessRecoveryPreview(input) {
20858
21461
  const embeddingRetried = input.recovery.embedding.retriedFailed;
20859
21462
  const vectorRetried = input.recovery.vector.retriedFailed;
20860
21463
  const retriedTotal = embeddingRetried + vectorRetried;
20861
- const oldestProcessingAge = maxNullable(
21464
+ const oldestProcessingAge = maxNullable2(
20862
21465
  input.stats.embedding.oldestProcessingAgeMs,
20863
21466
  input.stats.vector.oldestProcessingAgeMs
20864
21467
  );
@@ -20873,7 +21476,7 @@ function formatProcessRecoveryPreview(input) {
20873
21476
  `Next command: claude-memory-layer process --project ${quoteCliArg(input.projectPath)}`
20874
21477
  ].join("\n");
20875
21478
  }
20876
- function maxNullable(a, b) {
21479
+ function maxNullable2(a, b) {
20877
21480
  if (a === null) return b;
20878
21481
  if (b === null) return a;
20879
21482
  return Math.max(a, b);
@@ -20933,15 +21536,15 @@ function formatVectorStatusJsonReport(input) {
20933
21536
  return JSON.stringify(buildVectorStatusReport(input), null, 2);
20934
21537
  }
20935
21538
  function buildVectorStatusReport(input) {
20936
- const embedding = normalizeQueue(input.outbox.embedding);
20937
- const vector = normalizeQueue(input.outbox.vector);
20938
- const totals = sumQueues(embedding, vector);
21539
+ const embedding = normalizeQueue2(input.outbox.embedding);
21540
+ const vector = normalizeQueue2(input.outbox.vector);
21541
+ const totals = sumQueues2(embedding, vector);
20939
21542
  const status = totals.failed > 0 || totals.stuckProcessing > 0 ? "needs-attention" : "ok";
20940
- const oldestProcessingAgeMs = maxNullable2(embedding.oldestProcessingAgeMs, vector.oldestProcessingAgeMs);
21543
+ const oldestProcessingAgeMs = maxNullable3(embedding.oldestProcessingAgeMs, vector.oldestProcessingAgeMs);
20941
21544
  return {
20942
21545
  storage: {
20943
- totalEvents: numberOrZero(input.stats.totalEvents),
20944
- vectorCount: numberOrZero(input.stats.vectorCount)
21546
+ totalEvents: numberOrZero2(input.stats.totalEvents),
21547
+ vectorCount: numberOrZero2(input.stats.vectorCount)
20945
21548
  },
20946
21549
  outbox: { embedding, vector, totals },
20947
21550
  status,
@@ -20954,19 +21557,19 @@ function selectRecommendedAction(totals) {
20954
21557
  if ((totals.quarantinedFailed ?? 0) > 0) return "inspect-quarantined";
20955
21558
  return "none";
20956
21559
  }
20957
- function normalizeQueue(queue) {
21560
+ function normalizeQueue2(queue) {
20958
21561
  return {
20959
- pending: numberOrZero(queue.pending),
20960
- processing: numberOrZero(queue.processing),
20961
- failed: numberOrZero(queue.failed),
20962
- retryableFailed: numberOrZero(queue.retryableFailed),
20963
- quarantinedFailed: numberOrZero(queue.quarantinedFailed),
20964
- total: numberOrZero(queue.total),
20965
- stuckProcessing: numberOrZero(queue.stuckProcessing),
21562
+ pending: numberOrZero2(queue.pending),
21563
+ processing: numberOrZero2(queue.processing),
21564
+ failed: numberOrZero2(queue.failed),
21565
+ retryableFailed: numberOrZero2(queue.retryableFailed),
21566
+ quarantinedFailed: numberOrZero2(queue.quarantinedFailed),
21567
+ total: numberOrZero2(queue.total),
21568
+ stuckProcessing: numberOrZero2(queue.stuckProcessing),
20966
21569
  oldestProcessingAgeMs: Number.isFinite(queue.oldestProcessingAgeMs ?? Number.NaN) ? queue.oldestProcessingAgeMs : null
20967
21570
  };
20968
21571
  }
20969
- function sumQueues(a, b) {
21572
+ function sumQueues2(a, b) {
20970
21573
  return {
20971
21574
  pending: a.pending + b.pending,
20972
21575
  processing: a.processing + b.processing,
@@ -20975,7 +21578,7 @@ function sumQueues(a, b) {
20975
21578
  quarantinedFailed: (a.quarantinedFailed ?? 0) + (b.quarantinedFailed ?? 0),
20976
21579
  stuckProcessing: a.stuckProcessing + b.stuckProcessing,
20977
21580
  total: a.total + b.total,
20978
- oldestProcessingAgeMs: maxNullable2(a.oldestProcessingAgeMs, b.oldestProcessingAgeMs)
21581
+ oldestProcessingAgeMs: maxNullable3(a.oldestProcessingAgeMs, b.oldestProcessingAgeMs)
20979
21582
  };
20980
21583
  }
20981
21584
  function formatQueueRow(label, queue) {
@@ -20991,12 +21594,12 @@ function formatQueueRow(label, queue) {
20991
21594
  formatDuration2(queue.oldestProcessingAgeMs).padStart(7)
20992
21595
  ].join(" ");
20993
21596
  }
20994
- function maxNullable2(a, b) {
21597
+ function maxNullable3(a, b) {
20995
21598
  if (a === null) return b;
20996
21599
  if (b === null) return a;
20997
21600
  return Math.max(a, b);
20998
21601
  }
20999
- function numberOrZero(value) {
21602
+ function numberOrZero2(value) {
21000
21603
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
21001
21604
  return Math.floor(value);
21002
21605
  }
@@ -21016,12 +21619,12 @@ function formatDuration2(ms) {
21016
21619
  }
21017
21620
 
21018
21621
  // src/apps/cli/mongo-sync-command.ts
21019
- import * as path23 from "node:path";
21622
+ import * as path25 from "node:path";
21020
21623
 
21021
21624
  // src/core/worker-lock.ts
21022
- import { closeSync, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
21625
+ import { closeSync, existsSync as existsSync16, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
21023
21626
  import * as os12 from "node:os";
21024
- import * as path22 from "node:path";
21627
+ import * as path24 from "node:path";
21025
21628
  import { randomUUID as randomUUID18 } from "node:crypto";
21026
21629
  var WorkerLock = class {
21027
21630
  lockPath;
@@ -21030,7 +21633,7 @@ var WorkerLock = class {
21030
21633
  now;
21031
21634
  isProcessRunning;
21032
21635
  acquired = false;
21033
- constructor(lockPath = path22.join(os12.tmpdir(), "claude-memory-layer", "vector-worker.lock"), options = {}) {
21636
+ constructor(lockPath = path24.join(os12.tmpdir(), "claude-memory-layer", "vector-worker.lock"), options = {}) {
21034
21637
  this.lockPath = lockPath;
21035
21638
  this.pid = options.pid ?? process.pid;
21036
21639
  this.ownerId = options.ownerId ?? randomUUID18();
@@ -21043,7 +21646,7 @@ var WorkerLock = class {
21043
21646
  acquire() {
21044
21647
  let staleRecovered = false;
21045
21648
  for (; ; ) {
21046
- mkdirSync10(path22.dirname(this.lockPath), { recursive: true });
21649
+ mkdirSync10(path24.dirname(this.lockPath), { recursive: true });
21047
21650
  const createResult = this.tryCreateLockFile();
21048
21651
  if (createResult === "created") {
21049
21652
  this.acquired = true;
@@ -21077,7 +21680,7 @@ var WorkerLock = class {
21077
21680
  }
21078
21681
  }
21079
21682
  release() {
21080
- if (!this.acquired || !existsSync14(this.lockPath)) return false;
21683
+ if (!this.acquired || !existsSync16(this.lockPath)) return false;
21081
21684
  const existing = this.readPayload();
21082
21685
  if (!this.isOwnedPayload(existing)) return false;
21083
21686
  try {
@@ -21175,7 +21778,7 @@ function resolveMongoSyncProcessOptions(options, cwd = process.cwd(), deps = {})
21175
21778
  projectPath,
21176
21779
  processAfterSync: options.processAfterSync === true,
21177
21780
  processIntervalMs,
21178
- lockPath: explicitLockPath ?? path23.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21781
+ lockPath: explicitLockPath ?? path25.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
21179
21782
  };
21180
21783
  }
21181
21784
  function createMongoSyncPostProcessor(options, deps = {}) {
@@ -21258,28 +21861,28 @@ function parsePositiveIntegerOption2(raw, flagName) {
21258
21861
  }
21259
21862
 
21260
21863
  // src/apps/cli/index.ts
21261
- var CLAUDE_SETTINGS_PATH = path24.join(os13.homedir(), ".claude", "settings.json");
21864
+ var CLAUDE_SETTINGS_PATH = path26.join(os13.homedir(), ".claude", "settings.json");
21262
21865
  function getPluginPath() {
21263
21866
  const possiblePaths = [
21264
- path24.join(__dirname, ".."),
21867
+ path26.join(__dirname, ".."),
21265
21868
  // When running from dist/cli
21266
- path24.join(__dirname, "../..", "dist"),
21869
+ path26.join(__dirname, "../..", "dist"),
21267
21870
  // When running from src
21268
- path24.join(process.cwd(), "dist")
21871
+ path26.join(process.cwd(), "dist")
21269
21872
  // Current working directory
21270
21873
  ];
21271
21874
  for (const p of possiblePaths) {
21272
- const hooksPath = path24.join(p, "hooks", "user-prompt-submit.js");
21273
- if (fs18.existsSync(hooksPath)) {
21875
+ const hooksPath = path26.join(p, "hooks", "user-prompt-submit.js");
21876
+ if (fs20.existsSync(hooksPath)) {
21274
21877
  return p;
21275
21878
  }
21276
21879
  }
21277
- return path24.join(os13.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
21880
+ return path26.join(os13.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
21278
21881
  }
21279
21882
  function loadClaudeSettings() {
21280
21883
  try {
21281
- if (fs18.existsSync(CLAUDE_SETTINGS_PATH)) {
21282
- const content = fs18.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
21884
+ if (fs20.existsSync(CLAUDE_SETTINGS_PATH)) {
21885
+ const content = fs20.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
21283
21886
  return JSON.parse(content);
21284
21887
  }
21285
21888
  } catch (error) {
@@ -21288,13 +21891,52 @@ function loadClaudeSettings() {
21288
21891
  return {};
21289
21892
  }
21290
21893
  function saveClaudeSettings(settings) {
21291
- const dir = path24.dirname(CLAUDE_SETTINGS_PATH);
21292
- if (!fs18.existsSync(dir)) {
21293
- fs18.mkdirSync(dir, { recursive: true });
21894
+ const dir = path26.dirname(CLAUDE_SETTINGS_PATH);
21895
+ if (!fs20.existsSync(dir)) {
21896
+ fs20.mkdirSync(dir, { recursive: true });
21294
21897
  }
21295
21898
  const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
21296
- fs18.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
21297
- fs18.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
21899
+ fs20.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
21900
+ fs20.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
21901
+ }
21902
+ function resolveHealthProjectIdentity(projectPath) {
21903
+ if (!projectPath || projectPath.trim().length === 0) return { scope: "global", id: "global" };
21904
+ const normalized = projectPath.trim();
21905
+ return { scope: "project", id: /^[a-f0-9]{8}$/.test(normalized) ? normalized : hashProjectPath(normalized) };
21906
+ }
21907
+ function resolveHealthStoragePath(projectPathOrHash) {
21908
+ const normalized = projectPathOrHash?.trim();
21909
+ return normalized && normalized.length > 0 ? resolveProjectStoragePath(normalized) : path26.join(os13.homedir(), ".claude-code", "memory");
21910
+ }
21911
+ function createHealthMemoryService(projectPathOrHash) {
21912
+ return new MemoryService({
21913
+ storagePath: resolveHealthStoragePath(projectPathOrHash),
21914
+ readOnly: true,
21915
+ lightweightMode: true,
21916
+ analyticsEnabled: false,
21917
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
21918
+ });
21919
+ }
21920
+ function healthStoreExists(projectPathOrHash) {
21921
+ return fs20.existsSync(path26.join(resolveHealthStoragePath(projectPathOrHash), "events.sqlite"));
21922
+ }
21923
+ function emptyHealthStats() {
21924
+ return { totalEvents: 0, vectorCount: 0, levelStats: [] };
21925
+ }
21926
+ function emptyHealthQueue() {
21927
+ return {
21928
+ pending: 0,
21929
+ processing: 0,
21930
+ failed: 0,
21931
+ retryableFailed: 0,
21932
+ quarantinedFailed: 0,
21933
+ total: 0,
21934
+ stuckProcessing: 0,
21935
+ oldestProcessingAgeMs: null
21936
+ };
21937
+ }
21938
+ function emptyHealthOutbox() {
21939
+ return { embedding: emptyHealthQueue(), vector: emptyHealthQueue() };
21298
21940
  }
21299
21941
  function parseCommaList(value) {
21300
21942
  if (!value) return void 0;
@@ -21354,7 +21996,7 @@ async function runCodexValidationCommand(options) {
21354
21996
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
21355
21997
  `);
21356
21998
  if (options.output) {
21357
- const outputPath = path24.resolve(options.output);
21999
+ const outputPath = path26.resolve(options.output);
21358
22000
  writeCodexValidationReport(outputPath, report, format);
21359
22001
  console.log(`
21360
22002
  Report written to ${outputPath}`);
@@ -21374,7 +22016,7 @@ async function runHermesValidationCommand(options) {
21374
22016
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
21375
22017
  `);
21376
22018
  if (options.output) {
21377
- const outputPath = path24.resolve(options.output);
22019
+ const outputPath = path26.resolve(options.output);
21378
22020
  writeHermesValidationReport(outputPath, report, format);
21379
22021
  console.log(`
21380
22022
  Report written to ${outputPath}`);
@@ -21408,18 +22050,18 @@ var OPERATION_PRIVACY_CONFIG = {
21408
22050
  }
21409
22051
  };
21410
22052
  function resolveOperationProject(project) {
21411
- const projectPath = path24.resolve(project ?? process.cwd());
22053
+ const projectPath = path26.resolve(project ?? process.cwd());
21412
22054
  const projectHash = hashProjectPath(projectPath);
21413
22055
  const storagePath = getProjectStoragePath(projectPath);
21414
22056
  return {
21415
22057
  projectPath,
21416
22058
  projectHash,
21417
22059
  storagePath,
21418
- dbPath: path24.join(storagePath, "events.sqlite")
22060
+ dbPath: path26.join(storagePath, "events.sqlite")
21419
22061
  };
21420
22062
  }
21421
22063
  function openOperationReadDatabase(context) {
21422
- if (!fs18.existsSync(context.dbPath)) return null;
22064
+ if (!fs20.existsSync(context.dbPath)) return null;
21423
22065
  return createSQLiteDatabase(context.dbPath, { readonly: true, walMode: false });
21424
22066
  }
21425
22067
  async function withOperationWriteDatabase(context, callback) {
@@ -21629,7 +22271,7 @@ async function runOperationCli(action, label) {
21629
22271
  }
21630
22272
  }
21631
22273
  var program = new Command();
21632
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.53");
22274
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.55");
21633
22275
  program.command("market-context").description("Fetch read-only DART/FRED/Finnhub context with structured MarketContextSnapshot bull/bear/risk/catalyst analysis").option("--company <name>", "Company name for DART fallback search and report subject").option("--dart-corp-code <code>", "Exact DART corp_code for issuer-specific filings").option("--symbol <ticker>", "Listed ticker for Finnhub company profile").option("--providers <list>", "Comma-separated providers: dart,fred,finnhub").option("--fred-series <list>", "Comma-separated FRED series IDs").option("--json", "Print structured JSON including analysis.marketSnapshot").option("--no-snapshot", "Disable MarketContextSnapshot and DART company snapshot analysis").action(async (options) => {
21634
22276
  try {
21635
22277
  await runMarketContextCommand(options);
@@ -21642,7 +22284,7 @@ program.command("install").description("Install hooks into Claude Code settings"
21642
22284
  try {
21643
22285
  const pluginPath = options.path || getPluginPath();
21644
22286
  const missingHooks = REQUIRED_HOOK_FILES.filter(
21645
- (file) => !fs18.existsSync(path24.join(pluginPath, "hooks", file))
22287
+ (file) => !fs20.existsSync(path26.join(pluginPath, "hooks", file))
21646
22288
  );
21647
22289
  if (missingHooks.length > 0) {
21648
22290
  console.error(`
@@ -21708,7 +22350,7 @@ program.command("status").description("Check plugin installation status").action
21708
22350
  console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21709
22351
  console.log(` Stop: ${hasStopHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21710
22352
  console.log(` SessionEnd: ${hasSessionEndHook ? "\u2705 Installed" : "\u274C Not installed"}`);
21711
- const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs18.existsSync(path24.join(pluginPath, "hooks", file)));
22353
+ const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs20.existsSync(path26.join(pluginPath, "hooks", file)));
21712
22354
  console.log(`
21713
22355
  Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
21714
22356
  console.log(` Path: ${pluginPath}`);
@@ -21843,6 +22485,40 @@ program.command("stats").description("View memory statistics").option("-p, --pro
21843
22485
  process.exit(1);
21844
22486
  }
21845
22487
  });
22488
+ program.command("health").description("Show aggregate health reports for agent productivity workflows").option("--productivity", "Print Project Health Report for agent productivity workflows").option("--json", "Print health report as JSON").option("-p, --project <path>", "Project path or 8-character project hash").option("--profile <profile>", "Agent profile: coder, reviewer, pm, support, researcher, team", "coder").option("--mode <mode>", "Report mode: observe, preview, enforce", "preview").action(async (options) => {
22489
+ let service;
22490
+ try {
22491
+ if (options.productivity !== true) {
22492
+ throw new Error("Use --productivity to select the Project Health Report");
22493
+ }
22494
+ if (options.json !== true) {
22495
+ throw new Error("Only --json output is currently supported for productivity health");
22496
+ }
22497
+ const profile = parseProductivityHealthProfile(options.profile);
22498
+ const mode = parseProductivityHealthMode(options.mode);
22499
+ const projectPath = options.project ?? process.cwd();
22500
+ const project = resolveHealthProjectIdentity(projectPath);
22501
+ let stats;
22502
+ let outbox;
22503
+ if (healthStoreExists(projectPath)) {
22504
+ service = createHealthMemoryService(projectPath);
22505
+ [stats, outbox] = await Promise.all([
22506
+ service.getStats(),
22507
+ service.getOutboxStats()
22508
+ ]);
22509
+ } else {
22510
+ stats = emptyHealthStats();
22511
+ outbox = emptyHealthOutbox();
22512
+ }
22513
+ console.log(JSON.stringify(buildProductivityHealthReport({ stats, outbox, project, profile, mode }), null, 2));
22514
+ } catch (error) {
22515
+ const message = error instanceof Error ? error.message : "Health report failed";
22516
+ console.error(message.startsWith("Invalid --") || message.startsWith("Use --") || message.startsWith("Only --") ? message : "Health report failed");
22517
+ process.exit(1);
22518
+ } finally {
22519
+ await service?.shutdown().catch(() => void 0);
22520
+ }
22521
+ });
21846
22522
  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) => {
21847
22523
  let service;
21848
22524
  try {
@@ -21943,8 +22619,8 @@ repairCommand.command("legacy-project-scope").description("Dry-run or apply proj
21943
22619
  apply: options.apply
21944
22620
  });
21945
22621
  const storagePath = projectPath ? getProjectStoragePath(projectPath) : resolveProjectStoragePath(repairOptions.projectHash);
21946
- const dbPath = path24.join(storagePath, "events.sqlite");
21947
- if (repairOptions.dryRun && !fs18.existsSync(dbPath)) {
22622
+ const dbPath = path26.join(storagePath, "events.sqlite");
22623
+ if (repairOptions.dryRun && !fs20.existsSync(dbPath)) {
21948
22624
  const projectHash = repairOptions.projectHash || hashProjectPath(repairOptions.projectPath);
21949
22625
  console.log(formatLegacyProjectScopeRepairResult({
21950
22626
  dryRun: true,
@@ -22191,8 +22867,8 @@ retentionCommand.command("audit").description("Run a dry-run retention audit for
22191
22867
  });
22192
22868
  const projectHash = auditOptions.projectHash ?? hashProjectPath(auditOptions.projectPath);
22193
22869
  const storagePath = auditOptions.projectPath ? getProjectStoragePath(auditOptions.projectPath) : resolveProjectStoragePath(projectHash);
22194
- const dbPath = path24.join(storagePath, "events.sqlite");
22195
- if (!fs18.existsSync(dbPath)) {
22870
+ const dbPath = path26.join(storagePath, "events.sqlite");
22871
+ if (!fs20.existsSync(dbPath)) {
22196
22872
  const emptyReport = emptyRetentionAuditReport(projectHash, auditOptions.limit);
22197
22873
  console.log(formatRetentionAuditReport(emptyReport, { json: auditOptions.json }));
22198
22874
  return;
@@ -22219,7 +22895,7 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
22219
22895
  const projectPath = options.project || process.cwd();
22220
22896
  const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
22221
22897
  const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
22222
- const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path24.basename(projectPath);
22898
+ const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path26.basename(projectPath);
22223
22899
  const direction = String(options.direction || "both").toLowerCase();
22224
22900
  if (!mongoUri || !mongoDb) {
22225
22901
  console.error("\n\u274C MongoDB sync is not configured.");
@@ -22249,14 +22925,14 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
22249
22925
  }
22250
22926
  })();
22251
22927
  const storagePath = getProjectStoragePath(projectPath);
22252
- if (!fs18.existsSync(storagePath)) {
22253
- fs18.mkdirSync(storagePath, { recursive: true });
22928
+ if (!fs20.existsSync(storagePath)) {
22929
+ fs20.mkdirSync(storagePath, { recursive: true });
22254
22930
  }
22255
22931
  const postProcessor = createMongoSyncPostProcessor(processOptions, {
22256
22932
  log: (message) => process.stdout.write(`${message}
22257
22933
  `)
22258
22934
  });
22259
- const sqliteStore = new SQLiteEventStore(path24.join(storagePath, "events.sqlite"));
22935
+ const sqliteStore = new SQLiteEventStore(path26.join(storagePath, "events.sqlite"));
22260
22936
  const worker = new MongoSyncWorker(sqliteStore, {
22261
22937
  uri: mongoUri,
22262
22938
  dbName: mongoDb,
@@ -22316,7 +22992,7 @@ function renderProgress(event) {
22316
22992
  break;
22317
22993
  case "session-start": {
22318
22994
  const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
22319
- const sessionName = path24.basename(event.filePath, ".jsonl").slice(0, 8);
22995
+ const sessionName = path26.basename(event.filePath, ".jsonl").slice(0, 8);
22320
22996
  process.stdout.write(
22321
22997
  `\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
22322
22998
  );
@@ -22391,9 +23067,9 @@ async function listMarkdownFiles(root) {
22391
23067
  const stack = [root];
22392
23068
  while (stack.length > 0) {
22393
23069
  const dir = stack.pop();
22394
- const entries = await fs18.promises.readdir(dir, { withFileTypes: true });
23070
+ const entries = await fs20.promises.readdir(dir, { withFileTypes: true });
22395
23071
  for (const e of entries) {
22396
- const full = path24.join(dir, e.name);
23072
+ const full = path26.join(dir, e.name);
22397
23073
  if (e.isDirectory()) stack.push(full);
22398
23074
  else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md") out.push(full);
22399
23075
  }
@@ -22401,8 +23077,8 @@ async function listMarkdownFiles(root) {
22401
23077
  return out.sort();
22402
23078
  }
22403
23079
  function deriveNamespaceCategory(sourceRoot, filePath) {
22404
- const rel = path24.relative(sourceRoot, filePath);
22405
- const dirSeg = path24.dirname(rel).split(path24.sep).filter(Boolean);
23080
+ const rel = path26.relative(sourceRoot, filePath);
23081
+ const dirSeg = path26.dirname(rel).split(path26.sep).filter(Boolean);
22406
23082
  if (dirSeg.length >= 2) {
22407
23083
  const namespace = sanitizeSegment3(dirSeg[0], "default");
22408
23084
  const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
@@ -22421,10 +23097,10 @@ function extractImportEvidence(markdown) {
22421
23097
  program.command("organize-import [sourceDir]").description("Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing").option("-p, --project <path>", "Project path (defaults to cwd)").option("--session <id>", "Session id for imported events (default: import:organized)").option("--limit <n>", "Limit number of files to import").option("--dry-run", "Preview mapping without writing").option("--bootstrap", "Force-generate structured markdown from codebase + git history before import").option("--bootstrap-if-empty", "Auto-bootstrap when source has no markdown files (default: true)", true).option("--no-bootstrap-if-empty", "Disable auto-bootstrap when source has no markdown files").option("--force-bootstrap", "Run bootstrap even when markdown files exist").option("--repo <path>", "Repository root for bootstrap analysis (default: project path)").option("--out <path>", "Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)").option("--since <range>", 'Git history range for bootstrap (default: "180 days ago")').option("--max-commits <n>", "Max commits to analyze for bootstrap (default: 1000)").option("--incremental", "Use previous bootstrap manifest as baseline for incremental updates (default: true)", true).option("--no-incremental", "Disable incremental bootstrap; regenerate full snapshot").action(async (sourceDir, options) => {
22422
23098
  const projectPath = options.project || process.cwd();
22423
23099
  const sessionId = options.session || "import:organized";
22424
- const sourceRoot = path24.resolve(sourceDir || options.out || projectPath);
22425
- const repoPath = path24.resolve(options.repo || projectPath);
22426
- if (!fs18.existsSync(sourceRoot)) {
22427
- fs18.mkdirSync(sourceRoot, { recursive: true });
23100
+ const sourceRoot = path26.resolve(sourceDir || options.out || projectPath);
23101
+ const repoPath = path26.resolve(options.repo || projectPath);
23102
+ if (!fs20.existsSync(sourceRoot)) {
23103
+ fs20.mkdirSync(sourceRoot, { recursive: true });
22428
23104
  }
22429
23105
  const service = getMemoryServiceForProject(projectPath);
22430
23106
  try {
@@ -22434,7 +23110,7 @@ program.command("organize-import [sourceDir]").description("Import existing mark
22434
23110
  const hasMarkdown = files.length > 0;
22435
23111
  const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
22436
23112
  if (shouldBootstrap) {
22437
- const outDir = path24.resolve(options.out || path24.join(sourceRoot, "bootstrap-kb"));
23113
+ const outDir = path26.resolve(options.out || path26.join(sourceRoot, "bootstrap-kb"));
22438
23114
  const since = options.since || "180 days ago";
22439
23115
  const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
22440
23116
  console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
@@ -22473,13 +23149,13 @@ program.command("organize-import [sourceDir]").description("Import existing mark
22473
23149
  let imported = 0;
22474
23150
  let skipped = 0;
22475
23151
  for (const file of targets) {
22476
- const text = await fs18.promises.readFile(file, "utf8");
23152
+ const text = await fs20.promises.readFile(file, "utf8");
22477
23153
  if (!text.trim()) {
22478
23154
  skipped += 1;
22479
23155
  continue;
22480
23156
  }
22481
23157
  const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
22482
- const rel = path24.relative(activeSourceRoot, file);
23158
+ const rel = path26.relative(activeSourceRoot, file);
22483
23159
  const evidence = extractImportEvidence(text);
22484
23160
  if (options.dryRun) {
22485
23161
  console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);