@openpalm/lib 0.9.8 → 0.10.1

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.
Files changed (56) hide show
  1. package/README.md +31 -71
  2. package/package.json +1 -1
  3. package/src/control-plane/audit.ts +4 -4
  4. package/src/control-plane/backup.ts +31 -0
  5. package/src/control-plane/channels.ts +88 -156
  6. package/src/control-plane/cleanup-guardrails.test.ts +289 -0
  7. package/src/control-plane/compose-args.test.ts +170 -0
  8. package/src/control-plane/compose-args.ts +57 -0
  9. package/src/control-plane/config-persistence.ts +270 -0
  10. package/src/control-plane/core-assets.ts +58 -234
  11. package/src/control-plane/crypto.ts +14 -0
  12. package/src/control-plane/docker.ts +94 -204
  13. package/src/control-plane/env-schema-validation.test.ts +118 -0
  14. package/src/control-plane/extends-support.test.ts +105 -0
  15. package/src/control-plane/home.ts +133 -0
  16. package/src/control-plane/install-edge-cases.test.ts +314 -717
  17. package/src/control-plane/lifecycle.ts +215 -233
  18. package/src/control-plane/lock.test.ts +194 -0
  19. package/src/control-plane/lock.ts +176 -0
  20. package/src/control-plane/memory-config.ts +34 -160
  21. package/src/control-plane/opencode-client.test.ts +154 -0
  22. package/src/control-plane/opencode-client.ts +113 -0
  23. package/src/control-plane/provider-config.ts +34 -0
  24. package/src/control-plane/redact-schema.ts +50 -0
  25. package/src/control-plane/registry-components.test.ts +313 -0
  26. package/src/control-plane/registry.test.ts +414 -0
  27. package/src/control-plane/registry.ts +418 -0
  28. package/src/control-plane/rollback.ts +128 -0
  29. package/src/control-plane/scheduler.ts +18 -190
  30. package/src/control-plane/secret-backend.test.ts +359 -0
  31. package/src/control-plane/secret-backend.ts +322 -0
  32. package/src/control-plane/secret-mappings.ts +185 -0
  33. package/src/control-plane/secrets.ts +186 -112
  34. package/src/control-plane/setup-config.schema.json +306 -0
  35. package/src/control-plane/setup-status.ts +15 -8
  36. package/src/control-plane/setup-validation.ts +90 -0
  37. package/src/control-plane/setup.test.ts +336 -929
  38. package/src/control-plane/setup.ts +159 -849
  39. package/src/control-plane/spec-to-env.test.ts +100 -0
  40. package/src/control-plane/spec-to-env.ts +195 -0
  41. package/src/control-plane/spec-validator.ts +159 -0
  42. package/src/control-plane/stack-spec.test.ts +150 -0
  43. package/src/control-plane/stack-spec.ts +101 -22
  44. package/src/control-plane/types.ts +6 -99
  45. package/src/control-plane/validate.ts +107 -0
  46. package/src/index.ts +101 -159
  47. package/src/provider-constants.ts +2 -31
  48. package/src/control-plane/connection-mapping.ts +0 -191
  49. package/src/control-plane/connection-migration-flags.ts +0 -40
  50. package/src/control-plane/connection-profiles.ts +0 -317
  51. package/src/control-plane/core-asset-provider.ts +0 -21
  52. package/src/control-plane/fs-asset-provider.ts +0 -65
  53. package/src/control-plane/fs-registry-provider.ts +0 -46
  54. package/src/control-plane/paths.ts +0 -77
  55. package/src/control-plane/registry-provider.ts +0 -19
  56. package/src/control-plane/staging.ts +0 -399
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Tests for orchestrator lock — acquisition, contention, stale cleanup,
3
+ * corrupt file handling, release, and idempotent release.
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
6
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import {
10
+ acquireLock,
11
+ releaseLock,
12
+ lockPath,
13
+ LockAcquisitionError,
14
+ } from "./lock.js";
15
+ import type { LockHandle, LockInfo } from "./lock.js";
16
+
17
+ let opHome: string;
18
+
19
+ beforeEach(() => {
20
+ opHome = mkdtempSync(join(tmpdir(), "lock-test-"));
21
+ mkdirSync(join(opHome, "data"), { recursive: true });
22
+ });
23
+
24
+ afterEach(() => {
25
+ rmSync(opHome, { recursive: true, force: true });
26
+ });
27
+
28
+ // ── Acquisition ──────────────────────────────────────────────────────────
29
+
30
+ describe("acquireLock", () => {
31
+ it("creates a lock file with correct JSON content", () => {
32
+ const handle = acquireLock(opHome, "install");
33
+ expect(existsSync(handle.path)).toBe(true);
34
+
35
+ const content = JSON.parse(readFileSync(handle.path, "utf-8"));
36
+ expect(content.pid).toBe(process.pid);
37
+ expect(content.operation).toBe("install");
38
+ expect(typeof content.acquiredAt).toBe("string");
39
+
40
+ releaseLock(handle);
41
+ });
42
+
43
+ it("returns a handle with correct info", () => {
44
+ const handle = acquireLock(opHome, "update");
45
+ expect(handle.info.pid).toBe(process.pid);
46
+ expect(handle.info.operation).toBe("update");
47
+ expect(handle.path).toBe(lockPath(opHome));
48
+
49
+ releaseLock(handle);
50
+ });
51
+
52
+ it("places lock at {opHome}/data/.openpalm.lock", () => {
53
+ const handle = acquireLock(opHome, "test");
54
+ expect(handle.path).toBe(join(opHome, "data", ".openpalm.lock"));
55
+ releaseLock(handle);
56
+ });
57
+ });
58
+
59
+ // ── Contention ───────────────────────────────────────────────────────────
60
+
61
+ describe("contention", () => {
62
+ it("throws LockAcquisitionError when lock is already held by this process", () => {
63
+ const handle = acquireLock(opHome, "install");
64
+
65
+ try {
66
+ expect(() => acquireLock(opHome, "update")).toThrow(LockAcquisitionError);
67
+ } finally {
68
+ releaseLock(handle);
69
+ }
70
+ });
71
+
72
+ it("error includes holder details", () => {
73
+ const handle = acquireLock(opHome, "install");
74
+
75
+ try {
76
+ acquireLock(opHome, "update");
77
+ expect.unreachable("should have thrown");
78
+ } catch (err) {
79
+ expect(err).toBeInstanceOf(LockAcquisitionError);
80
+ const lockErr = err as LockAcquisitionError;
81
+ expect(lockErr.holder.pid).toBe(process.pid);
82
+ expect(lockErr.holder.operation).toBe("install");
83
+ } finally {
84
+ releaseLock(handle);
85
+ }
86
+ });
87
+ });
88
+
89
+ // ── Stale PID cleanup ────────────────────────────────────────────────────
90
+
91
+ describe("stale PID cleanup", () => {
92
+ it("cleans up stale lock from a dead PID and acquires", () => {
93
+ // Write a lock file with a PID that does not exist
94
+ const stalePid = 99999999; // Very unlikely to be a real process
95
+ const staleInfo: LockInfo = {
96
+ pid: stalePid,
97
+ operation: "old-install",
98
+ acquiredAt: "2020-01-01T00:00:00.000Z",
99
+ };
100
+ writeFileSync(lockPath(opHome), JSON.stringify(staleInfo) + "\n");
101
+
102
+ // Should succeed because the PID is dead
103
+ const handle = acquireLock(opHome, "new-install");
104
+ expect(handle.info.pid).toBe(process.pid);
105
+ expect(handle.info.operation).toBe("new-install");
106
+
107
+ releaseLock(handle);
108
+ });
109
+ });
110
+
111
+ // ── Corrupt file handling ────────────────────────────────────────────────
112
+
113
+ describe("corrupt lock file", () => {
114
+ it("recovers from a corrupt lock file", () => {
115
+ writeFileSync(lockPath(opHome), "not valid json{{{");
116
+
117
+ const handle = acquireLock(opHome, "install");
118
+ expect(handle.info.pid).toBe(process.pid);
119
+
120
+ releaseLock(handle);
121
+ });
122
+
123
+ it("recovers from an empty lock file", () => {
124
+ writeFileSync(lockPath(opHome), "");
125
+
126
+ const handle = acquireLock(opHome, "install");
127
+ expect(handle.info.pid).toBe(process.pid);
128
+
129
+ releaseLock(handle);
130
+ });
131
+
132
+ it("recovers from a lock file with missing fields", () => {
133
+ writeFileSync(lockPath(opHome), JSON.stringify({ pid: 1 }));
134
+
135
+ const handle = acquireLock(opHome, "install");
136
+ expect(handle.info.pid).toBe(process.pid);
137
+
138
+ releaseLock(handle);
139
+ });
140
+ });
141
+
142
+ // ── Release ──────────────────────────────────────────────────────────────
143
+
144
+ describe("releaseLock", () => {
145
+ it("removes the lock file", () => {
146
+ const handle = acquireLock(opHome, "install");
147
+ expect(existsSync(handle.path)).toBe(true);
148
+
149
+ releaseLock(handle);
150
+ expect(existsSync(handle.path)).toBe(false);
151
+ });
152
+
153
+ it("is idempotent — second release is a no-op", () => {
154
+ const handle = acquireLock(opHome, "install");
155
+ releaseLock(handle);
156
+ expect(existsSync(handle.path)).toBe(false);
157
+
158
+ // Second release should not throw
159
+ releaseLock(handle);
160
+ expect(existsSync(handle.path)).toBe(false);
161
+ });
162
+
163
+ it("does not remove lock owned by a different PID", () => {
164
+ // Simulate a lock file owned by someone else
165
+ const otherInfo: LockInfo = {
166
+ pid: 99999999,
167
+ operation: "other",
168
+ acquiredAt: new Date().toISOString(),
169
+ };
170
+ writeFileSync(lockPath(opHome), JSON.stringify(otherInfo) + "\n");
171
+
172
+ // Create a handle that claims to own the lock
173
+ const fakeHandle: LockHandle = {
174
+ path: lockPath(opHome),
175
+ info: {
176
+ pid: process.pid, // Different from file content
177
+ operation: "mine",
178
+ acquiredAt: new Date().toISOString(),
179
+ },
180
+ };
181
+
182
+ releaseLock(fakeHandle);
183
+ // Lock file should still exist because PID doesn't match
184
+ expect(existsSync(lockPath(opHome))).toBe(true);
185
+ });
186
+ });
187
+
188
+ // ── lockPath ─────────────────────────────────────────────────────────────
189
+
190
+ describe("lockPath", () => {
191
+ it("returns the correct path", () => {
192
+ expect(lockPath("/home/user/.openpalm")).toBe("/home/user/.openpalm/data/.openpalm.lock");
193
+ });
194
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Orchestrator lock — prevents concurrent mutating operations.
3
+ *
4
+ * Uses O_CREAT | O_EXCL for atomic exclusive file creation.
5
+ * Lock file lives at {dataDir}/.openpalm.lock containing JSON
6
+ * with { pid, operation, acquiredAt }.
7
+ *
8
+ * Uses node:fs (not Bun) since lib must be Node-compatible for SvelteKit admin.
9
+ */
10
+ import { openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync, constants } from "node:fs";
11
+ import { dirname } from "node:path";
12
+
13
+ // ── Types ────────────────────────────────────────────────────────────────
14
+
15
+ export type LockInfo = {
16
+ pid: number;
17
+ operation: string;
18
+ acquiredAt: string;
19
+ };
20
+
21
+ export type LockHandle = {
22
+ path: string;
23
+ info: LockInfo;
24
+ };
25
+
26
+ // ── Error ────────────────────────────────────────────────────────────────
27
+
28
+ export class LockAcquisitionError extends Error {
29
+ public readonly holder: LockInfo;
30
+
31
+ constructor(holder: LockInfo) {
32
+ super(
33
+ `Cannot acquire lock: already held by PID ${holder.pid} ` +
34
+ `for "${holder.operation}" since ${holder.acquiredAt}`
35
+ );
36
+ this.name = "LockAcquisitionError";
37
+ this.holder = holder;
38
+ }
39
+ }
40
+
41
+ // ── Path ─────────────────────────────────────────────────────────────────
42
+
43
+ export function lockPath(opHome: string): string {
44
+ return `${opHome}/data/.openpalm.lock`;
45
+ }
46
+
47
+ // ── Stale PID Detection ──────────────────────────────────────────────────
48
+
49
+ function isProcessAlive(pid: number): boolean {
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ // ── Read existing lock info ──────────────────────────────────────────────
59
+
60
+ function readLockInfo(path: string): LockInfo | null {
61
+ try {
62
+ const content = readFileSync(path, "utf-8");
63
+ const parsed = JSON.parse(content);
64
+ if (
65
+ typeof parsed.pid === "number" &&
66
+ typeof parsed.operation === "string" &&
67
+ typeof parsed.acquiredAt === "string"
68
+ ) {
69
+ return parsed as LockInfo;
70
+ }
71
+ return null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ // ── Acquire / Release ────────────────────────────────────────────────────
78
+
79
+ export function acquireLock(opHome: string, operation: string): LockHandle {
80
+ const path = lockPath(opHome);
81
+ mkdirSync(dirname(path), { recursive: true });
82
+ const info: LockInfo = {
83
+ pid: process.pid,
84
+ operation,
85
+ acquiredAt: new Date().toISOString(),
86
+ };
87
+ const content = JSON.stringify(info) + "\n";
88
+
89
+ try {
90
+ // Atomic exclusive create — fails if file already exists
91
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o644);
92
+ try {
93
+ writeSync(fd, content);
94
+ } finally {
95
+ closeSync(fd);
96
+ }
97
+ return { path, info };
98
+ } catch (err: unknown) {
99
+ // File already exists — check if it's stale
100
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") {
101
+ const existing = readLockInfo(path);
102
+
103
+ if (existing && !isProcessAlive(existing.pid)) {
104
+ // Stale lock — remove and retry once
105
+ try {
106
+ unlinkSync(path);
107
+ } catch {
108
+ // Race: another process already removed it; fall through to retry
109
+ }
110
+ // Retry acquisition
111
+ try {
112
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o644);
113
+ try {
114
+ writeSync(fd, content);
115
+ } finally {
116
+ closeSync(fd);
117
+ }
118
+ return { path, info };
119
+ } catch (retryErr: unknown) {
120
+ if ((retryErr as NodeJS.ErrnoException).code === "EEXIST") {
121
+ // Another process won the race — read the new holder
122
+ const newHolder = readLockInfo(path);
123
+ throw new LockAcquisitionError(
124
+ newHolder ?? { pid: 0, operation: "unknown", acquiredAt: "unknown" }
125
+ );
126
+ }
127
+ throw retryErr;
128
+ }
129
+ }
130
+
131
+ // Lock is held by a live process (or corrupt file — treat as held)
132
+ if (existing) {
133
+ throw new LockAcquisitionError(existing);
134
+ }
135
+
136
+ // Corrupt lock file — remove and retry
137
+ try {
138
+ unlinkSync(path);
139
+ } catch {
140
+ // ignore
141
+ }
142
+ try {
143
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o644);
144
+ try {
145
+ writeSync(fd, content);
146
+ } finally {
147
+ closeSync(fd);
148
+ }
149
+ return { path, info };
150
+ } catch (retryErr: unknown) {
151
+ if ((retryErr as NodeJS.ErrnoException).code === "EEXIST") {
152
+ const newHolder = readLockInfo(path);
153
+ throw new LockAcquisitionError(
154
+ newHolder ?? { pid: 0, operation: "unknown", acquiredAt: "unknown" }
155
+ );
156
+ }
157
+ throw retryErr;
158
+ }
159
+ }
160
+
161
+ throw err;
162
+ }
163
+ }
164
+
165
+ export function releaseLock(handle: LockHandle): void {
166
+ // Verify ownership before deleting — only remove if we still own it
167
+ const existing = readLockInfo(handle.path);
168
+ if (!existing) return; // Already gone — idempotent
169
+ if (existing.pid !== handle.info.pid) return; // Not ours — don't touch
170
+
171
+ try {
172
+ unlinkSync(handle.path);
173
+ } catch {
174
+ // Already removed — idempotent
175
+ }
176
+ }
@@ -1,18 +1,8 @@
1
- /**
2
- * Memory LLM & Embedding configuration management.
3
- */
1
+ /** Memory LLM & Embedding configuration management. */
4
2
  import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
5
- import { loadSecretsEnvFile } from "./secrets.js";
6
- import {
7
- LLM_PROVIDERS,
8
- EMBEDDING_DIMS,
9
- PROVIDER_DEFAULT_URLS,
10
- } from "../provider-constants.js";
3
+ import { readStackEnv } from "./secrets.js";
4
+ import { EMBEDDING_DIMS, PROVIDER_DEFAULT_URLS } from "../provider-constants.js";
11
5
 
12
- // Re-export shared constants for barrel compatibility
13
- export { LLM_PROVIDERS, EMBEDDING_DIMS, PROVIDER_DEFAULT_URLS };
14
-
15
- // ── Types ────────────────────────────────────────────────────────────────
16
6
 
17
7
  export type MemoryConfig = {
18
8
  mem0: {
@@ -31,7 +21,6 @@ export type MemoryConfig = {
31
21
  memory: { custom_instructions: string };
32
22
  };
33
23
 
34
- // ── Constants (module-specific) ─────────────────────────────────────────
35
24
 
36
25
  export const EMBED_PROVIDERS = [
37
26
  "openai", "ollama", "huggingface", "lmstudio"
@@ -48,7 +37,6 @@ const ANTHROPIC_MODELS = [
48
37
  "claude-3-5-haiku-20241022",
49
38
  ];
50
39
 
51
- // ── API Key Resolution ──────────────────────────────────────────────────
52
40
 
53
41
  export function resolveApiKey(apiKeyRef: string, configDir: string): string {
54
42
  if (!apiKeyRef) return "";
@@ -57,11 +45,10 @@ export function resolveApiKey(apiKeyRef: string, configDir: string): string {
57
45
  const varName = apiKeyRef.slice(4);
58
46
  if (process.env[varName]) return process.env[varName]!;
59
47
 
60
- const secrets = loadSecretsEnvFile(configDir);
48
+ const secrets = readStackEnv(configDir);
61
49
  return secrets[varName] ?? "";
62
50
  }
63
51
 
64
- // ── Provider Model Listing ──────────────────────────────────────────────
65
52
 
66
53
  export type ModelDiscoveryReason =
67
54
  | 'none'
@@ -78,37 +65,15 @@ export type ProviderModelsResult = {
78
65
  error?: string;
79
66
  };
80
67
 
81
- function describeHttpStatus(status: number): string {
82
- switch (status) {
83
- case 401: return 'Invalid or missing API key';
84
- case 403: return 'Access deniedcheck API key permissions';
85
- case 404: return 'Endpoint not found verify the base URL';
86
- case 429: return 'Rate limited — try again shortly';
87
- case 500: return 'Provider internal error';
88
- case 502: return 'Provider returned a bad gateway error';
89
- case 503: return 'Provider is temporarily unavailable';
90
- default: return `HTTP ${status}`;
91
- }
92
- }
93
-
94
- async function extractProviderErrorDetail(res: Response): Promise<string> {
95
- try {
96
- const text = await res.text();
97
- const json = JSON.parse(text) as Record<string, unknown>;
98
- if (
99
- typeof json.error === 'object' && json.error !== null &&
100
- typeof (json.error as Record<string, unknown>).message === 'string'
101
- ) {
102
- return (json.error as Record<string, unknown>).message as string;
103
- }
104
- if (typeof json.error === 'string') return json.error;
105
- if (typeof json.message === 'string') return json.message;
106
- if (typeof json.detail === 'string') return json.detail;
107
- return '';
108
- } catch {
109
- return '';
110
- }
111
- }
68
+ const HTTP_STATUS_LABELS: Record<number, string> = {
69
+ 401: 'Invalid or missing API key',
70
+ 403: 'Access denied check API key permissions',
71
+ 404: 'Endpoint not found verify the base URL',
72
+ 429: 'Rate limitedtry again shortly',
73
+ 500: 'Provider internal error',
74
+ 502: 'Provider returned a bad gateway error',
75
+ 503: 'Provider is temporarily unavailable',
76
+ };
112
77
 
113
78
  export async function fetchProviderModels(
114
79
  provider: string,
@@ -132,7 +97,7 @@ export async function fetchProviderModels(
132
97
  models: [],
133
98
  status: 'recoverable_error',
134
99
  reason: 'provider_http',
135
- error: `Ollama API returned ${res.status}: ${describeHttpStatus(res.status)}`,
100
+ error: `Ollama API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
136
101
  };
137
102
  }
138
103
  const data = (await res.json()) as { models?: { name: string }[] };
@@ -158,14 +123,22 @@ export async function fetchProviderModels(
158
123
 
159
124
  const res = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
160
125
  if (!res.ok) {
161
- const detail = await extractProviderErrorDetail(res);
126
+ let detail = '';
127
+ try {
128
+ const json = JSON.parse(await res.text()) as Record<string, unknown>;
129
+ const errObj = json.error as Record<string, unknown> | string | undefined;
130
+ detail = (typeof errObj === 'object' && errObj !== null && typeof errObj.message === 'string') ? errObj.message
131
+ : typeof errObj === 'string' ? errObj
132
+ : typeof json.message === 'string' ? json.message
133
+ : typeof json.detail === 'string' ? json.detail : '';
134
+ } catch { /* ignore parse errors */ }
162
135
  return {
163
136
  models: [],
164
137
  status: 'recoverable_error',
165
138
  reason: 'provider_http',
166
139
  error: detail
167
140
  ? `Provider API returned ${res.status}: ${detail}`
168
- : `Provider API returned ${res.status}: ${describeHttpStatus(res.status)}`,
141
+ : `Provider API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
169
142
  };
170
143
  }
171
144
  const data = (await res.json()) as { data?: { id: string }[] };
@@ -185,7 +158,6 @@ export async function fetchProviderModels(
185
158
  }
186
159
  }
187
160
 
188
- // ── Default Config ───────────────────────────────────────────────────────
189
161
 
190
162
  export function getDefaultConfig(): MemoryConfig {
191
163
  return {
@@ -219,66 +191,27 @@ export function getDefaultConfig(): MemoryConfig {
219
191
  };
220
192
  }
221
193
 
222
- // ── File I/O ─────────────────────────────────────────────────────────────
223
-
224
- const CONFIG_FILENAME = "memory/default_config.json";
225
-
226
- function configPath(dataDir: string): string {
227
- return `${dataDir}/${CONFIG_FILENAME}`;
228
- }
229
194
 
230
195
  export function readMemoryConfig(dataDir: string): MemoryConfig {
231
- const path = configPath(dataDir);
196
+ const path = `${dataDir}/memory/default_config.json`;
232
197
  if (!existsSync(path)) return getDefaultConfig();
233
198
  try {
234
- const raw = readFileSync(path, "utf-8");
235
- return JSON.parse(raw) as MemoryConfig;
199
+ return JSON.parse(readFileSync(path, "utf-8")) as MemoryConfig;
236
200
  } catch {
237
201
  return getDefaultConfig();
238
202
  }
239
203
  }
240
204
 
241
- export function writeMemoryConfig(
242
- dataDir: string,
243
- config: MemoryConfig
244
- ): void {
245
- const path = configPath(dataDir);
205
+ export function writeMemoryConfig(dataDir: string, config: MemoryConfig): void {
246
206
  mkdirSync(`${dataDir}/memory`, { recursive: true });
247
- writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
207
+ writeFileSync(`${dataDir}/memory/default_config.json`, JSON.stringify(config, null, 2) + "\n");
248
208
  }
249
209
 
250
210
  export function ensureMemoryConfig(dataDir: string): void {
251
- const path = configPath(dataDir);
252
- if (existsSync(path)) return;
211
+ if (existsSync(`${dataDir}/memory/default_config.json`)) return;
253
212
  writeMemoryConfig(dataDir, getDefaultConfig());
254
213
  }
255
214
 
256
- // ── Config Resolution ────────────────────────────────────────────────
257
-
258
- export function resolveConfigForPush(
259
- config: MemoryConfig,
260
- configDir: string
261
- ): MemoryConfig {
262
- const resolved = structuredClone(config);
263
-
264
- if (typeof resolved.mem0.llm.config.api_key === "string") {
265
- resolved.mem0.llm.config.api_key = resolveApiKey(
266
- resolved.mem0.llm.config.api_key as string,
267
- configDir
268
- );
269
- }
270
-
271
- if (typeof resolved.mem0.embedder.config.api_key === "string") {
272
- resolved.mem0.embedder.config.api_key = resolveApiKey(
273
- resolved.mem0.embedder.config.api_key as string,
274
- configDir
275
- );
276
- }
277
-
278
- return resolved;
279
- }
280
-
281
- // ── Dimension Checking ──────────────────────────────────────────────
282
215
 
283
216
  export type VectorDimensionResult = {
284
217
  match: boolean;
@@ -286,9 +219,6 @@ export type VectorDimensionResult = {
286
219
  expectedDims: number;
287
220
  };
288
221
 
289
- /** @deprecated Use checkVectorDimensions instead */
290
- export type QdrantDimensionResult = VectorDimensionResult;
291
-
292
222
  export function checkVectorDimensions(
293
223
  dataDir: string,
294
224
  newConfig: MemoryConfig
@@ -299,9 +229,6 @@ export function checkVectorDimensions(
299
229
  return { match: currentDims === expectedDims, currentDims, expectedDims };
300
230
  }
301
231
 
302
- /** @deprecated Use checkVectorDimensions instead */
303
- export const checkQdrantDimensions = checkVectorDimensions;
304
-
305
232
  export function resetVectorStore(
306
233
  dataDir: string
307
234
  ): { ok: boolean; error?: string } {
@@ -334,79 +261,26 @@ export function resetVectorStore(
334
261
  }
335
262
  }
336
263
 
337
- /** @deprecated Use resetVectorStore instead */
338
- export const resetQdrantCollection = resetVectorStore;
339
-
340
- // ── Runtime API ──────────────────────────────────────────────────────────
341
264
 
342
- function getMemoryApiBases(): string[] {
343
- const configured =
344
- process.env.MEMORY_API_URL?.trim() ||
345
- process.env.OPENPALM_MEMORY_API_URL?.trim();
346
-
347
- const bases = configured
348
- ? [configured]
349
- : ["http://memory:8765", "http://127.0.0.1:8765"];
350
-
351
- return Array.from(new Set(bases.map((base) => base.replace(/\/+$/, ""))));
352
- }
353
-
354
- function getMemoryAuthHeaders(): Record<string, string> {
265
+ async function callMemoryApi(path: string, init?: RequestInit): Promise<Response> {
266
+ const configured = process.env.MEMORY_API_URL?.trim() || process.env.OP_MEMORY_API_URL?.trim();
267
+ const bases = configured ? [configured.replace(/\/+$/, "")] : ["http://memory:8765", "http://127.0.0.1:8765"];
355
268
  const token = process.env.MEMORY_AUTH_TOKEN?.trim();
356
- return token ? { authorization: `Bearer ${token}` } : {};
357
- }
358
-
359
- async function callMemoryApi(
360
- path: string,
361
- init?: RequestInit,
362
- ): Promise<Response> {
363
- const bases = getMemoryApiBases();
364
- const authHeaders = getMemoryAuthHeaders();
269
+ const authHeaders: Record<string, string> = token ? { authorization: `Bearer ${token}` } : {};
365
270
  let lastError: unknown;
366
271
 
367
272
  for (let i = 0; i < bases.length; i++) {
368
- const url = `${bases[i]}${path}`;
369
273
  try {
370
274
  const headers = { ...authHeaders, ...(init?.headers as Record<string, string>) };
371
- return await fetch(url, { ...init, headers });
275
+ return await fetch(`${bases[i]}${path}`, { ...init, headers });
372
276
  } catch (err) {
373
277
  lastError = err;
374
278
  if (i === bases.length - 1) throw err;
375
279
  }
376
280
  }
377
-
378
281
  throw lastError ?? new Error("Memory API request failed");
379
282
  }
380
283
 
381
- export async function pushConfigToMemory(
382
- config: MemoryConfig
383
- ): Promise<{ ok: boolean; error?: string }> {
384
- try {
385
- const res = await callMemoryApi("/api/v1/config/", {
386
- method: "PUT",
387
- headers: { "content-type": "application/json" },
388
- body: JSON.stringify(config),
389
- });
390
- if (!res.ok) {
391
- const text = await res.text().catch(() => "");
392
- return { ok: false, error: `HTTP ${res.status}: ${text}` };
393
- }
394
- return { ok: true };
395
- } catch (err) {
396
- return { ok: false, error: String(err) };
397
- }
398
- }
399
-
400
- export async function fetchConfigFromMemory(): Promise<MemoryConfig | null> {
401
- try {
402
- const res = await callMemoryApi("/api/v1/config/");
403
- if (!res.ok) return null;
404
- return (await res.json()) as MemoryConfig;
405
- } catch {
406
- return null;
407
- }
408
- }
409
-
410
284
  export async function provisionMemoryUser(
411
285
  userId: string,
412
286
  ): Promise<{ ok: boolean; error?: string }> {