**TL;DR:** Since ~2026-07-28 19:00 UTC, the non-TTY exec WebSocket (`wss://api.sprites.dev/v1/sprites/{name}/exec`) no longer delivers a command’s complete stdout. Output the guest process produces before the WebSocket client is attached is truncated to roughly the last 64KB, so any fast producer (e.g. `cat` or `tar -cf -` of anything larger than 64KB) arrives with its **head missing**. This silently corrupts every workflow that streams file content over exec stdout. The command’s exit code still reports 0.
## Environment
- API: `https://api.sprites.dev` (exec over WebSocket, non-TTY, `stdin=true`)
- Client: plain Node 22 (`globalThis.WebSocket`), no SDK — repro attached
- Started: first failures in our production logs ~2026-07-28 19:00 UTC; before that, the same client code pulled multi-MB tar streams reliably for weeks
## Expected
`exec` of `cat /tmp/f` (f = N bytes of `/dev/urandom`) delivers exactly N bytes on stdout frames, regardless of how fast the process produces them relative to the client attach.
## Actual
Output from the attached `repro.mjs` (creates a throwaway sprite, writes random files in the guest, cats them back over exec stdout, compares md5s; two runs):
```
[test 2] stream integrity: cat a random file over exec stdout
size exit bytes-received intact received==last-64KB-of-file
16KB 0 16384 true true
64KB 0 65536 true true
256KB 0 262144 true false
1024KB 0 65536 false true ← exactly the last 64KB of the file
4096KB 0 2015232 false false ← 2nd run: 1589248 (varies per run)
```
Key rows:
- **1MB file → 65,536 bytes received, and the md5 matches `tail -c 65536` of
the file.** The client got exactly the last 64KB; everything before it was
dropped. Reproduces every run.
- **4MB file → ~1.5–2.5MB received (varies run to run), head missing.** Looks
like: everything produced before attach is gone except the last 64KB of it,
then live streaming is intact from attach onward.
- Exit code is 0 in every case — the loss is undetectable from the exec result.
## Additional observations (same repro, plus variants)
1. The exec endpoint now emits debug/control messages we had never seen before this date, suggesting execs became attachable sessions with a history buffer — which would explain the ~64KB survival window if history is a 64KB ring and pre-attach output only lives there:
```
{“msg”:“session_created cmd=sh”,“pid”:5,“t_ms”:3,“type”:“debug”}
{“msg”:“normal_path history_len=0”,“pid”:5,“t_ms”:3,“type”:“debug”}
{“type”:“session_info”,“session_id”:“5”,“command”:“sh”,“created”:…,“is_owner”:false,“tty”:false}
```
Note `is_owner: false` — for a WebSocket that itself initiated the exec.
2. **stderr is sometimes merged into stdout frames** (intermittent, seems timing-dependent). With `sh -c ‘printf AAAA; printf BBBB 1>&2; printf CCCC’` we have observed a single stdout (stream-id 1) frame carrying `“AAAACCCCBBBB”`. For us this corrupts streamed archives whenever the producer writes any diagnostics to stderr.
3. **Data frames can arrive after the exit (stream-id 3) frame.** A client that treats the exit frame as end-of-stream (ours did) loses the tail as well.
4. Same behavior with `tar -cf -` as the producer: a 10MB tree arrived as
9,781,248 bytes beginning mid-file — no tar header at offset 0, so the archive is unreadable.
## Impact
We sync sandbox workspaces back to our host by streaming `tar -cf -` over exec
stdout. Since the change, **every** workspace pull fails integrity checks
(truncated archive → untar fails), which fails every run of our product on this
backend. Small control execs (probes, short commands) still work, which made
this hard to spot — only bulk streams die.
## Repro steps
```
SPRITES_API_TOKEN= node repro.mjs
```
Node >= 22, no dependencies. The script creates one sprite named
`exec-stream-repro-`, runs the tests above, prints the table, and deletes
the sprite. Runtime is ~1 minute.
## Questions
- Was the exec endpoint recently changed to session-with-history semantics?
- Is there a way (query param, protocol handshake) to make the exec stream
lossless again — i.e. block the process, or buffer fully, until the initiating
client is attached?
- If the session model is here to stay, what is the supported way to stream bulk data out of a sprite?
Happy to share a repro package