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