Skip to content

Commit 18ecc8e

Browse files
qin-ctxclaude
andauthored
fix(openclaw-memory-plugin): improve port management and preserve existing config (#513)
Replace fail-on-port-occupied with smart port preparation that kills stale OpenViking processes or auto-finds free ports. Simplify agent ID to default to "default" instead of random per-session generation. Change install scripts to merge into existing allow lists and load paths instead of overwriting them. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cd4bf44 commit 18ecc8e

File tree

6 files changed

+135
-46
lines changed

6 files changed

+135
-46
lines changed

examples/openclaw-memory-plugin/config.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,22 @@ const DEFAULT_TARGET_URI = "viking://user/memories";
3131
const DEFAULT_TIMEOUT_MS = 15000;
3232
const DEFAULT_CAPTURE_MODE = "semantic";
3333
const DEFAULT_CAPTURE_MAX_LENGTH = 24000;
34-
const DEFAULT_RECALL_LIMIT = 20;
34+
const DEFAULT_RECALL_LIMIT = 6;
3535
const DEFAULT_RECALL_SCORE_THRESHOLD = 0.01;
3636
const DEFAULT_INGEST_REPLY_ASSIST = true;
3737
const DEFAULT_INGEST_REPLY_ASSIST_MIN_SPEAKER_TURNS = 2;
3838
const DEFAULT_INGEST_REPLY_ASSIST_MIN_CHARS = 120;
3939
const DEFAULT_LOCAL_CONFIG_PATH = join(homedir(), ".openviking", "ov.conf");
4040

41-
function generateAgentId(): string {
42-
const { hostname } = require("node:os") as typeof import("node:os");
43-
const { randomBytes } = require("node:crypto") as typeof import("node:crypto");
44-
const host = hostname().split(".")[0]?.toLowerCase().replace(/[^a-z0-9-]/g, "") || "local";
45-
const random = randomBytes(4).toString("hex");
46-
return `openclaw-${host}-${random}`;
47-
}
41+
const DEFAULT_AGENT_ID = "default";
4842

4943
function resolveAgentId(configured: unknown): string {
5044
if (typeof configured === "string" && configured.trim()) {
5145
return configured.trim();
5246
}
53-
// 生成随机唯一的默认 ID,不持久化
54-
return generateAgentId();
47+
return DEFAULT_AGENT_ID;
5548
}
5649

57-
export { generateAgentId };
58-
5950
function resolveEnvVars(value: string): string {
6051
return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
6152
const envValue = process.env[envVar];
@@ -218,7 +209,7 @@ export const memoryOpenVikingConfigSchema = {
218209
agentId: {
219210
label: "Agent ID",
220211
placeholder: "auto-generated",
221-
help: "Identifies this agent to OpenViking (sent as X-OpenViking-Agent header). A random unique ID is generated per session if not set.",
212+
help: "Identifies this agent to OpenViking (sent as X-OpenViking-Agent header). Defaults to \"default\" if not set.",
222213
},
223214
apiKey: {
224215
label: "OpenViking API Key",

examples/openclaw-memory-plugin/index.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ import {
2828
waitForHealth,
2929
withTimeout,
3030
quickRecallPrecheck,
31-
quickHealthCheck,
32-
quickTcpProbe,
3331
resolvePythonCommand,
32+
prepareLocalPort,
3433
} from "./process-manager.js";
3534

3635
const memoryPlugin = {
@@ -539,34 +538,13 @@ const memoryPlugin = {
539538
id: "memory-openviking",
540539
start: async () => {
541540
if (cfg.mode === "local" && resolveLocalClient) {
542-
const baseUrl = cfg.baseUrl;
543541
const timeoutMs = 60_000;
544542
const intervalMs = 500;
545-
// 1. Check if a healthy OpenViking is already running on this port — reuse it
546-
const existingHealthy = await quickHealthCheck(baseUrl, 2000);
547-
if (existingHealthy) {
548-
const client = new OpenVikingClient(baseUrl, cfg.apiKey, cfg.agentId, cfg.timeoutMs);
549-
localClientCache.set(localCacheKey, { client, process: null });
550-
resolveLocalClient(client);
551-
rejectLocalClient = null;
552-
api.logger.info(
553-
`memory-openviking: reusing existing server at ${baseUrl}`,
554-
);
555-
return;
556-
}
557543

558-
// 2. Check if port is occupied by something else
559-
const portOccupied = await quickTcpProbe("127.0.0.1", cfg.port, 500);
560-
if (portOccupied) {
561-
const msg = `memory-openviking: port ${cfg.port} is occupied by another process (not OpenViking). ` +
562-
`Change the port in your plugin config or ov.conf, e.g.: ` +
563-
`openclaw config set plugins.entries.memory-openviking.config.port 1934`;
564-
api.logger.warn(msg);
565-
markLocalUnavailable("port occupied", new Error(msg));
566-
throw new Error(msg);
567-
}
544+
// Prepare port: kill stale OpenViking, or auto-find free port if occupied by others
545+
const actualPort = await prepareLocalPort(cfg.port, api.logger);
546+
const baseUrl = `http://127.0.0.1:${actualPort}`;
568547

569-
// 3. Port is free — start OpenViking
570548
const pythonCmd = resolvePythonCommand(api.logger);
571549

572550
// Inherit system environment; optionally override Go/Python paths via env vars
@@ -578,7 +556,7 @@ const memoryPlugin = {
578556
OPENVIKING_CONFIG_FILE: cfg.configPath,
579557
OPENVIKING_START_CONFIG: cfg.configPath,
580558
OPENVIKING_START_HOST: "127.0.0.1",
581-
OPENVIKING_START_PORT: String(cfg.port),
559+
OPENVIKING_START_PORT: String(actualPort),
582560
...(process.env.OPENVIKING_GO_PATH && { PATH: `${process.env.OPENVIKING_GO_PATH}${pathSep}${process.env.PATH || ""}` }),
583561
...(process.env.OPENVIKING_GOPATH && { GOPATH: process.env.OPENVIKING_GOPATH }),
584562
...(process.env.OPENVIKING_GOPROXY && { GOPROXY: process.env.OPENVIKING_GOPROXY }),

examples/openclaw-memory-plugin/install.sh

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,10 +612,24 @@ configure_openclaw_plugin() {
612612
fi
613613

614614
"${oc_env[@]}" openclaw config set plugins.enabled true
615-
"${oc_env[@]}" openclaw config set plugins.allow '["memory-openviking"]' --json
615+
# Merge into existing allow list instead of overwriting
616+
local existing_allow
617+
existing_allow=$("${oc_env[@]}" openclaw config get plugins.allow --json 2>/dev/null || echo '[]')
618+
if ! echo "$existing_allow" | grep -q '"memory-openviking"'; then
619+
existing_allow=$(echo "$existing_allow" | sed 's/]$//' | sed 's/$/,"memory-openviking"]/' | sed 's/\[,/[/')
620+
fi
621+
"${oc_env[@]}" openclaw config set plugins.allow "$existing_allow" --json
622+
616623
"${oc_env[@]}" openclaw config set gateway.mode local
617624
"${oc_env[@]}" openclaw config set plugins.slots.memory memory-openviking
618-
"${oc_env[@]}" openclaw config set plugins.load.paths "[\"${PLUGIN_DEST}\"]" --json
625+
626+
# Merge into existing load paths instead of overwriting
627+
local existing_paths
628+
existing_paths=$("${oc_env[@]}" openclaw config get plugins.load.paths --json 2>/dev/null || echo '[]')
629+
if ! echo "$existing_paths" | grep -q "\"${PLUGIN_DEST}\""; then
630+
existing_paths=$(echo "$existing_paths" | sed 's/]$//' | sed "s/$/,\"${PLUGIN_DEST}\"]/" | sed 's/\[,/[/')
631+
fi
632+
"${oc_env[@]}" openclaw config set plugins.load.paths "$existing_paths" --json
619633
"${oc_env[@]}" openclaw config set plugins.entries.memory-openviking.config.mode "${SELECTED_MODE}"
620634
"${oc_env[@]}" openclaw config set plugins.entries.memory-openviking.config.targetUri viking://user/memories
621635
"${oc_env[@]}" openclaw config set plugins.entries.memory-openviking.config.autoRecall true --json

examples/openclaw-memory-plugin/process-manager.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,102 @@ export async function quickRecallPrecheck(
134134

135135
export interface ProcessLogger {
136136
info?: (msg: string) => void;
137+
warn?: (msg: string) => void;
138+
}
139+
140+
/**
141+
* Prepare a port for local OpenViking startup.
142+
*
143+
* 1. If the port hosts an OpenViking instance (health check passes) → kill it, return same port.
144+
* 2. If the port is occupied by something else → auto-find the next free port.
145+
* 3. If the port is free → return it as-is.
146+
*/
147+
export async function prepareLocalPort(
148+
port: number,
149+
logger: ProcessLogger,
150+
maxRetries: number = 10,
151+
): Promise<number> {
152+
const isOpenViking = await quickHealthCheck(`http://127.0.0.1:${port}`, 2000);
153+
if (isOpenViking) {
154+
logger.info?.(`memory-openviking: killing stale OpenViking on port ${port}`);
155+
await killProcessOnPort(port, logger);
156+
return port;
157+
}
158+
159+
const occupied = await quickTcpProbe("127.0.0.1", port, 500);
160+
if (!occupied) {
161+
return port;
162+
}
163+
164+
// Port occupied by non-OpenViking process — find next free port
165+
logger.warn?.(`memory-openviking: port ${port} is occupied by another process, searching for a free port...`);
166+
for (let candidate = port + 1; candidate <= port + maxRetries; candidate++) {
167+
if (candidate > 65535) break;
168+
const taken = await quickTcpProbe("127.0.0.1", candidate, 300);
169+
if (!taken) {
170+
logger.info?.(`memory-openviking: using free port ${candidate} instead of ${port}`);
171+
return candidate;
172+
}
173+
}
174+
throw new Error(
175+
`memory-openviking: port ${port} is occupied and no free port found in range ${port + 1}-${port + maxRetries}`,
176+
);
177+
}
178+
179+
function killProcessOnPort(port: number, logger: ProcessLogger): Promise<void> {
180+
return IS_WIN ? killProcessOnPortWin(port, logger) : killProcessOnPortUnix(port, logger);
181+
}
182+
183+
async function killProcessOnPortWin(port: number, logger: ProcessLogger): Promise<void> {
184+
try {
185+
const netstatOut = execSync(
186+
`netstat -ano | findstr "LISTENING" | findstr ":${port}"`,
187+
{ encoding: "utf-8", shell: "cmd.exe" },
188+
).trim();
189+
if (!netstatOut) return;
190+
const pids = new Set<number>();
191+
for (const line of netstatOut.split(/\r?\n/)) {
192+
const m = line.trim().match(/\s(\d+)\s*$/);
193+
if (m) pids.add(Number(m[1]));
194+
}
195+
for (const pid of pids) {
196+
if (pid > 0) {
197+
logger.info?.(`memory-openviking: killing pid ${pid} on port ${port}`);
198+
try { execSync(`taskkill /PID ${pid} /F`, { shell: "cmd.exe" }); } catch { /* already gone */ }
199+
}
200+
}
201+
if (pids.size) await new Promise((r) => setTimeout(r, 500));
202+
} catch { /* netstat not available or no stale process */ }
203+
}
204+
205+
async function killProcessOnPortUnix(port: number, logger: ProcessLogger): Promise<void> {
206+
try {
207+
let pids: number[] = [];
208+
try {
209+
const lsofOut = execSync(`lsof -ti tcp:${port} -s tcp:listen 2>/dev/null || true`, {
210+
encoding: "utf-8",
211+
shell: "/bin/sh",
212+
}).trim();
213+
if (lsofOut) pids = lsofOut.split(/\s+/).map((s) => Number(s)).filter((n) => n > 0);
214+
} catch { /* lsof not available */ }
215+
if (pids.length === 0) {
216+
try {
217+
const ssOut = execSync(
218+
`ss -tlnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {gsub(/.*pid=/,""); gsub(/,.*/,""); print; exit}'`,
219+
{ encoding: "utf-8", shell: "/bin/sh" },
220+
).trim();
221+
if (ssOut) {
222+
const n = Number(ssOut);
223+
if (n > 0) pids = [n];
224+
}
225+
} catch { /* ss not available */ }
226+
}
227+
for (const pid of pids) {
228+
logger.info?.(`memory-openviking: killing pid ${pid} on port ${port}`);
229+
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
230+
}
231+
if (pids.length) await new Promise((r) => setTimeout(r, 500));
232+
} catch { /* port check failed */ }
137233
}
138234

139235
export function resolvePythonCommand(logger: ProcessLogger): string {
@@ -171,7 +267,7 @@ export function resolvePythonCommand(logger: ProcessLogger): string {
171267
if (!pythonCmd) {
172268
if (IS_WIN) {
173269
try {
174-
pythonCmd = execSync("where python", { encoding: "utf-8", shell: true }).split(/\r?\n/)[0].trim();
270+
pythonCmd = execSync("where python", { encoding: "utf-8", shell: "cmd.exe" }).split(/\r?\n/)[0].trim();
175271
} catch {
176272
pythonCmd = "python";
177273
}

examples/openclaw-memory-plugin/setup-helper/cli.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,9 @@ async function configureOpenclawViaJson(pluginPath, serverPort, pluginMode = "lo
488488
try { cfg = JSON.parse(await readFile(cfgPath, "utf8")); } catch { /* start fresh */ }
489489
if (!cfg.plugins) cfg.plugins = {};
490490
cfg.plugins.enabled = true;
491-
cfg.plugins.allow = ["memory-openviking"];
491+
const allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow : [];
492+
if (!allow.includes("memory-openviking")) allow.push("memory-openviking");
493+
cfg.plugins.allow = allow;
492494
if (!cfg.plugins.slots) cfg.plugins.slots = {};
493495
cfg.plugins.slots.memory = "memory-openviking";
494496
if (!cfg.plugins.load) cfg.plugins.load = {};
@@ -535,7 +537,15 @@ async function configureOpenclawViaCli(pluginPath, serverPort, installMode, plug
535537
await runNoShell("openclaw", ["config", "set", "plugins.load.paths", JSON.stringify([pluginPath])], { silent: true }).catch(() => {});
536538
}
537539
await runNoShell("openclaw", ["config", "set", "plugins.enabled", "true"]);
538-
await runNoShell("openclaw", ["config", "set", "plugins.allow", JSON.stringify(["memory-openviking"]), "--json"]);
540+
// Merge into existing allow list instead of overwriting
541+
let currentAllow = [];
542+
try {
543+
const raw = await run("openclaw", ["config", "get", "plugins.allow", "--json"], { ...extraEnv, silent: true });
544+
currentAllow = JSON.parse(raw.stdout || "[]");
545+
} catch {}
546+
if (!Array.isArray(currentAllow)) currentAllow = [];
547+
if (!currentAllow.includes("memory-openviking")) currentAllow.push("memory-openviking");
548+
await runNoShell("openclaw", ["config", "set", "plugins.allow", JSON.stringify(currentAllow), "--json"]);
539549
await runNoShell("openclaw", ["config", "set", "gateway.mode", "local"]);
540550
await runNoShell("openclaw", ["config", "set", "plugins.slots.memory", "memory-openviking"]);
541551
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.mode", pluginMode]);

examples/openclaw-memory-plugin/skills/install-openviking-memory/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Example: User says "Forget my phone number"
6060
| `mode` | `remote` | `local` (start local server) or `remote` (connect to remote) |
6161
| `baseUrl` | `http://127.0.0.1:1933` | OpenViking server URL (remote mode) |
6262
| `apiKey` || OpenViking API Key (optional) |
63-
| `agentId` | `openclaw-<hostname>` | Identifies this agent (auto-generated, persisted to `~/.openviking/.agent-id`) |
63+
| `agentId` | `default` | Identifies this agent to OpenViking |
6464
| `configPath` | `~/.openviking/ov.conf` | Config file path (local mode) |
6565
| `port` | `1933` | Local server port (local mode) |
6666
| `targetUri` | `viking://user/memories` | Default search scope |

0 commit comments

Comments
 (0)