Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a122aa6
feat(config): apply edit tool and advisor changes mid-session
matthewyjiang Aug 10, 2026
7d028e7
feat(config): add auto edit tool preference by provider
matthewyjiang Aug 10, 2026
df7e296
feat(config): prefer str_replace for xai in auto edit tool
matthewyjiang Aug 10, 2026
ccd343f
docs(config): explain auto edit tool first-party harness defaults
matthewyjiang Aug 10, 2026
fe0120c
fix(tui): speed up tool expand without rebuilding history suffix
matthewyjiang Aug 10, 2026
8ed0e8f
refactor(config): consolidate mid-session edit tool apply path
matthewyjiang Aug 10, 2026
ce6c51b
fix(config): harden mid-session advisor and edit-tool transitions
matthewyjiang Aug 10, 2026
701b345
test(tui): cover Auto edit tool handoff on provider switch
matthewyjiang Aug 10, 2026
062df1c
fix(ci): satisfy rustfmt and clippy on edit-tool branch
matthewyjiang Aug 10, 2026
fcb25aa
fix(runtime): roll back history when advisor notice persistence fails
matthewyjiang Aug 10, 2026
b6c57e5
fix(tui): harden edit-tool switch, history cache, and syntax restart …
matthewyjiang Aug 10, 2026
4a244e1
build(sdk): cut 1.18.0 for Session::replace_history
matthewyjiang Aug 10, 2026
b657548
fix(ci): refresh downstream fixture lockfile for rho-sdk 1.18.0
matthewyjiang Aug 10, 2026
f4ba0a0
fix(tui): align edit-tool rollback notices across histories
matthewyjiang Aug 10, 2026
81cf87d
fix(test): make edit-tool save-failure coverage OS-independent
matthewyjiang Aug 10, 2026
c660b97
fix(test): scope config save-failure injection to the repository
matthewyjiang Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"crates/rho": "1.33.1",
"crates/rho-sdk": "1.17.3",
"crates/rho-sdk": "1.18.0",
"crates/rho-providers": "0.18.2",
"crates/rho-tools": "0.14.0"
}
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/rho-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repository = "https://github.com/matthewyjiang/rho"
readme = "README.md"

