Skip to content

feat(minimald): implement session destroy + RPC - #462

Merged
twitchyliquid64 merged 1 commit into
mainfrom
tom/session-destroy
Jun 18, 2026
Merged

feat(minimald): implement session destroy + RPC#462
twitchyliquid64 merged 1 commit into
mainfrom
tom/session-destroy

Conversation

@twitchyliquid64

@twitchyliquid64 twitchyliquid64 commented Jun 17, 2026

Copy link
Copy Markdown
Member
$> echo "{\"id\": \"019eb7a0-36d9-7d41-8286-265d76e9b3ab\"}" | \
    ssh -s  -o ProxyCommand="socat - UNIX-CONNECT:$XDG_STATE_DIR/minimal/providers/local-0/ssh.sock" \
                -o 'UserKnownHostsFile=/home/xxx/.local/state/minimal/providers/local-0/known_hosts' \
                local-0 minimald-v1-DestroySession
$> 

Summary by CodeRabbit

New Features

  • Added session-destroy support that cleanly stops running sessions and deletes their on-disk records.
  • Sessions manager now removes destroyed sessions from listings and frees names for reuse.
  • Teardown is now acknowledged, allowing callers to await completion.
  • Exposed the capability via a new SSH oneshot RPC endpoint.

Bug Fixes

  • Improved PTY master error handling to prevent panics and ensure orderly teardown on read/write readiness and IO failures.
  • Improved teardown reliability to ensure the session process is reaped and termination outcomes are handled safely.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements end-to-end session destruction. The sessions/store gains a disk-level delete API. session_host::Host::spawn now returns a JoinHandle alongside the HostHandle for awaitable teardown, and PTY error handling is centralized via notify_remote_pty_err. The Session actor gains a Destroy message processed with ControlFlow, the sessions::Manager gains DestroySession that cascades teardown through all layers, and a new DestroySession RPC is wired end-to-end with server dispatch and integration tests.

Changes

Session Destruction End-to-End

Layer / File(s) Summary
Disk store delete API and index removal
crates/sessions/src/store.rs
Loader trait gains delete method tolerating missing directories; Index gains remove and name_by_id lookup methods; DiskLoader::delete flushes index first then removes the directory tree; tests verify deletion clears queries, frees names for reuse, and persists across reinitializations.
Host::spawn returns JoinHandle and centralized PTY error handling
crates/minimald/src/session_host.rs
Host::spawn returns (HostHandle, JoinHandle) instead of detaching; notify_remote_pty_err centralizes PTY master failure notification; Message::Kill warns on kill failure and forces teardown via Err(()); readable/writable panic and todo! paths replaced with unified error handling; test verifies kill terminates mainloop within timeout.
Session actor Destroy message and ControlFlow loop exit
crates/minimald/src/session.rs
SessionMessage::Destroy variant added with oneshot acknowledgement; Session::host stores (HostHandle, JoinHandle) tuple; handle_message returns ControlFlow and mainloop breaks on destroy; GetHostAttrs and attach destructure the new tuple; internal destroy() kills host and awaits the handle; SessionHandle::destroy() sends message and awaits acknowledgement.
Manager DestroySession message, handler, and API
crates/minimald/src/sessions.rs
ManagerMessage::DestroySession added; Manager::handle_message resolves ID to key, optionally awaits running session's destroy(), then calls DiskLoader::delete; ManagerHandle::destroy_session exposed as public API; three async tests cover non-running deletion with name reuse, running session teardown, and NotFound for unknown IDs.
RPC contract definition and server dispatch
crates/minimald-rpc/src/lib.rs, crates/minimald/src/rpc.rs
minimald-rpc defines DestroySession RPC type with request/response and OneshotSshRpc implementation; minimald adds serve_destroy_session handler calling sessions manager and extending handle_ssh_rpc dispatch by subsystem name; integration tests verify destroyed sessions vanish from ListSessions/GetSessionRecord and unknown IDs return Errorable::Err.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ManagerHandle
  participant Manager
  participant SessionHandle
  participant SessionActor
  participant HostHandle
  participant DiskLoader

  rect rgba(70, 130, 180, 0.5)
    note over Client,ManagerHandle: Public API call
    Client->>ManagerHandle: destroy_session(id)
    ManagerHandle->>Manager: DestroySession(id, responder)
  end

  rect rgba(180, 100, 70, 0.5)
    note over Manager,HostHandle: Cascading teardown
    Manager->>Manager: resolve id → key
    alt session running
      Manager->>SessionHandle: destroy().await
      SessionHandle->>SessionActor: Destroy(oneshot_tx)
      SessionActor->>HostHandle: kill()
      HostHandle-->>SessionActor: mainloop JoinHandle resolves
      SessionActor-->>SessionHandle: oneshot_tx.send(())
    end
  end

  rect rgba(70, 180, 100, 0.5)
    note over Manager,Client: Disk cleanup and response
    Manager->>DiskLoader: delete(key)
    DiskLoader-->>Manager: Ok(())
    Manager-->>Client: Ok(())
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • gominimal/minimal#375: Introduces foundational HostHandle::kill and session lifecycle teardown behavior that this PR builds upon for coordinated destruction.
  • gominimal/minimal#420: Refactors oneshot SSH RPC definitions into minimald-rpc crate; this PR extends that refactored dispatch with the new DestroySession RPC handler.

