8000 T9 — End a box when its entrypoint returns, never for idleness or client loss by minimal-foundry[bot] · Pull Request #2850 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T9 — End a box when its entrypoint returns, never for idleness or client loss - #2850

Merged
mitodrummer merged 12 commits into
mainfrom
agent/2401-end-a-box-when-its-entrypoint-returns-never-for
Nov 3, 2026
Merged

mitodrummer merged 12 commits into
mainfrom
agent/2401-end-a-box-when-its-entrypoint-returns-never-for

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Nov 2, 2026 •
Copy link
Copy Markdown
Contributor

T9 — End a box when its entrypoint returns, never for idleness or client loss

slice: S2
kind: task
covers: BOX-015, BOX-038
size: L
est: 360 code, 240 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: daemon log names each box's exit
observability: exited event carries reason and code
layers:
- runtime:
- crates/minimald/src/box_runtime.rs: entrypoint return ends box
- crates/minimald/src/box_runtime/binding.rs: client loss never stops; no prompt
- crates/minimald/src/box_runtime/tests.rs: tests
test: cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client
- actor:
- crates/minimald/src/box_actor.rs: records exited and code
- crates/minimald/src/box_actor/host.rs: idleness never stops
- crates/minimald/src/box_actor/tests.rs: tests
test: cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss
produces: none
consumes: none
proof: A session whose shell exits ends exited, with no prompt.
depends on:
- blocked by #2399

Proof

When a session's shell exits, the attached client sees no shell-exit prompt and its channel closes. The daemon sets the box's record to exited with the shell's exit code and writes an exited event with reason "exit" and the exit_code. It does this with or without a client attached, and the record is kept. A client that drops, or a box that sits idle, leaves the box running.

Notes from the agent: Design: the host loop's HostExit now has a requested flag, which is true when a kill from the daemon ended the loop. launch_host wraps each launch's loop in a watcher (host.rs watch_entrypoint_exit). The actor mainloop selects on that watch beside its mailbox, and messages go first. When the entrypoint returns on its own, the spec lifetime is until_complete (or no spec is stored), and the record is running, the actor stops what ran beside the host and marks the name stopped. It then writes RecordState::Exited(Exit{exit_code}) and the exited event, logs 'box exited: its entrypoint returned' with box_id, exit_code and reason, and evicts itself, as a stop does. A later launch replaces the watch, so a replaced host never ends the box.

Prompt scope: the shell-exit prompt is skipped only when the box ends with its entrypoint (HostParams/Binding ends_with_entrypoint). A box whose spec sets lifetime = "until_stopped" keeps the prompt, because that box outlives its shell. A full removal makes code dead outside this task's layers: workspace_delta.rs (changed_paths, archive_changed), BoxControl::destroy, and the delta/archives_dir/holds_host_row plumbing. The tests about a box kept after its shell exits (prompt keep/delete/save, workspace baseline, detach and destroy hooks, host-row remint) now create an until_stopped box with a new test helper.

Out of scope, touched: crates/minima 8000 ld/src/rpc/tests.rs. shutdown_without_force_proceeds_once_a_sessions_shell_has_exited waited for the prompt and failed without this change. It now exits the shell and waits for the channel …[truncated]

Tests

line exit tail
cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-c0b6... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-cc10... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

Verify

min task run agent-verify passed ([tasks.verify]).

Base

The base branch moved while the coder worked; it was merged in before the tests ran.

Out of scope

These files are outside the task's layers:, so this PR is a draft:

  • crates/minimald/src/rpc/tests.rs

Closes #2401


Undrafted by the maintainer: the one edit outside the layers, crates/minimald/src/rpc/tests.rs, is accepted. It follows the removal of the session-exit prompt (BOX-038): exiting the shell ends the box, and nothing keeps the channel open.

The host loop now says whether a kill ended it. The actor watches each
launch's loop and, when the entrypoint returned on its own and the spec
sets lifetime = "until_complete", sets the record to exited with the
exit code and records an exited event with reason "exit". The daemon
does this whether or not a client is attached. Such a box raises no
shell-exit prompt (BOX-038). A client that goes away never stops a box
(BOX-015).
Adds the actor tests for BOX-038 and BOX-015. The shell round trip now
expects no prompt and an exited record. Tests about a box kept after its
shell exits now create an until_stopped box. The rpc shutdown test no
longer answers a prompt.
@coderabbitai
coderabbitai Bot commented Nov 2, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7ec236ec-d903-4f2e-a366-2077537bb47f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on a failed check suite: the coder changed nothing. Changed nothing in this round. The fix for the failed checks is outside the layers of this task.

Cause: native-daemon-e2e fails with "session ... still listed after answering 'Delete' at the exit prompt" (scripts/session-e2e.sh:7357). This task makes a box whose spec sets lifetime = "until_complete" end when its entrypoint returns, keep its record (BOX-038), and show no shell-exit prompt (binding.rs, ProcessExited if self.ends_with_entrypoint). Every session spec reads as until_complete (mfile::spec::until_complete and Lifetime::UntilComplete for the session entry). So no prompt renders, the pty driver's default "delete" answer is never sent, and the exited session stays listed, as BOX-038 requires. ci-linux-native-success only aggregates that lane, so it fails for the same reason.

