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.
- package/.claude/settings.local.json +11 -0
- package/README.md +2 -0
- package/dist/cli/index.js +87 -17
- package/dist/cli/index.js.map +2 -2
- package/dist/core/index.js +30 -5
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +117 -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 +71 -16
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +156 -24
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +101 -18
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +291 -102
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +71 -16
- package/dist/server/api/index.js.map +2 -2
- package/dist/server/index.js +71 -16
- package/dist/server/index.js.map +2 -2
- package/dist/services/memory-service.js +71 -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 +1138 -1
- package/memory/session_summary/uncategorized/2026-03-04.md +31 -0
- package/memory/tool_observation/uncategorized/2026-03-04.md +785 -1
- package/memory/user_prompt/uncategorized/2026-03-04.md +438 -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 +15 -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
|
@@ -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
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
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(
|
|
76
|
+
function createDatabase(path7, options) {
|
|
77
77
|
if (options?.readOnly) {
|
|
78
|
-
return new duckdb.Database(
|
|
78
|
+
return new duckdb.Database(path7, { access_mode: "READ_ONLY" });
|
|
79
79
|
}
|
|
80
|
-
return new duckdb.Database(
|
|
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(
|
|
765
|
-
const dir = nodePath.dirname(
|
|
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(
|
|
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(([
|
|
3506
|
-
const actual =
|
|
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
|
-
|
|
6046
|
-
|
|
6047
|
-
this.
|
|
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.
|
|
6055
|
-
{ intervalMs: 3e4, batchSize: 500 }
|
|
6075
|
+
this.graduation
|
|
6056
6076
|
);
|
|
6057
|
-
this.
|
|
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 || "
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
7242
|
-
|
|
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
|
-
|
|
7247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7324
|
-
|
|
7325
|
-
semanticService.retrieveMemories(input.prompt, {
|
|
7326
|
-
topK: MAX_MEMORIES,
|
|
7327
|
-
minScore,
|
|
7509
|
+
mergedMemories = await retrieveSemanticMemories(
|
|
7510
|
+
{
|
|
7328
7511
|
sessionId: input.session_id,
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
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(
|
|
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(
|
|
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,
|