Skip to content

refactor: move all session logic into session actor, init loadouts in own RPC - #754

Merged
twitchyliquid64 merged 1 commit into
mainfrom
tom/session-inner
Jul 15, 2026
Merged

refactor: move all session logic into session actor, init loadouts in own RPC#754
twitchyliquid64 merged 1 commit into
mainfrom
tom/session-inner

Conversation

@twitchyliquid64

@twitchyliquid64 twitchyliquid64 commented Jul 14, 2026

Copy link
Copy Markdown
Member
  • All session logic now lives in the session actor.
  • The session activation flow now has another RPC, ConfigureLoadout, which is fired between the session creation and the submit verdict.

Summary by CodeRabbit

  • New Features

    • Session activation now creates the session first, then separately applies the loadout—making ready vs pending states clearer.
    • Interactive activation can offer to scaffold a missing project configuration, while non-interactive runs continue without blocking.
    • Sessions can attach using a default environment when no configuration is provided.
  • Bug Fixes

    • Improved behavior when sessions disconnect or vanish during execution, SFTP, and other session control operations.
    • Stop now correctly allows idle sessions and refuses those that are still pending approval.
    • RPC error handling and responses are more consistent and structured.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66707e74-7e54-4a5a-9abd-87fdaa02c075

📥 Commits

Reviewing files that changed from the base of the PR and between ba25074 and 78d0e04.

📒 Files selected for processing (12)
  • crates/minimal/src/lib.rs
  • crates/minimal/tests/cli.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/exec.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/sessions.rs
  • crates/minimald/src/sessions/composables.rs
  • crates/minimald/src/sftp.rs
  • crates/minimald/src/test_harness.rs
  • crates/minvmd/examples/exec.rs
  • crates/minvmd/tests/minimald_session_integration.rs
🚧 Files skipped from review as they are similar to previous changes (11)
  • crates/minimald/src/sessions/composables.rs
  • crates/minimald/src/sftp.rs
  • crates/minimal/tests/cli.rs
  • crates/minvmd/examples/exec.rs
  • crates/minvmd/tests/minimald_session_integration.rs
  • crates/minimald/src/test_harness.rs
  • crates/minimald/src/exec.rs
  • crates/minimal/src/lib.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/sessions.rs

📝 Walkthrough

Walkthrough

The session lifecycle now separates session allocation from loadout configuration. Session actors own Draft/Active transitions, RPC routing delegates lifecycle operations to actors, activation scaffolding is non-blocking for non-interactive input, and execution/SFTP paths handle actor disappearance explicitly.

Changes

Session activation and lifecycle

Layer / File(s) Summary
Separate creation and loadout configuration
crates/minimald-rpc/src/lib.rs, crates/minimal/src/lib.rs, crates/minvmd/..., crates/minimald/src/test_harness.rs, crates/minimal/tests/cli.rs
CreateSession now returns an ID only; ConfigureLoadout performs composition and reports Ready or Pending. Activation scaffolding proceeds when stdin is non-interactive or the user declines.
Actor-owned session lifecycle
crates/minimald/src/session.rs, crates/minimald/src/sessions.rs
Session actors manage Draft/Active state, loadout configuration, verdicts, attachment, host teardown, deletion, eviction, and dead-actor errors.
RPC routing and operational error handling
crates/minimald/src/rpc.rs, crates/minimald/src/exec.rs, crates/minimald/src/sftp.rs, crates/minimald/src/sessions/composables.rs
Daemon RPCs route through session actors, missing sessions map to structured errors, daemon paths are used for composition, and execution/SFTP operations stop cleanly when actors disappear.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • gominimal/inbox#151 — Covers the same split session-creation and loadout-configuration lifecycle.

Possibly related PRs

Suggested reviewers: evanspearman, gominimal-aw-bot[bot]

Poem

A rabbit wired two calls in line,
One makes the session, one makes it shine.
Draft hops to Active, configs stay neat,
Dead actors bow and leave softly on feet.
CI pipes onward—no prompt, no fright! 🐇

🚥 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 accurately reflects the main refactor and the new ConfigureLoadout RPC in the activation flow.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@twitchyliquid64
twitchyliquid64 force-pushed the tom/session-inner branch 5 times, most recently from 4deadd2 to ba25074 Compare July 15, 2026 03:07
@twitchyliquid64 twitchyliquid64 changed the title refactor(minimald): move all session logic into session actor refactor: move all session logic into session actor, init loadouts in own RPC Jul 15, 2026
@twitchyliquid64
twitchyliquid64 marked this pull request as ready for review July 15, 2026 03:10

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/minimald/src/sessions/composables.rs (1)

