Repository navigation
fix(health-check): detect a credential-proxy that's listening but wedged - #4874
Conversation
check_credential_proxy() only ever proved the port is TCP-listening
("probe=False: a forwarding proxy has no liveness endpoint, so an HTTP
probe is forwarded and misread as wedged"). That left the exact failure
mode from the 2026-09-27 incident invisible: the proxy stays up and
accepts connections while every request 401s/502s because its stored
credential can't be refreshed -- 55min-2h of every seat losing the API
before a human noticed, because health-check kept reporting "ok".
credential-proxy.ts already tracks this per request via
recordCredentialState(), writing credential_state/_detail/_at to
quota-state.json: 'ok' on each successful token injection, 'exhausted'
when a request couldn't get a usable credential. That signal already
exists and is self-healing (the next successful request flips it back
to 'ok'); nothing needed to change in the proxy itself.
check_credential_proxy() now reads it and escalates to 'warn' when the
proxy's own last-recorded state says 'exhausted', with the recorded
detail and how long ago. Only consulted while the port is already
'ok'/'stale' (never what marks a genuinely-down proxy), silent on any
read failure (advisory, same rule mark_stale_if_outdated already
follows for this exact file).
Known gap this does NOT close: recordCredentialState is only called
from the request path, so a background refresh failing before any
request arrives (the original incident's literal trigger) stays
invisible until #4842's give-up-after-N-failures exits the process --
which is now bounded (~15.5 min at the default threshold, down from the
unbounded wedge this incident hit) rather than closed. Proposed fix #4
(per-seat runtime fallback) is separate follow-on work, not attempted
here.
Fixes proposal #1 from the "Credential proxy (:7846) wedges silently"
post-mortem (ww room, Issues DB row rmukn84lu4akyf), alongside already-
merged #4842 (fix #2) and #4845 (fix #3, mine). Requested by Chi.
Also isolates tests/health-check-credential-proxy-staleness.test.py from
this host's real quota-state.json: it didn't patch WORKSPACE_DIR, and my
new read path would otherwise have read live host state through it.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 90%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed at e93598f303, including skills/quota-tracker/scripts/credential-proxy.ts at the same head, since the check reads what that file writes. The approach is right: it reuses the proxy's own recorded credential state rather than adding a probe. The field names (credential_state, credential_state_detail, credential_state_at) and the state/quota-state.json path match recordCredentialState() (credential-proxy.ts:722-731). Two things to fix before I'd approve.
1. False warn after a proxy restart (should fix).
recordCredentialStateis change-only (if (prev.credential_state === state) return;, line 725), and nothing resets the field when the proxy starts (lines 787-795).- So once the proxy is restarted after an
exhaustedwedge (the usual repair), the file keepsexhausteduntil the first request succeeds. Every health check in between reports the new, healthy proxy as wedged. test_down_proxy_never_consults_the_filecovers "down", but not "restarted and listening".- Suggested fix: ignore an
exhaustedwhosecredential_state_atis older than the current proxy process's start time.check_credential_proxyalready reads process start times via_proc_lstarts("credential-proxy"). Add a test: exhausted at T, proxy started at T+1, status staysok.
2. The docstring breaks the repo's comment rule (should fix).
- CLAUDE.md: "Code comments: at most 2 lines, and only what the code cannot state itself ... no incident history". The new function carries a 20-line docstring that includes the 2026-09-27 incident. The test module's docstring repeats it.
- The incident narrative belongs in the PR body (it's already there). In the code, keep the one constraint a reader needs: "advisory; never marks a non-listening proxy down; silent on read failure".
Minor (non-blocking): the local from datetime import datetime as _dt with a comment explaining why it isn't at module scope. If datetime really can't be imported at module scope here, fine; otherwise hoist it and drop the comment.
The tests I read pin the right cases (escalate ok/stale, self-heal, missing or unreadable file silent, down never consults). I did not run them; CI is green at this head.
Kewei/Qingyun review on #4874: recordCredentialState() writes on a transition only and is never reset at startup, so a restarted proxy (the exact recovery path #4842/#4845 rely on) inherits whatever 'exhausted' was last written before it existed -- a false warn until the first request succeeds. Compares credential_state_at against the current process's start time (_proc_lstarts, already used elsewhere in this same check) and ignores a record older than it. Also trims the function/module docstrings per review: the repo's comment rule, and prose-cap, flag narrative/incident history in comments -- that context now lives in the PR body instead.
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed at f16c27fc.
Fix 1 (restart false-warn): correct. _proc_lstarts returns (start timestamps, …), so at_ts < max(starts) means "the record predates the newest running proxy", and that case now returns early. A probe failure gives [], which falls through to warn, matching the new no-process-info test.
Fix 2 (comments): the incident history is gone, thanks.
One wording fix, then I approve. The docstring now says the opposite of the code: "Escalate … when … 'exhausted' and predates its current process incarnation". The code escalates only when the record does not predate the process, and ignores it when it does. Suggested wording: "…is 'exhausted' and was recorded by the current process (a record older than the process start is ignored)." The docstring is also still 5 lines against the 2-line rule; that one is a nit, but the inversion should be fixed, because a reader will trust it over the code.
CI was still running at this head when I looked (11 passing, 14 pending).
The escalation fires when the record was recorded by the current process, not when it predates it — the docstring said the opposite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approve at fb753b21. The only change since my last review (f16c27fc) is the docstring, which now matches the code: a record older than the running proxy's start is ignored. The logic was already verified at f16c27fc: restart false-warn fixed, probe failure still warns, 3 tests pin both. 3 checks were still pending when I looked; merge once they are green.
john-the-dev
left a comment
There was a problem hiding this comment.
Approve at fb753b21.
What I checked
- Field names and path match the writer.
recordCredentialState()(credential-proxy.ts:722-731) writescredential_state,credential_state_detailandcredential_state_atintostatusPath('quota-state.json').updateQuotaState()(:749-753) copies all three fields forward when it replaces the file, so a header write cannot erase anexhaustedrecord before this check sees it. - Tests pass at this head. I ran them in a clean worktree after clearing
__pycache__. The drivability suite passes 12/12; the PR body says 9, but the restart-guard cases were added later. The staleness suite passes 9/9. - The restart guard works against a real process, not just mocks.
_proc_lstarts("credential-proxy")found this host's running proxy, started 2026-09-28T13:36:49Z. Anexhaustedrecord stamped an hour before that start left the statusok. The same record stamped 60s ago raisedwarn. The two timestamps compare correctly:lstartis parsed as naive local time andcredential_state_atas aware UTC, and both become epoch seconds through.timestamp(). - The hardcoded-path, root-artifact and comment-length checks all pass on the full diff (
gh pr diff 4874 | bash scripts/review-checks.sh).
Non-blocking
- The restart guard only recognises a proxy process that belongs to this checkout.
_proc_lstartskeeps a process only if its command line or working directory is insideREPO_DIR(_filter_pids_this_checkout). A proxy launched from outside the checkout, such as a packagedruntime/servicesbuild, would givestarts == []. The guard would then be skipped, and a proxy restarted after anexhaustedwedge would read aswarnuntil its first successful request. That's the same false warn qingyun's first review flagged, just on a different install layout. I didn't hit it on this host, where the proxy runs from<checkout>/dist/credential-proxy.js. The failure is a false warn, which is the safer direction, so it's fine to follow up separately. - The PR body's test count is out of date (9 vs 12).
3 CI lanes were still running when I submitted: shell serial tail, and python standalone 2/5 and 3/5. The other 23 had passed.
Summary
check_credential_proxy()only ever proved the port is TCP-listening (probe=False). That left the exact 2026-09-27 incident's failure mode invisible: the proxy stays up while every request 401s/502s because its stored credential can't be refreshed — 55min-2h of every seat losing the API before a human noticed, because health-check kept reporting "ok".credential-proxy.tsalready tracks this per request viarecordCredentialState(), writingcredential_state/_detail/_attoquota-state.json. That signal is self-healing (next successful request flips it back took) — nothing needed to change in the proxy itself.check_credential_proxy()now reads it and escalates towarnwhen the last-recorded state isexhausted, only while the port is alreadyok/stale, silent on any read failure.Known gap this does NOT close:
recordCredentialStateis only called from the request path, so a background refresh failing before any request arrives stays invisible until #4842's give-up-after-N-failures exits the process — now bounded (~15.5 min) rather than unbounded, not eliminated. Fix #4 (per-seat runtime fallback) is separate follow-on work.Fixes proposal #1 from the "Credential proxy (:7846) wedges silently" post-mortem (ww room), alongside already-merged #4842 (fix #2) and #4845 (fix #3, mine). Requested by Chi directly.
Test plan
tests/health-check-credential-proxy-drivability.test.py(9 cases): escalates ok/stale to warn onexhausted, leavesokstate untouched, self-heals, silent on missing/unreadable file, never fires when the port itself is down, age reporting, unparseable-timestamp fallback.tests/health-check-credential-proxy-staleness.test.py(9 cases) still passes — also fixed a latent test-isolation gap it had (didn't patchWORKSPACE_DIR, so my new read path would have read this host's realquota-state.jsonthrough it).bash scripts/review-checks.shclean (hardcoded-paths + root-artifacts + prose-cap).python3 src/health-check.pyrun:credential-proxy ok port 7846— no crash, no false alarm (this host's realcredential_stateis currentlyok).🤖 Generated with Claude Code