Skip to content

perf(api): drop unread surface bodies from the session hydrate - #237

Merged
benvinegar merged 1 commit into
mainfrom
perf/slim-session-hydrate
Jul 29, 2026
Merged

perf(api): drop unread surface bodies from the session hydrate#237
benvinegar merged 1 commit into
mainfrom
perf/slim-session-hydrate

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Opening a session fetches every post in one request with ?hydrate=1. That response served each post through postDetailView — full current surfaces, plus every prior version's surfaces in history. The viewer can't use most of it.

A sandboxed surface (html, diff, markdown, terminal, mermaid, code) renders as an opaque-origin iframe pointed at /s/:id?part=N, which fetches its own body. Card only ever reads { id, kind, index } to build that URL — grepping viewer/src for reads of any content field (html, patch, markdown, text, mermaid, code) turns up nothing. So the one response that blocks the stream from rendering was carrying a second, unread copy of every surface in the session.

What changed

sessionPostHydratedView — same envelope (the viewer identifies a hydrated row by history being an array), minus the bodies:

  • Sandboxed surfaces lose only their content field. The field is resolved from the existing SURFACE_KIND_METADATA registry, so a newly added kind can't silently fall behind a hand-maintained list.
  • History surfaces reduce to refs. History is here to size the version dropdown; picking an older version re-points each iframe at ?ver=N rather than rendering from this payload.
  • Native kinds (image, trace, json) do render from inline data and are untouched.

GET /api/posts/:id and the MCP read_post tool still go through postDetailView and return full detail. ?hydrate=1 has exactly one consumer (viewer/src/state.ts).

Measured

Against the six largest sessions in a real store, comparing the new response to what postDetailView produced for the same posts:

raw gzipped
before 368 KB 58 KB
after 18 KB 5 KB
-95% -91%

The per-surface /s/:id?part=N fetches are unchanged — they're the sandbox boundary, and they're individually cacheable and lazy. This shrinks the blocking response, not the request count.

Testing

  • npm test — 440 pass, including a rewritten hydrate test plus a new one covering sandboxed-vs-native stripping across both current and history surfaces.
  • npm run test:e2e --project=chromium — 85 pass (webkit can't launch in this environment: missing system deps, pre-existing).
  • typecheck, lint, format:check clean.

🤖 Generated with Claude Code

Opening a session fetches every post in one go with `?hydrate=1`, which served
each post through postDetailView: full current surfaces plus every prior
version's surfaces. The viewer can't use most of that. A sandboxed surface
(html, diff, markdown, terminal, mermaid, code) renders as an opaque-origin
iframe pointed at /s/:id?part=N, which fetches its own body — Card only ever
reads { id, kind, index } to build that URL. So the response that blocks the
stream from rendering carried a second, unread copy of every surface.

Add sessionPostHydratedView: same envelope (the viewer keys "hydrated" off
`history` being an array), minus the bodies. Sandboxed surfaces lose just their
content field, resolved from the existing SURFACE_KIND_METADATA registry rather
than a second hand-maintained list. History surfaces reduce to refs — history
exists to size the version dropdown, and picking an older version re-points each
iframe at ?ver=N rather than rendering from this payload. Native kinds (image,
trace, json) do render from inline data and are untouched.

Across the six largest sessions in a real store: 368 KB -> 18 KB raw, 58 KB ->
5 KB gzipped. GET /api/posts/:id and the MCP read_post tool still return full
detail; only the session-list hydrate view changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@benvinegar
benvinegar merged commit 2b98bbf into main Jul 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant