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
@@ -67,11 +67,11 @@ function toDate(value) {
67
67
  return new Date(value);
68
68
  return new Date(String(value));
69
69
  }
70
- function createDatabase(path4, options) {
70
+ function createDatabase(path5, options) {
71
71
  if (options?.readOnly) {
72
- return new duckdb.Database(path4, { access_mode: "READ_ONLY" });
72
+ return new duckdb.Database(path5, { access_mode: "READ_ONLY" });
73
73
  }
74
- return new duckdb.Database(path4);
74
+ return new duckdb.Database(path5);
75
75
  }
76
76
  function dbRun(db, sql, params = []) {
77
77
  return new Promise((resolve2, reject) => {
@@ -755,12 +755,12 @@ import { randomUUID as randomUUID2 } from "crypto";
755
755
  import Database from "better-sqlite3";
756
756
  import * as fs from "fs";
757
757
  import * as nodePath from "path";
758
- function createSQLiteDatabase(path4, options) {
759
- const dir = nodePath.dirname(path4);
758
+ function createSQLiteDatabase(path5, options) {
759
+ const dir = nodePath.dirname(path5);
760
760
  if (!fs.existsSync(dir)) {
761
761
  fs.mkdirSync(dir, { recursive: true });
762
762
  }
763
- const db = new Database(path4, {
763
+ const db = new Database(path5, {
764
764
  readonly: options?.readonly ?? false
765
765
  });
766
766
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -1834,6 +1834,21 @@ var SQLiteEventStore = class {
1834
1834
  [id, eventId, sessionId, score, query.slice(0, 100)]
1835
1835
  );
1836
1836
  }
1837
+ /**
1838
+ * Get session IDs that have unevaluated retrievals (measured_at IS NULL).
1839
+ * Excludes the current session. Used to backfill sessions that ended without Stop hook.
1840
+ */
1841
+ async getUnevaluatedSessions(currentSessionId, limit = 5) {
1842
+ await this.initialize();
1843
+ const rows = sqliteAll(
1844
+ this.db,
1845
+ `SELECT DISTINCT session_id FROM memory_helpfulness
1846
+ WHERE measured_at IS NULL AND session_id != ?
1847
+ ORDER BY created_at DESC LIMIT ?`,
1848
+ [currentSessionId, limit]
1849
+ );
1850
+ return rows.map((r) => r.session_id);
1851
+ }
1837
1852
  /**
1838
1853
  * Evaluate helpfulness for all retrievals in a session
1839
1854
  * Called at session end - uses behavioral signals to compute score
@@ -2621,7 +2636,7 @@ var VectorStore = class {
2621
2636
 
2622
2637
  // src/core/embedder.ts
2623
2638
  import { pipeline } from "@huggingface/transformers";
2624
- var Embedder = class {
2639
+ var Embedder = class _Embedder {
2625
2640
  pipeline = null;
2626
2641
  modelName;
2627
2642
  activeModelName;
@@ -2652,6 +2667,11 @@ var Embedder = class {
2652
2667
  this.initialized = true;
2653
2668
  }
2654
2669
  }
2670
+ // ~4 chars per token; 512 tokens * 4 = 2048, use 2000 to be safe
2671
+ static MAX_CHARS = 2e3;
2672
+ truncate(text) {
2673
+ return text.length > _Embedder.MAX_CHARS ? text.slice(0, _Embedder.MAX_CHARS) : text;
2674
+ }
2655
2675
  /**
2656
2676
  * Generate embedding for a single text
2657
2677
  */
@@ -2660,9 +2680,11 @@ var Embedder = class {
2660
2680
  if (!this.pipeline) {
2661
2681
  throw new Error("Embedding pipeline not initialized");
2662
2682
  }
2663
- const output = await this.pipeline(text, {
2683
+ const output = await this.pipeline(this.truncate(text), {
2664
2684
  pooling: "mean",
2665
- normalize: true
2685
+ normalize: true,
2686
+ truncation: true,
2687
+ max_length: 512
2666
2688
  });
2667
2689
  const vector = Array.from(output.data);
2668
2690
  return {
@@ -2684,9 +2706,11 @@ var Embedder = class {
2684
2706
  for (let i = 0; i < texts.length; i += batchSize) {
2685
2707
  const batch = texts.slice(i, i + batchSize);
2686
2708
  for (const text of batch) {
2687
- const output = await this.pipeline(text, {
2709
+ const output = await this.pipeline(this.truncate(text), {
2688
2710
  pooling: "mean",
2689
- normalize: true
2711
+ normalize: true,
2712
+ truncation: true,
2713
+ max_length: 512
2690
2714
  });
2691
2715
  const vector = Array.from(output.data);
2692
2716
  results.push({
@@ -3496,8 +3520,8 @@ _Context:_ ${sessionContext}`;
3496
3520
  matchesMetadataScope(metadata, expected) {
3497
3521
  if (!metadata)
3498
3522
  return false;
3499
- return Object.entries(expected).every(([path4, value]) => {
3500
- const actual = path4.split(".").reduce((acc, key) => {
3523
+ return Object.entries(expected).every(([path5, value]) => {
3524
+ const actual = path5.split(".").reduce((acc, key) => {
3501
3525
  if (typeof acc !== "object" || acc === null)
3502
3526
  return void 0;
3503
3527
  return acc[key];
@@ -5981,6 +6005,7 @@ var MemoryService = class {
5981
6005
  projectPath = null;
5982
6006
  readOnly;
5983
6007
  lightweightMode;
6008
+ embeddingOnly;
5984
6009
  mdMirror;
5985
6010
  storagePath;
5986
6011
  constructor(config) {
@@ -5988,6 +6013,7 @@ var MemoryService = class {
5988
6013
  this.storagePath = storagePath;
5989
6014
  this.readOnly = config.readOnly ?? false;
5990
6015
  this.lightweightMode = config.lightweightMode ?? false;
6016
+ this.embeddingOnly = config.embeddingOnly ?? false;
5991
6017
  this.mdMirror = new MarkdownMirror2(process.cwd());
5992
6018
  if (!this.readOnly && !fs4.existsSync(storagePath)) {
5993
6019
  fs4.mkdirSync(storagePath, { recursive: true });
@@ -6061,19 +6087,21 @@ var MemoryService = class {
6061
6087
  this.embedder
6062
6088
  );
6063
6089
  this.vectorWorker.start();
6064
- this.retriever.setGraduationPipeline(this.graduation);
6065
- this.graduationWorker = createGraduationWorker(
6066
- this.sqliteStore,
6067
- this.graduation
6068
- );
6069
- this.graduationWorker.start();
6070
- if (this.analyticsStore) {
6071
- this.syncWorker = new SyncWorker(
6090
+ if (!this.embeddingOnly) {
6091
+ this.retriever.setGraduationPipeline(this.graduation);
6092
+ this.graduationWorker = createGraduationWorker(
6072
6093
  this.sqliteStore,
6073
- this.analyticsStore,
6074
- { intervalMs: 3e4, batchSize: 500 }
6094
+ this.graduation
6075
6095
  );
6076
- this.syncWorker.start();
6096
+ this.graduationWorker.start();
6097
+ if (this.analyticsStore) {
6098
+ this.syncWorker = new SyncWorker(
6099
+ this.sqliteStore,
6100
+ this.analyticsStore,
6101
+ { intervalMs: 3e4, batchSize: 500 }
6102
+ );
6103
+ this.syncWorker.start();
6104
+ }
6077
6105
  }
6078
6106
  const savedMode = await this.sqliteStore.getEndlessConfig("mode");
6079
6107
  if (savedMode === "endless") {
@@ -6797,6 +6825,19 @@ var MemoryService = class {
6797
6825
  await this.initialize();
6798
6826
  await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
6799
6827
  }
6828
+ /**
6829
+ * Record a query-level retrieval trace (used by user-prompt-submit hook).
6830
+ * Feeds the retrieval_traces table that powers dashboard stats.
6831
+ */
6832
+ async recordQueryTrace(input) {
6833
+ await this.initialize();
6834
+ await this.sqliteStore.recordRetrievalTrace({
6835
+ ...input,
6836
+ candidateDetails: [],
6837
+ selectedDetails: [],
6838
+ fallbackTrace: []
6839
+ });
6840
+ }
6800
6841
  /**
6801
6842
  * Evaluate helpfulness of retrievals in a session (called at session end)
6802
6843
  */
@@ -6804,6 +6845,20 @@ var MemoryService = class {
6804
6845
  await this.initialize();
6805
6846
  await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
6806
6847
  }
6848
+ /**
6849
+ * Backfill helpfulness evaluation for sessions that ended without Stop hook.
6850
+ * Call on first turn of a new session to catch missed evaluations.
6851
+ */
6852
+ async evaluatePendingSessions(currentSessionId) {
6853
+ await this.initialize();
6854
+ const sessions = await this.sqliteStore.getUnevaluatedSessions(currentSessionId, 5);
6855
+ for (const sid of sessions) {
6856
+ try {
6857
+ await this.sqliteStore.evaluateSessionHelpfulness(sid);
6858
+ } catch {
6859
+ }
6860
+ }
6861
+ }
6807
6862
  /**
6808
6863
  * Get most helpful memories ranked by helpfulness score
6809
6864
  */
@@ -7108,11 +7163,88 @@ function getLightweightMemoryService(sessionId) {
7108
7163
  return serviceCache.get(key);
7109
7164
  }
7110
7165
 
7166
+ // src/hooks/semantic-daemon-client.ts
7167
+ import { spawn } from "child_process";
7168
+ import * as fs5 from "fs";
7169
+ import * as net from "net";
7170
+ import * as os2 from "os";
7171
+ import * as path4 from "path";
7172
+ var DEFAULT_SOCKET_PATH = path4.join(
7173
+ os2.homedir(),
7174
+ ".claude-code",
7175
+ "memory",
7176
+ "semantic-daemon.sock"
7177
+ );
7178
+ var DAEMON_SOCKET_PATH = process.env.CLAUDE_MEMORY_SEMANTIC_SOCKET || DEFAULT_SOCKET_PATH;
7179
+ var DAEMON_START_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_DAEMON_START_MS || "1500");
7180
+ var daemonStartPromise = null;
7181
+ async function ensureDaemonRunning() {
7182
+ if (daemonStartPromise) {
7183
+ return daemonStartPromise;
7184
+ }
7185
+ daemonStartPromise = (async () => {
7186
+ if (await canConnect()) {
7187
+ return;
7188
+ }
7189
+ const daemonScriptPath = getDaemonScriptPath();
7190
+ if (!fs5.existsSync(daemonScriptPath)) {
7191
+ throw new Error(`semantic daemon script not found: ${daemonScriptPath}`);
7192
+ }
7193
+ const daemonDir = path4.dirname(DAEMON_SOCKET_PATH);
7194
+ if (!fs5.existsSync(daemonDir)) {
7195
+ fs5.mkdirSync(daemonDir, { recursive: true });
7196
+ }
7197
+ const child = spawn(process.execPath, [daemonScriptPath], {
7198
+ detached: true,
7199
+ stdio: "ignore",
7200
+ env: process.env
7201
+ });
7202
+ child.unref();
7203
+ const startDeadline = Date.now() + DAEMON_START_TIMEOUT_MS;
7204
+ while (Date.now() < startDeadline) {
7205
+ if (await canConnect()) {
7206
+ return;
7207
+ }
7208
+ await sleep(60);
7209
+ }
7210
+ throw new Error(`semantic daemon start timeout (${DAEMON_START_TIMEOUT_MS}ms)`);
7211
+ })();
7212
+ try {
7213
+ await daemonStartPromise;
7214
+ } finally {
7215
+ daemonStartPromise = null;
7216
+ }
7217
+ }
7218
+ function getDaemonScriptPath() {
7219
+ return path4.join(path4.dirname(new URL(import.meta.url).pathname), "semantic-daemon.js");
7220
+ }
7221
+ function canConnect() {
7222
+ return new Promise((resolve2) => {
7223
+ let settled = false;
7224
+ const client = net.createConnection(DAEMON_SOCKET_PATH);
7225
+ const finalize = (ok) => {
7226
+ if (settled)
7227
+ return;
7228
+ settled = true;
7229
+ client.destroy();
7230
+ resolve2(ok);
7231
+ };
7232
+ client.on("connect", () => finalize(true));
7233
+ client.on("error", () => finalize(false));
7234
+ setTimeout(() => finalize(false), 120).unref();
7235
+ });
7236
+ }
7237
+ function sleep(ms) {
7238
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
7239
+ }
7240
+
7111
7241
  // src/hooks/session-start.ts
7112
7242
  async function main() {
7113
7243
  const inputData = await readStdin();
7114
7244
  const input = JSON.parse(inputData);
7115
7245
  registerSession(input.session_id, input.cwd);
7246
+ ensureDaemonRunning().catch(() => {
7247
+ });
7116
7248
  const memoryService = getLightweightMemoryService(input.session_id);
7117
7249
  try {
7118
7250
  await memoryService.startSession(input.session_id, input.cwd);