openclaw-memory-decay 0.1.5 → 0.1.6

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 CHANGED
@@ -51,16 +51,20 @@ 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
- - **`/remember` skill** — users can explicitly ask the agent to remember something
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 memory-decay-core (the backend engine)
61
- pip install memory-decay
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
- # 2. Install this plugin from npm
66
+ # 2. Install this plugin while that venv is active
67
+ source ~/.openclaw/venvs/memory-decay/bin/activate
64
68
  openclaw plugins install openclaw-memory-decay
65
69
 
66
70
  # 3. (Optional but recommended) Restrict auto-load to trusted plugins only
@@ -70,13 +74,44 @@ openclaw config set plugins.allow '["memory-decay"]'
70
74
  openclaw gateway restart
71
75
  ```
72
76
 
73
- > **Note:** Steps 5 and 6 both succeed silently if the plugin is already loaded use `openclaw plugins list` to confirm status is `loaded` and origin is `config`.
77
+ > **Note:** If the plugin is already loaded, install may succeed silently. Use `openclaw plugins list` to confirm status is `loaded`.
74
78
 
75
79
  ### Prerequisites
76
80
 
77
81
  - [OpenClaw](https://openclaw.ai) installed globally
78
82
  - Python 3.10+
79
- - `memory-decay` Python package (`pip install memory-decay`)
83
+ - A Python virtualenv containing `memory-decay`
84
+
85
+ ## Migration From Deprecated Local Install
86
+
87
+ 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.
88
+
89
+ ```bash
90
+ # 1. Install from npm instead
91
+ openclaw plugins install openclaw-memory-decay
92
+
93
+ # 2. Restart gateway
94
+ openclaw gateway restart
95
+
96
+ # 3. Verify
97
+ openclaw plugins list | grep memory-decay # should show: memory-decay | loaded
98
+ curl -s http://127.0.0.1:8100/health # should show: {"status":"ok","current_tick":0}
99
+ ```
100
+
101
+ If auto-detection does not recover your backend path after migration, set the interpreter explicitly:
102
+
103
+ ```bash
104
+ openclaw config set plugins.entries.memory-decay.config.pythonPath "~/.openclaw/venvs/memory-decay/bin/python"
105
+ openclaw gateway restart
106
+ ```
107
+
108
+ **Your memories are safe.** The SQLite database (`memories.db`) is not affected by plugin reinstallation or migration to npm install.
109
+
110
+ To update in the future:
111
+ ```bash
112
+ openclaw plugins update memory-decay
113
+ openclaw gateway restart
114
+ ```
80
115
 
81
116
  ## Configuration
82
117
 
@@ -102,8 +137,8 @@ Add to `~/.openclaw/openclaw.json` under `plugins.entries.memory-decay.config`:
102
137
  | Option | Default | Description |
103
138
  |--------|---------|-------------|
104
139
  | `serverPort` | `8100` | Port for the memory-decay HTTP server |
105
- | `memoryDecayPath` | (auto) | Path to memory-decay-core. Auto-detected from `pip show memory-decay` if not set |
106
- | `pythonPath` | `python3` | Path to Python interpreter (use your venv) |
140
+ | `memoryDecayPath` | (auto) | Path to memory-decay-core. Auto-detected from the install-time Python environment if not set |
141
+ | `pythonPath` | `python3` | Path to Python interpreter. Set this explicitly if the gateway runs outside the install-time venv |
107
142
  | `dbPath` | `~/.openclaw/memory-decay-data/memories.db` | SQLite database location |
108
143
  | `autoSave` | `true` | Auto-save every conversation turn at low importance. Set `false` to let the agent decide what to save |
109
144
  | `embeddingProvider` | `local` | Embedding provider: `local`, `openai`, or `gemini` |
@@ -149,7 +184,7 @@ The bootstrap prompt and skills guide the agent to pick the right category and i
149
184
  | `fact` | Technical facts, API behaviors, architecture | 0.7–0.9 | "Auth service returns inconsistent 4xx on token expiry" |
150
185
  | `episode` | What was worked on, session context | 0.3–0.6 | "Finished migrating auth middleware" |
151
186
 
152
- The agent stores proactively based on conversation triggers — it doesn't wait for `/remember`.
187
+ The agent stores proactively based on conversation triggers — it doesn't wait to be explicitly asked.
153
188
 
154
189
  ## How It Works
155
190
 
@@ -192,17 +227,15 @@ The plugin manages the Python server lifecycle — starts with the gateway, stop
192
227
  4. **Reinforce** — recalled memories get boosted (testing effect), stability grows
193
228
  5. **Forget** — memories with very low activation become practically unretrievable
194
229
 
195
- ## Skills
230
+ ## Tools
196
231
 
197
- The plugin registers these skills:
232
+ The plugin registers these tools:
198
233
 
199
- | Skill | Trigger | Description |
200
- |-------|---------|-------------|
201
- | `/remember` | `/remember I prefer dark mode` | Save with correct category and calibrated importance |
202
- | `/recall` | `/recall what did we decide about the API?` | Search memories with freshness-aware action guidance |
203
- | `/forget` | `/forget the temp password` | Delete a specific memory |
204
- | `/memory-status` | `/memory-status` | Show memory count, tick, and decay stats |
205
- | `/migrate` | `/migrate` | Import Markdown files from `memory/` directory |
234
+ | Tool | Purpose |
235
+ |------|---------|
236
+ | `memory_search` | Search memories with decay-aware ranking and freshness indicators |
237
+ | `memory_store` | Save a single memory with calibrated importance and category |
238
+ | `memory_store_batch` | Save multiple memories efficiently in one call |
206
239
 
207
240
  ## Troubleshooting
208
241
 
@@ -224,16 +257,26 @@ openclaw plugins install openclaw-memory-decay
224
257
  ### `Memory service not running`
225
258
 
226
259
  ```bash
