Skip to content
Open
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
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
20 changes: 15 additions & 5 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ const overrides = new Map([
// config-bridge: get_agent_config_surface/write_agent_config_field/put_agent_session_config
// commands add ~40 lines. Queued to split.
// branch cut; override bumped to cover the merged total. Queued to split.
["src-tauri/src/commands/agents.rs", 1437],
// global-agent-config: build_deploy_payload threads global config fallback
// for provider/model/env_vars (+4 lines). cargo fmt reflowed 2 more lines.
// deploy-resolver: resolve_deploy_model_provider fn + doc comment (+6 lines).
// Queued to split.
["src-tauri/src/commands/agents.rs", 1449],
// #1418 read-path fix: get_thread_replies' blocker fix (shared TIMELINE_KINDS
// const + build_thread_replies_filter helper, mirroring the channel sibling so
// the two p-gate filters can't drift) plus two guard unit tests. The file was
Expand All @@ -84,7 +88,9 @@ const overrides = new Map([
// activity-feed threads avatar_url into build_managed_agent_summary for the
// assistant-bubble pinned snapshot.
// +1 for agent_pubkey field in setup payload (config-nudge card wire).
["src-tauri/src/managed_agents/runtime.rs", 2208],
// global-agent-config: spawn_agent_child loads global config and merges as
// lowest env layer (+8 lines). Queued to split.
["src-tauri/src/managed_agents/runtime.rs", 2216],
// config-bridge setup-payload env-boundary fix adds readiness wiring in
// spawn_agent_child; load-bearing security fix, queued to split.
["src-tauri/src/managed_agents/config_bridge/reader.rs", 1016],
Expand All @@ -94,7 +100,8 @@ const overrides = new Map([
// New file in this PR; queued to split.
// +2 readiness integration tests for flat-DATABRICKS_HOST canonicalization fix.
// +1 cargo fmt whitespace reformat (readiness.rs closures inline after rebase).
["src-tauri/src/managed_agents/readiness.rs", 1150],
// +16: resolve_effective_agent_env + global-config readiness wiring (#1448 base).
["src-tauri/src/managed_agents/readiness.rs", 1166],
// applyWorkspace reposDir parameter plus the validateReposDir binding,
// threaded through Tauri invokes for configurable repos_dir, plus the
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
Expand All @@ -114,7 +121,9 @@ const overrides = new Map([
// CreateAgentDialog). +23 lines of gate wiring. Queued to split.
// config-bridge-aware requirements: useRuntimeFileConfigQuery wiring adds
// ~16 lines. Queued to split.
["src/features/agents/ui/PersonaDialog.tsx", 1032],
// +2 lines: filter managed provider key from requiredEnvKeys at the
// PersonaAdvancedFields call site to suppress the dead-input locked row.
["src/features/agents/ui/PersonaDialog.tsx", 1034],
// harness-persona-sync feature growth, queued to split in the resolver-unify
// refactor followup. discovery.rs is dominated by the new test module
// (the effective_agent_command / divergent / create-time override matrix);
Expand Down Expand Up @@ -146,7 +155,8 @@ const overrides = new Map([
// props restored after 826d735fe removal (UserProfilePanel.tsx still needs them).
["src/features/profile/ui/UserProfilePanelSections.tsx", 1140],
// +14 for openEditAgent event subscription (config-nudge card "Open Edit Agent" action).
["src/features/profile/ui/UserProfilePanel.tsx", 1014],
// +11 for editAgentFocus state + initialFocus prop threading (deep-link granularity).
["src/features/profile/ui/UserProfilePanel.tsx", 1025],
// PersistBackend enum + marker-on-keyring-success plumbing and its three
// fail-closed regression tests (silent identity rotation on keyring outage).
// A small overage from load-bearing security plumbing on a file already at
Expand Down
152 changes: 139 additions & 13 deletions desktop/src-tauri/src/commands/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use crate::{
},
current_instance_id, effective_agent_command, known_acp_runtime, load_managed_agents,
load_personas, resolve_effective_prompt_model_provider, save_managed_agents,
sync_managed_agent_processes, KnownAcpRuntime, ManagedAgentRecord, PersonaRecord,
sync_managed_agent_processes, GlobalAgentConfig, KnownAcpRuntime, ManagedAgentRecord,
PersonaRecord,
},
};

Expand All @@ -36,13 +37,17 @@ pub struct RuntimeFileConfigSubset {
pub satisfied_env_keys: Vec<String>,
}

/// Resolve the config surface with persona values applied.
/// Resolve the config surface with persona and global default values applied.
///
/// The pipeline: resolve the linked persona's prompt/model/provider, inject
/// each into the record only where the record lacks its own value, let
/// `read_config_surface` tag those injected fields `BuzzExplicit`, then re-tag
/// exactly the injected fields to `PersonaDefault`.
///
/// Global defaults fill in when neither the record nor the linked persona
/// provides a value. They are re-tagged to `GlobalDefault` so the UI can
/// display "inherited from global defaults".
///
/// The re-tag is triple-gated — a field is re-tagged only when (a) the record
/// did not already have it (`!had_*`), (b) the surface produced the field, and
/// (c) the reader tagged it `BuzzExplicit`. A value the user set explicitly in
Expand All @@ -52,6 +57,7 @@ fn resolve_config_surface(
personas: &[PersonaRecord],
runtime_meta: Option<&KnownAcpRuntime>,
session_cache: Option<&SessionConfigCache>,
global: &GlobalAgentConfig,
) -> RuntimeConfigSurface {
let had_prompt =
record.system_prompt.is_some() || record.env_vars.contains_key("BUZZ_ACP_SYSTEM_PROMPT");
Expand Down Expand Up @@ -87,9 +93,21 @@ fn resolve_config_surface(
None
}
} else {
// Prefer persona as baseline, fall back to global when persona has none
// and the model was overridden mid-session (global-default agent).
persona_model
.clone()
.map(|m| (m, ConfigOrigin::PersonaDefault))
.or_else(|| {
if model_overridden {
global
.model
.clone()
.map(|m| (m, ConfigOrigin::GlobalDefault))
} else {
None
}
})
};

// Inject resolved persona values into the record where absent.
Expand All @@ -109,6 +127,24 @@ fn resolve_config_surface(
}
}

// Inject global defaults where neither the record nor the persona had a value.
// Track injection so we can re-tag to GlobalDefault after the reader.
let inject_global_model = !had_model && record.model.is_none();
let inject_global_provider = !had_provider
&& !provider_env_key.is_empty()
&& !record.env_vars.contains_key(provider_env_key);

if inject_global_model {
record.model = global.model.clone();
}
if inject_global_provider {
if let Some(ref gprov) = global.provider {
record
.env_vars
.insert(provider_env_key.to_string(), gprov.clone());
}
}

let mut surface = read_config_surface(
&record,
runtime_meta,
Expand All @@ -120,13 +156,21 @@ fn resolve_config_surface(
if !had_prompt {
retag_persona_default(&mut surface.normalized.system_prompt);
}
if !had_model {
if !had_model && !inject_global_model {
retag_persona_default(&mut surface.normalized.model);
}
if !had_provider && !provider_env_key.is_empty() {
if !had_provider && !provider_env_key.is_empty() && !inject_global_provider {
retag_persona_default(&mut surface.normalized.provider);
}

// Re-tag global-sourced fields from BuzzExplicit to GlobalDefault.
if inject_global_model {
retag_global_default(&mut surface.normalized.model);
}
if inject_global_provider {
retag_global_default(&mut surface.normalized.provider);
}

// Re-tag persona-snapshotted model from BuzzExplicit to PersonaDefault.
// Persona-created agents have record.model set at create time from the
// persona snapshot — had_model is true, but the model came from the persona,
Expand Down Expand Up @@ -184,6 +228,15 @@ pub fn get_runtime_file_config(runtime_id: String) -> Option<RuntimeFileConfigSu
}
}

/// Re-tag a field's origin from `BuzzExplicit` to `GlobalDefault`, leaving any
/// other origin untouched. No-op when the field is absent.
fn retag_global_default(field: &mut Option<NormalizedField>) {
if let Some(field) = field {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::GlobalDefault;
}
}
}
/// Get the full config surface for a managed agent.
///
/// Returns normalized + advanced config from all available tiers.
Expand Down Expand Up @@ -227,12 +280,14 @@ pub async fn get_agent_config_surface(
);
let runtime_meta = known_acp_runtime(&effective_cmd);
let session_cache = state.get_session_cache(&pubkey);
let global = crate::managed_agents::load_global_agent_config(&app).unwrap_or_default();

Ok(resolve_config_surface(
record,
&personas,
runtime_meta,
session_cache.as_ref(),
&global,
))
}

Expand Down Expand Up @@ -544,7 +599,13 @@ mod tests {
record.model = Some("explicit-model".to_string());
let personas = vec![persona_with_model("persona-model")];

let surface = resolve_config_surface(record, &personas, Some(goose_runtime()), None);
let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
);

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("explicit-model"));
Expand All @@ -565,8 +626,13 @@ mod tests {
let personas: Vec<PersonaRecord> = vec![];
let cache = session_cache("model-y", false);

let surface =
resolve_config_surface(record, &personas, Some(goose_runtime()), Some(&cache));
let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-x"));
Expand All @@ -588,8 +654,13 @@ mod tests {
let personas: Vec<PersonaRecord> = vec![];
let cache = session_cache("model-y", true);

let surface =
resolve_config_surface(record, &personas, Some(goose_runtime()), Some(&cache));
let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-y"));
Expand All @@ -610,8 +681,13 @@ mod tests {
let personas: Vec<PersonaRecord> = vec![];
let cache = session_cache("model-x", true);

let surface =
resolve_config_surface(record, &personas, Some(goose_runtime()), Some(&cache));
let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-x"));
Expand All @@ -629,13 +705,63 @@ mod tests {
let personas = vec![persona_with_model("persona-model")];
let cache = session_cache("model-y", true);

let surface =
resolve_config_surface(record, &personas, Some(goose_runtime()), Some(&cache));
let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-y"));
assert_eq!(model.origin, ConfigOrigin::RuntimeOverride);
assert_eq!(model.overridden_value.as_deref(), Some("persona-model"));
assert_eq!(model.overridden_origin, Some(ConfigOrigin::PersonaDefault));
}

/// Fix 2 regression: a global-default-only agent (no record model, no
/// persona model, but global has a model) that live-switches mid-session
/// must render the global model as the secondary tagged `GlobalDefault`.
/// Before the fix, `baseline` was `None` in the `!had_model` arm when
/// persona has no model, so `read_config_surface` had no secondary to
/// surface. Fails against pre-fix code where the baseline arm returned
/// `None` when `!had_model && persona_model.is_none() && model_overridden`.
#[test]
fn global_default_live_switch_renders_global_model_as_secondary_global_default() {
// Record has no model, no persona, global provides the model.
let mut record = agent_record();
record.persona_id = None;
// record.model = None (set by agent_record())
let personas: Vec<PersonaRecord> = vec![];
let cache = session_cache("model-y", true);
let global = crate::managed_agents::GlobalAgentConfig {
model: Some("global-model".to_string()),
..Default::default()
};

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&global,
);
let model = surface.normalized.model.expect("model resolved");

// Live model wins as primary.
assert_eq!(model.value.as_deref(), Some("model-y"));
assert_eq!(model.origin, ConfigOrigin::RuntimeOverride);
// Global model surfaces as secondary, tagged GlobalDefault.
assert_eq!(
model.overridden_value.as_deref(),
Some("global-model"),
"global model must be the override baseline secondary"
);
assert_eq!(
model.overridden_origin,
Some(ConfigOrigin::GlobalDefault),
"override baseline origin must be GlobalDefault, not PersonaDefault or BuzzExplicit"
);
}
}
Loading
Loading