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).
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) andRuntime.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).r.cfg/r.cfgPathsync — disk reloads only landed in the configsvc snapshot;GetConfig()(backingGET/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).SetLogConfig), tool-response truncator rebuild, observability usage cadence — applied byApplyConfigonly (round 6). Now extracted into a sharedapplyComponentConfigLockedhelper called from both paths.Known remaining gaps / related weaknesses (round 7 + review notes):
r.truncatordoes 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.goapplyComponentConfigLocked; also pre-existing inApplyConfig).Runtime.Truncator()reads the field without holdingr.muwhile both reload paths swap it under the lock.DetectConfigChangesreturns early on restart-required fields, so itsChangedFieldsunder-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).lastSelfWrite) is a single slot; back-to-backApplyConfigsaves 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
applyComponentConfigLockedor introduce anapplyConfigSideEffects(old, new)that both paths MUST call), so a new hot-reloadable side effect is added in exactly one place. Consider:ApplyConfigdoes post-save vs. whatReloadConfigurationdoes,Found by Codex review of PR #857 (rounds 5–7).