Never let a dropped event stream close the app

Both screens that follow a stream retried an `ApiException` and let
everything else through, and `Sse.run` opened its connection on a line
outside the `try` that maps failures onto that type. So a failure at open
time, or anything the framing did not expect, reached the top of the app
and closed it -- from a screen whose own comment says failures there are
deliberately quiet, because the listing already carries every state the
stream would have brought.

The open moves inside the guarded region, and both loops now retry on any
exception while rethrowing `CancellationException`, which is the screen
leaving rather than a failure -- swallowing that one would leave the loop
reconnecting to a stream nobody is watching.

This is hardening on the path that runs when a screen with a stream opens,
not a diagnosed fix: an import list loading against a server missing the
events route, and against 121 real transcripts, does not crash here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-31 21:57:36 -04:00
1 parent 778b2e3b04
commit 8257030280
3 files changed
+30 -6

No files matched your search

@@ -47,9 +47,16 @@ class Sse(private val settings: ServerSettings) {
* recovered from, indefinitely.
*/
fun run(path: String, onOpen: () -> Unit, onFrame: (name: String?, data: String) -> Unit) {
val connection = URL("${settings.baseUrl}$path").openConnection() as HttpURLConnection
this.connection = connection
// Opening is inside the try, not before it. Everything this method can fail at owes the
// caller the same kind of failure -- both callers retry an [ApiException] and let anything
// else reach the top of the app -- and a connection that could not even be constructed
// used to escape as a raw `IOException` from a line no `catch` covered.
var connection: HttpURLConnection? = null
try {
connection =
(URL("${settings.baseUrl}$path").openConnection() as HttpURLConnection).also {
this.connection = it
}
connection.applyPinnedTls()
connection.connectTimeout = CONNECT_TIMEOUT_MS
// No read timeout: between events there is nothing to read for as long as the thing
@@ -90,7 +97,7 @@ class Sse(private val settings: ServerSettings) {
)
}
} finally {
connection.disconnect()
connection?.disconnect()
this.connection = null
}
}