Suggested reviewers

  • 0chroma
  • evanspearman
  • norrietaylor

Poem

🐇 Hop, hop — no zombie shall remain,
The sessions vanish, disk and host and frame.
ControlFlow breaks the loop with grace,
JoinHandle joins at teardown's pace.
A rabbit tidies every trace! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(minimald): implement session destroy + RPC' clearly and concisely summarizes the main changes across all modified files, which collectively add session destruction functionality and RPC support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
crates/minimald/src/sessions.rs (1)

361-394: ⚡ Quick win

Exercise destroy with an attached host, not just a running actor.

This test populates running, but Session::host remains None; it will not catch regressions in the HostHandle::kill / host JoinHandle teardown path. Add a manager-level test that opens a shell, calls destroy_session(id), and asserts the channel closes and the session no longer resolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/sessions.rs` around lines 361 - 394, Add a new separate
async tokio test function that exercises the destroy path with an attached host.
In this test, create a session using the manager, open a shell on the session
(which attaches a host), then call destroy_session with that session id. After
destroying, assert that the session channel is closed and that subsequent calls
to get_session no longer resolve the session. This will exercise the
HostHandle::kill and host JoinHandle teardown path that the current
destroy_tears_down_a_running_session test does not cover.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/minimald/src/session_host.rs`:
- Around line 822-826: The notify_remote_pty_err method is using tx.send().await
which can cause a deadlock during teardown if the binding queue is full or
blocked forwarding data. Replace the awaited tx.send() call with try_send() when
sending BindingMsg::TeardownDueToStdoutErr(e) to avoid blocking the host thread
during teardown. This allows the teardown notification to be sent best-effort
without creating a circular wait condition between the host and binding.

In `@crates/minimald/src/session.rs`:
- Around line 138-141: The SessionMessage::Destroy handler silently discards
errors from both the self.destroy().await operation and the r.send(()) call
without justification, violating Rust error handling standards. Since the
manager deletes the session's disk tree immediately after destroy returns,
teardown failures must be propagated before that deletion occurs. Replace the
current pattern in the Destroy handler to check and propagate errors from both
self.destroy().await and r.send(), ensuring that success is only acknowledged if
both operations actually succeed rather than silently discarding the results.
Apply the same error propagation fix to the similar patterns mentioned at lines
153-161 and 320-328.

