mr-memory 3.7.3 → 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,7 +8,9 @@
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";
13
+ import os from "node:os";
12
14
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
13
15
  /** Strip media-attached references and OpenClaw media instruction text so
14
16
  * old screenshots/photos/audio don't get stored and re-injected forever. */
@@ -264,9 +266,6 @@ const WORKSPACE_FILES = [
264
266
  "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
265
267
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
266
268
  ];
267
- // ──────────────────────────────────────────────────────
268
- // Helpers
269
- // ──────────────────────────────────────────────────────
270
269
  function openClawConfigPath() {
271
270
  return process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
272
271
  }
@@ -290,7 +289,8 @@ async function writeOpenClawConfigFile(config) {
290
289
  if (err?.code !== "ENOENT")
291
290
  throw err;
292
291
  }
293
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
292
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
293
+ `, "utf8");
294
294
  }
295
295
  async function updateOpenClawPluginEntry(api, patch) {
296
296
  const config = await readOpenClawConfigFile();
@@ -544,17 +544,10 @@ If setting up MemoryRouter for another agent:
544
544
  contextPayload.push({ role: "user", content: taskPrompt });
545
545
  }
546
546
  }
547
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
548
- method: "POST",
549
- headers: {
550
- "Content-Type": "application/json",
551
- Authorization: `Bearer ${activeKey}`,
552
- },
553
- body: JSON.stringify({
554
- messages: contextPayload,
555
- density,
556
- ...(embeddings && { embeddings }),
557
- }),
547
+ const res = await prepareMemory(endpoint, activeKey, {
548
+ messages: contextPayload,
549
+ density,
550
+ ...(embeddings && { embeddings }),
558
551
  });
559
552
  if (!res.ok) {
560
553
  const errBody = await res.json().catch(() => null);
@@ -649,17 +642,10 @@ If setting up MemoryRouter for another agent:
649
642
  }
650
643
  }
651
644
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
652
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
653
- method: "POST",
654
- headers: {
655
- "Content-Type": "application/json",
656
- Authorization: `Bearer ${activeKey}`,
657
- },
658
- body: JSON.stringify({
659
- messages: contextPayload,
660
- density,
661
- ...(embeddings && { embeddings }),
662
- }),
645
+ const res = await prepareMemory(endpoint, activeKey, {
646
+ messages: contextPayload,
647
+ density,
648
+ ...(embeddings && { embeddings }),
663
649
  });
664
650
  if (!res.ok) {
665
651
  const errBody = await res.json().catch(() => null);
@@ -761,17 +747,10 @@ If setting up MemoryRouter for another agent:
761
747
  return;
762
748
  }
763
749
  // Fire and forget — don't block message delivery waiting for ingest
764
- fetch(`${endpoint}/v1/memory/ingest`, {
765
- method: "POST",
766
- headers: {
767
- "Content-Type": "application/json",
768
- Authorization: `Bearer ${activeKey}`,
769
- },
770
- body: JSON.stringify({
771
- messages: toStore,
772
- model: "unknown",
773
- ...(embeddings && { embeddings }),
774
- }),
750
+ ingestMemory(endpoint, activeKey, {
751
+ messages: toStore,
752
+ model: "unknown",
753
+ ...(embeddings && { embeddings }),
775
754
  }).then(async (res) => {
776
755
  if (!res.ok) {
777
756
  const details = await res.text().catch(() => "");
@@ -821,15 +800,7 @@ If setting up MemoryRouter for another agent:
821
800
  return jsonToolResult({ results: [], error: "query required" });
822
801
  const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
823
802
  try {
824
- const res = await fetch(`${endpoint}/v1/memory/search`, {
825
- method: "POST",
826
- headers: {
827
- Authorization: `Bearer ${toolKey}`,
828
- "Content-Type": "application/json",
829
- ...(embeddings && { "X-Embedding-Model": embeddings }),
830
- },
831
- body: JSON.stringify({ query, limit }),
832
- });
803
+ const res = await searchMemory(endpoint, toolKey, { query, limit, embeddings });
833
804
  if (!res.ok) {
834
805
  const errBody = await res.text().catch(() => "");
835
806
  return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
@@ -1022,12 +993,7 @@ If setting up MemoryRouter for another agent:
1022
993
  return;
1023
994
  }
1024
995
  try {
1025
- const statsUrl = embeddings
1026
- ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
1027
- : `${endpoint}/v1/memory/stats`;
1028
- const res = await fetch(statsUrl, {
1029
- headers: { Authorization: `Bearer ${effectiveKey}` },
1030
- });
996
+ const res = await getMemoryStats(endpoint, effectiveKey, embeddings);
1031
997
  const data = (await res.json());
1032
998
  if (opts.json) {
1033
999
  console.log(JSON.stringify({ enabled: true, key: effectiveKey, density, stats: data }, null, 2));
@@ -1085,15 +1051,7 @@ If setting up MemoryRouter for another agent:
1085
1051
  }
1086
1052
  const limit = parseInt(opts.limit, 10) || 50;
1087
1053
  try {
1088
- const res = await fetch(`${endpoint}/v1/memory/search`, {
1089
- method: "POST",
1090
- headers: {
1091
- Authorization: `Bearer ${effectiveKey}`,
1092
- "Content-Type": "application/json",
1093
- ...(embeddings && { "X-Embedding-Model": embeddings }),
1094
- },
1095
- body: JSON.stringify({ query, limit }),
1096
- });
1054
+ const res = await searchMemory(endpoint, effectiveKey, { query, limit, embeddings });
1097
1055
  if (!res.ok) {
1098
1056
  const err = await res.json();
1099
1057
  console.error(`Search failed: ${err.error || `HTTP ${res.status}`}`);
@@ -1141,13 +1099,7 @@ If setting up MemoryRouter for another agent:
1141
1099
  return;
1142
1100
  }
1143
1101
  try {
1144
- const deleteUrl = embeddings
1145
- ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
1146
- : `${endpoint}/v1/memory`;
1147
- const res = await fetch(deleteUrl, {
1148
- method: "DELETE",
1149
- headers: { Authorization: `Bearer ${effectiveKey}` },
1150
- });
1102
+ const res = await deleteMemories(endpoint, effectiveKey, embeddings);
1151
1103
  const data = (await res.json());
1152
1104
  const modelLabel = embeddings ? ` (${embeddings})` : "";
1153
1105
  console.log(`✓ ${data.message || `Vault cleared${modelLabel}`}`);
package/index.ts CHANGED
@@ -9,8 +9,10 @@
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";
15
+ import os from "node:os";
14
16
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
15
17
 
16
18
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
@@ -569,17 +571,10 @@ If setting up MemoryRouter for another agent:
569
571
  }
570
572
 
571
573
 
572
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
573
- method: "POST",
574
- headers: {
575
- "Content-Type": "application/json",
576
- Authorization: `Bearer ${activeKey}`,
577
- },
578
- body: JSON.stringify({
579
- messages: contextPayload,
580
- density,
581
- ...(embeddings && { embeddings }),
582
- }),
574
+ const res = await prepareMemory(endpoint, activeKey, {
575
+ messages: contextPayload,
576
+ density,
577
+ ...(embeddings && { embeddings }),
583
578
  });
584
579
 
585
580
  if (!res.ok) {
@@ -685,17 +680,10 @@ If setting up MemoryRouter for another agent:
685
680
 
686
681
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
687
682
 
688
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
689
- method: "POST",
690
- headers: {
691
- "Content-Type": "application/json",
692
- Authorization: `Bearer ${activeKey}`,
693
- },
694
- body: JSON.stringify({
695
- messages: contextPayload,
696
- density,
697
- ...(embeddings && { embeddings }),
698
- }),
683
+ const res = await prepareMemory(endpoint, activeKey, {
684
+ messages: contextPayload,
685
+ density,
686
+ ...(embeddings && { embeddings }),
699
687
  });
700
688
 
701
689
  if (!res.ok) {
@@ -810,17 +798,10 @@ If setting up MemoryRouter for another agent:
810
798
  }
811
799
 
812
800
  // Fire and forget — don't block message delivery waiting for ingest
813
- fetch(`${endpoint}/v1/memory/ingest`, {
814
- method: "POST",
815
- headers: {
816
- "Content-Type": "application/json",
817
- Authorization: `Bearer ${activeKey}`,
818
- },
819
- body: JSON.stringify({
820
- messages: toStore,
821
- model: "unknown",
822
- ...(embeddings && { embeddings }),
823
- }),
801
+ ingestMemory(endpoint, activeKey, {
802
+ messages: toStore,
803
+ model: "unknown",
804
+ ...(embeddings && { embeddings }),
824
805
  }).then(async (res) => {
825
806
  if (!res.ok) {
826
807
  const details = await res.text().catch(() => "");
@@ -868,15 +849,7 @@ If setting up MemoryRouter for another agent:
868
849
  if (!query) return jsonToolResult({ results: [], error: "query required" });
869
850
  const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
870
851
  try {
871
- const res = await fetch(`${endpoint}/v1/memory/search`, {
872
- method: "POST",
873
- headers: {
874
- Authorization: `Bearer ${toolKey}`,
875
- "Content-Type": "application/json",
876
- ...(embeddings && { "X-Embedding-Model": embeddings }),
877
- },
878
- body: JSON.stringify({ query, limit }),
879
- });
852
+ const res = await searchMemory(endpoint, toolKey, { query, limit, embeddings });
880
853
  if (!res.ok) {
881
854
  const errBody = await res.text().catch(() => "");
882
855
  return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
@@ -1074,12 +1047,7 @@ If setting up MemoryRouter for another agent:
1074
1047
  return;
1075
1048
  }
1076
1049
  try {
1077
- const statsUrl = embeddings
1078
- ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
1079
- : `${endpoint}/v1/memory/stats`;
1080
- const res = await fetch(statsUrl, {
1081
- headers: { Authorization: `Bearer ${effectiveKey}` },
1082
- });
1050
+ const res = await getMemoryStats(endpoint, effectiveKey, embeddings);
1083
1051
  const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
1084
1052
  if (opts.json) {
1085
1053
  console.log(JSON.stringify({ enabled: true, key: effectiveKey, density, stats: data }, null, 2));
@@ -1131,15 +1099,7 @@ If setting up MemoryRouter for another agent:
1131
1099
  if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1132
1100
  const limit = parseInt(opts.limit, 10) || 50;
1133
1101
  try {
1134
- const res = await fetch(`${endpoint}/v1/memory/search`, {
1135
- method: "POST",
1136
- headers: {
1137
- Authorization: `Bearer ${effectiveKey}`,
1138
- "Content-Type": "application/json",
1139
- ...(embeddings && { "X-Embedding-Model": embeddings }),
1140
- },
1141
- body: JSON.stringify({ query, limit }),
1142
- });
1102
+ const res = await searchMemory(endpoint, effectiveKey, { query, limit, embeddings });
1143
1103
  if (!res.ok) {
1144
1104
  const err = await res.json() as { error?: string };
1145
1105
  console.error(`Search failed: ${err.error || `HTTP ${res.status}`}`);
@@ -1204,13 +1164,7 @@ If setting up MemoryRouter for another agent:
1204
1164
  const effectiveKey = opts.key || memoryKey;
1205
1165
  if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1206
1166
  try {
1207
- const deleteUrl = embeddings
1208
- ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
1209
- : `${endpoint}/v1/memory`;
1210
- const res = await fetch(deleteUrl, {
1211
- method: "DELETE",
1212
- headers: { Authorization: `Bearer ${effectiveKey}` },
1213
- });
1167
+ const res = await deleteMemories(endpoint, effectiveKey, embeddings);
1214
1168
  const data = (await res.json()) as { message?: string };
1215
1169
  const modelLabel = embeddings ? ` (${embeddings})` : "";
1216
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.3",
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 };