Skip to content

fix(*): let raven's own sub-agents pick a model when their menu came back empty - #620

Merged
0xKT merged 3 commits into
mainfrom
fix/own_acp_row_model_menu
Sep 22, 2026
Merged

0xKT merged 3 commits into
mainfrom
fix/own_acp_row_model_menu

Conversation

@0xKT

@0xKT 0xKT commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Summary

On the agent hub, the Model pill of one of Raven's own acp rows (Raven-PPT on the reporting machine) read "follows the main Raven" and could not be opened. The row's model rule was keyed off its transport alone: an acp row could only pick from the choices its handshake advertised, this row's stored handshake advertised none, and subagents.update would have refused any value for the same reason. Nothing re-measures such a menu, because a capability snapshot is invalidated only by its launch-config fingerprint, which had not moved.

Three commits:

  1. fix(rpc): the model rule is decided per row, in one helper read by both the listing and the write. An acp row still picks from its advertised menu; one of raven's own that advertised none reports model_source: "raven" and is validated and stored through the existing host-catalogue branch, so the page offers the same provider menu the built-in row gets (no page code change) and stores the canonical provider-qualified id the child's own config.set accepts. A third party that advertised none is still refused. The model_source description says the same on the wire; the enum is unchanged and both generated clients are regenerated.
  2. fix(subagents): a ready snapshot of raven's own agent with a measured, empty menu wants another measurement, on connect and in the boot backfill. Scoped to raven's own agents so a third party that really offers none is not relaunched every boot.
  3. fix(*): found by an adversarial pass over the served page. The boot re-measure could give the row its menu while the reader had the host picker open, and the write then refused the pick it had just offered ("has no model ...; it offers 55", with a Retry of the same doomed write). The listing's rule is now a menu, not a gate: one of raven's own accepts an advertised choice or a host-qualified id under either rule, because it runs on this host's providers whatever its handshake advertised; a third-party row keeps the membership-only check, and an own row's refusal names both vocabularies. On the page, a host id kept across a re-measure is drawn with its provider label and clear control instead of as a raw string, and a refused model write refetches the listing so the sheet repaints from the current rule. The empty-menu re-measure is bounded: only when the host holds a usable provider credential, and once per row per process, so a child that genuinely advertises no model is not relaunched at every boot and connect.

Not in this PR (follow-up): the task pane's per-instance model picker keeps the old rule (SubagentManager.set_instance_model validates against the advertised menu only), so a host id picked for an own row on the hub is refused by subagents.instance.set_model. Known limit: a product launched on its own key does not inherit the host providers, so a host model picked for it is refused by the child and only logged (acp_agent._say_model_refused).

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • uv run pytest tests/test_rpc_subagents.py tests/test_rpc_subagents_model.py tests/test_rpc_schema_match.py tests/test_agent_schemas.py tests/test_subagent_probe.py tests/test_subagent_acp.py -q -- 817 passed after the first two commits. New: listing rule (own + empty -> raven, third party + empty -> agent, advertised menu kept verbatim), update accepts/refuses through the host branch for an own row, a third-party row with none is still refused; capabilities_wanted and the boot backfill re-measure own + ready + empty exactly once. The rule tests fail against the previous subagents.py; the probe tests fail against the previous probe.py.

  • The new tests rerun with an empty HOME -- 84 passed.

  • uv run pytest tests/test_rpc_subagents.py tests/test_rpc_subagents_model.py tests/test_subagent_probe.py tests/test_rpc_schema_match.py tests/test_subagent_acp.py -q -- 804 passed after the third commit. New: an own row WITH a menu takes a host id and still takes an advertised value; an id neither vocabulary holds is refused with a message naming both; a third-party row with a menu still refuses a host id; a row told twice it has no menu is not re-measured a third time in the same process; a key-less host re-measures nothing at boot or connect; capabilities_wanted answers false on a key-less home and true once a provider key is written. The update cases fail against the second commit's subagents.py; each bound of the probe change was shown by a targeted mutation to red exactly its own test.

  • npm test --prefix ui-web -- src/features/extAgents -- 95 passed (new: an agent row storing a host id outside its choices shows the split id under its provider label and still clears; a refused model pick repaints from the listing).

  • npm test --prefix ui-web -- 188 files, 2467 passed.

  • npm run gen:check --prefix ui-web, npm run lint:rpc --prefix ui-tui, uv run ruff check raven tests, npm run type-check --prefix ui-web, npm run lint --prefix ui-web -- clean.

  • Served page with an isolated agent home carrying a copy of the real capability snapshot (Raven-PPT measured with zero choices): before, subagents.list reports model_source: "agent" and the pill is disabled; after, it reports "raven", the pill opens the host picker, a pick is stored on the row as a provider-qualified id and shown with a clear control, the clear puts it back; a sibling with a measured menu and a third-party row with none are unchanged. Second pass, Raven-Code's stored menu emptied so the boot re-measure fires: the re-measure runs once and restores 55 choices; a pick made from the host picker opened before the re-measure and sent after it lands (stored openrouter/z-ai/glm-5.3-flash, pill shows the model and provider with the clear control, no Retry); a host id stored before the re-measure reads "anthropic/claude-opus-5 . OpenRouter" after it; refusals carry the combined sentence; the third-party row keeps its disabled pill.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

