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.
@@ -15,8 +15,8 @@ import { Hono } from "hono";
15
15
  import * as os5 from "os";
16
16
 
17
17
  // src/core/engine/memory-service-composition.ts
18
- import * as os from "os";
19
- import * as path6 from "path";
18
+ import * as os2 from "os";
19
+ import * as path7 from "path";
20
20
 
21
21
  // src/core/metadata-extractor.ts
22
22
  function createToolObservationEmbedding(toolName, metadata, success) {
@@ -1527,8 +1527,8 @@ function createEndlessMemoryServices(options) {
1527
1527
  }
1528
1528
 
1529
1529
  // src/core/engine/memory-engine-services.ts
1530
- import * as fs5 from "fs";
1531
- import * as path4 from "path";
1530
+ import * as fs6 from "fs";
1531
+ import * as path5 from "path";
1532
1532
 
1533
1533
  // src/extensions/vector/embedder.ts
1534
1534
  var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
@@ -2206,14 +2206,43 @@ function makeDedupeKey(content, sessionId) {
2206
2206
  return `${sessionId}:${contentHash}`;
2207
2207
  }
2208
2208
 
2209
+ // src/core/sqlite-event-store.ts
2210
+ import * as nodePath2 from "path";
2211
+
2212
+ // src/core/registry/project-path.ts
2213
+ import * as crypto2 from "crypto";
2214
+ import * as fs3 from "fs";
2215
+ import * as os from "os";
2216
+ import * as path3 from "path";
2217
+ function normalizeProjectPath(projectPath) {
2218
+ const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
2219
+ try {
2220
+ return fs3.realpathSync(expanded);
2221
+ } catch {
2222
+ return path3.resolve(expanded);
2223
+ }
2224
+ }
2225
+ function hashProjectPath(projectPath) {
2226
+ const normalizedPath = normalizeProjectPath(projectPath);
2227
+ return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
2228
+ }
2229
+ function getProjectStoragePath(projectPath) {
2230
+ const hash = hashProjectPath(projectPath);
2231
+ return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
2232
+ }
2233
+ function resolveProjectStoragePath(projectOrHash) {
2234
+ const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
2235
+ return isHash ? path3.join(os.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
2236
+ }
2237
+
2209
2238
  // src/core/sqlite-wrapper.ts
2210
2239
  import Database from "better-sqlite3";
2211
- import * as fs3 from "fs";
2240
+ import * as fs4 from "fs";
2212
2241
  import * as nodePath from "path";
2213
2242
  function createSQLiteDatabase(path13, options) {
2214
2243
  const dir = nodePath.dirname(path13);
2215
- if (!fs3.existsSync(dir)) {
2216
- fs3.mkdirSync(dir, { recursive: true });
2244
+ if (!fs4.existsSync(dir)) {
2245
+ fs4.mkdirSync(dir, { recursive: true });
2217
2246
  }
2218
2247
  const db = new Database(path13, {
2219
2248
  readonly: options?.readonly ?? false
@@ -2262,8 +2291,8 @@ function toSQLiteTimestamp(date) {
2262
2291
  }
2263
2292
 
2264
2293
  // src/core/markdown-mirror.ts
2265
- import * as fs4 from "fs/promises";
2266
- import * as path3 from "path";
2294
+ import * as fs5 from "fs/promises";
2295
+ import * as path4 from "path";
2267
2296
  var DEFAULT_NAMESPACE = "default";
2268
2297
  var DEFAULT_CATEGORY = "uncategorized";
2269
2298
  function sanitizeSegment2(input, fallback) {
@@ -2292,7 +2321,7 @@ function buildMirrorPath2(rootDir, event) {
2292
2321
  const yyyy = d.getFullYear();
2293
2322
  const mm = String(d.getMonth() + 1).padStart(2, "0");
2294
2323
  const dd = String(d.getDate()).padStart(2, "0");
2295
- return path3.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2324
+ return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
2296
2325
  }
2297
2326
  function formatMirrorEntry(event) {
2298
2327
  const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
@@ -2313,8 +2342,8 @@ var MarkdownMirror2 = class {
2313
2342
  }
2314
2343
  async append(event) {
2315
2344
  const outPath = buildMirrorPath2(this.rootDir, event);
2316
- await fs4.mkdir(path3.dirname(outPath), { recursive: true });
2317
- await fs4.appendFile(outPath, formatMirrorEntry(event), "utf8");
2345
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
2346
+ await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
2318
2347
  return outPath;
2319
2348
  }
2320
2349
  };
@@ -2327,6 +2356,143 @@ function normalizeQueryRewriteKind(value) {
2327
2356
  return "none";
2328
2357
  }
2329
2358
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2359
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2360
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2361
+ function emptyOutboxRecoveryResult() {
2362
+ return {
2363
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2364
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2365
+ };
2366
+ }
2367
+ function isRecord(value) {
2368
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2369
+ }
2370
+ function getNestedRecord(root, path13) {
2371
+ let cursor = root;
2372
+ for (const key of path13) {
2373
+ if (!isRecord(cursor))
2374
+ return void 0;
2375
+ cursor = cursor[key];
2376
+ }
2377
+ return isRecord(cursor) ? cursor : void 0;
2378
+ }
2379
+ function getNestedString(root, path13) {
2380
+ let cursor = root;
2381
+ for (const key of path13) {
2382
+ if (!isRecord(cursor))
2383
+ return void 0;
2384
+ cursor = cursor[key];
2385
+ }
2386
+ return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
2387
+ }
2388
+ function metadataProjectHash(metadata) {
2389
+ return getNestedString(metadata, ["scope", "project", "hash"]);
2390
+ }
2391
+ function metadataProjectPaths(metadata) {
2392
+ const candidates = [
2393
+ getNestedString(metadata, ["projectPath"]),
2394
+ getNestedString(metadata, ["sourceProjectPath"]),
2395
+ getNestedString(metadata, ["scope", "project", "path"])
2396
+ ];
2397
+ const paths = [];
2398
+ for (const value of candidates) {
2399
+ if (value && !paths.includes(value))
2400
+ paths.push(value);
2401
+ }
2402
+ return paths;
2403
+ }
2404
+ function metadataProjectPath(metadata) {
2405
+ return metadataProjectPaths(metadata)[0];
2406
+ }
2407
+ function isActiveQuarantinedMetadata(metadata) {
2408
+ const quarantine = getNestedRecord(metadata, ["quarantine"]);
2409
+ return quarantine?.status === "active";
2410
+ }
2411
+ function activeQuarantineStatusExpression(column = "metadata") {
2412
+ return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
2413
+ }
2414
+ function notActiveQuarantinedSql(column = "metadata") {
2415
+ return `${activeQuarantineStatusExpression(column)} != 'active'`;
2416
+ }
2417
+ function maybeQuarantinePredicate(options, column = "metadata") {
2418
+ return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
2419
+ }
2420
+ function safeParseMetadataValue(value) {
2421
+ if (!value)
2422
+ return void 0;
2423
+ if (typeof value === "object")
2424
+ return isRecord(value) ? value : void 0;
2425
+ if (typeof value !== "string")
2426
+ return void 0;
2427
+ try {
2428
+ const parsed = JSON.parse(value);
2429
+ return isRecord(parsed) ? parsed : void 0;
2430
+ } catch {
2431
+ return void 0;
2432
+ }
2433
+ }
2434
+ function isImportedOrLegacyScopedMetadata(metadata) {
2435
+ if (!metadata)
2436
+ return false;
2437
+ return Boolean(
2438
+ metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
2439
+ );
2440
+ }
2441
+ function addMetadataTag(metadata, tag) {
2442
+ const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
2443
+ if (!current.includes(tag))
2444
+ metadata.tags = [...current, tag];
2445
+ }
2446
+ function buildRepairResult(projectHash, dryRun) {
2447
+ return {
2448
+ dryRun,
2449
+ projectHash,
2450
+ scanned: 0,
2451
+ repaired: 0,
2452
+ quarantined: 0,
2453
+ alreadyScoped: 0,
2454
+ skipped: 0,
2455
+ samples: []
2456
+ };
2457
+ }
2458
+ function normalizeRepoName(value) {
2459
+ return value.replace(/\.git$/i, "").trim().toLowerCase();
2460
+ }
2461
+ function projectBasename(projectPath) {
2462
+ if (!projectPath)
2463
+ return void 0;
2464
+ const trimmed = projectPath.replace(/[\\/]+$/, "");
2465
+ const basename3 = nodePath2.basename(trimmed);
2466
+ return basename3 ? normalizeRepoName(basename3) : void 0;
2467
+ }
2468
+ function isProjectScopeRepairExplanation(content) {
2469
+ const normalized = content.toLowerCase();
2470
+ const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
2471
+ const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
2472
+ return hasRepairContext && hasExplanationContext;
2473
+ }
2474
+ function hasConflictingContentProjectHint(content, projectPath) {
2475
+ const currentName = projectBasename(projectPath);
2476
+ if (!currentName)
2477
+ return false;
2478
+ if (isProjectScopeRepairExplanation(content))
2479
+ return false;
2480
+ const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
2481
+ let githubMatch;
2482
+ while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
2483
+ const repo = normalizeRepoName(githubMatch[2] || "");
2484
+ if (repo && repo !== currentName)
2485
+ return true;
2486
+ }
2487
+ const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
2488
+ let workspaceMatch;
2489
+ while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
2490
+ const repo = normalizeRepoName(workspaceMatch[1] || "");
2491
+ if (repo && repo !== currentName)
2492
+ return true;
2493
+ }
2494
+ return false;
2495
+ }
2330
2496
  var SQLiteEventStore = class {
2331
2497
  db;
2332
2498
  initialized = false;
@@ -2825,11 +2991,11 @@ var SQLiteEventStore = class {
2825
2991
  /**
2826
2992
  * Get events by session ID
2827
2993
  */
2828
- async getSessionEvents(sessionId) {
2994
+ async getSessionEvents(sessionId, options) {
2829
2995
  await this.initialize();
2830
2996
  const rows = sqliteAll(
2831
2997
  this.db,
2832
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
2998
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2833
2999
  [sessionId]
2834
3000
  );
2835
3001
  return rows.map(this.rowToEvent);
@@ -2837,11 +3003,11 @@ var SQLiteEventStore = class {
2837
3003
  /**
2838
3004
  * Get recent events
2839
3005
  */
2840
- async getRecentEvents(limit = 100) {
3006
+ async getRecentEvents(limit = 100, options) {
2841
3007
  await this.initialize();
2842
3008
  const rows = sqliteAll(
2843
3009
  this.db,
2844
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
3010
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2845
3011
  [limit]
2846
3012
  );
2847
3013
  return rows.map(this.rowToEvent);
@@ -2849,11 +3015,11 @@ var SQLiteEventStore = class {
2849
3015
  /**
2850
3016
  * Get event by ID
2851
3017
  */
2852
- async getEvent(id) {
3018
+ async getEvent(id, options) {
2853
3019
  await this.initialize();
2854
3020
  const row = sqliteGet(
2855
3021
  this.db,
2856
- `SELECT * FROM events WHERE id = ?`,
3022
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2857
3023
  [id]
2858
3024
  );
2859
3025
  if (!row)
@@ -2863,11 +3029,11 @@ var SQLiteEventStore = class {
2863
3029
  /**
2864
3030
  * Get events since a timestamp (for sync)
2865
3031
  */
2866
- async getEventsSince(timestamp, limit = 1e3) {
3032
+ async getEventsSince(timestamp, limit = 1e3, options) {
2867
3033
  await this.initialize();
2868
3034
  const rows = sqliteAll(
2869
3035
  this.db,
2870
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
3036
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2871
3037
  [timestamp, limit]
2872
3038
  );
2873
3039
  return rows.map(this.rowToEvent);
@@ -2876,11 +3042,11 @@ var SQLiteEventStore = class {
2876
3042
  * Get events since a SQLite rowid (for robust incremental replication).
2877
3043
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2878
3044
  */
2879
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
3045
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2880
3046
  await this.initialize();
2881
3047
  const rows = sqliteAll(
2882
3048
  this.db,
2883
- `SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
3049
+ `SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
2884
3050
  [lastRowid, limit]
2885
3051
  );
2886
3052
  return rows.map((row) => ({
@@ -3077,7 +3243,9 @@ var SQLiteEventStore = class {
3077
3243
  const placeholders = ids.map(() => "?").join(",");
3078
3244
  sqliteRun(
3079
3245
  this.db,
3080
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3246
+ `UPDATE embedding_outbox
3247
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3248
+ WHERE id IN (${placeholders})`,
3081
3249
  ids
3082
3250
  );
3083
3251
  return pending.map((row) => ({
@@ -3113,19 +3281,19 @@ var SQLiteEventStore = class {
3113
3281
  /**
3114
3282
  * Count total events
3115
3283
  */
3116
- async countEvents() {
3284
+ async countEvents(options) {
3117
3285
  await this.initialize();
3118
- const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
3286
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
3119
3287
  return row?.count || 0;
3120
3288
  }
3121
3289
  /**
3122
3290
  * Get events page in timestamp ascending order (stable migration/reindex scans)
3123
3291
  */
3124
- async getEventsPage(limit = 1e3, offset = 0) {
3292
+ async getEventsPage(limit = 1e3, offset = 0, options) {
3125
3293
  await this.initialize();
3126
3294
  const rows = sqliteAll(
3127
3295
  this.db,
3128
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3296
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
3129
3297
  [limit, offset]
3130
3298
  );
3131
3299
  return rows.map(this.rowToEvent);
@@ -3147,6 +3315,197 @@ var SQLiteEventStore = class {
3147
3315
  [error, ...ids]
3148
3316
  );
3149
3317
  }
3318
+ /**
3319
+ * Recover abandoned outbox work after a worker/process crash.
3320
+ *
3321
+ * Rows in `processing` are claimed work. If the process exits before marking
3322
+ * them done/failed, they otherwise remain invisible to future processing.
3323
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3324
+ */
3325
+ async recoverStuckOutboxItems(options = {}) {
3326
+ await this.initialize();
3327
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3328
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3329
+ const now = options.now ?? /* @__PURE__ */ new Date();
3330
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3331
+ const result = emptyOutboxRecoveryResult();
3332
+ const embeddingRecovered = sqliteRun(
3333
+ this.db,
3334
+ `UPDATE embedding_outbox
3335
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3336
+ WHERE status = 'processing'
3337
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3338
+ [threshold]
3339
+ );
3340
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3341
+ const embeddingRetried = sqliteRun(
3342
+ this.db,
3343
+ `UPDATE embedding_outbox
3344
+ SET status = 'pending', error_message = NULL
3345
+ WHERE status = 'failed'
3346
+ AND retry_count < ?`,
3347
+ [maxRetries]
3348
+ );
3349
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3350
+ const vectorRecovered = sqliteRun(
3351
+ this.db,
3352
+ `UPDATE vector_outbox
3353
+ SET status = 'pending', updated_at = ?, error = NULL
3354
+ WHERE status = 'processing'
3355
+ AND datetime(updated_at) < datetime(?)`,
3356
+ [now.toISOString(), threshold]
3357
+ );
3358
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3359
+ const vectorRetried = sqliteRun(
3360
+ this.db,
3361
+ `UPDATE vector_outbox
3362
+ SET status = 'pending', updated_at = ?, error = NULL
3363
+ WHERE status = 'failed'
3364
+ AND retry_count < ?`,
3365
+ [now.toISOString(), maxRetries]
3366
+ );
3367
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3368
+ return result;
3369
+ }
3370
+ /**
3371
+ * Repair legacy imported events that predate canonical project scope metadata.
3372
+ *
3373
+ * Same-project legacy rows are tagged with scope.project.hash. Rows that look
3374
+ * imported but cannot be proven to belong to this project are quarantined so
3375
+ * dashboard default reads/search do not surface cross-project contamination.
3376
+ */
3377
+ async repairLegacyProjectScope(options = {}) {
3378
+ await this.initialize();
3379
+ const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
3380
+ if (!projectHash) {
3381
+ throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
3382
+ }
3383
+ if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
3384
+ throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
3385
+ }
3386
+ const dryRun = options.dryRun === true;
3387
+ const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
3388
+ const result = buildRepairResult(projectHash, dryRun);
3389
+ const rows = sqliteAll(
3390
+ this.db,
3391
+ `SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
3392
+ FROM events e
3393
+ LEFT JOIN sessions s ON s.id = e.session_id
3394
+ ORDER BY e.timestamp ASC`,
3395
+ []
3396
+ );
3397
+ const sample = (entry) => {
3398
+ if (result.samples.length < 20)
3399
+ result.samples.push(entry);
3400
+ };
3401
+ for (const row of rows) {
3402
+ result.scanned++;
3403
+ let metadata = {};
3404
+ let metadataParseInvalid = false;
3405
+ if (row.metadata) {
3406
+ const parsed = safeParseMetadataValue(row.metadata);
3407
+ if (parsed) {
3408
+ metadata = parsed;
3409
+ } else {
3410
+ metadataParseInvalid = true;
3411
+ }
3412
+ }
3413
+ if (isActiveQuarantinedMetadata(metadata)) {
3414
+ result.skipped++;
3415
+ continue;
3416
+ }
3417
+ const currentHash = metadataProjectHash(metadata);
3418
+ const explicitPath = metadataProjectPath(metadata);
3419
+ const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
3420
+ const candidatePaths = metadataProjectPaths(metadata);
3421
+ if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
3422
+ candidatePaths.push(sessionProjectPath);
3423
+ }
3424
+ const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
3425
+ const pathHashes = candidatePaths.map((candidate) => {
3426
+ try {
3427
+ return { path: candidate, hash: hashProjectPath(candidate) };
3428
+ } catch {
3429
+ return { path: candidate, hash: void 0 };
3430
+ }
3431
+ });
3432
+ const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
3433
+ const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
3434
+ let action = "skipped";
3435
+ let reason;
3436
+ let observedProjectHash;
3437
+ if (foreignPath) {
3438
+ action = "quarantined";
3439
+ reason = "project-path-mismatch";
3440
+ observedProjectHash = foreignPath.hash;
3441
+ } else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
3442
+ action = "quarantined";
3443
+ reason = "content-project-mismatch";
3444
+ } else if (currentHash === projectHash) {
3445
+ result.alreadyScoped++;
3446
+ continue;
3447
+ } else if (currentHash && currentHash !== projectHash) {
3448
+ action = "quarantined";
3449
+ reason = "scope-hash-mismatch";
3450
+ observedProjectHash = currentHash;
3451
+ } else if (matchingPath) {
3452
+ action = "repaired";
3453
+ reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
3454
+ } else if (candidatePaths.length > 0) {
3455
+ action = "quarantined";
3456
+ reason = "project-path-mismatch";
3457
+ } else if (importedOrLegacy) {
3458
+ action = "quarantined";
3459
+ reason = "missing-project-scope";
3460
+ }
3461
+ if (action === "skipped" || !reason) {
3462
+ result.skipped++;
3463
+ continue;
3464
+ }
3465
+ if (action === "repaired") {
3466
+ const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
3467
+ const project = isRecord(scope.project) ? { ...scope.project } : {};
3468
+ project.hash = projectHash;
3469
+ scope.project = project;
3470
+ metadata.scope = scope;
3471
+ metadata.repair = {
3472
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3473
+ legacyProjectScope: {
3474
+ action,
3475
+ reason,
3476
+ repairedAt: nowIso
3477
+ }
3478
+ };
3479
+ addMetadataTag(metadata, `proj:${projectHash}`);
3480
+ result.repaired++;
3481
+ } else {
3482
+ metadata.quarantine = {
3483
+ ...isRecord(metadata.quarantine) ? metadata.quarantine : {},
3484
+ status: "active",
3485
+ category: "project-scope",
3486
+ reason,
3487
+ detectedAt: nowIso,
3488
+ expectedProjectHash: projectHash,
3489
+ ...observedProjectHash ? { observedProjectHash } : {}
3490
+ };
3491
+ metadata.repair = {
3492
+ ...isRecord(metadata.repair) ? metadata.repair : {},
3493
+ legacyProjectScope: {
3494
+ action,
3495
+ reason,
3496
+ repairedAt: nowIso
3497
+ }
3498
+ };
3499
+ addMetadataTag(metadata, "quarantine:project-scope");
3500
+ result.quarantined++;
3501
+ }
3502
+ sample({ eventId: row.id, action, reason });
3503
+ if (!dryRun) {
3504
+ sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
3505
+ }
3506
+ }
3507
+ return result;
3508
+ }
3150
3509
  /**
3151
3510
  * Get embedding/vector outbox health statistics
3152
3511
  */
@@ -3194,7 +3553,11 @@ var SQLiteEventStore = class {
3194
3553
  await this.initialize();
3195
3554
  const rows = sqliteAll(
3196
3555
  this.db,
3197
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
3556
+ `SELECT ml.level, COUNT(*) as count
3557
+ FROM memory_levels ml
3558
+ INNER JOIN events e ON e.id = ml.event_id
3559
+ WHERE ${notActiveQuarantinedSql("e.metadata")}
3560
+ GROUP BY ml.level`
3198
3561
  );
3199
3562
  return rows;
3200
3563
  }
@@ -3210,6 +3573,7 @@ var SQLiteEventStore = class {
3210
3573
  `SELECT e.* FROM events e
3211
3574
  INNER JOIN memory_levels ml ON e.id = ml.event_id
3212
3575
  WHERE ml.level = ?
3576
+ AND ${notActiveQuarantinedSql("e.metadata")}
3213
3577
  ORDER BY e.timestamp DESC
3214
3578
  LIMIT ? OFFSET ?`,
3215
3579
  [level, limit, offset]
@@ -3302,12 +3666,13 @@ var SQLiteEventStore = class {
3302
3666
  /**
3303
3667
  * Get most accessed memories (falls back to recent events if none accessed)
3304
3668
  */
3305
- async getMostAccessed(limit = 10) {
3669
+ async getMostAccessed(limit = 10, options) {
3306
3670
  await this.initialize();
3307
3671
  let rows = sqliteAll(
3308
3672
  this.db,
3309
3673
  `SELECT * FROM events
3310
3674
  WHERE access_count > 0
3675
+ AND ${maybeQuarantinePredicate(options)}
3311
3676
  ORDER BY access_count DESC, last_accessed_at DESC
3312
3677
  LIMIT ?`,
3313
3678
  [limit]
@@ -3316,6 +3681,7 @@ var SQLiteEventStore = class {
3316
3681
  rows = sqliteAll(
3317
3682
  this.db,
3318
3683
  `SELECT * FROM events
3684
+ WHERE ${maybeQuarantinePredicate(options)}
3319
3685
  ORDER BY timestamp DESC
3320
3686
  LIMIT ?`,
3321
3687
  [limit]
@@ -3446,6 +3812,7 @@ var SQLiteEventStore = class {
3446
3812
  FROM memory_helpfulness mh
3447
3813
  JOIN events e ON e.id = mh.event_id
3448
3814
  WHERE mh.measured_at IS NOT NULL
3815
+ AND ${notActiveQuarantinedSql("e.metadata")}
3449
3816
  GROUP BY mh.event_id
3450
3817
  ORDER BY avg_score DESC
3451
3818
  LIMIT ?`,
@@ -3510,6 +3877,7 @@ var SQLiteEventStore = class {
3510
3877
  FROM events_fts fts
3511
3878
  JOIN events e ON e.id = fts.event_id
3512
3879
  WHERE events_fts MATCH ?
3880
+ AND ${notActiveQuarantinedSql("e.metadata")}
3513
3881
  ORDER BY fts.rank
3514
3882
  LIMIT ?`,
3515
3883
  [searchTerms, limit]
@@ -3524,6 +3892,7 @@ var SQLiteEventStore = class {
3524
3892
  this.db,
3525
3893
  `SELECT *, 0 as rank FROM events
3526
3894
  WHERE content LIKE ?
3895
+ AND ${notActiveQuarantinedSql()}
3527
3896
  ORDER BY timestamp DESC
3528
3897
  LIMIT ?`,
3529
3898
  [likePattern, limit]
@@ -3730,6 +4099,7 @@ var SQLiteEventStore = class {
3730
4099
  `SELECT turn_id, MIN(timestamp) as min_ts
3731
4100
  FROM events
3732
4101
  WHERE session_id = ? AND turn_id IS NOT NULL
4102
+ AND ${maybeQuarantinePredicate(options)}
3733
4103
  GROUP BY turn_id
3734
4104
  ORDER BY min_ts DESC
3735
4105
  LIMIT ? OFFSET ?`,
@@ -3737,7 +4107,7 @@ var SQLiteEventStore = class {
3737
4107
  );
3738
4108
  const turns = [];
3739
4109
  for (const turnRow of turnRows) {
3740
- const events = await this.getEventsByTurn(turnRow.turn_id);
4110
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3741
4111
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3742
4112
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3743
4113
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3756,11 +4126,11 @@ var SQLiteEventStore = class {
3756
4126
  /**
3757
4127
  * Get all events for a specific turn_id
3758
4128
  */
3759
- async getEventsByTurn(turnId) {
4129
+ async getEventsByTurn(turnId, options) {
3760
4130
  await this.initialize();
3761
4131
  const rows = sqliteAll(
3762
4132
  this.db,
3763
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
4133
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3764
4134
  [turnId]
3765
4135
  );
3766
4136
  return rows.map(this.rowToEvent);
@@ -3768,13 +4138,14 @@ var SQLiteEventStore = class {
3768
4138
  /**
3769
4139
  * Count total turns for a session
3770
4140
  */
3771
- async countSessionTurns(sessionId) {
4141
+ async countSessionTurns(sessionId, options) {
3772
4142
  await this.initialize();
3773
4143
  const row = sqliteGet(
3774
4144
  this.db,
3775
4145
  `SELECT COUNT(DISTINCT turn_id) as count
3776
4146
  FROM events
3777
- WHERE session_id = ? AND turn_id IS NOT NULL`,
4147
+ WHERE session_id = ? AND turn_id IS NOT NULL
4148
+ AND ${maybeQuarantinePredicate(options)}`,
3778
4149
  [sessionId]
3779
4150
  );
3780
4151
  return row?.count || 0;
@@ -3866,7 +4237,7 @@ var SQLiteEventStore = class {
3866
4237
  content: row.content,
3867
4238
  canonicalKey: row.canonical_key,
3868
4239
  dedupeKey: row.dedupe_key,
3869
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4240
+ metadata: safeParseMetadataValue(row.metadata)
3870
4241
  };
3871
4242
  if (row.access_count !== void 0) {
3872
4243
  event.access_count = row.access_count;
@@ -4018,6 +4389,7 @@ var VectorStore = class {
4018
4389
  * Get total count of vectors
4019
4390
  */
4020
4391
  async count() {
4392
+ await this.initialize();
4021
4393
  if (!this.table)
4022
4394
  return 0;
4023
4395
  const result = await this.table.countRows();
@@ -4456,6 +4828,14 @@ var MemoryQueryService = class {
4456
4828
  await this.initialize();
4457
4829
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4458
4830
  }
4831
+ async recoverStuckOutboxItems(options) {
4832
+ await this.initialize();
4833
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4834
+ }
4835
+ async repairLegacyProjectScope(options) {
4836
+ await this.initialize();
4837
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
4838
+ }
4459
4839
  async getStats() {
4460
4840
  await this.initialize();
4461
4841
  const deps = this.getStatsDeps();
@@ -6078,18 +6458,18 @@ function assertDefaultRetrieverStore(eventStore) {
6078
6458
  function createMemoryEngineServices(options) {
6079
6459
  const factories = options.factories ?? {};
6080
6460
  const storagePath = options.storagePath;
6081
- if (!options.readOnly && !fs5.existsSync(storagePath)) {
6082
- fs5.mkdirSync(storagePath, { recursive: true });
6461
+ if (!options.readOnly && !fs6.existsSync(storagePath)) {
6462
+ fs6.mkdirSync(storagePath, { recursive: true });
6083
6463
  }
6084
6464
  const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
6085
- path4.join(storagePath, "events.sqlite"),
6465
+ path5.join(storagePath, "events.sqlite"),
6086
6466
  {
6087
6467
  readonly: options.readOnly,
6088
6468
  markdownMirrorRoot: storagePath
6089
6469
  }
6090
6470
  );
6091
6471
  const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
6092
- path4.join(storagePath, "vectors")
6472
+ path5.join(storagePath, "vectors")
6093
6473
  );
6094
6474
  const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6095
6475
  const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
@@ -6500,8 +6880,8 @@ function createMemoryRuntimeService(deps) {
6500
6880
  }
6501
6881
 
6502
6882
  // src/extensions/shared-memory/shared-memory-services.ts
6503
- import * as fs6 from "fs";
6504
- import * as path5 from "path";
6883
+ import * as fs7 from "fs";
6884
+ import * as path6 from "path";
6505
6885
 
6506
6886
  // src/core/shared-event-store.ts
6507
6887
  var SharedEventStore = class {
@@ -7189,7 +7569,7 @@ var SharedMemoryServices = class {
7189
7569
  this.ensureDirectory(sharedPath, { allowCreate: true });
7190
7570
  const store = await this.openStore(sharedPath);
7191
7571
  this.sharedVectorStore = this.factories.createSharedVectorStore(
7192
- path5.join(sharedPath, "vectors")
7572
+ path6.join(sharedPath, "vectors")
7193
7573
  );
7194
7574
  await this.sharedVectorStore.initialize();
7195
7575
  this.sharedPromoter = this.factories.createSharedPromoter(
@@ -7262,7 +7642,7 @@ var SharedMemoryServices = class {
7262
7642
  async createOpenStorePromise(sharedPath) {
7263
7643
  if (!this.sharedEventStore) {
7264
7644
  const sharedEventStore = this.factories.createSharedEventStore(
7265
- path5.join(sharedPath, "shared.duckdb")
7645
+ path6.join(sharedPath, "shared.duckdb")
7266
7646
  );
7267
7647
  await sharedEventStore.initialize();
7268
7648
  this.sharedEventStore = sharedEventStore;
@@ -7282,9 +7662,9 @@ var SharedMemoryServices = class {
7282
7662
  }
7283
7663
  get factories() {
7284
7664
  return {
7285
- existsSync: this.options.factories?.existsSync ?? fs6.existsSync,
7665
+ existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
7286
7666
  mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
7287
- fs6.mkdirSync(targetPath, { recursive: true });
7667
+ fs7.mkdirSync(targetPath, { recursive: true });
7288
7668
  }),
7289
7669
  createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
7290
7670
  createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
@@ -7399,37 +7779,11 @@ function createMemoryServiceComposition(options) {
7399
7779
  }
7400
7780
  function defaultExpandPath(targetPath) {
7401
7781
  if (targetPath.startsWith("~")) {
7402
- return path6.join(os.homedir(), targetPath.slice(1));
7782
+ return path7.join(os2.homedir(), targetPath.slice(1));
7403
7783
  }
7404
7784
  return targetPath;
7405
7785
  }
7406
7786
 
7407
- // src/core/registry/project-path.ts
7408
- import * as crypto2 from "crypto";
7409
- import * as fs7 from "fs";
7410
- import * as os2 from "os";
7411
- import * as path7 from "path";
7412
- function normalizeProjectPath(projectPath) {
7413
- const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
7414
- try {
7415
- return fs7.realpathSync(expanded);
7416
- } catch {
7417
- return path7.resolve(expanded);
7418
- }
7419
- }
7420
- function hashProjectPath(projectPath) {
7421
- const normalizedPath = normalizeProjectPath(projectPath);
7422
- return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
7423
- }
7424
- function getProjectStoragePath(projectPath) {
7425
- const hash = hashProjectPath(projectPath);
7426
- return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
7427
- }
7428
- function resolveProjectStoragePath(projectOrHash) {
7429
- const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
7430
- return isHash ? path7.join(os2.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
7431
- }
7432
-
7433
7787
  // src/core/registry/session-registry.ts
7434
7788
  import * as fs8 from "fs";
7435
7789
  import * as os3 from "os";
@@ -7728,6 +8082,12 @@ var MemoryService = class {
7728
8082
  async getOutboxStats() {
7729
8083
  return this.queryService.getOutboxStats();
7730
8084
  }
8085
+ async recoverStuckOutboxItems(options) {
8086
+ return this.queryService.recoverStuckOutboxItems(options);
8087
+ }
8088
+ async repairLegacyProjectScope(options) {
8089
+ return this.queryService.repairLegacyProjectScope(options);
8090
+ }
7731
8091
  async getRetrievalTraceStats() {
7732
8092
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7733
8093
  }
@@ -8041,6 +8401,26 @@ function getServiceFromQuery(c) {
8041
8401
  }
8042
8402
  return getReadOnlyMemoryService();
8043
8403
  }
8404
+ function getWritableServiceFromQuery(c) {
8405
+ const project = c.req.query("project") || c.req.query("projectId");
8406
+ if (project) {
8407
+ const storagePath = resolveProjectStoragePath(project);
8408
+ return new MemoryService({
8409
+ storagePath,
8410
+ readOnly: false,
8411
+ lightweightMode: true,
8412
+ analyticsEnabled: false,
8413
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
8414
+ });
8415
+ }
8416
+ return new MemoryService({
8417
+ storagePath: "~/.claude-code/memory",
8418
+ readOnly: false,
8419
+ lightweightMode: true,
8420
+ analyticsEnabled: false,
8421
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
8422
+ });
8423
+ }
8044
8424
  function getLightweightServiceFromQuery(c) {
8045
8425
  const project = c.req.query("project") || c.req.query("projectId");
8046
8426
  if (project) {
@@ -9696,6 +10076,13 @@ import { Hono as Hono8 } from "hono";
9696
10076
  import { streamSSE } from "hono/streaming";
9697
10077
  import { spawn } from "child_process";
9698
10078
  var chatRouter = new Hono8();
10079
+ var ProviderFailure = class extends Error {
10080
+ constructor(code, message) {
10081
+ super(message);
10082
+ this.code = code;
10083
+ this.name = "ProviderFailure";
10084
+ }
10085
+ };
9699
10086
  var CLAUDE_TIMEOUT_MS = 12e4;
9700
10087
  chatRouter.post("/", async (c) => {
9701
10088
  let body;
@@ -9707,43 +10094,12 @@ chatRouter.post("/", async (c) => {
9707
10094
  if (!body.message?.trim()) {
9708
10095
  return c.json({ error: "Message is required" }, 400);
9709
10096
  }
9710
- const memoryService = getServiceFromQuery(c);
10097
+ const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
10098
+ const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
9711
10099
  try {
9712
10100
  await memoryService.initialize();
9713
- let memoryContext = "";
9714
- let statsContext = "";
9715
- try {
9716
- const result = await memoryService.retrieveMemories(body.message, {
9717
- topK: 8,
9718
- minScore: 0.5
9719
- });
9720
- if (result.memories.length > 0) {
9721
- const parts = ["## Relevant Memories\n"];
9722
- for (const m of result.memories) {
9723
- const date = new Date(m.event.timestamp).toISOString().split("T")[0];
9724
- const content = m.event.content.slice(0, 500);
9725
- parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
9726
- parts.push(content);
9727
- if (m.sessionContext) {
9728
- parts.push(`_Context: ${m.sessionContext}_`);
9729
- }
9730
- parts.push("");
9731
- }
9732
- memoryContext = parts.join("\n");
9733
- }
9734
- } catch {
9735
- }
9736
- try {
9737
- const stats = await memoryService.getStats();
9738
- const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
9739
- statsContext = [
9740
- "## Memory Stats",
9741
- `- Total events: ${stats.totalEvents}`,
9742
- `- Vector nodes: ${stats.vectorCount}`,
9743
- `- By level: ${levels}`
9744
- ].join("\n");
9745
- } catch {
9746
- }
10101
+ const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
10102
+ const statsContext = await collectStatsContext(memoryService);
9747
10103
  const fullPrompt = buildPrompt(
9748
10104
  statsContext,
9749
10105
  memoryContext,
@@ -9751,13 +10107,23 @@ chatRouter.post("/", async (c) => {
9751
10107
  body.message
9752
10108
  );
9753
10109
  return streamSSE(c, async (stream) => {
10110
+ if (memoryOnly) {
10111
+ await streamMemoryOnlyResponse(stream, {
10112
+ memoryContext,
10113
+ memoryHits,
10114
+ reason: "memory-only-mode"
10115
+ });
10116
+ return;
10117
+ }
9754
10118
  try {
9755
10119
  await streamClaudeResponse(fullPrompt, stream);
9756
10120
  } catch (err) {
10121
+ const diagnostic = providerDiagnostic(err);
9757
10122
  await stream.writeSSE({
9758
- event: "error",
9759
- data: JSON.stringify({ error: err.message })
10123
+ event: "provider_error",
10124
+ data: JSON.stringify(diagnostic)
9760
10125
  });
10126
+ await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
9761
10127
  }
9762
10128
  });
9763
10129
  } catch (error) {
@@ -9766,6 +10132,115 @@ chatRouter.post("/", async (c) => {
9766
10132
  await memoryService.shutdown();
9767
10133
  }
9768
10134
  });
10135
+ async function collectMemoryContext(memoryService, query) {
10136
+ let memoryHits = [];
10137
+ try {
10138
+ const result = await memoryService.retrieveMemories?.(query, {
10139
+ topK: 8,
10140
+ minScore: 0.5
10141
+ });
10142
+ memoryHits = result?.memories ?? [];
10143
+ } catch {
10144
+ memoryHits = [];
10145
+ }
10146
+ if (memoryHits.length === 0) {
10147
+ try {
10148
+ memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
10149
+ } catch {
10150
+ memoryHits = [];
10151
+ }
10152
+ }
10153
+ return {
10154
+ memoryContext: formatMemoryContext(memoryHits),
10155
+ memoryHits
10156
+ };
10157
+ }
10158
+ async function collectStatsContext(memoryService) {
10159
+ try {
10160
+ const stats = await memoryService.getStats?.();
10161
+ if (!stats)
10162
+ return "";
10163
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
10164
+ return [
10165
+ "## Memory Stats",
10166
+ `- Total events: ${stats.totalEvents}`,
10167
+ `- Vector nodes: ${stats.vectorCount}`,
10168
+ `- By level: ${levels}`
10169
+ ].join("\n");
10170
+ } catch {
10171
+ return "";
10172
+ }
10173
+ }
10174
+ function formatMemoryContext(memoryHits) {
10175
+ if (memoryHits.length === 0)
10176
+ return "";
10177
+ const parts = ["## Relevant Memories\n"];
10178
+ for (const m of memoryHits) {
10179
+ const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
10180
+ const content = (m.event.content ?? "").slice(0, 500);
10181
+ parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
10182
+ parts.push(content);
10183
+ if (m.sessionContext) {
10184
+ parts.push(`_Context: ${m.sessionContext}_`);
10185
+ }
10186
+ parts.push("");
10187
+ }
10188
+ return parts.join("\n");
10189
+ }
10190
+ async function streamMemoryOnlyResponse(stream, options) {
10191
+ await stream.writeSSE({
10192
+ event: "diagnostic",
10193
+ data: JSON.stringify({
10194
+ provider: "claude-cli",
10195
+ status: "skipped",
10196
+ mode: "memory-only",
10197
+ reason: options.reason,
10198
+ retrievedMemories: options.memoryHits.length
10199
+ })
10200
+ });
10201
+ await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
10202
+ }
10203
+ async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
10204
+ const content = memoryHits.length > 0 ? [
10205
+ "Provider unavailable or skipped; showing retrieved memory context directly.",
10206
+ "",
10207
+ memoryContext
10208
+ ].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
10209
+ await stream.writeSSE({
10210
+ event: "message",
10211
+ data: JSON.stringify({ content, mode: "memory-only" })
10212
+ });
10213
+ await stream.writeSSE({ event: "done", data: "{}" });
10214
+ }
10215
+ function providerDiagnostic(err) {
10216
+ if (err instanceof ProviderFailure) {
10217
+ return {
10218
+ provider: "claude-cli",
10219
+ code: err.code,
10220
+ message: err.message,
10221
+ fallback: "memory-only"
10222
+ };
10223
+ }
10224
+ return {
10225
+ provider: "claude-cli",
10226
+ code: "claude-cli-error",
10227
+ message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
10228
+ fallback: "memory-only"
10229
+ };
10230
+ }
10231
+ function classifyProviderFailure(message) {
10232
+ const normalized = message.toLowerCase();
10233
+ if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
10234
+ return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
10235
+ }
10236
+ if (normalized.includes("not found") || normalized.includes("enoent")) {
10237
+ return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
10238
+ }
10239
+ if (normalized.includes("timed out")) {
10240
+ return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
10241
+ }
10242
+ return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
10243
+ }
9769
10244
  function buildPrompt(statsContext, memoryContext, history, currentMessage) {
9770
10245
  const parts = [];
9771
10246
  parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
@@ -9808,12 +10283,13 @@ function streamClaudeResponse(prompt, stream) {
9808
10283
  });
9809
10284
  const timeout = setTimeout(() => {
9810
10285
  proc.kill("SIGTERM");
9811
- reject(new Error("Chat response timed out after 2 minutes"));
10286
+ reject(classifyProviderFailure("timed out"));
9812
10287
  }, CLAUDE_TIMEOUT_MS);
9813
10288
  proc.stdin.write(prompt);
9814
10289
  proc.stdin.end();
9815
10290
  let buffer = "";
9816
10291
  let lastSentText = "";
10292
+ let stderrText = "";
9817
10293
  proc.stdout.on("data", async (chunk) => {
9818
10294
  buffer += chunk.toString();
9819
10295
  const lines = buffer.split("\n");
@@ -9842,6 +10318,7 @@ function streamClaudeResponse(prompt, stream) {
9842
10318
  }
9843
10319
  });
9844
10320
  proc.stderr.on("data", (chunk) => {
10321
+ stderrText += chunk.toString();
9845
10322
  if (process.env.CLAUDE_MEMORY_DEBUG) {
9846
10323
  console.error("[chat] claude stderr:", chunk.toString());
9847
10324
  }
@@ -9849,7 +10326,7 @@ function streamClaudeResponse(prompt, stream) {
9849
10326
  proc.on("error", (err) => {
9850
10327
  clearTimeout(timeout);
9851
10328
  if (err.code === "ENOENT") {
9852
- reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
10329
+ reject(classifyProviderFailure("ENOENT not found"));
9853
10330
  } else {
9854
10331
  reject(err);
9855
10332
  }
@@ -9866,7 +10343,7 @@ function streamClaudeResponse(prompt, stream) {
9866
10343
  }
9867
10344
  }
9868
10345
  if (code !== 0 && code !== null) {
9869
- reject(new Error(`Claude CLI exited with code ${code}`));
10346
+ reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
9870
10347
  } else {
9871
10348
  resolve3();
9872
10349
  }
@@ -9915,6 +10392,49 @@ healthRouter.get("/", async (c) => {
9915
10392
  await memoryService.shutdown();
9916
10393
  }
9917
10394
  });
10395
+ healthRouter.post("/recover", async (c) => {
10396
+ const memoryService = getWritableServiceFromQuery(c);
10397
+ try {
10398
+ await memoryService.initialize();
10399
+ const body = await c.req.json().catch(() => ({}));
10400
+ const options = {};
10401
+ if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
10402
+ options.stuckThresholdMs = body.stuckThresholdMs;
10403
+ }
10404
+ if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
10405
+ options.maxRetries = body.maxRetries;
10406
+ }
10407
+ const before = await memoryService.getOutboxStats();
10408
+ const recovered = await memoryService.recoverStuckOutboxItems(options);
10409
+ const [stats, outbox] = await Promise.all([
10410
+ memoryService.getStats(),
10411
+ memoryService.getOutboxStats()
10412
+ ]);
10413
+ return c.json({
10414
+ status: "ok",
10415
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10416
+ recovered,
10417
+ before: {
10418
+ outbox: before
10419
+ },
10420
+ after: {
10421
+ storage: {
10422
+ totalEvents: stats.totalEvents,
10423
+ vectorCount: stats.vectorCount
10424
+ },
10425
+ outbox
10426
+ }
10427
+ });
10428
+ } catch (error) {
10429
+ return c.json({
10430
+ status: "error",
10431
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10432
+ error: error.message
10433
+ }, 500);
10434
+ } finally {
10435
+ await memoryService.shutdown();
10436
+ }
10437
+ });
9918
10438
 
9919
10439
  // src/apps/server/api/index.ts
9920
10440
  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);