SIGN IN SIGN UP

fix(acp): stop an agent's own re-pin from reverting a forked session's model

Carrying the parent's selectors across a fork applied cleanly and was
undone anyway. Claude re-pins its own model on the `session/resume` that
re-establishes the forked session: codeg's `set_config_option` is
answered OK, and ~2ms later a `config_option_update` arrives carrying the
agent's default. Effort changes too, as collateral — a model switch
re-scopes the effort option, so the next fork inherits the new model's
effort. The log reads

    Fork selectors after restore: model=Some("sonnet[1m]")
    agent pushed config_option_update: model=Some("claude-fable-5-1[1m]")

Such a push is indistinguishable from the user picking that model, so it
was applied verbatim. Record what `apply_preferred_session_options` got
the agent to CONFIRM (not what it asked for — a rejected pick must never
be re-asserted against the agent's own verdict), and let the idle loop
re-assert an option a push reverts. Two gates keep it from fighting the
user: at most one re-assert per option, and only until the first prompt,
after which every push is attributable to what the user asked for —
`/model` typed in chat is one. Defending only on the idle path falls out
of the same reasoning.

The replay is ordered model-first through `order_preferred_config_values`,
the same rule the establishment replay follows. Reverting a model re-pin
is exactly the case that drifts both the model and the effort scoped
under it, and a `BTreeMap`'s alphabetical order would replay `effort`
before `model` — letting the model switch re-scope effort straight back,
with both ledger entries already spent.

Establishment owns the ledger outright and clears it on entry, because
`SessionState` outlives a fork transition and two paths never write it
back: Grok's dedicated branch and the empty-preferences early return.
Merging into a parent's entries would defend values this session never
asserted, including ones the agent has already refused.

This is not fork-specific in principle; the fork is just the only path
that resumes a second session on an already-warm agent process, which is
where the push is observed.

Also gate the fork's inherited mode on the PARENT advertising modes.
`emit_session_modes` is a no-op for a modes-less session, so
`current_mode` outlives a transition into one, and a child that does
advertise modes would be handed an ancestor's. The parent's
`ActiveSession` is the only capability answer that cannot go stale, so
the gate is applied where the fork is requested rather than in the
handler. Its `current_mode_id` is deliberately unused: codeg tracks mode
through events the attach-time snapshot never sees.
X
xintaofei committed
d15c2ed4a017bd5cd1b7ae25f1a0e2a15cd6cfe7
Parent: 039ee69