fix(desktop): derive remote agent liveness from presence, not backend_agent_id - #5138
fix(desktop): derive remote agent liveness from presence, not backend_agent_id#5138dukedorje wants to merge 1 commit into
Conversation
…_agent_id
A provider-backed agent stayed "online" forever after shutting down, with the
primary action pinned at "Shutdown" — so there was no way to redeploy it.
Remote status is derived from `backend_agent_id`, which v1 never clears (there
is no `undeploy` op). Every liveness-shaped UI decision keyed off that field,
which contradicts invariant I3 ("Presence is the status", docs/remote-agents.md):
the deployment axis is bookkeeping, not liveness. I3 also promises a *bounded*
wrong dot (180s PRESENCE_TTL_SECS); this one survived app restart and reboot.
Adds `isManagedAgentLive(agent, presence)` and routes the liveness-shaped call
sites through it, keeping `isManagedAgentActive` for genuinely control-plane
questions (e.g. the orphan warning in deleteManagedAgentWithRules, which already
read presence and got this right):
- primary action label + `agentActionLive` (profile panel)
- `handleAgentPrimaryAction` — this is what made the deploy arm reachable again
- runtime tab dot, status badge, members-sidebar badge/icon/action
`get_presence` omits offline pubkeys, so an absent entry is indistinguishable
from "not loaded yet". `ManagedAgentPresence` carries both axes and liveness
falls back to the control-plane axis until presence resolves — otherwise every
remote agent would flash "Deploy" on app start, trading one unbounded lie for
another. Local agents are untouched: their status is a real pid probe.
Also invalidates presence / relay-agents / managed-agents after a `!shutdown`
send. It is a message, not a mutation, so nothing invalidated on its own and the
roster lagged up to 5 minutes right when the user was watching for feedback.
`useAgentLifecycleActions` now owns its presence subscription rather than taking
it as a prop, so every caller gets the presence-aware branch; react-query dedupes
it against the panel's existing query.
Fixes block#4730
Signed-off-by: Duke Jones <104690+dukejones@users.noreply.github.com>
|
Correcting my own "Related" section — I said "none found", and on a closer read that isn't quite right for one PR. #3449 ( The two are complementary rather than duplicate:
Only two files overlap ( One design difference worth surfacing for whoever reconciles them, since it's a real tradeoff rather than an oversight. |
Fixes #4730.
The problem
A provider-backed agent stays online with a live Shutdown button forever after it has shut down — and because the primary action never flips back to Deploy, there is no way to bring it back from that surface.
The agent is not at fault: it publishes
kind:10100with"status":"offline"andkind:20001presenceoffline, and the relay applies both. The desktop just never looks.build_managed_agent_summary()derives remote status exclusively fromrecord.backend_agent_id.is_some(), which is write-once — there is noundeployin v1 — sostatusis pinned at"deployed"for the life of the record, and every liveness-shaped UI decision keys off it.This contradicts invariant I3 ("Presence is the status",
docs/remote-agents.md): the deployment axis "is bookkeeping, not liveness". I3 also promises a bounded wrong dot (180sPRESENCE_TTL_SECS); today's wrong dot survives shutdown, app restart, and reboot, because it is not a presence dot at all.The fix
Follows suggestion (b) from the issue — keep the Rust summary purely control-plane and add a frontend liveness helper — so
deployed/not_deployedstay honest as bookkeeping, per I3.isManagedAgentLive(agent, presence)is now the "is this thing alive" predicate.isManagedAgentActivekeeps its name and its meaning, and is reserved for genuinely control-plane questions — e.g. the orphan warning indeleteManagedAgentWithRules, which already readpresenceLookupand got this right.Routed through it:
getManagedAgentPrimaryActionLabel"Shutdown", never"Deploy"handleAgentPrimaryActionisManagedAgentActive→ thestartManagedAgentWithRulesarm was unreachableagentActionLive(profile)status === "running" || "deployed"resolveRuntimeTabStatusdeployed→"running"→ greenAgentStatusBadgestatus === "running", so it could never fire for a remote agentOnce the label is honest, Deploy reaches
startManagedAgentWithRules→deploy, which is already converge-to-at-most-one-live-instance (§Deploy State Machine) and safely re-adopts or recreates. That closes the "cannot bring it back" half without needing the v2undeploy.On the loading window
get_presenceomits offline/unknown pubkeys (noted inpresence.ts), so an absent entry is indistinguishable from "the query hasn't resolved". A barepresence !== undefinedtest would make every remote agent flash Deploy on app start.So
ManagedAgentPresencecarries both axes —{ status, loaded }— and liveness falls back to the control-plane axis until presence resolves. That keeps I3's promise of a bounded wrong signal instead of trading one unbounded lie for another.AgentStatusBadge's existing 15s grace period is reused for the same reason.Local agents are untouched: their status comes from a real pid probe, so the control-plane axis is liveness for them.
isManagedAgentLivereturnsisManagedAgentActiveunchanged forbackend.type !== "provider".Secondary: nothing was refetched after
!shutdownhandleStopinvalidated nothing on the provider branch (unlikehandleStart), and the same held foruseMembersSidebarActions. WithuseRelayAgentsQuerypolling at 5 minutes andusePresenceQuerybackstopping at 60s, fixing the root cause alone would still have left a visible multi-minute lie right when the user is watching for feedback on an action they just took. Both paths now invalidate["presence"],relay-agents, andmanaged-agents. (The live transition itself still arrives over thekind:20001WS subscription; these keep the other two axes from contradicting it.)One structural note
useAgentLifecycleActionsnow owns its presence subscription instead of taking it as a prop, so every caller gets the presence-aware branch rather than each having to remember to pass it. react-query dedupes it against the panel's existing query. This also keepsUserProfilePanel.tsxat its 1000-line ratchet — the two presence props onProfileSummaryViewwere merged into onepresenceobject for the same reason.Testing
pnpm test— 4488 pass, 0 fail, including 8 new cases inmanagedAgentControlActions.test.mjscovering: absent-vs-loaded presence, online/away, the never-deployed case, local-agent fallback, theDeploy/Shutdownlabel flip, and unchanged local labels.npx tsc --noEmit— clean.pnpm check— clean (biome + file-size ratchet + px-text + pubkey-truncation).Manual: deploy a provider-backed agent,
!shutdown, confirm the dot goes grey, the badge de-escalates, the runtime tab reads stopped, and the button becomes Deploy — then press it and confirm the agent comes back under the same identity.Related
Searched open PRs; none found addressing this. #2798 (provider config saves don't redeploy) and #4605 (no management surface on other desktops) both cite the missing
undeploy, but are different defects. #4537 is presence for relay-discovered agents — opposite direction, different files.