Skip to content

refactor(code): remove legacy resolve_scalar config path - #5755

Merged
Mason Daugherty (mdrxy) merged 57 commits into
mainfrom
mdrxy/code/retire-legacy-config-wrap
Aug 24, 2026
Merged

refactor(code): remove legacy resolve_scalar config path#5755
Mason Daugherty (mdrxy) merged 57 commits into
mainfrom
mdrxy/code/retire-legacy-config-wrap

Conversation

@mdrxy

@mdrxy Mason Daugherty (mdrxy) commented Aug 24, 2026

Copy link
Copy Markdown
Member

ConfigResolver (with its cached per-process snapshot) shipped in #5736, but every production reader still went through the legacy resolve_scalar / resolve_ranked_scalar wrappers, which re-parse config.toml on every call. This PR retires those wrappers and settles the question the wrappers had been hiding: when is configuration read?

Behavior change

Configuration files are now read once into a single process-wide generation. Every reader resolves against that generation, so no two parts of the process can disagree about a setting.

Editing config.toml while the app is running therefore has no effect until the generation advances, which happens in exactly two places: an in-app write (toggling a preference refreshes the generation itself) and /reload. A file that fails to parse leaves the previous generation in force rather than half-applying the new one.

This is the convention every long-running Unix service uses — read at start, change on an explicit signal. Watching files for edits is deliberately not done: a partly applied configuration is a worse failure than a stale one, and per-option exceptions (some live, some cached) would make the effective configuration unpredictable per setting. The policy is now written down in ARCHITECTURE.md and pinned by tests, so it stops being an accident of which call path a reader happens to take.

Users who edit the file by hand and expect immediate effect will need /reload. Worth a release note.

Retiring the wrappers

  1. Plain single-key readers move to the shared resolver. app.py, config.py, cold_cache.py, cost_tracking.py, main.py, plugins/discovery.py, and the two TUI widgets now call get_config_resolver().get(option), replacing per-call file parsing with the resolver's cached snapshot. Diagnostics behavior is preserved by calling _emit_ranked_diagnostics(option, resolved) explicitly at each site. Settings._reload_values resolves through get_config_resolver(refresh_managed=refresh_managed), so /reload and later readers observe the same generation instead of the cache going stale; the env tier still comes from the method's env argument, and the "a failed or blocked reload never drops policy in force" invariant is unchanged.

  2. Explicit-snapshot callers build ad-hoc resolvers. Callers that pass toml_data=/managed_toml_data= deliberately inspect a specific file generation rather than process state, so pointing them at the shared cache would be wrong. The decision rule: if the caller snapshots one generation itself — the config CLI (one read per invocation), update_check (health reported next to the value), the sandbox/theme loaders (a non-default config_path excludes managed policy), and the managed-policy validators (a candidate generation not yet in force) — it builds resolver_from_snapshots(TomlSnapshot(...), TomlSnapshot(...)) and calls .get(option).

  3. The legacy path is deleted. resolve_scalar, resolve_ranked_scalar, and the now-dead _coerce_env helper are gone from config_manifest. The manifest's remaining bespoke readers share a private _resolve_option, which resolves through the shared generation when the caller supplies no tables. The migration-parity equivalence test is deleted with them — its job (proving the two paths agree) is done. Tests that drove the wrappers directly now exercise the same coercion, precedence, and diagnostics assertions through ConfigResolver/resolver_from_snapshots or real TOML files under the test-redirected config path.

  4. The remaining fresh-parse readers, and why. Two keep a concrete parse, and neither is on a live path:

    • resolve_read_project_dotenv runs during dotenv bootstrap, before the project .env is layered into os.environ; seeding the shared generation there would capture an env tier later readers do not see.
    • resolve_startup_mode_with_source inspects the raw user table on its fall-through path, which the resolver does not expose. Its only production caller (dcode config) passes an explicit generation.

Fixes found while reviewing this branch

  • /reload previews read a stale user tier. The preview path shares _reload_values with refresh_managed=False, so its cached user snapshot could predate the [shell].allow_list edit being previewed. The preview then reported no change while the accepted reload applied it — diverging on a security-sensitive auto-approval setting, and reaching the cwd-switch consent prompt as project_settings_change_detected=False. The preview now reads the user file fresh while keeping the managed snapshot the process is enforcing (a preview must not refresh policy in force). This is the atomic-swap half of the policy, not a liveness exception: preview and apply must agree on one generation.
  • A lost diagnostic on UI preference writes. _save_ui_bool_result was the one migrated reader that did not pick up the wrapper's implicit diagnostics call, so a malformed managed [ui] entry was reported nowhere — removing the one signal an administrator has that their policy is inert.
  • /reload reloaded every provider twice. get_config_resolver(refresh_managed=True) already reloads on a cache hit, so the explicit .reload() was redundant: one /reload read the managed file four times and re-ran managed_policy_violations with each.
  • Merge strategies for real manifest options were unasserted. The deleted test_populated_tiers_actually_reach_the_resolver did not depend on the resolve_scalar oracle, and merge_strategy appeared nowhere else in the suite — so flipping threads.columns from DEEP_MERGE to REPLACE, or mcp.disabled_servers from UNION to REPLACE, passed the full suite. Restored and mutation-checked in both directions.
  • refresh_managed was untested. Removing the refresh from /reload kept the suite green, because the existing reload tests drive env rather than a file edit and monkeypatch DEFAULT_CONFIG_PATH (which changes the resolver cache key and rebuilds a fresh resolver, hiding staleness). Now pinned in both directions.
  • Comment rot. Roughly ten comments still described per-call file parsing, including one that promised live edits took effect without a restart — the exact behavior the migration removed.

