Skip to content

System prompt: Exec-Approval guidance + Authorized Senders sit above SYSTEM_PROMPT_CACHE_BOUNDARY, forking the cacheable prefix at ~token 1,460 #98261

Description

@headbouyJB

Relates to #40256, #43148, #65438.

Problem

buildAgentSystemPrompt() (src/agents/system-prompt.ts) emits two channel-varying sections into the static prefix, above SYSTEM_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.

The two leaking sections

  1. Exec-approval guidancebuildExecApprovalPromptGuidance({ runtimeChannel, inlineButtonsEnabled, runtimeCapabilities })
    • 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).
  2. Authorized SendersbuildUserIdentitySection(ownerLine, isMinimal)
    • 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 lines before lines.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:

 iMessage : ## Authorized Senders\nAuthorized senders: <owner-email>. …
 cron     : (section absent — isMinimal)

Root cause

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 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):

   // ── static, cacheable prefix ───────────────────────────────
-  ... buildExecApprovalPromptGuidance({ runtimeChannel, inlineButtonsEnabled, runtimeCapabilities })  // :1102
   ...
-  ... buildUserIdentitySection(ownerLine, isMinimal)                                                  // :1205
   ...
   lines.push(SYSTEM_PROMPT_CACHE_BOUNDARY);                                                            // :1236
+  // ── volatile suffix (below the boundary; channel-varying) ──
+  lines.push(... buildExecApprovalPromptGuidance({ runtimeChannel, inlineButtonsEnabled, ... }))
+  lines.push(... buildUserIdentitySection(ownerLine, isMinimal))
   return lines.filter(Boolean).join("\n");

(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 above SYSTEM_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.)

Expected improvement

  • Shared cacheable prefix: ~1,460 → ~15K+ tokens (channel-independent).
  • Post-cron interactive latency on local models: minutes → seconds.
  • No behavioural change; pure cache-stability fix.

Environment

  • OpenClaw 2026.6.10 (bug also present on main, 2026-06-30)
  • llama.cpp (ROCm), Qwen3.5-122B-A10B Q4_K_M, llama-swap front, -c 225280 --parallel 2 --no-kv-unified, q8 KV
  • Single-user, multi-channel (iMessage interactive + ~7 cron/heartbeat jobs)

Notes

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExclude from stale automation

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions