Skip to content

fix(sdk): unavailable terminal event and reconnect budget for failed connects - #4310

Merged
ScriptedAlchemy merged 3 commits into
masterfrom
fix/4210-4211-sdk-stream-terminals
Oct 18, 2026
Merged

ScriptedAlchemy merged 3 commits into
masterfrom
fix/4210-4211-sdk-stream-terminals

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4211
Fixes #4210

What was broken

  • Both SDKs hand-listed terminal events and omitted unavailable, so a valid unavailable terminal was rejected (Rust: "SSE event name is not canonical") or ignored (TypeScript: stream treated as disconnected).
  • The TypeScript reconnect fetch sat outside the retry try/catch, so one failed reconnect ended the generator with budget left. Rust returned the first failed connect() as an error without counting it, never retried mid-stream read failures, and kept the iterator live after the error.

Fix

  • TerminalState::ALL and as_str() in tracedecay-tool-catalog are the one list. The Rust SDK's is_terminal_event reads them, and codegen emits OPERATION_TERMINATIONS into the generated types.ts (and OperationTermination derives from it), which client.ts uses. A new server variant now breaks compilation in the exhaustive as_str match and in a unit test.
  • Reconnect contract, same in both SDKs: every reconnect attempt, including a failed connect, spends 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 yields None.

Evidence

  • Tests in both languages cover every terminal variant ending the stream with no reconnect, an unavailable terminal with a disagreeing receipt, two dropped reconnects recovering within a budget of 3, exhaustion giving one error then None, first connection not retried, and a 401 on reconnect not retried.
  • Mutation: removing unavailable from 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").
  • Passed: bazel test --config=clippy //crates/tracedecay-sdk/... //crates/tracedecay-tool-catalog/..., rustfmt_check_2024 for the SDK, pnpm --dir sdks/typescript run build|typecheck|test (56 tests), pnpm --dir dashboard run contracts:check. plugin/chatgpt-extension/embedded/server.mjs is 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.


Devin Review

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.
@changeset-bot

changeset-bot Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8ef3685

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

)));
ClientError::Transport("event stream ended before a terminal event".into())
}
Err(error @ ClientError::Transport(_)) => error,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy
ScriptedAlchemy merged commit 5c0b372 into master Oct 18, 2026
14 of 22 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

1 participant