Skip to content
C.W.K.
Lesson 07 of 08 · published

One Prefix From Every Doorway

~15 min · prompt-caching, master-chief, tool-use, observability

Level 0Visitor
0 XP0/44 lessons0/11 achievements
0/100 XP to next level100 XP to go0% complete

Thirteen Rewrites a Day

Lesson 2 ended with a repair: on 2026-10-18 the Chief's doorway line left the system prompt, because every doorway switch had been re-writing 330K to 415K tokens of cached prompt. The repair was right, and it was not enough. On 2026-10-22 a review of the Master Chief (Coop todo #597) decrypted the Chief's ground-truth log and counted about thirteen turns a day whose first request wrote 280K to 500K tokens to the cache again. A rewrite pays the cache-write price on the whole prompt instead of the read price, and Dad waits for that write before the first word of the answer. Most of the thirteen had the same shape as the bug lesson 2 fixed: the turn came through a different doorway than the turn before.

The review had to find that out the hard way. Nothing recorded what a turn's prefix had been, so it named the causes by decrypting the log and reasoning backwards from the bill.

Five Ways the Doorway Still Reached the Prefix

A Claude request begins with the tools it advertises, then the system prompt, then the history replay, and the provider cache matches that prefix byte for byte. The origin line was gone from it. Five other things still followed the doorway into it:

What differedHow
The tool listThe WebUI sent its per-brain toggle list, the Pippa app the whole surface with Bash, Telegram the whole surface without it. The function that fixed it names this 10 of the 41 rewrites the review measured.
An app doorway's hostA turn from inside a sibling app added that app's host tools to the tool block and its rules to the system prompt.
A client's own addendumJoined the system prompt for that client's turns only.
The composer's effortA WebUI turn sent at medium named medium in the vessel meta, the first lines of the system prompt.
Referenced artifactsAppended to the system field for the one turn that named them, so the next turn wrote the prefix again.

None of the five was a mistake on its own. Each doorway had a reason to send what it sent. The mistake was that a conversation shared by every doorway let each one decide its own prefix.

One Posture, One System Prompt

The fix, landed the same day (commit a5605c92, review finding M1), makes the prefix a property of the Chief instead of the doorway:

  • One tool list. chief_tool_posture gives every Chief turn the Chief's whole surface: tools on, Bash on, no allowlist, with the app host tools added from every doorway. A doorway's per-tool toggles no longer narrow the Chief; the Pippa app never sent them, so they had never held there either. A turn with tools switched off entirely keeps them off.
  • Restrictions moved to the call. A doorway's explicit Bash switch (Telegram's /bash, off by default) still holds, but as a CallGate refused when the model calls Bash, not as a shorter list. The Claude adapter's PreToolUse hook and every other vessel's tool bridge ask call_refusal, and the refusal tells the model what to do instead.
  • One system prompt. It carries the Chief addendum alone. A client's addendum, the app doorway's rules and a Coop escalation's brief become turn notes (TurnState.notes), rendered after the origin line in the turn's own context.
  • Standing effort. In a Chief turn the vessel meta names the vessel's standing reasoning level (chief_reasoning); when the turn runs at another level, a sentence in its context says so, and any action record the turn writes carries the real level. That takes the level out of the system prompt's text, not out of the request: the real level still goes to the Claude SDK as the turn's effort setting, and the provider's cache treats a changed effort as a changed prompt from the history on (on some models from the very start). So a WebUI turn at medium between turns at high still writes the replayed history again.
  • Artifacts on the message. Referenced artifacts ride the current message in every conversation, Chief or not.

Look at what the second bullet trades. The model now sees a tool that this doorway will refuse, and learns that only when it tries. That costs a refused call now and then. A shorter list cost a full rewrite of a prompt of several hundred thousand tokens on every switch. Advertising is part of the prefix; refusing is not.

Measured, Not Guessed

The other half of the work (finding m6, 51b5cdd3) made the next review unnecessary. services/prompt_cache_watch.py fingerprints every Claude turn's prefix: hashes of the tool block (with the MCP servers behind it) and of the system prompt, plus the model, effort, thinking and fast mode. The fingerprint rides the turn's first pippa_usage event in the log. When a first request writes 50K tokens or more, the watcher compares the fingerprint with the same conversation's previous turn and names the cause on the [turn-provider] line:

  • rewrite=tools,system (or model, effort, thinking, fast): that part of the prefix changed;
  • ttl: nothing changed, and the previous turn is more than an hour old, so the cache entry had expired;
  • replay: nothing changed within the hour, so the history itself moved (a compaction, an edit, a healed row);
  • first-seen: no earlier turn of this conversation since the server started.

Admin → Health lists today's rewrites by cause and conversation. The counts live in memory, so a restart starts them again, and the Health line says that too.

What Still Rewrites, by Design

One prefix does not mean a prefix that never moves. The design doc lists what still rewrites on purpose: the first turn after an hour's quiet (the cache entry's life), a compaction, a vault file edit inside the prompt (the engine rebuilds within five minutes), a different model, a thinking switch, a look tool or On Air effects added to one turn, and the rare spoken turn whose voice is not Dad's, which loses its acting tools. One more belongs there and is missing from the list: a turn at a different effort from the turn before, since the real effort still rides the request. Each of these changes what the turn is or what it can do. A doorway switch on its own changes neither, which is why it no longer may; a doorway that also sends another effort still pays for the effort.

