Skip to content

Fix #280: persist /model, /verbose, expanded-view across restarts#312

Open
ericleepi314 wants to merge 1 commit into
feature/issue-279-provider-stream-workerfrom
fix/issue-280-appstate-persistence
Open

Fix #280: persist /model, /verbose, expanded-view across restarts#312
ericleepi314 wants to merge 1 commit into
feature/issue-279-provider-stream-workerfrom
fix/issue-280-appstate-persistence

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

Closes #280

Stacked on #311 (← #310#308#307#306#305#304). Merge in order; GitHub retargets automatically.

Summary

The AppState side-effect handlers for main_loop_model, verbose, and expanded_view were TODO stubs — all three preferences reset on restart (TS persists them via onChangeAppState.ts:123-136).

Write side

  • _persist_user_settings(**values) — single writer chokepoint (global config settings section + cache invalidate, fail-soft). The three pre-existing advisor handlers were deduped onto it (~70 lines of triplicated boilerplate removed, semantics pinned by tests).
  • /model actually persists now (critic-found gap: nothing in production wrote main_loop_model to the reactive store, so the handler was unreachable). ModelCommand._apply and the TUI picker route through persist_model_choice — via the store when wired (fires the router), else a direct settings write (the /advisor pattern). The contradicted module docstring is fixed.

Multi-provider guard (critic-found)

The persisted model is paired with its provider key (AppState.main_loop_provider + settings.model_provider — the advisor_model/advisor_provider precedent). get_persisted_model(provider_name) restores the model only when the launch provider matches; unpaired/legacy or mismatched → provider's own default. Without this, a model persisted on provider A would feed provider B an invalid model id (TS's single-provider world can't hit this).

Read side

  • Entrypoints (TUI / headless / REPL) resolve the startup model as cli option > persisted choice > provider default_model. get_persisted_model reads the config layers directly — the merged schema bakes in DEFAULT_SETTINGS.model, which must not override provider defaults for users who never ran /model.
  • get_default_app_state() seeds verbose and expanded_view (validated) from settings.

Test safety

test_model_command.py got the established config-isolation fixture — without it the new persist call would have written the developer's real ~/.clawcodex/config.json during tests.

Test plan

  • 12 new round-trip tests (change → file → cache-invalidate-as-restart → restore), incl. provider-mismatch, unpaired-legacy-model, store-path, write-failure fail-soft, bogus-persisted-value fallback
  • /model end-to-end: real AnthropicProvider set → restored for anthropic, None for glm
  • 119 tests across model/persistence/settings/advisor/theme/vim files pass
  • Full suite on the stack: 7814 passed, 0 failed, 5 skipped
  • Critic review loop: APPROVE after 1 revision round (the unreachable write path and the provider guard both came out of it)

🤖 Generated with Claude Code

)

The three side-effect handlers were TODO stubs — /model, /verbose, and
the expanded-view toggle reset on every restart.

- _persist_user_settings: single writer chokepoint (global config
  "settings" section + cache invalidate, fail-soft); the three advisor
  handlers deduped onto it (~70 lines of triplicated boilerplate)
- /model now actually persists: ModelCommand._apply and the TUI picker
  route through persist_model_choice (reactive store when wired, else
  direct write) — previously nothing wrote main_loop_model in
  production, so the handler never fired
- multi-provider guard: the model is persisted PAIRED with its provider
  key (new AppState.main_loop_provider + settings.model_provider, the
  advisor_model/advisor_provider precedent); get_persisted_model
  restores it only when the launch provider matches, so a model chosen
  on provider A can never feed provider B an invalid id
- entrypoints (TUI/headless/REPL) resolve startup model as
  cli option > persisted choice > provider default_model;
  get_persisted_model reads the config layers directly because the
  merged schema bakes in DEFAULT_SETTINGS.model
- verbose/expanded_view seed back into get_default_app_state
- test_model_command gets the theme/vim-style config isolation fixture
  (without it the new persist call would write the developer's real
  ~/.clawcodex/config.json during tests)

Closes #280

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant