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.
@@ -10,8 +10,8 @@ const __dirname = dirname(__filename);
10
10
  import * as os5 from "os";
11
11
 
12
12
  // src/core/engine/memory-service-composition.ts
13
- import * as os from "os";
14
- import * as path6 from "path";
13
+ import * as os2 from "os";
14
+ import * as path7 from "path";
15
15
 
16
16
  // src/core/metadata-extractor.ts
17
17
  function createToolObservationEmbedding(toolName, metadata, success) {
@@ -1522,8 +1522,8 @@ function createEndlessMemoryServices(options) {
1522
1522
  }
1523
1523
 
1524
1524
  // src/core/engine/memory-engine-services.ts
1525
- import * as fs5 from "fs";
1526
- import * as path4 from "path";
1525
+ import * as fs6 from "fs";
1526
+ import * as path5 from "path";
1527
1527
 
1528
1528
  // src/extensions/vector/embedder.ts
1529
1529
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -2201,14 +2201,39 @@ function makeDedupeKey(content, sessionId) {
2201
2201
  return `${sessionId}:${contentHash}`;
2202
2202
  }
2203
2203
 
2204
+ // src/core/sqlite-event-store.ts
2205
+ import * as nodePath2 from "path";
2206
+
2207
+ // src/core/registry/project-path.ts
2208
+ import * as crypto2 from "crypto";
2209
+ import * as fs3 from "fs";
2210
+ import * as os from "os";
2211
+ import * as path3 from "path";
2212
+ function normalizeProjectPath(projectPath) {
2213
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
2214
+ try {
2215
+ return fs3.realpathSync(expanded);
2216
+ } catch {
2217
+ return path3.resolve(expanded);
2218
+ }
2219
+ }
2220
+ function hashProjectPath(projectPath) {
2221
+ const normalizedPath = normalizeProjectPath(projectPath);
2222
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
2223
+ }
2224
+ function getProjectStoragePath(projectPath) {
2225
+ const hash = hashProjectPath(projectPath);
2226
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
2227
+ }
2228
+
2204
2229
  // src/core/sqlite-wrapper.ts
2205
2230
  import Database from "better-sqlite3";
2206
- import * as fs3 from "fs";
2231
+ import * as fs4 from "fs";
2207
2232
  import * as nodePath from "path";
2208
2233
  function createSQLiteDatabase(path12, options) {
2209
2234
  const dir = nodePath.dirname(path12);
2210
- if (!fs3.existsSync(dir)) {
2211
- fs3.mkdirSync(dir, { recursive: true });
2235
+ if (!fs4.existsSync(dir)) {
2236
+ fs4.mkdirSync(dir, { recursive: true });
2212
2237
  }
2213
2238
  const db = new Database(path12, {
2214
2239
  readonly: options?.readonly ?? false
@@ -2257,8 +2282,8 @@ function toSQLiteTimestamp(date) {
2257
2282
  }
2258
2283
 
2259
2284
  // src/core/markdown-mirror.ts
2260
- import * as fs4 from "fs/promises";
2261
- import * as path3 from "path";
2285
+ import * as fs5 from "fs/promises";
2286
+ import * as path4 from "path";
2262
2287
  var DEFAULT_NAMESPACE = "default";
2263
2288
  var DEFAULT_CATEGORY = "uncategorized";
2264
2289
  function sanitizeSegment2(input, fallback) {
@@ -2287,7 +2312,7 @@ function buildMirrorPath2(rootDir, event) {
2287
2312
  const yyyy = d.getFullYear();
2288
2313
  const mm = String(d.getMonth() + 1).padStart(2, "0");
2289
2314
  const dd = String(d.getDate()).padStart(2, "0");
2290
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2315
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2291
2316
  }
2292
2317
  function formatMirrorEntry(event) {
2293
2318
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -2308,8 +2333,8 @@ var MarkdownMirror2 = class {
2308
2333
  }
2309
2334
  async append(event) {
2310
2335
  const outPath = buildMirrorPath2(this.rootDir, event);
2311
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
2312
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
2336
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
2337
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
2313
2338
  return outPath;
2314
2339
  }
2315
2340
  };
@@ -2322,6 +2347,143 @@ function normalizeQueryRewriteKind(value) {
2322
2347
  return "none";
2323
2348
  }
2324
2349
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2350
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2351
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2352
+ function emptyOutboxRecoveryResult() {
2353
+ return {
2354
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2355
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2356
+ };
2357
+ }
2358
+ function isRecord(value) {
2359
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2360
+ }
2361
+ function getNestedRecord(root, path12) {
2362
+ let cursor = root;
2363
+ for (const key of path12) {
2364
+ if (!isRecord(cursor))
2365
+ return void 0;
2366
+ cursor = cursor[key];
2367
+ }
2368
+ return isRecord(cursor) ? cursor : void 0;
2369
+ }
2370
+ function getNestedString(root, path12) {
2371
+ let cursor = root;
2372
+ for (const key of path12) {
2373
+ if (!isRecord(cursor))
2374
+ return void 0;
2375
+ cursor = cursor[key];
2376
+ }
2377
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
2378
+ }
2379
+ function metadataProjectHash(metadata) {
2380
+ return getNestedString(metadata, ["scope", "project", "hash"]);
2381
+ }
2382
+ function metadataProjectPaths(metadata) {
2383
+ const candidates = [
2384
+ getNestedString(metadata, ["projectPath"]),
2385
+ getNestedString(metadata, ["sourceProjectPath"]),
2386
+ getNestedString(metadata, ["scope", "project", "path"])
2387
+ ];
2388
+ const paths = [];
2389
+ for (const value of candidates) {
2390
+ if (value && !paths.includes(value))
2391
+ paths.push(value);
2392
+ }
2393
+ return paths;
2394
+ }
2395
+ function metadataProjectPath(metadata) {
2396
+ return metadataProjectPaths(metadata)[0];
2397
+ }
2398
+ function isActiveQuarantinedMetadata(metadata) {
2399
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
2400
+ return quarantine?.status === "active";
2401
+ }
2402
+ function activeQuarantineStatusExpression(column = "metadata") {
2403
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
2404
+ }
2405
+ function notActiveQuarantinedSql(column = "metadata") {
2406
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
2407
+ }
2408
+ function maybeQuarantinePredicate(options, column = "metadata") {
2409
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
2410
+ }
2411
+ function safeParseMetadataValue(value) {
2412
+ if (!value)
2413
+ return void 0;
2414
+ if (typeof value === "object")
2415
+ return isRecord(value) ? value : void 0;
2416
+ if (typeof value !== "string")
2417
+ return void 0;
2418
+ try {
2419
+ const parsed = JSON.parse(value);
2420
+ return isRecord(parsed) ? parsed : void 0;
2421
+ } catch {
2422
+ return void 0;
2423
+ }
2424
+ }
2425
+ function isImportedOrLegacyScopedMetadata(metadata) {
2426
+ if (!metadata)
2427
+ return false;
2428
+ return Boolean(
2429
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
2430
+ );
2431
+ }
2432
+ function addMetadataTag(metadata, tag) {
2433
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
2434
+ if (!current.includes(tag))
2435
+ metadata.tags = [...current, tag];
2436
+ }
2437
+ function buildRepairResult(projectHash, dryRun) {
2438
+ return {
2439
+ dryRun,
2440
+ projectHash,
2441
+ scanned: 0,
2442
+ repaired: 0,
2443
+ quarantined: 0,
2444
+ alreadyScoped: 0,
2445
+ skipped: 0,
2446
+ samples: []
2447
+ };
2448
+ }
2449
+ function normalizeRepoName(value) {
2450
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
2451
+ }
2452
+ function projectBasename(projectPath) {
2453
+ if (!projectPath)
2454
+ return void 0;
2455
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
2456
+ const basename2 = nodePath2.basename(trimmed);
2457
+ return basename2 ? normalizeRepoName(basename2) : void 0;
2458
+ }
2459
+ function isProjectScopeRepairExplanation(content) {
2460
+ const normalized = content.toLowerCase();
2461
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
2462
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
2463
+ return hasRepairContext && hasExplanationContext;
2464
+ }
2465
+ function hasConflictingContentProjectHint(content, projectPath) {
2466
+ const currentName = projectBasename(projectPath);
2467
+ if (!currentName)
2468
+ return false;
2469
+ if (isProjectScopeRepairExplanation(content))
2470
+ return false;
2471
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
2472
+ let githubMatch;
2473
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
2474
+ const repo = normalizeRepoName(githubMatch[2] || "");
2475
+ if (repo && repo !== currentName)
2476
+ return true;
2477
+ }
2478
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
2479
+ let workspaceMatch;
2480
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
2481
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
2482
+ if (repo && repo !== currentName)
2483
+ return true;
2484
+ }
2485
+ return false;
2486
+ }
2325
2487
  var SQLiteEventStore = class {
2326
2488
  db;
2327
2489
  initialized = false;
@@ -2820,11 +2982,11 @@ var SQLiteEventStore = class {
2820
2982
  /**
2821
2983
  * Get events by session ID
2822
2984
  */
2823
- async getSessionEvents(sessionId) {
2985
+ async getSessionEvents(sessionId, options) {
2824
2986
  await this.initialize();
2825
2987
  const rows = sqliteAll(
2826
2988
  this.db,
2827
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
2989
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2828
2990
  [sessionId]
2829
2991
  );
2830
2992
  return rows.map(this.rowToEvent);
@@ -2832,11 +2994,11 @@ var SQLiteEventStore = class {
2832
2994
  /**
2833
2995
  * Get recent events
2834
2996
  */
2835
- async getRecentEvents(limit = 100) {
2997
+ async getRecentEvents(limit = 100, options) {
2836
2998
  await this.initialize();
2837
2999
  const rows = sqliteAll(
2838
3000
  this.db,
2839
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3001
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2840
3002
  [limit]
2841
3003
  );
2842
3004
  return rows.map(this.rowToEvent);
@@ -2844,11 +3006,11 @@ var SQLiteEventStore = class {
2844
3006
  /**
2845
3007
  * Get event by ID
2846
3008
  */
2847
- async getEvent(id) {
3009
+ async getEvent(id, options) {
2848
3010
  await this.initialize();
2849
3011
  const row = sqliteGet(
2850
3012
  this.db,
2851
- `SELECT * FROM events WHERE id = ?`,
3013
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2852
3014
  [id]
2853
3015
  );
2854
3016
  if (!row)
@@ -2858,11 +3020,11 @@ var SQLiteEventStore = class {
2858
3020
  /**
2859
3021
  * Get events since a timestamp (for sync)
2860
3022
  */
2861
- async getEventsSince(timestamp, limit = 1e3) {
3023
+ async getEventsSince(timestamp, limit = 1e3, options) {
2862
3024
  await this.initialize();
2863
3025
  const rows = sqliteAll(
2864
3026
  this.db,
2865
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3027
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2866
3028
  [timestamp, limit]
2867
3029
  );
2868
3030
  return rows.map(this.rowToEvent);
@@ -2871,11 +3033,11 @@ var SQLiteEventStore = class {
2871
3033
  * Get events since a SQLite rowid (for robust incremental replication).
2872
3034
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2873
3035
  */
2874
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3036
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2875
3037
  await this.initialize();
2876
3038
  const rows = sqliteAll(
2877
3039
  this.db,
2878
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
3040
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
2879
3041
  [lastRowid, limit]
2880
3042
  );
2881
3043
  return rows.map((row) => ({
@@ -3072,7 +3234,9 @@ var SQLiteEventStore = class {
3072
3234
  const placeholders = ids.map(() => "?").join(",");
3073
3235
  sqliteRun(
3074
3236
  this.db,
3075
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3237
+ `UPDATE embedding_outbox
3238
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3239
+ WHERE id IN (${placeholders})`,
3076
3240
  ids
3077
3241
  );
3078
3242
  return pending.map((row) => ({
@@ -3108,19 +3272,19 @@ var SQLiteEventStore = class {
3108
3272
  /**
3109
3273
  * Count total events
3110
3274
  */
3111
- async countEvents() {
3275
+ async countEvents(options) {
3112
3276
  await this.initialize();
3113
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
3277
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
3114
3278
  return row?.count || 0;
3115
3279
  }
3116
3280
  /**
3117
3281
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3118
3282
  */
3119
- async getEventsPage(limit = 1e3, offset = 0) {
3283
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3120
3284
  await this.initialize();
3121
3285
  const rows = sqliteAll(
3122
3286
  this.db,
3123
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3287
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3124
3288
  [limit, offset]
3125
3289
  );
3126
3290
  return rows.map(this.rowToEvent);
@@ -3142,6 +3306,197 @@ var SQLiteEventStore = class {
3142
3306
  [error, ...ids]
3143
3307
  );
3144
3308
  }
3309
+ /**
3310
+ * Recover abandoned outbox work after a worker/process crash.
3311
+ *
3312
+ * Rows in `processing` are claimed work. If the process exits before marking
3313
+ * them done/failed, they otherwise remain invisible to future processing.
3314
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3315
+ */
3316
+ async recoverStuckOutboxItems(options = {}) {
3317
+ await this.initialize();
3318
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3319
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3320
+ const now = options.now ?? /* @__PURE__ */ new Date();
3321
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3322
+ const result = emptyOutboxRecoveryResult();
3323
+ const embeddingRecovered = sqliteRun(
3324
+ this.db,
3325
+ `UPDATE embedding_outbox
3326
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3327
+ WHERE status = 'processing'
3328
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3329
+ [threshold]
3330
+ );
3331
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3332
+ const embeddingRetried = sqliteRun(
3333
+ this.db,
3334
+ `UPDATE embedding_outbox
3335
+ SET status = 'pending', error_message = NULL
3336
+ WHERE status = 'failed'
3337
+ AND retry_count < ?`,
3338
+ [maxRetries]
3339
+ );
3340
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3341
+ const vectorRecovered = sqliteRun(
3342
+ this.db,
3343
+ `UPDATE vector_outbox
3344
+ SET status = 'pending', updated_at = ?, error = NULL
3345
+ WHERE status = 'processing'
3346
+ AND datetime(updated_at) < datetime(?)`,
3347
+ [now.toISOString(), threshold]
3348
+ );
3349
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3350
+ const vectorRetried = sqliteRun(
3351
+ this.db,
3352
+ `UPDATE vector_outbox
3353
+ SET status = 'pending', updated_at = ?, error = NULL
3354
+ WHERE status = 'failed'
3355
+ AND retry_count < ?`,
3356
+ [now.toISOString(), maxRetries]
3357
+ );
3358
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3359
+ return result;
3360
+ }
3361
+ /**
3362
+ * Repair legacy imported events that predate canonical project scope metadata.
3363
+ *
3364
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
3365
+ * imported but cannot be proven to belong to this project are quarantined so
3366
+ * dashboard default reads/search do not surface cross-project contamination.
3367
+ */
3368
+ async repairLegacyProjectScope(options = {}) {
3369
+ await this.initialize();
3370
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
3371
+ if (!projectHash) {
3372
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
3373
+ }
3374
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
3375
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
3376
+ }
3377
+ const dryRun = options.dryRun === true;
3378
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
3379
+ const result = buildRepairResult(projectHash, dryRun);
3380
+ const rows = sqliteAll(
3381
+ this.db,
3382
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
3383
+ FROM events e
3384
+ LEFT JOIN sessions s ON s.id = e.session_id
3385
+ ORDER BY e.timestamp ASC`,
3386
+ []
3387
+ );
3388
+ const sample = (entry) => {
3389
+ if (result.samples.length < 20)
3390
+ result.samples.push(entry);
3391
+ };
3392
+ for (const row of rows) {
3393
+ result.scanned++;
3394
+ let metadata = {};
3395
+ let metadataParseInvalid = false;
3396
+ if (row.metadata) {
3397
+ const parsed = safeParseMetadataValue(row.metadata);
3398
+ if (parsed) {
3399
+ metadata = parsed;
3400
+ } else {
3401
+ metadataParseInvalid = true;
3402
+ }
3403
+ }
3404
+ if (isActiveQuarantinedMetadata(metadata)) {
3405
+ result.skipped++;
3406
+ continue;
3407
+ }
3408
+ const currentHash = metadataProjectHash(metadata);
3409
+ const explicitPath = metadataProjectPath(metadata);
3410
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
3411
+ const candidatePaths = metadataProjectPaths(metadata);
3412
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
3413
+ candidatePaths.push(sessionProjectPath);
3414
+ }
3415
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
3416
+ const pathHashes = candidatePaths.map((candidate) => {
3417
+ try {
3418
+ return { path: candidate, hash: hashProjectPath(candidate) };
3419
+ } catch {
3420
+ return { path: candidate, hash: void 0 };
3421
+ }
3422
+ });
3423
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
3424
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
3425
+ let action = "skipped";
3426
+ let reason;
3427
+ let observedProjectHash;
3428
+ if (foreignPath) {
3429
+ action = "quarantined";
3430
+ reason = "project-path-mismatch";
3431
+ observedProjectHash = foreignPath.hash;
3432
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
3433
+ action = "quarantined";
3434
+ reason = "content-project-mismatch";
3435
+ } else if (currentHash === projectHash) {
3436
+ result.alreadyScoped++;
3437
+ continue;
3438
+ } else if (currentHash && currentHash !== projectHash) {
3439
+ action = "quarantined";
3440
+ reason = "scope-hash-mismatch";
3441
+ observedProjectHash = currentHash;
3442
+ } else if (matchingPath) {
3443
+ action = "repaired";
3444
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
3445
+ } else if (candidatePaths.length > 0) {
3446
+ action = "quarantined";
3447
+ reason = "project-path-mismatch";
3448
+ } else if (importedOrLegacy) {
3449
+ action = "quarantined";
3450
+ reason = "missing-project-scope";
3451
+ }
3452
+ if (action === "skipped" || !reason) {
3453
+ result.skipped++;
3454
+ continue;
3455
+ }
3456
+ if (action === "repaired") {
3457
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
3458
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
3459
+ project.hash = projectHash;
3460
+ scope.project = project;
3461
+ metadata.scope = scope;
3462
+ metadata.repair = {
3463
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3464
+ legacyProjectScope: {
3465
+ action,
3466
+ reason,
3467
+ repairedAt: nowIso
3468
+ }
3469
+ };
3470
+ addMetadataTag(metadata, `proj:${projectHash}`);
3471
+ result.repaired++;
3472
+ } else {
3473
+ metadata.quarantine = {
3474
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
3475
+ status: "active",
3476
+ category: "project-scope",
3477
+ reason,
3478
+ detectedAt: nowIso,
3479
+ expectedProjectHash: projectHash,
3480
+ ...observedProjectHash ? { observedProjectHash } : {}
3481
+ };
3482
+ metadata.repair = {
3483
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3484
+ legacyProjectScope: {
3485
+ action,
3486
+ reason,
3487
+ repairedAt: nowIso
3488
+ }
3489
+ };
3490
+ addMetadataTag(metadata, "quarantine:project-scope");
3491
+ result.quarantined++;
3492
+ }
3493
+ sample({ eventId: row.id, action, reason });
3494
+ if (!dryRun) {
3495
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
3496
+ }
3497
+ }
3498
+ return result;
3499
+ }
3145
3500
  /**
3146
3501
  * Get embedding/vector outbox health statistics
3147
3502
  */
@@ -3189,7 +3544,11 @@ var SQLiteEventStore = class {
3189
3544
  await this.initialize();
3190
3545
  const rows = sqliteAll(
3191
3546
  this.db,
3192
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
3547
+ `SELECT ml.level, COUNT(*) as count
3548
+ FROM memory_levels ml
3549
+ INNER JOIN events e ON e.id = ml.event_id
3550
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
3551
+ GROUP BY ml.level`
3193
3552
  );
3194
3553
  return rows;
3195
3554
  }
@@ -3205,6 +3564,7 @@ var SQLiteEventStore = class {
3205
3564
  `SELECT e.* FROM events e
3206
3565
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3207
3566
  WHERE ml.level = ?
3567
+ AND ${notActiveQuarantinedSql("e.metadata")}
3208
3568
  ORDER BY e.timestamp DESC
3209
3569
  LIMIT ? OFFSET ?`,
3210
3570
  [level, limit, offset]
@@ -3297,12 +3657,13 @@ var SQLiteEventStore = class {
3297
3657
  /**
3298
3658
  * Get most accessed memories (falls back to recent events if none accessed)
3299
3659
  */
3300
- async getMostAccessed(limit = 10) {
3660
+ async getMostAccessed(limit = 10, options) {
3301
3661
  await this.initialize();
3302
3662
  let rows = sqliteAll(
3303
3663
  this.db,
3304
3664
  `SELECT * FROM events
3305
3665
  WHERE access_count > 0
3666
+ AND ${maybeQuarantinePredicate(options)}
3306
3667
  ORDER BY access_count DESC, last_accessed_at DESC
3307
3668
  LIMIT ?`,
3308
3669
  [limit]
@@ -3311,6 +3672,7 @@ var SQLiteEventStore = class {
3311
3672
  rows = sqliteAll(
3312
3673
  this.db,
3313
3674
  `SELECT * FROM events
3675
+ WHERE ${maybeQuarantinePredicate(options)}
3314
3676
  ORDER BY timestamp DESC
3315
3677
  LIMIT ?`,
3316
3678
  [limit]
@@ -3441,6 +3803,7 @@ var SQLiteEventStore = class {
3441
3803
  FROM memory_helpfulness mh
3442
3804
  JOIN events e ON e.id = mh.event_id
3443
3805
  WHERE mh.measured_at IS NOT NULL
3806
+ AND ${notActiveQuarantinedSql("e.metadata")}
3444
3807
  GROUP BY mh.event_id
3445
3808
  ORDER BY avg_score DESC
3446
3809
  LIMIT ?`,
@@ -3505,6 +3868,7 @@ var SQLiteEventStore = class {
3505
3868
  FROM events_fts fts
3506
3869
  JOIN events e ON e.id = fts.event_id
3507
3870
  WHERE events_fts MATCH ?
3871
+ AND ${notActiveQuarantinedSql("e.metadata")}
3508
3872
  ORDER BY fts.rank
3509
3873
  LIMIT ?`,
3510
3874
  [searchTerms, limit]
@@ -3519,6 +3883,7 @@ var SQLiteEventStore = class {
3519
3883
  this.db,
3520
3884
  `SELECT *, 0 as rank FROM events
3521
3885
  WHERE content LIKE ?
3886
+ AND ${notActiveQuarantinedSql()}
3522
3887
  ORDER BY timestamp DESC
3523
3888
  LIMIT ?`,
3524
3889
  [likePattern, limit]
@@ -3725,6 +4090,7 @@ var SQLiteEventStore = class {
3725
4090
  `SELECT turn_id, MIN(timestamp) as min_ts
3726
4091
  FROM events
3727
4092
  WHERE session_id = ? AND turn_id IS NOT NULL
4093
+ AND ${maybeQuarantinePredicate(options)}
3728
4094
  GROUP BY turn_id
3729
4095
  ORDER BY min_ts DESC
3730
4096
  LIMIT ? OFFSET ?`,
@@ -3732,7 +4098,7 @@ var SQLiteEventStore = class {
3732
4098
  );
3733
4099
  const turns = [];
3734
4100
  for (const turnRow of turnRows) {
3735
- const events = await this.getEventsByTurn(turnRow.turn_id);
4101
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3736
4102
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3737
4103
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3738
4104
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3751,11 +4117,11 @@ var SQLiteEventStore = class {
3751
4117
  /**
3752
4118
  * Get all events for a specific turn_id
3753
4119
  */
3754
- async getEventsByTurn(turnId) {
4120
+ async getEventsByTurn(turnId, options) {
3755
4121
  await this.initialize();
3756
4122
  const rows = sqliteAll(
3757
4123
  this.db,
3758
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4124
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3759
4125
  [turnId]
3760
4126
  );
3761
4127
  return rows.map(this.rowToEvent);
@@ -3763,13 +4129,14 @@ var SQLiteEventStore = class {
3763
4129
  /**
3764
4130
  * Count total turns for a session
3765
4131
  */
3766
- async countSessionTurns(sessionId) {
4132
+ async countSessionTurns(sessionId, options) {
3767
4133
  await this.initialize();
3768
4134
  const row = sqliteGet(
3769
4135
  this.db,
3770
4136
  `SELECT COUNT(DISTINCT turn_id) as count
3771
4137
  FROM events
3772
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4138
+ WHERE session_id = ? AND turn_id IS NOT NULL
4139
+ AND ${maybeQuarantinePredicate(options)}`,
3773
4140
  [sessionId]
3774
4141
  );
3775
4142
  return row?.count || 0;
@@ -3861,7 +4228,7 @@ var SQLiteEventStore = class {
3861
4228
  content: row.content,
3862
4229
  canonicalKey: row.canonical_key,
3863
4230
  dedupeKey: row.dedupe_key,
3864
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4231
+ metadata: safeParseMetadataValue(row.metadata)
3865
4232
  };
3866
4233
  if (row.access_count !== void 0) {
3867
4234
  event.access_count = row.access_count;
@@ -4013,6 +4380,7 @@ var VectorStore = class {
4013
4380
  * Get total count of vectors
4014
4381
  */
4015
4382
  async count() {
4383
+ await this.initialize();
4016
4384
  if (!this.table)
4017
4385
  return 0;
4018
4386
  const result = await this.table.countRows();
@@ -4451,6 +4819,14 @@ var MemoryQueryService = class {
4451
4819
  await this.initialize();
4452
4820
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4453
4821
  }
4822
+ async recoverStuckOutboxItems(options) {
4823
+ await this.initialize();
4824
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4825
+ }
4826
+ async repairLegacyProjectScope(options) {
4827
+ await this.initialize();
4828
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4829
+ }
4454
4830
  async getStats() {
4455
4831
  await this.initialize();
4456
4832
  const deps = this.getStatsDeps();
@@ -6073,18 +6449,18 @@ function assertDefaultRetrieverStore(eventStore) {
6073
6449
  function createMemoryEngineServices(options) {
6074
6450
  const factories = options.factories ?? {};
6075
6451
  const storagePath = options.storagePath;
6076
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6077
- fs5.mkdirSync(storagePath, { recursive: true });
6452
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6453
+ fs6.mkdirSync(storagePath, { recursive: true });
6078
6454
  }
6079
6455
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6080
- path4.join(storagePath, "events.sqlite"),
6456
+ path5.join(storagePath, "events.sqlite"),
6081
6457
  {
6082
6458
  readonly: options.readOnly,
6083
6459
  markdownMirrorRoot: storagePath
6084
6460
  }
6085
6461
  );
6086
6462
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6087
- path4.join(storagePath, "vectors")
6463
+ path5.join(storagePath, "vectors")
6088
6464
  );
6089
6465
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6090
6466
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6495,8 +6871,8 @@ function createMemoryRuntimeService(deps) {
6495
6871
  }
6496
6872
 
6497
6873
  // src/extensions/shared-memory/shared-memory-services.ts
6498
- import * as fs6 from "fs";
6499
- import * as path5 from "path";
6874
+ import * as fs7 from "fs";
6875
+ import * as path6 from "path";
6500
6876
 
6501
6877
  // src/core/shared-event-store.ts
6502
6878
  var SharedEventStore = class {
@@ -7184,7 +7560,7 @@ var SharedMemoryServices = class {
7184
7560
  this.ensureDirectory(sharedPath, { allowCreate: true });
7185
7561
  const store = await this.openStore(sharedPath);
7186
7562
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7187
- path5.join(sharedPath, "vectors")
7563
+ path6.join(sharedPath, "vectors")
7188
7564
  );
7189
7565
  await this.sharedVectorStore.initialize();
7190
7566
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7257,7 +7633,7 @@ var SharedMemoryServices = class {
7257
7633
  async createOpenStorePromise(sharedPath) {
7258
7634
  if (!this.sharedEventStore) {
7259
7635
  const sharedEventStore = this.factories.createSharedEventStore(
7260
- path5.join(sharedPath, "shared.duckdb")
7636
+ path6.join(sharedPath, "shared.duckdb")
7261
7637
  );
7262
7638
  await sharedEventStore.initialize();
7263
7639
  this.sharedEventStore = sharedEventStore;
@@ -7277,9 +7653,9 @@ var SharedMemoryServices = class {
7277
7653
  }
7278
7654
  get factories() {
7279
7655
  return {
7280
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7656
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7281
7657
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7282
- fs6.mkdirSync(targetPath, { recursive: true });
7658
+ fs7.mkdirSync(targetPath, { recursive: true });
7283
7659
  }),
7284
7660
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7285
7661
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7394,33 +7770,11 @@ function createMemoryServiceComposition(options) {
7394
7770
  }
7395
7771
  function defaultExpandPath(targetPath) {
7396
7772
  if (targetPath.startsWith("~")) {
7397
- return path6.join(os.homedir(), targetPath.slice(1));
7773
+ return path7.join(os2.homedir(), targetPath.slice(1));
7398
7774
  }
7399
7775
  return targetPath;
7400
7776
  }
7401
7777
 
7402
- // src/core/registry/project-path.ts
7403
- import * as crypto2 from "crypto";
7404
- import * as fs7 from "fs";
7405
- import * as os2 from "os";
7406
- import * as path7 from "path";
7407
- function normalizeProjectPath(projectPath) {
7408
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7409
- try {
7410
- return fs7.realpathSync(expanded);
7411
- } catch {
7412
- return path7.resolve(expanded);
7413
- }
7414
- }
7415
- function hashProjectPath(projectPath) {
7416
- const normalizedPath = normalizeProjectPath(projectPath);
7417
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7418
- }
7419
- function getProjectStoragePath(projectPath) {
7420
- const hash = hashProjectPath(projectPath);
7421
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7422
- }
7423
-
7424
7778
  // src/core/registry/session-registry.ts
7425
7779
  import * as fs8 from "fs";
7426
7780
  import * as os3 from "os";
@@ -7719,6 +8073,12 @@ var MemoryService = class {
7719
8073
  async getOutboxStats() {
7720
8074
  return this.queryService.getOutboxStats();
7721
8075
  }
8076
+ async recoverStuckOutboxItems(options) {
8077
+ return this.queryService.recoverStuckOutboxItems(options);
8078
+ }
8079
+ async repairLegacyProjectScope(options) {
8080
+ return this.queryService.repairLegacyProjectScope(options);
8081
+ }
7722
8082
  async getRetrievalTraceStats() {
7723
8083
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7724
8084
  }
@@ -8104,10 +8464,10 @@ var SENSITIVE_PATTERNS = [
8104
8464
  // Redact the whole URI so usernames, credentials, hosts, paths, and query
8105
8465
  // params do not leak either.
8106
8466
  /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>/@]+@[^\s'"`<>]+/gi,
8107
- /password\s*[:=]\s*['"]?[^\s'"]+/gi,
8108
- /api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/gi,
8109
- /secret\s*[:=]\s*['"]?[^\s'"]+/gi,
8110
- /token\s*[:=]\s*['"]?[^\s'"]+/gi,
8467
+ /(?:[\w.-]+[-_])?password\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8468
+ /(?:[\w.-]+[-_])?api[-_]?key\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8469
+ /(?:[\w.-]+[-_])?secret\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8470
+ /(?:[\w.-]+[-_])?token\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8111
8471
  /bearer\s+[a-zA-Z0-9\-_.]+/gi,
8112
8472
  /AWS[_-]?ACCESS[_-]?KEY[_-]?ID\s*[:=]\s*['"]?[A-Z0-9]+/gi,
8113
8473
  /AWS[_-]?SECRET[_-]?ACCESS[_-]?KEY\s*[:=]\s*['"]?[^\s'"]+/gi,
@@ -8117,6 +8477,35 @@ var SENSITIVE_PATTERNS = [
8117
8477
  /sk-[a-zA-Z0-9]{48}/g
8118
8478
  // OpenAI API Key
8119
8479
  ];
8480
+ var CLI_SECRET_OPTION_PATTERNS = [
8481
+ /(--(?:[a-z0-9]+[-_])*(?:password|secret|api[-_]?key|token|bearer)(?:[-_][a-z0-9]+)*(?:\s+|=))(?:"[^"]*"|'[^']*'|[^\s'"`<>]+)/gi
8482
+ ];
8483
+ var URL_FOLLOWING_SECRET_PATTERN = /((?:https?:\/\/[^\s'"`<>]+)\s*\r?\n\s*)([A-Za-z0-9!@#$%^&*._+\-~:=/]{6,})(?=\s*(?:\r?\n|$))/gi;
8484
+ function looksLikePastedSecret(value) {
8485
+ return value.length >= 8 && /(?:\d|[^A-Za-z0-9])/.test(value);
8486
+ }
8487
+ function maskUrlFollowingSecret(value) {
8488
+ let count = 0;
8489
+ const content = value.replace(URL_FOLLOWING_SECRET_PATTERN, (_match, prefix, secret) => {
8490
+ if (!looksLikePastedSecret(secret))
8491
+ return `${prefix}${secret}`;
8492
+ count++;
8493
+ return `${prefix}[REDACTED]`;
8494
+ });
8495
+ return { content, count };
8496
+ }
8497
+ function maskCliSecretOptions(value) {
8498
+ let count = 0;
8499
+ let filtered = value;
8500
+ for (const pattern of CLI_SECRET_OPTION_PATTERNS) {
8501
+ pattern.lastIndex = 0;
8502
+ filtered = filtered.replace(pattern, (_match, prefix) => {
8503
+ count++;
8504
+ return `${prefix}[REDACTED]`;
8505
+ });
8506
+ }
8507
+ return { content: filtered, count };
8508
+ }
8120
8509
  function applyPrivacyFilter(content, config) {
8121
8510
  let filtered = content;
8122
8511
  let privateTagCount = 0;
@@ -8130,6 +8519,12 @@ function applyPrivacyFilter(content, config) {
8130
8519
  filtered = tagResult.filtered;
8131
8520
  privateTagCount = tagResult.stats.count;
8132
8521
  }
8522
+ const cliResult = maskCliSecretOptions(filtered);
8523
+ filtered = cliResult.content;
8524
+ patternMatchCount += cliResult.count;
8525
+ const urlSecretResult = maskUrlFollowingSecret(filtered);
8526
+ filtered = urlSecretResult.content;
8527
+ patternMatchCount += urlSecretResult.count;
8133
8528
  for (const pattern of SENSITIVE_PATTERNS) {
8134
8529
  pattern.lastIndex = 0;
8135
8530
  const matches = filtered.match(pattern);
@@ -8141,13 +8536,13 @@ function applyPrivacyFilter(content, config) {
8141
8536
  for (const patternStr of config.excludePatterns || []) {
8142
8537
  try {
8143
8538
  const regex = new RegExp(
8144
- `(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
8539
+ `(^|[^\\w-])(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
8145
8540
  "gi"
8146
8541
  );
8147
8542
  const matches = filtered.match(regex);
8148
8543
  if (matches) {
8149
8544
  patternMatchCount += matches.length;
8150
- filtered = filtered.replace(regex, "[REDACTED]");
8545
+ filtered = filtered.replace(regex, "$1[REDACTED]");
8151
8546
  }
8152
8547
  } catch {
8153
8548
  }