cf-memory-mcp 3.18.0 → 3.19.0

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.
@@ -215,7 +215,8 @@ const TOOLS_LIST = [
215
215
  branch: { type: 'string', description: 'Current git branch. Mismatch downgrades match_confidence but does not hide. Bridge auto-fills from git.' },
216
216
  session_id_hint: { type: 'string', description: 'Resume a specific session by id (overrides repo/branch matching). Full UUID or short prefix (>=8 chars). Useful after seeing recent_handoffs.' },
217
217
  max_age_minutes: { type: 'number', description: 'Hard cutoff: hide handoffs older than this many minutes. Without it, older handoffs surface with reduced confidence.' },
218
- goal_contains: { type: 'string', description: 'Substring filter on handoff content (goal/notes/decisions). Case-insensitive. Lets agents find a specific thread by what it was about.' }
218
+ goal_contains: { type: 'string', description: 'Substring filter on handoff content (goal/notes/decisions). Case-insensitive. Lets agents find a specific thread by what it was about.' },
219
+ since: { type: 'string', description: 'Lower bound on handoff timestamp (ISO 8601). Scope resume to a time window.' }
219
220
  }
220
221
  }
221
222
  },
@@ -945,7 +946,27 @@ class CFMemoryMCP {
945
946
  // (try/catch'd), and the agent's hand-written list always wins.
946
947
  this.trackToolActivity(message);
947
948
 
948
- let response = await this.makeRequest(message);
949
+ let response;
950
+ try {
951
+ response = await this.makeRequest(message);
952
+ } catch (err) {
953
+ // Network failure / worker unreachable. For resume bootstrap
954
+ // calls, serve from local disk so the agent isn't stuck
955
+ // when Cloudflare is down or the user is offline.
956
+ if (message.method === 'tools/call' &&
957
+ message.params?.name === 'get_context_bootstrap' &&
958
+ message.params.arguments?.resume) {
959
+ const fallback = this.loadResumeFromDisk(message.id);
960
+ if (fallback) {
961
+ _mcpTrace('DISPATCH_DONE', `id=${message.id} method=${message.method} fallback=disk elapsed=${Date.now()-_t0}ms`);
962
+ process.stdout.write(JSON.stringify(fallback) + '\n');
963
+ return;
964
+ }
965
+ }
966
+ // Not a resume call, or no disk fallback available: surface
967
+ // the original error to the client.
968
+ throw err;
969
+ }
949
970
  _mcpTrace('DISPATCH_DONE', `id=${message.id} method=${message.method} elapsed=${Date.now()-_t0}ms`);
950
971
 
951
972
  // Annotate retrieve_context results with local staleness when
@@ -988,14 +1009,21 @@ class CFMemoryMCP {
988
1009
  this.cacheImplicitSessionFromResponse(response);
989
1010
  }
990
1011
 
991
- // Inject bridge-side git diff into resume responses. Lets the
992
- // agent see what's changed in the repo since the handoff was
993
- // written files modified, commits added, branch moves
994
- // without having to call git themselves.
1012
+ // Resume bootstrap: if the server returned an error envelope
1013
+ // (not just a fetch failure), try the disk fallback. Then
1014
+ // run the normal diff injection + disk persist for success.
995
1015
  if (message.method === 'tools/call' &&
996
1016
  message.params?.name === 'get_context_bootstrap' &&
997
1017
  message.params.arguments?.resume) {
1018
+ if (response?.error) {
1019
+ const fallback = this.loadResumeFromDisk(message.id);
1020
+ if (fallback) {
1021
+ response = fallback;
1022
+ }
1023
+ }
998
1024
  this.injectRepoDiffIntoResume(response);
1025
+ // Persist to disk for offline fallback on the next call.
1026
+ this.saveResumeToDisk(response);
999
1027
  }
1000
1028
 
1001
1029
  // Send response to stdout
@@ -2744,6 +2772,92 @@ class CFMemoryMCP {
2744
2772
  .join('\n');
2745
2773
  }
2746
2774
 
2775
+ /**
2776
+ * Local-disk cache for the resume packet. Path is
2777
+ * ~/.cf-memory/handoff-<sha256-prefix-of-cwd>.json
2778
+ * Used as a fallback when the worker is unreachable (network down,
2779
+ * Cloudflare outage). Refreshed on every successful resume fetch.
2780
+ */
2781
+ getDiskCachePath() {
2782
+ try {
2783
+ const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
2784
+ const crypto = require('crypto');
2785
+ const hash = crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16);
2786
+ const dir = path.join(os.homedir(), '.cf-memory');
2787
+ return path.join(dir, `handoff-${hash}.json`);
2788
+ } catch (_) { return null; }
2789
+ }
2790
+
2791
+ /**
2792
+ * Persist a resume bootstrap response to local disk. Called after
2793
+ * a successful resume fetch (prewarm or live). Fire-and-forget;
2794
+ * disk errors are non-fatal.
2795
+ */
2796
+ saveResumeToDisk(response) {
2797
+ try {
2798
+ const optOut = process.env.CF_MEMORY_DISK_CACHE;
2799
+ if (optOut === '0' || optOut === 'false' || optOut === 'off') return;
2800
+ const p = this.getDiskCachePath();
2801
+ if (!p) return;
2802
+ const text = response?.result?.content?.[0]?.text;
2803
+ if (!text) return;
2804
+ // Only persist when there's actually a handoff to cache.
2805
+ try {
2806
+ const parsed = JSON.parse(text);
2807
+ if (!parsed?.resume_handoff) return;
2808
+ } catch (_) { return; }
2809
+ const dir = path.dirname(p);
2810
+ if (!fs.existsSync(dir)) {
2811
+ fs.mkdirSync(dir, { recursive: true });
2812
+ }
2813
+ const entry = {
2814
+ cached_at: new Date().toISOString(),
2815
+ cwd: process.env.CF_MEMORY_WATCH_PATH || process.cwd(),
2816
+ response_text: text,
2817
+ };
2818
+ fs.writeFileSync(p, JSON.stringify(entry, null, 2));
2819
+ _mcpTrace('DISK_CACHE', `saved resume packet to ${p}`);
2820
+ } catch (err) {
2821
+ this.logDebug(`saveResumeToDisk failed: ${err && err.message}`);
2822
+ }
2823
+ }
2824
+
2825
+ /**
2826
+ * Load the most-recent disk-cached resume packet for the current cwd.
2827
+ * Returns a response-shaped object the bridge can write to stdout, or
2828
+ * null when nothing cached / disk unreadable. The returned envelope
2829
+ * is tagged with a `from_disk_cache:true` marker + `disk_cache_age`
2830
+ * so the agent knows this is a fallback, not a fresh fetch.
2831
+ */
2832
+ loadResumeFromDisk(messageId) {
2833
+ try {
2834
+ const p = this.getDiskCachePath();
2835
+ if (!p || !fs.existsSync(p)) return null;
2836
+ const entry = JSON.parse(fs.readFileSync(p, 'utf8'));
2837
+ const parsed = JSON.parse(entry.response_text);
2838
+ const cachedAt = entry.cached_at;
2839
+ const ageMinutes = Math.round((Date.now() - new Date(cachedAt).getTime()) / 60000);
2840
+ // Annotate the envelope so the agent knows it's fallback.
2841
+ if (parsed.resume_handoff) {
2842
+ parsed.resume_handoff.from_disk_cache = true;
2843
+ parsed.resume_handoff.disk_cache_age_minutes = ageMinutes;
2844
+ parsed.resume_handoff.disk_cache_cached_at = cachedAt;
2845
+ }
2846
+ // Replace empty_hint with one that explains the fallback.
2847
+ parsed.empty_hint = `Worker unreachable; served from local disk cache (cached ${ageMinutes} min ago at ${cachedAt}). Refresh by calling get_context_bootstrap when the worker is back online.`;
2848
+ const response = {
2849
+ jsonrpc: '2.0',
2850
+ id: messageId,
2851
+ result: { content: [{ type: 'text', text: JSON.stringify(parsed) }] },
2852
+ };
2853
+ _mcpTrace('DISK_CACHE', `served resume from disk (age=${ageMinutes}min)`);
2854
+ return response;
2855
+ } catch (err) {
2856
+ this.logDebug(`loadResumeFromDisk failed: ${err && err.message}`);
2857
+ return null;
2858
+ }
2859
+ }
2860
+
2747
2861
  /**
2748
2862
  * Pre-fetch the resume handoff for the current cwd at bridge startup.
2749
2863
  * The result is cached and surfaced when the agent's first call is
@@ -2785,6 +2899,8 @@ class CFMemoryMCP {
2785
2899
  const parsed = JSON.parse(text || '{}');
2786
2900
  handoff_present = !!parsed.resume_handoff;
2787
2901
  } catch (_) { /* ignore */ }
2902
+ // Persist to disk for offline fallback. Best-effort.
2903
+ if (handoff_present) this.saveResumeToDisk(response);
2788
2904
  _mcpTrace('RESUME_PREWARM', `elapsed=${elapsed}ms handoff_present=${handoff_present}`);
2789
2905
  } catch (err) {
2790
2906
  this.logDebug(`prewarmResumeContext failed: ${err && err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "description": "Cloudflare-hosted MCP server for code indexing, retrieval, and assistant memory with a direct remote MCP endpoint and local stdio bridge.",
5
5
  "main": "bin/cf-memory-mcp.js",
6
6
  "bin": {