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/cli/index.js CHANGED
@@ -17,8 +17,8 @@ import * as os11 from "os";
17
17
  import * as os5 from "os";
18
18
 
19
19
  // src/core/engine/memory-service-composition.ts
20
- import * as os from "os";
21
- import * as path6 from "path";
20
+ import * as os2 from "os";
21
+ import * as path7 from "path";
22
22
 
23
23
  // src/core/metadata-extractor.ts
24
24
  function createToolObservationEmbedding(toolName, metadata, success) {
@@ -1529,8 +1529,8 @@ function createEndlessMemoryServices(options) {
1529
1529
  }
1530
1530
 
1531
1531
  // src/core/engine/memory-engine-services.ts
1532
- import * as fs5 from "fs";
1533
- import * as path4 from "path";
1532
+ import * as fs6 from "fs";
1533
+ import * as path5 from "path";
1534
1534
 
1535
1535
  // src/extensions/vector/embedder.ts
1536
1536
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -2208,14 +2208,43 @@ function makeDedupeKey(content, sessionId) {
2208
2208
  return `${sessionId}:${contentHash}`;
2209
2209
  }
2210
2210
 
2211
+ // src/core/sqlite-event-store.ts
2212
+ import * as nodePath2 from "path";
2213
+
2214
+ // src/core/registry/project-path.ts
2215
+ import * as crypto2 from "crypto";
2216
+ import * as fs3 from "fs";
2217
+ import * as os from "os";
2218
+ import * as path3 from "path";
2219
+ function normalizeProjectPath(projectPath) {
2220
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
2221
+ try {
2222
+ return fs3.realpathSync(expanded);
2223
+ } catch {
2224
+ return path3.resolve(expanded);
2225
+ }
2226
+ }
2227
+ function hashProjectPath(projectPath) {
2228
+ const normalizedPath = normalizeProjectPath(projectPath);
2229
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
2230
+ }
2231
+ function getProjectStoragePath(projectPath) {
2232
+ const hash = hashProjectPath(projectPath);
2233
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
2234
+ }
2235
+ function resolveProjectStoragePath(projectOrHash) {
2236
+ const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
2237
+ return isHash ? path3.join(os.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
2238
+ }
2239
+
2211
2240
  // src/core/sqlite-wrapper.ts
2212
2241
  import Database from "better-sqlite3";
2213
- import * as fs3 from "fs";
2242
+ import * as fs4 from "fs";
2214
2243
  import * as nodePath from "path";
2215
2244
  function createSQLiteDatabase(path22, options) {
2216
2245
  const dir = nodePath.dirname(path22);
2217
- if (!fs3.existsSync(dir)) {
2218
- fs3.mkdirSync(dir, { recursive: true });
2246
+ if (!fs4.existsSync(dir)) {
2247
+ fs4.mkdirSync(dir, { recursive: true });
2219
2248
  }
2220
2249
  const db = new Database(path22, {
2221
2250
  readonly: options?.readonly ?? false
@@ -2264,8 +2293,8 @@ function toSQLiteTimestamp(date) {
2264
2293
  }
2265
2294
 
2266
2295
  // src/core/markdown-mirror.ts
2267
- import * as fs4 from "fs/promises";
2268
- import * as path3 from "path";
2296
+ import * as fs5 from "fs/promises";
2297
+ import * as path4 from "path";
2269
2298
  var DEFAULT_NAMESPACE = "default";
2270
2299
  var DEFAULT_CATEGORY = "uncategorized";
2271
2300
  function sanitizeSegment2(input, fallback) {
@@ -2294,7 +2323,7 @@ function buildMirrorPath2(rootDir, event) {
2294
2323
  const yyyy = d.getFullYear();
2295
2324
  const mm = String(d.getMonth() + 1).padStart(2, "0");
2296
2325
  const dd = String(d.getDate()).padStart(2, "0");
2297
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2326
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2298
2327
  }
2299
2328
  function formatMirrorEntry(event) {
2300
2329
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -2315,8 +2344,8 @@ var MarkdownMirror2 = class {
2315
2344
  }
2316
2345
  async append(event) {
2317
2346
  const outPath = buildMirrorPath2(this.rootDir, event);
2318
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
2319
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
2347
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
2348
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
2320
2349
  return outPath;
2321
2350
  }
2322
2351
  };
@@ -2329,6 +2358,143 @@ function normalizeQueryRewriteKind(value) {
2329
2358
  return "none";
2330
2359
  }
2331
2360
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2361
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2362
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2363
+ function emptyOutboxRecoveryResult() {
2364
+ return {
2365
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2366
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2367
+ };
2368
+ }
2369
+ function isRecord(value) {
2370
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2371
+ }
2372
+ function getNestedRecord(root, path22) {
2373
+ let cursor = root;
2374
+ for (const key of path22) {
2375
+ if (!isRecord(cursor))
2376
+ return void 0;
2377
+ cursor = cursor[key];
2378
+ }
2379
+ return isRecord(cursor) ? cursor : void 0;
2380
+ }
2381
+ function getNestedString(root, path22) {
2382
+ let cursor = root;
2383
+ for (const key of path22) {
2384
+ if (!isRecord(cursor))
2385
+ return void 0;
2386
+ cursor = cursor[key];
2387
+ }
2388
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
2389
+ }
2390
+ function metadataProjectHash(metadata) {
2391
+ return getNestedString(metadata, ["scope", "project", "hash"]);
2392
+ }
2393
+ function metadataProjectPaths(metadata) {
2394
+ const candidates = [
2395
+ getNestedString(metadata, ["projectPath"]),
2396
+ getNestedString(metadata, ["sourceProjectPath"]),
2397
+ getNestedString(metadata, ["scope", "project", "path"])
2398
+ ];
2399
+ const paths = [];
2400
+ for (const value of candidates) {
2401
+ if (value && !paths.includes(value))
2402
+ paths.push(value);
2403
+ }
2404
+ return paths;
2405
+ }
2406
+ function metadataProjectPath(metadata) {
2407
+ return metadataProjectPaths(metadata)[0];
2408
+ }
2409
+ function isActiveQuarantinedMetadata(metadata) {
2410
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
2411
+ return quarantine?.status === "active";
2412
+ }
2413
+ function activeQuarantineStatusExpression(column = "metadata") {
2414
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
2415
+ }
2416
+ function notActiveQuarantinedSql(column = "metadata") {
2417
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
2418
+ }
2419
+ function maybeQuarantinePredicate(options, column = "metadata") {
2420
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
2421
+ }
2422
+ function safeParseMetadataValue(value) {
2423
+ if (!value)
2424
+ return void 0;
2425
+ if (typeof value === "object")
2426
+ return isRecord(value) ? value : void 0;
2427
+ if (typeof value !== "string")
2428
+ return void 0;
2429
+ try {
2430
+ const parsed = JSON.parse(value);
2431
+ return isRecord(parsed) ? parsed : void 0;
2432
+ } catch {
2433
+ return void 0;
2434
+ }
2435
+ }
2436
+ function isImportedOrLegacyScopedMetadata(metadata) {
2437
+ if (!metadata)
2438
+ return false;
2439
+ return Boolean(
2440
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
2441
+ );
2442
+ }
2443
+ function addMetadataTag(metadata, tag) {
2444
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
2445
+ if (!current.includes(tag))
2446
+ metadata.tags = [...current, tag];
2447
+ }
2448
+ function buildRepairResult(projectHash, dryRun) {
2449
+ return {
2450
+ dryRun,
2451
+ projectHash,
2452
+ scanned: 0,
2453
+ repaired: 0,
2454
+ quarantined: 0,
2455
+ alreadyScoped: 0,
2456
+ skipped: 0,
2457
+ samples: []
2458
+ };
2459
+ }
2460
+ function normalizeRepoName(value) {
2461
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
2462
+ }
2463
+ function projectBasename(projectPath) {
2464
+ if (!projectPath)
2465
+ return void 0;
2466
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
2467
+ const basename7 = nodePath2.basename(trimmed);
2468
+ return basename7 ? normalizeRepoName(basename7) : void 0;
2469
+ }
2470
+ function isProjectScopeRepairExplanation(content) {
2471
+ const normalized = content.toLowerCase();
2472
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
2473
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
2474
+ return hasRepairContext && hasExplanationContext;
2475
+ }
2476
+ function hasConflictingContentProjectHint(content, projectPath) {
2477
+ const currentName = projectBasename(projectPath);
2478
+ if (!currentName)
2479
+ return false;
2480
+ if (isProjectScopeRepairExplanation(content))
2481
+ return false;
2482
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
2483
+ let githubMatch;
2484
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
2485
+ const repo = normalizeRepoName(githubMatch[2] || "");
2486
+ if (repo && repo !== currentName)
2487
+ return true;
2488
+ }
2489
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
2490
+ let workspaceMatch;
2491
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
2492
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
2493
+ if (repo && repo !== currentName)
2494
+ return true;
2495
+ }
2496
+ return false;
2497
+ }
2332
2498
  var SQLiteEventStore = class {
2333
2499
  db;
2334
2500
  initialized = false;
@@ -2827,11 +2993,11 @@ var SQLiteEventStore = class {
2827
2993
  /**
2828
2994
  * Get events by session ID
2829
2995
  */
2830
- async getSessionEvents(sessionId) {
2996
+ async getSessionEvents(sessionId, options) {
2831
2997
  await this.initialize();
2832
2998
  const rows = sqliteAll(
2833
2999
  this.db,
2834
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
3000
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2835
3001
  [sessionId]
2836
3002
  );
2837
3003
  return rows.map(this.rowToEvent);
@@ -2839,11 +3005,11 @@ var SQLiteEventStore = class {
2839
3005
  /**
2840
3006
  * Get recent events
2841
3007
  */
2842
- async getRecentEvents(limit = 100) {
3008
+ async getRecentEvents(limit = 100, options) {
2843
3009
  await this.initialize();
2844
3010
  const rows = sqliteAll(
2845
3011
  this.db,
2846
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3012
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2847
3013
  [limit]
2848
3014
  );
2849
3015
  return rows.map(this.rowToEvent);
@@ -2851,11 +3017,11 @@ var SQLiteEventStore = class {
2851
3017
  /**
2852
3018
  * Get event by ID
2853
3019
  */
2854
- async getEvent(id) {
3020
+ async getEvent(id, options) {
2855
3021
  await this.initialize();
2856
3022
  const row = sqliteGet(
2857
3023
  this.db,
2858
- `SELECT * FROM events WHERE id = ?`,
3024
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2859
3025
  [id]
2860
3026
  );
2861
3027
  if (!row)
@@ -2865,11 +3031,11 @@ var SQLiteEventStore = class {
2865
3031
  /**
2866
3032
  * Get events since a timestamp (for sync)
2867
3033
  */
2868
- async getEventsSince(timestamp, limit = 1e3) {
3034
+ async getEventsSince(timestamp, limit = 1e3, options) {
2869
3035
  await this.initialize();
2870
3036
  const rows = sqliteAll(
2871
3037
  this.db,
2872
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3038
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2873
3039
  [timestamp, limit]
2874
3040
  );
2875
3041
  return rows.map(this.rowToEvent);
@@ -2878,11 +3044,11 @@ var SQLiteEventStore = class {
2878
3044
  * Get events since a SQLite rowid (for robust incremental replication).
2879
3045
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2880
3046
  */
2881
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3047
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2882
3048
  await this.initialize();
2883
3049
  const rows = sqliteAll(
2884
3050
  this.db,
2885
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
3051
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
2886
3052
  [lastRowid, limit]
2887
3053
  );
2888
3054
  return rows.map((row) => ({
@@ -3079,7 +3245,9 @@ var SQLiteEventStore = class {
3079
3245
  const placeholders = ids.map(() => "?").join(",");
3080
3246
  sqliteRun(
3081
3247
  this.db,
3082
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3248
+ `UPDATE embedding_outbox
3249
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3250
+ WHERE id IN (${placeholders})`,
3083
3251
  ids
3084
3252
  );
3085
3253
  return pending.map((row) => ({
@@ -3115,19 +3283,19 @@ var SQLiteEventStore = class {
3115
3283
  /**
3116
3284
  * Count total events
3117
3285
  */
3118
- async countEvents() {
3286
+ async countEvents(options) {
3119
3287
  await this.initialize();
3120
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
3288
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
3121
3289
  return row?.count || 0;
3122
3290
  }
3123
3291
  /**
3124
3292
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3125
3293
  */
3126
- async getEventsPage(limit = 1e3, offset = 0) {
3294
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3127
3295
  await this.initialize();
3128
3296
  const rows = sqliteAll(
3129
3297
  this.db,
3130
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3298
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3131
3299
  [limit, offset]
3132
3300
  );
3133
3301
  return rows.map(this.rowToEvent);
@@ -3149,6 +3317,197 @@ var SQLiteEventStore = class {
3149
3317
  [error, ...ids]
3150
3318
  );
3151
3319
  }
3320
+ /**
3321
+ * Recover abandoned outbox work after a worker/process crash.
3322
+ *
3323
+ * Rows in `processing` are claimed work. If the process exits before marking
3324
+ * them done/failed, they otherwise remain invisible to future processing.
3325
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3326
+ */
3327
+ async recoverStuckOutboxItems(options = {}) {
3328
+ await this.initialize();
3329
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3330
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3331
+ const now = options.now ?? /* @__PURE__ */ new Date();
3332
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3333
+ const result = emptyOutboxRecoveryResult();
3334
+ const embeddingRecovered = sqliteRun(
3335
+ this.db,
3336
+ `UPDATE embedding_outbox
3337
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3338
+ WHERE status = 'processing'
3339
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3340
+ [threshold]
3341
+ );
3342
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3343
+ const embeddingRetried = sqliteRun(
3344
+ this.db,
3345
+ `UPDATE embedding_outbox
3346
+ SET status = 'pending', error_message = NULL
3347
+ WHERE status = 'failed'
3348
+ AND retry_count < ?`,
3349
+ [maxRetries]
3350
+ );
3351
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3352
+ const vectorRecovered = sqliteRun(
3353
+ this.db,
3354
+ `UPDATE vector_outbox
3355
+ SET status = 'pending', updated_at = ?, error = NULL
3356
+ WHERE status = 'processing'
3357
+ AND datetime(updated_at) < datetime(?)`,
3358
+ [now.toISOString(), threshold]
3359
+ );
3360
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3361
+ const vectorRetried = sqliteRun(
3362
+ this.db,
3363
+ `UPDATE vector_outbox
3364
+ SET status = 'pending', updated_at = ?, error = NULL
3365
+ WHERE status = 'failed'
3366
+ AND retry_count < ?`,
3367
+ [now.toISOString(), maxRetries]
3368
+ );
3369
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3370
+ return result;
3371
+ }
3372
+ /**
3373
+ * Repair legacy imported events that predate canonical project scope metadata.
3374
+ *
3375
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
3376
+ * imported but cannot be proven to belong to this project are quarantined so
3377
+ * dashboard default reads/search do not surface cross-project contamination.
3378
+ */
3379
+ async repairLegacyProjectScope(options = {}) {
3380
+ await this.initialize();
3381
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
3382
+ if (!projectHash) {
3383
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
3384
+ }
3385
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
3386
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
3387
+ }
3388
+ const dryRun = options.dryRun === true;
3389
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
3390
+ const result = buildRepairResult(projectHash, dryRun);
3391
+ const rows = sqliteAll(
3392
+ this.db,
3393
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
3394
+ FROM events e
3395
+ LEFT JOIN sessions s ON s.id = e.session_id
3396
+ ORDER BY e.timestamp ASC`,
3397
+ []
3398
+ );
3399
+ const sample = (entry) => {
3400
+ if (result.samples.length < 20)
3401
+ result.samples.push(entry);
3402
+ };
3403
+ for (const row of rows) {
3404
+ result.scanned++;
3405
+ let metadata = {};
3406
+ let metadataParseInvalid = false;
3407
+ if (row.metadata) {
3408
+ const parsed = safeParseMetadataValue(row.metadata);
3409
+ if (parsed) {
3410
+ metadata = parsed;
3411
+ } else {
3412
+ metadataParseInvalid = true;
3413
+ }
3414
+ }
3415
+ if (isActiveQuarantinedMetadata(metadata)) {
3416
+ result.skipped++;
3417
+ continue;
3418
+ }
3419
+ const currentHash = metadataProjectHash(metadata);
3420
+ const explicitPath = metadataProjectPath(metadata);
3421
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
3422
+ const candidatePaths = metadataProjectPaths(metadata);
3423
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
3424
+ candidatePaths.push(sessionProjectPath);
3425
+ }
3426
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
3427
+ const pathHashes = candidatePaths.map((candidate) => {
3428
+ try {
3429
+ return { path: candidate, hash: hashProjectPath(candidate) };
3430
+ } catch {
3431
+ return { path: candidate, hash: void 0 };
3432
+ }
3433
+ });
3434
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
3435
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
3436
+ let action = "skipped";
3437
+ let reason;
3438
+ let observedProjectHash;
3439
+ if (foreignPath) {
3440
+ action = "quarantined";
3441
+ reason = "project-path-mismatch";
3442
+ observedProjectHash = foreignPath.hash;
3443
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
3444
+ action = "quarantined";
3445
+ reason = "content-project-mismatch";
3446
+ } else if (currentHash === projectHash) {
3447
+ result.alreadyScoped++;
3448
+ continue;
3449
+ } else if (currentHash && currentHash !== projectHash) {
3450
+ action = "quarantined";
3451
+ reason = "scope-hash-mismatch";
3452
+ observedProjectHash = currentHash;
3453
+ } else if (matchingPath) {
3454
+ action = "repaired";
3455
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
3456
+ } else if (candidatePaths.length > 0) {
3457
+ action = "quarantined";
3458
+ reason = "project-path-mismatch";
3459
+ } else if (importedOrLegacy) {
3460
+ action = "quarantined";
3461
+ reason = "missing-project-scope";
3462
+ }
3463
+ if (action === "skipped" || !reason) {
3464
+ result.skipped++;
3465
+ continue;
3466
+ }
3467
+ if (action === "repaired") {
3468
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
3469
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
3470
+ project.hash = projectHash;
3471
+ scope.project = project;
3472
+ metadata.scope = scope;
3473
+ metadata.repair = {
3474
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3475
+ legacyProjectScope: {
3476
+ action,
3477
+ reason,
3478
+ repairedAt: nowIso
3479
+ }
3480
+ };
3481
+ addMetadataTag(metadata, `proj:${projectHash}`);
3482
+ result.repaired++;
3483
+ } else {
3484
+ metadata.quarantine = {
3485
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
3486
+ status: "active",
3487
+ category: "project-scope",
3488
+ reason,
3489
+ detectedAt: nowIso,
3490
+ expectedProjectHash: projectHash,
3491
+ ...observedProjectHash ? { observedProjectHash } : {}
3492
+ };
3493
+ metadata.repair = {
3494
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3495
+ legacyProjectScope: {
3496
+ action,
3497
+ reason,
3498
+ repairedAt: nowIso
3499
+ }
3500
+ };
3501
+ addMetadataTag(metadata, "quarantine:project-scope");
3502
+ result.quarantined++;
3503
+ }
3504
+ sample({ eventId: row.id, action, reason });
3505
+ if (!dryRun) {
3506
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
3507
+ }
3508
+ }
3509
+ return result;
3510
+ }
3152
3511
  /**
3153
3512
  * Get embedding/vector outbox health statistics
3154
3513
  */
@@ -3196,7 +3555,11 @@ var SQLiteEventStore = class {
3196
3555
  await this.initialize();
3197
3556
  const rows = sqliteAll(
3198
3557
  this.db,
3199
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
3558
+ `SELECT ml.level, COUNT(*) as count
3559
+ FROM memory_levels ml
3560
+ INNER JOIN events e ON e.id = ml.event_id
3561
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
3562
+ GROUP BY ml.level`
3200
3563
  );
3201
3564
  return rows;
3202
3565
  }
@@ -3212,6 +3575,7 @@ var SQLiteEventStore = class {
3212
3575
  `SELECT e.* FROM events e
3213
3576
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3214
3577
  WHERE ml.level = ?
3578
+ AND ${notActiveQuarantinedSql("e.metadata")}
3215
3579
  ORDER BY e.timestamp DESC
3216
3580
  LIMIT ? OFFSET ?`,
3217
3581
  [level, limit, offset]
@@ -3304,12 +3668,13 @@ var SQLiteEventStore = class {
3304
3668
  /**
3305
3669
  * Get most accessed memories (falls back to recent events if none accessed)
3306
3670
  */
3307
- async getMostAccessed(limit = 10) {
3671
+ async getMostAccessed(limit = 10, options) {
3308
3672
  await this.initialize();
3309
3673
  let rows = sqliteAll(
3310
3674
  this.db,
3311
3675
  `SELECT * FROM events
3312
3676
  WHERE access_count > 0
3677
+ AND ${maybeQuarantinePredicate(options)}
3313
3678
  ORDER BY access_count DESC, last_accessed_at DESC
3314
3679
  LIMIT ?`,
3315
3680
  [limit]
@@ -3318,6 +3683,7 @@ var SQLiteEventStore = class {
3318
3683
  rows = sqliteAll(
3319
3684
  this.db,
3320
3685
  `SELECT * FROM events
3686
+ WHERE ${maybeQuarantinePredicate(options)}
3321
3687
  ORDER BY timestamp DESC
3322
3688
  LIMIT ?`,
3323
3689
  [limit]
@@ -3448,6 +3814,7 @@ var SQLiteEventStore = class {
3448
3814
  FROM memory_helpfulness mh
3449
3815
  JOIN events e ON e.id = mh.event_id
3450
3816
  WHERE mh.measured_at IS NOT NULL
3817
+ AND ${notActiveQuarantinedSql("e.metadata")}
3451
3818
  GROUP BY mh.event_id
3452
3819
  ORDER BY avg_score DESC
3453
3820
  LIMIT ?`,
@@ -3512,6 +3879,7 @@ var SQLiteEventStore = class {
3512
3879
  FROM events_fts fts
3513
3880
  JOIN events e ON e.id = fts.event_id
3514
3881
  WHERE events_fts MATCH ?
3882
+ AND ${notActiveQuarantinedSql("e.metadata")}
3515
3883
  ORDER BY fts.rank
3516
3884
  LIMIT ?`,
3517
3885
  [searchTerms, limit]
@@ -3526,6 +3894,7 @@ var SQLiteEventStore = class {
3526
3894
  this.db,
3527
3895
  `SELECT *, 0 as rank FROM events
3528
3896
  WHERE content LIKE ?
3897
+ AND ${notActiveQuarantinedSql()}
3529
3898
  ORDER BY timestamp DESC
3530
3899
  LIMIT ?`,
3531
3900
  [likePattern, limit]
@@ -3732,6 +4101,7 @@ var SQLiteEventStore = class {
3732
4101
  `SELECT turn_id, MIN(timestamp) as min_ts
3733
4102
  FROM events
3734
4103
  WHERE session_id = ? AND turn_id IS NOT NULL
4104
+ AND ${maybeQuarantinePredicate(options)}
3735
4105
  GROUP BY turn_id
3736
4106
  ORDER BY min_ts DESC
3737
4107
  LIMIT ? OFFSET ?`,
@@ -3739,7 +4109,7 @@ var SQLiteEventStore = class {
3739
4109
  );
3740
4110
  const turns = [];
3741
4111
  for (const turnRow of turnRows) {
3742
- const events = await this.getEventsByTurn(turnRow.turn_id);
4112
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3743
4113
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3744
4114
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3745
4115
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3758,11 +4128,11 @@ var SQLiteEventStore = class {
3758
4128
  /**
3759
4129
  * Get all events for a specific turn_id
3760
4130
  */
3761
- async getEventsByTurn(turnId) {
4131
+ async getEventsByTurn(turnId, options) {
3762
4132
  await this.initialize();
3763
4133
  const rows = sqliteAll(
3764
4134
  this.db,
3765
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4135
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3766
4136
  [turnId]
3767
4137
  );
3768
4138
  return rows.map(this.rowToEvent);
@@ -3770,13 +4140,14 @@ var SQLiteEventStore = class {
3770
4140
  /**
3771
4141
  * Count total turns for a session
3772
4142
  */
3773
- async countSessionTurns(sessionId) {
4143
+ async countSessionTurns(sessionId, options) {
3774
4144
  await this.initialize();
3775
4145
  const row = sqliteGet(
3776
4146
  this.db,
3777
4147
  `SELECT COUNT(DISTINCT turn_id) as count
3778
4148
  FROM events
3779
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4149
+ WHERE session_id = ? AND turn_id IS NOT NULL
4150
+ AND ${maybeQuarantinePredicate(options)}`,
3780
4151
  [sessionId]
3781
4152
  );
3782
4153
  return row?.count || 0;
@@ -3868,7 +4239,7 @@ var SQLiteEventStore = class {
3868
4239
  content: row.content,
3869
4240
  canonicalKey: row.canonical_key,
3870
4241
  dedupeKey: row.dedupe_key,
3871
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4242
+ metadata: safeParseMetadataValue(row.metadata)
3872
4243
  };
3873
4244
  if (row.access_count !== void 0) {
3874
4245
  event.access_count = row.access_count;
@@ -4020,6 +4391,7 @@ var VectorStore = class {
4020
4391
  * Get total count of vectors
4021
4392
  */
4022
4393
  async count() {
4394
+ await this.initialize();
4023
4395
  if (!this.table)
4024
4396
  return 0;
4025
4397
  const result = await this.table.countRows();
@@ -4458,6 +4830,14 @@ var MemoryQueryService = class {
4458
4830
  await this.initialize();
4459
4831
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4460
4832
  }
4833
+ async recoverStuckOutboxItems(options) {
4834
+ await this.initialize();
4835
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4836
+ }
4837
+ async repairLegacyProjectScope(options) {
4838
+ await this.initialize();
4839
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4840
+ }
4461
4841
  async getStats() {
4462
4842
  await this.initialize();
4463
4843
  const deps = this.getStatsDeps();
@@ -6080,18 +6460,18 @@ function assertDefaultRetrieverStore(eventStore) {
6080
6460
  function createMemoryEngineServices(options) {
6081
6461
  const factories = options.factories ?? {};
6082
6462
  const storagePath = options.storagePath;
6083
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6084
- fs5.mkdirSync(storagePath, { recursive: true });
6463
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6464
+ fs6.mkdirSync(storagePath, { recursive: true });
6085
6465
  }
6086
6466
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6087
- path4.join(storagePath, "events.sqlite"),
6467
+ path5.join(storagePath, "events.sqlite"),
6088
6468
  {
6089
6469
  readonly: options.readOnly,
6090
6470
  markdownMirrorRoot: storagePath
6091
6471
  }
6092
6472
  );
6093
6473
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6094
- path4.join(storagePath, "vectors")
6474
+ path5.join(storagePath, "vectors")
6095
6475
  );
6096
6476
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6097
6477
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6502,8 +6882,8 @@ function createMemoryRuntimeService(deps) {
6502
6882
  }
6503
6883
 
6504
6884
  // src/extensions/shared-memory/shared-memory-services.ts
6505
- import * as fs6 from "fs";
6506
- import * as path5 from "path";
6885
+ import * as fs7 from "fs";
6886
+ import * as path6 from "path";
6507
6887
 
6508
6888
  // src/core/shared-event-store.ts
6509
6889
  var SharedEventStore = class {
@@ -7191,7 +7571,7 @@ var SharedMemoryServices = class {
7191
7571
  this.ensureDirectory(sharedPath, { allowCreate: true });
7192
7572
  const store = await this.openStore(sharedPath);
7193
7573
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7194
- path5.join(sharedPath, "vectors")
7574
+ path6.join(sharedPath, "vectors")
7195
7575
  );
7196
7576
  await this.sharedVectorStore.initialize();
7197
7577
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7264,7 +7644,7 @@ var SharedMemoryServices = class {
7264
7644
  async createOpenStorePromise(sharedPath) {
7265
7645
  if (!this.sharedEventStore) {
7266
7646
  const sharedEventStore = this.factories.createSharedEventStore(
7267
- path5.join(sharedPath, "shared.duckdb")
7647
+ path6.join(sharedPath, "shared.duckdb")
7268
7648
  );
7269
7649
  await sharedEventStore.initialize();
7270
7650
  this.sharedEventStore = sharedEventStore;
@@ -7284,9 +7664,9 @@ var SharedMemoryServices = class {
7284
7664
  }
7285
7665
  get factories() {
7286
7666
  return {
7287
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7667
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7288
7668
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7289
- fs6.mkdirSync(targetPath, { recursive: true });
7669
+ fs7.mkdirSync(targetPath, { recursive: true });
7290
7670
  }),
7291
7671
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7292
7672
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7401,37 +7781,11 @@ function createMemoryServiceComposition(options) {
7401
7781
  }
7402
7782
  function defaultExpandPath(targetPath) {
7403
7783
  if (targetPath.startsWith("~")) {
7404
- return path6.join(os.homedir(), targetPath.slice(1));
7784
+ return path7.join(os2.homedir(), targetPath.slice(1));
7405
7785
  }
7406
7786
  return targetPath;
7407
7787
  }
7408
7788
 
7409
- // src/core/registry/project-path.ts
7410
- import * as crypto2 from "crypto";
7411
- import * as fs7 from "fs";
7412
- import * as os2 from "os";
7413
- import * as path7 from "path";
7414
- function normalizeProjectPath(projectPath) {
7415
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7416
- try {
7417
- return fs7.realpathSync(expanded);
7418
- } catch {
7419
- return path7.resolve(expanded);
7420
- }
7421
- }
7422
- function hashProjectPath(projectPath) {
7423
- const normalizedPath = normalizeProjectPath(projectPath);
7424
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7425
- }
7426
- function getProjectStoragePath(projectPath) {
7427
- const hash = hashProjectPath(projectPath);
7428
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7429
- }
7430
- function resolveProjectStoragePath(projectOrHash) {
7431
- const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
7432
- return isHash ? path7.join(os2.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
7433
- }
7434
-
7435
7789
  // src/core/registry/session-registry.ts
7436
7790
  import * as fs8 from "fs";
7437
7791
  import * as os3 from "os";
@@ -7755,6 +8109,12 @@ var MemoryService = class {
7755
8109
  async getOutboxStats() {
7756
8110
  return this.queryService.getOutboxStats();
7757
8111
  }
8112
+ async recoverStuckOutboxItems(options) {
8113
+ return this.queryService.recoverStuckOutboxItems(options);
8114
+ }
8115
+ async repairLegacyProjectScope(options) {
8116
+ return this.queryService.repairLegacyProjectScope(options);
8117
+ }
7758
8118
  async getRetrievalTraceStats() {
7759
8119
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7760
8120
  }
@@ -8484,7 +8844,7 @@ import * as os7 from "os";
8484
8844
  import * as readline2 from "readline";
8485
8845
  import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
8486
8846
  var CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS = 1e4;
8487
- function isRecord(value) {
8847
+ function isRecord2(value) {
8488
8848
  return typeof value === "object" && value !== null;
8489
8849
  }
8490
8850
  function normalizeMaybeRealpath(p) {
@@ -8501,7 +8861,7 @@ function extractCodexContentText(content, maxContentChars = CODEX_VALIDATION_DEF
8501
8861
  texts.push(content);
8502
8862
  } else if (Array.isArray(content)) {
8503
8863
  for (const block of content) {
8504
- if (!isRecord(block))
8864
+ if (!isRecord2(block))
8505
8865
  continue;
8506
8866
  const b = block;
8507
8867
  const t = typeof b.type === "string" ? b.type : "";
@@ -8589,7 +8949,7 @@ async function readCodexSessionMeta(filePath) {
8589
8949
  const obj = JSON.parse(line);
8590
8950
  if (obj.type !== "session_meta")
8591
8951
  continue;
8592
- if (!isRecord(obj.payload))
8952
+ if (!isRecord2(obj.payload))
8593
8953
  break;
8594
8954
  const payload = obj.payload;
8595
8955
  const sessionId = typeof payload.id === "string" ? payload.id : null;
@@ -8727,7 +9087,7 @@ async function normalizeCodexSessionFile(filePath, options = {}) {
8727
9087
  }
8728
9088
  if (entry.type === "session_meta")
8729
9089
  continue;
8730
- if (entry.type !== "response_item" || !isRecord(entry.payload)) {
9090
+ if (entry.type !== "response_item" || !isRecord2(entry.payload)) {
8731
9091
  summary.skippedUnsupportedRecords += 1;
8732
9092
  continue;
8733
9093
  }
@@ -8882,7 +9242,7 @@ var CodexSessionHistoryImporter = class {
8882
9242
  const obj = JSON.parse(line);
8883
9243
  if (obj.type !== "session_meta")
8884
9244
  continue;
8885
- if (!isRecord(obj.payload))
9245
+ if (!isRecord2(obj.payload))
8886
9246
  break;
8887
9247
  const payload = obj.payload;
8888
9248
  const sessionId = typeof payload.id === "string" ? payload.id : null;
@@ -9098,7 +9458,7 @@ var CodexSessionHistoryImporter = class {
9098
9458
  try {
9099
9459
  const entry = JSON.parse(line);
9100
9460
  result.totalMessages++;
9101
- if (entry.type === "response_item" && isRecord(entry.payload)) {
9461
+ if (entry.type === "response_item" && isRecord2(entry.payload)) {
9102
9462
  const payload = entry.payload;
9103
9463
  if (payload.type !== "message")
9104
9464
  continue;
@@ -9290,10 +9650,10 @@ var SENSITIVE_PATTERNS = [
9290
9650
  // Redact the whole URI so usernames, credentials, hosts, paths, and query
9291
9651
  // params do not leak either.
9292
9652
  /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>/@]+@[^\s'"`<>]+/gi,
9293
- /password\s*[:=]\s*['"]?[^\s'"]+/gi,
9294
- /api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/gi,
9295
- /secret\s*[:=]\s*['"]?[^\s'"]+/gi,
9296
- /token\s*[:=]\s*['"]?[^\s'"]+/gi,
9653
+ /(?:[\w.-]+[-_])?password\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
9654
+ /(?:[\w.-]+[-_])?api[-_]?key\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
9655
+ /(?:[\w.-]+[-_])?secret\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
9656
+ /(?:[\w.-]+[-_])?token\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
9297
9657
  /bearer\s+[a-zA-Z0-9\-_.]+/gi,
9298
9658
  /AWS[_-]?ACCESS[_-]?KEY[_-]?ID\s*[:=]\s*['"]?[A-Z0-9]+/gi,
9299
9659
  /AWS[_-]?SECRET[_-]?ACCESS[_-]?KEY\s*[:=]\s*['"]?[^\s'"]+/gi,
@@ -9303,6 +9663,35 @@ var SENSITIVE_PATTERNS = [
9303
9663
  /sk-[a-zA-Z0-9]{48}/g
9304
9664
  // OpenAI API Key
9305
9665
  ];
9666
+ var CLI_SECRET_OPTION_PATTERNS = [
9667
+ /(--(?:[a-z0-9]+[-_])*(?:password|secret|api[-_]?key|token|bearer)(?:[-_][a-z0-9]+)*(?:\s+|=))(?:"[^"]*"|'[^']*'|[^\s'"`<>]+)/gi
9668
+ ];
9669
+ var URL_FOLLOWING_SECRET_PATTERN = /((?:https?:\/\/[^\s'"`<>]+)\s*\r?\n\s*)([A-Za-z0-9!@#$%^&*._+\-~:=/]{6,})(?=\s*(?:\r?\n|$))/gi;
9670
+ function looksLikePastedSecret(value) {
9671
+ return value.length >= 8 && /(?:\d|[^A-Za-z0-9])/.test(value);
9672
+ }
9673
+ function maskUrlFollowingSecret(value) {
9674
+ let count = 0;
9675
+ const content = value.replace(URL_FOLLOWING_SECRET_PATTERN, (_match, prefix, secret) => {
9676
+ if (!looksLikePastedSecret(secret))
9677
+ return `${prefix}${secret}`;
9678
+ count++;
9679
+ return `${prefix}[REDACTED]`;
9680
+ });
9681
+ return { content, count };
9682
+ }
9683
+ function maskCliSecretOptions(value) {
9684
+ let count = 0;
9685
+ let filtered = value;
9686
+ for (const pattern of CLI_SECRET_OPTION_PATTERNS) {
9687
+ pattern.lastIndex = 0;
9688
+ filtered = filtered.replace(pattern, (_match, prefix) => {
9689
+ count++;
9690
+ return `${prefix}[REDACTED]`;
9691
+ });
9692
+ }
9693
+ return { content: filtered, count };
9694
+ }
9306
9695
  function applyPrivacyFilter(content, config) {
9307
9696
  let filtered = content;
9308
9697
  let privateTagCount = 0;
@@ -9316,6 +9705,12 @@ function applyPrivacyFilter(content, config) {
9316
9705
  filtered = tagResult.filtered;
9317
9706
  privateTagCount = tagResult.stats.count;
9318
9707
  }
9708
+ const cliResult = maskCliSecretOptions(filtered);
9709
+ filtered = cliResult.content;
9710
+ patternMatchCount += cliResult.count;
9711
+ const urlSecretResult = maskUrlFollowingSecret(filtered);
9712
+ filtered = urlSecretResult.content;
9713
+ patternMatchCount += urlSecretResult.count;
9319
9714
  for (const pattern of SENSITIVE_PATTERNS) {
9320
9715
  pattern.lastIndex = 0;
9321
9716
  const matches = filtered.match(pattern);
@@ -9327,13 +9722,13 @@ function applyPrivacyFilter(content, config) {
9327
9722
  for (const patternStr of config.excludePatterns || []) {
9328
9723
  try {
9329
9724
  const regex = new RegExp(
9330
- `(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
9725
+ `(^|[^\\w-])(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
9331
9726
  "gi"
9332
9727
  );
9333
9728
  const matches = filtered.match(regex);
9334
9729
  if (matches) {
9335
9730
  patternMatchCount += matches.length;
9336
- filtered = filtered.replace(regex, "[REDACTED]");
9731
+ filtered = filtered.replace(regex, "$1[REDACTED]");
9337
9732
  }
9338
9733
  } catch {
9339
9734
  }
@@ -10345,6 +10740,26 @@ function getServiceFromQuery(c) {
10345
10740
  }
10346
10741
  return getReadOnlyMemoryService();
10347
10742
  }
10743
+ function getWritableServiceFromQuery(c) {
10744
+ const project = c.req.query("project") || c.req.query("projectId");
10745
+ if (project) {
10746
+ const storagePath = resolveProjectStoragePath(project);
10747
+ return new MemoryService({
10748
+ storagePath,
10749
+ readOnly: false,
10750
+ lightweightMode: true,
10751
+ analyticsEnabled: false,
10752
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
10753
+ });
10754
+ }
10755
+ return new MemoryService({
10756
+ storagePath: "~/.claude-code/memory",
10757
+ readOnly: false,
10758
+ lightweightMode: true,
10759
+ analyticsEnabled: false,
10760
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
10761
+ });
10762
+ }
10348
10763
  function getLightweightServiceFromQuery(c) {
10349
10764
  const project = c.req.query("project") || c.req.query("projectId");
10350
10765
  if (project) {
@@ -12000,6 +12415,13 @@ import { Hono as Hono8 } from "hono";
12000
12415
  import { streamSSE } from "hono/streaming";
12001
12416
  import { spawn } from "child_process";
12002
12417
  var chatRouter = new Hono8();
12418
+ var ProviderFailure = class extends Error {
12419
+ constructor(code, message) {
12420
+ super(message);
12421
+ this.code = code;
12422
+ this.name = "ProviderFailure";
12423
+ }
12424
+ };
12003
12425
  var CLAUDE_TIMEOUT_MS = 12e4;
12004
12426
  chatRouter.post("/", async (c) => {
12005
12427
  let body;
@@ -12011,43 +12433,12 @@ chatRouter.post("/", async (c) => {
12011
12433
  if (!body.message?.trim()) {
12012
12434
  return c.json({ error: "Message is required" }, 400);
12013
12435
  }
12014
- const memoryService = getServiceFromQuery(c);
12436
+ const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
12437
+ const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
12015
12438
  try {
12016
12439
  await memoryService.initialize();
12017
- let memoryContext = "";
12018
- let statsContext = "";
12019
- try {
12020
- const result = await memoryService.retrieveMemories(body.message, {
12021
- topK: 8,
12022
- minScore: 0.5
12023
- });
12024
- if (result.memories.length > 0) {
12025
- const parts = ["## Relevant Memories\n"];
12026
- for (const m of result.memories) {
12027
- const date = new Date(m.event.timestamp).toISOString().split("T")[0];
12028
- const content = m.event.content.slice(0, 500);
12029
- parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
12030
- parts.push(content);
12031
- if (m.sessionContext) {
12032
- parts.push(`_Context: ${m.sessionContext}_`);
12033
- }
12034
- parts.push("");
12035
- }
12036
- memoryContext = parts.join("\n");
12037
- }
12038
- } catch {
12039
- }
12040
- try {
12041
- const stats = await memoryService.getStats();
12042
- const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
12043
- statsContext = [
12044
- "## Memory Stats",
12045
- `- Total events: ${stats.totalEvents}`,
12046
- `- Vector nodes: ${stats.vectorCount}`,
12047
- `- By level: ${levels}`
12048
- ].join("\n");
12049
- } catch {
12050
- }
12440
+ const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
12441
+ const statsContext = await collectStatsContext(memoryService);
12051
12442
  const fullPrompt = buildPrompt(
12052
12443
  statsContext,
12053
12444
  memoryContext,
@@ -12055,13 +12446,23 @@ chatRouter.post("/", async (c) => {
12055
12446
  body.message
12056
12447
  );
12057
12448
  return streamSSE(c, async (stream) => {
12449
+ if (memoryOnly) {
12450
+ await streamMemoryOnlyResponse(stream, {
12451
+ memoryContext,
12452
+ memoryHits,
12453
+ reason: "memory-only-mode"
12454
+ });
12455
+ return;
12456
+ }
12058
12457
  try {
12059
12458
  await streamClaudeResponse(fullPrompt, stream);
12060
12459
  } catch (err) {
12460
+ const diagnostic = providerDiagnostic(err);
12061
12461
  await stream.writeSSE({
12062
- event: "error",
12063
- data: JSON.stringify({ error: err.message })
12462
+ event: "provider_error",
12463
+ data: JSON.stringify(diagnostic)
12064
12464
  });
12465
+ await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
12065
12466
  }
12066
12467
  });
12067
12468
  } catch (error) {
@@ -12070,6 +12471,115 @@ chatRouter.post("/", async (c) => {
12070
12471
  await memoryService.shutdown();
12071
12472
  }
12072
12473
  });
12474
+ async function collectMemoryContext(memoryService, query) {
12475
+ let memoryHits = [];
12476
+ try {
12477
+ const result = await memoryService.retrieveMemories?.(query, {
12478
+ topK: 8,
12479
+ minScore: 0.5
12480
+ });
12481
+ memoryHits = result?.memories ?? [];
12482
+ } catch {
12483
+ memoryHits = [];
12484
+ }
12485
+ if (memoryHits.length === 0) {
12486
+ try {
12487
+ memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
12488
+ } catch {
12489
+ memoryHits = [];
12490
+ }
12491
+ }
12492
+ return {
12493
+ memoryContext: formatMemoryContext(memoryHits),
12494
+ memoryHits
12495
+ };
12496
+ }
12497
+ async function collectStatsContext(memoryService) {
12498
+ try {
12499
+ const stats = await memoryService.getStats?.();
12500
+ if (!stats)
12501
+ return "";
12502
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
12503
+ return [
12504
+ "## Memory Stats",
12505
+ `- Total events: ${stats.totalEvents}`,
12506
+ `- Vector nodes: ${stats.vectorCount}`,
12507
+ `- By level: ${levels}`
12508
+ ].join("\n");
12509
+ } catch {
12510
+ return "";
12511
+ }
12512
+ }
12513
+ function formatMemoryContext(memoryHits) {
12514
+ if (memoryHits.length === 0)
12515
+ return "";
12516
+ const parts = ["## Relevant Memories\n"];
12517
+ for (const m of memoryHits) {
12518
+ const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
12519
+ const content = (m.event.content ?? "").slice(0, 500);
12520
+ parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
12521
+ parts.push(content);
12522
+ if (m.sessionContext) {
12523
+ parts.push(`_Context: ${m.sessionContext}_`);
12524
+ }
12525
+ parts.push("");
12526
+ }
12527
+ return parts.join("\n");
12528
+ }
12529
+ async function streamMemoryOnlyResponse(stream, options) {
12530
+ await stream.writeSSE({
12531
+ event: "diagnostic",
12532
+ data: JSON.stringify({
12533
+ provider: "claude-cli",
12534
+ status: "skipped",
12535
+ mode: "memory-only",
12536
+ reason: options.reason,
12537
+ retrievedMemories: options.memoryHits.length
12538
+ })
12539
+ });
12540
+ await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
12541
+ }
12542
+ async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
12543
+ const content = memoryHits.length > 0 ? [
12544
+ "Provider unavailable or skipped; showing retrieved memory context directly.",
12545
+ "",
12546
+ memoryContext
12547
+ ].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
12548
+ await stream.writeSSE({
12549
+ event: "message",
12550
+ data: JSON.stringify({ content, mode: "memory-only" })
12551
+ });
12552
+ await stream.writeSSE({ event: "done", data: "{}" });
12553
+ }
12554
+ function providerDiagnostic(err) {
12555
+ if (err instanceof ProviderFailure) {
12556
+ return {
12557
+ provider: "claude-cli",
12558
+ code: err.code,
12559
+ message: err.message,
12560
+ fallback: "memory-only"
12561
+ };
12562
+ }
12563
+ return {
12564
+ provider: "claude-cli",
12565
+ code: "claude-cli-error",
12566
+ message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
12567
+ fallback: "memory-only"
12568
+ };
12569
+ }
12570
+ function classifyProviderFailure(message) {
12571
+ const normalized = message.toLowerCase();
12572
+ if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
12573
+ return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
12574
+ }
12575
+ if (normalized.includes("not found") || normalized.includes("enoent")) {
12576
+ return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
12577
+ }
12578
+ if (normalized.includes("timed out")) {
12579
+ return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
12580
+ }
12581
+ return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
12582
+ }
12073
12583
  function buildPrompt(statsContext, memoryContext, history, currentMessage) {
12074
12584
  const parts = [];
12075
12585
  parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
@@ -12112,12 +12622,13 @@ function streamClaudeResponse(prompt, stream) {
12112
12622
  });
12113
12623
  const timeout = setTimeout(() => {
12114
12624
  proc.kill("SIGTERM");
12115
- reject(new Error("Chat response timed out after 2 minutes"));
12625
+ reject(classifyProviderFailure("timed out"));
12116
12626
  }, CLAUDE_TIMEOUT_MS);
12117
12627
  proc.stdin.write(prompt);
12118
12628
  proc.stdin.end();
12119
12629
  let buffer = "";
12120
12630
  let lastSentText = "";
12631
+ let stderrText = "";
12121
12632
  proc.stdout.on("data", async (chunk) => {
12122
12633
  buffer += chunk.toString();
12123
12634
  const lines = buffer.split("\n");
@@ -12146,6 +12657,7 @@ function streamClaudeResponse(prompt, stream) {
12146
12657
  }
12147
12658
  });
12148
12659
  proc.stderr.on("data", (chunk) => {
12660
+ stderrText += chunk.toString();
12149
12661
  if (process.env.CLAUDE_MEMORY_DEBUG) {
12150
12662
  console.error("[chat] claude stderr:", chunk.toString());
12151
12663
  }
@@ -12153,7 +12665,7 @@ function streamClaudeResponse(prompt, stream) {
12153
12665
  proc.on("error", (err) => {
12154
12666
  clearTimeout(timeout);
12155
12667
  if (err.code === "ENOENT") {
12156
- reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
12668
+ reject(classifyProviderFailure("ENOENT not found"));
12157
12669
  } else {
12158
12670
  reject(err);
12159
12671
  }
@@ -12170,7 +12682,7 @@ function streamClaudeResponse(prompt, stream) {
12170
12682
  }
12171
12683
  }
12172
12684
  if (code !== 0 && code !== null) {
12173
- reject(new Error(`Claude CLI exited with code ${code}`));
12685
+ reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
12174
12686
  } else {
12175
12687
  resolve8();
12176
12688
  }
@@ -12219,6 +12731,49 @@ healthRouter.get("/", async (c) => {
12219
12731
  await memoryService.shutdown();
12220
12732
  }
12221
12733
  });
12734
+ healthRouter.post("/recover", async (c) => {
12735
+ const memoryService = getWritableServiceFromQuery(c);
12736
+ try {
12737
+ await memoryService.initialize();
12738
+ const body = await c.req.json().catch(() => ({}));
12739
+ const options = {};
12740
+ if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
12741
+ options.stuckThresholdMs = body.stuckThresholdMs;
12742
+ }
12743
+ if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
12744
+ options.maxRetries = body.maxRetries;
12745
+ }
12746
+ const before = await memoryService.getOutboxStats();
12747
+ const recovered = await memoryService.recoverStuckOutboxItems(options);
12748
+ const [stats, outbox] = await Promise.all([
12749
+ memoryService.getStats(),
12750
+ memoryService.getOutboxStats()
12751
+ ]);
12752
+ return c.json({
12753
+ status: "ok",
12754
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12755
+ recovered,
12756
+ before: {
12757
+ outbox: before
12758
+ },
12759
+ after: {
12760
+ storage: {
12761
+ totalEvents: stats.totalEvents,
12762
+ vectorCount: stats.vectorCount
12763
+ },
12764
+ outbox
12765
+ }
12766
+ });
12767
+ } catch (error) {
12768
+ return c.json({
12769
+ status: "error",
12770
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12771
+ error: error.message
12772
+ }, 500);
12773
+ } finally {
12774
+ await memoryService.shutdown();
12775
+ }
12776
+ });
12222
12777
 
12223
12778
  // src/apps/server/api/index.ts
12224
12779
  var apiRouter = new Hono10().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter).route("/health", healthRouter);
@@ -13402,6 +13957,54 @@ function resolveDashboardCommandOptions(options) {
13402
13957
  };
13403
13958
  }
13404
13959
 
13960
+ // src/apps/cli/repair-command.ts
13961
+ function resolveLegacyProjectScopeRepairOptions(options) {
13962
+ if (options.project !== void 0 && options.project.trim().length === 0) {
13963
+ throw new Error("legacy project-scope repair --project must not be empty");
13964
+ }
13965
+ if (options.projectHash !== void 0 && options.projectHash.trim().length === 0) {
13966
+ throw new Error("legacy project-scope repair --project-hash must not be empty");
13967
+ }
13968
+ const projectPath = typeof options.project === "string" && options.project.length > 0 ? options.project : void 0;
13969
+ const projectHash = typeof options.projectHash === "string" && options.projectHash.length > 0 ? options.projectHash : void 0;
13970
+ if (!projectPath && !projectHash) {
13971
+ throw new Error("legacy project-scope repair requires --project or --project-hash");
13972
+ }
13973
+ if (projectHash && !/^[a-f0-9]{8}$/.test(projectHash)) {
13974
+ throw new Error("legacy project-scope repair --project-hash must be an 8-character lowercase hex hash");
13975
+ }
13976
+ if (projectPath && projectHash && hashProjectPath(projectPath) !== projectHash) {
13977
+ throw new Error("legacy project-scope repair --project and --project-hash refer to different project stores");
13978
+ }
13979
+ return {
13980
+ projectPath,
13981
+ projectHash,
13982
+ dryRun: options.apply !== true
13983
+ };
13984
+ }
13985
+ function formatLegacyProjectScopeRepairResult(result) {
13986
+ const lines = [
13987
+ "Legacy project-scope repair",
13988
+ `Mode: ${result.dryRun ? "dry-run" : "apply"}`,
13989
+ `Project: ${result.projectHash}`,
13990
+ `Scanned: ${result.scanned}`,
13991
+ `Already scoped: ${result.alreadyScoped}`,
13992
+ `Repaired: ${result.repaired}`,
13993
+ `Quarantined: ${result.quarantined}`,
13994
+ `Skipped: ${result.skipped}`
13995
+ ];
13996
+ if (result.samples.length > 0) {
13997
+ lines.push("Samples:");
13998
+ for (const sample of result.samples) {
13999
+ lines.push(`- ${sample.eventId} ${sample.action} ${sample.reason}`);
14000
+ }
14001
+ }
14002
+ if (result.dryRun) {
14003
+ lines.push("Dry-run only. Re-run with --apply to mutate event metadata.");
14004
+ }
14005
+ return lines.join("\n");
14006
+ }
14007
+
13405
14008
  // src/core/external-market-context.ts
13406
14009
  var MAX_RENDERED_ITEMS = 8;
13407
14010
  var MAX_FRED_SERIES = 10;
@@ -14012,7 +14615,7 @@ async function runMarketContextCommand(options) {
14012
14615
  }
14013
14616
  }
14014
14617
  var program = new Command();
14015
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.36");
14618
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.38");
14016
14619
  program.command("market-context").description("Fetch read-only DART/FRED/Finnhub context with structured MarketContextSnapshot bull/bear/risk/catalyst analysis").option("--company <name>", "Company name for DART fallback search and report subject").option("--dart-corp-code <code>", "Exact DART corp_code for issuer-specific filings").option("--symbol <ticker>", "Listed ticker for Finnhub company profile").option("--providers <list>", "Comma-separated providers: dart,fred,finnhub").option("--fred-series <list>", "Comma-separated FRED series IDs").option("--json", "Print structured JSON including analysis.marketSnapshot").option("--no-snapshot", "Disable MarketContextSnapshot and DART company snapshot analysis").action(async (options) => {
14017
14620
  try {
14018
14621
  await runMarketContextCommand(options);
@@ -14247,11 +14850,18 @@ program.command("forget [eventId]").description("Remove memories from storage").
14247
14850
  process.exit(1);
14248
14851
  }
14249
14852
  });
14250
- program.command("process").description("Process pending embeddings").option("-p, --project <path>", "Project path (defaults to cwd)").action(async (options) => {
14853
+ program.command("process").description("Process pending embeddings").option("-p, --project <path>", "Project path (defaults to cwd)").option("--no-recover-stuck", "Skip stale processing outbox recovery before processing").action(async (options) => {
14251
14854
  const projectPath = options.project || process.cwd();
14252
14855
  const service = getMemoryServiceForProject(projectPath);
14253
14856
  try {
14254
14857
  await service.initialize();
14858
+ if (options.recoverStuck !== false) {
14859
+ const recovered = await service.recoverStuckOutboxItems();
14860
+ const recoveredCount = recovered.embedding.recoveredProcessing + recovered.embedding.retriedFailed + recovered.vector.recoveredProcessing + recovered.vector.retriedFailed;
14861
+ if (recoveredCount > 0) {
14862
+ console.log(`\u267B\uFE0F Recovered stuck outbox work: embedding=${recovered.embedding.recoveredProcessing}/${recovered.embedding.retriedFailed}, vector=${recovered.vector.recoveredProcessing}/${recovered.vector.retriedFailed}`);
14863
+ }
14864
+ }
14255
14865
  console.log("\u23F3 Processing pending embeddings...");
14256
14866
  const count = await service.processPendingEmbeddings();
14257
14867
  console.log(`\u2705 Processed ${count} embeddings`);
@@ -14261,6 +14871,46 @@ program.command("process").description("Process pending embeddings").option("-p,
14261
14871
  process.exit(1);
14262
14872
  }
14263
14873
  });
14874
+ var repairCommand = program.command("repair").description("Repair or quarantine legacy memory metadata");
14875
+ repairCommand.command("legacy-project-scope").description("Dry-run or apply project-scope repair/quarantine for legacy imported events").option("-p, --project <path>", "Project path (defaults to cwd unless --project-hash is used)").option("--project-hash <hash>", "Project storage hash for hash-only repair flows").option("--apply", "Apply metadata changes (default is dry-run)").action(async (options) => {
14876
+ try {
14877
+ const projectPath = options.project !== void 0 ? options.project : !options.projectHash ? process.cwd() : void 0;
14878
+ const repairOptions = resolveLegacyProjectScopeRepairOptions({
14879
+ project: projectPath,
14880
+ projectHash: options.projectHash,
14881
+ apply: options.apply
14882
+ });
14883
+ const storagePath = projectPath ? getProjectStoragePath(projectPath) : resolveProjectStoragePath(repairOptions.projectHash);
14884
+ const dbPath = path21.join(storagePath, "events.sqlite");
14885
+ if (repairOptions.dryRun && !fs18.existsSync(dbPath)) {
14886
+ const projectHash = repairOptions.projectHash || hashProjectPath(repairOptions.projectPath);
14887
+ console.log(formatLegacyProjectScopeRepairResult({
14888
+ dryRun: true,
14889
+ projectHash,
14890
+ scanned: 0,
14891
+ repaired: 0,
14892
+ quarantined: 0,
14893
+ alreadyScoped: 0,
14894
+ skipped: 0,
14895
+ samples: []
14896
+ }));
14897
+ return;
14898
+ }
14899
+ const store = new SQLiteEventStore(dbPath, {
14900
+ readonly: repairOptions.dryRun
14901
+ });
14902
+ try {
14903
+ const result = await store.repairLegacyProjectScope(repairOptions);
14904
+ console.log(formatLegacyProjectScopeRepairResult(result));
14905
+ } finally {
14906
+ await store.close().catch(() => void 0);
14907
+ }
14908
+ } catch (error) {
14909
+ const message = error instanceof Error ? error.message : String(error);
14910
+ console.error(`Repair failed: ${message}`);
14911
+ process.exit(1);
14912
+ }
14913
+ });
14264
14914
  program.command("mongo-sync").description("Sync events with MongoDB for multi-server collaboration (optional)").option("-p, --project <path>", "Project path (defaults to cwd)").option("--mongo-uri <uri>", "MongoDB connection URI (env: CLAUDE_MEMORY_MONGO_URI)").option("--mongo-db <name>", "MongoDB database name (env: CLAUDE_MEMORY_MONGO_DB)").option("--mongo-project <key>", "Remote project key (env: CLAUDE_MEMORY_MONGO_PROJECT, default: basename(projectPath))").option("--direction <dir>", "push|pull|both", "both").option("--batch-size <n>", "Batch size", "500").option("--interval <ms>", "Watch interval ms", "30000").option("--watch", "Run continuously").action(async (options) => {
14265
14915
  const projectPath = options.project || process.cwd();
14266
14916
  const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;