feat(desktop): run managed agents in every community#2083
Closed
tlongwell-block wants to merge 12 commits into
Closed
feat(desktop): run managed agents in every community#2083tlongwell-block wants to merge 12 commits into
tlongwell-block wants to merge 12 commits into
Conversation
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>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(desktop): run managed agents in every community
Summary
Revives and updates Matt Toohey's original work in #1951 on current
mainso managed agents remain usable when Desktop switches communities without restarting.This successor preserves #1951's core design:
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
apply_workspacedeferral semantics.repos_dirisolation and concurrency hardening.hooks.tsafter 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:
tokenfield;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.HEAD == remote branch == ls-remotecaptured in the same shellFocused checks run while assembling the stack:
Test plan