deferred=True keeps the four heavy Action columns out of a bulk read, but it
makes narrowness the thing a future column has to remember to ask for -- and
both egress blowouts this project has had were a column nobody remembered.
Listing what each list response renders inverts the default: a new column
costs nothing on these paths until someone adds it to the tuple.
The adventures index was not merely a future risk. It loaded whole Adventure
entities to render a title, a stamp and a snippet, and an Adventure carries
script_state, world_state, placeholders, story_summary, memory, authors_note
and ai_instructions -- ~15 kB a row in production, none of it on that screen,
all of it fetched once per adventure on every index load. Measured on six
adventures with 78 kB of body each: 469.7 kB entity-loaded against 318 B
projected.
The memories drawer stops walking adventure.memories. The walk is what
retrieval used to do and the reason a turn cost megabytes; a relationship load
takes whole entities, so it picks up whatever the model happens to grow.
Nothing changes today -- embedding_blob is already deferred -- which is the
point.
world_delta stays on the action list because ActionOut.world_changes is
computed from it. Leaving it off would not save the bytes, it would spend them
one row at a time as a lazy load.
Two tests cover the index: one asserts the listing query names none of the
body columns, one puts a byte ceiling on six adventures carrying 80 kB apiece.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7