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.
@@ -27,8 +27,8 @@ import { Hono } from "hono";
27
27
  import * as os5 from "os";
28
28
 
29
29
  // src/core/engine/memory-service-composition.ts
30
- import * as os from "os";
31
- import * as path6 from "path";
30
+ import * as os2 from "os";
31
+ import * as path7 from "path";
32
32
 
33
33
  // src/core/metadata-extractor.ts
34
34
  function createToolObservationEmbedding(toolName, metadata, success) {
@@ -1539,8 +1539,8 @@ function createEndlessMemoryServices(options) {
1539
1539
  }
1540
1540
 
1541
1541
  // src/core/engine/memory-engine-services.ts
1542
- import * as fs5 from "fs";
1543
- import * as path4 from "path";
1542
+ import * as fs6 from "fs";
1543
+ import * as path5 from "path";
1544
1544
 
1545
1545
  // src/extensions/vector/embedder.ts
1546
1546
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -2218,14 +2218,43 @@ function makeDedupeKey(content, sessionId) {
2218
2218
  return `${sessionId}:${contentHash}`;
2219
2219
  }
2220
2220
 
2221
+ // src/core/sqlite-event-store.ts
2222
+ import * as nodePath2 from "path";
2223
+
2224
+ // src/core/registry/project-path.ts
2225
+ import * as crypto2 from "crypto";
2226
+ import * as fs3 from "fs";
2227
+ import * as os from "os";
2228
+ import * as path3 from "path";
2229
+ function normalizeProjectPath(projectPath) {
2230
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
2231
+ try {
2232
+ return fs3.realpathSync(expanded);
2233
+ } catch {
2234
+ return path3.resolve(expanded);
2235
+ }
2236
+ }
2237
+ function hashProjectPath(projectPath) {
2238
+ const normalizedPath = normalizeProjectPath(projectPath);
2239
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
2240
+ }
2241
+ function getProjectStoragePath(projectPath) {
2242
+ const hash = hashProjectPath(projectPath);
2243
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
2244
+ }
2245
+ function resolveProjectStoragePath(projectOrHash) {
2246
+ const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
2247
+ return isHash ? path3.join(os.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
2248
+ }
2249
+
2221
2250
  // src/core/sqlite-wrapper.ts
2222
2251
  import Database from "better-sqlite3";
2223
- import * as fs3 from "fs";
2252
+ import * as fs4 from "fs";
2224
2253
  import * as nodePath from "path";
2225
2254
  function createSQLiteDatabase(path14, options) {
2226
2255
  const dir = nodePath.dirname(path14);
2227
- if (!fs3.existsSync(dir)) {
2228
- fs3.mkdirSync(dir, { recursive: true });
2256
+ if (!fs4.existsSync(dir)) {
2257
+ fs4.mkdirSync(dir, { recursive: true });
2229
2258
  }
2230
2259
  const db = new Database(path14, {
2231
2260
  readonly: options?.readonly ?? false
@@ -2274,8 +2303,8 @@ function toSQLiteTimestamp(date) {
2274
2303
  }
2275
2304
 
2276
2305
  // src/core/markdown-mirror.ts
2277
- import * as fs4 from "fs/promises";
2278
- import * as path3 from "path";
2306
+ import * as fs5 from "fs/promises";
2307
+ import * as path4 from "path";
2279
2308
  var DEFAULT_NAMESPACE = "default";
2280
2309
  var DEFAULT_CATEGORY = "uncategorized";
2281
2310
  function sanitizeSegment2(input, fallback) {
@@ -2304,7 +2333,7 @@ function buildMirrorPath2(rootDir, event) {
2304
2333
  const yyyy = d.getFullYear();
2305
2334
  const mm = String(d.getMonth() + 1).padStart(2, "0");
2306
2335
  const dd = String(d.getDate()).padStart(2, "0");
2307
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2336
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2308
2337
  }
2309
2338
  function formatMirrorEntry(event) {
2310
2339
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -2325,8 +2354,8 @@ var MarkdownMirror2 = class {
2325
2354
  }
2326
2355
  async append(event) {
2327
2356
  const outPath = buildMirrorPath2(this.rootDir, event);
2328
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
2329
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
2357
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
2358
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
2330
2359
  return outPath;
2331
2360
  }
2332
2361
  };
@@ -2339,6 +2368,143 @@ function normalizeQueryRewriteKind(value) {
2339
2368
  return "none";
2340
2369
  }
2341
2370
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2371
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2372
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2373
+ function emptyOutboxRecoveryResult() {
2374
+ return {
2375
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2376
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2377
+ };
2378
+ }
2379
+ function isRecord(value) {
2380
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2381
+ }
2382
+ function getNestedRecord(root, path14) {
2383
+ let cursor = root;
2384
+ for (const key of path14) {
2385
+ if (!isRecord(cursor))
2386
+ return void 0;
2387
+ cursor = cursor[key];
2388
+ }
2389
+ return isRecord(cursor) ? cursor : void 0;
2390
+ }
2391
+ function getNestedString(root, path14) {
2392
+ let cursor = root;
2393
+ for (const key of path14) {
2394
+ if (!isRecord(cursor))
2395
+ return void 0;
2396
+ cursor = cursor[key];
2397
+ }
2398
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
2399
+ }
2400
+ function metadataProjectHash(metadata) {
2401
+ return getNestedString(metadata, ["scope", "project", "hash"]);
2402
+ }
2403
+ function metadataProjectPaths(metadata) {
2404
+ const candidates = [
2405
+ getNestedString(metadata, ["projectPath"]),
2406
+ getNestedString(metadata, ["sourceProjectPath"]),
2407
+ getNestedString(metadata, ["scope", "project", "path"])
2408
+ ];
2409
+ const paths = [];
2410
+ for (const value of candidates) {
2411
+ if (value && !paths.includes(value))
2412
+ paths.push(value);
2413
+ }
2414
+ return paths;
2415
+ }
2416
+ function metadataProjectPath(metadata) {
2417
+ return metadataProjectPaths(metadata)[0];
2418
+ }
2419
+ function isActiveQuarantinedMetadata(metadata) {
2420
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
2421
+ return quarantine?.status === "active";
2422
+ }
2423
+ function activeQuarantineStatusExpression(column = "metadata") {
2424
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
2425
+ }
2426
+ function notActiveQuarantinedSql(column = "metadata") {
2427
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
2428
+ }
2429
+ function maybeQuarantinePredicate(options, column = "metadata") {
2430
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
2431
+ }
2432
+ function safeParseMetadataValue(value) {
2433
+ if (!value)
2434
+ return void 0;
2435
+ if (typeof value === "object")
2436
+ return isRecord(value) ? value : void 0;
2437
+ if (typeof value !== "string")
2438
+ return void 0;
2439
+ try {
2440
+ const parsed = JSON.parse(value);
2441
+ return isRecord(parsed) ? parsed : void 0;
2442
+ } catch {
2443
+ return void 0;
2444
+ }
2445
+ }
2446
+ function isImportedOrLegacyScopedMetadata(metadata) {
2447
+ if (!metadata)
2448
+ return false;
2449
+ return Boolean(
2450
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
2451
+ );
2452
+ }
2453
+ function addMetadataTag(metadata, tag) {
2454
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
2455
+ if (!current.includes(tag))
2456
+ metadata.tags = [...current, tag];
2457
+ }
2458
+ function buildRepairResult(projectHash, dryRun) {
2459
+ return {
2460
+ dryRun,
2461
+ projectHash,
2462
+ scanned: 0,
2463
+ repaired: 0,
2464
+ quarantined: 0,
2465
+ alreadyScoped: 0,
2466
+ skipped: 0,
2467
+ samples: []
2468
+ };
2469
+ }
2470
+ function normalizeRepoName(value) {
2471
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
2472
+ }
2473
+ function projectBasename(projectPath) {
2474
+ if (!projectPath)
2475
+ return void 0;
2476
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
2477
+ const basename3 = nodePath2.basename(trimmed);
2478
+ return basename3 ? normalizeRepoName(basename3) : void 0;
2479
+ }
2480
+ function isProjectScopeRepairExplanation(content) {
2481
+ const normalized = content.toLowerCase();
2482
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
2483
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
2484
+ return hasRepairContext && hasExplanationContext;
2485
+ }
2486
+ function hasConflictingContentProjectHint(content, projectPath) {
2487
+ const currentName = projectBasename(projectPath);
2488
+ if (!currentName)
2489
+ return false;
2490
+ if (isProjectScopeRepairExplanation(content))
2491
+ return false;
2492
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
2493
+ let githubMatch;
2494
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
2495
+ const repo = normalizeRepoName(githubMatch[2] || "");
2496
+ if (repo && repo !== currentName)
2497
+ return true;
2498
+ }
2499
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
2500
+ let workspaceMatch;
2501
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
2502
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
2503
+ if (repo && repo !== currentName)
2504
+ return true;
2505
+ }
2506
+ return false;
2507
+ }
2342
2508
  var SQLiteEventStore = class {
2343
2509
  db;
2344
2510
  initialized = false;
@@ -2837,11 +3003,11 @@ var SQLiteEventStore = class {
2837
3003
  /**
2838
3004
  * Get events by session ID
2839
3005
  */
2840
- async getSessionEvents(sessionId) {
3006
+ async getSessionEvents(sessionId, options) {
2841
3007
  await this.initialize();
2842
3008
  const rows = sqliteAll(
2843
3009
  this.db,
2844
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
3010
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2845
3011
  [sessionId]
2846
3012
  );
2847
3013
  return rows.map(this.rowToEvent);
@@ -2849,11 +3015,11 @@ var SQLiteEventStore = class {
2849
3015
  /**
2850
3016
  * Get recent events
2851
3017
  */
2852
- async getRecentEvents(limit = 100) {
3018
+ async getRecentEvents(limit = 100, options) {
2853
3019
  await this.initialize();
2854
3020
  const rows = sqliteAll(
2855
3021
  this.db,
2856
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3022
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2857
3023
  [limit]
2858
3024
  );
2859
3025
  return rows.map(this.rowToEvent);
@@ -2861,11 +3027,11 @@ var SQLiteEventStore = class {
2861
3027
  /**
2862
3028
  * Get event by ID
2863
3029
  */
2864
- async getEvent(id) {
3030
+ async getEvent(id, options) {
2865
3031
  await this.initialize();
2866
3032
  const row = sqliteGet(
2867
3033
  this.db,
2868
- `SELECT * FROM events WHERE id = ?`,
3034
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2869
3035
  [id]
2870
3036
  );
2871
3037
  if (!row)
@@ -2875,11 +3041,11 @@ var SQLiteEventStore = class {
2875
3041
  /**
2876
3042
  * Get events since a timestamp (for sync)
2877
3043
  */
2878
- async getEventsSince(timestamp, limit = 1e3) {
3044
+ async getEventsSince(timestamp, limit = 1e3, options) {
2879
3045
  await this.initialize();
2880
3046
  const rows = sqliteAll(
2881
3047
  this.db,
2882
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3048
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2883
3049
  [timestamp, limit]
2884
3050
  );
2885
3051
  return rows.map(this.rowToEvent);
@@ -2888,11 +3054,11 @@ var SQLiteEventStore = class {
2888
3054
  * Get events since a SQLite rowid (for robust incremental replication).
2889
3055
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2890
3056
  */
2891
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3057
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2892
3058
  await this.initialize();
2893
3059
  const rows = sqliteAll(
2894
3060
  this.db,
2895
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
3061
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
2896
3062
  [lastRowid, limit]
2897
3063
  );
2898
3064
  return rows.map((row) => ({
@@ -3089,7 +3255,9 @@ var SQLiteEventStore = class {
3089
3255
  const placeholders = ids.map(() => "?").join(",");
3090
3256
  sqliteRun(
3091
3257
  this.db,
3092
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3258
+ `UPDATE embedding_outbox
3259
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3260
+ WHERE id IN (${placeholders})`,
3093
3261
  ids
3094
3262
  );
3095
3263
  return pending.map((row) => ({
@@ -3125,19 +3293,19 @@ var SQLiteEventStore = class {
3125
3293
  /**
3126
3294
  * Count total events
3127
3295
  */
3128
- async countEvents() {
3296
+ async countEvents(options) {
3129
3297
  await this.initialize();
3130
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
3298
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
3131
3299
  return row?.count || 0;
3132
3300
  }
3133
3301
  /**
3134
3302
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3135
3303
  */
3136
- async getEventsPage(limit = 1e3, offset = 0) {
3304
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3137
3305
  await this.initialize();
3138
3306
  const rows = sqliteAll(
3139
3307
  this.db,
3140
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3308
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3141
3309
  [limit, offset]
3142
3310
  );
3143
3311
  return rows.map(this.rowToEvent);
@@ -3159,6 +3327,197 @@ var SQLiteEventStore = class {
3159
3327
  [error, ...ids]
3160
3328
  );
3161
3329
  }
3330
+ /**
3331
+ * Recover abandoned outbox work after a worker/process crash.
3332
+ *
3333
+ * Rows in `processing` are claimed work. If the process exits before marking
3334
+ * them done/failed, they otherwise remain invisible to future processing.
3335
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3336
+ */
3337
+ async recoverStuckOutboxItems(options = {}) {
3338
+ await this.initialize();
3339
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3340
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3341
+ const now = options.now ?? /* @__PURE__ */ new Date();
3342
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3343
+ const result = emptyOutboxRecoveryResult();
3344
+ const embeddingRecovered = sqliteRun(
3345
+ this.db,
3346
+ `UPDATE embedding_outbox
3347
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3348
+ WHERE status = 'processing'
3349
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3350
+ [threshold]
3351
+ );
3352
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3353
+ const embeddingRetried = sqliteRun(
3354
+ this.db,
3355
+ `UPDATE embedding_outbox
3356
+ SET status = 'pending', error_message = NULL
3357
+ WHERE status = 'failed'
3358
+ AND retry_count < ?`,
3359
+ [maxRetries]
3360
+ );
3361
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3362
+ const vectorRecovered = sqliteRun(
3363
+ this.db,
3364
+ `UPDATE vector_outbox
3365
+ SET status = 'pending', updated_at = ?, error = NULL
3366
+ WHERE status = 'processing'
3367
+ AND datetime(updated_at) < datetime(?)`,
3368
+ [now.toISOString(), threshold]
3369
+ );
3370
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3371
+ const vectorRetried = sqliteRun(
3372
+ this.db,
3373
+ `UPDATE vector_outbox
3374
+ SET status = 'pending', updated_at = ?, error = NULL
3375
+ WHERE status = 'failed'
3376
+ AND retry_count < ?`,
3377
+ [now.toISOString(), maxRetries]
3378
+ );
3379
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3380
+ return result;
3381
+ }
3382
+ /**
3383
+ * Repair legacy imported events that predate canonical project scope metadata.
3384
+ *
3385
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
3386
+ * imported but cannot be proven to belong to this project are quarantined so
3387
+ * dashboard default reads/search do not surface cross-project contamination.
3388
+ */
3389
+ async repairLegacyProjectScope(options = {}) {
3390
+ await this.initialize();
3391
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
3392
+ if (!projectHash) {
3393
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
3394
+ }
3395
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
3396
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
3397
+ }
3398
+ const dryRun = options.dryRun === true;
3399
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
3400
+ const result = buildRepairResult(projectHash, dryRun);
3401
+ const rows = sqliteAll(
3402
+ this.db,
3403
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
3404
+ FROM events e
3405
+ LEFT JOIN sessions s ON s.id = e.session_id
3406
+ ORDER BY e.timestamp ASC`,
3407
+ []
3408
+ );
3409
+ const sample = (entry) => {
3410
+ if (result.samples.length < 20)
3411
+ result.samples.push(entry);
3412
+ };
3413
+ for (const row of rows) {
3414
+ result.scanned++;
3415
+ let metadata = {};
3416
+ let metadataParseInvalid = false;
3417
+ if (row.metadata) {
3418
+ const parsed = safeParseMetadataValue(row.metadata);
3419
+ if (parsed) {
3420
+ metadata = parsed;
3421
+ } else {
3422
+ metadataParseInvalid = true;
3423
+ }
3424
+ }
3425
+ if (isActiveQuarantinedMetadata(metadata)) {
3426
+ result.skipped++;
3427
+ continue;
3428
+ }
3429
+ const currentHash = metadataProjectHash(metadata);
3430
+ const explicitPath = metadataProjectPath(metadata);
3431
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
3432
+ const candidatePaths = metadataProjectPaths(metadata);
3433
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
3434
+ candidatePaths.push(sessionProjectPath);
3435
+ }
3436
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
3437
+ const pathHashes = candidatePaths.map((candidate) => {
3438
+ try {
3439
+ return { path: candidate, hash: hashProjectPath(candidate) };
3440
+ } catch {
3441
+ return { path: candidate, hash: void 0 };
3442
+ }
3443
+ });
3444
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
3445
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
3446
+ let action = "skipped";
3447
+ let reason;
3448
+ let observedProjectHash;
3449
+ if (foreignPath) {
3450
+ action = "quarantined";
3451
+ reason = "project-path-mismatch";
3452
+ observedProjectHash = foreignPath.hash;
3453
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
3454
+ action = "quarantined";
3455
+ reason = "content-project-mismatch";
3456
+ } else if (currentHash === projectHash) {
3457
+ result.alreadyScoped++;
3458
+ continue;
3459
+ } else if (currentHash && currentHash !== projectHash) {
3460
+ action = "quarantined";
3461
+ reason = "scope-hash-mismatch";
3462
+ observedProjectHash = currentHash;
3463
+ } else if (matchingPath) {
3464
+ action = "repaired";
3465
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
3466
+ } else if (candidatePaths.length > 0) {
3467
+ action = "quarantined";
3468
+ reason = "project-path-mismatch";
3469
+ } else if (importedOrLegacy) {
3470
+ action = "quarantined";
3471
+ reason = "missing-project-scope";
3472
+ }
3473
+ if (action === "skipped" || !reason) {
3474
+ result.skipped++;
3475
+ continue;
3476
+ }
3477
+ if (action === "repaired") {
3478
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
3479
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
3480
+ project.hash = projectHash;
3481
+ scope.project = project;
3482
+ metadata.scope = scope;
3483
+ metadata.repair = {
3484
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3485
+ legacyProjectScope: {
3486
+ action,
3487
+ reason,
3488
+ repairedAt: nowIso
3489
+ }
3490
+ };
3491
+ addMetadataTag(metadata, `proj:${projectHash}`);
3492
+ result.repaired++;
3493
+ } else {
3494
+ metadata.quarantine = {
3495
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
3496
+ status: "active",
3497
+ category: "project-scope",
3498
+ reason,
3499
+ detectedAt: nowIso,
3500
+ expectedProjectHash: projectHash,
3501
+ ...observedProjectHash ? { observedProjectHash } : {}
3502
+ };
3503
+ metadata.repair = {
3504
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3505
+ legacyProjectScope: {
3506
+ action,
3507
+ reason,
3508
+ repairedAt: nowIso
3509
+ }
3510
+ };
3511
+ addMetadataTag(metadata, "quarantine:project-scope");
3512
+ result.quarantined++;
3513
+ }
3514
+ sample({ eventId: row.id, action, reason });
3515
+ if (!dryRun) {
3516
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
3517
+ }
3518
+ }
3519
+ return result;
3520
+ }
3162
3521
  /**
3163
3522
  * Get embedding/vector outbox health statistics
3164
3523
  */
@@ -3206,7 +3565,11 @@ var SQLiteEventStore = class {
3206
3565
  await this.initialize();
3207
3566
  const rows = sqliteAll(
3208
3567
  this.db,
3209
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
3568
+ `SELECT ml.level, COUNT(*) as count
3569
+ FROM memory_levels ml
3570
+ INNER JOIN events e ON e.id = ml.event_id
3571
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
3572
+ GROUP BY ml.level`
3210
3573
  );
3211
3574
  return rows;
3212
3575
  }
@@ -3222,6 +3585,7 @@ var SQLiteEventStore = class {
3222
3585
  `SELECT e.* FROM events e
3223
3586
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3224
3587
  WHERE ml.level = ?
3588
+ AND ${notActiveQuarantinedSql("e.metadata")}
3225
3589
  ORDER BY e.timestamp DESC
3226
3590
  LIMIT ? OFFSET ?`,
3227
3591
  [level, limit, offset]
@@ -3314,12 +3678,13 @@ var SQLiteEventStore = class {
3314
3678
  /**
3315
3679
  * Get most accessed memories (falls back to recent events if none accessed)
3316
3680
  */
3317
- async getMostAccessed(limit = 10) {
3681
+ async getMostAccessed(limit = 10, options) {
3318
3682
  await this.initialize();
3319
3683
  let rows = sqliteAll(
3320
3684
  this.db,
3321
3685
  `SELECT * FROM events
3322
3686
  WHERE access_count > 0
3687
+ AND ${maybeQuarantinePredicate(options)}
3323
3688
  ORDER BY access_count DESC, last_accessed_at DESC
3324
3689
  LIMIT ?`,
3325
3690
  [limit]
@@ -3328,6 +3693,7 @@ var SQLiteEventStore = class {
3328
3693
  rows = sqliteAll(
3329
3694
  this.db,
3330
3695
  `SELECT * FROM events
3696
+ WHERE ${maybeQuarantinePredicate(options)}
3331
3697
  ORDER BY timestamp DESC
3332
3698
  LIMIT ?`,
3333
3699
  [limit]
@@ -3458,6 +3824,7 @@ var SQLiteEventStore = class {
3458
3824
  FROM memory_helpfulness mh
3459
3825
  JOIN events e ON e.id = mh.event_id
3460
3826
  WHERE mh.measured_at IS NOT NULL
3827
+ AND ${notActiveQuarantinedSql("e.metadata")}
3461
3828
  GROUP BY mh.event_id
3462
3829
  ORDER BY avg_score DESC
3463
3830
  LIMIT ?`,
@@ -3522,6 +3889,7 @@ var SQLiteEventStore = class {
3522
3889
  FROM events_fts fts
3523
3890
  JOIN events e ON e.id = fts.event_id
3524
3891
  WHERE events_fts MATCH ?
3892
+ AND ${notActiveQuarantinedSql("e.metadata")}
3525
3893
  ORDER BY fts.rank
3526
3894
  LIMIT ?`,
3527
3895
  [searchTerms, limit]
@@ -3536,6 +3904,7 @@ var SQLiteEventStore = class {
3536
3904
  this.db,
3537
3905
  `SELECT *, 0 as rank FROM events
3538
3906
  WHERE content LIKE ?
3907
+ AND ${notActiveQuarantinedSql()}
3539
3908
  ORDER BY timestamp DESC
3540
3909
  LIMIT ?`,
3541
3910
  [likePattern, limit]
@@ -3742,6 +4111,7 @@ var SQLiteEventStore = class {
3742
4111
  `SELECT turn_id, MIN(timestamp) as min_ts
3743
4112
  FROM events
3744
4113
  WHERE session_id = ? AND turn_id IS NOT NULL
4114
+ AND ${maybeQuarantinePredicate(options)}
3745
4115
  GROUP BY turn_id
3746
4116
  ORDER BY min_ts DESC
3747
4117
  LIMIT ? OFFSET ?`,
@@ -3749,7 +4119,7 @@ var SQLiteEventStore = class {
3749
4119
  );
3750
4120
  const turns = [];
3751
4121
  for (const turnRow of turnRows) {
3752
- const events = await this.getEventsByTurn(turnRow.turn_id);
4122
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3753
4123
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3754
4124
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3755
4125
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3768,11 +4138,11 @@ var SQLiteEventStore = class {
3768
4138
  /**
3769
4139
  * Get all events for a specific turn_id
3770
4140
  */
3771
- async getEventsByTurn(turnId) {
4141
+ async getEventsByTurn(turnId, options) {
3772
4142
  await this.initialize();
3773
4143
  const rows = sqliteAll(
3774
4144
  this.db,
3775
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4145
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3776
4146
  [turnId]
3777
4147
  );
3778
4148
  return rows.map(this.rowToEvent);
@@ -3780,13 +4150,14 @@ var SQLiteEventStore = class {
3780
4150
  /**
3781
4151
  * Count total turns for a session
3782
4152
  */
3783
- async countSessionTurns(sessionId) {
4153
+ async countSessionTurns(sessionId, options) {
3784
4154
  await this.initialize();
3785
4155
  const row = sqliteGet(
3786
4156
  this.db,
3787
4157
  `SELECT COUNT(DISTINCT turn_id) as count
3788
4158
  FROM events
3789
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4159
+ WHERE session_id = ? AND turn_id IS NOT NULL
4160
+ AND ${maybeQuarantinePredicate(options)}`,
3790
4161
  [sessionId]
3791
4162
  );
3792
4163
  return row?.count || 0;
@@ -3878,7 +4249,7 @@ var SQLiteEventStore = class {
3878
4249
  content: row.content,
3879
4250
  canonicalKey: row.canonical_key,
3880
4251
  dedupeKey: row.dedupe_key,
3881
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4252
+ metadata: safeParseMetadataValue(row.metadata)
3882
4253
  };
3883
4254
  if (row.access_count !== void 0) {
3884
4255
  event.access_count = row.access_count;
@@ -4030,6 +4401,7 @@ var VectorStore = class {
4030
4401
  * Get total count of vectors
4031
4402
  */
4032
4403
  async count() {
4404
+ await this.initialize();
4033
4405
  if (!this.table)
4034
4406
  return 0;
4035
4407
  const result = await this.table.countRows();
@@ -4468,6 +4840,14 @@ var MemoryQueryService = class {
4468
4840
  await this.initialize();
4469
4841
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4470
4842
  }
4843
+ async recoverStuckOutboxItems(options) {
4844
+ await this.initialize();
4845
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4846
+ }
4847
+ async repairLegacyProjectScope(options) {
4848
+ await this.initialize();
4849
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4850
+ }
4471
4851
  async getStats() {
4472
4852
  await this.initialize();
4473
4853
  const deps = this.getStatsDeps();
@@ -6090,18 +6470,18 @@ function assertDefaultRetrieverStore(eventStore) {
6090
6470
  function createMemoryEngineServices(options) {
6091
6471
  const factories = options.factories ?? {};
6092
6472
  const storagePath = options.storagePath;
6093
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6094
- fs5.mkdirSync(storagePath, { recursive: true });
6473
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6474
+ fs6.mkdirSync(storagePath, { recursive: true });
6095
6475
  }
6096
6476
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6097
- path4.join(storagePath, "events.sqlite"),
6477
+ path5.join(storagePath, "events.sqlite"),
6098
6478
  {
6099
6479
  readonly: options.readOnly,
6100
6480
  markdownMirrorRoot: storagePath
6101
6481
  }
6102
6482
  );
6103
6483
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6104
- path4.join(storagePath, "vectors")
6484
+ path5.join(storagePath, "vectors")
6105
6485
  );
6106
6486
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6107
6487
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6512,8 +6892,8 @@ function createMemoryRuntimeService(deps) {
6512
6892
  }
6513
6893
 
6514
6894
  // src/extensions/shared-memory/shared-memory-services.ts
6515
- import * as fs6 from "fs";
6516
- import * as path5 from "path";
6895
+ import * as fs7 from "fs";
6896
+ import * as path6 from "path";
6517
6897
 
6518
6898
  // src/core/shared-event-store.ts
6519
6899
  var SharedEventStore = class {
@@ -7201,7 +7581,7 @@ var SharedMemoryServices = class {
7201
7581
  this.ensureDirectory(sharedPath, { allowCreate: true });
7202
7582
  const store = await this.openStore(sharedPath);
7203
7583
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7204
- path5.join(sharedPath, "vectors")
7584
+ path6.join(sharedPath, "vectors")
7205
7585
  );
7206
7586
  await this.sharedVectorStore.initialize();
7207
7587
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7274,7 +7654,7 @@ var SharedMemoryServices = class {
7274
7654
  async createOpenStorePromise(sharedPath) {
7275
7655
  if (!this.sharedEventStore) {
7276
7656
  const sharedEventStore = this.factories.createSharedEventStore(
7277
- path5.join(sharedPath, "shared.duckdb")
7657
+ path6.join(sharedPath, "shared.duckdb")
7278
7658
  );
7279
7659
  await sharedEventStore.initialize();
7280
7660
  this.sharedEventStore = sharedEventStore;
@@ -7294,9 +7674,9 @@ var SharedMemoryServices = class {
7294
7674
  }
7295
7675
  get factories() {
7296
7676
  return {
7297
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7677
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7298
7678
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7299
- fs6.mkdirSync(targetPath, { recursive: true });
7679
+ fs7.mkdirSync(targetPath, { recursive: true });
7300
7680
  }),
7301
7681
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7302
7682
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7411,37 +7791,11 @@ function createMemoryServiceComposition(options) {
7411
7791
  }
7412
7792
  function defaultExpandPath(targetPath) {
7413
7793
  if (targetPath.startsWith("~")) {
7414
- return path6.join(os.homedir(), targetPath.slice(1));
7794
+ return path7.join(os2.homedir(), targetPath.slice(1));
7415
7795
  }
7416
7796
  return targetPath;
7417
7797
  }
7418
7798
 
7419
- // src/core/registry/project-path.ts
7420
- import * as crypto2 from "crypto";
7421
- import * as fs7 from "fs";
7422
- import * as os2 from "os";
7423
- import * as path7 from "path";
7424
- function normalizeProjectPath(projectPath) {
7425
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7426
- try {
7427
- return fs7.realpathSync(expanded);
7428
- } catch {
7429
- return path7.resolve(expanded);
7430
- }
7431
- }
7432
- function hashProjectPath(projectPath) {
7433
- const normalizedPath = normalizeProjectPath(projectPath);
7434
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7435
- }
7436
- function getProjectStoragePath(projectPath) {
7437
- const hash = hashProjectPath(projectPath);
7438
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7439
- }
7440
- function resolveProjectStoragePath(projectOrHash) {
7441
- const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
7442
- return isHash ? path7.join(os2.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
7443
- }
7444
-
7445
7799
  // src/core/registry/session-registry.ts
7446
7800
  import * as fs8 from "fs";
7447
7801
  import * as os3 from "os";
@@ -7740,6 +8094,12 @@ var MemoryService = class {
7740
8094
  async getOutboxStats() {
7741
8095
  return this.queryService.getOutboxStats();
7742
8096
  }
8097
+ async recoverStuckOutboxItems(options) {
8098
+ return this.queryService.recoverStuckOutboxItems(options);
8099
+ }
8100
+ async repairLegacyProjectScope(options) {
8101
+ return this.queryService.repairLegacyProjectScope(options);
8102
+ }
7743
8103
  async getRetrievalTraceStats() {
7744
8104
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7745
8105
  }
@@ -8053,6 +8413,26 @@ function getServiceFromQuery(c) {
8053
8413
  }
8054
8414
  return getReadOnlyMemoryService();
8055
8415
  }
8416
+ function getWritableServiceFromQuery(c) {
8417
+ const project = c.req.query("project") || c.req.query("projectId");
8418
+ if (project) {
8419
+ const storagePath = resolveProjectStoragePath(project);
8420
+ return new MemoryService({
8421
+ storagePath,
8422
+ readOnly: false,
8423
+ lightweightMode: true,
8424
+ analyticsEnabled: false,
8425
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
8426
+ });
8427
+ }
8428
+ return new MemoryService({
8429
+ storagePath: "~/.claude-code/memory",
8430
+ readOnly: false,
8431
+ lightweightMode: true,
8432
+ analyticsEnabled: false,
8433
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
8434
+ });
8435
+ }
8056
8436
  function getLightweightServiceFromQuery(c) {
8057
8437
  const project = c.req.query("project") || c.req.query("projectId");
8058
8438
  if (project) {
@@ -9708,6 +10088,13 @@ import { Hono as Hono8 } from "hono";
9708
10088
  import { streamSSE } from "hono/streaming";
9709
10089
  import { spawn } from "child_process";
9710
10090
  var chatRouter = new Hono8();
10091
+ var ProviderFailure = class extends Error {
10092
+ constructor(code, message) {
10093
+ super(message);
10094
+ this.code = code;
10095
+ this.name = "ProviderFailure";
10096
+ }
10097
+ };
9711
10098
  var CLAUDE_TIMEOUT_MS = 12e4;
9712
10099
  chatRouter.post("/", async (c) => {
9713
10100
  let body;
@@ -9719,43 +10106,12 @@ chatRouter.post("/", async (c) => {
9719
10106
  if (!body.message?.trim()) {
9720
10107
  return c.json({ error: "Message is required" }, 400);
9721
10108
  }
9722
- const memoryService = getServiceFromQuery(c);
10109
+ const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
10110
+ const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
9723
10111
  try {
9724
10112
  await memoryService.initialize();
9725
- let memoryContext = "";
9726
- let statsContext = "";
9727
- try {
9728
- const result = await memoryService.retrieveMemories(body.message, {
9729
- topK: 8,
9730
- minScore: 0.5
9731
- });
9732
- if (result.memories.length > 0) {
9733
- const parts = ["## Relevant Memories\n"];
9734
- for (const m of result.memories) {
9735
- const date = new Date(m.event.timestamp).toISOString().split("T")[0];
9736
- const content = m.event.content.slice(0, 500);
9737
- parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
9738
- parts.push(content);
9739
- if (m.sessionContext) {
9740
- parts.push(`_Context: ${m.sessionContext}_`);
9741
- }
9742
- parts.push("");
9743
- }
9744
- memoryContext = parts.join("\n");
9745
- }
9746
- } catch {
9747
- }
9748
- try {
9749
- const stats = await memoryService.getStats();
9750
- const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
9751
- statsContext = [
9752
- "## Memory Stats",
9753
- `- Total events: ${stats.totalEvents}`,
9754
- `- Vector nodes: ${stats.vectorCount}`,
9755
- `- By level: ${levels}`
9756
- ].join("\n");
9757
- } catch {
9758
- }
10113
+ const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
10114
+ const statsContext = await collectStatsContext(memoryService);
9759
10115
  const fullPrompt = buildPrompt(
9760
10116
  statsContext,
9761
10117
  memoryContext,
@@ -9763,13 +10119,23 @@ chatRouter.post("/", async (c) => {
9763
10119
  body.message
9764
10120
  );
9765
10121
  return streamSSE(c, async (stream) => {
10122
+ if (memoryOnly) {
10123
+ await streamMemoryOnlyResponse(stream, {
10124
+ memoryContext,
10125
+ memoryHits,
10126
+ reason: "memory-only-mode"
10127
+ });
10128
+ return;
10129
+ }
9766
10130
  try {
9767
10131
  await streamClaudeResponse(fullPrompt, stream);
9768
10132
  } catch (err) {
10133
+ const diagnostic = providerDiagnostic(err);
9769
10134
  await stream.writeSSE({
9770
- event: "error",
9771
- data: JSON.stringify({ error: err.message })
10135
+ event: "provider_error",
10136
+ data: JSON.stringify(diagnostic)
9772
10137
  });
10138
+ await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
9773
10139
  }
9774
10140
  });
9775
10141
  } catch (error) {
@@ -9778,6 +10144,115 @@ chatRouter.post("/", async (c) => {
9778
10144
  await memoryService.shutdown();
9779
10145
  }
9780
10146
  });
10147
+ async function collectMemoryContext(memoryService, query) {
10148
+ let memoryHits = [];
10149
+ try {
10150
+ const result = await memoryService.retrieveMemories?.(query, {
10151
+ topK: 8,
10152
+ minScore: 0.5
10153
+ });
10154
+ memoryHits = result?.memories ?? [];
10155
+ } catch {
10156
+ memoryHits = [];
10157
+ }
10158
+ if (memoryHits.length === 0) {
10159
+ try {
10160
+ memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
10161
+ } catch {
10162
+ memoryHits = [];
10163
+ }
10164
+ }
10165
+ return {
10166
+ memoryContext: formatMemoryContext(memoryHits),
10167
+ memoryHits
10168
+ };
10169
+ }
10170
+ async function collectStatsContext(memoryService) {
10171
+ try {
10172
+ const stats = await memoryService.getStats?.();
10173
+ if (!stats)
10174
+ return "";
10175
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
10176
+ return [
10177
+ "## Memory Stats",
10178
+ `- Total events: ${stats.totalEvents}`,
10179
+ `- Vector nodes: ${stats.vectorCount}`,
10180
+ `- By level: ${levels}`
10181
+ ].join("\n");
10182
+ } catch {
10183
+ return "";
10184
+ }
10185
+ }
10186
+ function formatMemoryContext(memoryHits) {
10187
+ if (memoryHits.length === 0)
10188
+ return "";
10189
+ const parts = ["## Relevant Memories\n"];
10190
+ for (const m of memoryHits) {
10191
+ const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
10192
+ const content = (m.event.content ?? "").slice(0, 500);
10193
+ parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
10194
+ parts.push(content);
10195
+ if (m.sessionContext) {
10196
+ parts.push(`_Context: ${m.sessionContext}_`);
10197
+ }
10198
+ parts.push("");
10199
+ }
10200
+ return parts.join("\n");
10201
+ }
10202
+ async function streamMemoryOnlyResponse(stream, options) {
10203
+ await stream.writeSSE({
10204
+ event: "diagnostic",
10205
+ data: JSON.stringify({
10206
+ provider: "claude-cli",
10207
+ status: "skipped",
10208
+ mode: "memory-only",
10209
+ reason: options.reason,
10210
+ retrievedMemories: options.memoryHits.length
10211
+ })
10212
+ });
10213
+ await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
10214
+ }
10215
+ async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
10216
+ const content = memoryHits.length > 0 ? [
10217
+ "Provider unavailable or skipped; showing retrieved memory context directly.",
10218
+ "",
10219
+ memoryContext
10220
+ ].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
10221
+ await stream.writeSSE({
10222
+ event: "message",
10223
+ data: JSON.stringify({ content, mode: "memory-only" })
10224
+ });
10225
+ await stream.writeSSE({ event: "done", data: "{}" });
10226
+ }
10227
+ function providerDiagnostic(err) {
10228
+ if (err instanceof ProviderFailure) {
10229
+ return {
10230
+ provider: "claude-cli",
10231
+ code: err.code,
10232
+ message: err.message,
10233
+ fallback: "memory-only"
10234
+ };
10235
+ }
10236
+ return {
10237
+ provider: "claude-cli",
10238
+ code: "claude-cli-error",
10239
+ message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
10240
+ fallback: "memory-only"
10241
+ };
10242
+ }
10243
+ function classifyProviderFailure(message) {
10244
+ const normalized = message.toLowerCase();
10245
+ if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
10246
+ return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
10247
+ }
10248
+ if (normalized.includes("not found") || normalized.includes("enoent")) {
10249
+ return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
10250
+ }
10251
+ if (normalized.includes("timed out")) {
10252
+ return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
10253
+ }
10254
+ return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
10255
+ }
9781
10256
  function buildPrompt(statsContext, memoryContext, history, currentMessage) {
9782
10257
  const parts = [];
9783
10258
  parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
@@ -9820,12 +10295,13 @@ function streamClaudeResponse(prompt, stream) {
9820
10295
  });
9821
10296
  const timeout = setTimeout(() => {
9822
10297
  proc.kill("SIGTERM");
9823
- reject(new Error("Chat response timed out after 2 minutes"));
10298
+ reject(classifyProviderFailure("timed out"));
9824
10299
  }, CLAUDE_TIMEOUT_MS);
9825
10300
  proc.stdin.write(prompt);
9826
10301
  proc.stdin.end();
9827
10302
  let buffer = "";
9828
10303
  let lastSentText = "";
10304
+ let stderrText = "";
9829
10305
  proc.stdout.on("data", async (chunk) => {
9830
10306
  buffer += chunk.toString();
9831
10307
  const lines = buffer.split("\n");
@@ -9854,6 +10330,7 @@ function streamClaudeResponse(prompt, stream) {
9854
10330
  }
9855
10331
  });
9856
10332
  proc.stderr.on("data", (chunk) => {
10333
+ stderrText += chunk.toString();
9857
10334
  if (process.env.CLAUDE_MEMORY_DEBUG) {
9858
10335
  console.error("[chat] claude stderr:", chunk.toString());
9859
10336
  }
@@ -9861,7 +10338,7 @@ function streamClaudeResponse(prompt, stream) {
9861
10338
  proc.on("error", (err) => {
9862
10339
  clearTimeout(timeout);
9863
10340
  if (err.code === "ENOENT") {
9864
- reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
10341
+ reject(classifyProviderFailure("ENOENT not found"));
9865
10342
  } else {
9866
10343
  reject(err);
9867
10344
  }
@@ -9878,7 +10355,7 @@ function streamClaudeResponse(prompt, stream) {
9878
10355
  }
9879
10356
  }
9880
10357
  if (code !== 0 && code !== null) {
9881
- reject(new Error(`Claude CLI exited with code ${code}`));
10358
+ reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
9882
10359
  } else {
9883
10360
  resolve4();
9884
10361
  }
@@ -9927,6 +10404,49 @@ healthRouter.get("/", async (c) => {
9927
10404
  await memoryService.shutdown();
9928
10405
  }
9929
10406
  });
10407
+ healthRouter.post("/recover", async (c) => {
10408
+ const memoryService = getWritableServiceFromQuery(c);
10409
+ try {
10410
+ await memoryService.initialize();
10411
+ const body = await c.req.json().catch(() => ({}));
10412
+ const options = {};
10413
+ if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
10414
+ options.stuckThresholdMs = body.stuckThresholdMs;
10415
+ }
10416
+ if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
10417
+ options.maxRetries = body.maxRetries;
10418
+ }
10419
+ const before = await memoryService.getOutboxStats();
10420
+ const recovered = await memoryService.recoverStuckOutboxItems(options);
10421
+ const [stats, outbox] = await Promise.all([
10422
+ memoryService.getStats(),
10423
+ memoryService.getOutboxStats()
10424
+ ]);
10425
+ return c.json({
10426
+ status: "ok",
10427
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10428
+ recovered,
10429
+ before: {
10430
+ outbox: before
10431
+ },
10432
+ after: {
10433
+ storage: {
10434
+ totalEvents: stats.totalEvents,
10435
+ vectorCount: stats.vectorCount
10436
+ },
10437
+ outbox
10438
+ }
10439
+ });
10440
+ } catch (error) {
10441
+ return c.json({
10442
+ status: "error",
10443
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10444
+ error: error.message
10445
+ }, 500);
10446
+ } finally {
10447
+ await memoryService.shutdown();
10448
+ }
10449
+ });
9930
10450
 
9931
10451
  // src/apps/server/api/index.ts
9932
10452
  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);