The e2e pins the old behaviour in many places, not one. Examples: scripts/session-e2e.sh:7262-7357 (terminal relay step 4), 7450-7454 (sandbox proof teardown), 6687-6695, and 24656-24716, which…[truncated]

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on a failed check suite: the coder changed nothing. Revise round 2. I made no change this round. The failed checks are a conflict in the plan, and I cannot fix them inside this task's layers.

Cause: every box today is lifetime = "until_complete". crates/mfile/src/spec.rs hard-codes Lifetime::UntilComplete when it builds a spec, and nothing sets until_stopped. So this task's proof ("a session whose shell exits ends exited, with no prompt") removes the shell-exit prompt for every session. It also makes the daemon retain the exited record (BOX-038).

scripts/session-e2e.sh pins the old behaviour, and that file is outside the layers. Line 7357 (terminal relay, step 4) and line 7454 (the interactive teardown proof) expect a 'Delete' answer at the exit prompt to remove the session from min ls. Now no prompt renders, and the record stays listed as exited. Both native-daemon-e2e and e2e fail on exactly that check. The run stops at the first 8000 failure, so later cases can fail too. For example, line 24716 checks the text of the NET-1…[truncated]

@mitodrummer
mitodrummer marked this pull request as ready for review November 2, 2026 22:11
@mitodrummer
mitodrummer requested a review from a team as a code owner November 2, 2026 22:11
@minimal-foundry
minimal-foundry Bot commented Nov 2, 2026 •
Copy link
Copy Markdown
Contributor Author

Reviewed: 1 finding, none blocking at 8178ac9c2050.

Address the comments on the diff.

Clobber: none seen.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The change conforms to the design. Line 499 of architecture.md says a box is exited when its entrypoint returns. It also says that idleness or client loss never stops a box. The actor now ends an until_complete box from the daemon, with or without a client. It records reason exit and the exit_code, and it keeps the record. The exited event uses the same field names as a stop. Client loss and idleness leave the box running, and the new tests prove both rules.

Ruled at a7b77d8 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/box_actor.rs Resolved
@mitodrummer
Copy link
Copy Markdown
Member

/revise The maintainer accepts two more files outside the layers: scripts/session-e2e.sh and scripts/e2e-attach-pty.py. The maintainer confirmed the design: no session-exit prompt. Exiting the shell ends the box (BOX-038), which stays listed as exited; detach and re-attach are unchanged (BOX-015, BOX-016).

All three e2e lanes (native-daemon-e2e, e2e, test-kvm) fail the "terminal relay (resize, paste, detach + re-attach, transport drop, exit prompt)" leg: "session … still listed after answering 'Delete' at the exit prompt". Update the e2e to the new behaviour:

  • In scripts/e2e-attach-pty.py, drop the Detach/Delete prompt answering (E2E_PTY_ANSWER, ANSWER_LABEL_PREFIX and the wait for the prompt) and update its module doc. The pty helper only needs to drive the command stream and see the attach end when the shell exits.
  • In scripts/session-e2e.sh, every leg that exited a shell and answered the prompt now asserts that the box is listed as exited with the shell's exit code. Where it answered Delete, it then runs min session destroy <name> and asserts the box is gone. Where it answered keep/Exit, it asserts the box is still listed as exited. Grep the script for other uses of the prompt or of E2E_PTY_ANSWER and update them the same way.
    Touch nothing else.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2401. Your note is kept and goes into the next round here, which starts when that coder finishes.

Nothing to type again: the command gets a rocket when its round starts, or a refusal that names the reason. No round was counted for it yet.

The host and the watch are gone by the time the write runs, so a
single failed write left the box running with nothing in it. Retry it
with a short backoff, and pin the retry with a test seam.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The compare adds only a retry of the exited record write in end_on_entrypoint_return, and a test that proves it. Line 499 of the design still decides the result: a box whose entrypoint returns becomes exited with reason and exit_code. The retry makes that result more likely and changes nothing else. The ruling on a7b77d8c stands for everything else. No conditions.

Ruled at 975bb0b against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/box_actor.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Revise round 2 ran against ci-linux-native-success, native-daemon-e2e and left it red; a coder round cannot clear it, a person should look. No round was started for the failed suite on 975bb0b26c24.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on the runtime's review findings, pushed as 975bb0b26c24.

line exit tail
cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-604b... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-fef5... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/rpc/tests.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_actor.rs:1657: The exited-record write in end_on_entrypoint_return now retries up to 5 times with backoff. Persistent failure still returns Continue so a min stop can finish the record, as a failed StopBox does. Pinned by until_complete_exit_record_write_retries_after_failure.

A session whose shell exits ends as exited with its exit code and an exited event, with no prompt. The record write that ends it is retried, so a write that fails once no longer leaves the box running with no host.

Notes from the agent: Revise round 3. end_on_entrypoint_return now tries the exited-record write up to 5 times, with a backoff of 100 ms times the attempt number. If every attempt fails, the actor still returns Continue and logs an error. A Break would evict the actor and leave an Active record with no actor, and nothing would reconcile that record. With Continue, a min stop can set the record, the same as a failed StopBox. A new cfg(test) seam, exit_record_seam, injects the failures for one box id. cargo clippy -p minimald --all-targets is clean. just clippy-strict did not run.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 2, 2026 22:34
The session-exit prompt is gone: exiting the shell ends the box, which
stays listed as exited. The pty driver no longer answers a prompt. Each
leg that exited a shell now checks the box is exited with code 0, and
destroys it where it used to answer Delete.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 4 of 10 on a /revise note, pushed as 03036b808eb9.

line exit tail
cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-77ad... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-6781... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/rpc/tests.rs
  • scripts/e2e-attach-pty.py
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_actor.rs:2128: Moved the NET-079 doc comment from above EXIT_RECORD_WRITE_ATTEMPTS back onto mod launch_record_seam. The new constants and exit_record_seam keep their own docs.

A session whose shell exits ends as exited, with no prompt. The session e2e now checks this: after each shell exit, min box show must read State: exited and Exit code: 0, and nothing answers a prompt.

Notes from the agent: Revise round 4. This round touched the two e2e scripts the maintainer accepted. scripts/e2e-attach-pty.py: E2E_PTY_ANSWER, ANSWER_LABEL_PREFIX, the exit-prompt wait and the menu parsing are removed, and the module doc is updated. scripts/session-e2e.sh: two new helpers, assert_box_exited / box_exited_check and destroy_box_and_assert_gone. The first polls min box show for State: exited and Exit code: <n>. The legs that answered keep now assert that the box is exited: deny-all host-address fetch, NET-080 install, none-box attach, hooks re-attach, relay paste, NET-138 registered box, alpha box on a named VM, both ask boxes, and attach by name across two VMs. The NET-138 leg asserted the Keep item's text. That text no longer exists, so the leg asserts the exited state. The legs that answered Delete now assert exited, then destroy and assert the box is gone: declared shell, shell fallback, patched .bashrc, relay after the drop, and the sandbox proof. The destroys use min session destroy --force, not bare destroy as the directive wrote. Without a terminal, a destroy of a box with in-session files refuses unless --force is given (destroy_gate in crates/minimal/src/cmd/boxes/lifecycle.rs), and the relay box writes /home/relay-paste. I ran no e2e lane. This box has no KVM and no daemon e2e setup, so the e2e changes are not verified here. Risks for the lanes: (1) Several legs run min session exec on the box after its shell exits, for example the hida/net080 refusal checks after the attach and the NET-138 exec-resumes-the-row check. I did not confirm whether exec resumes an exited box. If exec refuses an exited box, the NET-138 leg fails. The hida/net080 refusal checks then pass without proving anything. (2) In the hooks proof,…[truncated]

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Revise round 2 ran against ci-macos-success, e2e and left it red; a coder round cannot clear it, a person should look. No round was started for the failed suite on 03036b808eb9.

@mitodrummer
Copy link
Copy Markdown
Member

/revise The remaining red (test-kvm and macOS e2e, on two different runners) is one sub-leg of scripts/session-e2e.sh, in the group "min net expose from inside the box … (NET-043, NET-045, NET-046, NET-047)": the "registered box (VM lane) … e2e-inside-kept" part. After the shell exits, the box is now exited (BOX-038), and the step "exec into the kept registered box" correctly gets "box e2e-inside-kept is exited. Only a running box can run a command." An exec runs only in a running box (spec line 568, enforced since T30). Under BOX-025 and user story line 30, an exited session comes back through min session attach (or min box resume) under the same id and name.

Change that sub-leg: after asserting the box is exited and its row was withdrawn after the detach grace (keep both assertions), resume it with min session attach e2e-inside-kept through the pty helper (a non-interactive attach that runs a short command and exits with the shell, or attach then detach, whichever the helper supports now that there is no exit prompt). Assert that the box is running again under the same box id, and that minvmd status --row e2e-inside-kept shows its row again (NET-138). Then run the exec into the running box, as the leg did before. Rewrite the ::error:: text "the CLI must resume its row before the exec" to match. Touch nothing else.

An exec runs only in a running box. After the shell exits and the row is
withdrawn, the leg now resumes the box with a pty 'min session attach',
checks that it runs under the same id and that its row is back, and then
runs the exec as before.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on a /revise note, pushed as 11bad68fd8e8.

line exit tail
cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-ca10... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-6cbd... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/rpc/tests.rs
  • scripts/e2e-attach-pty.py
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

A session whose shell exits ends exited, with no prompt. In the e2e kept-box leg, the exited box comes back running under the same id through 'min session attach', its row shows again in 'minvmd status --row', and the exec into it runs.

