Problem
buildAgentSystemPrompt() places dynamic, per-conversation content (Messaging/inline buttons, Group Chat Context, Reactions) before the large static Project Context block. This invalidates LLM prefix caching on every request where the channel context differs, causing 8-16x slower follow-up turns on local models.
Evidence
Analysis of 49 requests from a local agent (Qwen3-Coder-30B via MLX on Apple M3 Ultra):
- 8 distinct system prompt variants across 47 requests (24,591–26,418 chars)
- First divergence at char ~8,510 — the Messaging section's inline buttons conditional
| Scenario |
Prompt tokens |
Cache rate |
Response time |
| Stable system prompt (cache hit) |
15,000 |
99-100% |
1.0-1.8s |
| Changed system prompt (cache miss) |
15,000 |
10-16% |
10-16s |
The 4 slowest requests (10-16s) all correlate with system prompt changes.
Root cause
In buildAgentSystemPrompt() (src/agents/system-prompt.ts), the prompt is assembled in this order:
- ✅ Static: Identity, Tooling, Tool Call Style, Safety, CLI Reference, Skills, Memory, Self-Update, Model Aliases, Time, Workspace, Docs, Sandbox, User Identity
- ❌ Dynamic (line ~24348): Reply Tags, Messaging section (inline buttons conditional varies by channel), Voice
- ❌ Dynamic (line ~24365): Group Chat Context / extraSystemPrompt
- ❌ Dynamic (line ~24369): Reactions (channel-dependent guidance)
- ✅ Static: Reasoning Format
- ✅ Static: Project Context (workspace files — often the largest block)
- ✅ Static: Silent Replies, Heartbeats, Runtime
The dynamic sections (#2-#4) sit in the middle, causing the entire Project Context block and everything after it to cache-miss whenever the channel context changes.
The specific line causing the most variance (line 24080 in compiled output):
params.inlineButtonsEnabled
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data,style?}]]`..."
: params.runtimeChannel
? `- Inline buttons not enabled for ${params.runtimeChannel}...`
: ""
Suggested fix
Move the dynamic sections (Reply Tags, Messaging, Voice, Group Chat Context, Reactions) to after Runtime — i.e., to the very end of the system prompt. LLM prefix caching matches token sequences from the start, so if the first ~90% of the prompt is always identical, the cached KV state can be reused.
Alternatively, inject dynamic per-conversation context as a separate message (e.g., a user or system message after the main system prompt) so the base system prompt remains 100% stable.
Expected improvement
With a stable ~10K token system prompt prefix:
- Cache hit rate: 84% → ~95%+
- Follow-up turn latency: 10-16s → 1-2s consistently
- Total prompt tokens reprocessed per session: ~800K → ~200K (75% reduction)
Environment
- OpenClaw 2026.3.2
- MLX-LM 0.31.0 with
--prompt-cache-size 20
- Qwen3-Coder-30B-A3B-Instruct-8bit (MoE, 3B active)
- Apple M3 Ultra, 512GB unified memory
- Agent served via Telegram group chat
Notes
This primarily impacts local model deployments (MLX, llama.cpp, Ollama) where prefix caching is client-managed. Cloud providers (Anthropic, OpenAI) handle caching server-side and may be less affected, though the principle still applies.
Ref: rhclaw/pip-workspace#51
Problem
buildAgentSystemPrompt()places dynamic, per-conversation content (Messaging/inline buttons, Group Chat Context, Reactions) before the large static Project Context block. This invalidates LLM prefix caching on every request where the channel context differs, causing 8-16x slower follow-up turns on local models.Evidence
Analysis of 49 requests from a local agent (Qwen3-Coder-30B via MLX on Apple M3 Ultra):
The 4 slowest requests (10-16s) all correlate with system prompt changes.
Root cause
In
buildAgentSystemPrompt()(src/agents/system-prompt.ts), the prompt is assembled in this order:The dynamic sections (#2-#4) sit in the middle, causing the entire Project Context block and everything after it to cache-miss whenever the channel context changes.
The specific line causing the most variance (line 24080 in compiled output):
Suggested fix
Move the dynamic sections (Reply Tags, Messaging, Voice, Group Chat Context, Reactions) to after Runtime — i.e., to the very end of the system prompt. LLM prefix caching matches token sequences from the start, so if the first ~90% of the prompt is always identical, the cached KV state can be reused.
Alternatively, inject dynamic per-conversation context as a separate message (e.g., a
userorsystemmessage after the main system prompt) so the base system prompt remains 100% stable.Expected improvement
With a stable ~10K token system prompt prefix:
Environment
--prompt-cache-size 20Notes
This primarily impacts local model deployments (MLX, llama.cpp, Ollama) where prefix caching is client-managed. Cloud providers (Anthropic, OpenAI) handle caching server-side and may be less affected, though the principle still applies.
Ref: rhclaw/pip-workspace#51