Skip to content

fix(desktop): fold baked build env into in-process model discovery#1376

Merged
wpfleger96 merged 8 commits into
mainfrom
duncan/databricks-discovery-baked-env
Jun 30, 2026
Merged

fix(desktop): fold baked build env into in-process model discovery#1376
wpfleger96 merged 8 commits into
mainfrom
duncan/databricks-discovery-baked-env

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Internal builds bake DATABRICKS_HOST (and other provider config) into the binary via BUZZ_DESKTOP_BUILD_AGENT_ENV. The baked pairs were applied only onto subprocess Commands via build_buzz_agent_provider_defaults. The in-process discovery path (discover_databricks_models, discover_openai_compatible_models, discover_anthropic_models) read config from merged_env + std::env::var — neither of which carries the baked value in a GUI-launched DMG (no inherited shell env).

The Edit Agent dialog also used a one-shot snapshot path (get_agent_models) that fired once on open keyed on the saved provider. This produced only "default model / custom model" for any agent whose provider was null or whose snapshot env lacked DATABRICKS_HOST.

These are the root causes of the v0.3.38 Databricks model dropdown regression.

Fix

Baked-floor discovery (commit 1)

Add baked_build_env() returning the baked pairs as a BTreeMap<String, String>, factored through build_env_map() so the assembly logic is unit-testable. In both get_agent_models and discover_agent_models, fold the baked map as a floor under merged_env before the discovery calls:

let merged_env = discovery_env_with_baked_floor(merged_env); // baked pairs = floor

Precedence (lowest → highest): baked build env → derived runtime env → user env_vars

OSS builds (all option_env! unset) return an empty map — no behavior change.

Live provider-driven discovery in Edit Agent dialog (commit 2)

Replace the snapshot discovery path with the same live, form-driven path the New Agent / Persona dialog already uses:

  • EditAgentDialog: add provider + isCustomProviderEditing state seeded from the saved record. Derive selectedRuntime from the runtimes catalog via agentCommand. Wire usePersonaModelDiscovery so the model dropdown re-fires on every provider/env change without saving. Show EditAgentProviderField when the runtime supports LLM provider selection OR the agent already has a saved provider.
  • UpdateManagedAgentInput (types.ts): add provider?: string | null with tri-state semantics.
  • UpdateManagedAgentRequest (types.rs): add provider: Option<Option<String>> with double_option deserializer so absent/null/value are correctly distinguished at the Tauri boundary.
  • update_managed_agent handler: apply provider tri-state alongside model.

Files Changed

  • desktop/src-tauri/src/managed_agents/agent_env.rs — add baked_build_env() / build_env_map(), reimplement build_buzz_agent_provider_defaults, add 6 unit tests
  • desktop/src-tauri/src/managed_agents/mod.rs — export baked_build_env
  • desktop/src-tauri/src/commands/agent_models.rs — fold baked floor in both discovery commands; apply provider tri-state in update handler
  • desktop/src-tauri/src/managed_agents/types.rs — add provider: Option<Option<String>> to UpdateManagedAgentRequest
  • desktop/src-tauri/src/managed_agents/types/tests.rs — 3 new provider tri-state tests
  • desktop/src/features/agents/ui/EditAgentDialog.tsx — live discovery via usePersonaModelDiscovery, provider dropdown, send provider on submit
  • desktop/src/shared/api/types.ts — add provider?: string | null to UpdateManagedAgentInput
  • desktop/src/features/agents/ui/editAgentProviderDiscovery.test.mjs — 8 new JS tests

@wpfleger96 wpfleger96 force-pushed the duncan/databricks-discovery-baked-env branch 5 times, most recently from 8685821 to f96835c Compare June 30, 2026 17:59
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 8 commits June 30, 2026 17:30
The baked BUZZ_DESKTOP_BUILD_AGENT_ENV blob (e.g. DATABRICKS_HOST) was
only applied onto subprocess Commands via build_buzz_agent_provider_defaults.
The in-process discovery path (discover_databricks_models et al.) reads
provider config from merged_env + std::env::var — neither of which carries
the baked value in a GUI-launched DMG. Result: discover_databricks_models
returned Ok(None) on every DMG launch, falling through to the subprocess
which only returns goose's static config-options, not the dynamic list.

Add baked_build_env() returning the baked pairs as a BTreeMap, factored
through build_env_map() so the assembly logic is testable without
compile-time option_env! values. Implement build_buzz_agent_provider_defaults
in terms of baked_build_env() to eliminate duplication.

In both get_agent_models and discover_agent_models, fold the baked map
under merged_env before the discovery calls:

    let mut discovery_env = baked_build_env(); // floor
    discovery_env.extend(merged_env);          // user env_vars win

Precedence: baked build env < derived runtime env < user env_vars.
OSS builds (all option_env! unset) return an empty map — no-op.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The Edit Agent dialog used a one-shot snapshot path (getAgentModels →
get_agent_models) that fired once on open keyed on the saved provider.
This produced only 'default model / custom model' for any agent whose
provider was null or missing DATABRICKS_HOST in the snapshot env.

