openclaw-memory-decay 0.1.9 → 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
@@ -390,6 +390,67 @@ openclaw gateway restart
390
390
  | `~/.openclaw/memory-decay-data/memories.db` | Memory database |
391
391
  | `~/.openclaw/workspace/memory/` | Legacy memory files (can migrate) |
392
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
+
393
454
  ## License
394
455
 
395
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.9",
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.9",
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"
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
@@ -26,11 +26,12 @@ const PYTHON_COMMAND_CANDIDATES = process.platform === "win32"
26
26
 
27
27
  export function mergePythonEnv(
28
28
  configured: PythonEnvLike,
29
- detected: PythonEnvLike = {},
30
- ): { memoryDecayPath: string; pythonPath: string } {
29
+ detected: { memoryDecayPath?: string; pythonPath?: string; backendVersion?: string } = {},
30
+ ): { memoryDecayPath: string; pythonPath: string; backendVersion: string } {
31
31
  return {
32
32
  memoryDecayPath: configured.memoryDecayPath || detected.memoryDecayPath || "",
33
33
  pythonPath: configured.pythonPath || detected.pythonPath || "",
34
+ backendVersion: detected.backendVersion || "unknown",
34
35
  };
35
36
  }
36
37
 
@@ -96,6 +97,8 @@ function resolvePythonPath(
96
97
  return execFileSync(resolver, [command], { encoding: "utf8" }).trim().split(/\r?\n/)[0];
97
98
  }
98
99
 
100
+ export const REQUIRED_BACKEND_VERSION = "0.1.3";
101
+
99
102
  export function detectPythonEnv({
100
103
  pluginRoot,
101
104
  env = process.env,
@@ -106,7 +109,7 @@ export function detectPythonEnv({
106
109
  makeTempDir = () => mkdtempSync(join(tmpdir(), "memory-decay-python-env-")),
107
110
  readPathFile = (path) => readFileSync(path, "utf8"),
108
111
  removeTempDir = (path) => rmSync(path, { recursive: true, force: true }),
109
- }: DetectPythonEnvOptions): { memoryDecayPath: string; pythonPath: string } | null {
112
+ }: DetectPythonEnvOptions): { memoryDecayPath: string; pythonPath: string; backendVersion: string } | null {
110
113
  for (const candidate of buildPythonCandidates({ pluginRoot, env, isWin, homedir, existsSync })) {
111
114
  try {
112
115
  execFileSync(candidate, ["-c", "import memory_decay.server"], { stdio: "ignore" });
@@ -126,9 +129,19 @@ export function detectPythonEnv({
126
129
  const moduleFile = readPathFile(outputPath).trim();
127
130
  removeTempDir(tempDir);
128
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
+
129
141
  return {
130
142
  pythonPath: resolvePythonPath(candidate, { execFileSync, isWin }),
131
143
  memoryDecayPath: resolveMemoryDecayPath(moduleFile),
144
+ backendVersion,
132
145
  };
133
146
  } catch {}
134
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
  }