claude-memory-layer 1.0.18 → 1.0.20

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 (42) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +532 -79
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/core/index.js +49 -4
  5. package/dist/core/index.js.map +2 -2
  6. package/dist/hooks/post-tool-use.js +140 -3
  7. package/dist/hooks/post-tool-use.js.map +2 -2
  8. package/dist/hooks/session-end.js +140 -3
  9. package/dist/hooks/session-end.js.map +2 -2
  10. package/dist/hooks/session-start.js +140 -3
  11. package/dist/hooks/session-start.js.map +2 -2
  12. package/dist/hooks/stop.js +140 -3
  13. package/dist/hooks/stop.js.map +2 -2
  14. package/dist/hooks/user-prompt-submit.js +379 -34
  15. package/dist/hooks/user-prompt-submit.js.map +3 -3
  16. package/dist/server/api/index.js +467 -34
  17. package/dist/server/api/index.js.map +3 -3
  18. package/dist/server/index.js +474 -41
  19. package/dist/server/index.js.map +3 -3
  20. package/dist/services/memory-service.js +140 -3
  21. package/dist/services/memory-service.js.map +2 -2
  22. package/dist/ui/app.js +362 -4
  23. package/dist/ui/index.html +90 -0
  24. package/dist/ui/style.css +41 -0
  25. package/memory/_index.md +3 -0
  26. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  27. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  28. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  29. package/package.json +3 -2
  30. package/scripts/delete-unknown-projects.js +154 -0
  31. package/src/cli/index.ts +23 -1
  32. package/src/core/embedder.ts +3 -2
  33. package/src/core/sqlite-event-store.ts +32 -0
  34. package/src/core/types.ts +2 -2
  35. package/src/core/vector-store.ts +20 -0
  36. package/src/hooks/user-prompt-submit.ts +225 -29
  37. package/src/server/api/events.ts +7 -0
  38. package/src/server/api/stats.ts +346 -0
  39. package/src/services/memory-service.ts +119 -2
  40. package/src/ui/app.js +362 -4
  41. package/src/ui/index.html +90 -0
  42. package/src/ui/style.css +41 -0
@@ -8,6 +8,9 @@ const __dirname = dirname(__filename);
8
8
 
9
9
  // src/hooks/user-prompt-submit.ts
10
10
  import { randomUUID as randomUUID9 } from "crypto";
11
+ import * as fs6 from "fs";
12
+ import * as path5 from "path";
13
+ import * as os3 from "os";
11
14
 
12
15
  // src/services/memory-service.ts
13
16
  import * as path3 from "path";
@@ -70,11 +73,11 @@ function toDate(value) {
70
73
  return new Date(value);
71
74
  return new Date(String(value));
72
75
  }
