Skip to content

fix(agents): move channel-varying system-prompt sections below the ca…#98285

Closed
LEXES7 wants to merge 1 commit into
openclaw:mainfrom
LEXES7:fix/system-prompt-cache-boundary-leak
Closed

fix(agents): move channel-varying system-prompt sections below the ca…#98285
LEXES7 wants to merge 1 commit into
openclaw:mainfrom
LEXES7:fix/system-prompt-cache-boundary-leak

Conversation

@LEXES7

@LEXES7 LEXES7 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Closes #98261

What Problem This Solves

Fixes a prompt-cache stability bug in buildAgentSystemPrompt() (src/agents/system-prompt.ts). Two channel/identity-varying sections were emitted into the static prefix, above SYSTEM_PROMPT_CACHE_BOUNDARY:

  1. Exec-approval guidance — the /approve instruction differs by channel (CLI/plain /approve vs native approval-card channels like iMessage / Slack / Telegram-with-inline-buttons).
  2. ## Authorized Senders — varies by owner identity, and is dropped entirely in minimal (cron/subagent) mode.

Because their content differs by channel/identity, they fork the supposedly byte-identical cacheable prefix very early. On local models with client-managed prefix caching (llama.cpp / MLX / Ollama), this re-prefills the entire system prompt from scratch on any turn whose channel context differs from the previous one — e.g. an interactive turn right after background cron/heartbeat traffic — which the linked issue measured as multi-minute cold prefills.

This is the direct follow-on to #40256, which relocated Messaging/Voice/Reactions below the boundary but missed these two sections.

Why This Change Was Made

Both sections are instructional context — position-independent for correctness — so they were moved into the volatile suffix below SYSTEM_PROMPT_CACHE_BOUNDARY, mirroring the existing treatment of Messaging/Voice/Reactions. The channel-independent /approve rules stay above the boundary, keeping the stable prefix as large as possible.

One behavioral subtlety is preserved: exec-approval guidance previously lived inside the tool_call_style fallback array, so a provider that overrode tool_call_style suppressed it. The relocated emission keeps that gating (it stays suppressed when providerSectionOverrides.tool_call_style is set). This is therefore a pure cache-stability change with no behavioral difference on any channel.

User Impact

Local-model deployments stop paying repeated cold prefills when the channel context changes between turns: the cacheable prefix is now channel/identity-independent, so it stays warm across interactive/cron/heartbeat traffic. No behavioral change for any channel or provider; cloud providers (server-side caching) are unaffected either way. No configuration changes.

Evidence

Real behavior proof (standalone, non-test)

Drove the real buildAgentSystemPrompt from a standalone script (node --import tsx) for two interactive turns on different channels/owners (CLI vs iMessage), and measured the byte-identical prefix they share — i.e. the region a prefix cache can actually reuse. Run on a minimal workspace, so absolute offsets are smaller than the issue's full ~17.8K-token prompt, but the mechanism and direction are identical:

BEFORE — unpatched main:

CLI prompt:       7217 chars; cache boundary at char 6560
iMessage prompt:  7324 chars; cache boundary at char 6653
shared byte-identical prefix across the two channels: 2020 chars
shared prefix reaches the cache boundary (full prefix cacheable): false
first divergence at char 2020:
  CLI : "send the exact /approve command from \"Reply with:\"; do not ask for another code.\nNever exe"
  iMsg: "use native approval card/buttons first. Include a plain /approve command only when the too"

AFTER — this branch (identical inputs):

CLI prompt:       7217 chars; cache boundary at char 6324
iMessage prompt:  7324 chars; cache boundary at char 6324
shared byte-identical prefix across the two channels: 6850 chars
shared prefix reaches the cache boundary (full prefix cacheable): true
first divergence at char 6850:
  CLI : "send the exact /approve command from \"Reply with:\"; do not ask for another code.\n## Author"
  iMsg: "use native approval card/buttons first. Include a plain /approve command only when the too"

The byte-identical cacheable prefix grows from 2,020 → 6,850 chars and now reaches the boundary (false → true); the first cross-channel divergence (the exec-approval line) moves from char 2,020 (above the boundary) to char 6,850 (below it). The two channels' boundaries also converge to the same offset (6560/6653 → 6324/6324), confirming the prefix is byte-identical.

Regression test

vitest run src/agents/system-prompt.test.ts91 passed (90 existing + 1 new guard).

