Skip to content

fix(tui_gateway): reconcile every build-relevant override with the deferred agent build - #75385

Open
briandevans wants to merge 3 commits into
NousResearch:mainfrom
briandevans:fix/tui-gateway-deferred-build-override-race-63998
Open

fix(tui_gateway): reconcile every build-relevant override with the deferred agent build#75385
briandevans wants to merge 3 commits into
NousResearch:mainfrom
briandevans:fix/tui-gateway-deferred-build-override-race-63998

Conversation

@briandevans

@briandevans briandevans commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Supersedes #63998.

What does this PR do?

_start_agent_build snapshots model_override, create_reasoning_override and
create_service_tier_override into the build kwargs (tui_gateway/server.py),
then calls _make_agent, which blocks for seconds (MCP discovery, prompt/skill
build), and finally installs the result with current["agent"] = agent — with
no re-read of those keys.

Every writer of those keys gates its live-apply on session["agent"] being set.
So a pick made during the build window reaches neither the kwargs (already
snapshotted) nor the agent (not yet installed), and is silently lost for the
life of the session. The user picks a model, sees no error, and the session runs
on the old one until they restart it.

#63998 identified this race but locks reasoning only. The maintainer review
on that PR asked for two things it does not do — this PR implements both:

  1. Synchronize all build-relevant override writers with installation, and
    restore/clear a failed reconciled model override.
  2. Add barrier tests through the actual explicit-provider model and /moa
    routes, plus a failed-switch regression test.

That PR also retains a failed switch_model() target in model_override; this
one restores it, matching the live path. (#63998 additionally predates
f67ca220a refactor(tui): split @method handlers into methods_* modules, so its
diff is written against the pre-refactor layout and it currently reads
CONFLICTING.)

Related Issue

No filed issue — found via the deferred-build path in tui_gateway/server.py.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • tui_gateway/server.py — new _reconcile_deferred_build_overrides(); snapshot
    the three keys immediately before _make_agent; reconcile after
    _wire_callbacks, so the existing session.info emit publishes the reconciled
    identity. Model is reconciled first (the switch rebuilds the client, and
    fast-mode overrides resolve against the new model), then tier, then reasoning
    (a switch_model can reset reasoning_config).
  • tui_gateway/methods_tools.py — correct the stale /moa comment.

Writer coverage (the whole root cause, not one key)

Writer Key Racy? Covered
config.set reasoning (session scope) create_reasoning_override yes ✅ (#63998's only case)
config.set reasoning (global scope) clears create_reasoning_override yes — the removal is a transition too ✅ (added in ab86f7d1e)
config.set model w/ explicit provider model_override yes — this route skips the _wait_agent initialization wait entirely
_apply_model_switch model_override yes
pre-agent /moa model_override yes
config.set fast create_service_tier_override yes — same agent-gated live-apply
methods_session.py resume model_override no — eager resume path; the agent is built synchronously before it, so no deferred build is in flight screened, excluded

The /moa else-branch carried the comment "the override is consumed by the
first build"
— false while a build is already in flight, and the site's own
neighbouring comment already records the sibling failure (#53444: setting session["model_override"] alone never switched the already-built agent). The
comment is now accurate.

Failed-switch handling

A failed reconcile restores the override the build baked in (or clears it)
instead of retaining the failed target. This matches the live path, which raises
before committing model_override (#50163). A retained failure would be
resurrected by the next /new or resume, rebuilding onto a model that does not
work.

The reconcile passes confirm_expensive_model=True: the pick is already
committed to the session, so its cost confirmation was answered by whoever wrote
it, and nothing is listening to this background build — a confirm_required
return would drop the user's model on the floor.

How to Test

  1. uv run --with pytest --with pytest-asyncio python3 -m pytest tests/test_tui_gateway_server.py -q → 501 passed.
  2. The five new tests park a real build inside _make_agent (mirroring the
    existing _slow_make_agent barrier idiom in this file), issue the override
    through its real RPC route while session["agent"] is still None,
    release the build, then assert the installed agent carries the pick.

Red-before proof (two-way)

New test Full revert Revert only the model/tier half (= #63998's reasoning-only shape) With this PR
test_reasoning_pick_during_agent_build_reaches_installed_agent ❌ fail ✅ pass ✅ pass
test_global_reasoning_clear_during_agent_build_reaches_installed_agent ❌ fail ❌ fail ✅ pass
test_explicit_provider_model_switch_during_agent_build_reaches_agent ❌ fail ❌ fail ✅ pass
test_moa_one_shot_during_agent_build_reaches_agent ❌ fail ❌ fail ✅ pass
test_failed_reconcile_does_not_leave_failed_model_override_pinned ❌ fail ❌ fail ✅ pass
test_fast_mode_pick_during_agent_build_reaches_installed_agent ❌ fail ❌ fail ✅ pass

The middle column is the point: reducing this PR to #63998's coverage leaves five
of the six failures live.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15 (Darwin 25.4)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A — pure Python, no platform-specific calls
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Related / Positioning

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tracing the deferred-build window across the explicit-provider, MoA, fast-mode, and reasoning paths. The underlying race is present on current main: _start_agent_build snapshots overrides before _make_agent (tui_gateway/server.py:1960-1966) and installs the agent afterward (tui_gateway/server.py:1972).

Problems

  • Blocking: the new reconciliation only applies reasoning when create_reasoning_override is non-null (tui_gateway/server.py:1944-1947 in this PR). But the existing global reasoning route writes the new global setting and removes that session key (tui_gateway/server.py:10382-10385), applying it live only when an agent already exists (tui_gateway/server.py:10393-10394). A globally scoped change during the parked build therefore leaves the agent on the old snapshotted reasoning value.

Suggested changes

  • Treat an override removal as a reconciled transition and apply the effective global parsed setting after installation.
  • Add the corresponding barrier regression test; the existing live-agent clear test is at tests/test_tui_gateway_server.py:6214-6241.

This is an automated hermes-sweeper review.

Comment thread tui_gateway/server.py Outdated
changed = True

reasoning = session.get("create_reasoning_override")
if reasoning is not None and reasoning != before.get("reasoning"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: this ignores a cleared override. config.set reasoning with scope=global removes create_reasoning_override while the build can still be parked (server.py:10382-10385 on main); since the live apply also requires an agent, this leaves the newly installed agent with the old snapshotted reasoning config. Reconcile the removal to the new effective global value and add a barrier test for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in ab86f7d1e.

You're right that a removal is a transition. The reconcile now handles the cleared case explicitly: when create_reasoning_override was present at snapshot time and is absent after installation, it resolves the effective global setting via parse_reasoning_effort(agent.reasoning_effort) from the freshly written config and applies that to the installed agent.

reasoning = session.get("create_reasoning_override")
before_reasoning = before.get("reasoning")
if reasoning is not None:
    if reasoning != before_reasoning:
        agent.reasoning_config = reasoning
        changed = True
elif before_reasoning is not None:
    cfg = _load_cfg()
    agent_cfg = cfg.get("agent") if isinstance(cfg.get("agent"), dict) else {}
    global_reasoning = parse_reasoning_effort(agent_cfg.get("reasoning_effort"))
    if global_reasoning is not None:
        agent.reasoning_config = global_reasoning
        changed = True

Barrier test added as test_global_reasoning_clear_during_agent_build_reaches_installed_agent, driven through the real scope=global route (idiom borrowed from the live-agent clear test you pointed at). It asserts the pin is gone and the agent is still unbuilt at request time, then that the installed agent carries {"enabled": True, "effort": "high"}. Verified red before the change (assert None == {'effort': 'high', 'enabled': True}) and green after.

I also screened the other two snapshotted keys for the same removal shape: nothing pops create_service_tier_override or model_override during a deferred build. The only clearing site is _reset_session_agent (/new), which builds and installs its own agent synchronously rather than going through _start_agent_build, so it is not this race.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correction to the SHA above: ab86f7d1e was rebased away and resolves to nothing on this branch. The work is in 77438cd88c ("fix(tui_gateway): reconcile a cleared reasoning override with the deferred build"), which is on the current head b4e23402c9 — the commit after it did not touch this branch of the function. _reconcile_deferred_build_overrides now starts at tui_gateway/server.py:1948, with the reasoning handling at 2048–2068.

There a removal is treated as a transition: create_reasoning_override present in the pre-build snapshot and absent after installation falls to elif before_reasoning is not None, which re-reads the config through _load_cfg() — so it sees the agent.reasoning_effort that scope=global just wrote, not the snapshot — resolves it with parse_reasoning_effort, applies it to the installed agent, and sets changed so _persist_live_session_runtime runs. The barrier test is test_global_reasoning_clear_during_agent_build_reaches_installed_agent (tests/test_tui_gateway_server.py:15789): it seeds agent.reasoning_effort: medium in config.yaml, parks a build holding create_reasoning_override={"enabled": True, "effort": "low"}, then drives the real config.set route with key=reasoning, value=high, scope=global. It asserts the session pin is gone while session["agent"] is still None — i.e. the write genuinely landed inside the build window — and then, after releasing the build, that the installed agent carries {"enabled": True, "effort": "high"}. That value is distinct from both the snapshotted low and the seeded medium, so the test fails if the reconcile reads either instead of the new effective global.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correction to the SHA above: the branch has been rebased onto current main, so 77438cd88c and the head b4e23402c9 named above are no longer on it. The cleared-override work is now be46bbfa1bc ("fix(tui_gateway): reconcile a cleared reasoning override with the deferred build"), on the current head e5cce58bbab. Nothing about the fix changed — the rebase's only conflict was an append-at-EOF collision in tests/test_tui_gateway_server.py, and the branch diff against main is unchanged at 623 insertions / 3 deletions across the same three files.

The mechanism is as described: a removal is treated as a transition. create_reasoning_override present in the pre-build snapshot and absent after installation falls to elif before_reasoning is not None, which re-reads the config through _load_cfg() — so it sees the agent.reasoning_effort that scope=global just wrote rather than the snapshot — resolves it with parse_reasoning_effort, applies it to the installed agent, and sets changed so _persist_live_session_runtime runs.

Updated anchors: _reconcile_deferred_build_overrides starts at tui_gateway/server.py:2129, with the reasoning handling at 2228–2249. The barrier test keeps its name — test_global_reasoning_clear_during_agent_build_reaches_installed_agent, now tests/test_tui_gateway_server.py:16547. Full file passes: 530 passed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correction to the SHA above: the branch has been rebased onto current main, so be46bbfa1bc and the head e5cce58bbab named in my previous note are no longer on it. The cleared-override work is now 655eae43769 ("fix(tui_gateway): reconcile a cleared reasoning override with the deferred build"), on the current head a13480d142f. Nothing about the fix changed — the branch diff against main is unchanged at 623 insertions / 3 deletions across the same three files, and the PR reads MERGEABLE again.

Since commit ids do not survive a rebase, the durable anchor for this thread is the test: test_global_reasoning_clear_during_agent_build_reaches_installed_agent in tests/test_tui_gateway_server.py. It parks a build inside _make_agent, issues config.set reasoning with scope=global — which writes agent.reasoning_effort and pops create_reasoning_override — then releases the build and asserts the installed agent carries the new effective global effort rather than the one the build snapshotted. The handling lives in _reconcile_deferred_build_overrides (tui_gateway/server.py, currently at :2129): a key present in the pre-build snapshot and absent after installation falls to the elif before_reasoning is not None branch, which re-reads the effective global value through parse_reasoning_effort and applies it to the agent. A removal is treated as a transition, as you asked.

One structural note on this rebase: main had grown its own block at the end of tests/test_tui_gateway_server.py, so rather than re-append behind it I moved the deferred-build tests up beside test_start_agent_build_passes_session_model_override, which is the cluster they belong with. No test bodies changed.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades needs-decision Awaiting maintainer decision before any implementation labels Jul 31, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #63998 addresses the same deferred-build override-loss race. This patch additionally covers explicit-provider model picks, /moa, fast tier, and failed-reconcile restoration; the two current mechanisms need a maintainer consolidation choice.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 31, 2026
Copilot AI review requested due to automatic review settings August 2, 2026 20:01
@briandevans
briandevans force-pushed the fix/tui-gateway-deferred-build-override-race-63998 branch from ab86f7d to 77438cd Compare August 2, 2026 20:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a race in the TUI gateway’s deferred AIAgent build path where build-relevant session overrides (model, reasoning, fast/service-tier) can be written while _make_agent() is in-flight and then silently never reach the installed agent. The change adds a post-install reconciliation step and expands test coverage to exercise the real RPC writer routes during the build window.

Changes:

  • Add _reconcile_deferred_build_overrides() to adopt model / reasoning / fast-mode overrides written during deferred agent construction.
  • Update the /moa pre-agent branch comment to reflect that in-flight builds may have already snapshotted the previous override.
  • Add a suite of barrier-style tests covering each override writer path (including explicit-provider model switches, /moa, global reasoning clears, and failed-switch rollback behavior).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tui_gateway/server.py Adds deferred-build override reconciliation and snapshots build-time override baselines.
tui_gateway/methods_tools.py Updates /moa comment to match the new reconcile behavior during an in-flight build.
tests/test_tui_gateway_server.py Adds regression tests that mutate overrides mid-build and assert the installed agent reflects them.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tui_gateway/server.py
Comment on lines +2007 to +2020
tier = session.get("create_service_tier_override")
if tier is not None and tier != before.get("tier"):
agent.service_tier = "priority" if tier == "priority" else None
request_overrides = dict(getattr(agent, "request_overrides", {}) or {})
request_overrides.pop("service_tier", None)
request_overrides.pop("speed", None)
if tier == "priority":
from hermes_cli.models import resolve_fast_mode_overrides

fast_overrides = resolve_fast_mode_overrides(getattr(agent, "model", None))
if fast_overrides:
request_overrides.update(fast_overrides)
agent.request_overrides = request_overrides
changed = True

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in b4e23402c.

The divergence is real. config.set fast resolves first and returns 4002 "fast mode is not available for this model" before it writes anything, so create_service_tier_override == "priority" has always carried the implication that resolve_fast_mode_overrides resolved for the session's model — agent.service_tier = "priority" is simply unreachable when it didn't. The reconcile applies the model switch first (deliberately — the switch rebuilds the client and fast overrides must resolve against the new model), which can invalidate that implication after the fact, and then set the tier unconditionally anyway. Result: a session advertising service_tier="priority" in session.info with no service_tier/speed in request_overrides — a state the live path structurally cannot produce.

Returning the live path's error isn't available here; nothing is listening to a background build, and the pin is already committed. So the fix mirrors the resolver's precedence rather than its error: when tier == "priority" and resolve_fast_mode_overrides() comes back None for the post-switch model, the pin is popped from the session and agent.service_tier is left None. The tier assignment is now "priority" if fast_overrides else None, which states the invariant the live path gets for free from its early return. Popping rather than pinning "" matters for rebuilds too — the user asked for fast, not for normal, so the session goes back to having no opinion instead of inheriting a stale unhonorable pin.

Regression test: test_reconcile_drops_the_fast_pin_when_the_switched_model_cannot_honor_it in tests/test_tui_gateway_server.py, beside the other build-window reconcile tests. It picks fast and then an unsupported model inside the same parked build window (fast first — picking the unsupported model first would just 4002 at the door), then asserts the installed agent has no tier, no service_tier/speed overrides, and no surviving pin. Verified to fail against the previous behaviour with assert 'priority' is None.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correcting a stale commit reference in my earlier reply on this thread — that SHA is no longer on the branch after a rebase, so the link points at nothing. Anchoring on the test name instead, which survives future rebases.

This is addressed on the current head. _reconcile_deferred_build_overrides now resolves the fast-mode overrides before deciding the tier: when resolve_fast_mode_overrides(agent.model) returns None for the post-switch model it pops create_service_tier_override from the session, and agent.service_tier is set to "priority" if fast_overrides else None. service_tier/speed are stripped from request_overrides unless the overrides actually resolved, so the session can no longer advertise service_tier="priority" in session.info with no matching request overrides behind it.

Erroring is not available on this path (nothing is listening to a background build), so it mirrors the resolver precedence the live path enforces and drops the pin instead.

Regression test: test_reconcile_drops_the_fast_pin_when_the_switched_model_cannot_honor_it in tests/test_tui_gateway_server.py.

Comment thread tui_gateway/server.py
Comment on lines +1969 to +1989
override = session.get("model_override")
if isinstance(override, dict) and override != before.get("model"):
model = str(override.get("model") or "").strip()
provider = str(override.get("provider") or "").strip()
if model:
raw = f"{model} --provider {provider}" if provider else model
try:
_apply_model_switch(
sid,
session,
raw,
# The pick is already committed to the session, so its
# cost confirmation (if any) was answered by whoever wrote
# it. Re-prompting here would strand the switch: nothing is
# listening to this background build, so a confirm_required
# return would drop the user's model on the floor.
confirm_expensive_model=True,
pin_session_override=True,
# Reconciling a session-scoped pick — never write config.yaml.
persist_override=False,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Accepted — fixed in the same commit, b4e23402c.

The mechanism checks out. The guard is override != before.get("model"), and before is the pre-build snapshot rather than the agent's live state, so it cannot distinguish "picked during the build and never applied" from "already applied live". A writer taking its live branch in the window between current["agent"] = agent and this reconcile does both halves itself — mutates the agent and pins the rich model_override that this function then reads back — which leaves the guard true and re-runs the switch on a model the agent is already on. The duplicate isn't inert: _restart_slash_worker, a second _append_model_switch_marker entry in the transcript, and a redundant session.info emit.

I agree the window is narrow, so I kept the fix to the cheapest thing that closes it and reused the file's own idiom rather than inventing one — _sync_agent_model_with_config already makes exactly this baseline-adoption check for the same reason ("already running the configured model … adopt the baseline without a redundant switch"):

already_live = model == getattr(agent, "model", "") and (
    not provider or provider == getattr(agent, "provider", "")
)
if model and not already_live:

Worth noting why this can't under-apply: the guard only fires when the agent is already on the target, which by construction means a live-apply already ran and therefore already wrote the rich model_override (with base_url/api_key/api_mode). The other shape — a pre-build writer that pins without touching the agent — leaves agent.model on the built-in model, so the guard doesn't fire and the reconcile switches exactly as before.

Regression test: test_reconcile_skips_a_model_switch_a_live_writer_already_applied, which drives the reconcile with the post-live-apply state and asserts switch_model is never resolved, the client is never swapped, none of the three side effects fire, and the live writer's pin survives the skip. Verified to fail without the guard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correcting a stale commit reference in my earlier reply on this thread — that SHA is no longer on the branch after a rebase, so the link points at nothing. Anchoring on the test name instead, which survives future rebases.

This is addressed on the current head. _reconcile_deferred_build_overrides now computes an already_live check before calling _apply_model_switch() — the agent's model matches the requested override, and its provider matches when one is set — and skips the switch in that case. It is the same baseline-adoption check _sync_agent_model_with_config makes.

That closes exactly the race described here: a writer that took its LIVE branch between the agent's installation and this call has already applied the switch and pinned the model_override we read, but before is the pre-build snapshot so the earlier guard could not see it. The redundant call no longer produces a second slash-worker restart, a duplicate switch marker in the transcript, or a redundant session.info.

Regression test: test_reconcile_skips_a_model_switch_a_live_writer_already_applied in tests/test_tui_gateway_server.py.

@briandevans
briandevans force-pushed the fix/tui-gateway-deferred-build-override-race-63998 branch from b4e2340 to e5cce58 Compare August 7, 2026 19:38
…ferred agent build

_start_agent_build snapshots model_override, create_reasoning_override and
create_service_tier_override into the build kwargs, then blocks for seconds
inside _make_agent (MCP discovery, prompt/skill build) before installing the
agent. Every writer of those three keys gates its live-apply on
session["agent"] being set, so a pick made inside that window reaches neither
the kwargs (already read) nor the agent (not yet installed). The user's choice
is silently lost for the life of the session.

The racy writers are config.set reasoning, config.set fast, _apply_model_switch,
the pre-agent /moa branch, and the explicit-provider config.set model path --
which skips the initialization wait entirely, so it is the easiest to hit.

Snapshot the three keys immediately before _make_agent and reconcile them once
the agent is installed and wired, applying whatever changed through the same
helpers the live paths use. Model is reconciled first: the switch rebuilds the
agent's client and fast-mode overrides must resolve against the new model.

A failed reconcile restores the previously built override instead of retaining
the failed target. This matches the live path, which raises before committing
model_override (NousResearch#50163); a retained failure would otherwise be resurrected by
the next /new or resume and rebuild the session onto a model that does not work.
…erred build

config.set reasoning with scope=global writes agent.reasoning_effort and pops
create_reasoning_override, applying the new effort live only when an agent
already exists. Both halves are skipped during the deferred build window, so
the reconcile treated the removal as "nothing changed" and the freshly
installed agent kept the effort the build had snapshotted.

Treat an override removal as a reconciled transition and apply the effective
global parsed setting after installation, with a barrier regression test
through the real global-scope route.
…esolver it mirrors

Two divergences in _reconcile_deferred_build_overrides, both stemming
from it comparing against the pre-build snapshot rather than the agent's
live state.

Fast tier: the tier branch set agent.service_tier unconditionally and
only applied resolve_fast_mode_overrides when it returned something. The
live config.set fast path does the opposite — it resolves first and
returns 4002 ("fast mode is not available for this model") before
pinning anything, so create_service_tier_override == "priority" has
always implied the overrides resolved. The model switch this function
reconciles first can land on a model without fast support, at which
point the session advertised service_tier="priority" in session.info
with no request overrides behind it. The reconcile cannot error (nothing
is listening to a background build), so mirror the resolver's precedence
instead: drop the pin and leave the tier unset. The docstring already
said fast-mode overrides must resolve against the newly selected model;
the code did not finish the thought.

Model switch: the guard is `override != before.get("model")`, and before
is the pre-build snapshot, not live state. A writer taking its live
branch after session["agent"] is set but before this runs applies the
switch itself and pins the same model_override read back here, leaving
the guard true and re-running the switch — a second slash-worker
restart, a second switch marker in the transcript, a redundant
session.info. Adopt the state instead when the agent already runs the
requested model and provider, the same baseline check
_sync_agent_model_with_config makes.
@briandevans
briandevans force-pushed the fix/tui-gateway-deferred-build-override-race-63998 branch from e5cce58 to a13480d Compare August 9, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants