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
85 changes: 84 additions & 1 deletion crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ pub enum DedupMode {
Queue,
}

/// Scope used for scheduling and retained ACP session identity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum SessionScope {
/// One independent execution lane and session per outermost conversation root.
Thread,
/// Legacy behavior: one serialized execution lane and session per channel.
Channel,
}

/// How to handle new @mentions while a turn is already in-flight for that channel.
#[derive(Debug, Clone, Copy, PartialEq, clap::ValueEnum)]
pub enum MultipleEventHandling {
Expand Down Expand Up @@ -467,6 +476,16 @@ pub struct CliArgs {
/// Publish encrypted ACP observer frames over the relay.
#[arg(long, env = "BUZZ_ACP_RELAY_OBSERVER", default_value_t = false)]
pub relay_observer: bool,

/// Scheduling and retained-session scope. Thread scope is the durable default;
/// channel scope preserves the legacy one-session-per-channel behavior.
#[arg(
long,
env = "BUZZ_ACP_SESSION_SCOPE",
default_value = "thread",
value_enum
)]
pub session_scope: SessionScope,
}

/// Merged NIP-01 subscription filter for a single channel.
Expand Down Expand Up @@ -537,6 +556,8 @@ pub struct Config {
pub has_generated_codex_config: bool,
/// Whether to publish encrypted observer frames through the relay.
pub relay_observer: bool,
/// Scheduling and retained-session scope.
pub session_scope: SessionScope,
/// Agent owner pubkey (hex). Used for `--respond-to=owner-only` gate.
/// Replaces the old REST-based owner lookup.
pub agent_owner: Option<String>,
Expand Down Expand Up @@ -706,7 +727,26 @@ pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<Strin
/// `#[tokio::main]` ensures worker threads are not yet alive.
///
/// // Must be called before tokio runtime starts — see Rust 2024 edition safety.
pub fn propagate_legacy_env_vars() {
fn legacy_session_scope(
canonical: Option<&str>,
legacy: Option<&str>,
) -> Result<Option<String>, ConfigError> {
if canonical.is_some() {
return Ok(None);
}
let Some(value) = legacy else {
return Ok(None);
};
match value.trim().to_ascii_lowercase().as_str() {
"true" => Ok(Some("thread".into())),
"false" => Ok(Some("channel".into())),
_ => Err(ConfigError::ConfigFile(format!(
"BUZZ_ACP_TOP_LEVEL_SESSIONS must be true or false, got {value:?}"
))),
}
}

pub fn propagate_legacy_env_vars() -> Result<(), ConfigError> {
for (legacy, canonical) in [
("BUZZ_ACP_PRIVATE_KEY", "BUZZ_PRIVATE_KEY"),
("BUZZ_ACP_API_TOKEN", "BUZZ_API_TOKEN"),
Expand All @@ -717,6 +757,12 @@ pub fn propagate_legacy_env_vars() {
}
}
}
let canonical = std::env::var("BUZZ_ACP_SESSION_SCOPE").ok();
let legacy = std::env::var("BUZZ_ACP_TOP_LEVEL_SESSIONS").ok();
if let Some(scope) = legacy_session_scope(canonical.as_deref(), legacy.as_deref())? {
std::env::set_var("BUZZ_ACP_SESSION_SCOPE", scope);
}
Ok(())
}

impl Config {
Expand Down Expand Up @@ -996,6 +1042,7 @@ impl Config {
persona_env_vars,
has_generated_codex_config,
relay_observer: args.relay_observer,
session_scope: args.session_scope,
agent_owner: args.agent_owner.map(|s| s.trim().to_ascii_lowercase()),
no_base_prompt: args.no_base_prompt,
base_prompt_content,
Expand Down Expand Up @@ -1369,6 +1416,7 @@ mod tests {
has_generated_codex_config: false,
relay_observer: false,
agent_owner: None,
session_scope: SessionScope::Channel,
no_base_prompt: false,
base_prompt_content: None,
}
Expand Down Expand Up @@ -2380,6 +2428,21 @@ channels = "ALL"

// ── Multiple-event-handling validation + default ──────────────────────────

#[test]
fn test_session_scope_defaults_to_thread_and_accepts_legacy_channel_mode() {
let default_args = CliArgs::parse_from(["buzz-acp", "--private-key", &"0".repeat(64)]);
assert_eq!(default_args.session_scope, SessionScope::Thread);

let channel_args = CliArgs::parse_from([
"buzz-acp",
"--private-key",
&"0".repeat(64),
"--session-scope",
"channel",
]);
assert_eq!(channel_args.session_scope, SessionScope::Channel);
}

#[test]
fn test_multiple_event_handling_default_is_steer() {
// Parse a minimal arg set; the default for --multiple-event-handling
Expand Down Expand Up @@ -2683,6 +2746,26 @@ channels = "ALL"
);
}

#[test]
fn legacy_session_scope_translation_and_precedence() {
assert_eq!(
legacy_session_scope(None, Some("true")).unwrap().as_deref(),
Some("thread")
);
assert_eq!(
legacy_session_scope(None, Some("false"))
.unwrap()
.as_deref(),
Some("channel")
);
assert_eq!(
legacy_session_scope(Some("channel"), Some("true")).unwrap(),
None
);
assert_eq!(legacy_session_scope(None, None).unwrap(), None);
assert!(legacy_session_scope(None, Some("not-a-bool")).is_err());
}

#[test]
fn max_turn_duration_ceiling_cannot_overflow_in_flight_deadline() {
// The in-flight deadline is max_turn + 100s buffer (IN_FLIGHT_DEADLINE_BUFFER_SECS).
Expand Down
Loading
Loading