claude-memory-layer 1.0.22 → 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 +87 -17
  4. package/dist/cli/index.js.map +2 -2
  5. package/dist/core/index.js +30 -5
  6. package/dist/core/index.js.map +2 -2
  7. package/dist/hooks/post-tool-use.js +117 -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 +71 -16
  12. package/dist/hooks/session-end.js.map +2 -2
  13. package/dist/hooks/session-start.js +156 -24
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +101 -18
  16. package/dist/hooks/stop.js.map +2 -2
  17. package/dist/hooks/user-prompt-submit.js +291 -102
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +71 -16
  20. package/dist/server/api/index.js.map +2 -2
  21. package/dist/server/index.js +71 -16
  22. package/dist/server/index.js.map +2 -2
  23. package/dist/services/memory-service.js +71 -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 +1138 -1
  29. package/memory/session_summary/uncategorized/2026-03-04.md +31 -0
  30. package/memory/tool_observation/uncategorized/2026-03-04.md +785 -1
  31. package/memory/user_prompt/uncategorized/2026-03-04.md +438 -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 +15 -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,9 +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
- normalize: true
2691
+ normalize: true,
2692
+ truncation: true,
2693
+ max_length: 512
2672
2694
  });
2673
2695
  const vector = Array.from(output.data);
2674
2696
  return {
@@ -2690,9 +2712,11 @@ var Embedder = class {
2690
2712
  for (let i = 0; i < texts.length; i += batchSize) {
2691
2713
  const batch = texts.slice(i, i + batchSize);
2692
2714
  for (const text of batch) {
2693
- const output = await this.pipeline(text, {
2715
+ const output = await this.pipeline(this.truncate(text), {
2694
2716
  pooling: "mean",
2695
- normalize: true
2717
+ normalize: true,
2718
+ truncation: true,
2719
+ max_length: 512
2696
2720
  });
2697
2721
  const vector = Array.from(output.data);
2698
2722
  results.push({
@@ -3502,8 +3526,8 @@ _Context:_ ${sessionContext}`;
3502
3526
  matchesMetadataScope(metadata, expected) {
3503
3527
  if (!metadata)
3504
3528
  return false;
3505
- return Object.entries(expected).every(([path6, value]) => {
3506
- const actual = path6.split(".").reduce((acc, key) => {
3529
+ return Object.entries(expected).every(([path7, value]) => {
3530
+ const actual = path7.split(".").reduce((acc, key) => {
3507
3531
  if (typeof acc !== "object" || acc === null)
3508
3532
  return void 0;
3509
3533
  return acc[key];
@@ -5962,6 +5986,7 @@ var MemoryService = class {
5962
5986
  projectPath = null;
5963
5987
  readOnly;
5964
5988
  lightweightMode;
5989
+ embeddingOnly;
5965
5990
  mdMirror;
5966
5991
  storagePath;
5967
5992
  constructor(config) {
@@ -5969,6 +5994,7 @@ var MemoryService = class {
5969
5994
  this.storagePath = storagePath;
5970
5995
  this.readOnly = config.readOnly ?? false;
5971
5996
  this.lightweightMode = config.lightweightMode ?? false;
5997
+ this.embeddingOnly = config.embeddingOnly ?? false;
5972
5998
  this.mdMirror = new MarkdownMirror2(process.cwd());
5973
5999
  if (!this.readOnly && !fs4.existsSync(storagePath)) {
5974
6000
  fs4.mkdirSync(storagePath, { recursive: true });
@@ -6042,19 +6068,21 @@ var MemoryService = class {
6042
6068
  this.embedder
6043
6069
  );
6044
6070
  this.vectorWorker.start();
6045
- this.retriever.setGraduationPipeline(this.graduation);
6046
- this.graduationWorker = createGraduationWorker(
6047
- this.sqliteStore,
6048
- this.graduation
6049
- );
6050
- this.graduationWorker.start();
6051
- if (this.analyticsStore) {
6052
- this.syncWorker = new SyncWorker(
6071
+ if (!this.embeddingOnly) {
6072
+ this.retriever.setGraduationPipeline(this.graduation);
6073
+ this.graduationWorker = createGraduationWorker(
6053
6074
  this.sqliteStore,
6054
- this.analyticsStore,
6055
- { intervalMs: 3e4, batchSize: 500 }
6075
+ this.graduation
6056
6076
  );
6057
- 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
+ }
6058
6086
  }
6059
6087
  const savedMode = await this.sqliteStore.getEndlessConfig("mode");
6060
6088
  if (savedMode === "endless") {
@@ -6778,6 +6806,19 @@ var MemoryService = class {
6778
6806
  await this.initialize();
6779
6807
  await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
6780
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
+ }
6781
6822
  /**
6782
6823
  * Evaluate helpfulness of retrievals in a session (called at session end)
6783
6824
  */
@@ -6785,6 +6826,20 @@ var MemoryService = class {
6785
6826
  await this.initialize();
6786
6827
  await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
6787
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
+ }
6788
6843
  /**
6789
6844
  * Get most helpful memories ranked by helpfulness score
6790
6845
  */
@@ -7071,42 +7126,6 @@ var MemoryService = class {
7071
7126
  }
7072
7127
  };
7073
7128
  var serviceCache = /* @__PURE__ */ new Map();
7074
- var GLOBAL_KEY = "__global__";
7075
- function getDefaultMemoryService() {
7076
- if (!serviceCache.has(GLOBAL_KEY)) {
7077
- serviceCache.set(GLOBAL_KEY, new MemoryService({
7078
- storagePath: "~/.claude-code/memory",
7079
- analyticsEnabled: false,
7080
- // Hooks don't need DuckDB
7081
- sharedStoreConfig: { enabled: false }
7082
- // Shared store uses DuckDB too
7083
- }));
7084
- }
7085
- return serviceCache.get(GLOBAL_KEY);
7086
- }
7087
- function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
7088
- const hash = hashProjectPath(projectPath);
7089
- if (!serviceCache.has(hash)) {
7090
- const storagePath = getProjectStoragePath(projectPath);
7091
- serviceCache.set(hash, new MemoryService({
7092
- storagePath,
7093
- projectHash: hash,
7094
- projectPath,
7095
- // Override shared store config - hooks don't need DuckDB
7096
- sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
7097
- analyticsEnabled: false
7098
- // Hooks don't need DuckDB
7099
- }));
7100
- }
7101
- return serviceCache.get(hash);
7102
- }
7103
- function getMemoryServiceForSession(sessionId) {
7104
- const projectInfo = getSessionProject(sessionId);
7105
- if (projectInfo) {
7106
- return getMemoryServiceForProject(projectInfo.projectPath);
7107
- }
7108
- return getDefaultMemoryService();
7109
- }
7110
7129
  function getLightweightMemoryService(sessionId) {
7111
7130
  const projectInfo = getSessionProject(sessionId);
7112
7131
  const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : "lightweight_global";
@@ -7153,6 +7172,176 @@ function writeTurnState(sessionId, turnId) {
7153
7172
  }
7154
7173
  }
7155
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
+ }
7156
7345
 
7157
7346
  // src/hooks/user-prompt-submit.ts
7158
7347
  var MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5");
@@ -7160,9 +7349,9 @@ var BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.4");
7160
7349
  var FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || "0.3");
7161
7350
  var ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== "false";
7162
7351
  var RETRIEVAL_MODE = process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || "hybrid";
7163
- 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");
7164
7353
  var ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || "3");
7165
- var ADHERENCE_STATE_DIR = path5.join(os3.homedir(), ".claude-code", "memory");
7354
+ var ADHERENCE_STATE_DIR = path6.join(os4.homedir(), ".claude-code", "memory");
7166
7355
  function shouldStorePrompt(prompt) {
7167
7356
  const trimmed = prompt.trim();
7168
7357
  if (trimmed.startsWith("/"))
@@ -7181,18 +7370,6 @@ function getDynamicMinScore(prompt) {
7181
7370
  return Math.max(0.3, BASE_MIN_SCORE - 0.05);
7182
7371
  return BASE_MIN_SCORE;
7183
7372
  }
7184
- function withTimeout(promise, timeoutMs) {
7185
- return new Promise((resolve2, reject) => {
7186
- const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
7187
- promise.then((result) => {
7188
- clearTimeout(timer);
7189
- resolve2(result);
7190
- }).catch((error) => {
7191
- clearTimeout(timer);
7192
- reject(error);
7193
- });
7194
- });
7195
- }
7196
7373
  function formatMemoryContext(items) {
7197
7374
  if (items.length === 0)
7198
7375
  return "";
@@ -7205,12 +7382,12 @@ function formatMemoryContext(items) {
7205
7382
  ${lines.join("\n\n")}`;
7206
7383
  }
7207
7384
  function getAdherenceStatePath(sessionId) {
7208
- return path5.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
7385
+ return path6.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
7209
7386
  }
7210
7387
  function readAdherenceState(sessionId) {
7211
7388
  try {
7212
7389
  const filePath = getAdherenceStatePath(sessionId);
7213
- if (!fs6.existsSync(filePath)) {
7390
+ if (!fs7.existsSync(filePath)) {
7214
7391
  return {
7215
7392
  sessionId,
7216
7393
  turnCount: 0,
@@ -7220,7 +7397,7 @@ function readAdherenceState(sessionId) {
7220
7397
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7221
7398
  };
7222
7399
  }
7223
- const data = fs6.readFileSync(filePath, "utf8");
7400
+ const data = fs7.readFileSync(filePath, "utf8");
7224
7401
  const parsed = JSON.parse(data);
7225
7402
  if (parsed.sessionId !== sessionId)
7226
7403
  throw new Error("session mismatch");
@@ -7238,13 +7415,13 @@ function readAdherenceState(sessionId) {
7238
7415
  }
7239
7416
  function writeAdherenceState(state) {
7240
7417
  try {
7241
- if (!fs6.existsSync(ADHERENCE_STATE_DIR)) {
7242
- fs6.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
7418
+ if (!fs7.existsSync(ADHERENCE_STATE_DIR)) {
7419
+ fs7.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
7243
7420
  }
7244
7421
  const filePath = getAdherenceStatePath(state.sessionId);
7245
7422
  const tempPath = filePath + ".tmp";
7246
- fs6.writeFileSync(tempPath, JSON.stringify(state));
7247
- fs6.renameSync(tempPath, filePath);
7423
+ fs7.writeFileSync(tempPath, JSON.stringify(state));
7424
+ fs7.renameSync(tempPath, filePath);
7248
7425
  } catch {
7249
7426
  }
7250
7427
  }
@@ -7300,6 +7477,10 @@ async function main() {
7300
7477
  const currentTurn = adherenceState.turnCount + 1;
7301
7478
  const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
7302
7479
  logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
7480
+ if (currentTurn === 1) {
7481
+ memoryService.evaluatePendingSessions(input.session_id).catch(() => {
7482
+ });
7483
+ }
7303
7484
  if (shouldStorePrompt(input.prompt)) {
7304
7485
  await memoryService.storeUserPrompt(
7305
7486
  input.session_id,
@@ -7314,41 +7495,37 @@ async function main() {
7314
7495
  }
7315
7496
  );
7316
7497
  }
7317
- 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) {
7318
7500
  const minScore = getDynamicMinScore(input.prompt);
7319
7501
  let mergedMemories = [];
7502
+ const lastSnippet = currentTurn > 1 ? readLastAssistantSnippet(input.session_id) : null;
7503
+ const retrievalQuery = lastSnippet ? `${lastSnippet}
7504
+
7505
+ ${input.prompt}` : input.prompt;
7320
7506
  const canUseSemantic = RETRIEVAL_MODE === "semantic" || RETRIEVAL_MODE === "hybrid";
7321
7507
  if (canUseSemantic) {
7322
7508
  try {
7323
- const semanticService = getMemoryServiceForSession(input.session_id);
7324
- const semantic = await withTimeout(
7325
- semanticService.retrieveMemories(input.prompt, {
7326
- topK: MAX_MEMORIES,
7327
- minScore,
7509
+ mergedMemories = await retrieveSemanticMemories(
7510
+ {
7328
7511
  sessionId: input.session_id,
7329
- intentRewrite: true,
7330
- adaptiveRerank: true,
7331
- projectScopeMode: "strict"
7332
- }),
7512
+ prompt: retrievalQuery,
7513
+ topK: MAX_MEMORIES,
7514
+ minScore
7515
+ },
7333
7516
  SEMANTIC_TIMEOUT_MS
7334
7517
  );
7335
- mergedMemories = semantic.memories.map((m) => ({
7336
- type: m.event.eventType,
7337
- content: m.event.content,
7338
- id: m.event.id,
7339
- score: m.score
7340
- }));
7341
7518
  } catch {
7342
7519
  }
7343
7520
  }
7344
7521
  const shouldUseKeywordFallback = RETRIEVAL_MODE === "keyword" || RETRIEVAL_MODE === "hybrid" || mergedMemories.length === 0;
7345
7522
  if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
7346
- let results = await memoryService.keywordSearch(input.prompt, {
7523
+ let results = await memoryService.keywordSearch(retrievalQuery, {
7347
7524
  topK: MAX_MEMORIES,
7348
7525
  minScore
7349
7526
  });
7350
7527
  if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
7351
- results = await memoryService.keywordSearch(input.prompt, {
7528
+ results = await memoryService.keywordSearch(retrievalQuery, {
7352
7529
  topK: MAX_MEMORIES,
7353
7530
  minScore: FALLBACK_MIN_SCORE
7354
7531
  });
@@ -7387,6 +7564,18 @@ async function main() {
7387
7564
  }
7388
7565
  context = formatMemoryContext(mergedMemories);
7389
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
+ }
7390
7579
  }
7391
7580
  writeAdherenceState({
7392
7581
  sessionId: input.session_id,