get_option, get_config_options, _emit_ranked_diagnostics, and the manifest types stay public and unchanged; managed config remains read-only; no new dependencies.

Follow-ups

  • _emit_ranked_diagnostics and _ranked_source now have nine external importers, and the snapshot-construction block appears at six production sites. A small resolve_for_display(option) seam (get + emit + label) would restore the encapsulation the wrapper provided and make the missing-emit bug above structurally impossible.
  • A file corrupted after its generation is taken is not reported. Corruption present when the file is read still logs with exact line and column; only mid-session corruption is silent. A cheap mtime/size check on /reload would close it.
  • Five cast(...) calls exist only because ConfigResolver.get returns ResolvedValue[object]; three sit behind validating predicates that could be TypeGuards. AGENTS.md treats cast as a last resort.

PR #5736 introduced `ConfigResolver` with ranked providers but left every
production reader on the legacy `resolve_scalar(option, toml_data=...)`
wrapper, which re-parses `config.toml` on every call. Migrate the plain
single-key readers to `get_config_resolver().get(option)` so they share the
resolver's cached snapshot instead:

- `app.py`: `_load_float_option` (drops its now-pointless `toml_data`
  parameter), `_load_cursor_style_preference`, and the compact-on-resume
  threshold read.
- `config.py`: `is_langsmith_redaction_enabled`,
  `is_memory_auto_save_enabled`, `is_yolo_switcher_enabled`,
  `is_openai_prompt_cache_key_enabled`,
  `resolve_goal_auto_accept_criteria`, and the managed-tier read in
  `resolve_auto_classifier_model_with_problem`.
- `cold_cache.py`, `cost_tracking.py`, `main.py`, `plugins/discovery.py`,
  `tui/widgets/_links.py`, `tui/widgets/_paste_textarea.py`.

`resolve_scalar` emitted provider diagnostics internally; the resolver
returns them on the `ResolvedValue` instead, so each migrated site now calls
`_emit_ranked_diagnostics(option, resolved)` explicitly to preserve the
existing logging behavior.

`/reload` also re-resolved through the legacy path, which left the shared
resolver cache stale after a reload. `Settings._reload_values` now calls
`get_config_resolver(refresh_managed=True).reload()` before resolving, so a
real reload and later `get_config_resolver()` readers observe the same
generation. A preview (`refresh_managed=False`) still refreshes nothing,
and the env tier is still taken from the `env` argument rather than the
resolver (whose env provider reads `os.environ` directly), so the preview
semantics and the "failed or blocked reload never drops policy in force"
invariant are unchanged.

Tests that patched the now-dead `load_config_toml` seam for these readers
write the redirected `DEFAULT_CONFIG_PATH` file (or a managed file via
`redirect_managed_config`) instead; the monkeypatched
`config_manifest.resolve_scalar` test in `test_app.py` now stubs
`ConfigResolver.get` at the same semantic level.
…pers

Call sites that pass explicit `toml_data=`/`managed_toml_data=` deliberately
inspect a specific file generation rather than process state, so they now
build an ad-hoc resolver with
`resolver_from_snapshots(TomlSnapshot(...), TomlSnapshot(...))` and call
`.get(option)` instead of the legacy wrappers:

- `client/commands/config.py`: `_resolve` and `_option_provenance` report
  the one managed/user generation the command snapshots per invocation.
- `update_check.py`: `_resolve_update_setting` reports source health next
  to the value, so both must come from the same snapshot read.
- `integrations/sandbox_config.py` and `theme.py`: a non-default
  `config_path` deliberately excludes managed policy, which the shared
  process cache cannot express.
- `configuration/service.py`: `resolve_managed_option` inspects a supplied
  managed generation — often a candidate being validated before it takes
  force — against an empty user tier.
- `app.py`: the post-save managed-policy probe resolves the freshly loaded
  managed table against an empty user tier.
- `config.py`: the blank-env veto path in
  `resolve_auto_classifier_model_with_problem` names the user-level value
  it overrides, so it resolves the already-read table with the managed tier
  excluded.

No site turned out to be reading explicit data without reason, so none
moved to the shared cache.
With every production reader on `ConfigResolver`, remove the compatibility
wrappers from `config_manifest`:

