Repository navigation
Keep telemetry running after an unreadable saved login or a malformed collector answer, and refuse an unsendable collector token - #38
Merged
anth-volk merged 18 commits intoOct 29, 2026
Conversation
A saved Hugging Face login this user may not read, or one that is not text, raises out of TelemetryModule.tick() today, so the host stops the module. The test expects the run to become local-only for the reason a missing login gives, with one warning naming only the error's type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
huggingface_token() now raises UnreadableLoginError, an OSError naming only the original error's type, when only the login saved by `hf auth login` is left and huggingface_hub cannot read it. The telemetry module catches it and makes the run local-only with missing_huggingface_credential, as for a missing login, printing one warning with the error's type. `telemetry-token` uses the same error for its exit 3, so the classification lives in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A loopback collector answers one run registration or event upload with a truncated chunked answer (IncompleteRead) or bytes that are not HTTP (BadStatusLine). Today the HTTPException raises out of TelemetryModule.tick(), so the host stops the module. The test expects the run and its queued event to stay as they were, and the request to be retried after the backoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run registration and event upload now catch http.client's HTTPException with OSError and TimeoutError, so an answer that is not valid HTTP or ends early is retried with backoff, as the README says network failures are, instead of stopping the telemetry module. The run's state does not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 200 exchange answer whose access_token contains a space, a line break or a non-ASCII character is accepted today: `telemetry-token` prints it and exits 0, and a workstation session keeps it, so the telemetry module's next request raises ValueError. The test expects such an answer to count as one without a usable token: exit 5, and `unavailable` from the session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exchange_huggingface_credential now applies _SENDABLE_CREDENTIAL to the collector's access_token, as it does to the Hugging Face credential. Any other token counts as an answer without a usable token: `unavailable`, so `telemetry-token` exits 5 and a workstation session never keeps a token its next request would refuse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With HF_OIDC_RESOURCE set, huggingface_hub's get_token() exchanges a CI OIDC token instead of reading the saved login, and a 503 from the Hub raises HfHubHTTPError, an OSError. huggingface_token() wraps every OSError today, so that outage is reported as an unreadable saved login. The tests expect it, an OIDC answer that is not text, and an OSError that names no file to pass through unchanged, as on main, and `telemetry-token` to exit 7 for the outage instead of 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
huggingface_token() now raises UnreadableLoginError only for a PermissionError, a UnicodeDecodeError, or another OSError that names a file, and never while HF_OIDC_RESOURCE is set, when huggingface_hub exchanges a CI OIDC token instead of reading the saved login. Every other error of get_token(), such as the HfHubHTTPError of a 503 from the Hub, propagates unchanged, as on main; `telemetry-token` reports it as an unexpected error (exit 7). The new error is raised outside the except block, so it no longer keeps the original error, whose text names the file, as its context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local-only cannot be undone, and the branch applies it to every pending run in the event queue file, including earlier builds' runs that main left there. The revised test expects this run and an earlier one to stay pending with the reason huggingface_login_unreadable, the login to be read again only after the backoff, one warning naming only the error's type, the shutdown drain to end at once, and the next emitter with a readable login to deliver both runs and clear the reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the branch, the run is local-only after the first unreadable read, so it is never delivered. The test expects the run to wait, with waiting_for_credential set so the shutdown drain ends at once, and to be delivered by the same emitter once the file can be read, after which the drain waits for deliveries again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unreadable saved login no longer makes runs local-only, which cannot be undone and applied to every pending run in the event queue file. CollectorDelivery records the pending reason huggingface_login_unreadable, warns once with the error's type, and reads the login again after the backoff used for collector outages. waiting_for_credential is set meanwhile, so the shutdown drain ends at once, and cleared when a credential is obtained again. A successful registration clears the reason, so this emitter or a later one delivers the runs once the login can be read. README and changelog fragment updated to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
core.auth's NETWORK_FAILURES (OSError, TimeoutError, HTTPException) now serves both the token exchange and telemetry delivery, which kept their own lists before; the bug this pull request fixes came from those lists differing. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exchange answers 200 with an access_token containing a space. The test runs TelemetryModule.tick() and expects nothing to be sent with that token, the run and its event to stay as they were, and the exchange to be retried after the backoff. Without the exchange's check of the token, the module's registration request raises ValueError out of tick(), which stops the module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two cases the branch leaves behind. A run this emitter already registered, whose login cannot be read when the exchanged token is renewed, keeps huggingface_login_unreadable after its events are delivered, because only registration clears the reason. And once the login can be read but the collector does not answer, waiting_for_credential stays set, so the shutdown drain stops at once instead of retrying the outage. The tests expect the reason cleared and the flag unset as soon as the login is read again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CollectorDelivery now keeps the runs it recorded as waiting for the saved login in a set, and waiting_for_credential is a property: the given token can no longer be used, or that set is not empty. When the session next answers without UnreadableLoginError, whatever the exchange answered, the set's runs get their reason cleared, registered or not, and the set is emptied. So a delivered run no longer keeps huggingface_login_unreadable, and a collector outage afterwards gets the usual shutdown retries. The given-token warning keeps its own flag. README updated to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the branch for an OSError that names a file (IsADirectoryError, and a plain OSError for a symbolic link loop) alongside PermissionError and UnicodeDecodeError, and checks that the new error keeps no context or cause. Without the filename branch the two OSError cases fail; with the error raised inside the except block every case fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CollectorSession's docstring and the comment on TelemetryRunRecord.local_only_reason now describe the huggingface_login_unreadable pending reason. The README's exit-code table, the comment on the exit codes and the changelog fragment say that a failed OIDC token exchange while HF_OIDC_RESOURCE is set exits 7: it fails before the collector is contacted, so not 5, and is not a missing or unreadable login, so not 3. The fragment also says the reason is cleared as soon as the login can be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 29, 2026 18:47
anth-volk
deleted the
claude/telemetry-unreadable-login-malformed-answer
branch
October 29, 2026 18:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #37
The third review of #34 found two kinds of failure that stop a workstation emitter's telemetry module, and a token exchange that accepts a collector token it cannot send. Each behaviour change has its own failing test, committed first.
Changes
1. A saved login that cannot be read
huggingface_token()raisesUnreadableLoginErrorwhen neitherHF_TOKENnorHUGGINGFACE_TOKENis set and reading the login saved byhf auth loginfails: aPermissionError, aUnicodeDecodeError(the file is not text), or anotherOSErrorthat names a file, such asIsADirectoryError. The error keeps only the original error's type name. It is raised outside theexceptblock, so it does not keep the original error as its context either: that error's text names the file, and aUnicodeDecodeErrorholds the file's bytes.get_token()propagates unchanged, as onmain. WhileHF_OIDC_RESOURCEis set, huggingface_hub exchanges a CI OIDC token instead of reading the saved login, so none of its errors count: for example theHfHubHTTPError(anOSErrorthat names no file) of a 503 from the Hub.CollectorDeliveryrecords the pending reasonhuggingface_login_unreadablefor each run it tries, and prints one warning per module that names only the error's type:Microcosm telemetry is waiting: the Hugging Face login saved by `hf auth login` could not be read (PermissionError), and neither HF_TOKEN nor HUGGINGFACE_TOKEN is set. Queued events stay pending, and are delivered once the login can be read. The dataset build will continue._defer_retry, 1 second doubling to 60).waiting_for_credentialis true, so the shutdown drain ends at once instead of waiting 15 seconds, as for a given token that can no longer be used. It is now a property: the given token can no longer be used, or the set of runs waiting for the login is not empty.UnreadableLoginError, whatever the exchange answered, this emitter clears the reason of every run in that set, including runs it had already registered, and empties the set. So a collector outage afterwards gets the usual shutdown retries, and a delivered run does not keep the reason.telemetry-tokencatches the same error for its exit 3, so the errors that mean "the saved login cannot be read" are listed in one function.Why pending, not local-only. The issue allows either. Local-only cannot be undone, and
flush_oncewould apply it to every pending run in the user's event queue file, including earlier builds' runs. Onmainthose runs stayed in the file. A login file that exists can be repaired, for example a token file owned by another user, and the next emitter with a readable login should then deliver them.UnreadableLoginErroris anOSError, so the other modules that read the login behave as onmain: the Orrery module (which shares the telemetry module's session throughshared_session) still catches it inexcept (OSError, ValueError, KeyError, TypeError), and the collector submission worker (which has its own session) still catches it inexcept Exception.2. A malformed collector answer
core.auth.NETWORK_FAILURES(OSError,TimeoutError,http.client.HTTPException) lists what_http_postraises when it gets no usable answer. The token exchange, run registration and the event upload inflush_onceall use this one tuple. An answer that is not valid HTTP (BadStatusLine) or ends early, such as a truncated chunked answer (IncompleteRead), is therefore retried with backoff, as the README says network failures are. The run's state and its queued events do not change.3. A collector token that cannot be sent
exchange_huggingface_credentialapplies_SENDABLE_CREDENTIALto the collector'saccess_token, as it does to the Hugging Face credential. A token with a space, a line break or a non-ASCII character is an answer without a usable token:unavailable.telemetry-tokenthen exits 5, and a workstation's telemetry module retries the exchange after the backoff instead of keeping a token that its next request would refuse withValueError.Documentation
telemetry-tokenexit codes: row 5 names the visible-ASCII rule foraccess_token, and row 7 says a failed OIDC token exchange exits 7 and why.CollectorSession's docstring and the comment onTelemetryRunRecord.local_only_reasondescribe the new pending reason.changelog.d/telemetry-keeps-running.fixed.md: merging publishes a patch release (0.11.1).Not in scope
The issue's parts about
deliver --spooland #36: #36 was closed unmerged, so that command does not exist.Behaviour that differs from
mainoutside the three fixestelemetry-tokenwithHF_OIDC_RESOURCEset, when the OIDC exchange fails with anOSErrorsuch asHfHubHTTPError, now exits 7 ("an unexpected HfHubHTTPError occurred") instead of 3. Onmainits ownexcept (OSError, UnicodeDecodeError)reported that failure as a saved login that could not be read. It is not 5 either: that code means the collector, and the OIDC exchange fails at the CI provider or the Hub before the collector is contacted. Every OIDC failure now exits 7; onmain,OIDCErrorand httpx errors already did.Tests
test_an_unreadable_saved_login_keeps_runs_pending_until_it_can_be_read(not readable by this user, not text): runsTelemetryModule.tick()with this run and an earlier one in the event queue filemain: raised out oftick(). At round 1's local-only fix: both runs local-onlytest_delivery_resumes_once_the_saved_login_can_be_readtest_a_registered_run_loses_the_unreadable_login_reason_once_it_can_be_read: the login cannot be read when the exchanged token is renewedhuggingface_login_unreadabletest_a_collector_outage_after_the_login_can_be_read_is_retried_at_shutdown: the login can be read again, the exchange answers 503waiting_for_credentialstayed truetest_a_saved_login_that_cannot_be_read_raises_an_error_without_its_details(PermissionError,IsADirectoryError, anOSErrorfor a symbolic link loop,UnicodeDecodeError)OSErrorthat names a file, the twoOSErrorcases fail; with the error raised inside theexceptblock, all four fail (context kept)test_only_a_saved_login_that_cannot_be_read_counts_as_unreadable(Hub outage during the OIDC exchange, OIDC answer that is not text, Hub outage without OIDC)UnreadableLoginErrortelemetry-tokenexit codes, new case "OIDC token exchange that fails"test_a_malformed_collector_answer_is_retried_like_a_network_failure(registration or events, each with a truncated chunked answer or one that is not HTTP, from a loopback collector): runsTelemetryModule.tick()IncompleteReadandBadStatusLineraised out oftick()test_a_token_answer_without_a_usable_token_and_a_lifetime_is_unusable, new cases: a token with a space, a line break, a non-ASCII charactertest_a_collector_token_that_cannot_be_sent_is_retried_like_an_outage: runsTelemetryModule.tick()ValueErrorraised out oftick()Locally, at
8ea9c54:uv run --no-sync pytest -m "not collector": 830 passed.tools/check-quality.shandtools/check-changelog.shpass.🤖 Generated with Claude Code