# Phase 1 Step 3 — Implementation Notes ## Status Done. ## Plan reference - Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` - Section: "Step 3: Migrate global-read methods to AppConfig" ## Summary Added 7 global-read methods to `AppConfig` as inherent methods duplicating the bodies that still exist on `Config`. The planned approach (deprecated forwarders + caller migration) turned out to be the wrong shape for this step because callers hold `Config` instances, not `AppConfig` instances, and giving them an `AppConfig` would require either a sync'd `Arc` field on `Config` (which Step 4's global-write migration would immediately break) or cloning on every call. The clean answer is to duplicate during the bridge window and let callers migrate naturally when Steps 8-9 switch them from `Config` to `RequestContext` + `AppState`. The duplication is 7 methods / ~100 lines and deletes itself when `Config` is removed in Step 10. **Three methods from the plan's Step 3 target list were deferred to Step 7** because they read runtime state, not just serialized state (see "Deviations from plan"). ## What was changed ### Modified files - **`src/config/app_config.rs`** — added 6 new imports (`MarkdownRender`, `RenderOptions`, `IS_STDOUT_TERMINAL`, `decode_bin`, `anyhow`, `env`, `ThemeSet`) and a new `impl AppConfig` block with 7 methods under `#[allow(dead_code)]`: - `vault_password_file(&self) -> PathBuf` - `editor(&self) -> Result` - `sync_models_url(&self) -> String` - `light_theme(&self) -> bool` - `render_options(&self) -> Result` - `print_markdown(&self, text) -> Result<()>` - `rag_template(&self, embeddings, sources, text) -> String` All bodies are copy-pasted verbatim from the originals on `Config`, with the following adjustments for the new module location: - `EDITOR` static → `super::EDITOR` (shared across both impls) - `SYNC_MODELS_URL` const → `super::SYNC_MODELS_URL` - `RAG_TEMPLATE` const → `super::RAG_TEMPLATE` - `LIGHT_THEME` / `DARK_THEME` consts → `super::LIGHT_THEME` / `super::DARK_THEME` - `paths::local_path()` continues to work unchanged (already in the right module from Step 2) ### Unchanged files - **`src/config/mod.rs`** — the original `Config::vault_password_file`, `editor`, `sync_models_url`, `light_theme`, `render_options`, `print_markdown`, `rag_template` method definitions are deliberately left intact. They continue to work for every existing caller. The deletion of these happens in Step 10 when `Config` is removed entirely. - **All external callers** (26 callsites across 6 files) — also unchanged. They continue to call `config.editor()`, `config.render_options()`, etc. on their `Config` instances. ## Key decisions ### 1. Duplicate method bodies instead of `#[deprecated]` forwarders The plan prescribed the same shape as Step 2: add the new version, add a `#[deprecated]` forwarder on the old location, migrate callers, delete forwarders. This worked cleanly in Step 2 because the new location was a free-standing `paths` module — callers could switch from `Config::method()` (associated function) to `paths::method()` (free function) without needing any instance. Step 3 is fundamentally different: `AppConfig::method(&self)` needs an `AppConfig` instance. Callers today hold `Config` instances. Giving them an `AppConfig` means one of: (a) Add an `app_config: Arc` field to `Config` and have the forwarder do `self.app_config.method()`. **Rejected** because Step 4 (global-write) will mutate `Config` fields via `set_wrap`, `update`, etc. — keeping the `Arc` in sync would require either rebuilding it on every write (slow and racy) or tracking dirty state (premature complexity). (b) Have the forwarder do `self.to_app_config().method()`. **Rejected** because `to_app_config` clones all 40 serialized fields on every call — a >100x slowdown for simple accessors like `light_theme()`. (c) Duplicate the method bodies on both `Config` and `AppConfig`, let each caller use whichever instance it has, delete the `Config` versions when `Config` itself is deleted in Step 10. **Chosen.** Option (c) has a small ongoing cost (~100 lines of duplicated logic) but is strictly additive, has zero runtime overhead, and automatically cleans up in Step 10. It also matches how Rust's type system prefers to handle this — parallel impls are cheaper than synchronized state. ### 2. Caller migration is deferred to Steps 8-9 With duplication in place, the migration from `Config` to `AppConfig` happens organically later: - When Step 8 rewrites `main.rs` to construct an `AppState` and `RequestContext` instead of a `GlobalConfig`, the `main.rs` callers of `config.editor()` naturally become `ctx.app.config.editor()` — calling into `AppConfig`'s version. - Same for every other callsite that gets migrated in Step 8+. - By Step 10, the old `Config::editor()` etc. have zero callers and get deleted along with the rest of `Config`. This means Step 3 is "additive only, no caller touches" — deliberately smaller in scope than Step 2. That's the correct call given the instance-type constraint. ### 3. `EDITOR` static is shared between `Config::editor` and `AppConfig::editor` `editor()` caches the resolved editor path in a module-level `static EDITOR: OnceLock>` in `src/config/mod.rs`. Both `Config::editor(&self)` and `AppConfig::editor(&self)` read and initialize the same static via `super::EDITOR`. This matches the current behavior: whichever caller resolves first wins the `OnceLock::get_or_init` race and subsequent callers see the cached value. There's a latent bug here (if `Config.editor` and `AppConfig.editor` fields ever differ, the first caller wins regardless) but it's pre-existing and preserved during the bridge window. Step 10 resolves it by deleting `Config` entirely. ### 4. Three methods deferred to Step 7 See "Deviations from plan." ## Deviations from plan ### `select_functions`, `select_enabled_functions`, `select_enabled_mcp_servers` belong in Step 7 The plan's Step 3 table lists all three. Reading their bodies (in `src/config/mod.rs` at lines 1816, 1828, 1923), they all touch `self.functions` and `self.agent` — both of which are `#[serde(skip)]` runtime fields that do NOT exist on `AppConfig` and will never exist there (they're per-request state living on `RequestContext` and `AgentRuntime`). These are "mixed" methods in the plan's Step 7 taxonomy — they conditionally read serialized config + runtime state depending on whether an agent is active. Moving them to `AppConfig` now would require `AppConfig` to hold `functions` and `agent` fields, which directly contradicts the Step 0 / Step 6.5 design. **Action taken:** left all three on `Config` unchanged. They get migrated in Step 7 with the new signature `(app: &AppConfig, ctx: &RequestContext, role: &Role) -> Vec<...>` as described in the plan. **Action required from Step 7:** pick up these three methods. The call graph is: - `Config::select_functions` is called from `src/config/input.rs:243` (one external caller) - `Config::select_functions` internally calls the two private helpers - The private helpers read both `self.functions` (runtime, per-request) and `self.agent` (runtime, per-request) — so they fundamentally need `RequestContext` not `AppConfig` ### Step 3 count: 7 methods, not 10 The plan's table listed 10 target methods. After excluding the three `select_*` methods, Step 3 migrated 7. This is documented here rather than silently completing a smaller Step 3 so Step 7's scope is clear. ## Verification ### Compilation - `cargo check` — clean, **zero warnings, zero errors** - `cargo clippy` — clean ### Tests - `cargo test` — **63 passed, 0 failed** (same as Steps 1–2) Step 3 added no new tests because it's duplication — there's nothing new to verify. The existing test suite confirms: (a) the original `Config` methods still work (they weren't touched) (b) `AppConfig` still compiles and its `Default` impl is intact (needed for Step 1's bridge test which uses `build_populated_config()` → `to_app_config()`) Running `cargo test bridge` specifically: ``` test config::bridge::tests::round_trip_default_config ... ok test config::bridge::tests::to_app_config_copies_every_serialized_field ... ok test config::bridge::tests::to_request_context_copies_every_runtime_field ... ok test config::bridge::tests::round_trip_preserves_all_non_lossy_fields ... ok test result: ok. 4 passed ``` The bridge's round-trip test still works, which proves the new methods on `AppConfig` don't interfere with the struct layout or deserialization. They're purely additive impl-level methods. ### Manual smoke test Not applicable — no runtime behavior changed. CLI and REPL still call `Config::editor()` etc. as before. ## Handoff to next step ### What Step 4 can rely on Step 4 (migrate global-write methods) can rely on: - `AppConfig` now has 7 inherent read methods that mirror the corresponding `Config` methods exactly - `#[allow(dead_code)]` on the `impl AppConfig` block in `app_config.rs` — safe to leave as-is, it'll go away when the first caller is migrated in Step 8+ - `Config` is unchanged for all 7 methods and continues to work for every current caller - The bridge (`Config::to_app_config`, `to_request_context`, `from_parts`) from Step 1 still works - The `paths` module from Step 2 is unchanged - `Config::select_functions`, `select_enabled_functions`, `select_enabled_mcp_servers` are **still on `Config`** and must stay there through Step 6. They get migrated in Step 7. ### What Step 4 should watch for - **The Step 4 target list** (from `PHASE-1-IMPLEMENTATION-PLAN.md`): `set_wrap`, `update`, `load_envs`, `load_functions`, `load_mcp_servers`, `setup_model`, `setup_document_loaders`, `setup_user_agent`. These are global-write methods that initialize or mutate serialized fields. - **Tension with Step 3's duplication decision:** Step 4 methods mutate `Config` fields. If we also duplicate them on `AppConfig`, then mutations through one path don't affect the other — but no caller ever mutates both, so this is fine in practice during the bridge window. - **`load_functions` and `load_mcp_servers`** are initialization- only (called once in `Config::init`). They're arguably not "global-write" in the same sense — they populate runtime-only fields (`functions`, `mcp_registry`). Step 4 should carefully classify each: fields that belong to `AppConfig` vs fields that belong to `RequestContext` vs fields that go away in Step 6.5 (`mcp_registry`). - **Strategy for Step 4:** because writes are typically one-shot (`update` is called from `.set` REPL command; `load_envs` is called once at startup), you can be more lenient about duplication vs consolidation. Consider: the write methods might not need to exist on `AppConfig` at all if they're only used during `Config::init` and never during request handling. Step 4 should evaluate each one individually. ### What Step 4 should NOT do - Don't add an `app_config: Arc` field to `Config` (see Key Decision #1 for why). - Don't touch the 7 methods added to `AppConfig` in Step 3 — they stay until Step 8+ caller migration, and Step 10 deletion. - Don't migrate `select_*` methods — those are Step 7. - Don't try to migrate callers of the Step 3 methods to go through `AppConfig` yet. The call sites still hold `Config`, and forcing a conversion would require either a clone or a sync'd field. ### Files to re-read at the start of Step 4 - `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 4 section - This notes file — specifically the "Deviations from plan" and "What Step 4 should watch for" sections - `src/config/mod.rs` — the current `Config::set_wrap`, `update`, `load_*`, `setup_*` method bodies (search for `pub fn set_wrap`, `pub fn update`, `pub fn load_envs`, etc.) - `src/config/app_config.rs` — the current shape with 7 new methods ## Follow-up (not blocking Step 4) ### 1. The `EDITOR` static sharing is pre-existing fragility Both `Config::editor` and `AppConfig::editor` now share the same `static EDITOR: OnceLock>`. If two Configs with different `editor` fields exist (unlikely in practice but possible during tests), the first caller wins. This isn't new — the single `Config` version had the same property. Step 10's `Config` deletion will leave only `AppConfig::editor` which eliminates the theoretical bug. Worth noting so nobody introduces a test that assumes per-instance editor caching. ### 2. `impl AppConfig` block grows across Steps 3-7 By the end of Step 7, `AppConfig` will have accumulated: 7 methods from Step 3, potentially some from Step 4, more from Step 7's mixed-method splits. The `#[allow(dead_code)]` currently covers the whole block. As callers migrate in Step 8+, the warning suppression can be removed. Don't narrow it prematurely during Steps 4-7. ### 3. Imports added to `app_config.rs` Step 3 added `MarkdownRender`, `RenderOptions`, `IS_STDOUT_TERMINAL`, `decode_bin`, `anyhow::{Context, Result, anyhow}`, `env`, `ThemeSet`. Future steps may add more. The import list is small enough to stay clean; no reorganization needed. ## References - Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` - Step 2 notes: `docs/implementation/PHASE-1-STEP-2-NOTES.md` - Modified file: `src/config/app_config.rs` (imports + new `impl AppConfig` block) - Unchanged but relevant: `src/config/mod.rs` (original `Config` methods still exist for now), `src/config/bridge.rs` (still passes round-trip tests)