New guard keeps channel/identity-varying sections below the cache boundary (#98261) asserts both sections sit below the boundary, and that the above-boundary prefix is byte-identical when only the owner identity changes, and when only the approval-UI capability changes. It fails on main and passes here:

× keeps channel/identity-varying sections below the cache boundary (#98261)
  AssertionError: expected 2020 to be greater than 6653

(the exec-approval guidance is at index 2020, above the boundary at 6653, on main).

Sibling suites also pass: system-prompt.memory.test.ts, bootstrap-budget.test.ts, sanitize-for-prompt.test.ts, heartbeat-system-prompt.test.ts → 36 passed. oxlint and oxfmt --check are clean on both files.

Local note: this machine's disk was too constrained for a full tsgo run; the change reuses existing typed helpers with no signature changes, and CI check-prod-types / check-test-types cover typechecking.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Jun 30, 2026
@clawsweeper

clawsweeper Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close this PR as superseded: it implements the same runtime fix as the earlier, still-open and mergeable canonical PR, while that PR has stronger maintainer-facing state with completed CI, sufficient proof, and provider-override regression coverage.

Root-cause cluster
Relationship: superseded
Canonical: #98267
Summary: The earlier PR is the viable canonical landing path for the same prompt-cache fix; this PR duplicates that runtime change with only a small alternate regression-test shape.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Use #98267 as the canonical landing path for #98261, optionally porting this PR's prefix-equality assertion before merge.

So I’m closing this here and keeping the remaining discussion on #98267 and #98261.

Review details

Best possible solution:

Use #98267 as the canonical landing path for #98261, optionally porting this PR's prefix-equality assertion before merge.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main still emits exec-approval guidance and Authorized Senders before SYSTEM_PROMPT_CACHE_BOUNDARY, and both open PRs demonstrate the source-level relocation below the boundary.

Is this the best way to solve the issue?

No for this PR as the landing path. The earlier open PR is mergeable, proof-positive, CI-green, and includes provider-override coverage, so this branch is redundant except for a small test variant.

Security review:

Security review cleared: The diff only reorders existing prompt text and updates a colocated test, with no dependency, workflow, secret, install, auth, or code-execution surface.

AGENTS.md: found and applied where relevant.

What I checked:

Likely related people:

  • steipete: Authored the merged prompt suffix stabilization that moved volatile channel/session prompt sections below the cache boundary and updated the same source, tests, and docs. (role: feature-history contributor; confidence: high; commits: 8f4cbbbe6658; files: src/agents/system-prompt.ts, src/agents/system-prompt.test.ts, docs/concepts/system-prompt.md)
  • vincentkoc: Introduced the system-prompt cache boundary and earlier native approval guidance changes that intersect this PR's cache and exec-approval surfaces. (role: prompt-cache boundary and approval contributor; confidence: high; commits: 64f28906de09, 07c60ae4611d, b73dd9b326b1; files: src/agents/system-prompt.ts, src/agents/system-prompt-cache-boundary.ts, src/agents/system-prompt.test.ts)
  • eleqtrizit: Current blame for the implicated prompt-builder lines points to the recent iMessage authorization PR that recreated and carried the current file shape. (role: recent area carrier; confidence: medium; commits: 587eefe5ad91; files: src/agents/system-prompt.ts, src/agents/system-prompt.test.ts)
  • thewilloftheshadow: Earlier history for the Authorized Senders wording points to this contributor's commit in the same prompt surface. (role: authorized-sender prompt contributor; confidence: medium; commits: 3100b77f12eb; files: src/agents/system-prompt.ts)

Codex review notes: model internal, reasoning high; reviewed against 3811001d2783.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 30, 2026
@steipete

steipete commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Superseded by #98267, now merged on main as 518937be4e9414fdaa4f59954426fe5a760d5f54.

The canonical landing moves both exec-approval guidance and Authorized Senders below SYSTEM_PROMPT_CACHE_BOUNDARY, preserves the provider tool_call_style whole-section override, removes the now-suffix-only inputs from the stable-prefix memo key, and carries the useful byte-equality regression coverage. The final test varies webchat inline-button capability so the approval guidance actually changes, in addition to varying owner identity; both stable prefixes remain byte-identical while the suffixes differ as expected.

Exact-head CI, Blacksmith Testbox/ARM/build-artifacts gates, Dependency Guard, and mandatory autoreview all passed. Full maintainer proof is recorded on #98267: #98267 (comment)

Closing this overlapping implementation in favor of the landed canonical fix. Thanks for independently identifying and proving the cache-boundary issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants