fix(openclaw): inject context via system prompt instead of overwriting MEMORY.md (#1386)

* fix(openclaw): inject context via system prompt instead of overwriting MEMORY.md

The OpenClaw plugin was overwriting each agent's MEMORY.md with a large
auto-generated observation dump (~12-15KB) on every before_agent_start
and tool_result_persist event. This conflicts with OpenClaw's design
where MEMORY.md is agent-curated long-term memory.

Migrate context injection from file-based (writeFile MEMORY.md) to
OpenClaw's native before_prompt_build hook, which returns context via
appendSystemContext. This keeps MEMORY.md under agent control while
still providing cross-session observation context to the LLM.

Changes:
- Add before_prompt_build hook that returns { appendSystemContext }
- Remove writeFile/MEMORY.md sync from before_agent_start
- Remove MEMORY.md sync from tool_result_persist (observations still recorded)
- Add 60s TTL cache to avoid re-fetching context on every LLM turn
- Add syncMemoryFileExclude config for per-agent opt-out
- Remove dead workspaceDirsBySessionKey tracking map
- Rewrite test suite to verify prompt injection instead of file writes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): align settings defaults with backend and use nullish coalescing

The web UI had two issues causing settings inflation:

1. DEFAULT_SETTINGS in the UI used FULL_COUNT='5' and all token columns
   'true', while SettingsDefaultsManager (backend) uses FULL_COUNT='0'
   and token columns 'false'. Opening the settings modal and saving
   without changes would silently inflate the context.

2. useSettings used || for fallback, which treats '0' and 'false' as
   falsy — even when the backend correctly returns these values, the UI
   would replace them with inflated defaults. Changed to ?? (nullish
   coalescing) so only null/undefined trigger the fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(openclaw): update integration docs for system prompt injection

Reflect the migration from MEMORY.md file writes to before_prompt_build
hook-based context injection:

- Update architecture diagram and overview to show new hook flow
- Replace "MEMORY.md Live Sync" section with "System Prompt Context Injection"
- Update event lifecycle steps (before_agent_start, tool_result_persist)
- Add before_prompt_build step with TTL cache description
- Document new syncMemoryFileExclude config parameter
- Update session tracking to reflect removed workspaceDirsBySessionKey

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix terminology and update SKILL.md for system prompt injection

Replace "prompt injection" with "context injection" in docs to avoid
confusion with the OWASP security term. Update openclaw/SKILL.md to
reflect the new before_prompt_build hook and remove stale MEMORY.md
references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alex Newman <thedotmack@gmail.com>
This commit is contained in:
Glucksberg
2026-03-17 20:14:30 -04:00
committed by GitHub
parent 9e7b08445f
commit 9361e33b6d
7 changed files with 267 additions and 204 deletions

View File

