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>
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>
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>
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>
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>