215-249: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Client-facing error now leaks the daemon's internal workspace path instead of the client's project path.

project_path here is now a DaemonAbsPath (the session's daemon-side workspace), but the InvalidInput error text at Line 232 still reads as if it's describing the client's own project directory: "project minimal.toml at {project_path} is invalid: {e}". This error propagates to the client via ConfigureLoadout's Errorable::Err, so the client will see the daemon's internal session-workspace path rather than anything they recognize (their own upload's root), which is both confusing and exposes internal daemon file-layout details.

🩹 Reference something client-meaningful instead of the daemon path
         Err(e) => {
             return Err(std::io::Error::new(
                 std::io::ErrorKind::InvalidInput,
-                format!("project `minimal.toml` at {project_path} is invalid: {e}"),
+                format!("project `minimal.toml` in the session workspace is invalid: {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/sessions/composables.rs` around lines 215 - 249, Update
the InvalidInput error construction in resolve_project_ctx_and_graph so it does
not interpolate the daemon-side project_path into the client-facing message. Use
a client-meaningful description of the project or minimal.toml location while
preserving the existing error propagation and underlying error details.
🤖 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.rs`:
- Around line 535-594: Update configure_loadout to reject sessions in
SessionInner::Draft with pending: Some(_) in addition to Active, using the same
AlreadyExists error path, so an outstanding composition cannot be overwritten.
Leave reconfiguration permitted for drafts without pending resume state.

In `@crates/minimald/src/sessions/composables.rs`:
- Around line 404-409: Update the ProjectComposable construction in the
effective composable path to preserve the daemon workspace path’s provenance
instead of converting it with paths::Host and new_unchecked. Use the appropriate
daemon/workspace path type or existing conversion expected by
ProjectComposable::new, while leaving the effective composable handling
unchanged.

In `@crates/minvmd/examples/exec.rs`:
- Around line 189-231: Update the ConfigureLoadout response handling block to
deserialize into <ConfigureLoadout as OneshotSshRpc>::Response, which is
Errorable<ConfigureLoadoutResponse>, instead of ConfigureLoadoutResponse
directly. Call .ok() and propagate the daemon-side error message before matching
the Ready/Pending ConfigureLoadoutResponse variants, following the existing test
helper behavior.

---

Outside diff comments:
In `@crates/minimald/src/sessions/composables.rs`:
- Around line 215-249: Update the InvalidInput error construction in
resolve_project_ctx_and_graph so it does not interpolate the daemon-side
project_path into the client-facing message. Use a client-meaningful description
of the project or minimal.toml location while preserving the existing error
propagation and underlying error details.
🪄 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: fd4dd052-8c5d-4e41-a2a2-2c61630496ef

📥 Commits

Reviewing files that changed from the base of the PR and between 6917f1d and ba25074.

📒 Files selected for processing (12)
  • crates/minimal/src/lib.rs
  • crates/minimal/tests/cli.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/exec.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/sessions.rs
  • crates/minimald/src/sessions/composables.rs
  • crates/minimald/src/sftp.rs
  • crates/minimald/src/test_harness.rs
  • crates/minvmd/examples/exec.rs
  • crates/minvmd/tests/minimald_session_integration.rs

Comment on lines +535 to +594
async fn configure_loadout(
&mut self,
contribution: WireContribution,
) -> Result<Option<ContributionResponse>, std::io::Error> {
if matches!(self.inner, SessionInner::Active { .. }) {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"session loadout is already configured",
));
}
let object = self.record.object().await?;
let workspace_path = object.workspace_path();

// Deliberately no scaffold here: composing is not the moment to
// fabricate a project. `scaffold_default_mfile` resolves the default
// package repo's branch head over the network, and the default's
// packages reach the sandbox through the launcher's context (built
// from the workspace mfile) rather than through the composition —
// so paying for it here would buy nothing. A bare workspace composes
// to an empty loadout, and the scaffold lands at context-build time.

// Phase 1+2: resolve the project and drive the composer. Kept fully
// synchronous — its non-`Send` intermediaries must not cross an
// `.await`.
let outcome = composables::run_compose(&self.daemon_ctx, &workspace_path, contribution)?;

match outcome {
// The composition is complete: promote the record
// `Pending → Active` and hold the composition for the launcher.
ComposeOutcome::Ready(composition) => {
let mut record = object.record().clone();
record.status = SessionStatus::Active;
self.record.write(record.clone()).await?;
self.inner = SessionInner::Active {
composition: Some(Arc::new(composition)),
host: None,
context: None,
};
// The session is Active now — publish its PTask route
// (R3.1/R3.6).
#[cfg(target_os = "linux")]
self.register_hostname(&record);
Ok(None)
}
// The client must gate items before the composition completes.
// Park in `Draft` with the daemon-side resume state; the record
// is already `Pending` on disk, so nothing to write.
ComposeOutcome::Pending {
mut response,
state,
} => {
// The composer ran before the allocated id was known to it.
response.session_id = *self.record.id();
self.inner = SessionInner::Draft {
pending: Some(Box::new(state)),
};
Ok(Some(response))
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for any existing coverage of a duplicate ConfigureLoadout call
# against a session already parked Draft{pending:Some}.
rg -n -A5 'fn configure_loadout' crates/minimald/src/sessions.rs crates/minimald/src/session.rs
rg -n 'configure_loadout' crates/minimald/src/sessions.rs | rg -i 'pending|twice|again|duplicate'

Repository: gominimal/minimal

Length of output: 2200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the session state machine and read the relevant implementations/tests.
ast-grep outline crates/minimald/src/session.rs --view expanded | sed -n '1,220p'

printf '\n--- configure_loadout and related state ---\n'
sed -n '500,700p' crates/minimald/src/session.rs

printf '\n--- submit verdict / pending-state handling ---\n'
rg -n -A40 -B20 'SubmitVerdict|submit_verdict|pending: Some|pending:None|Draft \{' crates/minimald/src/session.rs crates/minimald/src/sessions.rs

printf '\n--- tests mentioning Pending / verdict / reconfigure ---\n'
rg -n -A3 -B3 'Pending|verdict|configure_loadout|Draft' crates/minimald/src/sessions.rs | sed -n '1,240p'

Repository: gominimal/minimal

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- attach / loadout call sites ---'
rg -n -A20 -B20 'ConfigureLoadout|SessionPending|LoadoutFailed|configure_loadout\(' crates/minimald/src/session.rs crates/minimald/src/sessions.rs | sed -n '1,260p'

printf '\n%s\n' '--- helpers / pending-session tests ---'
rg -n -A25 -B10 'create_pending_session|create_and_configure|Pending session|awaiting a verdict|resumable|reconfigure' crates/minimald/src/sessions.rs | sed -n '1,260p'

printf '\n%s\n' '--- docs/comments about Draft/Pending semantics ---'
rg -n -A3 -B3 'ready for another attempt|awaiting a verdict|can correct it and re-submit|already configured' crates/minimald/src/session.rs crates/minimald/src/sessions.rs | sed -n '1,220p'

Repository: gominimal/minimal

Length of output: 34734


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the attach path and any places that can call configure_loadout implicitly.
sed -n '695,910p' crates/minimald/src/session.rs

printf '\n--- callers of configure_loadout on handle/session ---\n'
rg -n -A3 -B3 'configure_loadout\(' crates/minimald/src/session.rs crates/minimald/src/sessions.rs | sed -n '1,220p'

printf '\n--- any guard for busy/pending state in manager APIs ---\n'
rg -n -A20 -B10 'SessionPending|is_busy|busy|pending: Some|AlreadyExists' crates/minimald/src/session.rs crates/minimald/src/sessions.rs | sed -n '1,220p'

Repository: gominimal/minimal

Length of output: 31949


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the attach implementation and any state transitions around loadout configuration.
sed -n '695,910p' crates/minimald/src/session.rs

printf '\n--- direct callers of configure_loadout ---\n'
rg -n -A4 -B4 'configure_loadout\(' crates/minimald/src/session.rs crates/minimald/src/sessions.rs | sed -n '1,220p'

printf '\n--- busy/pending semantics ---\n'
rg -n -A20 -B8 'SessionPending|is_busy|pending: Some|AlreadyExists|awaiting a verdict' crates/minimald/src/session.rs crates/minimald/src/sessions.rs | sed -n '1,240p'

Repository: gominimal/minimal

Length of output: 34121


Reject reconfiguration while a verdict is outstanding
configure_loadout() only blocks Active, so a Draft { pending: Some(_) } session can be recomposed and overwrite the pending resume state. That invalidates the existing Pending response/item ids and makes a later SubmitVerdict hit the wrong state. Guard Draft { pending: Some(_) } too, or document that reconfiguring intentionally restarts the flow.

🤖 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 535 - 594, Update
configure_loadout to reject sessions in SessionInner::Draft with pending:
Some(_) in addition to Active, using the same AlreadyExists error path, so an
outstanding composition cannot be overwritten. Leave reconfiguration permitted
for drafts without pending resume state.

Comment on lines 404 to 409
(!effective.is_empty()).then(|| {
mfile::ProjectComposable::new(paths::HostPath::from(project_path.clone()), effective)
mfile::ProjectComposable::new(
paths::AbsPath::<paths::Host>::new_unchecked(project_path.as_utf8_path()).into(),
effective,
)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect ProjectComposable's definition and how its path field is consumed/surfaced.
ast-grep outline crates/mfile --items all --type struct,impl --match 'ProjectComposable'
rg -nP -C4 '\bstruct\s+ProjectComposable\b' crates/mfile/src
rg -nP -C4 'impl\s+ProjectComposable\b' crates/mfile/src
rg -nP 'ProjectComposable::new' crates

Repository: gominimal/minimal

Length of output: 1875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' crates/mfile/src/project_composable.rs
printf '\n----\n'
sed -n '360,450p' crates/minimald/src/sessions/composables.rs
printf '\n----\n'
rg -n 'project_path|HostPath|Source::Project|composition provenance|log|serde|Display|Debug' crates/mfile/src/project_composable.rs crates/mfile/src -g '!target'

Repository: gominimal/minimal

Length of output: 21526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Source type and whether provenance is serialized/surfaced.
sed -n '1,240p' crates/sessions/src/core/source.rs
printf '\n----\n'
sed -n '1,260p' crates/sessions/src/core/compose.rs
printf '\n----\n'
rg -n 'Source::Project|Source::Package|Provenanced|serde::Serialize|Serialize for Contribution|Debug for Contribution|Display for Source|path:' crates/sessions/src crates/mfile/src crates/minimald/src

Repository: gominimal/minimal

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether composition errors and provenance are exposed in client-facing paths.
rg -n --max-count 40 'Conflict::|ComposeError|Error::Conflict|error.*source|source\)|Display for Error|thiserror::Error|to_string\(\)|format!\(".*\{source\}' crates/sessions/src crates/minimald/src crates/mfile/src
printf '\n----\n'
sed -n '1,220p' crates/sessions/src/core/compose.rs | sed -n '1,120p'
printf '\n----\n'
sed -n '300,430p' crates/minimald/src/session_host.rs
printf '\n----\n'
sed -n '1,220p' crates/minimald/src/rpc.rs

Repository: gominimal/minimal

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect path type aliases/newtypes and the conversion APIs around Host/Daemon paths.
rg -n --max-count 80 'type HostPath|HostPath|DaemonAbsPath|AbsPath<paths::Host>|new_unchecked|as_utf8_path|into\(\)' crates -g '!target'
printf '\n----\n'
sed -n '1,220p' crates/paths/src/lib.rs
printf '\n----\n'
sed -n '1,220p' crates/paths/src/abs.rs

Repository: gominimal/minimal

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the path-branding types and the wire-facing source type.
rg -n --max-count 80 'struct HostPath|struct HostAbsPath|struct DaemonAbsPath|new_unchecked|as_utf8_path|type HostPath|type DaemonAbsPath' crates/paths crates -g '!target'
printf '\n----\n'
sed -n '1,260p' crates/paths/src/lib.rs
printf '\n----\n'
sed -n '1,260p' crates/sessions/src/wire/primitives.rs

Repository: gominimal/minimal

Length of output: 30634


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '180,235p' crates/minimald/src/sessions/composables.rs
printf '\n----\n'
sed -n '500,560p' crates/sessions/src/core/compose.rs
printf '\n----\n'
sed -n '620,710p' crates/sessions/src/wire/errors.rs
printf '\n----\n'
sed -n '240,320p' crates/sessions/src/client/handler.rs

Repository: gominimal/minimal

Length of output: 8591


Don't cast the daemon workspace path into host provenance. ProjectComposable stores this in Source::Project, which is sent to the client and rendered in compose errors, so new_unchecked can expose the daemon workspace as if it were the client's host path.

🤖 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/composables.rs` around lines 404 - 409, Update
the ProjectComposable construction in the effective composable path to preserve
the daemon workspace path’s provenance instead of converting it with paths::Host
and new_unchecked. Use the appropriate daemon/workspace path type or existing
conversion expected by ProjectComposable::new, while leaving the effective
composable handling unchanged.

Comment thread crates/minvmd/examples/exec.rs
@twitchyliquid64
twitchyliquid64 enabled auto-merge (squash) July 15, 2026 03:41
@twitchyliquid64
twitchyliquid64 merged commit 197fa1e into main Jul 15, 2026
27 checks passed
@twitchyliquid64
twitchyliquid64 deleted the tom/session-inner branch July 15, 2026 03:53
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