Skip to content

Take managoat_sandbox 0.2.0: a close before the exit frame is an error, not exit 0 - #1470

Merged
jhgaylor merged 1 commit into
mainfrom
deps/1363-sprites-0.2.2
Sep 3, 2026
Merged

Take managoat_sandbox 0.2.0: a close before the exit frame is an error, not exit 0#1470
jhgaylor merged 1 commit into
mainfrom
deps/1363-sprites-0.2.2

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #1363.

managoat_sandbox, managoat_runner and managoat_runtimes all go to 0.2.0, and the contract they carry has changed: a command stream that closes without an exit frame is {:error, %{ref: ref}, :closed_before_exit} where it used to be a synthesised {:exit, %{ref: ref}, 0}. The upstream sprites client made that change in 0.2.2 and the libraries adopted it, because an unexplained close was never a clean exit — reporting one as a clean exit is the mechanism behind #880, where all 533 exit codes we had ever recorded turned out to be synthetic zeros.

What was reading that zero here

ConversationServer wrote a completed turn for a dropped connection. A mid-turn close arrived as {:exit, _, 0} and landed on the :exit handler, which writes status: "completed", exit_code: 0 — a turn that never finished, recorded as a clean one, in the conversation the user is watching. The {:error, ...} branch beside it (the #413 handler) already does the right thing with any reason, so the frame simply lands there now and the turn fails. The new test pins it: the correctness is which handler the frame reaches, and nothing about the two handlers says so on its own.

TurnMachine.drain_exited_command/1 matched only the exit frame. It drains a mailbox after a stdin write comes back :command_exited, and the error frame is as stranded as the output around it — current_command_ref is unset on that path, so leaving it there costs the full 50 ms deadline and then drops it, which is #608 over again. It ends the drain now, with a nil code, because nobody measured one.

Nothing else in the tree matched {:exit, _, 0} on a dropped connection (conversation_server.ex and turn_machine.ex are the only two consumers of the frames).

The library side

Repo PR What it took
managoat/managoat_sandbox #4 the contract line, the conformance rule (spawn_drop asserts the error frame and refutes an exit behind it), Sprites.Errors classifying the reason as {:unavailable, :closed_before_exit} so Retry.transient?/1 retries a wake, the E2B adapter (which synthesised a zero in two places), and an exact 0.2.2 pin with a dependency-contract test that drives Sprites.Command's close path directly
managoat/managoat_runner #1 nothing in the adapter: the protocol has no way for a session to end without an exit code, so a lost exit can only be a lost connection, which already reports :runner_disconnected. Only the FakeDaemon's drop was faking a zero
managoat/managoat_runtimes #4 Codex.prepare_sandbox/3 matched {:exit, _, 0} as a successful codex login --with-api-key, so a transport that died mid-login was reported as a login that worked and provisioning carried on against a sandbox with no ~/.codex/auth.json

Verification

  • mix precommit on this branch: compile with warnings-as-errors, unused deps, format, credo --strict (5,970 mods/funs, no issues), sobelow, dialyzer (0 errors), and the suite.
  • Before the rebase onto current main, the full suite ran 4,114 tests, 0 failures against the three libraries as local path deps — that run is what checked the library changes end to end from here.
  • After the rebase and on the hex pins, the suite is 4,119 tests with one failure, unrelated to this change: Fountain.ActivationTest "first_reply_by_user/0 the earliest replied turn, across conversations". It passes on its own (15 tests, 0 failures), and the cause is in the test — at(4) is evaluated once when the row is written and again in the assertion, so the two differ by a second whenever the test straddles a second boundary, and DateTime.compare/2 returns :lt. Filed separately rather than fixed here.
  • Targeted runs: the whole test/fountain/conversations/ directory plus the sandbox reaper (606 tests), and the runner context and controller (43 tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_016SwfnLHB6NXqB2t6ZCbBAd

…e is an error (#1363)

`managoat_sandbox`, `managoat_runner` and `managoat_runtimes` all go to
0.2.0. The contract they carry has changed: a command stream that closes
without an exit frame is `{:error, %{ref: ref}, :closed_before_exit}`,
where it used to be a synthesised `{:exit, %{ref: ref}, 0}`. The upstream
`sprites` client made that change in 0.2.2 and the libraries adopted it,
because an unexplained close was never a clean exit — reporting one is
the mechanism behind #880, where all 533 exit codes we had ever recorded
turned out to be synthetic zeros.

Two things here were reading that zero.

`ConversationServer`'s `:exit` handler wrote a **completed** turn with
exit code 0 for a mid-turn close: a turn that never finished, recorded as
a clean one, in the conversation the user is watching. The `{:error, ...}`
branch beside it — the #413 handler — already does the right thing with
any reason, so the frame simply lands there now and the turn fails. The
new test pins that, because the correctness here is which handler the
frame reaches, and nothing about the handlers themselves says so.

`TurnMachine.drain_exited_command/1` drains a mailbox after a stdin write
comes back `:command_exited`, and matched only the exit frame. The error
frame is as stranded as the output around it — `current_command_ref` is
unset on that path, so leaving it costs the full 50ms deadline and then
drops it, which is #608 again. It ends the drain now, with a nil code,
because nobody measured one.

Nothing else in the tree matched `{:exit, _, 0}` on a dropped connection.

Library side, for the record: managoat/managoat_sandbox#4 (the contract,
the conformance rule, the E2B adapter, an exact 0.2.2 pin with a
dependency-contract test on the close path), managoat/managoat_runner#1
(the protocol cannot lose an exit, so only the FakeDaemon's `drop` was
faking one) and managoat/managoat_runtimes#4, where the fabricated zero
had been reporting a `codex login` whose transport died as a login that
worked.

Closes #1363.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SwfnLHB6NXqB2t6ZCbBAd
@jhgaylor
jhgaylor merged commit fded952 into main Sep 3, 2026
16 checks passed
@jhgaylor
jhgaylor deleted the deps/1363-sprites-0.2.2 branch September 3, 2026 07:11
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.

managoat_sandbox: upgrade the Sprites client to 0.2.2 (close-before-exit becomes an error frame)

1 participant