Notes from the agent: This round changes only the kept-box sub-leg of scripts/session-e2e.sh (eib_vm_kept_box_resumes_leg), as the /revise note asked. This file is outside the task's layers, and earlier rounds of this pull request already changed it. The leg keeps its two assertions: the box is exited, and its row is withdrawn after the detach grace. Then it resumes the box with a pty 'min session attach e2e-inside-kept' (E2E_PTY_COMMANDS='echo KEPT_RESUMED', E2E_PTY_DETACH=1). The detach chord keeps the resumed shell running for the exec. It checks that 'min box show e2e-inside-kept' reads State: running and the same ID, and that 'minvmd status --row e2e-inside-kept' exits 0. Then it runs the exec and the checks that follow, which do not change. The old ::error:: text is now 'an exec into the resumed registered box failed (NET-138)'. I also updated the leg's header comment, because it said that the exec resumes the row. I did not run the e2e itself. It needs a KVM or macOS VM lane, and this box cannot run it. The test-kvm and macOS e2e lanes will prove it.

@mitodrummer
mitodrummer marked this pull request as ready for review November 2, 2026 23:40
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The compare since 975bb0b2 changes only the e2e scripts and the position of a doc comment. The e2e now checks that each box whose shell exits is listed as exited with exit code 0, as architecture.md line 499 says. The kept-box leg resumes the exited box with min session attach before its exec, as line 491 allows. Teardowns now fail the run when a destroy misses. The earlier PASS stands.

Ruled at 11bad68 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

@mitodrummer
Copy link
Copy Markdown
Member

/revise Two race fixes from my review at d19cf31, in the layers.

  1. (medium) An attach or exec racing the natural end relaunches the box. In crates/minimald/src/box_actor.rs (~1496-1500), the mainloop's biased select handles every queued message ahead of the entrypoint watch. When the entrypoint has returned but an Attach/EnsureHost (or an exec that ensures a host) is already queued, it runs first. It finds no live host and an Active record, and ensure_host/mint_session_host (box_actor/host.rs ~1255) launches a new host, replacing entrypoint_watch (host.rs ~1067). For an until_complete box, the entrypoint then runs again, the exit code is lost, and no exited event is written (against BOX-038). Make the exit win: before handling a message that would launch a host, check entrypoint_watch (for example now_or_never), or check it in ensure_host/attach when the host slot is dead, and end the box first. A message that then finds an exited box gets the normal exited-box answer, so attach resumes per BOX-025 and exec is refused. Keep only Stop, StopBox and Destroy ahead of the exit, as the existing comment intends. Add a test that queues an attach/EnsureHost at the moment the entrypoint returns, and asserts the box ends exited with its code and an exited event, and is not relaunched silently.

  2. (low) A min stop racing the natural end returns "the session ended before its stop finished". In crates/minimald/src/box_manager.rs (~2355-2371), the fallback accepts only RecordState::Stopped. Also accept RecordState::Exited, answering that the box is already exited, as success with nothing to stop. Add a test.

Touch nothing else.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — every review thread is resolved; @mitodrummer closed the last one. The review of d19cf31606a4 withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Nov 3, 2026
A message queued beside a returned entrypoint ran first and launched
a new host, so the entrypoint ran again and its exit was lost. Now
only a stop or a destroy goes ahead of the exit, and ensure_host
refuses a stopped or exited box.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The new commit conforms to architecture.md line 499. An entrypoint exit now ends the box before a queued attach or exec, so the exit is not lost. ensure_host refuses an ended box, as line 497 limits exec to a running box. The new actor test proves this ordering. The merges of main add nothing of their own. The PASS on d19cf316 stands for everything else.

Ruled at d3074a9 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/box_runtime.rs Resolved
@minimal-foundry minimal-foundry Bot removed the review:approved minimal-review approved the current head label Nov 3, 2026
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 7 of 10 on a /revise note, pushed as d3074a9b0452.

line exit tail
cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-05da... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-5920... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/rpc/tests.rs
  • scripts/e2e-attach-pty.py
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

A session whose shell exits ends exited, with no prompt. An attach or an exec that is queued at the moment the shell exits no longer starts the shell again: the box ends exited with its exit code and an exited event first.

Notes from the agent: Item 1 (medium) is fixed. In the mainloop, every message except Stop, StopBox and Destroy now checks the entrypoint watch with try_recv first. If an exit is already in, the actor ends the box before it handles the message. After the message, the actor ends unless the message resumed the box (an attach resumes per BOX-025). ensure_host now refuses a stopped or exited record, so an exec gets 'box is exited. Only a running box can run a command.' and no host starts. The new test until_complete_ends_on_entrypoint_return_without_client_before_queued_ensure_host holds the actor, lets the entrypoint return, queues an EnsureHost, and then releases the actor. It asserts the exec is refused, the record is Exited { exit_code: 0 }, an exited event carries reason and code, and no second started event exists. With the check disabled, the test fails with 'the queued exec launched a host in an exited box'. Item 2 (low) is not done. The fallback is in crates/minimald/src/box_manager.rs, and the task's layers do not name that file. The fix is also wider than that file: stop_box returns StopEnding, which cannot express an exited box. To answer success for an Exited record, the return type must change, and so must the RPC answer and the min CLI text that read it. Do it in its own task that names box_manager.rs, the stop RPC and the CLI stop output. clippy -p minimald --all-targets is clean. just clippy-strict was not run.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 3, 2026 01:02
@mitodrummer
mitodrummer marked this pull request as ready for review November 3, 2026 01:10
@mitodrummer mitodrummer left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at d3074a9. A box whose until_complete entrypoint returns ends exited with its code and record, attached or not, and a queued attach or exec can't relaunch it first (pinned by until_complete_ends_on_entrypoint_return_without_client_before_queued_ensure_host). Client loss and idleness never stop a box. The exit-record write retries. The e2e now expects exited and resumes by attach before exec (BOX-025), with no session-exit prompt. Remaining stop-vs-exit races are T61 #2862.