73
- function createDatabase(path5, options) {
76
+ function createDatabase(path6, options) {
74
77
  if (options?.readOnly) {
75
- return new duckdb.Database(path5, { access_mode: "READ_ONLY" });
78
+ return new duckdb.Database(path6, { access_mode: "READ_ONLY" });
76
79
  }
77
- return new duckdb.Database(path5);
80
+ return new duckdb.Database(path6);
78
81
  }
79
82
  function dbRun(db, sql, params = []) {
80
83
  return new Promise((resolve2, reject) => {
@@ -758,12 +761,12 @@ import { randomUUID as randomUUID2 } from "crypto";
758
761
  import Database from "better-sqlite3";
759
762
  import * as fs from "fs";
760
763
  import * as nodePath from "path";
761
- function createSQLiteDatabase(path5, options) {
762
- const dir = nodePath.dirname(path5);
764
+ function createSQLiteDatabase(path6, options) {
765
+ const dir = nodePath.dirname(path6);
763
766
  if (!fs.existsSync(dir)) {
764
767
  fs.mkdirSync(dir, { recursive: true });
765
768
  }
766
- const db = new Database(path5, {
769
+ const db = new Database(path6, {
767
770
  readonly: options?.readonly ?? false
768
771
  });
769
772
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -1602,6 +1605,33 @@ var SQLiteEventStore = class {
1602
1605
  ids
1603
1606
  );
1604
1607
  }
1608
+ /**
1609
+ * Clear embedding outbox (used for embedding model migration)
1610
+ */
1611
+ async clearEmbeddingOutbox() {
1612
+ await this.initialize();
1613
+ sqliteRun(this.db, `DELETE FROM embedding_outbox`);
1614
+ }
1615
+ /**
1616
+ * Count total events
1617
+ */
1618
+ async countEvents() {
1619
+ await this.initialize();
1620
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
1621
+ return row?.count || 0;
1622
+ }
1623
+ /**
1624
+ * Get events page in timestamp ascending order (stable migration/reindex scans)
1625
+ */
1626
+ async getEventsPage(limit = 1e3, offset = 0) {
1627
+ await this.initialize();
1628
+ const rows = sqliteAll(
1629
+ this.db,
1630
+ `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
1631
+ [limit, offset]
1632
+ );
1633
+ return rows.map(this.rowToEvent);
1634
+ }
1605
1635
  /**
1606
1636
  * Mark outbox items as failed
1607
1637
  */
@@ -2567,6 +2597,23 @@ var VectorStore = class {
2567
2597
  const result = await this.table.countRows();
2568
2598
  return result;
2569
2599
  }
2600
+ /**
2601
+ * Clear all vectors (used for embedding model migration)
2602
+ */
2603
+ async clearAll() {
2604
+ await this.initialize();
2605
+ if (!this.db)
2606
+ return;
2607
+ try {
2608
+ if (typeof this.db.dropTable === "function") {
2609
+ await this.db.dropTable(this.tableName);
2610
+ } else if (typeof this.db.drop_table === "function") {
2611
+ await this.db.drop_table(this.tableName);
2612
+ }
2613
+ } catch {
2614
+ }
2615
+ this.table = null;
2616
+ }
2570
2617
  /**
2571
2618
  * Check if vector exists for event
2572
2619
  */
@@ -2584,7 +2631,7 @@ var Embedder = class {
2584
2631
  pipeline = null;
2585
2632
  modelName;
2586
2633
  initialized = false;
2587
- constructor(modelName = "Xenova/all-MiniLM-L6-v2") {
2634
+ constructor(modelName = "jinaai/jina-embeddings-v5-text-nano") {
2588
2635
  this.modelName = modelName;
2589
2636
  }
2590
2637
  /**
@@ -2664,8 +2711,9 @@ var Embedder = class {
2664
2711
  };
2665
2712
  var defaultEmbedder = null;
2666
2713
  function getDefaultEmbedder() {
2714
+ const envModel = process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
2667
2715
  if (!defaultEmbedder) {
2668
- defaultEmbedder = new Embedder();
2716
+ defaultEmbedder = new Embedder(envModel || void 0);
2669
2717
  }
2670
2718
  return defaultEmbedder;
2671
2719
  }
@@ -3439,8 +3487,8 @@ _Context:_ ${sessionContext}`;
3439
3487
  matchesMetadataScope(metadata, expected) {
3440
3488
  if (!metadata)
3441
3489
  return false;
3442
- return Object.entries(expected).every(([path5, value]) => {
3443
- const actual = path5.split(".").reduce((acc, key) => {
3490
+ return Object.entries(expected).every(([path6, value]) => {
3491
+ const actual = path6.split(".").reduce((acc, key) => {
3444
3492
  if (typeof acc !== "object" || acc === null)
3445
3493
  return void 0;
3446
3494
  return acc[key];
@@ -5900,8 +5948,10 @@ var MemoryService = class {
5900
5948
  readOnly;
5901
5949
  lightweightMode;
5902
5950
  mdMirror;
5951
+ storagePath;
5903
5952
  constructor(config) {
5904
5953
  const storagePath = this.expandPath(config.storagePath);
5954
+ this.storagePath = storagePath;
5905
5955
  this.readOnly = config.readOnly ?? false;
5906
5956
  this.lightweightMode = config.lightweightMode ?? false;
5907
5957
  this.mdMirror = new MarkdownMirror2(process.cwd());
@@ -5937,7 +5987,8 @@ var MemoryService = class {
5937
5987
  );
5938
5988
  }
5939
5989
  this.vectorStore = new VectorStore(path3.join(storagePath, "vectors"));
5940
- this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
5990
+ const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
5991
+ this.embedder = embeddingModel ? new Embedder(embeddingModel) : getDefaultEmbedder();
5941
5992
  this.matcher = getDefaultMatcher();
5942
5993
  this.retriever = createRetriever(
5943
5994
  this.sqliteStore,
@@ -6881,6 +6932,95 @@ var MemoryService = class {
6881
6932
  recordMemoryAccess(eventId, sessionId, confidence = 1) {
6882
6933
  this.graduation.recordAccess(eventId, sessionId, confidence);
6883
6934
  }
6935
+ getEmbeddingModelName() {
6936
+ return this.embedder.getModelName();
6937
+ }
6938
+ /**
6939
+ * Ensure embedding model metadata is in sync and optionally migrate vectors.
6940
+ * Migration strategy: clear vector index + clear embedding outbox + re-enqueue all events.
6941
+ */
6942
+ async ensureEmbeddingModelForImport(options) {
6943
+ await this.initialize();
6944
+ const currentModel = this.getEmbeddingModelName();
6945
+ const metaPath = path3.join(this.storagePath, "embedding-meta.json");
6946
+ let previousModel = null;
6947
+ try {
6948
+ if (fs4.existsSync(metaPath)) {
6949
+ const parsed = JSON.parse(fs4.readFileSync(metaPath, "utf-8"));
6950
+ previousModel = parsed?.model || null;
6951
+ }
6952
+ } catch {
6953
+ previousModel = null;
6954
+ }
6955
+ const stats = await this.getStats();
6956
+ const hasExistingVectors = (stats.vectorCount || 0) > 0;
6957
+ if (!previousModel && !hasExistingVectors) {
6958
+ fs4.writeFileSync(metaPath, JSON.stringify({ model: currentModel, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
6959
+ return { changed: false, previousModel: null, currentModel, enqueued: 0, reason: "initialized-meta" };
6960
+ }
6961
+ const modelChanged = previousModel !== currentModel;
6962
+ const legacyUnknownButVectorsExist = !previousModel && hasExistingVectors;
6963
+ if (!modelChanged && !legacyUnknownButVectorsExist) {
6964
+ return { changed: false, previousModel, currentModel, enqueued: 0 };
6965
+ }
6966
+ if (options?.autoMigrate === false) {
6967
+ return {
6968
+ changed: true,
6969
+ previousModel,
6970
+ currentModel,
6971
+ enqueued: 0,
6972
+ reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
6973
+ };
6974
+ }
6975
+ const wasRunning = this.vectorWorker?.isRunning() || false;
6976
+ if (wasRunning)
6977
+ this.vectorWorker?.stop();
6978
+ await this.vectorStore.clearAll();
6979
+ await this.sqliteStore.clearEmbeddingOutbox();
6980
+ const pageSize = 1e3;
6981
+ let offset = 0;
6982
+ let enqueued = 0;
6983
+ while (true) {
6984
+ const page = await this.sqliteStore.getEventsPage(pageSize, offset);
6985
+ if (page.length === 0)
6986
+ break;
6987
+ for (const event of page) {
6988
+ await this.sqliteStore.enqueueForEmbedding(event.id, event.content);
6989
+ enqueued += 1;
6990
+ }
6991
+ offset += page.length;
6992
+ if (page.length < pageSize)
6993
+ break;
6994
+ }
6995
+ fs4.writeFileSync(
6996
+ metaPath,
6997
+ JSON.stringify(
6998
+ {
6999
+ model: currentModel,
7000
+ previousModel,
7001
+ migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
7002
+ enqueued
7003
+ },
7004
+ null,
7005
+ 2
7006
+ )
7007
+ );
7008
+ if (wasRunning)
7009
+ this.vectorWorker?.start();
7010
+ return {
7011
+ changed: true,
7012
+ previousModel,
7013
+ currentModel,
7014
+ enqueued,
7015
+ reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
7016
+ };
7017
+ }
7018
+ /**
7019
+ * Backward-compatible alias used by some hooks
7020
+ */
7021
+ async close() {
7022
+ await this.shutdown();
7023
+ }
6884
7024
  /**
6885
7025
  * Shutdown service
6886
7026
  */
@@ -6916,6 +7056,42 @@ var MemoryService = class {
6916
7056
  }
6917
7057
  };
6918
7058
  var serviceCache = /* @__PURE__ */ new Map();
7059
+ var GLOBAL_KEY = "__global__";
7060
+ function getDefaultMemoryService() {
7061
+ if (!serviceCache.has(GLOBAL_KEY)) {
7062
+ serviceCache.set(GLOBAL_KEY, new MemoryService({
7063
+ storagePath: "~/.claude-code/memory",
7064
+ analyticsEnabled: false,
7065
+ // Hooks don't need DuckDB
7066
+ sharedStoreConfig: { enabled: false }
7067
+ // Shared store uses DuckDB too
7068
+ }));
7069
+ }
7070
+ return serviceCache.get(GLOBAL_KEY);
7071
+ }
7072
+ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
7073
+ const hash = hashProjectPath(projectPath);
7074
+ if (!serviceCache.has(hash)) {
7075
+ const storagePath = getProjectStoragePath(projectPath);
7076
+ serviceCache.set(hash, new MemoryService({
7077
+ storagePath,
7078
+ projectHash: hash,
7079
+ projectPath,
7080
+ // Override shared store config - hooks don't need DuckDB
7081
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
7082
+ analyticsEnabled: false
7083
+ // Hooks don't need DuckDB
7084
+ }));
7085
+ }
7086
+ return serviceCache.get(hash);
7087
+ }
7088
+ function getMemoryServiceForSession(sessionId) {
7089
+ const projectInfo = getSessionProject(sessionId);
7090
+ if (projectInfo) {
7091
+ return getMemoryServiceForProject(projectInfo.projectPath);
7092
+ }
7093
+ return getDefaultMemoryService();
7094
+ }
6919
7095
  function getLightweightMemoryService(sessionId) {
6920
7096
  const projectInfo = getSessionProject(sessionId);
6921
7097
  const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : "lightweight_global";
@@ -6968,6 +7144,10 @@ var MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5");
6968
7144
  var BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.4");
6969
7145
  var FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || "0.3");
6970
7146
  var ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== "false";
7147
+ var RETRIEVAL_MODE = process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || "hybrid";
7148
+ var SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || "1200");
7149
+ var ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || "3");
7150
+ var ADHERENCE_STATE_DIR = path5.join(os3.homedir(), ".claude-code", "memory");
6971
7151
  function shouldStorePrompt(prompt) {
6972
7152
  const trimmed = prompt.trim();
6973
7153
  if (trimmed.startsWith("/"))
@@ -6986,6 +7166,113 @@ function getDynamicMinScore(prompt) {
6986
7166
  return Math.max(0.3, BASE_MIN_SCORE - 0.05);
6987
7167
  return BASE_MIN_SCORE;
6988
7168
  }
7169
+ function withTimeout(promise, timeoutMs) {
7170
+ return new Promise((resolve2, reject) => {
7171
+ const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
7172
+ promise.then((result) => {
7173
+ clearTimeout(timer);
7174
+ resolve2(result);
7175
+ }).catch((error) => {
7176
+ clearTimeout(timer);
7177
+ reject(error);
7178
+ });
7179
+ });
7180
+ }
7181
+ function formatMemoryContext(items) {
7182
+ if (items.length === 0)
7183
+ return "";
7184
+ const lines = items.map((m) => {
7185
+ const preview = m.content.length > 300 ? m.content.substring(0, 300) + "..." : m.content;
7186
+ return `- [${m.type}] ${preview}`;
7187
+ });
7188
+ return `\u{1F4A1} **Related memories found:**
7189
+
7190
+ ${lines.join("\n\n")}`;
7191
+ }
7192
+ function getAdherenceStatePath(sessionId) {
7193
+ return path5.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
7194
+ }
7195
+ function readAdherenceState(sessionId) {
7196
+ try {
7197
+ const filePath = getAdherenceStatePath(sessionId);
7198
+ if (!fs6.existsSync(filePath)) {
7199
+ return {
7200
+ sessionId,
7201
+ turnCount: 0,
7202
+ lastCheckedTurn: 0,
7203
+ lastPrompt: "",
7204
+ lastReason: "init",
7205
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7206
+ };
7207
+ }
7208
+ const data = fs6.readFileSync(filePath, "utf8");
7209
+ const parsed = JSON.parse(data);
7210
+ if (parsed.sessionId !== sessionId)
7211
+ throw new Error("session mismatch");
7212
+ return parsed;
7213
+ } catch {
7214
+ return {
7215
+ sessionId,
7216
+ turnCount: 0,
7217
+ lastCheckedTurn: 0,
7218
+ lastPrompt: "",
7219
+ lastReason: "init",
7220
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7221
+ };
7222
+ }
7223
+ }
7224
+ function writeAdherenceState(state) {
7225
+ try {
7226
+ if (!fs6.existsSync(ADHERENCE_STATE_DIR)) {
7227
+ fs6.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
7228
+ }
7229
+ const filePath = getAdherenceStatePath(state.sessionId);
7230
+ const tempPath = filePath + ".tmp";
7231
+ fs6.writeFileSync(tempPath, JSON.stringify(state));
7232
+ fs6.renameSync(tempPath, filePath);
7233
+ } catch {
7234
+ }
7235
+ }
7236
+ function hasWriteIntent(prompt) {
7237
+ return /(fix|refactor|implement|change|modify|edit|update|rewrite|patch|create|add|remove|delete|버그|수정|리팩터|구현|추가|삭제|개선)/i.test(prompt);
7238
+ }
7239
+ function tokenize(text) {
7240
+ const stopwords = /* @__PURE__ */ new Set(["the", "and", "for", "with", "that", "this", "from", "have", "what", "when", "where", "how", "why", "\uADF8\uB9AC\uACE0", "\uADF8\uB9AC\uACE0\uC694", "\uC774\uAC70", "\uADF8\uAC70", "\uD574\uC8FC\uC138\uC694", "\uD574\uC918", "\uC880", "\uC5D0\uC11C", "\uC73C\uB85C", "\uD558\uB294", "\uD574"]);
7241
+ return text.toLowerCase().replace(/[^a-z0-9가-힣\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !stopwords.has(w));
7242
+ }
7243
+ function isTopicShift(currentPrompt, lastPrompt) {
7244
+ if (!lastPrompt || lastPrompt.length < 10)
7245
+ return false;
7246
+ const a = new Set(tokenize(currentPrompt));
7247
+ const b = new Set(tokenize(lastPrompt));
7248
+ if (a.size === 0 || b.size === 0)
7249
+ return false;
7250
+ let intersection = 0;
7251
+ for (const token of a) {
7252
+ if (b.has(token))
7253
+ intersection++;
7254
+ }
7255
+ const union = a.size + b.size - intersection;
7256
+ const similarity = union > 0 ? intersection / union : 0;
7257
+ return similarity < 0.2;
7258
+ }
7259
+ function shouldRunAdherenceCheck(turnCount, prompt, state) {
7260
+ if (turnCount === 1)
7261
+ return { run: true, reason: "first-turn" };
7262
+ if (hasWriteIntent(prompt))
7263
+ return { run: true, reason: "write-intent" };
7264
+ if (isTopicShift(prompt, state.lastPrompt))
7265
+ return { run: true, reason: "topic-shift" };
7266
+ if (turnCount - state.lastCheckedTurn >= ADHERENCE_INTERVAL_TURNS)
7267
+ return { run: true, reason: "interval" };
7268
+ return { run: false, reason: "skip" };
7269
+ }
7270
+ function logAdherenceDecision(sessionId, turn, run, reason) {
7271
+ if (!process.env.CLAUDE_MEMORY_DEBUG)
7272
+ return;
7273
+ const mode = run ? "enforced" : "skipped";
7274
+ console.error(`[adherence] session=${sessionId} turn=${turn} mode=${mode} reason=${reason}`);
7275
+ }
6989
7276
  async function main() {
6990
7277
  const inputData = await readStdin();
6991
7278
  const input = JSON.parse(inputData);
@@ -6993,49 +7280,107 @@ async function main() {
6993
7280
  writeTurnState(input.session_id, turnId);
6994
7281
  const memoryService = getLightweightMemoryService(input.session_id);
6995
7282
  try {
7283
+ let context = "";
7284
+ const adherenceState = readAdherenceState(input.session_id);
7285
+ const currentTurn = adherenceState.turnCount + 1;
7286
+ const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
7287
+ logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
6996
7288
  if (shouldStorePrompt(input.prompt)) {
6997
7289
  await memoryService.storeUserPrompt(
6998
7290
  input.session_id,
6999
7291
  input.prompt,
7000
- { turnId }
7292
+ {
7293
+ turnId,
7294
+ adherence: {
7295
+ checked: adherenceDecision.run,
7296
+ reason: adherenceDecision.reason,
7297
+ turn: currentTurn
7298
+ }
7299
+ }
7001
7300
  );
7002
7301
  }
7003
- let context = "";
7004
- if (ENABLE_SEARCH && input.prompt.length > 10) {
7302
+ if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
7005
7303
  const minScore = getDynamicMinScore(input.prompt);
7006
- let results = await memoryService.keywordSearch(input.prompt, {
7007
- topK: MAX_MEMORIES,
7008
- minScore
7009
- });
7010
- if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
7011
- results = await memoryService.keywordSearch(input.prompt, {
7304
+ let mergedMemories = [];
7305
+ const canUseSemantic = RETRIEVAL_MODE === "semantic" || RETRIEVAL_MODE === "hybrid";
7306
+ if (canUseSemantic) {
7307
+ try {
7308
+ const semanticService = getMemoryServiceForSession(input.session_id);
7309
+ const semantic = await withTimeout(
7310
+ semanticService.retrieveMemories(input.prompt, {
7311
+ topK: MAX_MEMORIES,
7312
+ minScore,
7313
+ sessionId: input.session_id,
7314
+ intentRewrite: true,
7315
+ adaptiveRerank: true,
7316
+ projectScopeMode: "strict"
7317
+ }),
7318
+ SEMANTIC_TIMEOUT_MS
7319
+ );
7320
+ mergedMemories = semantic.memories.map((m) => ({
7321
+ type: m.event.eventType,
7322
+ content: m.event.content,
7323
+ id: m.event.id,
7324
+ score: m.score
7325
+ }));
7326
+ } catch {
7327
+ }
7328
+ }
7329
+ const shouldUseKeywordFallback = RETRIEVAL_MODE === "keyword" || RETRIEVAL_MODE === "hybrid" || mergedMemories.length === 0;
7330
+ if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
7331
+ let results = await memoryService.keywordSearch(input.prompt, {
7012
7332
  topK: MAX_MEMORIES,
7013
- minScore: FALLBACK_MIN_SCORE
7333
+ minScore
7014
7334
  });
7015
- }
7016
- if (results.length > 0) {
7017
- const eventIds = results.map((r) => r.event.id);
7018
- await memoryService.incrementMemoryAccess(eventIds);
7335
+ if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
7336
+ results = await memoryService.keywordSearch(input.prompt, {
7337
+ topK: MAX_MEMORIES,
7338
+ minScore: FALLBACK_MIN_SCORE
7339
+ });
7340
+ }
7341
+ const existingIds = new Set(mergedMemories.map((m) => m.id).filter(Boolean));
7019
7342
  for (const r of results) {
7343
+ if (existingIds.has(r.event.id))
7344
+ continue;
7345
+ mergedMemories.push({
7346
+ type: r.event.eventType,
7347
+ content: r.event.content,
7348
+ id: r.event.id,
7349
+ score: r.score
7350
+ });
7351
+ if (mergedMemories.length >= MAX_MEMORIES)
7352
+ break;
7353
+ }
7354
+ }
7355
+ if (mergedMemories.length > 0) {
7356
+ const eventIds = mergedMemories.map((m) => m.id).filter((v) => Boolean(v));
7357
+ if (eventIds.length > 0) {
7358
+ await memoryService.incrementMemoryAccess(eventIds);
7359
+ }
7360
+ for (const m of mergedMemories) {
7361
+ if (!m.id)
7362
+ continue;
7020
7363
  try {
7021
7364
  await memoryService.recordRetrieval(
7022
- r.event.id,
7365
+ m.id,
7023
7366
  input.session_id,
7024
- r.score,
7367
+ m.score ?? minScore,
7025
7368
  input.prompt
7026
7369
  );
7027
7370
  } catch {
7028
7371
  }
7029
7372
  }
7030
- const memories = results.map((r) => {
7031
- const preview = r.event.content.length > 300 ? r.event.content.substring(0, 300) + "..." : r.event.content;
7032
- return `- [${r.event.eventType}] ${preview}`;
7033
- });
7034
- context = `\u{1F4A1} **Related memories found:**
7035
-
7036
- ${memories.join("\n\n")}`;
7373
+ context = formatMemoryContext(mergedMemories);
7037
7374
  }
7038
7375
  }
7376
+ writeAdherenceState({
7377
+ sessionId: input.session_id,
7378
+ turnCount: currentTurn,
7379
+ lastCheckedTurn: adherenceDecision.run ? currentTurn : adherenceState.lastCheckedTurn,
7380
+ lastPrompt: input.prompt,
7381
+ lastReason: adherenceDecision.reason,
7382
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7383
+ });
7039
7384
  const output = { context };
7040
7385
  console.log(JSON.stringify(output));
7041
7386
  } catch (error) {