Preset rows in Settings → Agent runtimes fell back to initials because
DoctorSettingsPanel's RuntimeLogo only knew the builtin logo map, while
presets deliberately emit an empty avatar_url (no remote or user-supplied
icon URLs). Route preset ids through RuntimeIcon — the same component the
preset gallery uses — so both surfaces share PRESET_LOGOS and the per-logo
contrast treatments. Builtins keep the ProfileAvatar path; customs keep
the terminal glyph by design.
Playwright regression in harness-management.spec.ts asserts the doctor
rows render the bundled hermes/openclaw files, not initials.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Dawn's blocker: the loader-boundary collision/dedup enforcement was
untested — deleting either block left the full suite green. The stale
load_applies_id_collision_check only called check_id_collision directly
and its comment described the pre-fix architecture. Replaced with a
version that drives the REAL load_custom_harnesses against a shadowing
goose.json, plus a dedup twin (two files, same id -> len()==1). Both
verified to kill the mutants locally.
Wren's test-quality gap: the interleaving regressions pinned the
warm_harness_registry_locked seam but never executed discovery — a
stale-publish mutant of discover_acp_runtimes_from stayed green. Added
a cfg(test) pre_publish_test_hook between discovery's scan and its
registry publish, and two production-path regressions that land a
save/delete through the real discover_acp_runtimes_from call. Verified
both red under the stale-publish mutant.
Wren's product gap: the delete confirmation could be confirmed before
the blast-radius queries settled (pending queries rendered as zero
dependents with an enabled destructive button). New deleteConfirmState
drives the confirm UI: disabled + 'Checking which agents…' while
either query is initial-loading; on query failure deletion stays
possible but the copy says the count is unknown instead of claiming
zero. Unit-tested for pending/error/settled states.
Dawn + Wren both: get_agent_models now routes harness-resolution
errors through user_facing_harness_error (model_discovery_error
helper), so a dangling harness renders as a sentence, never the raw
DANGLING_HARNESS_ID sentinel. Also added Dawn's requested comment at
discovery's publish noting which tests pin the seam.
File-size ratchets bumped with documented deltas (all test/test-seam
growth).
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Conflicts resolved in agent_models.rs / agent_models_tests.rs: main's #2890
(live Databricks discovery via effective_discovery_provider +
provider_env_var) composed with this branch's unified
resolve_effective_harness_descriptor. get_agent_models now passes the
record's saved provider plus the runtime's provider_env_var (from
known_acp_runtime on the descriptor command) into
effective_discovery_provider, replacing both main's
SavedAgentModelDiscoveryConfig snapshot and this branch's ad-hoc
BUZZ_AGENT_PROVIDER env read.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Frontend layer of the review fixes:
- installHint survives edit: form seeding extracted to
formValuesFromCatalogEntry (single source of truth for edit
initialization — every user-editable field must appear there), input
rendered, and a lossless entry -> form -> save-payload round-trip
regression covers the full definition, not one field at a time.
- Delete confirmation counts referencing agents (direct record pins plus
persona-inherited runtime) and names the blast radius: 'N agents use
this harness and will stop launching.' Queries only run while the
confirmation is open.
- Comma-in-args rejection mirrored inline in the form with an error
naming the offending argument; the authoritative gate stays in
validate_harness_definition on the Rust side.
- ManagedAgent (TS) carries the record-level runtime pin; e2e bridge
mirrors the native wire shape (key always present, null when unpinned).
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Fixes#2896
**Root cause:** `arboard = "3"` compiles with default features only
(`image-data`), so the Linux build has no Wayland backend. In a Wayland
session every copy lands on the XWayland clipboard; the compositor only
mirrors X11 selections while an XWayland window holds focus, and
arboard's clipboard client is windowless, so Wayland-native apps read
nothing. `set_text` still returns `Ok`, which is why the invite dialog
shows "Copied" with an empty clipboard.
**Fix:** enable arboard's `wayland-data-control` feature. Under Wayland
it uses the `zwlr-data-control` protocol; when `WAYLAND_DISPLAY` is
absent it falls back to X11 as before. The `wl-clipboard-rs` dependency
is target-gated inside arboard, so macOS and Windows builds are
unchanged.
**Verified:** `cargo check` passes on the desktop crate; wl-clipboard-rs
now resolves in the lockfile.
Signed-off-by: Shawn Yeager <shawn@shawnyeager.com>
Review fixes for the BYOH generic ACP harness PR (backend layer):
- Reject commas in harness args at validate_harness_definition, the
shared boundary used by both save_custom_harness and the disk loader,
so a definition that would corrupt the comma-joined agent_args wire
format can neither be saved nor loaded.
- Dangling harness references (agent pins a deleted harness id) now
resolve to a typed sentinel: summaries render 'harness (deleted): <id>'
instead of silently falling back to buzz-agent, and spawn converts the
sentinel to an actionable sentence via user_facing_harness_error.
- ManagedAgentSummary carries the record-level runtime pin so the UI can
count referencing agents in the delete confirmation.
- Collision filtering moved into load_custom_harnesses so both loaders
inherit preset-shadowing rules; discovery publishes the registry by
re-reading the directory under persist_mutex (lock scoped to the
publish only, never across auth probes), with deterministic
interleaving tests for save-during-discovery and delete-during-discovery.
- load_global_agent_config hoisted out of build_managed_agent_summary
into a parameter: one disk read per caller instead of per record.
- Cleanup: drop discarded sweep arg from buzz_sweep_owns_process, remove
duplicated doc paragraph and stray SAFETY comment.
- De-flake grandchild PGID test: keep the process-group leader alive
through the getpgid assertion with a trailing sleep.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Refs #2062
This carries forward the relay-mesh recovery work from #2304 by @Bartok9
(cherry-picked with original authorship/sign-offs) and adds the
startup/readiness supervision and packaging fixes found while validating
it against a real two-machine Buzz setup.
## From #2304
- Watch the local OpenAI ingress (`:9337`) after launch and re-arm a
stale relay-mesh runtime.
- Require consecutive failed probes before eviction so a transient
inference stall does not cause a cold restart.
- Bound stale-runtime shutdown, preserve runtime identity across the
asynchronous probe, and never evict a concurrent replacement.
- Re-arm only for agents that are actually running; deliberately stopped
agents stay stopped.
- Persist an actionable sentinel error under the managed-agent store
lock and clear only that error after recovery.
- Treat serve-to-client fallback as an intentional fail-safe; configured
serve restoration remains on its existing path.
## Added here
- Use the inference ingress (`:9337`), rather than management port
`:3131`, as Buzz's client-readiness boundary. A usable client no longer
fails or holds agent-save open merely because management startup is
still pending.
- Supervise the embedded SDK startup asynchronously, publish a pending
status while management is unavailable, and keep its mesh identity
alive.
- Avoid racing a replacement while SDK startup still owns the embedded
runtime. If that pending startup later loses ingress while a running
agent still needs it, request a controlled Buzz restart to reclaim the
otherwise-unreachable SDK thread.
- Defer roster-driven replacement while client management startup is
pending.
- Keep post-launch recovery in a dedicated module so the mesh entry
point remains within the desktop file-size gate.
- Explicitly mark generated Unix sidecars executable. On macOS, copying
over an existing non-executable destination preserved its old mode,
causing packaged `buzz-acp`, `buzz-agent`, and tool sidecars to be
reported as missing.
## Validation
Automated:
- `just ci` — passed, including formatting, Clippy with warnings denied,
desktop/web/mobile checks and tests, and builds.
- Full Tauri mesh-feature suite — 1,702 passed, 0 failed, 15 ignored.
- Mesh-feature Clippy with `-D warnings` — passed.
- Release macOS app bundle with `mesh-llm` — built successfully; every
bundled sidecar passed executable-mode and deep code-signature
verification.
Live two-machine E2E:
- M5: released Buzz serving `unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M`.
- Mac mini: this branch's packaged Buzz running a saved
`buzz-agent`/relay-mesh agent.
- Confirmed `:9337` accepted inference and the saved ACP harness started
with no error while `:3131` was still unavailable.
- Exact inference succeeded before restart (`CORRECTED-MINI-E2E-OK`).
- Bespoke Buzz shut down cleanly in 2.3s, then restored ingress and the
saved harness in 18.8s while `:3131` was still unavailable.
- Exact inference succeeded after restart (`AFTER-RESTART-E2E-OK`).
- A real Buzz `@C55` message traversed desktop → `buzz-acp` →
`buzz-agent` → mini `:9337` → M5 compute and published the requested
reply successfully.
---------
Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Bartok9 <danielrpike9@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
## Problem
The Databricks model dropdown offers a handful of stale models — and
there's no way to tell that list apart from the real one. The AI Gateway
exposes **66** chat/embedding endpoints on `block-lakehouse-production`,
but the picker was showing a short list that includes models the gateway
no longer serves and embedding endpoints that can't chat at all.
Three independent defects, all on the discovery path:
**1. Live discovery never ran for agents with no saved provider.**
`get_agent_models` gates every in-process discovery attempt on the
provider (`is_openai_compatible_provider` / `is_anthropic_provider` /
`is_databricks_provider`), reading it straight from `record.provider`.
That field is `null` for every agent record created before provider
persistence — and for any agent that inherits its provider from the
build. So all three gates saw `None`, no HTTP discovery ran, and the
request fell through to the `buzz-acp models` subprocess. On the
Databricks path that subprocess returns `discovery_failure_fallback` —
the small hardcoded `DATABRICKS_V2_KNOWN_MODELS` catalog — which the
frontend renders exactly like a live catalog. An internal DMG that bakes
`BUZZ_AGENT_PROVIDER=databricks_v2` and a `DATABRICKS_HOST` still got
the fallback.
**2. The fallback list couldn't represent the running model.**
When discovery genuinely fails, the picker should at minimum be able to
show what the agent is actually configured with. For `DatabricksV2` it
couldn't: the fallback returned only the hardcoded slate, so a model
like `databricks-gpt-5-5` wasn't selectable in its own picker.
**3. Embedding endpoints were offered as chat models.**
`databricks-bge-large-en` was selectable (visible in the dialog today).
The v2 endpoints payload carries no `task` or `state` field, so there is
nothing to filter on but the name.
## Changes
- **`effective_discovery_provider`** (new,
`desktop/src-tauri/src/commands/agent_models_env.rs`) — an explicit
provider (saved record value, or the create/edit dialog's current form
value) still always wins; when there is none, discovery falls back to
the runtime's own provider env var (`GOOSE_PROVIDER`,
`BUZZ_AGENT_PROVIDER`, …) read off the merged env, which by that point
already carries the baked build floor and the process env. Wired into
both `get_agent_models` and `discover_agent_models`.
`SavedAgentModelDiscoveryConfig` now carries `provider_env_var` from
`known_acp_runtime`, so each runtime reads *its own* key rather than a
shared guess.
- The relay-mesh branches in `discover_agent_models` deliberately keep
using `input.provider`: those key off a deliberate provider selection,
never a baked default.
- **Asserted vs inferred matters for missing credentials.** The OpenAI
and Anthropic gates error on a missing API key, while the Databricks
gate falls through; an inferred provider hitting the first two would
have replaced a working subprocess catalog with `config:
ANTHROPIC_API_KEY required` (`export GOOSE_PROVIDER=anthropic` is
goose's documented way to pick a provider, and it keeps the key in its
own keyring). So `effective_discovery_provider` returns a
`DiscoveryProvider` that remembers how the value was resolved, and
`required_env` only reports a missing credential for an asserted
provider. A wrong guess declines and lets the subprocess answer.
- **`is_chat_capable_endpoint`** (new,
`crates/buzz-agent/src/catalog.rs`) — applied in
`parse_v2_endpoints_page`. Drops `*embedding*` and segment-matched `bge`
/ `gte` endpoints; keeps everything unrecognised (fail-open, so a new
model family is never hidden). Segment matching is why it's `split('-')`
and not `contains`: a substring check would swallow legitimate names.
- **`discovery_failure_fallback`** for `Provider::DatabricksV2` now
leads with the configured model (deduped against the known slate,
blank-tolerant), so a failed discovery still yields a picker that can
show the running model. The configured model is trimmed once up front —
`resolve_model` doesn't trim, so a padded `DATABRICKS_MODEL` used to
slip past the dedupe and appear twice.
- **`sort_v2_endpoints_newest_first`** (new, second commit) — the
catalog is now ordered newest-first on each endpoint's
`created_timestamp`, ties broken by name. Previously Buzz sorted
nothing, so the gateway's own order reached the picker: it pages in two
phases (Databricks-managed, then workspace-created — the page token
decodes to `{"phase":"user"}`), each alphabetical, which buried
`databricks-claude-opus-5` 8th behind five older Claude endpoints and
`goose-claude-opus-5` — the newest endpoint in the catalog — 55th of 63.
Sorting in `fetch_v2_models` means both discovery paths inherit it with
no wire or type changes, and the combobox filter preserves incoming
order. Endpoints with an absent or unparseable timestamp sort last
rather than first, so a wire-shape change degrades to "unordered at the
bottom" instead of "shuffled to the top".
- The name tiebreak is load-bearing: eleven managed endpoints share one
placeholder timestamp (`1699610000000`), so without it their relative
order would vary between runs. That placeholder is also not always
accurate — a few genuinely recent endpoints
(`databricks-kimi-k2-7-code`, `databricks-llama-4-maverick`) land at the
bottom with the 2023 batch. The gateway offers nothing better to sort
on.
- Env/provider lookup helpers moved out of `agent_models.rs` into
`agent_models_env.rs`. This keeps the command module under the file-size
limit **without ratcheting the override up** — the existing 1079 entry
is untouched (file is now 1066 lines).
## Verification
Live against `block-lakehouse-production`, release build:
```
BUZZ_ACP_AGENT_COMMAND=$PWD/target/release/buzz-agent \
BUZZ_AGENT_PROVIDER=databricks_v2 \
DATABRICKS_HOST=https://block-lakehouse-production.cloud.databricks.com \
DATABRICKS_MODEL=databricks-gpt-5-5 \
./target/release/buzz-acp models --json
```
- before: 66 endpoints, including `databricks-bge-large-en`,
`databricks-gte-large-en`, `databricks-qwen3-embedding-0-6b`
- after: **63** endpoints, `[.models[] | select(.id |
test("embedding|-bge-|-gte-"))]` → `[]`
Top of the list after the sort commit:
```
goose-claude-opus-5 2026-07-24
databricks-claude-opus-5 2026-07-23
databricks-gemini-3-6-flash 2026-07-20
databricks-gemini-3-5-flash-lite 2026-07-20
databricks-inkling 2026-07-14
```
Tests: 15 new (8 in `catalog.rs` — including the two-wire-shape
timestamp parse, the sort's tiebreak/no-timestamp cases, and the
padded-model dedupe — and 7 plus one assertion in
`agent_models_tests.rs`, 3 of them covering the asserted/inferred
credential split), two existing tests updated. `just check`, `just
test-unit`, and `just desktop-tauri-test` all pass (1636 desktop-tauri
tests, 274 buzz-agent lib tests).
Not run locally: the Docker-backed integration suite (`just test`) —
this diff touches neither `buzz-relay`, `buzz-db`, nor `buzz-auth`.
## Follow-ups (deliberately out of scope)
Two inference-path defects found while investigating, both reproduced
live against the gateway and both independent of discovery:
1. **Gemini thought signatures are dropped.** The gateway returns a bare
`thoughtSignature` on tool calls; the external-model serving endpoints
return it nested as `extra_content.google.thought_signature`. Neither
shape is round-tripped, so multi-turn tool use on `databricks-gemini-*`
fails with a 400 on the second turn.
2. **Array-shaped `content` is silently discarded.** Some models return
OpenAI `content` as a block array rather than a string; `parse_openai`'s
`str_field` returns `None` and the text is dropped.
The legacy `serving-endpoints` path does not work around either one, and
costs reasoning support on the GPT-5 family.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
- retire Virtua prepend reconciliation on every ordinary reader wheel
event, including events suppressed from its separate wheel-timing
heuristic
- keep Ctrl+wheel browser zoom excluded from reader ownership
- arm the Ctrl+wheel prepend probe before pagination begins so it cannot
miss the commit
- preserve ESM/CJS patch parity and update the patch lock hash
## Why main was red
PR #2855 added the reader-wheel retirement action after Virtua's
existing suppression guard. Once the first event set that guard, later
events in the same wheel burst returned before retiring prepend mode. A
late ResizeObserver correction could then pull the viewport backward by
20–40px. The same test had failed twice on #2855 but passed its final
retry, so the PR job appeared green; the merge commit lost all three
retries.
The separate Ctrl+wheel failure was a test race: its MutationObserver
was registered after the request had already been triggered and could
miss the prepend commit.
## Validation
- desktop full unit suite: 3,515 passed
- pre-push desktop check: passed
- `git diff --check`: passed
- CI is the E2E verification; no local E2E was run
Fixes the main-branch failure in CI run 30180099007.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- make Virtua the sole prepend geometry/correction owner by accumulating
active prepend ResizeObserver corrections from the live DOM offset
- retire prepend reconciliation on ordinary reader wheel input, while
preserving it for Ctrl+wheel browser zoom
- remove Buzz's competing three-second semantic-anchor watcher and
corrective `scrollBy` loop
- keep ESM/CJS Virtua patch behavior equivalent and update the patch
lock hash
## Why
Buzz admitted prepended rows using seeded estimates, then Virtua
received multiple measurement corrections for the same transaction. Each
correction was based on the same stale model offset, so a later write
replaced an earlier correction instead of accumulating it. In the
reproduced first page, that resurrected 452px of anchor drift; the
app-level watcher merely corrected the lost virtualizer write afterward.
This fixes the correction inside Virtua and deletes the competing app
writer, following the single-owner geometry invariant used by Berd
rather than copying its spacer implementation.
## Validation
- watcher-off desktop virtualization matrix: 11/11 passed, including 15
cascading prepends, continued wheel input, detached rich-row growth,
channel switching, bottom follow, and buffered live arrivals
- focused cascading prepend/Ctrl+wheel regression passed
- desktop typecheck passed
- desktop unit suite passed: 3,495 tests
- Biome passed on changed desktop files
- `git diff --check` clean
## Manual behavior
Load older history repeatedly while scrolling upward, then wheel
downward during/after a prepend. The visible anchor should stay within
the existing 5px contract during reconciliation, and deliberate reader
movement should not be pulled back. Ctrl+wheel during the prepend commit
must not cancel reconciliation.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Desktop's active-turn store capped tracked concurrent turns per agent at
`4` while the harness runs `DEFAULT_AGENT_PARALLELISM = 24` parallel
agent subprocesses and accepts up to `32` (`--agents` /
`BUZZ_ACP_AGENTS`, `value_parser range(1..=32)`). Turns above the cap
were silently evicted, so a genuinely-running turn lost its working
badge in the sidebar and the agents popover.
The eviction also caused the badge set to rotate indefinitely. Evicted
turns are still alive, so their hosts keep emitting `turn_liveness`
every 10s; `recordActivity` can't find the evicted turn, `resurrectTurn`
recreates it, and that eviction drops one of the surviving turns. With
two live turns above the cap the visible set churned every 10 seconds.
`MAX_TURNS_PER_AGENT` exists only to bound map growth, so it now sits at
the harness's hard upper bound of `32` — unreachable for any
legitimately-configured agent while still keeping the per-agent map
bounded. `MAX_TERMINAL_TOMBSTONES` derives from it (`* 4`), so the
tombstone cap moves from 16 to 128.
Two regressions cover the reported symptom: a default-parallelism agent
working in 24 channels keeps all 24 badges, and the tracked channel set
stays stable as `turn_liveness` arrives for turns that previously would
have been evicted.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Why
Manual relay recovery polls every three seconds, repeatedly consuming
the session's exponential-backoff timer and leaving reconnect behavior
stuck or noisy on degraded networks.
## What
- Replace fixed-cadence Phase 3 polling with observation of the
RelayClient background reconnect loop
- Raise the fast-path deadline above the native websocket timeout and
enforce that contract in a regression test
- Keep the existing 120-second backstop as a soft UI timeout without
stopping background retries
## Risk Assessment
Medium — this changes live relay recovery timing, but removes a
competing retry loop rather than adding one. The existing
connection-state subscription remains the success signal.
## References
- Stacked on #2310 (`lazyjoe/reconnect-testability-refactor`)
- Investigation: `RESEARCH/BUG_RELAY_RECONNECT_HANG.md`
- `just ci`
- `cd desktop && pnpm typecheck && pnpm check && pnpm test` (3485 pass)
- `git diff --check`
Generated with Codex
Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co>
Replace the incorrect Oh My Posh and Dify marks with the exact upstream Oh My Pi and Kimi Code assets. Use xAI’s official Grok logomark under its nominative brand terms, with presentation-only backgrounds for marks that need contrast.
Remove the unproven Cursor bitmap and deliberately retain the generic terminal fallback because Cursor publishes assets without granting redistribution permission. Record exact provenance and keep the drift guard explicit about that exception.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
## Why
Prepare the relay reconnect controller for an isolated behavior-fix PR
without changing runtime behavior in this one.
## What
- Export the current reconnect timing policy from
`RelayReconnectController`
- Allow controller tests to inject the complete timing policy
- Add characterization coverage for the production timing values and
injected fast-path/poll/backstop timers
## Risk Assessment
Low — this preserves the existing production timing values and only
replaces private module constants with a default policy object used by
the controller. The weak reconnect-timer and backstop wrapper
extractions were removed from this PR.
## References
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check`
- `cd desktop && pnpm test` (3373 pass)
- `git diff --check`
- Push hooks were bypassed after the requested desktop validation
because the broad pre-push hook runs without Hermit here and fails on
Node 20/pnpm 11 plus unhealthy local Postgres services.
Generated with Codex
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: npub1m0vvn9qm5md0a080p27qzkm9uaw49e699ukwfq7fc0756xq0y5zqhzhdk2 <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@sprout-oss.stage.blox.sqprod.co>
When Desktop stops or restarts a managed-agent pair, the harness is
SIGKILLed after 1s and `turn_completed` never lands.
`activeAgentTurnsStore` then sees the "all turns silent at once" pattern
and delays badge cleanup for up to 3 min (the pause that protects live
badges during transient relay-stream gaps). This is unnecessary when
Desktop itself issued the kill — there is no relay-gap ambiguity.
## What changed
**`activeAgentTurnsStore.ts`** — new `clearActiveTurnsForAgent(pubkey)`
- Tombstones every live turn for the agent via `recordTerminal` (blocks
in-flight `turn_liveness` frames from resurrecting them via
`resurrectTurn`)
- Removes the agent's entry from `activeTurnsByAgent`
- Preserves `lastProcessed` (watermark) — full-buffer replay after the
clear is a no-op
- Preserves `clockOffsetByAgent` — still valid, harmless
**`managedAgentRuntimeHooks.ts`** — clearing at the successful-stop
boundary
- New `clearActiveTurnsForAgentOnStop(pubkey, relayUrl?)` — relay-scope
gate: only clears when the stopped pair's relay matches the active
community (pair-scoped), or when an active community is configured
(agent-wide ops)
- New `restartManagedAgentPair(pubkey, relayUrl, stop, clear, start)` —
dependency-injected stop → clear → start sequence; the `restart` branch
of `useManagedAgentRuntimeAction`'s `mutationFn` is a single call into
it. The clear fires after a successful stop and before start begins, so
the badge is gone even when start fails, a failed stop clears nothing,
and no clear can run after the new process is spawned (genuinely-new
turns are never wiped)
- `useManagedAgentRuntimeAction.onSuccess` clears for `stop` actions,
before the query-cache update
**`managedAgentControlActions.ts`** — `onStopped` callback on
`respawnManagedAgentWithRules`, invoked after the stop promise resolves
and before start begins
**`welcomeKickoff.ts`** — same `onStopped` boundary on
`restartWelcomeTeammate`
**Call sites covered (all stop/restart UI paths):**
- `useManagedAgentRuntimeAction` — pair-scoped stop (`onSuccess`) and
restart (`restartManagedAgentPair` in `mutationFn`); Members-sidebar +
settings card
- `useMembersSidebarActions.handleRespawnAll` — via `onStopped`
- `useMembersSidebarActions.handleStopAll` — direct local-stop branch
- `useMembersSidebarActions.handleLifecycleAction` — local-stop fallback
branch
- `useAgentLifecycleActions.handleAgentPrimaryAction` — Agents-tab stop
- `useAgentLifecycleActions.handleAgentRestart` — via `onStopped`
- `useManagedAgentActions.handleStop` / `handleBulkStopRunning` — Agents
screen
- `useAutoRestartPolicy` — inline, between stop and start
- `restartWelcomeTeammate` call site — via `onStopped`
Provider agents are excluded at each site: they go through `!shutdown`
(relay message), not a direct harness kill.
## Tests
Twelve behavior tests across three files:
- `activeAgentTurnsStore.test.mjs` (6) — clear removes the agent's turns
and notifies subscribers, other agents untouched; full-buffer replay
after clear is a no-op (watermark preserved); late `turn_liveness` frame
with timestamp ≤ clear time does not resurrect (tombstone); new
`turn_started` after clear is tracked normally; badge gone when stop
succeeds even if start fails; new frame during start-pending does not
resurrect the cleared badge
- `managedAgentControlActions.test.mjs` (3) — `onStopped` fires on
stop-success/start-failure; does not fire on stop-failure; strict stop →
`onStopped` → start ordering
- `managedAgentRuntimeHooks.test.mjs` (3) — pair-restart seam: clear ran
when start fails (rejection propagates); stop failure invokes neither
clear nor start; strict stop → clear → start ordering
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The C-10 harness-management spec was 5 passed / 5 failed and had never run in
CI — it was added without an entry in either playwright.config.ts project's
testMatch, so no gate covered it. Three test-side defects, no product defects.
1. Ambiguous "Save" selector. getByRole("button", {name: "Save"}) substring-
matches "Save defaults" from AgentDefaultsEditor, a sibling card in the
same Settings -> Agents panel. Scoped to the form, exact match.
2. The mock catalog ignored its own mutation store. handleDiscoverAcpRuntimes
returned a test-supplied acpRuntimesCatalog early, never merging
mockCustomHarnesses — so save/delete mutated a store the seeded specs
never read back, and add/edit/rename/delete all silently no-oped.
Fixed with one mergeMockCustomHarnesses used by both catalog paths. A
deleted *seeded* row has no store entry to remove, so removal needs a
tombstone set rather than just the map; the same mechanism covers the id
vacated by a rename.
3. The onboarding-nav test was unsatisfiable as written. It needs machine
onboarding to run (community must not vouch for the active identity) and
needsSetup to be false afterwards (or App.tsx renders WelcomeSetup instead
of the router). skipCommunitySeed leaves zero communities and fails the
second; the default seed stamps the active pubkey and fails the first.
Seeding a community under a foreign pubkey satisfies both.
Instrumenting this showed the navigation itself always worked — the hash
became #/settings?section=agents on click, with WelcomeSetup painted over
it because the router never mounted. The route intent is correct.
Adds unit coverage for the merge/tombstone logic; reverting the tombstone
filter fails exactly the delete and rename cases.
Verified at 91080e0a9: harness-management 10/10, unit suite 3560/3561 (the
one failure is the pre-existing styled-qr-code / missing qrcode package
fault, identical on a pristine tree), tsc clean, biome clean 1641 files,
check-file-sizes clean.
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Fixes#1822
## Problem
Renaming an agent via the agent settings dialog performs a local save
(always succeeds) and a best-effort relay kind:0 republish (can fail:
network blip, auth expiry, relay unreachable). On sync failure the
dialog `console.warn`'d the error and closed as a clean success — the
user had no signal that the relay still holds the old name, which breaks
`@mention` resolution and shows the stale name in other agents' `From:`
lines (discovered via #1743).
## Fix
Surface `profileSyncError` as a `toast.warning` in
`AgentInstanceEditDialog`, matching the treatment the create and
persona-save paths already give the same field
(`useManagedAgentActions.ts`, `UserProfilePanelPersonaSubmit.ts`). The
save is not blocked — the local rename is valid and persists, per the
issue's guidance.
The toast points at the retry path that actually works: **restarting the
agent**. Re-saving the same name does not retry — `update_managed_agent`
computes `name_changed` against the already-updated record, so a second
identical save skips the sync — but `start_managed_agent` fires
`reconcile_agent_profile`, which queries the relay's kind:0 and
republishes when the display name diverges.
Scope notes:
- `EditRespondToDialog` (the third caller of the update mutation) never
changes the name, and the Rust side only sets `profile_sync_error` when
the name changed — so no change needed there.
- The alternative fix in the issue (retry-with-backoff in
`sync_managed_agent_profile`) is not taken here;
`reconcile_agent_profile` on agent start already provides self-healing,
and this change makes that path discoverable at the moment of failure.
## Testing
- `just desktop-check` — biome + file-size + px-text + pubkey-truncation
guards clean.
- `just desktop-test` — 3331 passed, 0 failed.
- The handler branch is a straight conditional on the mutation result;
the repo's `.test.mjs` convention covers extracted pure logic, and there
is no extracted logic here to unit-test (consistent with the equivalent
toast branches on the create/persona paths).
---------
Signed-off-by: ayobamiseun <adegokeayobamiseun@gmail.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Brings the branch up to date with origin/main (2a051a404) so CI and local
gates run against the tree that will actually merge. Requested as a merge
commit rather than a rebase to keep Duncan's authored history intact.
Two ratchet entries in desktop/scripts/check-file-sizes.mjs needed
composing, since both sides raised per-file ceilings from a shared base:
- src-tauri/src/commands/agent_discovery.rs — textual conflict. BYOH took
1826 -> 2038 (+212); main #2767 took 1826 -> 1836 (+10). Resolved to
2048 with both comment blocks kept, preserving the slack each side
intended rather than discarding one growth.
- src-tauri/src/managed_agents/discovery.rs — no textual conflict, but the
BYOH ceiling of 1715 predates main's +17 and the merged file is 1729
lines, so the check failed on a clean auto-merge. Composed to 1732.
The overrides map is a ceiling (violation only when lineCount > limit), so
composing deltas is the resolution that matches the file's convention.
Verified on the merged tree: desktop file-size check clean, frontend suite
3554/3555 (the one failure is the pre-existing styled-qr-code 'Cannot find
package qrcode' local module-resolution fault, which fails identically on
pristine heads), presetLogos drift guard 10/10 with all 8 preset ids still
parsing out of main's modified discovery.rs, tsc --noEmit clean, biome
clean over 1640 files, just test-unit 5/5 crates, and desktop-tauri-test
1691/1691.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
## Why
The Codex adapter install-plan test depended on the process-global
login-shell PATH cache, making it flaky under concurrent or loaded CI
runs.
## What
- Thread an explicit probe PATH through adapter install planning
- Use a controlled PATH in Codex install-plan tests
- Update the existing desktop file-size allowance for the focused seam
## Risk Assessment
Low — production behavior keeps using the same augmented PATH; only
dependency injection and deterministic tests change.
## References
- Failure:
https://github.com/block/buzz/actions/runs/30110680608/job/89539202266
- Validation: `just desktop-tauri-test`; `cd desktop && pnpm check`;
full pre-push hooks
Generated with Codex
---------
Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz>
Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
PRESET_HARNESSES ships 8 tier-2 presets; PRESET_LOGOS and
desktop/public/harness-logos/ covered only 6. `hermes` and `openclaw`
fell through to the generic TerminalSquare glyph beside six siblings
showing real brand marks, and RuntimeIcon's onError fallback made a
missing file indistinguishable from an unmapped id at runtime.
Add both marks from their MIT-licensed upstreams:
- hermes.png from NousResearch/hermes-agent@6ad632b
website/static/img/logo.png — cropped the baked-in border frame
(a dark box once downscaled), padded square, 64x64, 16-colour
palette. 1.7 KB, in line with the existing 1.0-3.4 KB siblings.
- openclaw.svg from openclaw/openclaw@b06f40a ui/public/favicon.svg —
upstream animates the mascot with SMIL specifically so it plays
inside <img>-loaded SVGs, which is exactly how RuntimeIcon renders
it. Stripped the animation elements to hold the rest pose (verified
pixel-identical to the upstream t=0 frame at 256px) so a settings
list doesn't bob and blink.
Provenance and modifications are recorded in a new CREDITS.md next to
the assets, and the contributor guide's step 4 now requires that row.
The guard: the two sides of this mapping are a Rust slice and a TS
record, so nothing typechecks them against each other — which is why
the gap went unnoticed through review and screenshots. presetLogos.test.mjs
parses PRESET_HARNESSES out of discovery.rs and asserts every preset id
has a PRESET_LOGOS entry whose file exists on disk, plus the reverse
direction for stale entries. It follows the cross-language fixture
precedent in effortTable.fixture.test.mjs. Verified failing on each of
the three drift modes it claims to catch, not just passing green.
Importing RuntimeIcon.tsx from a test needed the node test loader to
tolerate Vite asset imports (`./claude.png?inline`), which it resolved
as a bare package and threw on; assets now load as an inert stub, the
same treatment CSS already gets.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
C-10 (full form — Playwright spec):
Add desktop/tests/e2e/harness-management.spec.ts with 10 tests covering:
- Preset gallery: detected badge present for available preset
- Preset gallery: install link shown, no badge for not-installed preset
- Add custom harness: form save → row appears in list
- Edit preserves env vars: definitionEnv round-trip via edit form
- Same-ID edit replaces row without duplication
- Rename: old row gone, new row present
- Delete success: row disappears
- Delete failure: inline error shown, row kept (error-injection knob)
- PATH badge on custom row: Detected badge when availability === 'available'
- F8 onboarding navigate: setup-page More-harnesses click →
completes onboarding → Settings → Agents (settings-harness-management)
This exercises the real App.tsx effect that gates router.navigate()
on machine.stage === 'ready', which postOnboardingNav.test.mjs cannot
cover (it simulates the predicate, not the render path).
Delete-error injection knob:
- Add deleteCustomHarnessError?: string to MockBridgeOptions in bridge.ts
- Add same field to MockE2eConfig in e2eBridge.ts
- Thread config through to handleDeleteCustomHarness so the e2e bridge
throws when the knob is set (mirrors acpAuthMethodsError pattern)
- Add unit test: throws when knob set + entry survives in store
D-15 (README execution-locus sentence, full placement):
The install_hint placement (already in discovery.rs) is correct and stays.
Add the missing sentence to the README blockquote at line 265 per the brief:
Desktop-injected BUZZ_* env vars do NOT reach the execution locus unless
set on the Gateway's own environment.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
D-12: Revert mobile/pubspec.lock to origin/main. The meta 1.17→1.18 /
test 1.30→1.31 churn was a no-source-change lockfile bump introduced by
accident; reverting eliminates the unrelated diff.
D-15: Add OpenClaw execution-locus note to the preset's install_hint.
Eva's finding: openclaw acp executes tools inside the Gateway daemon, not
in the Desktop process. Desktop-injected BUZZ_* env vars reach the
openclaw harness process itself but do NOT automatically propagate to
the Gateway's execution environment. The install_hint now surfaces this
caveat so users who need BUZZ_* credentials at execution time know they
must set them on the Gateway's own environment.
F8 (onboarding navigate test): New pure-logic test file
postOnboardingNav.test.mjs proves the App.tsx postOnboardingNav
useEffect predicate — navigateAfterComplete does not fire before
machine.stage reaches 'ready', fires exactly once on the ready
transition, is cleared after firing (no double-fire), fires immediately
if nav arrives while already ready, and carries the exact
{to: '/settings', search: {section: 'agents'}} shape from
MachineOnboardingFlow's navigateToAgentSettings action.
C-10 (e2eBridge + handler tests): Extract save_custom_harness /
delete_custom_harness handler logic into e2eBridgeCustomHarnesses.ts
(exported module-level functions + mockCustomHarnesses Map +
resetMockCustomHarnesses). Wire the handlers into e2eBridge.ts:
- mockCustomHarnesses imported and appended to discover_acp_providers
results so saved custom harnesses appear in future discovery calls
- case 'save_custom_harness' → handleSaveCustomHarness
- case 'delete_custom_harness' → handleDeleteCustomHarness
New test file e2eBridgeCustomHarnesses.test.mjs (14 tests across 3
describe groups) proves: save → store, non-empty env preserved,
empty env absent, same-ID edit replaces, rename removes old + inserts
new, delete removes, delete idempotent, Map reference is shared.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
B-4 (persistence contract): Replace the two simulated round-trip tests that
called raw fs::write/remove_file with six tests that exercise the real
save_custom_harness_to_dir helper directly:
- save_to_dir_create_writes_file_and_loads_back
- save_to_dir_same_id_edit_replaces_content
- save_to_dir_backup_is_cleaned_up_after_same_id_edit
- save_to_dir_rename_removes_old_file_and_creates_new
- save_to_dir_rename_nonexistent_old_id_is_non_fatal
- save_to_dir_roundtrip_with_env_preserves_values
B-3 (env validation boundary): Six tests exercising validate_harness_definition_pub
integration with validate_user_env_keys for the documented attack surfaces:
- validate_rejects_malformed_key_with_equals_sign (BUZZ_AUTH_TAG=x forgery shape)
- validate_rejects_reserved_key_buzz_managed_agent (ownership marker)
- validate_rejects_reserved_key_case_insensitive (lowercase bypass)
- validate_rejects_nul_byte_in_value (Command::env panic protection)
- validate_rejects_value_over_per_value_size_limit
- validate_accepts_well_formed_env
B-2 (API boundary): Export fromRawAcpRuntimeCatalogEntry from tauri.ts and
add four tests to tauri.test.mjs proving the Rust definition_env snake_case
field is mapped to definitionEnv camelCase, that absent definition_env defaults
to {} (not undefined), and that env round-trips end-to-end through the mapper
so a save-then-edit cycle cannot erase definition env.
B-6 (concurrent-safe registry): Add save_and_warm + delete_and_warm functions
in custom_harnesses.rs that hold a PERSIST_MUTEX for the filesystem mutation
and the subsequent warm_harness_registry_from_dir call as an atomic unit.
Update save_custom_harness and delete_custom_harness Tauri commands to use
these helpers. Eliminates the lost-update window where two concurrent saves
could interleave their warm calls and produce a stale registry snapshot.
File-size override added for custom_harnesses.rs (1042 lines after the new
tests; queued to split once the feature stabilizes).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- render mobile pairing QR codes locally with Wallet-inspired styling
- hide the raw pairing URI behind a matching copy button
- add focused rendering and dialog coverage
## Test plan
- desktop lint and 3,489 desktop tests
- focused pairing dialog Playwright test
- decoded the rendered QR back to the exact pairing URI
## Summary
- Replace all references to a specific corporate VPN product name with
generic "VPN" / "corporate VPN" / "VPN tunnel" / "VPN CLI" language
across 15 files (21 lines)
- Comment-only and doc-only changes — zero functional impact
- Keeps the OSS repo free of vendor-specific assumptions
`rg -i warp` returns zero hits after this change (excluding
`node_modules`, lockfiles).
## Summary
- add a macOS local-network purpose string
- explain that LAN access is for optional Share Compute and local relay
connections
- clarify that messaging through a remote relay does not need it
## Context
macOS currently falls back to its generic Local Network prompt because
Buzz's `Info.plist` has no `NSLocalNetworkUsageDescription`. The alert
says Buzz can find and collect data from devices without explaining
which Buzz features use that capability, which makes an optional feature
feel invasive.
Buzz can intentionally make local-network connections for Share Compute
peers and user-configured local relays. Ordinary messaging through a
remote relay does not require that access.
Apple recommends that any app using the local network directly or
indirectly include `NSLocalNetworkUsageDescription`:
https://developer.apple.com/documentation/bundleresources/information-property-list/nslocalnetworkusagedescription
This PR improves the disclosure only. It does not change when macOS
asks. The exact unexpected trigger should be handled separately once it
can be reproduced and attributed.
## Verification
- `plutil -lint desktop/src-tauri/Info.plist`
- built a debug macOS `.app` bundle and confirmed its final `Info.plist`
contains the exact purpose string
- `just ci`
Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>
## Why
Installing the Codex, Claude, or Goose desktop app does not install the
command-line harness Buzz needs. The current UI makes that distinction
unclear, links some missing-CLI states to adapter documentation, and can
report a successful install from the installer exit code even when
runtime discovery still fails. On Windows, Buzz also invokes Goose's
Bash installer, which writes the executable somewhere Buzz does not
discover.
## What
- distinguish missing vendor CLIs from missing or outdated ACP adapters
in runtime metadata and UI guidance
- link Codex, Claude Code, and Goose missing-CLI states to their
official CLI installation documentation
- explain in Settings, onboarding, and agent configuration that the
desktop app alone is not sufficient
- use Goose's official PowerShell installer on Windows
- refresh PATH and rediscover the requested runtime after installation,
keeping the control retryable if the runtime is still unavailable
- add Rust and Playwright regression coverage for Windows installer
selection, CLI/adapter guidance, false-success prevention, verified
installs, and onboarding copy
## Risk Assessment
Medium. This changes desktop onboarding and runtime installation
behavior. Successful installs now require the runtime catalog to verify
availability; previously hidden discovery failures will surface as
actionable errors instead of a false success state.
## References
- [Codex CLI installation](https://developers.openai.com/codex/cli/)
- [Claude Code
installation](https://code.claude.com/docs/en/getting-started)
- [Goose
installation](https://goose-docs.ai/docs/getting-started/installation/)
- Follow-up to #2563 and #2587
## Validation
- `just desktop-typecheck`
- `just desktop-test` — 3,455 passed
- focused Rust post-install verification tests
- focused Playwright Doctor/onboarding coverage (in progress; CI and
local sequential rerun will provide final results)
Generated with Codex
---------
Signed-off-by: Atish Patel <atish@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Goose <opensource@block.xyz>
## Problem
On Windows, `install_shell_command` wraps every install command in Git
Bash `-l -c`, including the Windows-specific `powershell.exe … irm
https://chatgpt.com/codex/install.ps1 | iex` command. A Git Bash login
shell prepends its POSIX dirs (`C:\Program Files\Git\usr\bin`) to PATH,
so when Codex's `install.ps1` shells out to bare `tar -xzf C:\…`, it
resolves Git's GNU tar (`/usr/bin/tar`) instead of Windows bundled
bsdtar. GNU tar parses `C:` as a remote host:
```
tar (child): Cannot connect to C: resolve failed
gzip: stdin: unexpected end of file
/usr/bin/tar: Child returned status 128
/usr/bin/tar: Error is not recoverable: exiting now
Downloaded Codex package archive did not contain the expected package layout.
```
Claude's installer doesn't hit the same failure because it doesn't shell
out to `tar`.
## Solution
On Windows, detect `powershell.exe` install commands and spawn them
natively (`Command::new("powershell.exe")`) instead of routing them
through Git Bash. The discriminator is a case-insensitive prefix check
on the first whitespace-delimited token — minimal and precise.
The native spawn preserves everything `install_shell_command` provides
that applies:
- `NPM_CONFIG_*` / `COREPACK` env strip + managed npm prefix env
- PATH composed from managed Buzz dirs + inherited process PATH (no
POSIX login-shell dirs)
- `CREATE_NO_WINDOW` so no console flash
- stdin null, piped-drain in `run_install_command`
- The retry/backoff/annotate logic is fully shared
The `-Command` body is split correctly at the boundary
(case-insensitive) and passed as a single argument to preserve pipes and
spaces inside the installer script call.
Non-PowerShell commands (e.g. `npm install -g …` adapter steps) continue
through the existing Git Bash path unchanged.
## Tests
6 unit tests:
1. `is_powershell_command` detection (positive + negative)
2. Routing: PowerShell → native spawn on Windows, non-PowerShell → Git
Bash
3. Unix: non-Windows path returns the shell command unchanged
(compile-time cfg)
4. `-Command` body preservation (no bash args in native spawn; body is
single arg)
Full `just desktop-tauri-test` suite: 1627/1627 passing. Windows CI will
validate end-to-end.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
On Windows, the model dropdown never populates for CLI harnesses (Claude
Code, Codex) while the same flow works on macOS.
Model discovery spawns `buzz-acp models` with `PATH` taken from
`login_shell_path()` — which by design always returns `None` on Windows
(Git Bash's POSIX-shaped PATH would poison native children). The
discovery child therefore ran with only the raw inherited process PATH,
missing the Buzz-managed Node/npm directories and exe-parent sidecar
dir, so the ACP adapter's `.cmd` shims failed to resolve `node` and
discovery returned nothing. macOS worked only because a login-shell PATH
exists there.
The fix reuses the existing `augmented_path()` helper (already used by
CLI login probes and auth commands, built on the same
`build_augmented_path` kernel as the real agent spawn), so model
discovery resolves the identical toolchain the agent will actually run
with. `login_shell_path()` remains the login-shell component inside that
composition — on macOS the composed PATH is a superset of the previous
value.
Related: #2661 (managed Node fallback these entries point at), reported
in the Windows install-issues follow-up.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz>
## Summary
- Preserve plain-string errors returned by Tauri and show an actionable
message when huddle audio is unavailable in the relay deployment.
- Use one error formatter across channel, timeline, wave, and profile
huddle actions while preserving other relay and device errors.
- Complete the huddle lifecycle when audio setup fails after publishing
a start event, preventing peers and reloaded clients from reconstructing
a phantom active huddle.
- Cover unavailable-audio formatting and START → rollback-END
reconstruction with regression tests.
## Behavior
The Tauri huddle commands reject with a plain string. Several desktop
toast call sites only preserved JavaScript `Error` objects, so the relay
message was discarded and replaced with “Failed to join huddle.”
The desktop now recognizes `huddle_audio_unavailable` and the current
relay message, then shows:
> Huddle audio isn’t available on this server. Ask an administrator to
turn it on.
Other relay and device messages remain intact, including microphone
errors.
`start_huddle` also publishes `KIND_HUDDLE_STARTED` before audio setup.
If setup fails, rollback now publishes `KIND_HUDDLE_ENDED` through the
normal end-and-archive path before resetting local state. This makes the
failed start observable to lifecycle reconstruction and prevents stale
join affordances.
## Checks
- `cd desktop && pnpm test` — 3,405 passed
- `cd desktop/src-tauri && cargo test` — 1,560 passed, 13 ignored; 3
diagnostic tests passed
- `cd desktop && pnpm exec playwright test tests/e2e/channels.spec.ts
--project=smoke --grep 'huddle rollback end event'` — passed
- `just desktop-tauri-check`
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check`
- Pre-commit and pre-push hooks