[dependencies]
rho-sdk = { version = "1.17.3", path = "../rho-sdk" }
rho-sdk = { version = "1.18.0", path = "../rho-sdk" }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "stream"] }
serde = { version = "1", features = ["derive"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/rho-sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rho-sdk"
version = "1.17.3"
version = "1.18.0"
edition = "2021"
rust-version = "1.86"
description = "Embeddable, headless agent runtime for Rho"
Expand Down
10 changes: 10 additions & 0 deletions crates/rho-sdk/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,16 @@ impl Session {
self.core.commit(history)
}

/// Replaces committed history while the session is idle.
///
/// Hosts use this after durable persistence fails following
/// [`Self::append_message`], so model-visible history stays aligned with
/// storage instead of remaining one message ahead of a failed snapshot.
pub fn replace_history(&self, history: Vec<Message>) -> Result<Revision, Error> {
let _inactive = self.core.lock_inactive()?;
self.core.commit(history)
}

pub fn reset(&self) -> Result<(), Error> {
let _inactive = self.core.lock_inactive()?;
let system_prompt = match &self.core.runtime().system_prompt {
Expand Down
20 changes: 20 additions & 0 deletions crates/rho-sdk/src/session_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,23 @@ async fn stale_finalization_cannot_clear_a_newer_run_owner() {
assert!(!session.is_running());
assert_eq!(session.state(), SessionState::Idle);
}

#[tokio::test]
async fn replace_history_restores_model_visible_state_after_a_failed_host_persist() {
let runtime = Rho::builder()
.provider(ScriptedProvider::new(identity(), []))
.build()
.unwrap();
let session = runtime.session(SessionOptions::default()).await.unwrap();
let before = session.history();

session
.append_message(crate::model::Message::user_text(
"notice that never persisted",
))
.unwrap();
assert_eq!(session.history().len(), before.len() + 1);

session.replace_history(before.clone()).unwrap();
assert_eq!(session.history(), before);
}
2 changes: 1 addition & 1 deletion crates/rho-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ document-pdf = ["dep:flate2", "dep:lopdf", "dep:pdf-inspector"]
document-spreadsheets = ["dep:calamine", "dep:zip"]

[dependencies]
rho-sdk = { version = "1.17.3", path = "../rho-sdk" }
rho-sdk = { version = "1.18.0", path = "../rho-sdk" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
2 changes: 1 addition & 1 deletion crates/rho/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ name = "rho"
path = "src/main.rs"

[dependencies]
rho-sdk = { version = "1.17.3", path = "../rho-sdk" }
rho-sdk = { version = "1.18.0", path = "../rho-sdk" }
rho-providers = { version = "0.18.2", path = "../rho-providers", default-features = false }
rho-tools = { version = "0.14.0", path = "../rho-tools", package = "rho-agent-tools" }
tokio = { version = "1", features = ["full"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/rho/src/app/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ async fn run_session_with_output(
workspace,
workspace_policy: AppPolicy::for_mode(startup.config.permission_mode),
approval_session: startup.approval_session.clone(),
system_prompt: system_prompt.for_advisor_mode(tool_set.advisor_registered()),
system_prompt,
reasoning: sdk_options.runtime.reasoning,
service_tier: sdk_options.runtime.service_tier,
compaction,
Expand Down
26 changes: 25 additions & 1 deletion crates/rho/src/app/config_repository.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::path::PathBuf;
#[cfg(test)]
use std::sync::Arc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};

use crate::config::Config;

Expand All @@ -10,6 +13,11 @@ pub(crate) struct ConfigRepository {
path: Option<PathBuf>,
#[cfg(test)]
_temp_dir: Option<Arc<tempfile::TempDir>>,
/// When set, the next [`Self::save`] fails once after load/mutation so
/// callers can exercise durable-save rollback without OS-specific FS locks.
/// Shared across clones so the request is not tied to an OS thread.
#[cfg(test)]
fail_next_save: Arc<AtomicBool>,
}

impl ConfigRepository {
Expand All @@ -18,6 +26,8 @@ impl ConfigRepository {
path,
#[cfg(test)]
_temp_dir: None,
#[cfg(test)]
fail_next_save: Arc::new(AtomicBool::new(false)),
}
}

Expand All @@ -27,9 +37,17 @@ impl ConfigRepository {
Ok(Self {
path: Some(temp_dir.path().join("config.toml")),
_temp_dir: Some(temp_dir),
fail_next_save: Arc::new(AtomicBool::new(false)),
})
}

/// Fail this repository's next `save` once. Shared across clones; not
/// thread-local, so Tokio worker hops still observe the request.
#[cfg(test)]
pub(crate) fn fail_next_save_for_tests(&self) {
self.fail_next_save.store(true, Ordering::SeqCst);
}

pub(crate) fn configured_path(&self) -> anyhow::Result<PathBuf> {
self.path
.clone()
Expand All @@ -42,6 +60,12 @@ impl ConfigRepository {
}

pub(crate) fn save(&self, config: &Config) -> anyhow::Result<()> {
#[cfg(test)]
{
if self.fail_next_save.swap(false, Ordering::SeqCst) {
anyhow::bail!("injected config save failure");
}
}
config.save(self.path.clone())
}

Expand Down
30 changes: 30 additions & 0 deletions crates/rho/src/app/config_repository_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,33 @@ fn failed_save_does_not_return_the_update_value() {

assert!(result.is_err());
}

// Covers: injected save failure is stored on the repository and shared by clones
// Owner: config repository
#[test]
fn injected_save_failure_is_instance_scoped_and_shared_by_clones() {
let repository = ConfigRepository::temporary_for_tests().unwrap();
let clone = repository.clone();
repository.fail_next_save_for_tests();

let failed = clone
.update(|config| {
config.max_output_bytes = 42;
config.max_output_bytes
})
.expect_err("clone must observe the injected save failure");
assert!(
failed.to_string().contains("injected config save failure"),
"{failed}"
);

// One-shot: the next save on either handle succeeds.
let value = repository
.update(|config| {
config.max_output_bytes = 7;
config.max_output_bytes
})
.expect("injection is consumed after one save");
assert_eq!(value, 7);
assert_eq!(repository.load().unwrap().max_output_bytes, 7);
}
59 changes: 50 additions & 9 deletions crates/rho/src/app/interactive_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use {

#[path = "interactive_runtime_advisor.rs"]
mod advisor;
#[path = "interactive_runtime_edit_tool.rs"]
pub(crate) mod edit_tool;
#[path = "interactive_runtime_hooks.rs"]
mod session_hooks;
#[path = "interactive_runtime_startup.rs"]
Expand All @@ -29,7 +31,6 @@ use super::{
policy::AppPolicy,
provider_controller::ProviderController,
runtime_builder::{build_compaction, build_runtime, RuntimeBuildOptions},
tools_prompt::SystemPromptVariants,
};

pub(crate) use super::interactive_run_controller::{
Expand Down Expand Up @@ -72,7 +73,7 @@ pub(crate) struct InteractiveRuntime {
mcp_report: crate::tools::mcp::McpSessionReport,
plugins_report: crate::plugins::PluginLoadReport,
workspace: Workspace,
system_prompt: SystemPromptVariants,
system_prompt: rho_sdk::SystemPrompt,
compaction: CompactionConfig,
context_window: Option<u64>,
usage_recording: rho_sdk::ProviderRequestUsageRecording,
Expand Down Expand Up @@ -713,13 +714,45 @@ impl InteractiveRuntime {
model: String,
display: String,
) -> anyhow::Result<()> {
self.sessions
.session()
.append_message(Message::user_text(model))?;
self.sessions
.save_snapshot(&[Message::user_text(display)])?;
self.refresh_context_usage();
Ok(())
let session = self.sessions.session();
let history_before = session.history();
session.append_message(Message::user_text(model))?;

let save_result = {
#[cfg(test)]
{
if advisor::take_fail_next_advisor_notice_snapshot_save_for_tests() {
Err(anyhow::anyhow!(
"injected advisor switch notice snapshot save failure"
))
} else {
self.sessions.save_snapshot(&[Message::user_text(display)])
}
}
#[cfg(not(test))]
{
self.sessions.save_snapshot(&[Message::user_text(display)])
}
};

match save_result {
Ok(()) => {
self.refresh_context_usage();
Ok(())
}
Err(error) => {
// Append already advanced model-visible history. Roll it back so a
// failed durable write cannot leave the live session describing a
// notice the host never persisted.
if let Err(rollback_error) = self.sessions.session().replace_history(history_before)
{
return Err(error.context(format!(
"failed to roll back live history after snapshot save failure: {rollback_error}"
)));
}
Err(error)
}
}
}

pub(crate) async fn shutdown(&mut self) {
Expand Down Expand Up @@ -889,3 +922,11 @@ impl InteractiveRuntime {
#[cfg(test)]
#[path = "interactive_runtime_tests.rs"]
mod tests;

/// Test factory for TUI seams that need a live edit-capable runtime.
#[cfg(test)]
pub(crate) async fn test_edit_tool_runtime(
edit_tool: crate::config::EditTool,
) -> InteractiveRuntime {
tests::edit_tool_runtime(edit_tool).await
}
Loading
Loading