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 getFileType(filePath) {
@@ -1641,8 +1641,8 @@ function createEndlessMemoryServices(options) {
1641
1641
  }
1642
1642
 
1643
1643
  // src/core/engine/memory-engine-services.ts
1644
- import * as fs5 from "fs";
1645
- import * as path4 from "path";
1644
+ import * as fs6 from "fs";
1645
+ import * as path5 from "path";
1646
1646
 
1647
1647
  // src/extensions/vector/embedder.ts
1648
1648
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -2320,14 +2320,39 @@ function makeDedupeKey(content, sessionId) {
2320
2320
  return `${sessionId}:${contentHash}`;
2321
2321
  }
2322
2322
 
2323
+ // src/core/sqlite-event-store.ts
2324
+ import * as nodePath2 from "path";
2325
+
2326
+ // src/core/registry/project-path.ts
2327
+ import * as crypto2 from "crypto";
2328
+ import * as fs3 from "fs";
2329
+ import * as os from "os";
2330
+ import * as path3 from "path";
2331
+ function normalizeProjectPath(projectPath) {
2332
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
2333
+ try {
2334
+ return fs3.realpathSync(expanded);
2335
+ } catch {
2336
+ return path3.resolve(expanded);
2337
+ }
2338
+ }
2339
+ function hashProjectPath(projectPath) {
2340
+ const normalizedPath = normalizeProjectPath(projectPath);
2341
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
2342
+ }
2343
+ function getProjectStoragePath(projectPath) {
2344
+ const hash = hashProjectPath(projectPath);
2345
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
2346
+ }
2347
+
2323
2348
  // src/core/sqlite-wrapper.ts
2324
2349
  import Database from "better-sqlite3";
2325
- import * as fs3 from "fs";
2350
+ import * as fs4 from "fs";
2326
2351
  import * as nodePath from "path";
2327
2352
  function createSQLiteDatabase(path12, options) {
2328
2353
  const dir = nodePath.dirname(path12);
2329
- if (!fs3.existsSync(dir)) {
2330
- fs3.mkdirSync(dir, { recursive: true });
2354
+ if (!fs4.existsSync(dir)) {
2355
+ fs4.mkdirSync(dir, { recursive: true });
2331
2356
  }
2332
2357
  const db = new Database(path12, {
2333
2358
  readonly: options?.readonly ?? false
@@ -2376,8 +2401,8 @@ function toSQLiteTimestamp(date) {
2376
2401
  }
2377
2402
 
2378
2403
  // src/core/markdown-mirror.ts
2379
- import * as fs4 from "fs/promises";
2380
- import * as path3 from "path";
2404
+ import * as fs5 from "fs/promises";
2405
+ import * as path4 from "path";
2381
2406
  var DEFAULT_NAMESPACE = "default";
2382
2407
  var DEFAULT_CATEGORY = "uncategorized";
2383
2408
  function sanitizeSegment2(input, fallback) {
@@ -2406,7 +2431,7 @@ function buildMirrorPath2(rootDir, event) {
2406
2431
  const yyyy = d.getFullYear();
2407
2432
  const mm = String(d.getMonth() + 1).padStart(2, "0");
2408
2433
  const dd = String(d.getDate()).padStart(2, "0");
2409
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2434
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2410
2435
  }
2411
2436
  function formatMirrorEntry(event) {
2412
2437
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -2427,8 +2452,8 @@ var MarkdownMirror2 = class {
2427
2452
  }
2428
2453
  async append(event) {
2429
2454
  const outPath = buildMirrorPath2(this.rootDir, event);
2430
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
2431
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
2455
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
2456
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
2432
2457
  return outPath;
2433
2458
  }
2434
2459
  };
@@ -2441,6 +2466,143 @@ function normalizeQueryRewriteKind(value) {
2441
2466
  return "none";
2442
2467
  }
2443
2468
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2469
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2470
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2471
+ function emptyOutboxRecoveryResult() {
2472
+ return {
2473
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2474
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2475
+ };
2476
+ }
2477
+ function isRecord(value) {
2478
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2479
+ }
2480
+ function getNestedRecord(root, path12) {
2481
+ let cursor = root;
2482
+ for (const key of path12) {
2483
+ if (!isRecord(cursor))
2484
+ return void 0;
2485
+ cursor = cursor[key];
2486
+ }
2487
+ return isRecord(cursor) ? cursor : void 0;
2488
+ }
2489
+ function getNestedString(root, path12) {
2490
+ let cursor = root;
2491
+ for (const key of path12) {
2492
+ if (!isRecord(cursor))
2493
+ return void 0;
2494
+ cursor = cursor[key];
2495
+ }
2496
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
2497
+ }
2498
+ function metadataProjectHash(metadata) {
2499
+ return getNestedString(metadata, ["scope", "project", "hash"]);
2500
+ }
2501
+ function metadataProjectPaths(metadata) {
2502
+ const candidates = [
2503
+ getNestedString(metadata, ["projectPath"]),
2504
+ getNestedString(metadata, ["sourceProjectPath"]),
2505
+ getNestedString(metadata, ["scope", "project", "path"])
2506
+ ];
2507
+ const paths = [];
2508
+ for (const value of candidates) {
2509
+ if (value && !paths.includes(value))
2510
+ paths.push(value);
2511
+ }
2512
+ return paths;
2513
+ }
2514
+ function metadataProjectPath(metadata) {
2515
+ return metadataProjectPaths(metadata)[0];
2516
+ }
2517
+ function isActiveQuarantinedMetadata(metadata) {
2518
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
2519
+ return quarantine?.status === "active";
2520
+ }
2521
+ function activeQuarantineStatusExpression(column = "metadata") {
2522
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
2523
+ }
2524
+ function notActiveQuarantinedSql(column = "metadata") {
2525
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
2526
+ }
2527
+ function maybeQuarantinePredicate(options, column = "metadata") {
2528
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
2529
+ }
2530
+ function safeParseMetadataValue(value) {
2531
+ if (!value)
2532
+ return void 0;
2533
+ if (typeof value === "object")
2534
+ return isRecord(value) ? value : void 0;
2535
+ if (typeof value !== "string")
2536
+ return void 0;
2537
+ try {
2538
+ const parsed = JSON.parse(value);
2539
+ return isRecord(parsed) ? parsed : void 0;
2540
+ } catch {
2541
+ return void 0;
2542
+ }
2543
+ }
2544
+ function isImportedOrLegacyScopedMetadata(metadata) {
2545
+ if (!metadata)
2546
+ return false;
2547
+ return Boolean(
2548
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
2549
+ );
2550
+ }
2551
+ function addMetadataTag(metadata, tag) {
2552
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
2553
+ if (!current.includes(tag))
2554
+ metadata.tags = [...current, tag];
2555
+ }
2556
+ function buildRepairResult(projectHash, dryRun) {
2557
+ return {
2558
+ dryRun,
2559
+ projectHash,
2560
+ scanned: 0,
2561
+ repaired: 0,
2562
+ quarantined: 0,
2563
+ alreadyScoped: 0,
2564
+ skipped: 0,
2565
+ samples: []
2566
+ };
2567
+ }
2568
+ function normalizeRepoName(value) {
2569
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
2570
+ }
2571
+ function projectBasename(projectPath) {
2572
+ if (!projectPath)
2573
+ return void 0;
2574
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
2575
+ const basename2 = nodePath2.basename(trimmed);
2576
+ return basename2 ? normalizeRepoName(basename2) : void 0;
2577
+ }
2578
+ function isProjectScopeRepairExplanation(content) {
2579
+ const normalized = content.toLowerCase();
2580
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
2581
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
2582
+ return hasRepairContext && hasExplanationContext;
2583
+ }
2584
+ function hasConflictingContentProjectHint(content, projectPath) {
2585
+ const currentName = projectBasename(projectPath);
2586
+ if (!currentName)
2587
+ return false;
2588
+ if (isProjectScopeRepairExplanation(content))
2589
+ return false;
2590
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
2591
+ let githubMatch;
2592
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
2593
+ const repo = normalizeRepoName(githubMatch[2] || "");
2594
+ if (repo && repo !== currentName)
2595
+ return true;
2596
+ }
2597
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
2598
+ let workspaceMatch;
2599
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
2600
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
2601
+ if (repo && repo !== currentName)
2602
+ return true;
2603
+ }
2604
+ return false;
2605
+ }
2444
2606
  var SQLiteEventStore = class {
2445
2607
  db;
2446
2608
  initialized = false;
@@ -2939,11 +3101,11 @@ var SQLiteEventStore = class {
2939
3101
  /**
2940
3102
  * Get events by session ID
2941
3103
  */
2942
- async getSessionEvents(sessionId) {
3104
+ async getSessionEvents(sessionId, options) {
2943
3105
  await this.initialize();
2944
3106
  const rows = sqliteAll(
2945
3107
  this.db,
2946
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
3108
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2947
3109
  [sessionId]
2948
3110
  );
2949
3111
  return rows.map(this.rowToEvent);
@@ -2951,11 +3113,11 @@ var SQLiteEventStore = class {
2951
3113
  /**
2952
3114
  * Get recent events
2953
3115
  */
2954
- async getRecentEvents(limit = 100) {
3116
+ async getRecentEvents(limit = 100, options) {
2955
3117
  await this.initialize();
2956
3118
  const rows = sqliteAll(
2957
3119
  this.db,
2958
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3120
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2959
3121
  [limit]
2960
3122
  );
2961
3123
  return rows.map(this.rowToEvent);
@@ -2963,11 +3125,11 @@ var SQLiteEventStore = class {
2963
3125
  /**
2964
3126
  * Get event by ID
2965
3127
  */
2966
- async getEvent(id) {
3128
+ async getEvent(id, options) {
2967
3129
  await this.initialize();
2968
3130
  const row = sqliteGet(
2969
3131
  this.db,
2970
- `SELECT * FROM events WHERE id = ?`,
3132
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2971
3133
  [id]
2972
3134
  );
2973
3135
  if (!row)
@@ -2977,11 +3139,11 @@ var SQLiteEventStore = class {
2977
3139
  /**
2978
3140
  * Get events since a timestamp (for sync)
2979
3141
  */
2980
- async getEventsSince(timestamp, limit = 1e3) {
3142
+ async getEventsSince(timestamp, limit = 1e3, options) {
2981
3143
  await this.initialize();
2982
3144
  const rows = sqliteAll(
2983
3145
  this.db,
2984
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3146
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2985
3147
  [timestamp, limit]
2986
3148
  );
2987
3149
  return rows.map(this.rowToEvent);
@@ -2990,11 +3152,11 @@ var SQLiteEventStore = class {
2990
3152
  * Get events since a SQLite rowid (for robust incremental replication).
2991
3153
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2992
3154
  */
2993
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3155
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2994
3156
  await this.initialize();
2995
3157
  const rows = sqliteAll(
2996
3158
  this.db,
2997
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
3159
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
2998
3160
  [lastRowid, limit]
2999
3161
  );
3000
3162
  return rows.map((row) => ({
@@ -3191,7 +3353,9 @@ var SQLiteEventStore = class {
3191
3353
  const placeholders = ids.map(() => "?").join(",");
3192
3354
  sqliteRun(
3193
3355
  this.db,
3194
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3356
+ `UPDATE embedding_outbox
3357
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3358
+ WHERE id IN (${placeholders})`,
3195
3359
  ids
3196
3360
  );
3197
3361
  return pending.map((row) => ({
@@ -3227,19 +3391,19 @@ var SQLiteEventStore = class {
3227
3391
  /**
3228
3392
  * Count total events
3229
3393
  */
3230
- async countEvents() {
3394
+ async countEvents(options) {
3231
3395
  await this.initialize();
3232
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
3396
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
3233
3397
  return row?.count || 0;
3234
3398
  }
3235
3399
  /**
3236
3400
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3237
3401
  */
3238
- async getEventsPage(limit = 1e3, offset = 0) {
3402
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3239
3403
  await this.initialize();
3240
3404
  const rows = sqliteAll(
3241
3405
  this.db,
3242
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3406
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3243
3407
  [limit, offset]
3244
3408
  );
3245
3409
  return rows.map(this.rowToEvent);
@@ -3261,6 +3425,197 @@ var SQLiteEventStore = class {
3261
3425
  [error, ...ids]
3262
3426
  );
3263
3427
  }
3428
+ /**
3429
+ * Recover abandoned outbox work after a worker/process crash.
3430
+ *
3431
+ * Rows in `processing` are claimed work. If the process exits before marking
3432
+ * them done/failed, they otherwise remain invisible to future processing.
3433
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3434
+ */
3435
+ async recoverStuckOutboxItems(options = {}) {
3436
+ await this.initialize();
3437
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3438
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3439
+ const now = options.now ?? /* @__PURE__ */ new Date();
3440
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3441
+ const result = emptyOutboxRecoveryResult();
3442
+ const embeddingRecovered = sqliteRun(
3443
+ this.db,
3444
+ `UPDATE embedding_outbox
3445
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3446
+ WHERE status = 'processing'
3447
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3448
+ [threshold]
3449
+ );
3450
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3451
+ const embeddingRetried = sqliteRun(
3452
+ this.db,
3453
+ `UPDATE embedding_outbox
3454
+ SET status = 'pending', error_message = NULL
3455
+ WHERE status = 'failed'
3456
+ AND retry_count < ?`,
3457
+ [maxRetries]
3458
+ );
3459
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3460
+ const vectorRecovered = sqliteRun(
3461
+ this.db,
3462
+ `UPDATE vector_outbox
3463
+ SET status = 'pending', updated_at = ?, error = NULL
3464
+ WHERE status = 'processing'
3465
+ AND datetime(updated_at) < datetime(?)`,
3466
+ [now.toISOString(), threshold]
3467
+ );
3468
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3469
+ const vectorRetried = sqliteRun(
3470
+ this.db,
3471
+ `UPDATE vector_outbox
3472
+ SET status = 'pending', updated_at = ?, error = NULL
3473
+ WHERE status = 'failed'
3474
+ AND retry_count < ?`,
3475
+ [now.toISOString(), maxRetries]
3476
+ );
3477
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3478
+ return result;
3479
+ }
3480
+ /**
3481
+ * Repair legacy imported events that predate canonical project scope metadata.
3482
+ *
3483
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
3484
+ * imported but cannot be proven to belong to this project are quarantined so
3485
+ * dashboard default reads/search do not surface cross-project contamination.
3486
+ */
3487
+ async repairLegacyProjectScope(options = {}) {
3488
+ await this.initialize();
3489
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
3490
+ if (!projectHash) {
3491
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
3492
+ }
3493
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
3494
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
3495
+ }
3496
+ const dryRun = options.dryRun === true;
3497
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
3498
+ const result = buildRepairResult(projectHash, dryRun);
3499
+ const rows = sqliteAll(
3500
+ this.db,
3501
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
3502
+ FROM events e
3503
+ LEFT JOIN sessions s ON s.id = e.session_id
3504
+ ORDER BY e.timestamp ASC`,
3505
+ []
3506
+ );
3507
+ const sample = (entry) => {
3508
+ if (result.samples.length < 20)
3509
+ result.samples.push(entry);
3510
+ };
3511
+ for (const row of rows) {
3512
+ result.scanned++;
3513
+ let metadata = {};
3514
+ let metadataParseInvalid = false;
3515
+ if (row.metadata) {
3516
+ const parsed = safeParseMetadataValue(row.metadata);
3517
+ if (parsed) {
3518
+ metadata = parsed;
3519
+ } else {
3520
+ metadataParseInvalid = true;
3521
+ }
3522
+ }
3523
+ if (isActiveQuarantinedMetadata(metadata)) {
3524
+ result.skipped++;
3525
+ continue;
3526
+ }
3527
+ const currentHash = metadataProjectHash(metadata);
3528
+ const explicitPath = metadataProjectPath(metadata);
3529
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
3530
+ const candidatePaths = metadataProjectPaths(metadata);
3531
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
3532
+ candidatePaths.push(sessionProjectPath);
3533
+ }
3534
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
3535
+ const pathHashes = candidatePaths.map((candidate) => {
3536
+ try {
3537
+ return { path: candidate, hash: hashProjectPath(candidate) };
3538
+ } catch {
3539
+ return { path: candidate, hash: void 0 };
3540
+ }
3541
+ });
3542
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
3543
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
3544
+ let action = "skipped";
3545
+ let reason;
3546
+ let observedProjectHash;
3547
+ if (foreignPath) {
3548
+ action = "quarantined";
3549
+ reason = "project-path-mismatch";
3550
+ observedProjectHash = foreignPath.hash;
3551
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
3552
+ action = "quarantined";
3553
+ reason = "content-project-mismatch";
3554
+ } else if (currentHash === projectHash) {
3555
+ result.alreadyScoped++;
3556
+ continue;
3557
+ } else if (currentHash && currentHash !== projectHash) {
3558
+ action = "quarantined";
3559
+ reason = "scope-hash-mismatch";
3560
+ observedProjectHash = currentHash;
3561
+ } else if (matchingPath) {
3562
+ action = "repaired";
3563
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
3564
+ } else if (candidatePaths.length > 0) {
3565
+ action = "quarantined";
3566
+ reason = "project-path-mismatch";
3567
+ } else if (importedOrLegacy) {
3568
+ action = "quarantined";
3569
+ reason = "missing-project-scope";
3570
+ }
3571
+ if (action === "skipped" || !reason) {
3572
+ result.skipped++;
3573
+ continue;
3574
+ }
3575
+ if (action === "repaired") {
3576
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
3577
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
3578
+ project.hash = projectHash;
3579
+ scope.project = project;
3580
+ metadata.scope = scope;
3581
+ metadata.repair = {
3582
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3583
+ legacyProjectScope: {
3584
+ action,
3585
+ reason,
3586
+ repairedAt: nowIso
3587
+ }
3588
+ };
3589
+ addMetadataTag(metadata, `proj:${projectHash}`);
3590
+ result.repaired++;
3591
+ } else {
3592
+ metadata.quarantine = {
3593
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
3594
+ status: "active",
3595
+ category: "project-scope",
3596
+ reason,
3597
+ detectedAt: nowIso,
3598
+ expectedProjectHash: projectHash,
3599
+ ...observedProjectHash ? { observedProjectHash } : {}
3600
+ };
3601
+ metadata.repair = {
3602
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3603
+ legacyProjectScope: {
3604
+ action,
3605
+ reason,
3606
+ repairedAt: nowIso
3607
+ }
3608
+ };
3609
+ addMetadataTag(metadata, "quarantine:project-scope");
3610
+ result.quarantined++;
3611
+ }
3612
+ sample({ eventId: row.id, action, reason });
3613
+ if (!dryRun) {
3614
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
3615
+ }
3616
+ }
3617
+ return result;
3618
+ }
3264
3619
  /**
3265
3620
  * Get embedding/vector outbox health statistics
3266
3621
  */
@@ -3308,7 +3663,11 @@ var SQLiteEventStore = class {
3308
3663
  await this.initialize();
3309
3664
  const rows = sqliteAll(
3310
3665
  this.db,
3311
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
3666
+ `SELECT ml.level, COUNT(*) as count
3667
+ FROM memory_levels ml
3668
+ INNER JOIN events e ON e.id = ml.event_id
3669
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
3670
+ GROUP BY ml.level`
3312
3671
  );
3313
3672
  return rows;
3314
3673
  }
@@ -3324,6 +3683,7 @@ var SQLiteEventStore = class {
3324
3683
  `SELECT e.* FROM events e
3325
3684
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3326
3685
  WHERE ml.level = ?
3686
+ AND ${notActiveQuarantinedSql("e.metadata")}
3327
3687
  ORDER BY e.timestamp DESC
3328
3688
  LIMIT ? OFFSET ?`,
3329
3689
  [level, limit, offset]
@@ -3416,12 +3776,13 @@ var SQLiteEventStore = class {
3416
3776
  /**
3417
3777
  * Get most accessed memories (falls back to recent events if none accessed)
3418
3778
  */
3419
- async getMostAccessed(limit = 10) {
3779
+ async getMostAccessed(limit = 10, options) {
3420
3780
  await this.initialize();
3421
3781
  let rows = sqliteAll(
3422
3782
  this.db,
3423
3783
  `SELECT * FROM events
3424
3784
  WHERE access_count > 0
3785
+ AND ${maybeQuarantinePredicate(options)}
3425
3786
  ORDER BY access_count DESC, last_accessed_at DESC
3426
3787
  LIMIT ?`,
3427
3788
  [limit]
@@ -3430,6 +3791,7 @@ var SQLiteEventStore = class {
3430
3791
  rows = sqliteAll(
3431
3792
  this.db,
3432
3793
  `SELECT * FROM events
3794
+ WHERE ${maybeQuarantinePredicate(options)}
3433
3795
  ORDER BY timestamp DESC
3434
3796
  LIMIT ?`,
3435
3797
  [limit]
@@ -3560,6 +3922,7 @@ var SQLiteEventStore = class {
3560
3922
  FROM memory_helpfulness mh
3561
3923
  JOIN events e ON e.id = mh.event_id
3562
3924
  WHERE mh.measured_at IS NOT NULL
3925
+ AND ${notActiveQuarantinedSql("e.metadata")}
3563
3926
  GROUP BY mh.event_id
3564
3927
  ORDER BY avg_score DESC
3565
3928
  LIMIT ?`,
@@ -3624,6 +3987,7 @@ var SQLiteEventStore = class {
3624
3987
  FROM events_fts fts
3625
3988
  JOIN events e ON e.id = fts.event_id
3626
3989
  WHERE events_fts MATCH ?
3990
+ AND ${notActiveQuarantinedSql("e.metadata")}
3627
3991
  ORDER BY fts.rank
3628
3992
  LIMIT ?`,
3629
3993
  [searchTerms, limit]
@@ -3638,6 +4002,7 @@ var SQLiteEventStore = class {
3638
4002
  this.db,
3639
4003
  `SELECT *, 0 as rank FROM events
3640
4004
  WHERE content LIKE ?
4005
+ AND ${notActiveQuarantinedSql()}
3641
4006
  ORDER BY timestamp DESC
3642
4007
  LIMIT ?`,
3643
4008
  [likePattern, limit]
@@ -3844,6 +4209,7 @@ var SQLiteEventStore = class {
3844
4209
  `SELECT turn_id, MIN(timestamp) as min_ts
3845
4210
  FROM events
3846
4211
  WHERE session_id = ? AND turn_id IS NOT NULL
4212
+ AND ${maybeQuarantinePredicate(options)}
3847
4213
  GROUP BY turn_id
3848
4214
  ORDER BY min_ts DESC
3849
4215
  LIMIT ? OFFSET ?`,
@@ -3851,7 +4217,7 @@ var SQLiteEventStore = class {
3851
4217
  );
3852
4218
  const turns = [];
3853
4219
  for (const turnRow of turnRows) {
3854
- const events = await this.getEventsByTurn(turnRow.turn_id);
4220
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3855
4221
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3856
4222
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3857
4223
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3870,11 +4236,11 @@ var SQLiteEventStore = class {
3870
4236
  /**
3871
4237
  * Get all events for a specific turn_id
3872
4238
  */
3873
- async getEventsByTurn(turnId) {
4239
+ async getEventsByTurn(turnId, options) {
3874
4240
  await this.initialize();
3875
4241
  const rows = sqliteAll(
3876
4242
  this.db,
3877
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4243
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3878
4244
  [turnId]
3879
4245
  );
3880
4246
  return rows.map(this.rowToEvent);
@@ -3882,13 +4248,14 @@ var SQLiteEventStore = class {
3882
4248
  /**
3883
4249
  * Count total turns for a session
3884
4250
  */
3885
- async countSessionTurns(sessionId) {
4251
+ async countSessionTurns(sessionId, options) {
3886
4252
  await this.initialize();
3887
4253
  const row = sqliteGet(
3888
4254
  this.db,
3889
4255
  `SELECT COUNT(DISTINCT turn_id) as count
3890
4256
  FROM events
3891
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4257
+ WHERE session_id = ? AND turn_id IS NOT NULL
4258
+ AND ${maybeQuarantinePredicate(options)}`,
3892
4259
  [sessionId]
3893
4260
  );
3894
4261
  return row?.count || 0;
@@ -3980,7 +4347,7 @@ var SQLiteEventStore = class {
3980
4347
  content: row.content,
3981
4348
  canonicalKey: row.canonical_key,
3982
4349
  dedupeKey: row.dedupe_key,
3983
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4350
+ metadata: safeParseMetadataValue(row.metadata)
3984
4351
  };
3985
4352
  if (row.access_count !== void 0) {
3986
4353
  event.access_count = row.access_count;
@@ -4132,6 +4499,7 @@ var VectorStore = class {
4132
4499
  * Get total count of vectors
4133
4500
  */
4134
4501
  async count() {
4502
+ await this.initialize();
4135
4503
  if (!this.table)
4136
4504
  return 0;
4137
4505
  const result = await this.table.countRows();
@@ -4570,6 +4938,14 @@ var MemoryQueryService = class {
4570
4938
  await this.initialize();
4571
4939
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4572
4940
  }
4941
+ async recoverStuckOutboxItems(options) {
4942
+ await this.initialize();
4943
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4944
+ }
4945
+ async repairLegacyProjectScope(options) {
4946
+ await this.initialize();
4947
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4948
+ }
4573
4949
  async getStats() {
4574
4950
  await this.initialize();
4575
4951
  const deps = this.getStatsDeps();
@@ -6192,18 +6568,18 @@ function assertDefaultRetrieverStore(eventStore) {
6192
6568
  function createMemoryEngineServices(options) {
6193
6569
  const factories = options.factories ?? {};
6194
6570
  const storagePath = options.storagePath;
6195
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6196
- fs5.mkdirSync(storagePath, { recursive: true });
6571
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6572
+ fs6.mkdirSync(storagePath, { recursive: true });
6197
6573
  }
6198
6574
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6199
- path4.join(storagePath, "events.sqlite"),
6575
+ path5.join(storagePath, "events.sqlite"),
6200
6576
  {
6201
6577
  readonly: options.readOnly,
6202
6578
  markdownMirrorRoot: storagePath
6203
6579
  }
6204
6580
  );
6205
6581
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6206
- path4.join(storagePath, "vectors")
6582
+ path5.join(storagePath, "vectors")
6207
6583
  );
6208
6584
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6209
6585
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6614,8 +6990,8 @@ function createMemoryRuntimeService(deps) {
6614
6990
  }
6615
6991
 
6616
6992
  // src/extensions/shared-memory/shared-memory-services.ts
6617
- import * as fs6 from "fs";
6618
- import * as path5 from "path";
6993
+ import * as fs7 from "fs";
6994
+ import * as path6 from "path";
6619
6995
 
6620
6996
  // src/core/shared-event-store.ts
6621
6997
  var SharedEventStore = class {
@@ -7303,7 +7679,7 @@ var SharedMemoryServices = class {
7303
7679
  this.ensureDirectory(sharedPath, { allowCreate: true });
7304
7680
  const store = await this.openStore(sharedPath);
7305
7681
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7306
- path5.join(sharedPath, "vectors")
7682
+ path6.join(sharedPath, "vectors")
7307
7683
  );
7308
7684
  await this.sharedVectorStore.initialize();
7309
7685
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7376,7 +7752,7 @@ var SharedMemoryServices = class {
7376
7752
  async createOpenStorePromise(sharedPath) {
7377
7753
  if (!this.sharedEventStore) {
7378
7754
  const sharedEventStore = this.factories.createSharedEventStore(
7379
- path5.join(sharedPath, "shared.duckdb")
7755
+ path6.join(sharedPath, "shared.duckdb")
7380
7756
  );
7381
7757
  await sharedEventStore.initialize();
7382
7758
  this.sharedEventStore = sharedEventStore;
@@ -7396,9 +7772,9 @@ var SharedMemoryServices = class {
7396
7772
  }
7397
7773
  get factories() {
7398
7774
  return {
7399
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7775
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7400
7776
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7401
- fs6.mkdirSync(targetPath, { recursive: true });
7777
+ fs7.mkdirSync(targetPath, { recursive: true });
7402
7778
  }),
7403
7779
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7404
7780
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7513,33 +7889,11 @@ function createMemoryServiceComposition(options) {
7513
7889
  }
7514
7890
  function defaultExpandPath(targetPath) {
7515
7891
  if (targetPath.startsWith("~")) {
7516
- return path6.join(os.homedir(), targetPath.slice(1));
7892
+ return path7.join(os2.homedir(), targetPath.slice(1));
7517
7893
  }
7518
7894
  return targetPath;
7519
7895
  }
7520
7896
 
7521
- // src/core/registry/project-path.ts
7522
- import * as crypto2 from "crypto";
7523
- import * as fs7 from "fs";
7524
- import * as os2 from "os";
7525
- import * as path7 from "path";
7526
- function normalizeProjectPath(projectPath) {
7527
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7528
- try {
7529
- return fs7.realpathSync(expanded);
7530
- } catch {
7531
- return path7.resolve(expanded);
7532
- }
7533
- }
7534
- function hashProjectPath(projectPath) {
7535
- const normalizedPath = normalizeProjectPath(projectPath);
7536
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7537
- }
7538
- function getProjectStoragePath(projectPath) {
7539
- const hash = hashProjectPath(projectPath);
7540
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7541
- }
7542
-
7543
7897
  // src/core/registry/session-registry.ts
7544
7898
  import * as fs8 from "fs";
7545
7899
  import * as os3 from "os";
@@ -7838,6 +8192,12 @@ var MemoryService = class {
7838
8192
  async getOutboxStats() {
7839
8193
  return this.queryService.getOutboxStats();
7840
8194
  }
8195
+ async recoverStuckOutboxItems(options) {
8196
+ return this.queryService.recoverStuckOutboxItems(options);
8197
+ }
8198
+ async repairLegacyProjectScope(options) {
8199
+ return this.queryService.repairLegacyProjectScope(options);
8200
+ }
7841
8201
  async getRetrievalTraceStats() {
7842
8202
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7843
8203
  }
@@ -8223,10 +8583,10 @@ var SENSITIVE_PATTERNS = [
8223
8583
  // Redact the whole URI so usernames, credentials, hosts, paths, and query
8224
8584
  // params do not leak either.
8225
8585
  /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>/@]+@[^\s'"`<>]+/gi,
8226
- /password\s*[:=]\s*['"]?[^\s'"]+/gi,
8227
- /api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/gi,
8228
- /secret\s*[:=]\s*['"]?[^\s'"]+/gi,
8229
- /token\s*[:=]\s*['"]?[^\s'"]+/gi,
8586
+ /(?:[\w.-]+[-_])?password\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8587
+ /(?:[\w.-]+[-_])?api[-_]?key\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8588
+ /(?:[\w.-]+[-_])?secret\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8589
+ /(?:[\w.-]+[-_])?token\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
8230
8590
  /bearer\s+[a-zA-Z0-9\-_.]+/gi,
8231
8591
  /AWS[_-]?ACCESS[_-]?KEY[_-]?ID\s*[:=]\s*['"]?[A-Z0-9]+/gi,
8232
8592
  /AWS[_-]?SECRET[_-]?ACCESS[_-]?KEY\s*[:=]\s*['"]?[^\s'"]+/gi,
@@ -8236,8 +8596,37 @@ var SENSITIVE_PATTERNS = [
8236
8596
  /sk-[a-zA-Z0-9]{48}/g
8237
8597
  // OpenAI API Key
8238
8598
  ];
8239
- function maskSensitiveString(value) {
8599
+ var CLI_SECRET_OPTION_PATTERNS = [
8600
+ /(--(?:[a-z0-9]+[-_])*(?:password|secret|api[-_]?key|token|bearer)(?:[-_][a-z0-9]+)*(?:\s+|=))(?:"[^"]*"|'[^']*'|[^\s'"`<>]+)/gi
8601
+ ];
8602
+ var URL_FOLLOWING_SECRET_PATTERN = /((?:https?:\/\/[^\s'"`<>]+)\s*\r?\n\s*)([A-Za-z0-9!@#$%^&*._+\-~:=/]{6,})(?=\s*(?:\r?\n|$))/gi;
8603
+ function looksLikePastedSecret(value) {
8604
+ return value.length >= 8 && /(?:\d|[^A-Za-z0-9])/.test(value);
8605
+ }
8606
+ function maskUrlFollowingSecret(value) {
8607
+ let count = 0;
8608
+ const content = value.replace(URL_FOLLOWING_SECRET_PATTERN, (_match, prefix, secret) => {
8609
+ if (!looksLikePastedSecret(secret))
8610
+ return `${prefix}${secret}`;
8611
+ count++;
8612
+ return `${prefix}[REDACTED]`;
8613
+ });
8614
+ return { content, count };
8615
+ }
8616
+ function maskCliSecretOptions(value) {
8617
+ let count = 0;
8240
8618
  let filtered = value;
8619
+ for (const pattern of CLI_SECRET_OPTION_PATTERNS) {
8620
+ pattern.lastIndex = 0;
8621
+ filtered = filtered.replace(pattern, (_match, prefix) => {
8622
+ count++;
8623
+ return `${prefix}[REDACTED]`;
8624
+ });
8625
+ }
8626
+ return { content: filtered, count };
8627
+ }
8628
+ function maskSensitiveString(value) {
8629
+ let filtered = maskCliSecretOptions(value).content;
8241
8630
  for (const pattern of SENSITIVE_PATTERNS) {
8242
8631
  pattern.lastIndex = 0;
8243
8632
  filtered = filtered.replace(pattern, "[REDACTED]");
@@ -8257,6 +8646,12 @@ function applyPrivacyFilter(content, config) {
8257
8646
  filtered = tagResult.filtered;
8258
8647
  privateTagCount = tagResult.stats.count;
8259
8648
  }
8649
+ const cliResult = maskCliSecretOptions(filtered);
8650
+ filtered = cliResult.content;
8651
+ patternMatchCount += cliResult.count;
8652
+ const urlSecretResult = maskUrlFollowingSecret(filtered);
8653
+ filtered = urlSecretResult.content;
8654
+ patternMatchCount += urlSecretResult.count;
8260
8655
  for (const pattern of SENSITIVE_PATTERNS) {
8261
8656
  pattern.lastIndex = 0;
8262
8657
  const matches = filtered.match(pattern);
@@ -8268,13 +8663,13 @@ function applyPrivacyFilter(content, config) {
8268
8663
  for (const patternStr of config.excludePatterns || []) {
8269
8664
  try {
8270
8665
  const regex = new RegExp(
8271
- `(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
8666
+ `(^|[^\\w-])(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
8272
8667
  "gi"
8273
8668
  );
8274
8669
  const matches = filtered.match(regex);
8275
8670
  if (matches) {
8276
8671
  patternMatchCount += matches.length;
8277
- filtered = filtered.replace(regex, "[REDACTED]");
8672
+ filtered = filtered.replace(regex, "$1[REDACTED]");
8278
8673
  }
8279
8674
  } catch {
8280
8675
  }