November 8, 2026•3 min read

The answer: The Breakpoint Was on the Wrong Block

Eight measured states of the same loop, from 0.0% to 90.9% cache hits: which fix moved the bill, which two moved nothing, and why the transcript got longer while the cost fell.

Read in Indonesian

Lab repo

The lab lives in folder 0006.prompt-caching

The deliberately broken code, the dataset, and the target live in a public repo. Everything runs in Docker. make check

This is the answer article. If you have not worked lab 6 and still want to try it yourself, close the page first. Everything below is the answer, with the measured numbers.

The interesting part is not the fix. It is that three of the five edits changed no number at all for two stages, and the one that did change everything is a single line nobody would flag in review.

Key takeaways

  • The tools array was the single expensive line. A registry timestamp inside a tool description kept the hit rate at 0.0%; making the definitions constant took it to 78.4%.
  • Moving the clock and the breakpoint alone changed nothing. Both were necessary, neither was sufficient, and the check output says so at every stage.
  • The transcript rewrite was buying 10,029 tokens and costing four times the bill. The fixed loop sends 6% more tokens and pays 34% of the no-cache run.
  • The one-hour write is what survives a ten-minute pause. After the pause, the turn that follows reads 6,135 tokens from cache instead of rewriting them.

Eight states, one loop

Measured with scripts/check.py, on the same machine, without touching data/, scripts/check.py or agent/provider.py:

stage change hit rate bill green
0 as shipped 0.0% $0.4841 0/7
1 the clock leaves the system prompt 0.0% $0.4840 0/7
2 + a breakpoint at the end of the handbook 0.0% $0.4840 0/7
3 + a stable tools array 78.4% $0.1865 2/7
4 + an append-only transcript 80.4% $0.1857 4/7
5 + the date moved into the user turn 80.4% $0.1860 5/7
6 + a one-hour TTL for the handbook 90.9% $0.1437 6/7
7 + the loop reports its own numbers 90.9% $0.1437 7/7

The same requests with caching switched off cost $0.4017 at stage 0 and $0.4218 at stage 7. The second number is bigger because the fixed loop carries its transcripts instead of summarising them.

The tools array, which is where the money was

PYTHON
def tools_for_request() -> list[dict]:
    return TOOLS

Stage 3 is one line, and it takes the hit rate from nothing to most of it. The provider hashes the tool definitions before it hashes the system prompt, so a notes field holding the registry snapshot time invalidates everything below it, on every request, forever. The model never read that field. It was there for a human.

The order matters too. Removing the timestamp from the system prompt at stage 1 was correct and bought nothing, because the tools were still handing the cache a different prefix each turn. A prefix is only as stable as its least stable part, and the tools sit at the top.

The breakpoint, and the date that has to move

A breakpoint does two jobs: it writes an entry at that boundary, and it is the position the next request looks back from. Parking it on the last message means the entry is written at a boundary that the next turn extends, so the next request finds nothing unless the lookback window reaches an older write.

PYTHON
blocks = [Block(role="system", text=system_prompt(), cache=True, ttl=LONG_TTL)]

Two breakpoints end up being the right shape: one at the end of the handbook, kept for an hour, and one at the end of the growing transcript, kept for the default five minutes. The first is what every ticket shares; the second is what saves the previous turn.

The date then has to move rather than disappear. Check C fails if the static prefix changes between two days, and fails again if the date never reaches the model, so the only fix that passes both is to stamp the turn:

PYTHON
history.append({"role": "user", "text": f"{text}\n\n[session date {today(provider.clock)}]"})

Stage 5 is where check C turns green and the bill does not move at all, which is the point. Correctness and cost are separate gates here.

The rewrite nobody counted

Every third turn the loop replaced the transcript with a one-line summary. It saved 10,029 tokens over the run and it made every prefix after the second one useless. The fixed version sends 174,883 tokens against the original 164,854 and pays $0.1437 against $0.4841.

If you need a summary, append it as a new message at the end. The earlier blocks stay untouched, the prefix still matches, and the summary becomes part of the cached history instead of a hole in it.

The lifetime is a bill multiplier, not a detail

With a five-minute default, the customer's ten-minute silence costs one full rewrite of 6,135 tokens per ticket. With the one-hour write at 1.25x on that block only, the pause costs nothing: the turn after it reads 6,135 tokens from cache. The write premium is paid once per hour per handbook, and the handbook does not change.

Note the shape of that decision. The static block gets the expensive, long-lived write; the transcript gets the cheap, short-lived one. One TTL for the whole prompt would be wrong in both directions.

The counter is part of the fix

Check G is the last one for a reason. It runs the loop as a program and compares the line it prints with the ledger:

Output
$ python3 agent/loop.py
summary turns=24 hit_rate=0.909 cost_usd=0.143682

Without that line, stage 0 and stage 7 are indistinguishable from the outside: both answer every ticket, both exit 0, both look fine. A number that the loop reports about itself and a checker re-derives is the difference between a fix and a belief.

Get better at shipping code.

Join the other developers getting tech updates, tips, and relaxed tutorials in their inbox every week.

Stay in the loop

Tech updates, coding tips, and relaxed insights straight to your inbox every week.

No spam. Unsubscribe anytime.

Read next

See all
Struktur DataWhat a Linked List Actually Is

A linked list is a linear chain of nodes, each holding data and a pointer to the next node. Here is what that chain looks like, what it costs next to an array, and the same minimal implementation in Java, Python, and JavaScript.

For developers who learn by struggling

© 2026 Inva.dev.