From 0313c8042e22085a4c8e18f47a01404e9f1dc1f2 Mon Sep 17 00:00:00 2001 From: Valeriy Selitskiy <239034+iamwavecut@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:41:36 +0200 Subject: [PATCH] Report SAFE run_python files under sent, like a real delivery After #271 the SAFE result said the chat received the chart, but its data still carried send_withheld. Gemma read that as unfinished work and called a nonexistent send_files tool twice before the turn fell back to Qwen. A withheld delivery now fills sent exactly as a real one does; tool_mode SAFE already tells operators that nothing left the sandbox. --- crates/openplotva-app/src/python_sandbox.rs | 22 ++++--------------- crates/openplotva-dialog/src/lib.rs | 3 ++- .../specs/2026-10-20-python-sandbox-design.md | 4 ++-- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/crates/openplotva-app/src/python_sandbox.rs b/crates/openplotva-app/src/python_sandbox.rs index 93d19f6a..cc1710b1 100644 --- a/crates/openplotva-app/src/python_sandbox.rs +++ b/crates/openplotva-app/src/python_sandbox.rs @@ -226,7 +226,7 @@ impl PythonSandboxTool { } else { match delivery_paths(&send_files, &response) { Err(count) => outcome.unselected = count, - Ok(paths) if request.withhold_delivery => outcome.withheld = paths, + Ok(paths) if request.withhold_delivery => outcome.report.sent = paths, Ok(paths) if paths.is_empty() => {} Ok(paths) => { outcome.report = self @@ -318,9 +318,6 @@ struct DeliveryOutcome { report: DeliveryReport, /// Named files left unsent because the script failed. skipped: Vec, - /// Files a run with withheld delivery would have sent. The model reads - /// them as received, so a test dialog continues as a real chat would. - withheld: Vec, /// Changed files left unsent because there were too many to send unasked. unselected: usize, } @@ -437,7 +434,6 @@ fn run_result(response: &RunResponse, inputs: Vec, outcome: DeliveryOutc let DeliveryOutcome { report, skipped, - withheld, unselected, } = outcome; let mut data = Map::new(); @@ -495,22 +491,14 @@ fn run_result(response: &RunResponse, inputs: Vec, outcome: DeliveryOutc if !skipped.is_empty() { data.insert("send_skipped".to_owned(), json!(skipped)); } - if !withheld.is_empty() { - data.insert("send_withheld".to_owned(), json!(withheld)); - } let data = Some(Value::Object(data)); let (code, reason) = match (response.termination, response.exit_code) { (Termination::Exited, Some(0)) if report.errors.is_empty() => { - let received = if report.sent.is_empty() { - &withheld - } else { - &report.sent - }; - let message = if !received.is_empty() { + let message = if !report.sent.is_empty() { format!( "The script finished; the chat received {}.", - received.join(", ") + report.sent.join(", ") ) } else if unselected > 0 { format!( @@ -1304,9 +1292,7 @@ mod tests { result.message, "The script finished; the chat received chart.png." ); - let data = result.data.expect("data"); - assert_eq!(data["send_withheld"], json!(["chart.png"])); - assert!(data.get("sent").is_none()); + assert_eq!(result.data.expect("data")["sent"], json!(["chart.png"])); } #[tokio::test] diff --git a/crates/openplotva-dialog/src/lib.rs b/crates/openplotva-dialog/src/lib.rs index 51df7879..3ad66033 100644 --- a/crates/openplotva-dialog/src/lib.rs +++ b/crates/openplotva-dialog/src/lib.rs @@ -267,7 +267,8 @@ pub struct PythonRunRequest { /// Caption for the delivered files. #[serde(default, skip_serializing_if = "String::is_empty")] pub caption: String, - /// Run the script but report the files instead of sending them. + /// Run the script and report its files as sent without sending them, + /// so a test dialog continues the way a real chat would. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub withhold_delivery: bool, } diff --git a/docs/superpowers/specs/2026-10-20-python-sandbox-design.md b/docs/superpowers/specs/2026-10-20-python-sandbox-design.md index 0a5175b2..a13cf986 100644 --- a/docs/superpowers/specs/2026-10-20-python-sandbox-design.md +++ b/docs/superpowers/specs/2026-10-20-python-sandbox-design.md @@ -79,5 +79,5 @@ scripts themselves never see that network. - The stored tool result renders the call's `code` next to its output, which is how a follow-up turn finds the script to edit. - The tool is offered only when `SANDBOX_URL` is set. SAFE runtime virtual - dialogs run the script and report the files they would send as - `send_withheld`. + dialogs run the script and report its files as sent without sending them, + next to `tool_mode: "SAFE"`.