Back to Projects

The File That Was 269 Characters Too Big

Alex Wilson6 min read
Brass balance scale tipping under a thick stack of manuscript pages

Draft Desk is a writing app I've been building for a few months. The entire pitch, the reason it exists at all, is that the assistant inside it has complete access to your project. Not a chat window you paste chapters into. It reads the manuscript, the character sheets, the world bible, all of it, and answers from that.

Last Monday it told me it couldn't see the world bible.

Not "the file is empty." Not an error. It named the file, told me it was there in the project, and then said it didn't have the contents and could I paste them. Which is a strange and specific kind of wrong, and it took me four commits over two days to actually fix, because the first three were me fixing things that were also broken but were not the problem.

Attempt one: it must be the path

The reference matcher only looked for a folder called reference/ at the project root. My file lived at notes/reference/universe.md. So of course it wasn't picked up.

That was true. It was also a bad design, and I ripped it out rather than patch it: no magic folder, no magic filename. Anything in the project that isn't a draft chapter, a foundation doc, or a system file now gets inlined as reference material, wherever the writer put it. Whatever you add, the assistant sees.

I deployed that, opened the app, and asked again. Same answer. It could not see the bible.

Attempt two: it must be the model

This is the trap, and I walked straight into it. The assistant was telling me, in confident prose, that it had no file access. There is a version of that which is a real problem: the skill runners in Draft Desk genuinely do only see the open document, and I'd written that limitation into the prompt. The model had generalized "a skill sees only the open document" into "I cannot read files," and was cheerfully saying so.

So I rewrote the prompt. Added a section called "What You Can See Directly." Told it that reference material and foundation docs are provided in full below, that if a file's contents appear then it has them, and to never claim it can't see a file or ask me to paste text that's already in the context. Scoped so it only claims access to what's actually rendered.

Good change. Still wrong answer. And that should have been the tell much earlier than it was: I had spent an evening arguing with the narrator instead of checking the tape.

Attempt three: the actual bug

universe.md is 24,269 characters. REFERENCE_BUDGET was 24,000.

The file was larger than the entire budget allotted to reference material. And the assembly loop was the obvious one everybody writes:

for (const doc of docs) {
  if (used + doc.length > BUDGET) break;
  include(doc);
  used += doc.length;
}

If adding this document exceeds the budget, stop. Which means a document that cannot fit is not truncated, it is deleted. Silently, with no note in the prompt, no log line, nothing. And because the docs were sorted small-to-large, the budget was already partly consumed by minor notes by the time the loop reached the one file that mattered, so it never had a chance even in principle.

The assistant was not lying to me. It was accurately reporting what was in front of it. The file name came through in the project tree; the contents never made it into the prompt. It could see that the bible existed and could not read a word of it. That is exactly what it said, and I'd spent two rounds not believing it.

Three changes: raise the budget to 40,000 to match the cap on the other side of the wire, sort reference docs largest-first so the biggest and usually most important file is never the one squeezed out by leftovers, and truncate an individual document to fit rather than dropping it whole. A partial world bible with a note saying it was cut is infinitely more useful than a missing one.

Attempt four: the asymmetry underneath

Fixing that surfaced the real structural flaw, which was worse.

Draft Desk has two paths into the model. Skills run through my Mac Mini worker and send the entire manuscript, every chapter, uncapped. Chat ran through a different assembler that only sent chapters when you had a draft chapter open, and only the newest 24k characters, newest first.

Read that again in terms of consequences. If you had a note open instead of a chapter, chat saw zero chapters. If you had a chapter open, it filled the budget backward from the end, so the first thing dropped was always chapter one. That's why the complaint I kept hearing was so oddly specific: "I don't have chapter one, I'd have to rewrite it." Of course it didn't. Chapter one was structurally guaranteed to be the first casualty of a newest-first budget, on every project, forever.

So the same app had two different answers to "what can you see," depending on which door you came through. Chat now always assembles the manuscript, in reading order, earliest first, with the budget raised to 400k as a guard against pathological size rather than a normal working limit.

And then, because no fixed number is ever actually complete, I gave the assistant a way out: it can emit [READ_FILE: path] for anything it hasn't been shown, and the app fetches it and hands it back mid-turn. Bounded by a step count and a per-request cap so it can't loop forever. Inlining covers real projects with zero round trips. The directive covers everything else.

What I'd take from it

Two things, and neither is about writing apps.

A context budget that drops whole documents is a system that deletes your most important file first, because your most important file is usually your biggest one. Truncate with a note. Never drop in silence. If something didn't fit, say so in the prompt, so the model can tell you what it's missing instead of guessing.

And when the thing reporting the bug is also the thing you're debugging, separate the symptom from its explanation. The assistant's symptom was correct and its explanation was invented, and I treated them as one claim. Two evenings of my life went into fixing the invented half. The measurement I needed the whole time was one wc -c on a file.

24,269 against 24,000. Two hundred and sixty-nine characters over the line.

Share: