Files
multica/server
Multica Eve 07a78d910d fix(daemon): discover agent CLIs installed after startup (MUL-5439) (#6086)
* fix(daemon): discover agent CLIs installed after startup (MUL-5439)

The built-in agent availability set was built exactly once, in LoadConfig, and
every later consumer read that static map. A CLI installed while the daemon was
running was therefore invisible until the daemon process restarted.

On Desktop that is worse than it sounds: the app defaults to autoStop=false and
auto-start only compares CLI versions, so quitting and reopening the app does
not restart the daemon. A user who installed a CLI, verified it in their shell,
and relaunched the app was left with a runtime that never appeared — which is
GH #6077, reported against Antigravity but not specific to it.

- Extract discovery into probeAgentCLIs (pure availability, no version gate).
- Add agentDiscoveryLoop: re-probe every 2 minutes and register providers that
  appeared, reusing applyRegisterResponseInPlace so nothing restarts and no
  in-flight task is interrupted. RecoverOrphans is deliberately not called here
  (MUL-3332): surviving runtimes may be executing tasks.
- Additive only. A provider that stops resolving is kept, because a narrower
  PATH or a version manager mid-upgrade would otherwise tear down a working
  runtime. Removal stays with an explicit restart.
- Hold the set in an atomic.Pointer copy-on-write: cfg.Agents was read unlocked
  from task-execution paths, so a mutable map would be a data race.
- Cache the login-shell PATH fallback process-wide with a 30m TTL keyed on
  PATH/SHELL/HOME, so the 2-minute loop stays a pure LookPath sweep instead of
  forking the user's rc files every round.
- Report skipped_agents on /health with the reason a discovered provider was
  dropped at registration, so "not installed" and "installed but rejected" stop
  looking identical (they were only distinguishable in the daemon log).
- Fix the onboarding template in all four locales: it told users that
  restarting the desktop app was enough, which is exactly the false lead the
  reporter followed.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): retry discovery until registered, and never evict live runtimes

Addresses both P1s from review.

P1-1: a failed first attempt was never retried. refreshAgentAvailability
published a newly discovered provider into the availability set and only acted
on providers "gained" in that same round, so a version probe that timed out or a
register call that failed left the provider permanently unregistered — the user
was back to restarting the daemon, with skipped_agents able to explain the
problem but not fix it.

Registration is now driven by live state instead of by the round that discovered
the provider. providersMissingRuntimes derives, from runtimeIndex, which
discovered providers lack a built-in runtime in each tracked workspace;
convergeRuntimeRegistrations registers only those, for only the workspaces that
need them. Nothing records "already handled", so a version-probe failure, a
register failure, or a partial failure across workspaces all retry on the next
tick, and a provider rejected for being below the minimum version recovers on
its own after an in-place upgrade. A permanently stuck provider is bounded by
exponential backoff (one discovery interval up to 30m) on the expensive half
only; discovery itself stays on its 2-minute cadence, and the steady state
issues no version probes and no register calls at all.

P1-2: the "additive only" refresh could evict existing custom runtimes.
applyRegisterResponseInPlace treats the response as authoritative and drops
prior runtime IDs it does not mention — correct for the convergence paths that
re-derive a whole runtime set, wrong here. appendProfileRuntimes is best-effort,
so one failed GetRuntimeProfiles call yields a builtins-only response, which
would evict the workspace's custom profile runtimes from runtimeIndex and stop
their heartbeats, possibly mid-task.

Added mergeRegisterResponseInPlace: indexes and appends returned runtimes, never
deletes an unmentioned one, and keeps profileSetSig when the fetch failed. Safe
because the server's register endpoint is a pure per-entry upsert that prunes
nothing, so omitted runtimes still exist server-side. ID rotation is still
handled destructively for that one runtime, so a re-issued ID replaces its
predecessor instead of leaving two heartbeat goroutines.

New regression tests: first version probe fails then recovers; first register
call fails then recovers; partial failure across two workspaces retries only the
one that needs it and makes exactly one call; a failed profile fetch leaves the
custom runtime indexed, watched, and its signature intact; below-minimum
provider registers after an upgrade; steady state re-probes nothing; rotated
runtime ID is swapped not duplicated; stuck provider is retried but bounded.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): keep CLI discovery out of custom-profile convergence (MUL-5439)

Third-round review P1: discovery could mask a concurrent profile disable.

The discovery path registered through registerRuntimesForWorkspaceBatch, which
fetches the workspace's custom runtime profiles and returns their content
signature, and the additive merge cached that signature. So if a user disabled a
custom profile at the same moment a newly installed CLI was discovered, the merge
correctly kept the disabled profile's runtime ID (it must not delete unmentioned
runtimes) while recording the POST-disable signature. refreshWorkspaceRuntimeProfiles
short-circuits on a matching signature, so the drift path then saw "already
converged" and the disabled runtime stayed tracked and heartbeating forever.

Discovery is now strictly built-ins only:

- registerBuiltinRuntimesForWorkspace posts a builtins-only register request. It
  never calls appendProfileRuntimes, so discovery cannot observe the profile set
  at all and there is no signature to cache.
- mergeRegisterResponseInPlace becomes mergeBuiltinRegisterResponse: it ignores
  any entry carrying a ProfileID (invariant guard — only the drift path may
  introduce a custom runtime), and neither reads nor writes profileSetSig.
- Custom profile add/edit/disable remains owned exclusively by
  refreshWorkspaceRuntimeProfiles.

Regression tests: a profile disabled concurrently with a new CLI install is
still converged away by the drift path (this test reproduces the reviewer's
exact failure — "disabled custom runtime rt-2 remained tracked" — when the old
signature write is replayed); the merge ignores profile-bearing entries; the
merge leaves profileSetSig untouched. The profile-fetch-failure test now also
asserts the signature is unchanged rather than merely non-empty.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-29 15:08:56 +08:00
..