model_source: "raven" now means "picks from raven's catalogue" rather than "is the built-in row"; the agent hub page is its only reader. A stored host id on an own acp row is an ordinary provider-qualified model id, readable by older code as a value outside the advertised menu (shown and clearable). The boot re-measure costs one child handshake per process for an own agent that keeps advertising no menu, and none on a host without provider credentials. Rollback: revert the three commits in reverse order.

Related Issues

N/A

0xKT and others added 2 commits September 22, 2026 05:04
…t model

The agent hub's model pill on one of Raven's own acp rows (Raven-PPT here)
read "follows the main Raven" and could not be opened: the row's model rule was
keyed off its transport alone, so an acp row could only pick from the choices
its handshake advertised, and this row's stored handshake advertised none.
subagents.update would have refused any value for the same reason. Nothing
re-measures that empty menu, because the snapshot's fingerprint covers only the
launch config, which has not moved.

Decide the rule per row, in one place read by both the listing and the write.
An acp row still picks from its advertised menu; one of raven's own that
advertised none reports model_source "raven" and is validated and stored
through the existing host-catalogue branch, so the page offers the same
provider menu the built-in row gets and the stored id is the canonical
provider-qualified one the child's own config.set accepts. A third party that
advertised none is still taken at its word. The page needs no change: it
already draws the host picker for a "raven" row.

The model_source description on the wire says the same, so both generated
clients are regenerated; the enum is unchanged.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…l menu

A capability snapshot is invalidated only by its launch-config fingerprint, and
a measured-empty model menu is kept as a fact. For one of raven's own acp
agents that is a handshake taken before this raven's providers reached it, and
it survived every boot and connect: the row here had measured zero models two
weeks before its siblings measured fifty-five.

Treat a ready snapshot of raven's own agent with a measured, empty menu as
wanting another measurement, both on connect (capabilities_wanted) and in the
boot backfill. Scoped to raven's own agents on purpose: a third party that
really offers none would otherwise be relaunched at every boot to be told so
again.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@0xKT
0xKT requested a review from LivXue as a code owner September 21, 2026 21:25
@0xKT
0xKT requested a review from gloryfromca September 21, 2026 21:25

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the full diff plus the capability-snapshot, model-dispatch, RPC, and web-picker callers. I also checked the relevant history, backward compatibility of the optional wire field, AGENTS.md and CONTEXT architecture constraints, and whether existing tests were weakened; the new cases preserve the third-party behavior while covering Raven-owned empty menus and remeasurement.

Verification: uv run pytest tests/test_rpc_subagents.py tests/test_rpc_subagents_model.py tests/test_subagent_probe.py -x (193 passed); uv run pytest tests/test_rpc_schema_match.py -x (392 passed); npm test -- --run src/features/extAgents/ExtAgentsPage.test.tsx (36 passed); npm run lint -- --quiet (passed); source-language check and git diff --check passed.

…e, bound the re-measure

The listing's model_source is a menu, not a gate: one of raven's own acp
rows now accepts an advertised choice or a host-qualified id under either
rule, because it runs on this host's providers whatever its handshake
advertised. Before, the boot re-measure could flip the rule from raven to
agent under an open sheet and refuse the pick the reader had just been
offered. A third-party row keeps the membership-only check. The refusal
for an own row names both vocabularies.

On the page, a host id an own row kept across a re-measure is drawn with
its provider label and clear control instead of as a raw string, and a
refused model write refetches the listing so the sheet repaints from the
current rule rather than offering a retry of a write whose vocabulary is
gone.

The empty-menu re-measure of an own row is bounded: only when the host
holds a usable provider credential, and once per row per process, so a
child that genuinely advertises no model is not relaunched at every boot
and connect.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@0xKT

0xKT commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

Third commit pushed, 5cc6b578, after an adversarial pass over the served page with a snapshot whose own row had an emptied menu, so the boot re-measure of the second commit fires.

# Finding Disposition
1 The rule flipped under an open sheet: the boot re-measure gave the row its 55 choices while the page still offered the host picker, so the pick was refused ("has no model ...; it offers 55") and the sheet offered a Retry of the same doomed write. Fixed on the write, not the listing: an own acp row (snapshot.agent_name == "raven") takes an advertised choice OR a host-qualified id under either rule, through the existing _host_pair branch, because it runs on this host's providers whatever it advertised. A third-party row keeps the membership-only check. An own row's refusal names both vocabularies. The page also refetches the listing after a refused model write so the sheet repaints from the current rule.
2 A host id stored before the re-measure rendered as a raw string with no provider chip afterwards (shownModel looked the value up in model_choices only). Fixed for own rows: a value outside the choices that carries a / falls back to the host split, so it reads "anthropic/claude-opus-5 . OpenRouter" with its clear control. Not applied to third-party rows, whose ids are their own.
3 An own row whose menu is genuinely empty was re-measured at every boot and every Connect, forever. Bounded: the empty-menu reason fires only when the host itself holds a usable provider credential (credential_status(..., include_external=True), the predicate the model picker's "configured" flag reads; section_has_credentials reports never-signed-in OAuth vendors as usable and would not bind), and once per row per process. Stale, unmeasured-menu and credential-refusal reasons are untouched.
4 subagents.instance.set_model (task pane, per instance) still validates against the advertised menu only, so a host id picked on the hub is refused there. Not in this PR; already named as the follow-up in the description.

Tests: own row with a menu accepts a host id and still accepts an advertised value; an id neither vocabulary holds is refused with a message naming both; third-party row with a menu still refuses a host id; a row told twice it has no menu is not re-measured a third time in one process; a key-less host re-measures nothing; capabilities_wanted is false on a key-less home and true once a key is written; on the page, an agent row storing a host id outside its choices shows the split id under its provider label and clears; a refused pick repaints from the listing. The update cases fail against the previous subagents.py; each probe bound was shown by a targeted mutation to red exactly its own test; the two vitest cases fail against the previous page files. Python 804 passed on the related set; whole npm test --prefix ui-web 188 files / 2467 passed; gen:check and lint:rpc in sync (the model_source description gained one clause, so openrpc.json and both generated clients moved).

Served page: the boot re-measure still runs exactly once and restores the 55 choices; a pick made from the host picker opened before the re-measure and sent after it lands (stored openrouter/z-ai/glm-5.3-flash, pill shows model and provider with the clear control, no Retry); a host id stored before the re-measure reads with its provider after it; the third-party row keeps its disabled pill. The once-per-process half could not be staged end to end (it needs a child raven that answers ready with no menu, which a credential-less child does not do); it rests on the unit tests and mutations above.

PR description updated (three commits).

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The new revision closes the listing/write race coherently: Raven-owned ACP rows accept either advertised or host-qualified model IDs, the UI preserves the host provider badge after remeasurement, failed writes refresh the held listing, and empty-menu remeasurement is bounded without changing the stale/auth/old-snapshot paths. I found no new inline defect; the only remaining item is the author's explicitly deferred follow-up that subagents.instance.set_model still validates per-instance choices against the advertised menu rather than also accepting host IDs.

I reviewed the delta and rechecked its capability-snapshot and model-dispatch callers, wire/generated-client compatibility, UI state flow, relevant history, AGENTS.md and CONTEXT architecture constraints, and test changes. Verification: the related Python and RPC/schema set passed 589 tests; the two web suites passed 47 tests; web lint, source-language validation, and git diff --check passed.

@0xKT
0xKT merged commit 95cbf61 into main Sep 22, 2026
26 checks passed
@0xKT
0xKT deleted the fix/own_acp_row_model_menu branch September 22, 2026 06:25
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…back empty (#620)

## Summary

On the agent hub, the Model pill of one of Raven's own acp rows
(Raven-PPT on the reporting machine) read "follows the main Raven" and
could not be opened. The row's model rule was keyed off its transport
alone: an acp row could only pick from the choices its handshake
advertised, this row's stored handshake advertised none, and
`subagents.update` would have refused any value for the same reason.
Nothing re-measures such a menu, because a capability snapshot is
invalidated only by its launch-config fingerprint, which had not moved.

Three commits:

1. `fix(rpc)`: the model rule is decided per row, in one helper read by
both the listing and the write. An acp row still picks from its
advertised menu; one of raven's own that advertised none reports
`model_source: "raven"` and is validated and stored through the existing
host-catalogue branch, so the page offers the same provider menu the
built-in row gets (no page code change) and stores the canonical
provider-qualified id the child's own `config.set` accepts. A third
party that advertised none is still refused. The `model_source`
description says the same on the wire; the enum is unchanged and both
generated clients are regenerated.
2. `fix(subagents)`: a ready snapshot of raven's own agent with a
measured, empty menu wants another measurement, on connect and in the
boot backfill. Scoped to raven's own agents so a third party that really
offers none is not relaunched every boot.
3. `fix(*)`: found by an adversarial pass over the served page. The boot
re-measure could give the row its menu while the reader had the host
picker open, and the write then refused the pick it had just offered
("has no model ...; it offers 55", with a Retry of the same doomed
write). The listing's rule is now a menu, not a gate: one of raven's own
accepts an advertised choice or a host-qualified id under either rule,
because it runs on this host's providers whatever its handshake
advertised; a third-party row keeps the membership-only check, and an
own row's refusal names both vocabularies. On the page, a host id kept
across a re-measure is drawn with its provider label and clear control
instead of as a raw string, and a refused model write refetches the
listing so the sheet repaints from the current rule. The empty-menu
re-measure is bounded: only when the host holds a usable provider
credential, and once per row per process, so a child that genuinely
advertises no model is not relaunched at every boot and connect.

Not in this PR (follow-up): the task pane's per-instance model picker
keeps the old rule (`SubagentManager.set_instance_model` validates
against the advertised menu only), so a host id picked for an own row on
the hub is refused by `subagents.instance.set_model`. Known limit: a
product launched on its own key does not inherit the host providers, so
a host model picked for it is refused by the child and only logged
(`acp_agent._say_model_refused`).

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `uv run pytest tests/test_rpc_subagents.py
tests/test_rpc_subagents_model.py tests/test_rpc_schema_match.py
tests/test_agent_schemas.py tests/test_subagent_probe.py
tests/test_subagent_acp.py -q` -- 817 passed after the first two
commits. New: listing rule (own + empty -> `raven`, third party + empty
-> `agent`, advertised menu kept verbatim), update accepts/refuses
through the host branch for an own row, a third-party row with none is
still refused; `capabilities_wanted` and the boot backfill re-measure
own + ready + empty exactly once. The rule tests fail against the
previous `subagents.py`; the probe tests fail against the previous
`probe.py`.
- The new tests rerun with an empty `HOME` -- 84 passed.
- `uv run pytest tests/test_rpc_subagents.py
tests/test_rpc_subagents_model.py tests/test_subagent_probe.py
tests/test_rpc_schema_match.py tests/test_subagent_acp.py -q` -- 804
passed after the third commit. New: an own row WITH a menu takes a host
id and still takes an advertised value; an id neither vocabulary holds
is refused with a message naming both; a third-party row with a menu
still refuses a host id; a row told twice it has no menu is not
re-measured a third time in the same process; a key-less host
re-measures nothing at boot or connect; `capabilities_wanted` answers
false on a key-less home and true once a provider key is written. The
update cases fail against the second commit's `subagents.py`; each bound
of the probe change was shown by a targeted mutation to red exactly its
own test.
- `npm test --prefix ui-web -- src/features/extAgents` -- 95 passed
(new: an `agent` row storing a host id outside its choices shows the
split id under its provider label and still clears; a refused model pick
repaints from the listing).
- `npm test --prefix ui-web` -- 188 files, 2467 passed.
- `npm run gen:check --prefix ui-web`, `npm run lint:rpc --prefix
ui-tui`, `uv run ruff check raven tests`, `npm run type-check --prefix
ui-web`, `npm run lint --prefix ui-web` -- clean.
- Served page with an isolated agent home carrying a copy of the real
capability snapshot (Raven-PPT measured with zero choices): before,
`subagents.list` reports `model_source: "agent"` and the pill is
disabled; after, it reports `"raven"`, the pill opens the host picker, a
pick is stored on the row as a provider-qualified id and shown with a
clear control, the clear puts it back; a sibling with a measured menu
and a third-party row with none are unchanged. Second pass, Raven-Code's
stored menu emptied so the boot re-measure fires: the re-measure runs
once and restores 55 choices; a pick made from the host picker opened
before the re-measure and sent after it lands (stored
`openrouter/z-ai/glm-5.3-flash`, pill shows the model and provider with
the clear control, no Retry); a host id stored before the re-measure
reads "anthropic/claude-opus-5 . OpenRouter" after it; refusals carry
the combined sentence; the third-party row keeps its disabled pill.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

`model_source: "raven"` now means "picks from raven's catalogue" rather
than "is the built-in row"; the agent hub page is its only reader. A
stored host id on an own acp row is an ordinary provider-qualified model
id, readable by older code as a value outside the advertised menu (shown
and clearable). The boot re-measure costs one child handshake per
process for an own agent that keeps advertising no menu, and none on a
host without provider credentials. Rollback: revert the three commits in
reverse order.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
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.

2 participants