Summary
docs/guides/frontend/overview.mdx §"Resumable sessions" tells database-backed chat apps to persist events per-arrival with onEvent, snapshot in onFinish, and "reconnect an interrupted in-flight turn with session.stream({ startIndex: savedEvents.length })". That recipe is correct only while the saved log is an exact 1:1 mirror of the events received — an invariant the docs never state, and one that real apps can't guarantee (a single failed persistence call breaks it permanently, because the count-as-cursor is then wrong forever and every reconnect replays events the app already saved).
We root-caused a production runaway to exactly this in an eve-chat-template app (full report with deterministic repro and a working fix: vercel-labs/eve-chat-template#23): failed snapshot → stale cursor → whole-log replay per send → unbounded duplication. Suggested doc additions below; all of them document guarantees eve already provides but only in .d.ts comments today.
Suggested additions to the Resumable Sessions guide
- State the invariant and the failure mode.
startIndex: savedEvents.length assumes lossless, duplicate-free persistence. Say so, and say what happens otherwise (replays events the client already consumed — the client must reconcile idempotently, mirroring the durability contract's "an interrupted step re-runs, so make non-idempotent side effects idempotent", which currently reads as server-side-only guidance).
- Document the per-event identity clients should reconcile on. Every turn-scoped content event carries
data.turnId + data.sequence, and replays serve recorded events verbatim — "Replays must preserve the original meta.at… replay never invents new timestamps" lives only in protocol/message.d.ts doc comments. That contract is exactly what a client needs for safe dedup/reconciliation, and it deserves a docs surface. (Caveat worth documenting: subagent.completed carries callId but no turnId; session.waiting carries neither and is content-identical across turns.)
- Warn about the cumulative-delta footprint.
message.appended/reasoning.appended carry cumulative text, so a naively persisted raw event log is O(len²) in reply length per turn. Reasoning models emit ~10× the deltas of models with summarized/empty reasoning, which is how this crosses real-world transport limits (e.g. Next Server Actions' 1 MB default) — worth a sentence where the recipe says "persist stream events as they arrive."
Related framework gap (optional, separate concern)
The Workflow SDK that backs eve's streams supports tail-relative reads precisely to avoid replaying "potentially thousands of chunks", but eve's client stream({ startIndex }) only accepts an absolute index. For long reasoning-model sessions, exposing a tail-relative or server-authoritative resume would remove the client's cursor-bookkeeping burden entirely.
Happy to provide the full cited analysis or a docs PR.
Summary
docs/guides/frontend/overview.mdx§"Resumable sessions" tells database-backed chat apps to persist events per-arrival withonEvent, snapshot inonFinish, and "reconnect an interrupted in-flight turn withsession.stream({ startIndex: savedEvents.length })". That recipe is correct only while the saved log is an exact 1:1 mirror of the events received — an invariant the docs never state, and one that real apps can't guarantee (a single failed persistence call breaks it permanently, because the count-as-cursor is then wrong forever and every reconnect replays events the app already saved).We root-caused a production runaway to exactly this in an eve-chat-template app (full report with deterministic repro and a working fix: vercel-labs/eve-chat-template#23): failed snapshot → stale cursor → whole-log replay per send → unbounded duplication. Suggested doc additions below; all of them document guarantees eve already provides but only in
.d.tscomments today.Suggested additions to the Resumable Sessions guide
startIndex: savedEvents.lengthassumes lossless, duplicate-free persistence. Say so, and say what happens otherwise (replays events the client already consumed — the client must reconcile idempotently, mirroring the durability contract's "an interrupted step re-runs, so make non-idempotent side effects idempotent", which currently reads as server-side-only guidance).data.turnId+data.sequence, and replays serve recorded events verbatim — "Replays must preserve the originalmeta.at… replay never invents new timestamps" lives only inprotocol/message.d.tsdoc comments. That contract is exactly what a client needs for safe dedup/reconciliation, and it deserves a docs surface. (Caveat worth documenting:subagent.completedcarriescallIdbut noturnId;session.waitingcarries neither and is content-identical across turns.)message.appended/reasoning.appendedcarry cumulative text, so a naively persisted raw event log is O(len²) in reply length per turn. Reasoning models emit ~10× the deltas of models with summarized/empty reasoning, which is how this crosses real-world transport limits (e.g. Next Server Actions' 1 MB default) — worth a sentence where the recipe says "persist stream events as they arrive."Related framework gap (optional, separate concern)
The Workflow SDK that backs eve's streams supports tail-relative reads precisely to avoid replaying "potentially thousands of chunks", but eve's client
stream({ startIndex })only accepts an absolute index. For long reasoning-model sessions, exposing a tail-relative or server-authoritative resume would remove the client's cursor-bookkeeping burden entirely.Happy to provide the full cited analysis or a docs PR.