openclaw-memory-decay 0.1.5 → 0.1.7
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/README.md +75 -34
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -1
- package/scripts/detect-python-lib.mjs +90 -0
- package/scripts/detect-python.mjs +9 -25
- package/src/index.ts +18 -8
- package/src/python-env.ts +133 -0
package/README.md
CHANGED
|
@@ -51,14 +51,17 @@ Activation
|
|
|
51
51
|
- **Proactive agent saves** — the agent stores preferences, decisions, facts, and episodes without being asked
|
|
52
52
|
- **Freshness indicators** — search results include `fresh` / `normal` / `stale` so the agent can judge reliability
|
|
53
53
|
- **Dual-score model** — storage score (can it be found?) and retrieval score (how easily?) are tracked separately
|
|
54
|
-
-
|
|
54
|
+
- **Memory tools** — the plugin exposes `memory_search`, `memory_store`, and `memory_store_batch`
|
|
55
55
|
- **Markdown migration** — imports existing `~/.openclaw/workspace/memory/` files on first run
|
|
56
56
|
|
|
57
57
|
## Quick Start
|
|
58
58
|
|
|
59
|
+
> Deprecated: installing from a cloned checkout via `openclaw plugins install -l .` is no longer the recommended path. Use the published npm package instead.
|
|
60
|
+
|
|
59
61
|
```bash
|
|
60
|
-
# 1. Install
|
|
61
|
-
|
|
62
|
+
# 1. Install the backend into a dedicated virtualenv
|
|
63
|
+
python3 -m venv ~/.openclaw/venvs/memory-decay
|
|
64
|
+
~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
|
|
62
65
|
|
|
63
66
|
# 2. Install this plugin from npm
|
|
64
67
|
openclaw plugins install openclaw-memory-decay
|
|
@@ -70,13 +73,44 @@ openclaw config set plugins.allow '["memory-decay"]'
|
|
|
70
73
|
openclaw gateway restart
|
|
71
74
|
```
|
|
72
75
|
|
|
73
|
-
> **Note:**
|
|
76
|
+
> **Note:** If the plugin is already loaded, install may succeed silently. Use `openclaw plugins list` to confirm status is `loaded`.
|
|
74
77
|
|
|
75
78
|
### Prerequisites
|
|
76
79
|
|
|
77
80
|
- [OpenClaw](https://openclaw.ai) installed globally
|
|
78
81
|
- Python 3.10+
|
|
79
|
-
-
|
|
82
|
+
- A Python virtualenv containing `memory-decay`
|
|
83
|
+
|
|
84
|
+
## Migration From Deprecated Local Install
|
|
85
|
+
|
|
86
|
+
If you previously installed this plugin from a local checkout via `openclaw plugins install -l .`, migrate to the npm package. Local/path-based installs are now deprecated in favor of the published npm package.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# 1. Install from npm instead
|
|
90
|
+
openclaw plugins install openclaw-memory-decay
|
|
91
|
+
|
|
92
|
+
# 2. Restart gateway
|
|
93
|
+
openclaw gateway restart
|
|
94
|
+
|
|
95
|
+
# 3. Verify
|
|
96
|
+
openclaw plugins list | grep memory-decay # should show: memory-decay | loaded
|
|
97
|
+
curl -s http://127.0.0.1:8100/health # should show: {"status":"ok","current_tick":0}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If auto-detection does not recover your backend path after migration, set the interpreter explicitly:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
openclaw config set plugins.entries.memory-decay.config.pythonPath "~/.openclaw/venvs/memory-decay/bin/python"
|
|
104
|
+
openclaw gateway restart
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Your memories are safe.** The SQLite database (`memories.db`) is not affected by plugin reinstallation or migration to npm install.
|
|
108
|
+
|
|
109
|
+
To update in the future:
|
|
110
|
+
```bash
|
|
111
|
+
openclaw plugins update memory-decay
|
|
112
|
+
openclaw gateway restart
|
|
113
|
+
```
|
|
80
114
|
|
|
81
115
|
## Configuration
|
|
82
116
|
|
|
@@ -102,8 +136,8 @@ Add to `~/.openclaw/openclaw.json` under `plugins.entries.memory-decay.config`:
|
|
|
102
136
|
| Option | Default | Description |
|
|
103
137
|
|--------|---------|-------------|
|
|
104
138
|
| `serverPort` | `8100` | Port for the memory-decay HTTP server |
|
|
105
|
-
| `memoryDecayPath` | (auto) | Path to memory-decay-core. Auto-detected from
|
|
106
|
-
| `pythonPath` | `python3` | Path to Python interpreter
|
|
139
|
+
| `memoryDecayPath` | (auto) | Path to memory-decay-core. Auto-detected from the install-time Python environment if not set |
|
|
140
|
+
| `pythonPath` | `python3` | Path to Python interpreter. Set this explicitly if you use a custom venv path instead of `~/.openclaw/venvs/memory-decay` |
|
|
107
141
|
| `dbPath` | `~/.openclaw/memory-decay-data/memories.db` | SQLite database location |
|
|
108
142
|
| `autoSave` | `true` | Auto-save every conversation turn at low importance. Set `false` to let the agent decide what to save |
|
|
109
143
|
| `embeddingProvider` | `local` | Embedding provider: `local`, `openai`, or `gemini` |
|
|
@@ -149,7 +183,7 @@ The bootstrap prompt and skills guide the agent to pick the right category and i
|
|
|
149
183
|
| `fact` | Technical facts, API behaviors, architecture | 0.7–0.9 | "Auth service returns inconsistent 4xx on token expiry" |
|
|
150
184
|
| `episode` | What was worked on, session context | 0.3–0.6 | "Finished migrating auth middleware" |
|
|
151
185
|
|
|
152
|
-
The agent stores proactively based on conversation triggers — it doesn't wait
|
|
186
|
+
The agent stores proactively based on conversation triggers — it doesn't wait to be explicitly asked.
|
|
153
187
|
|
|
154
188
|
## How It Works
|
|
155
189
|
|
|
@@ -192,17 +226,15 @@ The plugin manages the Python server lifecycle — starts with the gateway, stop
|
|
|
192
226
|
4. **Reinforce** — recalled memories get boosted (testing effect), stability grows
|
|
193
227
|
5. **Forget** — memories with very low activation become practically unretrievable
|
|
194
228
|
|
|
195
|
-
##
|
|
229
|
+
## Tools
|
|
196
230
|
|
|
197
|
-
The plugin registers these
|
|
231
|
+
The plugin registers these tools:
|
|
198
232
|
|
|
199
|
-
|
|
|
200
|
-
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
| `/memory-status` | `/memory-status` | Show memory count, tick, and decay stats |
|
|
205
|
-
| `/migrate` | `/migrate` | Import Markdown files from `memory/` directory |
|
|
233
|
+
| Tool | Purpose |
|
|
234
|
+
|------|---------|
|
|
235
|
+
| `memory_search` | Search memories with decay-aware ranking and freshness indicators |
|
|
236
|
+
| `memory_store` | Save a single memory with calibrated importance and category |
|
|
237
|
+
| `memory_store_batch` | Save multiple memories efficiently in one call |
|
|
206
238
|
|
|
207
239
|
## Troubleshooting
|
|
208
240
|
|
|
@@ -224,16 +256,26 @@ openclaw plugins install openclaw-memory-decay
|
|
|
224
256
|
### `Memory service not running`
|
|
225
257
|
|
|
226
258
|
```bash
|
|
227
|
-
# Check if
|
|
228
|
-
|
|
259
|
+
# Check if the configured Python can import the server
|
|
260
|
+
~/.openclaw/venvs/memory-decay/bin/python -c "import memory_decay.server; print('ok')"
|
|
229
261
|
|
|
230
262
|
# Check server health
|
|
231
263
|
curl http://127.0.0.1:8100/health
|
|
232
264
|
```
|
|
233
265
|
|
|
234
|
-
If
|
|
266
|
+
If the import check fails, install the backend into a virtualenv and either reactivate that venv before reinstalling the plugin or set `pythonPath` explicitly:
|
|
235
267
|
```bash
|
|
236
|
-
|
|
268
|
+
python3 -m venv ~/.openclaw/venvs/memory-decay
|
|
269
|
+
~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### `error: externally-managed-environment`
|
|
273
|
+
|
|
274
|
+
Your system Python is PEP 668 managed. Install `memory-decay` into a virtualenv instead of the system interpreter:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
python3 -m venv ~/.openclaw/venvs/memory-decay
|
|
278
|
+
~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
|
|
237
279
|
```
|
|
238
280
|
|
|
239
281
|
### Plugin shows `error` status
|
|
@@ -245,27 +287,26 @@ openclaw plugins doctor
|
|
|
245
287
|
## For AI Agents: Self-Installation Guide
|
|
246
288
|
|
|
247
289
|
> This section is written for LLM agents to follow autonomously. If you're a human, use the [Quick Start](#quick-start) section above.
|
|
290
|
+
> Use the published npm package. Do not install this plugin from a cloned checkout unless you are actively developing it.
|
|
248
291
|
|
|
249
292
|
### Quick Install (one-shot)
|
|
250
293
|
|
|
251
294
|
```bash
|
|
252
|
-
# 1. Install the backend
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
# 2. Verify
|
|
256
|
-
pip show memory-decay | head -3
|
|
295
|
+
# 1. Install the backend into a dedicated virtualenv
|
|
296
|
+
python3 -m venv ~/.openclaw/venvs/memory-decay
|
|
297
|
+
~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
|
|
257
298
|
|
|
258
|
-
#
|
|
299
|
+
# 2. Install plugin from npm
|
|
259
300
|
openclaw plugins install openclaw-memory-decay
|
|
260
301
|
|
|
261
|
-
#
|
|
302
|
+
# 3. Restart gateway
|
|
262
303
|
openclaw gateway restart
|
|
263
304
|
|
|
264
|
-
#
|
|
305
|
+
# 4. Verify plugin is loaded
|
|
265
306
|
openclaw plugins list
|
|
266
307
|
# Look for: memory-decay | loaded
|
|
267
308
|
|
|
268
|
-
#
|
|
309
|
+
# 5. Check server health
|
|
269
310
|
curl -s http://127.0.0.1:8100/health
|
|
270
311
|
# Expected: {"status":"ok","current_tick":0}
|
|
271
312
|
```
|
|
@@ -285,14 +326,14 @@ openclaw plugins install openclaw-memory-decay
|
|
|
285
326
|
|
|
286
327
|
**`Memory service not running`:**
|
|
287
328
|
```bash
|
|
288
|
-
# Check backend
|
|
289
|
-
|
|
329
|
+
# Check the configured Python can import the backend server
|
|
330
|
+
~/.openclaw/venvs/memory-decay/bin/python -c "import memory_decay.server; print('ok')"
|
|
290
331
|
|
|
291
332
|
# Check if gateway started it
|
|
292
333
|
curl -s http://127.0.0.1:8100/health
|
|
293
334
|
|
|
294
|
-
# If
|
|
295
|
-
pip install memory-decay
|
|
335
|
+
# If the import fails, reinstall into the venv:
|
|
336
|
+
~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
|
|
296
337
|
openclaw gateway restart
|
|
297
338
|
```
|
|
298
339
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-memory-decay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw memory plugin backed by memory-decay engine",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -41,5 +41,8 @@
|
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|
|
43
43
|
"url": "https://github.com/memory-decay/openclaw-memory-decay"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync as nodeExistsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { homedir as nodeHomedir, tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
function resolvePythonPath(command, { execFileSync = nodeExecFileSync, isWin = process.platform === "win32" } = {}) {
|
|
7
|
+
if (isAbsolute(command)) {
|
|
8
|
+
return command;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const resolver = isWin ? "where" : "which";
|
|
12
|
+
return execFileSync(resolver, [command], { encoding: "utf8" }).trim().split(/\r?\n/)[0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveMemoryDecayPath(moduleFile) {
|
|
16
|
+
const packageDir = dirname(resolve(moduleFile));
|
|
17
|
+
const packageParent = dirname(packageDir);
|
|
18
|
+
return basename(packageParent) === "src" ? dirname(packageParent) : packageParent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildPythonCandidates({
|
|
22
|
+
pluginRoot,
|
|
23
|
+
env = process.env,
|
|
24
|
+
isWin = process.platform === "win32",
|
|
25
|
+
existsSync = nodeExistsSync,
|
|
26
|
+
homedir = nodeHomedir,
|
|
27
|
+
} = {}) {
|
|
28
|
+
const openclawStateDir = resolve(env.OPENCLAW_HOME || homedir(), ".openclaw");
|
|
29
|
+
const siblingRoots = [
|
|
30
|
+
resolve(pluginRoot, "../memory-decay"),
|
|
31
|
+
resolve(pluginRoot, "../memory-decay-core"),
|
|
32
|
+
];
|
|
33
|
+
const pathCandidates = [];
|
|
34
|
+
|
|
35
|
+
if (env.MD_PYTHON_PATH) pathCandidates.push(env.MD_PYTHON_PATH);
|
|
36
|
+
pathCandidates.push(
|
|
37
|
+
join(openclawStateDir, "venvs", "memory-decay", isWin ? "Scripts/python.exe" : "bin/python"),
|
|
38
|
+
);
|
|
39
|
+
if (env.VIRTUAL_ENV) {
|
|
40
|
+
pathCandidates.push(join(env.VIRTUAL_ENV, isWin ? "Scripts/python.exe" : "bin/python"));
|
|
41
|
+
}
|
|
42
|
+
for (const root of siblingRoots) {
|
|
43
|
+
pathCandidates.push(join(root, isWin ? ".venv/Scripts/python.exe" : ".venv/bin/python"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const commandCandidates = isWin ? ["python"] : ["python3", "python"];
|
|
47
|
+
return [...new Set([
|
|
48
|
+
...pathCandidates.filter((candidate) => existsSync(candidate)),
|
|
49
|
+
...commandCandidates,
|
|
50
|
+
])];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function detectPythonEnv({
|
|
54
|
+
pluginRoot,
|
|
55
|
+
env = process.env,
|
|
56
|
+
isWin = process.platform === "win32",
|
|
57
|
+
existsSync = nodeExistsSync,
|
|
58
|
+
execFileSync = nodeExecFileSync,
|
|
59
|
+
makeTempDir = () => mkdtempSync(join(tmpdir(), "memory-decay-python-env-")),
|
|
60
|
+
readPathFile = (path) => readFileSync(path, "utf8"),
|
|
61
|
+
removeTempDir = (path) => rmSync(path, { recursive: true, force: true }),
|
|
62
|
+
} = {}) {
|
|
63
|
+
for (const candidate of buildPythonCandidates({ pluginRoot, env, isWin, existsSync })) {
|
|
64
|
+
try {
|
|
65
|
+
execFileSync(candidate, ["-c", "import memory_decay.server"], { stdio: "ignore" });
|
|
66
|
+
const tempDir = makeTempDir();
|
|
67
|
+
const outputPath = join(tempDir, "memory-decay-module-path.txt");
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
execFileSync(candidate, [
|
|
71
|
+
"-c",
|
|
72
|
+
`import memory_decay, pathlib; pathlib.Path(${JSON.stringify(outputPath)}).write_text(str(pathlib.Path(memory_decay.__file__).resolve()))`,
|
|
73
|
+
], { stdio: "ignore" });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
removeTempDir(tempDir);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const moduleFile = readPathFile(outputPath).trim();
|
|
80
|
+
removeTempDir(tempDir);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
pythonPath: resolvePythonPath(candidate, { execFileSync, isWin }),
|
|
84
|
+
memoryDecayPath: resolveMemoryDecayPath(moduleFile),
|
|
85
|
+
};
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { writeFileSync } from "node:fs";
|
|
4
3
|
import { resolve, dirname } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { detectPythonEnv } from "./detect-python-lib.mjs";
|
|
6
6
|
|
|
7
7
|
const root = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
9
|
-
const candidates = isWin ? ["python"] : ["python3", "python"];
|
|
8
|
+
const detected = detectPythonEnv({ pluginRoot: resolve(root, "..") });
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
);
|
|
17
|
-
return true;
|
|
18
|
-
} catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
if (!python) {
|
|
24
|
-
console.warn("[memory-decay] memory_decay not found — run: pip install memory-decay");
|
|
10
|
+
if (!detected) {
|
|
11
|
+
console.warn(
|
|
12
|
+
"[memory-decay] memory_decay server not found in an active or sibling Python environment. " +
|
|
13
|
+
"Install the backend in a venv and rerun install, or set pythonPath in plugin config.",
|
|
14
|
+
);
|
|
25
15
|
process.exit(0);
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`${python} -c "import memory_decay,os; print(os.path.dirname(os.path.dirname(memory_decay.__file__)))"`,
|
|
31
|
-
{ encoding: "utf8" }
|
|
32
|
-
).trim();
|
|
33
|
-
|
|
34
|
-
writeFileSync(resolve(root, "../.python-env.json"), JSON.stringify({ pythonPath, memoryDecayPath }, null, 2));
|
|
35
|
-
console.log(`[memory-decay] Detected: ${pythonPath}`);
|
|
18
|
+
writeFileSync(resolve(root, "../.python-env.json"), JSON.stringify(detected, null, 2));
|
|
19
|
+
console.log(`[memory-decay] Detected: ${detected.pythonPath}`);
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
|
3
3
|
import { MemoryDecayClient } from "./client.js";
|
|
4
4
|
import { MemoryDecayService, type ServiceConfig } from "./service.js";
|
|
5
5
|
import { shouldMigrate, migrateMarkdownMemories } from "./migrator.js";
|
|
6
|
+
import { detectPythonEnv, mergePythonEnv } from "./python-env.js";
|
|
6
7
|
import { toFreshness } from "./types.js";
|
|
7
8
|
|
|
8
9
|
const BOOTSTRAP_PROMPT = `## Memory System (memory-decay)
|
|
@@ -88,21 +89,30 @@ const memoryDecayPlugin = {
|
|
|
88
89
|
// Resolve pythonPath + memoryDecayPath: config > .python-env.json (set at install time) > error
|
|
89
90
|
let memoryDecayPath = (cfg.memoryDecayPath as string) ?? "";
|
|
90
91
|
let pythonPath = (cfg.pythonPath as string) ?? "";
|
|
91
|
-
|
|
92
|
+
let detectedEnv: { memoryDecayPath?: string; pythonPath?: string } = {};
|
|
93
|
+
const { dirname, resolve } = await import("node:path");
|
|
94
|
+
const { fileURLToPath } = await import("node:url");
|
|
95
|
+
const pluginRoot = dirname(fileURLToPath(import.meta.url));
|
|
96
|
+
if (!memoryDecayPath || !pythonPath) {
|
|
92
97
|
try {
|
|
93
98
|
const { readFileSync } = await import("node:fs");
|
|
94
|
-
|
|
95
|
-
const { fileURLToPath } = await import("node:url");
|
|
96
|
-
const pluginRoot = dirname(fileURLToPath(import.meta.url));
|
|
97
|
-
const detected = JSON.parse(readFileSync(resolve(pluginRoot, "../.python-env.json"), "utf8"));
|
|
98
|
-
if (!pythonPath && detected.pythonPath) pythonPath = detected.pythonPath;
|
|
99
|
-
if (detected.memoryDecayPath) memoryDecayPath = detected.memoryDecayPath;
|
|
99
|
+
detectedEnv = JSON.parse(readFileSync(resolve(pluginRoot, "../.python-env.json"), "utf8"));
|
|
100
100
|
} catch {}
|
|
101
101
|
}
|
|
102
|
+
if (!detectedEnv.memoryDecayPath || !detectedEnv.pythonPath) {
|
|
103
|
+
detectedEnv = mergePythonEnv(
|
|
104
|
+
detectedEnv,
|
|
105
|
+
detectPythonEnv({ pluginRoot }) ?? {},
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
({ memoryDecayPath, pythonPath } = mergePythonEnv(
|
|
109
|
+
{ memoryDecayPath, pythonPath },
|
|
110
|
+
detectedEnv,
|
|
111
|
+
));
|
|
102
112
|
if (!memoryDecayPath) {
|
|
103
113
|
ctx.logger.error(
|
|
104
114
|
"Could not auto-detect memory-decay installation. " +
|
|
105
|
-
"
|
|
115
|
+
"Install the backend into ~/.openclaw/venvs/memory-decay or set pythonPath/memoryDecayPath in plugin config."
|
|
106
116
|
);
|
|
107
117
|
return;
|
|
108
118
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync as nodeExistsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { homedir as nodeHomedir, tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface PythonEnvLike {
|
|
7
|
+
memoryDecayPath?: string;
|
|
8
|
+
pythonPath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DetectPythonEnvOptions {
|
|
12
|
+
pluginRoot: string;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
isWin?: boolean;
|
|
15
|
+
homedir?: () => string;
|
|
16
|
+
existsSync?: (path: string) => boolean;
|
|
17
|
+
execFileSync?: typeof nodeExecFileSync;
|
|
18
|
+
makeTempDir?: () => string;
|
|
19
|
+
readPathFile?: (path: string) => string;
|
|
20
|
+
removeTempDir?: (path: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mergePythonEnv(
|
|
24
|
+
configured: PythonEnvLike,
|
|
25
|
+
detected: PythonEnvLike = {},
|
|
26
|
+
): { memoryDecayPath: string; pythonPath: string } {
|
|
27
|
+
return {
|
|
28
|
+
memoryDecayPath: configured.memoryDecayPath || detected.memoryDecayPath || "",
|
|
29
|
+
pythonPath: configured.pythonPath || detected.pythonPath || "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveOpenClawStateDir(
|
|
34
|
+
env: NodeJS.ProcessEnv,
|
|
35
|
+
homedir: () => string,
|
|
36
|
+
): string {
|
|
37
|
+
return resolve(env.OPENCLAW_HOME || homedir(), ".openclaw");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveMemoryDecayPath(moduleFile: string): string {
|
|
41
|
+
const packageDir = dirname(resolve(moduleFile));
|
|
42
|
+
const packageParent = dirname(packageDir);
|
|
43
|
+
return basename(packageParent) === "src" ? dirname(packageParent) : packageParent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildPythonCandidates({
|
|
47
|
+
pluginRoot,
|
|
48
|
+
env = process.env,
|
|
49
|
+
isWin = process.platform === "win32",
|
|
50
|
+
homedir = nodeHomedir,
|
|
51
|
+
existsSync = nodeExistsSync,
|
|
52
|
+
}: Omit<DetectPythonEnvOptions, "execFileSync" | "makeTempDir" | "readPathFile" | "removeTempDir">): string[] {
|
|
53
|
+
const siblingRoots = [
|
|
54
|
+
resolve(pluginRoot, "../../memory-decay"),
|
|
55
|
+
resolve(pluginRoot, "../../memory-decay-core"),
|
|
56
|
+
resolve(pluginRoot, "../memory-decay"),
|
|
57
|
+
resolve(pluginRoot, "../memory-decay-core"),
|
|
58
|
+
];
|
|
59
|
+
const openclawStateDir = resolveOpenClawStateDir(env, homedir);
|
|
60
|
+
const recommendedVenv = join(
|
|
61
|
+
openclawStateDir,
|
|
62
|
+
"venvs",
|
|
63
|
+
"memory-decay",
|
|
64
|
+
isWin ? "Scripts/python.exe" : "bin/python",
|
|
65
|
+
);
|
|
66
|
+
const pathCandidates = [
|
|
67
|
+
env.MD_PYTHON_PATH,
|
|
68
|
+
recommendedVenv,
|
|
69
|
+
env.VIRTUAL_ENV
|
|
70
|
+
? join(env.VIRTUAL_ENV, isWin ? "Scripts/python.exe" : "bin/python")
|
|
71
|
+
: undefined,
|
|
72
|
+
...siblingRoots.map((root) =>
|
|
73
|
+
join(root, isWin ? ".venv/Scripts/python.exe" : ".venv/bin/python")),
|
|
74
|
+
].filter((candidate): candidate is string => typeof candidate === "string" && candidate.length > 0 && existsSync(candidate));
|
|
75
|
+
|
|
76
|
+
const commandCandidates = isWin ? ["python"] : ["python3", "python"];
|
|
77
|
+
return [...new Set([...pathCandidates, ...commandCandidates])];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolvePythonPath(
|
|
81
|
+
command: string,
|
|
82
|
+
{
|
|
83
|
+
execFileSync = nodeExecFileSync,
|
|
84
|
+
isWin = process.platform === "win32",
|
|
85
|
+
}: Pick<DetectPythonEnvOptions, "execFileSync" | "isWin"> = {},
|
|
86
|
+
): string {
|
|
87
|
+
if (isAbsolute(command)) {
|
|
88
|
+
return command;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolver = isWin ? "where" : "which";
|
|
92
|
+
return execFileSync(resolver, [command], { encoding: "utf8" }).trim().split(/\r?\n/)[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function detectPythonEnv({
|
|
96
|
+
pluginRoot,
|
|
97
|
+
env = process.env,
|
|
98
|
+
isWin = process.platform === "win32",
|
|
99
|
+
homedir = nodeHomedir,
|
|
100
|
+
existsSync = nodeExistsSync,
|
|
101
|
+
execFileSync = nodeExecFileSync,
|
|
102
|
+
makeTempDir = () => mkdtempSync(join(tmpdir(), "memory-decay-python-env-")),
|
|
103
|
+
readPathFile = (path) => readFileSync(path, "utf8"),
|
|
104
|
+
removeTempDir = (path) => rmSync(path, { recursive: true, force: true }),
|
|
105
|
+
}: DetectPythonEnvOptions): { memoryDecayPath: string; pythonPath: string } | null {
|
|
106
|
+
for (const candidate of buildPythonCandidates({ pluginRoot, env, isWin, homedir, existsSync })) {
|
|
107
|
+
try {
|
|
108
|
+
execFileSync(candidate, ["-c", "import memory_decay.server"], { stdio: "ignore" });
|
|
109
|
+
const tempDir = makeTempDir();
|
|
110
|
+
const outputPath = join(tempDir, "memory-decay-module-path.txt");
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
execFileSync(candidate, [
|
|
114
|
+
"-c",
|
|
115
|
+
`import memory_decay, pathlib; pathlib.Path(${JSON.stringify(outputPath)}).write_text(str(pathlib.Path(memory_decay.__file__).resolve()))`,
|
|
116
|
+
], { stdio: "ignore" });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
removeTempDir(tempDir);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const moduleFile = readPathFile(outputPath).trim();
|
|
123
|
+
removeTempDir(tempDir);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
pythonPath: resolvePythonPath(candidate, { execFileSync, isWin }),
|
|
127
|
+
memoryDecayPath: resolveMemoryDecayPath(moduleFile),
|
|
128
|
+
};
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|