227
- # Check if memory-decay is installed
228
- pip show memory-decay
260
+ # Check if the configured Python can import the server
261
+ ~/.openclaw/venvs/memory-decay/bin/python -c "import memory_decay.server; print('ok')"
229
262
 
230
263
  # Check server health
231
264
  curl http://127.0.0.1:8100/health
232
265
  ```
233
266
 
234
- If `pip show memory-decay` returns nothing, install it:
267
+ If the import check fails, install the backend into a virtualenv and either reactivate that venv before reinstalling the plugin or set `pythonPath` explicitly:
268
+ ```bash
269
+ python3 -m venv ~/.openclaw/venvs/memory-decay
270
+ ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
271
+ ```
272
+
273
+ ### `error: externally-managed-environment`
274
+
275
+ Your system Python is PEP 668 managed. Install `memory-decay` into a virtualenv instead of the system interpreter:
276
+
235
277
  ```bash
236
- pip install memory-decay
278
+ python3 -m venv ~/.openclaw/venvs/memory-decay
279
+ ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
237
280
  ```
238
281
 
239
282
  ### Plugin shows `error` status
@@ -245,15 +288,17 @@ openclaw plugins doctor
245
288
  ## For AI Agents: Self-Installation Guide
246
289
 
247
290
  > This section is written for LLM agents to follow autonomously. If you're a human, use the [Quick Start](#quick-start) section above.
291
+ > Use the published npm package. Do not install this plugin from a cloned checkout unless you are actively developing it.
248
292
 
249
293
  ### Quick Install (one-shot)
250
294
 
251
295
  ```bash
252
- # 1. Install the backend engine (requires Python 3.10+)
253
- pip install memory-decay
296
+ # 1. Install the backend into a dedicated virtualenv
297
+ python3 -m venv ~/.openclaw/venvs/memory-decay
298
+ ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
254
299
 
255
- # 2. Verify
256
- pip show memory-decay | head -3
300
+ # 2. Activate the venv so postinstall detects the same interpreter
301
+ source ~/.openclaw/venvs/memory-decay/bin/activate
257
302
 
258
303
  # 3. Install plugin from npm
259
304
  openclaw plugins install openclaw-memory-decay
@@ -285,14 +330,14 @@ openclaw plugins install openclaw-memory-decay
285
330
 
286
331
  **`Memory service not running`:**
287
332
  ```bash
288
- # Check backend is installed
289
- pip show memory-decay
333
+ # Check the configured Python can import the backend server
334
+ ~/.openclaw/venvs/memory-decay/bin/python -c "import memory_decay.server; print('ok')"
290
335
 
291
336
  # Check if gateway started it
292
337
  curl -s http://127.0.0.1:8100/health
293
338
 
294
- # If pip show returns nothing:
295
- pip install memory-decay
339
+ # If the import fails, reinstall into the venv:
340
+ ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
296
341
  openclaw gateway restart
297
342
  ```
298
343
 
@@ -2,7 +2,7 @@
2
2
  "id": "memory-decay",
3
3
  "name": "Memory Decay",
4
4
  "description": "Human-like memory with decay and reinforcement for OpenClaw agents",
5
- "version": "0.1.4",
5
+ "version": "0.1.6",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-memory-decay",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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,85 @@
1
+ import { execFileSync as nodeExecFileSync } from "node:child_process";
2
+ import { existsSync as nodeExistsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { 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
+ } = {}) {
27
+ const siblingRoots = [
28
+ resolve(pluginRoot, "../memory-decay"),
29
+ resolve(pluginRoot, "../memory-decay-core"),
30
+ ];
31
+ const pathCandidates = [];
32
+
33
+ if (env.MD_PYTHON_PATH) pathCandidates.push(env.MD_PYTHON_PATH);
34
+ if (env.VIRTUAL_ENV) {
35
+ pathCandidates.push(join(env.VIRTUAL_ENV, isWin ? "Scripts/python.exe" : "bin/python"));
36
+ }
37
+ for (const root of siblingRoots) {
38
+ pathCandidates.push(join(root, isWin ? ".venv/Scripts/python.exe" : ".venv/bin/python"));
39
+ }
40
+
41
+ const commandCandidates = isWin ? ["python"] : ["python3", "python"];
42
+ return [...new Set([
43
+ ...pathCandidates.filter((candidate) => existsSync(candidate)),
44
+ ...commandCandidates,
45
+ ])];
46
+ }
47
+
48
+ export function detectPythonEnv({
49
+ pluginRoot,
50
+ env = process.env,
51
+ isWin = process.platform === "win32",
52
+ existsSync = nodeExistsSync,
53
+ execFileSync = nodeExecFileSync,
54
+ makeTempDir = () => mkdtempSync(join(tmpdir(), "memory-decay-python-env-")),
55
+ readPathFile = (path) => readFileSync(path, "utf8"),
56
+ removeTempDir = (path) => rmSync(path, { recursive: true, force: true }),
57
+ } = {}) {
58
+ for (const candidate of buildPythonCandidates({ pluginRoot, env, isWin, existsSync })) {
59
+ try {
60
+ execFileSync(candidate, ["-c", "import memory_decay.server"], { stdio: "ignore" });
61
+ const tempDir = makeTempDir();
62
+ const outputPath = join(tempDir, "memory-decay-module-path.txt");
63
+
64
+ try {
65
+ execFileSync(candidate, [
66
+ "-c",
67
+ `import memory_decay, pathlib; pathlib.Path(${JSON.stringify(outputPath)}).write_text(str(pathlib.Path(memory_decay.__file__).resolve()))`,
68
+ ], { stdio: "ignore" });
69
+ } catch (error) {
70
+ removeTempDir(tempDir);
71
+ throw error;
72
+ }
73
+
74
+ const moduleFile = readPathFile(outputPath).trim();
75
+ removeTempDir(tempDir);
76
+
77
+ return {
78
+ pythonPath: resolvePythonPath(candidate, { execFileSync, isWin }),
79
+ memoryDecayPath: resolveMemoryDecayPath(moduleFile),
80
+ };
81
+ } catch {}
82
+ }
83
+
84
+ return null;
85
+ }
@@ -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 isWin = process.platform === "win32";
9
- const candidates = isWin ? ["python"] : ["python3", "python"];
8
+ const detected = detectPythonEnv({ pluginRoot: resolve(root, "..") });
10
9
 
11
- const python = candidates.find((py) => {
12
- try {
13
- execSync(
14
- `${py} -c "import memory_decay; import sqlite3; sqlite3.connect(':memory:').enable_load_extension(True)"`,
15
- { stdio: "ignore" },
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
- const pythonPath = execSync(isWin ? `where ${python}` : `which ${python}`, { encoding: "utf8" }).trim().split("\n")[0];
29
- const memoryDecayPath = execSync(
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 { mergePythonEnv } from "./python-env.js";
6
7
  import { toFreshness } from "./types.js";
7
8
 
8
9
  const BOOTSTRAP_PROMPT = `## Memory System (memory-decay)
@@ -88,17 +89,20 @@ 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
- if (!memoryDecayPath) {
92
+ let detectedEnv: { memoryDecayPath?: string; pythonPath?: string } = {};
93
+ if (!memoryDecayPath || !pythonPath) {
92
94
  try {
93
95
  const { readFileSync } = await import("node:fs");
94
96
  const { resolve, dirname } = await import("node:path");
95
97
  const { fileURLToPath } = await import("node:url");
96
98
  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
+ ({ memoryDecayPath, pythonPath } = mergePythonEnv(
103
+ { memoryDecayPath, pythonPath },
104
+ detectedEnv,
105
+ ));
102
106
  if (!memoryDecayPath) {
103
107
  ctx.logger.error(
104
108
  "Could not auto-detect memory-decay installation. " +
@@ -0,0 +1,14 @@
1
+ export interface PythonEnvLike {
2
+ memoryDecayPath?: string;
3
+ pythonPath?: string;
4
+ }
5
+
6
+ export function mergePythonEnv(
7
+ configured: PythonEnvLike,
8
+ detected: PythonEnvLike = {},
9
+ ): { memoryDecayPath: string; pythonPath: string } {
10
+ return {
11
+ memoryDecayPath: configured.memoryDecayPath || detected.memoryDecayPath || "",
12
+ pythonPath: configured.pythonPath || detected.pythonPath || "",
13
+ };
14
+ }