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
|
@@ -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,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(([
|
|
3508
|
-
const actual =
|
|
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
|
-
|
|
6048
|
-
|
|
6049
|
-
this.
|
|
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.
|
|
6057
|
-
{ intervalMs: 3e4, batchSize: 500 }
|
|
6075
|
+
this.graduation
|
|
6058
6076
|
);
|
|
6059
|
-
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
|
+
}
|
|
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 || "
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
7244
|
-
|
|
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
|
-
|
|
7249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7326
|
-
|
|
7327
|
-
semanticService.retrieveMemories(input.prompt, {
|
|
7328
|
-
topK: MAX_MEMORIES,
|
|
7329
|
-
minScore,
|
|
7509
|
+
mergedMemories = await retrieveSemanticMemories(
|
|
7510
|
+
{
|
|
7330
7511
|
sessionId: input.session_id,
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
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(
|
|
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(
|
|
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,
|