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.
@@ -16,8 +16,8 @@ import * as os8 from "os";
16
16
  import * as os5 from "os";
17
17
 
18
18
  // src/core/engine/memory-service-composition.ts
19
- import * as os from "os";
20
- import * as path6 from "path";
19
+ import * as os2 from "os";
20
+ import * as path7 from "path";
21
21
 
22
22
  // src/core/metadata-extractor.ts
23
23
  function createToolObservationEmbedding(toolName, metadata, success) {
@@ -1528,8 +1528,8 @@ function createEndlessMemoryServices(options) {
1528
1528
  }
1529
1529
 
1530
1530
  // src/core/engine/memory-engine-services.ts
1531
- import * as fs5 from "fs";
1532
- import * as path4 from "path";
1531
+ import * as fs6 from "fs";
1532
+ import * as path5 from "path";
1533
1533
 
1534
1534
  // src/extensions/vector/embedder.ts
1535
1535
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -2207,14 +2207,39 @@ function makeDedupeKey(content, sessionId) {
2207
2207
  return `${sessionId}:${contentHash}`;
2208
2208
  }
2209
2209
 
2210
+ // src/core/sqlite-event-store.ts
2211
+ import * as nodePath2 from "path";
2212
+
2213
+ // src/core/registry/project-path.ts
2214
+ import * as crypto2 from "crypto";
2215
+ import * as fs3 from "fs";
2216
+ import * as os from "os";
2217
+ import * as path3 from "path";
2218
+ function normalizeProjectPath(projectPath) {
2219
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
2220
+ try {
2221
+ return fs3.realpathSync(expanded);
2222
+ } catch {
2223
+ return path3.resolve(expanded);
2224
+ }
2225
+ }
2226
+ function hashProjectPath(projectPath) {
2227
+ const normalizedPath = normalizeProjectPath(projectPath);
2228
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
2229
+ }
2230
+ function getProjectStoragePath(projectPath) {
2231
+ const hash = hashProjectPath(projectPath);
2232
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
2233
+ }
2234
+
2210
2235
  // src/core/sqlite-wrapper.ts
2211
2236
  import Database from "better-sqlite3";
2212
- import * as fs3 from "fs";
2237
+ import * as fs4 from "fs";
2213
2238
  import * as nodePath from "path";
2214
2239
  function createSQLiteDatabase(path14, options) {
2215
2240
  const dir = nodePath.dirname(path14);
2216
- if (!fs3.existsSync(dir)) {
2217
- fs3.mkdirSync(dir, { recursive: true });
2241
+ if (!fs4.existsSync(dir)) {
2242
+ fs4.mkdirSync(dir, { recursive: true });
2218
2243
  }
2219
2244
  const db = new Database(path14, {
2220
2245
  readonly: options?.readonly ?? false
@@ -2263,8 +2288,8 @@ function toSQLiteTimestamp(date) {
2263
2288
  }
2264
2289
 
2265
2290
  // src/core/markdown-mirror.ts
2266
- import * as fs4 from "fs/promises";
2267
- import * as path3 from "path";
2291
+ import * as fs5 from "fs/promises";
2292
+ import * as path4 from "path";
2268
2293
  var DEFAULT_NAMESPACE = "default";
2269
2294
  var DEFAULT_CATEGORY = "uncategorized";
2270
2295
  function sanitizeSegment2(input, fallback) {
@@ -2293,7 +2318,7 @@ function buildMirrorPath2(rootDir, event) {
2293
2318
  const yyyy = d.getFullYear();
2294
2319
  const mm = String(d.getMonth() + 1).padStart(2, "0");
2295
2320
  const dd = String(d.getDate()).padStart(2, "0");
2296
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2321
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2297
2322
  }
2298
2323
  function formatMirrorEntry(event) {
2299
2324
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -2314,8 +2339,8 @@ var MarkdownMirror2 = class {
2314
2339
  }
2315
2340
  async append(event) {
2316
2341
  const outPath = buildMirrorPath2(this.rootDir, event);
2317
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
2318
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
2342
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
2343
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
2319
2344
  return outPath;
2320
2345
  }
2321
2346
  };
@@ -2328,6 +2353,143 @@ function normalizeQueryRewriteKind(value) {
2328
2353
  return "none";
2329
2354
  }
2330
2355
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2356
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2357
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2358
+ function emptyOutboxRecoveryResult() {
2359
+ return {
2360
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2361
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2362
+ };
2363
+ }
2364
+ function isRecord(value) {
2365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2366
+ }
2367
+ function getNestedRecord(root, path14) {
2368
+ let cursor = root;
2369
+ for (const key of path14) {
2370
+ if (!isRecord(cursor))
2371
+ return void 0;
2372
+ cursor = cursor[key];
2373
+ }
2374
+ return isRecord(cursor) ? cursor : void 0;
2375
+ }
2376
+ function getNestedString(root, path14) {
2377
+ let cursor = root;
2378
+ for (const key of path14) {
2379
+ if (!isRecord(cursor))
2380
+ return void 0;
2381
+ cursor = cursor[key];
2382
+ }
2383
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
2384
+ }
2385
+ function metadataProjectHash(metadata) {
2386
+ return getNestedString(metadata, ["scope", "project", "hash"]);
2387
+ }
2388
+ function metadataProjectPaths(metadata) {
2389
+ const candidates = [
2390
+ getNestedString(metadata, ["projectPath"]),
2391
+ getNestedString(metadata, ["sourceProjectPath"]),
2392
+ getNestedString(metadata, ["scope", "project", "path"])
2393
+ ];
2394
+ const paths = [];
2395
+ for (const value of candidates) {
2396
+ if (value && !paths.includes(value))
2397
+ paths.push(value);
2398
+ }
2399
+ return paths;
2400
+ }
2401
+ function metadataProjectPath(metadata) {
2402
+ return metadataProjectPaths(metadata)[0];
2403
+ }
2404
+ function isActiveQuarantinedMetadata(metadata) {
2405
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
2406
+ return quarantine?.status === "active";
2407
+ }
2408
+ function activeQuarantineStatusExpression(column = "metadata") {
2409
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
2410
+ }
2411
+ function notActiveQuarantinedSql(column = "metadata") {
2412
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
2413
+ }
2414
+ function maybeQuarantinePredicate(options, column = "metadata") {
2415
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
2416
+ }
2417
+ function safeParseMetadataValue(value) {
2418
+ if (!value)
2419
+ return void 0;
2420
+ if (typeof value === "object")
2421
+ return isRecord(value) ? value : void 0;
2422
+ if (typeof value !== "string")
2423
+ return void 0;
2424
+ try {
2425
+ const parsed = JSON.parse(value);
2426
+ return isRecord(parsed) ? parsed : void 0;
2427
+ } catch {
2428
+ return void 0;
2429
+ }
2430
+ }
2431
+ function isImportedOrLegacyScopedMetadata(metadata) {
2432
+ if (!metadata)
2433
+ return false;
2434
+ return Boolean(
2435
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
2436
+ );
2437
+ }
2438
+ function addMetadataTag(metadata, tag) {
2439
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
2440
+ if (!current.includes(tag))
2441
+ metadata.tags = [...current, tag];
2442
+ }
2443
+ function buildRepairResult(projectHash, dryRun) {
2444
+ return {
2445
+ dryRun,
2446
+ projectHash,
2447
+ scanned: 0,
2448
+ repaired: 0,
2449
+ quarantined: 0,
2450
+ alreadyScoped: 0,
2451
+ skipped: 0,
2452
+ samples: []
2453
+ };
2454
+ }
2455
+ function normalizeRepoName(value) {
2456
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
2457
+ }
2458
+ function projectBasename(projectPath) {
2459
+ if (!projectPath)
2460
+ return void 0;
2461
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
2462
+ const basename2 = nodePath2.basename(trimmed);
2463
+ return basename2 ? normalizeRepoName(basename2) : void 0;
2464
+ }
2465
+ function isProjectScopeRepairExplanation(content) {
2466
+ const normalized = content.toLowerCase();
2467
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
2468
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
2469
+ return hasRepairContext && hasExplanationContext;
2470
+ }
2471
+ function hasConflictingContentProjectHint(content, projectPath) {
2472
+ const currentName = projectBasename(projectPath);
2473
+ if (!currentName)
2474
+ return false;
2475
+ if (isProjectScopeRepairExplanation(content))
2476
+ return false;
2477
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
2478
+ let githubMatch;
2479
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
2480
+ const repo = normalizeRepoName(githubMatch[2] || "");
2481
+ if (repo && repo !== currentName)
2482
+ return true;
2483
+ }
2484
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
2485
+ let workspaceMatch;
2486
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
2487
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
2488
+ if (repo && repo !== currentName)
2489
+ return true;
2490
+ }
2491
+ return false;
2492
+ }
2331
2493
  var SQLiteEventStore = class {
2332
2494
  db;
2333
2495
  initialized = false;
@@ -2826,11 +2988,11 @@ var SQLiteEventStore = class {
2826
2988
  /**
2827
2989
  * Get events by session ID
2828
2990
  */
2829
- async getSessionEvents(sessionId) {
2991
+ async getSessionEvents(sessionId, options) {
2830
2992
  await this.initialize();
2831
2993
  const rows = sqliteAll(
2832
2994
  this.db,
2833
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
2995
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2834
2996
  [sessionId]
2835
2997
  );
2836
2998
  return rows.map(this.rowToEvent);
@@ -2838,11 +3000,11 @@ var SQLiteEventStore = class {
2838
3000
  /**
2839
3001
  * Get recent events
2840
3002
  */
2841
- async getRecentEvents(limit = 100) {
3003
+ async getRecentEvents(limit = 100, options) {
2842
3004
  await this.initialize();
2843
3005
  const rows = sqliteAll(
2844
3006
  this.db,
2845
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3007
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2846
3008
  [limit]
2847
3009
  );
2848
3010
  return rows.map(this.rowToEvent);
@@ -2850,11 +3012,11 @@ var SQLiteEventStore = class {
2850
3012
  /**
2851
3013
  * Get event by ID
2852
3014
  */
2853
- async getEvent(id) {
3015
+ async getEvent(id, options) {
2854
3016
  await this.initialize();
2855
3017
  const row = sqliteGet(
2856
3018
  this.db,
2857
- `SELECT * FROM events WHERE id = ?`,
3019
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2858
3020
  [id]
2859
3021
  );
2860
3022
  if (!row)
@@ -2864,11 +3026,11 @@ var SQLiteEventStore = class {
2864
3026
  /**
2865
3027
  * Get events since a timestamp (for sync)
2866
3028
  */
2867
- async getEventsSince(timestamp, limit = 1e3) {
3029
+ async getEventsSince(timestamp, limit = 1e3, options) {
2868
3030
  await this.initialize();
2869
3031
  const rows = sqliteAll(
2870
3032
  this.db,
2871
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3033
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2872
3034
  [timestamp, limit]
2873
3035
  );
2874
3036
  return rows.map(this.rowToEvent);
@@ -2877,11 +3039,11 @@ var SQLiteEventStore = class {
2877
3039
  * Get events since a SQLite rowid (for robust incremental replication).
2878
3040
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2879
3041
  */
2880
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3042
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2881
3043
  await this.initialize();
2882
3044
  const rows = sqliteAll(
2883
3045
  this.db,
2884
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
3046
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
2885
3047
  [lastRowid, limit]
2886
3048
  );
2887
3049
  return rows.map((row) => ({
@@ -3078,7 +3240,9 @@ var SQLiteEventStore = class {
3078
3240
  const placeholders = ids.map(() => "?").join(",");
3079
3241
  sqliteRun(
3080
3242
  this.db,
3081
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3243
+ `UPDATE embedding_outbox
3244
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3245
+ WHERE id IN (${placeholders})`,
3082
3246
  ids
3083
3247
  );
3084
3248
  return pending.map((row) => ({
@@ -3114,19 +3278,19 @@ var SQLiteEventStore = class {
3114
3278
  /**
3115
3279
  * Count total events
3116
3280
  */
3117
- async countEvents() {
3281
+ async countEvents(options) {
3118
3282
  await this.initialize();
3119
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
3283
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
3120
3284
  return row?.count || 0;
3121
3285
  }
3122
3286
  /**
3123
3287
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3124
3288
  */
3125
- async getEventsPage(limit = 1e3, offset = 0) {
3289
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3126
3290
  await this.initialize();
3127
3291
  const rows = sqliteAll(
3128
3292
  this.db,
3129
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3293
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3130
3294
  [limit, offset]
3131
3295
  );
3132
3296
  return rows.map(this.rowToEvent);
@@ -3148,6 +3312,197 @@ var SQLiteEventStore = class {
3148
3312
  [error, ...ids]
3149
3313
  );
3150
3314
  }
3315
+ /**
3316
+ * Recover abandoned outbox work after a worker/process crash.
3317
+ *
3318
+ * Rows in `processing` are claimed work. If the process exits before marking
3319
+ * them done/failed, they otherwise remain invisible to future processing.
3320
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3321
+ */
3322
+ async recoverStuckOutboxItems(options = {}) {
3323
+ await this.initialize();
3324
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3325
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3326
+ const now = options.now ?? /* @__PURE__ */ new Date();
3327
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3328
+ const result = emptyOutboxRecoveryResult();
3329
+ const embeddingRecovered = sqliteRun(
3330
+ this.db,
3331
+ `UPDATE embedding_outbox
3332
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3333
+ WHERE status = 'processing'
3334
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3335
+ [threshold]
3336
+ );
3337
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3338
+ const embeddingRetried = sqliteRun(
3339
+ this.db,
3340
+ `UPDATE embedding_outbox
3341
+ SET status = 'pending', error_message = NULL
3342
+ WHERE status = 'failed'
3343
+ AND retry_count < ?`,
3344
+ [maxRetries]
3345
+ );
3346
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3347
+ const vectorRecovered = sqliteRun(
3348
+ this.db,
3349
+ `UPDATE vector_outbox
3350
+ SET status = 'pending', updated_at = ?, error = NULL
3351
+ WHERE status = 'processing'
3352
+ AND datetime(updated_at) < datetime(?)`,
3353
+ [now.toISOString(), threshold]
3354
+ );
3355
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3356
+ const vectorRetried = sqliteRun(
3357
+ this.db,
3358
+ `UPDATE vector_outbox
3359
+ SET status = 'pending', updated_at = ?, error = NULL
3360
+ WHERE status = 'failed'
3361
+ AND retry_count < ?`,
3362
+ [now.toISOString(), maxRetries]
3363
+ );
3364
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3365
+ return result;
3366
+ }
3367
+ /**
3368
+ * Repair legacy imported events that predate canonical project scope metadata.
3369
+ *
3370
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
3371
+ * imported but cannot be proven to belong to this project are quarantined so
3372
+ * dashboard default reads/search do not surface cross-project contamination.
3373
+ */
3374
+ async repairLegacyProjectScope(options = {}) {
3375
+ await this.initialize();
3376
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
3377
+ if (!projectHash) {
3378
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
3379
+ }
3380
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
3381
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
3382
+ }
3383
+ const dryRun = options.dryRun === true;
3384
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
3385
+ const result = buildRepairResult(projectHash, dryRun);
3386
+ const rows = sqliteAll(
3387
+ this.db,
3388
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
3389
+ FROM events e
3390
+ LEFT JOIN sessions s ON s.id = e.session_id
3391
+ ORDER BY e.timestamp ASC`,
3392
+ []
3393
+ );
3394
+ const sample = (entry) => {
3395
+ if (result.samples.length < 20)
3396
+ result.samples.push(entry);
3397
+ };
3398
+ for (const row of rows) {
3399
+ result.scanned++;
3400
+ let metadata = {};
3401
+ let metadataParseInvalid = false;
3402
+ if (row.metadata) {
3403
+ const parsed = safeParseMetadataValue(row.metadata);
3404
+ if (parsed) {
3405
+ metadata = parsed;
3406
+ } else {
3407
+ metadataParseInvalid = true;
3408
+ }
3409
+ }
3410
+ if (isActiveQuarantinedMetadata(metadata)) {
3411
+ result.skipped++;
3412
+ continue;
3413
+ }
3414
+ const currentHash = metadataProjectHash(metadata);
3415
+ const explicitPath = metadataProjectPath(metadata);
3416
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
3417
+ const candidatePaths = metadataProjectPaths(metadata);
3418
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
3419
+ candidatePaths.push(sessionProjectPath);
3420
+ }
3421
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
3422
+ const pathHashes = candidatePaths.map((candidate) => {
3423
+ try {
3424
+ return { path: candidate, hash: hashProjectPath(candidate) };
3425
+ } catch {
3426
+ return { path: candidate, hash: void 0 };
3427
+ }
3428
+ });
3429
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
3430
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
3431
+ let action = "skipped";
3432
+ let reason;
3433
+ let observedProjectHash;
3434
+ if (foreignPath) {
3435
+ action = "quarantined";
3436
+ reason = "project-path-mismatch";
3437
+ observedProjectHash = foreignPath.hash;
3438
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
3439
+ action = "quarantined";
3440
+ reason = "content-project-mismatch";
3441
+ } else if (currentHash === projectHash) {
3442
+ result.alreadyScoped++;
3443
+ continue;
3444
+ } else if (currentHash && currentHash !== projectHash) {
3445
+ action = "quarantined";
3446
+ reason = "scope-hash-mismatch";
3447
+ observedProjectHash = currentHash;
3448
+ } else if (matchingPath) {
3449
+ action = "repaired";
3450
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
3451
+ } else if (candidatePaths.length > 0) {
3452
+ action = "quarantined";
3453
+ reason = "project-path-mismatch";
3454
+ } else if (importedOrLegacy) {
3455
+ action = "quarantined";
3456
+ reason = "missing-project-scope";
3457
+ }
3458
+ if (action === "skipped" || !reason) {
3459
+ result.skipped++;
3460
+ continue;
3461
+ }
3462
+ if (action === "repaired") {
3463
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
3464
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
3465
+ project.hash = projectHash;
3466
+ scope.project = project;
3467
+ metadata.scope = scope;
3468
+ metadata.repair = {
3469
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3470
+ legacyProjectScope: {
3471
+ action,
3472
+ reason,
3473
+ repairedAt: nowIso
3474
+ }
3475
+ };
3476
+ addMetadataTag(metadata, `proj:${projectHash}`);
3477
+ result.repaired++;
3478
+ } else {
3479
+ metadata.quarantine = {
3480
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
3481
+ status: "active",
3482
+ category: "project-scope",
3483
+ reason,
3484
+ detectedAt: nowIso,
3485
+ expectedProjectHash: projectHash,
3486
+ ...observedProjectHash ? { observedProjectHash } : {}
3487
+ };
3488
+ metadata.repair = {
3489
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3490
+ legacyProjectScope: {
3491
+ action,
3492
+ reason,
3493
+ repairedAt: nowIso
3494
+ }
3495
+ };
3496
+ addMetadataTag(metadata, "quarantine:project-scope");
3497
+ result.quarantined++;
3498
+ }
3499
+ sample({ eventId: row.id, action, reason });
3500
+ if (!dryRun) {
3501
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
3502
+ }
3503
+ }
3504
+ return result;
3505
+ }
3151
3506
  /**
3152
3507
  * Get embedding/vector outbox health statistics
3153
3508
  */
@@ -3195,7 +3550,11 @@ var SQLiteEventStore = class {
3195
3550
  await this.initialize();
3196
3551
  const rows = sqliteAll(
3197
3552
  this.db,
3198
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
3553
+ `SELECT ml.level, COUNT(*) as count
3554
+ FROM memory_levels ml
3555
+ INNER JOIN events e ON e.id = ml.event_id
3556
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
3557
+ GROUP BY ml.level`
3199
3558
  );
3200
3559
  return rows;
3201
3560
  }
@@ -3211,6 +3570,7 @@ var SQLiteEventStore = class {
3211
3570
  `SELECT e.* FROM events e
3212
3571
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3213
3572
  WHERE ml.level = ?
3573
+ AND ${notActiveQuarantinedSql("e.metadata")}
3214
3574
  ORDER BY e.timestamp DESC
3215
3575
  LIMIT ? OFFSET ?`,
3216
3576
  [level, limit, offset]
@@ -3303,12 +3663,13 @@ var SQLiteEventStore = class {
3303
3663
  /**
3304
3664
  * Get most accessed memories (falls back to recent events if none accessed)
3305
3665
  */
3306
- async getMostAccessed(limit = 10) {
3666
+ async getMostAccessed(limit = 10, options) {
3307
3667
  await this.initialize();
3308
3668
  let rows = sqliteAll(
3309
3669
  this.db,
3310
3670
  `SELECT * FROM events
3311
3671
  WHERE access_count > 0
3672
+ AND ${maybeQuarantinePredicate(options)}
3312
3673
  ORDER BY access_count DESC, last_accessed_at DESC
3313
3674
  LIMIT ?`,
3314
3675
  [limit]
@@ -3317,6 +3678,7 @@ var SQLiteEventStore = class {
3317
3678
  rows = sqliteAll(
3318
3679
  this.db,
3319
3680
  `SELECT * FROM events
3681
+ WHERE ${maybeQuarantinePredicate(options)}
3320
3682
  ORDER BY timestamp DESC
3321
3683
  LIMIT ?`,
3322
3684
  [limit]
@@ -3447,6 +3809,7 @@ var SQLiteEventStore = class {
3447
3809
  FROM memory_helpfulness mh
3448
3810
  JOIN events e ON e.id = mh.event_id
3449
3811
  WHERE mh.measured_at IS NOT NULL
3812
+ AND ${notActiveQuarantinedSql("e.metadata")}
3450
3813
  GROUP BY mh.event_id
3451
3814
  ORDER BY avg_score DESC
3452
3815
  LIMIT ?`,
@@ -3511,6 +3874,7 @@ var SQLiteEventStore = class {
3511
3874
  FROM events_fts fts
3512
3875
  JOIN events e ON e.id = fts.event_id
3513
3876
  WHERE events_fts MATCH ?
3877
+ AND ${notActiveQuarantinedSql("e.metadata")}
3514
3878
  ORDER BY fts.rank
3515
3879
  LIMIT ?`,
3516
3880
  [searchTerms, limit]
@@ -3525,6 +3889,7 @@ var SQLiteEventStore = class {
3525
3889
  this.db,
3526
3890
  `SELECT *, 0 as rank FROM events
3527
3891
  WHERE content LIKE ?
3892
+ AND ${notActiveQuarantinedSql()}
3528
3893
  ORDER BY timestamp DESC
3529
3894
  LIMIT ?`,
3530
3895
  [likePattern, limit]
@@ -3731,6 +4096,7 @@ var SQLiteEventStore = class {
3731
4096
  `SELECT turn_id, MIN(timestamp) as min_ts
3732
4097
  FROM events
3733
4098
  WHERE session_id = ? AND turn_id IS NOT NULL
4099
+ AND ${maybeQuarantinePredicate(options)}
3734
4100
  GROUP BY turn_id
3735
4101
  ORDER BY min_ts DESC
3736
4102
  LIMIT ? OFFSET ?`,
@@ -3738,7 +4104,7 @@ var SQLiteEventStore = class {
3738
4104
  );
3739
4105
  const turns = [];
3740
4106
  for (const turnRow of turnRows) {
3741
- const events = await this.getEventsByTurn(turnRow.turn_id);
4107
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3742
4108
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3743
4109
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3744
4110
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3757,11 +4123,11 @@ var SQLiteEventStore = class {
3757
4123
  /**
3758
4124
  * Get all events for a specific turn_id
3759
4125
  */
3760
- async getEventsByTurn(turnId) {
4126
+ async getEventsByTurn(turnId, options) {
3761
4127
  await this.initialize();
3762
4128
  const rows = sqliteAll(
3763
4129
  this.db,
3764
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4130
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3765
4131
  [turnId]
3766
4132
  );
3767
4133
  return rows.map(this.rowToEvent);
@@ -3769,13 +4135,14 @@ var SQLiteEventStore = class {
3769
4135
  /**
3770
4136
  * Count total turns for a session
3771
4137
  */
3772
- async countSessionTurns(sessionId) {
4138
+ async countSessionTurns(sessionId, options) {
3773
4139
  await this.initialize();
3774
4140
  const row = sqliteGet(
3775
4141
  this.db,
3776
4142
  `SELECT COUNT(DISTINCT turn_id) as count
3777
4143
  FROM events
3778
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4144
+ WHERE session_id = ? AND turn_id IS NOT NULL
4145
+ AND ${maybeQuarantinePredicate(options)}`,
3779
4146
  [sessionId]
3780
4147
  );
3781
4148
  return row?.count || 0;
@@ -3867,7 +4234,7 @@ var SQLiteEventStore = class {
3867
4234
  content: row.content,
3868
4235
  canonicalKey: row.canonical_key,
3869
4236
  dedupeKey: row.dedupe_key,
3870
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4237
+ metadata: safeParseMetadataValue(row.metadata)
3871
4238
  };
3872
4239
  if (row.access_count !== void 0) {
3873
4240
  event.access_count = row.access_count;
@@ -4019,6 +4386,7 @@ var VectorStore = class {
4019
4386
  * Get total count of vectors
4020
4387
  */
4021
4388
  async count() {
4389
+ await this.initialize();
4022
4390
  if (!this.table)
4023
4391
  return 0;
4024
4392
  const result = await this.table.countRows();
@@ -4457,6 +4825,14 @@ var MemoryQueryService = class {
4457
4825
  await this.initialize();
4458
4826
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4459
4827
  }
4828
+ async recoverStuckOutboxItems(options) {
4829
+ await this.initialize();
4830
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4831
+ }
4832
+ async repairLegacyProjectScope(options) {
4833
+ await this.initialize();
4834
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4835
+ }
4460
4836
  async getStats() {
4461
4837
  await this.initialize();
4462
4838
  const deps = this.getStatsDeps();
@@ -6079,18 +6455,18 @@ function assertDefaultRetrieverStore(eventStore) {
6079
6455
  function createMemoryEngineServices(options) {
6080
6456
  const factories = options.factories ?? {};
6081
6457
  const storagePath = options.storagePath;
6082
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6083
- fs5.mkdirSync(storagePath, { recursive: true });
6458
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6459
+ fs6.mkdirSync(storagePath, { recursive: true });
6084
6460
  }
6085
6461
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6086
- path4.join(storagePath, "events.sqlite"),
6462
+ path5.join(storagePath, "events.sqlite"),
6087
6463
  {
6088
6464
  readonly: options.readOnly,
6089
6465
  markdownMirrorRoot: storagePath
6090
6466
  }
6091
6467
  );
6092
6468
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6093
- path4.join(storagePath, "vectors")
6469
+ path5.join(storagePath, "vectors")
6094
6470
  );
6095
6471
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6096
6472
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6501,8 +6877,8 @@ function createMemoryRuntimeService(deps) {
6501
6877
  }
6502
6878
 
6503
6879
  // src/extensions/shared-memory/shared-memory-services.ts
6504
- import * as fs6 from "fs";
6505
- import * as path5 from "path";
6880
+ import * as fs7 from "fs";
6881
+ import * as path6 from "path";
6506
6882
 
6507
6883
  // src/core/shared-event-store.ts
6508
6884
  var SharedEventStore = class {
@@ -7190,7 +7566,7 @@ var SharedMemoryServices = class {
7190
7566
  this.ensureDirectory(sharedPath, { allowCreate: true });
7191
7567
  const store = await this.openStore(sharedPath);
7192
7568
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7193
- path5.join(sharedPath, "vectors")
7569
+ path6.join(sharedPath, "vectors")
7194
7570
  );
7195
7571
  await this.sharedVectorStore.initialize();
7196
7572
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7263,7 +7639,7 @@ var SharedMemoryServices = class {
7263
7639
  async createOpenStorePromise(sharedPath) {
7264
7640
  if (!this.sharedEventStore) {
7265
7641
  const sharedEventStore = this.factories.createSharedEventStore(
7266
- path5.join(sharedPath, "shared.duckdb")
7642
+ path6.join(sharedPath, "shared.duckdb")
7267
7643
  );
7268
7644
  await sharedEventStore.initialize();
7269
7645
  this.sharedEventStore = sharedEventStore;
@@ -7283,9 +7659,9 @@ var SharedMemoryServices = class {
7283
7659
  }
7284
7660
  get factories() {
7285
7661
  return {
7286
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7662
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7287
7663
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7288
- fs6.mkdirSync(targetPath, { recursive: true });
7664
+ fs7.mkdirSync(targetPath, { recursive: true });
7289
7665
  }),
7290
7666
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7291
7667
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7400,33 +7776,11 @@ function createMemoryServiceComposition(options) {
7400
7776
  }
7401
7777
  function defaultExpandPath(targetPath) {
7402
7778
  if (targetPath.startsWith("~")) {
7403
- return path6.join(os.homedir(), targetPath.slice(1));
7779
+ return path7.join(os2.homedir(), targetPath.slice(1));
7404
7780
  }
7405
7781
  return targetPath;
7406
7782
  }
7407
7783
 
7408
- // src/core/registry/project-path.ts
7409
- import * as crypto2 from "crypto";
7410
- import * as fs7 from "fs";
7411
- import * as os2 from "os";
7412
- import * as path7 from "path";
7413
- function normalizeProjectPath(projectPath) {
7414
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7415
- try {
7416
- return fs7.realpathSync(expanded);
7417
- } catch {
7418
- return path7.resolve(expanded);
7419
- }
7420
- }
7421
- function hashProjectPath(projectPath) {
7422
- const normalizedPath = normalizeProjectPath(projectPath);
7423
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7424
- }
7425
- function getProjectStoragePath(projectPath) {
7426
- const hash = hashProjectPath(projectPath);
7427
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7428
- }
7429
-
7430
7784
  // src/core/registry/session-registry.ts
7431
7785
  import * as fs8 from "fs";
7432
7786
  import * as os3 from "os";
@@ -7725,6 +8079,12 @@ var MemoryService = class {
7725
8079
  async getOutboxStats() {
7726
8080
  return this.queryService.getOutboxStats();
7727
8081
  }
8082
+ async recoverStuckOutboxItems(options) {
8083
+ return this.queryService.recoverStuckOutboxItems(options);
8084
+ }
8085
+ async repairLegacyProjectScope(options) {
8086
+ return this.queryService.repairLegacyProjectScope(options);
8087
+ }
7728
8088
  async getRetrievalTraceStats() {
7729
8089
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7730
8090
  }