claude-memory-layer 1.0.36 → 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.
@@ -2339,6 +2339,14 @@ function normalizeQueryRewriteKind(value) {
2339
2339
  return "none";
2340
2340
  }
2341
2341
  var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
2342
+ var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
2343
+ var DEFAULT_OUTBOX_MAX_RETRIES = 3;
2344
+ function emptyOutboxRecoveryResult() {
2345
+ return {
2346
+ embedding: { recoveredProcessing: 0, retriedFailed: 0 },
2347
+ vector: { recoveredProcessing: 0, retriedFailed: 0 }
2348
+ };
2349
+ }
2342
2350
  var SQLiteEventStore = class {
2343
2351
  db;
2344
2352
  initialized = false;
@@ -3089,7 +3097,9 @@ var SQLiteEventStore = class {
3089
3097
  const placeholders = ids.map(() => "?").join(",");
3090
3098
  sqliteRun(
3091
3099
  this.db,
3092
- `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
3100
+ `UPDATE embedding_outbox
3101
+ SET status = 'processing', processed_at = datetime('now'), error_message = NULL
3102
+ WHERE id IN (${placeholders})`,
3093
3103
  ids
3094
3104
  );
3095
3105
  return pending.map((row) => ({
@@ -3159,6 +3169,58 @@ var SQLiteEventStore = class {
3159
3169
  [error, ...ids]
3160
3170
  );
3161
3171
  }
3172
+ /**
3173
+ * Recover abandoned outbox work after a worker/process crash.
3174
+ *
3175
+ * Rows in `processing` are claimed work. If the process exits before marking
3176
+ * them done/failed, they otherwise remain invisible to future processing.
3177
+ * Recovery is deliberately age-gated so an active worker is not disturbed.
3178
+ */
3179
+ async recoverStuckOutboxItems(options = {}) {
3180
+ await this.initialize();
3181
+ const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
3182
+ const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
3183
+ const now = options.now ?? /* @__PURE__ */ new Date();
3184
+ const threshold = new Date(now.getTime() - thresholdMs).toISOString();
3185
+ const result = emptyOutboxRecoveryResult();
3186
+ const embeddingRecovered = sqliteRun(
3187
+ this.db,
3188
+ `UPDATE embedding_outbox
3189
+ SET status = 'pending', processed_at = NULL, error_message = NULL
3190
+ WHERE status = 'processing'
3191
+ AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
3192
+ [threshold]
3193
+ );
3194
+ result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
3195
+ const embeddingRetried = sqliteRun(
3196
+ this.db,
3197
+ `UPDATE embedding_outbox
3198
+ SET status = 'pending', error_message = NULL
3199
+ WHERE status = 'failed'
3200
+ AND retry_count < ?`,
3201
+ [maxRetries]
3202
+ );
3203
+ result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
3204
+ const vectorRecovered = sqliteRun(
3205
+ this.db,
3206
+ `UPDATE vector_outbox
3207
+ SET status = 'pending', updated_at = ?, error = NULL
3208
+ WHERE status = 'processing'
3209
+ AND datetime(updated_at) < datetime(?)`,
3210
+ [now.toISOString(), threshold]
3211
+ );
3212
+ result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
3213
+ const vectorRetried = sqliteRun(
3214
+ this.db,
3215
+ `UPDATE vector_outbox
3216
+ SET status = 'pending', updated_at = ?, error = NULL
3217
+ WHERE status = 'failed'
3218
+ AND retry_count < ?`,
3219
+ [now.toISOString(), maxRetries]
3220
+ );
3221
+ result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
3222
+ return result;
3223
+ }
3162
3224
  /**
3163
3225
  * Get embedding/vector outbox health statistics
3164
3226
  */
@@ -4030,6 +4092,7 @@ var VectorStore = class {
4030
4092
  * Get total count of vectors
4031
4093
  */
4032
4094
  async count() {
4095
+ await this.initialize();
4033
4096
  if (!this.table)
4034
4097
  return 0;
4035
4098
  const result = await this.table.countRows();
@@ -4468,6 +4531,10 @@ var MemoryQueryService = class {
4468
4531
  await this.initialize();
4469
4532
  return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
4470
4533
  }
4534
+ async recoverStuckOutboxItems(options) {
4535
+ await this.initialize();
4536
+ return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
4537
+ }
4471
4538
  async getStats() {
4472
4539
  await this.initialize();
4473
4540
  const deps = this.getStatsDeps();
@@ -7740,6 +7807,9 @@ var MemoryService = class {
7740
7807
  async getOutboxStats() {
7741
7808
  return this.queryService.getOutboxStats();
7742
7809
  }
7810
+ async recoverStuckOutboxItems(options) {
7811
+ return this.queryService.recoverStuckOutboxItems(options);
7812
+ }
7743
7813
  async getRetrievalTraceStats() {
7744
7814
  return this.retrievalAnalyticsService.getRetrievalTraceStats();
7745
7815
  }
@@ -8053,6 +8123,26 @@ function getServiceFromQuery(c) {
8053
8123
  }
8054
8124
  return getReadOnlyMemoryService();
8055
8125
  }
8126
+ function getWritableServiceFromQuery(c) {
8127
+ const project = c.req.query("project") || c.req.query("projectId");
8128
+ if (project) {
8129
+ const storagePath = resolveProjectStoragePath(project);
8130
+ return new MemoryService({
8131
+ storagePath,
8132
+ readOnly: false,
8133
+ lightweightMode: true,
8134
+ analyticsEnabled: false,
8135
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
8136
+ });
8137
+ }
8138
+ return new MemoryService({
8139
+ storagePath: "~/.claude-code/memory",
8140
+ readOnly: false,
8141
+ lightweightMode: true,
8142
+ analyticsEnabled: false,
8143
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
8144
+ });
8145
+ }
8056
8146
  function getLightweightServiceFromQuery(c) {
8057
8147
  const project = c.req.query("project") || c.req.query("projectId");
8058
8148
  if (project) {
@@ -9708,6 +9798,13 @@ import { Hono as Hono8 } from "hono";
9708
9798
  import { streamSSE } from "hono/streaming";
9709
9799
  import { spawn } from "child_process";
9710
9800
  var chatRouter = new Hono8();
9801
+ var ProviderFailure = class extends Error {
9802
+ constructor(code, message) {
9803
+ super(message);
9804
+ this.code = code;
9805
+ this.name = "ProviderFailure";
9806
+ }
9807
+ };
9711
9808
  var CLAUDE_TIMEOUT_MS = 12e4;
9712
9809
  chatRouter.post("/", async (c) => {
9713
9810
  let body;
@@ -9719,43 +9816,12 @@ chatRouter.post("/", async (c) => {
9719
9816
  if (!body.message?.trim()) {
9720
9817
  return c.json({ error: "Message is required" }, 400);
9721
9818
  }
9722
- const memoryService = getServiceFromQuery(c);
9819
+ const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
9820
+ const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
9723
9821
  try {
9724
9822
  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
- }
9823
+ const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
9824
+ const statsContext = await collectStatsContext(memoryService);
9759
9825
  const fullPrompt = buildPrompt(
9760
9826
  statsContext,
9761
9827
  memoryContext,
@@ -9763,13 +9829,23 @@ chatRouter.post("/", async (c) => {
9763
9829
  body.message
9764
9830
  );
9765
9831
  return streamSSE(c, async (stream) => {
9832
+ if (memoryOnly) {
9833
+ await streamMemoryOnlyResponse(stream, {
9834
+ memoryContext,
9835
+ memoryHits,
9836
+ reason: "memory-only-mode"
9837
+ });
9838
+ return;
9839
+ }
9766
9840
  try {
9767
9841
  await streamClaudeResponse(fullPrompt, stream);
9768
9842
  } catch (err) {
9843
+ const diagnostic = providerDiagnostic(err);
9769
9844
  await stream.writeSSE({
9770
- event: "error",
9771
- data: JSON.stringify({ error: err.message })
9845
+ event: "provider_error",
9846
+ data: JSON.stringify(diagnostic)
9772
9847
  });
9848
+ await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
9773
9849
  }
9774
9850
  });
9775
9851
  } catch (error) {
@@ -9778,6 +9854,115 @@ chatRouter.post("/", async (c) => {
9778
9854
  await memoryService.shutdown();
9779
9855
  }
9780
9856
  });
9857
+ async function collectMemoryContext(memoryService, query) {
9858
+ let memoryHits = [];
9859
+ try {
9860
+ const result = await memoryService.retrieveMemories?.(query, {
9861
+ topK: 8,
9862
+ minScore: 0.5
9863
+ });
9864
+ memoryHits = result?.memories ?? [];
9865
+ } catch {
9866
+ memoryHits = [];
9867
+ }
9868
+ if (memoryHits.length === 0) {
9869
+ try {
9870
+ memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
9871
+ } catch {
9872
+ memoryHits = [];
9873
+ }
9874
+ }
9875
+ return {
9876
+ memoryContext: formatMemoryContext(memoryHits),
9877
+ memoryHits
9878
+ };
9879
+ }
9880
+ async function collectStatsContext(memoryService) {
9881
+ try {
9882
+ const stats = await memoryService.getStats?.();
9883
+ if (!stats)
9884
+ return "";
9885
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
9886
+ return [
9887
+ "## Memory Stats",
9888
+ `- Total events: ${stats.totalEvents}`,
9889
+ `- Vector nodes: ${stats.vectorCount}`,
9890
+ `- By level: ${levels}`
9891
+ ].join("\n");
9892
+ } catch {
9893
+ return "";
9894
+ }
9895
+ }
9896
+ function formatMemoryContext(memoryHits) {
9897
+ if (memoryHits.length === 0)
9898
+ return "";
9899
+ const parts = ["## Relevant Memories\n"];
9900
+ for (const m of memoryHits) {
9901
+ const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
9902
+ const content = (m.event.content ?? "").slice(0, 500);
9903
+ parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
9904
+ parts.push(content);
9905
+ if (m.sessionContext) {
9906
+ parts.push(`_Context: ${m.sessionContext}_`);
9907
+ }
9908
+ parts.push("");
9909
+ }
9910
+ return parts.join("\n");
9911
+ }
9912
+ async function streamMemoryOnlyResponse(stream, options) {
9913
+ await stream.writeSSE({
9914
+ event: "diagnostic",
9915
+ data: JSON.stringify({
9916
+ provider: "claude-cli",
9917
+ status: "skipped",
9918
+ mode: "memory-only",
9919
+ reason: options.reason,
9920
+ retrievedMemories: options.memoryHits.length
9921
+ })
9922
+ });
9923
+ await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
9924
+ }
9925
+ async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
9926
+ const content = memoryHits.length > 0 ? [
9927
+ "Provider unavailable or skipped; showing retrieved memory context directly.",
9928
+ "",
9929
+ memoryContext
9930
+ ].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
9931
+ await stream.writeSSE({
9932
+ event: "message",
9933
+ data: JSON.stringify({ content, mode: "memory-only" })
9934
+ });
9935
+ await stream.writeSSE({ event: "done", data: "{}" });
9936
+ }
9937
+ function providerDiagnostic(err) {
9938
+ if (err instanceof ProviderFailure) {
9939
+ return {
9940
+ provider: "claude-cli",
9941
+ code: err.code,
9942
+ message: err.message,
9943
+ fallback: "memory-only"
9944
+ };
9945
+ }
9946
+ return {
9947
+ provider: "claude-cli",
9948
+ code: "claude-cli-error",
9949
+ message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
9950
+ fallback: "memory-only"
9951
+ };
9952
+ }
9953
+ function classifyProviderFailure(message) {
9954
+ const normalized = message.toLowerCase();
9955
+ if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
9956
+ return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
9957
+ }
9958
+ if (normalized.includes("not found") || normalized.includes("enoent")) {
9959
+ return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
9960
+ }
9961
+ if (normalized.includes("timed out")) {
9962
+ return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
9963
+ }
9964
+ return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
9965
+ }
9781
9966
  function buildPrompt(statsContext, memoryContext, history, currentMessage) {
9782
9967
  const parts = [];
9783
9968
  parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
@@ -9820,12 +10005,13 @@ function streamClaudeResponse(prompt, stream) {
9820
10005
  });
9821
10006
  const timeout = setTimeout(() => {
9822
10007
  proc.kill("SIGTERM");
9823
- reject(new Error("Chat response timed out after 2 minutes"));
10008
+ reject(classifyProviderFailure("timed out"));
9824
10009
  }, CLAUDE_TIMEOUT_MS);
9825
10010
  proc.stdin.write(prompt);
9826
10011
  proc.stdin.end();
9827
10012
  let buffer = "";
9828
10013
  let lastSentText = "";
10014
+ let stderrText = "";
9829
10015
  proc.stdout.on("data", async (chunk) => {
9830
10016
  buffer += chunk.toString();
9831
10017
  const lines = buffer.split("\n");
@@ -9854,6 +10040,7 @@ function streamClaudeResponse(prompt, stream) {
9854
10040
  }
9855
10041
  });
9856
10042
  proc.stderr.on("data", (chunk) => {
10043
+ stderrText += chunk.toString();
9857
10044
  if (process.env.CLAUDE_MEMORY_DEBUG) {
9858
10045
  console.error("[chat] claude stderr:", chunk.toString());
9859
10046
  }
@@ -9861,7 +10048,7 @@ function streamClaudeResponse(prompt, stream) {
9861
10048
  proc.on("error", (err) => {
9862
10049
  clearTimeout(timeout);
9863
10050
  if (err.code === "ENOENT") {
9864
- reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
10051
+ reject(classifyProviderFailure("ENOENT not found"));
9865
10052
  } else {
9866
10053
  reject(err);
9867
10054
  }
@@ -9878,7 +10065,7 @@ function streamClaudeResponse(prompt, stream) {
9878
10065
  }
9879
10066
  }
9880
10067
  if (code !== 0 && code !== null) {
9881
- reject(new Error(`Claude CLI exited with code ${code}`));
10068
+ reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
9882
10069
  } else {
9883
10070
  resolve4();
9884
10071
  }
@@ -9927,6 +10114,49 @@ healthRouter.get("/", async (c) => {
9927
10114
  await memoryService.shutdown();
9928
10115
  }
9929
10116
  });
10117
+ healthRouter.post("/recover", async (c) => {
10118
+ const memoryService = getWritableServiceFromQuery(c);
10119
+ try {
10120
+ await memoryService.initialize();
10121
+ const body = await c.req.json().catch(() => ({}));
10122
+ const options = {};
10123
+ if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
10124
+ options.stuckThresholdMs = body.stuckThresholdMs;
10125
+ }
10126
+ if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
10127
+ options.maxRetries = body.maxRetries;
10128
+ }
10129
+ const before = await memoryService.getOutboxStats();
10130
+ const recovered = await memoryService.recoverStuckOutboxItems(options);
10131
+ const [stats, outbox] = await Promise.all([
10132
+ memoryService.getStats(),
10133
+ memoryService.getOutboxStats()
10134
+ ]);
10135
+ return c.json({
10136
+ status: "ok",
10137
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10138
+ recovered,
10139
+ before: {
10140
+ outbox: before
10141
+ },
10142
+ after: {
10143
+ storage: {
10144
+ totalEvents: stats.totalEvents,
10145
+ vectorCount: stats.vectorCount
10146
+ },
10147
+ outbox
10148
+ }
10149
+ });
10150
+ } catch (error) {
10151
+ return c.json({
10152
+ status: "error",
10153
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10154
+ error: error.message
10155
+ }, 500);
10156
+ } finally {
10157
+ await memoryService.shutdown();
10158
+ }
10159
+ });
9930
10160
 
9931
10161
  // src/apps/server/api/index.ts
9932
10162
  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);