Skip to content

Commit 7df9f7d

Browse files
authored
Openclaw local plugin 20260408 (#1450)
## Description Please include a summary of the change, the problem it solves, the implementation approach, and relevant context. List any dependencies required for this change. Related Issue (Required): Fixes #issue_number ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (does not change functionality, e.g. code style improvements, linting) - [ ] Documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - [ ] Unit Test - [ ] Test Script Or Test Steps (please provide) - [ ] Pipeline Automated API Test (please provide) ## Checklist - [ ] I have performed a self-review of my own code | 我已自行检查了自己的代码 - [ ] I have commented my code in hard-to-understand areas | 我已在难以理解的地方对代码进行了注释 - [ ] I have added tests that prove my fix is effective or that my feature works | 我已添加测试以证明我的修复有效或功能正常 - [ ] I have created related documentation issue/PR in [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) (if applicable) | 我已在 [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) 中创建了相关的文档 issue/PR(如果适用) - [ ] I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用) - [ ] I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人 ## Reviewer Checklist - [ ] closes #xxxx (Replace xxxx with the GitHub issue number) - [ ] Made sure Checks passed - [ ] Tests have been provided
2 parents 45f4c1b + abfe2f4 commit 7df9f7d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+36352
-20
lines changed

apps/memos-local-openclaw/index.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,43 @@ const NEW_SESSION_PROMPT_RE = /A new session was started via \/new or \/reset\./
5757
const INTERNAL_CONTEXT_RE = /OpenClaw runtime context \(internal\):[\s\S]*/i;
5858
const CONTINUE_PROMPT_RE = /^Continue where you left off\.[\s\S]*/i;
5959

60+
const buildMemoryPromptSection = ({ availableTools, citationsMode }: {
61+
availableTools: Set<string>;
62+
citationsMode?: string;
63+
}) => {
64+
const lines: string[] = [];
65+
const hasMemorySearch = availableTools.has("memory_search");
66+
const hasMemoryGet = availableTools.has("memory_get");
67+
68+
if (!hasMemorySearch && !hasMemoryGet) {
69+
return lines;
70+
}
71+
72+
lines.push("## Memory Recall");
73+
lines.push(
74+
"This workspace uses MemOS Local as the active memory slot. Prefer recalled memories and the memory tools before claiming prior context is unavailable.",
75+
);
76+
77+
if (hasMemorySearch && hasMemoryGet) {
78+
lines.push(
79+
"Use `memory_search` to locate relevant memories, then `memory_get` or `memory_timeline` when you need the full source text or surrounding context.",
80+
);
81+
} else if (hasMemorySearch) {
82+
lines.push("Use `memory_search` before answering questions about prior conversations, preferences, plans, or decisions.");
83+
} else {
84+
lines.push("Use `memory_get` or `memory_timeline` to inspect the referenced memory before answering.");
85+
}
86+
87+
if (citationsMode === "off") {
88+
lines.push("Citations are disabled, so avoid mentioning internal memory ids unless the user asks.");
89+
} else {
90+
lines.push("When it helps the user verify a memory-backed claim, mention the relevant memory identifier or tool result.");
91+
}
92+
93+
lines.push("");
94+
return lines;
95+
};
96+
6097
function normalizeAutoRecallQuery(rawPrompt: string): string {
6198
let query = rawPrompt.trim();
6299

@@ -123,6 +160,10 @@ const memosLocalPlugin = {
123160
configSchema: pluginConfigSchema,
124161

125162
register(api: OpenClawPluginApi) {
163+
api.registerMemoryCapability({
164+
promptBuilder: buildMemoryPromptSection,
165+
});
166+
126167
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
127168
const localRequire = createRequire(import.meta.url);
128169
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
@@ -2399,8 +2440,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
23992440

24002441
// Fallback: OpenClaw may load this plugin via deferred reload after
24012442
// startPluginServices has already run, so service.start() never fires.
2402-
// Self-start the viewer after a grace period if it hasn't been started.
2403-
const SELF_START_DELAY_MS = 3000;
2443+
// Start on the next tick instead of waiting several seconds; the
2444+
// serviceStarted guard still prevents duplicate startup if the host calls
2445+
// service.start() immediately after registration.
2446+
const SELF_START_DELAY_MS = 0;
24042447
setTimeout(() => {
24052448
if (!serviceStarted) {
24062449
api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");

apps/memos-local-openclaw/install.sh

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ ensure_node22() {
150150
exit 1
151151
}
152152

153+
resolve_openclaw_bin() {
154+
if command -v openclaw >/dev/null 2>&1; then
155+
command -v openclaw
156+
return 0
157+
fi
158+
159+
error "Global openclaw CLI not found, 未找到全局 openclaw 命令"
160+
error "Install it first with: npm install -g openclaw@latest"
161+
exit 1
162+
}
163+
153164
print_banner() {
154165
echo -e "${BLUE}${BOLD}🧠 Memos Local OpenClaw Installer${NC}"
155166
echo -e "${BLUE}${DEFAULT_TAGLINE}${NC}"
@@ -208,6 +219,9 @@ if ! command -v node >/dev/null 2>&1; then
208219
exit 1
209220
fi
210221

222+
OPENCLAW_BIN="$(resolve_openclaw_bin)"
223+
success "Using global OpenClaw CLI, 使用全局 OpenClaw CLI: ${OPENCLAW_BIN}"
224+
211225
PACKAGE_SPEC="${PLUGIN_PACKAGE}@${PLUGIN_VERSION}"
212226
EXTENSION_DIR="${OPENCLAW_HOME}/extensions/${PLUGIN_ID}"
213227
OPENCLAW_CONFIG_PATH="${OPENCLAW_HOME}/openclaw.json"
@@ -302,7 +316,7 @@ NODE
302316
}
303317

304318
info "Stop OpenClaw Gateway, 停止 OpenClaw Gateway..."
305-
npx openclaw gateway stop >/dev/null 2>&1 || true
319+
"${OPENCLAW_BIN}" gateway stop >/dev/null 2>&1 || true
306320

307321
if command -v lsof >/dev/null 2>&1; then
308322
PIDS="$(lsof -i :"${PORT}" -t 2>/dev/null || true)"
@@ -388,21 +402,37 @@ fi
388402
update_openclaw_config
389403

390404
info "Install OpenClaw Gateway service, 安装 OpenClaw Gateway 服务..."
391-
npx openclaw gateway install --port "${PORT}" --force 2>&1 || true
405+
"${OPENCLAW_BIN}" gateway install --port "${PORT}" --force 2>&1 || true
392406

393407
success "Start OpenClaw Gateway service, 启动 OpenClaw Gateway 服务..."
394-
npx openclaw gateway start 2>&1
408+
"${OPENCLAW_BIN}" gateway start 2>&1
395409

396410
info "Starting Memory Viewer, 正在启动记忆面板..."
397-
for i in 1 2 3 4 5; do
398-
if command -v lsof >/dev/null 2>&1 && lsof -i :18799 -t >/dev/null 2>&1; then
411+
VIEWER_URL="http://127.0.0.1:18799"
412+
VIEWER_WAIT_SECONDS=30
413+
viewer_ready=0
414+
for ((i=1; i<=VIEWER_WAIT_SECONDS; i++)); do
415+
if command -v curl >/dev/null 2>&1; then
416+
if curl -fsS --max-time 2 "${VIEWER_URL}" >/dev/null 2>&1; then
417+
viewer_ready=1
418+
break
419+
fi
420+
elif command -v lsof >/dev/null 2>&1 && lsof -i :18799 -t >/dev/null 2>&1; then
421+
viewer_ready=1
399422
break
400423
fi
401424
printf "."
402425
sleep 1
403426
done
404427
echo ""
405428

429+
if [[ "${viewer_ready}" -eq 1 ]]; then
430+
success "Memory Viewer is ready, 记忆面板已就绪: ${VIEWER_URL}"
431+
else
432+
warn "Memory Viewer not ready after ${VIEWER_WAIT_SECONDS}s, 记忆面板在 ${VIEWER_WAIT_SECONDS} 秒后仍未就绪"
433+
warn "Check gateway logs if http://127.0.0.1:18799 is still unavailable."
434+
fi
435+
406436
echo ""
407437
success "=========================================="
408438
success " Installation complete! 安装完成!"

apps/memos-local-openclaw/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@memtensor/memos-local-openclaw-plugin",
3-
"version": "1.0.8",
3+
"version": "1.0.9-beta.1",
44
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
55
"type": "module",
66
"main": "index.ts",

apps/memos-local-openclaw/tests/incremental-sharing.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,21 @@ function makeApi(stateDir: string, pluginConfig: Record<string, unknown> = {}) {
1919
return input === "~/.openclaw" ? stateDir : input;
2020
},
2121
logger: noopLog,
22-
registerTool(def: any) {
22+
registerTool(def: any, meta?: { name?: string }) {
23+
if (typeof def === "function") {
24+
const key = meta?.name ?? def({ agentId: "main", sessionKey: "default" }).name;
25+
tools.set(key, {
26+
name: key,
27+
execute: (...args: any[]) => {
28+
const runtimeCtx = args[2] ?? { agentId: "main", sessionKey: "default" };
29+
return def(runtimeCtx).execute(...args);
30+
},
31+
});
32+
return;
33+
}
2334
tools.set(def.name, def);
2435
},
36+
registerMemoryCapability() {},
2537
registerService(def: any) {
2638
service = def;
2739
},

apps/memos-local-openclaw/tests/integration.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,21 @@ function makePluginApi(stateDir: string, pluginConfig: Record<string, unknown> =
3030
return input === "~/.openclaw" ? stateDir : input;
3131
},
3232
logger: noopLog,
33-
registerTool(def: any) {
33+
registerTool(def: any, meta?: { name?: string }) {
34+
if (typeof def === "function") {
35+
const key = meta?.name ?? def({ agentId: "main", sessionKey: "default" }).name;
36+
tools.set(key, {
37+
name: key,
38+
execute: (...args: any[]) => {
39+
const runtimeCtx = args[2] ?? { agentId: "main", sessionKey: "default" };
40+
return def(runtimeCtx).execute(...args);
41+
},
42+
});
43+
return;
44+
}
3445
tools.set(def.name, def);
3546
},
47+
registerMemoryCapability() {},
3648
registerService(def: any) {
3749
service = def;
3850
},
@@ -837,9 +849,9 @@ describe("Integration: root plugin memory_search network scope", () => {
837849
expect(searchTool).toBeDefined();
838850

839851
const result = await searchTool.execute("call-root-search", { query: "rollout checklist", scope: "all", maxResults: 5 }, { agentId: "main" });
840-
expect(result.details.local.hits.length).toBeGreaterThan(0);
841-
expect(result.details.hub.hits.length).toBeGreaterThan(0);
842-
expect(result.details.hub.hits[0].remoteHitId).toBeTruthy();
852+
expect((result.details.filtered ?? []).some((hit: any) => hit.origin !== "hub-remote")).toBe(true);
853+
expect((result.details.filtered ?? []).some((hit: any) => hit.origin === "hub-remote")).toBe(true);
854+
expect((result.details.hubCandidates ?? []).length).toBeGreaterThan(0);
843855
} finally {
844856
await teardownRootMemorySearchHarness(harness);
845857
}

apps/memos-local-openclaw/tests/plugin-impl-access.test.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,21 @@ function makeApi(stateDir: string, pluginConfig: Record<string, unknown> = {}) {
2121
info: () => {},
2222
warn: () => {},
2323
},
24-
registerTool(def: any) {
24+
registerTool(def: any, meta?: { name?: string }) {
25+
if (typeof def === "function") {
26+
const key = meta?.name ?? def({ agentId: "main", sessionKey: "default" }).name;
27+
tools.set(key, {
28+
name: key,
29+
execute: (...args: any[]) => {
30+
const runtimeCtx = args[2] ?? { agentId: "main", sessionKey: "default" };
31+
return def(runtimeCtx).execute(...args);
32+
},
33+
});
34+
return;
35+
}
2536
tools.set(def.name, def);
2637
},
38+
registerMemoryCapability() {},
2739
registerService(def: any) {
2840
service = def;
2941
},
@@ -219,7 +231,7 @@ describe("plugin-impl owner isolation", () => {
219231
const search = tools.get("memory_search");
220232
await waitFor(async () => {
221233
const result = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "alpha" });
222-
return (result?.details?.hits?.length ?? 0) > 0;
234+
return (result?.details?.filtered?.length ?? 0) > 0;
223235
});
224236
});
225237

@@ -243,20 +255,20 @@ describe("plugin-impl owner isolation", () => {
243255
const beta = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "beta" });
244256
const publicHit = await search.execute("call-search", { query: "shared public marker", maxResults: 5, minScore: 0.1 }, { agentId: "beta" });
245257

246-
expect(alpha.details.hits.length).toBeGreaterThan(0);
247-
const betaAlphaHits = (beta.details?.hits ?? []).filter((h: any) =>
258+
expect(alpha.details.filtered.length).toBeGreaterThan(0);
259+
const betaAlphaHits = (beta.details?.filtered ?? []).filter((h: any) =>
248260
h.original_excerpt?.includes("alpha") || h.summary?.includes("alpha"),
249261
);
250262
expect(betaAlphaHits).toHaveLength(0);
251-
expect(publicHit.details.hits.length).toBeGreaterThan(0);
263+
expect(publicHit.details.filtered.length).toBeGreaterThan(0);
252264
});
253265

254266
it("memory_timeline should not leak another agent's private neighbors", async () => {
255267
const search = tools.get("memory_search");
256268
const timeline = tools.get("memory_timeline");
257269

258270
const alpha = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "alpha" });
259-
const chunkId = alpha.details.hits[0].chunkId;
271+
const chunkId = alpha.details.filtered[0].chunkId;
260272
const betaTimeline = await timeline.execute("call-timeline", { chunkId }, { agentId: "beta" });
261273

262274
expect(betaTimeline.details.entries).toEqual([]);
@@ -416,7 +428,7 @@ describe("plugin-impl owner isolation", () => {
416428
const getTool = tools.get("memory_get");
417429

418430
const alpha = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "alpha" });
419-
const chunkId = alpha.details.hits[0].chunkId;
431+
const chunkId = alpha.details.filtered[0].chunkId;
420432
const betaGet = await getTool.execute("call-get", { chunkId }, { agentId: "beta" });
421433

422434
expect(betaGet.details.error).toBe("not_found");

apps/memos-local-openclaw/tests/plugin-openclaw-wiring.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ describe("plugin-impl OpenClaw wiring", () => {
112112
resolvePath: () => "/tmp/memos-openclaw-wiring",
113113
logger: { info() {}, warn() {} },
114114
registerTool: () => {},
115+
registerMemoryCapability: () => {},
115116
registerService: () => {},
116117
on: () => {},
117118
} as any);

apps/memos-local-openclaw/tests/shutdown-lifecycle.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe("shutdown lifecycle", () => {
113113
resolvePath: () => "/tmp/memos-service-stop",
114114
logger: noopLog,
115115
registerTool: () => {},
116+
registerMemoryCapability: () => {},
116117
registerService: (service: any) => { registeredService = service; },
117118
on: () => {},
118119
} as any);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ─── Embedding API ───
2+
# Use any OpenAI-compatible embedding service, or leave blank for local offline model (Xenova/all-MiniLM-L6-v2)
3+
EMBEDDING_PROVIDER=openai_compatible
4+
EMBEDDING_API_KEY=your-embedding-api-key
5+
EMBEDDING_ENDPOINT=https://your-embedding-api.com/v1
6+
EMBEDDING_MODEL=bge-m3
7+
8+
# ─── Summarizer API ───
9+
# OpenAI-compatible LLM for one-sentence chunk summaries
10+
# Leave blank to use rule-based fallback (no LLM needed)
11+
SUMMARIZER_PROVIDER=openai_compatible
12+
SUMMARIZER_API_KEY=your-summarizer-api-key
13+
SUMMARIZER_ENDPOINT=https://api.openai.com/v1
14+
SUMMARIZER_MODEL=gpt-4o-mini
15+
SUMMARIZER_TEMPERATURE=0
16+
17+
# ─── Memory Viewer ───
18+
# Port for the web-based Memory Viewer (default: 18799)
19+
# VIEWER_PORT=18799
20+
21+
# ─── Tavily Search (optional) ───
22+
# API key for Tavily web search (get from https://app.tavily.com)
23+
# TAVILY_API_KEY=tvly-your-tavily-api-key
24+
25+
# ─── Telemetry (opt-out) ───
26+
# Anonymous usage analytics to help improve the plugin.
27+
# No memory content, queries, or personal data is ever sent — only tool names, latencies, and version info.
28+
# Enabled by default. Set to false to opt-out.
29+
# TELEMETRY_ENABLED=false
30+
#
31+
# Telemetry backend credentials (for maintainers / CI only).
32+
# End users do NOT need to set these — they are bundled into the npm package at publish time.
33+
# If not set and telemetry.credentials.json is absent, telemetry is silently disabled.
34+
# MEMOS_ARMS_ENDPOINT=https://your-arms-endpoint.log.aliyuncs.com/rum/web/v2?workspace=...&service_id=...
35+
# MEMOS_ARMS_PID=your-arms-pid
36+
# MEMOS_ARMS_ENV=prod

apps/memos-local-plugin/.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
node_modules/
2+
dist/
3+
*.tsbuildinfo
4+
.env
5+
6+
# Compiled output (root level only)
7+
/*.js
8+
/*.js.map
9+
/*.d.ts
10+
/*.d.ts.map
11+
12+
# OS files
13+
.DS_Store
14+
Thumbs.db
15+
16+
# IDE
17+
.vscode/
18+
.idea/
19+
20+
# Generated / non-essential
21+
package-lock.json
22+
.installed-version
23+
ppt/
24+
*.tgz
25+
26+
# Prebuilt native binaries (included in npm package via `files`, not in git)
27+
prebuilds/
28+
29+
# Telemetry credentials (generated by CI, not committed to git)
30+
telemetry.credentials.json
31+
32+
# Database files
33+
*.sqlite
34+
*.sqlite-journal
35+
*.db
36+
/~/.openclaw/

0 commit comments

Comments
 (0)