In `@crates/sessions/src/store.rs`:
- Around line 89-96: The delete() method currently returns NotFound when the
session directory is already missing, leaving stale index entries in short_to_id
and name_to_id behind, which violates the documented contract that a missing
directory tree is not an error. Fix the delete() implementation (around line 389
where record.json is read) to handle the case where the directory is already
gone by either reading the record after index cleanup or catching the NotFound
error and proceeding with index cleanup regardless, ensuring that stale index
entries are always removed even when the directory tree is missing.

---

Nitpick comments:
In `@crates/minimald/src/sessions.rs`:
- Around line 361-394: Add a new separate async tokio test function that
exercises the destroy path with an attached host. In this test, create a session
using the manager, open a shell on the session (which attaches a host), then
call destroy_session with that session id. After destroying, assert that the
session channel is closed and that subsequent calls to get_session no longer
resolve the session. This will exercise the HostHandle::kill and host JoinHandle
teardown path that the current destroy_tears_down_a_running_session test does
not cover.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83537a3d-953e-4233-8162-35945b18f274

📥 Commits

Reviewing files that changed from the base of the PR and between ccd3213 and 1ed65bf.

📒 Files selected for processing (4)
  • crates/minimald/src/session.rs
  • crates/minimald/src/session_host.rs
  • crates/minimald/src/sessions.rs
  • crates/sessions/src/store.rs