In a shared conversation, the prefix belongs to the conversation. Anything a doorway or a turn needs to say goes after the cache breakpoint, in the turn's own context. Anything a doorway must forbid is refused at the call. And every turn records what its prefix was, so the next rewrite names its own cause.

Code

verbatim, services/master_chief.py, as of 2026-10-27: every Chief turn gets the Chief's surface, and a doorway's Bash switch becomes a gate at the call·python
def chief_tool_posture(payload: dict) -> CallGate | None:
    """Give a Chief turn the Chief's own tool surface; return what the
    doorway's switches still hold back, as a call gate.

    Each doorway sent its own tool wire (the WebUI its per-brain toggle list,
    the Pippa app the whole surface with Bash, Telegram the whole surface
    without it), so the tool block, the first bytes of every Claude request,
    changed with the doorway, and every switch between devices re-wrote the
    Chief's whole cached prompt (10 of the 41 rewrites the review measured).
    The Chief now advertises one list from every doorway: the whole surface,
    Bash included, with the app's host tools (``merge_sidekick_tool_allowlist``).
    A doorway's per-tool toggles do not narrow the Chief (the app never sent
    them, so they never held there either); a doorway's explicit Bash switch
    (Telegram's ``/bash``, off by default) still holds, at the call. A turn
    with tools switched off entirely keeps that: no gate, no change."""
    from services.tool_policy import TOOL_BASH

    if payload.get("tools_enabled") is False:
        return None
    bash_off = payload.get("bash_enabled") is not True
    payload.update(tools_enabled=True, bash_enabled=True, tool_allowlist=None)
    if not bash_off:
        return None
    where = payload.get("source") or (payload.get("host_context") or {}).get("origin_surface") or "this doorway"
    return CallGate(denied=frozenset({TOOL_BASH}),
                    reason=f"Bash is switched off for turns from {where}; answer without it, or tell Dad that "
                           "this needs Bash switched on there.")
A minimal sketch you can run: the same six turns with a prefix per doorway, then with one prefix, each rewrite named the way the watcher names it (stdlib, Python 3.9+)·python
"""A minimal sketch: fingerprint each turn's prefix, and name what changed when a turn writes the cache again."""
import hashlib
import json

REWRITE_TOKENS = 50_000  # a first request writing this much rewrote the prompt
CACHE_TTL = 3600         # the cache entry's life, in seconds
PARTS = ("tools", "system", "effort")


def sha(value) -> str:
    return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()[:12]


def fingerprint(tools, system, effort):
    return {"tools": sha(tools), "system": sha(system), "effort": effort}


def cause(previous, current, written, quiet_seconds):
    if written < REWRITE_TOKENS:
        return None  # read from the cache; only the newest exchange was written
    if previous is None:
        return "first-seen"
    changed = [part for part in PARTS if previous[part] != current[part]]
    if changed:
        return ",".join(changed)
    return "ttl" if quiet_seconds > CACHE_TTL else "replay"


SYSTEM = "Identity, shared rules, the Chief addendum."
SURFACE = ["coop", "vestige_memory", "web_search", "Bash"]


def per_doorway(doorway):
    """Before: each doorway sent its own wire, and the app its rules too."""
    tools = {"webui": ["coop", "web_search"], "app": SURFACE, "telegram": SURFACE[:-1]}[doorway]
    system = SYSTEM + ("\n(the app's host rules)" if doorway == "app" else "")
    effort = "medium" if doorway == "webui" else "high"
    return fingerprint(tools, system, effort)


def one_prefix(doorway):
    """After: one posture and one system prompt. Telegram's Bash switch is a
    gate refused at the call, and the app's rules ride the turn's own context.
    The turn's effort still rides the request, so a WebUI turn at medium
    still rewrites."""
    effort = "medium" if doorway == "webui" else "high"
    return fingerprint(SURFACE, SYSTEM, effort)


DOORWAYS = ["app", "app", "webui", "telegram", "telegram", "app"]
for label, build in (("prefix per doorway", per_doorway), ("one prefix", one_prefix)):
    print(f"--- {label}")
    previous = None
    for doorway in DOORWAYS:
        current = build(doorway)
        moved = previous is None or any(previous[p] != current[p] for p in PARTS)
        written = 400_000 if moved else 3_000  # a rewrite, or only the newest exchange
        print(f"  {doorway:9} wrote {written:>7,} tokens  rewrite={cause(previous, current, written, 120)}")
        previous = current

External links

Exercise

If one conversation in your app is reached from more than one client (a web page, a phone app, a bot), log a fingerprint of each request's prefix with the client's name: a hash of the tool definitions, a hash of the system prompt, the model and any effort setting. Run it for a day and list every client switch that changed the fingerprint. For each difference decide where it belongs: in the prefix for every client, in the newest message for one turn, or in a refusal at the moment the model calls a tool it may not use from that client.
Hint
Hash the tools exactly as you send them, in the order you send them, not sorted and not as you think of them: a reordered list is a different prefix to the provider. Pippa's own watcher sorts tool names before hashing, so an order-only change would read to it as replay; the sketch hashes the list as sent. Count a rewrite only when the provider's own numbers say so (Anthropic reports cache_creation_input_tokens on each request), and compare against the same conversation's previous turn, not the previous request in the whole app.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.