- `resolve_scalar` and `resolve_ranked_scalar` are deleted. The manifest's
  remaining bespoke readers (`resolve_read_project_dotenv`,
  `load_bool_display_preference`, the bounded auto-classifier/recursion
  resolvers, and the `startup.mode`/`auto_classifier` source renderers) now
  share a private `_resolve_option` helper that builds an ad-hoc
  `resolver_from_snapshots` resolver from the caller's table generation.
- `_coerce_env` is deleted; its delegate-rejection guard lives in
  `providers.coerce_environment_value`, and the one test that exercised the
  wrapper now asserts that `Invalid` result directly. `_coerce_toml` stays:
  the public `option_accepts_toml` still validates through it.
- The migration-parity test
  (`test_resolver_get_matches_resolve_scalar_for_every_manifest_option`) and
  its fixture guard are deleted — with the wrapper gone, they would test
  nothing.
- Tests that drove the wrappers directly now build resolvers via
  `resolver_from_snapshots` through a test-local helper with the same
  `(value, source)` surface, keeping every coercion, precedence, and
  diagnostics assertion unchanged.
- `test_run_get_section_skips_store_when_no_credentials` no longer depends
  on process-global bootstrap state: it pins `_bootstrap_state.done` so the
  store-read counts do not depend on which tests ran earlier in the worker.
@github-actions github-actions Bot added dcode Related to `deepagents-code` internal User is a member of the `langchain-ai` GitHub organization refactor Code change that neither fixes a bug nor adds a feature size: XL 1000+ LOC labels Aug 24, 2026

@open-swe open-swe Bot 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.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/config.py Outdated
@mdrxy Mason Daugherty (mdrxy) changed the title refactor(code): remove legacy resolve_scalar config path refactor(code): remove legacy resolve_scalar config path Aug 24, 2026
`preview_reload_from_environment` shares `_reload_values` with
`refresh_managed=False`, so the shared resolver is not reloaded — and its
cached user snapshot may predate the `[shell].allow_list` edit being
previewed. The preview then reports no change while the accepted reload
applies the edit, diverging on a security-sensitive auto-approval setting.

Resolve the preview's shell allow-list against a fresh user-file read built
from `resolver_from_snapshots` while keeping the current managed snapshot
(a preview still must not refresh policy the process is enforcing). The
skills read needs no fresh user tier: it only accepts managed-decided
values.
`_save_ui_bool_result` was the one reader migrated off `resolve_scalar` that
did not pick up the wrapper's implicit diagnostics call. The wrapper emitted
provider rejections on every resolution; `resolver_from_snapshots(...).get()`
does not.

The effect: with a malformed managed entry (say `[ui] show_message_timestamps
= "false"`), toggling the preference reports "Preference saved" and the
rejection is logged nowhere -- removing the one signal an administrator has
that their policy is inert. This is also the moment the user is told policy
still wins, so the rejection is what makes that message actionable.

Pair the resolution with `_emit_ranked_diagnostics`, matching every other
converted site.
`get_config_resolver(refresh_managed=True)` already calls `resolver.reload()`
internally on a cache hit and builds a fresh resolver on a miss, so the
explicit `.reload()` in `_reload_values` was a second full reload.

One `/reload` read the managed file four times and `config.toml` twice, and
each `get_managed_snapshot(refresh=True)` re-runs `managed_policy_violations`,
which resolves every enforced key through a freshly built resolver. On a
managed deployment that work repeated on every `/reload` and every accepted
cwd switch.

Pass `refresh_managed` straight through instead. Behavior is unchanged on both
paths: True reloads once, False keeps serving the cached snapshot.
`test_populated_tiers_actually_reach_the_resolver` was deleted alongside the
`resolve_scalar` equivalence test it guarded, but it did not depend on that
oracle -- and it was the only test asserting `MergeStrategy` wiring for real
manifest options. The remaining merge tests use synthetic providers, and
`merge_strategy` appears nowhere else in the suite.

Restore it under a name that states what it protects, with a docstring that no
longer refers to the deleted oracle. Mutation-checked both arms: flipping
`threads.columns` from DEEP_MERGE to REPLACE, or `mcp.disabled_servers` from
UNION to REPLACE, fails the test. Before this commit both mutants passed the
full suite.
Readers moved from a per-call `load_config_toml()` parse to the shared cached
resolver, but several comments still describe the old behavior. One of them
promised a guarantee the migration removed, which is how the next reader
reintroduces the bug it warns about.

- `main.py`: `_resume_term_program` no longer reads `config.toml` from disk.
- `cost_tracking.py`: re-resolving no longer re-reads the file; the surviving
  reason is that the updater thread starts once and is never stopped.
- `config_manifest.py`: `option_accepts_toml` is the public coercion seam, not
  the private `_coerce_toml`; `THEME_DELEGATE` is resolved by `_resolve_theme`
  outside the ranked resolver, not by a provider branch; `_ranked_source` is
  now the only source-label producer, so "compatibility" is vestigial; and
  `_emit_ranked_diagnostics` no longer cites the deleted wrapper it inherited
  its contract from.
- `_resolve_option`: the property that separates it from `get_config_resolver()`
  is that it resolves a freshly parsed generation, not that callers supply the
  table -- two of its callers parse the file themselves. Also record that it
  emits no diagnostics, and retain the `TomlSnapshot` status/table invariant
  that was lost with the wrapper docstring.
- tests: two references to the deleted `_coerce_env`, one of them a test name.
Reverts 77c0a75. Making one option re-read the file per turn treated the
symptom and entrenched the cause: a per-option split between live and cached
reads means two settings edited in the same sitting behave differently, with
nothing to tell a user or a reader which is which.

Long-running Unix services settled this a long time ago -- read config at
start, change it on an explicit signal. `nginx`, `sshd`, and `postfix` all
reload rather than watch, and an nginx reload builds a whole new configuration
and swaps it in one step rather than letting values drift in independently. A
partly applied config is a worse failure than a stale one.

`warnings.trusted_cache_endpoints` goes back to the shared generation, and the
two comments that justified themselves with a per-turn parse cost are corrected
to describe what the code now does.
`_resolve_option` required a parsed table, so its callers each fell back to
their own `load_config_toml()` -- re-parsing the file per access and leaving
half the manifest on a different generation from the readers already migrated
to the shared resolver.

Make `toml_data` optional: with no caller-supplied tables, resolution goes
through the shared process resolver. Three bounded resolvers also eagerly
parsed managed config, which forced the explicit-snapshot path even when the
caller supplied nothing; they now let the parameter flow down. Converted:
the display-preference loader (seven `display.*` options, previously parsed on
every render), the auto-classifier model and timeout resolvers, and
`runtime.recursion_limit`. Their bounded retries -- which re-resolve with one
env var popped -- pass the same `None` through, so a retry stays on the
generation the first pass used.

Two readers deliberately keep a concrete parse:

- `resolve_read_project_dotenv` runs during dotenv bootstrap, before the
  project `.env` is layered into `os.environ`; seeding the shared generation
  there would capture an env tier later readers do not see.
- `resolve_startup_mode_with_source` inspects the raw user table on its
  fall-through path, which the resolver does not expose.

Both are consumed once per process -- the first at startup, the second only by
`dcode config`, which passes an explicit generation -- so neither is on a live
path and cadence is moot for both.

`test_show_diff_line_numbers_reads_app_config` stubbed `load_config_toml`,
which the display loader no longer calls; it now writes the real file under the
test-redirected config path, matching the other converted tests.

@open-swe open-swe Bot 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.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/config_manifest.py
`model_config` edits `config.toml` in place rather than through
`configuration.writer.update_user_config`, so it never reached
`refresh_shared_resolver`. Once the manifest readers moved onto the shared
generation, a saved preference landed on disk and stayed invisible to the
process that wrote it: the UI reported "saved" and the old value kept
deciding behavior until the next restart or `/reload`.

Eleven writers were affected. Four cleared `_default_config_cache`, which
covers only the `[models]` table; seven -- the `warnings`, `mcp`, and
`threads` writers -- invalidated nothing at all.

`_invalidate_config_caches` now drops both caches together at every
committed write, and `refresh_shared_resolver` loses its underscore so a
sibling module can reuse the default-path rule and the best-effort failure
handling rather than restating them.

`models.auto_classifier` decides which model reviews gated actions, so a
stale read there is the security-relevant case:
`test_round_trips_through_the_launch_resolver` was already red on this
branch. `TestWritesReachTheSharedResolver` pins the general property for
saves and clears; all five tests die when the refresh is removed.
The configuration section claimed every reader resolves against one
generation, so no two parts of the process can disagree. A grep disproves
it: `get_config_sources` calls `TomlFileProvider(...).load()` on each
invocation, and roughly a dozen readers -- `model_config`, `theme`,
`sandbox_config`, `agent`, `mcp_disabled` among them -- reach the file
through it. The env tier is live by design as well.

Also corrected: the generation is built on the first read, not at startup
(nothing forces it early), and an in-app write refreshes it only on the
default path.

The exceptions are now named, and the closing paragraph distinguishes what
the policy actually forbids -- an option that is live for one reader and
cached for another -- from a caller deliberately snapshotting one file
generation, which is allowed and reported next to its health.
The comment justified the local parse with two claims, both wrong.

`EnvProvider` is `durable = False` and reads `os.environ` inside `get()`
(`configuration/providers.py:739`), so seeding the shared generation here
could not "capture an env tier that later readers do not see" -- only the
TOML tiers are cached. And the answer is not "consumed once, at startup":
`config._load_dotenv` re-runs on every cwd switch, and
`_preview_dotenv_environ` on every reload preview.

The real reason is the tier shape. This resolution layers `global_dotenv`
between the env and TOML tiers, and the resolver has no provider for it, so
the resolver's own TOML tier is discarded on the fall-through path. Keeping
the parse local additionally keeps dotenv bootstrap from establishing the
process generation as a side effect.

