Skip to content

an unrecognised command holds the exec session open for stdin - #20

Merged
lex00 merged 1 commit into
mainfrom
feat/exec-holds-for-stdin
Aug 4, 2026
Merged

an unrecognised command holds the exec session open for stdin#20
lex00 merged 1 commit into
mainfrom
feat/exec-holds-for-stdin

Conversation

@lex00

@lex00 lex00 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes #18. The narrow option from that thread, approved: known verbs are untouched, only unrecognised commands hold open.

Why

The interpreter answers a known verb and is done — echo, cat, rm have said everything they are going to say. An unknown command is a different situation: a real sprite would have started a process the caller can now write to, and callers do. fountain opens exec for its runtime command and then writes the prompt in as stdin, so exiting immediately meant that write landed on a process already gone:

** (stop) exited in: GenServer.call(#PID<...>, {:write_stdin, "say hello"}, 5000)
    ** (EXIT) normal      ... ConversationServer.kick_turn/4

Its conversation server crashed on a call to a dead pid rather than getting an error back.

What changed

An unrecognised command stays open and echoes stdin back on stdout until StreamStdinEOF or close. Still not execution — the same echo the interpreter already does, extended over time.

ExecResult.Unrecognised is set by the interpreter's default branch, so the server can tell the two situations apart without guessing at the command string.

Blast radius: none for existing clients

Known verbs exit immediately exactly as before, which is what chant's Fly activities depend on. TestKnownVerbStillExitsImmediately asserts that separately from the new path so the two cannot drift.

Verified against real fountain

On k3d, with this build swapped in. Before, every turn ended:

reattach interrupted  {"reason":"no_active_session","outcome":"turn_orphaned"}

Now:

stage turn started
output  claude --dangerously-skip-permissions --print --verbose --output-format stream-json ...
output  say hello
stage turn done  {"exit_code":0}

The reply is the prompt echoed back — what an emulator should produce, and what the downstream docs already say a local conversation proves. It is the difference between fountain-ops provisioning a sandbox and fountain-ops holding a conversation offline.

go test ./... and go vet ./... green.

Closes #18.

The interpreter answers a known verb and is done, which is right — echo, cat
and rm have said everything they are going to say. An unknown command is a
different situation: a real sprite would have started a process the caller can
now write to, and callers do. fountain opens exec for its runtime command and
then writes the prompt in as stdin, so exiting immediately meant that write
landed on a process already gone, and its conversation server crashed on a call
to a dead pid rather than getting an error back.

So an unrecognised command now stays open and echoes stdin back on stdout until
StreamStdinEOF or close. Still not execution — the same echo the interpreter
already does, extended over time.

Deliberately narrow, per the discussion on #18: known verbs keep exiting
immediately and every existing client, including chant's Fly activities, sees
exactly today's semantics. A test asserts that separately from the new path so
the two cannot drift.

Verified against a real fountain deployment on k3d. Before, every turn ended

    reattach interrupted  {"reason":"no_active_session","outcome":"turn_orphaned"}

Now the turn runs to completion:

    stage turn started
    output  claude --dangerously-skip-permissions --print --verbose ...
    output  say hello
    stage turn done  {"exit_code":0}

The reply is the prompt echoed back, which is what an emulator should produce
and what the downstream docs already say a local conversation proves. It is the
difference between fountain-ops provisioning a sandbox and fountain-ops holding
a conversation offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPvVBaE97cfmvzytEvJCBr
@lex00
lex00 merged commit 7c9f66c into main Aug 4, 2026
2 checks passed
@lex00
lex00 deleted the feat/exec-holds-for-stdin branch August 4, 2026 01:05
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fountain's reattach calls exec over plain HTTP and gets 426, orphaning every turn

1 participant