The watermark that skipped 600 turns
Two components, each behaving exactly as designed, combining into permanent data loss. The trim was capped; the marker was not.
Two components, both correct
A checkpoint sends a session transcript to an extraction model, which returns candidate decisions, constraints, and open questions. Two mechanisms sit in that path.
A reducer, which trims an oversized transcript down to a character cap before it is sent, because a model has a context window and a long session does not fit in it. And a watermark, which records how far through the session we have already extracted, so the next checkpoint resumes rather than re-reading and re-paying for everything.
Both did exactly what they said. The reducer trimmed to its cap. The watermark advanced to the last turn it was handed.
The bug is in the gap between them
The watermark advanced to the last turn fed in, not the last turn actually sent.
On a short session those are the same turn and nothing is wrong. On a session larger than the cap, the reducer drops everything past the limit - and the watermark sails right over the dropped turns and marks them as done. The next checkpoint resumes from beyond them. They are never extracted, and nothing anywhere reports a loss.
A real session on this repository: 1,357 turns, 1.31M characters, against a 700,000-character cap. Roughly half the transcript was discarded, which is on the order of six hundred turns marked as captured that were never sent. The checkpoint reported success.
That is the shape of failure we care about most, and the reason it is worth a post. It is not a crash. There is no error, no retry, no degraded status - just a session that is quietly half-remembered, in the exact product whose entire promise is that it does not forget.
Splitting instead of trimming
The instinct is to move the watermark to the last turn sent. That is necessary and not sufficient - it stops the data loss but leaves the tail permanently unreachable, because every subsequent run trims at the same place.
The actual fix has three parts:
- Split rather than trim. An oversized transcript is chunked into pieces that each fit, instead of having its tail cut off. The cap on the server was removed entirely; chunking replaced it.
- Advance only on success. The watermark moves after a chunk parses, never before. A chunk that fails leaves the marker where it was, so the next run retries it rather than skipping it.
- Stop the client trimming too. The CLI had its own 700,000-character cap doing the same thing one layer up. A session larger than one request is now uploaded across successive runs, and the remainder is reported as pending rather than dropped.
A related failure lived next door and is worth naming, because it is the same class. Failover to a smaller fallback model did not check whether the prompt fit the window it was failing over to - so an oversized prompt was handed to a smaller model and failed there, reporting the wrong cause. The fallback now refuses a window that cannot hold the prompt and says so.
What we took from it
Neither component was wrong in isolation, and neither had a bug you could find by reading it alone. The defect existed only in the seam - in an assumption one made about the other that was true at small sizes and false at large ones.
The generalisable part: a progress marker must never be derived from an input, only from a confirmed output. "How far did I read" and "how far did I successfully process" look identical until something in between silently discards work, and by then the marker has already told you a lie you cannot detect.
Both paths are now covered by tests that fail if the watermark moves over an unsent turn, which is the only version of this fix that stays fixed.