fix(agents): persist the catalog the availability probe already fetched - #735
Merged
Conversation
The agent picker showed no modes / models / config options for agents that
had never been opened in a conversation, even ones the availability check
reported `online`.
The probe is not a shallow liveness check: it opens a real `session/new` so an
auth-gated agent surfaces `auth_required` instead of silently looking healthy.
That response carries the agent's advertised modes, models and config options —
and the probe discarded all of it:
Ok(_) => ProbeOutcome::Ok,
The columns were then filled only when a live conversation opened a session and
`emit_snapshot_events` broadcast to `CatalogForwarder`. So the data was fetched,
proven, and thrown away, and the picker stayed empty until first use. On this
machine that left 27 of 39 ACP rows blank while probing online.
Keep the response and persist it through the same channel the live session uses:
- `catalog_partial_from_session_new` projects modes / models / config options
into an `AgentHandshake` directly. It lives beside `catalog_partial_from_event`
and a parity test feeds one session state through both routes and asserts the
blobs are byte-identical — they write the same columns, so which route ran
last must not change what the picker reads. `available_commands` has no
counterpart: it arrives as a session/update notification, never in the
`session/new` response.
- `try_connect_custom_agent_with_catalog` returns that partial alongside the
verdict. The existing `try_connect_custom_agent` signature is unchanged, so
the try-connect endpoint and test-on-save are unaffected, and no SDK type
leaks into `aionui-api-types`.
- The availability probe sends it via `registry.catalog_sender()` — the
`registry` parameter was already threaded in and unused.
Additive by construction: `apply_handshake` skips `None` fields, so a probe can
never blank a catalog a real session filled in; an agent advertising nothing
sends nothing; and an auth-gated probe (LIVE: stakpak / poolside answer
`session/new` with `-32000 Authentication required` and no payload) yields
`None`, so unauthenticated agents correctly stay empty until they are logged in
and re-probed.
No schema change: the columns and the write path already exist. Values land at
runtime on the next probe, which is better than a seed — a probe reads the
catalog of the version actually installed on the user's machine.
piorpua
pushed a commit
that referenced
this pull request
Jul 31, 2026
🤖 I have created a release *beep* *boop* --- ## [0.1.56](v0.1.55...v0.1.56) (2026-07-31) ### Features * **project:** hide OS-junk and VCS-internal noise from listings ([#727](#727)) ([6197117](6197117)) ### Bug Fixes * **agents:** persist the catalog the availability probe already fetched ([#735](#735)) ([2438095](2438095)) * **ai-agent:** token usage for the direct-CLI backends (claude / codex) ([#733](#733)) ([93a24f4](93a24f4)) * **conversation:** request plaintext thinking from claude, drop blank thought cards ([#731](#731)) ([9cd4fb9](9cd4fb9)) * **project/monitor:** attribute watched-subdir events to parent so tree reflects dir delete/rename ([#734](#734)) ([8494a1f](8494a1f)) * **session:** settle cancelled workflows and stop per-turn pump state leaking across turns ([#732](#732)) ([b8a4a88](b8a4a88)) * **team:** derive team capability from probed MCP transports, not a stored veto ([#725](#725)) ([d60fc62](d60fc62)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.
Problem
The agent picker shows no modes / models / config options for agents that have never been opened in a conversation — including ones the availability check reports
online.The availability probe is not a shallow liveness check. It deliberately opens a real
session/newso an auth-gated agent surfacesauth_requiredinstead of silently looking healthy. That response carries exactly the catalog the picker needs — and the probe threw it away:The four catalog columns were filled only when a live conversation opened a session and
emit_snapshot_eventsbroadcast toCatalogForwarder. So the data was fetched, proven, and dropped; the picker stayed empty until first use.Measured on one dev machine: 27 of 39 ACP rows had
config_options/available_modes/available_modelsNULL, several of them probingonline. Verified the data is really there — a live probe ofkiloreturns a fullconfigOptionsarray (model / effort / mode with all options) while its storedconfig_optionsis NULL.Fix
Keep the response and persist it through the same channel a live session uses.
catalog_partial_from_session_new(catalog_forwarder.rs) projects modes / models / config options into anAgentHandshakedirectly. It sits besidecatalog_partial_from_event, and a parity test feeds one session state through both routes and asserts the blobs are byte-identical — both write the sameagent_metadatacolumns, so which route ran last must not change what the picker reads.available_commandshas no counterpart here: it arrives as asession/updatenotification, never in thesession/newresponse.try_connect_custom_agent_with_catalogreturns that partial alongside the verdict. The existingtry_connect_custom_agentsignature is unchanged, so the try-connect endpoint and test-on-save are untouched, and no SDK type leaks intoaionui-api-types.registry.catalog_sender(). Theregistryparameter was already threaded intorun_probeand unused (_registry).Safety
Additive by construction:
None— nothing is sentapply_handshakeskipsNonefields, so a probe never blanks itsession/newwith-32000 Authentication requiredand no payload. TheAuthbranch yieldsNone, so unauthenticated agents correctly stay empty until logged in and re-probedNo schema change
The columns and the write path already exist; this only stops discarding data. Values land at runtime on the next probe — better than a seed migration, since a probe reads the catalog of the version actually installed on the user's machine rather than pinning one capture.
Tests
probe_projection_matches_event_projection(new) — probe route vs event route produce identical blobs for the same session state; also pins that an empty/absent catalog produces no writecatalog_partial_covers_session_fields— unchanged, still pins the event route84 targeted tests green (catalog / availability / probe / registry).
cargo clippyandcargo fmtclean. Fullcargo nextestintentionally not run.