The Client Builds the Context. The Server Decides What It Is Allowed to Say.

Tim Shen · October 18, 2026 · 5 min read

Every AI product ends up with the same machine somewhere in it: gather what the model needs to know, trim it to fit, turn it into a prompt. Ours is called the context packet, and it runs on every chat message, every inline suggestion, every outline pass.

The part worth writing about is not what we put in it. It is the line we had to draw through the middle of it — because the packet is built in the browser, and a prompt assembled from client-supplied text is a prompt the client can write.

The shape

Four steps, two of them on each side of the wire.

EditorV2 (browser)
  → buildEditorV2RawContext()    everything the editor has in hand
  → applyProfile(raw)            trim to one of four budgets    → ContextPacket
  ─────────────────────────── the wire ───────────────────────────
  → enrichPacketWithFacts()      the accepted fact ledger, from the database
  → loadWorldCanonSlice()        established world state, folded to this chapter
  → buildPromptFromPacket()      the system prompt

The client half is a pure function over data the editor already holds: the chapter, the entities it mentions, the style sheet, recent timeline events, open Guardian notes. No database, no secrets, nothing the user could not already read on their own screen.

The server half is the two things the client is not allowed to supply.

Why those two things cross the wire the other way

The fact ledger is the set of claims about the story the writer has accepted — Mara's eye colour, when the war started — folded forward to the chapter in hand. The world canon is the same idea for state the prose established rather than the writer typed.

Both are database reads, so the client cannot produce them. But that is the weaker reason. The real one is in a comment on the profile config:

The canon text itself is deliberately NOT on the packet. The fold is a database read, and the packet is untrusted input — a client-supplied "established world state" would be the client putting words in the model's mouth. Only the budget rides the wire, and the loader clamps it.

That is the whole boundary in one sentence. The packet carries a number — how many characters of world state this profile may receive — and the server decides what fills it. A modified packet can ask for a bigger section; it cannot decide what the section says.

The fact enrichment has the same note, for the same reason: "this is the one place the ledger enters, and it enters from the database."

Folded to this chapter, never the whole book

Both server-side reads are temporal. A request from chapter five sees chapter-five state.

That sounds like an optimisation and is not. If the fold ignored position, a model answering a chapter-five question would cheerfully reason from facts chapter forty establishes — a spoiler the writer has not written yet, presented as something their own story already said.

Four budgets, because one size is wrong four different ways

  • inline (a sentence-level suggestion) — 1,500 tokens, 8,000 chars of chapter, no world state. It has to come back before the writer's hand moves.
  • outline — 6,000 tokens, 8,000 chars, no world state.
  • chat — 26,000 tokens, 40,000 chars, 2,000 chars of world state.
  • book (whole-manuscript work) — 32,000 tokens, 40,000 chars, 3,000 chars of world state.

The 26,000 is not a round number someone liked. It is the smallest budget that lets a 40,000-character chapter survive the prompt builder's own arithmetic, and the comment shows the work: at four characters per token that is 104,000 characters, minus 600 of fixed overhead, minus the 1,132-character retrieval reserve the chat route passes, minus the rules cap and the outline cap, leaving 97,168 — of which the knowledge section takes 60%. It was 8,000 before, which quietly capped that section near 18k and truncated long chapters nobody had complained about yet.

Budgets derived from the thing they have to fit are maintainable. Budgets that are round numbers are not, because the next person cannot tell which constraint they were respecting.

Everything degrades to less context, never to an error

Both server-side steps are best-effort, and the comment in the chat route says why better than I can:

A fact ledger is an enrichment, and a writer mid-sentence would rather have an answer without the chronicle than an error with it.

So the fact enrichment is wrapped and logs on failure. The world canon returns null rather than throwing, and it has four separate ways to do so: the deployment has not opted in, the book has no world log, the fold failed, or it took longer than 1,200 ms. All four are treated identically by the caller, as today's behaviour rather than as an error state.

There is one more case, and it is the one that is easy to get wrong: in agent mode the system prompt never carries the world-state section at all. So the route skips the fold entirely — not to save the queries, but because the "Context sent" disclosure we show the writer would otherwise claim a world state the model never saw.

It is off by default anyway

The world canon ships behind an environment variable that is not set.

Not because it is unfinished, but because the world log is unreviewed LLM extraction, and our rule is that unreviewed facts do not steer generation. The same reason three of our four theory-of-mind categories are still dark. The section is built, measured, disclosed, and switched off until the facts behind it carry a review state.

Which is the other half of a trust boundary. The first half is "the client cannot put words in the model's mouth." The second is that we cannot either, just because the words came out of our own extractor.