Surface config hot-reload watcher status in health#99267
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 2, 2026, 9:17 PM ET / 01:17 UTC. Summary PR surface: Source +51, Tests +325. Total +376 across 17 files. Reproducibility: yes. Source inspection shows current main tracks hotReloadStatus() in the config watcher but drops it at the managed reloader and health paths; I did not run a local gateway because this review is read-only. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Next step before merge
Security Review detailsBest possible solution: Land the narrow additive health plumbing after normal maintainer approval, keeping the signal derived from live runtime state and the status type in a leaf contract. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main tracks hotReloadStatus() in the config watcher but drops it at the managed reloader and health paths; I did not run a local gateway because this review is read-only. Is this the best way to solve the issue? Yes. Forwarding the existing live accessor through the managed handle, full-refresh path, and cached health merge path is the narrow owner-boundary fix, and the leaf type avoids the earlier implementation-cycle problem. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 381b44a9fc26. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +51, Tests +325. Total +376 across 17 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
Thread the config reloader's existing hotReloadStatus() accessor through the managed reloader handle (previously only stop() survived the handoff) and into openclaw health as an optional configReload.hotReloadStatus field, so operators can tell when the watcher has permanently disabled itself after exhausting retries instead of silently running with stale config.
…h responses Thread getConfigReloaderHotReloadStatus through GatewayRequestContext (mirroring the existing getEventLoopHealth pattern) so the health RPC's cache-hit merge path picks up a live watcher disable within the same request instead of waiting up to HEALTH_REFRESH_INTERVAL_MS for the background refresh to catch up.
Move GatewayHotReloadStatus out of config-reload.ts into a leaf config-reload-status.types.ts so health/request-context/runtime-handles callers no longer pull the full config-reload implementation into the shared server-method type graph (ClawSweeper-flagged architecture cycle).
3325910 to
cd4027a
Compare
|
Maintainer update: rebased the contributor branch onto current main, preserved both the new delivery-queue health surface and this PR's config-reload health surface, then updated the new hot-reload test fixture for the latest |
* fix(gateway): surface config hot-reload watcher status in health Thread the config reloader's existing hotReloadStatus() accessor through the managed reloader handle (previously only stop() survived the handoff) and into openclaw health as an optional configReload.hotReloadStatus field, so operators can tell when the watcher has permanently disabled itself after exhausting retries instead of silently running with stale config. * fix(gateway): keep configReload.hotReloadStatus fresh on cached health responses Thread getConfigReloaderHotReloadStatus through GatewayRequestContext (mirroring the existing getEventLoopHealth pattern) so the health RPC's cache-hit merge path picks up a live watcher disable within the same request instead of waiting up to HEALTH_REFRESH_INTERVAL_MS for the background refresh to catch up. * fix(health): move config reload status type to leaf contract Move GatewayHotReloadStatus out of config-reload.ts into a leaf config-reload-status.types.ts so health/request-context/runtime-handles callers no longer pull the full config-reload implementation into the shared server-method type graph (ClawSweeper-flagged architecture cycle). * test(gateway): update config reloader fixture --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(gateway): surface config hot-reload watcher status in health Thread the config reloader's existing hotReloadStatus() accessor through the managed reloader handle (previously only stop() survived the handoff) and into openclaw health as an optional configReload.hotReloadStatus field, so operators can tell when the watcher has permanently disabled itself after exhausting retries instead of silently running with stale config. * fix(gateway): keep configReload.hotReloadStatus fresh on cached health responses Thread getConfigReloaderHotReloadStatus through GatewayRequestContext (mirroring the existing getEventLoopHealth pattern) so the health RPC's cache-hit merge path picks up a live watcher disable within the same request instead of waiting up to HEALTH_REFRESH_INTERVAL_MS for the background refresh to catch up. * fix(health): move config reload status type to leaf contract Move GatewayHotReloadStatus out of config-reload.ts into a leaf config-reload-status.types.ts so health/request-context/runtime-handles callers no longer pull the full config-reload implementation into the shared server-method type graph (ClawSweeper-flagged architecture cycle). * test(gateway): update config reloader fixture --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
What Problem This Solves
Fixes an issue where operators running a managed gateway would keep working
with a silently disabled config hot-reload watcher. #92027 and #92852
(both merged) taught the watcher to retry past transient native/polling
errors and record a terminal
"disabled"status once retries areexhausted, but that status was discarded at the managed-reloader boundary
and never reached
openclaw health. An operator editingopenclaw.jsonand expecting a hot reload would see nothing happen, with no way to tell
"the gateway ignored this edit" apart from "the edit produced a no-op
reload plan."
Why This Change Was Made
The watcher already computes the right terminal state; this change is pure
plumbing to surface it, not new reload policy.
Changed surface:
src/gateway/server-runtime-handles.ts/src/gateway/server-reload-handlers.ts—
GatewayConfigReloaderHandlegains an optionalhotReloadStatus()accessor alongside
stop();startManagedGatewayConfigReloaderreturnsthe watcher's real accessor instead of a handle that only exposes
stop.src/commands/health.types.ts/src/commands/health.ts— new optionalHealthSummary.configReload.hotReloadStatusfield + a text-output linethat only prints on
"disabled".src/gateway/server.impl.ts/src/gateway/server/health-state.ts/src/gateway/server-methods/shared-types.ts/src/gateway/server-request-context.ts/src/gateway/server-methods/health.ts— thread the live accessor through both the full-refresh path and the
cache-hit merge path (one new
GatewayRequestContextgetter, wired at thesingle
createGatewayRequestContextcall site).Each layer above exists because the health RPC has two independent response
paths (full refresh vs. cache-hit merge), and both already carry other live
runtime facts (
eventLoop,modelPricing,contextEngines) the same way —this change reuses that existing plumbing pattern rather than adding a new
one. No generic "conditions" abstraction was introduced because there is
exactly one signal to surface today; a future second signal would be the
trigger to generalize.
configReload.hotReloadStatusfollows the existingmodelPricing/contextEnginessibling pattern: the JSON field is present whenever a realreloader is running (
"active"or"disabled"), and omitted only whenthere is no reloader at all (
minimalTestGateway, pre-startup inert state).Text output additionally suppresses the
"active"case and only prints aline on
"disabled", matching howformatModelPricingHealthLineandformatContextEngineHealthLinestay silent on the healthy/ok case.Non-goals: no new persisted state (the field is derived, not stored), no
doctor repair (this surfaces existing in-memory watcher state, not a
migratable config shape), no generic "conditions" enum (see above), no
change to reload/retry policy.
User Impact
Operators can now run
openclaw health(text or--json) and seeConfig hot reload: disabled (watcher retries exhausted; restart the gateway to restore it)when the gateway's config watcher has permanently given up,instead of silently continuing to run on stale config with no visible
signal. The status is fresh on every call, including cache-hit responses,
so it's visible immediately rather than after a cache refresh delay.
--jsonincludesconfigReload.hotReloadStatuswhenever a reloader isrunning, whether
"active"or"disabled"; text output only prints a linefor the
"disabled"case, so the common healthy case stays quiet. Thefield is omitted entirely only when no reloader exists at all (e.g.
minimal/test gateways).
Evidence
Production-path health capture:
OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/port,gateway.auth.mode=none, channels/providersskipped so no real credentials were needed) and ran
openclaw health --json; the response included"configReload":{"hotReloadStatus":"active"}.This validates the
"active"plumbing end to end through the realproduction chain (real
startGatewayConfigReloader→runtimeState.configReloader→GatewayRequestContext→healthHandlers.health→ JSON output), not just a mocked/unit-test path."disabled"path in arunning production gateway — that terminal transition is covered
separately by
src/gateway/config-reload.test.ts's existing real-watcherretry/polling exhaustion test (cited below), not a live capture. Forcing
real chokidar/inotify exhaustion in a production-path capture would be
flaky and wasn't attempted.
Existing signal proof (unchanged, cited not duplicated):
src/gateway/config-reload.test.ts:1675—"degrades to polling then disables after both native and polling retries are exhausted"— alreadyproves
hotReloadStatus()flips"active"→"disabled"after native +polling retries are exhausted (introduced by #92027/#92852).
New plumbing proof (
src/gateway/server-reload-handlers.hot-reload-status.test.ts):mocks
startGatewayConfigReloaderto return a controllable live accessor,then proves
startManagedGatewayConfigReloader(...).hotReloadStatusis afunction that reflects a live mutation of the underlying watcher's status
(
"active"→"disabled") without recreating the managed handle — this isexactly the regression a copied/snapshotted value would fail.
New health summary proof (
src/commands/health.snapshot.test.ts):"omits configReload when no config reloader status is supplied""surfaces a disabled config hot-reload watcher in the health snapshot"New health-state passthrough proof (
src/gateway/server/health-state.test.ts):"passes the config reloader hot-reload status only when the hook returns one"(mirrors the existing
getEventLoopHealthpassthrough test)New cache-hit freshness proof (
src/gateway/server-methods/server-methods.test.ts,describe("gateway healthHandlers.health cache freshness")):"merges a live disabled config hot-reload status into cached health responses"— cached snapshot says
"active", live accessor says"disabled"; thecache-hit response (
{ cached: true }) reflects"disabled"immediately"preserves the cached config hot-reload status when no live accessor is available"— no getter on context → last cached value survives unchangedNew live request-context read proof (
src/gateway/server-request-context.test.ts):"reads config hot-reload status live from runtime state"— provescontext.getConfigReloaderHotReloadStatus()reflects live mutation ofruntimeState.configReloaderat the single wiring site, mirroring theexisting live
cron/cronStorePathrequest-context read tests.New CLI output proof (
src/commands/health.test.ts):formatConfigReloadHealthLineunit tests: disabled → line text, active →null, absent →null"surfaces a disabled config hot-reload watcher in JSON output""prints the config hot-reload disabled line in text output""omits the config hot-reload line in text output when the reloader is active"Validation run (worktree
repo/worktrees/config-hot-reload-health-status,node scripts/run-vitest.mjs, rebased onto upstream/maindad7768da8500d1c785ef33e19aabee9d01081bd):src/gateway/config-reload.test.ts— 304/304 passedsrc/gateway/server-reload-handlers.test.ts+src/gateway/server-reload-handlers.hot-reload-status.test.ts+src/gateway/server-request-context.test.ts+src/gateway/server-methods/server-methods.test.ts+src/gateway/server/health-state.test.ts(gateway config project,15 files) — 553/553 passed
src/commands/health.test.ts+src/commands/health.snapshot.test.ts—35/35 passed
pnpm exec oxfmt --checkon all touched files — cleannode scripts/run-oxlint.mjson all touched files — cleannode scripts/run-tsgo.mjs -p tsconfig.core.json(prod typecheck) — cleannode scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json(testtypecheck) — clean
node --import tsx scripts/check-import-cycles.ts— 0 runtime value cyclesNo live/E2E proof was added beyond the production-path capture above: this
is plumbing of an already-tested terminal state through existing accessor
boundaries, not new watcher/chokidar behavior, so no OS-specific or
timing-sensitive CI test was introduced per repo guidance against flaky
watcher tests in CI.
Precheck note
ocw review precheck config-hot-reload-health-statusreached the broadgateway-serverVitest lane but failed in pre-existing shared-lane testsunrelated to this branch.
node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway-server.config.ts src/gateway/server.health.test.ts src/gateway/server-startup-log.test.tsreproduces the same
server-startup-log.test.tswarning failures.upstream/main; this branch does not touch them.src/gateway/test-helpers.mocks.tsmutatesprocess.env.OPENCLAW_SKIP_CHANNELS = "1"at module top level withoutcleanup, then leaks across the non-isolated gateway-server project
depending on file order.
and production-path health capture all passed.