openclaw-memory-decay 0.1.8 → 0.1.10

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
@@ -86,17 +86,22 @@ openclaw gateway restart
86
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
87
 
88
88
  ```bash
89
- # 1. Install from npm instead
89
+ # 1. Remove the old pre-0.1.8 plugin install if it exists
90
+ openclaw plugins uninstall memory-decay
91
+
92
+ # 2. Install from npm instead
90
93
  openclaw plugins install openclaw-memory-decay
91
94
 
92
- # 2. Restart gateway
95
+ # 3. Restart gateway
93
96
  openclaw gateway restart
94
97
 
95
- # 3. Verify
98
+ # 4. Verify
96
99
  openclaw plugins list | grep openclaw-memory-decay # should show: openclaw-memory-decay | loaded
97
100
  curl -s http://127.0.0.1:8100/health # should show: {"status":"ok","current_tick":0}
98
101
  ```
99
102
 
103
+ If the old install is already gone, `openclaw plugins uninstall memory-decay` will simply report that nothing was removed.
104
+
100
105
  If auto-detection does not recover your backend path after migration, set the interpreter explicitly:
101
106
 
102
107
  ```bash
@@ -104,6 +109,12 @@ openclaw config set plugins.entries.openclaw-memory-decay.config.pythonPath "~/.
104
109
  openclaw gateway restart
105
110
  ```
106
111
 
112
+ If you are upgrading from an older release, note the plugin id changed from `memory-decay` to `openclaw-memory-decay` in `0.1.8`.
113
+
114
+ - New installs should use `plugins.entries.openclaw-memory-decay`.
115
+ - Older configs under `plugins.entries.memory-decay.config` are still read as a compatibility fallback.
116
+ - For a clean config, migrate to the new key when convenient.
117
+
107
118
  **Your memories are safe.** The SQLite database (`memories.db`) is not affected by plugin reinstallation or migration to npm install.
108
119
 
109
120
  To update in the future:
@@ -136,8 +147,8 @@ Add to `~/.openclaw/openclaw.json` under `plugins.entries.openclaw-memory-decay.
136
147
  | Option | Default | Description |
137
148
  |--------|---------|-------------|
138
149
  | `serverPort` | `8100` | Port for the memory-decay HTTP server |
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` |
150
+ | `memoryDecayPath` | (auto) | Path to memory-decay-core. Auto-detected at runtime from the documented default venv or from explicit config |
151
+ | `pythonPath` | `python3` | Path to Python interpreter. Set this explicitly if you do not use `~/.openclaw/venvs/memory-decay` |
141
152
  | `dbPath` | `~/.openclaw/memory-decay-data/memories.db` | SQLite database location |
142
153
  | `autoSave` | `true` | Auto-save every conversation turn at low importance. Set `false` to let the agent decide what to save |
143
154
  | `embeddingProvider` | `local` | Embedding provider: `local`, `openai`, or `gemini` |
@@ -263,12 +274,32 @@ openclaw plugins install openclaw-memory-decay
263
274
  curl http://127.0.0.1:8100/health
264
275
  ```
265
276
 
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:
277
+ If the import check fails, install the backend into the documented default venv path or set `pythonPath` explicitly:
267
278
  ```bash
268
279
  python3 -m venv ~/.openclaw/venvs/memory-decay
269
280
  ~/.openclaw/venvs/memory-decay/bin/pip install memory-decay
