claude-memory-layer 1.0.10 → 1.0.12

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.
Files changed (142) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +3577 -389
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1383 -138
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1917 -214
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1813 -231
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1802 -205
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1909 -248
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1861 -206
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +2341 -217
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +2350 -226
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1805 -206
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +1447 -55
  49. package/dist/ui/index.html +318 -147
  50. package/dist/ui/style.css +892 -0
  51. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  52. package/docs/MEMU_ADOPTION.md +40 -0
  53. package/docs/OPERATIONS.md +18 -0
  54. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  55. package/memory/_index.md +405 -0
  56. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  57. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  58. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  59. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  60. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  61. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  62. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  63. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  64. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  65. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  66. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  67. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  68. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  69. package/package.json +9 -2
  70. package/scripts/build.ts +6 -0
  71. package/scripts/fix-sync-gap.js +32 -0
  72. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  73. package/scripts/report-sync-gap.js +26 -0
  74. package/scripts/review-queue-auto-resolve.js +21 -0
  75. package/scripts/sync-gap-auto-heal.sh +17 -0
  76. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  77. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  78. package/src/cli/index.ts +391 -60
  79. package/src/core/consolidated-store.ts +63 -1
  80. package/src/core/consolidation-worker.ts +115 -6
  81. package/src/core/event-store.ts +14 -0
  82. package/src/core/index.ts +1 -0
  83. package/src/core/ingest-interceptor.ts +80 -0
  84. package/src/core/markdown-mirror.ts +70 -0
  85. package/src/core/md-mirror.ts +92 -0
  86. package/src/core/mongo-sync-config.ts +165 -0
  87. package/src/core/mongo-sync-worker.ts +381 -0
  88. package/src/core/retriever.ts +540 -150
  89. package/src/core/sqlite-event-store.ts +794 -7
  90. package/src/core/sqlite-wrapper.ts +8 -0
  91. package/src/core/tag-taxonomy.ts +51 -0
  92. package/src/core/turn-state.ts +159 -0
  93. package/src/core/types.ts +51 -8
  94. package/src/core/vector-store.ts +21 -3
  95. package/src/hooks/post-tool-use.ts +68 -23
  96. package/src/hooks/session-end.ts +8 -3
  97. package/src/hooks/stop.ts +96 -25
  98. package/src/hooks/user-prompt-submit.ts +44 -5
  99. package/src/server/api/chat.ts +244 -0
  100. package/src/server/api/citations.ts +3 -3
  101. package/src/server/api/events.ts +30 -5
  102. package/src/server/api/health.ts +53 -0
  103. package/src/server/api/index.ts +9 -1
  104. package/src/server/api/projects.ts +74 -0
  105. package/src/server/api/search.ts +3 -3
  106. package/src/server/api/sessions.ts +3 -3
  107. package/src/server/api/stats.ts +89 -8
  108. package/src/server/api/turns.ts +143 -0
  109. package/src/server/api/utils.ts +46 -0
  110. package/src/services/bootstrap-organizer.ts +443 -0
  111. package/src/services/codex-session-history-importer.ts +474 -0
  112. package/src/services/memory-service.ts +508 -71
  113. package/src/services/session-history-importer.ts +215 -51
  114. package/src/ui/app.js +1447 -55
  115. package/src/ui/index.html +318 -147
  116. package/src/ui/style.css +892 -0
  117. package/tests/bootstrap-organizer.test.ts +111 -0
  118. package/tests/consolidation-worker.test.ts +75 -0
  119. package/tests/ingest-interceptor.test.ts +38 -0
  120. package/tests/markdown-mirror.test.ts +85 -0
  121. package/tests/md-mirror.test.ts +50 -0
  122. package/tests/retriever-fallback-chain.test.ts +223 -0
  123. package/tests/retriever-strategy-scope.test.ts +97 -0
  124. package/tests/retriever.memu-adoption.test.ts +122 -0
  125. package/tests/sqlite-event-store-replication.test.ts +92 -0
  126. package/.claude/settings.local.json +0 -27
  127. package/.claude-memory/test.sqlite +0 -0
  128. package/.history/package_20260201112328.json +0 -45
  129. package/.history/package_20260201113602.json +0 -45
  130. package/.history/package_20260201113713.json +0 -45
  131. package/.history/package_20260201114110.json +0 -45
  132. package/.history/package_20260201114632.json +0 -46
  133. package/.history/package_20260201133143.json +0 -45
  134. package/.history/package_20260201134319.json +0 -45
  135. package/.history/package_20260201134326.json +0 -45
  136. package/.history/package_20260201134334.json +0 -45
  137. package/.history/package_20260201134912.json +0 -45
  138. package/.history/package_20260201142928.json +0 -46
  139. package/.history/package_20260201192048.json +0 -47
  140. package/.history/package_20260202114053.json +0 -49
  141. package/.history/package_20260202121115.json +0 -49
  142. package/test_access.js +0 -49
package/dist/cli/index.js CHANGED
@@ -5,18 +5,25 @@ import { dirname } from 'path';
5
5
  const require = createRequire(import.meta.url);
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined")
12
+ return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
8
15
 
9
16
  // src/cli/index.ts
10
17
  import { Command } from "commander";
11
18
  import { exec } from "child_process";
12
- import * as fs4 from "fs";
13
- import * as path4 from "path";
14
- import * as os3 from "os";
19
+ import * as fs9 from "fs";
20
+ import * as path9 from "path";
21
+ import * as os6 from "os";
15
22
 
16
23
  // src/services/memory-service.ts
17
- import * as path from "path";
24
+ import * as path3 from "path";
18
25
  import * as os from "os";
19
- import * as fs from "fs";
26
+ import * as fs4 from "fs";
20
27
  import * as crypto2 from "crypto";
21
28
 
22
29
  // src/core/event-store.ts
@@ -74,57 +81,57 @@ function toDate(value) {
74
81
  return new Date(value);
75
82
  return new Date(String(value));
76
83
  }
77
- function createDatabase(path5, options) {
84
+ function createDatabase(path10, options) {
78
85
  if (options?.readOnly) {
79
- return new duckdb.Database(path5, { access_mode: "READ_ONLY" });
86
+ return new duckdb.Database(path10, { access_mode: "READ_ONLY" });
80
87
  }
81
- return new duckdb.Database(path5);
88
+ return new duckdb.Database(path10);
82
89
  }
83
90
  function dbRun(db, sql, params = []) {
84
- return new Promise((resolve2, reject) => {
91
+ return new Promise((resolve4, reject) => {
85
92
  if (params.length === 0) {
86
93
  db.run(sql, (err) => {
87
94
  if (err)
88
95
  reject(err);
89
96
  else
90
- resolve2();
97
+ resolve4();
91
98
  });
92
99
  } else {
93
100
  db.run(sql, ...params, (err) => {
94
101
  if (err)
95
102
  reject(err);
96
103
  else
97
- resolve2();
104
+ resolve4();
98
105
  });
99
106
  }
100
107
  });
101
108
  }
102
109
  function dbAll(db, sql, params = []) {
103
- return new Promise((resolve2, reject) => {
110
+ return new Promise((resolve4, reject) => {
104
111
  if (params.length === 0) {
105
112
  db.all(sql, (err, rows) => {
106
113
  if (err)
107
114
  reject(err);
108
115
  else
109
- resolve2(convertBigInts(rows || []));
116
+ resolve4(convertBigInts(rows || []));
110
117
  });
111
118
  } else {
112
119
  db.all(sql, ...params, (err, rows) => {
113
120
  if (err)
114
121
  reject(err);
115
122
  else
116
- resolve2(convertBigInts(rows || []));
123
+ resolve4(convertBigInts(rows || []));
117
124
  });
118
125
  }
119
126
  });
120
127
  }
121
128
  function dbClose(db) {
122
- return new Promise((resolve2, reject) => {
129
+ return new Promise((resolve4, reject) => {
123
130
  db.close((err) => {
124
131
  if (err)
125
132
  reject(err);
126
133
  else
127
- resolve2();
134
+ resolve4();
128
135
  });
129
136
  });
130
137
  }
@@ -342,6 +349,17 @@ var EventStore = class {
342
349
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
343
350
  )
344
351
  `);
352
+ await dbRun(this.db, `
353
+ CREATE TABLE IF NOT EXISTS consolidated_rules (
354
+ rule_id VARCHAR PRIMARY KEY,
355
+ rule TEXT NOT NULL,
356
+ topics JSON,
357
+ source_memory_ids JSON,
358
+ source_events JSON,
359
+ confidence FLOAT DEFAULT 0.5,
360
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
361
+ )
362
+ `);
345
363
  await dbRun(this.db, `
346
364
  CREATE TABLE IF NOT EXISTS endless_config (
347
365
  key VARCHAR PRIMARY KEY,
@@ -361,6 +379,7 @@ var EventStore = class {
361
379
  await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at)`);
362
380
  await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score DESC)`);
363
381
  await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence DESC)`);
382
+ await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_rules_confidence ON consolidated_rules(confidence DESC)`);
364
383
  await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at)`);
365
384
  this.initialized = true;
366
385
  }
@@ -748,8 +767,14 @@ import { randomUUID as randomUUID2 } from "crypto";
748
767
 
749
768
  // src/core/sqlite-wrapper.ts
750
769
  import Database from "better-sqlite3";
751
- function createSQLiteDatabase(path5, options) {
752
- const db = new Database(path5, {
770
+ import * as fs from "fs";
771
+ import * as nodePath from "path";
772
+ function createSQLiteDatabase(path10, options) {
773
+ const dir = nodePath.dirname(path10);
774
+ if (!fs.existsSync(dir)) {
775
+ fs.mkdirSync(dir, { recursive: true });
776
+ }
777
+ const db = new Database(path10, {
753
778
  readonly: options?.readonly ?? false
754
779
  });
755
780
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -790,6 +815,64 @@ function toSQLiteTimestamp(date) {
790
815
  return date.toISOString();
791
816
  }
792
817
 
818
+ // src/core/markdown-mirror.ts
819
+ import * as fs2 from "fs/promises";
820
+ import * as path from "path";
821
+ var DEFAULT_NAMESPACE = "default";
822
+ var DEFAULT_CATEGORY = "uncategorized";
823
+ function sanitizeSegment(input, fallback) {
824
+ const raw = String(input ?? "").trim().toLowerCase();
825
+ const safe = raw.normalize("NFKD").replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
826
+ if (!safe || safe === "." || safe === "..")
827
+ return fallback;
828
+ return safe;
829
+ }
830
+ function getCategorySegments(metadata, eventType) {
831
+ const raw = metadata?.categoryPath;
832
+ if (Array.isArray(raw) && raw.length > 0) {
833
+ return raw.map((s) => sanitizeSegment(s, DEFAULT_CATEGORY));
834
+ }
835
+ const single = metadata?.category;
836
+ if (typeof single === "string" && single.trim()) {
837
+ return [sanitizeSegment(single, DEFAULT_CATEGORY)];
838
+ }
839
+ return [sanitizeSegment(eventType, DEFAULT_CATEGORY)];
840
+ }
841
+ function buildMirrorPath(rootDir, event) {
842
+ const metadata = event.metadata;
843
+ const namespace = sanitizeSegment(metadata?.namespace, DEFAULT_NAMESPACE);
844
+ const categories = getCategorySegments(metadata, event.eventType);
845
+ const d = event.timestamp;
846
+ const yyyy = d.getFullYear();
847
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
848
+ const dd = String(d.getDate()).padStart(2, "0");
849
+ return path.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
850
+ }
851
+ function formatMirrorEntry(event) {
852
+ const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
853
+ return [
854
+ "",
855
+ `- ts: ${event.timestamp.toISOString()}`,
856
+ ` id: ${event.id}`,
857
+ ` type: ${event.eventType}`,
858
+ ` session: ${event.sessionId}`,
859
+ ` category: ${category}`,
860
+ " content: |",
861
+ ...event.content.split("\n").map((line) => ` ${line}`)
862
+ ].join("\n") + "\n";
863
+ }
864
+ var MarkdownMirror = class {
865
+ constructor(rootDir) {
866
+ this.rootDir = rootDir;
867
+ }
868
+ async append(event) {
869
+ const outPath = buildMirrorPath(this.rootDir, event);
870
+ await fs2.mkdir(path.dirname(outPath), { recursive: true });
871
+ await fs2.appendFile(outPath, formatMirrorEntry(event), "utf8");
872
+ return outPath;
873
+ }
874
+ };
875
+
793
876
  // src/core/sqlite-event-store.ts
794
877
  var SQLiteEventStore = class {
795
878
  constructor(dbPath, options) {
@@ -799,10 +882,12 @@ var SQLiteEventStore = class {
799
882
  readonly: this.readOnly,
800
883
  walMode: !this.readOnly
801
884
  });
885
+ this.markdownMirror = this.readOnly || !options?.markdownMirrorRoot ? null : new MarkdownMirror(options.markdownMirrorRoot);
802
886
  }
803
887
  db;
804
888
  initialized = false;
805
889
  readOnly;
890
+ markdownMirror;
806
891
  /**
807
892
  * Initialize database schema
808
893
  */
@@ -1009,6 +1094,17 @@ var SQLiteEventStore = class {
1009
1094
  created_at TEXT DEFAULT (datetime('now'))
1010
1095
  );
1011
1096
 
1097
+ -- Consolidated Rules table (long-term stable memory)
1098
+ CREATE TABLE IF NOT EXISTS consolidated_rules (
1099
+ rule_id TEXT PRIMARY KEY,
1100
+ rule TEXT NOT NULL,
1101
+ topics TEXT,
1102
+ source_memory_ids TEXT,
1103
+ source_events TEXT,
1104
+ confidence REAL DEFAULT 0.5,
1105
+ created_at TEXT DEFAULT (datetime('now'))
1106
+ );
1107
+
1012
1108
  -- Endless Mode Config table
1013
1109
  CREATE TABLE IF NOT EXISTS endless_config (
1014
1110
  key TEXT PRIMARY KEY,
@@ -1016,6 +1112,41 @@ var SQLiteEventStore = class {
1016
1112
  updated_at TEXT DEFAULT (datetime('now'))
1017
1113
  );
1018
1114
 
1115
+ -- Memory Helpfulness tracking
1116
+ CREATE TABLE IF NOT EXISTS memory_helpfulness (
1117
+ id TEXT PRIMARY KEY,
1118
+ event_id TEXT NOT NULL,
1119
+ session_id TEXT NOT NULL,
1120
+ retrieval_score REAL DEFAULT 0,
1121
+ query_preview TEXT,
1122
+ session_continued INTEGER DEFAULT 0,
1123
+ prompt_count_after INTEGER DEFAULT 0,
1124
+ tool_success_count INTEGER DEFAULT 0,
1125
+ tool_total_count INTEGER DEFAULT 0,
1126
+ was_reasked INTEGER DEFAULT 0,
1127
+ helpfulness_score REAL DEFAULT 0.5,
1128
+ created_at TEXT DEFAULT (datetime('now')),
1129
+ measured_at TEXT
1130
+ );
1131
+
1132
+ -- Retrieval trace log (query -> candidates -> selected for context)
1133
+ CREATE TABLE IF NOT EXISTS retrieval_traces (
1134
+ trace_id TEXT PRIMARY KEY,
1135
+ session_id TEXT,
1136
+ project_hash TEXT,
1137
+ query_text TEXT NOT NULL,
1138
+ strategy TEXT,
1139
+ candidate_event_ids TEXT,
1140
+ selected_event_ids TEXT,
1141
+ candidate_details_json TEXT,
1142
+ selected_details_json TEXT,
1143
+ candidate_count INTEGER DEFAULT 0,
1144
+ selected_count INTEGER DEFAULT 0,
1145
+ confidence TEXT,
1146
+ fallback_trace TEXT,
1147
+ created_at TEXT DEFAULT (datetime('now'))
1148
+ );
1149
+
1019
1150
  -- Sync position tracking (for SQLite -> DuckDB sync)
1020
1151
  CREATE TABLE IF NOT EXISTS sync_positions (
1021
1152
  target_name TEXT PRIMARY KEY,
@@ -1040,7 +1171,14 @@ var SQLiteEventStore = class {
1040
1171
  CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
1041
1172
  CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
1042
1173
  CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
1174
+ CREATE INDEX IF NOT EXISTS idx_consolidated_rules_confidence ON consolidated_rules(confidence);
1043
1175
  CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
1176
+ CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
1177
+ CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
1178
+ CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
1179
+ CREATE INDEX IF NOT EXISTS idx_retrieval_traces_created_at ON retrieval_traces(created_at DESC);
1180
+ CREATE INDEX IF NOT EXISTS idx_retrieval_traces_project_hash ON retrieval_traces(project_hash);
1181
+ CREATE INDEX IF NOT EXISTS idx_retrieval_traces_session_id ON retrieval_traces(session_id);
1044
1182
 
1045
1183
  -- FTS5 Full-Text Search for fast keyword search
1046
1184
  CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
@@ -1064,6 +1202,14 @@ var SQLiteEventStore = class {
1064
1202
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1065
1203
  END;
1066
1204
  `);
1205
+ try {
1206
+ sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
1207
+ } catch {
1208
+ }
1209
+ try {
1210
+ sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
1211
+ } catch {
1212
+ }
1067
1213
  const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
1068
1214
  const columnNames = tableInfo.map((col) => col.name);
1069
1215
  if (!columnNames.includes("access_count")) {
@@ -1084,6 +1230,15 @@ var SQLiteEventStore = class {
1084
1230
  console.error("Error adding last_accessed_at column:", err);
1085
1231
  }
1086
1232
  }
1233
+ if (!columnNames.includes("turn_id")) {
1234
+ try {
1235
+ sqliteExec(this.db, `
1236
+ ALTER TABLE events ADD COLUMN turn_id TEXT;
1237
+ `);
1238
+ } catch (err) {
1239
+ console.error("Error adding turn_id column:", err);
1240
+ }
1241
+ }
1087
1242
  try {
1088
1243
  sqliteExec(this.db, `
1089
1244
  CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
@@ -1096,6 +1251,12 @@ var SQLiteEventStore = class {
1096
1251
  `);
1097
1252
  } catch (err) {
1098
1253
  }
1254
+ try {
1255
+ sqliteExec(this.db, `
1256
+ CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
1257
+ `);
1258
+ } catch (err) {
1259
+ }
1099
1260
  this.initialized = true;
1100
1261
  }
1101
1262
  /**
@@ -1120,9 +1281,11 @@ var SQLiteEventStore = class {
1120
1281
  const id = randomUUID2();
1121
1282
  const timestamp = toSQLiteTimestamp(input.timestamp);
1122
1283
  try {
1284
+ const metadata = input.metadata || {};
1285
+ const turnId = metadata.turnId || null;
1123
1286
  const insertEvent = this.db.prepare(`
1124
- INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
1125
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1287
+ INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
1288
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1126
1289
  `);
1127
1290
  const insertDedup = this.db.prepare(`
1128
1291
  INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
@@ -1139,12 +1302,28 @@ var SQLiteEventStore = class {
1139
1302
  input.content,
1140
1303
  canonicalKey,
1141
1304
  dedupeKey,
1142
- JSON.stringify(input.metadata || {})
1305
+ JSON.stringify(metadata),
1306
+ turnId
1143
1307
  );
1144
1308
  insertDedup.run(dedupeKey, id);
1145
1309
  insertLevel.run(id);
1146
1310
  });
1147
1311
  transaction();
1312
+ if (this.markdownMirror) {
1313
+ const event = {
1314
+ id,
1315
+ eventType: input.eventType,
1316
+ sessionId: input.sessionId,
1317
+ timestamp: input.timestamp,
1318
+ content: input.content,
1319
+ canonicalKey,
1320
+ dedupeKey,
1321
+ metadata
1322
+ };
1323
+ this.markdownMirror.append(event).catch((err) => {
1324
+ console.warn("[SQLiteEventStore] markdown mirror append failed:", err);
1325
+ });
1326
+ }
1148
1327
  return { success: true, eventId: id, isDuplicate: false };
1149
1328
  } catch (error) {
1150
1329
  return {
@@ -1203,6 +1382,92 @@ var SQLiteEventStore = class {
1203
1382
  );
1204
1383
  return rows.map(this.rowToEvent);
1205
1384
  }
1385
+ /**
1386
+ * Get events since a SQLite rowid (for robust incremental replication).
1387
+ * Rowid is monotonic for append-only tables, independent of client timestamps.
1388
+ */
1389
+ async getEventsSinceRowid(lastRowid, limit = 1e3) {
1390
+ await this.initialize();
1391
+ const rows = sqliteAll(
1392
+ this.db,
1393
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
1394
+ [lastRowid, limit]
1395
+ );
1396
+ return rows.map((row) => ({
1397
+ rowid: row._rowid,
1398
+ event: this.rowToEvent(row)
1399
+ }));
1400
+ }
1401
+ /**
1402
+ * Import events with fixed IDs (used for cross-machine replication).
1403
+ * Idempotent: skips if event id or dedupeKey already exists.
1404
+ *
1405
+ * NOTE: This bypasses the append() id generation to preserve stable IDs.
1406
+ */
1407
+ async importEvents(events) {
1408
+ if (events.length === 0)
1409
+ return { inserted: 0, skipped: 0 };
1410
+ if (this.readOnly)
1411
+ return { inserted: 0, skipped: events.length };
1412
+ await this.initialize();
1413
+ const getById = this.db.prepare(`SELECT id FROM events WHERE id = ?`);
1414
+ const getByDedupe = this.db.prepare(`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`);
1415
+ const insertEvent = this.db.prepare(`
1416
+ INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
1417
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1418
+ `);
1419
+ const insertDedup = this.db.prepare(`
1420
+ INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
1421
+ `);
1422
+ const insertLevel = this.db.prepare(`
1423
+ INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
1424
+ `);
1425
+ let inserted = 0;
1426
+ let skipped = 0;
1427
+ const insertedEvents = [];
1428
+ const tx = this.db.transaction((batch) => {
1429
+ for (const ev of batch) {
1430
+ const existingById = getById.get(ev.id);
1431
+ if (existingById) {
1432
+ skipped++;
1433
+ continue;
1434
+ }
1435
+ const canonicalKey = ev.canonicalKey || makeCanonicalKey(ev.content);
1436
+ const dedupeKey = ev.dedupeKey || makeDedupeKey(ev.content, ev.sessionId);
1437
+ const existingByDedupe = getByDedupe.get(dedupeKey);
1438
+ if (existingByDedupe) {
1439
+ skipped++;
1440
+ continue;
1441
+ }
1442
+ const metadata = ev.metadata || {};
1443
+ const turnId = metadata.turnId;
1444
+ insertEvent.run(
1445
+ ev.id,
1446
+ ev.eventType,
1447
+ ev.sessionId,
1448
+ toSQLiteTimestamp(ev.timestamp),
1449
+ ev.content,
1450
+ canonicalKey,
1451
+ dedupeKey,
1452
+ JSON.stringify(metadata),
1453
+ turnId ?? null
1454
+ );
1455
+ insertDedup.run(dedupeKey, ev.id);
1456
+ insertLevel.run(ev.id);
1457
+ inserted++;
1458
+ insertedEvents.push(ev);
1459
+ }
1460
+ });
1461
+ tx(events);
1462
+ if (this.markdownMirror && insertedEvents.length > 0) {
1463
+ for (const ev of insertedEvents) {
1464
+ this.markdownMirror.append(ev).catch((err) => {
1465
+ console.warn("[SQLiteEventStore] markdown mirror append failed:", err);
1466
+ });
1467
+ }
1468
+ }
1469
+ return { inserted, skipped };
1470
+ }
1206
1471
  /**
1207
1472
  * Create or update session
1208
1473
  */
@@ -1365,6 +1630,35 @@ var SQLiteEventStore = class {
1365
1630
  [error, ...ids]
1366
1631
  );
1367
1632
  }
1633
+ /**
1634
+ * Get embedding/vector outbox health statistics
1635
+ */
1636
+ async getOutboxStats() {
1637
+ await this.initialize();
1638
+ const embeddingRows = sqliteAll(
1639
+ this.db,
1640
+ `SELECT status, COUNT(*) as count FROM embedding_outbox GROUP BY status`
1641
+ );
1642
+ const vectorRows = sqliteAll(
1643
+ this.db,
1644
+ `SELECT status, COUNT(*) as count FROM vector_outbox GROUP BY status`
1645
+ );
1646
+ const fromRows = (rows) => {
1647
+ const out = { pending: 0, processing: 0, failed: 0, total: 0 };
1648
+ for (const row of rows) {
1649
+ const key = row.status;
1650
+ if (key === "pending" || key === "processing" || key === "failed") {
1651
+ out[key] += row.count;
1652
+ }
1653
+ out.total += row.count;
1654
+ }
1655
+ return out;
1656
+ };
1657
+ return {
1658
+ embedding: fromRows(embeddingRows),
1659
+ vector: fromRows(vectorRows)
1660
+ };
1661
+ }
1368
1662
  /**
1369
1663
  * Update memory level
1370
1664
  */
@@ -1489,11 +1783,11 @@ var SQLiteEventStore = class {
1489
1783
  );
1490
1784
  }
1491
1785
  /**
1492
- * Get most accessed memories
1786
+ * Get most accessed memories (falls back to recent events if none accessed)
1493
1787
  */
