Skip to content

chore(sessions): loadouts flow cleanup - #639

Merged
evanspearman merged 1 commit into
mainfrom
evan/createsession-wire4
Jul 6, 2026
Merged

chore(sessions): loadouts flow cleanup#639
evanspearman merged 1 commit into
mainfrom
evan/createsession-wire4

Conversation

@evanspearman

@evanspearman evanspearman commented Jul 3, 2026

Copy link
Copy Markdown
Member

Depends on #618

Cleans up the multi-round contribution-composition scaffolding that we ended up not building out, and replaces the removed abort path with a properly-scoped single-round AbortSession RPC for the flow we actually shipped.

  • Removes dead multi-round scaffolding. SessionCreate and SessionAbort RPCs plus their wire types (SessionCreateRequest, Abort, AbortReason) are gone — the multi-round design that was going to subsume CreateSession was abandoned in favor of the CreateSession + SubmitVerdict flow that landed in wire2/wire3. SessionStep::Response variant is also removed; no daemon-side producer exists post-simplification, and the client's defensive arm went with it.
  • Adds AbortSession. Needed to close an actual gap: when Phase 3 gating aborts (user cancels, policy hook returns Abort, resolution / expansion fails), the client has to tell the daemon to drop its stash entry and delete the on-disk Pending record. Otherwise a client-side abort silently leaks a Pending session (name held, stash slot burned) until the next daemon restart's reap pass. Wire type is minimal: AbortSessionRequest { id }Errorable<AbortSessionResponse>. Manager arm pops the stash + deletes the record; refuses non-Pending (InvalidInput) and unknown ids (NotFound). Client sends it from drive_pending_to_active on any handle_response failure.
  • Tightens handle_response docstring. Now names the actual AbortSession call as the required cleanup step (not "planned"), documents the MAX_PENDING_SESSIONS cap + ResourceBusy semantics, and is precise about restart cleanup (in-memory stash cleared by restart; on-disk Pending cleared by reap_orphan_pending at next startup).
  • Wire schema tidying. SessionStep retargeted to describe SubmitVerdict's reply specifically (two variants: Active terminal, Fault). wire::request module docstring loses references to the deleted types. Client client_flow1 integration test renamed and rescoped to wire_contribution_round_trips_through_json (the round-trip essence, without the deleted wrapper).
  • COMPOSITION.md. Mermaid diagram updated: SessionCreateCreateSession, terminal SessionStep::Active reply arrow added.

Summary by CodeRabbit

  • New Features

    • Added support for pending session activation, including completion after verdict submission.
    • Introduced a new session abort flow that can cancel pending sessions and clear stored state.
  • Bug Fixes

    • Improved handling when session gating is not approved, with clearer failure behavior.
    • Session lifecycle responses now distinguish between finalized active sessions and terminal faults.
  • Documentation

    • Updated session flow diagrams and protocol notes to reflect the new pending-to-active lifecycle.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebe0a08f-0cf2-4d78-8694-9b4e950d903d

📥 Commits

Reviewing files that changed from the base of the PR and between 86a87a2 and 5ba2525.

📒 Files selected for processing (8)
  • crates/minimal/src/main.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/sessions.rs
  • crates/sessions/docs/COMPOSITION.md
  • crates/sessions/src/client/handler.rs
  • crates/sessions/src/wire/request.rs
  • crates/sessions/tests/client_flow1.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/sessions/src/client/handler.rs
  • crates/sessions/docs/COMPOSITION.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/sessions/tests/client_flow1.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimal/src/main.rs

📝 Walkthrough

Walkthrough

This PR replaces the multi-round SessionCreate/SessionAbort RPCs with a Pending-session resume model: SessionStep gains an Active variant and drops Response/Abort, a new AbortSession RPC is added, the daemon manager and RPC server implement abort handling for Pending sessions, and the minimal client drives Pending sessions to Active, aborting on gating failure.

Changes

AbortSession and Pending resume flow

