Skip to content

feat(desktop): run managed agents per workspace with lazy activation#1951

Open
matt2e wants to merge 10 commits into
mainfrom
lazy-multi-workspace-agents
Open

feat(desktop): run managed agents per workspace with lazy activation#1951
matt2e wants to merge 10 commits into
mainfrom
lazy-multi-workspace-agents

Conversation

@matt2e

@matt2e matt2e commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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)

  • Stamp the resolved workspace relay onto agent records at create, relay-edit, and snapshot/team import; migrate legacy blank-relay records on first apply after boot.
  • Shared relay-URL normalizer (Rust + TS) feeds the spawn-config hash so cosmetic URL differences no longer trip the restart badge.
  • New rebind_agent_relay command re-pins records when a community's relay URL is edited.

Phase 2 — lazy activation on workspace visit (a0dd28d)

  • Replace the one-shot boot restore with activate_workspace_agents, run on every apply_workspace and filtered to agents pinned to that relay.
  • Each relay activates at most once per app session (bouncing A→B→A never resurrects manually stopped agents); nothing is stopped on switch, so each workspace's agents keep running against their own relay.

Phase 3 — scope the agents UI per community (289ddd3)

  • New agentRelayScope helpers filter every managed-agent surface (list, bulk actions, liveness poll, auto-restart, known-pubkey merge) to the active community's relay.
  • Auto-restart is held for agents pinned to other communities so a workspace switch can't fire a foreign restart.
  • An "N agents running in other communities" header line keeps background agents discoverable.

Phase 4 — isolate agent repos dirs per workspace (695dfe0)

  • Persist repos_dir per relay URL in a new .repos-dirs.json nest dotfile and hand each agent its own resolved path via BUZZ_REPOS_DIR, so a mid-task workspace switch that re-points the shared REPOS symlink can no longer land a workspace-A agent in workspace B's checkouts.
  • A configured-but-unresolvable entry fails the spawn closed; the map entry migrates on relay rebind.

Fixes along the way

  • fix(desktop): remove the dead community API token plumbing — the Rust command stopped declaring the token arg long ago, so the field was a secret-shaped no-op persisted in localStorage; the migration now strips it (0b5525f).
  • fix(cli): buzz users presence reported the relay signer's pubkey for relay-synthesized presence events instead of the p-tag subject (f3baa96).
  • fix(desktop): concurrency hardening — serialize the .repos-dirs.json upsert, 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 clean
  • cargo test -p buzz-cli --lib — 149 passed (incl. 3 new presence-mapping tests)
  • clippy --all-targets -D warnings + rustfmt clean; full pre-push suite green on push

🤖 Generated with Claude Code

@matt2e matt2e requested a review from a team as a code owner July 16, 2026 13:48
matt2e and others added 10 commits July 16, 2026 23:50
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>
@matt2e matt2e force-pushed the lazy-multi-workspace-agents branch from 7744ece to b43f3b8 Compare July 16, 2026 14:13
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.

1 participant