1494
1788
  async getMostAccessed(limit = 10) {
1495
1789
  await this.initialize();
1496
- const rows = sqliteAll(
1790
+ let rows = sqliteAll(
1497
1791
  this.db,
1498
1792
  `SELECT * FROM events
1499
1793
  WHERE access_count > 0
@@ -1501,8 +1795,166 @@ var SQLiteEventStore = class {
1501
1795
  LIMIT ?`,
1502
1796
  [limit]
1503
1797
  );
1798
+ if (rows.length === 0) {
1799
+ rows = sqliteAll(
1800
+ this.db,
1801
+ `SELECT * FROM events
1802
+ ORDER BY timestamp DESC
1803
+ LIMIT ?`,
1804
+ [limit]
1805
+ );
1806
+ }
1504
1807
  return rows.map((row) => this.rowToEvent(row));
1505
1808
  }
1809
+ /**
1810
+ * Record a memory retrieval for helpfulness tracking
1811
+ */
1812
+ async recordRetrieval(eventId, sessionId, score, query) {
1813
+ if (this.readOnly)
1814
+ return;
1815
+ await this.initialize();
1816
+ const id = randomUUID2();
1817
+ sqliteRun(
1818
+ this.db,
1819
+ `INSERT INTO memory_helpfulness (id, event_id, session_id, retrieval_score, query_preview, created_at)
1820
+ VALUES (?, ?, ?, ?, ?, datetime('now'))`,
1821
+ [id, eventId, sessionId, score, query.slice(0, 100)]
1822
+ );
1823
+ }
1824
+ /**
1825
+ * Evaluate helpfulness for all retrievals in a session
1826
+ * Called at session end - uses behavioral signals to compute score
1827
+ */
1828
+ async evaluateSessionHelpfulness(sessionId) {
1829
+ if (this.readOnly)
1830
+ return;
1831
+ await this.initialize();
1832
+ const retrievals = sqliteAll(
1833
+ this.db,
1834
+ `SELECT * FROM memory_helpfulness WHERE session_id = ? AND measured_at IS NULL`,
1835
+ [sessionId]
1836
+ );
1837
+ if (retrievals.length === 0)
1838
+ return;
1839
+ const sessionEvents = sqliteAll(
1840
+ this.db,
1841
+ `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
1842
+ [sessionId]
1843
+ );
1844
+ const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
1845
+ const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
1846
+ let toolSuccessCount = 0;
1847
+ let toolTotalCount = toolEvents.length;
1848
+ for (const t of toolEvents) {
1849
+ try {
1850
+ const content = JSON.parse(t.content);
1851
+ if (content.success !== false)
1852
+ toolSuccessCount++;
1853
+ } catch {
1854
+ toolSuccessCount++;
1855
+ }
1856
+ }
1857
+ const toolSuccessRatio = toolTotalCount > 0 ? toolSuccessCount / toolTotalCount : 0.5;
1858
+ for (const retrieval of retrievals) {
1859
+ const retrievalTime = retrieval.created_at;
1860
+ const eventsAfter = sessionEvents.filter((e) => e.timestamp > retrievalTime);
1861
+ const sessionContinued = eventsAfter.length > 0 ? 1 : 0;
1862
+ const promptsAfter = promptEvents.filter((e) => e.timestamp > retrievalTime);
1863
+ const promptCountAfter = promptsAfter.length;
1864
+ const queryWords = new Set((retrieval.query_preview || "").toLowerCase().split(/\s+/).filter((w) => w.length > 2));
1865
+ let wasReasked = 0;
1866
+ for (const p of promptsAfter) {
1867
+ const pWords = new Set(p.content.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
1868
+ let overlap = 0;
1869
+ for (const w of queryWords) {
1870
+ if (pWords.has(w))
1871
+ overlap++;
1872
+ }
1873
+ if (queryWords.size > 0 && overlap / queryWords.size > 0.5) {
1874
+ wasReasked = 1;
1875
+ break;
1876
+ }
1877
+ }
1878
+ const retrievalScore = retrieval.retrieval_score || 0;
1879
+ const helpfulnessScore = 0.3 * Math.min(retrievalScore, 1) + 0.25 * (sessionContinued ? 1 : 0) + 0.25 * toolSuccessRatio + 0.2 * (wasReasked ? 0 : 1);
1880
+ sqliteRun(
1881
+ this.db,
1882
+ `UPDATE memory_helpfulness
1883
+ SET session_continued = ?, prompt_count_after = ?,
1884
+ tool_success_count = ?, tool_total_count = ?,
1885
+ was_reasked = ?, helpfulness_score = ?,
1886
+ measured_at = datetime('now')
1887
+ WHERE id = ?`,
1888
+ [
1889
+ sessionContinued,
1890
+ promptCountAfter,
1891
+ toolSuccessCount,
1892
+ toolTotalCount,
1893
+ wasReasked,
1894
+ helpfulnessScore,
1895
+ retrieval.id
1896
+ ]
1897
+ );
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Get most helpful memories ranked by helpfulness score
1902
+ */
1903
+ async getHelpfulMemories(limit = 10) {
1904
+ await this.initialize();
1905
+ const rows = sqliteAll(
1906
+ this.db,
1907
+ `SELECT
1908
+ mh.event_id,
1909
+ AVG(mh.helpfulness_score) as avg_score,
1910
+ COUNT(*) as eval_count,
1911
+ e.content,
1912
+ e.access_count
1913
+ FROM memory_helpfulness mh
1914
+ JOIN events e ON e.id = mh.event_id
1915
+ WHERE mh.measured_at IS NOT NULL
1916
+ GROUP BY mh.event_id
1917
+ ORDER BY avg_score DESC
1918
+ LIMIT ?`,
1919
+ [limit]
1920
+ );
1921
+ return rows.map((r) => ({
1922
+ eventId: r.event_id,
1923
+ summary: r.content.substring(0, 200) + (r.content.length > 200 ? "..." : ""),
1924
+ helpfulnessScore: Math.round(r.avg_score * 100) / 100,
1925
+ accessCount: r.access_count || 0,
1926
+ evaluationCount: r.eval_count
1927
+ }));
1928
+ }
1929
+ /**
1930
+ * Get helpfulness statistics for dashboard
1931
+ */
1932
+ async getHelpfulnessStats() {
1933
+ await this.initialize();
1934
+ const stats = sqliteGet(
1935
+ this.db,
1936
+ `SELECT
1937
+ AVG(helpfulness_score) as avg_score,
1938
+ COUNT(*) as total_evaluated,
1939
+ SUM(CASE WHEN helpfulness_score >= 0.7 THEN 1 ELSE 0 END) as helpful,
1940
+ SUM(CASE WHEN helpfulness_score >= 0.4 AND helpfulness_score < 0.7 THEN 1 ELSE 0 END) as neutral,
1941
+ SUM(CASE WHEN helpfulness_score < 0.4 THEN 1 ELSE 0 END) as unhelpful
1942
+ FROM memory_helpfulness
1943
+ WHERE measured_at IS NOT NULL`
1944
+ );
1945
+ const totalRow = sqliteGet(
1946
+ this.db,
1947
+ `SELECT COUNT(*) as total FROM memory_helpfulness`
1948
+ );
1949
+ return {
1950
+ avgScore: Math.round((stats?.avg_score || 0) * 100) / 100,
1951
+ totalEvaluated: stats?.total_evaluated || 0,
1952
+ totalRetrievals: totalRow?.total || 0,
1953
+ helpful: stats?.helpful || 0,
1954
+ neutral: stats?.neutral || 0,
1955
+ unhelpful: stats?.unhelpful || 0
1956
+ };
1957
+ }
1506
1958
  /**
1507
1959
  * Fast keyword search using FTS5
1508
1960
  * Returns events matching the search query, ranked by relevance
@@ -1565,12 +2017,222 @@ var SQLiteEventStore = class {
1565
2017
  getDatabase() {
1566
2018
  return this.db;
1567
2019
  }
2020
+ async recordRetrievalTrace(input) {
2021
+ await this.initialize();
2022
+ const traceId = randomUUID2();
2023
+ sqliteRun(
2024
+ this.db,
2025
+ `INSERT INTO retrieval_traces (
2026
+ trace_id, session_id, project_hash, query_text, strategy,
2027
+ candidate_event_ids, selected_event_ids, candidate_details_json, selected_details_json,
2028
+ candidate_count, selected_count, confidence, fallback_trace
2029
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2030
+ [
2031
+ traceId,
2032
+ input.sessionId || null,
2033
+ input.projectHash || null,
2034
+ input.queryText,
2035
+ input.strategy || null,
2036
+ JSON.stringify(input.candidateEventIds || []),
2037
+ JSON.stringify(input.selectedEventIds || []),
2038
+ JSON.stringify(input.candidateDetails || []),
2039
+ JSON.stringify(input.selectedDetails || []),
2040
+ (input.candidateEventIds || []).length,
2041
+ (input.selectedEventIds || []).length,
2042
+ input.confidence || null,
2043
+ JSON.stringify(input.fallbackTrace || [])
2044
+ ]
2045
+ );
2046
+ }
2047
+ async getRecentRetrievalTraces(limit = 50) {
2048
+ await this.initialize();
2049
+ const rows = sqliteAll(
2050
+ this.db,
2051
+ `SELECT * FROM retrieval_traces ORDER BY created_at DESC LIMIT ?`,
2052
+ [limit]
2053
+ );
2054
+ return rows.map((row) => ({
2055
+ traceId: row.trace_id,
2056
+ sessionId: row.session_id || void 0,
2057
+ projectHash: row.project_hash || void 0,
2058
+ queryText: row.query_text,
2059
+ strategy: row.strategy || void 0,
2060
+ candidateEventIds: row.candidate_event_ids ? JSON.parse(row.candidate_event_ids) : [],
2061
+ selectedEventIds: row.selected_event_ids ? JSON.parse(row.selected_event_ids) : [],
2062
+ candidateDetails: row.candidate_details_json ? JSON.parse(row.candidate_details_json) : [],
2063
+ selectedDetails: row.selected_details_json ? JSON.parse(row.selected_details_json) : [],
2064
+ candidateCount: Number(row.candidate_count || 0),
2065
+ selectedCount: Number(row.selected_count || 0),
2066
+ confidence: row.confidence || void 0,
2067
+ fallbackTrace: row.fallback_trace ? JSON.parse(row.fallback_trace) : [],
2068
+ createdAt: toDateFromSQLite(row.created_at)
2069
+ }));
2070
+ }
2071
+ async getRetrievalTraceStats() {
2072
+ await this.initialize();
2073
+ const row = sqliteGet(
2074
+ this.db,
2075
+ `SELECT
2076
+ COUNT(*) as total_queries,
2077
+ AVG(candidate_count) as avg_candidate_count,
2078
+ AVG(selected_count) as avg_selected_count,
2079
+ CASE
2080
+ WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
2081
+ ELSE 0
2082
+ END as selection_rate
2083
+ FROM retrieval_traces`,
2084
+ []
2085
+ );
2086
+ return {
2087
+ totalQueries: Number(row?.total_queries || 0),
2088
+ avgCandidateCount: Number(row?.avg_candidate_count || 0),
2089
+ avgSelectedCount: Number(row?.avg_selected_count || 0),
2090
+ selectionRate: Number(row?.selection_rate || 0)
2091
+ };
2092
+ }
1568
2093
  /**
1569
2094
  * Close database connection
1570
2095
  */
1571
2096
  async close() {
1572
2097
  sqliteClose(this.db);
1573
2098
  }
2099
+ /**
2100
+ * Get events grouped by turn_id for a session
2101
+ * Returns turns ordered by first event timestamp (newest first)
2102
+ */
2103
+ async getSessionTurns(sessionId, options) {
2104
+ await this.initialize();
2105
+ const limit = options?.limit || 20;
2106
+ const offset = options?.offset || 0;
2107
+ const turnRows = sqliteAll(
2108
+ this.db,
2109
+ `SELECT turn_id, MIN(timestamp) as min_ts
2110
+ FROM events
2111
+ WHERE session_id = ? AND turn_id IS NOT NULL
2112
+ GROUP BY turn_id
2113
+ ORDER BY min_ts DESC
2114
+ LIMIT ? OFFSET ?`,
2115
+ [sessionId, limit, offset]
2116
+ );
2117
+ const turns = [];
2118
+ for (const turnRow of turnRows) {
2119
+ const events = await this.getEventsByTurn(turnRow.turn_id);
2120
+ const promptEvent = events.find((e) => e.eventType === "user_prompt");
2121
+ const toolEvents = events.filter((e) => e.eventType === "tool_observation");
2122
+ const hasResponse = events.some((e) => e.eventType === "agent_response");
2123
+ turns.push({
2124
+ turnId: turnRow.turn_id,
2125
+ events,
2126
+ startedAt: toDateFromSQLite(turnRow.min_ts),
2127
+ promptPreview: promptEvent ? promptEvent.content.slice(0, 200) + (promptEvent.content.length > 200 ? "..." : "") : "(no prompt)",
2128
+ eventCount: events.length,
2129
+ toolCount: toolEvents.length,
2130
+ hasResponse
2131
+ });
2132
+ }
2133
+ return turns;
2134
+ }
2135
+ /**
2136
+ * Get all events for a specific turn_id
2137
+ */
2138
+ async getEventsByTurn(turnId) {
2139
+ await this.initialize();
2140
+ const rows = sqliteAll(
2141
+ this.db,
2142
+ `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
2143
+ [turnId]
2144
+ );
2145
+ return rows.map(this.rowToEvent);
2146
+ }
2147
+ /**
2148
+ * Count total turns for a session
2149
+ */
2150
+ async countSessionTurns(sessionId) {
2151
+ await this.initialize();
2152
+ const row = sqliteGet(
2153
+ this.db,
2154
+ `SELECT COUNT(DISTINCT turn_id) as count
2155
+ FROM events
2156
+ WHERE session_id = ? AND turn_id IS NOT NULL`,
2157
+ [sessionId]
2158
+ );
2159
+ return row?.count || 0;
2160
+ }
2161
+ /**
2162
+ * Migrate existing events: backfill turn_id for events that have turnId in metadata
2163
+ * but no turn_id column value (for events stored before this migration)
2164
+ */
2165
+ async backfillTurnIds() {
2166
+ await this.initialize();
2167
+ const rows = sqliteAll(
2168
+ this.db,
2169
+ `SELECT id, metadata FROM events
2170
+ WHERE turn_id IS NULL AND metadata IS NOT NULL AND metadata LIKE '%turnId%'`
2171
+ );
2172
+ let updated = 0;
2173
+ for (const row of rows) {
2174
+ try {
2175
+ const metadata = JSON.parse(row.metadata);
2176
+ if (metadata.turnId) {
2177
+ sqliteRun(
2178
+ this.db,
2179
+ `UPDATE events SET turn_id = ? WHERE id = ?`,
2180
+ [metadata.turnId, row.id]
2181
+ );
2182
+ updated++;
2183
+ }
2184
+ } catch {
2185
+ }
2186
+ }
2187
+ return updated;
2188
+ }
2189
+ /**
2190
+ * Delete all events for a session (for force reimport)
2191
+ */
2192
+ async deleteSessionEvents(sessionId) {
2193
+ await this.initialize();
2194
+ const events = sqliteAll(
2195
+ this.db,
2196
+ `SELECT id FROM events WHERE session_id = ?`,
2197
+ [sessionId]
2198
+ );
2199
+ if (events.length === 0)
2200
+ return 0;
2201
+ const eventIds = events.map((e) => e.id);
2202
+ const placeholders = eventIds.map(() => "?").join(",");
2203
+ const ftsTriggersDropped = [];
2204
+ for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
2205
+ try {
2206
+ sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
2207
+ ftsTriggersDropped.push(triggerName);
2208
+ } catch {
2209
+ }
2210
+ }
2211
+ for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
2212
+ try {
2213
+ sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
2214
+ } catch {
2215
+ }
2216
+ }
2217
+ const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
2218
+ if (ftsTriggersDropped.length > 0) {
2219
+ try {
2220
+ sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
2221
+ sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
2222
+ INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
2223
+ END`);
2224
+ sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
2225
+ INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
2226
+ END`);
2227
+ sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
2228
+ INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
2229
+ INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
2230
+ END`);
2231
+ } catch {
2232
+ }
2233
+ }
2234
+ return result.changes || 0;
2235
+ }
1574
2236
  /**
1575
2237
  * Convert database row to MemoryEvent
1576
2238
  */
@@ -1591,6 +2253,9 @@ var SQLiteEventStore = class {
1591
2253
  if (row.last_accessed_at !== void 0) {
1592
2254
  event.last_accessed_at = row.last_accessed_at;
1593
2255
  }
2256
+ if (row.turn_id !== void 0 && row.turn_id !== null) {
2257
+ event.turn_id = row.turn_id;
2258
+ }
1594
2259
  return event;
1595
2260
  }
1596
2261
  };
@@ -1742,7 +2407,7 @@ var SyncWorker = class {
1742
2407
  * Sleep utility
1743
2408
  */
1744
2409
  sleep(ms) {
1745
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2410
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1746
2411
  }
1747
2412
  /**
1748
2413
  * Get sync statistics
@@ -1802,8 +2467,17 @@ var VectorStore = class {
1802
2467
  metadata: JSON.stringify(record.metadata || {})
1803
2468
  };
1804
2469
  if (!this.table) {
1805
- this.table = await this.db.createTable(this.tableName, [data]);
1806
- } else {
2470
+ try {
2471
+ this.table = await this.db.createTable(this.tableName, [data]);
2472
+ } catch (e) {
2473
+ if (e?.message?.includes("already exists")) {
2474
+ this.table = await this.db.openTable(this.tableName);
2475
+ await this.table.add([data]);
2476
+ } else {
2477
+ throw e;
2478
+ }
2479
+ }
2480
+ } else {
1807
2481
  await this.table.add([data]);
1808
2482
  }
1809
2483
  }
@@ -1828,7 +2502,16 @@ var VectorStore = class {
1828
2502
  metadata: JSON.stringify(record.metadata || {})
1829
2503
  }));
1830
2504
  if (!this.table) {
1831
- this.table = await this.db.createTable(this.tableName, data);
2505
+ try {
2506
+ this.table = await this.db.createTable(this.tableName, data);
2507
+ } catch (e) {
2508
+ if (e?.message?.includes("already exists")) {
2509
+ this.table = await this.db.openTable(this.tableName);
2510
+ await this.table.add(data);
2511
+ } else {
2512
+ throw e;
2513
+ }
2514
+ }
1832
2515
  } else {
1833
2516
  await this.table.add(data);
1834
2517
  }
@@ -2268,7 +2951,20 @@ var DEFAULT_OPTIONS = {
2268
2951
  topK: 5,
2269
2952
  minScore: 0.7,
2270
2953
  maxTokens: 2e3,
2271
- includeSessionContext: true
2954
+ includeSessionContext: true,
2955
+ strategy: "auto",
2956
+ rerankWithKeyword: true,
2957
+ decayPolicy: {
2958
+ enabled: true,
2959
+ windowDays: 30,
2960
+ maxPenalty: 0.15
2961
+ },
2962
+ graphHop: {
2963
+ enabled: true,
2964
+ maxHops: 1,
2965
+ hopPenalty: 0.08
2966
+ },
2967
+ projectScopeMode: "global"
2272
2968
  };
2273
2969
  var Retriever = class {
2274
2970
  eventStore;
@@ -2278,6 +2974,7 @@ var Retriever = class {
2278
2974
  sharedStore;
2279
2975
  sharedVectorStore;
2280
2976
  graduation;
2977
+ queryRewriter;
2281
2978
  constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
2282
2979
  this.eventStore = eventStore;
2283
2980
  this.vectorStore = vectorStore;
@@ -2286,47 +2983,105 @@ var Retriever = class {
2286
2983
  this.sharedStore = sharedOptions?.sharedStore;
2287
2984
  this.sharedVectorStore = sharedOptions?.sharedVectorStore;
2288
2985
  }
2289
- /**
2290
- * Set graduation pipeline for access tracking
2291
- */
2292
2986
  setGraduationPipeline(graduation) {
2293
2987
  this.graduation = graduation;
2294
2988
  }
2295
- /**
2296
- * Set shared stores after construction
2297
- */
2298
2989
  setSharedStores(sharedStore, sharedVectorStore) {
2299
2990
  this.sharedStore = sharedStore;
2300
2991
  this.sharedVectorStore = sharedVectorStore;
2301
2992
  }
2302
- /**
2303
- * Retrieve relevant memories for a query
2304
- */
2993
+ setQueryRewriter(rewriter) {
2994
+ this.queryRewriter = rewriter;
2995
+ }
2305
2996
  async retrieve(query, options = {}) {
2306
2997
  const opts = { ...DEFAULT_OPTIONS, ...options };
2307
- const queryEmbedding = await this.embedder.embed(query);
2308
- const searchResults = await this.vectorStore.search(queryEmbedding.vector, {
2309
- limit: opts.topK * 2,
2310
- // Get extra for filtering
2998
+ const sessionFilter = opts.scope?.sessionId ?? opts.sessionId;
2999
+ const fallbackTrace = [];
3000
+ const fallbackEnabled = (opts.strategy ?? "auto") === "auto";
3001
+ const primaryStrategy = opts.strategy === "auto" ? "fast" : opts.strategy || "fast";
3002
+ let current = await this.runStage(query, {
3003
+ strategy: primaryStrategy,
3004
+ topK: opts.topK,
2311
3005
  minScore: opts.minScore,
2312
- sessionId: opts.sessionId
3006
+ sessionId: sessionFilter,
3007
+ scope: opts.scope,
3008
+ rerankWithKeyword: opts.rerankWithKeyword !== false,
3009
+ rerankWeights: opts.rerankWeights,
3010
+ decayPolicy: opts.decayPolicy,
3011
+ intentRewrite: opts.intentRewrite === true,
3012
+ graphHop: opts.graphHop,
3013
+ projectScopeMode: opts.projectScopeMode,
3014
+ projectHash: opts.projectHash,
3015
+ allowedProjectHashes: opts.allowedProjectHashes
2313
3016
  });
2314
- const matchResult = this.matcher.matchSearchResults(
2315
- searchResults,
2316
- (eventId) => this.getEventAgeDays(eventId)
2317
- );
2318
- const memories = await this.enrichResults(searchResults.slice(0, opts.topK), opts);
3017
+ fallbackTrace.push(`stage:primary:${primaryStrategy}`);
3018
+ if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results) && primaryStrategy !== "deep") {
3019
+ current = await this.runStage(query, {
3020
+ strategy: "deep",
3021
+ topK: opts.topK,
3022
+ minScore: opts.minScore,
3023
+ sessionId: sessionFilter,
3024
+ scope: opts.scope,
3025
+ rerankWithKeyword: opts.rerankWithKeyword !== false,
3026
+ rerankWeights: opts.rerankWeights,
3027
+ decayPolicy: opts.decayPolicy,
3028
+ graphHop: opts.graphHop,
3029
+ projectScopeMode: opts.projectScopeMode,
3030
+ projectHash: opts.projectHash,
3031
+ allowedProjectHashes: opts.allowedProjectHashes
3032
+ });
3033
+ fallbackTrace.push("fallback:deep");
3034
+ }
3035
+ if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results)) {
3036
+ current = await this.runStage(query, {
3037
+ strategy: "deep",
3038
+ topK: opts.topK,
3039
+ minScore: Math.max(0.5, opts.minScore - 0.15),
3040
+ sessionId: void 0,
3041
+ scope: void 0,
3042
+ rerankWithKeyword: true,
3043
+ rerankWeights: opts.rerankWeights,
3044
+ decayPolicy: opts.decayPolicy,
3045
+ graphHop: opts.graphHop,
3046
+ projectScopeMode: opts.projectScopeMode,
3047
+ projectHash: opts.projectHash,
3048
+ allowedProjectHashes: opts.allowedProjectHashes
3049
+ });
3050
+ fallbackTrace.push("fallback:scope-expanded");
3051
+ }
3052
+ if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results)) {
3053
+ const summary = await this.buildSummaryFallback(query, opts.topK);
3054
+ current = {
3055
+ results: summary,
3056
+ candidateResults: summary,
3057
+ matchResult: this.matcher.matchSearchResults(summary, () => 0)
3058
+ };
3059
+ fallbackTrace.push("fallback:summary");
3060
+ }
3061
+ const memories = await this.enrichResults(current.results.slice(0, opts.topK), opts);
2319
3062
  const context = this.buildContext(memories, opts.maxTokens);
2320
3063
  return {
2321
3064
  memories,
2322
- matchResult,
3065
+ matchResult: current.matchResult,
2323
3066
  totalTokens: this.estimateTokens(context),
2324
- context
3067
+ context,
3068
+ fallbackTrace,
3069
+ selectedDebug: current.results.slice(0, opts.topK).map((r) => ({
3070
+ eventId: r.eventId,
3071
+ score: r.score,
3072
+ semanticScore: r.semanticScore,
3073
+ lexicalScore: r.lexicalScore,
3074
+ recencyScore: r.recencyScore
3075
+ })),
3076
+ candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => ({
3077
+ eventId: r.eventId,
3078
+ score: r.score,
3079
+ semanticScore: r.semanticScore,
3080
+ lexicalScore: r.lexicalScore,
3081
+ recencyScore: r.recencyScore
3082
+ }))
2325
3083
  };
2326
3084
  }