Layer / File(s) Summary
Wire protocol contract: SessionStep and AbortSession RPC
crates/sessions/src/wire/request.rs, crates/minimald-rpc/src/lib.rs, crates/sessions/tests/client_flow1.rs
Abort/AbortReason and SessionStep::Response are removed, SessionStep::Active { id } is added, SessionCreate/SessionAbort are replaced with AbortSession/AbortSessionRequest/AbortSessionResponse, and tests are updated to round-trip WireContribution and the new SessionStep variants.
Daemon manager pending stash abort handling
crates/minimald/src/sessions.rs
A new ManagerMessage::AbortSession variant and handler validate shutdown/NotFound/InvalidInput states, remove the pending stash entry and on-disk record, and ManagerHandle::abort_session forwards the request; tests cover unknown ids and refusal on Active sessions.
RPC server dispatch for AbortSession
crates/minimald/src/rpc.rs
serve_abort_session forwards to the manager's abort_session and maps results to Errorable, and AbortSession::NAME is added to the known-subsystem check and dispatch match.
Client-side Pending activation and abort flow
crates/minimal/src/main.rs, crates/sessions/src/client/handler.rs, crates/sessions/docs/COMPOSITION.md
AbortOnUnapproved policy hooks and drive_pending_to_active run handle_response, submit verdicts, handle SessionStep::Fault, and best-effort abort via send_abort on failure; cmd_activate handles CreateSessionResponse::Pending; docs describe the new abort/resume semantics.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DaemonRPC
    participant SessionsManager

    Client->>DaemonRPC: CreateSession
    DaemonRPC-->>Client: CreateSessionResponse::Pending
    Client->>Client: handle_response(UserPolicy, AbortOnUnapproved)
    alt gating fails
        Client->>DaemonRPC: AbortSession(id)
        DaemonRPC->>SessionsManager: abort_session(id)
        SessionsManager-->>DaemonRPC: Ok/Err
        DaemonRPC-->>Client: AbortSessionResponse
    else gating succeeds
        Client->>DaemonRPC: SubmitVerdict(verdict)
        DaemonRPC->>SessionsManager: submit_verdict(verdict)
        SessionsManager-->>DaemonRPC: SessionStep::Active or Fault
        DaemonRPC-->>Client: SessionStep
    end
Loading

Possibly related PRs

  • gominimal/minimal#420: The main PR's AbortSession handling in crates/minimald/src/rpc.rs builds directly on this PR's dispatcher-based RPC routing refactor.
  • gominimal/minimal#443: Main PR's removal of SessionCreate/SessionAbort in favor of AbortSession directly follows this PR's introduction of those original RPC types.
  • gominimal/minimal#600: Main PR's Pending-session activation and abort logic directly consumes the CreateSessionResponse::Pending variant introduced in this PR.

Suggested reviewers: norrietaylor, 0chroma

Poem

A session paused, then gently freed,
With AbortSession sown like a seed 🌱
No more leaked burrows left behind,
Pending records now cleared, aligned.
Hop, hop, active — the flow succeeds! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is only loosely related and uses "loadouts" which does not match the sessions-flow changes in this PR. Rename it to something specific to the actual change, e.g. "chore(sessions): clean up session flow and add AbortSession".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

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

🧹 Nitpick comments (1)
crates/minimal/src/main.rs (1)

508-518: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider aborting on SubmitVerdict failure to avoid leaked Pending records.

The gating-failure path calls send_abort, but a SubmitVerdict transport error (Line 511) or an Errorable::Err reply (Line 514) returns Err(()) without one. If the daemon hasn't processed/torn down the record, the Pending record and its stash slot leak until the startup reap — counting against MAX_PENDING_SESSIONS in the meantime. abort_session safely refuses non-Pending records, so a best-effort abort here is harmless if the session was already promoted.

The Fault arm correctly skips the abort (daemon already tore its side down).

♻️ Optional: best-effort abort on RPC/handler failure
     let resp = client
         .oneshot_rpc::<SubmitVerdict>(verdict)
         .await
-        .map_err(|e| eprintln!("SubmitVerdict RPC failed: {e}"))?;
+        .map_err(|e| eprintln!("SubmitVerdict RPC failed: {e}"));
+    let resp = match resp {
+        Ok(r) => r,
+        Err(()) => {
+            send_abort(client, session_id).await;
+            return Err(());
+        }
+    };
     let step = match resp {
         minimald_rpc::Errorable::Ok(s) => s,
         minimald_rpc::Errorable::Err { error } => {
             eprintln!("SubmitVerdict failed: {error}");
+            send_abort(client, session_id).await;
             return Err(());
         }
     };
🤖 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/minimal/src/main.rs` around lines 508 - 518, The SubmitVerdict failure
path in main should perform a best-effort abort before returning, because both
the oneshot_rpc transport error and the Errorable::Err branch currently exit
without cleaning up a possibly still-Pending session. Update the SubmitVerdict
handling around client.oneshot_rpc::<SubmitVerdict>(verdict) and the subsequent
match on minimald_rpc::Errorable so that any RPC/handler failure calls the same
abort_session/send_abort cleanup used by the gating-failure path, while leaving
the Fault arm unchanged since it already tears down the daemon side.
🤖 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.

Nitpick comments:
In `@crates/minimal/src/main.rs`:
- Around line 508-518: The SubmitVerdict failure path in main should perform a
best-effort abort before returning, because both the oneshot_rpc transport error
and the Errorable::Err branch currently exit without cleaning up a possibly
still-Pending session. Update the SubmitVerdict handling around
client.oneshot_rpc::<SubmitVerdict>(verdict) and the subsequent match on
minimald_rpc::Errorable so that any RPC/handler failure calls the same
abort_session/send_abort cleanup used by the gating-failure path, while leaving
the Fault arm unchanged since it already tears down the daemon side.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 835408a4-1a3f-437b-a8e6-636ce8d2c587

📥 Commits

Reviewing files that changed from the base of the PR and between bc78fb7 and 86a87a2.

📒 Files selected for processing (13)
  • crates/minimal/src/main.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/sessions.rs
  • crates/sessions/docs/COMPOSITION.md
  • crates/sessions/src/client/handler.rs
  • crates/sessions/src/core/compose.rs
  • crates/sessions/src/daemon/composer.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/store.rs
  • crates/sessions/src/wire/errors.rs
  • crates/sessions/src/wire/request.rs
  • crates/sessions/tests/client_flow1.rs

@evanspearman
evanspearman force-pushed the evan/createsession-wire4 branch from 86a87a2 to 5ba2525 Compare July 3, 2026 21:54

@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: 2

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

1167-1202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing happy-path test for AbortSession on a Pending session.

Tests cover the two refusal paths (unknown id, Active session) but there's no test asserting that aborting an actual Pending session succeeds and removes both the in-memory stash entry and the on-disk record.

🤖 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 1167 - 1202, Add a happy-path
test for AbortSession using the existing manager() and create_and_unwrap_id
helpers to create a Pending session, then call abort_session on that id and
assert it succeeds. Verify the Pending session is removed from both the
in-memory stash and the persisted record by checking
get_record(SessionKeyPredicate::Id(id)) returns None after the abort, and keep
the test alongside abort_unknown_id_errors and abort_refuses_active_session for
coverage symmetry.
🤖 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/rpc.rs`:
- Around line 264-283: serve_abort_session is swallowing all failures into
Errorable::Err, which breaks the same client/internal error split used by
serve_create_session and serve_submit_verdict. Update the
AbortSession.handle_channel callback to only convert the expected session-level
outcomes into Errorable::Err, and let unexpected errors from
sessions_manager().abort_session(req.id) propagate as ConnectionError::Internal
so the outer res in serve_abort_session can trigger the tracing::warn! path.
Keep the existing AbortSession::NAME-based logging behavior and preserve
client-facing typed errors only for the known, recoverable cases.

In `@crates/minimald/src/sessions.rs`:
- Around line 171-179: `DestroySession` is leaving behind entries in the
in-memory `pending` stash when it destroys a `Pending` session, so update the
`DestroySession` handler in `Manager`/`Manager::handle_message` to also remove
the matching `SessionId` from `self.pending` alongside the existing on-disk
cleanup and `running` teardown. Use the `pending: BTreeMap<SessionId,
PendingComposeState>` field as the authoritative stash to clear, and make sure
the normal destroy path mirrors the new abort cleanup so a destroyed pending
session fully releases its in-flight state.

---

Nitpick comments:
In `@crates/minimald/src/sessions.rs`:
- Around line 1167-1202: Add a happy-path test for AbortSession using the
existing manager() and create_and_unwrap_id helpers to create a Pending session,
then call abort_session on that id and assert it succeeds. Verify the Pending
session is removed from both the in-memory stash and the persisted record by
checking get_record(SessionKeyPredicate::Id(id)) returns None after the abort,
and keep the test alongside abort_unknown_id_errors and
abort_refuses_active_session for coverage symmetry.
🪄 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: ebe0a08f-0cf2-4d78-8694-9b4e950d903d

📥 Commits

Reviewing files that changed from the base of the PR and between 86a87a2 and 5ba2525.

