Skip to content

Cycling the model selector discards a resumable Claude binding, and the recorded invalidation cause is replaced by registry_miss #1747

Description

@code-yeongyu

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.

  1. 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.
  2. 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)

  1. 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.
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions