feat(desktop): run managed agents per workspace with lazy activation#1951
Open
matt2e wants to merge 10 commits into
Open
feat(desktop): run managed agents per workspace with lazy activation#1951matt2e wants to merge 10 commits into
matt2e wants to merge 10 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>
applyCommunity sent a `token` arg that the Rust apply_workspace command
stopped declaring when the relay moved to pure Nostr key auth ("Sprout
speaks Nostr. Nothing else.", #475). Tauri silently drops invoke args
the command doesn't declare, so the Community.token collected by the
add/edit dialogs was persisted to localStorage and never applied
anywhere — a secret-shaped field with no consumer. Resolve the arg
mismatch by deleting the plumbing rather than re-accepting the token:
- Stop sending `token` from applyCommunity and align its invoke args
with the Rust signature (relayUrl, nsec, reposDir).
- Remove the API Token field from the Add/Edit Community dialogs, and
drop token handling from useCommunities (update-result matrix,
dup-merge on add, reinit trigger) and the sidebar/switcher prop
types.
- Retire Community.token to a `@deprecated token?: never` marker and
strip the stale secret from persisted entries in loadCommunities
(extending the existing nsec-strip migration), so it cannot leak
from localStorage into future sessions.
- Regression tests: applyCommunity must send exactly the arg keys
apply_workspace declares (guards the silent-drop IPC contract), and
loadCommunities strips token/nsec and persists the cleaned list.
Tested: desktop pnpm test (2941 passed, incl. the 2 new regression
tests), tsc --noEmit, biome check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
buzz users presence misidentified who is online: the relay's /query bridge intercepts presence filters and synthesizes relay-signed kind:20001 events (bridge.rs synthesize_presence) with the subject in the p tag, but cmd_get_presence mapped event.pubkey — the relay keypair — so every row reported the relay signer's pubkey. Extract the row mapping into map_presence_events, which reports the p-tag subject when present and falls back to the event author otherwise. The fallback keeps real user-signed kind:20001 updates correct (build_presence_update emits no p tag — the author is the subject there) and guards against a malformed bare ["p"] tag blanking the pubkey. Regression tests cover all three shapes: synthesized (p-tag subject wins over the relay signer), user-signed (author reported), and malformed p tag (author fallback). Tested: cargo test -p buzz-cli --lib (149 passed, incl. the 3 new tests), cargo clippy -p buzz-cli --all-targets -D warnings, cargo fmt --check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Give each community button in the experimental far-left rail
(workspaceRail) a green status dot when that community has active
managed agents, so concurrently running workspaces stay visible at a
glance (extends the Phase 3 per-community agents UI scoping):
- Add countActiveAgentsByCommunity to agentRelayScope: active (running
or deployed, mirroring isManagedAgentActive) agents per community id,
matched on the normalized relay pin. A blank pin counts toward the
active community only — the per-surface "blank follows the community
being viewed" fallback would light every rail dot for one stray
unstamped record. Communities sharing a relay URL both light up.
- Render the dot at the button's top-right (unread badge/dot keeps
bottom-right), emerald to mirror the agents list's "online"
PresenceDot, with the count woven into the tooltip/aria label via a
new pure communityRailTooltipLabel ("Bravo — 1 agent active").
- Add a slow 60s tier to the useManagedAgentsQuery poll gate while
agents run only in other communities, so a background process dying
clears their rail dots (and the "running in other communities" line)
instead of staying green forever — previously that state could go
permanently stale once the active community had nothing running.
- Seed support: mock managed agents accept a relayUrl pin in the E2E
bridge, and the rail spec covers dot-on-running / no-dot-on-stopped
across two communities.
- Re-register the rail Playwright spec: playwright.config.ts still
matched the old workspace-rail.spec.ts name after the community-rail
rename, so the spec had silently stopped running.
Tested: desktop pnpm test (2954 passed, incl. 13 new unit tests), tsc
--noEmit, biome check, Playwright community-rail spec (8 passed, incl.
the new dot test), plus a visual check of the rendered dot.
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>
Revert 0f8fa7e ("feat(desktop): show an active-agents dot on the community rail"), backing out the rail status dot and its supporting plumbing: - countActiveAgentsByCommunity / hasRunningAgentAnywhere leave agentRelayScope, the dot and communityRailTooltipLabel leave CommunityRail, the slow 60s cross-community tier leaves the useManagedAgentsQuery poll gate, and the E2E bridge drops the managed-agent relayUrl seed plus the rail dot spec test. One deliberate deviation from a pure revert: playwright.config.ts keeps matching community-rail.spec.ts. The reverted commit had fixed a stale workspace-rail.spec.ts testMatch left over from the spec's rename; restoring that line would silently de-register the spec's seven remaining pre-existing tests, which are unrelated to the dot feature. The later hardening commit a497408's fixture-agreement test and normalizer doc comments in agentRelayScope are untouched by the revert. Tested: desktop pnpm test (2942 passed), tsc --noEmit, biome check, Playwright community-rail spec (7 passed) against a fresh build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The lazy-activation comment rewrite tipped lib.rs over the 1000-line guard (desktop-check, pre-push). Move the self-contained window-chrome block — sidebar haptic, title-bar double-click, and the initial-window reveal sequence — into window_chrome.rs, following the archive module's qualified-path pattern in the invoke handler. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7744ece to
b43f3b8
Compare
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.
Video shows me having agents in two workspaces simulataneously trying to write me haikus on a 1 minute schedule.
(Ignore the green dot on the workspaces panel, that was just debug ui to indicate which ones had active agents while testing)
Screen.Recording.2026-07-16.at.11.37.18.pm.mov
Summary
Makes managed agents first-class citizens of multi-workspace setups: each agent is owned by its home relay, activates lazily when its workspace is visited, and stays isolated — in the UI and on disk — from agents running concurrently in other workspaces.
Phase 1 — pin managed agents to a home relay (98390db)
rebind_agent_relaycommand re-pins records when a community's relay URL is edited.Phase 2 — lazy activation on workspace visit (a0dd28d)
activate_workspace_agents, run on everyapply_workspaceand filtered to agents pinned to that relay.Phase 3 — scope the agents UI per community (289ddd3)
agentRelayScopehelpers filter every managed-agent surface (list, bulk actions, liveness poll, auto-restart, known-pubkey merge) to the active community's relay.Phase 4 — isolate agent repos dirs per workspace (695dfe0)
repos_dirper relay URL in a new.repos-dirs.jsonnest dotfile and hand each agent its own resolved path viaBUZZ_REPOS_DIR, so a mid-task workspace switch that re-points the sharedREPOSsymlink can no longer land a workspace-A agent in workspace B's checkouts.Fixes along the way
tokenarg long ago, so the field was a secret-shaped no-op persisted in localStorage; the migration now strips it (0b5525f).buzz users presencereported the relay signer's pubkey for relay-synthesized presence events instead of the p-tag subject (f3baa96)..repos-dirs.jsonupsert, write it via temp-file + rename, pin the Rust/TS relay normalizers together with a shared fixture, and sequence community updates after the rebind IPC settles (a497408).A community-rail active-agents dot was added (0f8fa7e) and deliberately reverted (7744ece); the net diff keeps only its incidental fixes (re-registering the community-rail Playwright spec, normalizer fixture test).
Test plan
cargo test --manifest-path desktop/src-tauri/Cargo.toml— 1435 passed (incl. new relay-pinning, activation, repos-map, spawn-hash, and temp-file tests)desktop pnpm test— 2942 passed (incl. new agentRelayScope, knownAgentPubkeys, autoRestartPolicy, applyCommunity-contract, and communityStorage tests);tsc --noEmit, biome, file-size guards cleancargo test -p buzz-cli --lib— 149 passed (incl. 3 new presence-mapping tests)--all-targets -D warnings+ rustfmt clean; full pre-push suite green on push🤖 Generated with Claude Code