📒 Files selected for processing (8)
  • crates/minimal/src/main.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/sessions.rs
  • crates/sessions/docs/COMPOSITION.md
  • crates/sessions/src/client/handler.rs
  • crates/sessions/src/wire/request.rs
  • crates/sessions/tests/client_flow1.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/sessions/src/client/handler.rs
  • crates/sessions/docs/COMPOSITION.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/sessions/tests/client_flow1.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimal/src/main.rs

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

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

1167-1202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing happy-path test for AbortSession on a Pending session.

Tests cover the two refusal paths (unknown id, Active session) but there's no test asserting that aborting an actual Pending session succeeds and removes both the in-memory stash entry and the on-disk record.

🤖 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 1167 - 1202, Add a happy-path
test for AbortSession using the existing manager() and create_and_unwrap_id
helpers to create a Pending session, then call abort_session on that id and
assert it succeeds. Verify the Pending session is removed from both the
in-memory stash and the persisted record by checking
get_record(SessionKeyPredicate::Id(id)) returns None after the abort, and keep
the test alongside abort_unknown_id_errors and abort_refuses_active_session for
coverage symmetry.
🤖 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/rpc.rs`:
- Around line 264-283: serve_abort_session is swallowing all failures into
Errorable::Err, which breaks the same client/internal error split used by
serve_create_session and serve_submit_verdict. Update the
AbortSession.handle_channel callback to only convert the expected session-level
outcomes into Errorable::Err, and let unexpected errors from
sessions_manager().abort_session(req.id) propagate as ConnectionError::Internal
so the outer res in serve_abort_session can trigger the tracing::warn! path.
Keep the existing AbortSession::NAME-based logging behavior and preserve
client-facing typed errors only for the known, recoverable cases.

In `@crates/minimald/src/sessions.rs`:
- Around line 171-179: `DestroySession` is leaving behind entries in the
in-memory `pending` stash when it destroys a `Pending` session, so update the
`DestroySession` handler in `Manager`/`Manager::handle_message` to also remove
the matching `SessionId` from `self.pending` alongside the existing on-disk
cleanup and `running` teardown. Use the `pending: BTreeMap<SessionId,
PendingComposeState>` field as the authoritative stash to clear, and make sure
the normal destroy path mirrors the new abort cleanup so a destroyed pending
session fully releases its in-flight state.

---

Nitpick comments:
In `@crates/minimald/src/sessions.rs`:
- Around line 1167-1202: Add a happy-path test for AbortSession using the
existing manager() and create_and_unwrap_id helpers to create a Pending session,
then call abort_session on that id and assert it succeeds. Verify the Pending
session is removed from both the in-memory stash and the persisted record by
checking get_record(SessionKeyPredicate::Id(id)) returns None after the abort,
and keep the test alongside abort_unknown_id_errors and
abort_refuses_active_session for coverage symmetry.
🪄 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: ebe0a08f-0cf2-4d78-8694-9b4e950d903d

📥 Commits

Reviewing files that changed from the base of the PR and between 86a87a2 and 5ba2525.

📒 Files selected for processing (8)
  • crates/minimal/src/main.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/sessions.rs
  • crates/sessions/docs/COMPOSITION.md
  • crates/sessions/src/client/handler.rs
  • crates/sessions/src/wire/request.rs
  • crates/sessions/tests/client_flow1.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/sessions/src/client/handler.rs
  • crates/sessions/docs/COMPOSITION.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/sessions/tests/client_flow1.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimal/src/main.rs
🛑 Comments failed to post (2)
crates/minimald/src/rpc.rs (1)

264-283: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

serve_abort_session maps every error to a typed client-facing error, breaking the sibling-RPC convention.

Unlike serve_create_session/serve_submit_verdict, which only turn specific expected error kinds into Errorable::Err and let anything else fall through to ConnectionError::Internal (which triggers the tracing::warn! below and shows up in daemon logs), serve_abort_session converts any error — including the shutdown ConnectionRefused and any unexpected store.delete I/O failure — into Ok(Errorable::Err{...}). That keeps the outer res as Ok(()), so the tracing::warn! in this function never fires and a genuine internal failure is silently hidden from daemon logs while being reported to the client as an indistinguishable generic string.

🔧 Proposed fix
 async fn serve_abort_session(s: ServerStateHandle, c: RuChannel<Msg>) {
     let res = AbortSession
         .handle_channel(c, async |req| {
-            let res = s.sessions_manager().await.abort_session(req.id).await;
-            match res {
-                Ok(()) => Ok(Errorable::Ok(AbortSessionResponse)),
-                Err(e) => Ok(Errorable::Err {
-                    error: e.to_string(),
-                }),
-            }
+            match s.sessions_manager().await.abort_session(req.id).await {
+                Ok(()) => Ok(Errorable::Ok(AbortSessionResponse)),
+                // Expected, structured outcomes the client should
+                // handle as clean typed errors.
+                Err(e)
+                    if matches!(
+                        e.kind(),
+                        std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
+                    ) =>
+                {
+                    Ok(Errorable::Err {
+                        error: e.to_string(),
+                    })
+                }
+                // Shutdown-refusal and any unexpected I/O error —
+                // surface as a transport failure so it's logged below.
+                Err(e) => Err(ConnectionError::Internal(e.to_string())),
+            }
         })
         .await;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

/// `AbortSession`: drop a `Pending` session's stash entry and delete
/// its on-disk record. See the manager arm for the actor-side rules
/// (refuses `Active` records and unknown ids).
async fn serve_abort_session(s: ServerStateHandle, c: RuChannel<Msg>) {
    let res = AbortSession
        .handle_channel(c, async |req| {
            match s.sessions_manager().await.abort_session(req.id).await {
                Ok(()) => Ok(Errorable::Ok(AbortSessionResponse)),
                // Expected, structured outcomes the client should
                // handle as clean typed errors.
                Err(e)
                    if matches!(
                        e.kind(),
                        std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
                    ) =>
                {
                    Ok(Errorable::Err {
                        error: e.to_string(),
                    })
                }
                // Shutdown-refusal and any unexpected I/O error —
                // surface as a transport failure so it's logged below.
                Err(e) => Err(ConnectionError::Internal(e.to_string())),
            }
        })
        .await;
    if let Err(e) = res {
        tracing::warn!("RPC handler for {} failed: {}", AbortSession::NAME, e);
    }
}
🤖 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/rpc.rs` around lines 264 - 283, serve_abort_session is
swallowing all failures into Errorable::Err, which breaks the same
client/internal error split used by serve_create_session and
serve_submit_verdict. Update the AbortSession.handle_channel callback to only
convert the expected session-level outcomes into Errorable::Err, and let
unexpected errors from sessions_manager().abort_session(req.id) propagate as
ConnectionError::Internal so the outer res in serve_abort_session can trigger
the tracing::warn! path. Keep the existing AbortSession::NAME-based logging
behavior and preserve client-facing typed errors only for the known, recoverable
cases.
crates/minimald/src/sessions.rs (1)

