Add consolidated XMem agent plugin bundles#206
Conversation
|
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces persistent XMem memory plugins and connectors for Claude Code, Codex, Cursor, Hermes, and OpenClaw, enabling seamless context searching and saving across sessions. The review feedback focuses on improving the robustness of the OpenClaw plugin. Specifically, it highlights a stream-reading issue in the HTTP client, a potential null-pointer crash during configuration parsing, and several unhandled exception risks in the hooks and slash commands that could disrupt the agent's execution if the XMem API is unavailable.
| let body: any | ||
| try { | ||
| body = await response.json() | ||
| } catch { | ||
| body = { error: await response.text() } | ||
| } |
There was a problem hiding this comment.
In the Fetch API, the response body stream can only be read once. If response.json() fails (for example, if the server returns HTML or plain text on a 502/504 error), the stream is already disturbed/consumed. Calling response.text() in the catch block will throw a TypeError: body stream already read instead of returning the text, masking the actual error.
To fix this, read the response as text first, and then attempt to parse it as JSON.
| let body: any | |
| try { | |
| body = await response.json() | |
| } catch { | |
| body = { error: await response.text() } | |
| } | |
| const text = await response.text() | |
| let body: any | |
| try { | |
| body = JSON.parse(text) | |
| } catch { | |
| body = { error: text } | |
| } |
There was a problem hiding this comment.
Fixed in 21da785: the client now reads response.text() once and then parses JSON from that string, falling back to the raw text for non-JSON errors.
| export function parseConfig(raw: Record<string, unknown> = {}): XMemOpenClawConfig { | ||
| if (raw && typeof raw === "object" && Object.keys(raw).length > 0) { | ||
| assertAllowedKeys(raw) | ||
| } |
There was a problem hiding this comment.
If api.pluginConfig is null (which is common for unconfigured plugins in some environments), the default parameter raw = {} will not be applied because default parameters only apply to undefined. This will cause raw.apiKey to throw a TypeError: Cannot read properties of null when returning the configuration object.
Normalize raw to an empty object if it is null or undefined to prevent runtime crashes.
| export function parseConfig(raw: Record<string, unknown> = {}): XMemOpenClawConfig { | |
| if (raw && typeof raw === "object" && Object.keys(raw).length > 0) { | |
| assertAllowedKeys(raw) | |
| } | |
| export function parseConfig(rawConfig: Record<string, unknown> | null | undefined = {}): XMemOpenClawConfig { | |
| const raw = rawConfig ?? {} | |
| if (Object.keys(raw).length > 0) { | |
| assertAllowedKeys(raw) | |
| } |
There was a problem hiding this comment.
Fixed in 21da785: parseConfig now accepts null | undefined, normalizes to {}, and then validates keys before reading fields.
| export function buildCaptureHandler(client: XMemClient) { | ||
| return async (event: Record<string, unknown>) => { | ||
| const text = String(event.text || event.output || event.message || "") | ||
| if (text.trim().length < 80) return | ||
| await client.addMemory(text, { | ||
| type: detectCategory(text), | ||
| source: "openclaw_auto_capture", | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
If client.addMemory throws an error (e.g., due to a temporary network issue or API timeout), the error will propagate up and could crash the entire OpenClaw agent session during the agent_end event.
Since auto-capture is a background/non-blocking operation, any errors should be caught and logged gracefully to prevent disrupting the user's session.
| export function buildCaptureHandler(client: XMemClient) { | |
| return async (event: Record<string, unknown>) => { | |
| const text = String(event.text || event.output || event.message || "") | |
| if (text.trim().length < 80) return | |
| await client.addMemory(text, { | |
| type: detectCategory(text), | |
| source: "openclaw_auto_capture", | |
| }) | |
| } | |
| } | |
| export function buildCaptureHandler(client: XMemClient) { | |
| return async (event: Record<string, unknown>) => { | |
| try { | |
| const text = String(event.text || event.output || event.message || "") | |
| if (text.trim().length < 80) return | |
| await client.addMemory(text, { | |
| type: detectCategory(text), | |
| source: "openclaw_auto_capture", | |
| }) | |
| } catch (error) { | |
| console.error("XMem auto-capture failed:", error) | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Fixed in 21da785: the installer now refuses to overwrite existing generated files by default and supports --force for intentional replacement. Added tests for both paths.
There was a problem hiding this comment.
Fixed in 56be9b2: auto-capture now catches XMem API failures and logs a warning instead of propagating errors from the agent_end hook.
| export function buildRecallHandler(client: XMemClient, cfg: XMemOpenClawConfig) { | ||
| return async (event: Record<string, unknown>) => { | ||
| const prompt = String(event.prompt || event.input || event.message || "") | ||
| if (!prompt.trim()) return | ||
| const results = await client.search(prompt, cfg.maxRecallResults) | ||
| if (!results.length) return | ||
| return { | ||
| additionalContext: `<xmem-context>\n${results.map((r, i) => `${i + 1}. ${r.content || ""}`).join("\n\n")}\n</xmem-context>`, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
If client.search throws an error (e.g., due to network issues or an invalid API key), it could crash the prompt building process in OpenClaw.
Since auto-recall is an enhancement, it should fail gracefully by logging the error and returning no additional context, allowing the agent to continue working.
| export function buildRecallHandler(client: XMemClient, cfg: XMemOpenClawConfig) { | |
| return async (event: Record<string, unknown>) => { | |
| const prompt = String(event.prompt || event.input || event.message || "") | |
| if (!prompt.trim()) return | |
| const results = await client.search(prompt, cfg.maxRecallResults) | |
| if (!results.length) return | |
| return { | |
| additionalContext: `<xmem-context>\n${results.map((r, i) => `${i + 1}. ${r.content || ""}`).join("\n\n")}\n</xmem-context>`, | |
| } | |
| } | |
| } | |
| export function buildRecallHandler(client: XMemClient, cfg: XMemOpenClawConfig) { | |
| return async (event: Record<string, unknown>) => { | |
| try { | |
| const prompt = String(event.prompt || event.input || event.message || "") | |
| if (!prompt.trim()) return | |
| const results = await client.search(prompt, cfg.maxRecallResults) | |
| if (!results.length) return | |
| return { | |
| additionalContext: "<xmem-context>\n" + results.map((r, i) => (i + 1) + ". " + (r.content || "")).join("\n\n") + "\n</xmem-context>", | |
| } | |
| } catch (error) { | |
| if (cfg.debug) { | |
| console.error("XMem auto-recall failed:", error) | |
| } | |
| return | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Fixed in 56be9b2: auto-recall now catches search failures, logs only in debug mode, and returns no extra context so prompt building can continue.
| export function registerCommands(api: OpenClawPluginApi, client: XMemClient): void { | ||
| api.registerCommand({ | ||
| name: "remember", | ||
| description: "Save something to XMem", | ||
| acceptsArgs: true, | ||
| requireAuth: true, | ||
| handler: async (ctx: { args?: string }) => { | ||
| const text = ctx.args?.trim() | ||
| if (!text) return { text: "Usage: /remember <text to remember>" } | ||
| await client.addMemory(text, { type: detectCategory(text), source: "openclaw_command" }) | ||
| const preview = text.length > 60 ? `${text.slice(0, 60)}...` : text | ||
| return { text: `Remembered in XMem: "${preview}"` } | ||
| }, | ||
| }) | ||
| api.registerCommand({ | ||
| name: "recall", | ||
| description: "Search XMem memories", | ||
| acceptsArgs: true, | ||
| requireAuth: true, | ||
| handler: async (ctx: { args?: string }) => { | ||
| const query = ctx.args?.trim() | ||
| if (!query) return { text: "Usage: /recall <search query>" } | ||
| const results = await client.search(query, 8) | ||
| if (!results.length) return { text: `No XMem memories found for: "${query}"` } | ||
| return { text: `Found ${results.length} XMem memories:\n\n${results.map((r, i) => `${i + 1}. ${r.content || ""}`).join("\n")}` } | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
If the XMem API is down or misconfigured, executing /remember or /recall will throw an uncaught exception, which might crash the command execution or print an unhelpful stack trace to the user.
Wrap the API calls in try/catch blocks to return a clean, user-friendly error message.
export function registerCommands(api: OpenClawPluginApi, client: XMemClient): void {
api.registerCommand({
name: "remember",
description: "Save something to XMem",
acceptsArgs: true,
requireAuth: true,
handler: async (ctx: { args?: string }) => {
const text = ctx.args?.trim()
if (!text) return { text: "Usage: /remember <text to remember>" }
try {
await client.addMemory(text, { type: detectCategory(text), source: "openclaw_command" })
const preview = text.length > 60 ? `
return { text: `Remembered in XMem: "
} catch (error) {
return { text: `Failed to save memory:
}
},
})
api.registerCommand({
name: "recall",
description: "Search XMem memories",
acceptsArgs: true,
requireAuth: true,
handler: async (ctx: { args?: string }) => {
const query = ctx.args?.trim()
if (!query) return { text: "Usage: /recall <search query>" }
try {
const results = await client.search(query, 8)
if (!results.length) return { text: `No XMem memories found for: "
return { text: `Found
} catch (error) {
return { text: `Failed to search memories:
}
},
})
}There was a problem hiding this comment.
Fixed in 56be9b2: /remember and /recall now wrap XMem calls in try/catch, log warnings, and return user-friendly failure messages.
|
| Filename | Overview |
|---|---|
| plugin/xmem-opencode/src/index.ts | Main plugin entry point; chat.message hook correctly gates context injection on both autoRecallEveryPrompt and isFirstMessage. No new issues found. |
| plugin/xmem-opencode/src/services/compaction.ts | Compaction hook with summarizedSessions cleanup in catch block (previously flagged issue is fixed); token-threshold logic and cooldown guard look correct. |
| plugin/xmem-opencode/src/services/auth.ts | Browser auth flow uses execFile (shell-injection fix verified); credentials saved with 0o600/0o700 permissions; local callback server correctly guards against double-resolution. |
| plugin/xmem-hermes/src/cli.js | Install/doctor/smoke-test CLI; overwrite guard and bounds-checked flag parsing are fixed; YAML generation interpolates apiUrl unquoted, and mcpCommand split can't handle paths with spaces. |
| plugin/xmem-openclaw/index.ts | ensureOpenClawMemoryStore moved inside register(); duplicate tool registrations removed; plugin struct looks correct. |
| plugin/xmem-openclaw/memory.ts | redactSecrets now correctly handles Authorization: Bearer tokens with two-pass patterns; previously flagged issue resolved. |
| plugin/xmem-opencode/src/config.ts | Config loading with JSONC support; credentials loaded from file with correct fallback chain; autoRecallEveryPrompt defaulting logic is intentional. |
| plugin/xmem-claude/scripts/lib/xmem-client.cjs | Previously missing lib/ helper; now present with Bearer redaction and response parsing. |
Sequence Diagram
sequenceDiagram
participant User
participant Agent as AI Agent (opencode/cursor/claude)
participant Plugin as XMem Plugin
participant XMemAPI as XMem API
User->>Agent: Send message
Agent->>Plugin: chat.message hook (input, output)
Plugin->>Plugin: detectMemoryKeyword(userMessage)
alt keyword detected
Plugin->>output: push MEMORY_NUDGE synthetic part
end
alt autoRecallEveryPrompt OR isFirstMessage
Plugin->>XMemAPI: retrieve(userMessage, user tags)
Plugin->>XMemAPI: search(userMessage, project tags)
Plugin->>output: unshift context synthetic part
end
Plugin->>Agent: return modified output
Agent->>Plugin: event(message.updated) assistant finish
Plugin->>Plugin: checkAndTriggerCompaction()
alt "usage > threshold"
Plugin->>XMemAPI: searchProjectMemories()
Plugin->>Disk: injectHookMessage (compaction prompt)
Plugin->>Agent: session.summarize()
Agent-->>Plugin: "event(message.updated) summary=true finish=true"
Plugin->>Agent: session.messages() fetch summary
Plugin->>XMemAPI: ingest(summaryContent, project tags)
end
User->>Agent: "xmem tool call (mode=add/search/recall)"
Agent->>Plugin: tool.xmem.execute(args)
Plugin->>Plugin: stripPrivateContent / isFullyPrivate
Plugin->>XMemAPI: ingest / search / retrieve / codeQuery
XMemAPI-->>Plugin: result
Plugin-->>Agent: JSON result
Reviews (7): Last reviewed commit: "Fix OpenCode recall and remove inert rel..." | Re-trigger Greptile
|
Added Extra validation run:
Generated |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
| if: steps.version-check.outputs.changed == 'true' | ||
| run: bun run typecheck | ||
|
|
||
| - name: Build | ||
| if: steps.version-check.outputs.changed == 'true' | ||
| run: bun run build | ||
|
|
||
| - name: Publish | ||
| if: steps.version-check.outputs.changed == 'true' | ||
| run: npm publish --access public --provenance |
There was a problem hiding this comment.
npm publish will fail — missing NODE_AUTH_TOKEN and wrong working directory
The setup-node step sets the npm registry to https://registry.npmjs.org and configures the client to read authentication from the NODE_AUTH_TOKEN environment variable, but the variable is never populated (no env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}). The npm publish step will fail with a 401 UNAUTHENTICATED error. Additionally, none of the bun install, bun run typecheck, or bun run build steps set a working-directory: plugin/xmem-opencode, so they run against the repo root rather than this package. Note also that GitHub Actions only reads workflows from the repository root .github/workflows/; a file at plugin/xmem-opencode/.github/workflows/ is never executed.
There was a problem hiding this comment.
Fixed in 12e3387: removed the copied nested workflow from plugin/xmem-opencode/.github/workflows/release.yml. In this monorepo location GitHub would not execute it, and leaving an inert package publish workflow there was misleading.
Validation:
npm.cmd run typecheckinplugin/xmem-opencodenpm.cmd run buildinplugin/xmem-opencodegit diff --check- Plugin credential scan found no committed XMem API keys
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
Summary
plugin/Validation
npm run check-json --prefix plugin\xmem-openclawnpm run build --prefix plugin\xmem-hermesnpm test --prefix plugin\xmem-hermesplugin/; only README source-attribution references remainNotes
uv.lock