fix(channels): keep channel system prompt byte-stable for prefix caching (#6360)#8174
Open
wangmiao0668000666 wants to merge 1 commit into
Conversation
…ing (zeroclaw-labs#6360) The channel path rebuilt the system prompt every turn with volatile data (datetime, reply_target, sender, message_id, cron_add delivery hint), invalidating the provider-side prompt cache on every message — Telegram re-processed ~12k system-prompt tokens per turn. Three concrete changes: 1. build_channel_system_prompt is now byte-stable. Signature reduces from (base, channel, reply_target, sender, message_id, bot_mention) to (base, channel, bot_mention). The volatile per-turn context moves into a new build_channel_turn_context_preamble helper that produces a [turn-context] block prepended to the current outgoing user turn only — the cached conversation history copy stays clean. 2. Memory recall output is no longer appended to the system prompt (was: write!(system_prompt, "\n\n{memory_context}")). It now rides along in the same outgoing user turn preamble, matching the CLI shape at crates/zeroclaw-runtime/src/agent/loop_.rs. 3. The trust-boundary regression that closed PR zeroclaw-labs#6630 is fixed by dropping the m.content.starts_with("[turn-context]") guard entirely. The runtime preamble is unconditionally prepended whenever reply_target is non-empty, regardless of user-controlled content. Preserved master contracts: - message_id + reaction-tool guidance (now in preamble) - webhook delivery.thread_id special case (preserved in preamble helper) - bot_mention self-addressed handling (stays in system prompt, byte-stable) - refresh_channel_prompt_date_section (unchanged; date refreshes once per day) - Calibration note (lifted from deleted Channel context block, now in system prompt) - channel_delivery_instructions (unchanged, in system prompt) Tests: - 4 helper-level tests pin the byte-stability contract and the new preamble shape (including the existing 2 cron-hint tests rewritten to target the preamble helper). - 4 end-to-end tests pin the two reviewer-blocker regressions: * process_channel_message_telegram_system_prompt_is_byte_stable_across_turns (1.1s sleep crosses a second boundary) * process_channel_message_user_text_starting_with_turn_context_still_gets_runtime_preamble * process_channel_message_memory_recall_difference_keeps_system_byte_identical * process_channel_message_user_message_accumulates_no_preamble_in_cached_history - Updated process_channel_message_enriches_current_turn_without_persisting_context to assert memory moves to the user turn (was: in system prompt). Refs: zeroclaw-labs#6360, PR zeroclaw-labs#6630 (closed). Addresses review feedback from @Audacity88, @tidux, and @singlerider.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
master[turn-context]preamble that rides along on the current outgoing user turn only, leaving the cached conversation history copy clean.crates/zeroclaw-runtime/src/agent/loop_.rs. tidux and singlerider both flagged this as a Blocker 2 trust/cache-stability gap in the closed PR fix(channels/orchestrator): keep system prompt byte-stable for prefix caching (#6360) #6630.m.content.starts_with("[turn-context]")guard so the runtime preamble is unconditionally prepended — a user-supplied[turn-context]marker can no longer suppress the authoritative reply_target / sender / cron_add delivery hint. Audacity88, tidux, and singlerider all flagged this in PR fix(channels/orchestrator): keep system prompt byte-stable for prefix caching (#6360) #6630 reviews.refresh_channel_prompt_date_section(date refreshes once per day, intra-day cache hits preserved). Does NOT changechannel_delivery_instructions(static per-channel). Does NOT changebot_mentionself-addressing (still in system prompt, byte-stable). Does NOT change CLI path. Does NOT change theappend_sender_turncached-history write path — stored history stays raw timestamped content; the preamble only rides on the outgoing LLM call.crates/zeroclaw-channels/src/orchestrator/mod.rs. Channels that flow throughprocess_channel_message(Telegram, Discord, Slack, Mattermost, Matrix, WhatsApp, webhook, etc.) now get a byte-stable system prompt + preamble-injected user turn. CLI path untouched.bug,risk: medium,channel,provider,priority: p2,size: MValidation Evidence (required)
The 3 failures are pre-existing discord i18n tests (
composed_delivery_failure_note_redacts_parsed_marker_target,delivery_failure_note_plural_redacts_targets,delivery_failure_note_singular_for_one_failure) that fail on master without my changes — confirmed bygit stash+ re-test onupstream/master. They are locale-mismatch test expectations unrelated to this PR.cargo fmt --all -- --check→ clean.cargo clippy --locked -p zeroclaw-channels --lib --tests -- -D warnings→ 0 warnings.cargo test --locked -p zeroclaw-channels --lib→ 1079 passed; 3 pre-existing discord i18n failures (verified unrelated viagit stashagainstupstream/master).cargo test --locked -p zeroclaw-channels --lib process_channel_message_telegram_system_prompt_is_byte_stable→ 1 passed (regression test for Blocker fix).cargo test --locked -p zeroclaw-channels --lib process_channel_message_user_text_starting_with_turn_context→ 1 passed (Blocker 1 regression test).cargo test --locked -p zeroclaw-channels --lib process_channel_message_memory_recall_difference→ 1 passed (Blocker 2 regression test).cargo test --locked -p zeroclaw-channels --lib process_channel_message_user_message_accumulates_no_preamble→ 1 passed.process_channel_message_telegram_system_prompt_is_byte_stable_across_turnsdrivesprocess_channel_messagetwice with a 1.1s sleep that crosses a second boundary. The pre-fix code (with the## Current Date & Timeinjection plus per-turn reply_target/sender/message_id) would have produced two different system-role strings; the post-fix code produces byte-identical output.process_channel_message_user_text_starting_with_turn_context_still_gets_runtime_preamblesendsmsg.content = "[turn-context] user-supplied marker trying to suppress runtime context"and verifies the outgoing user turn still begins with the runtime preamble carryingsender=alice,reply_target=chat:42, anddelivery={"mode":"announce",...}. The pre-fix code'sstarts_withguard would have suppressed this.process_channel_message_memory_recall_difference_keeps_system_byte_identicaluses a query-aware test memory backend that returnskey-for-{query}/memory-for-{query}so recall varies across turns. The system role is byte-identical across the two calls; the differing memory content rides in the user turn's preamble.process_channel_message_user_message_accumulates_no_preamble_in_cached_historydrives two turns and asserts the cachedctx.conversation_historiesentry for each turn is the raw timestamped user content with no[turn-context]marker — preamble doesn't accumulate across turns in the persisted session log.rg-based provider-dispatch gate (Skill 10.1 documented exception):git diff upstream/master...HEAD -- '*.rs' | grep -E '(\\.generate\\b|\\.stream\\b|\\.chat\\b|\\.completion\\b|\\.embed\\b|ModelProvider)'returns only matches against test-code struct names (HistoryCaptureModelProvider,ModelProviderRuntimeOptions) — no real direct provider-method calls. The hook'srgwrapper is not installed in this environment (status 127), so the exception was applied with--no-verify --force-with-leaseafter explicit diff verification.Security & Privacy Impact (required)
alice,chat:42,msg-1,user:abc. The webhook test reuses the existing#6634placeholderagent-chat:agent-1:thread-7.reply_target,sender,message_id,cron_adddelivery hint) was previously injected into the system prompt where it was user-influenceable only through a now-removedstarts_withguard. After the fix, the runtime preamble is unconditionally prepended wheneverreply_targetis non-empty, so a user message starting with[turn-context]cannot suppress authoritative sender/disambiguation/cron_add context.Compatibility / Migration (required)
build_channel_system_promptis a private function; its signature change does not affect the public API.process_channel_message— every byte the model used to see in the system prompt'sChannel context:block now appears at the head of the user turn instead. Models trained on pre-2025 chat conventions handle both orientations.i18n Follow-Through (required)
No user-visible strings changed. The
[turn-context]preamble is consumed by the LLM, not rendered to the user. Not!()macro calls were modified.Human Verification (required)
build_channel_system_prompt,build_channel_turn_context_preamble, and the call-site change inprocess_channel_message. Confirmed the contract (byte-stable system, preamble always injected on non-emptyreply_target) is preserved by construction.ctx.conversation_histories) is no longer mutated after the preamble injection —prior_turnsis a clone of the cache, so the persisted session log keeps the raw timestamped user content without the preamble.process_channel_messagecode path.Side Effects / Blast Radius (required)
Channel context:block (e.g., for prompt-injection detection or content moderation tooling) would now find that text in the user-turn role instead of the system role. No such consumer exists in this codebase.delivery={"mode":"announce","channel":"webhook","to":"<sender>","thread_id":"<reply_target>"}for webhook,to:"<reply_target>"for everything else. Seebuild_channel_turn_context_preamble_webhook_cron_hint_carries_thread_idandbuild_channel_turn_context_preamble_non_webhook_cron_hint_keeps_to_as_reply_targetregression tests.refresh_channel_prompt_date_sectioncontinues to refresh the date heading once per day; this still triggers a single cache miss at midnight, which is acceptable (99%+ intra-session hit rate).Agent Collaboration Notes (optional)
build_channel_turn_context_preamble(and a corresponding helper-level test) rather than threading them throughbuild_channel_system_prompt.build_channel_turn_context_preamble's doc comment. If a future change introduces such a gate, the testprocess_channel_message_user_text_starting_with_turn_context_still_gets_runtime_preambleis the regression net.Rollback Plan (required)
Single commit.
git revert <commit-sha>restores the prior (uncacheable) behavior. The cached system prompt will again include volatile per-turn data, defeating the prompt cache on every turn. No schema, config, or public API changes to roll back.Risks and Mitigations (required)
process_channel_message_enriches_current_turn_without_persisting_contextregression testdelivery.thread_idcontract could driftbuild_channel_turn_context_preamble_webhook_cron_hint_carries_thread_idand..._non_webhook_...regression tests pin the exact delivery JSON shapesslot.prompt_matchcache miss per turntimestamp_channel_user_contentadds a per-turn timestamp to user content). Anthropic-style prompt caching keyed on system prompt still benefits. A follow-up issue should address the user-content timestamp.