claude-memory-layer 1.0.18 → 1.0.20
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/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +532 -79
- package/dist/cli/index.js.map +3 -3
- package/dist/core/index.js +49 -4
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +140 -3
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +140 -3
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +140 -3
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +140 -3
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +379 -34
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +467 -34
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +474 -41
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +140 -3
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +362 -4
- package/dist/ui/index.html +90 -0
- package/dist/ui/style.css +41 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/cli/index.ts +23 -1
- package/src/core/embedder.ts +3 -2
- package/src/core/sqlite-event-store.ts +32 -0
- package/src/core/types.ts +2 -2
- package/src/core/vector-store.ts +20 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +7 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +119 -2
- package/src/ui/app.js +362 -4
- package/src/ui/index.html +90 -0
- package/src/ui/style.css +41 -0
|
@@ -8,6 +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
14
|
|
|
12
15
|
// src/services/memory-service.ts
|
|
13
16
|
import * as path3 from "path";
|
|
@@ -70,11 +73,11 @@ function toDate(value) {
|
|
|
70
73
|
return new Date(value);
|
|
71
74
|
return new Date(String(value));
|
|
72
75
|
}
|
|
73
|
-
function createDatabase(
|
|
76
|
+
function createDatabase(path6, options) {
|
|
74
77
|
if (options?.readOnly) {
|
|
75
|
-
return new duckdb.Database(
|
|
78
|
+
return new duckdb.Database(path6, { access_mode: "READ_ONLY" });
|
|
76
79
|
}
|
|
77
|
-
return new duckdb.Database(
|
|
80
|
+
return new duckdb.Database(path6);
|
|
78
81
|
}
|
|
79
82
|
function dbRun(db, sql, params = []) {
|
|
80
83
|
return new Promise((resolve2, reject) => {
|
|
@@ -758,12 +761,12 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
758
761
|
import Database from "better-sqlite3";
|
|
759
762
|
import * as fs from "fs";
|
|
760
763
|
import * as nodePath from "path";
|
|
761
|
-
function createSQLiteDatabase(
|
|
762
|
-
const dir = nodePath.dirname(
|
|
764
|
+
function createSQLiteDatabase(path6, options) {
|
|
765
|
+
const dir = nodePath.dirname(path6);
|
|
763
766
|
if (!fs.existsSync(dir)) {
|
|
764
767
|
fs.mkdirSync(dir, { recursive: true });
|
|
765
768
|
}
|
|
766
|
-
const db = new Database(
|
|
769
|
+
const db = new Database(path6, {
|
|
767
770
|
readonly: options?.readonly ?? false
|
|
768
771
|
});
|
|
769
772
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -1602,6 +1605,33 @@ var SQLiteEventStore = class {
|
|
|
1602
1605
|
ids
|
|
1603
1606
|
);
|
|
1604
1607
|
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Clear embedding outbox (used for embedding model migration)
|
|
1610
|
+
*/
|
|
1611
|
+
async clearEmbeddingOutbox() {
|
|
1612
|
+
await this.initialize();
|
|
1613
|
+
sqliteRun(this.db, `DELETE FROM embedding_outbox`);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Count total events
|
|
1617
|
+
*/
|
|
1618
|
+
async countEvents() {
|
|
1619
|
+
await this.initialize();
|
|
1620
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
1621
|
+
return row?.count || 0;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
1625
|
+
*/
|
|
1626
|
+
async getEventsPage(limit = 1e3, offset = 0) {
|
|
1627
|
+
await this.initialize();
|
|
1628
|
+
const rows = sqliteAll(
|
|
1629
|
+
this.db,
|
|
1630
|
+
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
1631
|
+
[limit, offset]
|
|
1632
|
+
);
|
|
1633
|
+
return rows.map(this.rowToEvent);
|
|
1634
|
+
}
|
|
1605
1635
|
/**
|
|
1606
1636
|
* Mark outbox items as failed
|
|
1607
1637
|
*/
|
|
@@ -2567,6 +2597,23 @@ var VectorStore = class {
|
|
|
2567
2597
|
const result = await this.table.countRows();
|
|
2568
2598
|
return result;
|
|
2569
2599
|
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Clear all vectors (used for embedding model migration)
|
|
2602
|
+
*/
|
|
2603
|
+
async clearAll() {
|
|
2604
|
+
await this.initialize();
|
|
2605
|
+
if (!this.db)
|
|
2606
|
+
return;
|
|
2607
|
+
try {
|
|
2608
|
+
if (typeof this.db.dropTable === "function") {
|
|
2609
|
+
await this.db.dropTable(this.tableName);
|
|
2610
|
+
} else if (typeof this.db.drop_table === "function") {
|
|
2611
|
+
await this.db.drop_table(this.tableName);
|
|
2612
|
+
}
|
|
2613
|
+
} catch {
|
|
2614
|
+
}
|
|
2615
|
+
this.table = null;
|
|
2616
|
+
}
|
|
2570
2617
|
/**
|
|
2571
2618
|
* Check if vector exists for event
|
|
2572
2619
|
*/
|
|
@@ -2584,7 +2631,7 @@ var Embedder = class {
|
|
|
2584
2631
|
pipeline = null;
|
|
2585
2632
|
modelName;
|
|
2586
2633
|
initialized = false;
|
|
2587
|
-
constructor(modelName = "
|
|
2634
|
+
constructor(modelName = "jinaai/jina-embeddings-v5-text-nano") {
|
|
2588
2635
|
this.modelName = modelName;
|
|
2589
2636
|
}
|
|
2590
2637
|
/**
|
|
@@ -2664,8 +2711,9 @@ var Embedder = class {
|
|
|
2664
2711
|
};
|
|
2665
2712
|
var defaultEmbedder = null;
|
|
2666
2713
|
function getDefaultEmbedder() {
|
|
2714
|
+
const envModel = process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
2667
2715
|
if (!defaultEmbedder) {
|
|
2668
|
-
defaultEmbedder = new Embedder();
|
|
2716
|
+
defaultEmbedder = new Embedder(envModel || void 0);
|
|
2669
2717
|
}
|
|
2670
2718
|
return defaultEmbedder;
|
|
2671
2719
|
}
|
|
@@ -3439,8 +3487,8 @@ _Context:_ ${sessionContext}`;
|
|
|
3439
3487
|
matchesMetadataScope(metadata, expected) {
|
|
3440
3488
|
if (!metadata)
|
|
3441
3489
|
return false;
|
|
3442
|
-
return Object.entries(expected).every(([
|
|
3443
|
-
const actual =
|
|
3490
|
+
return Object.entries(expected).every(([path6, value]) => {
|
|
3491
|
+
const actual = path6.split(".").reduce((acc, key) => {
|
|
3444
3492
|
if (typeof acc !== "object" || acc === null)
|
|
3445
3493
|
return void 0;
|
|
3446
3494
|
return acc[key];
|
|
@@ -5900,8 +5948,10 @@ var MemoryService = class {
|
|
|
5900
5948
|
readOnly;
|
|
5901
5949
|
lightweightMode;
|
|
5902
5950
|
mdMirror;
|
|
5951
|
+
storagePath;
|
|
5903
5952
|
constructor(config) {
|
|
5904
5953
|
const storagePath = this.expandPath(config.storagePath);
|
|
5954
|
+
this.storagePath = storagePath;
|
|
5905
5955
|
this.readOnly = config.readOnly ?? false;
|
|
5906
5956
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
5907
5957
|
this.mdMirror = new MarkdownMirror2(process.cwd());
|
|
@@ -5937,7 +5987,8 @@ var MemoryService = class {
|
|
|
5937
5987
|
);
|
|
5938
5988
|
}
|
|
5939
5989
|
this.vectorStore = new VectorStore(path3.join(storagePath, "vectors"));
|
|
5940
|
-
|
|
5990
|
+
const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
5991
|
+
this.embedder = embeddingModel ? new Embedder(embeddingModel) : getDefaultEmbedder();
|
|
5941
5992
|
this.matcher = getDefaultMatcher();
|
|
5942
5993
|
this.retriever = createRetriever(
|
|
5943
5994
|
this.sqliteStore,
|
|
@@ -6881,6 +6932,95 @@ var MemoryService = class {
|
|
|
6881
6932
|
recordMemoryAccess(eventId, sessionId, confidence = 1) {
|
|
6882
6933
|
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
6883
6934
|
}
|
|
6935
|
+
getEmbeddingModelName() {
|
|
6936
|
+
return this.embedder.getModelName();
|
|
6937
|
+
}
|
|
6938
|
+
/**
|
|
6939
|
+
* Ensure embedding model metadata is in sync and optionally migrate vectors.
|
|
6940
|
+
* Migration strategy: clear vector index + clear embedding outbox + re-enqueue all events.
|
|
6941
|
+
*/
|
|
6942
|
+
async ensureEmbeddingModelForImport(options) {
|
|
6943
|
+
await this.initialize();
|
|
6944
|
+
const currentModel = this.getEmbeddingModelName();
|
|
6945
|
+
const metaPath = path3.join(this.storagePath, "embedding-meta.json");
|
|
6946
|
+
let previousModel = null;
|
|
6947
|
+
try {
|
|
6948
|
+
if (fs4.existsSync(metaPath)) {
|
|
6949
|
+
const parsed = JSON.parse(fs4.readFileSync(metaPath, "utf-8"));
|
|
6950
|
+
previousModel = parsed?.model || null;
|
|
6951
|
+
}
|
|
6952
|
+
} catch {
|
|
6953
|
+
previousModel = null;
|
|
6954
|
+
}
|
|
6955
|
+
const stats = await this.getStats();
|
|
6956
|
+
const hasExistingVectors = (stats.vectorCount || 0) > 0;
|
|
6957
|
+
if (!previousModel && !hasExistingVectors) {
|
|
6958
|
+
fs4.writeFileSync(metaPath, JSON.stringify({ model: currentModel, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
6959
|
+
return { changed: false, previousModel: null, currentModel, enqueued: 0, reason: "initialized-meta" };
|
|
6960
|
+
}
|
|
6961
|
+
const modelChanged = previousModel !== currentModel;
|
|
6962
|
+
const legacyUnknownButVectorsExist = !previousModel && hasExistingVectors;
|
|
6963
|
+
if (!modelChanged && !legacyUnknownButVectorsExist) {
|
|
6964
|
+
return { changed: false, previousModel, currentModel, enqueued: 0 };
|
|
6965
|
+
}
|
|
6966
|
+
if (options?.autoMigrate === false) {
|
|
6967
|
+
return {
|
|
6968
|
+
changed: true,
|
|
6969
|
+
previousModel,
|
|
6970
|
+
currentModel,
|
|
6971
|
+
enqueued: 0,
|
|
6972
|
+
reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
|
|
6973
|
+
};
|
|
6974
|
+
}
|
|
6975
|
+
const wasRunning = this.vectorWorker?.isRunning() || false;
|
|
6976
|
+
if (wasRunning)
|
|
6977
|
+
this.vectorWorker?.stop();
|
|
6978
|
+
await this.vectorStore.clearAll();
|
|
6979
|
+
await this.sqliteStore.clearEmbeddingOutbox();
|
|
6980
|
+
const pageSize = 1e3;
|
|
6981
|
+
let offset = 0;
|
|
6982
|
+
let enqueued = 0;
|
|
6983
|
+
while (true) {
|
|
6984
|
+
const page = await this.sqliteStore.getEventsPage(pageSize, offset);
|
|
6985
|
+
if (page.length === 0)
|
|
6986
|
+
break;
|
|
6987
|
+
for (const event of page) {
|
|
6988
|
+
await this.sqliteStore.enqueueForEmbedding(event.id, event.content);
|
|
6989
|
+
enqueued += 1;
|
|
6990
|
+
}
|
|
6991
|
+
offset += page.length;
|
|
6992
|
+
if (page.length < pageSize)
|
|
6993
|
+
break;
|
|
6994
|
+
}
|
|
6995
|
+
fs4.writeFileSync(
|
|
6996
|
+
metaPath,
|
|
6997
|
+
JSON.stringify(
|
|
6998
|
+
{
|
|
6999
|
+
model: currentModel,
|
|
7000
|
+
previousModel,
|
|
7001
|
+
migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7002
|
+
enqueued
|
|
7003
|
+
},
|
|
7004
|
+
null,
|
|
7005
|
+
2
|
|
7006
|
+
)
|
|
7007
|
+
);
|
|
7008
|
+
if (wasRunning)
|
|
7009
|
+
this.vectorWorker?.start();
|
|
7010
|
+
return {
|
|
7011
|
+
changed: true,
|
|
7012
|
+
previousModel,
|
|
7013
|
+
currentModel,
|
|
7014
|
+
enqueued,
|
|
7015
|
+
reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
|
|
7016
|
+
};
|
|
7017
|
+
}
|
|
7018
|
+
/**
|
|
7019
|
+
* Backward-compatible alias used by some hooks
|
|
7020
|
+
*/
|
|
7021
|
+
async close() {
|
|
7022
|
+
await this.shutdown();
|
|
7023
|
+
}
|
|
6884
7024
|
/**
|
|
6885
7025
|
* Shutdown service
|
|
6886
7026
|
*/
|
|
@@ -6916,6 +7056,42 @@ var MemoryService = class {
|
|
|
6916
7056
|
}
|
|
6917
7057
|
};
|
|
6918
7058
|
var serviceCache = /* @__PURE__ */ new Map();
|
|
7059
|
+
var GLOBAL_KEY = "__global__";
|
|
7060
|
+
function getDefaultMemoryService() {
|
|
7061
|
+
if (!serviceCache.has(GLOBAL_KEY)) {
|
|
7062
|
+
serviceCache.set(GLOBAL_KEY, new MemoryService({
|
|
7063
|
+
storagePath: "~/.claude-code/memory",
|
|
7064
|
+
analyticsEnabled: false,
|
|
7065
|
+
// Hooks don't need DuckDB
|
|
7066
|
+
sharedStoreConfig: { enabled: false }
|
|
7067
|
+
// Shared store uses DuckDB too
|
|
7068
|
+
}));
|
|
7069
|
+
}
|
|
7070
|
+
return serviceCache.get(GLOBAL_KEY);
|
|
7071
|
+
}
|
|
7072
|
+
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
7073
|
+
const hash = hashProjectPath(projectPath);
|
|
7074
|
+
if (!serviceCache.has(hash)) {
|
|
7075
|
+
const storagePath = getProjectStoragePath(projectPath);
|
|
7076
|
+
serviceCache.set(hash, new MemoryService({
|
|
7077
|
+
storagePath,
|
|
7078
|
+
projectHash: hash,
|
|
7079
|
+
projectPath,
|
|
7080
|
+
// Override shared store config - hooks don't need DuckDB
|
|
7081
|
+
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
7082
|
+
analyticsEnabled: false
|
|
7083
|
+
// Hooks don't need DuckDB
|
|
7084
|
+
}));
|
|
7085
|
+
}
|
|
7086
|
+
return serviceCache.get(hash);
|
|
7087
|
+
}
|
|
7088
|
+
function getMemoryServiceForSession(sessionId) {
|
|
7089
|
+
const projectInfo = getSessionProject(sessionId);
|
|
7090
|
+
if (projectInfo) {
|
|
7091
|
+
return getMemoryServiceForProject(projectInfo.projectPath);
|
|
7092
|
+
}
|
|
7093
|
+
return getDefaultMemoryService();
|
|
7094
|
+
}
|
|
6919
7095
|
function getLightweightMemoryService(sessionId) {
|
|
6920
7096
|
const projectInfo = getSessionProject(sessionId);
|
|
6921
7097
|
const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : "lightweight_global";
|
|
@@ -6968,6 +7144,10 @@ var MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5");
|
|
|
6968
7144
|
var BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.4");
|
|
6969
7145
|
var FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || "0.3");
|
|
6970
7146
|
var ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== "false";
|
|
7147
|
+
var RETRIEVAL_MODE = process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || "hybrid";
|
|
7148
|
+
var SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || "1200");
|
|
7149
|
+
var ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || "3");
|
|
7150
|
+
var ADHERENCE_STATE_DIR = path5.join(os3.homedir(), ".claude-code", "memory");
|
|
6971
7151
|
function shouldStorePrompt(prompt) {
|
|
6972
7152
|
const trimmed = prompt.trim();
|
|
6973
7153
|
if (trimmed.startsWith("/"))
|
|
@@ -6986,6 +7166,113 @@ function getDynamicMinScore(prompt) {
|
|
|
6986
7166
|
return Math.max(0.3, BASE_MIN_SCORE - 0.05);
|
|
6987
7167
|
return BASE_MIN_SCORE;
|
|
6988
7168
|
}
|
|
7169
|
+
function withTimeout(promise, timeoutMs) {
|
|
7170
|
+
return new Promise((resolve2, reject) => {
|
|
7171
|
+
const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
|
|
7172
|
+
promise.then((result) => {
|
|
7173
|
+
clearTimeout(timer);
|
|
7174
|
+
resolve2(result);
|
|
7175
|
+
}).catch((error) => {
|
|
7176
|
+
clearTimeout(timer);
|
|
7177
|
+
reject(error);
|
|
7178
|
+
});
|
|
7179
|
+
});
|
|
7180
|
+
}
|
|
7181
|
+
function formatMemoryContext(items) {
|
|
7182
|
+
if (items.length === 0)
|
|
7183
|
+
return "";
|
|
7184
|
+
const lines = items.map((m) => {
|
|
7185
|
+
const preview = m.content.length > 300 ? m.content.substring(0, 300) + "..." : m.content;
|
|
7186
|
+
return `- [${m.type}] ${preview}`;
|
|
7187
|
+
});
|
|
7188
|
+
return `\u{1F4A1} **Related memories found:**
|
|
7189
|
+
|
|
7190
|
+
${lines.join("\n\n")}`;
|
|
7191
|
+
}
|
|
7192
|
+
function getAdherenceStatePath(sessionId) {
|
|
7193
|
+
return path5.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
|
|
7194
|
+
}
|
|
7195
|
+
function readAdherenceState(sessionId) {
|
|
7196
|
+
try {
|
|
7197
|
+
const filePath = getAdherenceStatePath(sessionId);
|
|
7198
|
+
if (!fs6.existsSync(filePath)) {
|
|
7199
|
+
return {
|
|
7200
|
+
sessionId,
|
|
7201
|
+
turnCount: 0,
|
|
7202
|
+
lastCheckedTurn: 0,
|
|
7203
|
+
lastPrompt: "",
|
|
7204
|
+
lastReason: "init",
|
|
7205
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7206
|
+
};
|
|
7207
|
+
}
|
|
7208
|
+
const data = fs6.readFileSync(filePath, "utf8");
|
|
7209
|
+
const parsed = JSON.parse(data);
|
|
7210
|
+
if (parsed.sessionId !== sessionId)
|
|
7211
|
+
throw new Error("session mismatch");
|
|
7212
|
+
return parsed;
|
|
7213
|
+
} catch {
|
|
7214
|
+
return {
|
|
7215
|
+
sessionId,
|
|
7216
|
+
turnCount: 0,
|
|
7217
|
+
lastCheckedTurn: 0,
|
|
7218
|
+
lastPrompt: "",
|
|
7219
|
+
lastReason: "init",
|
|
7220
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7221
|
+
};
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
function writeAdherenceState(state) {
|
|
7225
|
+
try {
|
|
7226
|
+
if (!fs6.existsSync(ADHERENCE_STATE_DIR)) {
|
|
7227
|
+
fs6.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
|
|
7228
|
+
}
|
|
7229
|
+
const filePath = getAdherenceStatePath(state.sessionId);
|
|
7230
|
+
const tempPath = filePath + ".tmp";
|
|
7231
|
+
fs6.writeFileSync(tempPath, JSON.stringify(state));
|
|
7232
|
+
fs6.renameSync(tempPath, filePath);
|
|
7233
|
+
} catch {
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
function hasWriteIntent(prompt) {
|
|
7237
|
+
return /(fix|refactor|implement|change|modify|edit|update|rewrite|patch|create|add|remove|delete|버그|수정|리팩터|구현|추가|삭제|개선)/i.test(prompt);
|
|
7238
|
+
}
|
|
7239
|
+
function tokenize(text) {
|
|
7240
|
+
const stopwords = /* @__PURE__ */ new Set(["the", "and", "for", "with", "that", "this", "from", "have", "what", "when", "where", "how", "why", "\uADF8\uB9AC\uACE0", "\uADF8\uB9AC\uACE0\uC694", "\uC774\uAC70", "\uADF8\uAC70", "\uD574\uC8FC\uC138\uC694", "\uD574\uC918", "\uC880", "\uC5D0\uC11C", "\uC73C\uB85C", "\uD558\uB294", "\uD574"]);
|
|
7241
|
+
return text.toLowerCase().replace(/[^a-z0-9가-힣\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !stopwords.has(w));
|
|
7242
|
+
}
|
|
7243
|
+
function isTopicShift(currentPrompt, lastPrompt) {
|
|
7244
|
+
if (!lastPrompt || lastPrompt.length < 10)
|
|
7245
|
+
return false;
|
|
7246
|
+
const a = new Set(tokenize(currentPrompt));
|
|
7247
|
+
const b = new Set(tokenize(lastPrompt));
|
|
7248
|
+
if (a.size === 0 || b.size === 0)
|
|
7249
|
+
return false;
|
|
7250
|
+
let intersection = 0;
|
|
7251
|
+
for (const token of a) {
|
|
7252
|
+
if (b.has(token))
|
|
7253
|
+
intersection++;
|
|
7254
|
+
}
|
|
7255
|
+
const union = a.size + b.size - intersection;
|
|
7256
|
+
const similarity = union > 0 ? intersection / union : 0;
|
|
7257
|
+
return similarity < 0.2;
|
|
7258
|
+
}
|
|
7259
|
+
function shouldRunAdherenceCheck(turnCount, prompt, state) {
|
|
7260
|
+
if (turnCount === 1)
|
|
7261
|
+
return { run: true, reason: "first-turn" };
|
|
7262
|
+
if (hasWriteIntent(prompt))
|
|
7263
|
+
return { run: true, reason: "write-intent" };
|
|
7264
|
+
if (isTopicShift(prompt, state.lastPrompt))
|
|
7265
|
+
return { run: true, reason: "topic-shift" };
|
|
7266
|
+
if (turnCount - state.lastCheckedTurn >= ADHERENCE_INTERVAL_TURNS)
|
|
7267
|
+
return { run: true, reason: "interval" };
|
|
7268
|
+
return { run: false, reason: "skip" };
|
|
7269
|
+
}
|
|
7270
|
+
function logAdherenceDecision(sessionId, turn, run, reason) {
|
|
7271
|
+
if (!process.env.CLAUDE_MEMORY_DEBUG)
|
|
7272
|
+
return;
|
|
7273
|
+
const mode = run ? "enforced" : "skipped";
|
|
7274
|
+
console.error(`[adherence] session=${sessionId} turn=${turn} mode=${mode} reason=${reason}`);
|
|
7275
|
+
}
|
|
6989
7276
|
async function main() {
|
|
6990
7277
|
const inputData = await readStdin();
|
|
6991
7278
|
const input = JSON.parse(inputData);
|
|
@@ -6993,49 +7280,107 @@ async function main() {
|
|
|
6993
7280
|
writeTurnState(input.session_id, turnId);
|
|
6994
7281
|
const memoryService = getLightweightMemoryService(input.session_id);
|
|
6995
7282
|
try {
|
|
7283
|
+
let context = "";
|
|
7284
|
+
const adherenceState = readAdherenceState(input.session_id);
|
|
7285
|
+
const currentTurn = adherenceState.turnCount + 1;
|
|
7286
|
+
const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
|
|
7287
|
+
logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
|
|
6996
7288
|
if (shouldStorePrompt(input.prompt)) {
|
|
6997
7289
|
await memoryService.storeUserPrompt(
|
|
6998
7290
|
input.session_id,
|
|
6999
7291
|
input.prompt,
|
|
7000
|
-
{
|
|
7292
|
+
{
|
|
7293
|
+
turnId,
|
|
7294
|
+
adherence: {
|
|
7295
|
+
checked: adherenceDecision.run,
|
|
7296
|
+
reason: adherenceDecision.reason,
|
|
7297
|
+
turn: currentTurn
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7001
7300
|
);
|
|
7002
7301
|
}
|
|
7003
|
-
|
|
7004
|
-
if (ENABLE_SEARCH && input.prompt.length > 10) {
|
|
7302
|
+
if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
|
|
7005
7303
|
const minScore = getDynamicMinScore(input.prompt);
|
|
7006
|
-
let
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7304
|
+
let mergedMemories = [];
|
|
7305
|
+
const canUseSemantic = RETRIEVAL_MODE === "semantic" || RETRIEVAL_MODE === "hybrid";
|
|
7306
|
+
if (canUseSemantic) {
|
|
7307
|
+
try {
|
|
7308
|
+
const semanticService = getMemoryServiceForSession(input.session_id);
|
|
7309
|
+
const semantic = await withTimeout(
|
|
7310
|
+
semanticService.retrieveMemories(input.prompt, {
|
|
7311
|
+
topK: MAX_MEMORIES,
|
|
7312
|
+
minScore,
|
|
7313
|
+
sessionId: input.session_id,
|
|
7314
|
+
intentRewrite: true,
|
|
7315
|
+
adaptiveRerank: true,
|
|
7316
|
+
projectScopeMode: "strict"
|
|
7317
|
+
}),
|
|
7318
|
+
SEMANTIC_TIMEOUT_MS
|
|
7319
|
+
);
|
|
7320
|
+
mergedMemories = semantic.memories.map((m) => ({
|
|
7321
|
+
type: m.event.eventType,
|
|
7322
|
+
content: m.event.content,
|
|
7323
|
+
id: m.event.id,
|
|
7324
|
+
score: m.score
|
|
7325
|
+
}));
|
|
7326
|
+
} catch {
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
const shouldUseKeywordFallback = RETRIEVAL_MODE === "keyword" || RETRIEVAL_MODE === "hybrid" || mergedMemories.length === 0;
|
|
7330
|
+
if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
|
|
7331
|
+
let results = await memoryService.keywordSearch(input.prompt, {
|
|
7012
7332
|
topK: MAX_MEMORIES,
|
|
7013
|
-
minScore
|
|
7333
|
+
minScore
|
|
7014
7334
|
});
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7335
|
+
if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
|
|
7336
|
+
results = await memoryService.keywordSearch(input.prompt, {
|
|
7337
|
+
topK: MAX_MEMORIES,
|
|
7338
|
+
minScore: FALLBACK_MIN_SCORE
|
|
7339
|
+
});
|
|
7340
|
+
}
|
|
7341
|
+
const existingIds = new Set(mergedMemories.map((m) => m.id).filter(Boolean));
|
|
7019
7342
|
for (const r of results) {
|
|
7343
|
+
if (existingIds.has(r.event.id))
|
|
7344
|
+
continue;
|
|
7345
|
+
mergedMemories.push({
|
|
7346
|
+
type: r.event.eventType,
|
|
7347
|
+
content: r.event.content,
|
|
7348
|
+
id: r.event.id,
|
|
7349
|
+
score: r.score
|
|
7350
|
+
});
|
|
7351
|
+
if (mergedMemories.length >= MAX_MEMORIES)
|
|
7352
|
+
break;
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
if (mergedMemories.length > 0) {
|
|
7356
|
+
const eventIds = mergedMemories.map((m) => m.id).filter((v) => Boolean(v));
|
|
7357
|
+
if (eventIds.length > 0) {
|
|
7358
|
+
await memoryService.incrementMemoryAccess(eventIds);
|
|
7359
|
+
}
|
|
7360
|
+
for (const m of mergedMemories) {
|
|
7361
|
+
if (!m.id)
|
|
7362
|
+
continue;
|
|
7020
7363
|
try {
|
|
7021
7364
|
await memoryService.recordRetrieval(
|
|
7022
|
-
|
|
7365
|
+
m.id,
|
|
7023
7366
|
input.session_id,
|
|
7024
|
-
|
|
7367
|
+
m.score ?? minScore,
|
|
7025
7368
|
input.prompt
|
|
7026
7369
|
);
|
|
7027
7370
|
} catch {
|
|
7028
7371
|
}
|
|
7029
7372
|
}
|
|
7030
|
-
|
|
7031
|
-
const preview = r.event.content.length > 300 ? r.event.content.substring(0, 300) + "..." : r.event.content;
|
|
7032
|
-
return `- [${r.event.eventType}] ${preview}`;
|
|
7033
|
-
});
|
|
7034
|
-
context = `\u{1F4A1} **Related memories found:**
|
|
7035
|
-
|
|
7036
|
-
${memories.join("\n\n")}`;
|
|
7373
|
+
context = formatMemoryContext(mergedMemories);
|
|
7037
7374
|
}
|
|
7038
7375
|
}
|
|
7376
|
+
writeAdherenceState({
|
|
7377
|
+
sessionId: input.session_id,
|
|
7378
|
+
turnCount: currentTurn,
|
|
7379
|
+
lastCheckedTurn: adherenceDecision.run ? currentTurn : adherenceState.lastCheckedTurn,
|
|
7380
|
+
lastPrompt: input.prompt,
|
|
7381
|
+
lastReason: adherenceDecision.reason,
|
|
7382
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7383
|
+
});
|
|
7039
7384
|
const output = { context };
|
|
7040
7385
|
console.log(JSON.stringify(output));
|
|
7041
7386
|
} catch (error) {
|