@@ -1,6 +1,6 @@
---
title: OpenClaw Integration
description: Persistent memory for OpenClaw agents — observation recording, MEMORY.md live sync, and real-time observation feeds
description: Persistent memory for OpenClaw agents — observation recording, system prompt context injection, and real-time observation feeds
icon: dragon
---
@@ -9,7 +9,7 @@ icon: dragon
The OpenClaw plugin gives claude-mem persistent memory to agents running on the [OpenClaw](https://openclaw.ai) gateway. It handles three things:
1. **Observation recording** — Captures tool usage from OpenClaw's embedded runner and sends it to the claude-mem worker for AI processing
2. **MEMORY.md live sync** — Writes a continuously-updated timeline to each agent's workspace so agents always have context from previous sessions
2. **System prompt context injection** — Injects the observation timeline into each agent's system prompt via the `before_prompt_build` hook, keeping `MEMORY.md` free for agent-curated memory
3. **Observation feed** — Streams new observations to messaging channels (Telegram, Discord, Slack, etc.) in real-time via SSE
<Info>
@@ -21,10 +21,11 @@ OpenClaw's embedded runner (`pi-embedded`) calls the Anthropic API directly with
```plaintext
OpenClaw Gateway
├── before_agent_start ──→ Sync MEMORY.md + Init session
├── tool_result_persist ──→ Record observation + Re-sync MEMORY.md
├── before_agent_start ──→ Init session
├── before_prompt_build ──→ Inject context into system prompt
├── tool_result_persist ──→ Record observation
├── agent_end ────────────→ Summarize + Complete session
└── gateway_start ────────→ Reset session tracking
└── gateway_start ────────→ Reset session tracking + context cache
Claude-Mem Worker (localhost:37777)
@@ -32,7 +33,7 @@ OpenClaw Gateway
├── POST /api/sessions/observations
├── POST /api/sessions/summarize
├── POST /api/sessions/complete
├── GET /api/context/inject ──→ MEMORY.md content
├── GET /api/context/inject ──→ System prompt context
└── GET /stream ─────────────→ SSE → Messaging channels
```
@@ -40,21 +41,15 @@ OpenClaw Gateway
<Steps>
<Step title="Agent starts (before_agent_start)">
When an OpenClaw agent starts, the plugin does two things:
When an OpenClaw agent starts, the plugin initializes a session by sending the user prompt to `POST /api/sessions/init` so the worker can create a new session and start processing.
</Step>
<Step title="Context injected (before_prompt_build)">
Before each LLM call, the plugin fetches the observation timeline from the worker's `/api/context/inject` endpoint and returns it as `appendSystemContext`. This injects cross-session context directly into the agent's system prompt without writing any files.
1. **Syncs MEMORY.md** — Fetches the latest timeline from the worker's `/api/context/inject` endpoint and writes it to `MEMORY.md` in the agent's workspace directory. This gives the agent context from all previous sessions before it starts working.
2. **Initializes a session** — Sends the user prompt to `POST /api/sessions/init` so the worker can create a new session and start processing.
Short prompts (under 10 characters) skip session init but still sync MEMORY.md.
The context is cached for 60 seconds to avoid re-fetching on every LLM turn within a session.
</Step>
<Step title="Tool use recorded (tool_result_persist)">
Every time the agent uses a tool (Read, Write, Bash, etc.), the plugin:
1. **Sends the observation** to `POST /api/sessions/observations` with the tool name, input, and truncated response (max 1000 chars)
2. **Re-syncs MEMORY.md** with the latest timeline from the worker
Both operations are fire-and-forget — they don't block the agent from continuing work. The MEMORY.md file gets progressively richer as the session continues.
Every time the agent uses a tool (Read, Write, Bash, etc.), the plugin sends the observation to `POST /api/sessions/observations` with the tool name, input, and truncated response (max 1000 chars). This is fire-and-forget — it doesn't block the agent from continuing work.
Tools prefixed with `memory_` are skipped to avoid recursive recording.
</Step>
@@ -62,21 +57,18 @@ OpenClaw Gateway
When the agent completes, the plugin extracts the last assistant message and sends it to `POST /api/sessions/summarize`, then calls `POST /api/sessions/complete` to close the session. Both are fire-and-forget.
</Step>
<Step title="Gateway restarts (gateway_start)">
Clears all session tracking (session IDs, workspace directory mappings) so agents get fresh state after a gateway restart.
Clears all session tracking (session IDs, context cache) so agents get fresh state after a gateway restart.
</Step>
</Steps>
### MEMORY.md Live Sync
### System Prompt Context Injection
The plugin writes a `MEMORY.md` file to each agent's workspace directory containing the full timeline of observations and summaries from previous sessions. This file is updated:
The plugin injects cross-session observation context into each agent's system prompt via OpenClaw's `before_prompt_build` hook. The content comes from the worker's `GET /api/context/inject?projects=<project>` endpoint, which generates a formatted markdown timeline from the SQLite database.
- On every `before_agent_start` event (agent gets fresh context before starting)
- On every `tool_result_persist` event (context stays current during the session)
The content comes from the worker's `GET /api/context/inject?projects=<project>` endpoint, which generates a formatted markdown timeline from the SQLite database.
This approach keeps `MEMORY.md` under the agent's control for curated long-term memory (decisions, preferences, durable facts), while the observation timeline is delivered through the system prompt where it belongs.
<Info>
MEMORY.md updates are fire-and-forget. They run in the background without blocking the agent. The file reflects whatever the worker has processed so far — it doesn't wait for the current observation to be fully processed before writing.
Context is cached for 60 seconds per project to avoid re-fetching on every LLM turn. The cache is cleared on gateway restart. Use `syncMemoryFileExclude` to opt specific agents out of context injection entirely.
</Info>
### Observation Feed (SSE → Messaging)
@@ -319,7 +311,11 @@ The claude-mem worker service must be running on the same machine as the OpenCla
</ParamField>
<ParamField body="syncMemoryFile" type="boolean" default={true}>
Enable automatic MEMORY.md sync to agent workspaces. Set to `false` if you don't want the plugin writing files to workspace directories.
Inject observation context into the agent system prompt via `before_prompt_build` hook. When `true`, agents receive cross-session context automatically. Set to `false` to disable context injection entirely (observations are still recorded).
</ParamField>
<ParamField body="syncMemoryFileExclude" type="string[]" default={[]}>
Agent IDs excluded from automatic context injection. Useful for agents that curate their own memory and don't need the observation timeline (e.g., `["snarf", "debugger"]`). Observations are still recorded for excluded agents — only the context injection is skipped.
</ParamField>
<ParamField body="workerPort" type="number" default={37777}>
@@ -374,9 +370,9 @@ The plugin uses HTTP calls to the already-running claude-mem worker service rath
Each OpenClaw agent session gets a unique `contentSessionId` (format: `openclaw-<sessionKey>-<timestamp>`) that maps to a claude-mem session in the worker. The plugin tracks:
- `sessionIds` — Maps OpenClaw session keys to content session IDs
- `workspaceDirsBySessionKey` — Maps session keys to workspace directories so `tool_result_persist` events can sync MEMORY.md even when the event context doesn't include `workspaceDir`
- `contextCache` — TTL cache (60s) for context injection responses, keyed by project
Both maps are cleared on `gateway_start`.
Both are cleared on `gateway_start`.
## Requirements

View File

@@ -1,6 +1,6 @@
# Claude-Mem OpenClaw Plugin — Setup Guide
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway. By the end, your agents will have persistent memory across sessions via system prompt context injection, and optionally a real-time observation feed streaming to a messaging channel.
## Quick Install (Recommended)
@@ -138,7 +138,9 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
- **`project`** (string, default: `"openclaw"`) — The project name that scopes all observations in the memory database. Use a unique name per gateway/use-case so observations don't mix. For example, if this gateway runs a coding bot, use `"coding-bot"`.
- **`syncMemoryFile`** (boolean, default: `true`) — When enabled, the plugin writes a `MEMORY.md` file to each agent's workspace directory. This file contains the full timeline of observations and summaries from previous sessions, and it updates on every tool use so agents always have fresh context. Set to `false` only if you don't want the plugin writing files to agent workspaces.
- **`syncMemoryFile`** (boolean, default: `true`) — When enabled, the plugin injects the observation timeline into each agent's system prompt via the `before_prompt_build` hook. This gives agents cross-session context without writing to MEMORY.md. Set to `false` to disable context injection entirely (observations are still recorded).
- **`syncMemoryFileExclude`** (string[], default: `[]`) — Agent IDs excluded from automatic context injection. Useful for agents that curate their own memory. Observations are still recorded for excluded agents.
- **`workerPort`** (number, default: `37777`) — The port where the claude-mem worker service is listening. Only change this if you configured the worker to use a different port.
@@ -168,13 +170,14 @@ The observation feed shows `disconnected` because we haven't configured it yet.
Have an agent do some work. The plugin automatically records observations through these OpenClaw events:
1. **`before_agent_start`** — Initializes a claude-mem session when the agent starts, syncs MEMORY.md to the workspace
2. **`tool_result_persist`** — Records each tool use (Read, Write, Bash, etc.) as an observation, re-syncs MEMORY.md
3. **`agent_end`** — Summarizes the session and marks it complete
1. **`before_agent_start`** — Initializes a claude-mem session when the agent starts
2. **`before_prompt_build`** — Injects the observation timeline into the agent's system prompt (cached for 60s)
3. **`tool_result_persist`** — Records each tool use (Read, Write, Bash, etc.) as an observation
4. **`agent_end`** — Summarizes the session and marks it complete
All of this happens automatically. No additional configuration needed.
To verify it's working, check the agent's workspace directory for a `MEMORY.md` file after the agent runs. It should contain a formatted timeline of observations.
To verify it's working, check the worker's viewer UI at http://localhost:37777 to see observations appearing after the agent runs.
You can also check the worker's viewer UI at http://localhost:37777 to see observations appearing in real time.
@@ -372,10 +375,11 @@ Shows observation feed status. Accepts optional `on`/`off` argument.
```
OpenClaw Gateway
├── before_agent_start ──→ Sync MEMORY.md + Init session
├── tool_result_persist ──→ Record observation + Re-sync MEMORY.md
├── before_agent_start ──→ Init session
├── before_prompt_build ──→ Inject context into system prompt
├── tool_result_persist ──→ Record observation
├── agent_end ────────────→ Summarize + Complete session
└── gateway_start ────────→ Reset session tracking
└── gateway_start ────────→ Reset session tracking + context cache
Claude-Mem Worker (localhost:37777)
@@ -383,17 +387,15 @@ OpenClaw Gateway
├── POST /api/sessions/observations
├── POST /api/sessions/summarize
├── POST /api/sessions/complete
├── GET /api/context/inject ──→ MEMORY.md content
├── GET /api/context/inject ──→ System prompt context
└── GET /stream ─────────────→ SSE → Messaging channels
```
### MEMORY.md live sync
### System prompt context injection
The plugin writes `MEMORY.md` to each agent's workspace with the full observation timeline. It updates:
- On every `before_agent_start` — agent gets fresh context before starting
- On every `tool_result_persist` — context stays current as the agent works
The plugin injects the observation timeline into each agent's system prompt via the `before_prompt_build` hook. The content comes from the worker's `GET /api/context/inject` endpoint. Context is cached for 60 seconds per project to avoid re-fetching on every LLM turn. The cache is cleared on gateway restart.
Updates are fire-and-forget (non-blocking). The agent is never held up waiting for MEMORY.md to write.
This keeps MEMORY.md under the agent's control for curated long-term memory, while the observation timeline is delivered through the system prompt.
### Observation recording
@@ -401,10 +403,11 @@ Every tool use (Read, Write, Bash, etc.) is sent to the claude-mem worker as an
### Session lifecycle
- **`before_agent_start`** — Creates a session in the worker, syncs MEMORY.md. Short prompts (under 10 chars) skip session init but still sync.
- **`tool_result_persist`** — Records observation (fire-and-forget), re-syncs MEMORY.md (fire-and-forget). Tool responses are truncated to 1000 characters.
- **`before_agent_start`** — Creates a session in the worker.
- **`before_prompt_build`** — Fetches the observation timeline and returns it as `appendSystemContext`. Cached for 60s.
- **`tool_result_persist`** — Records observation (fire-and-forget). Tool responses are truncated to 1000 characters.
- **`agent_end`** — Sends the last assistant message for summarization, then completes the session. Both fire-and-forget.
- **`gateway_start`** — Clears all session tracking (session IDs, workspace mappings) so agents start fresh.
- **`gateway_start`** — Clears all session tracking (session IDs, context cache) so agents start fresh.
### Observation feed
@@ -417,7 +420,7 @@ A background service connects to the worker's SSE stream and forwards `new_obser
| Worker health check fails | Is bun installed? (`bun --version`). Is something else on port 37777? (`lsof -i :37777`). Try running directly: `bun plugin/scripts/worker-service.cjs start` |
| Worker started from Claude Code install but not responding | Check `cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:status`. May need `npm run worker:restart`. |
| Worker started from cloned repo but not responding | Check `cd /path/to/claude-mem && npm run worker:status`. Make sure you ran `npm install && npm run build` first. |
| No MEMORY.md appearing | Check that `syncMemoryFile` is not set to `false`. Verify the agent's event context includes `workspaceDir`. |
| No context in agent system prompt | Check that `syncMemoryFile` is not set to `false`. Check that the agent's ID is not in `syncMemoryFileExclude`. Verify the worker is running and has observations. |
| Observations not being recorded | Check gateway logs for `[claude-mem]` messages. The worker must be running and reachable on localhost:37777. |
| Feed shows `disconnected` | Worker's `/stream` endpoint not reachable. Check `workerPort` matches the actual worker port. |
| Feed shows `reconnecting` | Connection dropped. The plugin auto-reconnects — wait up to 30 seconds. |
@@ -451,7 +454,8 @@ A background service connects to the worker's SSE stream and forwards `new_obser
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `project` | string | `"openclaw"` | Project name scoping observations in the database |
| `syncMemoryFile` | boolean | `true` | Write MEMORY.md to agent workspaces |
| `syncMemoryFile` | boolean | `true` | Inject observation context into agent system prompt |
| `syncMemoryFileExclude` | string[] | `[]` | Agent IDs excluded from context injection |
| `workerPort` | number | `37777` | Claude-mem worker service port |
| `observationFeed.enabled` | boolean | `false` | Stream observations to a messaging channel |
| `observationFeed.channel` | string | — | Channel type: `telegram`, `discord`, `slack`, `signal`, `whatsapp`, `line` |

View File

@@ -14,7 +14,13 @@
"syncMemoryFile": {
"type": "boolean",
"default": true,
"description": "Automatically sync MEMORY.md on session start"
"description": "Inject observation context into the agent system prompt via before_prompt_build hook. When true, agents receive cross-session context without MEMORY.md being overwritten."
},
"syncMemoryFileExclude": {
"type": "array",
"items": { "type": "string" },
"default": [],
"description": "Agent IDs excluded from automatic context injection (observations are still recorded, only prompt injection is skipped)"
},
"workerPort": {
"type": "number",

View File

@@ -87,9 +87,11 @@ function createMockApi(pluginConfigOverride: Record<string, any> = {}) {
getEventHandlers: (event: string) => eventHandlers.get(event) || [],
fireEvent: async (event: string, data: any, ctx: any = {}) => {
const handlers = eventHandlers.get(event) || [];
let lastResult: any;
for (const handler of handlers) {
await handler(data, ctx);
lastResult = await handler(data, ctx);
}
return lastResult;
},
};
}
@@ -106,6 +108,7 @@ describe("claudeMemPlugin", () => {
assert.ok(getEventHandlers("session_start").length > 0, "session_start handler registered");
assert.ok(getEventHandlers("after_compaction").length > 0, "after_compaction handler registered");
assert.ok(getEventHandlers("before_agent_start").length > 0, "before_agent_start handler registered");
assert.ok(getEventHandlers("before_prompt_build").length > 0, "before_prompt_build handler registered");
assert.ok(getEventHandlers("tool_result_persist").length > 0, "tool_result_persist handler registered");
assert.ok(getEventHandlers("agent_end").length > 0, "agent_end handler registered");
assert.ok(getEventHandlers("gateway_start").length > 0, "gateway_start handler registered");
@@ -535,11 +538,10 @@ describe("Observation I/O event handlers", () => {
});
});
describe("MEMORY.md context sync", () => {
describe("before_prompt_build context injection", () => {
let workerServer: Server;
let workerPort: number;
let receivedRequests: Array<{ method: string; url: string; body: any }> = [];
let tmpDir: string;
let contextResponse = "# Claude-Mem Context\n\n## Timeline\n- Session 1: Did some work";
function startWorkerMock(): Promise<number> {
@@ -586,21 +588,20 @@ describe("MEMORY.md context sync", () => {
receivedRequests = [];
contextResponse = "# Claude-Mem Context\n\n## Timeline\n- Session 1: Did some work";
workerPort = await startWorkerMock();
tmpDir = await mkdtemp(join(tmpdir(), "claude-mem-test-"));
});
afterEach(async () => {
workerServer?.close();
await rm(tmpDir, { recursive: true, force: true });
});
it("writes MEMORY.md to workspace on before_agent_start", async () => {
it("returns appendSystemContext from before_prompt_build", async () => {
const { api, logs, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
await fireEvent("before_agent_start", {
const result = await fireEvent("before_prompt_build", {
prompt: "Help me write a function",
}, { sessionKey: "sync-test", workspaceDir: tmpDir });
messages: [],
}, { agentId: "main" });
await new Promise((resolve) => setTimeout(resolve, 200));
@@ -608,142 +609,143 @@ describe("MEMORY.md context sync", () => {
assert.ok(contextRequest, "should request context from worker");
assert.ok(contextRequest!.url!.includes("projects=openclaw"));
const memoryContent = await readFile(join(tmpDir, "MEMORY.md"), "utf-8");
assert.ok(memoryContent.includes("Claude-Mem Context"), "MEMORY.md should contain context");
assert.ok(memoryContent.includes("Session 1"), "MEMORY.md should contain timeline");
assert.ok(logs.some((l) => l.includes("MEMORY.md synced")));
assert.ok(result, "should return a result");
assert.ok(result.appendSystemContext, "should return appendSystemContext");
assert.ok(result.appendSystemContext.includes("Claude-Mem Context"), "should contain context");
assert.ok(result.appendSystemContext.includes("Session 1"), "should contain timeline");
assert.ok(logs.some((l) => l.includes("Context injected via system prompt")));
});
it("syncs MEMORY.md on every before_agent_start call", async () => {
const { api, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
it("does not write MEMORY.md on before_agent_start", async () => {
const tmpDir = await mkdtemp(join(tmpdir(), "claude-mem-test-"));
try {
const { api, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
await fireEvent("before_agent_start", {
prompt: "First prompt for this agent",
}, { sessionKey: "agent-a", workspaceDir: tmpDir });
await fireEvent("before_agent_start", {
prompt: "Help me write a function",
}, { sessionKey: "sync-test", workspaceDir: tmpDir });
await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 200));
const firstContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(firstContextRequests.length, 1, "first call should fetch context");
await fireEvent("before_agent_start", {
prompt: "Second prompt for same agent",
}, { sessionKey: "agent-a", workspaceDir: tmpDir });
await new Promise((resolve) => setTimeout(resolve, 200));
const allContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(allContextRequests.length, 2, "should re-fetch context on every call");
let memoryExists = true;
try {
await readFile(join(tmpDir, "MEMORY.md"), "utf-8");
} catch {
memoryExists = false;
}
assert.ok(!memoryExists, "MEMORY.md should not be created by before_agent_start");
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
});
it("syncs MEMORY.md on tool_result_persist via fire-and-forget", async () => {
const { api, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
it("does not sync MEMORY.md on tool_result_persist", async () => {
const tmpDir = await mkdtemp(join(tmpdir(), "claude-mem-test-"));
try {
const { api, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
// Init session to register workspace dir
await fireEvent("before_agent_start", {
prompt: "Help me write a function",
}, { sessionKey: "tool-sync", workspaceDir: tmpDir });
await fireEvent("before_agent_start", {
prompt: "Help me write a function",
}, { sessionKey: "tool-sync", workspaceDir: tmpDir });
await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 200));
const preToolContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(preToolContextRequests.length, 1, "before_agent_start should sync once");
await fireEvent("tool_result_persist", {
toolName: "Read",
params: { file_path: "/src/app.ts" },
message: { content: [{ type: "text", text: "file contents" }] },
}, { sessionKey: "tool-sync" });
// Fire tool result — should trigger another MEMORY.md sync
await fireEvent("tool_result_persist", {
toolName: "Read",
params: { file_path: "/src/app.ts" },
message: { content: [{ type: "text", text: "file contents" }] },
}, { sessionKey: "tool-sync" });
await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 200));
const contextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(contextRequests.length, 0, "tool_result_persist should not fetch context");
const postToolContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(postToolContextRequests.length, 2, "tool_result_persist should trigger another sync");
const memoryContent = await readFile(join(tmpDir, "MEMORY.md"), "utf-8");
assert.ok(memoryContent.includes("Claude-Mem Context"), "MEMORY.md should be updated");
let memoryExists = true;
try {
await readFile(join(tmpDir, "MEMORY.md"), "utf-8");
} catch {
memoryExists = false;
}
assert.ok(!memoryExists, "MEMORY.md should not be written by tool_result_persist");
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
});
it("skips MEMORY.md sync when syncMemoryFile is false", async () => {
it("skips context injection when syncMemoryFile is false", async () => {
const { api, fireEvent } = createMockApi({ workerPort, syncMemoryFile: false });
claudeMemPlugin(api);
await fireEvent("before_agent_start", {
const result = await fireEvent("before_prompt_build", {
prompt: "Help me write a function",
}, { sessionKey: "no-sync", workspaceDir: tmpDir });
messages: [],
}, { agentId: "main" });
await new Promise((resolve) => setTimeout(resolve, 200));
const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject"));
assert.ok(!contextRequest, "should not fetch context when sync disabled");
assert.ok(!contextRequest, "should not fetch context when injection disabled");
assert.equal(result, undefined, "should return undefined when injection disabled");
});
it("skips MEMORY.md sync when no workspaceDir in context", async () => {
const { api, fireEvent } = createMockApi({ workerPort });
it("skips context injection for excluded agents", async () => {
const { api, fireEvent } = createMockApi({ workerPort, syncMemoryFileExclude: ["snarf"] });
claudeMemPlugin(api);
await fireEvent("before_agent_start", {
prompt: "Help me write a function",
}, { sessionKey: "no-workspace" });
const result = await fireEvent("before_prompt_build", {
prompt: "Help me",
messages: [],
}, { agentId: "snarf" });
await new Promise((resolve) => setTimeout(resolve, 200));
const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject"));
assert.ok(!contextRequest, "should not fetch context without workspaceDir");
assert.ok(!contextRequest, "should not fetch context for excluded agent");
assert.equal(result, undefined, "should return undefined for excluded agent");
});
it("skips writing MEMORY.md when context is empty", async () => {
it("injects context for non-excluded agents", async () => {
const { api, fireEvent } = createMockApi({ workerPort, syncMemoryFileExclude: ["snarf"] });
claudeMemPlugin(api);
const result = await fireEvent("before_prompt_build", {
prompt: "Help me",
messages: [],
}, { agentId: "main" });
await new Promise((resolve) => setTimeout(resolve, 200));
assert.ok(result, "should return a result for non-excluded agent");
assert.ok(result.appendSystemContext, "should inject context for non-excluded agent");
});
it("returns undefined when context is empty", async () => {
contextResponse = " ";
const { api, logs, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
await fireEvent("before_agent_start", {
const result = await fireEvent("before_prompt_build", {
prompt: "Help me write a function",
}, { sessionKey: "empty-ctx", workspaceDir: tmpDir });
messages: [],
}, { agentId: "main" });
await new Promise((resolve) => setTimeout(resolve, 200));
assert.ok(!logs.some((l) => l.includes("MEMORY.md synced")), "should not log sync for empty context");
});
it("gateway_start resets sync tracking so next agent re-syncs", async () => {
const { api, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
// First sync
await fireEvent("before_agent_start", {
prompt: "Help me write a function",
}, { sessionKey: "agent-1", workspaceDir: tmpDir });
await new Promise((resolve) => setTimeout(resolve, 200));
const firstContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(firstContextRequests.length, 1);
// Gateway restart
await fireEvent("gateway_start", {}, {});
// Second sync after gateway restart — same workspace should re-sync
await fireEvent("before_agent_start", {
prompt: "Help me after gateway restart",
}, { sessionKey: "agent-1", workspaceDir: tmpDir });
await new Promise((resolve) => setTimeout(resolve, 200));
const allContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject"));
assert.equal(allContextRequests.length, 2, "should re-fetch context after gateway restart");
assert.equal(result, undefined, "should return undefined for empty context");
assert.ok(!logs.some((l) => l.includes("Context injected")), "should not log injection for empty context");
});
it("uses custom project name in context inject URL", async () => {
const { api, fireEvent } = createMockApi({ workerPort, project: "my-bot" });
claudeMemPlugin(api);
await fireEvent("before_agent_start", {
await fireEvent("before_prompt_build", {
prompt: "Help me write a function",
}, { sessionKey: "proj-test", workspaceDir: tmpDir });
messages: [],
}, { agentId: "main" });
await new Promise((resolve) => setTimeout(resolve, 200));
@@ -751,6 +753,23 @@ describe("MEMORY.md context sync", () => {
assert.ok(contextRequest, "should request context");
assert.ok(contextRequest!.url!.includes("projects=my-bot"), "should use custom project name");
});
it("includes agent-scoped project in context request", async () => {
const { api, fireEvent } = createMockApi({ workerPort });
claudeMemPlugin(api);
await fireEvent("before_prompt_build", {
prompt: "Help me",
messages: [],
}, { agentId: "debugger" });
await new Promise((resolve) => setTimeout(resolve, 200));
const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject"));
assert.ok(contextRequest, "should request context");
const url = decodeURIComponent(contextRequest!.url!);
assert.ok(url.includes("openclaw,openclaw-debugger"), "should include both base and agent-scoped projects");
});
});
describe("SSE stream integration", () => {

View File

@@ -1,5 +1,5 @@
import { writeFile } from "fs/promises";
import { join } from "path";
// No file-system imports needed — context is injected via system prompt hook,
// not by writing to MEMORY.md.
// Minimal type declarations for the OpenClaw Plugin SDK.
// These match the real OpenClawPluginApi provided by the gateway at runtime.
@@ -35,6 +35,18 @@ interface BeforeAgentStartEvent {
prompt?: string;
}
interface BeforePromptBuildEvent {
prompt: string;
messages: unknown[];
}
interface BeforePromptBuildResult {
systemPrompt?: string;
prependContext?: string;
prependSystemContext?: string;
appendSystemContext?: string;
}
interface ToolResultPersistEvent {
toolName?: string;
params?: Record<string, unknown>;
@@ -87,6 +99,7 @@ interface MessageContext {
}
type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
type PromptBuildCallback = (event: BeforePromptBuildEvent, ctx: EventContext) => BeforePromptBuildResult | Promise<BeforePromptBuildResult | void> | void;
type MessageEventCallback<T> = (event: T, ctx: MessageContext) => void | Promise<void>;
interface OpenClawPluginApi {
@@ -109,7 +122,8 @@ interface OpenClawPluginApi {
requireAuth?: boolean;
handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise<PluginCommandResult>;
}) => void;
on: ((event: "before_agent_start", callback: EventCallback<BeforeAgentStartEvent>) => void) &
on: ((event: "before_prompt_build", callback: PromptBuildCallback) => void) &
((event: "before_agent_start", callback: EventCallback<BeforeAgentStartEvent>) => void) &
((event: "tool_result_persist", callback: EventCallback<ToolResultPersistEvent>) => void) &
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
@@ -166,6 +180,7 @@ interface FeedEmojiConfig {
interface ClaudeMemPluginConfig {
syncMemoryFile?: boolean;
syncMemoryFileExclude?: string[];
project?: string;
workerPort?: number;
observationFeed?: {
@@ -532,8 +547,8 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
// Session tracking for observation I/O
// ------------------------------------------------------------------
const sessionIds = new Map<string, string>();
const workspaceDirsBySessionKey = new Map<string, string>();
const syncMemoryFile = userConfig.syncMemoryFile !== false; // default true
const syncMemoryFileExclude = new Set(userConfig.syncMemoryFileExclude || []);
function getContentSessionId(sessionKey?: string): string {
const key = sessionKey || "default";
@@ -543,27 +558,45 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
return sessionIds.get(key)!;
}
async function syncMemoryToWorkspace(workspaceDir: string, ctx?: EventContext): Promise<void> {
function shouldInjectContext(ctx?: EventContext): boolean {
if (!syncMemoryFile) return false;
const agentId = ctx?.agentId;
if (agentId && syncMemoryFileExclude.has(agentId)) return false;
return true;
}
// TTL cache for context injection to avoid re-fetching on every LLM turn.
// before_prompt_build fires on every turn; caching for 60s keeps the worker
// load manageable while still picking up new observations reasonably quickly.
const CONTEXT_CACHE_TTL_MS = 60_000;
const contextCache = new Map<string, { text: string; fetchedAt: number }>();
async function getContextForPrompt(ctx?: EventContext): Promise<string | null> {
// Include both the base project and agent-scoped project (e.g. "openclaw" + "openclaw-main")
const projects = [baseProjectName];
const agentProject = ctx ? getProjectName(ctx) : null;
if (agentProject && agentProject !== baseProjectName) {
projects.push(agentProject);
}
const cacheKey = projects.join(",");
// Return cached context if still fresh
const cached = contextCache.get(cacheKey);
if (cached && Date.now() - cached.fetchedAt < CONTEXT_CACHE_TTL_MS) {
return cached.text;
}
const contextText = await workerGetText(
workerPort,
`/api/context/inject?projects=${encodeURIComponent(projects.join(","))}`,
`/api/context/inject?projects=${encodeURIComponent(cacheKey)}`,
api.logger
);
if (contextText && contextText.trim().length > 0) {
try {
await writeFile(join(workspaceDir, "MEMORY.md"), contextText, "utf-8");
api.logger.info(`[claude-mem] MEMORY.md synced to ${workspaceDir}`);
} catch (writeError: unknown) {
const msg = writeError instanceof Error ? writeError.message : String(writeError);
api.logger.warn(`[claude-mem] Failed to write MEMORY.md: ${msg}`);
}
const trimmed = contextText.trim();
contextCache.set(cacheKey, { text: trimmed, fetchedAt: Date.now() });
return trimmed;
}
return null;
}
// ------------------------------------------------------------------
@@ -611,14 +644,9 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
});
// ------------------------------------------------------------------
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
// Event: before_agent_start — init session
// ------------------------------------------------------------------
api.on("before_agent_start", async (event, ctx) => {
// Track workspace dir so tool_result_persist can sync MEMORY.md later
if (ctx.workspaceDir) {
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
}
// Initialize session in the worker so observations are not skipped
// (the privacy check requires a stored user prompt to exist)
const contentSessionId = getContentSessionId(ctx.sessionKey);
@@ -627,15 +655,28 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
project: getProjectName(ctx),
prompt: event.prompt || "agent run",
}, api.logger);
});
// Sync MEMORY.md before agent runs (provides context to agent)
if (syncMemoryFile && ctx.workspaceDir) {
await syncMemoryToWorkspace(ctx.workspaceDir, ctx);
// ------------------------------------------------------------------
// Event: before_prompt_build — inject context into system prompt
//
// Instead of writing to MEMORY.md (which conflicts with agent-curated
// memory), inject the observation timeline via appendSystemContext.
// This keeps MEMORY.md under the agent's control while still providing
// cross-session context to the LLM.
// ------------------------------------------------------------------
api.on("before_prompt_build", async (_event, ctx) => {
if (!shouldInjectContext(ctx)) return;
const contextText = await getContextForPrompt(ctx);
if (contextText) {
api.logger.info(`[claude-mem] Context injected via system prompt for agent=${ctx.agentId ?? "unknown"}`);
return { appendSystemContext: contextText };
}
});
// ------------------------------------------------------------------
// Event: tool_result_persist — record tool observations + sync MEMORY.md
// Event: tool_result_persist — record tool observations
// ------------------------------------------------------------------
api.on("tool_result_persist", (event, ctx) => {
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
@@ -663,7 +704,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
}
// Fire-and-forget: send observation + sync MEMORY.md in parallel
// Fire-and-forget: send observation to worker
workerPostFireAndForget(workerPort, "/api/sessions/observations", {
contentSessionId,
tool_name: toolName,
@@ -671,11 +712,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
tool_response: toolResponseText,
cwd: "",
}, api.logger);
const workspaceDir = ctx.workspaceDir || workspaceDirsBySessionKey.get(ctx.sessionKey || "default");
if (syncMemoryFile && workspaceDir) {
syncMemoryToWorkspace(workspaceDir, ctx);
}
});
// ------------------------------------------------------------------
@@ -722,15 +758,14 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
api.on("session_end", async (_event, ctx) => {
const key = ctx.sessionKey || "default";
sessionIds.delete(key);
workspaceDirsBySessionKey.delete(key);
});
// ------------------------------------------------------------------
// Event: gateway_start — clear session tracking for fresh start
// ------------------------------------------------------------------
api.on("gateway_start", async () => {
workspaceDirsBySessionKey.clear();
sessionIds.clear();
contextCache.clear();
api.logger.info("[claude-mem] Gateway started — session tracking reset");
});

View File

@@ -18,14 +18,14 @@ export const DEFAULT_SETTINGS = {
CLAUDE_MEM_OPENROUTER_APP_NAME: 'claude-mem',
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true',
// Token Economics (all true for backwards compatibility)
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'true',
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'true',
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'true',
// Token Economics — match SettingsDefaultsManager defaults (off by default to keep context lean)
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'false',
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'false',
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'false',
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
// Display Configuration
CLAUDE_MEM_CONTEXT_FULL_COUNT: '5',
// Display Configuration — match SettingsDefaultsManager defaults
CLAUDE_MEM_CONTEXT_FULL_COUNT: '0',
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
CLAUDE_MEM_CONTEXT_SESSION_COUNT: '10',

View File

@@ -14,38 +14,41 @@ export function useSettings() {
fetch(API_ENDPOINTS.SETTINGS)
.then(res => res.json())
.then(data => {
// Use ?? (nullish coalescing) instead of || so that falsy values
// like '0', 'false', and '' from the backend are preserved.
// Using || would silently replace them with the UI defaults.
setSettings({
CLAUDE_MEM_MODEL: data.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: data.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: data.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
CLAUDE_MEM_WORKER_HOST: data.CLAUDE_MEM_WORKER_HOST || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_HOST,
CLAUDE_MEM_MODEL: data.CLAUDE_MEM_MODEL ?? DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: data.CLAUDE_MEM_CONTEXT_OBSERVATIONS ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: data.CLAUDE_MEM_WORKER_PORT ?? DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
CLAUDE_MEM_WORKER_HOST: data.CLAUDE_MEM_WORKER_HOST ?? DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_HOST,
// AI Provider Configuration
CLAUDE_MEM_PROVIDER: data.CLAUDE_MEM_PROVIDER || DEFAULT_SETTINGS.CLAUDE_MEM_PROVIDER,
CLAUDE_MEM_GEMINI_API_KEY: data.CLAUDE_MEM_GEMINI_API_KEY || DEFAULT_SETTINGS.CLAUDE_MEM_GEMINI_API_KEY,
CLAUDE_MEM_GEMINI_MODEL: data.CLAUDE_MEM_GEMINI_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_GEMINI_MODEL,
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: data.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED || DEFAULT_SETTINGS.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED,
CLAUDE_MEM_PROVIDER: data.CLAUDE_MEM_PROVIDER ?? DEFAULT_SETTINGS.CLAUDE_MEM_PROVIDER,
CLAUDE_MEM_GEMINI_API_KEY: data.CLAUDE_MEM_GEMINI_API_KEY ?? DEFAULT_SETTINGS.CLAUDE_MEM_GEMINI_API_KEY,
CLAUDE_MEM_GEMINI_MODEL: data.CLAUDE_MEM_GEMINI_MODEL ?? DEFAULT_SETTINGS.CLAUDE_MEM_GEMINI_MODEL,
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: data.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED ?? DEFAULT_SETTINGS.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED,
// OpenRouter Configuration
CLAUDE_MEM_OPENROUTER_API_KEY: data.CLAUDE_MEM_OPENROUTER_API_KEY || DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_API_KEY,
CLAUDE_MEM_OPENROUTER_MODEL: data.CLAUDE_MEM_OPENROUTER_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_MODEL,
CLAUDE_MEM_OPENROUTER_SITE_URL: data.CLAUDE_MEM_OPENROUTER_SITE_URL || DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_SITE_URL,
CLAUDE_MEM_OPENROUTER_APP_NAME: data.CLAUDE_MEM_OPENROUTER_APP_NAME || DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_APP_NAME,
CLAUDE_MEM_OPENROUTER_API_KEY: data.CLAUDE_MEM_OPENROUTER_API_KEY ?? DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_API_KEY,
CLAUDE_MEM_OPENROUTER_MODEL: data.CLAUDE_MEM_OPENROUTER_MODEL ?? DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_MODEL,
CLAUDE_MEM_OPENROUTER_SITE_URL: data.CLAUDE_MEM_OPENROUTER_SITE_URL ?? DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_SITE_URL,
CLAUDE_MEM_OPENROUTER_APP_NAME: data.CLAUDE_MEM_OPENROUTER_APP_NAME ?? DEFAULT_SETTINGS.CLAUDE_MEM_OPENROUTER_APP_NAME,
// Token Economics Display
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: data.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: data.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: data.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: data.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
// Display Configuration
CLAUDE_MEM_CONTEXT_FULL_COUNT: data.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: data.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: data.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
CLAUDE_MEM_CONTEXT_FULL_COUNT: data.CLAUDE_MEM_CONTEXT_FULL_COUNT ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: data.CLAUDE_MEM_CONTEXT_FULL_FIELD ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: data.CLAUDE_MEM_CONTEXT_SESSION_COUNT ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: data.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: data.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: data.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: data.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE ?? DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
});
})
.catch(error => {