A carve-out is only as good as its stated reason; this one would not have
survived the first reader who checked it.
`resolve_scalar` emitted ranked diagnostics itself, so no caller could
forget them. Retiring it replaced that guarantee with a two-line convention
repeated at 19 readers -- and the convention was already dropped once, in
`_save_ui_bool_result`, where a malformed managed `[ui]` entry stopped being
reported anywhere.

Nothing caught it: deleting every external `_emit_ranked_diagnostics` call
leaves the suite green, because the failure is invisible at runtime. The
value falls back to its default and no diagnostic is written.

This walks the package AST for functions that build a resolver and read an
option, and requires a paired emit. Five readers report health through
another channel (`dcode config` provenance, `dcode doctor`, the managed
validators) and are listed with that reason, as is `_resolve_option`, which
is the resolution primitive rather than a reader.

The check matches a *call*, not the name: these readers import the helper
inside the function body, so a substring check still passes after the call
is deleted -- the first version of this test failed to kill that mutant.
`require_healthy_managed_config` gates the managed file only. A user file
that fails to parse retained the previous snapshot -- the right runtime
behavior -- but said so only through a `logger.warning`, which
`install_log_buffer` routes to the in-memory debug buffer. The report the
user reads printed "Configuration reloaded. No changes detected."

So managed corruption was surfaced as a notice and user corruption was
silent, which is backwards: the user just edited `config.toml` themselves
and is watching for the edit to take.

`_reload_values` already carries a notice slot for the managed block, and
the two cases cannot collide because a managed block returns early. An
unusable user provider now fills the same slot. The preview reports its own
fresh read rather than the shared resolver's status, so what a user accepts
matches what they were shown.

The notice reports a rejection, not a rollback -- the retained value stays
in force, which the tests assert alongside it.
`_resolve` mapped `managed_toml_data=None` to an empty managed tier, so an
omitted argument reported the user or env value as effective while managed
policy actually decided. The retired `resolve_ranked_scalar` loaded the file
in that case, so the fail-open shape arrived with the migration.

All three production callers pass `_load_managed_generation()[0]`, which is
never `None`, so nothing misreports today. Dropping the default makes the
type checker reject the next caller that omits it, rather than leaving
`or {}` to silently decide.

An invocation with no policy installed still passes an empty table -- that
says something different from "not supplied", and only one of the two is now
expressible.

Fifteen test call sites relied on the default and now state the empty tier
outright.
Mason Daugherty (mdrxy) and others added 28 commits August 24, 2026 01:09
The bounded readers' managed fall-through recursed with
`managed_toml_data={}` while leaving `toml_data=None`, so `_resolve_option`
took its ad-hoc path and re-parsed `config.toml` from disk instead of using
the cached process generation. A hand edit after startup could then change
`models.auto_classifier_timeout` or `runtime.recursion_limit` for a
subsequently built agent without `/reload`, while other readers stayed on
the old generation.

Add `_resolve_option_without_managed`, which masks the managed tier while
pairing the caller's user table -- or, when none was supplied, the shared
resolver's cached user snapshot via the new `ConfigResolver.toml_snapshot`
accessor -- so the fall-through observes the same generation as every other
reader. The env tier still reads `os.environ` live.
`_invalidate_config_caches` was inserted directly below `clear_caches`, and
the `invalidate_thread_config_cache()` call that was `clear_caches`'s last
statement became the new function's last statement instead of being added to
it. `_thread_config_cache` was then left with no invalidator other than the
four in-app thread writers, and its read path deliberately has none, so a
hand edit to `[threads]` was never picked up for the life of the process --
contradicting the `/reload` contract this branch writes into ARCHITECTURE.md.

Both functions need the call: `clear_caches` for `/reload` and the auth and
model-selector resets, `_invalidate_config_caches` for a committed write.

The regression left the suite green because the four thread writers
invalidate the cache themselves, so only hand edits were affected. Pinned
with a test that fails without the restored call.
`TestManagedVerdict` exercises `_managed_verdict` in isolation, so nothing
connected the helper to the messages it exists to produce. Deleting the whole
probe from `_save_goal_auto_accept_preference`, or the `rejected` branch from
`_save_ui_bool_result`, left the full suite green.

Both gaps sit on the same claim: a write reports "saved" whether or not policy
overrode it, so the probe is the only thing that tells the user otherwise --
and for a rejected entry it is the only signal the administrator's policy is
inert. The `decided` branch of `_save_ui_bool_result` was already covered;
this adds its `rejected` sibling and all three outcomes for the goal path.

The goal-preference test drives the real coroutine with a recorder in place of
the app, since the method uses nothing but `self.notify`.
The restored merge test names three options, so it pinned those three and
nothing else -- flipping `display.terminal_themes`, `agents.async_subagents`,
`sandboxes.providers`, or `mcp.disabled_project_servers` to `REPLACE` passed
the whole suite. That is the same bug class the named options were restored
for, on the six that were not named.

Covers all nine non-`REPLACE` options against a frozen table of expected
strategies, plus a test that the table still matches the manifest so a new
composing option cannot ship uncovered.

