Complete the cross-community reuse fix on the *creation* path. The attach
guards (commit b49523e7f) stopped a foreign-relay agent from being
silently attached, but the reuse finders that decide "reuse existing vs
create new" were still relay-unfiltered: a persona/generic agent pinned to
community A's relay would surface as a reuse candidate while Desktop viewed
community B, so creating a B agent could return the deaf A agent instead of
a fresh B one — user-visible wrongness (Eva's scope call).
Relay-scope all three reuse finders with the existing
`agentBelongsToRelay` helper:
1. `findReusablePersonaAgent` / `findReusableGenericAgent` gain a REQUIRED
`activeRelayUrl` and filter candidates to the active community's relay
before preference selection. A foreign-relay candidate is excluded, so
the caller falls through to creating a fresh agent on the active relay.
2. `findReusableAgent` (the routing wrapper) threads `activeRelayUrl`
through to both finders.
3. Both consumers pass it: `provisionChannelManagedAgent`'s context (via
`createChannelManagedAgents(..., activeRelayUrl)` and the provision
mutation hook) and the `useReusableAgentDetection` UI guardrail, each
sourcing the relay from `useActiveRelayUrl()`.
The param is REQUIRED so the compiler finds every caller and no future
call can silently bypass the scope. `agentBelongsToRelay` stays permissive
for blank pins / blank active relay, so existing tests and legacy blank-pin
records are unaffected.
Four unit tests cover the new behavior: a B-pinned persona/generic
candidate is excluded when the active relay is A (fresh agent created, no
error), a same-relay candidate is still reused, and the wrapper routes the
foreign-relay persona case to a fresh agent. 42/42 agentReuse tests pass;
tsc --noEmit and biome check clean.
Stacked on wren/agents-every-community-successor (commit #4 on b49523e7f).
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Close the cross-community silent-success trap: a managed agent pinned to
community A's relay could be selected and "attached" to a channel while
Desktop was viewing community B — membership was added against a process
frozen on relay A that never hears B (visible-but-deaf, with
`membershipAdded: true` reported over a deaf agent).
Two guards on the attach path, both using the existing
`agentBelongsToRelay` relay-scope helper:
1. `pickPreferredChannelPresetAgent` now filters reuse candidates to the
active community's relay before selection, so a foreign-relay agent is
never reused; `ensureChannelAgentPresetInChannel` falls through to
creating a new agent on the active relay (correct per-community
behavior for scope (a)).
2. `attachManagedAgentToChannel` gains a REQUIRED `activeRelayUrl` and
asserts the agent belongs to it via the exported
`assertAgentBelongsToActiveRelay` guard, throwing an actionable error
(names the agent, its home relay, and the active relay) rather than
reporting false success. Required — not optional — so the compiler
finds every caller and no future direct caller can silently bypass it.
The relay is sourced inside the mutation/attachment hooks via
`useActiveRelayUrl()` and threaded down, so UI callers are unchanged. All
five call sites (attach/ensure/create mutations, template apply, and the
created-agent attachment hook) surface the thrown message rather than
swallow it.
Direct unit tests exercise the real selection and attach guards (not a
re-derivation of `agentBelongsToRelay`): a B-pinned running agent is not
selected in A (both member and name-match branches) and throws on direct
attach, while same-relay and blank-pin agents remain eligible.
Stacked on wren/agents-every-community-successor.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Drive plan_workspace_activation across the cross-community switch
sequence (apply A, switch to B, switch back to A), asserting the
observable spawn plan at each step. The existing restore tests exercise
begin_relay_activation and record_activates_on_relay as isolated
predicates; these close the coverage gap by proving the composition:
a later apply must not re-spawn A, stop A to serve B, or let a
blank-pinned record float from A to B.
Both core invariants are verified load-bearing by mutation: removing the
already_running exclusion double-spawns a live agent; neutering the
relay filter leaks B's agent into A's plan. Test-only; no production
lines touched.
Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Resolve the review findings on the lazy multi-workspace agent stack
(review of f3baa96) — four hardening fixes, no behavior change on the
happy path:
- Serialize apply_workspace's .repos-dirs.json upsert on
managed_agents_store_lock. A community relay edit fires
rebind_agent_relay (which moves map entries under that lock) nearly
simultaneously with the reinit apply, and both are read-modify-writes
of the same file — unserialized, one side's update could be lost.
- Write .repos-dirs.json via temp file + rename. A crash mid-write left
truncated JSON, which read_repos_dir_map degrades to an empty map —
silently sending spawns back to the shared REPOS fallback, exactly
the cross-workspace hazard the map exists to prevent.
- Pin the Rust/TS relay-URL normalizers together with a shared fixture
(desktop/fixtures/relay-url-normalization.json) consumed by both
relay tests and agentRelayScope.test.mjs, so an edit that lands on
only one side fails the other side's tests instead of shipping a
scoping skew. Both normalizers' doc comments now point at it.
- Sequence updateCommunity's state commit (and the reinit it triggers)
after the rebindAgentRelay IPC settles. Fire-and-forget let
apply_workspace(newUrl) race ahead of the rebind: activation ran
while start-on-launch agents were still pinned to the old URL and
marked the relay activated for the session, silently skipping them
until app relaunch. A rebind failure still commits — pins stay
recoverable by re-editing the community.
The fifth finding (stale "running in other communities" count) was
already resolved by the slow cross-community poll tier added in
0f8fa7e. Split relay.rs's inline test module into relay/tests.rs
(file-size guard, mirroring repos/tests.rs).
Tested: cargo test --manifest-path desktop/src-tauri/Cargo.toml (1435
passed, incl. the new temp-file and fixture-agreement tests), clippy
--all-targets -D warnings, rustfmt, desktop pnpm test (2955 passed,
incl. the new fixture-agreement test), tsc --noEmit, biome check,
pnpm check:file-sizes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Make agents immune to the shared REPOS symlink re-pointing on workspace
switches (Phase 4 of lazy multi-workspace agents, building on the Phase
1-3 relay pinning, lazy activation, and UI scoping):
- Persist repos_dir per relay URL in a new .repos-dirs.json nest dotfile
(keys normalized like agent relay pins), written by every
apply_workspace alongside the single-value .repos-dir that keeps
driving the boot-time symlink resolve. The REPOS symlink itself keeps
following the active workspace as a human/tooling convention.
- At spawn, resolve the agent's own workspace entry (keyed by its
effective relay) to a canonical real path and hand it to the child as
BUZZ_REPOS_DIR, so a mid-task switch that re-points REPOS can no
longer land a workspace-A agent in workspace B's checkouts. A
configured-but-unresolvable entry fails the spawn closed (same
rationale as resolve_repos_at_boot); no entry falls back to the nest
REPOS path, preserving pre-map behavior.
- Adjust the nest AGENTS.md instructions (template v5) that referenced
REPOS/ relatively: agents are told to address checkouts through
$BUZZ_REPOS_DIR whenever it is set.
- Hash the raw per-relay map value into the spawn-config hash so a
repos-dir edit badges needsRestart while a pure workspace switch (and
filesystem state alone) cannot.
- Migrate the map entry in rebind_agent_relay when a community's relay
URL is edited, so rebound agents keep their repos-dir isolation.
- Split the grown repos.rs test module into repos/tests.rs (file-size
guard), mirroring nest/tests.rs and spawn_hash/tests.rs.
Tested: cargo test --manifest-path desktop/src-tauri/Cargo.toml (1433
passed, incl. 7 new repos-map/resolution tests and 2 new spawn-hash
tests), clippy --all-targets -D warnings, rustfmt --check, cargo check
--features mesh-llm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Filter every managed-agent surface to the active community's relay so
concurrently running workspaces stop bleeding into each other's UI
(Phase 3 of lazy multi-workspace agents, building on the Phase 1 relay
pinning and Phase 2 lazy activation):
- Add agentRelayScope helpers: a frontend mirror of the backend relay
normalizer plus agentBelongsToRelay / partitionAgentsByRelay /
hasRunningAgentInCommunity. A blank pin follows the active community
(same defense-in-depth fallback as effective_agent_relay_url), and a
missing provider/community degrades to unscoped rather than blanking
every surface.
- Scope the useManagedAgentActions list — and the bulk stop, start/
stop/delete lookups, and presence derived from it — to the active
relay via a new lenient useActiveRelayUrl hook. Persona delete keeps
counting instances against the unscoped record set since deleting a
persona removes instances in every community.
- Gate the 5s managed-agents liveness poll on a running agent in this
community; agents running in other communities render no process
state here, so they no longer keep the poll alive.
- Hold auto-restart for agents pinned to other communities — their
working/observer signals are read from the active relay only, so a
pure workspace switch must never fire a restart — and re-check the
pin in the pre-fire re-fetch so a rebind cannot restart a foreign
agent.
- Scope mergeKnownAgentPubkeys' managed-agent source to the active
relay; the relay-agent (kind:10100) source stays unfiltered.
- Surface an "N agents running in other communities" line in the
agents header so concurrent background agents stay discoverable.
Tested: desktop pnpm test (2939 passed, incl. new agentRelayScope,
knownAgentPubkeys, and autoRestartPolicy unit tests), tsc --noEmit,
biome check, Playwright smoke project (537 passed, 5 failed; 2 of the
failures passed on rerun and the other 3 — channels intro-scroll,
video review mode, shared-compute empty state — reproduce on baseline
HEAD without this change, so all 5 are pre-existing flakes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Extract agent activation out of the one-shot boot restore into a
relay-filtered step that runs on every workspace apply (Phase 2 of lazy
multi-workspace agents, building on the Phase 1 relay pinning):
- Rename restore_managed_agents_on_launch to activate_workspace_agents,
taking the applied workspace relay and starting only local
start-on-launch agents pinned to that relay (normalized match; a blank
pin matches the visited workspace, mirroring the
effective_agent_relay_url defense-in-depth fallback).
- Call it from every apply_workspace instead of only the first — boot
restore becomes the session's first activation. The one-shot
managed_agent_restore_pending flag now gates only the mesh-llm Share
Compute restore.
- Track activated relays in AppState (activated_agent_relays) so each
workspace activates at most once per app session: bouncing A→B→A never
resurrects agents the user manually stopped in A. Nothing is stopped
on switch — each workspace's agents keep running against their own
relay.
- Gate the boot-time repos-dir/identity-recovery safety checks behind a
new session-wide managed_agent_activation_enabled flag, and run the
stale-process/orphan sweeps only on the session's first activation so
a later activation cannot reap a concurrent activation's
not-yet-tracked children.
Tested: cargo test --manifest-path desktop/src-tauri/Cargo.toml (1424
passed, incl. 7 new activation unit tests), clippy -D warnings, rustfmt,
cargo check --features mesh-llm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Stop agent records from floating to whichever workspace is active — the
relay URL becomes the backend-side ownership key for agents (Phase 1 of
lazy multi-workspace agents):
- Stamp the resolved workspace relay onto records at create, on an
explicit relay edit that clears the field, and on snapshot/team
imports, instead of persisting blank.
- Migrate legacy blank-relay records on the first apply_workspace after
boot. Behavior-preserving: blank resolved to exactly that relay at
boot restore.
- Add a shared relay-URL normalizer (trailing slash, scheme/host case)
and hash the normalized resolved relay in the spawn-config hash so
cosmetic URL differences no longer trip the restart badge.
- Add a rebind_agent_relay command, invoked by updateCommunity when a
community's relay URL is edited, re-pinning records from the old URL
onto the new one so those agents don't orphan.
effective_agent_relay_url keeps its blank-to-workspace fallback as
defense-in-depth for records that escaped stamping. The pure record
mutations live in the new managed_agents::relay_pinning module.
Tested: cargo test --manifest-path desktop/src-tauri/Cargo.toml (1420
passed), clippy -D warnings, rustfmt, desktop pnpm test (2920 passed),
tsc, biome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
## Why
On macOS, GUI apps launched from Finder/Spotlight/Dock inherit a minimal PATH from launchd (`/usr/bin:/bin:/usr/sbin:/sbin`). The `discover_provider_candidates()` function in `backend.rs` only scanned this process PATH when looking for `buzz-backend-*` executables. As a result, backend providers (like `buzz-backend-blox`) were never found, and the "Run on" dropdown in Agent Settings was hidden — even though the binary existed both in the app bundle (`Contents/MacOS/`) and in `~/.local/bin`.
## What
Augment the search directories in `discover_provider_candidates()` with:
1. **exe parent dir** (`Contents/MacOS/` in a `.app` bundle) — so bundled providers are always found regardless of how the desktop was launched
2. **`~/.local/bin`** — the conventional location for user-installed provider binaries (symlinks created by install scripts)
Both directories are only added if not already present in PATH (deduplication). The exe parent dir is prepended (highest priority for bundled providers); `~/.local/bin` is appended.
## Verification
- `cargo check --lib` passes
- All 17 `managed_agents::backend` unit tests pass
- Pre-push hook `desktop-tauri-test` passes (38s)
## References
Root cause confirmed by inspecting the running `buzz-desktop` process environment:
```
$ ps eww <pid> | grep PATH
PATH=/usr/bin:/bin:/usr/sbin:/sbin
```
---
Generated with Fizz