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.
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) {
@@ -12000,6 +12090,13 @@ import { Hono as Hono8 } from "hono";
12000
12090
  import { streamSSE } from "hono/streaming";
12001
12091
  import { spawn } from "child_process";
12002
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
+ };
12003
12100
  var CLAUDE_TIMEOUT_MS = 12e4;
12004
12101
  chatRouter.post("/", async (c) => {
12005
12102
  let body;
@@ -12011,43 +12108,12 @@ chatRouter.post("/", async (c) => {
12011
12108
  if (!body.message?.trim()) {
12012
12109
  return c.json({ error: "Message is required" }, 400);
12013
12110
  }
12014
- const memoryService = getServiceFromQuery(c);
12111
+ const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
12112
+ const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
12015
12113
  try {
12016
12114
  await memoryService.initialize();
12017
- let memoryContext = "";
12018
- let statsContext = "";
12019
- try {
12020
- const result = await memoryService.retrieveMemories(body.message, {
12021
- topK: 8,
12022
- minScore: 0.5
12023
- });
12024
- if (result.memories.length > 0) {
12025
- const parts = ["## Relevant Memories\n"];
12026
- for (const m of result.memories) {
12027
- const date = new Date(m.event.timestamp).toISOString().split("T")[0];
12028
- const content = m.event.content.slice(0, 500);
12029
- parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
12030
- parts.push(content);
12031
- if (m.sessionContext) {
12032
- parts.push(`_Context: ${m.sessionContext}_`);
12033
- }
12034
- parts.push("");
12035
- }
12036
- memoryContext = parts.join("\n");
12037
- }
12038
- } catch {
12039
- }
12040
- try {
12041
- const stats = await memoryService.getStats();
12042
- const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
12043
- statsContext = [
12044
- "## Memory Stats",
12045
- `- Total events: ${stats.totalEvents}`,
12046
- `- Vector nodes: ${stats.vectorCount}`,
12047
- `- By level: ${levels}`
12048
- ].join("\n");
12049
- } catch {
12050
- }
12115
+ const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
12116
+ const statsContext = await collectStatsContext(memoryService);
12051
12117
  const fullPrompt = buildPrompt(
12052
12118
  statsContext,
12053
12119
  memoryContext,
@@ -12055,13 +12121,23 @@ chatRouter.post("/", async (c) => {
12055
12121
  body.message
12056
12122
  );
12057
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
+ }
12058
12132
  try {
12059
12133
  await streamClaudeResponse(fullPrompt, stream);
12060
12134
  } catch (err) {
12135
+ const diagnostic = providerDiagnostic(err);
12061
12136
  await stream.writeSSE({
12062
- event: "error",
12063
- data: JSON.stringify({ error: err.message })
12137
+ event: "provider_error",
12138
+ data: JSON.stringify(diagnostic)
12064
12139
  });
12140
+ await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
12065
12141
  }
12066
12142
  });
12067
12143
  } catch (error) {
@@ -12070,6 +12146,115 @@ chatRouter.post("/", async (c) => {
12070
12146
  await memoryService.shutdown();
12071
12147
  }
12072
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
+ }
12073
12258
  function buildPrompt(statsContext, memoryContext, history, currentMessage) {
12074
12259
  const parts = [];
12075
12260
  parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
@@ -12112,12 +12297,13 @@ function streamClaudeResponse(prompt, stream) {
12112
12297
  });
12113
12298
  const timeout = setTimeout(() => {
12114
12299
  proc.kill("SIGTERM");
12115
- reject(new Error("Chat response timed out after 2 minutes"));
12300
+ reject(classifyProviderFailure("timed out"));
12116
12301
  }, CLAUDE_TIMEOUT_MS);
12117
12302
  proc.stdin.write(prompt);
12118
12303
  proc.stdin.end();
12119
12304
  let buffer = "";
12120
12305
  let lastSentText = "";
12306
+ let stderrText = "";
12121
12307
  proc.stdout.on("data", async (chunk) => {
12122
12308
  buffer += chunk.toString();
12123
12309
  const lines = buffer.split("\n");
@@ -12146,6 +12332,7 @@ function streamClaudeResponse(prompt, stream) {
12146
12332
  }
12147
12333
  });
12148
12334
  proc.stderr.on("data", (chunk) => {
12335
+ stderrText += chunk.toString();
12149
12336
  if (process.env.CLAUDE_MEMORY_DEBUG) {
12150
12337
  console.error("[chat] claude stderr:", chunk.toString());
12151
12338
  }
@@ -12153,7 +12340,7 @@ function streamClaudeResponse(prompt, stream) {
12153
12340
  proc.on("error", (err) => {
12154
12341
  clearTimeout(timeout);
12155
12342
  if (err.code === "ENOENT") {
12156
- reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
12343
+ reject(classifyProviderFailure("ENOENT not found"));
12157
12344
  } else {
12158
12345
  reject(err);
12159
12346
  }
@@ -12170,7 +12357,7 @@ function streamClaudeResponse(prompt, stream) {
12170
12357
  }
12171
12358
  }
12172
12359
  if (code !== 0 && code !== null) {
12173
- reject(new Error(`Claude CLI exited with code ${code}`));
12360
+ reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
12174
12361
  } else {
12175
12362
  resolve8();
12176
12363
  }
@@ -12219,6 +12406,49 @@ healthRouter.get("/", async (c) => {
12219
12406
  await memoryService.shutdown();
12220
12407
  }
12221
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
+ });
12222
12452
 
12223
12453
  // src/apps/server/api/index.ts
12224
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);
@@ -14012,7 +14242,7 @@ async function runMarketContextCommand(options) {
14012
14242
  }
14013
14243
  }
14014
14244
  var program = new Command();
14015
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.36");
14245
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.37");
14016
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) => {
14017
14247
  try {
14018
14248
  await runMarketContextCommand(options);
@@ -14247,11 +14477,18 @@ program.command("forget [eventId]").description("Remove memories from storage").
14247
14477
  process.exit(1);
14248
14478
  }
14249
14479
  });
14250
- 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) => {
14251
14481
  const projectPath = options.project || process.cwd();
14252
14482
  const service = getMemoryServiceForProject(projectPath);
14253
14483
  try {
14254
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
+ }
14255
14492
  console.log("\u23F3 Processing pending embeddings...");
14256
14493
  const count = await service.processPendingEmbeddings();
14257
14494
  console.log(`\u2705 Processed ${count} embeddings`);