a plain GET on the exec path is the session list, not a failed upgrade - #19
Merged
Conversation
The exec path serves two protocols in the real API. sprites-ex's
Session.list_by_name/2 is literally
Req.get(client.req, url: "/v1/sprites/#{name}/exec")
and reads body["sessions"]. spritzer registered that path as a WebSocket
handler only, so an unupgraded GET fell through to websocket.Accept and came
back 426 with a protocol-violation body.
Found from fountain, which asks for the session list before deciding whether to
reattach or start fresh. The 426 aborted that decision and orphaned the turn
before anything ran — #18.
An idle sprite has no sessions and the honest answer is an empty list, which is
also the answer that lets a client get on with it. A missing sprite is still a
404: "no sessions" and "no sprite" are different answers and a client
reattaching should be able to tell them apart.
The upgrade check reads both headers as the lists they are — Connection is
commonly "keep-alive, Upgrade" from a proxy, and an equality check would route
that to the session list and break exec behind any proxy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPvVBaE97cfmvzytEvJCBr
jhgaylor
added a commit
that referenced
this pull request
Aug 7, 2026
#19 and #20 both merged after v0.4.1 was tagged, so no released image carries either. Neither had a changelog entry, and `just release` refuses without a `## [0.5.0]` section — this adds it so the tag can be cut. Minor rather than patch: an unupgraded GET on the exec path is new behavior, and holding an unrecognised command open changes exec's lifetime for the clients that hit that branch. Co-authored-by: Claude Opus 5 (1M context) <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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fixes the 426 in #18.
The exec path serves two protocols
sprites-ex's
Session.list_by_name/2is literally:spritzer registered that path as a WebSocket handler only, so an unupgraded GET fell through to
websocket.Acceptand came back426with a protocol-violation body:fountain asks for the session list before deciding whether to reattach or start fresh, so the 426 aborted that decision and orphaned the turn before anything ran.
An idle sprite has no sessions, so the honest answer is
{"sessions": []}— which is also the answer that lets a client get on with it. A missing sprite is still a 404: "no sessions" and "no sprite" are different answers, and a client reattaching should be able to tell them apart.The upgrade check reads both headers as the lists they are.
Connection: keep-alive, Upgradeis what proxies routinely send, and an equality check would route it to the session list and break exec behind any proxy — that case is a table test.It fixes the 426 and does not make a fountain turn complete
Verified against a real fountain deployment on k3d, with this build swapped in. The failure moves and does not go away:
and the actual root cause is now legible in fountain's log:
fountain opens the exec socket, then writes the prompt to it as stdin. spritzer's interpreter runs the command and exits immediately, so the process is already gone by the time the write lands and the conversation server crashes.
That is a separate and larger question than this PR — whether exec should hold a session open until stdin EOF rather than being one-shot. I have written it up on #18 rather than deciding it here, because it changes exec semantics for every client and is the kind of thing that should be a deliberate choice about how far the emulator goes.
go test ./...andgo vet ./...green. The health endpoint now advertises both halves of the path, and a test asserts it does.