Skip to content

fix(runtime): a conversation that delegated can choose another model, and finish the turn - #34

Merged
benjipeng merged 1 commit into
mainfrom
fix/switch-refusal
Sep 19, 2026
Merged

benjipeng merged 1 commit into
mainfrom
fix/switch-refusal

Conversation

@benjipeng

Copy link
Copy Markdown
Contributor

fix(runtime): a conversation that delegated can choose another model, and finish the turn

Two defects, both found by driving the real binary through a real switch on a copy
of a real session. Neither came from the degradation change that shipped in #33;
that change simply stopped hiding them behind a sentence about replay.

The switch

A journal projection keeps a delegated turn as a canonical reference (CIN-2), and
the request path resolves it to its source before the codec ever sees it. The
model-switch pre-flight took the projection straight from budget_basis and
encoded it unresolved, so EncodeError::UnresolvedCollaboration refused the
switch. Every conversation that had ever delegated was welded to one model — and
not even to a particular one: asking for the model it already had was refused the
same way, because the failure had nothing to do with the destination.

The pre-flight now resolves exactly as the request path does. It is supposed to
encode what the next request will encode; skipping a step the next request takes
is how it answered a question nobody asked.

One refusal became three, because the old sentence named the model as the problem
in two cases where no choice of model was one: the conversation is too long for
the destination, its delegated context could not be read, or its history cannot be
encoded.

The turn

With the switch working, a real reply streamed back in full and was then thrown
away: this gateway attaches provider_metadata to the final chat delta, beside
finish_reason, and PRV-5's unknown-field rule refused it as possibly semantic.
Captured from the wire, every field of it is billing and provenance — cost, cache
hit counts, routing attempts and their status codes. No output. So the answer
arrived complete and the turn died at its last chunk, which is the worst moment
available to lose one, and it would have done that for every Chat-dialect model on
this gateway.

Chat now admits that one name on that one surface. The exemption is earned by
being read and named, never by resembling something that was: the same payload
under another key, or on a Gemini part, still fails — the test pins both.

Evidence

scripts/smoke-delegate.py gains a second configured model and switches to it and
back on a conversation that has delegated, at the point where the root holds the
child's letter. Reverting the resolution step fails it, with both commands left
sitting unconfirmed in the draft.

End to end on an isolated copy of a real session: Luna (Responses, encrypted
reasoning, one delegated child) switches to DeepSeek (Chat). The menu closes, the
notice reports that earlier reasoning is now plain text, the status line shows a
real budget under the smaller window, and a sent turn streams reasoning and
content back from the live provider and completes. That is the one thing no local
test could establish.

… and finish the turn

Two defects, both found by driving the real binary through a real switch on a copy
of a real session. Neither came from the degradation change that shipped in #33;
that change simply stopped hiding them behind a sentence about replay.

## The switch

A journal projection keeps a delegated turn as a canonical reference (CIN-2), and
the request path resolves it to its source before the codec ever sees it. The
model-switch pre-flight took the projection straight from `budget_basis` and
encoded it unresolved, so `EncodeError::UnresolvedCollaboration` refused the
switch. Every conversation that had ever delegated was welded to one model — and
not even to a particular one: asking for the model it already had was refused the
same way, because the failure had nothing to do with the destination.

The pre-flight now resolves exactly as the request path does. It is supposed to
encode what the next request will encode; skipping a step the next request takes
is how it answered a question nobody asked.

One refusal became three, because the old sentence named the model as the problem
in two cases where no choice of model was one: the conversation is too long for
the destination, its delegated context could not be read, or its history cannot be
encoded.

## The turn

With the switch working, a real reply streamed back in full and was then thrown
away: this gateway attaches `provider_metadata` to the final chat delta, beside
`finish_reason`, and PRV-5's unknown-field rule refused it as possibly semantic.
Captured from the wire, every field of it is billing and provenance — cost, cache
hit counts, routing attempts and their status codes. No output. So the answer
arrived complete and the turn died at its last chunk, which is the worst moment
available to lose one, and it would have done that for every Chat-dialect model on
this gateway.

Chat now admits that one name on that one surface. The exemption is earned by
being read and named, never by resembling something that was: the same payload
under another key, or on a Gemini part, still fails — the test pins both.

## Evidence

`scripts/smoke-delegate.py` gains a second configured model and switches to it and
back on a conversation that has delegated, at the point where the root holds the
child's letter. Reverting the resolution step fails it, with both commands left
sitting unconfirmed in the draft.

End to end on an isolated copy of a real session: Luna (Responses, encrypted
reasoning, one delegated child) switches to DeepSeek (Chat). The menu closes, the
notice reports that earlier reasoning is now plain text, the status line shows a
real budget under the smaller window, and a sent turn streams reasoning and
content back from the live provider and completes. That is the one thing no local
test could establish.
@benjipeng
benjipeng merged commit e584715 into main Sep 19, 2026
4 of 6 checks passed
@benjipeng
benjipeng deleted the fix/switch-refusal branch September 19, 2026 03:05
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.

1 participant