claude-memory-layer 1.0.37 → 1.0.38
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/dist/cli/index.js +497 -84
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +309 -22
- package/dist/core/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +399 -74
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +357 -67
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +357 -67
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +357 -67
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +398 -73
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +357 -67
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +318 -28
- package/dist/index.js.map +3 -3
- package/dist/mcp/index.js +403 -78
- package/dist/mcp/index.js.map +4 -4
- package/dist/server/api/index.js +361 -71
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +361 -71
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +357 -67
- package/dist/services/memory-service.js.map +4 -4
- package/package.json +1 -1
|
@@ -9,8 +9,8 @@ const __dirname = dirname(__filename);
|
|
|
9
9
|
import * as os5 from "os";
|
|
10
10
|
|
|
11
11
|
// src/core/engine/memory-service-composition.ts
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
12
|
+
import * as os2 from "os";
|
|
13
|
+
import * as path7 from "path";
|
|
14
14
|
|
|
15
15
|
// src/core/metadata-extractor.ts
|
|
16
16
|
function createToolObservationEmbedding(toolName, metadata, success) {
|
|
@@ -1521,8 +1521,8 @@ function createEndlessMemoryServices(options) {
|
|
|
1521
1521
|
}
|
|
1522
1522
|
|
|
1523
1523
|
// src/core/engine/memory-engine-services.ts
|
|
1524
|
-
import * as
|
|
1525
|
-
import * as
|
|
1524
|
+
import * as fs6 from "fs";
|
|
1525
|
+
import * as path5 from "path";
|
|
1526
1526
|
|
|
1527
1527
|
// src/extensions/vector/embedder.ts
|
|
1528
1528
|
var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
|
|
@@ -2200,14 +2200,39 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2200
2200
|
return `${sessionId}:${contentHash}`;
|
|
2201
2201
|
}
|
|
2202
2202
|
|
|
2203
|
+
// src/core/sqlite-event-store.ts
|
|
2204
|
+
import * as nodePath2 from "path";
|
|
2205
|
+
|
|
2206
|
+
// src/core/registry/project-path.ts
|
|
2207
|
+
import * as crypto2 from "crypto";
|
|
2208
|
+
import * as fs3 from "fs";
|
|
2209
|
+
import * as os from "os";
|
|
2210
|
+
import * as path3 from "path";
|
|
2211
|
+
function normalizeProjectPath(projectPath) {
|
|
2212
|
+
const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
2213
|
+
try {
|
|
2214
|
+
return fs3.realpathSync(expanded);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return path3.resolve(expanded);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function hashProjectPath(projectPath) {
|
|
2220
|
+
const normalizedPath = normalizeProjectPath(projectPath);
|
|
2221
|
+
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
2222
|
+
}
|
|
2223
|
+
function getProjectStoragePath(projectPath) {
|
|
2224
|
+
const hash = hashProjectPath(projectPath);
|
|
2225
|
+
return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2203
2228
|
// src/core/sqlite-wrapper.ts
|
|
2204
2229
|
import Database from "better-sqlite3";
|
|
2205
|
-
import * as
|
|
2230
|
+
import * as fs4 from "fs";
|
|
2206
2231
|
import * as nodePath from "path";
|
|
2207
2232
|
function createSQLiteDatabase(path11, options) {
|
|
2208
2233
|
const dir = nodePath.dirname(path11);
|
|
2209
|
-
if (!
|
|
2210
|
-
|
|
2234
|
+
if (!fs4.existsSync(dir)) {
|
|
2235
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2211
2236
|
}
|
|
2212
2237
|
const db = new Database(path11, {
|
|
2213
2238
|
readonly: options?.readonly ?? false
|
|
@@ -2256,8 +2281,8 @@ function toSQLiteTimestamp(date) {
|
|
|
2256
2281
|
}
|
|
2257
2282
|
|
|
2258
2283
|
// src/core/markdown-mirror.ts
|
|
2259
|
-
import * as
|
|
2260
|
-
import * as
|
|
2284
|
+
import * as fs5 from "fs/promises";
|
|
2285
|
+
import * as path4 from "path";
|
|
2261
2286
|
var DEFAULT_NAMESPACE = "default";
|
|
2262
2287
|
var DEFAULT_CATEGORY = "uncategorized";
|
|
2263
2288
|
function sanitizeSegment2(input, fallback) {
|
|
@@ -2286,7 +2311,7 @@ function buildMirrorPath2(rootDir, event) {
|
|
|
2286
2311
|
const yyyy = d.getFullYear();
|
|
2287
2312
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
2288
2313
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
2289
|
-
return
|
|
2314
|
+
return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
|
|
2290
2315
|
}
|
|
2291
2316
|
function formatMirrorEntry(event) {
|
|
2292
2317
|
const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
|
|
@@ -2307,8 +2332,8 @@ var MarkdownMirror2 = class {
|
|
|
2307
2332
|
}
|
|
2308
2333
|
async append(event) {
|
|
2309
2334
|
const outPath = buildMirrorPath2(this.rootDir, event);
|
|
2310
|
-
await
|
|
2311
|
-
await
|
|
2335
|
+
await fs5.mkdir(path4.dirname(outPath), { recursive: true });
|
|
2336
|
+
await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
|
|
2312
2337
|
return outPath;
|
|
2313
2338
|
}
|
|
2314
2339
|
};
|
|
@@ -2329,6 +2354,135 @@ function emptyOutboxRecoveryResult() {
|
|
|
2329
2354
|
vector: { recoveredProcessing: 0, retriedFailed: 0 }
|
|
2330
2355
|
};
|
|
2331
2356
|
}
|
|
2357
|
+
function isRecord(value) {
|
|
2358
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2359
|
+
}
|
|
2360
|
+
function getNestedRecord(root, path11) {
|
|
2361
|
+
let cursor = root;
|
|
2362
|
+
for (const key of path11) {
|
|
2363
|
+
if (!isRecord(cursor))
|
|
2364
|
+
return void 0;
|
|
2365
|
+
cursor = cursor[key];
|
|
2366
|
+
}
|
|
2367
|
+
return isRecord(cursor) ? cursor : void 0;
|
|
2368
|
+
}
|
|
2369
|
+
function getNestedString(root, path11) {
|
|
2370
|
+
let cursor = root;
|
|
2371
|
+
for (const key of path11) {
|
|
2372
|
+
if (!isRecord(cursor))
|
|
2373
|
+
return void 0;
|
|
2374
|
+
cursor = cursor[key];
|
|
2375
|
+
}
|
|
2376
|
+
return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
|
|
2377
|
+
}
|
|
2378
|
+
function metadataProjectHash(metadata) {
|
|
2379
|
+
return getNestedString(metadata, ["scope", "project", "hash"]);
|
|
2380
|
+
}
|
|
2381
|
+
function metadataProjectPaths(metadata) {
|
|
2382
|
+
const candidates = [
|
|
2383
|
+
getNestedString(metadata, ["projectPath"]),
|
|
2384
|
+
getNestedString(metadata, ["sourceProjectPath"]),
|
|
2385
|
+
getNestedString(metadata, ["scope", "project", "path"])
|
|
2386
|
+
];
|
|
2387
|
+
const paths = [];
|
|
2388
|
+
for (const value of candidates) {
|
|
2389
|
+
if (value && !paths.includes(value))
|
|
2390
|
+
paths.push(value);
|
|
2391
|
+
}
|
|
2392
|
+
return paths;
|
|
2393
|
+
}
|
|
2394
|
+
function metadataProjectPath(metadata) {
|
|
2395
|
+
return metadataProjectPaths(metadata)[0];
|
|
2396
|
+
}
|
|
2397
|
+
function isActiveQuarantinedMetadata(metadata) {
|
|
2398
|
+
const quarantine = getNestedRecord(metadata, ["quarantine"]);
|
|
2399
|
+
return quarantine?.status === "active";
|
|
2400
|
+
}
|
|
2401
|
+
function activeQuarantineStatusExpression(column = "metadata") {
|
|
2402
|
+
return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
|
|
2403
|
+
}
|
|
2404
|
+
function notActiveQuarantinedSql(column = "metadata") {
|
|
2405
|
+
return `${activeQuarantineStatusExpression(column)} != 'active'`;
|
|
2406
|
+
}
|
|
2407
|
+
function maybeQuarantinePredicate(options, column = "metadata") {
|
|
2408
|
+
return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
|
|
2409
|
+
}
|
|
2410
|
+
function safeParseMetadataValue(value) {
|
|
2411
|
+
if (!value)
|
|
2412
|
+
return void 0;
|
|
2413
|
+
if (typeof value === "object")
|
|
2414
|
+
return isRecord(value) ? value : void 0;
|
|
2415
|
+
if (typeof value !== "string")
|
|
2416
|
+
return void 0;
|
|
2417
|
+
try {
|
|
2418
|
+
const parsed = JSON.parse(value);
|
|
2419
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2420
|
+
} catch {
|
|
2421
|
+
return void 0;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
function isImportedOrLegacyScopedMetadata(metadata) {
|
|
2425
|
+
if (!metadata)
|
|
2426
|
+
return false;
|
|
2427
|
+
return Boolean(
|
|
2428
|
+
metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
function addMetadataTag(metadata, tag) {
|
|
2432
|
+
const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
|
|
2433
|
+
if (!current.includes(tag))
|
|
2434
|
+
metadata.tags = [...current, tag];
|
|
2435
|
+
}
|
|
2436
|
+
function buildRepairResult(projectHash, dryRun) {
|
|
2437
|
+
return {
|
|
2438
|
+
dryRun,
|
|
2439
|
+
projectHash,
|
|
2440
|
+
scanned: 0,
|
|
2441
|
+
repaired: 0,
|
|
2442
|
+
quarantined: 0,
|
|
2443
|
+
alreadyScoped: 0,
|
|
2444
|
+
skipped: 0,
|
|
2445
|
+
samples: []
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function normalizeRepoName(value) {
|
|
2449
|
+
return value.replace(/\.git$/i, "").trim().toLowerCase();
|
|
2450
|
+
}
|
|
2451
|
+
function projectBasename(projectPath) {
|
|
2452
|
+
if (!projectPath)
|
|
2453
|
+
return void 0;
|
|
2454
|
+
const trimmed = projectPath.replace(/[\\/]+$/, "");
|
|
2455
|
+
const basename2 = nodePath2.basename(trimmed);
|
|
2456
|
+
return basename2 ? normalizeRepoName(basename2) : void 0;
|
|
2457
|
+
}
|
|
2458
|
+
function isProjectScopeRepairExplanation(content) {
|
|
2459
|
+
const normalized = content.toLowerCase();
|
|
2460
|
+
const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
|
|
2461
|
+
const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
|
|
2462
|
+
return hasRepairContext && hasExplanationContext;
|
|
2463
|
+
}
|
|
2464
|
+
function hasConflictingContentProjectHint(content, projectPath) {
|
|
2465
|
+
const currentName = projectBasename(projectPath);
|
|
2466
|
+
if (!currentName)
|
|
2467
|
+
return false;
|
|
2468
|
+
if (isProjectScopeRepairExplanation(content))
|
|
2469
|
+
return false;
|
|
2470
|
+
const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
|
|
2471
|
+
let githubMatch;
|
|
2472
|
+
while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
|
|
2473
|
+
const repo = normalizeRepoName(githubMatch[2] || "");
|
|
2474
|
+
if (repo && repo !== currentName)
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
|
|
2478
|
+
let workspaceMatch;
|
|
2479
|
+
while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
|
|
2480
|
+
const repo = normalizeRepoName(workspaceMatch[1] || "");
|
|
2481
|
+
if (repo && repo !== currentName)
|
|
2482
|
+
return true;
|
|
2483
|
+
}
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2332
2486
|
var SQLiteEventStore = class {
|
|
2333
2487
|
db;
|
|
2334
2488
|
initialized = false;
|
|
@@ -2827,11 +2981,11 @@ var SQLiteEventStore = class {
|
|
|
2827
2981
|
/**
|
|
2828
2982
|
* Get events by session ID
|
|
2829
2983
|
*/
|
|
2830
|
-
async getSessionEvents(sessionId) {
|
|
2984
|
+
async getSessionEvents(sessionId, options) {
|
|
2831
2985
|
await this.initialize();
|
|
2832
2986
|
const rows = sqliteAll(
|
|
2833
2987
|
this.db,
|
|
2834
|
-
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
2988
|
+
`SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
2835
2989
|
[sessionId]
|
|
2836
2990
|
);
|
|
2837
2991
|
return rows.map(this.rowToEvent);
|
|
@@ -2839,11 +2993,11 @@ var SQLiteEventStore = class {
|
|
|
2839
2993
|
/**
|
|
2840
2994
|
* Get recent events
|
|
2841
2995
|
*/
|
|
2842
|
-
async getRecentEvents(limit = 100) {
|
|
2996
|
+
async getRecentEvents(limit = 100, options) {
|
|
2843
2997
|
await this.initialize();
|
|
2844
2998
|
const rows = sqliteAll(
|
|
2845
2999
|
this.db,
|
|
2846
|
-
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
3000
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
|
|
2847
3001
|
[limit]
|
|
2848
3002
|
);
|
|
2849
3003
|
return rows.map(this.rowToEvent);
|
|
@@ -2851,11 +3005,11 @@ var SQLiteEventStore = class {
|
|
|
2851
3005
|
/**
|
|
2852
3006
|
* Get event by ID
|
|
2853
3007
|
*/
|
|
2854
|
-
async getEvent(id) {
|
|
3008
|
+
async getEvent(id, options) {
|
|
2855
3009
|
await this.initialize();
|
|
2856
3010
|
const row = sqliteGet(
|
|
2857
3011
|
this.db,
|
|
2858
|
-
`SELECT * FROM events WHERE id =
|
|
3012
|
+
`SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
|
|
2859
3013
|
[id]
|
|
2860
3014
|
);
|
|
2861
3015
|
if (!row)
|
|
@@ -2865,11 +3019,11 @@ var SQLiteEventStore = class {
|
|
|
2865
3019
|
/**
|
|
2866
3020
|
* Get events since a timestamp (for sync)
|
|
2867
3021
|
*/
|
|
2868
|
-
async getEventsSince(timestamp, limit = 1e3) {
|
|
3022
|
+
async getEventsSince(timestamp, limit = 1e3, options) {
|
|
2869
3023
|
await this.initialize();
|
|
2870
3024
|
const rows = sqliteAll(
|
|
2871
3025
|
this.db,
|
|
2872
|
-
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
3026
|
+
`SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
|
|
2873
3027
|
[timestamp, limit]
|
|
2874
3028
|
);
|
|
2875
3029
|
return rows.map(this.rowToEvent);
|
|
@@ -2878,11 +3032,11 @@ var SQLiteEventStore = class {
|
|
|
2878
3032
|
* Get events since a SQLite rowid (for robust incremental replication).
|
|
2879
3033
|
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
2880
3034
|
*/
|
|
2881
|
-
async getEventsSinceRowid(lastRowid, limit = 1e3) {
|
|
3035
|
+
async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
|
|
2882
3036
|
await this.initialize();
|
|
2883
3037
|
const rows = sqliteAll(
|
|
2884
3038
|
this.db,
|
|
2885
|
-
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
3039
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
|
|
2886
3040
|
[lastRowid, limit]
|
|
2887
3041
|
);
|
|
2888
3042
|
return rows.map((row) => ({
|
|
@@ -3117,19 +3271,19 @@ var SQLiteEventStore = class {
|
|
|
3117
3271
|
/**
|
|
3118
3272
|
* Count total events
|
|
3119
3273
|
*/
|
|
3120
|
-
async countEvents() {
|
|
3274
|
+
async countEvents(options) {
|
|
3121
3275
|
await this.initialize();
|
|
3122
|
-
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
3276
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
|
|
3123
3277
|
return row?.count || 0;
|
|
3124
3278
|
}
|
|
3125
3279
|
/**
|
|
3126
3280
|
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
3127
3281
|
*/
|
|
3128
|
-
async getEventsPage(limit = 1e3, offset = 0) {
|
|
3282
|
+
async getEventsPage(limit = 1e3, offset = 0, options) {
|
|
3129
3283
|
await this.initialize();
|
|
3130
3284
|
const rows = sqliteAll(
|
|
3131
3285
|
this.db,
|
|
3132
|
-
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3286
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3133
3287
|
[limit, offset]
|
|
3134
3288
|
);
|
|
3135
3289
|
return rows.map(this.rowToEvent);
|
|
@@ -3203,6 +3357,145 @@ var SQLiteEventStore = class {
|
|
|
3203
3357
|
result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
|
|
3204
3358
|
return result;
|
|
3205
3359
|
}
|
|
3360
|
+
/**
|
|
3361
|
+
* Repair legacy imported events that predate canonical project scope metadata.
|
|
3362
|
+
*
|
|
3363
|
+
* Same-project legacy rows are tagged with scope.project.hash. Rows that look
|
|
3364
|
+
* imported but cannot be proven to belong to this project are quarantined so
|
|
3365
|
+
* dashboard default reads/search do not surface cross-project contamination.
|
|
3366
|
+
*/
|
|
3367
|
+
async repairLegacyProjectScope(options = {}) {
|
|
3368
|
+
await this.initialize();
|
|
3369
|
+
const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
|
|
3370
|
+
if (!projectHash) {
|
|
3371
|
+
throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
|
|
3372
|
+
}
|
|
3373
|
+
if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
|
|
3374
|
+
throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
|
|
3375
|
+
}
|
|
3376
|
+
const dryRun = options.dryRun === true;
|
|
3377
|
+
const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
|
|
3378
|
+
const result = buildRepairResult(projectHash, dryRun);
|
|
3379
|
+
const rows = sqliteAll(
|
|
3380
|
+
this.db,
|
|
3381
|
+
`SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
|
|
3382
|
+
FROM events e
|
|
3383
|
+
LEFT JOIN sessions s ON s.id = e.session_id
|
|
3384
|
+
ORDER BY e.timestamp ASC`,
|
|
3385
|
+
[]
|
|
3386
|
+
);
|
|
3387
|
+
const sample = (entry) => {
|
|
3388
|
+
if (result.samples.length < 20)
|
|
3389
|
+
result.samples.push(entry);
|
|
3390
|
+
};
|
|
3391
|
+
for (const row of rows) {
|
|
3392
|
+
result.scanned++;
|
|
3393
|
+
let metadata = {};
|
|
3394
|
+
let metadataParseInvalid = false;
|
|
3395
|
+
if (row.metadata) {
|
|
3396
|
+
const parsed = safeParseMetadataValue(row.metadata);
|
|
3397
|
+
if (parsed) {
|
|
3398
|
+
metadata = parsed;
|
|
3399
|
+
} else {
|
|
3400
|
+
metadataParseInvalid = true;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
if (isActiveQuarantinedMetadata(metadata)) {
|
|
3404
|
+
result.skipped++;
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3407
|
+
const currentHash = metadataProjectHash(metadata);
|
|
3408
|
+
const explicitPath = metadataProjectPath(metadata);
|
|
3409
|
+
const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
|
|
3410
|
+
const candidatePaths = metadataProjectPaths(metadata);
|
|
3411
|
+
if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
|
|
3412
|
+
candidatePaths.push(sessionProjectPath);
|
|
3413
|
+
}
|
|
3414
|
+
const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
|
|
3415
|
+
const pathHashes = candidatePaths.map((candidate) => {
|
|
3416
|
+
try {
|
|
3417
|
+
return { path: candidate, hash: hashProjectPath(candidate) };
|
|
3418
|
+
} catch {
|
|
3419
|
+
return { path: candidate, hash: void 0 };
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
|
|
3423
|
+
const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
|
|
3424
|
+
let action = "skipped";
|
|
3425
|
+
let reason;
|
|
3426
|
+
let observedProjectHash;
|
|
3427
|
+
if (foreignPath) {
|
|
3428
|
+
action = "quarantined";
|
|
3429
|
+
reason = "project-path-mismatch";
|
|
3430
|
+
observedProjectHash = foreignPath.hash;
|
|
3431
|
+
} else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
|
|
3432
|
+
action = "quarantined";
|
|
3433
|
+
reason = "content-project-mismatch";
|
|
3434
|
+
} else if (currentHash === projectHash) {
|
|
3435
|
+
result.alreadyScoped++;
|
|
3436
|
+
continue;
|
|
3437
|
+
} else if (currentHash && currentHash !== projectHash) {
|
|
3438
|
+
action = "quarantined";
|
|
3439
|
+
reason = "scope-hash-mismatch";
|
|
3440
|
+
observedProjectHash = currentHash;
|
|
3441
|
+
} else if (matchingPath) {
|
|
3442
|
+
action = "repaired";
|
|
3443
|
+
reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
|
|
3444
|
+
} else if (candidatePaths.length > 0) {
|
|
3445
|
+
action = "quarantined";
|
|
3446
|
+
reason = "project-path-mismatch";
|
|
3447
|
+
} else if (importedOrLegacy) {
|
|
3448
|
+
action = "quarantined";
|
|
3449
|
+
reason = "missing-project-scope";
|
|
3450
|
+
}
|
|
3451
|
+
if (action === "skipped" || !reason) {
|
|
3452
|
+
result.skipped++;
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
if (action === "repaired") {
|
|
3456
|
+
const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
|
|
3457
|
+
const project = isRecord(scope.project) ? { ...scope.project } : {};
|
|
3458
|
+
project.hash = projectHash;
|
|
3459
|
+
scope.project = project;
|
|
3460
|
+
metadata.scope = scope;
|
|
3461
|
+
metadata.repair = {
|
|
3462
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3463
|
+
legacyProjectScope: {
|
|
3464
|
+
action,
|
|
3465
|
+
reason,
|
|
3466
|
+
repairedAt: nowIso
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
addMetadataTag(metadata, `proj:${projectHash}`);
|
|
3470
|
+
result.repaired++;
|
|
3471
|
+
} else {
|
|
3472
|
+
metadata.quarantine = {
|
|
3473
|
+
...isRecord(metadata.quarantine) ? metadata.quarantine : {},
|
|
3474
|
+
status: "active",
|
|
3475
|
+
category: "project-scope",
|
|
3476
|
+
reason,
|
|
3477
|
+
detectedAt: nowIso,
|
|
3478
|
+
expectedProjectHash: projectHash,
|
|
3479
|
+
...observedProjectHash ? { observedProjectHash } : {}
|
|
3480
|
+
};
|
|
3481
|
+
metadata.repair = {
|
|
3482
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3483
|
+
legacyProjectScope: {
|
|
3484
|
+
action,
|
|
3485
|
+
reason,
|
|
3486
|
+
repairedAt: nowIso
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
addMetadataTag(metadata, "quarantine:project-scope");
|
|
3490
|
+
result.quarantined++;
|
|
3491
|
+
}
|
|
3492
|
+
sample({ eventId: row.id, action, reason });
|
|
3493
|
+
if (!dryRun) {
|
|
3494
|
+
sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return result;
|
|
3498
|
+
}
|
|
3206
3499
|
/**
|
|
3207
3500
|
* Get embedding/vector outbox health statistics
|
|
3208
3501
|
*/
|
|
@@ -3250,7 +3543,11 @@ var SQLiteEventStore = class {
|
|
|
3250
3543
|
await this.initialize();
|
|
3251
3544
|
const rows = sqliteAll(
|
|
3252
3545
|
this.db,
|
|
3253
|
-
`SELECT level, COUNT(*) as count
|
|
3546
|
+
`SELECT ml.level, COUNT(*) as count
|
|
3547
|
+
FROM memory_levels ml
|
|
3548
|
+
INNER JOIN events e ON e.id = ml.event_id
|
|
3549
|
+
WHERE ${notActiveQuarantinedSql("e.metadata")}
|
|
3550
|
+
GROUP BY ml.level`
|
|
3254
3551
|
);
|
|
3255
3552
|
return rows;
|
|
3256
3553
|
}
|
|
@@ -3266,6 +3563,7 @@ var SQLiteEventStore = class {
|
|
|
3266
3563
|
`SELECT e.* FROM events e
|
|
3267
3564
|
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
3268
3565
|
WHERE ml.level = ?
|
|
3566
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3269
3567
|
ORDER BY e.timestamp DESC
|
|
3270
3568
|
LIMIT ? OFFSET ?`,
|
|
3271
3569
|
[level, limit, offset]
|
|
@@ -3358,12 +3656,13 @@ var SQLiteEventStore = class {
|
|
|
3358
3656
|
/**
|
|
3359
3657
|
* Get most accessed memories (falls back to recent events if none accessed)
|
|
3360
3658
|
*/
|
|
3361
|
-
async getMostAccessed(limit = 10) {
|
|
3659
|
+
async getMostAccessed(limit = 10, options) {
|
|
3362
3660
|
await this.initialize();
|
|
3363
3661
|
let rows = sqliteAll(
|
|
3364
3662
|
this.db,
|
|
3365
3663
|
`SELECT * FROM events
|
|
3366
3664
|
WHERE access_count > 0
|
|
3665
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3367
3666
|
ORDER BY access_count DESC, last_accessed_at DESC
|
|
3368
3667
|
LIMIT ?`,
|
|
3369
3668
|
[limit]
|
|
@@ -3372,6 +3671,7 @@ var SQLiteEventStore = class {
|
|
|
3372
3671
|
rows = sqliteAll(
|
|
3373
3672
|
this.db,
|
|
3374
3673
|
`SELECT * FROM events
|
|
3674
|
+
WHERE ${maybeQuarantinePredicate(options)}
|
|
3375
3675
|
ORDER BY timestamp DESC
|
|
3376
3676
|
LIMIT ?`,
|
|
3377
3677
|
[limit]
|
|
@@ -3502,6 +3802,7 @@ var SQLiteEventStore = class {
|
|
|
3502
3802
|
FROM memory_helpfulness mh
|
|
3503
3803
|
JOIN events e ON e.id = mh.event_id
|
|
3504
3804
|
WHERE mh.measured_at IS NOT NULL
|
|
3805
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3505
3806
|
GROUP BY mh.event_id
|
|
3506
3807
|
ORDER BY avg_score DESC
|
|
3507
3808
|
LIMIT ?`,
|
|
@@ -3566,6 +3867,7 @@ var SQLiteEventStore = class {
|
|
|
3566
3867
|
FROM events_fts fts
|
|
3567
3868
|
JOIN events e ON e.id = fts.event_id
|
|
3568
3869
|
WHERE events_fts MATCH ?
|
|
3870
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3569
3871
|
ORDER BY fts.rank
|
|
3570
3872
|
LIMIT ?`,
|
|
3571
3873
|
[searchTerms, limit]
|
|
@@ -3580,6 +3882,7 @@ var SQLiteEventStore = class {
|
|
|
3580
3882
|
this.db,
|
|
3581
3883
|
`SELECT *, 0 as rank FROM events
|
|
3582
3884
|
WHERE content LIKE ?
|
|
3885
|
+
AND ${notActiveQuarantinedSql()}
|
|
3583
3886
|
ORDER BY timestamp DESC
|
|
3584
3887
|
LIMIT ?`,
|
|
3585
3888
|
[likePattern, limit]
|
|
@@ -3786,6 +4089,7 @@ var SQLiteEventStore = class {
|
|
|
3786
4089
|
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
3787
4090
|
FROM events
|
|
3788
4091
|
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4092
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3789
4093
|
GROUP BY turn_id
|
|
3790
4094
|
ORDER BY min_ts DESC
|
|
3791
4095
|
LIMIT ? OFFSET ?`,
|
|
@@ -3793,7 +4097,7 @@ var SQLiteEventStore = class {
|
|
|
3793
4097
|
);
|
|
3794
4098
|
const turns = [];
|
|
3795
4099
|
for (const turnRow of turnRows) {
|
|
3796
|
-
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
4100
|
+
const events = await this.getEventsByTurn(turnRow.turn_id, options);
|
|
3797
4101
|
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
3798
4102
|
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
3799
4103
|
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
@@ -3812,11 +4116,11 @@ var SQLiteEventStore = class {
|
|
|
3812
4116
|
/**
|
|
3813
4117
|
* Get all events for a specific turn_id
|
|
3814
4118
|
*/
|
|
3815
|
-
async getEventsByTurn(turnId) {
|
|
4119
|
+
async getEventsByTurn(turnId, options) {
|
|
3816
4120
|
await this.initialize();
|
|
3817
4121
|
const rows = sqliteAll(
|
|
3818
4122
|
this.db,
|
|
3819
|
-
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
4123
|
+
`SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3820
4124
|
[turnId]
|
|
3821
4125
|
);
|
|
3822
4126
|
return rows.map(this.rowToEvent);
|
|
@@ -3824,13 +4128,14 @@ var SQLiteEventStore = class {
|
|
|
3824
4128
|
/**
|
|
3825
4129
|
* Count total turns for a session
|
|
3826
4130
|
*/
|
|
3827
|
-
async countSessionTurns(sessionId) {
|
|
4131
|
+
async countSessionTurns(sessionId, options) {
|
|
3828
4132
|
await this.initialize();
|
|
3829
4133
|
const row = sqliteGet(
|
|
3830
4134
|
this.db,
|
|
3831
4135
|
`SELECT COUNT(DISTINCT turn_id) as count
|
|
3832
4136
|
FROM events
|
|
3833
|
-
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4137
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4138
|
+
AND ${maybeQuarantinePredicate(options)}`,
|
|
3834
4139
|
[sessionId]
|
|
3835
4140
|
);
|
|
3836
4141
|
return row?.count || 0;
|
|
@@ -3922,7 +4227,7 @@ var SQLiteEventStore = class {
|
|
|
3922
4227
|
content: row.content,
|
|
3923
4228
|
canonicalKey: row.canonical_key,
|
|
3924
4229
|
dedupeKey: row.dedupe_key,
|
|
3925
|
-
metadata:
|
|
4230
|
+
metadata: safeParseMetadataValue(row.metadata)
|
|
3926
4231
|
};
|
|
3927
4232
|
if (row.access_count !== void 0) {
|
|
3928
4233
|
event.access_count = row.access_count;
|
|
@@ -4517,6 +4822,10 @@ var MemoryQueryService = class {
|
|
|
4517
4822
|
await this.initialize();
|
|
4518
4823
|
return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
|
|
4519
4824
|
}
|
|
4825
|
+
async repairLegacyProjectScope(options) {
|
|
4826
|
+
await this.initialize();
|
|
4827
|
+
return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
|
|
4828
|
+
}
|
|
4520
4829
|
async getStats() {
|
|
4521
4830
|
await this.initialize();
|
|
4522
4831
|
const deps = this.getStatsDeps();
|
|
@@ -6139,18 +6448,18 @@ function assertDefaultRetrieverStore(eventStore) {
|
|
|
6139
6448
|
function createMemoryEngineServices(options) {
|
|
6140
6449
|
const factories = options.factories ?? {};
|
|
6141
6450
|
const storagePath = options.storagePath;
|
|
6142
|
-
if (!options.readOnly && !
|
|
6143
|
-
|
|
6451
|
+
if (!options.readOnly && !fs6.existsSync(storagePath)) {
|
|
6452
|
+
fs6.mkdirSync(storagePath, { recursive: true });
|
|
6144
6453
|
}
|
|
6145
6454
|
const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
|
|
6146
|
-
|
|
6455
|
+
path5.join(storagePath, "events.sqlite"),
|
|
6147
6456
|
{
|
|
6148
6457
|
readonly: options.readOnly,
|
|
6149
6458
|
markdownMirrorRoot: storagePath
|
|
6150
6459
|
}
|
|
6151
6460
|
);
|
|
6152
6461
|
const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
|
|
6153
|
-
|
|
6462
|
+
path5.join(storagePath, "vectors")
|
|
6154
6463
|
);
|
|
6155
6464
|
const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
6156
6465
|
const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
|
|
@@ -6561,8 +6870,8 @@ function createMemoryRuntimeService(deps) {
|
|
|
6561
6870
|
}
|
|
6562
6871
|
|
|
6563
6872
|
// src/extensions/shared-memory/shared-memory-services.ts
|
|
6564
|
-
import * as
|
|
6565
|
-
import * as
|
|
6873
|
+
import * as fs7 from "fs";
|
|
6874
|
+
import * as path6 from "path";
|
|
6566
6875
|
|
|
6567
6876
|
// src/core/shared-event-store.ts
|
|
6568
6877
|
var SharedEventStore = class {
|
|
@@ -7250,7 +7559,7 @@ var SharedMemoryServices = class {
|
|
|
7250
7559
|
this.ensureDirectory(sharedPath, { allowCreate: true });
|
|
7251
7560
|
const store = await this.openStore(sharedPath);
|
|
7252
7561
|
this.sharedVectorStore = this.factories.createSharedVectorStore(
|
|
7253
|
-
|
|
7562
|
+
path6.join(sharedPath, "vectors")
|
|
7254
7563
|
);
|
|
7255
7564
|
await this.sharedVectorStore.initialize();
|
|
7256
7565
|
this.sharedPromoter = this.factories.createSharedPromoter(
|
|
@@ -7323,7 +7632,7 @@ var SharedMemoryServices = class {
|
|
|
7323
7632
|
async createOpenStorePromise(sharedPath) {
|
|
7324
7633
|
if (!this.sharedEventStore) {
|
|
7325
7634
|
const sharedEventStore = this.factories.createSharedEventStore(
|
|
7326
|
-
|
|
7635
|
+
path6.join(sharedPath, "shared.duckdb")
|
|
7327
7636
|
);
|
|
7328
7637
|
await sharedEventStore.initialize();
|
|
7329
7638
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -7343,9 +7652,9 @@ var SharedMemoryServices = class {
|
|
|
7343
7652
|
}
|
|
7344
7653
|
get factories() {
|
|
7345
7654
|
return {
|
|
7346
|
-
existsSync: this.options.factories?.existsSync ??
|
|
7655
|
+
existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
|
|
7347
7656
|
mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
|
|
7348
|
-
|
|
7657
|
+
fs7.mkdirSync(targetPath, { recursive: true });
|
|
7349
7658
|
}),
|
|
7350
7659
|
createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
|
|
7351
7660
|
createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
|
|
@@ -7460,33 +7769,11 @@ function createMemoryServiceComposition(options) {
|
|
|
7460
7769
|
}
|
|
7461
7770
|
function defaultExpandPath(targetPath) {
|
|
7462
7771
|
if (targetPath.startsWith("~")) {
|
|
7463
|
-
return
|
|
7772
|
+
return path7.join(os2.homedir(), targetPath.slice(1));
|
|
7464
7773
|
}
|
|
7465
7774
|
return targetPath;
|
|
7466
7775
|
}
|
|
7467
7776
|
|
|
7468
|
-
// src/core/registry/project-path.ts
|
|
7469
|
-
import * as crypto2 from "crypto";
|
|
7470
|
-
import * as fs7 from "fs";
|
|
7471
|
-
import * as os2 from "os";
|
|
7472
|
-
import * as path7 from "path";
|
|
7473
|
-
function normalizeProjectPath(projectPath) {
|
|
7474
|
-
const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
|
|
7475
|
-
try {
|
|
7476
|
-
return fs7.realpathSync(expanded);
|
|
7477
|
-
} catch {
|
|
7478
|
-
return path7.resolve(expanded);
|
|
7479
|
-
}
|
|
7480
|
-
}
|
|
7481
|
-
function hashProjectPath(projectPath) {
|
|
7482
|
-
const normalizedPath = normalizeProjectPath(projectPath);
|
|
7483
|
-
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
7484
|
-
}
|
|
7485
|
-
function getProjectStoragePath(projectPath) {
|
|
7486
|
-
const hash = hashProjectPath(projectPath);
|
|
7487
|
-
return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
|
|
7488
|
-
}
|
|
7489
|
-
|
|
7490
7777
|
// src/core/registry/session-registry.ts
|
|
7491
7778
|
import * as fs8 from "fs";
|
|
7492
7779
|
import * as os3 from "os";
|
|
@@ -7813,6 +8100,9 @@ var MemoryService = class {
|
|
|
7813
8100
|
async recoverStuckOutboxItems(options) {
|
|
7814
8101
|
return this.queryService.recoverStuckOutboxItems(options);
|
|
7815
8102
|
}
|
|
8103
|
+
async repairLegacyProjectScope(options) {
|
|
8104
|
+
return this.queryService.repairLegacyProjectScope(options);
|
|
8105
|
+
}
|
|
7816
8106
|
async getRetrievalTraceStats() {
|
|
7817
8107
|
return this.retrievalAnalyticsService.getRetrievalTraceStats();
|
|
7818
8108
|
}
|