You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
buildAgentSystemPrompt() (src/agents/system-prompt.ts) emits two channel-varying sections into the static prefix, aboveSYSTEM_PROMPT_CACHE_BOUNDARY. Because their content differs by channel (CLI vs iMessage vs cron), they fork the cacheable prefix very early — at ~token 1,460 — invalidating client-side prefix caching for everything after them. On local models this causes the full ~17.8K-token system prompt to be re-prefilled from scratch on any turn where the channel context differs, i.e. 6–16 minute cold prefills for interactive turns following background (cron/heartbeat) traffic.
This is a follow-on to #40256. That issue moved the Messaging / Group Chat / Reactions sections below the boundary (the SYSTEM_PROMPT_CACHE_BOUNDARY mechanism). These two sections were missed by that fix and still sit above the line, so the prefix forks even earlier than #40256's char ~8,510 — at char ~5,854 / token ~1,460.
defined src/agents/system-prompt.ts:258, called :1102 (inside the ## Tool Call Style fallback array, within the static prefix).
Varies by channel: the def (:264–268) branches on inlineButtonsEnabled || hasNativeApprovalPromptRuntimeCapability(...) || isKnownNativeApprovalPromptChannel(runtimeChannel) → the /approve slash-command line (CLI) vs the native-approval-card line (iMessage) differ (same variance class as System prompt section ordering breaks LLM prefix caching for local models #40256's inline-buttons line).
returns ["## Authorized Senders", ownerLine, ""], called :1205 (in the static prefix array).
ownerLine varies by channel/identity, and the whole section is dropped when isMinimal (cron/subagent) — so interactive vs minimal prompts diverge here too.
Both are pushed into linesbeforelines.push(SYSTEM_PROMPT_CACHE_BOUNDARY) (:1236), so they land in the supposedly byte-identical cacheable prefix.
Evidence (local deployment)
Qwen3.5-122B-A10B Q4_K_M on llama.cpp (ROCm, Strix Halo), fronted by llama-swap; OpenClaw 2026.6.10.
Metric
Before (sections above boundary)
After (moved below boundary)
Shared cross-channel prefix
~1,460 tok
~15,486 tok
First message of the morning
7m51s cold
7m49s seed once, then warm
Subsequent interactive turns
minutes (after any cron)
8–16 s warm
13-step booking task
repeated cold prefills
9 turns, 0 cold, ~18 s avg
Measured first divergence
char ~5,854
— (identical to boundary)
First divergence was traced to these two sections via a per-request system-prompt diff (cache-trace.jsonl). Relocating both below SYSTEM_PROMPT_CACHE_BOUNDARY grew the byte-identical prefix from ~1,460 → ~15,486 tokens and eliminated the post-cron cold prefills.
Rendered divergence at the first fork (CLI/cron vs iMessage; owner email redacted):
…First-class tool exists: use it; do not ask user to run equivalent CLI/slash command.
CLI/cron : If exec returns approval-pending, send the exact /approve command from "Reply with:"; do not ask for another code.
iMessage : If exec returns approval-pending, use native approval card/buttons first. Include a plain /approve command …
Second fork (~token 4.5K) — the Authorized Senders section is present for the interactive (iMessage) prompt but absent for isMinimal cron/subagent prompts:
The SYSTEM_PROMPT_CACHE_BOUNDARY design (from #40256) keeps channel-varying content in the volatile suffix so the static prefix stays byte-identical across turns. These two builders predate / were missed by that relocation and still emit into the static region. The boundary fix was applied to Messaging/Voice/Reactions but not to Exec-Approval guidance or Authorized Senders.
Suggested fix
Relocate both sections to afterSYSTEM_PROMPT_CACHE_BOUNDARY (into the volatile suffix), mirroring the existing treatment of Messaging/Voice/Reactions. They are instructional context — position-independent for correctness — so moving them below the line changes no behaviour, only cache stability.
(Exact insertion points to be confirmed against the release tag; the principle is: any builder whose output depends on runtimeChannel, inlineButtonsEnabled, ownerLine, or isMinimal belongs below the boundary.)
Optional hardening
Add a dev-time assertion/test that fails if any section emitted aboveSYSTEM_PROMPT_CACHE_BOUNDARY varies across a representative set of {channel, isMinimal, inlineButtonsEnabled} combinations — so future channel-varying sections can't silently leak above the line again. (This would have caught both #40256 and this issue.)
Primarily impacts local deployments (llama.cpp / MLX / Ollama) where prefix caching is client-managed; cloud providers cache server-side and are less affected, but the principle holds. Workaround in use locally: relocate the two builders below the boundary (lost on each npm update until fixed upstream — hence this report).
Relates to #40256, #43148, #65438.
Problem
buildAgentSystemPrompt()(src/agents/system-prompt.ts) emits two channel-varying sections into the static prefix, aboveSYSTEM_PROMPT_CACHE_BOUNDARY. Because their content differs by channel (CLI vs iMessage vs cron), they fork the cacheable prefix very early — at ~token 1,460 — invalidating client-side prefix caching for everything after them. On local models this causes the full ~17.8K-token system prompt to be re-prefilled from scratch on any turn where the channel context differs, i.e. 6–16 minute cold prefills for interactive turns following background (cron/heartbeat) traffic.This is a follow-on to #40256. That issue moved the Messaging / Group Chat / Reactions sections below the boundary (the
SYSTEM_PROMPT_CACHE_BOUNDARYmechanism). These two sections were missed by that fix and still sit above the line, so the prefix forks even earlier than #40256's char ~8,510 — at char ~5,854 / token ~1,460.The two leaking sections
buildExecApprovalPromptGuidance({ runtimeChannel, inlineButtonsEnabled, runtimeCapabilities })src/agents/system-prompt.ts:258, called:1102(inside the## Tool Call Stylefallback array, within the static prefix).:264–268) branches oninlineButtonsEnabled || hasNativeApprovalPromptRuntimeCapability(...) || isKnownNativeApprovalPromptChannel(runtimeChannel)→ the/approveslash-command line (CLI) vs the native-approval-card line (iMessage) differ (same variance class as System prompt section ordering breaks LLM prefix caching for local models #40256's inline-buttons line).buildUserIdentitySection(ownerLine, isMinimal)["## Authorized Senders", ownerLine, ""], called:1205(in the static prefix array).ownerLinevaries by channel/identity, and the whole section is dropped whenisMinimal(cron/subagent) — so interactive vs minimal prompts diverge here too.Both are pushed into
linesbeforelines.push(SYSTEM_PROMPT_CACHE_BOUNDARY)(:1236), so they land in the supposedly byte-identical cacheable prefix.Evidence (local deployment)
Qwen3.5-122B-A10B Q4_K_M on llama.cpp (ROCm, Strix Halo), fronted by llama-swap; OpenClaw 2026.6.10.
First divergence was traced to these two sections via a per-request system-prompt diff (
cache-trace.jsonl). Relocating both belowSYSTEM_PROMPT_CACHE_BOUNDARYgrew the byte-identical prefix from ~1,460 → ~15,486 tokens and eliminated the post-cron cold prefills.Rendered divergence at the first fork (CLI/cron vs iMessage; owner email redacted):
Second fork (~token 4.5K) — the Authorized Senders section is present for the interactive (iMessage) prompt but absent for
isMinimalcron/subagent prompts:Root cause
The
SYSTEM_PROMPT_CACHE_BOUNDARYdesign (from #40256) keeps channel-varying content in the volatile suffix so the static prefix stays byte-identical across turns. These two builders predate / were missed by that relocation and still emit into the static region. The boundary fix was applied to Messaging/Voice/Reactions but not to Exec-Approval guidance or Authorized Senders.Suggested fix
Relocate both sections to after
SYSTEM_PROMPT_CACHE_BOUNDARY(into the volatile suffix), mirroring the existing treatment of Messaging/Voice/Reactions. They are instructional context — position-independent for correctness — so moving them below the line changes no behaviour, only cache stability.Sketch (
buildAgentSystemPrompt,src/agents/system-prompt.ts):(Exact insertion points to be confirmed against the release tag; the principle is: any builder whose output depends on
runtimeChannel,inlineButtonsEnabled,ownerLine, orisMinimalbelongs below the boundary.)Optional hardening
Add a dev-time assertion/test that fails if any section emitted above
SYSTEM_PROMPT_CACHE_BOUNDARYvaries across a representative set of{channel, isMinimal, inlineButtonsEnabled}combinations — so future channel-varying sections can't silently leak above the line again. (This would have caught both #40256 and this issue.)Expected improvement
Environment
main, 2026-06-30)-c 225280 --parallel 2 --no-kv-unified, q8 KVNotes
Primarily impacts local deployments (llama.cpp / MLX / Ollama) where prefix caching is client-managed; cloud providers cache server-side and are less affected, but the principle holds. Workaround in use locally: relocate the two builders below the boundary (lost on each
npm updateuntil fixed upstream — hence this report).