Summary
Two independent defects make a Claude session re-send its entire conversation after the user simply cycles the model selector, and then misreport why.
- Selecting a non-Claude model destroys a resumable binding. The
model_select handler treats "the new model is not this provider" as a reason to invalidate, even though the module already has a non-destructive path for exactly this situation.
- The recorded invalidation cause never reaches the user. The cause is written to the ledger and then dropped, so the next turn's warning reports the generic no-binding default instead of what actually happened.
Field reports: two users in September 2026 saw a full-conversation re-send on Claude models (hundreds of KB per turn, one measured 226 messages / ~881 KB) with the cause shown as registry_miss, one of them after nothing more than moving between models and back. Downstream report: oh-my-openagent#8308.
Evidence
packages/coding-agent/src/core/extensions/builtin/claude-sdk-oauth/session-registry-wiring.ts, model_select:
if (event.model?.provider !== CLAUDE_SDK_OAUTH_PROVIDER_ID) {
closeSession(sessionId, "model_selected");
await invalidateBinding(pi, ctx, "model_selected"); // destroys the sidecar
return;
}
if (!(await switchSessionModel(sessionId, event.model.id))) {
keepBindingThenClose(sessionId, "model_selected"); // keeps it
}
keepBindingThenClose (same file) closes the live SDK session but keeps the binding, and thinking_level_select already uses it. So the module has both behaviors and picks the destructive one for a provider excursion.
invalidateBinding (same file) does three things: forgetBinding(sessionId), deleteStoredBinding(sessionFile), and appends a ledger record { schemaVersion: 1, invalidated: true, reason }. Nothing reads that reason back. With no binding left, the next turn cannot reach decideFromBinding in session-continuity.ts, so the user-visible reason is the no-binding default registry_miss (session-continuity.ts, the drift ?? "registry_miss" returns and the !binding.lastAssistantUuid flatten).
Expected (ideal state)
- A provider switch keeps the binding. Leaving this provider closes the live session and keeps the binding, the same as a thinking-level change. Returning later reattaches at the recorded prefix and sends only the messages added while away — which is precisely what the reattach path is built for:
sentPrefixHash / commonPrefixLength reattach with from: binding.sentCount. The existing safety nets stay in charge: identityDrift still flattens on model_changed, and an unconfirmed SDK id is still refused.
- A genuine invalidation reports its own cause. When a binding was invalidated with a recorded reason, the next turn's continuity diagnostic names that reason (for example
model_selected, compaction, tree_changed) instead of registry_miss. registry_miss should mean what it says: no record was ever found.
Acceptance criteria:
- Selecting a non-Claude model and then returning to the same Claude model reattaches; the turn sends the delta, not the full history.
- A binding invalidated for a recorded reason produces a user-visible cause equal to that reason;
registry_miss appears only when nothing was recorded.
- Model identity drift, an unconfirmed SDK session id, a missing transcript and a diverged sent stream all keep flattening as they do today.
Scope
In: the model_select branch, carrying the recorded invalidation reason into the next continuity decision, and regression tests for both.
Out: the eval-summary clamp cause (#1472, addressed by #1498), compaction-driven invalidation, and account/root drift behavior.
Related
Summary
Two independent defects make a Claude session re-send its entire conversation after the user simply cycles the model selector, and then misreport why.
model_selecthandler treats "the new model is not this provider" as a reason to invalidate, even though the module already has a non-destructive path for exactly this situation.Field reports: two users in September 2026 saw a full-conversation re-send on Claude models (hundreds of KB per turn, one measured 226 messages / ~881 KB) with the cause shown as
registry_miss, one of them after nothing more than moving between models and back. Downstream report: oh-my-openagent#8308.Evidence
packages/coding-agent/src/core/extensions/builtin/claude-sdk-oauth/session-registry-wiring.ts,model_select:keepBindingThenClose(same file) closes the live SDK session but keeps the binding, andthinking_level_selectalready uses it. So the module has both behaviors and picks the destructive one for a provider excursion.invalidateBinding(same file) does three things:forgetBinding(sessionId),deleteStoredBinding(sessionFile), and appends a ledger record{ schemaVersion: 1, invalidated: true, reason }. Nothing reads thatreasonback. With no binding left, the next turn cannot reachdecideFromBindinginsession-continuity.ts, so the user-visible reason is the no-binding defaultregistry_miss(session-continuity.ts, thedrift ?? "registry_miss"returns and the!binding.lastAssistantUuidflatten).Expected (ideal state)
sentPrefixHash/commonPrefixLengthreattach withfrom: binding.sentCount. The existing safety nets stay in charge:identityDriftstill flattens onmodel_changed, and an unconfirmed SDK id is still refused.model_selected,compaction,tree_changed) instead ofregistry_miss.registry_missshould mean what it says: no record was ever found.Acceptance criteria:
registry_missappears only when nothing was recorded.Scope
In: the
model_selectbranch, carrying the recorded invalidation reason into the next continuity decision, and regression tests for both.Out: the eval-summary clamp cause (#1472, addressed by #1498), compaction-driven invalidation, and account/root drift behavior.
Related
assistant_rewritten).