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 +61 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -1
- package/src/index.ts +10 -3
- package/src/python-env.ts +16 -3
- package/src/service.ts +59 -7
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
|
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.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:
|
|
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("
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
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(`
|
|
132
|
+
throw new Error(`Server failed to start within ${timeoutMs}ms${this.formatStderrContext()}`);
|
|
81
133
|
}
|
|
82
134
|
}
|