cloudflare / cloudflare/agents
sessions: optional R2 tier for the attachment store
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
#2196 gives Sessions a content-addressed attachment store: media declared with a non-text media type is kept out of the message row, stored under its SHA-256 in `cf_agents_session_attachment_chunks`, and inlined again on read. That store is SQLite-only, so the bytes live in the same Durable Object as the conversation.
## The problem
A Durable Object holds 10 GB and degrades noticeably past roughly 5. Attachments are the only thing in a session that can approach that: measured, roughly 39,000 200 KB images fills an object. Everything else — prose, tool output, compaction overlays — is orders of magnitude smaller.
So the object's ceiling is effectively an attachment ceiling, and a conversation that accumulates media has no way to shed bytes without deleting history.
## Shape
The store is already the right seam for this. `AttachmentStore` addresses payloads by content hash and derives their lifetime from reference rows; where the bytes physically sit is behind `put` / `get` / `#collect` and nothing above it knows.
An R2 tier would:
- write payloads above some size to R2 under a key derived from the hash, keeping metadata and reference rows in SQLite
- read them back through the same `get`, so `resolveAttachments` and every caller stay unchanged
- delete the object when the last reference goes, the same way chunk rows are collected today
Content addressing makes this easier than it would otherwise be: the key is a pure function of the bytes, so a retried write is idempotent and there is no rename or move to coordinate.
## Open questions
- **Where the threshold sits, and whether there is one at all.** Sending everything to R2 is simpler and makes the SQLite footprint of media effectively zero; a threshold keeps small images fast to read but adds a size-dependent branch of the kind #2196 deliberately removed from extraction.
- **Read latency.** A SQLite chunk read is local; an R2 read is a network round trip on the hydration path. This likely wants pointer-mode reads to stay the default for context assembly so payloads are fetched only when something actually needs the bytes.
- **Durability ordering.** Today bytes and the message row commit in one synchronous transaction, so a stored pointer always has bytes behind it. An R2 write cannot join that transaction, which reintroduces the ordering problem the current design avoids — write bytes first and risk orphans on failure, or write the row first and risk a pointer with nothing behind it.
- **Configuration.** The previous R2 tier was removed in #2196 partly because it was configuration nobody had switched on. This should not come back until there is a host that wants it.
## Prior art
An R2 tier existed earlier in #2196 and was removed along with the reactive offload layer. This is not a revert of that: the old one existed to make an over-budget row fit, and it competed with row chunking. This one would be a storage tier under a store that already exists and already works, which is a much smaller thing.
Contributor guide
Research direction
Start with the existing AttachmentStore seam described in #2196, tracing put, get, and #collect and then resolveAttachments to understand the current SQLite-only lifecycle. Before implementation, resolve the threshold, read mode, durability ordering, and host configuration questions; done would mean an agreed R2-backed tier that preserves reference-based cleanup without changing callers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sqlite, typescript
- Domain
- backend, cloud, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100