Skip to content

feat(desktop): run managed agents in every community#2083

Closed
tlongwell-block wants to merge 12 commits into
mainfrom
wren/agents-every-community-successor
Closed

feat(desktop): run managed agents in every community#2083
tlongwell-block wants to merge 12 commits into
mainfrom
wren/agents-every-community-successor

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

feat(desktop): run managed agents in every community

Summary

Revives and updates Matt Toohey's original work in #1951 on current main so managed agents remain usable when Desktop switches communities without restarting.

This successor preserves #1951's core design:

  • pin each managed-agent record to its home relay while keeping the same agent key;
  • lazily activate a workspace's agents on each first visit, without stopping agents from other workspaces;
  • relay-scope managed-agent UI, lifecycle, attachment, and reuse paths so foreign-community records cannot leak into the active community;
  • isolate agent repository directories per relay so background agents keep using the correct workspace checkouts.

Kind-0 profile transfer and memory transfer remain intentionally out of scope.

Credit and lineage

This is a conflict-resolved successor to #1951, feat(desktop): run managed agents per workspace with lazy activation, authored by @matt2e. The architecture and initial implementation are Matt's. This branch re-lifts that work onto current main, preserves the per-relay repos-dir phase, reconciles newer workspace/onboarding and agents-UI seams, and adds regression coverage for the resulting production orchestration and cross-community attach/reuse boundaries.

Closes/supersedes #1951 once merged.

What changed from #1951

  • Reconciled launch restoration with current apply_workspace deferral semantics.
  • Preserved the one-shot legacy relay-stamp gate and made its synchronous ordering directly testable.
  • Added orchestration coverage for first-visit activation, repeat visits, and relay scoping.
  • Relay-scoped channel-agent attachment so a foreign-community record fails closed rather than attaching in the active community.
  • Relay-scoped persona and generic reuse candidates so a foreign candidate produces a fresh local agent instead of a creation error.
  • Retained feat(desktop): run managed agents per workspace with lazy activation #1951's per-relay repos_dir isolation and concurrency hardening.
  • Extracted the cohesive background query-invalidation helpers from hooks.ts after the stack moved that file from 985 past the 1,000-line gate; the real extraction restores headroom without adding an exception.

Deliberately not carried from #1951

A branch-to-branch audit confirmed the four mission phases are intact, including byte-identical per-relay repos-dir implementation and tests. Three unrelated changes bundled with #1951 remain out of scope here:

  • removal of the legacy, currently ignored community API token field;
  • the CLI relay-synthesized presence subject fix;
  • the behavior-neutral native window-chrome extraction.

The first two remain worthwhile as focused follow-ups; omitting them keeps this successor limited to cross-community managed agents.

Validation

Merge gate: the live two-community fast-switch pass is currently in flight. This PR must not merge until that pass is complete and blessed.

Final verification is bound to exact SHA: bf34c4eeebda3725078a6d1dd928bdbeaf320893.

  • Full Lefthook pre-push chain
  • Rust package suite
  • Desktop test suite
  • TypeScript / Biome / file-size checks
  • Live two-community fast-switch pass
  • Clean tree and HEAD == remote branch == ls-remote captured in the same shell

Focused checks run while assembling the stack:

  • relay-stamp one-shot/order regression
  • workspace activation planner/orchestration tests
  • channel-agent attach-path tests
  • foreign persona reuse → fresh local agent regression

Test plan

  1. Create or provision an agent in community A.
  2. Fast-switch to community B without restarting Desktop.
  3. Confirm A's agent remains running against A's relay and repository directory.
  4. Confirm B's agents UI and lifecycle actions include only B-scoped records.
  5. Provision/attach an agent in B while a matching foreign persona record exists in A; confirm Desktop creates a fresh B-local agent without error.
  6. Switch A → B → A and confirm activation is once per relay per app session and manually stopped agents are not resurrected.

matt2e and others added 12 commits July 17, 2026 21:06
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>
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>
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>
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>
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>
Reconcile the multi-workspace agent stack with current main without dropping the authenticated-media redirect boundary, agent-managed profile setting, or shared-compute status publication. Keep the file-size exceptions explicit for the combined state and command wiring.

Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Separate the once-per-relay spawn decision from Tauri persistence and OS child creation so orchestration tests can drive A-to-B-to-A activation with a fake running-process view. Keep store locking, boot sweeps, parallel spawning, shutdown serialization, and PID writeback in the production wrapper.

Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Extract the pending relay-stamp gate behind a synchronous helper so the production apply_workspace call and focused test share the same one-shot behavior. The helper completes the stamp before returning; workspace activation remains scheduled only after the enclosing blocking apply finishes.

Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@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>
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>
Complete the cross-community reuse fix on the *creation* path. The attach
guards (commit b49523e) 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 b49523e).

Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Move the cohesive background query-invalidation helpers out of the oversized agents hooks module. Preserve the existing hooks exports and behavior while restoring meaningful headroom under the desktop file-size limit.

Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
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.

2 participants