Replace the snapshot path with the same live, form-driven discovery
path the New Agent / Persona dialog already uses:

- EditAgentDialog: add provider + isCustomProviderEditing state seeded
  from the saved record. Derive selectedRuntime from the runtimes catalog
  via agentCommand. Wire usePersonaModelDiscovery with live envVars,
  provider, and selectedRuntime so the model dropdown re-fires on every
  provider/env change without saving. Show EditAgentProviderField when
  the runtime supports LLM provider selection OR the agent already has a
  saved provider.

- UpdateManagedAgentInput (types.ts): add provider?: string | null with
  tri-state semantics (absent = don't touch, null = clear, value = set).

- UpdateManagedAgentRequest (types.rs): add provider: Option<Option<String>>
  with double_option deserializer so absent/null/value are correctly
  distinguished at the Tauri boundary.

- update_managed_agent handler: apply provider tri-state alongside model.

DATABRICKS_HOST coverage: discover_agent_models calls
discovery_env_with_baked_floor (the fix from this PR), which folds the
baked DATABRICKS_HOST floor into the live discovery env. Verified at
source — the live path carries the host for any Databricks provider.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Address Thufir's three blocking findings from the Pass-1 review:

Finding 1 — fallback regression: EditAgentModelField was disabled
entirely when discoveredModelOptions was null, and the custom model
input was hidden unless discovery had returned options. The select is
now only disabled for mutation-pending or loading states, mirroring
Persona's additive discovery pattern. Static fallback options are
always shown; the custom input renders whenever custom mode is active
or the current model is custom.

Finding 2 — runtime selector missing: the dialog had no catalog-backed
runtime dropdown, so switching runtime to buzz-agent (Will's primary
workflow) was impossible. A full runtime dropdown is now rendered,
backed by sortPersonaRuntimes + the same catalog as Persona. Selecting
a catalog runtime updates agentCommand/agentArgs to its resolved
command. The custom-command input remains for unknown/pinned runtimes.

Finding 3 — provider locked runtime leak: llmProviderFieldVisible was
keyed on agent.provider (the saved value), so a Claude agent with a
stale databricks provider kept the picker visible and could persist a
conflicting provider. Visibility is now keyed exclusively on the LIVE
selectedRuntimeId via runtimeSupportsLlmProviderSelection. Switching to
a provider-locked runtime clears provider state and any associated API
key env var; the submit handler gates provider persistence on the live
runtime.

Also: lift isCustomModelEditing from EditAgentModelField to parent so
the model-clear useEffect can coordinate with it; consolidate provider
change handling into a single handleProviderDropdownChange; fix
relay_mesh.rs fixture to include persona_source_version and provider
(struct fields added in the preceding commit).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…t dialog

Finding A: the open-effect seeded selectedRuntimeId from the runtime catalog
synchronously, but useAcpRuntimesQuery loads async. When the dialog opened
before the catalog arrived, runtimes was [], the command match failed, and
selectedRuntimeId was stuck at 'custom' forever. A no-op save then sent
provider: null (llmProviderFieldVisible keyed on selectedRuntimeId was false),
silently clearing a valid databricks provider.

Fix: add a runtimeTouched ref (reset false on open, set true when the user
picks a runtime). Add a separate effect keyed on [open, runtimes,
agent.agentCommand] that re-derives selectedRuntimeId from the catalog once
it loads, but only while !runtimeTouched.current. The open-effect's biome-ignore
deps stay unchanged — no re-fire on the 5s poll, no edit-wipe.

Finding B: handleRuntimeDropdownChange updated selectedRuntimeId/agentCommand/
agentArgs but never touched inheritHarness. Submit resolved agentCommandUpdate
through the inheritHarness branch (sending undefined for an inherited agent with
no override), but llmProviderFieldVisible keyed on the live dropdown selection.
Inherited Claude agent → dropdown buzz-agent → provider databricks_v2 → save
= undefined command (still inherits Claude) + databricks_v2 provider persisted
= conflicting pair.

Fix: in handleRuntimeDropdownChange, when a concrete catalog runtime is selected
(nextRuntime.command is set), call setInheritHarness(false) to pin the runtime.
The dropdown is now the authoritative harness-pin control. The existing inherit
checkbox at line ~521 still works for the 'go back to inheriting' direction.
Provider persistence already gates on llmProviderFieldVisible which keys on the
live selectedRuntimeId — with inheritHarness pinned, agentCommandUpdate resolves
to the concrete command, making the persisted pair always consistent.

Tests added (+6): catalog re-derives runtime when not touched; catalog does not
overwrite user selection; no-op save preserves provider when catalog late;
runtime dropdown pins harness on concrete selection; inherited agent runtime
switch produces consistent command+provider pair; the guard-against-overwrite
case.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…pdown visibility

The inherit checkbox handler only called setInheritHarness(event.target.checked)
without re-deriving selectedRuntimeId or clearing provider. Submit gated provider
persistence on llmProviderFieldVisible (driven by the live dropdown), not the
runtime that would actually run after save.

Bad path: inherited Claude agent → pick buzz-agent (inherit→false) → choose
databricks_v2 → re-check inherit (inherit→true, selectedRuntimeId stays
buzz-agent) → save. agentCommandUpdate resolves to undefined (inheriting, no
override to clear), so the agent still runs inherited Claude. But
llmProviderFieldVisible=true (dropdown is buzz-agent), so provider='databricks_v2'
persists — the exact runtime/provider mismatch the prior passes were eliminating.

Fix: split visibility from persistence. Derive llmProviderCanPersistAtSubmit =
!inheritHarness && llmProviderFieldVisible and use it for the provider submit
branch. When inheritHarness is true, the effective runtime is the inherited
persona runtime — conservatively treat it as not-provider-capable so the UI
dropdown state can never persist a provider against a runtime that won't run it.
llmProviderFieldVisible is unchanged (still drives the provider picker UX).

The conservative approach (inherit=true → provider not persisted) is both
simpler and correct: the alternate path (resolve and check the persona's
runtime) would introduce a dependency on persona query data in the submit
path and is unnecessary for correctness.

Tests added (+2): inherit-checkbox round-trip does not persist provider against
inherited runtime; reverting to inherit when a provider was previously saved
clears it (sends null).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
When an agent inherits a provider-capable runtime (e.g. buzz-agent/Goose),
llmProviderCanPersistAtSubmit must be true so a name-only save does not
silently clear the agent's valid databricks_v2 provider snapshot.

The pass-4 conservative fix (!inheritHarness) was too broad: it correctly
blocked persisting a provider on an inherited Claude runtime, but also
incorrectly cleared the provider snapshot on any inherited provider-capable
runtime — including buzz-agent/Goose, where the snapshot is load-bearing.

Replace the !inheritHarness proxy with a real effective-runtime capability
check: when inheriting, match agent.agentCommand against the loaded catalog
(the same match the catalog-arrival effect already performs) and call
runtimeSupportsLlmProviderSelection on the matched runtime id. If no catalog
entry matches, fall through to the not-provider-capable path as the safe
unknown default. When pinned, behaviour is unchanged (defers to the live
dropdown selection, same as before).

This collapses the gate to the single correct source of truth and handles
all four cases:
  - inherited buzz-agent/Goose → provider-capable → preserve snapshot
  - inherited Claude → not-provider-capable → clear stale provider
  - unknown inherited command → conservative not-capable (safe default)
  - pinned runtime → unchanged (live dropdown gates as before)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…talog clear

Replace the boolean llmProviderCanPersistAtSubmit gate with a tri-state
ProviderRuntimeCapability ("capable" | "locked" | "unknown") so the
provider submit branch can distinguish between a known-locked runtime
(where clearing is correct) and an unknown capability state (where
clearing is data loss).

Two reachable forms previously conflated false/locked with unknown:

  Form 1: runtimes still loading at submit (runtimes=[]) — the catalog
  match returns nothing, yielding effectiveRuntimeIdForSubmit="",
  which runtimeSupportsLlmProviderSelection returned false for. This
  caused the else-branch to send provider:null, silently clearing a
  valid databricks snapshot on a name-only save.

  Form 2: catalog present but entry has command:null (adapter not
  installed) — the command-based match fails for the same reason.
  Added an id-based fallback to resolve known runtimes even when their
  adapter binary is absent.

The tri-state rule:
  capable  -> persist (value if changed, omit if unchanged)
  locked   -> clear: send null if provider was set, else omit
  unknown  -> omit always -- a transient discovery state is never
              a valid reason to write null to persistent storage

Adds four new behavior tests covering empty catalog (form 1),
command:null entry (form 2), locked-still-clears, and loaded-capable
no-regression. Total JS tests: 1286 (was 1282).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The runtime-seeding logic in both the open-effect and catalog-arrival
effect matched using r.command vs agent.agentCommand. For buzz-agent,
the catalog stores the resolved binary path (e.g.
/Applications/Buzz.app/.../buzz-agent) while effective_agent_command
returns the short command name ("buzz-agent"). These are never equal,
so selectedRuntimeId stayed "custom", selectedRuntime was undefined,
and canDiscoverModelOptions was false — discovery never fired for
inherited agents.

Add the same id-based fallback already used by effectiveRuntimeIdForSubmit:
try command-path match first, fall back to id === agentCommand.trim().
This makes inherited-runtime agents (No preference / app default)
drive databricks model discovery identically to explicit buzz-agent picks.

Also standardize visible copy on "runtime" (drop "harness" from the
checkbox label and helper text; internal inheritHarness variable kept
to avoid a noisy diff).

1290 JS tests (4 new regression tests for Bug A).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/databricks-discovery-baked-env branch from 308ab59 to 5c118cb Compare June 30, 2026 21:31
@wpfleger96 wpfleger96 merged commit f061ae9 into main Jun 30, 2026
25 checks passed
@wpfleger96 wpfleger96 deleted the duncan/databricks-discovery-baked-env branch June 30, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant