claude-memory-layer 1.0.36 → 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.
@@ -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 os from "os";
13
- import * as path6 from "path";
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 fs5 from "fs";
1525
- import * as path4 from "path";
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 fs3 from "fs";
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 (!fs3.existsSync(dir)) {
2210
- fs3.mkdirSync(dir, { recursive: true });
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 fs4 from "fs/promises";
2260
- import * as path3 from "path";
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 path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
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 fs4.mkdir(path3.dirname(outPath), { recursive: true });
2311
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
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
  };
@@ -2321,6 +2346,143 @@ function normalizeQueryRewriteKind(value) {
2321
2346
  return "none";
2322
2347
  }
2323
2348
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2349
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2350
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2351
+ function emptyOutboxRecoveryResult() {
2352
+ return {
2353
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2354
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2355
+ };
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
+ }
2324
2486
  var SQLiteEventStore = class {
2325
2487
  db;
2326
2488
  initialized = false;
@@ -2819,11 +2981,11 @@ var SQLiteEventStore = class {
2819
2981
  /**
2820
2982
  * Get events by session ID
2821
2983
  */
2822
- async getSessionEvents(sessionId) {
2984
+ async getSessionEvents(sessionId, options) {
2823
2985
  await this.initialize();
2824
2986
  const rows = sqliteAll(
2825
2987
  this.db,
2826
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
2988
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2827
2989
  [sessionId]
2828
2990
  );
2829
2991
  return rows.map(this.rowToEvent);
@@ -2831,11 +2993,11 @@ var SQLiteEventStore = class {
2831
2993
  /**
2832
2994
  * Get recent events
2833
2995
  */
2834
- async getRecentEvents(limit = 100) {
2996
+ async getRecentEvents(limit = 100, options) {
2835
2997
  await this.initialize();
2836
2998
  const rows = sqliteAll(
2837
2999
  this.db,
2838
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3000
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2839
3001
  [limit]
2840
3002
  );
2841
3003
  return rows.map(this.rowToEvent);
@@ -2843,11 +3005,11 @@ var SQLiteEventStore = class {
2843
3005
  /**
2844
3006
  * Get event by ID
2845
3007
  */
2846
- async getEvent(id) {
3008
+ async getEvent(id, options) {
2847
3009
  await this.initialize();
2848
3010
  const row = sqliteGet(
2849
3011
  this.db,
2850
- `SELECT * FROM events WHERE id = ?`,
3012
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2851
3013
  [id]
2852
3014
  );
2853
3015
  if (!row)
@@ -2857,11 +3019,11 @@ var SQLiteEventStore = class {
2857
3019
  /**
2858
3020
  * Get events since a timestamp (for sync)
2859
3021
  */
2860
- async getEventsSince(timestamp, limit = 1e3) {
3022
+ async getEventsSince(timestamp, limit = 1e3, options) {
2861
3023
  await this.initialize();
2862
3024
  const rows = sqliteAll(
2863
3025
  this.db,
2864
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3026
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2865
3027
  [timestamp, limit]
2866
3028
  );
2867
3029
  return rows.map(this.rowToEvent);
@@ -2870,11 +3032,11 @@ var SQLiteEventStore = class {
2870
3032
  * Get events since a SQLite rowid (for robust incremental replication).
2871
3033
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2872
3034
  */
2873
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3035
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2874
3036
  await this.initialize();
2875
3037
  const rows = sqliteAll(
2876
3038
  this.db,
2877
- `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 ?`,
2878
3040
  [lastRowid, limit]
2879
3041
  );
2880
3042
  return rows.map((row) => ({
@@ -3071,7 +3233,9 @@ var SQLiteEventStore = class {
3071
3233
  const placeholders = ids.map(() => "?").join(",");
3072
3234
  sqliteRun(
3073
3235
  this.db,
3074
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3236
+ `UPDATE embedding_outbox
3237
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3238
+ WHERE id IN (${placeholders})`,
3075
3239
  ids
3076
3240
  );
3077
3241
  return pending.map((row) => ({
@@ -3107,19 +3271,19 @@ var SQLiteEventStore = class {
3107
3271
  /**
3108
3272
  * Count total events
3109
3273
  */
3110
- async countEvents() {
3274
+ async countEvents(options) {
3111
3275
  await this.initialize();
3112
- 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)}`);
3113
3277
  return row?.count || 0;
3114
3278
  }
3115
3279
  /**
3116
3280
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3117
3281
  */
3118
- async getEventsPage(limit = 1e3, offset = 0) {
3282
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3119
3283
  await this.initialize();
3120
3284
  const rows = sqliteAll(
3121
3285
  this.db,
3122
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3286
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3123
3287
  [limit, offset]
3124
3288
  );
3125
3289
  return rows.map(this.rowToEvent);
@@ -3141,6 +3305,197 @@ var SQLiteEventStore = class {
3141
3305
  [error, ...ids]
3142
3306
  );
3143
3307
  }
3308
+ /**
3309
+ * Recover abandoned outbox work after a worker/process crash.
3310
+ *
3311
+ * Rows in `processing` are claimed work. If the process exits before marking
3312
+ * them done/failed, they otherwise remain invisible to future processing.
3313
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3314
+ */
3315
+ async recoverStuckOutboxItems(options = {}) {
3316
+ await this.initialize();
3317
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3318
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3319
+ const now = options.now ?? /* @__PURE__ */ new Date();
3320
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3321
+ const result = emptyOutboxRecoveryResult();
3322
+ const embeddingRecovered = sqliteRun(
3323
+ this.db,
3324
+ `UPDATE embedding_outbox
3325
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3326
+ WHERE status = 'processing'
3327
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3328
+ [threshold]
3329
+ );
3330
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3331
+ const embeddingRetried = sqliteRun(
3332
+ this.db,
3333
+ `UPDATE embedding_outbox
3334
+ SET status = 'pending', error_message = NULL
3335
+ WHERE status = 'failed'
3336
+ AND retry_count < ?`,
3337
+ [maxRetries]
3338
+ );
3339
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3340
+ const vectorRecovered = sqliteRun(
3341
+ this.db,
3342
+ `UPDATE vector_outbox
3343
+ SET status = 'pending', updated_at = ?, error = NULL
3344
+ WHERE status = 'processing'
3345
+ AND datetime(updated_at) < datetime(?)`,
3346
+ [now.toISOString(), threshold]
3347
+ );
3348
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3349
+ const vectorRetried = sqliteRun(
3350
+ this.db,
3351
+ `UPDATE vector_outbox
3352
+ SET status = 'pending', updated_at = ?, error = NULL
3353
+ WHERE status = 'failed'
3354
+ AND retry_count < ?`,
3355
+ [now.toISOString(), maxRetries]
3356
+ );
3357
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3358
+ return result;
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
+ }
3144
3499
  /**
3145
3500
  * Get embedding/vector outbox health statistics
3146
3501
  */
@@ -3188,7 +3543,11 @@ var SQLiteEventStore = class {
3188
3543
  await this.initialize();
3189
3544
  const rows = sqliteAll(
3190
3545
  this.db,
3191
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
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`
3192
3551
  );
3193
3552
  return rows;
3194
3553
  }
@@ -3204,6 +3563,7 @@ var SQLiteEventStore = class {
3204
3563
  `SELECT e.* FROM events e
3205
3564
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3206
3565
  WHERE ml.level = ?
3566
+ AND ${notActiveQuarantinedSql("e.metadata")}
3207
3567
  ORDER BY e.timestamp DESC
3208
3568
  LIMIT ? OFFSET ?`,
3209
3569
  [level, limit, offset]
@@ -3296,12 +3656,13 @@ var SQLiteEventStore = class {
3296
3656
  /**
3297
3657
  * Get most accessed memories (falls back to recent events if none accessed)
3298
3658
  */
3299
- async getMostAccessed(limit = 10) {
3659
+ async getMostAccessed(limit = 10, options) {
3300
3660
  await this.initialize();
3301
3661
  let rows = sqliteAll(
3302
3662
  this.db,
3303
3663
  `SELECT * FROM events
3304
3664
  WHERE access_count > 0
3665
+ AND ${maybeQuarantinePredicate(options)}
3305
3666
  ORDER BY access_count DESC, last_accessed_at DESC
3306
3667
  LIMIT ?`,
3307
3668
  [limit]
@@ -3310,6 +3671,7 @@ var SQLiteEventStore = class {
3310
3671
  rows = sqliteAll(
3311
3672
  this.db,
3312
3673
  `SELECT * FROM events
3674
+ WHERE ${maybeQuarantinePredicate(options)}
3313
3675
  ORDER BY timestamp DESC
3314
3676
  LIMIT ?`,
3315
3677
  [limit]
@@ -3440,6 +3802,7 @@ var SQLiteEventStore = class {
3440
3802
  FROM memory_helpfulness mh
3441
3803
  JOIN events e ON e.id = mh.event_id
3442
3804
  WHERE mh.measured_at IS NOT NULL
3805
+ AND ${notActiveQuarantinedSql("e.metadata")}
3443
3806
  GROUP BY mh.event_id
3444
3807
  ORDER BY avg_score DESC
3445
3808
  LIMIT ?`,
@@ -3504,6 +3867,7 @@ var SQLiteEventStore = class {
3504
3867
  FROM events_fts fts
3505
3868
  JOIN events e ON e.id = fts.event_id
3506
3869
  WHERE events_fts MATCH ?
3870
+ AND ${notActiveQuarantinedSql("e.metadata")}
3507
3871
  ORDER BY fts.rank
3508
3872
  LIMIT ?`,
3509
3873
  [searchTerms, limit]
@@ -3518,6 +3882,7 @@ var SQLiteEventStore = class {
3518
3882
  this.db,
3519
3883
  `SELECT *, 0 as rank FROM events
3520
3884
  WHERE content LIKE ?
3885
+ AND ${notActiveQuarantinedSql()}
3521
3886
  ORDER BY timestamp DESC
3522
3887
  LIMIT ?`,
3523
3888
  [likePattern, limit]
@@ -3724,6 +4089,7 @@ var SQLiteEventStore = class {
3724
4089
  `SELECT turn_id, MIN(timestamp) as min_ts
3725
4090
  FROM events
3726
4091
  WHERE session_id = ? AND turn_id IS NOT NULL
4092
+ AND ${maybeQuarantinePredicate(options)}
3727
4093
  GROUP BY turn_id
3728
4094
  ORDER BY min_ts DESC
3729
4095
  LIMIT ? OFFSET ?`,
@@ -3731,7 +4097,7 @@ var SQLiteEventStore = class {
3731
4097
  );
3732
4098
  const turns = [];
3733
4099
  for (const turnRow of turnRows) {
3734
- const events = await this.getEventsByTurn(turnRow.turn_id);
4100
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3735
4101
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3736
4102
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3737
4103
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3750,11 +4116,11 @@ var SQLiteEventStore = class {
3750
4116
  /**
3751
4117
  * Get all events for a specific turn_id
3752
4118
  */
3753
- async getEventsByTurn(turnId) {
4119
+ async getEventsByTurn(turnId, options) {
3754
4120
  await this.initialize();
3755
4121
  const rows = sqliteAll(
3756
4122
  this.db,
3757
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4123
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3758
4124
  [turnId]
3759
4125
  );
3760
4126
  return rows.map(this.rowToEvent);
@@ -3762,13 +4128,14 @@ var SQLiteEventStore = class {
3762
4128
  /**
3763
4129
  * Count total turns for a session
3764
4130
  */
3765
- async countSessionTurns(sessionId) {
4131
+ async countSessionTurns(sessionId, options) {
3766
4132
  await this.initialize();
3767
4133
  const row = sqliteGet(
3768
4134
  this.db,
3769
4135
  `SELECT COUNT(DISTINCT turn_id) as count
3770
4136
  FROM events
3771
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4137
+ WHERE session_id = ? AND turn_id IS NOT NULL
4138
+ AND ${maybeQuarantinePredicate(options)}`,
3772
4139
  [sessionId]
3773
4140
  );
3774
4141
  return row?.count || 0;
@@ -3860,7 +4227,7 @@ var SQLiteEventStore = class {
3860
4227
  content: row.content,
3861
4228
  canonicalKey: row.canonical_key,
3862
4229
  dedupeKey: row.dedupe_key,
3863
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4230
+ metadata: safeParseMetadataValue(row.metadata)
3864
4231
  };
3865
4232
  if (row.access_count !== void 0) {
3866
4233
  event.access_count = row.access_count;
@@ -4012,6 +4379,7 @@ var VectorStore = class {
4012
4379
  * Get total count of vectors
4013
4380
  */
4014
4381
  async count() {
4382
+ await this.initialize();
4015
4383
  if (!this.table)
4016
4384
  return 0;
4017
4385
  const result = await this.table.countRows();
@@ -4450,6 +4818,14 @@ var MemoryQueryService = class {
4450
4818
  await this.initialize();
4451
4819
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4452
4820
  }
4821
+ async recoverStuckOutboxItems(options) {
4822
+ await this.initialize();
4823
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4824
+ }
4825
+ async repairLegacyProjectScope(options) {
4826
+ await this.initialize();
4827
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4828
+ }
4453
4829
  async getStats() {
4454
4830
  await this.initialize();
4455
4831
  const deps = this.getStatsDeps();
@@ -6072,18 +6448,18 @@ function assertDefaultRetrieverStore(eventStore) {
6072
6448
  function createMemoryEngineServices(options) {
6073
6449
  const factories = options.factories ?? {};
6074
6450
  const storagePath = options.storagePath;
6075
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6076
- fs5.mkdirSync(storagePath, { recursive: true });
6451
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6452
+ fs6.mkdirSync(storagePath, { recursive: true });
6077
6453
  }
6078
6454
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6079
- path4.join(storagePath, "events.sqlite"),
6455
+ path5.join(storagePath, "events.sqlite"),
6080
6456
  {
6081
6457
  readonly: options.readOnly,
6082
6458
  markdownMirrorRoot: storagePath
6083
6459
  }
6084
6460
  );
6085
6461
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6086
- path4.join(storagePath, "vectors")
6462
+ path5.join(storagePath, "vectors")
6087
6463
  );
6088
6464
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6089
6465
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6494,8 +6870,8 @@ function createMemoryRuntimeService(deps) {
6494
6870
  }
6495
6871
 
6496
6872
  // src/extensions/shared-memory/shared-memory-services.ts
6497
- import * as fs6 from "fs";
6498
- import * as path5 from "path";
6873
+ import * as fs7 from "fs";
6874
+ import * as path6 from "path";
6499
6875
 
6500
6876
  // src/core/shared-event-store.ts
6501
6877
  var SharedEventStore = class {
@@ -7183,7 +7559,7 @@ var SharedMemoryServices = class {
7183
7559
  this.ensureDirectory(sharedPath, { allowCreate: true });
7184
7560
  const store = await this.openStore(sharedPath);
7185
7561
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7186
- path5.join(sharedPath, "vectors")
7562
+ path6.join(sharedPath, "vectors")
7187
7563
  );
7188
7564
  await this.sharedVectorStore.initialize();
7189
7565
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7256,7 +7632,7 @@ var SharedMemoryServices = class {
7256
7632
  async createOpenStorePromise(sharedPath) {
7257
7633
  if (!this.sharedEventStore) {
7258
7634
  const sharedEventStore = this.factories.createSharedEventStore(
7259
- path5.join(sharedPath, "shared.duckdb")
7635
+ path6.join(sharedPath, "shared.duckdb")
7260
7636
  );
7261
7637
  await sharedEventStore.initialize();
7262
7638
  this.sharedEventStore = sharedEventStore;
@@ -7276,9 +7652,9 @@ var SharedMemoryServices = class {
7276
7652
  }
7277
7653
  get factories() {
7278
7654
  return {
7279
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7655
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7280
7656
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7281
- fs6.mkdirSync(targetPath, { recursive: true });
7657
+ fs7.mkdirSync(targetPath, { recursive: true });
7282
7658
  }),
7283
7659
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7284
7660
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7393,33 +7769,11 @@ function createMemoryServiceComposition(options) {
7393
7769
  }
7394
7770
  function defaultExpandPath(targetPath) {
7395
7771
  if (targetPath.startsWith("~")) {
7396
- return path6.join(os.homedir(), targetPath.slice(1));
7772
+ return path7.join(os2.homedir(), targetPath.slice(1));
7397
7773
  }
7398
7774
  return targetPath;
7399
7775
  }
7400
7776
 
7401
- // src/core/registry/project-path.ts
7402
- import * as crypto2 from "crypto";
7403
- import * as fs7 from "fs";
7404
- import * as os2 from "os";
7405
- import * as path7 from "path";
7406
- function normalizeProjectPath(projectPath) {
7407
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7408
- try {
7409
- return fs7.realpathSync(expanded);
7410
- } catch {
7411
- return path7.resolve(expanded);
7412
- }
7413
- }
7414
- function hashProjectPath(projectPath) {
7415
- const normalizedPath = normalizeProjectPath(projectPath);
7416
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7417
- }
7418
- function getProjectStoragePath(projectPath) {
7419
- const hash = hashProjectPath(projectPath);
7420
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7421
- }
7422
-
7423
7777
  // src/core/registry/session-registry.ts
7424
7778
  import * as fs8 from "fs";
7425
7779
  import * as os3 from "os";
@@ -7743,6 +8097,12 @@ var MemoryService = class {
7743
8097
  async getOutboxStats() {
7744
8098
  return this.queryService.getOutboxStats();
7745
8099
  }
8100
+ async recoverStuckOutboxItems(options) {
8101
+ return this.queryService.recoverStuckOutboxItems(options);
8102
+ }
8103
+ async repairLegacyProjectScope(options) {
8104
+ return this.queryService.repairLegacyProjectScope(options);
8105
+ }
7746
8106
  async getRetrievalTraceStats() {
7747
8107
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7748
8108
  }