openclaw-memory-decay 0.1.6 → 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 CHANGED
@@ -63,8 +63,7 @@ Activation
63
63
  python3 -m venv ~/.openclaw/venvs/memory-decay
64
64
  ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
65
65
 
66
- # 2. Install this plugin while that venv is active
67
- source ~/.openclaw/venvs/memory-decay/bin/activate
66
+ # 2. Install this plugin from npm
68
67
  openclaw plugins install openclaw-memory-decay
69
68
 
70
69
  # 3. (Optional but recommended) Restrict auto-load to trusted plugins only
@@ -138,7 +137,7 @@ Add to `~/.openclaw/openclaw.json` under `plugins.entries.memory-decay.config`:
138
137
  |--------|---------|-------------|
139
138
  | `serverPort` | `8100` | Port for the memory-decay HTTP server |
140
139
  | `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 |
140
+ | `pythonPath` | `python3` | Path to Python interpreter. Set this explicitly if you use a custom venv path instead of `~/.openclaw/venvs/memory-decay` |
142
141
  | `dbPath` | `~/.openclaw/memory-decay-data/memories.db` | SQLite database location |
143
142
  | `autoSave` | `true` | Auto-save every conversation turn at low importance. Set `false` to let the agent decide what to save |
144
143
  | `embeddingProvider` | `local` | Embedding provider: `local`, `openai`, or `gemini` |
@@ -297,20 +296,17 @@ openclaw plugins doctor
297
296
  python3 -m venv ~/.openclaw/venvs/memory-decay
298
297
  ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
299
298
 
300
- # 2. Activate the venv so postinstall detects the same interpreter
301
- source ~/.openclaw/venvs/memory-decay/bin/activate
302
-
303
- # 3. Install plugin from npm
299
+ # 2. Install plugin from npm
304
300
  openclaw plugins install openclaw-memory-decay
305
301
 
306
- # 4. Restart gateway
302
+ # 3. Restart gateway
307
303
  openclaw gateway restart
308
304
 
309
- # 5. Verify plugin is loaded
305
+ # 4. Verify plugin is loaded
310
306
  openclaw plugins list
311
307
  # Look for: memory-decay | loaded
312
308
 
313
- # 6. Check server health
309
+ # 5. Check server health
314
310
  curl -s http://127.0.0.1:8100/health
315
311
  # Expected: {"status":"ok","current_tick":0}
316
312
  ```
@@ -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.6",
5
+ "version": "0.1.7",
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.6",
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",
@@ -1,6 +1,6 @@
1
1
  import { execFileSync as nodeExecFileSync } from "node:child_process";
2
2
  import { existsSync as nodeExistsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
3
+ import { homedir as nodeHomedir, tmpdir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
 
6
6
  function resolvePythonPath(command, { execFileSync = nodeExecFileSync, isWin = process.platform === "win32" } = {}) {
@@ -23,7 +23,9 @@ export function buildPythonCandidates({
23
23
  env = process.env,
24
24
  isWin = process.platform === "win32",
25
25
  existsSync = nodeExistsSync,
26
+ homedir = nodeHomedir,
26
27
  } = {}) {
28
+ const openclawStateDir = resolve(env.OPENCLAW_HOME || homedir(), ".openclaw");
27
29
  const siblingRoots = [
28
30
  resolve(pluginRoot, "../memory-decay"),
29
31
  resolve(pluginRoot, "../memory-decay-core"),
@@ -31,6 +33,9 @@ export function buildPythonCandidates({
31
33
  const pathCandidates = [];
32
34
 
33
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
+ );
34
39
  if (env.VIRTUAL_ENV) {
35
40
  pathCandidates.push(join(env.VIRTUAL_ENV, isWin ? "Scripts/python.exe" : "bin/python"));
36
41
  }
package/src/index.ts CHANGED
@@ -3,7 +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
+ import { detectPythonEnv, mergePythonEnv } from "./python-env.js";
7
7
  import { toFreshness } from "./types.js";
8
8
 
9
9
  const BOOTSTRAP_PROMPT = `## Memory System (memory-decay)
@@ -90,15 +90,21 @@ const memoryDecayPlugin = {
90
90
  let memoryDecayPath = (cfg.memoryDecayPath as string) ?? "";
91
91
  let pythonPath = (cfg.pythonPath as string) ?? "";
92
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));
93
96
  if (!memoryDecayPath || !pythonPath) {
94
97
  try {
95
98
  const { readFileSync } = await import("node:fs");
96
- const { resolve, dirname } = await import("node:path");
97
- const { fileURLToPath } = await import("node:url");
98
- const pluginRoot = dirname(fileURLToPath(import.meta.url));
99
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
+ }
102
108
  ({ memoryDecayPath, pythonPath } = mergePythonEnv(
103
109
  { memoryDecayPath, pythonPath },
104
110
  detectedEnv,
@@ -106,7 +112,7 @@ const memoryDecayPlugin = {
106
112
  if (!memoryDecayPath) {
107
113
  ctx.logger.error(
108
114
  "Could not auto-detect memory-decay installation. " +
109
- "Run `pip install memory-decay` or set memoryDecayPath in plugin config."
115
+ "Install the backend into ~/.openclaw/venvs/memory-decay or set pythonPath/memoryDecayPath in plugin config."
110
116
  );
111
117
  return;
112
118
  }
package/src/python-env.ts CHANGED
@@ -1,8 +1,25 @@
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
+
1
6
  export interface PythonEnvLike {
2
7
  memoryDecayPath?: string;
3
8
  pythonPath?: string;
4
9
  }
5
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
+
6
23
  export function mergePythonEnv(
7
24
  configured: PythonEnvLike,
8
25
  detected: PythonEnvLike = {},
@@ -12,3 +29,105 @@ export function mergePythonEnv(
12
29
  pythonPath: configured.pythonPath || detected.pythonPath || "",
13
30
  };
14
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
+ }