Comment thread crates/minimald/src/session_host.rs
Comment on lines +138 to +141
SessionMessage::Destroy(r) => {
self.destroy().await;
let _ = r.send(());
return ControlFlow::Break(());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 'SessionMessage::Destroy|async fn destroy|task\.await|destroy_session' crates/minimald/src/session.rs crates/minimald/src/sessions.rs

Repository: gominimal/minimal

Length of output: 6873


🏁 Script executed:

cat crates/minimald/src/session.rs | head -170 | tail -40

Repository: gominimal/minimal

Length of output: 1479


🏁 Script executed:

find . -name "rust-coding-standards.md" -o -name "*coding*standards*" | head -20

Repository: gominimal/minimal

Length of output: 93


🏁 Script executed:

rg -n "SessionMessage::Destroy" crates/minimald/src/session.rs -A 10 -B 5

Repository: gominimal/minimal

Length of output: 1350


🏁 Script executed:

cat docs/rust-coding-standards.md

Repository: gominimal/minimal

Length of output: 4815


🏁 Script executed:

rg -n "destroy_session" crates/minimald/src/sessions.rs -A 15 | head -50

Repository: gominimal/minimal

Length of output: 1977


🏁 Script executed:

grep -n "store.delete\|destroy.*await" crates/minimald/src/sessions.rs | head -20

Repository: gominimal/minimal

Length of output: 264


Propagate host teardown failures before acknowledging destroy — violates error handling standards.

The Rust coding standards explicitly forbid silent error discards without justification ("Never swallow errors. No let _ = result; without a comment justifying it"). Lines 138–141 and 153–161 discard JoinHandle results, causing SessionMessage::Destroy to acknowledge success even if the host task panicked or mainloop() failed. This breaks the semantic contract: the manager (sessions.rs:223–225) deletes the session's disk tree immediately after destroy returns, so teardown failures must propagate before that deletion occurs.

Suggested shape
-    Destroy(oneshot::Sender<()>),
+    Destroy(oneshot::Sender<Result<(), std::io::Error>>),
             SessionMessage::Destroy(r) => {
-                self.destroy().await;
-                let _ = r.send(());
+                let result = self.destroy().await;
+                let _ = r.send(result);
                 return ControlFlow::Break(());
             }
-    async fn destroy(&mut self) {
+    async fn destroy(&mut self) -> Result<(), std::io::Error> {
         if let Some((host, task)) = self.host.take() {
             // Signal the process to die, then await the runtime loop so the
             // sandbox files backing its rootfs are released before the caller
             // removes the session's directory tree.
             let _ = host.kill().await;
-            let _ = task.await;
+            let _exit = task.await.map_err(std::io::Error::other)??;
         }
+        Ok(())
     }
-    pub(crate) async fn destroy(&self) {
+    pub(crate) async fn destroy(&self) -> Result<(), std::io::Error> {
         let (send, recv) = oneshot::channel();
         // Ignore send errors - the recv will also fail.
         let _ = self.0.send(SessionMessage::Destroy(send)).await;
         // If the actor died before acking, the session is gone all the same.
-        let _ = recv.await;
+        recv.await.unwrap_or(Ok(()))
     }

The manager caller should then propagate hnd.destroy().await? before store.delete(&k)?.

Also applies to lines 153–161 and 320–328.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/session.rs` around lines 138 - 141, The
SessionMessage::Destroy handler silently discards errors from both the
self.destroy().await operation and the r.send(()) call without justification,
violating Rust error handling standards. Since the manager deletes the session's
disk tree immediately after destroy returns, teardown failures must be
propagated before that deletion occurs. Replace the current pattern in the
Destroy handler to check and propagate errors from both self.destroy().await and
r.send(), ensuring that success is only acknowledged if both operations actually
succeed rather than silently discarding the results. Apply the same error
propagation fix to the similar patterns mentioned at lines 153-161 and 320-328.

Comment thread crates/sessions/src/store.rs
@twitchyliquid64 twitchyliquid64 changed the title feat(minimald): implement session destroy/teardown feat(minimald): implement session destroy + RPC Jun 17, 2026

@coderabbitai coderabbitai 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.

♻️ Duplicate comments (1)
crates/sessions/src/store.rs (1)

89-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale docstring references record read that no longer occurs.

The implementation now looks up the session name from the in-memory index (line 400), not from the on-disk record. The documented error condition "if the record cannot be read" is no longer accurate.

Suggested fix
     /// Deletes the session with the given key, dropping its index entries and
     /// removing its on-disk directory tree (record, workspace, home, cache).
     ///
     /// # Errors
     ///
-    /// Returns an I/O error if the record cannot be read or the index cannot be
-    /// flushed. A missing directory tree is not an error.
+    /// Returns an I/O error if the index cannot be flushed or the on-disk tree
+    /// cannot be removed. A missing directory tree is not an error.
     fn delete(&mut self, key: &Self::Key) -> Result<(), std::io::Error>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sessions/src/store.rs` around lines 89 - 96, The docstring for the
delete method contains a stale error condition that references reading the
on-disk record, but the current implementation retrieves the session name from
the in-memory index instead. Update the # Errors section of the delete method's
docstring to remove or correct the outdated reference to record reading,
ensuring the documentation accurately reflects that the error conditions are
limited to index flushing failures and not record I/O operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@crates/sessions/src/store.rs`:
- Around line 89-96: The docstring for the delete method contains a stale error
condition that references reading the on-disk record, but the current
implementation retrieves the session name from the in-memory index instead.
Update the # Errors section of the delete method's docstring to remove or
correct the outdated reference to record reading, ensuring the documentation
accurately reflects that the error conditions are limited to index flushing
failures and not record I/O operations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29df0cd3-e14f-46e4-bdec-283908f67e2a

📥 Commits

Reviewing files that changed from the base of the PR and between 0b89c27 and 74fcc62.

📒 Files selected for processing (6)
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/session_host.rs
  • crates/minimald/src/sessions.rs
  • crates/sessions/src/store.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/sessions.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/session_host.rs
  • crates/minimald/src/session.rs

@twitchyliquid64
twitchyliquid64 enabled auto-merge (rebase) June 18, 2026 00:22
@twitchyliquid64
twitchyliquid64 merged commit b3295b2 into main Jun 18, 2026
16 checks passed
@twitchyliquid64
twitchyliquid64 deleted the tom/session-destroy branch June 18, 2026 00:36
0chroma added a commit that referenced this pull request Jun 18, 2026
Wire cmd_destroy to the DestroySession RPC added in #462. Resolves the
session by UUID or name via GetSessionRecord (matching cmd_attach), then
issues the destroy RPC and prints confirmation.
0chroma added a commit that referenced this pull request Jun 19, 2026
…destroy) (#434)

* feat(minimal2): implement client interface with SSH transport (#159)

- Add CLI subcommands: activate, attach, destroy, dash (fzf picker stub)
- Implement SSH client transport over UDS to minimald
- Wire oneshot RPC calls for ls (list sessions), activate (create session)
- Add exec_in_session for non-interactive command execution in sessions
- PTY interactive shell is blocked on daemon PTY support (exec.rs:650-654)
- Update Cargo.toml with minimald-rpc, russh, sessions, serde_json deps

* feat(minimal2): add --raw flag and 'dash' fzf session picker (#159)

- ls --raw: outputs one session ID per line for fzf piping
- minimal dash: interactive fzf picker that chains into attach
- Falls back to plain table if fzf is not installed
- Update implementation plan with Phase 5 details

* feat(minimal2): interactive attach via ssh ProxyCommand (#159)

Implement twitchyliquid64's suggestion: shell out to ssh for interactive
PTY attachment instead of reporting it as unsupported. The daemon's
shell_request handler already mints PTY-backed session shells, so we
avoid reimplementing termios/PTY management by deferring to openssh.

Adds a hidden `proxy` subcommand that pipes stdio to the daemon UDS,
used as ssh's ProxyCommand so we don't depend on socat or nc being
installed. Also fixes pre-existing fmt/clippy failures.

* feat(minimal2): wire up destroy command via DestroySession RPC (#159)

Wire cmd_destroy to the DestroySession RPC added in #462. Resolves the
session by UUID or name via GetSessionRecord (matching cmd_attach), then
issues the destroy RPC and prints confirmation.

* refactor(minimal2): migrate attach --command to ssh shell-out (#159)

Per reviewer feedback, exec_in_session used the daemon's old exec codepath
which doesn't hit the sessions module. Remove it entirely and shell out
to ssh for both interactive and --command attachment — ssh handles
termios/PTY reconfiguration and the daemon's shell_request handler
mints the session shell. exec_request remains on the daemon side for
git-receive-pack and vscode remote only.

* feat(minimal2): auto-spawn minimald on Linux (#159)

On Linux, check if the daemon UDS is connectable before each command.
If not, spawn `minimald run` as a detached background process (setsid +
null stdio) and poll the UDS until it becomes available (4s timeout).

Also fixes resolve_socket_path() to match minimald's listen_on() on
Linux ($XDG_STATE_HOME/minimal/providers/local-0/ssh.sock), which was
previously broken — it only worked on macOS via minvmd.

No state machine or state.toml — just socket polling. The lifecycle
management PR (#435) can layer richer state tracking on top later.

* fix(minimal2): thread --minimal-dir through auto-spawn

Auto-spawn was checking the default UDS path instead of the --minimal-dir
override, causing it to fail when the daemon was already running with a
custom state dir. Also pass --minimal-state-dir to the spawned minimald
so it listens on the same path we're polling.

E2E verified: ls, activate, attach --command, destroy (by UUID and
name), auto-spawn (default and --minimal-dir paths).

* refactor(minimal2): remove temporary dash/fzf picker

Remove the `dash` subcommand, `ls --raw` flag, and `LsArgs` struct.
These were temporary scaffolding for an fzf-based session picker — a
proper TUI will be built later. Net -125 lines.

* revert(minimal2): add back ls --raw flag

--raw has standalone value for scripting (e.g. `minimal ls --raw | fzf`),
independent of the removed dash subcommand.

* style(minimal2): add missing blank line between functions

* feat(minimald,minimal2): detect starting daemon via PID file

minimald now writes a PID file at startup. The minimal2 auto-spawn logic
checks this file before spawning: if the PID is alive the daemon is
already starting (e.g. spawned concurrently), so it waits for the UDS
rather than spawning a duplicate. The spawn path uses Child::try_wait
to detect a crash during startup and fail fast instead of exhausting
the 4s timeout.

Addresses review feedback on #434.
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.

2 participants