2327
- /**
2328
- * Retrieve with unified search (project + shared)
2329
- */
2330
3085
  async retrieveUnified(query, options = {}) {
2331
3086
  const projectResult = await this.retrieve(query, options);
2332
3087
  if (!options.includeShared || !this.sharedStore || !this.sharedVectorStore) {
@@ -2334,22 +3089,19 @@ var Retriever = class {
2334
3089
  }
2335
3090
  try {
2336
3091
  const queryEmbedding = await this.embedder.embed(query);
2337
- const sharedVectorResults = await this.sharedVectorStore.search(
2338
- queryEmbedding.vector,
2339
- {
2340
- limit: options.topK || 5,
2341
- minScore: options.minScore || 0.7,
2342
- excludeProjectHash: options.projectHash
2343
- }
2344
- );
3092
+ const sharedVectorResults = await this.sharedVectorStore.search(queryEmbedding.vector, {
3093
+ limit: options.topK || 5,
3094
+ minScore: options.minScore || 0.7,
3095
+ excludeProjectHash: options.projectHash
3096
+ });
2345
3097
  const sharedMemories = [];
2346
3098
  for (const result of sharedVectorResults) {
2347
3099
  const entry = await this.sharedStore.get(result.entryId);
2348
- if (entry) {
2349
- if (!options.projectHash || entry.sourceProjectHash !== options.projectHash) {
2350
- sharedMemories.push(entry);
2351
- await this.sharedStore.recordUsage(entry.entryId);
2352
- }
3100
+ if (!entry)
3101
+ continue;
3102
+ if (!options.projectHash || entry.sourceProjectHash !== options.projectHash) {
3103
+ sharedMemories.push(entry);
3104
+ await this.sharedStore.recordUsage(entry.entryId);
2353
3105
  }
2354
3106
  }
2355
3107
  const unifiedContext = this.buildUnifiedContext(projectResult, sharedMemories);
@@ -2364,50 +3116,243 @@ var Retriever = class {
2364
3116
  return projectResult;
2365
3117
  }
2366
3118
  }
2367
- /**
2368
- * Build unified context combining project and shared memories
2369
- */
2370
- buildUnifiedContext(projectResult, sharedMemories) {
2371
- let context = projectResult.context;
2372
- if (sharedMemories.length > 0) {
2373
- context += "\n\n## Cross-Project Knowledge\n\n";
2374
- for (const memory of sharedMemories.slice(0, 3)) {
2375
- context += `### ${memory.title}
2376
- `;
2377
- if (memory.symptoms.length > 0) {
2378
- context += `**Symptoms:** ${memory.symptoms.join(", ")}
2379
- `;
2380
- }
2381
- context += `**Root Cause:** ${memory.rootCause}
2382
- `;
2383
- context += `**Solution:** ${memory.solution}
2384
- `;
2385
- if (memory.technologies && memory.technologies.length > 0) {
2386
- context += `**Technologies:** ${memory.technologies.join(", ")}
2387
- `;
3119
+ async runStage(query, input) {
3120
+ let initialResults = await this.searchByStrategy(query, {
3121
+ strategy: input.strategy,
3122
+ topK: input.topK,
3123
+ minScore: input.minScore,
3124
+ sessionId: input.sessionId
3125
+ });
3126
+ if (input.intentRewrite && input.strategy === "deep" && this.queryRewriter) {
3127
+ const rewritten = (await this.queryRewriter(query))?.trim();
3128
+ if (rewritten && rewritten !== query) {
3129
+ const rewrittenResults = await this.searchByStrategy(rewritten, {
3130
+ strategy: "deep",
3131
+ topK: input.topK,
3132
+ minScore: Math.max(0.5, input.minScore - 0.1),
3133
+ sessionId: input.sessionId
3134
+ });
3135
+ initialResults = this.mergeResults(initialResults, rewrittenResults, input.topK * 3);
3136
+ }
3137
+ }
3138
+ const expandedResults = input.graphHop?.enabled === false ? initialResults : await this.expandGraphHops(initialResults, {
3139
+ maxHops: Math.max(1, input.graphHop?.maxHops ?? 1),
3140
+ hopPenalty: Math.max(0, input.graphHop?.hopPenalty ?? 0.08),
3141
+ limit: input.topK * 4
3142
+ });
3143
+ const rerankedResults = input.rerankWithKeyword ? this.rerankByKeywordOverlap(expandedResults, query, input.rerankWeights, input.decayPolicy) : expandedResults;
3144
+ const filtered = await this.applyScopeFilters(rerankedResults, {
3145
+ scope: input.scope,
3146
+ projectScopeMode: input.projectScopeMode,
3147
+ projectHash: input.projectHash,
3148
+ allowedProjectHashes: input.allowedProjectHashes
3149
+ });
3150
+ const top = filtered.slice(0, input.topK);
3151
+ const matchResult = this.matcher.matchSearchResults(top, () => 0);
3152
+ return { results: top, candidateResults: filtered, matchResult };
3153
+ }
3154
+ mergeResults(primary, secondary, limit) {
3155
+ const byId = /* @__PURE__ */ new Map();
3156
+ for (const row of primary)
3157
+ byId.set(row.eventId, row);
3158
+ for (const row of secondary) {
3159
+ const prev = byId.get(row.eventId);
3160
+ if (!prev || row.score > prev.score) {
3161
+ byId.set(row.eventId, row);
3162
+ }
3163
+ }
3164
+ return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, limit);
3165
+ }
3166
+ async expandGraphHops(seeds, opts) {
3167
+ const byId = /* @__PURE__ */ new Map();
3168
+ for (const s of seeds)
3169
+ byId.set(s.eventId, s);
3170
+ let frontier = seeds.map((s) => ({ row: s, hop: 0 }));
3171
+ for (let hop = 1; hop <= opts.maxHops; hop += 1) {
3172
+ const next = [];
3173
+ for (const f of frontier) {
3174
+ const ev = await this.eventStore.getEvent(f.row.eventId);
3175
+ if (!ev)
3176
+ continue;
3177
+ const rel = ev.metadata?.relatedEventIds ?? [];
3178
+ const relatedIds = Array.isArray(rel) ? rel.filter((x) => typeof x === "string") : [];
3179
+ for (const rid of relatedIds) {
3180
+ if (byId.has(rid))
3181
+ continue;
3182
+ const target = await this.eventStore.getEvent(rid);
3183
+ if (!target)
3184
+ continue;
3185
+ const score = Math.max(0, f.row.score - opts.hopPenalty * hop);
3186
+ const row = {
3187
+ id: `hop-${hop}-${rid}`,
3188
+ eventId: target.id,
3189
+ content: target.content,
3190
+ score,
3191
+ sessionId: target.sessionId,
3192
+ eventType: target.eventType,
3193
+ timestamp: target.timestamp.toISOString()
3194
+ };
3195
+ byId.set(row.eventId, row);
3196
+ next.push({ row, hop });
3197
+ if (byId.size >= opts.limit)
3198
+ break;
2388
3199
  }
2389
- context += `_Confidence: ${(memory.confidence * 100).toFixed(0)}%_
2390
-
2391
- `;
3200
+ if (byId.size >= opts.limit)
3201
+ break;
2392
3202
  }
3203
+ frontier = next;
3204
+ if (frontier.length === 0 || byId.size >= opts.limit)
3205
+ break;
2393
3206
  }
2394
- return context;
3207
+ return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, opts.limit);
3208
+ }
3209
+ shouldFallback(matchResult, results) {
3210
+ if (results.length === 0)
3211
+ return true;
3212
+ if (matchResult.confidence === "none")
3213
+ return true;
3214
+ return false;
3215
+ }
3216
+ async buildSummaryFallback(query, topK) {
3217
+ const recent = await this.eventStore.getRecentEvents(Math.max(topK * 6, 20));
3218
+ const q = this.tokenize(query);
3219
+ const ranked = recent.map((e) => ({ e, overlap: this.keywordOverlap(q, this.tokenize(e.content)) })).filter((r) => r.overlap > 0).sort((a, b) => b.overlap - a.overlap).slice(0, topK).map((row, idx) => ({
3220
+ id: `summary-${row.e.id}`,
3221
+ eventId: row.e.id,
3222
+ content: row.e.content,
3223
+ score: Math.max(0.25, 0.6 - idx * 0.05),
3224
+ sessionId: row.e.sessionId,
3225
+ eventType: row.e.eventType,
3226
+ timestamp: row.e.timestamp.toISOString()
3227
+ }));
3228
+ return ranked;
3229
+ }
3230
+ async searchByStrategy(query, input) {
3231
+ const strategy = input.strategy === "auto" ? "deep" : input.strategy;
3232
+ if (strategy === "fast") {
3233
+ const keyword = await this.searchByKeyword(query, {
3234
+ limit: Math.max(5, input.topK * 3),
3235
+ sessionId: input.sessionId
3236
+ });
3237
+ return keyword;
3238
+ }
3239
+ const queryEmbedding = await this.embedder.embed(query);
3240
+ return this.vectorStore.search(queryEmbedding.vector, {
3241
+ limit: Math.max(5, input.topK * 3),
3242
+ minScore: input.minScore,
3243
+ sessionId: input.sessionId
3244
+ });
3245
+ }
3246
+ async searchByKeyword(query, input) {
3247
+ if (this.eventStore.keywordSearch) {
3248
+ const rows = await this.eventStore.keywordSearch(query, input.limit);
3249
+ const filtered2 = input.sessionId ? rows.filter((r) => r.event.sessionId === input.sessionId) : rows;
3250
+ return filtered2.map((row, idx) => ({
3251
+ id: `kw-${row.event.id}`,
3252
+ eventId: row.event.id,
3253
+ content: row.event.content,
3254
+ score: Math.max(0.4, 1 - idx * 0.04),
3255
+ sessionId: row.event.sessionId,
3256
+ eventType: row.event.eventType,
3257
+ timestamp: row.event.timestamp.toISOString()
3258
+ }));
3259
+ }
3260
+ const recent = await this.eventStore.getRecentEvents(input.limit * 4);
3261
+ const tokens = this.tokenize(query);
3262
+ const filtered = recent.filter((e) => input.sessionId ? e.sessionId === input.sessionId : true).map((e) => ({ e, overlap: this.keywordOverlap(tokens, this.tokenize(e.content)) })).filter((r) => r.overlap > 0).sort((a, b) => b.overlap - a.overlap).slice(0, input.limit);
3263
+ return filtered.map((row, idx) => ({
3264
+ id: `kw-fallback-${row.e.id}`,
3265
+ eventId: row.e.id,
3266
+ content: row.e.content,
3267
+ score: Math.max(0.3, 0.9 - idx * 0.05),
3268
+ sessionId: row.e.sessionId,
3269
+ eventType: row.e.eventType,
3270
+ timestamp: row.e.timestamp.toISOString()
3271
+ }));
3272
+ }
3273
+ rerankByKeywordOverlap(results, query, weights, decayPolicy) {
3274
+ const q = this.tokenize(query);
3275
+ const now = Date.now();
3276
+ const sw = Math.max(0, weights?.semantic ?? 0.7);
3277
+ const lw = Math.max(0, weights?.lexical ?? 0.2);
3278
+ const rw = Math.max(0, weights?.recency ?? 0.1);
3279
+ const total = sw + lw + rw || 1;
3280
+ const decayEnabled = decayPolicy?.enabled !== false;
3281
+ const decayWindow = Math.max(1, decayPolicy?.windowDays ?? 30);
3282
+ const decayMaxPenalty = Math.max(0, decayPolicy?.maxPenalty ?? 0.15);
3283
+ return [...results].map((r) => {
3284
+ const overlap = this.keywordOverlap(q, this.tokenize(r.content));
3285
+ const recencyDays = Math.max(0, (now - new Date(r.timestamp).getTime()) / (1e3 * 60 * 60 * 24));
3286
+ const recency = Math.max(0, 1 - recencyDays / decayWindow);
3287
+ let blended = (r.score * sw + overlap * lw + recency * rw) / total;
3288
+ if (decayEnabled && recencyDays > decayWindow && overlap < 0.5) {
3289
+ const ageFactor = Math.min(1, (recencyDays - decayWindow) / decayWindow);
3290
+ blended -= decayMaxPenalty * ageFactor;
3291
+ }
3292
+ return { ...r, score: Math.max(0, blended), semanticScore: r.score, lexicalScore: overlap, recencyScore: recency };
3293
+ }).sort((a, b) => b.score - a.score);
3294
+ }
3295
+ async applyScopeFilters(results, options) {
3296
+ const scope = options?.scope;
3297
+ const projectScopeMode = options?.projectScopeMode ?? "global";
3298
+ const allowedProjectHashes = new Set(
3299
+ [options?.projectHash, ...options?.allowedProjectHashes || []].filter(
3300
+ (value) => typeof value === "string" && value.length > 0
3301
+ )
3302
+ );
3303
+ if (!scope && projectScopeMode === "global")
3304
+ return results;
3305
+ const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
3306
+ const filtered = [];
3307
+ for (const result of results) {
3308
+ if (scope?.sessionId && result.sessionId !== scope.sessionId)
3309
+ continue;
3310
+ if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix))
3311
+ continue;
3312
+ if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType))
3313
+ continue;
3314
+ const event = await this.eventStore.getEvent(result.eventId);
3315
+ if (!event)
3316
+ continue;
3317
+ if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix))
3318
+ continue;
3319
+ if (normalizedIncludes.length > 0) {
3320
+ const lc = event.content.toLowerCase();
3321
+ if (!normalizedIncludes.some((needle) => lc.includes(needle)))
3322
+ continue;
3323
+ }
3324
+ if (scope?.metadata && !this.matchesMetadataScope(event.metadata, scope.metadata))
3325
+ continue;
3326
+ const projectHash = this.extractProjectHash(event.metadata);
3327
+ filtered.push({ result, projectHash });
3328
+ }
3329
+ if (projectScopeMode === "global" || allowedProjectHashes.size === 0) {
3330
+ return filtered.map((x) => x.result);
3331
+ }
3332
+ const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
3333
+ if (projectScopeMode === "strict") {
3334
+ return projectMatched.map((x) => x.result);
3335
+ }
3336
+ return (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
3337
+ }
3338
+ extractProjectHash(metadata) {
3339
+ if (!metadata || typeof metadata !== "object")
3340
+ return void 0;
3341
+ const scope = metadata.scope;
3342
+ if (!scope || typeof scope !== "object")
3343
+ return void 0;
3344
+ const project = scope.project;
3345
+ if (!project || typeof project !== "object")
3346
+ return void 0;
3347
+ const hash = project.hash;
3348
+ return typeof hash === "string" && hash.length > 0 ? hash : void 0;
2395
3349
  }
2396
- /**
2397
- * Retrieve memories from a specific session
2398
- */
2399
3350
  async retrieveFromSession(sessionId) {
2400
3351
  return this.eventStore.getSessionEvents(sessionId);
2401
3352
  }
2402
- /**
2403
- * Get recent memories across all sessions
2404
- */
2405
3353
  async retrieveRecent(limit = 100) {
2406
3354
  return this.eventStore.getRecentEvents(limit);
2407
3355
  }
2408
- /**
2409
- * Enrich search results with full event data
2410
- */
2411
3356
  async enrichResults(results, options) {
2412
3357
  const memories = [];
2413
3358
  for (const result of results) {
@@ -2415,27 +3360,16 @@ var Retriever = class {
2415
3360
  if (!event)
2416
3361
  continue;
2417
3362
  if (this.graduation) {
2418
- this.graduation.recordAccess(
2419
- event.id,
2420
- options.sessionId || "unknown",
2421
- result.score
2422
- );
3363
+ this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
2423
3364
  }
2424
3365
  let sessionContext;
2425
3366
  if (options.includeSessionContext) {
2426
3367
  sessionContext = await this.getSessionContext(event.sessionId, event.id);
2427
3368
  }
2428
- memories.push({
2429
- event,
2430
- score: result.score,
2431
- sessionContext
2432
- });
3369
+ memories.push({ event, score: result.score, sessionContext });
2433
3370
  }
2434
3371
  return memories;
2435
3372
  }
2436
- /**
2437
- * Get surrounding context from the same session
2438
- */
2439
3373
  async getSessionContext(sessionId, eventId) {
2440
3374
  const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
2441
3375
  const eventIndex = sessionEvents.findIndex((e) => e.id === eventId);
@@ -2448,55 +3382,86 @@ var Retriever = class {
2448
3382
  return void 0;
2449
3383
  return contextEvents.filter((e) => e.id !== eventId).map((e) => `[${e.eventType}]: ${e.content.slice(0, 200)}...`).join("\n");
2450
3384
  }
2451
- /**
2452
- * Build context string from memories (respecting token limit)
2453
- */
3385
+ buildUnifiedContext(projectResult, sharedMemories) {
3386
+ let context = projectResult.context;
3387
+ if (sharedMemories.length === 0)
3388
+ return context;
3389
+ context += "\n\n## Cross-Project Knowledge\n\n";
3390
+ for (const memory of sharedMemories.slice(0, 3)) {
3391
+ context += `### ${memory.title}
3392
+ `;
3393
+ if (memory.symptoms.length > 0)
3394
+ context += `**Symptoms:** ${memory.symptoms.join(", ")}
3395
+ `;
3396
+ context += `**Root Cause:** ${memory.rootCause}
3397
+ `;
3398
+ context += `**Solution:** ${memory.solution}
3399
+ `;
3400
+ if (memory.technologies && memory.technologies.length > 0)
3401
+ context += `**Technologies:** ${memory.technologies.join(", ")}
3402
+ `;
3403
+ context += `_Confidence: ${(memory.confidence * 100).toFixed(0)}%_
3404
+
3405
+ `;
3406
+ }
3407
+ return context;
3408
+ }
2454
3409
  buildContext(memories, maxTokens) {
2455
3410
  const parts = [];
2456
3411
  let currentTokens = 0;
2457
3412
  for (const memory of memories) {
2458
3413
  const memoryText = this.formatMemory(memory);
2459
3414
  const memoryTokens = this.estimateTokens(memoryText);
2460
- if (currentTokens + memoryTokens > maxTokens) {
3415
+ if (currentTokens + memoryTokens > maxTokens)
2461
3416
  break;
2462
- }
2463
3417
  parts.push(memoryText);
2464
3418
  currentTokens += memoryTokens;
2465
3419
  }
2466
- if (parts.length === 0) {
3420
+ if (parts.length === 0)
2467
3421
  return "";
2468
- }
2469
3422
  return `## Relevant Memories
2470
3423
 
2471
3424
  ${parts.join("\n\n---\n\n")}`;
2472
3425
  }
2473
- /**
2474
- * Format a single memory for context
2475
- */
2476
3426
  formatMemory(memory) {
2477
3427
  const { event, score, sessionContext } = memory;
2478
3428
  const date = event.timestamp.toISOString().split("T")[0];
2479
3429
  let text = `**${event.eventType}** (${date}, score: ${score.toFixed(2)})
2480
3430
  ${event.content}`;
2481
- if (sessionContext) {
3431
+ if (sessionContext)
2482
3432
  text += `
2483
3433
 
2484
3434
  _Context:_ ${sessionContext}`;
2485
- }
2486
3435
  return text;
2487
3436
  }
2488
- /**
2489
- * Estimate token count (rough approximation)
2490
- */
3437
+ matchesMetadataScope(metadata, expected) {
3438
+ if (!metadata)
3439
+ return false;
3440
+ return Object.entries(expected).every(([path10, value]) => {
3441
+ const actual = path10.split(".").reduce((acc, key) => {
3442
+ if (typeof acc !== "object" || acc === null)
3443
+ return void 0;
3444
+ return acc[key];
3445
+ }, metadata);
3446
+ return actual === value;
3447
+ });
3448
+ }
3449
+ tokenize(text) {
3450
+ return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((t) => t.length >= 2).slice(0, 64);
3451
+ }
3452
+ keywordOverlap(a, b) {
3453
+ if (a.length === 0 || b.length === 0)
3454
+ return 0;
3455
+ const bs = new Set(b);
3456
+ let hit = 0;
3457
+ for (const t of a)
3458
+ if (bs.has(t))
3459
+ hit += 1;
3460
+ return hit / a.length;
3461
+ }
2491
3462
  estimateTokens(text) {
2492
3463
  return Math.ceil(text.length / 4);
2493
3464
  }
2494
- /**
2495
- * Get event age in days (for recency scoring)
2496
- */
2497
- getEventAgeDays(eventId) {
2498
- return 0;
2499
- }
2500
3465
  };
2501
3466
  function createRetriever(eventStore, vectorStore, embedder, matcher) {
2502
3467
  return new Retriever(eventStore, vectorStore, embedder, matcher);
@@ -3786,6 +4751,59 @@ var ConsolidatedStore = class {
3786
4751
  [memoryId]
3787
4752
  );
3788
4753
  }
4754
+ /**
4755
+ * Create a long-term rule promoted from stable summaries
4756
+ */
4757
+ async createRule(input) {
4758
+ const ruleId = randomUUID6();
4759
+ await dbRun(
4760
+ this.db,
4761
+ `INSERT INTO consolidated_rules
4762
+ (rule_id, rule, topics, source_memory_ids, source_events, confidence, created_at)
4763
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
4764
+ [
4765
+ ruleId,
4766
+ input.rule,
4767
+ JSON.stringify(input.topics),
4768
+ JSON.stringify(input.sourceMemoryIds),
4769
+ JSON.stringify(input.sourceEvents),
4770
+ input.confidence
4771
+ ]
4772
+ );
4773
+ return ruleId;
4774
+ }
4775
+ async getRules(options) {
4776
+ const limit = options?.limit || 100;
4777
+ const rows = await dbAll(
4778
+ this.db,
4779
+ `SELECT * FROM consolidated_rules ORDER BY confidence DESC, created_at DESC LIMIT ?`,
4780
+ [limit]
4781
+ );
4782
+ return rows.map((row) => ({
4783
+ ruleId: row.rule_id,
4784
+ rule: row.rule,
4785
+ topics: JSON.parse(row.topics || "[]"),
4786
+ sourceMemoryIds: JSON.parse(row.source_memory_ids || "[]"),
4787
+ sourceEvents: JSON.parse(row.source_events || "[]"),
4788
+ confidence: Number(row.confidence ?? 0.5),
4789
+ createdAt: toDate(row.created_at) || /* @__PURE__ */ new Date()
4790
+ }));
4791
+ }
4792
+ async countRules() {
4793
+ const result = await dbAll(
4794
+ this.db,
4795
+ `SELECT COUNT(*) as count FROM consolidated_rules`
4796
+ );
4797
+ return result[0]?.count || 0;
4798
+ }
4799
+ async hasRuleForSourceMemory(memoryId) {
4800
+ const rows = await dbAll(
4801
+ this.db,
4802
+ `SELECT COUNT(*) as count FROM consolidated_rules WHERE source_memory_ids LIKE ?`,
4803
+ [`%"${memoryId}"%`]
4804
+ );
4805
+ return (rows[0]?.count || 0) > 0;
4806
+ }
3789
4807
  /**
3790
4808
  * Get count of consolidated memories
3791
4809
  */
@@ -3935,7 +4953,14 @@ var ConsolidationWorker = class {
3935
4953
  * Force a consolidation run (manual trigger)
3936
4954
  */
3937
4955
  async forceRun() {
3938
- return await this.consolidate();
4956
+ const out = await this.consolidateWithReport();
4957
+ return out.consolidatedCount;
4958
+ }
4959
+ /**
4960
+ * Force a consolidation run and return metrics report
4961
+ */
4962
+ async forceRunWithReport() {
4963
+ return this.consolidateWithReport();
3939
4964
  }
3940
4965
  /**
3941
4966
  * Schedule the next consolidation check
@@ -3975,12 +5000,21 @@ var ConsolidationWorker = class {
3975
5000
  * Perform consolidation
3976
5001
  */
3977
5002
  async consolidate() {
5003
+ const out = await this.consolidateWithReport();
5004
+ return out.consolidatedCount;
5005
+ }
5006
+ async consolidateWithReport() {
3978
5007
  const workingSet = await this.workingSetStore.get();
3979
5008
  if (workingSet.recentEvents.length < 3) {
3980
- return 0;
5009
+ return {
5010
+ consolidatedCount: 0,
5011
+ promotedRuleCount: 0,
5012
+ report: this.buildCostQualityReport(workingSet.recentEvents, [], 0)
5013
+ };
3981
5014
  }
3982
5015
  const groups = this.groupByTopic(workingSet.recentEvents);
3983
5016
  let consolidatedCount = 0;
5017
+ const createdMemoryIds = [];
3984
5018
  for (const group of groups) {
3985
5019
  if (group.events.length < 3)
3986
5020
  continue;
@@ -3989,14 +5023,16 @@ var ConsolidationWorker = class {
3989
5023
  if (alreadyConsolidated)
3990
5024
  continue;
3991
5025
  const summary = await this.summarize(group);
3992
- await this.consolidatedStore.create({
5026
+ const memoryId = await this.consolidatedStore.create({
3993
5027
  summary,
3994
5028
  topics: group.topics,
3995
5029
  sourceEvents: eventIds,
3996
5030
  confidence: this.calculateConfidence(group)
3997
5031
  });
5032
+ createdMemoryIds.push(memoryId);
3998
5033
  consolidatedCount++;
3999
5034
  }
5035
+ const promotedRuleCount = await this.promoteStableSummariesToRules(createdMemoryIds);
4000
5036
  if (consolidatedCount > 0) {
4001
5037
  const consolidatedEventIds = groups.filter((g) => g.events.length >= 3).flatMap((g) => g.events.map((e) => e.id));
4002
5038
  const oldEventIds = consolidatedEventIds.filter((id) => {
@@ -4010,7 +5046,61 @@ var ConsolidationWorker = class {
4010
5046
  await this.workingSetStore.prune(oldEventIds);
4011
5047
  }
4012
5048
  }
4013
- return consolidatedCount;
5049
+ const report = this.buildCostQualityReport(workingSet.recentEvents, groups, consolidatedCount);
5050
+ return { consolidatedCount, promotedRuleCount, report };
5051
+ }
5052
+ async promoteStableSummariesToRules(memoryIds) {
5053
+ let promoted = 0;
5054
+ for (const memoryId of memoryIds) {
5055
+ const memory = await this.consolidatedStore.get(memoryId);
5056
+ if (!memory)
5057
+ continue;
5058
+ if (memory.confidence < 0.55)
5059
+ continue;
5060
+ if (memory.sourceEvents.length < 4)
5061
+ continue;
5062
+ const exists = await this.consolidatedStore.hasRuleForSourceMemory(memoryId);
5063
+ if (exists)
5064
+ continue;
5065
+ const rule = this.buildRuleFromSummary(memory.summary, memory.topics);
5066
+ if (!rule)
5067
+ continue;
5068
+ await this.consolidatedStore.createRule({
5069
+ rule,
5070
+ topics: memory.topics,
5071
+ sourceMemoryIds: [memory.memoryId],
5072
+ sourceEvents: memory.sourceEvents,
5073
+ confidence: Math.min(1, memory.confidence + 0.08)
5074
+ });
5075
+ promoted++;
5076
+ }
5077
+ return promoted;
5078
+ }
5079
+ buildRuleFromSummary(summary, topics) {
5080
+ const lines = summary.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).filter((l) => !l.toLowerCase().startsWith("topics:"));
5081
+ const bullet = lines.find((l) => l.startsWith("- "))?.replace(/^-\s*/, "");
5082
+ const seed = bullet || lines[0];
5083
+ if (!seed || seed.length < 8)
5084
+ return null;
5085
+ const topicPrefix = topics.length > 0 ? `[${topics.slice(0, 2).join(", ")}] ` : "";
5086
+ return `${topicPrefix}${seed}`;
5087
+ }
5088
+ buildCostQualityReport(events, groups, consolidatedCount) {
5089
+ const beforeTokenEstimate = events.reduce((acc, e) => acc + this.estimateTokens(e.content), 0);
5090
+ const afterSummaries = groups.filter((g) => g.events.length >= 3).slice(0, Math.max(consolidatedCount, 1));
5091
+ const afterTokenEstimate = afterSummaries.length > 0 ? afterSummaries.reduce((acc, g) => acc + this.estimateTokens(this.ruleBasedSummary(g)), 0) : beforeTokenEstimate;
5092
+ const reductionRatio = beforeTokenEstimate > 0 ? Math.max(0, (beforeTokenEstimate - afterTokenEstimate) / beforeTokenEstimate) : 0;
5093
+ const qualityGuardPassed = consolidatedCount === 0 ? true : groups.filter((g) => g.events.length >= 3).every((g) => this.calculateConfidence(g) >= 0.55);
5094
+ return {
5095
+ beforeTokenEstimate,
5096
+ afterTokenEstimate,
5097
+ reductionRatio,
5098
+ qualityGuardPassed,
5099
+ details: `groups=${groups.length}, consolidated=${consolidatedCount}`
5100
+ };
5101
+ }
5102
+ estimateTokens(text) {
5103
+ return Math.ceil((text || "").length / 4);
4014
5104
  }
4015
5105
  /**
4016
5106
  * Check if consolidation should run
@@ -4570,13 +5660,185 @@ function createGraduationWorker(eventStore, graduation, config) {
4570
5660
  );
4571
5661
  }
4572
5662
 
5663
+ // src/core/md-mirror.ts
5664
+ import * as fs3 from "node:fs";
5665
+ import * as path2 from "node:path";
5666
+ function sanitizeSegment2(input, fallback) {
5667
+ const v = (input || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
5668
+ return v || fallback;
5669
+ }
5670
+ function getAtPath(obj, dotted) {
5671
+ if (!obj)
5672
+ return void 0;
5673
+ return dotted.split(".").reduce((acc, key) => {
5674
+ if (!acc || typeof acc !== "object")
5675
+ return void 0;
5676
+ return acc[key];
5677
+ }, obj);
5678
+ }
5679
+ function buildMirrorPath2(rootDir, event) {
5680
+ const meta = event.metadata;
5681
+ const namespaceRaw = getAtPath(meta, "namespace") ?? getAtPath(meta, "scope.namespace") ?? event.eventType;
5682
+ const namespace = sanitizeSegment2(typeof namespaceRaw === "string" ? namespaceRaw : void 0, "general");
5683
+ const categoryRaw = getAtPath(meta, "categoryPath") ?? getAtPath(meta, "scope.categoryPath");
5684
+ const categoryPath = Array.isArray(categoryRaw) && categoryRaw.length > 0 ? categoryRaw.map((x) => sanitizeSegment2(typeof x === "string" ? x : void 0, "uncategorized")) : ["uncategorized"];
5685
+ const d = event.timestamp;
5686
+ const yyyy = d.getFullYear();
5687
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
5688
+ const dd = String(d.getDate()).padStart(2, "0");
5689
+ return path2.join(rootDir, "memory", namespace, ...categoryPath, `${yyyy}-${mm}-${dd}.md`);
5690
+ }
5691
+ var MarkdownMirror2 = class {
5692
+ constructor(rootDir) {
5693
+ this.rootDir = rootDir;
5694
+ }
5695
+ async append(event, eventId) {
5696
+ const out = buildMirrorPath2(this.rootDir, event);
5697
+ fs3.mkdirSync(path2.dirname(out), { recursive: true });
5698
+ const lines = [
5699
+ "",
5700
+ `## ${event.timestamp.toISOString()} | ${eventId ?? "pending-id"}`,
5701
+ `- type: ${event.eventType}`,
5702
+ `- session: ${event.sessionId}`,
5703
+ event.content
5704
+ ];
5705
+ await fs3.promises.appendFile(out, lines.join("\n"), "utf8");
5706
+ await this.refreshIndex();
5707
+ }
5708
+ async refreshIndex() {
5709
+ const memoryRoot = path2.join(this.rootDir, "memory");
5710
+ await fs3.promises.mkdir(memoryRoot, { recursive: true });
5711
+ const files = [];
5712
+ await this.walk(memoryRoot, files);
5713
+ const mdFiles = files.filter((f) => f.endsWith(".md")).map((f) => path2.relative(this.rootDir, f)).filter((rel) => rel !== path2.join("memory", "_index.md")).sort();
5714
+ const index = [
5715
+ "# Memory Index",
5716
+ "",
5717
+ "Generated automatically by MarkdownMirror.",
5718
+ "",
5719
+ ...mdFiles.map((rel) => `- ${rel}`),
5720
+ ""
5721
+ ].join("\n");
5722
+ await fs3.promises.writeFile(path2.join(memoryRoot, "_index.md"), index, "utf8");
5723
+ }
5724
+ async walk(dir, out) {
5725
+ const entries = await fs3.promises.readdir(dir, { withFileTypes: true });
5726
+ for (const e of entries) {
5727
+ const full = path2.join(dir, e.name);
5728
+ if (e.isDirectory()) {
5729
+ await this.walk(full, out);
5730
+ } else {
5731
+ out.push(full);
5732
+ }
5733
+ }
5734
+ }
5735
+ };
5736
+
5737
+ // src/core/ingest-interceptor.ts
5738
+ var IngestInterceptorRegistry = class {
5739
+ before = [];
5740
+ after = [];
5741
+ onError = [];
5742
+ registerBefore(interceptor) {
5743
+ this.before.push(interceptor);
5744
+ return () => {
5745
+ this.before = this.before.filter((i) => i !== interceptor);
5746
+ };
5747
+ }
5748
+ registerAfter(interceptor) {
5749
+ this.after.push(interceptor);
5750
+ return () => {
5751
+ this.after = this.after.filter((i) => i !== interceptor);
5752
+ };
5753
+ }
5754
+ registerOnError(interceptor) {
5755
+ this.onError.push(interceptor);
5756
+ return () => {
5757
+ this.onError = this.onError.filter((i) => i !== interceptor);
5758
+ };
5759
+ }
5760
+ async run(stage, context) {
5761
+ const interceptors = stage === "before" ? this.before : stage === "after" ? this.after : this.onError;
5762
+ for (const interceptor of interceptors) {
5763
+ await interceptor({ ...context, stage });
5764
+ }
5765
+ }
5766
+ };
5767
+ function mergeHierarchicalMetadata(base, patch) {
5768
+ if (!base && !patch)
5769
+ return void 0;
5770
+ if (!base)
5771
+ return patch;
5772
+ if (!patch)
5773
+ return base;
5774
+ const result = { ...base };
5775
+ for (const [key, value] of Object.entries(patch)) {
5776
+ const current = result[key];
5777
+ if (typeof current === "object" && current !== null && !Array.isArray(current) && typeof value === "object" && value !== null && !Array.isArray(value)) {
5778
+ result[key] = mergeHierarchicalMetadata(
5779
+ current,
5780
+ value
5781
+ );
5782
+ } else {
5783
+ result[key] = value;
5784
+ }
5785
+ }
5786
+ return result;
5787
+ }
5788
+
5789
+ // src/core/tag-taxonomy.ts
5790
+ var TAG_NAMESPACES = {
5791
+ SYSTEM: "sys:",
5792
+ QUALITY: "q:",
5793
+ PROJECT: "proj:",
5794
+ TOPIC: "topic:",
5795
+ TEMPORAL: "t:",
5796
+ USER: "user:",
5797
+ AGENT: "agent:"
5798
+ };
5799
+ var VALID_TAG_NAMESPACES = new Set(Object.values(TAG_NAMESPACES));
5800
+ function parseTag(tag) {
5801
+ const value = (tag || "").trim();
5802
+ const idx = value.indexOf(":");
5803
+ if (idx <= 0)
5804
+ return { value };
5805
+ const namespace = `${value.slice(0, idx)}:`;
5806
+ const tagValue = value.slice(idx + 1);
5807
+ if (!tagValue)
5808
+ return { value };
5809
+ return { namespace, value: tagValue };
5810
+ }
5811
+ function validateTag(tag) {
5812
+ const normalized = (tag || "").trim();
5813
+ if (!normalized)
5814
+ return false;
5815
+ const { namespace } = parseTag(normalized);
5816
+ if (!namespace)
5817
+ return true;
5818
+ return VALID_TAG_NAMESPACES.has(namespace);
5819
+ }
5820
+ function normalizeTags(tags) {
5821
+ if (!Array.isArray(tags))
5822
+ return [];
5823
+ const dedup = /* @__PURE__ */ new Set();
5824
+ for (const item of tags) {
5825
+ if (typeof item !== "string")
5826
+ continue;
5827
+ const normalized = item.trim();
5828
+ if (!validateTag(normalized))
5829
+ continue;
5830
+ dedup.add(normalized);
5831
+ }
5832
+ return [...dedup];
5833
+ }
5834
+
4573
5835
  // src/services/memory-service.ts