…when-its-entrypoint-returns-never-for

# Conflicts:
#	crates/minimald/src/box_runtime.rs
#	crates/minimald/src/box_runtime/binding.rs
#	crates/minimald/src/box_runtime/tests.rs
@mitodrummer
Copy link
Copy Markdown
Member

/revise Merge origin/main and resolve the conflicts. T10 (#2852) just merged and touches the same box runtime and binding code. Keep both sides:

  • T10's output log, written from the box's start in Host::run's pty read arm, and its store-box log-open refusal in Host::build;
  • T10's binding change: ClientLost runs control.detached() and records the detached event (BOX-016) for Detach, ClientLost and Shed;
  • this PR's natural-end behaviour: the entrypoint-return watch, no session-exit prompt for until_complete boxes, the exit-wins-before-queued-messages check, and the exit-record retry.
    Where both changed Binding::run, a shell exit on an until_complete box ends the box (no prompt), and a client loss records detached and runs on_detach without ending it. Run this PR's test lines and T10's pty_client_loss_records_detach, pty_client_loss_records_detach_and_runs_detach_hooks and box_output_logged_from_start_complete_after_disconnect. Touch nothing else.

@minimal-foundry
minimal-foundry Bot commented Nov 3, 2026 8
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2401. Your note is kept and goes into the next round here, which starts when that coder finishes.

Nothing to type again: the command gets a rocket when its round starts, or a refusal that names the reason. No round was counted for it yet.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The merge of main (#2852) into this branch keeps the ruling on d3074a9b. The new ClientLost arm in binding.rs ends only the binding and writes a detached event, and the box runs on. That conforms to architecture.md line 499. A box whose lifetime is until_complete still ends with its entrypoint, with no prompt. No condition is open.

Ruled at 8178ac9 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

A merge of main before round 8, not counted against the cap (1 of 3), pushed as 8178ac9c2050.

Resolved conflicts in: crates/minimald/src/box_runtime.rs, crates/minimald/src/box_runtime/binding.rs, crates/minimald/src/box_runtime/tests.rs

line exit tail
cargo nextest run -p minimald until_complete_ends_on_entrypoint_return_without_client 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-d4a9... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald daemon_never_stops_for_idle_or_client_loss 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-6af1... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/rpc/tests.rs
  • scripts/e2e-attach-pty.py
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

A session whose shell exits ends exited, with no shell-exit prompt. The branch now merges cleanly with main, so the box log and detached event from main also apply.

Notes from the agent: Conflict round: I resolved the three conflicted files and committed the merge. Binding::spawn now takes ends_with_entrypoint from this branch and box_dir from main. For client loss, I took main's arm: a channel close or EOF breaks the binding loop with ClientLost. The box runs on and gets a detached event, so BOX-015 still holds. That arm replaces this branch's remote_open flag, and I added a BOX-015 comment to it. In tests.rs I kept both sides: this branch's until_complete_host and its test, and main's StoredBox helper. StoredBox::params now sets ends_with_entrypoint: false. An extra run of every box_runtime:: test also passed (101 tests). I did not run clippy-strict.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 3, 2026 02:17
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 8 of 10 on a /revise note: the coder changed nothing. The merge of origin/main with T10 (#2852) is already on the branch at 8178ac9, so this round needed no new commit. I checked the resolution against the note. Binding::run keeps the ProcessExited-if-ends_with_entrypoint arm with no prompt. ClientLost keeps control.detached() and the BOX-016 record_detached for Detach, ClientLost and Shed. Host keeps T10's BoxOutputLog::open in build and the write in the pty read arm. Host keeps this PR's requested flag on HostExit. The exit-record retry and the exit-before-queued-message check in box_actor.rs are unchanged. I ran all five named tests in one nextest invocation: 8 passed, 0 failed. Nothing in the tree changed.

@mitodrummer
mitodrummer marked this pull request as ready for review November 3, 2026 02:25
@mitodrummer mitodrummer left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at 8178ac9, the merge with T10 (#2852). Both sides are kept: T10's output log (opened before launch, written in the pty read arm) and its detached/on_detach handling and tests, plus this PR's exit-first check and natural-end behaviour. The tests lane passes on this head.

@mitodrummer
mitodrummer added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit f6fcbab Nov 3, 2026
36 checks passed
@mitodrummer
mitodrummer deleted the agent/2401-end-a-box-when-its-entrypoint-returns-never-for branch November 3, 2026 03:15
norrietaylor added a commit that referenced this pull request Nov 3, 2026
* fix(deps): bump pulldown-cmark to 0.13.6 for RUSTSEC-2026-0433 (#2846)

Claude-Session: https://claude.ai/code/session_01NSAwDroXL6KqsSxUzdbZGw

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(minimald): hand a reaped name over in one manager step (#2842)

A reap read the records and then sent the handover as a second message.
A removal that began between the two left the name with no row while
another retained box still had it. The manager now releases the reaped
box's row, picks the successor among boxes not being removed, and
registers it in one message. The reap and the prune return after both.

The daemon log names the release (name-released) before the handover
(name-handed-over), both with box ids.

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T126 — A host-address box's port that collides on the host is shown at session start and in `min ls` (#2830)

* feat(minvmd): answer a node's collided mirror ports over control

The host control socket answers a read-only mirror_collisions verb
with the node's mirrored ports that collided with a holder on the
host. The node mirror keeps the time each port first collided.

* feat(minimal): show collided mirror ports in min ls and activate

min ls prints a MIRROR PORTS row per VM whose host_ip box ports are
held by another program on the host, and --json carries
mirror_collisions per VM. min session activate of a host_ip box on a
VM host prints the same sentence per port when it does not attach.

* fix(minimal): show an unread VM's mirror ports as null in min ls

Read each VM's collided mirror ports concurrently, and keep a failed
read apart from an empty one: --json writes null for a VM with no
answer and [] only for an answered empty read.

* fix(minimal): name a VM host whose mirror ports were not read

min ls printed nothing for a running VM host that gave no answer, so
it looked the same as one with no collisions. It now prints one
MIRROR PORTS row that says the VM did not report its ports.

* fix(minimal): skip the mirror-port read on a native host

A native host has no VM host daemon. The fallback listing's control
socket there is minimald's, which does not serve the collided-port
read, so min ls printed a false MIRROR PORTS row on every native host.

* fix(minimal): name an unread VM host at activate, enter the span

min session activate now prints the unread sentence that min ls prints
when the VM host daemon gives no answer. The ListSessions span wraps the
collided-port reads, and a native host's --json carries no
mirror_collisions field.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T42 — Start every box's entrypoint from its spec through one launcher (#2841)

* feat(mfile): carry entrypoint and lifetime in the box spec

The spec gains an [execution] table with the box's lifetime and, for a
box that does not run its login shell, its entrypoint argv. A v0 spec
stored before the table existed reads with the entrypoint its type
implies.

* feat(minimald): start each box's entrypoint from its spec

One launcher decision, LaunchSpec::entrypoint, reads the first process
from the box's stored spec: the login shell, or the spec's argv. The
actor reads the stored spec once, keeps its hold, and hands it to every
launcher. A launch minted for an exec keeps the shell, because the exec
runs the box's command. Each launch logs its entrypoint in a
box.launch span.

* fix(minimald): keep the shell on every launch of an exec-run box

A task's run arrives over the exec channel. A hook launch or an attach
can come before or after that exec, and a hook launch's gated first
process starts at the next attach. Only the exec-minted launch held
the shell, so a task box could run its task twice, and an attach got
the task, not a shell. Every launch now says an exec carries the run,
and the launcher starts the shell for it.

* refactor(mfile): write box.lifetime and execution.exec in the spec

Rename Lifetime::Persistent to UntilStopped, move the lifetime to
[box] with a until_complete default for v0 specs, and serialize the
entrypoint as execution.exec. List the argv shape as a v0 exception.

* fix(minimald): refuse a launch whose stored spec cannot be read

held_spec returns an error for unreadable spec bytes or a failed store
read, and launch_host refuses with SpawnFailed instead of starting the
shell. Mark run_by_exec as an interim that T43 (#2405) removes.

* test(minimald): store a readable spec in the stopped-box record test

A launch now refuses a stored spec it cannot read, so the test's
placeholder bytes failed its ensure_host. Store a session spec instead.

* docs(mfile): say that no launch reads the spec's lifetime yet

A box ends when its entrypoint returns, whatever box.lifetime says.

* fix(minimald): keep entrypoint arguments out of the launch log

The box.launch span and its info event printed every argument of an
argv entrypoint. An argument can carry a secret, so they now name the
program (or the login shell) and the argument count only. A test pins
that a secret-looking argument does not reach the log.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Co-authored-by: Karl Godard <karl@minimal.dev>

* T30 — Run a command in a box with a terminal or detached, under an exec id (#2843)

* feat(minimald-rpc): carry exec mode, timeout and exec id on the wire

min://exec carries an argv with its mode (attached, tty or detached) and
an optional timeout. min://exec/attach, min://exec/output and
min://exec/wait name an exec by its UUIDv7 id. The box store places
each exec's transcript beside the record and appends events by path.

* feat(minimald): run box execs under an id, with a terminal or detached

Every command for a box gets a UUIDv7 exec id, and a box that is not
running refuses it with exit 2 naming its state. min://exec runs an
argv with no shell: attached, on a daemon terminal, or detached with
stdin closed. Tty and detached execs answer with their id at once,
capture their output beside the box record, and keep their exit for
min://exec/wait. An exec's own timeout stops it and records an
exec_exited event with reason timeout, and the box keeps running.

* style(minimald-rpc): format the exec id parse and its tests

* fix(minimald): bound the wait after an exec timeout; drop reaped execs

A command that survives the stop at its timeout no longer holds its
wait open: the wait after the stop is bounded, then SIGKILLed and
recorded as a timeout. Ended execs of a reaped box leave the exec map.

* fix(minimald): exit 124 on exec timeout; wait reads stored exit

An exec that its own timeout ends now exits 124, attached or detached.
A wait on an exec that this daemon holds no entry for reads the box's
stored exec_exited event, so a detached exec's exit code outlives a
daemon restart.

* fix(minimald): cap exec transcripts at 8 MiB and stream reads

A detached exec now writes through a pipe and a pump, as a tty exec
does, so the daemon can cap its transcript. Past the cap the transcript
keeps a mark and the newest half of the cap, and the command keeps
running. The output read and the attach replay copy the transcript in
64 KiB chunks, up to its length at the open, and the attach takes the
terminal lock only to open the file and subscribe.

* fix(minimald): bound tty exec input; no deadline past the clock

A tty exec's terminal input went through an unbounded channel, so a
program that never reads stdin let the daemon queue grow without
limit. The terminal now holds typed input in a HeldInput with the
attached path's bounds (EXEC_INPUT_HELD_MAX per exec,
EXEC_INPUT_HELD_DAEMON_MAX for the daemon). Past either bound the
chunk is dropped and the log says so once per exec. The exec's end
releases what it held.

An attached exec's timeout too large for the clock panicked after the
spawn. timeout_deadline now uses checked_add and gives no deadline.

Tests: unread tty input stays within the cap, a timeout of u64::MAX
seconds runs the exec to its end, and an exec id from one box finds no
exec, output or exit in another.

* fix(minimald): wait on exec terminals in the runtime, not a thread

The tty feed wrote to the terminal from a blocking thread. When the
program never read its input, that write never returned, and each such
exec left a blocking thread behind for good. The terminal and the
detached pipe are now non-blocking and watched through AsyncFd, so the
supervisor can stop the pump and the feed at the exec's end. Both stop
on a closed state too, because tokio never clears it and a wait for it
returns at once.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Co-authored-by: Karl Godard <karl@minimal.dev>

* fix(minimald): hand a destroyed box's name to the next holder (#2849)

A destroy removed the box that held a name's row and left the name
with no row, while another retained box still had that name. The
destroy now runs the same handover as a reap: the row goes to the
running box with the name, else to the most recent creation. The
destroy answers after the handover.

The daemon log names the destroyed box and the successor, and the
destroy span records the successor id or "none".

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T114 — `min ls` and the box commands tell the truth, end to end (#2839)

* test(e2e): prove min ls and the box commands across two VMs

Adds the ls_and_box_commands_tell_the_truth case: a stopped VM's boxes
in min ls, the ADDRESS and EGRESS cells against min session policy,
policy and forward without --vm, the unknown-box and stopped-VM
refusals, both proxy ports after the preferred port frees, and the
--public and out-of-range expose refusals.

* test(e2e): record ls_and_box gates as NOT RUN, check declared rules

The four gates of ls_and_box_commands_tell_the_truth now call not_run,
so the lane summary names the case. A 'declared rules' EGRESS cell is
now checked against the subnets rule row that min session policy
prints, not only against an egress heading.

* test(e2e): read the expose address past a trailing carriage return

The ls_and_box case read the box's address from the in-box expose reply
with an end-of-line anchor. A reply over the exec channel can end in a
carriage return, which left the address empty.

* test(e2e): brace lt_port before a bracket class in the expose read

shellcheck reads $lt_port[[:space:]] as an array index (SC1087).
The braces keep the same match and make the script lint-clean again.

* fix(minimal): poll the owning VM in min net forward

A forward to a box on another VM polled the selected VM for its
session. That VM does not hold the box, so the forward closed on the
first tick. Reconnect the poll to the owning VM, version-gated.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Co-authored-by: Norrie Taylor <91171431+norrietaylor@users.noreply.github.com>

* docs(spec): specify dns steering and the host_ip lane for spec 24 (#2851)

* docs(spec): specify dns steering and the host_ip lane for spec 24

Add BEP-080 to BEP-087 so the default `dns` steering works for own_ip
and host_ip boxes on a VM host, as architecture D6 and networking §5.4
require.

- BEP-080, BEP-081: direct TLS to the proxy on port 443, routed by
  SNI, and admitting dns and both on a VM host (#2805).
- BEP-082 to BEP-087: the host_ip lane: a host-side box id, the cohort
  proxy lane, cohort-attributed delivery with the named box's egress
  and audit subject, the in-effect rule, per-box steering, and the
  live proof.
- BEP-015 gains a host_ip proxy_env case.
- Security considerations states the cohort lane and per-box steering.

* docs(spec): keep the first slice paragraph unchanged and move the host_ip e2e checks to BEP-087

* docs(spec): address review on the host_ip lane requirements

- BEP-083: the cohort lane also admits TCP 443 at the proxy's address,
  the direct TLS path dns steering uses.
- BEP-084: refuse and audit a cohort-attributed value that names no
  live host_ip box of this host.
- BEP-086: cite NET-003's VM-backed resolver requirement, not NET-079.
- BEP-087: the proxy_env case is its own sub-requirement.
- Security considerations: BEP-083 and BEP-084 join the redemption
  invariant, and BEP-084 the egress invariant.

* T10 — Log a box's output from its start, out of the box's reach (#2852)

* feat(boxes): name a box's output log beside its record

Adds LOG_FILE and BoxDir, the box's directory as code outside the store
sees it: the log path, a lookup by id through the index, a log read by
offset, and an event append for a writer that holds no DiskBox.

* feat(minimald-rpc): add a streamed read of a box's output log

* feat(minimald): log a box's output from its start

The host appends every read from the box's terminal to its output log
before it hands the bytes to an attached client, so the client is a
second reader. A binding whose client closes its channel, drops its
connection or is shed records a detached event, and the box runs on.

* feat(minimald): serve a box's output log by its id

ReadLog streams the log from a byte offset, and with follow sends new
output until the box is reaped. A test shows that files written into
the box's own directories reach neither the log nor the record.

* fix(minimald): read a box's log on the blocking pool

The followed log read polled the store w
ABD2
ith blocking std::fs calls on an
async task. The find, the chunk reads and the retained check now run
through spawn_blocking, so a slow disk does not hold a runtime worker.

* fix(boxes): cap a box's output log and read it across a drop

The log keeps a drop mark and the newest half past 8 MiB. A reader
keeps its file open and moves to the replaced file with no byte sent
twice. The read refuses a link or a file that is not regular.

* fix(minimald): start no store box without its output log

BoxOutputLog::open returns the open's error for a box in the store, and
Host::build stops before the launch. The log writes through the capped
store writer, and ReadLog keeps one reader for the whole stream.

* fix(minimald): run detach hooks when a PTY client is lost

A lost client already records a detached event. Its on_detach hooks now
run too, so the event and the hooks agree. A test drives a box past the
log cap.

* style: format the log cap tests

* docs(boxes): mark the 8 MiB log cap as not yet in the design

The cap drops the start of a box's output past 8 MiB. That departs from
architecture.md line 473. gominimal/arch#159 decides the bound.

* docs(specs): record the interim 8 MiB box log cap in BOX

The decision that logs a box's output from its start now states the
8 MiB cap, what the log keeps past it, and the arch#159 decision that
removes the cap or changes BOX-155.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T9 — End a box when its entrypoint returns, never for idleness or client loss (#2850)

* feat(minimald): end an until_complete box when its entrypoint returns

The host loop now says whether a kill ended it. The actor watches each
launch's loop and, when the entrypoint returned on its own and the spec
sets lifetime = "until_complete", sets the record to exited with the
exit code and records an exited event with reason "exit". The daemon
does this whether or not a client is attached. Such a box raises no
shell-exit prompt (BOX-038). A client that goes away never stops a box
(BOX-015).

* test(minimald): pin box ends on entrypoint return, not client loss

Adds the actor tests for BOX-038 and BOX-015. The shell round trip now
expects no prompt and an exited record. Tests about a box kept after its
shell exits now create an until_stopped box. The rpc shutdown test no
longer answers a prompt.

* fix(minimald): retry the exited-record write when an entrypoint ends

The host and the watch are gone by the time the write runs, so a
single failed write left the box running with nothing in it. Retry it
with a short backoff, and pin the retry with a test seam.

* test(e2e): expect an exited box, not a prompt, when a shell exits

The session-exit prompt is gone: exiting the shell ends the box, which
stays listed as exited. The pty driver no longer answers a prompt. Each
leg that exited a shell now checks the box is exited with code 0, and
destroys it where it used to answer Delete.

* docs(minimald): put the launch-record seam doc back on its module

* test(e2e): resume the exited kept box before the exec into it

An exec runs only in a running box. After the shell exits and the row is
withdrawn, the leg now resumes the box with a pty 'min session attach',
checks that it runs under the same id and that its row is back, and then
runs the exec as before.

* test(minimald): fail when the channel stays open after shell exit

The wait loop ended on a timeout as well as on close, so the test
passed even if the channel never closed. Panic on the timeout.

* fix(minimald): end an exited box before a queued attach or exec

A message queued beside a returned entrypoint ran first and launched
a new host, so the entrypoint ran again and its exit was lost. Now
only a stop or a destroy goes ahead of the exit, and ensure_host
refuses a stopped or exited box.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* build(deps): bump sysinfo to 0.40

sysinfo 0.40 makes System construction fallible (System::new returns
Result<System, sysinfo::Error>). The three callers keep their 0.39
behaviour on an unsupported platform: minvmd's metrics sample and
minimald's resource-pool probe return None, and minvmd's host capacity
probe reports zero memory.

Unblocks the dependabot cargo group (#2844), which bumped sysinfo
without migrating the callers.

---------

Co-authored-by: Karl Godard <karl@minimal.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.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.

T9 — End a box when its entrypoint returns, never for idleness or client loss

1 participant

0