The table has to be written down rather than derived: reading the expected
strategy from `option.merge_strategy` is a tautology, because a downgraded
option drops out of the enumeration and stops being tested. A first draft did
exactly that and caught none of the five mutations; this version catches all
of them.
`TestWritesReachTheSharedResolver` covers five writers by name out of eleven,
so removing `_invalidate_config_caches` from `save_thread_columns` or
`clear_default_agent` left the suite green. The failure is invisible at
runtime -- the file on disk is correct and the UI reports "saved" -- so the
class needs a structural guard, the same shape as the diagnostics guard that
already covers resolver readers.

A writer is a function taking a `config_path` that commits it with an atomic
replace, which matches the eleven writers exactly. `touch_recent_model`
writes the MRU state file the same way but takes a `state_dir`, so it is
correctly not counted.
`get_config_resolver` honors `managed_snapshot` on a cache miss and installs
it on a refresh, but a cache hit without `refresh_managed` returned the cached
resolver and dropped the argument with no signal. The parameter had two
behaviors for one argument.

That is correct for the preview path, which passes the generation it is
already enforcing -- but only by coincidence: `get_healthy_managed_snapshot`
with `refresh=False` happens to return the same object, and nothing in the
signature preserves it. A caller passing a newer snapshot got silence.

Rejects a mismatch instead of discarding it, so the invariant that makes the
preview safe is checked rather than assumed. The `refresh_managed` docstring
also claimed it re-reads all providers, which inverts what the branch does:
`reload_with_replacements` reloads only the providers it is not replacing, so
the managed tier is the one tier deliberately not re-read.
`data` was annotated `Mapping[str, Any]`, but 17 sites across the package
tell a table from a scalar with `isinstance(value, dict)`. Any `Mapping` that
is not a `dict` -- a `mappingproxy`, a `ChainMap`, a test double -- satisfied
the annotation at construction and then made every option under a nested
table fall through to its next source, with no error and no diagnostic.

The docstring already described the `MappingProxyType` case as a deliberate
exclusion, but the exclusion is general, and a comment cannot stop a caller
from passing one. The narrower annotation makes it a type error at the call
site instead.

Propagates through the `toml_data` / `managed_toml_data` / `managed_data`
parameters that feed a snapshot, and through `load_managed_config_toml` and
`_load_managed_generation`, which return one. No behavior change: every
production caller already passes a `tomllib` parse result.
`_managed_verdict` built its managed snapshot as
`TomlSnapshot(load_managed_config_toml(), ProviderStatus(..., OK))`.
`load_managed_config_toml` returns an empty table for a file it could not
read, so asserting `OK` over it threw away the one thing that distinguishes
"no policy installed" from "policy unreadable".

The consequences were all silent: the provider never attached a rejection, the
managed tier resolved as `Unset` rather than reporting a problem, and the user
got a bare "Preference saved." The administrator who wrote the broken file is
not at that keyboard, which is the same reasoning the function's own docstring
uses to argue the probe should exist at all.

Passes the snapshot whole so its health travels with its data, which is also
the invariant `TomlSnapshot.__post_init__` exists to enforce.
The `default` read degraded to `None` with nothing logged anywhere, while its
sibling `providers` read inspected `tier_health` and warned. A managed
`sandboxes.default = 3` was rejected by coercion, fell through, and left the
process with no default sandbox and no explanation -- on the option that
decides which sandbox executes agent code.

The reader is in `_SILENT_RESOLVER_READERS` under the rationale that
`dcode doctor` reports file health, but file health is not value health: the
file parses, and `doctor` confirms it parses. The rejected value was reported
nowhere.
Dropping `{raw!r}` was the price of removing `_raw_toml_auto_classifier`, but
the message is returned to the caller for display on "a surface the user
actually reads" -- so the user was told their `[models].auto_classifier` is
malformed with no indication of what they wrote, on the setting that decides
whether the agent grades its own gated actions.

The rejection is already carried by the user tier's `Invalid.reason`
("Ignoring [models].auto_classifier=42 in config.toml (expected str)"), so the
value comes back without reopening a possibly newer file generation -- which
is the reason the raw read was removed in the first place.
`_warned_non_table_paths` was cleared in `reload_with_replacements` only. The
resolver cache is keyed on `(user path, managed path)`, so installing or
removing managed policy takes the cache-miss branch and builds a whole new
resolver -- a new generation, with the previous generation's dedup set still
live. The module docstring's promise that "each generation gets to report its
own problems" did not hold across that transition, and `reset_config_resolver`
had the same hole.

Also moves the clear behind a public `config_manifest.reset_source_diagnostics`
instead of reaching into the private set from `configuration.resolver`, whose
own docstring says it is "intentionally unaware of the manifest".

The test asserts on the set rather than on log records: rejection reasons
carry the source paths, so they differ across a rebuild and counting warnings
passes either way. It also moves the user path rather than installing policy,
because the managed route runs through `invalidate_config_sources`, which
clears the set itself and masks the path under test. Both wrong versions were
tried first and caught nothing.
`refresh_shared_resolver` caught only `OSError`, but `reload()` can also raise
`ValueError` from the `TomlSnapshot` and `ResolvedValue` invariants or from
repeated provider ranks, and `RuntimeError` from a provider that produced no
snapshot or a resolver with no fallback.