4574
5836
  function normalizePath(projectPath) {
4575
- const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
5837
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
4576
5838
  try {
4577
- return fs.realpathSync(expanded);
5839
+ return fs4.realpathSync(expanded);
4578
5840
  } catch {
4579
- return path.resolve(expanded);
5841
+ return path3.resolve(expanded);
4580
5842
  }
4581
5843
  }
4582
5844
  function hashProjectPath(projectPath) {
@@ -4585,10 +5847,46 @@ function hashProjectPath(projectPath) {
4585
5847
  }
4586
5848
  function getProjectStoragePath(projectPath) {
4587
5849
  const hash = hashProjectPath(projectPath);
4588
- return path.join(os.homedir(), ".claude-code", "memory", "projects", hash);
5850
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
5851
+ }
5852
+ var REGISTRY_PATH = path3.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
5853
+ var SHARED_STORAGE_PATH = path3.join(os.homedir(), ".claude-code", "memory", "shared");
5854
+ function loadSessionRegistry() {
5855
+ try {
5856
+ if (fs4.existsSync(REGISTRY_PATH)) {
5857
+ const data = fs4.readFileSync(REGISTRY_PATH, "utf-8");
5858
+ return JSON.parse(data);
5859
+ }
5860
+ } catch (error) {
5861
+ console.error("Failed to load session registry:", error);
5862
+ }
5863
+ return { version: 1, sessions: {} };
5864
+ }
5865
+ function saveSessionRegistry(registry) {
5866
+ const dir = path3.dirname(REGISTRY_PATH);
5867
+ if (!fs4.existsSync(dir)) {
5868
+ fs4.mkdirSync(dir, { recursive: true });
5869
+ }
5870
+ const tempPath = REGISTRY_PATH + ".tmp";
5871
+ fs4.writeFileSync(tempPath, JSON.stringify(registry, null, 2));
5872
+ fs4.renameSync(tempPath, REGISTRY_PATH);
5873
+ }
5874
+ function registerSession(sessionId, projectPath) {
5875
+ const registry = loadSessionRegistry();
5876
+ registry.sessions[sessionId] = {
5877
+ projectPath: normalizePath(projectPath),
5878
+ projectHash: hashProjectPath(projectPath),
5879
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5880
+ };
5881
+ const entries = Object.entries(registry.sessions);
5882
+ if (entries.length > 1e3) {
5883
+ const sorted = entries.sort(
5884
+ (a, b) => new Date(b[1].registeredAt).getTime() - new Date(a[1].registeredAt).getTime()
5885
+ );
5886
+ registry.sessions = Object.fromEntries(sorted.slice(0, 1e3));
5887
+ }
5888
+ saveSessionRegistry(registry);
4589
5889
  }
4590
- var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
4591
- var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
4592
5890
  var MemoryService = class {
4593
5891
  // Primary store: SQLite (WAL mode) - for hooks, always available
4594
5892
  sqliteStore;
@@ -4603,6 +5901,7 @@ var MemoryService = class {
4603
5901
  vectorWorker = null;
4604
5902
  graduationWorker = null;
4605
5903
  initialized = false;
5904
+ ingestInterceptors = new IngestInterceptorRegistry();
4606
5905
  // Endless Mode components
4607
5906
  workingSetStore = null;
4608
5907
  consolidatedStore = null;
@@ -4616,20 +5915,27 @@ var MemoryService = class {
4616
5915
  sharedPromoter = null;
4617
5916
  sharedStoreConfig = null;
4618
5917
  projectHash = null;
5918
+ projectPath = null;
4619
5919
  readOnly;
4620
5920
  lightweightMode;
5921
+ mdMirror;
4621
5922
  constructor(config) {
4622
5923
  const storagePath = this.expandPath(config.storagePath);
4623
5924
  this.readOnly = config.readOnly ?? false;
4624
5925
  this.lightweightMode = config.lightweightMode ?? false;
4625
- if (!this.readOnly && !fs.existsSync(storagePath)) {
4626
- fs.mkdirSync(storagePath, { recursive: true });
5926
+ this.mdMirror = new MarkdownMirror2(process.cwd());
5927
+ if (!this.readOnly && !fs4.existsSync(storagePath)) {
5928
+ fs4.mkdirSync(storagePath, { recursive: true });
4627
5929
  }
4628
5930
  this.projectHash = config.projectHash || null;
5931
+ this.projectPath = config.projectPath || null;
4629
5932
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
4630
5933
  this.sqliteStore = new SQLiteEventStore(
4631
- path.join(storagePath, "events.sqlite"),
4632
- { readonly: this.readOnly }
5934
+ path3.join(storagePath, "events.sqlite"),
5935
+ {
5936
+ readonly: this.readOnly,
5937
+ markdownMirrorRoot: storagePath
5938
+ }
4633
5939
  );
4634
5940
  const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
4635
5941
  if (!analyticsEnabled) {
@@ -4637,7 +5943,7 @@ var MemoryService = class {
4637
5943
  } else if (this.readOnly) {
4638
5944
  try {
4639
5945
  this.analyticsStore = new EventStore(
4640
- path.join(storagePath, "analytics.duckdb"),
5946
+ path3.join(storagePath, "analytics.duckdb"),
4641
5947
  { readOnly: true }
4642
5948
  );
4643
5949
  } catch {
@@ -4645,11 +5951,11 @@ var MemoryService = class {
4645
5951
  }
4646
5952
  } else {
4647
5953
  this.analyticsStore = new EventStore(
4648
- path.join(storagePath, "analytics.duckdb"),
5954
+ path3.join(storagePath, "analytics.duckdb"),
4649
5955
  { readOnly: false }
4650
5956
  );
4651
5957
  }
4652
- this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
5958
+ this.vectorStore = new VectorStore(path3.join(storagePath, "vectors"));
4653
5959
  this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
4654
5960
  this.matcher = getDefaultMatcher();
4655
5961
  this.retriever = createRetriever(
@@ -4659,6 +5965,7 @@ var MemoryService = class {
4659
5965
  this.embedder,
4660
5966
  this.matcher
4661
5967
  );
5968
+ this.retriever.setQueryRewriter((q) => this.rewriteQueryIntent(q));
4662
5969
  this.graduation = createGraduationPipeline(this.sqliteStore);
4663
5970
  }
4664
5971
  /**
@@ -4718,16 +6025,16 @@ var MemoryService = class {
4718
6025
  */
4719
6026
  async initializeSharedStore() {
4720
6027
  const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
4721
- if (!fs.existsSync(sharedPath)) {
4722
- fs.mkdirSync(sharedPath, { recursive: true });
6028
+ if (!fs4.existsSync(sharedPath)) {
6029
+ fs4.mkdirSync(sharedPath, { recursive: true });
4723
6030
  }
4724
6031
  this.sharedEventStore = createSharedEventStore(
4725
- path.join(sharedPath, "shared.duckdb")
6032
+ path3.join(sharedPath, "shared.duckdb")
4726
6033
  );
4727
6034
  await this.sharedEventStore.initialize();
4728
6035
  this.sharedStore = createSharedStore(this.sharedEventStore);
4729
6036
  this.sharedVectorStore = createSharedVectorStore(
4730
- path.join(sharedPath, "vectors")
6037
+ path3.join(sharedPath, "vectors")
4731
6038
  );
4732
6039
  await this.sharedVectorStore.initialize();
4733
6040
  this.sharedPromoter = createSharedPromoter(
@@ -4738,18 +6045,98 @@ var MemoryService = class {
4738
6045
  );
4739
6046
  this.retriever.setSharedStores(this.sharedStore, this.sharedVectorStore);
4740
6047
  }
4741
- /**
4742
- * Start a new session
4743
- */
4744
- async startSession(sessionId, projectPath) {
4745
- await this.initialize();
4746
- await this.sqliteStore.upsertSession({
4747
- id: sessionId,
4748
- startedAt: /* @__PURE__ */ new Date(),
4749
- projectPath
4750
- });
6048
+ registerIngestBefore(interceptor) {
6049
+ return this.ingestInterceptors.registerBefore(interceptor);
4751
6050
  }
4752
- /**
6051
+ registerIngestAfter(interceptor) {
6052
+ return this.ingestInterceptors.registerAfter(interceptor);
6053
+ }
6054
+ registerIngestOnError(interceptor) {
6055
+ return this.ingestInterceptors.registerOnError(interceptor);
6056
+ }
6057
+ async ingestWithInterceptors(operation, input, onSuccess) {
6058
+ const normalizedInput = {
6059
+ ...input,
6060
+ metadata: mergeHierarchicalMetadata(
6061
+ {
6062
+ ingest: {
6063
+ operation,
6064
+ pipeline: "default",
6065
+ ts: (/* @__PURE__ */ new Date()).toISOString()
6066
+ },
6067
+ ...this.projectHash ? {
6068
+ scope: {
6069
+ project: {
6070
+ hash: this.projectHash,
6071
+ ...this.projectPath ? { path: this.projectPath } : {}
6072
+ }
6073
+ },
6074
+ tags: [`proj:${this.projectHash}`]
6075
+ } : {}
6076
+ },
6077
+ input.metadata
6078
+ )
6079
+ };
6080
+ if (this.projectHash && normalizedInput.metadata) {
6081
+ const meta = normalizedInput.metadata;
6082
+ const currentTags = Array.isArray(meta.tags) ? meta.tags.filter((x) => typeof x === "string") : [];
6083
+ const projectTag = `proj:${this.projectHash}`;
6084
+ if (!currentTags.includes(projectTag)) {
6085
+ meta.tags = [...currentTags, projectTag];
6086
+ }
6087
+ }
6088
+ if (normalizedInput.metadata) {
6089
+ const meta = normalizedInput.metadata;
6090
+ const normalizedTags = normalizeTags(meta.tags);
6091
+ if (normalizedTags.length > 0) {
6092
+ meta.tags = normalizedTags;
6093
+ }
6094
+ }
6095
+ await this.ingestInterceptors.run("before", {
6096
+ operation,
6097
+ sessionId: normalizedInput.sessionId,
6098
+ event: normalizedInput
6099
+ });
6100
+ try {
6101
+ const result = await this.sqliteStore.append(normalizedInput);
6102
+ if (result.success && !result.isDuplicate) {
6103
+ if (onSuccess) {
6104
+ await onSuccess(result.eventId);
6105
+ }
6106
+ try {
6107
+ await this.mdMirror.append(normalizedInput, result.eventId);
6108
+ } catch {
6109
+ }
6110
+ }
6111
+ await this.ingestInterceptors.run("after", {
6112
+ operation,
6113
+ sessionId: normalizedInput.sessionId,
6114
+ event: normalizedInput
6115
+ });
6116
+ return result;
6117
+ } catch (error) {
6118
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
6119
+ await this.ingestInterceptors.run("error", {
6120
+ operation,
6121
+ sessionId: normalizedInput.sessionId,
6122
+ event: normalizedInput,
6123
+ error: normalizedError
6124
+ });
6125
+ throw error;
6126
+ }
6127
+ }
6128
+ /**
6129
+ * Start a new session
6130
+ */
6131
+ async startSession(sessionId, projectPath) {
6132
+ await this.initialize();
6133
+ await this.sqliteStore.upsertSession({
6134
+ id: sessionId,
6135
+ startedAt: /* @__PURE__ */ new Date(),
6136
+ projectPath
6137
+ });
6138
+ }
6139
+ /**
4753
6140
  * End a session
4754
6141
  */
4755
6142
  async endSession(sessionId, summary) {
@@ -4765,50 +6152,57 @@ var MemoryService = class {
4765
6152
  */
4766
6153
  async storeUserPrompt(sessionId, content, metadata) {
4767
6154
  await this.initialize();
4768
- const result = await this.sqliteStore.append({
4769
- eventType: "user_prompt",
4770
- sessionId,
4771
- timestamp: /* @__PURE__ */ new Date(),
4772
- content,
4773
- metadata
4774
- });
4775
- if (result.success && !result.isDuplicate) {
4776
- await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
4777
- }
4778
- return result;
6155
+ return this.ingestWithInterceptors(
6156
+ "user_prompt",
6157
+ {
6158
+ eventType: "user_prompt",
6159
+ sessionId,
6160
+ timestamp: /* @__PURE__ */ new Date(),
6161
+ content,
6162
+ metadata
6163
+ },
6164
+ async (eventId) => {
6165
+ await this.sqliteStore.enqueueForEmbedding(eventId, content);
6166
+ }
6167
+ );
4779
6168
  }
4780
6169
  /**
4781
6170
  * Store an agent response
4782
6171
  */
4783
6172
  async storeAgentResponse(sessionId, content, metadata) {
4784
6173
  await this.initialize();
4785
- const result = await this.sqliteStore.append({
4786
- eventType: "agent_response",
4787
- sessionId,
4788
- timestamp: /* @__PURE__ */ new Date(),
4789
- content,
4790
- metadata
4791
- });
4792
- if (result.success && !result.isDuplicate) {
4793
- await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
4794
- }
4795
- return result;
6174
+ return this.ingestWithInterceptors(
6175
+ "agent_response",
6176
+ {
6177
+ eventType: "agent_response",
6178
+ sessionId,
6179
+ timestamp: /* @__PURE__ */ new Date(),
6180
+ content,
6181
+ metadata
6182
+ },
6183
+ async (eventId) => {
6184
+ await this.sqliteStore.enqueueForEmbedding(eventId, content);
6185
+ }
6186
+ );
4796
6187
  }
4797
6188
  /**
4798
6189
  * Store a session summary
4799
6190
  */
4800
- async storeSessionSummary(sessionId, summary) {
6191
+ async storeSessionSummary(sessionId, summary, metadata) {
4801
6192
  await this.initialize();
4802
- const result = await this.sqliteStore.append({
4803
- eventType: "session_summary",
4804
- sessionId,
4805
- timestamp: /* @__PURE__ */ new Date(),
4806
- content: summary
4807
- });
4808
- if (result.success && !result.isDuplicate) {
4809
- await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
4810
- }
4811
- return result;
6193
+ return this.ingestWithInterceptors(
6194
+ "session_summary",
6195
+ {
6196
+ eventType: "session_summary",
6197
+ sessionId,
6198
+ timestamp: /* @__PURE__ */ new Date(),
6199
+ content: summary,
6200
+ metadata
6201
+ },
6202
+ async (eventId) => {
6203
+ await this.sqliteStore.enqueueForEmbedding(eventId, summary);
6204
+ }
6205
+ );
4812
6206
  }
4813
6207
  /**
4814
6208
  * Store a tool observation
@@ -4816,39 +6210,182 @@ var MemoryService = class {
4816
6210
  async storeToolObservation(sessionId, payload) {
4817
6211
  await this.initialize();
4818
6212
  const content = JSON.stringify(payload);
4819
- const result = await this.sqliteStore.append({
4820
- eventType: "tool_observation",
4821
- sessionId,
4822
- timestamp: /* @__PURE__ */ new Date(),
4823
- content,
4824
- metadata: {
4825
- toolName: payload.toolName,
4826
- success: payload.success
6213
+ const turnId = payload.metadata?.turnId;
6214
+ return this.ingestWithInterceptors(
6215
+ "tool_observation",
6216
+ {
6217
+ eventType: "tool_observation",
6218
+ sessionId,
6219
+ timestamp: /* @__PURE__ */ new Date(),
6220
+ content,
6221
+ metadata: {
6222
+ toolName: payload.toolName,
6223
+ success: payload.success,
6224
+ ...turnId ? { turnId } : {}
6225
+ }
6226
+ },
6227
+ async (eventId) => {
6228
+ const embeddingContent = createToolObservationEmbedding(
6229
+ payload.toolName,
6230
+ payload.metadata || {},
6231
+ payload.success
6232
+ );
6233
+ await this.sqliteStore.enqueueForEmbedding(eventId, embeddingContent);
4827
6234
  }
4828
- });
4829
- if (result.success && !result.isDuplicate) {
4830
- const embeddingContent = createToolObservationEmbedding(
4831
- payload.toolName,
4832
- payload.metadata || {},
4833
- payload.success
4834
- );
4835
- await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
4836
- }
4837
- return result;
6235
+ );
4838
6236
  }
4839
6237
  /**
4840
6238
  * Retrieve relevant memories for a query
4841
6239
  */
4842
6240
  async retrieveMemories(query, options) {
4843
6241
  await this.initialize();
6242
+ const rerankWeights = await this.getRerankWeights(options?.adaptiveRerank === true);
6243
+ let result;
4844
6244
  if (options?.includeShared && this.sharedStore) {
4845
- return this.retriever.retrieveUnified(query, {
6245
+ result = await this.retriever.retrieveUnified(query, {
4846
6246
  ...options,
6247
+ intentRewrite: options?.intentRewrite === true,
6248
+ rerankWeights,
4847
6249
  includeShared: true,
4848
- projectHash: this.projectHash || void 0
6250
+ projectHash: this.projectHash || void 0,
6251
+ projectScopeMode: options?.projectScopeMode ?? (this.projectHash ? "strict" : "global"),
6252
+ allowedProjectHashes: options?.allowedProjectHashes
6253
+ });
6254
+ } else {
6255
+ result = await this.retriever.retrieve(query, {
6256
+ ...options,
6257
+ intentRewrite: options?.intentRewrite === true,
6258
+ rerankWeights,
6259
+ projectHash: this.projectHash || void 0,
6260
+ projectScopeMode: options?.projectScopeMode ?? (this.projectHash ? "strict" : "global"),
6261
+ allowedProjectHashes: options?.allowedProjectHashes
6262
+ });
6263
+ }
6264
+ try {
6265
+ const selectedEventIds = result.memories.map((m) => m.event.id);
6266
+ const selectedDetails = (result.selectedDebug || []).map((d) => ({
6267
+ eventId: d.eventId,
6268
+ score: d.score,
6269
+ semanticScore: d.semanticScore,
6270
+ lexicalScore: d.lexicalScore,
6271
+ recencyScore: d.recencyScore
6272
+ }));
6273
+ const candidateDetails = (result.candidateDebug || []).map((d) => ({
6274
+ eventId: d.eventId,
6275
+ score: d.score,
6276
+ semanticScore: d.semanticScore,
6277
+ lexicalScore: d.lexicalScore,
6278
+ recencyScore: d.recencyScore
6279
+ }));
6280
+ const candidateEventIds = candidateDetails.length > 0 ? candidateDetails.map((d) => d.eventId) : selectedEventIds;
6281
+ await this.sqliteStore.recordRetrievalTrace({
6282
+ sessionId: options?.sessionId,
6283
+ projectHash: this.projectHash || void 0,
6284
+ queryText: query,
6285
+ strategy: options?.strategy || "auto",
6286
+ candidateEventIds,
6287
+ selectedEventIds,
6288
+ candidateDetails,
6289
+ selectedDetails,
6290
+ confidence: result.matchResult.confidence,
6291
+ fallbackTrace: result.fallbackTrace || []
6292
+ });
6293
+ } catch {
6294
+ }
6295
+ return result;
6296
+ }
6297
+ getConfiguredRerankWeights() {
6298
+ const semantic = Number(process.env.MEMORY_RERANK_WEIGHT_SEMANTIC ?? "");
6299
+ const lexical = Number(process.env.MEMORY_RERANK_WEIGHT_LEXICAL ?? "");
6300
+ const recency = Number(process.env.MEMORY_RERANK_WEIGHT_RECENCY ?? "");
6301
+ const allFinite = [semantic, lexical, recency].every((v) => Number.isFinite(v));
6302
+ if (!allFinite)
6303
+ return void 0;
6304
+ const nonNegative = [semantic, lexical, recency].every((v) => v >= 0);
6305
+ const total = semantic + lexical + recency;
6306
+ if (!nonNegative || total <= 0)
6307
+ return void 0;
6308
+ return {
6309
+ semantic: semantic / total,
6310
+ lexical: lexical / total,
6311
+ recency: recency / total
6312
+ };
6313
+ }
6314
+ async getRerankWeights(adaptive) {
6315
+ const configured = this.getConfiguredRerankWeights();
6316
+ if (configured)
6317
+ return configured;
6318
+ if (adaptive)
6319
+ return this.getAdaptiveRerankWeights();
6320
+ return void 0;
6321
+ }
6322
+ async rewriteQueryIntent(query) {
6323
+ if (process.env.MEMORY_INTENT_REWRITE_ENABLED !== "1")
6324
+ return null;
6325
+ const apiUrl = process.env.COMPANY_STOCK_API_URL || process.env.COMPANY_INT_API_URL;
6326
+ if (!apiUrl)
6327
+ return null;
6328
+ const controller = new AbortController();
6329
+ const timeoutMs = Number(process.env.MEMORY_INTENT_REWRITE_TIMEOUT_MS || 5e3);
6330
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
6331
+ try {
6332
+ const prompt = [
6333
+ "Rewrite user query for memory retrieval intent expansion.",
6334
+ "Return plain text only, one line, no markdown.",
6335
+ `Query: ${query}`
6336
+ ].join("\n");
6337
+ const res = await fetch(apiUrl, {
6338
+ method: "POST",
6339
+ headers: {
6340
+ "Content-Type": "application/json",
6341
+ Accept: "*/*",
6342
+ Origin: process.env.COMPANY_INT_ORIGIN || "http://company-int.aplusai.ai",
6343
+ Referer: process.env.COMPANY_INT_REFERER || "http://company-int.aplusai.ai/"
6344
+ },
6345
+ body: JSON.stringify({
6346
+ question: prompt,
6347
+ company_name: null,
6348
+ conversation_id: null
6349
+ }),
6350
+ signal: controller.signal
4849
6351
  });
6352
+ const text = (await res.text()).trim();
6353
+ if (!text)
6354
+ return null;
6355
+ const oneLine = text.replace(/^data:\s*/gm, "").split(/\r?\n/).map((x) => x.trim()).filter(Boolean).join(" ").slice(0, 240);
6356
+ if (!oneLine || oneLine.toLowerCase() === query.toLowerCase())
6357
+ return null;
6358
+ return oneLine;
6359
+ } catch {
6360
+ return null;
6361
+ } finally {
6362
+ clearTimeout(timeout);
6363
+ }
6364
+ }
6365
+ async getAdaptiveRerankWeights() {
6366
+ try {
6367
+ const s = await this.sqliteStore.getHelpfulnessStats();
6368
+ if (s.totalEvaluated < 20)
6369
+ return void 0;
6370
+ let semantic = 0.7;
6371
+ let lexical = 0.2;
6372
+ let recency = 0.1;
6373
+ if (s.avgScore < 0.45) {
6374
+ semantic -= 0.1;
6375
+ lexical += 0.1;
6376
+ } else if (s.avgScore > 0.75) {
6377
+ semantic += 0.05;
6378
+ lexical -= 0.05;
6379
+ }
6380
+ if (s.unhelpful > s.helpful) {
6381
+ recency += 0.05;
6382
+ semantic -= 0.03;
6383
+ lexical -= 0.02;
6384
+ }
6385
+ return { semantic, lexical, recency };
6386
+ } catch {
6387
+ return void 0;
4850
6388
  }
4851
- return this.retriever.retrieve(query, options);
4852
6389
  }
4853
6390
  /**
4854
6391
  * Fast keyword search using SQLite FTS5
@@ -4890,6 +6427,18 @@ var MemoryService = class {
4890
6427
  /**
4891
6428
  * Get memory statistics
4892
6429
  */
6430
+ async getOutboxStats() {
6431
+ await this.initialize();
6432
+ return this.sqliteStore.getOutboxStats();
6433
+ }
6434
+ async getRetrievalTraceStats() {
6435
+ await this.initialize();
6436
+ return this.sqliteStore.getRetrievalTraceStats();
6437
+ }
6438
+ async getRecentRetrievalTraces(limit = 50) {
6439
+ await this.initialize();
6440
+ return this.sqliteStore.getRecentRetrievalTraces(limit);
6441
+ }
4893
6442
  async getStats() {
4894
6443
  await this.initialize();
4895
6444
  const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
@@ -5106,6 +6655,31 @@ var MemoryService = class {
5106
6655
  return [];
5107
6656
  return this.consolidatedStore.getAll({ limit });
5108
6657
  }
6658
+ /**
6659
+ * Extract topic keywords from event content (markdown headings and key terms)
6660
+ */
6661
+ extractTopicsFromContent(content) {
6662
+ const topics = /* @__PURE__ */ new Set();
6663
+ const headings = content.match(/^#{1,3}\s+(.+)$/gm);
6664
+ if (headings) {
6665
+ for (const h of headings.slice(0, 5)) {
6666
+ const text = h.replace(/^#+\s+/, "").replace(/[*_`#]/g, "").trim();
6667
+ if (text.length > 2 && text.length < 50) {
6668
+ topics.add(text);
6669
+ }
6670
+ }
6671
+ }
6672
+ const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
6673
+ if (boldTerms) {
6674
+ for (const b of boldTerms.slice(0, 5)) {
6675
+ const text = b.replace(/\*\*/g, "").trim();
6676
+ if (text.length > 2 && text.length < 30) {
6677
+ topics.add(text);
6678
+ }
6679
+ }
6680
+ }
6681
+ return Array.from(topics).slice(0, 5);
6682
+ }
5109
6683
  /**
5110
6684
  * Increment access count for memories that were used in prompts
5111
6685
  */
@@ -5129,8 +6703,7 @@ var MemoryService = class {
5129
6703
  return events.map((event) => ({
5130
6704
  memoryId: event.id,
5131
6705
  summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
5132
- topics: [],
5133
- // Could extract topics from content if needed
6706
+ topics: this.extractTopicsFromContent(event.content),
5134
6707
  accessCount: event.access_count || 0,
5135
6708
  lastAccessed: event.last_accessed_at || null,
5136
6709
  confidence: 1,
@@ -5151,6 +6724,34 @@ var MemoryService = class {
5151
6724
  }
5152
6725
  return [];
5153
6726
  }
6727
+ /**
6728
+ * Record a memory retrieval for helpfulness tracking
6729
+ */
6730
+ async recordRetrieval(eventId, sessionId, score, query) {
6731
+ await this.initialize();
6732
+ await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
6733
+ }
6734
+ /**
6735
+ * Evaluate helpfulness of retrievals in a session (called at session end)
6736
+ */
6737
+ async evaluateSessionHelpfulness(sessionId) {
6738
+ await this.initialize();
6739
+ await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
6740
+ }
6741
+ /**
6742
+ * Get most helpful memories ranked by helpfulness score
6743
+ */
6744
+ async getHelpfulMemories(limit = 10) {
6745
+ await this.initialize();
6746
+ return this.sqliteStore.getHelpfulMemories(limit);
6747
+ }
6748
+ /**
6749
+ * Get helpfulness statistics for dashboard
6750
+ */
6751
+ async getHelpfulnessStats() {
6752
+ await this.initialize();
6753
+ return this.sqliteStore.getHelpfulnessStats();
6754
+ }
5154
6755
  /**
5155
6756
  * Mark a consolidated memory as accessed
5156
6757
  */
@@ -5214,6 +6815,44 @@ var MemoryService = class {
5214
6815
  lastConsolidation
5215
6816
  };
5216
6817
  }
6818
+ // ============================================================
6819
+ // Turn Grouping Methods
6820
+ // ============================================================
6821
+ /**
6822
+ * Get events grouped by turn for a session
6823
+ */
6824
+ async getSessionTurns(sessionId, options) {
6825
+ await this.initialize();
6826
+ return this.sqliteStore.getSessionTurns(sessionId, options);
6827
+ }
6828
+ /**
6829
+ * Get all events for a specific turn
6830
+ */
6831
+ async getEventsByTurn(turnId) {
6832
+ await this.initialize();
6833
+ return this.sqliteStore.getEventsByTurn(turnId);
6834
+ }
6835
+ /**
6836
+ * Count total turns for a session
6837
+ */
6838
+ async countSessionTurns(sessionId) {
6839
+ await this.initialize();
6840
+ return this.sqliteStore.countSessionTurns(sessionId);
6841
+ }
6842
+ /**
6843
+ * Backfill turn_ids from metadata for events stored before the migration
6844
+ */
6845
+ async backfillTurnIds() {
6846
+ await this.initialize();
6847
+ return this.sqliteStore.backfillTurnIds();
6848
+ }
6849
+ /**
6850
+ * Delete all events for a session (for force reimport)
6851
+ */
6852
+ async deleteSessionEvents(sessionId) {
6853
+ await this.initialize();
6854
+ return this.sqliteStore.deleteSessionEvents(sessionId);
6855
+ }
5217
6856
  /**
5218
6857
  * Format Endless Mode context for Claude
5219
6858
  */
@@ -5290,7 +6929,7 @@ var MemoryService = class {
5290
6929
  */
5291
6930
  expandPath(p) {
5292
6931
  if (p.startsWith("~")) {
5293
- return path.join(os.homedir(), p.slice(1));
6932
+ return path3.join(os.homedir(), p.slice(1));
5294
6933
  }
5295
6934
  return p;
5296
6935
  }
@@ -5326,6 +6965,7 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
5326
6965
  serviceCache.set(hash, new MemoryService({
5327
6966
  storagePath,
5328
6967
  projectHash: hash,
6968
+ projectPath,
5329
6969
  // Override shared store config - hooks don't need DuckDB
5330
6970
  sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
5331
6971
  analyticsEnabled: false
@@ -5336,16 +6976,52 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
5336
6976
  }
5337
6977
 
5338
6978
  // src/services/session-history-importer.ts
5339
- import * as fs2 from "fs";
5340
- import * as path2 from "path";
6979
+ import * as fs5 from "fs";
6980
+ import * as path4 from "path";
5341
6981
  import * as os2 from "os";
5342
6982
  import * as readline from "readline";
6983
+ import { randomUUID as randomUUID9 } from "crypto";
6984
+ function classifyEntry(entry) {
6985
+ if (entry.type !== "user" && entry.type !== "assistant") {
6986
+ return "skip";
6987
+ }
6988
+ const content = entry.message?.content;
6989
+ if (!content)
6990
+ return "skip";
6991
+ if (entry.type === "user") {
6992
+ if (typeof content === "string")
6993
+ return "user_prompt";
6994
+ if (Array.isArray(content)) {
6995
+ const hasToolResult = content.some((b) => b.type === "tool_result");
6996
+ if (hasToolResult)
6997
+ return "tool_result";
6998
+ const hasText = content.some((b) => b.type === "text" && b.text);
6999
+ if (hasText)
7000
+ return "user_prompt";
7001
+ }
7002
+ return "skip";
7003
+ }
7004
+ if (Array.isArray(content)) {
7005
+ const hasToolUse = content.some((b) => b.type === "tool_use");
7006
+ if (hasToolUse)
7007
+ return "tool_use";
7008
+ const hasText = content.some((b) => b.type === "text" && b.text);
7009
+ if (hasText)
7010
+ return "agent_text";
7011
+ const hasThinking = content.some((b) => b.type === "thinking");
7012
+ if (hasThinking)
7013
+ return "thinking";
7014
+ } else if (typeof content === "string" && content.length > 0) {
7015
+ return "agent_text";
7016
+ }
7017
+ return "skip";
7018
+ }
5343
7019
  var SessionHistoryImporter = class {
5344
7020
  memoryService;
5345
7021
  claudeDir;
5346
7022
  constructor(memoryService) {
5347
7023
  this.memoryService = memoryService;
5348
- this.claudeDir = path2.join(os2.homedir(), ".claude");
7024
+ this.claudeDir = path4.join(os2.homedir(), ".claude");
5349
7025
  }
5350
7026
  /**
5351
7027
  * Import all sessions from a project
@@ -5359,6 +7035,8 @@ var SessionHistoryImporter = class {
5359
7035
  skippedDuplicates: 0,
5360
7036
  errors: []
5361
7037
  };
7038
+ const onProgress = options.onProgress;
7039
+ onProgress?.({ phase: "scan", message: "Scanning for session files..." });
5362
7040
  const projectDir = await this.findProjectDir(projectPath);
5363
7041
  if (!projectDir) {
5364
7042
  result.errors.push(`Project directory not found for: ${projectPath}`);
@@ -5366,16 +7044,29 @@ var SessionHistoryImporter = class {
5366
7044
  }
5367
7045
  const sessionFiles = await this.findSessionFiles(projectDir);
5368
7046
  result.totalSessions = sessionFiles.length;
7047
+ onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} sessions in ${path4.basename(projectDir)}` });
5369
7048
  if (options.verbose) {
5370
7049
  console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
5371
7050
  }
5372
- for (const sessionFile of sessionFiles) {
7051
+ for (let i = 0; i < sessionFiles.length; i++) {
7052
+ const sessionFile = sessionFiles[i];
5373
7053
  try {
5374
- const sessionResult = await this.importSessionFile(sessionFile, options);
7054
+ onProgress?.({ phase: "session-start", sessionIndex: i, totalSessions: sessionFiles.length, filePath: sessionFile });
7055
+ const sessionResult = await this.importSessionFile(sessionFile, {
7056
+ ...options,
7057
+ _sessionIndex: i
7058
+ });
5375
7059
  result.totalMessages += sessionResult.totalMessages;
5376
7060
  result.importedPrompts += sessionResult.importedPrompts;
5377
7061
  result.importedResponses += sessionResult.importedResponses;
5378
7062
  result.skippedDuplicates += sessionResult.skippedDuplicates;
7063
+ onProgress?.({
7064
+ phase: "session-done",
7065
+ sessionIndex: i,
7066
+ importedPrompts: sessionResult.importedPrompts,
7067
+ importedResponses: sessionResult.importedResponses,
7068
+ skipped: sessionResult.skippedDuplicates
7069
+ });
5379
7070
  } catch (error) {
5380
7071
  result.errors.push(`Failed to import ${sessionFile}: ${error}`);
5381
7072
  }
@@ -5394,60 +7085,105 @@ var SessionHistoryImporter = class {
5394
7085
  skippedDuplicates: 0,
5395
7086
  errors: []
5396
7087
  };
5397
- if (!fs2.existsSync(filePath)) {
7088
+ if (!fs5.existsSync(filePath)) {
5398
7089
  result.errors.push(`File not found: ${filePath}`);
5399
7090
  return result;
5400
7091
  }
5401
- const sessionId = path2.basename(filePath, ".jsonl");
7092
+ const sessionId = path4.basename(filePath, ".jsonl");
7093
+ if (options.force) {
7094
+ const deleted = await this.memoryService.deleteSessionEvents(sessionId);
7095
+ if (options.verbose && deleted > 0) {
7096
+ console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
7097
+ }
7098
+ }
5402
7099
  await this.memoryService.startSession(sessionId, options.projectPath);
5403
- const fileStream = fs2.createReadStream(filePath);
7100
+ const fileStream = fs5.createReadStream(filePath);
5404
7101
  const rl = readline.createInterface({
5405
7102
  input: fileStream,
5406
7103
  crlfDelay: Infinity
5407
7104
  });
5408
7105
  let lineCount = 0;
5409
7106
  const limit = options.limit || Infinity;
7107
+ const onProgress = options.onProgress;
7108
+ const sessionIndex = options._sessionIndex ?? 0;
7109
+ let lastProgressAt = 0;
7110
+ let currentTurnId = null;
7111
+ let textBuffer = [];
7112
+ let lastTimestamp;
7113
+ const flushTextBuffer = async () => {
7114
+ if (textBuffer.length === 0 || !currentTurnId)
7115
+ return;
7116
+ const substantive = textBuffer.filter((t) => t.length >= 100);
7117
+ const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
7118
+ if (!merged) {
7119
+ textBuffer = [];
7120
+ return;
7121
+ }
7122
+ const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
7123
+ const appendResult = await this.memoryService.storeAgentResponse(
7124
+ sessionId,
7125
+ truncated,
7126
+ { importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
7127
+ );
7128
+ if (appendResult.isDuplicate) {
7129
+ result.skippedDuplicates++;
7130
+ } else {
7131
+ result.importedResponses++;
7132
+ }
7133
+ lineCount++;
7134
+ textBuffer = [];
7135
+ };
5410
7136
  for await (const line of rl) {
5411
7137
  if (lineCount >= limit)
5412
7138
  break;
5413
7139
  try {
5414
7140
  const entry = JSON.parse(line);
5415
7141
  result.totalMessages++;
5416
- if (entry.type === "user" || entry.type === "assistant") {
7142
+ const msgClass = classifyEntry(entry);
7143
+ if (msgClass === "user_prompt") {
7144
+ await flushTextBuffer();
5417
7145
  const content = this.extractContent(entry);
5418
7146
  if (!content)
5419
7147
  continue;
5420
- if (entry.type === "user") {
5421
- const appendResult = await this.memoryService.storeUserPrompt(
5422
- sessionId,
5423
- content,
5424
- { importedFrom: filePath, originalTimestamp: entry.timestamp }
5425
- );
5426
- if (appendResult.isDuplicate) {
5427
- result.skippedDuplicates++;
5428
- } else {
5429
- result.importedPrompts++;
5430
- }
5431
- } else if (entry.type === "assistant") {
5432
- const truncatedContent = content.length > 5e3 ? content.slice(0, 5e3) + "...[truncated]" : content;
5433
- const appendResult = await this.memoryService.storeAgentResponse(
5434
- sessionId,
5435
- truncatedContent,
5436
- { importedFrom: filePath, originalTimestamp: entry.timestamp }
5437
- );
5438
- if (appendResult.isDuplicate) {
5439
- result.skippedDuplicates++;
5440
- } else {
5441
- result.importedResponses++;
5442
- }
7148
+ currentTurnId = randomUUID9();
7149
+ const appendResult = await this.memoryService.storeUserPrompt(
7150
+ sessionId,
7151
+ content,
7152
+ { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
7153
+ );
7154
+ if (appendResult.isDuplicate) {
7155
+ result.skippedDuplicates++;
7156
+ } else {
7157
+ result.importedPrompts++;
5443
7158
  }
5444
7159
  lineCount++;
7160
+ } else if (msgClass === "agent_text") {
7161
+ const content = this.extractContent(entry);
7162
+ if (content) {
7163
+ textBuffer.push(content);
7164
+ lastTimestamp = entry.timestamp;
7165
+ }
7166
+ }
7167
+ const now = Date.now();
7168
+ if (now - lastProgressAt > 200) {
7169
+ lastProgressAt = now;
7170
+ onProgress?.({
7171
+ phase: "session-progress",
7172
+ sessionIndex,
7173
+ messagesProcessed: result.totalMessages,
7174
+ imported: result.importedPrompts + result.importedResponses,
7175
+ skipped: result.skippedDuplicates
7176
+ });
5445
7177
  }
5446
7178
  } catch (parseError) {
5447
7179
  result.errors.push(`Parse error on line: ${parseError}`);
5448
7180
  }
5449
7181
  }
7182
+ await flushTextBuffer();
5450
7183
  await this.memoryService.endSession(sessionId);
7184
+ if (options.projectPath) {
7185
+ registerSession(sessionId, options.projectPath);
7186
+ }
5451
7187
  if (options.verbose) {
5452
7188
  console.log(`Imported ${result.importedPrompts} prompts, ${result.importedResponses} responses from ${filePath}`);
5453
7189
  }
@@ -5465,29 +7201,46 @@ var SessionHistoryImporter = class {
5465
7201
  skippedDuplicates: 0,
5466
7202
  errors: []
5467
7203
  };
5468
- const projectsDir = path2.join(this.claudeDir, "projects");
5469
- if (!fs2.existsSync(projectsDir)) {
7204
+ const onProgress = options.onProgress;
7205
+ const projectsDir = path4.join(this.claudeDir, "projects");
7206
+ if (!fs5.existsSync(projectsDir)) {
5470
7207
  result.errors.push(`Projects directory not found: ${projectsDir}`);
5471
7208
  return result;
5472
7209
  }
5473
- const projectDirs = fs2.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs2.statSync(p).isDirectory());
7210
+ onProgress?.({ phase: "scan", message: "Scanning all projects..." });
7211
+ const projectDirs = fs5.readdirSync(projectsDir).map((name) => path4.join(projectsDir, name)).filter((p) => fs5.statSync(p).isDirectory());
7212
+ const allSessionFiles = [];
7213
+ for (const projectDir of projectDirs) {
7214
+ const sessionFiles = await this.findSessionFiles(projectDir);
7215
+ allSessionFiles.push(...sessionFiles);
7216
+ }
7217
+ onProgress?.({ phase: "scan", message: `Found ${allSessionFiles.length} sessions across ${projectDirs.length} projects` });
5474
7218
  if (options.verbose) {
5475
- console.log(`Found ${projectDirs.length} project directories`);
7219
+ console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
5476
7220
  }
5477
- for (const projectDir of projectDirs) {
7221
+ for (let i = 0; i < allSessionFiles.length; i++) {
7222
+ const sessionFile = allSessionFiles[i];
5478
7223
  try {
5479
- const sessionFiles = await this.findSessionFiles(projectDir);
5480
- for (const sessionFile of sessionFiles) {
5481
- const sessionResult = await this.importSessionFile(sessionFile, options);
5482
- result.totalSessions++;
5483
- result.totalMessages += sessionResult.totalMessages;
5484
- result.importedPrompts += sessionResult.importedPrompts;
5485
- result.importedResponses += sessionResult.importedResponses;
5486
- result.skippedDuplicates += sessionResult.skippedDuplicates;
5487
- result.errors.push(...sessionResult.errors);
5488
- }
7224
+ onProgress?.({ phase: "session-start", sessionIndex: i, totalSessions: allSessionFiles.length, filePath: sessionFile });
7225
+ const sessionResult = await this.importSessionFile(sessionFile, {
7226
+ ...options,
7227
+ _sessionIndex: i
7228
+ });
7229
+ result.totalSessions++;
7230
+ result.totalMessages += sessionResult.totalMessages;
7231
+ result.importedPrompts += sessionResult.importedPrompts;
7232
+ result.importedResponses += sessionResult.importedResponses;
7233
+ result.skippedDuplicates += sessionResult.skippedDuplicates;
7234
+ result.errors.push(...sessionResult.errors);
7235
+ onProgress?.({
7236
+ phase: "session-done",
7237
+ sessionIndex: i,
7238
+ importedPrompts: sessionResult.importedPrompts,
7239
+ importedResponses: sessionResult.importedResponses,
7240
+ skipped: sessionResult.skippedDuplicates
7241
+ });
5489
7242
  } catch (error) {
5490
- result.errors.push(`Failed to process ${projectDir}: ${error}`);
7243
+ result.errors.push(`Failed to process ${sessionFile}: ${error}`);
5491
7244
  }
5492
7245
  }
5493
7246
  return result;
@@ -5496,14 +7249,14 @@ var SessionHistoryImporter = class {
5496
7249
  * Find project directory from project path
5497
7250
  */
5498
7251
  async findProjectDir(projectPath) {
5499
- const projectsDir = path2.join(this.claudeDir, "projects");
5500
- if (!fs2.existsSync(projectsDir)) {
7252
+ const projectsDir = path4.join(this.claudeDir, "projects");
7253
+ if (!fs5.existsSync(projectsDir)) {
5501
7254
  return null;
5502
7255
  }
5503
- const projectDirs = fs2.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs2.statSync(p).isDirectory());
7256
+ const projectDirs = fs5.readdirSync(projectsDir).map((name) => path4.join(projectsDir, name)).filter((p) => fs5.statSync(p).isDirectory());
5504
7257
  const normalizedPath = projectPath.replace(/\//g, "-").replace(/^-/, "");
5505
7258
  for (const dir of projectDirs) {
5506
- const dirName = path2.basename(dir);
7259
+ const dirName = path4.basename(dir);
5507
7260
  if (dirName.includes(normalizedPath) || normalizedPath.includes(dirName)) {
5508
7261
  return dir;
5509
7262
  }
@@ -5514,10 +7267,10 @@ var SessionHistoryImporter = class {
5514
7267
  * Find all JSONL session files in a directory
5515
7268
  */
5516
7269
  async findSessionFiles(dir) {
5517
- if (!fs2.existsSync(dir)) {
7270
+ if (!fs5.existsSync(dir)) {
5518
7271
  return [];
5519
7272
  }
5520
- return fs2.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path2.join(dir, name)).filter((p) => fs2.statSync(p).isFile());
7273
+ return fs5.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path4.join(dir, name)).filter((p) => fs5.statSync(p).isFile());
5521
7274
  }
5522
7275
  /**
5523
7276
  * Extract text content from Claude message
@@ -5548,17 +7301,17 @@ var SessionHistoryImporter = class {
5548
7301
  projectDirs = [projectDir];
5549
7302
  }
5550
7303
  } else {
5551
- const projectsDir = path2.join(this.claudeDir, "projects");
5552
- if (fs2.existsSync(projectsDir)) {
5553
- projectDirs = fs2.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs2.statSync(p).isDirectory());
7304
+ const projectsDir = path4.join(this.claudeDir, "projects");
7305
+ if (fs5.existsSync(projectsDir)) {
7306
+ projectDirs = fs5.readdirSync(projectsDir).map((name) => path4.join(projectsDir, name)).filter((p) => fs5.statSync(p).isDirectory());
5554
7307
  }
5555
7308
  }
5556
7309
  for (const projectDir of projectDirs) {
5557
7310
  const sessionFiles = await this.findSessionFiles(projectDir);
5558
7311
  for (const filePath of sessionFiles) {
5559
- const stats = fs2.statSync(filePath);
7312
+ const stats = fs5.statSync(filePath);
5560
7313
  sessions.push({
5561
- sessionId: path2.basename(filePath, ".jsonl"),
7314
+ sessionId: path4.basename(filePath, ".jsonl"),
5562
7315
  filePath,
5563
7316
  size: stats.size,
5564
7317
  modifiedAt: stats.mtime
@@ -5573,25 +7326,448 @@ function createSessionHistoryImporter(memoryService) {
5573
7326
  return new SessionHistoryImporter(memoryService);
5574
7327
  }
5575
7328
 
7329
+ // src/services/bootstrap-organizer.ts
7330
+ import * as fs6 from "node:fs";
7331
+ import * as path5 from "node:path";
7332
+ import { execSync } from "node:child_process";
7333
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo", "memory"]);
7334
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
7335
+ ".ts",
7336
+ ".tsx",
7337
+ ".js",
7338
+ ".jsx",
7339
+ ".mjs",
7340
+ ".cjs",
7341
+ ".py",
7342
+ ".go",
7343
+ ".rs",
7344
+ ".java",
7345
+ ".kt",
7346
+ ".swift",
7347
+ ".rb",
7348
+ ".php",
7349
+ ".cs",
7350
+ ".scala",
7351
+ ".sh",
7352
+ ".zsh",
7353
+ ".yaml",
7354
+ ".yml",
7355
+ ".json",
7356
+ ".sql",
7357
+ ".md"
7358
+ ]);
7359
+ function safeRel(base, target) {
7360
+ return path5.relative(base, target).replaceAll("\\", "/");
7361
+ }
7362
+ function mkdirp(dir) {
7363
+ fs6.mkdirSync(dir, { recursive: true });
7364
+ }
7365
+ function walkCodeFiles(root) {
7366
+ const out = [];
7367
+ function walk(dir) {
7368
+ const entries = fs6.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
7369
+ for (const e of entries) {
7370
+ const full = path5.join(dir, e.name);
7371
+ if (e.isDirectory()) {
7372
+ if (!EXCLUDED_DIRS.has(e.name))
7373
+ walk(full);
7374
+ } else if (e.isFile()) {
7375
+ const ext = path5.extname(e.name).toLowerCase();
7376
+ if (CODE_EXTENSIONS.has(ext))
7377
+ out.push(full);
7378
+ }
7379
+ }
7380
+ }
7381
+ walk(root);
7382
+ return out.sort();
7383
+ }
7384
+ function detectLanguage(file) {
7385
+ const ext = path5.extname(file).toLowerCase();
7386
+ const map = {
7387
+ ".ts": "TypeScript",
7388
+ ".tsx": "TypeScript",
7389
+ ".js": "JavaScript",
7390
+ ".jsx": "JavaScript",
7391
+ ".mjs": "JavaScript",
7392
+ ".cjs": "JavaScript",
7393
+ ".py": "Python",
7394
+ ".go": "Go",
7395
+ ".rs": "Rust",
7396
+ ".java": "Java",
7397
+ ".kt": "Kotlin",
7398
+ ".swift": "Swift",
7399
+ ".rb": "Ruby",
7400
+ ".php": "PHP",
7401
+ ".cs": "C#",
7402
+ ".scala": "Scala",
7403
+ ".sh": "Shell",
7404
+ ".zsh": "Shell",
7405
+ ".yaml": "YAML",
7406
+ ".yml": "YAML",
7407
+ ".json": "JSON",
7408
+ ".sql": "SQL",
7409
+ ".md": "Markdown"
7410
+ };
7411
+ return map[ext] || "Other";
7412
+ }
7413
+ function summarizeModules(repoPath, files) {
7414
+ const modules = /* @__PURE__ */ new Map();
7415
+ for (const abs of files) {
7416
+ const rel = safeRel(repoPath, abs);
7417
+ const seg = rel.split("/").filter(Boolean);
7418
+ const top = seg[0] || "root";
7419
+ if (!modules.has(top))
7420
+ modules.set(top, { files: [], langs: /* @__PURE__ */ new Map() });
7421
+ const bucket = modules.get(top);
7422
+ bucket.files.push(rel);
7423
+ const lang = detectLanguage(abs);
7424
+ bucket.langs.set(lang, (bucket.langs.get(lang) || 0) + 1);
7425
+ }
7426
+ return [...modules.entries()].map(([name, data]) => ({
7427
+ name,
7428
+ root: name,
7429
+ fileCount: data.files.length,
7430
+ languages: [...data.langs.entries()].sort((a, b) => b[1] - a[1]).map(([l]) => l).slice(0, 5),
7431
+ entryCandidates: data.files.filter((f) => /(index|main|app|server|cli)\./i.test(path5.basename(f))).slice(0, 10)
7432
+ })).sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
7433
+ }
7434
+ function runGit(repoPath, command) {
7435
+ return execSync(`git -C ${JSON.stringify(repoPath)} ${command}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7436
+ }
7437
+ function getGitCommits(repoPath, since = "180 days ago", maxCommits = 1e3) {
7438
+ try {
7439
+ const raw = runGit(
7440
+ repoPath,
7441
+ `log --since=${JSON.stringify(since)} -n ${Math.max(1, maxCommits)} --date=short --pretty=format:%H%x09%ad%x09%an%x09%s --name-only --reverse`
7442
+ );
7443
+ const lines = raw.split(/\r?\n/);
7444
+ const commits = [];
7445
+ let current = null;
7446
+ for (const line of lines) {
7447
+ if (!line.trim()) {
7448
+ if (current) {
7449
+ commits.push(current);
7450
+ current = null;
7451
+ }
7452
+ continue;
7453
+ }
7454
+ if (line.includes(" ") && line.split(" ").length >= 4) {
7455
+ if (current)
7456
+ commits.push(current);
7457
+ const [hash, date, author, ...subjectRest] = line.split(" ");
7458
+ current = { hash, date, author, subject: subjectRest.join(" ").trim(), files: [] };
7459
+ } else if (current) {
7460
+ current.files.push(line.trim());
7461
+ }
7462
+ }
7463
+ if (current)
7464
+ commits.push(current);
7465
+ return commits;
7466
+ } catch {
7467
+ return [];
7468
+ }
7469
+ }
7470
+ function extractDecisions(commits) {
7471
+ const decisionPattern = /(refactor|migrate|deprecat|remove|replace|introduce|adopt|switch|upgrade|breaking|architecture|feat|fix)/i;
7472
+ return commits.filter((c) => decisionPattern.test(c.subject));
7473
+ }
7474
+ function buildTimeline(commits) {
7475
+ const timeline = /* @__PURE__ */ new Map();
7476
+ for (const c of commits) {
7477
+ const key = (c.date || "").slice(0, 7) || "unknown";
7478
+ if (!timeline.has(key))
7479
+ timeline.set(key, []);
7480
+ timeline.get(key).push(c);
7481
+ }
7482
+ return new Map([...timeline.entries()].sort((a, b) => a[0].localeCompare(b[0])));
7483
+ }
7484
+ function buildGlossary(files) {
7485
+ const stop = /* @__PURE__ */ new Set(["src", "test", "dist", "lib", "core", "index", "main", "app", "server", "client", "utils"]);
7486
+ const freq = /* @__PURE__ */ new Map();
7487
+ for (const f of files) {
7488
+ const base = path5.basename(f, path5.extname(f));
7489
+ const tokens = base.split(/[^a-zA-Z0-9]+/).flatMap((t) => t.split(/(?=[A-Z])/)).map((t) => t.toLowerCase()).filter((t) => t.length >= 3 && !stop.has(t));
7490
+ for (const t of tokens)
7491
+ freq.set(t, (freq.get(t) || 0) + 1);
7492
+ }
7493
+ return [...freq.entries()].filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 80).map(([term]) => term);
7494
+ }
7495
+ function writeFile(filePath, content) {
7496
+ mkdirp(path5.dirname(filePath));
7497
+ fs6.writeFileSync(filePath, content, "utf8");
7498
+ }
7499
+ function confidenceByEvidence(sourceCount) {
7500
+ if (sourceCount >= 3)
7501
+ return "high";
7502
+ if (sourceCount >= 1)
7503
+ return "mid";
7504
+ return "low";
7505
+ }
7506
+ function sourceLine(source) {
7507
+ return `- source: ${source}`;
7508
+ }
7509
+ function loadExistingManifest(outDir) {
7510
+ try {
7511
+ const p = path5.join(outDir, "sources", "manifest.json");
7512
+ if (!fs6.existsSync(p))
7513
+ return null;
7514
+ const data = JSON.parse(fs6.readFileSync(p, "utf8"));
7515
+ return data;
7516
+ } catch {
7517
+ return null;
7518
+ }
7519
+ }
7520
+ function listMarkdownOutputs(outDir) {
7521
+ const out = [];
7522
+ const stack = [outDir];
7523
+ while (stack.length) {
7524
+ const dir = stack.pop();
7525
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
7526
+ const full = path5.join(dir, entry.name);
7527
+ if (entry.isDirectory())
7528
+ stack.push(full);
7529
+ else if (entry.isFile() && entry.name.endsWith(".md"))
7530
+ out.push(full);
7531
+ }
7532
+ }
7533
+ return out.sort((a, b) => a.localeCompare(b));
7534
+ }
7535
+ async function bootstrapKnowledgeBase(options) {
7536
+ const repoPath = path5.resolve(options.repoPath);
7537
+ const outDir = path5.resolve(options.outDir);
7538
+ const maxCommits = options.maxCommits ?? 1e3;
7539
+ const existingManifest = options.incremental ? loadExistingManifest(outDir) : null;
7540
+ const incrementalSince = existingManifest?.lastCommitDate || existingManifest?.generatedAt;
7541
+ const since = options.since || incrementalSince || "180 days ago";
7542
+ const codeFiles = walkCodeFiles(repoPath);
7543
+ const modules = summarizeModules(repoPath, codeFiles);
7544
+ const commits = getGitCommits(repoPath, since, maxCommits);
7545
+ const decisions = extractDecisions(commits);
7546
+ const timeline = buildTimeline(commits);
7547
+ const glossary = buildGlossary(codeFiles);
7548
+ const generatedFiles = [];
7549
+ const sections = {
7550
+ overview: path5.join(outDir, "overview"),
7551
+ modules: path5.join(outDir, "modules"),
7552
+ decisions: path5.join(outDir, "decisions"),
7553
+ timeline: path5.join(outDir, "timeline"),
7554
+ glossary: path5.join(outDir, "glossary"),
7555
+ sources: path5.join(outDir, "sources")
7556
+ };
7557
+ for (const sectionDir of Object.values(sections)) {
7558
+ mkdirp(sectionDir);
7559
+ }
7560
+ const overviewPath = path5.join(sections.overview, "overview.md");
7561
+ const overview = [
7562
+ "# Codebase Overview",
7563
+ "",
7564
+ `- generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}`,
7565
+ "- deterministicPipeline: true",
7566
+ `- repo: ${repoPath}`,
7567
+ `- filesAnalyzed: ${codeFiles.length}`,
7568
+ `- commitsAnalyzed: ${commits.length}`,
7569
+ `- confidence: ${confidenceByEvidence(modules.length > 0 ? 3 : 0)}`,
7570
+ "",
7571
+ "## Directory / Module Map",
7572
+ ...modules.slice(0, 50).map((m) => `- ${m.name}: ${m.fileCount} files (${m.languages.join(", ") || "n/a"})`),
7573
+ "",
7574
+ "## Fact",
7575
+ "- Generated from deterministic file scan and git history parsing.",
7576
+ "",
7577
+ "## Inference",
7578
+ "- Module responsibilities should be reviewed by maintainers for nuanced boundaries.",
7579
+ "",
7580
+ "## Sources",
7581
+ sourceLine(`repo-scan:${repoPath}`),
7582
+ sourceLine(`git-log:since=${since};max=${maxCommits}`),
7583
+ ""
7584
+ ].join("\n");
7585
+ writeFile(overviewPath, overview);
7586
+ generatedFiles.push(overviewPath);
7587
+ const touchedRoots = new Set(
7588
+ commits.flatMap((c) => c.files).map((f) => f.split("/").filter(Boolean)[0]).filter(Boolean)
7589
+ );
7590
+ const moduleTargets = options.incremental && touchedRoots.size > 0 ? modules.filter((m) => touchedRoots.has(m.root)).slice(0, 200) : modules.slice(0, 200);
7591
+ for (const m of moduleTargets) {
7592
+ const relatedCommits = commits.filter((c) => c.files.some((f) => f.startsWith(`${m.root}/`))).slice(0, 15);
7593
+ const content = [
7594
+ `# Module: ${m.name}`,
7595
+ "",
7596
+ `- responsibility: inferred from top-level path \`${m.root}/\``,
7597
+ `- files: ${m.fileCount}`,
7598
+ `- languages: ${m.languages.join(", ") || "n/a"}`,
7599
+ `- confidence: ${confidenceByEvidence(relatedCommits.length)}`,
7600
+ "",
7601
+ "## Entry Candidates",
7602
+ ...m.entryCandidates.length > 0 ? m.entryCandidates.map((f) => `- ${f}`) : ["- none detected"],
7603
+ "",
7604
+ "## Related Commits (recent sample)",
7605
+ ...relatedCommits.length > 0 ? relatedCommits.map((c) => `- ${c.date} ${c.hash.slice(0, 8)} ${c.subject}`) : ["- none in selected range"],
7606
+ "",
7607
+ "## Sources",
7608
+ sourceLine(`repo-path:${m.root}/**`),
7609
+ ...relatedCommits.map((c) => sourceLine(`commit:${c.hash}`)),
7610
+ ""
7611
+ ].join("\n");
7612
+ const modulePath = path5.join(sections.modules, `${m.name.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase()}.md`);
7613
+ writeFile(modulePath, content);
7614
+ generatedFiles.push(modulePath);
7615
+ }
7616
+ const decisionsPath = path5.join(sections.decisions, "decisions.md");
7617
+ const decisionsMd = [
7618
+ "# Decisions (extracted)",
7619
+ "",
7620
+ `- confidence: ${confidenceByEvidence(decisions.length)}`,
7621
+ "",
7622
+ ...decisions.length > 0 ? decisions.slice(0, 500).map((d) => [
7623
+ `## ${d.date} | ${d.subject}`,
7624
+ "- status: active (inferred)",
7625
+ sourceLine(`commit:${d.hash}`),
7626
+ `- author: ${d.author}`,
7627
+ `- changedFiles: ${d.files.length}`,
7628
+ `- confidence: ${confidenceByEvidence(d.files.length > 0 ? 2 : 1)}`,
7629
+ ""
7630
+ ].join("\n")) : ["- No decision-like commits found in selected range.", ""],
7631
+ "## Sources",
7632
+ sourceLine(`git-log:since=${since};max=${maxCommits}`),
7633
+ ""
7634
+ ].join("\n");
7635
+ writeFile(decisionsPath, decisionsMd);
7636
+ generatedFiles.push(decisionsPath);
7637
+ const timelinePath = path5.join(sections.timeline, "timeline.md");
7638
+ const timelineMd = [
7639
+ "# Timeline",
7640
+ "",
7641
+ `- confidence: ${confidenceByEvidence(commits.length > 0 ? 2 : 0)}`,
7642
+ "",
7643
+ ...[...timeline.entries()].flatMap(([month, list]) => [
7644
+ `## ${month}`,
7645
+ ...list.slice(0, 40).map((c) => `- ${c.date} ${c.hash.slice(0, 8)} ${c.subject}`),
7646
+ ""
7647
+ ]),
7648
+ "## Sources",
7649
+ sourceLine(`git-log:since=${since};max=${maxCommits}`),
7650
+ ""
7651
+ ].join("\n");
7652
+ writeFile(timelinePath, timelineMd);
7653
+ generatedFiles.push(timelinePath);
7654
+ const glossaryPath = path5.join(sections.glossary, "glossary.md");
7655
+ const glossaryMd = [
7656
+ "# Glossary (auto-extracted)",
7657
+ "",
7658
+ `- confidence: ${confidenceByEvidence(glossary.length > 0 ? 1 : 0)}`,
7659
+ "",
7660
+ ...glossary.map((t) => `- ${t}`),
7661
+ "",
7662
+ "## Sources",
7663
+ sourceLine(`repo-scan:${repoPath}`),
7664
+ ""
7665
+ ].join("\n");
7666
+ writeFile(glossaryPath, glossaryMd);
7667
+ generatedFiles.push(glossaryPath);
7668
+ const outputs = generatedFiles.map((f) => safeRel(outDir, f)).sort((a, b) => a.localeCompare(b));
7669
+ const allOutputs = listMarkdownOutputs(outDir).map((f) => safeRel(outDir, f));
7670
+ const sourceItems = [
7671
+ ...codeFiles.slice(0, 200).map((f) => ({ type: "file", ref: safeRel(repoPath, f) })),
7672
+ ...commits.slice(0, 400).map((c) => ({ type: "commit", ref: c.hash, date: c.date, subject: c.subject }))
7673
+ ];
7674
+ const latestCommitDate = commits.length > 0 ? commits[commits.length - 1].date : void 0;
7675
+ const manifest = {
7676
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7677
+ deterministicPipeline: true,
7678
+ mode: options.incremental ? "incremental" : "full",
7679
+ repoPath,
7680
+ options: { since, maxCommits, incremental: Boolean(options.incremental) },
7681
+ stats: {
7682
+ filesAnalyzed: codeFiles.length,
7683
+ modules: modules.length,
7684
+ modulesGenerated: moduleTargets.length,
7685
+ commits: commits.length,
7686
+ decisions: decisions.length,
7687
+ glossaryTerms: glossary.length
7688
+ },
7689
+ lastCommitDate: latestCommitDate,
7690
+ outputs,
7691
+ allOutputs,
7692
+ sources: sourceItems
7693
+ };
7694
+ const manifestJsonPath = path5.join(sections.sources, "manifest.json");
7695
+ writeFile(manifestJsonPath, `${JSON.stringify(manifest, null, 2)}
7696
+ `);
7697
+ generatedFiles.push(manifestJsonPath);
7698
+ const manifestMdPath = path5.join(sections.sources, "manifest.md");
7699
+ const manifestMd = [
7700
+ "# Sources Manifest",
7701
+ "",
7702
+ "- deterministicPipeline: true",
7703
+ `- mode: ${options.incremental ? "incremental" : "full"}`,
7704
+ `- sourceCount: ${sourceItems.length}`,
7705
+ "",
7706
+ "## Outputs",
7707
+ ...outputs.map((o) => `- ${o}`),
7708
+ "",
7709
+ "## Sources (sample)",
7710
+ ...sourceItems.slice(0, 300).map((s) => `- ${s.type}:${s.ref}`),
7711
+ ""
7712
+ ].join("\n");
7713
+ writeFile(manifestMdPath, manifestMd);
7714
+ generatedFiles.push(manifestMdPath);
7715
+ return {
7716
+ outDir,
7717
+ fileCount: codeFiles.length,
7718
+ moduleCount: modules.length,
7719
+ commitCount: commits.length,
7720
+ generatedFiles: generatedFiles.sort((a, b) => a.localeCompare(b))
7721
+ };
7722
+ }
7723
+
5576
7724
  // src/server/index.ts
5577
- import { Hono as Hono7 } from "hono";
7725
+ import { Hono as Hono11 } from "hono";
5578
7726
  import { cors } from "hono/cors";
5579
7727
  import { logger } from "hono/logger";
5580
7728
  import { serve } from "@hono/node-server";
5581
7729
  import { serveStatic } from "@hono/node-server/serve-static";
5582
- import * as path3 from "path";
5583
- import * as fs3 from "fs";
7730
+ import * as path8 from "path";
7731
+ import * as fs8 from "fs";
5584
7732
 
5585
7733
  // src/server/api/index.ts
5586
- import { Hono as Hono6 } from "hono";
7734
+ import { Hono as Hono10 } from "hono";
5587
7735
 
5588
7736
  // src/server/api/sessions.ts
5589
7737
  import { Hono } from "hono";
7738
+
7739
+ // src/server/api/utils.ts
7740
+ import * as path6 from "path";
7741
+ import * as os3 from "os";
7742
+ function getServiceFromQuery(c) {
7743
+ const project = c.req.query("project");
7744
+ if (project) {
7745
+ const isHash = /^[a-f0-9]{8}$/.test(project);
7746
+ let storagePath;
7747
+ if (isHash) {
7748
+ storagePath = path6.join(os3.homedir(), ".claude-code", "memory", "projects", project);
7749
+ } else {
7750
+ const crypto3 = __require("crypto");
7751
+ const normalized = project.replace(/\/+$/, "") || "/";
7752
+ const hash = crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
7753
+ storagePath = path6.join(os3.homedir(), ".claude-code", "memory", "projects", hash);
7754
+ }
7755
+ return new MemoryService({
7756
+ storagePath,
7757
+ readOnly: true,
7758
+ analyticsEnabled: false,
7759
+ sharedStoreConfig: { enabled: false }
7760
+ });
7761
+ }
7762
+ return getReadOnlyMemoryService();
7763
+ }
7764
+
7765
+ // src/server/api/sessions.ts
5590
7766
  var sessionsRouter = new Hono();
5591
7767
  sessionsRouter.get("/", async (c) => {
5592
7768
  const page = parseInt(c.req.query("page") || "1", 10);
5593
7769
  const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
5594
- const memoryService = getReadOnlyMemoryService();
7770
+ const memoryService = getServiceFromQuery(c);
5595
7771
  try {
5596
7772
  await memoryService.initialize();
5597
7773
  const recentEvents = await memoryService.getRecentEvents(1e3);
@@ -5635,7 +7811,7 @@ sessionsRouter.get("/", async (c) => {
5635
7811
  });
5636
7812
  sessionsRouter.get("/:id", async (c) => {
5637
7813
  const { id } = c.req.param();
5638
- const memoryService = getReadOnlyMemoryService();
7814
+ const memoryService = getServiceFromQuery(c);
5639
7815
  try {
5640
7816
  await memoryService.initialize();
5641
7817
  const events = await memoryService.getSessionHistory(id);
@@ -5676,18 +7852,36 @@ var eventsRouter = new Hono2();
5676
7852
  eventsRouter.get("/", async (c) => {
5677
7853
  const sessionId = c.req.query("sessionId");
5678
7854
  const eventType = c.req.query("type");
7855
+ const level = c.req.query("level");
7856
+ const sort = c.req.query("sort") || "recent";
5679
7857
  const limit = parseInt(c.req.query("limit") || "100", 10);
5680
7858
  const offset = parseInt(c.req.query("offset") || "0", 10);
5681
- const memoryService = getReadOnlyMemoryService();
7859
+ const memoryService = getServiceFromQuery(c);
5682
7860
  try {
5683
7861
  await memoryService.initialize();
5684
- let events = await memoryService.getRecentEvents(limit + offset + 1e3);
7862
+ let events;
7863
+ if (level) {
7864
+ events = await memoryService.getEventsByLevel(level, { limit: limit + offset + 1e3, offset: 0 });
7865
+ } else {
7866
+ events = await memoryService.getRecentEvents(limit + offset + 1e3);
7867
+ }
5685
7868
  if (sessionId) {
5686
7869
  events = events.filter((e) => e.sessionId === sessionId);
5687
7870
  }
5688
7871
  if (eventType) {
5689
7872
  events = events.filter((e) => e.eventType === eventType);
5690
7873
  }
7874
+ if (sort === "accessed") {
7875
+ events.sort((a, b) => {
7876
+ const aTime = a.last_accessed_at || "";
7877
+ const bTime = b.last_accessed_at || "";
7878
+ return bTime.localeCompare(aTime);
7879
+ });
7880
+ } else if (sort === "most-accessed") {
7881
+ events.sort((a, b) => (b.access_count || 0) - (a.access_count || 0));
7882
+ } else if (sort === "oldest") {
7883
+ events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
7884
+ }
5691
7885
  const total = events.length;
5692
7886
  events = events.slice(offset, offset + limit);
5693
7887
  return c.json({
@@ -5697,7 +7891,9 @@ eventsRouter.get("/", async (c) => {
5697
7891
  timestamp: e.timestamp,
5698
7892
  sessionId: e.sessionId,
5699
7893
  preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
5700
- contentLength: e.content.length
7894
+ contentLength: e.content.length,
7895
+ accessCount: e.access_count || 0,
7896
+ lastAccessedAt: e.last_accessed_at || null
5701
7897
  })),
5702
7898
  total,
5703
7899
  limit,
@@ -5712,7 +7908,7 @@ eventsRouter.get("/", async (c) => {
5712
7908
  });
5713
7909
  eventsRouter.get("/:id", async (c) => {
5714
7910
  const { id } = c.req.param();
5715
- const memoryService = getReadOnlyMemoryService();
7911
+ const memoryService = getServiceFromQuery(c);
5716
7912
  try {
5717
7913
  await memoryService.initialize();
5718
7914
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5752,7 +7948,7 @@ eventsRouter.get("/:id", async (c) => {
5752
7948
  import { Hono as Hono3 } from "hono";
5753
7949
  var searchRouter = new Hono3();
5754
7950
  searchRouter.post("/", async (c) => {
5755
- const memoryService = getReadOnlyMemoryService();
7951
+ const memoryService = getServiceFromQuery(c);
5756
7952
  try {
5757
7953
  const body = await c.req.json();
5758
7954
  if (!body.query) {
@@ -5796,7 +7992,7 @@ searchRouter.get("/", async (c) => {
5796
7992
  return c.json({ error: 'Query parameter "q" is required' }, 400);
5797
7993
  }
5798
7994
  const topK = parseInt(c.req.query("topK") || "5", 10);
5799
- const memoryService = getReadOnlyMemoryService();
7995
+ const memoryService = getServiceFromQuery(c);
5800
7996
  try {
5801
7997
  await memoryService.initialize();
5802
7998
  const result = await memoryService.retrieveMemories(query, { topK });
@@ -5824,7 +8020,7 @@ searchRouter.get("/", async (c) => {
5824
8020
  import { Hono as Hono4 } from "hono";
5825
8021
  var statsRouter = new Hono4();
5826
8022
  statsRouter.get("/shared", async (c) => {
5827
- const memoryService = getReadOnlyMemoryService();
8023
+ const memoryService = getServiceFromQuery(c);
5828
8024
  try {
5829
8025
  await memoryService.initialize();
5830
8026
  const sharedStats = await memoryService.getSharedStoreStats();
@@ -5881,7 +8077,7 @@ statsRouter.get("/levels/:level", async (c) => {
5881
8077
  if (!validLevels.includes(level)) {
5882
8078
  return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
5883
8079
  }
5884
- const memoryService = getReadOnlyMemoryService();
8080
+ const memoryService = getServiceFromQuery(c);
5885
8081
  try {
5886
8082
  await memoryService.initialize();
5887
8083
  let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
@@ -5928,7 +8124,7 @@ statsRouter.get("/levels/:level", async (c) => {
5928
8124
  }
5929
8125
  });
5930
8126
  statsRouter.get("/", async (c) => {
5931
- const memoryService = getReadOnlyMemoryService();
8127
+ const memoryService = getServiceFromQuery(c);
5932
8128
  try {
5933
8129
  await memoryService.initialize();
5934
8130
  const stats = await memoryService.getStats();
@@ -5945,6 +8141,7 @@ statsRouter.get("/", async (c) => {
5945
8141
  acc[day] = (acc[day] || 0) + 1;
5946
8142
  return acc;
5947
8143
  }, {});
8144
+ const retrievalTrace = await memoryService.getRetrievalTraceStats();
5948
8145
  return c.json({
5949
8146
  storage: {
5950
8147
  eventCount: stats.totalEvents,
@@ -5962,7 +8159,8 @@ statsRouter.get("/", async (c) => {
5962
8159
  heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
5963
8160
  heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
5964
8161
  },
5965
- levelStats: stats.levelStats
8162
+ levelStats: stats.levelStats,
8163
+ retrievalTrace
5966
8164
  });
5967
8165
  } catch (error) {
5968
8166
  return c.json({ error: error.message }, 500);
@@ -5972,7 +8170,7 @@ statsRouter.get("/", async (c) => {
5972
8170
  });
5973
8171
  statsRouter.get("/most-accessed", async (c) => {
5974
8172
  const limit = parseInt(c.req.query("limit") || "10", 10);
5975
- const memoryService = getReadOnlyMemoryService();
8173
+ const memoryService = getServiceFromQuery(c);
5976
8174
  try {
5977
8175
  await memoryService.initialize();
5978
8176
  console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
@@ -6003,7 +8201,7 @@ statsRouter.get("/most-accessed", async (c) => {
6003
8201
  });
6004
8202
  statsRouter.get("/timeline", async (c) => {
6005
8203
  const days = parseInt(c.req.query("days") || "7", 10);
6006
- const memoryService = getReadOnlyMemoryService();
8204
+ const memoryService = getServiceFromQuery(c);
6007
8205
  try {
6008
8206
  await memoryService.initialize();
6009
8207
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -6033,8 +8231,75 @@ statsRouter.get("/timeline", async (c) => {
6033
8231
  await memoryService.shutdown();
6034
8232
  }
6035
8233
  });
8234
+ statsRouter.get("/helpfulness", async (c) => {
8235
+ const limit = parseInt(c.req.query("limit") || "10", 10);
8236
+ const memoryService = getServiceFromQuery(c);
8237
+ try {
8238
+ await memoryService.initialize();
8239
+ const stats = await memoryService.getHelpfulnessStats();
8240
+ const topMemories = await memoryService.getHelpfulMemories(limit);
8241
+ return c.json({
8242
+ ...stats,
8243
+ topMemories: topMemories.map((m) => ({
8244
+ eventId: m.eventId,
8245
+ summary: m.summary,
8246
+ helpfulnessScore: m.helpfulnessScore,
8247
+ accessCount: m.accessCount,
8248
+ evaluationCount: m.evaluationCount
8249
+ }))
8250
+ });
8251
+ } catch (error) {
8252
+ return c.json({
8253
+ avgScore: 0,
8254
+ totalEvaluated: 0,
8255
+ totalRetrievals: 0,
8256
+ helpful: 0,
8257
+ neutral: 0,
8258
+ unhelpful: 0,
8259
+ topMemories: []
8260
+ });
8261
+ } finally {
8262
+ await memoryService.shutdown();
8263
+ }
8264
+ });
8265
+ statsRouter.get("/retrieval-traces", async (c) => {
8266
+ const limit = parseInt(c.req.query("limit") || "50", 10);
8267
+ const memoryService = getServiceFromQuery(c);
8268
+ try {
8269
+ await memoryService.initialize();
8270
+ const traces = await memoryService.getRecentRetrievalTraces(limit);
8271
+ const traceStats = await memoryService.getRetrievalTraceStats();
8272
+ return c.json({
8273
+ stats: traceStats,
8274
+ traces: traces.map((t) => ({
8275
+ traceId: t.traceId,
8276
+ sessionId: t.sessionId || null,
8277
+ projectHash: t.projectHash || null,
8278
+ queryText: t.queryText,
8279
+ strategy: t.strategy || null,
8280
+ candidateEventIds: t.candidateEventIds,
8281
+ selectedEventIds: t.selectedEventIds,
8282
+ candidateDetails: t.candidateDetails || [],
8283
+ selectedDetails: t.selectedDetails || [],
8284
+ candidateCount: t.candidateCount,
8285
+ selectedCount: t.selectedCount,
8286
+ confidence: t.confidence || null,
8287
+ fallbackTrace: t.fallbackTrace,
8288
+ createdAt: t.createdAt.toISOString()
8289
+ }))
8290
+ });
8291
+ } catch (error) {
8292
+ return c.json({
8293
+ stats: { totalQueries: 0, avgCandidateCount: 0, avgSelectedCount: 0, selectionRate: 0 },
8294
+ traces: [],
8295
+ error: error.message
8296
+ }, 500);
8297
+ } finally {
8298
+ await memoryService.shutdown();
8299
+ }
8300
+ });
6036
8301
  statsRouter.post("/graduation/run", async (c) => {
6037
- const memoryService = getReadOnlyMemoryService();
8302
+ const memoryService = getServiceFromQuery(c);
6038
8303
  try {
6039
8304
  await memoryService.initialize();
6040
8305
  const result = await memoryService.forceGraduation();
@@ -6095,7 +8360,7 @@ var citationsRouter = new Hono5();
6095
8360
  citationsRouter.get("/:id", async (c) => {
6096
8361
  const { id } = c.req.param();
6097
8362
  const citationId = parseCitationId(id) || id;
6098
- const memoryService = getReadOnlyMemoryService();
8363
+ const memoryService = getServiceFromQuery(c);
6099
8364
  try {
6100
8365
  await memoryService.initialize();
6101
8366
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -6129,7 +8394,7 @@ citationsRouter.get("/:id", async (c) => {
6129
8394
  citationsRouter.get("/:id/related", async (c) => {
6130
8395
  const { id } = c.req.param();
6131
8396
  const citationId = parseCitationId(id) || id;
6132
- const memoryService = getReadOnlyMemoryService();
8397
+ const memoryService = getServiceFromQuery(c);
6133
8398
  try {
6134
8399
  await memoryService.initialize();
6135
8400
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -6165,23 +8430,415 @@ citationsRouter.get("/:id/related", async (c) => {
6165
8430
  }
6166
8431
  });
6167
8432
 
8433
+ // src/server/api/turns.ts
8434
+ import { Hono as Hono6 } from "hono";
8435
+ var turnsRouter = new Hono6();
8436
+ turnsRouter.get("/", async (c) => {
8437
+ const sessionId = c.req.query("sessionId");
8438
+ const limit = parseInt(c.req.query("limit") || "20", 10);
8439
+ const offset = parseInt(c.req.query("offset") || "0", 10);
8440
+ if (!sessionId) {
8441
+ return c.json({ error: "sessionId is required" }, 400);
8442
+ }
8443
+ const memoryService = getServiceFromQuery(c);
8444
+ try {
8445
+ await memoryService.initialize();
8446
+ const turns = await memoryService.getSessionTurns(sessionId, { limit, offset });
8447
+ const totalTurns = await memoryService.countSessionTurns(sessionId);
8448
+ return c.json({
8449
+ turns: turns.map((t) => ({
8450
+ turnId: t.turnId,
8451
+ startedAt: t.startedAt.toISOString(),
8452
+ promptPreview: t.promptPreview,
8453
+ eventCount: t.eventCount,
8454
+ toolCount: t.toolCount,
8455
+ hasResponse: t.hasResponse,
8456
+ events: t.events.map((e) => ({
8457
+ id: e.id,
8458
+ eventType: e.eventType,
8459
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
8460
+ preview: e.content.slice(0, 300) + (e.content.length > 300 ? "..." : ""),
8461
+ contentLength: e.content.length
8462
+ }))
8463
+ })),
8464
+ total: totalTurns,
8465
+ limit,
8466
+ offset,
8467
+ hasMore: offset + limit < totalTurns
8468
+ });
8469
+ } catch (error) {
8470
+ return c.json({ error: error.message }, 500);
8471
+ } finally {
8472
+ await memoryService.shutdown();
8473
+ }
8474
+ });
8475
+ turnsRouter.get("/:turnId", async (c) => {
8476
+ const { turnId } = c.req.param();
8477
+ const memoryService = getServiceFromQuery(c);
8478
+ try {
8479
+ await memoryService.initialize();
8480
+ const events = await memoryService.getEventsByTurn(turnId);
8481
+ if (events.length === 0) {
8482
+ return c.json({ error: "Turn not found" }, 404);
8483
+ }
8484
+ const promptEvent = events.find((e) => e.eventType === "user_prompt");
8485
+ const toolEvents = events.filter((e) => e.eventType === "tool_observation");
8486
+ const responseEvents = events.filter((e) => e.eventType === "agent_response");
8487
+ return c.json({
8488
+ turnId,
8489
+ sessionId: events[0].sessionId,
8490
+ startedAt: events[0].timestamp instanceof Date ? events[0].timestamp.toISOString() : events[0].timestamp,
8491
+ prompt: promptEvent ? {
8492
+ id: promptEvent.id,
8493
+ content: promptEvent.content,
8494
+ timestamp: promptEvent.timestamp instanceof Date ? promptEvent.timestamp.toISOString() : promptEvent.timestamp
8495
+ } : null,
8496
+ tools: toolEvents.map((e) => {
8497
+ let toolName = "";
8498
+ let success = true;
8499
+ try {
8500
+ const parsed = JSON.parse(e.content);
8501
+ toolName = parsed.toolName || "";
8502
+ success = parsed.success !== false;
8503
+ } catch {
8504
+ }
8505
+ return {
8506
+ id: e.id,
8507
+ toolName,
8508
+ success,
8509
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
8510
+ preview: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : "")
8511
+ };
8512
+ }),
8513
+ responses: responseEvents.map((e) => ({
8514
+ id: e.id,
8515
+ content: e.content,
8516
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp
8517
+ })),
8518
+ totalEvents: events.length
8519
+ });
8520
+ } catch (error) {
8521
+ return c.json({ error: error.message }, 500);
8522
+ } finally {
8523
+ await memoryService.shutdown();
8524
+ }
8525
+ });
8526
+ turnsRouter.post("/backfill", async (c) => {
8527
+ const memoryService = getServiceFromQuery(c);
8528
+ try {
8529
+ await memoryService.initialize();
8530
+ const updated = await memoryService.backfillTurnIds();
8531
+ return c.json({
8532
+ success: true,
8533
+ updated,
8534
+ message: `Backfilled turn_id for ${updated} events`
8535
+ });
8536
+ } catch (error) {
8537
+ return c.json({
8538
+ success: false,
8539
+ error: error.message
8540
+ }, 500);
8541
+ } finally {
8542
+ await memoryService.shutdown();
8543
+ }
8544
+ });
8545
+
8546
+ // src/server/api/projects.ts
8547
+ import { Hono as Hono7 } from "hono";
8548
+ import * as fs7 from "fs";
8549
+ import * as path7 from "path";
8550
+ import * as os4 from "os";
8551
+ var projectsRouter = new Hono7();
8552
+ projectsRouter.get("/", async (c) => {
8553
+ try {
8554
+ const projectsDir = path7.join(os4.homedir(), ".claude-code", "memory", "projects");
8555
+ if (!fs7.existsSync(projectsDir)) {
8556
+ return c.json({ projects: [] });
8557
+ }
8558
+ const projectHashes = fs7.readdirSync(projectsDir).filter((name) => {
8559
+ const fullPath = path7.join(projectsDir, name);
8560
+ return fs7.statSync(fullPath).isDirectory();
8561
+ });
8562
+ const registry = loadSessionRegistry();
8563
+ const hashToPath = /* @__PURE__ */ new Map();
8564
+ for (const entry of Object.values(registry.sessions)) {
8565
+ if (!hashToPath.has(entry.projectHash)) {
8566
+ hashToPath.set(entry.projectHash, entry.projectPath);
8567
+ }
8568
+ }
8569
+ const projects = projectHashes.map((hash) => {
8570
+ const dirPath = path7.join(projectsDir, hash);
8571
+ const dbPath = path7.join(dirPath, "events.sqlite");
8572
+ let dbSize = 0;
8573
+ if (fs7.existsSync(dbPath)) {
8574
+ dbSize = fs7.statSync(dbPath).size;
8575
+ }
8576
+ const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
8577
+ return {
8578
+ hash,
8579
+ projectPath,
8580
+ projectName: path7.basename(projectPath),
8581
+ dbSize,
8582
+ dbSizeHuman: formatBytes(dbSize)
8583
+ };
8584
+ });
8585
+ projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
8586
+ return c.json({ projects });
8587
+ } catch (error) {
8588
+ return c.json({ projects: [], error: error.message }, 500);
8589
+ }
8590
+ });
8591
+ function formatBytes(bytes) {
8592
+ if (bytes === 0)
8593
+ return "0 B";
8594
+ const k = 1024;
8595
+ const sizes = ["B", "KB", "MB", "GB"];
8596
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
8597
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
8598
+ }
8599
+
8600
+ // src/server/api/chat.ts
8601
+ import { Hono as Hono8 } from "hono";
8602
+ import { streamSSE } from "hono/streaming";
8603
+ import { spawn } from "child_process";
8604
+ var chatRouter = new Hono8();
8605
+ var CLAUDE_TIMEOUT_MS = 12e4;
8606
+ chatRouter.post("/", async (c) => {
8607
+ let body;
8608
+ try {
8609
+ body = await c.req.json();
8610
+ } catch {
8611
+ return c.json({ error: "Invalid JSON body" }, 400);
8612
+ }
8613
+ if (!body.message?.trim()) {
8614
+ return c.json({ error: "Message is required" }, 400);
8615
+ }
8616
+ const memoryService = getServiceFromQuery(c);
8617
+ try {
8618
+ await memoryService.initialize();
8619
+ let memoryContext = "";
8620
+ let statsContext = "";
8621
+ try {
8622
+ const result = await memoryService.retrieveMemories(body.message, {
8623
+ topK: 8,
8624
+ minScore: 0.5
8625
+ });
8626
+ if (result.memories.length > 0) {
8627
+ const parts = ["## Relevant Memories\n"];
8628
+ for (const m of result.memories) {
8629
+ const date = new Date(m.event.timestamp).toISOString().split("T")[0];
8630
+ const content = m.event.content.slice(0, 500);
8631
+ parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
8632
+ parts.push(content);
8633
+ if (m.sessionContext) {
8634
+ parts.push(`_Context: ${m.sessionContext}_`);
8635
+ }
8636
+ parts.push("");
8637
+ }
8638
+ memoryContext = parts.join("\n");
8639
+ }
8640
+ } catch {
8641
+ }
8642
+ try {
8643
+ const stats = await memoryService.getStats();
8644
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
8645
+ statsContext = [
8646
+ "## Memory Stats",
8647
+ `- Total events: ${stats.totalEvents}`,
8648
+ `- Vector nodes: ${stats.vectorCount}`,
8649
+ `- By level: ${levels}`
8650
+ ].join("\n");
8651
+ } catch {
8652
+ }
8653
+ const fullPrompt = buildPrompt(
8654
+ statsContext,
8655
+ memoryContext,
8656
+ body.history || [],
8657
+ body.message
8658
+ );
8659
+ return streamSSE(c, async (stream) => {
8660
+ try {
8661
+ await streamClaudeResponse(fullPrompt, stream);
8662
+ } catch (err) {
8663
+ await stream.writeSSE({
8664
+ event: "error",
8665
+ data: JSON.stringify({ error: err.message })
8666
+ });
8667
+ }
8668
+ });
8669
+ } catch (error) {
8670
+ return c.json({ error: error.message }, 500);
8671
+ } finally {
8672
+ await memoryService.shutdown();
8673
+ }
8674
+ });
8675
+ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
8676
+ const parts = [];
8677
+ parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
8678
+ parts.push("The memory system tracks coding sessions, tool usage, prompts, and responses.");
8679
+ parts.push("Answer concisely based on the memory context below. If you don't have enough data, say so.");
8680
+ parts.push("Use markdown formatting in your responses.\n");
8681
+ if (statsContext) {
8682
+ parts.push(statsContext);
8683
+ parts.push("");
8684
+ }
8685
+ if (memoryContext) {
8686
+ parts.push(memoryContext);
8687
+ } else {
8688
+ parts.push("No directly relevant memories found for this query.");
8689
+ parts.push("Answer based on general knowledge or suggest the user rephrase.\n");
8690
+ }
8691
+ parts.push("---\n");
8692
+ const recentHistory = history.slice(-10);
8693
+ if (recentHistory.length > 0) {
8694
+ parts.push("## Conversation History\n");
8695
+ for (const msg of recentHistory) {
8696
+ const prefix = msg.role === "user" ? "User" : "Assistant";
8697
+ parts.push(`**${prefix}:** ${msg.content}
8698
+ `);
8699
+ }
8700
+ }
8701
+ parts.push(`**User:** ${currentMessage}`);
8702
+ return parts.join("\n");
8703
+ }
8704
+ function streamClaudeResponse(prompt, stream) {
8705
+ return new Promise((resolve4, reject) => {
8706
+ const proc = spawn("claude", [
8707
+ "-p",
8708
+ "--output-format",
8709
+ "stream-json",
8710
+ "--verbose"
8711
+ ], {
8712
+ stdio: ["pipe", "pipe", "pipe"],
8713
+ env: { ...process.env }
8714
+ });
8715
+ const timeout = setTimeout(() => {
8716
+ proc.kill("SIGTERM");
8717
+ reject(new Error("Chat response timed out after 2 minutes"));
8718
+ }, CLAUDE_TIMEOUT_MS);
8719
+ proc.stdin.write(prompt);
8720
+ proc.stdin.end();
8721
+ let buffer = "";
8722
+ let lastSentText = "";
8723
+ proc.stdout.on("data", async (chunk) => {
8724
+ buffer += chunk.toString();
8725
+ const lines = buffer.split("\n");
8726
+ buffer = lines.pop() || "";
8727
+ for (const line of lines) {
8728
+ if (!line.trim())
8729
+ continue;
8730
+ try {
8731
+ const parsed = JSON.parse(line);
8732
+ if (parsed.type === "assistant" && parsed.message?.content) {
8733
+ const textBlocks = parsed.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
8734
+ if (textBlocks.length > lastSentText.length) {
8735
+ const delta = textBlocks.slice(lastSentText.length);
8736
+ lastSentText = textBlocks;
8737
+ await stream.writeSSE({
8738
+ event: "message",
8739
+ data: JSON.stringify({ content: delta })
8740
+ });
8741
+ }
8742
+ }
8743
+ if (parsed.type === "result") {
8744
+ await stream.writeSSE({ event: "done", data: "{}" });
8745
+ }
8746
+ } catch {
8747
+ }
8748
+ }
8749
+ });
8750
+ proc.stderr.on("data", (chunk) => {
8751
+ if (process.env.CLAUDE_MEMORY_DEBUG) {
8752
+ console.error("[chat] claude stderr:", chunk.toString());
8753
+ }
8754
+ });
8755
+ proc.on("error", (err) => {
8756
+ clearTimeout(timeout);
8757
+ if (err.code === "ENOENT") {
8758
+ reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
8759
+ } else {
8760
+ reject(err);
8761
+ }
8762
+ });
8763
+ proc.on("close", async (code) => {
8764
+ clearTimeout(timeout);
8765
+ if (buffer.trim()) {
8766
+ try {
8767
+ const parsed = JSON.parse(buffer);
8768
+ if (parsed.type === "result") {
8769
+ await stream.writeSSE({ event: "done", data: "{}" });
8770
+ }
8771
+ } catch {
8772
+ }
8773
+ }
8774
+ if (code !== 0 && code !== null) {
8775
+ reject(new Error(`Claude CLI exited with code ${code}`));
8776
+ } else {
8777
+ resolve4();
8778
+ }
8779
+ });
8780
+ });
8781
+ }
8782
+
8783
+ // src/server/api/health.ts
8784
+ import { Hono as Hono9 } from "hono";
8785
+ var healthRouter = new Hono9();
8786
+ healthRouter.get("/", async (c) => {
8787
+ const memoryService = getServiceFromQuery(c);
8788
+ try {
8789
+ await memoryService.initialize();
8790
+ const [stats, outbox] = await Promise.all([
8791
+ memoryService.getStats(),
8792
+ memoryService.getOutboxStats()
8793
+ ]);
8794
+ const outboxPending = outbox.embedding.pending + outbox.vector.pending;
8795
+ const outboxFailed = outbox.embedding.failed + outbox.vector.failed;
8796
+ const status = outboxFailed > 0 ? "needs-attention" : "ok";
8797
+ return c.json({
8798
+ status,
8799
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8800
+ storage: {
8801
+ totalEvents: stats.totalEvents,
8802
+ vectorCount: stats.vectorCount
8803
+ },
8804
+ outbox: {
8805
+ embedding: outbox.embedding,
8806
+ vector: outbox.vector,
8807
+ totals: {
8808
+ pending: outboxPending,
8809
+ failed: outboxFailed
8810
+ }
8811
+ },
8812
+ levelStats: stats.levelStats
8813
+ });
8814
+ } catch (error) {
8815
+ return c.json({
8816
+ status: "error",
8817
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8818
+ error: error.message
8819
+ }, 500);
8820
+ } finally {
8821
+ await memoryService.shutdown();
8822
+ }
8823
+ });
8824
+
6168
8825
  // src/server/api/index.ts
6169
- var apiRouter = new Hono6().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter);
8826
+ var apiRouter = new Hono10().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter).route("/health", healthRouter);
6170
8827
 
6171
8828
  // src/server/index.ts
6172
- var app = new Hono7();
8829
+ var app = new Hono11();
6173
8830
  app.use("/*", cors());
6174
8831
  app.use("/*", logger());
6175
8832
  app.route("/api", apiRouter);
6176
8833
  app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
6177
- var uiPath = path3.join(__dirname, "../../dist/ui");
6178
- if (fs3.existsSync(uiPath)) {
8834
+ var uiPath = path8.join(__dirname, "../../dist/ui");
8835
+ if (fs8.existsSync(uiPath)) {
6179
8836
  app.use("/*", serveStatic({ root: uiPath }));
6180
8837
  }
6181
8838
  app.get("*", (c) => {
6182
- const indexPath = path3.join(uiPath, "index.html");
6183
- if (fs3.existsSync(indexPath)) {
6184
- return c.html(fs3.readFileSync(indexPath, "utf-8"));
8839
+ const indexPath = path8.join(uiPath, "index.html");
8840
+ if (fs8.existsSync(indexPath)) {
8841
+ return c.html(fs8.readFileSync(indexPath, "utf-8"));
6185
8842
  }
6186
8843
  return c.text('UI not built. Run "npm run build:ui" first.', 404);
6187
8844
  });
@@ -6218,29 +8875,303 @@ if (isMainModule) {
6218
8875
  startServer(port);
6219
8876
  }
6220
8877
 
8878
+ // src/core/mongo-sync-worker.ts
8879
+ import { randomUUID as randomUUID10 } from "crypto";
8880
+ import * as os5 from "os";
8881
+ import { MongoClient } from "mongodb";
8882
+ function redactMongoUri(uri) {
8883
+ const schemeIdx = uri.indexOf("://");
8884
+ if (schemeIdx === -1)
8885
+ return uri;
8886
+ const atIdx = uri.indexOf("@", schemeIdx + 3);
8887
+ if (atIdx === -1)
8888
+ return uri;
8889
+ const creds = uri.slice(schemeIdx + 3, atIdx);
8890
+ const colonIdx = creds.indexOf(":");
8891
+ if (colonIdx === -1)
8892
+ return uri;
8893
+ const prefix = uri.slice(0, schemeIdx + 3 + colonIdx + 1);
8894
+ const suffix = uri.slice(atIdx);
8895
+ return `${prefix}***${suffix}`;
8896
+ }
8897
+ function parseIntOrZero(value) {
8898
+ if (!value)
8899
+ return 0;
8900
+ const n = parseInt(value, 10);
8901
+ return Number.isFinite(n) ? n : 0;
8902
+ }
8903
+ var MongoSyncWorker = class {
8904
+ constructor(sqliteStore, config) {
8905
+ this.sqliteStore = sqliteStore;
8906
+ this.config = {
8907
+ uri: config.uri,
8908
+ dbName: config.dbName,
8909
+ projectKey: config.projectKey,
8910
+ direction: config.direction ?? "both",
8911
+ intervalMs: config.intervalMs ?? 3e4,
8912
+ batchSize: config.batchSize ?? 500,
8913
+ instanceId: config.instanceId ?? randomUUID10()
8914
+ };
8915
+ }
8916
+ config;
8917
+ intervalHandle = null;
8918
+ running = false;
8919
+ client = null;
8920
+ db = null;
8921
+ counters = null;
8922
+ events = null;
8923
+ indexesEnsured = false;
8924
+ stats = {
8925
+ lastSyncAt: null,
8926
+ pushedEvents: 0,
8927
+ pulledEvents: 0,
8928
+ errors: 0,
8929
+ status: "idle"
8930
+ };
8931
+ start() {
8932
+ if (this.running)
8933
+ return;
8934
+ this.running = true;
8935
+ this.stats.status = "idle";
8936
+ this.syncNow().catch((err) => {
8937
+ console.error("[MongoSyncWorker] Initial sync failed:", err);
8938
+ });
8939
+ this.intervalHandle = setInterval(() => {
8940
+ this.syncNow().catch((err) => {
8941
+ console.error("[MongoSyncWorker] Periodic sync failed:", err);
8942
+ });
8943
+ }, this.config.intervalMs);
8944
+ }
8945
+ stop() {
8946
+ this.running = false;
8947
+ this.stats.status = "stopped";
8948
+ if (this.intervalHandle) {
8949
+ clearInterval(this.intervalHandle);
8950
+ this.intervalHandle = null;
8951
+ }
8952
+ }
8953
+ async shutdown() {
8954
+ this.stop();
8955
+ await this.disconnect();
8956
+ }
8957
+ getStats() {
8958
+ return { ...this.stats };
8959
+ }
8960
+ isRunning() {
8961
+ return this.running;
8962
+ }
8963
+ async syncNow() {
8964
+ if (this.stats.status === "syncing")
8965
+ return { pushed: 0, pulled: 0 };
8966
+ this.stats.status = "syncing";
8967
+ let pushed = 0;
8968
+ let pulled = 0;
8969
+ try {
8970
+ await this.sqliteStore.initialize();
8971
+ await this.ensureConnected();
8972
+ await this.ensureIndexes();
8973
+ if (this.config.direction === "push" || this.config.direction === "both") {
8974
+ pushed = await this.pushEvents();
8975
+ this.stats.pushedEvents += pushed;
8976
+ }
8977
+ if (this.config.direction === "pull" || this.config.direction === "both") {
8978
+ pulled = await this.pullEvents();
8979
+ this.stats.pulledEvents += pulled;
8980
+ }
8981
+ this.stats.lastSyncAt = /* @__PURE__ */ new Date();
8982
+ this.stats.status = "idle";
8983
+ return { pushed, pulled };
8984
+ } catch (error) {
8985
+ this.stats.errors++;
8986
+ this.stats.status = "error";
8987
+ throw error;
8988
+ }
8989
+ }
8990
+ async ensureConnected() {
8991
+ if (this.client && this.db && this.counters && this.events)
8992
+ return;
8993
+ try {
8994
+ this.client = new MongoClient(this.config.uri, {
8995
+ appName: "claude-memory-layer",
8996
+ serverSelectionTimeoutMS: 5e3
8997
+ });
8998
+ await this.client.connect();
8999
+ this.db = this.client.db(this.config.dbName);
9000
+ this.counters = this.db.collection("cml_counters");
9001
+ this.events = this.db.collection("cml_events");
9002
+ } catch (err) {
9003
+ const safeUri = redactMongoUri(this.config.uri);
9004
+ throw new Error(`MongoDB connection failed (${safeUri}, db=${this.config.dbName}): ${String(err)}`);
9005
+ }
9006
+ }
9007
+ async disconnect() {
9008
+ try {
9009
+ await this.client?.close();
9010
+ } finally {
9011
+ this.client = null;
9012
+ this.db = null;
9013
+ this.counters = null;
9014
+ this.events = null;
9015
+ this.indexesEnsured = false;
9016
+ }
9017
+ }
9018
+ async ensureIndexes() {
9019
+ if (this.indexesEnsured)
9020
+ return;
9021
+ if (!this.events || !this.counters)
9022
+ throw new Error("Mongo not connected");
9023
+ try {
9024
+ await this.events.createIndex({ projectKey: 1, seq: 1 }, { unique: true });
9025
+ await this.events.createIndex({ projectKey: 1, eventId: 1 }, { unique: true });
9026
+ await this.events.createIndex({ projectKey: 1, dedupeKey: 1 });
9027
+ } catch (err) {
9028
+ console.warn("[MongoSyncWorker] Failed to ensure indexes (continuing):", err);
9029
+ }
9030
+ this.indexesEnsured = true;
9031
+ }
9032
+ counterKey(kind) {
9033
+ return `${kind}:${this.config.projectKey}`;
9034
+ }
9035
+ async allocateSeqRange(kind, count) {
9036
+ if (!this.counters)
9037
+ throw new Error("Mongo not connected");
9038
+ if (count <= 0)
9039
+ return 1;
9040
+ const key = this.counterKey(kind);
9041
+ const doc = await this.counters.findOneAndUpdate(
9042
+ { _id: key },
9043
+ { $inc: { seq: count } },
9044
+ { upsert: true, returnDocument: "after" }
9045
+ );
9046
+ const endSeq = doc?.seq;
9047
+ if (typeof endSeq !== "number") {
9048
+ throw new Error(`Failed to allocate seq range for ${key}`);
9049
+ }
9050
+ return endSeq - count + 1;
9051
+ }
9052
+ pushTargetName() {
9053
+ return `mongo_push_events_rowid:${this.config.projectKey}`;
9054
+ }
9055
+ pullTargetName() {
9056
+ return `mongo_pull_events_seq:${this.config.projectKey}`;
9057
+ }
9058
+ async pushEvents() {
9059
+ if (!this.events)
9060
+ throw new Error("Mongo not connected");
9061
+ const position = await this.sqliteStore.getSyncPosition(this.pushTargetName());
9062
+ let lastRowid = parseIntOrZero(position.lastEventId);
9063
+ let pushed = 0;
9064
+ while (true) {
9065
+ const batch = await this.sqliteStore.getEventsSinceRowid(lastRowid, this.config.batchSize);
9066
+ if (batch.length === 0)
9067
+ break;
9068
+ const startSeq = await this.allocateSeqRange("events", batch.length);
9069
+ const now = /* @__PURE__ */ new Date();
9070
+ const hostname2 = os5.hostname();
9071
+ const ops = batch.map((item, idx) => {
9072
+ const event = item.event;
9073
+ const seq = startSeq + idx;
9074
+ const docId = `${this.config.projectKey}:${event.id}`;
9075
+ return {
9076
+ updateOne: {
9077
+ filter: { _id: docId },
9078
+ update: {
9079
+ $setOnInsert: {
9080
+ _id: docId,
9081
+ projectKey: this.config.projectKey,
9082
+ seq,
9083
+ eventId: event.id,
9084
+ eventType: event.eventType,
9085
+ sessionId: event.sessionId,
9086
+ timestamp: event.timestamp,
9087
+ content: event.content,
9088
+ canonicalKey: event.canonicalKey,
9089
+ dedupeKey: event.dedupeKey,
9090
+ metadata: event.metadata ?? null,
9091
+ insertedAt: now,
9092
+ updatedAt: now,
9093
+ source: { hostname: hostname2, instanceId: this.config.instanceId }
9094
+ }
9095
+ },
9096
+ upsert: true
9097
+ }
9098
+ };
9099
+ });
9100
+ await this.events.bulkWrite(ops, { ordered: false });
9101
+ const last = batch[batch.length - 1];
9102
+ lastRowid = last.rowid;
9103
+ await this.sqliteStore.updateSyncPosition(
9104
+ this.pushTargetName(),
9105
+ String(lastRowid),
9106
+ last.event.timestamp.toISOString()
9107
+ );
9108
+ pushed += batch.length;
9109
+ if (batch.length < this.config.batchSize)
9110
+ break;
9111
+ }
9112
+ return pushed;
9113
+ }
9114
+ async pullEvents() {
9115
+ if (!this.events)
9116
+ throw new Error("Mongo not connected");
9117
+ const position = await this.sqliteStore.getSyncPosition(this.pullTargetName());
9118
+ let lastSeq = parseIntOrZero(position.lastEventId);
9119
+ let pulled = 0;
9120
+ while (true) {
9121
+ const docs = await this.events.find(
9122
+ { projectKey: this.config.projectKey, seq: { $gt: lastSeq } },
9123
+ { sort: { seq: 1 }, limit: this.config.batchSize }
9124
+ ).toArray();
9125
+ if (docs.length === 0)
9126
+ break;
9127
+ const events = docs.map((d) => ({
9128
+ id: d.eventId,
9129
+ eventType: d.eventType,
9130
+ sessionId: d.sessionId,
9131
+ timestamp: d.timestamp instanceof Date ? d.timestamp : new Date(d.timestamp),
9132
+ content: d.content,
9133
+ canonicalKey: d.canonicalKey,
9134
+ dedupeKey: d.dedupeKey,
9135
+ metadata: d.metadata ?? void 0
9136
+ }));
9137
+ const result = await this.sqliteStore.importEvents(events);
9138
+ pulled += result.inserted;
9139
+ lastSeq = docs[docs.length - 1].seq;
9140
+ await this.sqliteStore.updateSyncPosition(
9141
+ this.pullTargetName(),
9142
+ String(lastSeq),
9143
+ (/* @__PURE__ */ new Date()).toISOString()
9144
+ );
9145
+ if (docs.length < this.config.batchSize)
9146
+ break;
9147
+ }
9148
+ return pulled;
9149
+ }
9150
+ };
9151
+
6221
9152
  // src/cli/index.ts
6222
- var CLAUDE_SETTINGS_PATH = path4.join(os3.homedir(), ".claude", "settings.json");
9153
+ var CLAUDE_SETTINGS_PATH = path9.join(os6.homedir(), ".claude", "settings.json");
6223
9154
  function getPluginPath() {
6224
9155
  const possiblePaths = [
6225
- path4.join(__dirname, ".."),
9156
+ path9.join(__dirname, ".."),
6226
9157
  // When running from dist/cli
6227
- path4.join(__dirname, "../..", "dist"),
9158
+ path9.join(__dirname, "../..", "dist"),
6228
9159
  // When running from src
6229
- path4.join(process.cwd(), "dist")
9160
+ path9.join(process.cwd(), "dist")
6230
9161
  // Current working directory
6231
9162
  ];
6232
9163
  for (const p of possiblePaths) {
6233
- const hooksPath = path4.join(p, "hooks", "user-prompt-submit.js");
6234
- if (fs4.existsSync(hooksPath)) {
9164
+ const hooksPath = path9.join(p, "hooks", "user-prompt-submit.js");
9165
+ if (fs9.existsSync(hooksPath)) {
6235
9166
  return p;
6236
9167
  }
6237
9168
  }
6238
- return path4.join(os3.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
9169
+ return path9.join(os6.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
6239
9170
  }
6240
9171
  function loadClaudeSettings() {
6241
9172
  try {
6242
- if (fs4.existsSync(CLAUDE_SETTINGS_PATH)) {
6243
- const content = fs4.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
9173
+ if (fs9.existsSync(CLAUDE_SETTINGS_PATH)) {
9174
+ const content = fs9.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
6244
9175
  return JSON.parse(content);
6245
9176
  }
6246
9177
  } catch (error) {
@@ -6249,13 +9180,13 @@ function loadClaudeSettings() {
6249
9180
  return {};
6250
9181
  }
6251
9182
  function saveClaudeSettings(settings) {
6252
- const dir = path4.dirname(CLAUDE_SETTINGS_PATH);
6253
- if (!fs4.existsSync(dir)) {
6254
- fs4.mkdirSync(dir, { recursive: true });
9183
+ const dir = path9.dirname(CLAUDE_SETTINGS_PATH);
9184
+ if (!fs9.existsSync(dir)) {
9185
+ fs9.mkdirSync(dir, { recursive: true });
6255
9186
  }
6256
9187
  const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
6257
- fs4.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
6258
- fs4.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
9188
+ fs9.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
9189
+ fs9.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
6259
9190
  }
6260
9191
  function getHooksConfig(pluginPath) {
6261
9192
  return {
@@ -6265,7 +9196,7 @@ function getHooksConfig(pluginPath) {
6265
9196
  hooks: [
6266
9197
  {
6267
9198
  type: "command",
6268
- command: `node ${path4.join(pluginPath, "hooks", "user-prompt-submit.js")}`
9199
+ command: `node ${path9.join(pluginPath, "hooks", "user-prompt-submit.js")}`
6269
9200
  }
6270
9201
  ]
6271
9202
  }
@@ -6276,7 +9207,7 @@ function getHooksConfig(pluginPath) {
6276
9207
  hooks: [
6277
9208
  {
6278
9209
  type: "command",
6279
- command: `node ${path4.join(pluginPath, "hooks", "post-tool-use.js")}`
9210
+ command: `node ${path9.join(pluginPath, "hooks", "post-tool-use.js")}`
6280
9211
  }
6281
9212
  ]
6282
9213
  }
@@ -6284,12 +9215,12 @@ function getHooksConfig(pluginPath) {
6284
9215
  };
6285
9216
  }
6286
9217
  var program = new Command();
6287
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.0");
9218
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.12");
6288
9219
  program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
6289
9220
  try {
6290
9221
  const pluginPath = options.path || getPluginPath();
6291
- const userPromptHook = path4.join(pluginPath, "hooks", "user-prompt-submit.js");
6292
- if (!fs4.existsSync(userPromptHook)) {
9222
+ const userPromptHook = path9.join(pluginPath, "hooks", "user-prompt-submit.js");
9223
+ if (!fs9.existsSync(userPromptHook)) {
6293
9224
  console.error(`
6294
9225
  \u274C Hook files not found at: ${pluginPath}`);
6295
9226
  console.error(' Make sure you have built the plugin with "npm run build"');
@@ -6355,7 +9286,7 @@ program.command("status").description("Check plugin installation status").action
6355
9286
  console.log("Hooks:");
6356
9287
  console.log(` UserPromptSubmit: ${hasUserPromptHook ? "\u2705 Installed" : "\u274C Not installed"}`);
6357
9288
  console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
6358
- const hooksExist = fs4.existsSync(path4.join(pluginPath, "hooks", "user-prompt-submit.js"));
9289
+ const hooksExist = fs9.existsSync(path9.join(pluginPath, "hooks", "user-prompt-submit.js"));
6359
9290
  console.log(`
6360
9291
  Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
6361
9292
  console.log(` Path: ${pluginPath}`);
@@ -6483,95 +9414,352 @@ program.command("process").description("Process pending embeddings").option("-p,
6483
9414
  process.exit(1);
6484
9415
  }
6485
9416
  });
6486
- program.command("import").description("Import existing Claude Code conversation history").option("-p, --project <path>", "Import from specific project path").option("-s, --session <file>", "Import specific session file (JSONL)").option("-a, --all", "Import all sessions from all projects").option("-l, --limit <number>", "Limit messages per session").option("-v, --verbose", "Show detailed progress").action(async (options) => {
9417
+ program.command("mongo-sync").description("Sync events with MongoDB for multi-server collaboration (optional)").option("-p, --project <path>", "Project path (defaults to cwd)").option("--mongo-uri <uri>", "MongoDB connection URI (env: CLAUDE_MEMORY_MONGO_URI)").option("--mongo-db <name>", "MongoDB database name (env: CLAUDE_MEMORY_MONGO_DB)").option("--mongo-project <key>", "Remote project key (env: CLAUDE_MEMORY_MONGO_PROJECT, default: basename(projectPath))").option("--direction <dir>", "push|pull|both", "both").option("--batch-size <n>", "Batch size", "500").option("--interval <ms>", "Watch interval ms", "30000").option("--watch", "Run continuously").action(async (options) => {
9418
+ const projectPath = options.project || process.cwd();
9419
+ const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
9420
+ const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
9421
+ const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path9.basename(projectPath);
9422
+ const direction = String(options.direction || "both").toLowerCase();
9423
+ if (!mongoUri || !mongoDb) {
9424
+ console.error("\n\u274C MongoDB sync is not configured.");
9425
+ console.error(" Set --mongo-uri/--mongo-db or env CLAUDE_MEMORY_MONGO_URI/CLAUDE_MEMORY_MONGO_DB.\n");
9426
+ process.exit(1);
9427
+ }
9428
+ if (!["push", "pull", "both"].includes(direction)) {
9429
+ console.error("\n\u274C Invalid --direction. Use: push | pull | both\n");
9430
+ process.exit(1);
9431
+ }
9432
+ const storagePath = getProjectStoragePath(projectPath);
9433
+ if (!fs9.existsSync(storagePath)) {
9434
+ fs9.mkdirSync(storagePath, { recursive: true });
9435
+ }
9436
+ const batchSizeParsed = parseInt(options.batchSize, 10);
9437
+ const intervalParsed = parseInt(options.interval, 10);
9438
+ const batchSize = Number.isFinite(batchSizeParsed) && batchSizeParsed > 0 ? batchSizeParsed : 500;
9439
+ const intervalMs = Number.isFinite(intervalParsed) && intervalParsed > 0 ? intervalParsed : 3e4;
9440
+ const sqliteStore = new SQLiteEventStore(path9.join(storagePath, "events.sqlite"));
9441
+ const worker = new MongoSyncWorker(sqliteStore, {
9442
+ uri: mongoUri,
9443
+ dbName: mongoDb,
9444
+ projectKey,
9445
+ direction,
9446
+ batchSize,
9447
+ intervalMs
9448
+ });
9449
+ const runOnce = async () => {
9450
+ const { pushed, pulled } = await worker.syncNow();
9451
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
9452
+ process.stdout.write(`[mongo-sync] ${ts} project=${projectKey} pushed=${pushed} pulled=${pulled}
9453
+ `);
9454
+ };
9455
+ try {
9456
+ if (!options.watch) {
9457
+ await runOnce();
9458
+ await worker.shutdown();
9459
+ sqliteStore.close();
9460
+ return;
9461
+ }
9462
+ console.log(`[mongo-sync] Watch mode started (interval=${intervalMs}ms, project=${projectKey})`);
9463
+ const handle = setInterval(() => {
9464
+ runOnce().catch((err) => {
9465
+ console.error("[mongo-sync] Sync failed:", err);
9466
+ });
9467
+ }, intervalMs);
9468
+ const shutdown = async () => {
9469
+ clearInterval(handle);
9470
+ console.log("\n[mongo-sync] Shutting down...");
9471
+ try {
9472
+ await worker.shutdown();
9473
+ } finally {
9474
+ sqliteStore.close();
9475
+ }
9476
+ process.exit(0);
9477
+ };
9478
+ process.on("SIGINT", () => {
9479
+ void shutdown();
9480
+ });
9481
+ process.on("SIGTERM", () => {
9482
+ void shutdown();
9483
+ });
9484
+ await runOnce();
9485
+ await new Promise(() => {
9486
+ });
9487
+ } catch (error) {
9488
+ console.error("[mongo-sync] Failed:", error);
9489
+ process.exit(1);
9490
+ }
9491
+ });
9492
+ function renderProgress(event) {
9493
+ switch (event.phase) {
9494
+ case "scan":
9495
+ console.log(` \u{1F50D} ${event.message}`);
9496
+ break;
9497
+ case "session-start": {
9498
+ const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
9499
+ const sessionName = path9.basename(event.filePath, ".jsonl").slice(0, 8);
9500
+ process.stdout.write(
9501
+ `\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
9502
+ );
9503
+ break;
9504
+ }
9505
+ case "session-progress": {
9506
+ process.stdout.write(
9507
+ `\r \u{1F4C4} [${event.sessionIndex + 1}/...] ${event.messagesProcessed} msgs | +${event.imported} imported, ~${event.skipped} skipped `
9508
+ );
9509
+ break;
9510
+ }
9511
+ case "session-done": {
9512
+ const imported = event.importedPrompts + event.importedResponses;
9513
+ if (imported > 0) {
9514
+ process.stdout.write(
9515
+ `\r \u2705 [${event.sessionIndex + 1}] +${event.importedPrompts} prompts, +${event.importedResponses} responses${event.skipped > 0 ? `, ~${event.skipped} skipped` : ""}
9516
+ `
9517
+ );
9518
+ } else if (event.skipped > 0) {
9519
+ process.stdout.write(
9520
+ `\r \u23ED\uFE0F [${event.sessionIndex + 1}] All ${event.skipped} already imported
9521
+ `
9522
+ );
9523
+ } else {
9524
+ process.stdout.write(
9525
+ `\r \u23ED\uFE0F [${event.sessionIndex + 1}] Empty session
9526
+ `
9527
+ );
9528
+ }
9529
+ break;
9530
+ }
9531
+ case "embedding":
9532
+ process.stdout.write(
9533
+ `\r \u{1F9E0} Embeddings: ${event.processed}/${event.total} processed `
9534
+ );
9535
+ if (event.processed >= event.total) {
9536
+ process.stdout.write("\n");
9537
+ }
9538
+ break;
9539
+ case "done":
9540
+ break;
9541
+ }
9542
+ }
9543
+ function printImportSummary(result, embedCount) {
9544
+ console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
9545
+ console.log("\u2502 \u2705 Import Complete \u2502");
9546
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
9547
+ console.log(`\u2502 Sessions processed: ${String(result.totalSessions).padStart(8)} \u2502`);
9548
+ console.log(`\u2502 Total messages: ${String(result.totalMessages).padStart(8)} \u2502`);
9549
+ console.log(`\u2502 Imported prompts: ${String(result.importedPrompts).padStart(8)} \u2502`);
9550
+ console.log(`\u2502 Imported responses: ${String(result.importedResponses).padStart(8)} \u2502`);
9551
+ console.log(`\u2502 Skipped duplicates: ${String(result.skippedDuplicates).padStart(8)} \u2502`);
9552
+ console.log(`\u2502 Embeddings queued: ${String(embedCount).padStart(8)} \u2502`);
9553
+ console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
9554
+ if (result.errors.length > 0) {
9555
+ console.log(`
9556
+ \u26A0\uFE0F Errors (${result.errors.length}):`);
9557
+ for (const error of result.errors.slice(0, 5)) {
9558
+ console.log(` - ${error}`);
9559
+ }
9560
+ if (result.errors.length > 5) {
9561
+ console.log(` ... and ${result.errors.length - 5} more`);
9562
+ }
9563
+ }
9564
+ }
9565
+ function sanitizeSegment3(input, fallback) {
9566
+ const v = (input || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
9567
+ return v || fallback;
9568
+ }
9569
+ async function listMarkdownFiles(root) {
9570
+ const out = [];
9571
+ const stack = [root];
9572
+ while (stack.length > 0) {
9573
+ const dir = stack.pop();
9574
+ const entries = await fs9.promises.readdir(dir, { withFileTypes: true });
9575
+ for (const e of entries) {
9576
+ const full = path9.join(dir, e.name);
9577
+ if (e.isDirectory())
9578
+ stack.push(full);
9579
+ else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md")
9580
+ out.push(full);
9581
+ }
9582
+ }
9583
+ return out.sort();
9584
+ }
9585
+ function deriveNamespaceCategory(sourceRoot, filePath) {
9586
+ const rel = path9.relative(sourceRoot, filePath);
9587
+ const dirSeg = path9.dirname(rel).split(path9.sep).filter(Boolean);
9588
+ if (dirSeg.length >= 2) {
9589
+ const namespace = sanitizeSegment3(dirSeg[0], "default");
9590
+ const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
9591
+ return { namespace, categoryPath: categoryPath.length > 0 ? categoryPath : ["uncategorized"] };
9592
+ }
9593
+ return { namespace: "default", categoryPath: ["uncategorized"] };
9594
+ }
9595
+ function extractImportEvidence(markdown) {
9596
+ const confidenceMatch = markdown.match(/^-\s*confidence:\s*([^\n]+)/m);
9597
+ const sources = markdown.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("- source:")).map((line) => line.replace(/^-\s*source:\s*/i, "").trim()).filter(Boolean).slice(0, 30);
9598
+ return {
9599
+ confidence: confidenceMatch ? confidenceMatch[1].trim() : void 0,
9600
+ sources
9601
+ };
9602
+ }
9603
+ program.command("organize-import [sourceDir]").description("Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing").option("-p, --project <path>", "Project path (defaults to cwd)").option("--session <id>", "Session id for imported events (default: import:organized)").option("--limit <n>", "Limit number of files to import").option("--dry-run", "Preview mapping without writing").option("--bootstrap", "Force-generate structured markdown from codebase + git history before import").option("--bootstrap-if-empty", "Auto-bootstrap when source has no markdown files (default: true)", true).option("--no-bootstrap-if-empty", "Disable auto-bootstrap when source has no markdown files").option("--force-bootstrap", "Run bootstrap even when markdown files exist").option("--repo <path>", "Repository root for bootstrap analysis (default: project path)").option("--out <path>", "Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)").option("--since <range>", 'Git history range for bootstrap (default: "180 days ago")').option("--max-commits <n>", "Max commits to analyze for bootstrap (default: 1000)").option("--incremental", "Use previous bootstrap manifest as baseline for incremental updates (default: true)", true).option("--no-incremental", "Disable incremental bootstrap; regenerate full snapshot").action(async (sourceDir, options) => {
9604
+ const projectPath = options.project || process.cwd();
9605
+ const sessionId = options.session || "import:organized";
9606
+ const sourceRoot = path9.resolve(sourceDir || options.out || projectPath);
9607
+ const repoPath = path9.resolve(options.repo || projectPath);
9608
+ if (!fs9.existsSync(sourceRoot)) {
9609
+ fs9.mkdirSync(sourceRoot, { recursive: true });
9610
+ }
9611
+ const service = getMemoryServiceForProject(projectPath);
9612
+ try {
9613
+ let activeSourceRoot = sourceRoot;
9614
+ let importRoot = sourceRoot;
9615
+ let files = await listMarkdownFiles(importRoot);
9616
+ const hasMarkdown = files.length > 0;
9617
+ const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
9618
+ if (shouldBootstrap) {
9619
+ const outDir = path9.resolve(options.out || path9.join(sourceRoot, "bootstrap-kb"));
9620
+ const since = options.since || "180 days ago";
9621
+ const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
9622
+ console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
9623
+ const bootstrap = await bootstrapKnowledgeBase({
9624
+ repoPath,
9625
+ outDir,
9626
+ since,
9627
+ maxCommits,
9628
+ incremental: options.incremental
9629
+ });
9630
+ console.log(` Repo: ${repoPath}`);
9631
+ console.log(` Output: ${bootstrap.outDir}`);
9632
+ console.log(` Files analyzed: ${bootstrap.fileCount}`);
9633
+ console.log(` Commits analyzed: ${bootstrap.commitCount}`);
9634
+ console.log(` Modules: ${bootstrap.moduleCount}`);
9635
+ activeSourceRoot = outDir;
9636
+ importRoot = outDir;
9637
+ files = await listMarkdownFiles(importRoot);
9638
+ }
9639
+ if (files.length === 0) {
9640
+ console.error("\n\u274C organize-import found no markdown files to import.\n");
9641
+ process.exit(1);
9642
+ }
9643
+ const limit = options.limit ? Math.max(1, parseInt(options.limit, 10)) : files.length;
9644
+ const targets = files.slice(0, limit);
9645
+ console.log(`
9646
+ \u{1F4E6} organize-import`);
9647
+ console.log(` Source: ${activeSourceRoot}`);
9648
+ console.log(` Project: ${projectPath}`);
9649
+ console.log(` Files: ${targets.length}${targets.length < files.length ? `/${files.length}` : ""}`);
9650
+ console.log(` Dry-run: ${options.dryRun ? "yes" : "no"}
9651
+ `);
9652
+ if (!options.dryRun) {
9653
+ await service.initialize();
9654
+ }
9655
+ let imported = 0;
9656
+ let skipped = 0;
9657
+ for (const file of targets) {
9658
+ const text = await fs9.promises.readFile(file, "utf8");
9659
+ if (!text.trim()) {
9660
+ skipped += 1;
9661
+ continue;
9662
+ }
9663
+ const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
9664
+ const rel = path9.relative(activeSourceRoot, file);
9665
+ const evidence = extractImportEvidence(text);
9666
+ if (options.dryRun) {
9667
+ console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);
9668
+ continue;
9669
+ }
9670
+ await service.storeSessionSummary(sessionId, text, {
9671
+ namespace,
9672
+ categoryPath,
9673
+ confidence: evidence.confidence,
9674
+ sources: evidence.sources,
9675
+ import: {
9676
+ sourceFile: rel,
9677
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
9678
+ bootstrap: shouldBootstrap === true
9679
+ }
9680
+ });
9681
+ imported += 1;
9682
+ }
9683
+ if (!options.dryRun) {
9684
+ const embed = await service.processPendingEmbeddings();
9685
+ await service.shutdown();
9686
+ console.log(`
9687
+ \u2705 Imported: ${imported}, skipped-empty: ${skipped}, embeddings: ${embed}
9688
+ `);
9689
+ } else {
9690
+ console.log(`
9691
+ \u2705 Dry-run complete (planned imports: ${targets.length - skipped}, skipped-empty: ${skipped})
9692
+ `);
9693
+ }
9694
+ } catch (error) {
9695
+ console.error("\n\u274C organize-import failed:", error);
9696
+ process.exit(1);
9697
+ }
9698
+ });
9699
+ program.command("import").description("Import existing Claude Code conversation history").option("-p, --project <path>", "Import from specific project path").option("-s, --session <file>", "Import specific session file (JSONL)").option("-a, --all", "Import all sessions from all projects").option("-l, --limit <number>", "Limit messages per session").option("-f, --force", "Force reimport: delete existing events and reimport with turn_id grouping").option("-v, --verbose", "Show detailed progress").action(async (options) => {
9700
+ const startTime = Date.now();
6487
9701
  const targetProjectPath = options.project || process.cwd();
6488
9702
  const service = getMemoryServiceForProject(targetProjectPath);
6489
9703
  const importer = createSessionHistoryImporter(service);
9704
+ const importOpts = {
9705
+ limit: options.limit ? parseInt(options.limit) : void 0,
9706
+ force: options.force,
9707
+ verbose: options.verbose,
9708
+ onProgress: renderProgress
9709
+ };
6490
9710
  try {
9711
+ console.log("\n\u23F3 Initializing memory service...");
6491
9712
  await service.initialize();
9713
+ console.log(" \u2705 Ready\n");
9714
+ if (options.force) {
9715
+ console.log("\u{1F504} Force mode: existing events will be deleted and reimported with turn_id grouping\n");
9716
+ }
6492
9717
  let result;
6493
9718
  if (options.session) {
6494
- console.log(`
6495
- \u{1F4E5} Importing session: ${options.session}`);
6496
- console.log(` Target project: ${targetProjectPath}
9719
+ console.log(`\u{1F4E5} Importing session: ${options.session}`);
9720
+ console.log(` Target: ${targetProjectPath}
6497
9721
  `);
6498
9722
  result = await importer.importSessionFile(options.session, {
6499
- projectPath: targetProjectPath,
6500
- limit: options.limit ? parseInt(options.limit) : void 0,
6501
- verbose: options.verbose
9723
+ ...importOpts,
9724
+ projectPath: targetProjectPath
6502
9725
  });
6503
9726
  } else if (options.project) {
6504
- console.log(`
6505
- \u{1F4E5} Importing project: ${options.project}
9727
+ console.log(`\u{1F4E5} Importing project: ${options.project}
6506
9728
  `);
6507
- result = await importer.importProject(options.project, {
6508
- limit: options.limit ? parseInt(options.limit) : void 0,
6509
- verbose: options.verbose
6510
- });
9729
+ result = await importer.importProject(options.project, importOpts);
6511
9730
  } else if (options.all) {
6512
- console.log("\n\u{1F4E5} Importing all sessions from all projects");
9731
+ console.log("\u{1F4E5} Importing all sessions from all projects");
6513
9732
  console.log(" \u26A0\uFE0F Using global storage (use -p for project-specific)\n");
6514
9733
  const globalService = getDefaultMemoryService();
6515
9734
  const globalImporter = createSessionHistoryImporter(globalService);
6516
9735
  await globalService.initialize();
6517
- result = await globalImporter.importAll({
6518
- limit: options.limit ? parseInt(options.limit) : void 0,
6519
- verbose: options.verbose
6520
- });
6521
- console.log("\n\u23F3 Processing embeddings...");
9736
+ result = await globalImporter.importAll(importOpts);
9737
+ console.log("\n\u{1F9E0} Processing embeddings...");
6522
9738
  const embedCount2 = await globalService.processPendingEmbeddings();
6523
- console.log("\n\u2705 Import Complete\n");
6524
- console.log(`Sessions processed: ${result.totalSessions}`);
6525
- console.log(`Total messages: ${result.totalMessages}`);
6526
- console.log(`Imported prompts: ${result.importedPrompts}`);
6527
- console.log(`Imported responses: ${result.importedResponses}`);
6528
- console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
6529
- console.log(`Embeddings processed: ${embedCount2}`);
6530
- if (result.errors.length > 0) {
6531
- console.log(`
6532
- \u26A0\uFE0F Errors (${result.errors.length}):`);
6533
- for (const error of result.errors.slice(0, 5)) {
6534
- console.log(` - ${error}`);
6535
- }
6536
- if (result.errors.length > 5) {
6537
- console.log(` ... and ${result.errors.length - 5} more`);
6538
- }
6539
- }
9739
+ const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
9740
+ printImportSummary(result, embedCount2);
9741
+ console.log(`
9742
+ \u23F1\uFE0F Completed in ${elapsed2}s`);
6540
9743
  await globalService.shutdown();
6541
9744
  return;
6542
9745
  } else {
6543
9746
  const cwd = process.cwd();
6544
- console.log(`
6545
- \u{1F4E5} Importing sessions for current project: ${cwd}
9747
+ console.log(`\u{1F4E5} Importing sessions for: ${cwd}
6546
9748
  `);
6547
9749
  result = await importer.importProject(cwd, {
6548
- projectPath: cwd,
6549
- limit: options.limit ? parseInt(options.limit) : void 0,
6550
- verbose: options.verbose
9750
+ ...importOpts,
9751
+ projectPath: cwd
6551
9752
  });
6552
9753
  }
6553
- console.log("\n\u23F3 Processing embeddings...");
9754
+ console.log("\n\u{1F9E0} Processing embeddings...");
6554
9755
  const embedCount = await service.processPendingEmbeddings();
6555
- console.log("\n\u2705 Import Complete\n");
6556
- console.log(`Sessions processed: ${result.totalSessions}`);
6557
- console.log(`Total messages: ${result.totalMessages}`);
6558
- console.log(`Imported prompts: ${result.importedPrompts}`);
6559
- console.log(`Imported responses: ${result.importedResponses}`);
6560
- console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
6561
- console.log(`Embeddings processed: ${embedCount}`);
6562
- if (result.errors.length > 0) {
6563
- console.log(`
6564
- \u26A0\uFE0F Errors (${result.errors.length}):`);
6565
- for (const error of result.errors.slice(0, 5)) {
6566
- console.log(` - ${error}`);
6567
- }
6568
- if (result.errors.length > 5) {
6569
- console.log(` ... and ${result.errors.length - 5} more`);
6570
- }
6571
- }
9756
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
9757
+ printImportSummary(result, embedCount);
9758
+ console.log(`
9759
+ \u23F1\uFE0F Completed in ${elapsed}s`);
6572
9760
  await service.shutdown();
6573
9761
  } catch (error) {
6574
- console.error("Import failed:", error);
9762
+ console.error("\n\u274C Import failed:", error);
6575
9763
  process.exit(1);
6576
9764
  }
6577
9765
  });