Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,13 @@ jobs:
# Serial: windows_resolver_tests mutate process-global env
# (BUZZ_SHELL/GIT_BASH/SystemRoot) that SharedState::new reads.
run: cargo test -p buzz-dev-mcp --target $env:TARGET -- --test-threads=1
- name: Test (buzz-acp ledger)
# The pending-turn ledger persists snapshots by writing `<path>.tmp` and
# renaming it over the live file. Windows rename semantics differ from
# POSIX, so the overwrite path only gets real coverage if these tests run
# ON Windows. Scoped to `ledger::` — the full buzz-acp suite is already
# covered by the Linux jobs and is too slow to duplicate here.
run: "cargo test -p buzz-acp --target $env:TARGET ledger::"
# Smoke-test the new host-prereq contract: Git for Windows (which provides
# bash) is available on the runner, a shell command round-trips, and bash
# does NOT resolve from System32 (so WSL's launcher is never picked up).
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/buzz-acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ nix = { version = "0.31", default-features = false, features = ["signal"] }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
httparse = "1"
tempfile = "3"
84 changes: 83 additions & 1 deletion crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,29 @@ pub struct CliArgs {
)]
pub base_prompt_file: Option<PathBuf>,

/// Resume in-progress work on restart (kill -9, crash, manual restart,
/// or app update) by replaying a durable pending-turn ledger on boot.
/// On by default. `--resume-on-restart=false` disables.
#[arg(
long,
env = "BUZZ_ACP_RESUME_ON_RESTART",
default_value_t = true,
action = clap::ArgAction::Set,
num_args = 0..=1,
default_missing_value = "true",
)]
pub resume_on_restart: bool,

/// TTL (seconds) for resume ledger entries, by original admission time.
/// 0 = no expiry (default) — the ledger only ever holds never-completed
/// turns, so even a long-idle relaunch should still resume them.
#[arg(long, env = "BUZZ_ACP_RESUME_TTL", default_value_t = 0)]
pub resume_ttl_secs: u64,

/// Directory for the resume ledger file. Defaults to `<cwd>/.buzz-acp/`.
#[arg(long, env = "BUZZ_ACP_STATE_DIR")]
pub state_dir: Option<PathBuf>,

/// Desired LLM model ID. Applied to every new ACP session after creation.
/// Use `buzz-acp models` to discover available model IDs.
#[arg(long, env = "BUZZ_ACP_MODEL")]
Expand Down Expand Up @@ -561,6 +584,14 @@ pub struct Config {
/// `from_cli()`. `None` when using the compiled-in default or when
/// `--no-base-prompt` is set.
pub base_prompt_content: Option<String>,
/// Whether to replay the durable pending-turn ledger on boot. On by
/// default; `--resume-on-restart=false` restores today's stateless-boot
/// behavior.
pub resume_on_restart: bool,
/// TTL (seconds) for resume ledger entries. 0 = no expiry (default).
pub resume_ttl_secs: u64,
/// Directory for the resume ledger file. `None` = `<cwd>/.buzz-acp/`.
pub state_dir: Option<PathBuf>,
}

/// Maximum length, in characters, of a session title sent to the adapter.
Expand Down Expand Up @@ -1102,6 +1133,9 @@ impl Config {
agent_owner: args.agent_owner.map(|s| s.trim().to_ascii_lowercase()),
no_base_prompt: args.no_base_prompt,
base_prompt_content,
resume_on_restart: args.resume_on_restart,
resume_ttl_secs: args.resume_ttl_secs,
state_dir: args.state_dir,
};

Ok(config)
Expand All @@ -1123,7 +1157,7 @@ impl Config {
format!(" allowed_respond_to=[{}]", modes.join(","))
};
format!(
"relay={} pubkey={} agent_cmd={} {} mcp_cmd={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} {}{}",
"relay={} pubkey={} agent_cmd={} {} mcp_cmd={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} resume={} resume_ttl={}s {}{}",
self.relay_url,
self.keys.public_key().to_hex(),
self.agent_command,
Expand All @@ -1144,6 +1178,8 @@ impl Config {
self.memory_enabled,
self.model.as_deref().unwrap_or("(agent default)"),
self.permission_mode,
self.resume_on_restart,
self.resume_ttl_secs,
respond_to_detail,
allowed_respond_to_detail,
)
Expand Down Expand Up @@ -1472,6 +1508,9 @@ mod tests {
agent_owner: None,
no_base_prompt: false,
base_prompt_content: None,
resume_on_restart: true,
resume_ttl_secs: 0,
state_dir: None,
}
}

Expand Down Expand Up @@ -2840,6 +2879,49 @@ channels = "ALL"
}
}

/// `BUZZ_ACP_RESUME_ON_RESTART` is process-global; `resume_on_restart_
/// env_false_disables` below sets/clears it around its parse, so any
/// other test that parses `CliArgs` without an explicit
/// `--resume-on-restart` flag (and would therefore observe the env var)
/// must serialize against it with this same lock. Tests that pass an
/// explicit flag are unaffected — clap's explicit-arg-over-env
/// precedence means the flag always wins regardless of the race.
static RESUME_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

#[test]
fn resume_on_restart_default_is_true() {
let _guard = RESUME_ENV_LOCK.lock().unwrap();
let args = CliArgs::try_parse_from(["buzz-acp", "--private-key", TEST_PRIVATE_KEY])
.expect("clap should parse args");
assert!(args.resume_on_restart);
}

#[test]
fn resume_on_restart_equals_false_disables() {
let args = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--resume-on-restart=false",
])
.expect("clap should parse args");
assert!(!args.resume_on_restart);
}

#[test]
fn resume_on_restart_env_false_disables() {
// See RESUME_ENV_LOCK doc comment above — env var is process-global.
let _guard = RESUME_ENV_LOCK.lock().unwrap();
std::env::set_var("BUZZ_ACP_RESUME_ON_RESTART", "false");
let args = CliArgs::try_parse_from(["buzz-acp", "--private-key", TEST_PRIVATE_KEY]);
std::env::remove_var("BUZZ_ACP_RESUME_ON_RESTART");
let args = args.expect("clap should parse args");
assert!(
!args.resume_on_restart,
"BUZZ_ACP_RESUME_ON_RESTART=false must disable resume without a CLI flag"
);
}

#[test]
fn sanitize_session_title_collapses_whitespace_and_strips_control_chars() {
assert_eq!(
Expand Down
Loading
Loading