One lingering consequence of the v5.9.0 resonance-map rollout: the two interactive terminal launchers were still picking --model / --effort from the legacy client_runtime_profiles in config/schedule.json, not from the user's preferences.default_resonance. Users who switched their Resonance in NEXO Desktop Preferences (for example to Alto, which writes brain/calibration.json) kept getting the stale tier cached in the profile — usually max. Headless automation (run_automation_prompt) and NEXO Desktop new-sessions already honoured the preference correctly; only the terminal surfaces were stuck.

What was wrong

Two builders in src/agent_runner.py were the culprits:

Both called resolve_client_runtime_profile(selected, preferences=prefs) and then embedded profile["model"] / profile["reasoning_effort"] into the command. That table lives in config/schedule.json and predates the resonance map: on installs that went through v5.9.0 or earlier, it still holds values like {"claude_code": {"model": "claude-opus-4-7[1m]", "reasoning_effort": "max"}}. run_automation_interactive even computed the right resonance_tier afterward — but only for the telemetry row; the command was already built.

The fix

A small shared helper does the lookup in one place:

def _resolve_interactive_model_and_effort(
    caller: str,
    backend: str,
    *,
    preferences: dict | None = None,
    tier: str | None = None,
) -> tuple[str, str]:
    profile = resolve_client_runtime_profile(backend, preferences=preferences)
    try:
        from resonance_map import resolve_model_and_effort
        user_default = ""
        if isinstance(preferences, dict):
            user_default = str(preferences.get("default_resonance") or "").strip()
        explicit_tier = (tier or "").strip() or None
        model, effort = resolve_model_and_effort(
            caller, backend,
            user_default=user_default or None,
            explicit_tier=explicit_tier,
        )
    except Exception:
        model, effort = "", ""
    # Fall back to client_runtime_profiles only when the resonance
    # contract could not produce a pair (missing resonance_tiers.json,
    # unknown backend key, etc.).
    if not model: model = profile.get("model", "")
    if not effort: effort = profile.get("reasoning_effort", "")
    return model, effort

Both builders now use the helper. build_interactive_client_command grew a caller= kwarg (default "nexo_chat") and a tier= override that run_automation_interactive propagates through. build_followup_terminal_shell_command defaults to caller="nexo_followup_terminal".

Registry change

nexo_followup_terminal joins the user-facing caller map:

USER_FACING_CALLERS: dict[str, str] = {
    "nexo_chat": USE_USER_DEFAULT_SENTINEL,
    "desktop_new_session": USE_USER_DEFAULT_SENTINEL,
    "nexo_update_interactive": USE_USER_DEFAULT_SENTINEL,
    # v6.0.4 — dashboard "Open followup in Terminal" spawns a fresh
    # interactive Claude/Codex session. Treat it like nexo_chat so the
    # user's default_resonance preference flows through.
    "nexo_followup_terminal": USE_USER_DEFAULT_SENTINEL,
}

The sentinel means "ask the user's preference first, fall back to DEFAULT_RESONANCE". Registry integrity stays strict: every caller must still be registered or live under the personal/ prefix.

Before vs. after

SurfacePre-v6.0.4 tier sourcePost-v6.0.4 tier source
nexo chat (terminal CLI)client_runtime_profiles (stale)default_resonance via resonance_map
Dashboard → Open followup in Terminalclient_runtime_profiles (stale)default_resonance via resonance_map
NEXO Desktop (new conversation)calibration.json via lib/claude-runtime.jsunchanged
Headless crons, deep-sleep, followup-runner, etc.resolve_model_and_effort (correct)unchanged

Tests

Eight tests updated across test_agent_runner.py, test_cli_scripts.py, and test_resonance_map.py. The tests that previously asserted the legacy behaviour (--effort max from client_runtime_profiles) now either pin default_resonance explicitly or assert the resonance-mapped value (--effort xhigh for alto, high for codex). The registry-size guard in test_user_facing_registry_is_small gains the fourth caller.

Upgrade

nexo update picks up the new agent_runner.py. No config migration is needed — the resonance_map path is already the authority on every other surface. If your Desktop Preferences showed Alto but nexo chat kept spawning at Máximo, v6.0.4 closes that loop.

Full changelog →