claude-memory-layer 1.0.52 → 1.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -11261,7 +11261,13 @@ function toDate(value) {
11261
11261
  return new Date(String(value));
11262
11262
  }
11263
11263
  function createDatabase(dbPath, options) {
11264
- return new BetterSqlite3(dbPath, { readonly: options?.readOnly });
11264
+ const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
11265
+ db.pragma("busy_timeout = 5000");
11266
+ if (!options?.readOnly) {
11267
+ db.pragma("journal_mode = WAL");
11268
+ db.pragma("synchronous = NORMAL");
11269
+ }
11270
+ return db;
11265
11271
  }
11266
11272
  function dbRun(db, sql, params = []) {
11267
11273
  db.prepare(sql).run(...params);
@@ -11494,19 +11500,29 @@ var ConsolidatedStore = class {
11494
11500
  */
11495
11501
  async create(input) {
11496
11502
  const memoryId = randomUUID2();
11497
- await dbRun(
11498
- this.db,
11503
+ const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
11504
+ const insertMemory = this.db.prepare(
11499
11505
  `INSERT INTO consolidated_memories
11500
11506
  (memory_id, summary, topics, source_events, confidence, created_at)
11501
- VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
11502
- [
11507
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
11508
+ );
11509
+ const insertEvent = this.db.prepare(
11510
+ `INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
11511
+ );
11512
+ this.db.transaction(() => {
11513
+ insertMemory.run(
11503
11514
  memoryId,
11504
11515
  input.summary,
11505
11516
  JSON.stringify(input.topics),
11506
- JSON.stringify(input.sourceEvents),
11517
+ JSON.stringify(sourceEvents),
11507
11518
  input.confidence
11508
- ]
11509
- );
11519
+ );
11520
+ for (const eventId of sourceEvents) {
11521
+ if (typeof eventId === "string" && eventId.length > 0) {
11522
+ insertEvent.run(memoryId, eventId);
11523
+ }
11524
+ }
11525
+ })();
11510
11526
  return memoryId;
11511
11527
  }
11512
11528
  /**
@@ -11612,11 +11628,12 @@ var ConsolidatedStore = class {
11612
11628
  * Delete a consolidated memory
11613
11629
  */
11614
11630
  async delete(memoryId) {
11615
- await dbRun(
11616
- this.db,
11617
- `DELETE FROM consolidated_memories WHERE memory_id = ?`,
11618
- [memoryId]
11619
- );
11631
+ const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
11632
+ const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
11633
+ this.db.transaction(() => {
11634
+ deleteEvents.run(memoryId);
11635
+ deleteMemory.run(memoryId);
11636
+ })();
11620
11637
  }
11621
11638
  /**
11622
11639
  * Create a long-term rule promoted from stable summaries
@@ -11729,16 +11746,16 @@ var ConsolidatedStore = class {
11729
11746
  * Check if source events are already consolidated
11730
11747
  */
11731
11748
  async isAlreadyConsolidated(eventIds) {
11732
- for (const eventId of eventIds) {
11733
- const result2 = await dbAll(
11734
- this.db,
11735
- `SELECT COUNT(*) as count FROM consolidated_memories
11736
- WHERE source_events LIKE ?`,
11737
- [`%"${eventId}"%`]
11738
- );
11739
- if ((result2[0]?.count || 0) > 0) return true;
11740
- }
11741
- return false;
11749
+ if (eventIds.length === 0) return false;
11750
+ const placeholders2 = eventIds.map(() => "?").join(",");
11751
+ const rows = await dbAll(
11752
+ this.db,
11753
+ `SELECT memory_id FROM consolidated_memory_events
11754
+ WHERE event_id IN (${placeholders2})
11755
+ LIMIT 1`,
11756
+ eventIds
11757
+ );
11758
+ return rows.length > 0;
11742
11759
  }
11743
11760
  /**
11744
11761
  * Get the last consolidation time
@@ -13274,6 +13291,19 @@ function makeDedupeKey(content, sessionId) {
13274
13291
  return `${sessionId}:${contentHash}`;
13275
13292
  }
13276
13293
 
13294
+ // src/core/citation-generator.ts
13295
+ import { createHash as createHash2 } from "crypto";
13296
+ var ID_LENGTH = 6;
13297
+ var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
13298
+ function generateCitationId(eventId) {
13299
+ const hash = createHash2("sha256").update(eventId).digest();
13300
+ let id = "";
13301
+ for (let i = 0; i < ID_LENGTH; i++) {
13302
+ id += CHARSET[hash[i] % CHARSET.length];
13303
+ }
13304
+ return id;
13305
+ }
13306
+
13277
13307
  // src/core/sqlite-event-store.ts
13278
13308
  import * as nodePath2 from "path";
13279
13309
 
@@ -13318,17 +13348,28 @@ function createSQLiteDatabase(path15, options) {
13318
13348
  }
13319
13349
  return db;
13320
13350
  }
13351
+ var statementCache = /* @__PURE__ */ new WeakMap();
13352
+ function prepareCached(db, sql) {
13353
+ let cache = statementCache.get(db);
13354
+ if (!cache) {
13355
+ cache = /* @__PURE__ */ new Map();
13356
+ statementCache.set(db, cache);
13357
+ }
13358
+ let stmt = cache.get(sql);
13359
+ if (!stmt) {
13360
+ stmt = db.prepare(sql);
13361
+ cache.set(sql, stmt);
13362
+ }
13363
+ return stmt;
13364
+ }
13321
13365
  function sqliteRun(db, sql, params = []) {
13322
- const stmt = db.prepare(sql);
13323
- return stmt.run(...params);
13366
+ return prepareCached(db, sql).run(...params);
13324
13367
  }
13325
13368
  function sqliteAll(db, sql, params = []) {
13326
- const stmt = db.prepare(sql);
13327
- return stmt.all(...params);
13369
+ return prepareCached(db, sql).all(...params);
13328
13370
  }
13329
13371
  function sqliteGet(db, sql, params = []) {
13330
- const stmt = db.prepare(sql);
13331
- return stmt.get(...params);
13372
+ return prepareCached(db, sql).get(...params);
13332
13373
  }
13333
13374
  function sqliteExec(db, sql) {
13334
13375
  db.exec(sql);
@@ -13750,6 +13791,7 @@ function normalizeQueryRewriteKind(value) {
13750
13791
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
13751
13792
  var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
13752
13793
  var DEFAULT_OUTBOX_MAX_RETRIES = 3;
13794
+ var SQLITE_SCHEMA_VERSION = 1;
13753
13795
  function emptyOutboxRecoveryResult() {
13754
13796
  return {
13755
13797
  embedding: { recoveredProcessing: 0, retriedFailed: 0 },
@@ -14094,6 +14136,15 @@ var SQLiteEventStore = class {
14094
14136
  access_count INTEGER DEFAULT 0
14095
14137
  );
14096
14138
 
14139
+ -- Junction: consolidated memory -> source event id (indexed lookup that
14140
+ -- replaces per-event source_events LIKE scans during consolidation).
14141
+ CREATE TABLE IF NOT EXISTS consolidated_memory_events (
14142
+ memory_id TEXT NOT NULL,
14143
+ event_id TEXT NOT NULL,
14144
+ PRIMARY KEY (memory_id, event_id)
14145
+ );
14146
+ CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
14147
+
14097
14148
  -- Continuity Log table (tracks context transitions)
14098
14149
  CREATE TABLE IF NOT EXISTS continuity_log (
14099
14150
  log_id TEXT PRIMARY KEY,
@@ -14453,11 +14504,17 @@ var SQLiteEventStore = class {
14453
14504
  DELETE FROM events_fts WHERE rowid = OLD.rowid;
14454
14505
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
14455
14506
  END;
14507
+
14508
+ -- Reverse index for citation ids (a non-reversible hash of event id), so
14509
+ -- citation lookups are O(1) instead of scanning recent events and hashing
14510
+ -- each one. Populated lazily/self-healing by getEventByCitationId.
14511
+ CREATE TABLE IF NOT EXISTS event_citations (
14512
+ event_id TEXT PRIMARY KEY,
14513
+ citation_id TEXT NOT NULL
14514
+ );
14515
+ CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
14456
14516
  `);
14457
- try {
14458
- sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
14459
- } catch {
14460
- }
14517
+ this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
14461
14518
  try {
14462
14519
  const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
14463
14520
  const hasSourceAwareUnique = edgeIndexes.some((index) => {
@@ -14494,55 +14551,17 @@ var SQLiteEventStore = class {
14494
14551
  }
14495
14552
  } catch {
14496
14553
  }
14497
- try {
14498
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
14499
- } catch {
14500
- }
14501
- try {
14502
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
14503
- } catch {
14504
- }
14505
- try {
14506
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
14507
- } catch {
14508
- }
14509
- try {
14510
- sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
14511
- } catch {
14512
- }
14554
+ this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
14555
+ this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
14556
+ this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
14557
+ this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
14513
14558
  try {
14514
14559
  sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
14515
14560
  } catch {
14516
14561
  }
14517
- const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
14518
- const columnNames = tableInfo.map((col) => col.name);
14519
- if (!columnNames.includes("access_count")) {
14520
- try {
14521
- sqliteExec(this.db, `
14522
- ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
14523
- `);
14524
- } catch (err) {
14525
- console.error("Error adding access_count column:", err);
14526
- }
14527
- }
14528
- if (!columnNames.includes("last_accessed_at")) {
14529
- try {
14530
- sqliteExec(this.db, `
14531
- ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
14532
- `);
14533
- } catch (err) {
14534
- console.error("Error adding last_accessed_at column:", err);
14535
- }
14536
- }
14537
- if (!columnNames.includes("turn_id")) {
14538
- try {
14539
- sqliteExec(this.db, `
14540
- ALTER TABLE events ADD COLUMN turn_id TEXT;
14541
- `);
14542
- } catch (err) {
14543
- console.error("Error adding turn_id column:", err);
14544
- }
14545
- }
14562
+ this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
14563
+ this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
14564
+ this.addColumnIfMissing("events", "turn_id", "TEXT");
14546
14565
  try {
14547
14566
  sqliteExec(this.db, `
14548
14567
  CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
@@ -14559,8 +14578,18 @@ var SQLiteEventStore = class {
14559
14578
  sqliteExec(this.db, `
14560
14579
  CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
14561
14580
  `);
14562
- } catch (err) {
14581
+ } catch {
14563
14582
  }
14583
+ const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
14584
+ if ((cmeCount?.c ?? 0) === 0) {
14585
+ sqliteExec(this.db, `
14586
+ INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
14587
+ SELECT cm.memory_id, je.value
14588
+ FROM consolidated_memories cm, json_each(cm.source_events) je
14589
+ WHERE json_valid(cm.source_events);
14590
+ `);
14591
+ }
14592
+ sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
14564
14593
  this.initialized = true;
14565
14594
  }
14566
14595
  /**
@@ -14692,6 +14721,67 @@ var SQLiteEventStore = class {
14692
14721
  );
14693
14722
  return rows.map(this.rowToEvent);
14694
14723
  }
14724
+ /**
14725
+ * Aggregate event counts by type via SQL GROUP BY, instead of loading rows
14726
+ * into memory and counting in JS. Counts all events (the old recent-window
14727
+ * scan also under-counted stores larger than its cap).
14728
+ */
14729
+ async getEventTypeCounts(options) {
14730
+ await this.initialize();
14731
+ const rows = sqliteAll(
14732
+ this.db,
14733
+ `SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
14734
+ []
14735
+ );
14736
+ return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
14737
+ }
14738
+ /**
14739
+ * Fetch every event at/after an ISO timestamp (ascending), with no row cap.
14740
+ * For window-scoped analytics (KPI/usefulness) this fetches exactly the events
14741
+ * the window needs instead of an arbitrary "most recent N" slice that both
14742
+ * over-fetches sparse windows and truncates very active ones.
14743
+ */
14744
+ async getEventsAfter(sinceIso, options) {
14745
+ await this.initialize();
14746
+ const rows = sqliteAll(
14747
+ this.db,
14748
+ `SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
14749
+ [sinceIso]
14750
+ );
14751
+ return rows.map((row) => this.rowToEvent(row));
14752
+ }
14753
+ /** Count distinct sessions via SQL instead of materializing a Set in JS. */
14754
+ async getDistinctSessionCount(options) {
14755
+ await this.initialize();
14756
+ const row = sqliteGet(
14757
+ this.db,
14758
+ `SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
14759
+ []
14760
+ );
14761
+ return row?.count ?? 0;
14762
+ }
14763
+ /**
14764
+ * Per-day event counts (with type breakdown) since an ISO timestamp, computed
14765
+ * by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
14766
+ * strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
14767
+ * lexicographically.
14768
+ */
14769
+ async getDailyEventCounts(sinceIso, options) {
14770
+ await this.initialize();
14771
+ return sqliteAll(
14772
+ this.db,
14773
+ `SELECT substr(timestamp, 1, 10) as day,
14774
+ COUNT(*) as total,
14775
+ SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
14776
+ SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
14777
+ SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
14778
+ FROM events
14779
+ WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
14780
+ GROUP BY day
14781
+ ORDER BY day ASC`,
14782
+ [sinceIso]
14783
+ );
14784
+ }
14695
14785
  /**
14696
14786
  * Get event by ID
14697
14787
  */
@@ -14705,6 +14795,71 @@ var SQLiteEventStore = class {
14705
14795
  if (!row) return null;
14706
14796
  return this.rowToEvent(row);
14707
14797
  }
14798
+ /**
14799
+ * Batch-fetch events by id in a single query, applying the same quarantine
14800
+ * predicate as getEvent. Lets hot paths replace N sequential round-trips with
14801
+ * one. Missing ids are simply absent from the result (order not guaranteed).
14802
+ */
14803
+ async getEvents(ids, options) {
14804
+ await this.initialize();
14805
+ if (ids.length === 0) return [];
14806
+ const placeholders2 = ids.map(() => "?").join(",");
14807
+ const rows = sqliteAll(
14808
+ this.db,
14809
+ `SELECT * FROM events WHERE id IN (${placeholders2}) AND ${maybeQuarantinePredicate(options)}`,
14810
+ ids
14811
+ );
14812
+ return rows.map((row) => this.rowToEvent(row));
14813
+ }
14814
+ /**
14815
+ * Resolve an event from its citation id via the reverse index, instead of
14816
+ * scanning recent events and hashing each one. The index is self-healing: a
14817
+ * miss triggers a one-time backfill of any events not yet indexed (covering
14818
+ * freshly appended events) before retrying.
14819
+ */
14820
+ async getEventByCitationId(citationId, options) {
14821
+ await this.initialize();
14822
+ if (!citationId) return null;
14823
+ let row = sqliteGet(
14824
+ this.db,
14825
+ `SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
14826
+ [citationId]
14827
+ );
14828
+ if (!row) {
14829
+ this.indexMissingCitations();
14830
+ row = sqliteGet(
14831
+ this.db,
14832
+ `SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
14833
+ [citationId]
14834
+ );
14835
+ }
14836
+ if (!row) return null;
14837
+ return this.getEvent(row.event_id, options);
14838
+ }
14839
+ /**
14840
+ * Backfill citation ids for any events not yet present in event_citations.
14841
+ * Gated on a cheap count comparison so the common (fully-indexed) path does no
14842
+ * work, and the scan only runs when there are genuinely new events to index.
14843
+ */
14844
+ indexMissingCitations() {
14845
+ const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
14846
+ const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
14847
+ if (indexedCount >= eventCount) return;
14848
+ const unindexed = sqliteAll(
14849
+ this.db,
14850
+ `SELECT e.id FROM events e
14851
+ LEFT JOIN event_citations ec ON ec.event_id = e.id
14852
+ WHERE ec.event_id IS NULL`,
14853
+ []
14854
+ );
14855
+ if (unindexed.length === 0) return;
14856
+ const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
14857
+ this.db.transaction(() => {
14858
+ for (const { id } of unindexed) {
14859
+ insert.run(id, generateCitationId(id));
14860
+ }
14861
+ })();
14862
+ }
14708
14863
  /**
14709
14864
  * Get events since a timestamp (for sync)
14710
14865
  */
@@ -14906,25 +15061,22 @@ var SQLiteEventStore = class {
14906
15061
  */
14907
15062
  async getPendingOutboxItems(limit = 32) {
14908
15063
  await this.initialize();
14909
- const pending = sqliteAll(
14910
- this.db,
14911
- `SELECT * FROM embedding_outbox
14912
- WHERE status = 'pending'
14913
- ORDER BY created_at
14914
- LIMIT ?`,
14915
- [limit]
14916
- );
14917
- if (pending.length === 0) return [];
14918
- const ids = pending.map((r) => r.id);
14919
- const placeholders2 = ids.map(() => "?").join(",");
14920
- sqliteRun(
15064
+ const claimed = sqliteAll(
14921
15065
  this.db,
14922
15066
  `UPDATE embedding_outbox
14923
15067
  SET status = 'processing', processed_at = datetime('now'), error_message = NULL
14924
- WHERE id IN (${placeholders2})`,
14925
- ids
15068
+ WHERE id IN (
15069
+ SELECT id FROM embedding_outbox
15070
+ WHERE status = 'pending'
15071
+ ORDER BY created_at
15072
+ LIMIT ?
15073
+ )
15074
+ RETURNING *`,
15075
+ [limit]
14926
15076
  );
14927
- return pending.map((row) => ({
15077
+ if (claimed.length === 0) return [];
15078
+ claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
15079
+ return claimed.map((row) => ({
14928
15080
  id: row.id,
14929
15081
  eventId: row.event_id,
14930
15082
  content: row.content,
@@ -15528,7 +15680,7 @@ var SQLiteEventStore = class {
15528
15680
  const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
15529
15681
  const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
15530
15682
  let toolSuccessCount = 0;
15531
- let toolTotalCount = toolEvents.length;
15683
+ const toolTotalCount = toolEvents.length;
15532
15684
  for (const t of toolEvents) {
15533
15685
  try {
15534
15686
  const content = JSON.parse(t.content);
@@ -15691,10 +15843,14 @@ var SQLiteEventStore = class {
15691
15843
  * Rebuild FTS index from existing events
15692
15844
  * Call this once after upgrading to FTS5
15693
15845
  */
15694
- async rebuildFtsIndex() {
15695
- await this.initialize();
15696
- const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
15697
- const totalEvents = countRow?.count ?? 0;
15846
+ /**
15847
+ * Drop and recreate the events_fts virtual table + sync triggers, repopulating
15848
+ * from the current events table. Recreating the table (instead of issuing
15849
+ * DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
15850
+ * with `no such column: T.event_id` on synthetic deletes. Safe to call inside
15851
+ * a transaction (pure DDL + INSERT...SELECT, no transaction control).
15852
+ */
15853
+ recreateEventsFtsTable() {
15698
15854
  sqliteExec(this.db, `
15699
15855
  DROP TRIGGER IF EXISTS events_fts_insert;
15700
15856
  DROP TRIGGER IF EXISTS events_fts_delete;
@@ -15723,6 +15879,12 @@ var SQLiteEventStore = class {
15723
15879
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
15724
15880
  END;
15725
15881
  `);
15882
+ }
15883
+ async rebuildFtsIndex() {
15884
+ await this.initialize();
15885
+ const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
15886
+ const totalEvents = countRow?.count ?? 0;
15887
+ this.recreateEventsFtsTable();
15726
15888
  return totalEvents;
15727
15889
  }
15728
15890
  /**
@@ -15740,6 +15902,35 @@ var SQLiteEventStore = class {
15740
15902
  return false;
15741
15903
  }
15742
15904
  }
15905
+ hasTable(tableName) {
15906
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
15907
+ try {
15908
+ const rows = sqliteAll(
15909
+ this.db,
15910
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
15911
+ [tableName]
15912
+ );
15913
+ return rows.length > 0;
15914
+ } catch {
15915
+ return false;
15916
+ }
15917
+ }
15918
+ /**
15919
+ * Add a column only when its table exists and the column is missing.
15920
+ *
15921
+ * Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
15922
+ * migrated" / "table not present yet" (both safely skipped) from a genuine
15923
+ * failure (disk full, corruption), which is allowed to surface instead of
15924
+ * being silently swallowed as if the migration had succeeded.
15925
+ */
15926
+ addColumnIfMissing(table, column, definition) {
15927
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
15928
+ throw new Error(`Invalid identifier in migration: ${table}.${column}`);
15929
+ }
15930
+ if (!this.hasTable(table)) return;
15931
+ if (this.hasTableColumn(table, column)) return;
15932
+ sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
15933
+ }
15743
15934
  async recordRetrievalTrace(input) {
15744
15935
  await this.initialize();
15745
15936
  const traceId = randomUUID6();
@@ -15825,6 +16016,45 @@ var SQLiteEventStore = class {
15825
16016
  FROM retrieval_traces`,
15826
16017
  []
15827
16018
  );
16019
+ const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
16020
+ const strategyRows = sqliteAll(
16021
+ this.db,
16022
+ `SELECT
16023
+ ${strategyColumnSql} as strategy,
16024
+ COUNT(*) as total_queries,
16025
+ SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
16026
+ SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
16027
+ SUM(candidate_count) as total_candidate_count,
16028
+ SUM(selected_count) as total_selected_count,
16029
+ AVG(candidate_count) as avg_candidate_count,
16030
+ AVG(selected_count) as avg_selected_count,
16031
+ CASE
16032
+ WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
16033
+ ELSE 0
16034
+ END as selection_rate
16035
+ FROM retrieval_traces
16036
+ GROUP BY ${strategyColumnSql}
16037
+ ORDER BY total_queries DESC, strategy ASC`,
16038
+ []
16039
+ );
16040
+ const strategyBreakdown = strategyRows.map((strategyRow) => {
16041
+ const strategyTotalQueries = Number(strategyRow.total_queries || 0);
16042
+ const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
16043
+ const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
16044
+ return {
16045
+ strategy: String(strategyRow.strategy || "unknown"),
16046
+ totalQueries: strategyTotalQueries,
16047
+ queriesWithSelection: strategyQueriesWithSelection,
16048
+ rewrittenQueries: strategyRewrittenQueries,
16049
+ rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
16050
+ totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
16051
+ totalSelectedCount: Number(strategyRow.total_selected_count || 0),
16052
+ avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
16053
+ avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
16054
+ selectionRate: Number(strategyRow.selection_rate || 0),
16055
+ queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
16056
+ };
16057
+ });
15828
16058
  const totalQueries = Number(row?.total_queries || 0);
15829
16059
  const rewrittenQueries = Number(row?.rewritten_queries || 0);
15830
16060
  const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
@@ -15842,7 +16072,8 @@ var SQLiteEventStore = class {
15842
16072
  rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
15843
16073
  rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
15844
16074
  avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
15845
- avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
16075
+ avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
16076
+ strategyBreakdown
15846
16077
  };
15847
16078
  } catch (err) {
15848
16079
  if (err?.message?.includes("no such table")) {
@@ -15858,7 +16089,8 @@ var SQLiteEventStore = class {
15858
16089
  rewrittenSelectionRate: 0,
15859
16090
  rawSelectionRate: 0,
15860
16091
  avgSelectedCountForRewrittenQueries: 0,
15861
- avgSelectedCountForRawQueries: 0
16092
+ avgSelectedCountForRawQueries: 0,
16093
+ strategyBreakdown: []
15862
16094
  };
15863
16095
  }
15864
16096
  throw err;
@@ -15975,38 +16207,19 @@ var SQLiteEventStore = class {
15975
16207
  if (events.length === 0) return 0;
15976
16208
  const eventIds = events.map((e) => e.id);
15977
16209
  const placeholders2 = eventIds.map(() => "?").join(",");
15978
- const ftsTriggersDropped = [];
15979
- for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
15980
- try {
16210
+ const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
16211
+ const runDelete = this.db.transaction(() => {
16212
+ for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
15981
16213
  sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
15982
- ftsTriggersDropped.push(triggerName);
15983
- } catch {
15984
16214
  }
15985
- }
15986
- for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
15987
- try {
16215
+ for (const table of relatedTables) {
15988
16216
  sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders2})`, eventIds);
15989
- } catch {
15990
- }
15991
- }
15992
- const result2 = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
15993
- if (ftsTriggersDropped.length > 0) {
15994
- try {
15995
- sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
15996
- sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
15997
- INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
15998
- END`);
15999
- sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
16000
- DELETE FROM events_fts WHERE rowid = OLD.rowid;
16001
- END`);
16002
- sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
16003
- DELETE FROM events_fts WHERE rowid = OLD.rowid;
16004
- INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
16005
- END`);
16006
- } catch {
16007
16217
  }
16008
- }
16009
- return result2.changes || 0;
16218
+ const result2 = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
16219
+ this.recreateEventsFtsTable();
16220
+ return result2.changes || 0;
16221
+ });
16222
+ return runDelete();
16010
16223
  }
16011
16224
  /**
16012
16225
  * Convert database row to MemoryEvent
@@ -19937,7 +20150,7 @@ var ActorCardRepository = class {
19937
20150
  };
19938
20151
 
19939
20152
  // src/core/operations/perspective-observation-repository.ts
19940
- import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
20153
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
19941
20154
  function projectHashToStorage6(projectHash) {
19942
20155
  return projectHash ?? "";
19943
20156
  }
@@ -19974,7 +20187,7 @@ function sanitizeStoredRecord(value) {
19974
20187
  return sanitizeGovernanceAuditValue(value);
19975
20188
  }
19976
20189
  function stableHash(value) {
19977
- return createHash3("sha256").update(JSON.stringify(value)).digest("hex");
20190
+ return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
19978
20191
  }
19979
20192
  function evidenceHash(sourceEventIds, sourceObservationIds) {
19980
20193
  return stableHash({
@@ -20482,19 +20695,6 @@ function createPerspectiveDeriver(options) {
20482
20695
  return new PerspectiveDeriver(options);
20483
20696
  }
20484
20697
 
20485
- // src/core/citation-generator.ts
20486
- import { createHash as createHash4 } from "crypto";
20487
- var ID_LENGTH = 6;
20488
- var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
20489
- function generateCitationId(eventId) {
20490
- const hash = createHash4("sha256").update(eventId).digest();
20491
- let id = "";
20492
- for (let i = 0; i < ID_LENGTH; i++) {
20493
- id += CHARSET[hash[i] % CHARSET.length];
20494
- }
20495
- return id;
20496
- }
20497
-
20498
20698
  // src/core/vector-store.ts
20499
20699
  import * as lancedb from "@lancedb/lancedb";
20500
20700
  var VectorStore = class {
@@ -21102,6 +21302,30 @@ var MemoryQueryService = class {
21102
21302
  score: 1 - (r.rank - minRank) / rankRange
21103
21303
  })).filter((r) => !options?.minScore || r.score >= options.minScore);
21104
21304
  }
21305
+ async getEvent(id) {
21306
+ await this.initialize();
21307
+ return this.queryStore.getEvent(id);
21308
+ }
21309
+ async getEventByCitationId(citationId) {
21310
+ await this.initialize();
21311
+ return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
21312
+ }
21313
+ async getEventsAfter(sinceIso) {
21314
+ await this.initialize();
21315
+ return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
21316
+ }
21317
+ async getEventTypeCounts() {
21318
+ await this.initialize();
21319
+ return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
21320
+ }
21321
+ async getDistinctSessionCount() {
21322
+ await this.initialize();
21323
+ return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
21324
+ }
21325
+ async getDailyEventCounts(sinceIso) {
21326
+ await this.initialize();
21327
+ return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
21328
+ }
21105
21329
  async getSessionHistory(sessionId) {
21106
21330
  await this.initialize();
21107
21331
  return this.queryStore.getSessionEvents(sessionId);
@@ -22070,11 +22294,12 @@ var Retriever = class {
22070
22294
  if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
22071
22295
  const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
22072
22296
  const filtered = [];
22297
+ const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
22073
22298
  for (const result2 of results) {
22074
22299
  if (scope?.sessionId && result2.sessionId !== scope.sessionId) continue;
22075
22300
  if (scope?.sessionIdPrefix && !result2.sessionId.startsWith(scope.sessionIdPrefix)) continue;
22076
22301
  if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result2.eventType)) continue;
22077
- const event = await this.eventStore.getEvent(result2.eventId);
22302
+ const event = eventMap.get(result2.eventId);
22078
22303
  if (!event) continue;
22079
22304
  if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
22080
22305
  if (normalizedIncludes.length > 0) {
@@ -22183,10 +22408,30 @@ var Retriever = class {
22183
22408
  async retrieveRecent(limit = 100) {
22184
22409
  return this.eventStore.getRecentEvents(limit);
22185
22410
  }
22411
+ /**
22412
+ * Batch-load events by id into a Map in a single query, de-duplicating ids.
22413
+ * Replaces per-result getEvent round-trips in the scope/enrich hot paths.
22414
+ */
22415
+ async fetchEventsByIds(ids) {
22416
+ const map = /* @__PURE__ */ new Map();
22417
+ if (ids.length === 0) return map;
22418
+ const unique2 = [...new Set(ids)];
22419
+ if (typeof this.eventStore.getEvents === "function") {
22420
+ const events = await this.eventStore.getEvents(unique2);
22421
+ for (const event of events) map.set(event.id, event);
22422
+ } else {
22423
+ const events = await Promise.all(unique2.map((id) => this.eventStore.getEvent(id)));
22424
+ for (const event of events) {
22425
+ if (event) map.set(event.id, event);
22426
+ }
22427
+ }
22428
+ return map;
22429
+ }
22186
22430
  async enrichResults(results, options, query) {
22187
22431
  const memories = [];
22432
+ const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
22188
22433
  for (const result2 of results) {
22189
- const event = await this.eventStore.getEvent(result2.eventId);
22434
+ const event = eventMap.get(result2.eventId);
22190
22435
  if (!event) continue;
22191
22436
  if (this.graduation) {
22192
22437
  this.graduation.recordAccess(event.id, options.sessionId || "unknown", result2.score);
@@ -24939,6 +25184,34 @@ var MemoryService = class {
24939
25184
  async rebuildFtsIndex() {
24940
25185
  return this.queryService.rebuildFtsIndex();
24941
25186
  }
25187
+ /**
25188
+ * Get a single event by id (indexed lookup).
25189
+ */
25190
+ async getEvent(id) {
25191
+ return this.queryService.getEvent(id);
25192
+ }
25193
+ /**
25194
+ * Resolve an event from its citation id via the reverse index.
25195
+ */
25196
+ async getEventByCitationId(citationId) {
25197
+ return this.queryService.getEventByCitationId(citationId);
25198
+ }
25199
+ /** All events at/after an ISO timestamp (ascending, uncapped). */
25200
+ async getEventsAfter(sinceIso) {
25201
+ return this.queryService.getEventsAfter(sinceIso);
25202
+ }
25203
+ /** Aggregate event counts by type (SQL GROUP BY). */
25204
+ async getEventTypeCounts() {
25205
+ return this.queryService.getEventTypeCounts();
25206
+ }
25207
+ /** Count distinct sessions (SQL). */
25208
+ async getDistinctSessionCount() {
25209
+ return this.queryService.getDistinctSessionCount();
25210
+ }
25211
+ /** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
25212
+ async getDailyEventCounts(sinceIso) {
25213
+ return this.queryService.getDailyEventCounts(sinceIso);
25214
+ }
24942
25215
  /**
24943
25216
  * Get session history
24944
25217
  */
@@ -25268,6 +25541,20 @@ import * as path11 from "path";
25268
25541
  import * as os6 from "os";
25269
25542
  import * as readline from "readline";
25270
25543
  import { randomUUID as randomUUID16 } from "crypto";
25544
+
25545
+ // src/services/turn-buffering.ts
25546
+ var MIN_SUBSTANTIVE_RESPONSE_LEN = 100;
25547
+ var MAX_AGENT_RESPONSE_LEN = 1e4;
25548
+ function mergeAgentResponseBlocks(blocks) {
25549
+ const substantive = blocks.filter((block) => block.length >= MIN_SUBSTANTIVE_RESPONSE_LEN);
25550
+ if (substantive.length > 0) return substantive.join("\n\n");
25551
+ return blocks.reduce((longest, block) => longest.length >= block.length ? longest : block, "");
25552
+ }
25553
+ function truncateAgentResponse(text) {
25554
+ return text.length > MAX_AGENT_RESPONSE_LEN ? `${text.slice(0, MAX_AGENT_RESPONSE_LEN)}...[truncated]` : text;
25555
+ }
25556
+
25557
+ // src/services/session-history-importer.ts
25271
25558
  function isClaudeLocalCommandArtifact(content) {
25272
25559
  const trimmed = content.trim();
25273
25560
  return /^<local-command-(stdout|stderr)>/.test(trimmed) || /^<command-(name|message)>/.test(trimmed) || trimmed.includes("<command-name>") && trimmed.includes("<local-command-stdout>");
@@ -25399,6 +25686,34 @@ var SessionHistoryImporter = class {
25399
25686
  /**
25400
25687
  * Import a specific session file
25401
25688
  */
25689
+ /**
25690
+ * Pre-flight check used before a destructive force-reimport: returns true only
25691
+ * if the file can be opened and contains at least one line that parses as JSON.
25692
+ * Streams the file and short-circuits on the first valid record.
25693
+ */
25694
+ async hasParseableContent(filePath) {
25695
+ let stream;
25696
+ let rl;
25697
+ try {
25698
+ stream = fs9.createReadStream(filePath);
25699
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
25700
+ for await (const line of rl) {
25701
+ const trimmed = line.trim();
25702
+ if (!trimmed) continue;
25703
+ try {
25704
+ JSON.parse(trimmed);
25705
+ return true;
25706
+ } catch {
25707
+ }
25708
+ }
25709
+ return false;
25710
+ } catch {
25711
+ return false;
25712
+ } finally {
25713
+ rl?.close();
25714
+ stream?.close();
25715
+ }
25716
+ }
25402
25717
  async importSessionFile(filePath, options = {}) {
25403
25718
  const result2 = {
25404
25719
  totalSessions: 1,
@@ -25414,6 +25729,10 @@ var SessionHistoryImporter = class {
25414
25729
  }
25415
25730
  const sessionId = path11.basename(filePath, ".jsonl");
25416
25731
  if (options.force) {
25732
+ if (!await this.hasParseableContent(filePath)) {
25733
+ result2.errors.push(`Skipped force reimport (no parseable records): ${filePath}`);
25734
+ return result2;
25735
+ }
25417
25736
  const deleted = await this.memoryService.deleteSessionEvents(sessionId);
25418
25737
  if (options.verbose && deleted > 0) {
25419
25738
  console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
@@ -25435,13 +25754,12 @@ var SessionHistoryImporter = class {
25435
25754
  let lastTimestamp;
25436
25755
  const flushTextBuffer = async () => {
25437
25756
  if (textBuffer.length === 0 || !currentTurnId) return;
25438
- const substantive = textBuffer.filter((t) => t.length >= 100);
25439
- const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
25757
+ const merged = mergeAgentResponseBlocks(textBuffer);
25440
25758
  if (!merged) {
25441
25759
  textBuffer = [];
25442
25760
  return;
25443
25761
  }
25444
- const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
25762
+ const truncated = truncateAgentResponse(merged);
25445
25763
  const appendResult = await this.memoryService.storeAgentResponse(
25446
25764
  sessionId,
25447
25765
  truncated,
@@ -25455,55 +25773,60 @@ var SessionHistoryImporter = class {
25455
25773
  lineCount++;
25456
25774
  textBuffer = [];
25457
25775
  };
25458
- for await (const line of rl) {
25459
- if (lineCount >= limit) break;
25460
- try {
25461
- const entry = JSON.parse(line);
25462
- result2.totalMessages++;
25463
- const msgClass = classifyEntry(entry);
25464
- if (msgClass === "user_prompt") {
25465
- await flushTextBuffer();
25466
- const content = this.extractContent(entry);
25467
- if (!content) continue;
25468
- if (!isWorthStoringPrompt(content)) {
25469
- result2.skippedDuplicates++;
25470
- continue;
25471
- }
25472
- currentTurnId = randomUUID16();
25473
- const appendResult = await this.memoryService.storeUserPrompt(
25474
- sessionId,
25475
- content,
25476
- { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
25477
- );
25478
- if (appendResult.success && appendResult.isDuplicate) {
25479
- result2.skippedDuplicates++;
25480
- } else {
25481
- result2.importedPrompts++;
25776
+ try {
25777
+ for await (const line of rl) {
25778
+ if (lineCount >= limit) break;
25779
+ try {
25780
+ const entry = JSON.parse(line);
25781
+ result2.totalMessages++;
25782
+ const msgClass = classifyEntry(entry);
25783
+ if (msgClass === "user_prompt") {
25784
+ await flushTextBuffer();
25785
+ const content = this.extractContent(entry);
25786
+ if (!content) continue;
25787
+ if (!isWorthStoringPrompt(content)) {
25788
+ result2.skippedDuplicates++;
25789
+ continue;
25790
+ }
25791
+ currentTurnId = randomUUID16();
25792
+ const appendResult = await this.memoryService.storeUserPrompt(
25793
+ sessionId,
25794
+ content,
25795
+ { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
25796
+ );
25797
+ if (appendResult.success && appendResult.isDuplicate) {
25798
+ result2.skippedDuplicates++;
25799
+ } else {
25800
+ result2.importedPrompts++;
25801
+ }
25802
+ lineCount++;
25803
+ } else if (msgClass === "agent_text") {
25804
+ const content = this.extractContent(entry);
25805
+ if (content) {
25806
+ textBuffer.push(content);
25807
+ lastTimestamp = entry.timestamp;
25808
+ }
25482
25809
  }
25483
- lineCount++;
25484
- } else if (msgClass === "agent_text") {
25485
- const content = this.extractContent(entry);
25486
- if (content) {
25487
- textBuffer.push(content);
25488
- lastTimestamp = entry.timestamp;
25810
+ const now = Date.now();
25811
+ if (now - lastProgressAt > 200) {
25812
+ lastProgressAt = now;
25813
+ onProgress?.({
25814
+ phase: "session-progress",
25815
+ sessionIndex,
25816
+ messagesProcessed: result2.totalMessages,
25817
+ imported: result2.importedPrompts + result2.importedResponses,
25818
+ skipped: result2.skippedDuplicates
25819
+ });
25489
25820
  }
25821
+ } catch (parseError) {
25822
+ result2.errors.push(`Parse error on line: ${parseError}`);
25490
25823
  }
25491
- const now = Date.now();
25492
- if (now - lastProgressAt > 200) {
25493
- lastProgressAt = now;
25494
- onProgress?.({
25495
- phase: "session-progress",
25496
- sessionIndex,
25497
- messagesProcessed: result2.totalMessages,
25498
- imported: result2.importedPrompts + result2.importedResponses,
25499
- skipped: result2.skippedDuplicates
25500
- });
25501
- }
25502
- } catch (parseError) {
25503
- result2.errors.push(`Parse error on line: ${parseError}`);
25504
25824
  }
25825
+ await flushTextBuffer();
25826
+ } finally {
25827
+ rl.close();
25828
+ fileStream.close();
25505
25829
  }
25506
- await flushTextBuffer();
25507
25830
  await this.memoryService.endSession(sessionId);
25508
25831
  if (options.projectPath) {
25509
25832
  registerSession(sessionId, options.projectPath);
@@ -25711,6 +26034,29 @@ function extractCodexContentText(content, maxContentChars = CODEX_VALIDATION_DEF
25711
26034
  function extractTextFromContent(content) {
25712
26035
  return extractCodexContentText(content).text;
25713
26036
  }
26037
+ function listCodexSessionFilesRecursive(rootDir) {
26038
+ if (!fs10.existsSync(rootDir)) return [];
26039
+ const out = [];
26040
+ const stack = [rootDir];
26041
+ while (stack.length > 0) {
26042
+ const dir = stack.pop();
26043
+ let entries;
26044
+ try {
26045
+ entries = fs10.readdirSync(dir, { withFileTypes: true });
26046
+ } catch {
26047
+ continue;
26048
+ }
26049
+ for (const ent of entries) {
26050
+ const fullPath = path12.join(dir, ent.name);
26051
+ if (ent.isDirectory()) {
26052
+ stack.push(fullPath);
26053
+ } else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
26054
+ out.push(fullPath);
26055
+ }
26056
+ }
26057
+ }
26058
+ return out.sort();
26059
+ }
25714
26060
  function getFileMtimeMs2(filePath) {
25715
26061
  try {
25716
26062
  return fs10.statSync(filePath).mtimeMs;
@@ -25741,29 +26087,6 @@ var CodexSessionHistoryImporter = class {
25741
26087
  getSessionsRoot() {
25742
26088
  return this.sessionsRoot;
25743
26089
  }
25744
- listSessionFilesRecursive(rootDir) {
25745
- if (!fs10.existsSync(rootDir)) return [];
25746
- const out = [];
25747
- const stack = [rootDir];
25748
- while (stack.length > 0) {
25749
- const dir = stack.pop();
25750
- let entries;
25751
- try {
25752
- entries = fs10.readdirSync(dir, { withFileTypes: true });
25753
- } catch {
25754
- continue;
25755
- }
25756
- for (const ent of entries) {
25757
- const fullPath = path12.join(dir, ent.name);
25758
- if (ent.isDirectory()) {
25759
- stack.push(fullPath);
25760
- } else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
25761
- out.push(fullPath);
25762
- }
25763
- }
25764
- }
25765
- return out;
25766
- }
25767
26090
  async readSessionMeta(filePath) {
25768
26091
  const fileStream = fs10.createReadStream(filePath, { encoding: "utf-8" });
25769
26092
  const rl = readline2.createInterface({ input: fileStream, crlfDelay: Infinity });
@@ -25813,7 +26136,7 @@ var CodexSessionHistoryImporter = class {
25813
26136
  }
25814
26137
  const normalizedTarget = normalizeMaybeRealpath(projectPath);
25815
26138
  onProgress?.({ phase: "scan", message: "Scanning Codex session files..." });
25816
- const sessionFiles = this.listSessionFilesRecursive(sessionsRoot);
26139
+ const sessionFiles = listCodexSessionFilesRecursive(sessionsRoot);
25817
26140
  const matchingFiles = [];
25818
26141
  for (const filePath of sessionFiles) {
25819
26142
  try {
@@ -25878,7 +26201,7 @@ var CodexSessionHistoryImporter = class {
25878
26201
  return result2;
25879
26202
  }
25880
26203
  onProgress?.({ phase: "scan", message: "Scanning all Codex sessions..." });
25881
- const sessionFiles = this.listSessionFilesRecursive(sessionsRoot);
26204
+ const sessionFiles = listCodexSessionFilesRecursive(sessionsRoot);
25882
26205
  const selectedFiles = selectRecentCodexSessionFiles(sessionFiles, options.sessionLimit);
25883
26206
  onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} Codex session file(s)` });
25884
26207
  const totalLimit = normalizePositiveImportLimit(options.limit);
@@ -25957,13 +26280,12 @@ var CodexSessionHistoryImporter = class {
25957
26280
  return;
25958
26281
  }
25959
26282
  if (textBuffer.length === 0 || !currentTurnId) return;
25960
- const substantive = textBuffer.filter((t) => t.length >= 100);
25961
- const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
26283
+ const merged = mergeAgentResponseBlocks(textBuffer);
25962
26284
  if (!merged) {
25963
26285
  textBuffer = [];
25964
26286
  return;
25965
26287
  }
25966
- const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
26288
+ const truncated = truncateAgentResponse(merged);
25967
26289
  const appendResult = await this.memoryService.storeAgentResponse(
25968
26290
  sessionId,
25969
26291
  truncated,
@@ -26047,7 +26369,7 @@ var CodexSessionHistoryImporter = class {
26047
26369
  async listAvailableSessions(projectPath) {
26048
26370
  const sessionsRoot = this.getSessionsRoot();
26049
26371
  if (!fs10.existsSync(sessionsRoot)) return [];
26050
- const files = this.listSessionFilesRecursive(sessionsRoot);
26372
+ const files = listCodexSessionFilesRecursive(sessionsRoot);
26051
26373
  const sessions = [];
26052
26374
  const normalizedTarget = projectPath ? normalizeMaybeRealpath(projectPath) : null;
26053
26375
  for (const filePath of files) {
@@ -26345,9 +26667,7 @@ var HermesSessionHistoryImporter = class {
26345
26667
  return;
26346
26668
  }
26347
26669
  if (textBuffer.length === 0 || !currentTurnId) return;
26348
- const contentBlocks = textBuffer.map((item) => item.content);
26349
- const substantive = contentBlocks.filter((content) => content.length >= 100);
26350
- const merged = substantive.length > 0 ? substantive.join("\n\n") : contentBlocks.reduce((a, b) => a.length >= b.length ? a : b, "");
26670
+ const merged = mergeAgentResponseBlocks(textBuffer.map((item) => item.content));
26351
26671
  if (!merged) {
26352
26672
  textBuffer = [];
26353
26673
  return;
@@ -27959,10 +28279,10 @@ async function handleExternalMarketContext(args) {
27959
28279
  return textResult(renderExternalMarketContextReport(report));
27960
28280
  }
27961
28281
  async function handleMemSearch(memoryService, args) {
27962
- const query = args.query;
28282
+ const query = requiredOperationString(args.query, "query");
27963
28283
  const topK = numberArg(args.topK, 5, 1, 20);
27964
28284
  const fetchTopK = Math.min(topK * 3, 20);
27965
- const sessionId = args.sessionId;
28285
+ const sessionId = optionalString(args.sessionId);
27966
28286
  const eventType = eventTypeArg(args.eventType);
27967
28287
  const search = await retrieveMcpMemories(memoryService, query, { topK, fetchTopK, sessionId, eventType });
27968
28288
  const lines = [
@@ -28050,8 +28370,8 @@ function scoreKeywordMatch(content, queryTokens) {
28050
28370
  return hits / queryTokens.length;
28051
28371
  }
28052
28372
  async function handleMemTimeline(memoryService, args) {
28053
- const ids = args.ids;
28054
- const windowSize = args.windowSize || 3;
28373
+ const ids = stringArrayOperationArg(args.ids, 100);
28374
+ const windowSize = numberArg(args.windowSize, 3, 0, 50);
28055
28375
  const recentEvents = await memoryService.getRecentEvents(1e4);
28056
28376
  const lines = [
28057
28377
  "## Timeline Context",
@@ -28088,7 +28408,10 @@ async function handleMemTimeline(memoryService, args) {
28088
28408
  };
28089
28409
  }
28090
28410
  async function handleMemDetails(memoryService, args) {
28091
- const ids = args.ids;
28411
+ const ids = stringArrayOperationArg(args.ids, 200);
28412
+ if (ids.length === 0) {
28413
+ return { content: [{ type: "text", text: "## No memory ids provided." }] };
28414
+ }
28092
28415
  const recentEvents = await memoryService.getRecentEvents(1e4);
28093
28416
  const lines = [];
28094
28417
  for (const targetId of ids) {
@@ -28110,7 +28433,7 @@ async function handleMemDetails(memoryService, args) {
28110
28433
  lines.push("");
28111
28434
  lines.push("**Content**:");
28112
28435
  lines.push("```");
28113
- lines.push(event.content);
28436
+ lines.push(applyPrivacyFilter(event.content, MCP_PRIVACY_CONFIG).content);
28114
28437
  lines.push("```");
28115
28438
  lines.push("");
28116
28439
  lines.push("---");
@@ -28165,6 +28488,12 @@ async function handleMemContextPack(memoryService, args) {
28165
28488
  recentSessionIds,
28166
28489
  projectPath
28167
28490
  });
28491
+ await recordMcpContextPackTrace(memoryService, {
28492
+ query,
28493
+ sessionId,
28494
+ candidates: search.memories,
28495
+ selected: relevantMemories
28496
+ });
28168
28497
  const hasPerspectiveContext = optionalString(args.observerActorId) !== void 0 || optionalString(args.targetActorId) !== void 0 || optionalString(args.observedActorId) !== void 0;
28169
28498
  let perspectiveBundle;
28170
28499
  let perspectiveWarning;
@@ -28251,6 +28580,23 @@ async function handleMemContextPack(memoryService, args) {
28251
28580
  }
28252
28581
  return textResult(applyContextPackBudget(lines.join("\n"), maxContextChars));
28253
28582
  }
28583
+ async function recordMcpContextPackTrace(memoryService, input) {
28584
+ try {
28585
+ await memoryService.recordQueryTrace({
28586
+ sessionId: input.sessionId,
28587
+ queryText: sanitizeOperationString(input.query, 1e3),
28588
+ queryRewriteKind: "none",
28589
+ strategy: "mcp-context-pack",
28590
+ candidateEventIds: uniqueContextPackEventIds(input.candidates),
28591
+ selectedEventIds: uniqueContextPackEventIds(input.selected),
28592
+ confidence: input.selected.length > 0 ? "suggested" : "none"
28593
+ });
28594
+ } catch {
28595
+ }
28596
+ }
28597
+ function uniqueContextPackEventIds(memories) {
28598
+ return Array.from(new Set(memories.map((memory) => memory.event.id).filter(Boolean)));
28599
+ }
28254
28600
  async function runLatestImport(memoryService, options) {
28255
28601
  const summaries = [];
28256
28602
  for (const source of options.sources) {
@@ -28934,7 +29280,7 @@ function formatMcpProcessingAge(ageMs) {
28934
29280
  return `${Math.round(hours / 24)}d`;
28935
29281
  }
28936
29282
  function formatMcpOutboxQueueStats(stats) {
28937
- return `pending=${stats.pending}, processing=${stats.processing}, failed=${stats.failed}, stuck=${stats.stuckProcessing}, oldestProcessingAge=${formatMcpProcessingAge(stats.oldestProcessingAgeMs)}, total=${stats.total}`;
29283
+ return `pending=${stats.pending}, processing=${stats.processing}, failed=${stats.failed}, retryableFailed=${stats.retryableFailed ?? 0}, quarantinedFailed=${stats.quarantinedFailed ?? 0}, stuck=${stats.stuckProcessing}, oldestProcessingAge=${formatMcpProcessingAge(stats.oldestProcessingAgeMs)}, total=${stats.total}`;
28938
29284
  }
28939
29285
  async function handleMemStats(memoryService, args) {
28940
29286
  const stats = await memoryService.getStats();
@@ -28981,8 +29327,8 @@ async function readMcpOutboxStats(memoryService) {
28981
29327
  return await memoryService.getOutboxStats();
28982
29328
  } catch {
28983
29329
  return {
28984
- embedding: { pending: 0, processing: 0, failed: 0, total: 0, stuckProcessing: 0, oldestProcessingAgeMs: null },
28985
- vector: { pending: 0, processing: 0, failed: 0, total: 0, stuckProcessing: 0, oldestProcessingAgeMs: null }
29330
+ embedding: { pending: 0, processing: 0, failed: 0, retryableFailed: 0, quarantinedFailed: 0, total: 0, stuckProcessing: 0, oldestProcessingAgeMs: null },
29331
+ vector: { pending: 0, processing: 0, failed: 0, retryableFailed: 0, quarantinedFailed: 0, total: 0, stuckProcessing: 0, oldestProcessingAgeMs: null }
28986
29332
  };
28987
29333
  }
28988
29334
  }