Repository navigation
Conversation
commit: |
Member
|
I wonder if 90% of this would be the same if we didn't denormalize the text
field? Unfortunately we lose text search in that case though.. maybe text
search could be opt-in (similar to making embeddings opt-in). I have a hard
time believing the provider options & usage are very big compared to the
text & reasoning
…On Sun, Nov 8, 2026 at 6:09 PM pkg-pr-new[bot] ***@***.***> wrote:
*pkg-pr-new[bot]* left a comment (convex/agent#453)
<#453 (comment)>
Open in StackBlitz
<https://pkg.pr.new/template/1264283c-67ff-48d9-9556-bf20c8f0e678>
npm i ***@***.******@***.***
*commit: 4749a32 <https://github.com/convex/agent/runs/128940967543>*
—
Reply to this email directly, view it on GitHub
<#453?email_source=notifications&email_token=AACZQW5EBSXUAW7QV2H2N5D5YERL7A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMNZTGA2TINBVGUZKM4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSWGM33PORSXEX3DNRUWG2Y#issuecomment-6730544552>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACZQW3YSSOXOFTG7XVJWLD5YERL7AVCNFSNUABFKJSXA33TNF2G64TZHM4TMMJWHAYTCNBYHNEXG43VMU5TMMRXHA2TKOBSHEZ2C5QC>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
…ads stay whole Text lists read message rows alone, prompt context adds what the model reads, and full reads add result metadata. A byte budget counts what each view reads. Every read path returns what a whole row would, and deletes, updates, clones, finalize and pending replaces leave no stray rows. Saves write no empty sources or warnings, and no parentMessageId. Existing tests read messages through the API instead of raw rows. A payload leaves out the text its row holds, and a model read reads it once. Every message of the bench fixture's thread shapes, and generated messages with several, empty or space-edged text parts between other parts, reads back byte-identical from full, model and by-id reads, clones, updates and rows holding them whole. A row an older extractText wrote reads, clones and updates as its message, and a row whose text no longer matches its message fails to read. The stream recovery write-budget tests size their streams for a message written once.
A message's content moves to messagePayloads, and its provider metadata, usage, sources and warnings to messageRecords, each row present only when it has a field. Text lists read the message row, every context read (prompt context, fetchContextMessages and search) joins the content, and full reads join both, so MessageDoc is unchanged and older rows read as before. Rows record each tier's size, so maximumBytesRead counts what a view joins through convex-helpers' own pagination. Each join is a db.get, one of the 4,096 Convex allows a transaction, so a transaction reads at most 1,024 messages: pages stop there, and getMessagesByIds and search refuse more. A clone batch reads each file once for its refcounts, and stops before the row whose copy would pass a byte or db.get budget. Joins and copies run in bounded batches, under Convex's 1,000 concurrent operations. Rows carry the approval ids their message requests or answers, so an approval reads only its request's content. Updates replace tier rows in place, and every write, a clone included, stores the split shape. listMessages, listUIMessages and agent.listMessages take textOnly. Saves stop writing the deprecated parentMessageId, and empty sources or warnings. A message's row holds its text, so its payload leaves the text parts' text out, with each part's length, and reads put it back. A message whose row holds no text, as when hidden from search, is stored whole. Every write sets a row's text to its message's text, or leaves it unset when hidden, so rows an older extractText wrote come into line when updated or cloned, and a text view of a row saved whole extracts its text from the message.
robelest
force-pushed
the
perf/cheap-message-reads
branch
from
November 9, 2026 22:41
4749a32 to
713f739
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reading messages is often the main Convex cost of an Agent app. Convex bills a read by the whole document (https://docs.convex.dev/production/state/limits) and queries have no projection, so every list, save and context read pays for content, provider metadata, usage and sources it never uses.
message, provider options and metadata, usage, sources, warnings and legacy reasoning move to a newmessagePayloadsrow when they add 1 KiB or more over its text. Every read joins it back, soMessageDocis unchanged and older rows read as before, with no migration.listMessages,listUIMessagesandagent.listMessagestaketextOnly, which returns each non-tool message as its role and text from the row alone.maximumBytesReadcounts each payload a page joins, rows read ahead included, and convex-helpers' own pagination ends, splits and continues the page as it would rows saved whole.parentMessageId, which nothing reads.Bytes read per call, local backend (precompiled 2026-10-22), same seeded threads before and after. Before has no
textOnly, so its text column isexcludeToolMessages.A
useUIMessagesclient subscribed through a 10-step streamed generation (35 KB tool results): 98.6 → 99.1 KB per step, or 46.3 KB withtextOnly(page re-runs 553 → 28 KB; the rest is stream deltas).The 1 KiB threshold keeps plain chat whole. Rows saved before this change stay whole, so reads handle both shapes either way; the threshold only picks one per write. At 512 B, plain chat's text page would drop 38.6 → 20.9 KB, but its full pages and context reads would cost 3% more for apps that never pass
textOnly.Context reads still read every payload: the model needs them. Reading context once per segment and then incrementally is what makes those cheap, and needs nothing more from this storage.