`model_config._save_toml_field` and its siblings call this from the success
branch of their own write and return `bool`, so any of those escaped into UI
code after the bytes were already on disk -- exactly the outcome the
function's docstring says must not happen ("reporting a stale in-process view
as a failed write sends the user to retry a file that is already correct").

Broad by intent, with the reason stated: the write is committed and the
refresh is best-effort, so there is no failure here worth propagating.
`reload_with_replacements` installs its replacements without reloading them
and without checking health, which bypasses the guarantee `TomlFileProvider`
.reload` exists to give: a snapshot the source cannot use never displaces the
last usable one. An unusable snapshot carries an empty table, which resolution
reads as "this source declares nothing" -- so installing one at `MANAGED_RANK`
drops policy and lets lower ranks win.

Latent, not live: the only caller runs `get_healthy_managed_snapshot` first,
which raises. But the guard was in the caller while the contract permitted the
fail-open, and the method is public.

Also adds the first direct coverage for this method -- both the documented
unknown-rank `ValueError` and the new one were previously unexercised.
The comment sweep on this branch was driven by grep for the retired wrapper
names, so what it missed is everything that described the old behavior without
naming it. Five were factually wrong:

- `cold_cache.load_trusted_cache_endpoints` promised its `config=None` path
  "loaded from disk"; it resolves through the shared generation. The module
  docstring 60 lines up was fixed and the function's own contract was not,
  which is the half a caller actually reads.
- `_emit_ranked_diagnostics` said two rejections are deduplicated "once per
  process". There are three, and the scope is per generation -- the opposite
  of what the same file's `_warned_non_table_paths` docstring says. The local
  was named `once_per_process` too; both now say generation.
- `_resolve_option_without_managed` credited "the shared resolver's
  `EnvProvider`" with reading `os.environ` live. It builds a fresh
  `EnvProvider` and takes only the user snapshot from the shared resolver, so
  a reader chasing a stale-env bug was pointed at the wrong object.
- `require_healthy_managed_config` lost its `Raises:` when the body moved into
  `get_healthy_managed_snapshot`, though it still fails startup two ways.
- `load_config_toml` is now the one public per-call parse and said nothing
  about sitting outside the generation.

Also: `ConfigResolver.reload` re-arms the diagnostic dedup, which its
one-line docstring did not mention while `writer.py` calls it on every write;
`toml_snapshot` propagates a `RuntimeError`; `_resolve_option` had prose
wedged between `Args:` and `Returns:` where napoleon folds it into the args;
and the guard test's comment claimed "all five" over a seven-entry set whose
channel list named one command that is not in it.

Records the three blind spots the diagnostics guard cannot close, in the style
the file already uses elsewhere.
Three factual problems in the new paragraphs:

- "`/reload` swaps the result in atomically -- a config that fails to parse
  leaves the previous generation in force rather than half-applying the new
  one" overstated it. Retention is per provider, so a reload where the managed
  file parses and `config.toml` does not advances one tier and retains the
  other -- a half-applied generation, which the sentence promised cannot
  happen. `config.py` states it correctly and scoped; the doc generalized it.
- "Two kinds of reader sit outside that generation" read as a closed list and
  omitted at least four, three of them in-app: the two deliberate fresh-parse
  manifest readers, `update_check`, and the reload preview's fresh user read.
  A maintainer trusting the list would assume any reader not on it resolves
  through the shared generation.
- `dcode doctor` was grouped with callers that "inspect one file generation".
  It fabricates an empty user tier and inspects the managed file only.

Downgrades the unguarded claim that no option "is" live for one reader and
cached for another to what the code actually enforces, which is intent: the
option key is assembled from a string at one call site, so nothing structural
holds it.

Also drops the appeal to "the convention every long-running Unix service
uses" -- unfalsifiable, and the next sentence is the real reason and stands on
its own -- and replaces the ` -- ` dashes, which appeared on only those three
lines of a 79-line file. Shorter sentences throughout, per ASD-STE100.
`_managed_verdict` justified `refresh=False` with "the write just made cannot
have changed managed policy, and re-reading it here would swap the snapshot
every other reader observes." The second half was already false by the time
the probe runs: the write goes through `refresh_shared_resolver` ->
`reload()` -> `_reload_enforceable_managed_snapshot`, which re-reads the
managed file with `refresh=True` and installs it.

So every in-app preference toggle picks up administrator policy installed
since startup. That follows from keeping one generation -- refreshing only the
user tier would leave it ahead of the policy tier, the split state the design
exists to prevent -- but it was documented nowhere, and the one docstring that
touched it argued the opposite.

Says so where a reader looks, and restates the probe's own reason in terms of
what actually holds: it reads the generation in force, and re-reading would
risk a second generation inside one user action.
`test_invalid_cursor_style_falls_back_to_the_default` asserted only
`is_cursor_style` and that the default is a member of the valid set. It never
called `_load_cursor_style_preference`, so the fallback its name claimed to
cover was unexercised, and reverting that fallback to the original unchecked
`cast` left it green.

Renames it to what it tests, and adds one that drives the reader end to end.

Records what the new test found: coercion rejects the string at the provider,
so the reader receives the manifest default and its own `is_cursor_style`
guard never fires for a TOML value. The guard is defense in depth against a
value that bypassed coercion, not the branch that handles a bad config -- so
reverting it still passes, and the docstring says so rather than implying
coverage the test does not give.
`resolve_startup_mode_with_source` asserted `cast("str", resolved.value)` and
argued no non-`str` can reach it. The argument is sound, but it is the odd one
out in a branch that introduced `is_cursor_style` and `is_valid_recursion_limit`
as `TypeIs` predicates for exactly this shape -- and the two fail in opposite
directions. A predicate falls back; a `cast` lets a value that did slip past
coercion propagate typed as `str` into the display path.

The comment also cited a `VALID_STARTUP_MODES` that does not exist anywhere in
the package.

Removes the last `cast` from `config_manifest`, so the import goes with it.
The theme and terminal-mapping writes were the two siblings the managed-policy
reporting commit did not reach. They cannot use `_managed_verdict`: theme
resolution is bespoke, since a `[ui.terminal_themes]` entry decides only when
it matches the running terminal, so a per-option probe would announce policy
as effective when it is not.

What they did share was the health bug. Both read `load_managed_config_toml()`,
which returns an empty table for a file it could not read, so an unreadable
policy file produced the plain "Theme preference saved." -- indistinguishable
from no policy, and no signal to the administrator either way.

Adds `_managed_theme_note`, which checks the snapshot's health before asking
whether policy decides the theme, and collapses the duplicated block at both
call sites.
Each returns a usable value and says nothing, so the failure looks like a
setting the user never wrote:

- `_load_float_option` pre-seeds `value` with the valid default, so the
  invalid-value check below cannot fire for an unregistered key -- an unknown
  option returned the default with nothing logged, while every sibling reader
  reports it.
- `_load_user_themes` returned early on a `[themes]` table that is not a
  table. The reader emits no ranked diagnostics, so a managed or user theme
  block rejected as malformed left every user theme inert and unreported.
- `_resolve_option_without_managed` substitutes an empty user tier when the
  shared resolver has no user provider. That cannot happen without a
  programming error, and quietly resolving against nothing is
  indistinguishable from a user who declared nothing.
… key

Three shapes where the type was leaving a real decision to copy-paste.

`TomlSnapshot` was built inline at nine call sites, and the three that wanted
an empty user tier picked three different healths for it: `OK`, `MISSING`, and
`INDETERMINATE`. `ProviderStatus.usable` gates whether a source takes part in
resolution, so that is a fail-open/fail-closed choice. Adds `from_table`,
`declaring_nothing`, `absent`, and `unknown_origin` so each site states which
one it means.

The resolver cache key was `tuple[object, ...]`, in a docstring that argues
carefully for one field over two and then leaves the key half untyped. A key
built with different arity or field order compares unequal, silently rebuilds
the resolver, and loses the single-generation guarantee with no error. Now a
frozen `_ResolverKey(user_path, managed_path)`.

`resolver_from_snapshots` is keyword-only, with a docstring about how a
transposed positional argument puts user data at `MANAGED_RANK` and nothing
downstream catches it -- and then built both providers with six positional
arguments, including a bare `True` for `durable`. Its own argument applies.

Also drops the `TomlSnapshot(snapshot.data, status)` re-wrap in `doctor.py`.
`status` round-trips from a provider seeded with that same snapshot, so it was
a no-op that read as meaningful, and the one place `__post_init__` could raise
inside the command whose job is to survive a broken config.
Two public functions this branch relies on had no direct test.
`toml_snapshot` was reached only through
`_resolve_option_without_managed`, and `get_healthy_managed_snapshot` only
through `_reload_values` -- it is the startup gate for managed policy, so its
own contract is worth pinning: an enforceable file passes, a known section
declared as a scalar raises `ManagedPolicyError`, and a file that does not
parse raises `ManagedConfigError`.
`reset_config_resolver` cleared the source diagnostics through a lazy
`config_manifest` import, which broke the session-stats test that stubs that
module out of `sys.modules` to simulate an ImportError -- the failure landed in
the conftest teardown, not the test body.

The clear was redundant regardless: dropping the cache entry makes the next
`get_config_resolver` take the cache-miss branch, which re-arms the
diagnostics as part of building the new generation. Mutation-checked that the
rebuild-path clear is still the one being pinned.
@mdrxy
Mason Daugherty (mdrxy) merged commit a564f8e into main Aug 24, 2026
57 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/retire-legacy-config-wrap branch August 24, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` internal User is a member of the `langchain-ai` GitHub organization refactor Code change that neither fixes a bug nor adds a feature size: XL 1000+ LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant