Health check informational sub-checks gotcha
INFORMATIONAL_CHECKS in Rails code does not produce a neutral JSON signal
Applies to: Any health sub-check listed in HealthController::INFORMATIONAL_CHECKS that still performs a probe guaranteed to fail (e.g., a sidecar moved to a different service, a removed dependency).
HealthController (rails_api/app/controllers/health_controller.rb) supports a list of informational sub-checks:
INFORMATIONAL_CHECKS = %i[agent_runner].freezeChecks in this list are excluded from the gating logic — they do not flip the overall status to "degraded" or the HTTP status to 503. The /health response will still return HTTP 200 with "status": "ok" even if an informational check fails.
However, the sub-check result is still written into the checks hash and emitted verbatim in the JSON body. If the underlying probe fails, the JSON will contain:
"agent_runner": {
"status": "down",
"message": "Failed to open TCP connection to localhost:4001 (Connection refused)"
}Any monitoring system that reads raw checks.*.status values — including Penny's health-check autopilot — will see "status": "down" and fire an incident, regardless of the HTTP status code or the top-level "status" field.
The foot-gun: INFORMATIONAL_CHECKS is a Ruby-level classification. It has no effect on the JSON output shape. Monitoring systems cannot read Ruby.
What happened (OBJ-992, 2026-06-17)
PR #1173 (f9c43138f) correctly migrated the agent_runner sidecar from enkidu-api-staging (web) to enkidu-worker-staging (worker), and correctly added agent_runner to INFORMATIONAL_CHECKS so the overall /health verdict would not be affected. However, probe_agent_runner continued attempting a TCP connection to localhost:4001 — which is guaranteed to fail on the web service because the sidecar no longer runs there.
The result: every /health response on the web service included "agent_runner": { "status": "down", ... }. Penny's monitor, which scans individual checks.*.status values, fired a high-priority staging degradation incident. The staging environment was actually healthy; the AI Workforce was running on the worker service where it belongs.
Fix
For a sub-check that will always fail (permanently moved or removed dependency), return a neutral non-"down" status from the probe itself:
def probe_agent_runner
{ status: 'not_applicable', message: "agent_runner sidecar runs in enkidu-worker-#{Rails.env}" }
endThis removes the bad signal at the source. Penny's monitor ignores any status other than "down", so no monitoring changes are needed.
When returning not_applicable:
- Keep the check wired through
check_agent_runner— it still appears in the JSON body and on the status page. - Keep the check in
INFORMATIONAL_CHECKS— this documents intent and prevents it from gating the overall verdict even if the probe is later updated to return a non-ok status. - Update the spec to expect
"not_applicable":
it 'reports not_applicable because the sidecar runs in enkidu-worker-{env}' do
get '/health'
expect(response).to have_http_status(:ok)
body = JSON.parse(response.body)
expect(body['checks']['agent_runner']['status']).to eq('not_applicable')
endAlternative: update the monitor
A more robust long-term fix is to update the monitoring logic to trust the HTTP status code and top-level "status" field rather than scanning individual sub-checks. Only alert when response.status >= 500 OR body.status == "degraded". This makes monitoring resilient to any future informational sub-check that returns a non-ok value.
The source fix (returning not_applicable) is faster and more targeted. The monitor fix is a systemic improvement. Both are valid; together they are comprehensive.
Rule of thumb
If a health sub-check is both informational AND will always fail, it should return a neutral status (e.g.
not_applicable) rather than letting the underlying probe fail and emit"down".INFORMATIONAL_CHECKSprevents the check from gating HTTP 200 — but it does not change what the JSON body contains.
Related: Time.zone contamination in sharded RSpec (same incident)
This incident also surfaced a latent fragility in the test suite. The CI failure on PR #1180 was an unrelated spec:
spec/interactions/user_identity/backfill_onboarding_completion_spec.rb:25The failure was caused by TimeWithZone#iso8601 including the local timezone offset when Time.zone is non-UTC. If a prior spec in the same RSpec shard sets Time.zone and doesn't reset it, iso8601 produces "2026-06-17T17:19:32-04:00" instead of "2026-06-17T21:19:32Z", breaking timestamp comparisons.
Root cause: rails_api/spec/rails_helper.rb includes after(:each) hooks for DomainEvents.reset_for_testing! and Timecop.return, but not for Time.zone:
# rails_helper.rb — missing:
config.after(:each) { Time.zone = 'UTC' }Fix applied in this incident: Changed .iso8601 to .utc.iso8601 in backfill_onboarding_completion.rb — this is invariant to Time.zone state and correct regardless.
Root cause fixed in OBJ-994 / PR #1191: config.after(:each) { Time.zone = 'UTC' } has been added to rails_api/spec/rails_helper.rb alongside the existing DomainEvents and Timecop resets. This closes the underlying leak — no future spec in a sharded run can contaminate Time.zone for a subsequent example.
Related
rails_api/app/controllers/health_controller.rb—INFORMATIONAL_CHECKS,check_agent_runner,probe_agent_runnerrails_api/spec/requests/health_spec.rb— informational sub-check contract testsrails_api/spec/rails_helper.rb—Time.zone = 'UTC'reset added inafter(:each)(OBJ-994 / PR #1191)docs/operations/observability.md—/healthendpoint structure and component listdocs/operations/incident-response.md— Agent Runner failure mode and recoverydocs/operations/ai-usage-event-features-allowlist-gotcha.md— a related but distinct silent-drift failure shape gating the same/healthendpoint (stale allowlist vs. probe-always-fails)
Last updated: 2026-07-22