171-179: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

DestroySession doesn't clean up the new pending stash, leaking entries for destroyed Pending sessions.

ManagerMessage::List doesn't filter by status, so a Pending session is visible and can be targeted by a normal DestroySession call (instead of the new AbortSession). DestroySession's handler deletes the on-disk record and tears down running, but never calls self.pending.remove(&id). The stashed PendingComposeState then leaks in memory until daemon restart, silently eating into MAX_PENDING_SESSIONS capacity even though its backing record is gone.

🔧 Proposed fix
             ManagerMessage::DestroySession(id, r) => {
                 r.handle(async {
                     if self.in_shutdown {
                         return Err(SessionsError::new(
                             std::io::ErrorKind::ConnectionRefused,
                             "in shutdown",
                         ));
                     }
                     let k = self.store.find_by_id(&id)?.ok_or_else(|| {
                         std::io::Error::new(
                             NotFound,
                             format!("no session with ID `{}`", id.as_ref()),
                         )
                     })?;
+                    // A `Pending` session has no running host, but may
+                    // still hold a `PendingComposeState` stash entry —
+                    // drop it so DestroySession doesn't leak stash
+                    // capacity toward `MAX_PENDING_SESSIONS`.
+                    self.pending.remove(&id);
🤖 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 171 - 179, `DestroySession` is
leaving behind entries in the in-memory `pending` stash when it destroys a
`Pending` session, so update the `DestroySession` handler in
`Manager`/`Manager::handle_message` to also remove the matching `SessionId` from
`self.pending` alongside the existing on-disk cleanup and `running` teardown.
Use the `pending: BTreeMap<SessionId, PendingComposeState>` field as the
authoritative stash to clear, and make sure the normal destroy path mirrors the
new abort cleanup so a destroyed pending session fully releases its in-flight
state.

@evanspearman
evanspearman merged commit 1ff9c38 into main Jul 6, 2026
56 checks passed
@evanspearman
evanspearman deleted the evan/createsession-wire4 branch July 6, 2026 14:59
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