mr-memory 3.7.4 → 3.7.5

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/index.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * BYOK — provider API keys never touch MemoryRouter.
9
9
  */
10
10
  import { readFile, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
11
+ import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemory } from "./memoryrouter-api.js";
11
12
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
12
13
  import os from "node:os";
13
14
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
@@ -265,9 +266,6 @@ const WORKSPACE_FILES = [
265
266
  "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
266
267
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
267
268
  ];
268
- // ──────────────────────────────────────────────────────
269
- // Helpers
270
- // ──────────────────────────────────────────────────────
271
269
  function openClawConfigPath() {
272
270
  return process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
273
271
  }
@@ -291,7 +289,8 @@ async function writeOpenClawConfigFile(config) {
291
289
  if (err?.code !== "ENOENT")
292
290
  throw err;
293
291
  }
294
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
292
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
293
+ `, "utf8");
295
294
  }
296
295
  async function updateOpenClawPluginEntry(api, patch) {
297
296
  const config = await readOpenClawConfigFile();
@@ -545,17 +544,10 @@ If setting up MemoryRouter for another agent:
545
544
  contextPayload.push({ role: "user", content: taskPrompt });
546
545
  }
547
546
  }
548
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
549
- method: "POST",
550
- headers: {
551
- "Content-Type": "application/json",
552
- Authorization: `Bearer ${activeKey}`,
553
- },
554
- body: JSON.stringify({
555
- messages: contextPayload,
556
- density,
557
- ...(embeddings && { embeddings }),
558
- }),
547
+ const res = await prepareMemory(endpoint, activeKey, {
548
+ messages: contextPayload,
549
+ density,
550
+ ...(embeddings && { embeddings }),
559
551
  });
560
552
  if (!res.ok) {
561
553
  const errBody = await res.json().catch(() => null);
@@ -650,17 +642,10 @@ If setting up MemoryRouter for another agent:
650
642
  }
651
643
  }
652
644
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
653
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
654
- method: "POST",
655
- headers: {
656
- "Content-Type": "application/json",
657
- Authorization: `Bearer ${activeKey}`,
658
- },
659
- body: JSON.stringify({
660
- messages: contextPayload,
661
- density,
662
- ...(embeddings && { embeddings }),
663
- }),
645
+ const res = await prepareMemory(endpoint, activeKey, {
646
+ messages: contextPayload,
647
+ density,
648
+ ...(embeddings && { embeddings }),
664
649
  });
665
650
  if (!res.ok) {
666
651
  const errBody = await res.json().catch(() => null);
@@ -762,17 +747,10 @@ If setting up MemoryRouter for another agent:
762
747
  return;
763
748
  }
764
749
  // Fire and forget — don't block message delivery waiting for ingest
765
- fetch(`${endpoint}/v1/memory/ingest`, {
766
- method: "POST",
767
- headers: {
768
- "Content-Type": "application/json",
769
- Authorization: `Bearer ${activeKey}`,
770
- },
771
- body: JSON.stringify({
772
- messages: toStore,
773
- model: "unknown",
774
- ...(embeddings && { embeddings }),
775
- }),
750
+ ingestMemory(endpoint, activeKey, {
751
+ messages: toStore,
752
+ model: "unknown",
753
+ ...(embeddings && { embeddings }),
776
754
  }).then(async (res) => {
777
755
  if (!res.ok) {
778
756
  const details = await res.text().catch(() => "");
@@ -822,15 +800,7 @@ If setting up MemoryRouter for another agent:
822
800
  return jsonToolResult({ results: [], error: "query required" });
823
801
  const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
824
802
  try {
825
- const res = await fetch(`${endpoint}/v1/memory/search`, {
826
- method: "POST",
827
- headers: {
828
- Authorization: `Bearer ${toolKey}`,
829
- "Content-Type": "application/json",
830
- ...(embeddings && { "X-Embedding-Model": embeddings }),
831
- },
832
- body: JSON.stringify({ query, limit }),
833
- });
803
+ const res = await searchMemory(endpoint, toolKey, { query, limit, embeddings });
834
804
  if (!res.ok) {
835
805
  const errBody = await res.text().catch(() => "");
836
806
  return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
@@ -1023,12 +993,7 @@ If setting up MemoryRouter for another agent:
1023
993
  return;
1024
994
  }
1025
995
  try {
1026
- const statsUrl = embeddings
1027
- ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
1028
- : `${endpoint}/v1/memory/stats`;
1029
- const res = await fetch(statsUrl, {
1030
- headers: { Authorization: `Bearer ${effectiveKey}` },
1031
- });
996
+ const res = await getMemoryStats(endpoint, effectiveKey, embeddings);
1032
997
  const data = (await res.json());
1033
998
  if (opts.json) {
1034
999
  console.log(JSON.stringify({ enabled: true, key: effectiveKey, density, stats: data }, null, 2));
@@ -1086,15 +1051,7 @@ If setting up MemoryRouter for another agent:
1086
1051
  }
1087
1052
  const limit = parseInt(opts.limit, 10) || 50;
1088
1053
  try {
1089
- const res = await fetch(`${endpoint}/v1/memory/search`, {
1090
- method: "POST",
1091
- headers: {
1092
- Authorization: `Bearer ${effectiveKey}`,
1093
- "Content-Type": "application/json",
1094
- ...(embeddings && { "X-Embedding-Model": embeddings }),
1095
- },
1096
- body: JSON.stringify({ query, limit }),
1097
- });
1054
+ const res = await searchMemory(endpoint, effectiveKey, { query, limit, embeddings });
1098
1055
  if (!res.ok) {
1099
1056
  const err = await res.json();
1100
1057
  console.error(`Search failed: ${err.error || `HTTP ${res.status}`}`);
@@ -1142,13 +1099,7 @@ If setting up MemoryRouter for another agent:
1142
1099
  return;
1143
1100
  }
1144
1101
  try {
1145
- const deleteUrl = embeddings
1146
- ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
1147
- : `${endpoint}/v1/memory`;
1148
- const res = await fetch(deleteUrl, {
1149
- method: "DELETE",
1150
- headers: { Authorization: `Bearer ${effectiveKey}` },
1151
- });
1102
+ const res = await deleteMemories(endpoint, effectiveKey, embeddings);
1152
1103
  const data = (await res.json());
1153
1104
  const modelLabel = embeddings ? ` (${embeddings})` : "";
1154
1105
  console.log(`✓ ${data.message || `Vault cleared${modelLabel}`}`);
package/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { readFile, readdir, stat, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
12
+ import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemory } from "./memoryrouter-api.js";
12
13
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
13
14
  import path from "node:path";
14
15
  import os from "node:os";
@@ -570,17 +571,10 @@ If setting up MemoryRouter for another agent:
570
571
  }
571
572
 
572
573
 
573
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
574
- method: "POST",
575
- headers: {
576
- "Content-Type": "application/json",
577
- Authorization: `Bearer ${activeKey}`,
578
- },
579
- body: JSON.stringify({
580
- messages: contextPayload,
581
- density,
582
- ...(embeddings && { embeddings }),
583
- }),
574
+ const res = await prepareMemory(endpoint, activeKey, {
575
+ messages: contextPayload,
576
+ density,
577
+ ...(embeddings && { embeddings }),
584
578
  });
585
579
 
586
580
  if (!res.ok) {
@@ -686,17 +680,10 @@ If setting up MemoryRouter for another agent:
686
680
 
687
681
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
688
682
 
689
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
690
- method: "POST",
691
- headers: {
692
- "Content-Type": "application/json",
693
- Authorization: `Bearer ${activeKey}`,
694
- },
695
- body: JSON.stringify({
696
- messages: contextPayload,
697
- density,
698
- ...(embeddings && { embeddings }),
699
- }),
683
+ const res = await prepareMemory(endpoint, activeKey, {
684
+ messages: contextPayload,
685
+ density,
686
+ ...(embeddings && { embeddings }),
700
687
  });
701
688
 
702
689
  if (!res.ok) {
@@ -811,17 +798,10 @@ If setting up MemoryRouter for another agent:
811
798
  }
812
799
 
813
800
  // Fire and forget — don't block message delivery waiting for ingest
814
- fetch(`${endpoint}/v1/memory/ingest`, {
815
- method: "POST",
816
- headers: {
817
- "Content-Type": "application/json",
818
- Authorization: `Bearer ${activeKey}`,
819
- },
820
- body: JSON.stringify({
821
- messages: toStore,
822
- model: "unknown",
823
- ...(embeddings && { embeddings }),
824
- }),
801
+ ingestMemory(endpoint, activeKey, {
802
+ messages: toStore,
803
+ model: "unknown",
804
+ ...(embeddings && { embeddings }),
825
805
  }).then(async (res) => {
826
806
  if (!res.ok) {
827
807
  const details = await res.text().catch(() => "");
@@ -869,15 +849,7 @@ If setting up MemoryRouter for another agent:
869
849
  if (!query) return jsonToolResult({ results: [], error: "query required" });
870
850
  const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
871
851
  try {
872
- const res = await fetch(`${endpoint}/v1/memory/search`, {
873
- method: "POST",
874
- headers: {
875
- Authorization: `Bearer ${toolKey}`,
876
- "Content-Type": "application/json",
877
- ...(embeddings && { "X-Embedding-Model": embeddings }),
878
- },
879
- body: JSON.stringify({ query, limit }),
880
- });
852
+ const res = await searchMemory(endpoint, toolKey, { query, limit, embeddings });
881
853
  if (!res.ok) {
882
854
  const errBody = await res.text().catch(() => "");
883
855
  return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
@@ -1075,12 +1047,7 @@ If setting up MemoryRouter for another agent:
1075
1047
  return;
1076
1048
  }
1077
1049
  try {
1078
- const statsUrl = embeddings
1079
- ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
1080
- : `${endpoint}/v1/memory/stats`;
1081
- const res = await fetch(statsUrl, {
1082
- headers: { Authorization: `Bearer ${effectiveKey}` },
1083
- });
1050
+ const res = await getMemoryStats(endpoint, effectiveKey, embeddings);
1084
1051
  const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
1085
1052
  if (opts.json) {
1086
1053
  console.log(JSON.stringify({ enabled: true, key: effectiveKey, density, stats: data }, null, 2));
@@ -1132,15 +1099,7 @@ If setting up MemoryRouter for another agent:
1132
1099
  if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1133
1100
  const limit = parseInt(opts.limit, 10) || 50;
1134
1101
  try {
1135
- const res = await fetch(`${endpoint}/v1/memory/search`, {
1136
- method: "POST",
1137
- headers: {
1138
- Authorization: `Bearer ${effectiveKey}`,
1139
- "Content-Type": "application/json",
1140
- ...(embeddings && { "X-Embedding-Model": embeddings }),
1141
- },
1142
- body: JSON.stringify({ query, limit }),
1143
- });
1102
+ const res = await searchMemory(endpoint, effectiveKey, { query, limit, embeddings });
1144
1103
  if (!res.ok) {
1145
1104
  const err = await res.json() as { error?: string };
1146
1105
  console.error(`Search failed: ${err.error || `HTTP ${res.status}`}`);
@@ -1205,13 +1164,7 @@ If setting up MemoryRouter for another agent:
1205
1164
  const effectiveKey = opts.key || memoryKey;
1206
1165
  if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1207
1166
  try {
1208
- const deleteUrl = embeddings
1209
- ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
1210
- : `${endpoint}/v1/memory`;
1211
- const res = await fetch(deleteUrl, {
1212
- method: "DELETE",
1213
- headers: { Authorization: `Bearer ${effectiveKey}` },
1214
- });
1167
+ const res = await deleteMemories(endpoint, effectiveKey, embeddings);
1215
1168
  const data = (await res.json()) as { message?: string };
1216
1169
  const modelLabel = embeddings ? ` (${embeddings})` : "";
1217
1170
  console.log(`✓ ${data.message || `Vault cleared${modelLabel}`}`);
@@ -0,0 +1,49 @@
1
+ const JSON_HEADERS = { "Content-Type": "application/json" };
2
+ export async function prepareMemory(endpoint, key, payload) {
3
+ return fetch(`${endpoint}/v1/memory/prepare`, {
4
+ method: "POST",
5
+ headers: {
6
+ ...JSON_HEADERS,
7
+ Authorization: `Bearer ${key}`,
8
+ },
9
+ body: JSON.stringify(payload),
10
+ });
11
+ }
12
+ export function ingestMemory(endpoint, key, payload) {
13
+ return fetch(`${endpoint}/v1/memory/ingest`, {
14
+ method: "POST",
15
+ headers: {
16
+ ...JSON_HEADERS,
17
+ Authorization: `Bearer ${key}`,
18
+ },
19
+ body: JSON.stringify(payload),
20
+ });
21
+ }
22
+ export async function searchMemory(endpoint, key, payload) {
23
+ return fetch(`${endpoint}/v1/memory/search`, {
24
+ method: "POST",
25
+ headers: {
26
+ Authorization: `Bearer ${key}`,
27
+ "Content-Type": "application/json",
28
+ ...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
29
+ },
30
+ body: JSON.stringify({ query: payload.query, limit: payload.limit }),
31
+ });
32
+ }
33
+ export async function getMemoryStats(endpoint, key, embeddings) {
34
+ const statsUrl = embeddings
35
+ ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
36
+ : `${endpoint}/v1/memory/stats`;
37
+ return fetch(statsUrl, {
38
+ headers: { Authorization: `Bearer ${key}` },
39
+ });
40
+ }
41
+ export async function deleteMemories(endpoint, key, embeddings) {
42
+ const deleteUrl = embeddings
43
+ ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
44
+ : `${endpoint}/v1/memory`;
45
+ return fetch(deleteUrl, {
46
+ method: "DELETE",
47
+ headers: { Authorization: `Bearer ${key}` },
48
+ });
49
+ }
@@ -0,0 +1,60 @@
1
+ const JSON_HEADERS = { "Content-Type": "application/json" };
2
+
3
+ type PreparePayload = {
4
+ messages: Array<{ role: string; content: string }>;
5
+ density: string;
6
+ embeddings?: string;
7
+ };
8
+
9
+ export async function prepareMemory(endpoint: string, key: string, payload: PreparePayload): Promise<Response> {
10
+ return fetch(`${endpoint}/v1/memory/prepare`, {
11
+ method: "POST",
12
+ headers: {
13
+ ...JSON_HEADERS,
14
+ Authorization: `Bearer ${key}`,
15
+ },
16
+ body: JSON.stringify(payload),
17
+ });
18
+ }
19
+
20
+ export function ingestMemory(endpoint: string, key: string, payload: { messages: Array<{ role: string; content: string }>; model: string; embeddings?: string }): Promise<Response> {
21
+ return fetch(`${endpoint}/v1/memory/ingest`, {
22
+ method: "POST",
23
+ headers: {
24
+ ...JSON_HEADERS,
25
+ Authorization: `Bearer ${key}`,
26
+ },
27
+ body: JSON.stringify(payload),
28
+ });
29
+ }
30
+
31
+ export async function searchMemory(endpoint: string, key: string, payload: { query: string; limit: number; embeddings?: string }): Promise<Response> {
32
+ return fetch(`${endpoint}/v1/memory/search`, {
33
+ method: "POST",
34
+ headers: {
35
+ Authorization: `Bearer ${key}`,
36
+ "Content-Type": "application/json",
37
+ ...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
38
+ },
39
+ body: JSON.stringify({ query: payload.query, limit: payload.limit }),
40
+ });
41
+ }
42
+
43
+ export async function getMemoryStats(endpoint: string, key: string, embeddings?: string): Promise<Response> {
44
+ const statsUrl = embeddings
45
+ ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
46
+ : `${endpoint}/v1/memory/stats`;
47
+ return fetch(statsUrl, {
48
+ headers: { Authorization: `Bearer ${key}` },
49
+ });
50
+ }
51
+
52
+ export async function deleteMemories(endpoint: string, key: string, embeddings?: string): Promise<Response> {
53
+ const deleteUrl = embeddings
54
+ ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
55
+ : `${endpoint}/v1/memory`;
56
+ return fetch(deleteUrl, {
57
+ method: "DELETE",
58
+ headers: { Authorization: `Bearer ${key}` },
59
+ });
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.7.4",
3
+ "version": "3.7.5",
4
4
  "description": "mr-memory is the MemoryRouter plugin for OpenClaw — persistent memory across every conversation",
5
5
  "type": "module",
6
6
  "files": [
@@ -9,6 +9,10 @@
9
9
  "upload.js",
10
10
  "index.ts",
11
11
  "upload.ts",
12
+ "memoryrouter-api.js",
13
+ "memoryrouter-api.ts",
14
+ "upload-api.js",
15
+ "upload-api.ts",
12
16
  "openclaw.plugin.json"
13
17
  ],
14
18
  "keywords": [
package/upload-api.js ADDED
@@ -0,0 +1,40 @@
1
+ const MAX_HTTP_RETRIES = 3;
2
+ function sleep(ms) {
3
+ return new Promise((resolve) => setTimeout(resolve, ms));
4
+ }
5
+ export async function checkMemoryRouterHealth(endpoint) {
6
+ try {
7
+ const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
8
+ return res.ok;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export async function uploadMemoryBatch(params) {
15
+ const uploadUrl = `${params.endpoint}/v1/memory/upload`;
16
+ let lastError = null;
17
+ for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
18
+ try {
19
+ const res = await fetch(uploadUrl, {
20
+ method: "POST",
21
+ headers: {
22
+ Authorization: `Bearer ${params.memoryKey}`,
23
+ "Content-Type": "text/plain",
24
+ ...(params.embeddings && { "X-Embedding-Model": params.embeddings }),
25
+ },
26
+ body: params.jsonlBody,
27
+ signal: AbortSignal.timeout(30000),
28
+ });
29
+ if (res.ok || res.status < 500)
30
+ return res;
31
+ lastError = new Error(`HTTP ${res.status}`);
32
+ }
33
+ catch (err) {
34
+ lastError = err instanceof Error ? err : new Error(String(err));
35
+ }
36
+ if (attempt < MAX_HTTP_RETRIES)
37
+ await sleep(1000 * attempt);
38
+ }
39
+ throw lastError ?? new Error(`${params.label}: failed after ${MAX_HTTP_RETRIES} attempts`);
40
+ }
package/upload-api.ts ADDED
@@ -0,0 +1,45 @@
1
+ const MAX_HTTP_RETRIES = 3;
2
+
3
+ function sleep(ms: number): Promise<void> {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+
7
+ export async function checkMemoryRouterHealth(endpoint: string): Promise<boolean> {
8
+ try {
9
+ const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
10
+ return res.ok;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export async function uploadMemoryBatch(params: {
17
+ endpoint: string;
18
+ memoryKey: string;
19
+ jsonlBody: string;
20
+ embeddings?: string;
21
+ label: string;
22
+ }): Promise<Response> {
23
+ const uploadUrl = `${params.endpoint}/v1/memory/upload`;
24
+ let lastError: Error | null = null;
25
+ for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
26
+ try {
27
+ const res = await fetch(uploadUrl, {
28
+ method: "POST",
29
+ headers: {
30
+ Authorization: `Bearer ${params.memoryKey}`,
31
+ "Content-Type": "text/plain",
32
+ ...(params.embeddings && { "X-Embedding-Model": params.embeddings }),
33
+ },
34
+ body: params.jsonlBody,
35
+ signal: AbortSignal.timeout(30000),
36
+ });
37
+ if (res.ok || res.status < 500) return res;
38
+ lastError = new Error(`HTTP ${res.status}`);
39
+ } catch (err) {
40
+ lastError = err instanceof Error ? err : new Error(String(err));
41
+ }
42
+ if (attempt < MAX_HTTP_RETRIES) await sleep(1000 * attempt);
43
+ }
44
+ throw lastError ?? new Error(`${params.label}: failed after ${MAX_HTTP_RETRIES} attempts`);
45
+ }
package/upload.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { checkMemoryRouterHealth, uploadMemoryBatch } from "./upload-api.js";
6
7
  // ── Sanitization utilities (shared with index.ts ingest logic)
7
8
  // These MUST stay in sync with the patterns in index.ts
8
9
  const MEMORY_TAG_RE = /<mr-memory>[\s\S]*?<\/mr-memory>\s*/g;
@@ -132,7 +133,6 @@ const MAX_BATCH_BYTES = 2_000_000;
132
133
  const MAX_BATCH_COUNT_DEFAULT = 100;
133
134
  const MAX_BATCH_COUNT_QWEN = 25;
134
135
  const BATCH_SLEEP_MS = 150;
135
- const MAX_HTTP_RETRIES = 3;
136
136
  function sleep(ms) {
137
137
  return new Promise((resolve) => setTimeout(resolve, ms));
138
138
  }
@@ -302,33 +302,10 @@ async function discoverBrainFiles(stateDir) {
302
302
  }
303
303
  return files;
304
304
  }
305
- async function fetchWithRetry(url, init, label) {
306
- let lastError = null;
307
- for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
308
- try {
309
- const res = await fetch(url, { ...init, signal: AbortSignal.timeout(30000) });
310
- if (res.ok || res.status < 500)
311
- return res;
312
- lastError = new Error(`HTTP ${res.status}`);
313
- }
314
- catch (err) {
315
- lastError = err instanceof Error ? err : new Error(String(err));
316
- }
317
- if (attempt < MAX_HTTP_RETRIES)
318
- await sleep(1000 * attempt);
319
- }
320
- throw lastError ?? new Error(`${label}: failed after ${MAX_HTTP_RETRIES} attempts`);
321
- }
322
305
  export async function runUpload(params) {
323
306
  const { memoryKey, endpoint, targetPath, stateDir, embeddings } = params;
324
- const uploadUrl = `${endpoint}/v1/memory/upload`;
325
307
  // Validate API reachability
326
- try {
327
- const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
328
- if (!res.ok)
329
- throw new Error(`HTTP ${res.status}`);
330
- }
331
- catch {
308
+ if (!(await checkMemoryRouterHealth(endpoint))) {
332
309
  console.error("Error: Could not reach MemoryRouter API.");
333
310
  return;
334
311
  }
@@ -427,15 +404,13 @@ export async function runUpload(params) {
427
404
  process.stdout.write(` Batch ${i + 1}/${batches.length} (${batch.length} items)... `);
428
405
  }
429
406
  try {
430
- const response = await fetchWithRetry(uploadUrl, {
431
- method: "POST",
432
- headers: {
433
- Authorization: `Bearer ${memoryKey}`,
434
- "Content-Type": "text/plain",
435
- ...(embeddings && { "X-Embedding-Model": embeddings }),
436
- },
437
- body: jsonlBody,
438
- }, `Batch ${i + 1}`);
407
+ const response = await uploadMemoryBatch({
408
+ endpoint,
409
+ memoryKey,
410
+ jsonlBody,
411
+ embeddings,
412
+ label: `Batch ${i + 1}`,
413
+ });
439
414
  const result = (await response.json());
440
415
  const batchStored = result.stats?.stored ?? result.stats?.inputItems ?? batch.length;
441
416
  const batchFailed = result.stats?.failed ?? 0;
package/upload.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
+ import { checkMemoryRouterHealth, uploadMemoryBatch } from "./upload-api.js";
7
8
 
8
9
  // ── Sanitization utilities (shared with index.ts ingest logic)
9
10
  // These MUST stay in sync with the patterns in index.ts
@@ -146,7 +147,6 @@ const MAX_BATCH_BYTES = 2_000_000;
146
147
  const MAX_BATCH_COUNT_DEFAULT = 100;
147
148
  const MAX_BATCH_COUNT_QWEN = 25;
148
149
  const BATCH_SLEEP_MS = 150;
149
- const MAX_HTTP_RETRIES = 3;
150
150
 
151
151
  function sleep(ms: number): Promise<void> {
152
152
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -327,25 +327,6 @@ async function discoverBrainFiles(stateDir: string): Promise<string[]> {
327
327
  return files;
328
328
  }
329
329
 
330
- async function fetchWithRetry(
331
- url: string,
332
- init: RequestInit,
333
- label: string,
334
- ): Promise<Response> {
335
- let lastError: Error | null = null;
336
- for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
337
- try {
338
- const res = await fetch(url, { ...init, signal: AbortSignal.timeout(30000) });
339
- if (res.ok || res.status < 500) return res;
340
- lastError = new Error(`HTTP ${res.status}`);
341
- } catch (err) {
342
- lastError = err instanceof Error ? err : new Error(String(err));
343
- }
344
- if (attempt < MAX_HTTP_RETRIES) await sleep(1000 * attempt);
345
- }
346
- throw lastError ?? new Error(`${label}: failed after ${MAX_HTTP_RETRIES} attempts`);
347
- }
348
-
349
330
  export async function runUpload(params: {
350
331
  memoryKey: string;
351
332
  endpoint: string;
@@ -357,13 +338,8 @@ export async function runUpload(params: {
357
338
  embeddings?: string;
358
339
  }): Promise<void> {
359
340
  const { memoryKey, endpoint, targetPath, stateDir, embeddings } = params;
360
- const uploadUrl = `${endpoint}/v1/memory/upload`;
361
-
362
341
  // Validate API reachability
363
- try {
364
- const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
365
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
366
- } catch {
342
+ if (!(await checkMemoryRouterHealth(endpoint))) {
367
343
  console.error("Error: Could not reach MemoryRouter API.");
368
344
  return;
369
345
  }
@@ -467,19 +443,13 @@ export async function runUpload(params: {
467
443
  }
468
444
 
469
445
  try {
470
- const response = await fetchWithRetry(
471
- uploadUrl,
472
- {
473
- method: "POST",
474
- headers: {
475
- Authorization: `Bearer ${memoryKey}`,
476
- "Content-Type": "text/plain",
477
- ...(embeddings && { "X-Embedding-Model": embeddings }),
478
- },
479
- body: jsonlBody,
480
- },
481
- `Batch ${i + 1}`,
482
- );
446
+ const response = await uploadMemoryBatch({
447
+ endpoint,
448
+ memoryKey,
449
+ jsonlBody,
450
+ embeddings,
451
+ label: `Batch ${i + 1}`,
452
+ });
483
453
 
484
454
  const result = (await response.json()) as {
485
455
  stats?: { stored?: number; failed?: number; inputItems?: number; path?: string; diagnostic?: string };