270
281
  ```
271
282
 
283
+ If `memory-decay` is only installed in your system Python or in some other venv, the plugin may still fail to auto-detect it. In that case, point the plugin at the exact Python you want it to use:
284
+
285
+ ```bash
286
+ openclaw config set plugins.entries.openclaw-memory-decay.config.pythonPath "/absolute/path/to/python"
287
+ openclaw gateway restart
288
+ ```
289
+
290
+ Sanity check the exact interpreter before restarting:
291
+
292
+ ```bash
293
+ /absolute/path/to/python -c "import memory_decay.server; print('ok')"
294
+ ```
295
+
296
+ If you use a different venv path, configure it directly:
297
+
298
+ ```bash
299
+ openclaw config set plugins.entries.openclaw-memory-decay.config.pythonPath "/absolute/path/to/venv/bin/python"
300
+ openclaw gateway restart
301
+ ```
302
+
272
303
  ### `error: externally-managed-environment`
273
304
 
274
305
  Your system Python is PEP 668 managed. Install `memory-decay` into a virtualenv instead of the system interpreter:
@@ -359,6 +390,67 @@ openclaw gateway restart
359
390
  | `~/.openclaw/memory-decay-data/memories.db` | Memory database |
360
391
  | `~/.openclaw/workspace/memory/` | Legacy memory files (can migrate) |
361
392
 
393
+ ## Known Upgrade Issues
394
+
395
+ These are real migration issues we hit while moving from older releases. If an AI agent is helping with setup, it should check these before assuming the install itself is broken.
396
+
397
+ ### 1. Old `memory-decay` install still exists
398
+
399
+ Symptom:
400
+ - `duplicate plugin id detected`
401
+ - `plugin id mismatch`
402
+ - both `memory-decay` and `openclaw-memory-decay` appear in `openclaw plugins list`
403
+
404
+ Fix:
405
+
406
+ ```bash
407
+ openclaw plugins uninstall memory-decay
408
+ openclaw gateway restart
409
+ ```
410
+
411
+ If duplicate warnings still mention `.openclaw-install-stage-*`, remove stale failed-install directories:
412
+
413
+ ```bash
414
+ find ~/.openclaw/extensions -maxdepth 1 -type d -name '.openclaw-install-stage-*' -print
415
+ find ~/.openclaw/extensions -maxdepth 1 -type d -name '.openclaw-install-stage-*' -exec rm -rf {} +
416
+ openclaw gateway restart
417
+ ```
418
+
419
+ ### 2. Plugin updated, but port `8100` never comes up
420
+
421
+ Symptom:
422
+ - `curl http://127.0.0.1:8100/health` times out
423
+ - gateway log says `Could not auto-detect memory-decay installation`
424
+
425
+ Fix:
426
+
427
+ ```bash
428
+ openclaw config set plugins.entries.openclaw-memory-decay.config.pythonPath "~/.openclaw/venvs/memory-decay/bin/python"
429
+ openclaw config set plugins.entries.openclaw-memory-decay.config.serverPort 8100
430
+ openclaw gateway restart
431
+ ```
432
+
433
+ If you also need to restore the backend root explicitly:
434
+
435
+ ```bash
436
+ openclaw config set plugins.entries.openclaw-memory-decay.config.memoryDecayPath "/absolute/path/to/memory-decay-core"
437
+ openclaw gateway restart
438
+ ```
439
+
440
+ ### 3. `memory-decay` is installed, but only in system Python or another venv
441
+
442
+ Symptom:
443
+ - `pip show memory-decay` looks fine
444
+ - plugin still cannot find `memory_decay.server`
445
+
446
+ Fix:
447
+
448
+ ```bash
449
+ /absolute/path/to/python -c "import memory_decay.server; print('ok')"
450
+ openclaw config set plugins.entries.openclaw-memory-decay.config.pythonPath "/absolute/path/to/python"
451
+ openclaw gateway restart
452
+ ```
453
+
362
454
  ## License
363
455
 
364
456
  MIT
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-memory-decay",
3
3
  "name": "Memory Decay",
4
4
  "description": "Human-like memory with decay and reinforcement for OpenClaw agents",
5
- "version": "0.1.8",
5
+ "version": "0.1.10",
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.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "OpenClaw memory plugin backed by memory-decay engine",
6
6
  "main": "./src/index.js",
@@ -17,6 +17,8 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "tsc",
20
+ "test": "node --test test/*.test.mjs",
21
+ "test:integration": "docker build -t memory-decay-test -f Dockerfile .. && docker run --rm memory-decay-test",
20
22
  "prepublishOnly": "npm run build",
21
23
  "postinstall": "node scripts/detect-python.mjs",
22
24
  "setup": "node scripts/link-sdk.mjs"
