Skip to content

Unify ApplyConfig and ReloadConfiguration side-effect application (config dual-path parity) #861

Description

@Dumbris

Background

Codex cross-model review of PR #857 (config-file watcher / hot-reload) surfaced the same class of bug across multiple rounds: MCPProxy has two config-application paths — Runtime.ApplyConfig (API path) and Runtime.ReloadConfiguration (disk/watcher path) — and every live side effect must be wired into both by hand. Each round found another side effect that one path applied and the other silently skipped, leaving the config snapshot updated while running components kept stale values.

Parity gaps found so far (all on PR #857)

Fixed in the PR:

  • upstreamManager.SetGlobalConfig — disk reloads didn't propagate the global config to running managed clients (round 5).
  • Legacy r.cfg/r.cfgPath sync — disk reloads only landed in the configsvc snapshot; GetConfig() (backing GET/PATCH /api/v1/config) kept serving the stale config, and a subsequent PATCH would merge onto the stale base and silently revert the external edit (round 5).
  • Logging (SetLogConfig), tool-response truncator rebuild, observability usage cadence — applied by ApplyConfig only (round 6). Now extracted into a shared applyComponentConfigLocked helper called from both paths.

Known remaining gaps / related weaknesses (round 7 + review notes):

  • Replacing r.truncator does not reach the already-constructed MCP request handler, which captured the old truncator pointer — live tool-call responses keep the stale limit on BOTH paths (internal/runtime/runtime.go applyComponentConfigLocked; also pre-existing in ApplyConfig).
  • Runtime.Truncator() reads the field without holding r.mu while both reload paths swap it under the lock.
  • DetectConfigChanges returns early on restart-required fields, so its ChangedFields under-reports hot-reloadable changes riding along in the same edit — any consumer gating side effects on it inherits that trap (the shared helper now compares fields directly to avoid it).
  • The watcher's self-write suppression marker (lastSelfWrite) is a single slot; back-to-back ApplyConfig saves can overwrite it before the first debounce fires, letting a restart-required save be misread as an external edit and hot-applied (round 7 finding 1).

Proposal

Unify side-effect application behind one function (e.g. extend applyComponentConfigLocked or introduce an applyConfigSideEffects(old, new) that both paths MUST call), so a new hot-reloadable side effect is added in exactly one place. Consider:

  • an inventory/audit of everything ApplyConfig does post-save vs. what ReloadConfiguration does,
  • routing live component dependencies (truncator etc.) through accessor indirection instead of captured pointers,
  • a multi-slot / content-keyed recent-self-write set for the watcher suppression marker,
  • a regression test pattern that applies the same edit via API and via disk and asserts identical observable component state.

Found by Codex review of PR #857 (rounds 5–7).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/proxyCore MCP proxy engine and tool routingkind/refactorCode restructuring with no behaviour changepriority/mediumImportant but not blocking a release

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions