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.
package/dist/mcp/index.js CHANGED
@@ -10436,8 +10436,8 @@ import * as path14 from "node:path";
10436
10436
  import * as os5 from "os";
10437
10437
 
10438
10438
  // src/core/engine/memory-service-composition.ts
10439
- import * as os from "os";
10440
- import * as path6 from "path";
10439
+ import * as os2 from "os";
10440
+ import * as path7 from "path";
10441
10441
 
10442
10442
  // src/core/metadata-extractor.ts
10443
10443
  function createToolObservationEmbedding(toolName, metadata, success) {
@@ -11948,8 +11948,8 @@ function createEndlessMemoryServices(options) {
11948
11948
  }
11949
11949
 
11950
11950
  // src/core/engine/memory-engine-services.ts
11951
- import * as fs5 from "fs";
11952
- import * as path4 from "path";
11951
+ import * as fs6 from "fs";
11952
+ import * as path5 from "path";
11953
11953
 
11954
11954
  // src/extensions/vector/embedder.ts
11955
11955
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -12627,14 +12627,39 @@ function makeDedupeKey(content, sessionId) {
12627
12627
  return `${sessionId}:${contentHash}`;
12628
12628
  }
12629
12629
 
12630
+ // src/core/sqlite-event-store.ts
12631
+ import * as nodePath2 from "path";
12632
+
12633
+ // src/core/registry/project-path.ts
12634
+ import * as crypto2 from "crypto";
12635
+ import * as fs3 from "fs";
12636
+ import * as os from "os";
12637
+ import * as path3 from "path";
12638
+ function normalizeProjectPath(projectPath) {
12639
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
12640
+ try {
12641
+ return fs3.realpathSync(expanded);
12642
+ } catch {
12643
+ return path3.resolve(expanded);
12644
+ }
12645
+ }
12646
+ function hashProjectPath(projectPath) {
12647
+ const normalizedPath = normalizeProjectPath(projectPath);
12648
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
12649
+ }
12650
+ function getProjectStoragePath(projectPath) {
12651
+ const hash = hashProjectPath(projectPath);
12652
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
12653
+ }
12654
+
12630
12655
  // src/core/sqlite-wrapper.ts
12631
12656
  import Database from "better-sqlite3";
12632
- import * as fs3 from "fs";
12657
+ import * as fs4 from "fs";
12633
12658
  import * as nodePath from "path";
12634
12659
  function createSQLiteDatabase(path15, options) {
12635
12660
  const dir = nodePath.dirname(path15);
12636
- if (!fs3.existsSync(dir)) {
12637
- fs3.mkdirSync(dir, { recursive: true });
12661
+ if (!fs4.existsSync(dir)) {
12662
+ fs4.mkdirSync(dir, { recursive: true });
12638
12663
  }
12639
12664
  const db = new Database(path15, {
12640
12665
  readonly: options?.readonly ?? false
@@ -12683,8 +12708,8 @@ function toSQLiteTimestamp(date) {
12683
12708
  }
12684
12709
 
12685
12710
  // src/core/markdown-mirror.ts
12686
- import * as fs4 from "fs/promises";
12687
- import * as path3 from "path";
12711
+ import * as fs5 from "fs/promises";
12712
+ import * as path4 from "path";
12688
12713
  var DEFAULT_NAMESPACE = "default";
12689
12714
  var DEFAULT_CATEGORY = "uncategorized";
12690
12715
  function sanitizeSegment2(input, fallback) {
@@ -12713,7 +12738,7 @@ function buildMirrorPath2(rootDir, event) {
12713
12738
  const yyyy = d.getFullYear();
12714
12739
  const mm = String(d.getMonth() + 1).padStart(2, "0");
12715
12740
  const dd = String(d.getDate()).padStart(2, "0");
12716
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
12741
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
12717
12742
  }
12718
12743
  function formatMirrorEntry(event) {
12719
12744
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -12734,8 +12759,8 @@ var MarkdownMirror2 = class {
12734
12759
  }
12735
12760
  async append(event) {
12736
12761
  const outPath = buildMirrorPath2(this.rootDir, event);
12737
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
12738
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
12762
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
12763
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
12739
12764
  return outPath;
12740
12765
  }
12741
12766
  };
@@ -12748,6 +12773,143 @@ function normalizeQueryRewriteKind(value) {
12748
12773
  return "none";
12749
12774
  }
12750
12775
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
12776
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
12777
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
12778
+ function emptyOutboxRecoveryResult() {
12779
+ return {
12780
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
12781
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
12782
+ };
12783
+ }
12784
+ function isRecord(value) {
12785
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12786
+ }
12787
+ function getNestedRecord(root, path15) {
12788
+ let cursor = root;
12789
+ for (const key of path15) {
12790
+ if (!isRecord(cursor))
12791
+ return void 0;
12792
+ cursor = cursor[key];
12793
+ }
12794
+ return isRecord(cursor) ? cursor : void 0;
12795
+ }
12796
+ function getNestedString(root, path15) {
12797
+ let cursor = root;
12798
+ for (const key of path15) {
12799
+ if (!isRecord(cursor))
12800
+ return void 0;
12801
+ cursor = cursor[key];
12802
+ }
12803
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
12804
+ }
12805
+ function metadataProjectHash(metadata) {
12806
+ return getNestedString(metadata, ["scope", "project", "hash"]);
12807
+ }
12808
+ function metadataProjectPaths(metadata) {
12809
+ const candidates = [
12810
+ getNestedString(metadata, ["projectPath"]),
12811
+ getNestedString(metadata, ["sourceProjectPath"]),
12812
+ getNestedString(metadata, ["scope", "project", "path"])
12813
+ ];
12814
+ const paths = [];
12815
+ for (const value of candidates) {
12816
+ if (value && !paths.includes(value))
12817
+ paths.push(value);
12818
+ }
12819
+ return paths;
12820
+ }
12821
+ function metadataProjectPath(metadata) {
12822
+ return metadataProjectPaths(metadata)[0];
12823
+ }
12824
+ function isActiveQuarantinedMetadata(metadata) {
12825
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
12826
+ return quarantine?.status === "active";
12827
+ }
12828
+ function activeQuarantineStatusExpression(column = "metadata") {
12829
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
12830
+ }
12831
+ function notActiveQuarantinedSql(column = "metadata") {
12832
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
12833
+ }
12834
+ function maybeQuarantinePredicate(options, column = "metadata") {
12835
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
12836
+ }
12837
+ function safeParseMetadataValue(value) {
12838
+ if (!value)
12839
+ return void 0;
12840
+ if (typeof value === "object")
12841
+ return isRecord(value) ? value : void 0;
12842
+ if (typeof value !== "string")
12843
+ return void 0;
12844
+ try {
12845
+ const parsed = JSON.parse(value);
12846
+ return isRecord(parsed) ? parsed : void 0;
12847
+ } catch {
12848
+ return void 0;
12849
+ }
12850
+ }
12851
+ function isImportedOrLegacyScopedMetadata(metadata) {
12852
+ if (!metadata)
12853
+ return false;
12854
+ return Boolean(
12855
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
12856
+ );
12857
+ }
12858
+ function addMetadataTag(metadata, tag) {
12859
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
12860
+ if (!current.includes(tag))
12861
+ metadata.tags = [...current, tag];
12862
+ }
12863
+ function buildRepairResult(projectHash, dryRun) {
12864
+ return {
12865
+ dryRun,
12866
+ projectHash,
12867
+ scanned: 0,
12868
+ repaired: 0,
12869
+ quarantined: 0,
12870
+ alreadyScoped: 0,
12871
+ skipped: 0,
12872
+ samples: []
12873
+ };
12874
+ }
12875
+ function normalizeRepoName(value) {
12876
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
12877
+ }
12878
+ function projectBasename(projectPath) {
12879
+ if (!projectPath)
12880
+ return void 0;
12881
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
12882
+ const basename4 = nodePath2.basename(trimmed);
12883
+ return basename4 ? normalizeRepoName(basename4) : void 0;
12884
+ }
12885
+ function isProjectScopeRepairExplanation(content) {
12886
+ const normalized = content.toLowerCase();
12887
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
12888
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
12889
+ return hasRepairContext && hasExplanationContext;
12890
+ }
12891
+ function hasConflictingContentProjectHint(content, projectPath) {
12892
+ const currentName = projectBasename(projectPath);
12893
+ if (!currentName)
12894
+ return false;
12895
+ if (isProjectScopeRepairExplanation(content))
12896
+ return false;
12897
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
12898
+ let githubMatch;
12899
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
12900
+ const repo = normalizeRepoName(githubMatch[2] || "");
12901
+ if (repo && repo !== currentName)
12902
+ return true;
12903
+ }
12904
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
12905
+ let workspaceMatch;
12906
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
12907
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
12908
+ if (repo && repo !== currentName)
12909
+ return true;
12910
+ }
12911
+ return false;
12912
+ }
12751
12913
  var SQLiteEventStore = class {
12752
12914
  db;
12753
12915
  initialized = false;
@@ -13246,11 +13408,11 @@ var SQLiteEventStore = class {
13246
13408
  /**
13247
13409
  * Get events by session ID
13248
13410
  */
13249
- async getSessionEvents(sessionId) {
13411
+ async getSessionEvents(sessionId, options) {
13250
13412
  await this.initialize();
13251
13413
  const rows = sqliteAll(
13252
13414
  this.db,
13253
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
13415
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
13254
13416
  [sessionId]
13255
13417
  );
13256
13418
  return rows.map(this.rowToEvent);
@@ -13258,11 +13420,11 @@ var SQLiteEventStore = class {
13258
13420
  /**
13259
13421
  * Get recent events
13260
13422
  */
13261
- async getRecentEvents(limit = 100) {
13423
+ async getRecentEvents(limit = 100, options) {
13262
13424
  await this.initialize();
13263
13425
  const rows = sqliteAll(
13264
13426
  this.db,
13265
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
13427
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
13266
13428
  [limit]
13267
13429
  );
13268
13430
  return rows.map(this.rowToEvent);
@@ -13270,11 +13432,11 @@ var SQLiteEventStore = class {
13270
13432
  /**
13271
13433
  * Get event by ID
13272
13434
  */
13273
- async getEvent(id) {
13435
+ async getEvent(id, options) {
13274
13436
  await this.initialize();
13275
13437
  const row = sqliteGet(
13276
13438
  this.db,
13277
- `SELECT * FROM events WHERE id = ?`,
13439
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
13278
13440
  [id]
13279
13441
  );
13280
13442
  if (!row)
@@ -13284,11 +13446,11 @@ var SQLiteEventStore = class {
13284
13446
  /**
13285
13447
  * Get events since a timestamp (for sync)
13286
13448
  */
13287
- async getEventsSince(timestamp, limit = 1e3) {
13449
+ async getEventsSince(timestamp, limit = 1e3, options) {
13288
13450
  await this.initialize();
13289
13451
  const rows = sqliteAll(
13290
13452
  this.db,
13291
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
13453
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
13292
13454
  [timestamp, limit]
13293
13455
  );
13294
13456
  return rows.map(this.rowToEvent);
@@ -13297,11 +13459,11 @@ var SQLiteEventStore = class {
13297
13459
  * Get events since a SQLite rowid (for robust incremental replication).
13298
13460
  * Rowid is monotonic for append-only tables, independent of client timestamps.
13299
13461
  */
13300
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
13462
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
13301
13463
  await this.initialize();
13302
13464
  const rows = sqliteAll(
13303
13465
  this.db,
13304
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
13466
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
13305
13467
  [lastRowid, limit]
13306
13468
  );
13307
13469
  return rows.map((row) => ({
@@ -13498,7 +13660,9 @@ var SQLiteEventStore = class {
13498
13660
  const placeholders = ids.map(() => "?").join(",");
13499
13661
  sqliteRun(
13500
13662
  this.db,
13501
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
13663
+ `UPDATE embedding_outbox
13664
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
13665
+ WHERE id IN (${placeholders})`,
13502
13666
  ids
13503
13667
  );
13504
13668
  return pending.map((row) => ({
@@ -13534,19 +13698,19 @@ var SQLiteEventStore = class {
13534
13698
  /**
13535
13699
  * Count total events
13536
13700
  */
13537
- async countEvents() {
13701
+ async countEvents(options) {
13538
13702
  await this.initialize();
13539
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
13703
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
13540
13704
  return row?.count || 0;
13541
13705
  }
13542
13706
  /**
13543
13707
  * Get events page in timestamp ascending order (stable migration/reindex scans)
13544
13708
  */
13545
- async getEventsPage(limit = 1e3, offset = 0) {
13709
+ async getEventsPage(limit = 1e3, offset = 0, options) {
13546
13710
  await this.initialize();
13547
13711
  const rows = sqliteAll(
13548
13712
  this.db,
13549
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
13713
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
13550
13714
  [limit, offset]
13551
13715
  );
13552
13716
  return rows.map(this.rowToEvent);
@@ -13568,6 +13732,197 @@ var SQLiteEventStore = class {
13568
13732
  [error, ...ids]
13569
13733
  );
13570
13734
  }
13735
+ /**
13736
+ * Recover abandoned outbox work after a worker/process crash.
13737
+ *
13738
+ * Rows in `processing` are claimed work. If the process exits before marking
13739
+ * them done/failed, they otherwise remain invisible to future processing.
13740
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
13741
+ */
13742
+ async recoverStuckOutboxItems(options = {}) {
13743
+ await this.initialize();
13744
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
13745
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
13746
+ const now = options.now ?? /* @__PURE__ */ new Date();
13747
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
13748
+ const result = emptyOutboxRecoveryResult();
13749
+ const embeddingRecovered = sqliteRun(
13750
+ this.db,
13751
+ `UPDATE embedding_outbox
13752
+ SET status = 'pending', processed_at = NULL, error_message = NULL
13753
+ WHERE status = 'processing'
13754
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
13755
+ [threshold]
13756
+ );
13757
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
13758
+ const embeddingRetried = sqliteRun(
13759
+ this.db,
13760
+ `UPDATE embedding_outbox
13761
+ SET status = 'pending', error_message = NULL
13762
+ WHERE status = 'failed'
13763
+ AND retry_count < ?`,
13764
+ [maxRetries]
13765
+ );
13766
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
13767
+ const vectorRecovered = sqliteRun(
13768
+ this.db,
13769
+ `UPDATE vector_outbox
13770
+ SET status = 'pending', updated_at = ?, error = NULL
13771
+ WHERE status = 'processing'
13772
+ AND datetime(updated_at) < datetime(?)`,
13773
+ [now.toISOString(), threshold]
13774
+ );
13775
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
13776
+ const vectorRetried = sqliteRun(
13777
+ this.db,
13778
+ `UPDATE vector_outbox
13779
+ SET status = 'pending', updated_at = ?, error = NULL
13780
+ WHERE status = 'failed'
13781
+ AND retry_count < ?`,
13782
+ [now.toISOString(), maxRetries]
13783
+ );
13784
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
13785
+ return result;
13786
+ }
13787
+ /**
13788
+ * Repair legacy imported events that predate canonical project scope metadata.
13789
+ *
13790
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
13791
+ * imported but cannot be proven to belong to this project are quarantined so
13792
+ * dashboard default reads/search do not surface cross-project contamination.
13793
+ */
13794
+ async repairLegacyProjectScope(options = {}) {
13795
+ await this.initialize();
13796
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
13797
+ if (!projectHash) {
13798
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
13799
+ }
13800
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
13801
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
13802
+ }
13803
+ const dryRun = options.dryRun === true;
13804
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
13805
+ const result = buildRepairResult(projectHash, dryRun);
13806
+ const rows = sqliteAll(
13807
+ this.db,
13808
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
13809
+ FROM events e
13810
+ LEFT JOIN sessions s ON s.id = e.session_id
13811
+ ORDER BY e.timestamp ASC`,
13812
+ []
13813
+ );
13814
+ const sample = (entry) => {
13815
+ if (result.samples.length < 20)
13816
+ result.samples.push(entry);
13817
+ };
13818
+ for (const row of rows) {
13819
+ result.scanned++;
13820
+ let metadata = {};
13821
+ let metadataParseInvalid = false;
13822
+ if (row.metadata) {
13823
+ const parsed = safeParseMetadataValue(row.metadata);
13824
+ if (parsed) {
13825
+ metadata = parsed;
13826
+ } else {
13827
+ metadataParseInvalid = true;
13828
+ }
13829
+ }
13830
+ if (isActiveQuarantinedMetadata(metadata)) {
13831
+ result.skipped++;
13832
+ continue;
13833
+ }
13834
+ const currentHash = metadataProjectHash(metadata);
13835
+ const explicitPath = metadataProjectPath(metadata);
13836
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
13837
+ const candidatePaths = metadataProjectPaths(metadata);
13838
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
13839
+ candidatePaths.push(sessionProjectPath);
13840
+ }
13841
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
13842
+ const pathHashes = candidatePaths.map((candidate) => {
13843
+ try {
13844
+ return { path: candidate, hash: hashProjectPath(candidate) };
13845
+ } catch {
13846
+ return { path: candidate, hash: void 0 };
13847
+ }
13848
+ });
13849
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
13850
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
13851
+ let action = "skipped";
13852
+ let reason;
13853
+ let observedProjectHash;
13854
+ if (foreignPath) {
13855
+ action = "quarantined";
13856
+ reason = "project-path-mismatch";
13857
+ observedProjectHash = foreignPath.hash;
13858
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
13859
+ action = "quarantined";
13860
+ reason = "content-project-mismatch";
13861
+ } else if (currentHash === projectHash) {
13862
+ result.alreadyScoped++;
13863
+ continue;
13864
+ } else if (currentHash && currentHash !== projectHash) {
13865
+ action = "quarantined";
13866
+ reason = "scope-hash-mismatch";
13867
+ observedProjectHash = currentHash;
13868
+ } else if (matchingPath) {
13869
+ action = "repaired";
13870
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
13871
+ } else if (candidatePaths.length > 0) {
13872
+ action = "quarantined";
13873
+ reason = "project-path-mismatch";
13874
+ } else if (importedOrLegacy) {
13875
+ action = "quarantined";
13876
+ reason = "missing-project-scope";
13877
+ }
13878
+ if (action === "skipped" || !reason) {
13879
+ result.skipped++;
13880
+ continue;
13881
+ }
13882
+ if (action === "repaired") {
13883
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
13884
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
13885
+ project.hash = projectHash;
13886
+ scope.project = project;
13887
+ metadata.scope = scope;
13888
+ metadata.repair = {
13889
+ ...isRecord(metadata.repair) ? metadata.repair : {},
13890
+ legacyProjectScope: {
13891
+ action,
13892
+ reason,
13893
+ repairedAt: nowIso
13894
+ }
13895
+ };
13896
+ addMetadataTag(metadata, `proj:${projectHash}`);
13897
+ result.repaired++;
13898
+ } else {
13899
+ metadata.quarantine = {
13900
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
13901
+ status: "active",
13902
+ category: "project-scope",
13903
+ reason,
13904
+ detectedAt: nowIso,
13905
+ expectedProjectHash: projectHash,
13906
+ ...observedProjectHash ? { observedProjectHash } : {}
13907
+ };
13908
+ metadata.repair = {
13909
+ ...isRecord(metadata.repair) ? metadata.repair : {},
13910
+ legacyProjectScope: {
13911
+ action,
13912
+ reason,
13913
+ repairedAt: nowIso
13914
+ }
13915
+ };
13916
+ addMetadataTag(metadata, "quarantine:project-scope");
13917
+ result.quarantined++;
13918
+ }
13919
+ sample({ eventId: row.id, action, reason });
13920
+ if (!dryRun) {
13921
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
13922
+ }
13923
+ }
13924
+ return result;
13925
+ }
13571
13926
  /**
13572
13927
  * Get embedding/vector outbox health statistics
13573
13928
  */
@@ -13615,7 +13970,11 @@ var SQLiteEventStore = class {
13615
13970
  await this.initialize();
13616
13971
  const rows = sqliteAll(
13617
13972
  this.db,
13618
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
13973
+ `SELECT ml.level, COUNT(*) as count
13974
+ FROM memory_levels ml
13975
+ INNER JOIN events e ON e.id = ml.event_id
13976
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
13977
+ GROUP BY ml.level`
13619
13978
  );
13620
13979
  return rows;
13621
13980
  }
@@ -13631,6 +13990,7 @@ var SQLiteEventStore = class {
13631
13990
  `SELECT e.* FROM events e
13632
13991
  INNER JOIN memory_levels ml ON e.id = ml.event_id
13633
13992
  WHERE ml.level = ?
13993
+ AND ${notActiveQuarantinedSql("e.metadata")}
13634
13994
  ORDER BY e.timestamp DESC
13635
13995
  LIMIT ? OFFSET ?`,
13636
13996
  [level, limit, offset]
@@ -13723,12 +14083,13 @@ var SQLiteEventStore = class {
13723
14083
  /**
13724
14084
  * Get most accessed memories (falls back to recent events if none accessed)
13725
14085
  */
13726
- async getMostAccessed(limit = 10) {
14086
+ async getMostAccessed(limit = 10, options) {
13727
14087
  await this.initialize();
13728
14088
  let rows = sqliteAll(
13729
14089
  this.db,
13730
14090
  `SELECT * FROM events
13731
14091
  WHERE access_count > 0
14092
+ AND ${maybeQuarantinePredicate(options)}
13732
14093
  ORDER BY access_count DESC, last_accessed_at DESC
13733
14094
  LIMIT ?`,
13734
14095
  [limit]
@@ -13737,6 +14098,7 @@ var SQLiteEventStore = class {
13737
14098
  rows = sqliteAll(
13738
14099
  this.db,
13739
14100
  `SELECT * FROM events
14101
+ WHERE ${maybeQuarantinePredicate(options)}
13740
14102
  ORDER BY timestamp DESC
13741
14103
  LIMIT ?`,
13742
14104
  [limit]
@@ -13867,6 +14229,7 @@ var SQLiteEventStore = class {
13867
14229
  FROM memory_helpfulness mh
13868
14230
  JOIN events e ON e.id = mh.event_id
13869
14231
  WHERE mh.measured_at IS NOT NULL
14232
+ AND ${notActiveQuarantinedSql("e.metadata")}
13870
14233
  GROUP BY mh.event_id
13871
14234
  ORDER BY avg_score DESC
13872
14235
  LIMIT ?`,
@@ -13931,6 +14294,7 @@ var SQLiteEventStore = class {
13931
14294
  FROM events_fts fts
13932
14295
  JOIN events e ON e.id = fts.event_id
13933
14296
  WHERE events_fts MATCH ?
14297
+ AND ${notActiveQuarantinedSql("e.metadata")}
13934
14298
  ORDER BY fts.rank
13935
14299
  LIMIT ?`,
13936
14300
  [searchTerms, limit]
@@ -13945,6 +14309,7 @@ var SQLiteEventStore = class {
13945
14309
  this.db,
13946
14310
  `SELECT *, 0 as rank FROM events
13947
14311
  WHERE content LIKE ?
14312
+ AND ${notActiveQuarantinedSql()}
13948
14313
  ORDER BY timestamp DESC
13949
14314
  LIMIT ?`,
13950
14315
  [likePattern, limit]
@@ -14151,6 +14516,7 @@ var SQLiteEventStore = class {
14151
14516
  `SELECT turn_id, MIN(timestamp) as min_ts
14152
14517
  FROM events
14153
14518
  WHERE session_id = ? AND turn_id IS NOT NULL
14519
+ AND ${maybeQuarantinePredicate(options)}
14154
14520
  GROUP BY turn_id
14155
14521
  ORDER BY min_ts DESC
14156
14522
  LIMIT ? OFFSET ?`,
@@ -14158,7 +14524,7 @@ var SQLiteEventStore = class {
14158
14524
  );
14159
14525
  const turns = [];
14160
14526
  for (const turnRow of turnRows) {
14161
- const events = await this.getEventsByTurn(turnRow.turn_id);
14527
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
14162
14528
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
14163
14529
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
14164
14530
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -14177,11 +14543,11 @@ var SQLiteEventStore = class {
14177
14543
  /**
14178
14544
  * Get all events for a specific turn_id
14179
14545
  */
14180
- async getEventsByTurn(turnId) {
14546
+ async getEventsByTurn(turnId, options) {
14181
14547
  await this.initialize();
14182
14548
  const rows = sqliteAll(
14183
14549
  this.db,
14184
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
14550
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
14185
14551
  [turnId]
14186
14552
  );
14187
14553
  return rows.map(this.rowToEvent);
@@ -14189,13 +14555,14 @@ var SQLiteEventStore = class {
14189
14555
  /**
14190
14556
  * Count total turns for a session
14191
14557
  */
14192
- async countSessionTurns(sessionId) {
14558
+ async countSessionTurns(sessionId, options) {
14193
14559
  await this.initialize();
14194
14560
  const row = sqliteGet(
14195
14561
  this.db,
14196
14562
  `SELECT COUNT(DISTINCT turn_id) as count
14197
14563
  FROM events
14198
- WHERE session_id = ? AND turn_id IS NOT NULL`,
14564
+ WHERE session_id = ? AND turn_id IS NOT NULL
14565
+ AND ${maybeQuarantinePredicate(options)}`,
14199
14566
  [sessionId]
14200
14567
  );
14201
14568
  return row?.count || 0;
@@ -14287,7 +14654,7 @@ var SQLiteEventStore = class {
14287
14654
  content: row.content,
14288
14655
  canonicalKey: row.canonical_key,
14289
14656
  dedupeKey: row.dedupe_key,
14290
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
14657
+ metadata: safeParseMetadataValue(row.metadata)
14291
14658
  };
14292
14659
  if (row.access_count !== void 0) {
14293
14660
  event.access_count = row.access_count;
@@ -14439,6 +14806,7 @@ var VectorStore = class {
14439
14806
  * Get total count of vectors
14440
14807
  */
14441
14808
  async count() {
14809
+ await this.initialize();
14442
14810
  if (!this.table)
14443
14811
  return 0;
14444
14812
  const result = await this.table.countRows();
@@ -14877,6 +15245,14 @@ var MemoryQueryService = class {
14877
15245
  await this.initialize();
14878
15246
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
14879
15247
  }
15248
+ async recoverStuckOutboxItems(options) {
15249
+ await this.initialize();
15250
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
15251
+ }
15252
+ async repairLegacyProjectScope(options) {
15253
+ await this.initialize();
15254
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
15255
+ }
14880
15256
  async getStats() {
14881
15257
  await this.initialize();
14882
15258
  const deps = this.getStatsDeps();
@@ -16528,18 +16904,18 @@ function assertDefaultRetrieverStore(eventStore) {
16528
16904
  function createMemoryEngineServices(options) {
16529
16905
  const factories = options.factories ?? {};
16530
16906
  const storagePath = options.storagePath;
16531
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
16532
- fs5.mkdirSync(storagePath, { recursive: true });
16907
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
16908
+ fs6.mkdirSync(storagePath, { recursive: true });
16533
16909
  }
16534
16910
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
16535
- path4.join(storagePath, "events.sqlite"),
16911
+ path5.join(storagePath, "events.sqlite"),
16536
16912
  {
16537
16913
  readonly: options.readOnly,
16538
16914
  markdownMirrorRoot: storagePath
16539
16915
  }
16540
16916
  );
16541
16917
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
16542
- path4.join(storagePath, "vectors")
16918
+ path5.join(storagePath, "vectors")
16543
16919
  );
16544
16920
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
16545
16921
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -16950,8 +17326,8 @@ function createMemoryRuntimeService(deps) {
16950
17326
  }
16951
17327
 
16952
17328
  // src/extensions/shared-memory/shared-memory-services.ts
16953
- import * as fs6 from "fs";
16954
- import * as path5 from "path";
17329
+ import * as fs7 from "fs";
17330
+ import * as path6 from "path";
16955
17331
 
16956
17332
  // src/core/shared-event-store.ts
16957
17333
  var SharedEventStore = class {
@@ -17639,7 +18015,7 @@ var SharedMemoryServices = class {
17639
18015
  this.ensureDirectory(sharedPath, { allowCreate: true });
17640
18016
  const store = await this.openStore(sharedPath);
17641
18017
  this.sharedVectorStore = this.factories.createSharedVectorStore(
17642
- path5.join(sharedPath, "vectors")
18018
+ path6.join(sharedPath, "vectors")
17643
18019
  );
17644
18020
  await this.sharedVectorStore.initialize();
17645
18021
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -17712,7 +18088,7 @@ var SharedMemoryServices = class {
17712
18088
  async createOpenStorePromise(sharedPath) {
17713
18089
  if (!this.sharedEventStore) {
17714
18090
  const sharedEventStore = this.factories.createSharedEventStore(
17715
- path5.join(sharedPath, "shared.duckdb")
18091
+ path6.join(sharedPath, "shared.duckdb")
17716
18092
  );
17717
18093
  await sharedEventStore.initialize();
17718
18094
  this.sharedEventStore = sharedEventStore;
@@ -17732,9 +18108,9 @@ var SharedMemoryServices = class {
17732
18108
  }
17733
18109
  get factories() {
17734
18110
  return {
17735
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
18111
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
17736
18112
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
17737
- fs6.mkdirSync(targetPath, { recursive: true });
18113
+ fs7.mkdirSync(targetPath, { recursive: true });
17738
18114
  }),
17739
18115
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
17740
18116
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -17849,33 +18225,11 @@ function createMemoryServiceComposition(options) {
17849
18225
  }
17850
18226
  function defaultExpandPath(targetPath) {
17851
18227
  if (targetPath.startsWith("~")) {
17852
- return path6.join(os.homedir(), targetPath.slice(1));
18228
+ return path7.join(os2.homedir(), targetPath.slice(1));
17853
18229
  }
17854
18230
  return targetPath;
17855
18231
  }
17856
18232
 
17857
- // src/core/registry/project-path.ts
17858
- import * as crypto2 from "crypto";
17859
- import * as fs7 from "fs";
17860
- import * as os2 from "os";
17861
- import * as path7 from "path";
17862
- function normalizeProjectPath(projectPath) {
17863
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
17864
- try {
17865
- return fs7.realpathSync(expanded);
17866
- } catch {
17867
- return path7.resolve(expanded);
17868
- }
17869
- }
17870
- function hashProjectPath(projectPath) {
17871
- const normalizedPath = normalizeProjectPath(projectPath);
17872
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
17873
- }
17874
- function getProjectStoragePath(projectPath) {
17875
- const hash = hashProjectPath(projectPath);
17876
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
17877
- }
17878
-
17879
18233
  // src/core/registry/session-registry.ts
17880
18234
  import * as fs8 from "fs";
17881
18235
  import * as os3 from "os";
@@ -18199,6 +18553,12 @@ var MemoryService = class {
18199
18553
  async getOutboxStats() {
18200
18554
  return this.queryService.getOutboxStats();
18201
18555
  }
18556
+ async recoverStuckOutboxItems(options) {
18557
+ return this.queryService.recoverStuckOutboxItems(options);
18558
+ }
18559
+ async repairLegacyProjectScope(options) {
18560
+ return this.queryService.repairLegacyProjectScope(options);
18561
+ }
18202
18562
  async getRetrievalTraceStats() {
18203
18563
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
18204
18564
  }
@@ -18928,7 +19288,7 @@ import * as os7 from "os";
18928
19288
  import * as readline2 from "readline";
18929
19289
  import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
18930
19290
  var CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS = 1e4;
18931
- function isRecord(value) {
19291
+ function isRecord2(value) {
18932
19292
  return typeof value === "object" && value !== null;
18933
19293
  }
18934
19294
  function normalizeMaybeRealpath(p) {
@@ -18945,7 +19305,7 @@ function extractCodexContentText(content, maxContentChars = CODEX_VALIDATION_DEF
18945
19305
  texts.push(content);
18946
19306
  } else if (Array.isArray(content)) {
18947
19307
  for (const block of content) {
18948
- if (!isRecord(block))
19308
+ if (!isRecord2(block))
18949
19309
  continue;
18950
19310
  const b = block;
18951
19311
  const t = typeof b.type === "string" ? b.type : "";
@@ -19040,7 +19400,7 @@ var CodexSessionHistoryImporter = class {
19040
19400
  const obj = JSON.parse(line);
19041
19401
  if (obj.type !== "session_meta")
19042
19402
  continue;
19043
- if (!isRecord(obj.payload))
19403
+ if (!isRecord2(obj.payload))
19044
19404
  break;
19045
19405
  const payload = obj.payload;
19046
19406
  const sessionId = typeof payload.id === "string" ? payload.id : null;
@@ -19256,7 +19616,7 @@ var CodexSessionHistoryImporter = class {
19256
19616
  try {
19257
19617
  const entry = JSON.parse(line);
19258
19618
  result.totalMessages++;
19259
- if (entry.type === "response_item" && isRecord(entry.payload)) {
19619
+ if (entry.type === "response_item" && isRecord2(entry.payload)) {
19260
19620
  const payload = entry.payload;
19261
19621
  if (payload.type !== "message")
19262
19622
  continue;
@@ -19448,10 +19808,10 @@ var SENSITIVE_PATTERNS = [
19448
19808
  // Redact the whole URI so usernames, credentials, hosts, paths, and query
19449
19809
  // params do not leak either.
19450
19810
  /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>/@]+@[^\s'"`<>]+/gi,
19451
- /password\s*[:=]\s*['"]?[^\s'"]+/gi,
19452
- /api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/gi,
19453
- /secret\s*[:=]\s*['"]?[^\s'"]+/gi,
19454
- /token\s*[:=]\s*['"]?[^\s'"]+/gi,
19811
+ /(?:[\w.-]+[-_])?password\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
19812
+ /(?:[\w.-]+[-_])?api[-_]?key\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
19813
+ /(?:[\w.-]+[-_])?secret\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
19814
+ /(?:[\w.-]+[-_])?token\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
19455
19815
  /bearer\s+[a-zA-Z0-9\-_.]+/gi,
19456
19816
  /AWS[_-]?ACCESS[_-]?KEY[_-]?ID\s*[:=]\s*['"]?[A-Z0-9]+/gi,
19457
19817
  /AWS[_-]?SECRET[_-]?ACCESS[_-]?KEY\s*[:=]\s*['"]?[^\s'"]+/gi,
@@ -19461,8 +19821,37 @@ var SENSITIVE_PATTERNS = [
19461
19821
  /sk-[a-zA-Z0-9]{48}/g
19462
19822
  // OpenAI API Key
19463
19823
  ];
19464
- function maskSensitiveString(value) {
19824
+ var CLI_SECRET_OPTION_PATTERNS = [
19825
+ /(--(?:[a-z0-9]+[-_])*(?:password|secret|api[-_]?key|token|bearer)(?:[-_][a-z0-9]+)*(?:\s+|=))(?:"[^"]*"|'[^']*'|[^\s'"`<>]+)/gi
19826
+ ];
19827
+ var URL_FOLLOWING_SECRET_PATTERN = /((?:https?:\/\/[^\s'"`<>]+)\s*\r?\n\s*)([A-Za-z0-9!@#$%^&*._+\-~:=/]{6,})(?=\s*(?:\r?\n|$))/gi;
19828
+ function looksLikePastedSecret(value) {
19829
+ return value.length >= 8 && /(?:\d|[^A-Za-z0-9])/.test(value);
19830
+ }
19831
+ function maskUrlFollowingSecret(value) {
19832
+ let count = 0;
19833
+ const content = value.replace(URL_FOLLOWING_SECRET_PATTERN, (_match, prefix, secret) => {
19834
+ if (!looksLikePastedSecret(secret))
19835
+ return `${prefix}${secret}`;
19836
+ count++;
19837
+ return `${prefix}[REDACTED]`;
19838
+ });
19839
+ return { content, count };
19840
+ }
19841
+ function maskCliSecretOptions(value) {
19842
+ let count = 0;
19465
19843
  let filtered = value;
19844
+ for (const pattern of CLI_SECRET_OPTION_PATTERNS) {
19845
+ pattern.lastIndex = 0;
19846
+ filtered = filtered.replace(pattern, (_match, prefix) => {
19847
+ count++;
19848
+ return `${prefix}[REDACTED]`;
19849
+ });
19850
+ }
19851
+ return { content: filtered, count };
19852
+ }
19853
+ function maskSensitiveString(value) {
19854
+ let filtered = maskCliSecretOptions(value).content;
19466
19855
  for (const pattern of SENSITIVE_PATTERNS) {
19467
19856
  pattern.lastIndex = 0;
19468
19857
  filtered = filtered.replace(pattern, "[REDACTED]");
@@ -19482,6 +19871,12 @@ function applyPrivacyFilter(content, config) {
19482
19871
  filtered = tagResult.filtered;
19483
19872
  privateTagCount = tagResult.stats.count;
19484
19873
  }
19874
+ const cliResult = maskCliSecretOptions(filtered);
19875
+ filtered = cliResult.content;
19876
+ patternMatchCount += cliResult.count;
19877
+ const urlSecretResult = maskUrlFollowingSecret(filtered);
19878
+ filtered = urlSecretResult.content;
19879
+ patternMatchCount += urlSecretResult.count;
19485
19880
  for (const pattern of SENSITIVE_PATTERNS) {
19486
19881
  pattern.lastIndex = 0;
19487
19882
  const matches = filtered.match(pattern);
@@ -19493,13 +19888,13 @@ function applyPrivacyFilter(content, config) {
19493
19888
  for (const patternStr of config.excludePatterns || []) {
19494
19889
  try {
19495
19890
  const regex = new RegExp(
19496
- `(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
19891
+ `(^|[^\\w-])(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
19497
19892
  "gi"
19498
19893
  );
19499
19894
  const matches = filtered.match(regex);
19500
19895
  if (matches) {
19501
19896
  patternMatchCount += matches.length;
19502
- filtered = filtered.replace(regex, "[REDACTED]");
19897
+ filtered = filtered.replace(regex, "$1[REDACTED]");
19503
19898
  }
19504
19899
  } catch {
19505
19900
  }