@memfork/cli 0.1.32 → 0.1.33

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.
@@ -58,18 +58,39 @@ async function handleApiConfig(res) {
58
58
  });
59
59
  }
60
60
  /**
61
- * Recall entries from a MemWal namespace using the SDK (signed requests).
61
+ * Per-namespace cache + global rate-limit backoff. The relayer caps usage at
62
+ * 500 weighted-requests/hour, so we must avoid redundant calls:
62
63
  *
63
- * The MemWal HTTP API requires a cryptographically signed request; a plain
64
- * Bearer token is rejected, so we use the SDK directly.
65
- *
66
- * A single broad query at a high limit returns the whole namespace: semantic
67
- * recall returns the top-`limit` nearest entries, and when a namespace holds
68
- * fewer than `limit` commits (the common case) that's all of them regardless
69
- * of relevance. Issuing one query instead of several keeps us well under the
70
- * relayer's 500 weighted-requests/hour budget.
64
+ * Cache each namespace's recall result for CACHE_TTL_MS the /api/facts
65
+ * and /api/history endpoints both recall the same namespace, and the UI
66
+ * polls on a timer, so most requests are served from cache for free.
67
+ * On a 429, parse retry_after_seconds and refuse ALL relayer calls until
68
+ * it elapses, serving stale cache instead. This stops the polling loop
69
+ * from continually re-arming the ban.
70
+ */
71
+ const CACHE_TTL_MS = 60_000;
72
+ const recallCache = new Map();
73
+ let rateLimitedUntil = 0;
74
+ function parseRetryAfterMs(err) {
75
+ const m = err.match(/retry_after_seconds"?\s*:\s*(\d+)/);
76
+ const secs = m ? Number(m[1]) : 300;
77
+ return secs * 1_000;
78
+ }
79
+ /**
80
+ * Recall entries from a MemWal namespace using the SDK (signed requests),
81
+ * with caching and 429 backoff. A single broad query at a high limit returns
82
+ * the whole namespace, since recall returns the top-`limit` nearest entries
83
+ * and namespaces typically hold fewer than `limit` commits.
71
84
  */
72
85
  async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
86
+ const now = Date.now();
87
+ const cached = recallCache.get(namespace);
88
+ // Fresh cache hit — no relayer call needed.
89
+ if (cached && now - cached.ts < CACHE_TTL_MS)
90
+ return cached.data;
91
+ // In a rate-limit backoff window — serve stale cache (or empty), don't call.
92
+ if (now < rateLimitedUntil)
93
+ return cached?.data ?? [];
73
94
  const mw = MemWal.create({ key, accountId, serverUrl: relayer, namespace });
74
95
  const seen = new Set();
75
96
  const out = [];
@@ -85,14 +106,19 @@ async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
85
106
  out.push({ blob_id: blobId, text: String(r.text ?? ""), distance: r.distance });
86
107
  }
87
108
  }
109
+ recallCache.set(namespace, { ts: now, data: out });
110
+ return out;
88
111
  }
89
112
  catch (e) {
90
- // Surface rate limits so the caller can log; swallow other transient errors.
91
- if (String(e).includes("429")) {
92
- console.warn("[memforks] MemWal rate limit hit — backing off:", String(e));
113
+ const msg = String(e);
114
+ if (msg.includes("429")) {
115
+ const backoff = parseRetryAfterMs(msg);
116
+ rateLimitedUntil = Date.now() + backoff;
117
+ console.warn(`[memforks] MemWal rate limit hit — pausing relayer calls for ${Math.round(backoff / 1000)}s, serving cached data.`);
93
118
  }
119
+ // Serve stale cache if we have it; otherwise empty.
120
+ return cached?.data ?? [];
94
121
  }
95
- return out;
96
122
  }
97
123
  async function handleApiFacts(res, url) {
98
124
  const branch = url.searchParams.get("branch") ?? "main";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memfork/cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "MemForks CLI — init, commit, recall, merge, install plugins",
5
5
  "repository": {
6
6
  "type": "git",