fix(sbx): make /sbx save work on a live multipass sandbox
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run

Multipass can only snapshot a powered-off instance, so `/sbx save` on a
running multipass sandbox previously just surfaced multipass's "instance
must be stopped" error — making the snapshot that `/sbx load` needs
impossible to create through the UI.

Now multipass save powers the instance down before snapshotting, and the
handler tears the shared session down first (same as `/sbx stop`, but
WITHOUT purging so the instance — and thus the snapshot — survives for a
later `/sbx load`). Docker is unchanged: `docker commit` captures a live
container, so its save stays non-disruptive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-06 22:38:55 -07:00
parent 6160d957f5
commit b8a06077a4
2 changed files with 24 additions and 0 deletions
+14
View File
@@ -1673,6 +1673,20 @@ fn handle_command(
"saving sandbox state as '{label}'{}",
if local { " (+ local copy)" } else { "" }
));
// Docker commits a *live* container, so its save is non-
// disruptive. Multipass can only snapshot a powered-off
// instance, so saving it necessarily stops the shared shell —
// tear the live session down (same as `/sbx stop`, but WITHOUT
// purging: the instance must survive so the snapshot does too).
if be == sbx::Backend::Multipass {
if let Some(mut sb) = broker.take() {
sb.stop();
}
broker_meta.take();
*announced_dims = None;
send_frame(out_tx, room, json!({"_sbx":"status","state":"stopped"}));
app.sys("multipass must power off to snapshot — stopping the shared shell, then saving (reload it with `/sbx load`)");
}
let (tx, lbl) = (app_tx.clone(), label.clone());
tokio::spawn(async move {
let res = tokio::task::spawn_blocking(move || sbx::save_state(be, &name, &label, local)).await;
+10
View File
@@ -589,6 +589,16 @@ pub fn save_state(backend: Backend, name: &str, label: &str, local: bool) -> Res
Ok(format!("image {tag}"))
}
Backend::Multipass => {
// Multipass only snapshots a *stopped* instance (unlike docker, which
// commits a live container). Power it off first — the caller has
// already torn down the shared shell, so this is the save *and* stop.
// The instance stays registered (we don't purge), so `/sbx load` can
// restore the snapshot later.
let _ = Command::new("multipass")
.args(["stop", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
let out = Command::new("multipass")
.args(["snapshot", name, "--name", label])
.output()