google / google/adk-go

llminternal: a dropped live connection is resumable or fatal depending on which goroutine reports it, so live sessions stop resuming on Windows

Open
#1,602 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
8.8k
Forks
1k
Avg merge
3d 18h
Merged PRs (30d)
88

Description

## What happens

`Flow.RunLive` runs a reader goroutine and a sender goroutine against the same live connection. Both report failures into the same unbuffered `errChan`, and the flow's select consumes exactly one of them, so whichever arrives first decides whether the session resumes or dies. That verdict comes from `isResumable`, which matches substrings of the error text.

The two goroutines do not produce the same text for the same event:

- the reader gets the websocket layer's close error, `failed to receive message: websocket: close 1006 (abnormal closure): unexpected EOF`, which is the same on every platform
- the sender gets the raw socket write failure, whose wording is platform-specific

The substring list (`broken pipe`, `connection reset`, `EOF`, `1008`, `GoAway`) covers the reader's text and the POSIX sender text. It does not cover the Windows sender text. Measured with a probe that kills the peer and then calls each path directly:

| platform | sender error on a dropped connection | `isResumable` |
| --- | --- | --- |
| Linux | `write tcp ...: write: broken pipe` | `true` |
| Windows | `write tcp ...: wsasend: An established connection was aborted by the software in your host machine.` | **`false`** |

So on Windows, if the sender observes the loss first, the flow falls through to `sess.pushError(err); cleanup(); return` and never reconnects. If the reader observes it first, the identical connection loss resumes normally.

Go does not map WSA error numbers onto the POSIX constants, so `errors.Is(err, syscall.ECONNABORTED)` does not help either. The unwrap chain is:

```
*net.OpError -> *os.SyscallError -> syscall.Errno(10053) // WSAECONNABORTED
```

## Impact

On Windows a live or bidi streaming session drops instead of auto-resuming after a transient connection loss, non-deterministically. Linux is unaffected, which is why CI never sees it.

## How it surfaced

`TestRunLiveNoGoroutineLeak/realtime_sender_error_after_connection_loss_does_not_leak` fails intermittently on Windows:

```
base_flow_live_test.go:318: never received a model turn for the retried realtime send
base_flow_live_test.go:392: connection count = 1, want 2
```

`connection count = 1, want 2` is the tell: the flow never opened the second connection. Despite the test's name this is not a leak.

Being straight about the rate: I saw it once in roughly 550 runs of the test binary, and only under heavy CPU load, which is what makes the sender win the race often enough to observe. The mechanism above is what I can demonstrate on demand; the end-to-end flake itself is rare.

## Proposed fix

Classify the transport failure by type rather than by text. Any `*net.OpError` arriving on this path means the socket of an already-established live connection failed, since the dial is handled separately above, so it is resumable on every platform:

```go
var opErr *net.OpError
if errors.As(err, &opErr) {
return true
}
```

The existing substring checks stay for the websocket-level cases (1006, 1008, GoAway), which are not `*net.OpError`.

I am happy to send that. It needs `isResumable` lifted out of `RunLive` to a package-level function so it can be tested directly, plus a table test that synthesizes both platforms' error shapes so a Linux runner covers the Windows ones. I have that working locally if you want it.

## Environment

- `main` at f7e16e0
- Go 1.26.6, windows/amd64, cross-checked on linux/amd64 (WSL2)

Contributor guide

Open the contributing guide

Research direction

Start with Flow.RunLive and the isResumable logic, then read base_flow_live_test.go around the reported sender-error test. Compare the reader and sender error paths and add coverage for the Linux and Windows transport error shapes. Done means dropped live connections resume consistently on both platforms and the existing test passes without the intermittent failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.