claude-memory-layer 1.0.23 → 1.0.24

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 (51) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/README.md +2 -0
  3. package/dist/cli/index.js +85 -17
  4. package/dist/cli/index.js.map +2 -2
  5. package/dist/core/index.js +28 -5
  6. package/dist/core/index.js.map +2 -2
  7. package/dist/hooks/post-tool-use.js +115 -18
  8. package/dist/hooks/post-tool-use.js.map +2 -2
  9. package/dist/hooks/semantic-daemon.js +7337 -0
  10. package/dist/hooks/semantic-daemon.js.map +7 -0
  11. package/dist/hooks/session-end.js +69 -16
  12. package/dist/hooks/session-end.js.map +2 -2
  13. package/dist/hooks/session-start.js +154 -24
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +99 -18
  16. package/dist/hooks/stop.js.map +2 -2
  17. package/dist/hooks/user-prompt-submit.js +289 -102
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +69 -16
  20. package/dist/server/api/index.js.map +2 -2
  21. package/dist/server/index.js +69 -16
  22. package/dist/server/index.js.map +2 -2
  23. package/dist/services/memory-service.js +69 -16
  24. package/dist/services/memory-service.js.map +2 -2
  25. package/dist/ui/app.js +48 -1
  26. package/dist/ui/index.html +11 -3
  27. package/memory/_index.md +1 -0
  28. package/memory/agent_response/uncategorized/2026-03-04.md +1098 -1
  29. package/memory/session_summary/uncategorized/2026-03-04.md +31 -0
  30. package/memory/tool_observation/uncategorized/2026-03-04.md +733 -1
  31. package/memory/user_prompt/uncategorized/2026-03-04.md +371 -1
  32. package/package.json +1 -1
  33. package/scripts/build.ts +2 -1
  34. package/specs/selective-tool-observation/context.md +100 -0
  35. package/specs/selective-tool-observation/plan.md +158 -0
  36. package/specs/selective-tool-observation/spec.md +127 -0
  37. package/src/cli/index.ts +1 -0
  38. package/src/core/embedder.ts +13 -4
  39. package/src/core/sqlite-event-store.ts +16 -0
  40. package/src/core/turn-state.ts +48 -0
  41. package/src/core/types.ts +1 -0
  42. package/src/hooks/post-tool-use.ts +47 -2
  43. package/src/hooks/semantic-daemon-client.ts +208 -0
  44. package/src/hooks/semantic-daemon.ts +276 -0
  45. package/src/hooks/session-start.ts +7 -0
  46. package/src/hooks/stop.ts +19 -4
  47. package/src/hooks/user-prompt-submit.ts +48 -40
  48. package/src/services/memory-service.ts +59 -16
  49. package/src/services/session-history-importer.ts +18 -0
  50. package/src/ui/app.js +48 -1
  51. package/src/ui/index.html +11 -3
@@ -8,9 +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
+ import * as fs7 from "fs";
12
+ import * as path6 from "path";
13
+ import * as os4 from "os";
14
14
 
15
15
  // src/services/memory-service.ts
16
16
  import * as path3 from "path";
@@ -73,11 +73,11 @@ function toDate(value) {
73
73
  return new Date(value);
74
74
  return new Date(String(value));
75
75
  }
