You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Forward-port of #1847 (which targets stable) onto main.
Same mechanism: object/serialized streams now transparently auto-reconnect at the getReadable level. When the upstream server stream connection errors mid-run (e.g. a per-session duration timeout), the wrapper reopens the stream at startIndex + consumedFrames, counting 4-byte length-prefixed wire frames to compute the resume position. Partial in-flight frame bytes are discarded and resent in full from the new index. A clean EOF (no error) is treated as stream completion and does not reconnect. Negative startIndex (last-N) skips reconnect and behaves as a single-shot read.
Adapted to main's world API: reads go through world.streams.get(runId, name, startIndex), so runId is threaded into createReconnectingFramedStream.
Bounded by a consecutive-failure cap (resets on forward progress) and an absolute total-reconnect backstop so a misbehaving backend can never loop forever.
Object streams only. Byte-stream reconnect is a separate follow-up since it depends on byte-stream framing (#1853).
Adds transparent auto-reconnect for object/serialized streams at the
getReadable level. On upstream error mid-run, the wrapper reopens the
server stream at startIndex + consumedFrames by counting 4-byte
length-prefixed wire frames. Clean EOF is treated as completion. Byte
streams keep their existing raw, non-reconnecting path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Together these make run.getReadable() transparently reconnect when a stream's underlying connection ends mid-stream (e.g. the periodic server-side max-duration cutoff), instead of surfacing a truncated stream to the consumer.
How the pieces fit:
[core] Move stream reconnect logic to getReadable level #1847 (→ stable) — adds a reconnecting reader at the getReadable/core level. It counts the length-prefixed frames it has received and, on a connection error, transparently reopens the stream from startIndex + framesConsumed. A clean end-of-stream is still treated as completion. Applies to object/serialized streams; raw byte streams are opted out (no wire framing to count).
[core] Add wire-level framing for byte streams #1853 (→ main) — opt-in wire framing for byte streams. Independent of the reconnect PRs and not required by them; it's the groundwork that lets a later follow-up extend the same frame-counting reconnect to byte streams.
Behavioural note: the reconnecting reader only reopens on a connection error — a clean close means "complete". So the client change is inert on its own and only takes effect once paired with the coordinated server-side change that ends a timed-out connection with an error rather than a silent close (handled separately). Shipping the client first is therefore safe and must precede that server change.
The reason will be displayed to describe this comment to others. Learn more.
Approve — faithful forward-port, correctly adapted to main's world API
I reviewed this against the stable PR (#1847) side by side, built @workflow/core from this branch, and ran the test suites locally: all 11 reconnecting-framed-stream tests plus the 199 serialization tests pass (210 total).
Port fidelity
The reconnect mechanism is identical to #1847 — frame counting, partial-frame discard with resume at startIndex + consumedFrames, clean-EOF-equals-completion, negative-startIndex opt-out, consecutive cap with reset-on-progress, absolute backstop. The adaptations are exactly the ones main's API requires and nothing more:
world.readFromStream(name, idx) → world.streams.get(runId, name, idx), with runId threaded through createReconnectingFramedStream
getWorld() → await getWorldLazy()
Reviver hookup matches: byte streams keep WorkflowServerReadableStream (no reconnect — no framing to count, explicitly deferred to the byte-framing follow-up), object streams get the reconnecting wrapper
The test file gained one main-only case (11 vs 10) and the mock world is adapted to the streams.get shape. Changeset is correctly scoped (@workflow/core: patch).
Notes
Same non-blocking design note as the stable PR: a transient failure of the reopen itself (reconnect() → connect() throwing) is fatal rather than counted against the budget — only reader.read() errors are retried. Worth folding into the budgeted loop in a follow-up, here and on stable, so the wrapper is robust against the exact server blip it exists to survive.
Heads-up on a pending textual conflict: #1853 (byte-stream framing) touches the same reviver region of serialization.ts — specifically the value.type === 'bytes' branch this PR also edits (it adds framing dispatch where this PR adds the no-reconnect comment + scoped readable construction). Whichever lands second gets a small but real merge conflict; the resolution is straightforward (byte branch gains framing dispatch, object branch keeps the reconnecting wrapper) but worth coordinating so the second rebase is deliberate rather than auto-resolved.
CI: the three red nextjs-webpack jobs (Local Postgres / Local Prod / Local Dev, stable lazyDiscovery disabled) are the known pre-existing failure on main (Dynamic require of "stream" during next build page-data collection) — I verified the identical failure signature on main's latest Tests run. Not attributable to this PR. Everything else is green and the branch has no conflicts with current main.
Good to land. Same sequencing logic as the stable PR applies: this is safe to release ahead of the coordinated server-side behavior change, since the reconnect path simply never triggers until the server starts erroring timed-out connections instead of closing them.
Addresses review feedback on #2318: a transient failure of the reopen
itself (`connect()` throwing inside `reconnect()`) was fatal — only
`reader.read()` errors were retried. A brief server unavailability during
the reconnect window is exactly the blip this wrapper exists to survive,
so count it against the reconnect budget and retry rather than erroring
the stream. Budget exhaustion (a server that stays down) still terminates.
Adds a test: a reopen that throws once then succeeds recovers transparently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review feedback on #2318: a transient failure of the reopen
itself (`connect()` throwing inside `reconnect()`) was fatal — only
`reader.read()` errors were retried. A brief server unavailability during
the reconnect window is exactly the blip this wrapper exists to survive,
so count it against the reconnect budget and retry rather than erroring
the stream. Budget exhaustion (a server that stays down) still terminates.
Adds a test: a reopen that throws once then succeeds recovers transparently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Forward-port of #1847 (which targets
stable) ontomain.Same mechanism: object/serialized streams now transparently auto-reconnect at the getReadable level. When the upstream server stream connection errors mid-run (e.g. a per-session duration timeout), the wrapper reopens the stream at
startIndex + consumedFrames, counting 4-byte length-prefixed wire frames to compute the resume position. Partial in-flight frame bytes are discarded and resent in full from the new index. A clean EOF (no error) is treated as stream completion and does not reconnect. NegativestartIndex(last-N) skips reconnect and behaves as a single-shot read.Adapted to
main's world API: reads go throughworld.streams.get(runId, name, startIndex), sorunIdis threaded intocreateReconnectingFramedStream.Bounded by a consecutive-failure cap (resets on forward progress) and an absolute total-reconnect backstop so a misbehaving backend can never loop forever.
Object streams only. Byte-stream reconnect is a separate follow-up since it depends on byte-stream framing (#1853).
🤖 Generated with Claude Code