chore(sessions): loadouts flow cleanup - #639
Conversation
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThis 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. ChangesAbortSession and Pending resume flow
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
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/minimal/src/main.rs (1)
508-518: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider aborting on
SubmitVerdictfailure to avoid leakedPendingrecords.The gating-failure path calls
send_abort, but aSubmitVerdicttransport error (Line 511) or anErrorable::Errreply (Line 514) returnsErr(())without one. If the daemon hasn't processed/torn down the record, thePendingrecord and its stash slot leak until the startup reap — counting againstMAX_PENDING_SESSIONSin the meantime.abort_sessionsafely refuses non-Pendingrecords, so a best-effort abort here is harmless if the session was already promoted.The
Faultarm 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
📒 Files selected for processing (13)
crates/minimal/src/main.rscrates/minimald-rpc/src/lib.rscrates/minimald/src/rpc.rscrates/minimald/src/sessions.rscrates/sessions/docs/COMPOSITION.mdcrates/sessions/src/client/handler.rscrates/sessions/src/core/compose.rscrates/sessions/src/daemon/composer.rscrates/sessions/src/lib.rscrates/sessions/src/store.rscrates/sessions/src/wire/errors.rscrates/sessions/src/wire/request.rscrates/sessions/tests/client_flow1.rs
86a87a2 to
5ba2525
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
crates/minimald/src/sessions.rs (1)
1167-1202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing happy-path test for
AbortSessionon aPendingsession.Tests cover the two refusal paths (unknown id, Active session) but there's no test asserting that aborting an actual
Pendingsession 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
📒 Files selected for processing (8)
crates/minimal/src/main.rscrates/minimald-rpc/src/lib.rscrates/minimald/src/rpc.rscrates/minimald/src/sessions.rscrates/sessions/docs/COMPOSITION.mdcrates/sessions/src/client/handler.rscrates/sessions/src/wire/request.rscrates/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
There was a problem hiding this comment.
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 winMissing happy-path test for
AbortSessionon aPendingsession.Tests cover the two refusal paths (unknown id, Active session) but there's no test asserting that aborting an actual
Pendingsession 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
📒 Files selected for processing (8)
crates/minimal/src/main.rscrates/minimald-rpc/src/lib.rscrates/minimald/src/rpc.rscrates/minimald/src/sessions.rscrates/sessions/docs/COMPOSITION.mdcrates/sessions/src/client/handler.rscrates/sessions/src/wire/request.rscrates/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_sessionmaps 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 intoErrorable::Errand let anything else fall through toConnectionError::Internal(which triggers thetracing::warn!below and shows up in daemon logs),serve_abort_sessionconverts any error — including the shutdownConnectionRefusedand any unexpectedstore.deleteI/O failure — intoOk(Errorable::Err{...}). That keeps the outerresasOk(()), so thetracing::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
DestroySessiondoesn't clean up the newpendingstash, leaking entries for destroyed Pending sessions.
ManagerMessage::Listdoesn't filter by status, so aPendingsession is visible and can be targeted by a normalDestroySessioncall (instead of the newAbortSession).DestroySession's handler deletes the on-disk record and tears downrunning, but never callsself.pending.remove(&id). The stashedPendingComposeStatethen leaks in memory until daemon restart, silently eating intoMAX_PENDING_SESSIONScapacity 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.
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
AbortSessionRPC for the flow we actually shipped.SessionCreateandSessionAbortRPCs plus their wire types (SessionCreateRequest,Abort,AbortReason) are gone — the multi-round design that was going to subsumeCreateSessionwas abandoned in favor of theCreateSession+SubmitVerdictflow that landed in wire2/wire3.SessionStep::Responsevariant is also removed; no daemon-side producer exists post-simplification, and the client's defensive arm went with it.AbortSession. Needed to close an actual gap: when Phase 3 gating aborts (user cancels, policy hook returnsAbort, resolution / expansion fails), the client has to tell the daemon to drop its stash entry and delete the on-diskPendingrecord. Otherwise a client-side abort silently leaks aPendingsession (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 fromdrive_pending_to_activeon anyhandle_responsefailure.handle_responsedocstring. Now names the actualAbortSessioncall as the required cleanup step (not "planned"), documents theMAX_PENDING_SESSIONScap +ResourceBusysemantics, and is precise about restart cleanup (in-memory stash cleared by restart; on-diskPendingcleared byreap_orphan_pendingat next startup).SessionStepretargeted to describeSubmitVerdict's reply specifically (two variants:Activeterminal,Fault).wire::requestmodule docstring loses references to the deleted types. Clientclient_flow1integration test renamed and rescoped towire_contribution_round_trips_through_json(the round-trip essence, without the deleted wrapper).SessionCreate→CreateSession, terminalSessionStep::Activereply arrow added.Summary by CodeRabbit
New Features
Bug Fixes
Documentation