Repository navigation
fix(sdk): unavailable terminal event and reconnect budget for failed connects - #4310
Conversation
Derive terminal event names from TerminalState::ALL in the Rust SDK and the generated TypeScript OPERATION_TERMINATIONS so neither list can drift from the server's terminal variants.
TypeScript retried only read failures; Rust returned the first failed connect as an unbudgeted error and left the iterator live. Both now count failed connects, retry transport failures only, never retry the first connection, and end iteration after one final error.
|
There was a problem hiding this comment.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| ))); | ||
| ClientError::Transport("event stream ended before a terminal event".into()) | ||
| } | ||
| Err(error @ ClientError::Transport(_)) => error, |
There was a problem hiding this comment.
🟡 Malformed stream bytes get silently skipped
When SSE contains invalid UTF-8, read_event labels the decode failure as Transport, so next retries it. A successful reconnect can hide the malformed stream and deliver later events as valid.
Learn more
Rust's BufRead::read_line reports invalid UTF-8 as io::ErrorKind::InvalidData. read_event currently converts every such read error into ClientError::Transport. The new retry branch therefore reconnects from the last accepted frontier when stream bytes are malformed. A later valid response can make the iterator complete without reporting that the first response violated SSE encoding.
Example: A stream publishes a valid open with sequence 0 and a resume token, then sends 0xff before any terminal event. With one reconnect allowed, a resumed connection returning completed at sequence 0 makes the iterator return success instead of reporting the invalid UTF-8.
Recommended fix: Distinguish InvalidData from actual I/O failures in read_event, return ClientError::Protocol for malformed UTF-8, and test that an invalid UTF-8 frame never consumes reconnect budget.
Was this helpful? React with 👍 or 👎 to provide feedback.
Fixes #4211
Fixes #4210
What was broken
unavailable, so a valid unavailable terminal was rejected (Rust: "SSE event name is not canonical") or ignored (TypeScript: stream treated as disconnected).try/catch, so one failed reconnect ended the generator with budget left. Rust returned the first failedconnect()as an error without counting it, never retried mid-stream read failures, and kept the iterator live after the error.Fix
TerminalState::ALLandas_str()intracedecay-tool-catalogare the one list. The Rust SDK'sis_terminal_eventreads them, and codegen emitsOPERATION_TERMINATIONSinto the generatedtypes.ts(andOperationTerminationderives from it), whichclient.tsuses. A new server variant now breaks compilation in the exhaustiveas_strmatch and in a unit test.maxReconnects; only transport failures are retried (auth, problem and protocol errors surface at once); the first connection is never retried; the last accepted frontier is kept; after exhaustion one final error is raised, then Rust yieldsNone.Evidence
None, first connection not retried, and a 401 on reconnect not retried.unavailablefrom the terminal list fails the Rust and TypeScript terminal tests; making a failed connect fatal fails the Rust recovery, exhaustion and auth tests; restoring the old TypeScript fetch path fails the recovery and exhaustion tests ("expected 3 requests, got 2").bazel test --config=clippy //crates/tracedecay-sdk/... //crates/tracedecay-tool-catalog/...,rustfmt_check_2024for the SDK,pnpm --dir sdks/typescript run build|typecheck|test(56 tests),pnpm --dir dashboard run contracts:check.plugin/chatgpt-extension/embedded/server.mjsis rebuilt in each commit.Not changed: Rust has no reconnect delay or abort signal (blocking client), so the TypeScript
retry:delay and abort-during-wait have no Rust counterpart.