76
- function createDatabase(path6, options) {
76
+ function createDatabase(path7, options) {
77
77
  if (options?.readOnly) {
78
- return new duckdb.Database(path6, { access_mode: "READ_ONLY" });
78
+ return new duckdb.Database(path7, { access_mode: "READ_ONLY" });
79
79
  }
80
- return new duckdb.Database(path6);
80
+ return new duckdb.Database(path7);
81
81
  }
82
82
  function dbRun(db, sql, params = []) {
83
83
  return new Promise((resolve2, reject) => {
@@ -761,12 +761,12 @@ import { randomUUID as randomUUID2 } from "crypto";
761
761
  import Database from "better-sqlite3";
762
762
  import * as fs from "fs";
763
763
  import * as nodePath from "path";
764
- function createSQLiteDatabase(path6, options) {
765
- const dir = nodePath.dirname(path6);
764
+ function createSQLiteDatabase(path7, options) {
765
+ const dir = nodePath.dirname(path7);
766
766
  if (!fs.existsSync(dir)) {
767
767
  fs.mkdirSync(dir, { recursive: true });
768
768
  }
769
- const db = new Database(path6, {
769
+ const db = new Database(path7, {
770
770
  readonly: options?.readonly ?? false
771
771
  });
772
772
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -1840,6 +1840,21 @@ var SQLiteEventStore = class {
1840
1840
  [id, eventId, sessionId, score, query.slice(0, 100)]
1841
1841
  );
1842
1842
  }
1843
+ /**
1844
+ * Get session IDs that have unevaluated retrievals (measured_at IS NULL).
1845
+ * Excludes the current session. Used to backfill sessions that ended without Stop hook.
1846
+ */
1847
+ async getUnevaluatedSessions(currentSessionId, limit = 5) {
1848
+ await this.initialize();
1849
+ const rows = sqliteAll(
1850
+ this.db,
1851
+ `SELECT DISTINCT session_id FROM memory_helpfulness
1852
+ WHERE measured_at IS NULL AND session_id != ?
1853
+ ORDER BY created_at DESC LIMIT ?`,
1854
+ [currentSessionId, limit]
1855
+ );
1856
+ return rows.map((r) => r.session_id);
1857
+ }
1843
1858
  /**
1844
1859
  * Evaluate helpfulness for all retrievals in a session
1845
1860
  * Called at session end - uses behavioral signals to compute score
@@ -2627,7 +2642,7 @@ var VectorStore = class {
2627
2642
 
2628
2643
  // src/core/embedder.ts
2629
2644
  import { pipeline } from "@huggingface/transformers";
2630
- var Embedder = class {
2645
+ var Embedder = class _Embedder {
2631
2646
  pipeline = null;
2632
2647
  modelName;
2633
2648
  activeModelName;
@@ -2658,6 +2673,11 @@ var Embedder = class {
2658
2673
  this.initialized = true;
2659
2674
  }
2660
2675
  }
2676
+ // ~4 chars per token; 512 tokens * 4 = 2048, use 2000 to be safe
2677
+ static MAX_CHARS = 2e3;
2678
+ truncate(text) {
2679
+ return text.length > _Embedder.MAX_CHARS ? text.slice(0, _Embedder.MAX_CHARS) : text;
2680
+ }
2661
2681
  /**
2662
2682
  * Generate embedding for a single text
2663
2683
  */
@@ -2666,10 +2686,11 @@ var Embedder = class {
2666
2686
  if (!this.pipeline) {
2667
2687
  throw new Error("Embedding pipeline not initialized");
2668
2688
  }
2669
- const output = await this.pipeline(text, {
2689
+ const output = await this.pipeline(this.truncate(text), {
2670
2690
  pooling: "mean",
2671
2691
  normalize: true,
2672
- truncation: true
2692
+ truncation: true,
2693
+ max_length: 512
2673
2694
  });
2674
2695
  const vector = Array.from(output.data);
2675
2696
  return {
@@ -2691,10 +2712,11 @@ var Embedder = class {
2691
2712
  for (let i = 0; i < texts.length; i += batchSize) {
2692
2713
  const batch = texts.slice(i, i + batchSize);
2693
2714
  for (const text of batch) {
2694
- const output = await this.pipeline(text, {
2715
+ const output = await this.pipeline(this.truncate(text), {
2695
2716
  pooling: "mean",
2696
2717
  normalize: true,
2697
- truncation: true
2718
+ truncation: true,
2719
+ max_length: 512
2698
2720
  });
2699
2721
  const vector = Array.from(output.data);
2700
2722
  results.push({
@@ -3504,8 +3526,8 @@ _Context:_ ${sessionContext}`;
3504
3526
  matchesMetadataScope(metadata, expected) {
3505
3527
  if (!metadata)
3506
3528
  return false;
3507
- return Object.entries(expected).every(([path6, value]) => {
3508
- const actual = path6.split(".").reduce((acc, key) => {
3529
+ return Object.entries(expected).every(([path7, value]) => {
3530
+ const actual = path7.split(".").reduce((acc, key) => {
3509
3531
  if (typeof acc !== "object" || acc === null)
3510
3532
  return void 0;
3511
3533
  return acc[key];
@@ -5964,6 +5986,7 @@ var MemoryService = class {
5964
5986
  projectPath = null;
5965
5987
  readOnly;
5966
5988
  lightweightMode;
5989
+ embeddingOnly;
5967
5990
  mdMirror;
5968
5991
  storagePath;
5969
5992
  constructor(config) {
@@ -5971,6 +5994,7 @@ var MemoryService = class {
5971
5994
  this.storagePath = storagePath;
5972
5995
  this.readOnly = config.readOnly ?? false;
5973
5996
  this.lightweightMode = config.lightweightMode ?? false;
5997
+ this.embeddingOnly = config.embeddingOnly ?? false;
5974
5998
  this.mdMirror = new MarkdownMirror2(process.cwd());
5975
5999
  if (!this.readOnly && !fs4.existsSync(storagePath)) {
5976
6000
  fs4.mkdirSync(storagePath, { recursive: true });
@@ -6044,19 +6068,21 @@ var MemoryService = class {
6044
6068
  this.embedder
6045
6069
  );
6046
6070
  this.vectorWorker.start();
6047
- this.retriever.setGraduationPipeline(this.graduation);
6048
- this.graduationWorker = createGraduationWorker(
6049
- this.sqliteStore,
6050
- this.graduation
6051
- );
6052
- this.graduationWorker.start();
6053
- if (this.analyticsStore) {
6054
- this.syncWorker = new SyncWorker(
6071
+ if (!this.embeddingOnly) {
6072
+ this.retriever.setGraduationPipeline(this.graduation);
6073
+ this.graduationWorker = createGraduationWorker(
6055
6074
  this.sqliteStore,
6056
- this.analyticsStore,
6057
- { intervalMs: 3e4, batchSize: 500 }
6075
+ this.graduation
6058
6076
  );
6059
- this.syncWorker.start();
6077
+ this.graduationWorker.start();
6078
+ if (this.analyticsStore) {
6079
+ this.syncWorker = new SyncWorker(
6080
+ this.sqliteStore,
6081
+ this.analyticsStore,
6082
+ { intervalMs: 3e4, batchSize: 500 }
6083
+ );
6084
+ this.syncWorker.start();
6085
+ }
6060
6086
  }
6061
6087
  const savedMode = await this.sqliteStore.getEndlessConfig("mode");
6062
6088
  if (savedMode === "endless") {
@@ -6780,6 +6806,19 @@ var MemoryService = class {
6780
6806
  await this.initialize();
6781
6807
  await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
6782
6808
  }
6809
+ /**
6810
+ * Record a query-level retrieval trace (used by user-prompt-submit hook).
6811
+ * Feeds the retrieval_traces table that powers dashboard stats.
6812
+ */
6813
+ async recordQueryTrace(input) {
6814
+ await this.initialize();
6815
+ await this.sqliteStore.recordRetrievalTrace({
6816
+ ...input,
6817
+ candidateDetails: [],
6818
+ selectedDetails: [],
6819
+ fallbackTrace: []
6820
+ });
6821
+ }
6783
6822
  /**
6784
6823
  * Evaluate helpfulness of retrievals in a session (called at session end)
6785
6824
  */
@@ -6787,6 +6826,20 @@ var MemoryService = class {
6787
6826
  await this.initialize();
6788
6827
  await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
6789
6828
  }
6829
+ /**
6830
+ * Backfill helpfulness evaluation for sessions that ended without Stop hook.
6831
+ * Call on first turn of a new session to catch missed evaluations.
6832
+ */
6833
+ async evaluatePendingSessions(currentSessionId) {
6834
+ await this.initialize();
6835
+ const sessions = await this.sqliteStore.getUnevaluatedSessions(currentSessionId, 5);
6836
+ for (const sid of sessions) {
6837
+ try {
6838
+ await this.sqliteStore.evaluateSessionHelpfulness(sid);
6839
+ } catch {
6840
+ }
6841
+ }
6842
+ }
6790
6843
  /**
6791
6844
  * Get most helpful memories ranked by helpfulness score
6792
6845
  */
@@ -7073,42 +7126,6 @@ var MemoryService = class {
7073
7126
  }
7074
7127
  };
7075
7128
  var serviceCache = /* @__PURE__ */ new Map();
7076
- var GLOBAL_KEY = "__global__";
7077
- function getDefaultMemoryService() {
7078
- if (!serviceCache.has(GLOBAL_KEY)) {
7079
- serviceCache.set(GLOBAL_KEY, new MemoryService({
7080
- storagePath: "~/.claude-code/memory",
7081
- analyticsEnabled: false,
7082
- // Hooks don't need DuckDB
7083
- sharedStoreConfig: { enabled: false }
7084
- // Shared store uses DuckDB too
7085
- }));
7086
- }
7087
- return serviceCache.get(GLOBAL_KEY);
7088
- }
7089
- function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
7090
- const hash = hashProjectPath(projectPath);
7091
- if (!serviceCache.has(hash)) {
7092
- const storagePath = getProjectStoragePath(projectPath);
7093
- serviceCache.set(hash, new MemoryService({
7094
- storagePath,
7095
- projectHash: hash,
7096
- projectPath,
7097
- // Override shared store config - hooks don't need DuckDB
7098
- sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
7099
- analyticsEnabled: false
7100
- // Hooks don't need DuckDB
7101
- }));
7102
- }
7103
- return serviceCache.get(hash);
7104
- }
7105
- function getMemoryServiceForSession(sessionId) {
7106
- const projectInfo = getSessionProject(sessionId);
7107
- if (projectInfo) {
7108
- return getMemoryServiceForProject(projectInfo.projectPath);
7109
- }
7110
- return getDefaultMemoryService();
7111
- }
7112
7129
  function getLightweightMemoryService(sessionId) {
7113
7130
  const projectInfo = getSessionProject(sessionId);
7114
7131
  const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : "lightweight_global";
@@ -7155,6 +7172,176 @@ function writeTurnState(sessionId, turnId) {
7155
7172
  }
7156
7173
  }
7157
7174
  }
7175
+ function getLastResponsePath(sessionId) {
7176
+ return path4.join(TURN_STATE_DIR, `.last-response-${sessionId}.json`);
7177
+ }
7178
+ function readLastAssistantSnippet(sessionId) {
7179
+ try {
7180
+ const filePath = getLastResponsePath(sessionId);
7181
+ if (!fs5.existsSync(filePath))
7182
+ return null;
7183
+ const state = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
7184
+ if (state.sessionId !== sessionId)
7185
+ return null;
7186
+ if (Date.now() - new Date(state.createdAt).getTime() > 2 * 60 * 60 * 1e3)
7187
+ return null;
7188
+ return state.snippet || null;
7189
+ } catch {
7190
+ return null;
7191
+ }
7192
+ }
7193
+
7194
+ // src/hooks/semantic-daemon-client.ts
7195
+ import { spawn } from "child_process";
7196
+ import * as fs6 from "fs";
7197
+ import * as net from "net";
7198
+ import * as os3 from "os";
7199
+ import * as path5 from "path";
7200
+ var DEFAULT_SOCKET_PATH = path5.join(
7201
+ os3.homedir(),
7202
+ ".claude-code",
7203
+ "memory",
7204
+ "semantic-daemon.sock"
7205
+ );
7206
+ var DAEMON_SOCKET_PATH = process.env.CLAUDE_MEMORY_SEMANTIC_SOCKET || DEFAULT_SOCKET_PATH;
7207
+ var DAEMON_START_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_DAEMON_START_MS || "1500");
7208
+ var daemonStartPromise = null;
7209
+ async function retrieveSemanticMemories(request, timeoutMs) {
7210
+ const payload = {
7211
+ type: "retrieve",
7212
+ sessionId: request.sessionId,
7213
+ prompt: request.prompt,
7214
+ topK: request.topK,
7215
+ minScore: request.minScore
7216
+ };
7217
+ try {
7218
+ return await requestFromDaemon(payload, timeoutMs);
7219
+ } catch (error) {
7220
+ if (!isConnectionError(error)) {
7221
+ throw error;
7222
+ }
7223
+ await ensureDaemonRunning();
7224
+ return requestFromDaemon(payload, timeoutMs).catch((retryError) => {
7225
+ if (process.env.CLAUDE_MEMORY_DEBUG) {
7226
+ console.error("[semantic-client] retry failed after daemon start:", retryError);
7227
+ }
7228
+ throw retryError;
7229
+ });
7230
+ }
7231
+ }
7232
+ function requestFromDaemon(payload, timeoutMs) {
7233
+ return new Promise((resolve2, reject) => {
7234
+ const client = net.createConnection(DAEMON_SOCKET_PATH);
7235
+ client.setEncoding("utf8");
7236
+ let settled = false;
7237
+ let responseRaw = "";
7238
+ const timer = setTimeout(() => {
7239
+ const timeoutError = new Error(`semantic daemon timeout (${timeoutMs}ms)`);
7240
+ timeoutError.code = "ETIMEDOUT";
7241
+ settle(timeoutError);
7242
+ client.destroy();
7243
+ }, timeoutMs);
7244
+ const settle = (error, memories) => {
7245
+ if (settled)
7246
+ return;
7247
+ settled = true;
7248
+ clearTimeout(timer);
7249
+ if (error) {
7250
+ reject(error);
7251
+ } else {
7252
+ resolve2(memories || []);
7253
+ }
7254
+ };
7255
+ client.on("connect", () => {
7256
+ client.end(JSON.stringify(payload));
7257
+ });
7258
+ client.on("data", (chunk) => {
7259
+ responseRaw += chunk;
7260
+ if (responseRaw.length > 4 * 1024 * 1024) {
7261
+ settle(new Error("semantic daemon response too large"));
7262
+ client.destroy();
7263
+ }
7264
+ });
7265
+ client.on("end", () => {
7266
+ try {
7267
+ const parsed = JSON.parse(responseRaw || "{}");
7268
+ if (!parsed.ok) {
7269
+ settle(new Error(parsed.error || "semantic daemon error"));
7270
+ return;
7271
+ }
7272
+ settle(void 0, parsed.memories || []);
7273
+ } catch (error) {
7274
+ settle(error);
7275
+ }
7276
+ });
7277
+ client.on("error", (error) => {
7278
+ settle(error);
7279
+ });
7280
+ });
7281
+ }
7282
+ async function ensureDaemonRunning() {
7283
+ if (daemonStartPromise) {
7284
+ return daemonStartPromise;
7285
+ }
7286
+ daemonStartPromise = (async () => {
7287
+ if (await canConnect()) {
7288
+ return;
7289
+ }
7290
+ const daemonScriptPath = getDaemonScriptPath();
7291
+ if (!fs6.existsSync(daemonScriptPath)) {
7292
+ throw new Error(`semantic daemon script not found: ${daemonScriptPath}`);
7293
+ }
7294
+ const daemonDir = path5.dirname(DAEMON_SOCKET_PATH);
7295
+ if (!fs6.existsSync(daemonDir)) {
7296
+ fs6.mkdirSync(daemonDir, { recursive: true });
7297
+ }
7298
+ const child = spawn(process.execPath, [daemonScriptPath], {
7299
+ detached: true,
7300
+ stdio: "ignore",
7301
+ env: process.env
7302
+ });
7303
+ child.unref();
7304
+ const startDeadline = Date.now() + DAEMON_START_TIMEOUT_MS;
7305
+ while (Date.now() < startDeadline) {
7306
+ if (await canConnect()) {
7307
+ return;
7308
+ }
7309
+ await sleep(60);
7310
+ }
7311
+ throw new Error(`semantic daemon start timeout (${DAEMON_START_TIMEOUT_MS}ms)`);
7312
+ })();
7313
+ try {
7314
+ await daemonStartPromise;
7315
+ } finally {
7316
+ daemonStartPromise = null;
7317
+ }
7318
+ }
7319
+ function getDaemonScriptPath() {
7320
+ return path5.join(path5.dirname(new URL(import.meta.url).pathname), "semantic-daemon.js");
7321
+ }
7322
+ function canConnect() {
7323
+ return new Promise((resolve2) => {
7324
+ let settled = false;
7325
+ const client = net.createConnection(DAEMON_SOCKET_PATH);
7326
+ const finalize = (ok) => {
7327
+ if (settled)
7328
+ return;
7329
+ settled = true;
7330
+ client.destroy();
7331
+ resolve2(ok);
7332
+ };
7333
+ client.on("connect", () => finalize(true));
7334
+ client.on("error", () => finalize(false));
7335
+ setTimeout(() => finalize(false), 120).unref();
7336
+ });
7337
+ }
7338
+ function isConnectionError(error) {
7339
+ const code = error?.code;
7340
+ return code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE" || code === "ECONNRESET";
7341
+ }
7342
+ function sleep(ms) {
7343
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
7344
+ }
7158
7345
 
7159
7346
  // src/hooks/user-prompt-submit.ts
7160
7347
  var MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5");
@@ -7162,9 +7349,9 @@ var BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.4");
7162
7349
  var FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || "0.3");
7163
7350
  var ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== "false";
7164
7351
  var RETRIEVAL_MODE = process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || "hybrid";
7165
- var SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || "1200");
7352
+ var SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || "2000");
7166
7353
  var ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || "3");
7167
- var ADHERENCE_STATE_DIR = path5.join(os3.homedir(), ".claude-code", "memory");
7354
+ var ADHERENCE_STATE_DIR = path6.join(os4.homedir(), ".claude-code", "memory");
7168
7355
  function shouldStorePrompt(prompt) {
7169
7356
  const trimmed = prompt.trim();
7170
7357
  if (trimmed.startsWith("/"))
@@ -7183,18 +7370,6 @@ function getDynamicMinScore(prompt) {
7183
7370
  return Math.max(0.3, BASE_MIN_SCORE - 0.05);
7184
7371
  return BASE_MIN_SCORE;
7185
7372
  }
7186
- function withTimeout(promise, timeoutMs) {
7187
- return new Promise((resolve2, reject) => {
7188
- const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
7189
- promise.then((result) => {
7190
- clearTimeout(timer);
7191
- resolve2(result);
7192
- }).catch((error) => {
7193
- clearTimeout(timer);
7194
- reject(error);
7195
- });
7196
- });
7197
- }
7198
7373
  function formatMemoryContext(items) {
7199
7374
  if (items.length === 0)
7200
7375
  return "";
@@ -7207,12 +7382,12 @@ function formatMemoryContext(items) {
7207
7382
  ${lines.join("\n\n")}`;
7208
7383
  }
7209
7384
  function getAdherenceStatePath(sessionId) {
7210
- return path5.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
7385
+ return path6.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
7211
7386
  }
7212
7387
  function readAdherenceState(sessionId) {
7213
7388
  try {
7214
7389
  const filePath = getAdherenceStatePath(sessionId);
7215
- if (!fs6.existsSync(filePath)) {
7390
+ if (!fs7.existsSync(filePath)) {
7216
7391
  return {
7217
7392
  sessionId,
7218
7393
  turnCount: 0,
@@ -7222,7 +7397,7 @@ function readAdherenceState(sessionId) {
7222
7397
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7223
7398
  };
7224
7399
  }
7225
- const data = fs6.readFileSync(filePath, "utf8");
7400
+ const data = fs7.readFileSync(filePath, "utf8");
7226
7401
  const parsed = JSON.parse(data);
7227
7402
  if (parsed.sessionId !== sessionId)
7228
7403
  throw new Error("session mismatch");
@@ -7240,13 +7415,13 @@ function readAdherenceState(sessionId) {
7240
7415
  }
7241
7416
  function writeAdherenceState(state) {
7242
7417
  try {
7243
- if (!fs6.existsSync(ADHERENCE_STATE_DIR)) {
7244
- fs6.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
7418
+ if (!fs7.existsSync(ADHERENCE_STATE_DIR)) {
7419
+ fs7.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
7245
7420
  }
7246
7421
  const filePath = getAdherenceStatePath(state.sessionId);
7247
7422
  const tempPath = filePath + ".tmp";
7248
- fs6.writeFileSync(tempPath, JSON.stringify(state));
7249
- fs6.renameSync(tempPath, filePath);
7423
+ fs7.writeFileSync(tempPath, JSON.stringify(state));
7424
+ fs7.renameSync(tempPath, filePath);
7250
7425
  } catch {
7251
7426
  }
7252
7427
  }
@@ -7302,6 +7477,10 @@ async function main() {
7302
7477
  const currentTurn = adherenceState.turnCount + 1;
7303
7478
  const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
7304
7479
  logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
7480
+ if (currentTurn === 1) {
7481
+ memoryService.evaluatePendingSessions(input.session_id).catch(() => {
7482
+ });
7483
+ }
7305
7484
  if (shouldStorePrompt(input.prompt)) {
7306
7485
  await memoryService.storeUserPrompt(
7307
7486
  input.session_id,
@@ -7316,41 +7495,37 @@ async function main() {
7316
7495
  }
7317
7496
  );
7318
7497
  }
7319
- if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
7498
+ const isSlashCommand = input.prompt.trimStart().startsWith("/");
7499
+ if (ENABLE_SEARCH && !isSlashCommand && input.prompt.length > 10 && adherenceDecision.run) {
7320
7500
  const minScore = getDynamicMinScore(input.prompt);
7321
7501
  let mergedMemories = [];
7502
+ const lastSnippet = currentTurn > 1 ? readLastAssistantSnippet(input.session_id) : null;
7503
+ const retrievalQuery = lastSnippet ? `${lastSnippet}
7504
+
7505
+ ${input.prompt}` : input.prompt;
7322
7506
  const canUseSemantic = RETRIEVAL_MODE === "semantic" || RETRIEVAL_MODE === "hybrid";
7323
7507
  if (canUseSemantic) {
7324
7508
  try {
7325
- const semanticService = getMemoryServiceForSession(input.session_id);
7326
- const semantic = await withTimeout(
7327
- semanticService.retrieveMemories(input.prompt, {
7328
- topK: MAX_MEMORIES,
7329
- minScore,
7509
+ mergedMemories = await retrieveSemanticMemories(
7510
+ {
7330
7511
  sessionId: input.session_id,
7331
- intentRewrite: true,
7332
- adaptiveRerank: true,
7333
- projectScopeMode: "strict"
7334
- }),
7512
+ prompt: retrievalQuery,
7513
+ topK: MAX_MEMORIES,
7514
+ minScore
7515
+ },
7335
7516
  SEMANTIC_TIMEOUT_MS
7336
7517
  );
7337
- mergedMemories = semantic.memories.map((m) => ({
7338
- type: m.event.eventType,
7339
- content: m.event.content,
7340
- id: m.event.id,
7341
- score: m.score
7342
- }));
7343
7518
  } catch {
7344
7519
  }
7345
7520
  }
7346
7521
  const shouldUseKeywordFallback = RETRIEVAL_MODE === "keyword" || RETRIEVAL_MODE === "hybrid" || mergedMemories.length === 0;
7347
7522
  if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
7348
- let results = await memoryService.keywordSearch(input.prompt, {
7523
+ let results = await memoryService.keywordSearch(retrievalQuery, {
7349
7524
  topK: MAX_MEMORIES,
7350
7525
  minScore
7351
7526
  });
7352
7527
  if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
7353
- results = await memoryService.keywordSearch(input.prompt, {
7528
+ results = await memoryService.keywordSearch(retrievalQuery, {
7354
7529
  topK: MAX_MEMORIES,
7355
7530
  minScore: FALLBACK_MIN_SCORE
7356
7531
  });
@@ -7389,6 +7564,18 @@ async function main() {
7389
7564
  }
7390
7565
  context = formatMemoryContext(mergedMemories);
7391
7566
  }
7567
+ const allCandidateIds = mergedMemories.map((m) => m.id).filter((v) => Boolean(v));
7568
+ try {
7569
+ await memoryService.recordQueryTrace({
7570
+ sessionId: input.session_id,
7571
+ queryText: retrievalQuery,
7572
+ strategy: RETRIEVAL_MODE,
7573
+ candidateEventIds: allCandidateIds,
7574
+ selectedEventIds: allCandidateIds,
7575
+ confidence: mergedMemories.length > 0 ? "medium" : "none"
7576
+ });
7577
+ } catch {
7578
+ }
7392
7579
  }
7393
7580
  writeAdherenceState({
7394
7581
  sessionId: input.session_id,