@@ -3,6 +3,10 @@ import { existsSync as nodeExistsSync, mkdtempSync, readFileSync, rmSync } from
3
3
  import { homedir as nodeHomedir, tmpdir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
 
6
+ const PYTHON_COMMAND_CANDIDATES = process.platform === "win32"
7
+ ? ["python"]
8
+ : ["python3", "python", "python3.13", "python3.12", "python3.11", "python3.10"];
9
+
6
10
  function resolvePythonPath(command, { execFileSync = nodeExecFileSync, isWin = process.platform === "win32" } = {}) {
7
11
  if (isAbsolute(command)) {
8
12
  return command;
@@ -43,7 +47,7 @@ export function buildPythonCandidates({
43
47
  pathCandidates.push(join(root, isWin ? ".venv/Scripts/python.exe" : ".venv/bin/python"));
44
48
  }
45
49
 
46
- const commandCandidates = isWin ? ["python"] : ["python3", "python"];
50
+ const commandCandidates = isWin ? ["python"] : PYTHON_COMMAND_CANDIDATES;
47
51
  return [...new Set([
48
52
  ...pathCandidates.filter((candidate) => existsSync(candidate)),
49
53
  ...commandCandidates,
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { MemoryDecayClient } from "./client.js";
4
4
  import { loadLegacyPluginConfig } from "./legacy-config.js";
5
5
  import { MemoryDecayService, type ServiceConfig } from "./service.js";
6
6
  import { shouldMigrate, migrateMarkdownMemories } from "./migrator.js";
7
- import { detectPythonEnv, mergePythonEnv } from "./python-env.js";
7
+ import { detectPythonEnv, mergePythonEnv, REQUIRED_BACKEND_VERSION } from "./python-env.js";
8
8
  import { toFreshness } from "./types.js";
9
9
 
10
10
  const BOOTSTRAP_PROMPT = `## Memory System (memory-decay)
@@ -90,7 +90,7 @@ const memoryDecayPlugin = {
90
90
  // Resolve pythonPath + memoryDecayPath: config > .python-env.json (set at install time) > error
91
91
  let memoryDecayPath = (cfg.memoryDecayPath as string) ?? "";
92
92
  let pythonPath = (cfg.pythonPath as string) ?? "";
93
- let detectedEnv: { memoryDecayPath?: string; pythonPath?: string } = {};
93
+ let detectedEnv: { memoryDecayPath?: string; pythonPath?: string; backendVersion?: string } = {};
94
94
  const { dirname, resolve } = await import("node:path");
95
95
  const { fileURLToPath } = await import("node:url");
96
96
  const pluginRoot = dirname(fileURLToPath(import.meta.url));
@@ -110,6 +110,7 @@ const memoryDecayPlugin = {
110
110
  { memoryDecayPath, pythonPath },
111
111
  detectedEnv,
112
112
  ));
113
+ const backendVersion = detectedEnv.backendVersion ?? "unknown";
113
114
  if (!memoryDecayPath) {
114
115
  ctx.logger.error(
115
116
  "Could not auto-detect memory-decay installation. " +
@@ -131,9 +132,15 @@ const memoryDecayPlugin = {
131
132
  experimentDir: cfg.experimentDir as string | undefined,
132
133
  };
133
134
 
134
- service = new MemoryDecayService(config);
135
+ service = new MemoryDecayService(config, ctx.logger);
135
136
  await service.start();
136
137
  ctx.logger.info("Server started");
138
+ if (backendVersion !== "unknown" && backendVersion !== REQUIRED_BACKEND_VERSION) {
139
+ ctx.logger.info(
140
+ `Backend version ${backendVersion} does not match recommended ${REQUIRED_BACKEND_VERSION}. ` +
141
+ `To update: pip install memory-decay==${REQUIRED_BACKEND_VERSION}`
142
+ );
143
+ }
137
144
  },
138
145
  async stop(ctx) {
139
146
  if (service) {
package/src/python-env.ts CHANGED
@@ -20,13 +20,18 @@ interface DetectPythonEnvOptions {
20
20
  removeTempDir?: (path: string) => void;
21
21
  }
22
22
 
23
+ const PYTHON_COMMAND_CANDIDATES = process.platform === "win32"
24
+ ? ["python"]
25
+ : ["python3", "python", "python3.13", "python3.12", "python3.11", "python3.10"];
26
+
23
27
  export function mergePythonEnv(
24
28
  configured: PythonEnvLike,
25
- detected: PythonEnvLike = {},
26
- ): { memoryDecayPath: string; pythonPath: string } {
29
+ detected: { memoryDecayPath?: string; pythonPath?: string; backendVersion?: string } = {},
30
+ ): { memoryDecayPath: string; pythonPath: string; backendVersion: string } {
27
31
  return {
28
32
  memoryDecayPath: configured.memoryDecayPath || detected.memoryDecayPath || "",
29
33
  pythonPath: configured.pythonPath || detected.pythonPath || "",
34
+ backendVersion: detected.backendVersion || "unknown",
30
35
  };
31
36
  }
32
37
 
@@ -73,7 +78,7 @@ export function buildPythonCandidates({
73
78
  join(root, isWin ? ".venv/Scripts/python.exe" : ".venv/bin/python")),
74
79
  ].filter((candidate): candidate is string => typeof candidate === "string" && candidate.length > 0 && existsSync(candidate));
75
80
 
76
- const commandCandidates = isWin ? ["python"] : ["python3", "python"];
81
+ const commandCandidates = isWin ? ["python"] : PYTHON_COMMAND_CANDIDATES;
77
82
  return [...new Set([...pathCandidates, ...commandCandidates])];
78
83
  }
79
84
 
@@ -92,6 +97,8 @@ function resolvePythonPath(
92
97
  return execFileSync(resolver, [command], { encoding: "utf8" }).trim().split(/\r?\n/)[0];
93
98
  }
94
99
 
100
+ export const REQUIRED_BACKEND_VERSION = "0.1.3";
101
+
95
102
  export function detectPythonEnv({
96
103
  pluginRoot,
97
104
  env = process.env,
@@ -102,7 +109,7 @@ export function detectPythonEnv({
102
109
  makeTempDir = () => mkdtempSync(join(tmpdir(), "memory-decay-python-env-")),
103
110
  readPathFile = (path) => readFileSync(path, "utf8"),
104
111
  removeTempDir = (path) => rmSync(path, { recursive: true, force: true }),
105
- }: DetectPythonEnvOptions): { memoryDecayPath: string; pythonPath: string } | null {
112
+ }: DetectPythonEnvOptions): { memoryDecayPath: string; pythonPath: string; backendVersion: string } | null {
106
113
  for (const candidate of buildPythonCandidates({ pluginRoot, env, isWin, homedir, existsSync })) {
107
114
  try {
108
115
  execFileSync(candidate, ["-c", "import memory_decay.server"], { stdio: "ignore" });
@@ -122,9 +129,19 @@ export function detectPythonEnv({
122
129
  const moduleFile = readPathFile(outputPath).trim();
123
130
  removeTempDir(tempDir);
124
131
 
132
+ let backendVersion = "unknown";
133
+ try {
134
+ const versionOutput = execFileSync(candidate, [
135
+ "-c",
136
+ `import memory_decay; print(getattr(memory_decay, "__version__", "unknown"))`,
137
+ ], { encoding: "utf8" }).trim();
138
+ backendVersion = versionOutput || "unknown";
139
+ } catch {}
140
+
125
141
  return {
126
142
  pythonPath: resolvePythonPath(candidate, { execFileSync, isWin }),
127
143
  memoryDecayPath: resolveMemoryDecayPath(moduleFile),
144
+ backendVersion,
128
145
  };
129
146
  } catch {}
130
147
  }
package/src/service.ts CHANGED
@@ -13,19 +13,34 @@ export interface ServiceConfig {
13
13
  experimentDir?: string;
14
14
  }
15
15
 
16
+ export interface Logger {
17
+ info: (msg: string) => void;
18
+ warn: (msg: string) => void;
19
+ error: (msg: string) => void;
20
+ debug?: (msg: string) => void;
21
+ }
22
+
16
23
  export class MemoryDecayService {
17
24
  private process: ChildProcess | null = null;
18
25
  private client: MemoryDecayClient;
19
26
  private config: ServiceConfig;
27
+ private logger: Logger;
20
28
  private restartCount = 0;
21
29
  private maxRestarts = 3;
30
+ private stderrTail: string[] = [];
31
+ private stopped = false;
32
+ private static readonly STDERR_MAX_LINES = 50;
22
33
 
23
- constructor(config: ServiceConfig) {
34
+ constructor(config: ServiceConfig, logger: Logger) {
24
35
  this.config = config;
36
+ this.logger = logger;
25
37
  this.client = new MemoryDecayClient(config.port);
26
38
  }
27
39
 
28
40
  async start(): Promise<void> {
41
+ this.stopped = false;
42
+ this.stderrTail = [];
43
+
29
44
  const args = [
30
45
  "-m", "memory_decay.server",
31
46
  "--host", "127.0.0.1",
@@ -44,11 +59,37 @@ export class MemoryDecayService {
44
59
  stdio: ["ignore", "pipe", "pipe"],
45
60
  });
46
61
 
47
- this.process.on("exit", (code) => {
48
- if (code !== 0 && this.restartCount < this.maxRestarts) {
49
- this.restartCount++;
50
- console.error(`[memory-decay] Server exited with ${code}, restarting (${this.restartCount}/${this.maxRestarts})`);
51
- this.start();
62
+ this.process.stderr?.on("data", (chunk: Buffer) => {
63
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
64
+ this.stderrTail.push(line);
65
+ if (this.stderrTail.length > MemoryDecayService.STDERR_MAX_LINES) {
66
+ this.stderrTail.shift();
67
+ }
68
+ }
69
+ });
70
+
71
+ this.process.stdout?.on("data", (chunk: Buffer) => {
72
+ if (this.logger.debug) {
73
+ this.logger.debug(chunk.toString().trimEnd());
74
+ }
75
+ });
76
+
77
+ this.process.on("exit", (code, signal) => {
78
+ if (code !== 0) {
79
+ const context = this.formatStderrContext();
80
+ const sig = signal ? ` (signal: ${signal})` : "";
81
+
82
+ if (this.restartCount < this.maxRestarts) {
83
+ this.restartCount++;
84
+ this.logger.error(
85
+ `Server exited with code ${code}${sig}, restarting (${this.restartCount}/${this.maxRestarts})${context}`
86
+ );
87
+ this.start().catch(() => {});
88
+ } else {
89
+ this.logger.error(
90
+ `Server exited with code ${code}${sig}, max restarts (${this.maxRestarts}) exhausted${context}`
91
+ );
92
+ }
52
93
  }
53
94
  });
54
95
 
@@ -56,8 +97,11 @@ export class MemoryDecayService {
56
97
  }
57
98
 
58
99
  async stop(): Promise<void> {
100
+ this.stopped = true;
59
101
  if (this.process) {
60
102
  this.maxRestarts = 0;
103
+ this.process.stdout?.removeAllListeners("data");
104
+ this.process.stderr?.removeAllListeners("data");
61
105
  this.process.kill("SIGTERM");
62
106
  this.process = null;
63
107
  }
@@ -67,16 +111,24 @@ export class MemoryDecayService {
67
111
  return this.client;
68
112
  }
69
113
 
114
+ private formatStderrContext(): string {
115
+ return this.stderrTail.length
116
+ ? `\nLast stderr:\n ${this.stderrTail.slice(-10).join("\n ")}`
117
+ : "";
118
+ }
119
+
70
120
  private async waitForHealth(timeoutMs = 15000): Promise<void> {
71
121
  const start = Date.now();
72
122
  while (Date.now() - start < timeoutMs) {
123
+ if (this.stopped) return;
73
124
  try {
74
125
  await this.client.health();
126
+ this.restartCount = 0;
75
127
  return;
76
128
  } catch {
77
129
  await new Promise((r) => setTimeout(r, 500));
78
130
  }
79
131
  }
80
- throw new Error(`[memory-decay] Server failed to start within ${timeoutMs}ms`);
132
+ throw new Error(`Server failed to start within ${timeoutMs}ms${this.formatStderrContext()}`);
81
133
  }
82
134
  }