claude-memory-layer 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2329,6 +2329,14 @@ function normalizeQueryRewriteKind(value) {
2329
2329
  return "none";
2330
2330
  }
2331
2331
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2332
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2333
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2334
+ function emptyOutboxRecoveryResult() {
2335
+ return {
2336
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2337
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2338
+ };
2339
+ }
2332
2340
  var SQLiteEventStore = class {
2333
2341
  db;
2334
2342
  initialized = false;
@@ -3079,7 +3087,9 @@ var SQLiteEventStore = class {
3079
3087
  const placeholders = ids.map(() => "?").join(",");
3080
3088
  sqliteRun(
3081
3089
  this.db,
3082
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3090
+ `UPDATE embedding_outbox
3091
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3092
+ WHERE id IN (${placeholders})`,
3083
3093
  ids
3084
3094
  );
3085
3095
  return pending.map((row) => ({
@@ -3149,6 +3159,58 @@ var SQLiteEventStore = class {
3149
3159
  [error, ...ids]
3150
3160
  );
3151
3161
  }
3162
+ /**
3163
+ * Recover abandoned outbox work after a worker/process crash.
3164
+ *
3165
+ * Rows in `processing` are claimed work. If the process exits before marking
3166
+ * them done/failed, they otherwise remain invisible to future processing.
3167
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3168
+ */
3169
+ async recoverStuckOutboxItems(options = {}) {
3170
+ await this.initialize();
3171
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3172
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3173
+ const now = options.now ?? /* @__PURE__ */ new Date();
3174
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3175
+ const result = emptyOutboxRecoveryResult();
3176
+ const embeddingRecovered = sqliteRun(
3177
+ this.db,
3178
+ `UPDATE embedding_outbox
3179
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3180
+ WHERE status = 'processing'
3181
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3182
+ [threshold]
3183
+ );
3184
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3185
+ const embeddingRetried = sqliteRun(
3186
+ this.db,
3187
+ `UPDATE embedding_outbox
3188
+ SET status = 'pending', error_message = NULL
3189
+ WHERE status = 'failed'
3190
+ AND retry_count < ?`,
3191
+ [maxRetries]
3192
+ );
3193
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3194
+ const vectorRecovered = sqliteRun(
3195
+ this.db,
3196
+ `UPDATE vector_outbox
3197
+ SET status = 'pending', updated_at = ?, error = NULL
3198
+ WHERE status = 'processing'
3199
+ AND datetime(updated_at) < datetime(?)`,
3200
+ [now.toISOString(), threshold]
3201
+ );
3202
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3203
+ const vectorRetried = sqliteRun(
3204
+ this.db,
3205
+ `UPDATE vector_outbox
3206
+ SET status = 'pending', updated_at = ?, error = NULL
3207
+ WHERE status = 'failed'
3208
+ AND retry_count < ?`,
3209
+ [now.toISOString(), maxRetries]
3210
+ );
3211
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3212
+ return result;
3213
+ }
3152
3214
  /**
3153
3215
  * Get embedding/vector outbox health statistics
3154
3216
  */
@@ -4020,6 +4082,7 @@ var VectorStore = class {
4020
4082
  * Get total count of vectors
4021
4083
  */
4022
4084
  async count() {
4085
+ await this.initialize();
4023
4086
  if (!this.table)
4024
4087
  return 0;
4025
4088
  const result = await this.table.countRows();
@@ -4458,6 +4521,10 @@ var MemoryQueryService = class {
4458
4521
  await this.initialize();
4459
4522
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4460
4523
  }
4524
+ async recoverStuckOutboxItems(options) {
4525
+ await this.initialize();
4526
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4527
+ }
4461
4528
  async getStats() {
4462
4529
  await this.initialize();
4463
4530
  const deps = this.getStatsDeps();
@@ -7755,6 +7822,9 @@ var MemoryService = class {
7755
7822
  async getOutboxStats() {
7756
7823
  return this.queryService.getOutboxStats();
7757
7824
  }
7825
+ async recoverStuckOutboxItems(options) {
7826
+ return this.queryService.recoverStuckOutboxItems(options);
7827
+ }
7758
7828
  async getRetrievalTraceStats() {
7759
7829
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7760
7830
  }
@@ -10345,6 +10415,26 @@ function getServiceFromQuery(c) {
10345
10415
  }
10346
10416
  return getReadOnlyMemoryService();
10347
10417
  }
10418
+ function getWritableServiceFromQuery(c) {
10419
+ const project = c.req.query("project") || c.req.query("projectId");
10420
+ if (project) {
10421
+ const storagePath = resolveProjectStoragePath(project);
10422
+ return new MemoryService({
10423
+ storagePath,
10424
+ readOnly: false,
10425
+ lightweightMode: true,
10426
+ analyticsEnabled: false,
10427
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
10428
+ });
10429
+ }
10430
+ return new MemoryService({
10431
+ storagePath: "~/.claude-code/memory",
10432
+ readOnly: false,
10433
+ lightweightMode: true,
10434
+ analyticsEnabled: false,
10435
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
10436
+ });
10437
+ }
10348
10438
  function getLightweightServiceFromQuery(c) {
10349
10439
  const project = c.req.query("project") || c.req.query("projectId");
10350
10440
  if (project) {
@@ -10371,7 +10461,7 @@ var sessionsRouter = new Hono();
10371
10461
  sessionsRouter.get("/", async (c) => {
10372
10462
  const page = parseInt(c.req.query("page") || "1", 10);
10373
10463
  const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
10374
- const memoryService = getServiceFromQuery(c);
10464
+ const memoryService = getLightweightServiceFromQuery(c);
10375
10465
  try {
10376
10466
  await memoryService.initialize();
10377
10467
  const recentEvents = await memoryService.getRecentEvents(1e3);
@@ -10415,7 +10505,7 @@ sessionsRouter.get("/", async (c) => {
10415
10505
  });
10416
10506
  sessionsRouter.get("/:id", async (c) => {
10417
10507
  const { id } = c.req.param();
10418
- const memoryService = getServiceFromQuery(c);
10508
+ const memoryService = getLightweightServiceFromQuery(c);
10419
10509
  try {
10420
10510
  await memoryService.initialize();
10421
10511
  const events = await memoryService.getSessionHistory(id);
@@ -10461,7 +10551,7 @@ eventsRouter.get("/", async (c) => {
10461
10551
  const q = (c.req.query("q") || "").trim().toLowerCase();
10462
10552
  const limit = parseInt(c.req.query("limit") || "100", 10);
10463
10553
  const offset = parseInt(c.req.query("offset") || "0", 10);
10464
- const memoryService = getServiceFromQuery(c);
10554
+ const memoryService = getLightweightServiceFromQuery(c);
10465
10555
  try {
10466
10556
  await memoryService.initialize();
10467
10557
  let events;
@@ -10517,7 +10607,7 @@ eventsRouter.get("/", async (c) => {
10517
10607
  });
10518
10608
  eventsRouter.get("/:id", async (c) => {
10519
10609
  const { id } = c.req.param();
10520
- const memoryService = getServiceFromQuery(c);
10610
+ const memoryService = getLightweightServiceFromQuery(c);
10521
10611
  try {
10522
10612
  await memoryService.initialize();
10523
10613
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -10556,6 +10646,10 @@ eventsRouter.get("/:id", async (c) => {
10556
10646
  // src/apps/server/api/search.ts
10557
10647
  import { Hono as Hono3 } from "hono";
10558
10648
  var searchRouter = new Hono3();
10649
+ function isEmbeddingBackendUnavailable(error) {
10650
+ const message = error instanceof Error ? error.message : String(error);
10651
+ return /model file path or buffer|onnxruntime|transformers|embedding backend/i.test(message);
10652
+ }
10559
10653
  searchRouter.post("/", async (c) => {
10560
10654
  const memoryService = getServiceFromQuery(c);
10561
10655
  try {
@@ -10597,15 +10691,41 @@ searchRouter.post("/", async (c) => {
10597
10691
  });
10598
10692
  searchRouter.post("/disclosure", async (c) => {
10599
10693
  let memoryService;
10694
+ let body;
10600
10695
  try {
10601
- const body = await c.req.json();
10696
+ body = await c.req.json();
10602
10697
  if (!body.query) {
10603
10698
  return c.json({ error: "Query is required" }, 400);
10604
10699
  }
10605
- memoryService = body.options?.strategy === "fast" ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
10606
- await memoryService.initialize();
10607
- const result = await memoryService.searchDisclosure(body.query, body.options);
10608
- return c.json(result);
10700
+ const useFastStrategy = body.options?.strategy === "fast";
10701
+ memoryService = useFastStrategy ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
10702
+ try {
10703
+ await memoryService.initialize();
10704
+ const result = await memoryService.searchDisclosure(body.query, body.options);
10705
+ return c.json(result);
10706
+ } catch (error) {
10707
+ if (!useFastStrategy && isEmbeddingBackendUnavailable(error)) {
10708
+ await memoryService.shutdown();
10709
+ memoryService = getLightweightServiceFromQuery(c);
10710
+ await memoryService.initialize();
10711
+ const result = await memoryService.searchDisclosure(body.query, {
10712
+ ...body.options,
10713
+ strategy: "fast"
10714
+ });
10715
+ return c.json({
10716
+ ...result,
10717
+ meta: {
10718
+ ...result.meta,
10719
+ fallbackApplied: true,
10720
+ fallbackTrace: [
10721
+ ...result.meta.fallbackTrace || [],
10722
+ "fallback:embedding-backend-unavailable:fast"
10723
+ ]
10724
+ }
10725
+ });
10726
+ }
10727
+ throw error;
10728
+ }
10609
10729
  } catch (error) {
10610
10730
  return c.json({ error: error.message }, 500);
10611
10731
  } finally {
@@ -10613,7 +10733,7 @@ searchRouter.post("/disclosure", async (c) => {
10613
10733
  }
10614
10734
  });
10615
10735
  searchRouter.get("/disclosure/:resultId/expand", async (c) => {
10616
- const memoryService = getServiceFromQuery(c);
10736
+ const memoryService = getLightweightServiceFromQuery(c);
10617
10737
  try {
10618
10738
  const resultId = c.req.param("resultId");
10619
10739
  const rawWindowSize = c.req.query("windowSize");
@@ -10633,7 +10753,7 @@ searchRouter.get("/disclosure/:resultId/expand", async (c) => {
10633
10753
  }
10634
10754
  });
10635
10755
  searchRouter.get("/disclosure/:resultId/source", async (c) => {
10636
- const memoryService = getServiceFromQuery(c);
10756
+ const memoryService = getLightweightServiceFromQuery(c);
10637
10757
  try {
10638
10758
  const resultId = c.req.param("resultId");
10639
10759
  const result = await memoryService.sourceDisclosure(resultId);
@@ -11970,6 +12090,13 @@ import { Hono as Hono8 } from "hono";
11970
12090
  import { streamSSE } from "hono/streaming";
11971
12091
  import { spawn } from "child_process";
11972
12092
  var chatRouter = new Hono8();
12093
+ var ProviderFailure = class extends Error {
12094
+ constructor(code, message) {
12095
+ super(message);
12096
+ this.code = code;
12097
+ this.name = "ProviderFailure";
12098
+ }
12099
+ };
11973
12100
  var CLAUDE_TIMEOUT_MS = 12e4;
11974
12101
  chatRouter.post("/", async (c) => {
11975
12102
  let body;
@@ -11981,43 +12108,12 @@ chatRouter.post("/", async (c) => {
11981
12108
  if (!body.message?.trim()) {
11982
12109
  return c.json({ error: "Message is required" }, 400);
11983
12110
  }
11984
- const memoryService = getServiceFromQuery(c);
12111
+ const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
12112
+ const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
11985
12113
  try {
11986
12114
  await memoryService.initialize();
11987
- let memoryContext = "";
11988
- let statsContext = "";
11989
- try {
11990
- const result = await memoryService.retrieveMemories(body.message, {
11991
- topK: 8,
11992
- minScore: 0.5
11993
- });
11994
- if (result.memories.length > 0) {
11995
- const parts = ["## Relevant Memories\n"];
11996
- for (const m of result.memories) {
11997
- const date = new Date(m.event.timestamp).toISOString().split("T")[0];
11998
- const content = m.event.content.slice(0, 500);
11999
- parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
12000
- parts.push(content);
12001
- if (m.sessionContext) {
12002
- parts.push(`_Context: ${m.sessionContext}_`);
12003
- }
12004
- parts.push("");
12005
- }
12006
- memoryContext = parts.join("\n");
12007
- }
12008
- } catch {
12009
- }
12010
- try {
12011
- const stats = await memoryService.getStats();
12012
- const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
12013
- statsContext = [
12014
- "## Memory Stats",
12015
- `- Total events: ${stats.totalEvents}`,
12016
- `- Vector nodes: ${stats.vectorCount}`,
12017
- `- By level: ${levels}`
12018
- ].join("\n");
12019
- } catch {
12020
- }
12115
+ const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
12116
+ const statsContext = await collectStatsContext(memoryService);
12021
12117
  const fullPrompt = buildPrompt(
12022
12118
  statsContext,
12023
12119
  memoryContext,
@@ -12025,13 +12121,23 @@ chatRouter.post("/", async (c) => {
12025
12121
  body.message
12026
12122
  );
12027
12123
  return streamSSE(c, async (stream) => {
12124
+ if (memoryOnly) {
12125
+ await streamMemoryOnlyResponse(stream, {
12126
+ memoryContext,
12127
+ memoryHits,
12128
+ reason: "memory-only-mode"
12129
+ });
12130
+ return;
12131
+ }
12028
12132
  try {
12029
12133
  await streamClaudeResponse(fullPrompt, stream);
12030
12134
  } catch (err) {
12135
+ const diagnostic = providerDiagnostic(err);
12031
12136
  await stream.writeSSE({
12032
- event: "error",
12033
- data: JSON.stringify({ error: err.message })
12137
+ event: "provider_error",
12138
+ data: JSON.stringify(diagnostic)
12034
12139
  });
12140
+ await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
12035
12141
  }
12036
12142
  });
12037
12143
  } catch (error) {
@@ -12040,6 +12146,115 @@ chatRouter.post("/", async (c) => {
12040
12146
  await memoryService.shutdown();
12041
12147
  }
12042
12148
  });
12149
+ async function collectMemoryContext(memoryService, query) {
12150
+ let memoryHits = [];
12151
+ try {
12152
+ const result = await memoryService.retrieveMemories?.(query, {
12153
+ topK: 8,
12154
+ minScore: 0.5
12155
+ });
12156
+ memoryHits = result?.memories ?? [];
12157
+ } catch {
12158
+ memoryHits = [];
12159
+ }
12160
+ if (memoryHits.length === 0) {
12161
+ try {
12162
+ memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
12163
+ } catch {
12164
+ memoryHits = [];
12165
+ }
12166
+ }
12167
+ return {
12168
+ memoryContext: formatMemoryContext(memoryHits),
12169
+ memoryHits
12170
+ };
12171
+ }
12172
+ async function collectStatsContext(memoryService) {
12173
+ try {
12174
+ const stats = await memoryService.getStats?.();
12175
+ if (!stats)
12176
+ return "";
12177
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
12178
+ return [
12179
+ "## Memory Stats",
12180
+ `- Total events: ${stats.totalEvents}`,
12181
+ `- Vector nodes: ${stats.vectorCount}`,
12182
+ `- By level: ${levels}`
12183
+ ].join("\n");
12184
+ } catch {
12185
+ return "";
12186
+ }
12187
+ }
12188
+ function formatMemoryContext(memoryHits) {
12189
+ if (memoryHits.length === 0)
12190
+ return "";
12191
+ const parts = ["## Relevant Memories\n"];
12192
+ for (const m of memoryHits) {
12193
+ const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
12194
+ const content = (m.event.content ?? "").slice(0, 500);
12195
+ parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
12196
+ parts.push(content);
12197
+ if (m.sessionContext) {
12198
+ parts.push(`_Context: ${m.sessionContext}_`);
12199
+ }
12200
+ parts.push("");
12201
+ }
12202
+ return parts.join("\n");
12203
+ }
12204
+ async function streamMemoryOnlyResponse(stream, options) {
12205
+ await stream.writeSSE({
12206
+ event: "diagnostic",
12207
+ data: JSON.stringify({
12208
+ provider: "claude-cli",
12209
+ status: "skipped",
12210
+ mode: "memory-only",
12211
+ reason: options.reason,
12212
+ retrievedMemories: options.memoryHits.length
12213
+ })
12214
+ });
12215
+ await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
12216
+ }
12217
+ async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
12218
+ const content = memoryHits.length > 0 ? [
12219
+ "Provider unavailable or skipped; showing retrieved memory context directly.",
12220
+ "",
12221
+ memoryContext
12222
+ ].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
12223
+ await stream.writeSSE({
12224
+ event: "message",
12225
+ data: JSON.stringify({ content, mode: "memory-only" })
12226
+ });
12227
+ await stream.writeSSE({ event: "done", data: "{}" });
12228
+ }
12229
+ function providerDiagnostic(err) {
12230
+ if (err instanceof ProviderFailure) {
12231
+ return {
12232
+ provider: "claude-cli",
12233
+ code: err.code,
12234
+ message: err.message,
12235
+ fallback: "memory-only"
12236
+ };
12237
+ }
12238
+ return {
12239
+ provider: "claude-cli",
12240
+ code: "claude-cli-error",
12241
+ message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
12242
+ fallback: "memory-only"
12243
+ };
12244
+ }
12245
+ function classifyProviderFailure(message) {
12246
+ const normalized = message.toLowerCase();
12247
+ if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
12248
+ return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
12249
+ }
12250
+ if (normalized.includes("not found") || normalized.includes("enoent")) {
12251
+ return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
12252
+ }
12253
+ if (normalized.includes("timed out")) {
12254
+ return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
12255
+ }
12256
+ return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
12257
+ }
12043
12258
  function buildPrompt(statsContext, memoryContext, history, currentMessage) {
12044
12259
  const parts = [];
12045
12260
  parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
@@ -12082,12 +12297,13 @@ function streamClaudeResponse(prompt, stream) {
12082
12297
  });
12083
12298
  const timeout = setTimeout(() => {
12084
12299
  proc.kill("SIGTERM");
12085
- reject(new Error("Chat response timed out after 2 minutes"));
12300
+ reject(classifyProviderFailure("timed out"));
12086
12301
  }, CLAUDE_TIMEOUT_MS);
12087
12302
  proc.stdin.write(prompt);
12088
12303
  proc.stdin.end();
12089
12304
  let buffer = "";
12090
12305
  let lastSentText = "";
12306
+ let stderrText = "";
12091
12307
  proc.stdout.on("data", async (chunk) => {
12092
12308
  buffer += chunk.toString();
12093
12309
  const lines = buffer.split("\n");
@@ -12116,6 +12332,7 @@ function streamClaudeResponse(prompt, stream) {
12116
12332
  }
12117
12333
  });
12118
12334
  proc.stderr.on("data", (chunk) => {
12335
+ stderrText += chunk.toString();
12119
12336
  if (process.env.CLAUDE_MEMORY_DEBUG) {
12120
12337
  console.error("[chat] claude stderr:", chunk.toString());
12121
12338
  }
@@ -12123,7 +12340,7 @@ function streamClaudeResponse(prompt, stream) {
12123
12340
  proc.on("error", (err) => {
12124
12341
  clearTimeout(timeout);
12125
12342
  if (err.code === "ENOENT") {
12126
- reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
12343
+ reject(classifyProviderFailure("ENOENT not found"));
12127
12344
  } else {
12128
12345
  reject(err);
12129
12346
  }
@@ -12140,7 +12357,7 @@ function streamClaudeResponse(prompt, stream) {
12140
12357
  }
12141
12358
  }
12142
12359
  if (code !== 0 && code !== null) {
12143
- reject(new Error(`Claude CLI exited with code ${code}`));
12360
+ reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
12144
12361
  } else {
12145
12362
  resolve8();
12146
12363
  }
@@ -12189,6 +12406,49 @@ healthRouter.get("/", async (c) => {
12189
12406
  await memoryService.shutdown();
12190
12407
  }
12191
12408
  });
12409
+ healthRouter.post("/recover", async (c) => {
12410
+ const memoryService = getWritableServiceFromQuery(c);
12411
+ try {
12412
+ await memoryService.initialize();
12413
+ const body = await c.req.json().catch(() => ({}));
12414
+ const options = {};
12415
+ if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
12416
+ options.stuckThresholdMs = body.stuckThresholdMs;
12417
+ }
12418
+ if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
12419
+ options.maxRetries = body.maxRetries;
12420
+ }
12421
+ const before = await memoryService.getOutboxStats();
12422
+ const recovered = await memoryService.recoverStuckOutboxItems(options);
12423
+ const [stats, outbox] = await Promise.all([
12424
+ memoryService.getStats(),
12425
+ memoryService.getOutboxStats()
12426
+ ]);
12427
+ return c.json({
12428
+ status: "ok",
12429
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12430
+ recovered,
12431
+ before: {
12432
+ outbox: before
12433
+ },
12434
+ after: {
12435
+ storage: {
12436
+ totalEvents: stats.totalEvents,
12437
+ vectorCount: stats.vectorCount
12438
+ },
12439
+ outbox
12440
+ }
12441
+ });
12442
+ } catch (error) {
12443
+ return c.json({
12444
+ status: "error",
12445
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12446
+ error: error.message
12447
+ }, 500);
12448
+ } finally {
12449
+ await memoryService.shutdown();
12450
+ }
12451
+ });
12192
12452
 
12193
12453
  // src/apps/server/api/index.ts
12194
12454
  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);
@@ -13982,7 +14242,7 @@ async function runMarketContextCommand(options) {
13982
14242
  }
13983
14243
  }
13984
14244
  var program = new Command();
13985
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.35");
14245
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.37");
13986
14246
  program.command("market-context").description("Fetch read-only DART/FRED/Finnhub context with structured MarketContextSnapshot bull/bear/risk/catalyst analysis").option("--company <name>", "Company name for DART fallback search and report subject").option("--dart-corp-code <code>", "Exact DART corp_code for issuer-specific filings").option("--symbol <ticker>", "Listed ticker for Finnhub company profile").option("--providers <list>", "Comma-separated providers: dart,fred,finnhub").option("--fred-series <list>", "Comma-separated FRED series IDs").option("--json", "Print structured JSON including analysis.marketSnapshot").option("--no-snapshot", "Disable MarketContextSnapshot and DART company snapshot analysis").action(async (options) => {
13987
14247
  try {
13988
14248
  await runMarketContextCommand(options);
@@ -14217,11 +14477,18 @@ program.command("forget [eventId]").description("Remove memories from storage").
14217
14477
  process.exit(1);
14218
14478
  }
14219
14479
  });
14220
- program.command("process").description("Process pending embeddings").option("-p, --project <path>", "Project path (defaults to cwd)").action(async (options) => {
14480
+ program.command("process").description("Process pending embeddings").option("-p, --project <path>", "Project path (defaults to cwd)").option("--no-recover-stuck", "Skip stale processing outbox recovery before processing").action(async (options) => {
14221
14481
  const projectPath = options.project || process.cwd();
14222
14482
  const service = getMemoryServiceForProject(projectPath);
14223
14483
  try {
14224
14484
  await service.initialize();
14485
+ if (options.recoverStuck !== false) {
14486
+ const recovered = await service.recoverStuckOutboxItems();
14487
+ const recoveredCount = recovered.embedding.recoveredProcessing + recovered.embedding.retriedFailed + recovered.vector.recoveredProcessing + recovered.vector.retriedFailed;
14488
+ if (recoveredCount > 0) {
14489
+ console.log(`\u267B\uFE0F Recovered stuck outbox work: embedding=${recovered.embedding.recoveredProcessing}/${recovered.embedding.retriedFailed}, vector=${recovered.vector.recoveredProcessing}/${recovered.vector.retriedFailed}`);
14490
+ }
14491
+ }
14225
14492
  console.log("\u23F3 Processing pending embeddings...");
14226
14493
  const count = await service.processPendingEmbeddings();
14227
14494
  console.log(`\u2705 Processed ${count} embeddings`);