Commit b7e86f2
fix(webapp,core,sdk): page chat history and keep model context private
# Chat history reads a page at a time, and the model's context stays
private
Reading a `chat.agent` conversation's history used to download and parse
the whole conversation, so
it got slower as a chat grew. A conversation's stored transcript now
carries an index, so reading
the most recent messages fetches only those messages.
The same change closes a narrower problem: the model-side context an
agent keeps, its compacted
history and any injected context, was reachable from a browser. It is
not part of a transcript and
is no longer served with one.
## Before
```mermaid
sequenceDiagram
participant Browser
participant Webapp
participant Store as Object store
Browser->>Webapp: open a chat
Webapp-->>Browser: presigned URL for the whole snapshot
Browser->>Store: GET the entire object
Store-->>Browser: messages AND model context
Note over Browser,Store: the whole conversation, including context a transcript never shows
Webapp->>Store: GET the entire object (page read)
Webapp->>Webapp: parse all of it, return 50 messages
```
Every read paid for the whole conversation: request-thread CPU for the
API, bytes over the wire for
the browser. Paging alone could not fix it, because the messages a page
needs sit at the end of one
JSON document, so finding them meant parsing all of it.
## After
```mermaid
sequenceDiagram
participant Browser
participant Webapp
participant Store as Object store
Browser->>Webapp: open a chat
Webapp->>Store: ranged GET, end of the object
Store-->>Webapp: index + newest entries
Webapp-->>Browser: messages and a cursor
Note over Webapp,Store: one ranged read, and the browser never touches the store
```
The stored transcript is a header line holding the cursors and the
agent's own state, one line per
message, an index, and a fixed-width trailer giving the index's length.
A reader takes the end of
the object and decodes only the bytes holding the page it was asked for.
## Safety contract
- The private state lives in the header, so a page read cannot return it
by layout, not by
remembering to strip a field.
- A cursor the transcript no longer holds yields an empty page, never
the newest entries, which
would present recent messages as older ones.
- Page timestamps come from a message's position in the whole
transcript, not in its page, so pages
fetched newest-first still merge into conversation order.
- Trimming is expressed relative to the compaction watermark, because
everything after it is live
context the next boot converts and must never be dropped.
## Scope and impact
A conversation saved by an earlier version still reads correctly: the
reader recognises the old
format and reads the whole object. Each conversation moves to the new
layout the next time it
saves, so there is no migration step. An older reader cannot read the
new format, so roll forward
rather than back.
The built-in storage is deliberately basic about long conversations:
once an agent has compacted it
keeps roughly the last hundred messages and drops the rest, so what it
rewrites each turn stops
growing. A conversation that never compacts is kept whole. An app that
renders history further back
than that keeps its own transcript storage.
## Rollout controls
No flag. The read change is the same data through a cheaper path, and
gating the exposure fix would
mean leaving it open by default. Revert is a deploy revert: stored
objects are untouched and the
state is still written and read on the private path. Presigned URLs
already issued stay valid for
their lifetime, so the exposure fix is not retroactive for the few
minutes before a deploy.
What a regression looks like: `chat.agent: snapshot version/shape
mismatch` at run boot, or
`transcript endpoint: ranged read failed` in the webapp logs. The first
would mean a conversation a
later run cannot read, which is the one worth paging on.
## Verification
Tests prove: paging over byte offsets, including multibyte content where
a character-indexed table
would slice mid-message; reading the previous format; a conversation
whose stored media type
disagrees with its contents; a cursor the transcript no longer holds;
each page's position in the
whole transcript; that no byte a page read fetches contains the private
state; that a trimmed
transcript still restores the model's context; and that a watermark
outside the retained window
does not discard the summary. One test asserts the failure that guard
prevents, so it cannot be
dropped quietly. Real runs additionally cover a cold continuation
booting from a trimmed snapshot
and snapshot size plateauing over 130 turns.
Mono-RevId: 7e57e5e9b859592827719a35e6a624c1cd7123f61 parent c5491f0 commit b7e86f2
25 files changed
Lines changed: 1805 additions & 226 deletions
File tree
- .changeset
- apps/webapp
- app
- components/runs/v3/agent
- presenters/v3
- routes
- _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam
- services/realtime
- v3
- test
- docs/ai-chat
- internal-packages/testcontainers/src
- packages
- core
- src/v3
- schemas
- sessionStreams
- test
- trigger-sdk
- src/v3
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | | - | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
37 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | | - | |
| 87 | + | |
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
| 94 | + | |
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
115 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
116 | 125 | | |
117 | 126 | | |
118 | 127 | | |
| |||
234 | 243 | | |
235 | 244 | | |
236 | 245 | | |
237 | | - | |
| 246 | + | |
238 | 247 | | |
239 | 248 | | |
240 | 249 | | |
241 | 250 | | |
242 | 251 | | |
243 | 252 | | |
244 | 253 | | |
245 | | - | |
246 | | - | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
247 | 261 | | |
248 | 262 | | |
249 | 263 | | |
250 | 264 | | |
251 | 265 | | |
252 | 266 | | |
253 | | - | |
254 | | - | |
255 | | - | |
256 | | - | |
257 | | - | |
258 | | - | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
263 | 275 | | |
264 | | - | |
265 | | - | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
266 | 286 | | |
267 | 287 | | |
268 | 288 | | |
| |||
386 | 406 | | |
387 | 407 | | |
388 | 408 | | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
396 | | - | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
401 | | - | |
402 | | - | |
403 | | - | |
404 | | - | |
405 | | - | |
406 | | - | |
407 | | - | |
408 | | - | |
409 | | - | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
410 | 417 | | |
| 418 | + | |
| 419 | + | |
411 | 420 | | |
412 | 421 | | |
413 | 422 | | |
| |||
449 | 458 | | |
450 | 459 | | |
451 | 460 | | |
452 | | - | |
| 461 | + | |
453 | 462 | | |
454 | 463 | | |
455 | 464 | | |
| |||
656 | 665 | | |
657 | 666 | | |
658 | 667 | | |
659 | | - | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
660 | 701 | | |
661 | 702 | | |
662 | 703 | | |
| |||
670 | 711 | | |
671 | 712 | | |
672 | 713 | | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
673 | 721 | | |
674 | 722 | | |
675 | 723 | | |
| |||
Lines changed: 0 additions & 62 deletions
This file was deleted.
Lines changed: 0 additions & 32 deletions
This file was deleted.
0 commit comments