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.
- package/.claude/settings.local.json +11 -0
- package/README.md +2 -0
- package/dist/cli/index.js +85 -17
- package/dist/cli/index.js.map +2 -2
- package/dist/core/index.js +28 -5
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +115 -18
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/semantic-daemon.js +7337 -0
- package/dist/hooks/semantic-daemon.js.map +7 -0
- package/dist/hooks/session-end.js +69 -16
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +154 -24
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +99 -18
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +289 -102
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +69 -16
- package/dist/server/api/index.js.map +2 -2
- package/dist/server/index.js +69 -16
- package/dist/server/index.js.map +2 -2
- package/dist/services/memory-service.js +69 -16
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +48 -1
- package/dist/ui/index.html +11 -3
- package/memory/_index.md +1 -0
- package/memory/agent_response/uncategorized/2026-03-04.md +1098 -1
- package/memory/session_summary/uncategorized/2026-03-04.md +31 -0
- package/memory/tool_observation/uncategorized/2026-03-04.md +733 -1
- package/memory/user_prompt/uncategorized/2026-03-04.md +371 -1
- package/package.json +1 -1
- package/scripts/build.ts +2 -1
- package/specs/selective-tool-observation/context.md +100 -0
- package/specs/selective-tool-observation/plan.md +158 -0
- package/specs/selective-tool-observation/spec.md +127 -0
- package/src/cli/index.ts +1 -0
- package/src/core/embedder.ts +13 -4
- package/src/core/sqlite-event-store.ts +16 -0
- package/src/core/turn-state.ts +48 -0
- package/src/core/types.ts +1 -0
- package/src/hooks/post-tool-use.ts +47 -2
- package/src/hooks/semantic-daemon-client.ts +208 -0
- package/src/hooks/semantic-daemon.ts +276 -0
- package/src/hooks/session-start.ts +7 -0
- package/src/hooks/stop.ts +19 -4
- package/src/hooks/user-prompt-submit.ts +48 -40
- package/src/services/memory-service.ts +59 -16
- package/src/services/session-history-importer.ts +18 -0
- package/src/ui/app.js +48 -1
- 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(
|
|
70
|
+
function createDatabase(path5, options) {
|
|
71
71
|
if (options?.readOnly) {
|
|
72
|
-
return new duckdb.Database(
|
|
72
|
+
return new duckdb.Database(path5, { access_mode: "READ_ONLY" });
|
|
73
73
|
}
|
|
74
|
-
return new duckdb.Database(
|
|
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(
|
|
759
|
-
const dir = nodePath.dirname(
|
|
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(
|
|
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,10 +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
2685
|
normalize: true,
|
|
2666
|
-
truncation: true
|
|
2686
|
+
truncation: true,
|
|
2687
|
+
max_length: 512
|
|
2667
2688
|
});
|
|
2668
2689
|
const vector = Array.from(output.data);
|
|
2669
2690
|
return {
|
|
@@ -2685,10 +2706,11 @@ var Embedder = class {
|
|
|
2685
2706
|
for (let i = 0; i < texts.length; i += batchSize) {
|
|
2686
2707
|
const batch = texts.slice(i, i + batchSize);
|
|
2687
2708
|
for (const text of batch) {
|
|
2688
|
-
const output = await this.pipeline(text, {
|
|
2709
|
+
const output = await this.pipeline(this.truncate(text), {
|
|
2689
2710
|
pooling: "mean",
|
|
2690
2711
|
normalize: true,
|
|
2691
|
-
truncation: true
|
|
2712
|
+
truncation: true,
|
|
2713
|
+
max_length: 512
|
|
2692
2714
|
});
|
|
2693
2715
|
const vector = Array.from(output.data);
|
|
2694
2716
|
results.push({
|
|
@@ -3498,8 +3520,8 @@ _Context:_ ${sessionContext}`;
|
|
|
3498
3520
|
matchesMetadataScope(metadata, expected) {
|
|
3499
3521
|
if (!metadata)
|
|
3500
3522
|
return false;
|
|
3501
|
-
return Object.entries(expected).every(([
|
|
3502
|
-
const actual =
|
|
3523
|
+
return Object.entries(expected).every(([path5, value]) => {
|
|
3524
|
+
const actual = path5.split(".").reduce((acc, key) => {
|
|
3503
3525
|
if (typeof acc !== "object" || acc === null)
|
|
3504
3526
|
return void 0;
|
|
3505
3527
|
return acc[key];
|
|
@@ -5983,6 +6005,7 @@ var MemoryService = class {
|
|
|
5983
6005
|
projectPath = null;
|
|
5984
6006
|
readOnly;
|
|
5985
6007
|
lightweightMode;
|
|
6008
|
+
embeddingOnly;
|
|
5986
6009
|
mdMirror;
|
|
5987
6010
|
storagePath;
|
|
5988
6011
|
constructor(config) {
|
|
@@ -5990,6 +6013,7 @@ var MemoryService = class {
|
|
|
5990
6013
|
this.storagePath = storagePath;
|
|
5991
6014
|
this.readOnly = config.readOnly ?? false;
|
|
5992
6015
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
6016
|
+
this.embeddingOnly = config.embeddingOnly ?? false;
|
|
5993
6017
|
this.mdMirror = new MarkdownMirror2(process.cwd());
|
|
5994
6018
|
if (!this.readOnly && !fs4.existsSync(storagePath)) {
|
|
5995
6019
|
fs4.mkdirSync(storagePath, { recursive: true });
|
|
@@ -6063,19 +6087,21 @@ var MemoryService = class {
|
|
|
6063
6087
|
this.embedder
|
|
6064
6088
|
);
|
|
6065
6089
|
this.vectorWorker.start();
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
this.
|
|
6069
|
-
this.graduation
|
|
6070
|
-
);
|
|
6071
|
-
this.graduationWorker.start();
|
|
6072
|
-
if (this.analyticsStore) {
|
|
6073
|
-
this.syncWorker = new SyncWorker(
|
|
6090
|
+
if (!this.embeddingOnly) {
|
|
6091
|
+
this.retriever.setGraduationPipeline(this.graduation);
|
|
6092
|
+
this.graduationWorker = createGraduationWorker(
|
|
6074
6093
|
this.sqliteStore,
|
|
6075
|
-
this.
|
|
6076
|
-
{ intervalMs: 3e4, batchSize: 500 }
|
|
6094
|
+
this.graduation
|
|
6077
6095
|
);
|
|
6078
|
-
this.
|
|
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
|
+
}
|
|
6079
6105
|
}
|
|
6080
6106
|
const savedMode = await this.sqliteStore.getEndlessConfig("mode");
|
|
6081
6107
|
if (savedMode === "endless") {
|
|
@@ -6799,6 +6825,19 @@ var MemoryService = class {
|
|
|
6799
6825
|
await this.initialize();
|
|
6800
6826
|
await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
|
|
6801
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
|
+
}
|
|
6802
6841
|
/**
|
|
6803
6842
|
* Evaluate helpfulness of retrievals in a session (called at session end)
|
|
6804
6843
|
*/
|
|
@@ -6806,6 +6845,20 @@ var MemoryService = class {
|
|
|
6806
6845
|
await this.initialize();
|
|
6807
6846
|
await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
|
|
6808
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
|
+
}
|
|
6809
6862
|
/**
|
|
6810
6863
|
* Get most helpful memories ranked by helpfulness score
|
|
6811
6864
|
*/
|
|
@@ -7110,11 +7163,88 @@ function getLightweightMemoryService(sessionId) {
|
|
|
7110
7163
|
return serviceCache.get(key);
|
|
7111
7164
|
}
|
|
7112
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
|
+
|
|
7113
7241
|
// src/hooks/session-start.ts
|
|
7114
7242
|
async function main() {
|
|
7115
7243
|
const inputData = await readStdin();
|
|
7116
7244
|
const input = JSON.parse(inputData);
|
|
7117
7245
|
registerSession(input.session_id, input.cwd);
|
|
7246
|
+
ensureDaemonRunning().catch(() => {
|
|
7247
|
+
});
|
|
7118
7248
|
const memoryService = getLightweightMemoryService(input.session_id);
|
|
7119
7249
|
try {
|
|
7120
7250
|
await memoryService.startSession(input.session_id, input.cwd);
|