Skip to content

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 into
mainfrom
claude/telemetry-unreadable-login-malformed-answer
Oct 29, 2026
Merged

anth-volk merged 18 commits into
mainfrom
claude/telemetry-unreadable-login-malformed-answer

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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() raises UnreadableLoginError when neither HF_TOKEN nor HUGGINGFACE_TOKEN is set and reading the login saved by hf auth login fails: a PermissionError, a UnicodeDecodeError (the file is not text), or another OSError that names a file, such as IsADirectoryError. The error keeps only the original error's type name. It is raised outside the except block, so it does not keep the original error as its context either: that error's text names the file, and a UnicodeDecodeError holds the file's bytes.
  • Every other error of huggingface_hub's get_token() propagates unchanged, as on main. While HF_OIDC_RESOURCE is set, huggingface_hub exchanges a CI OIDC token instead of reading the saved login, so none of its errors count: for example the HfHubHTTPError (an OSError that names no file) of a 503 from the Hub.
  • The runs stay pending. CollectorDelivery records the pending reason huggingface_login_unreadable for 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.
    • It reads the login again after the same backoff as after a collector outage (_defer_retry, 1 second doubling to 60).
    • Meanwhile waiting_for_credential is 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.
    • As soon as the session answers without 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.
    • A later emitter with a readable login delivers the runs too, and clears the reason when it registers each one.
  • telemetry-token catches 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_once would apply it to every pending run in the user's event queue file, including earlier builds' runs. On main those 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.

UnreadableLoginError is an OSError, so the other modules that read the login behave as on main: the Orrery module (which shares the telemetry module's session through shared_session) still catches it in except (OSError, ValueError, KeyError, TypeError), and the collector submission worker (which has its own session) still catches it in except Exception.

2. A malformed collector answer

core.auth.NETWORK_FAILURES (OSError, TimeoutError, http.client.HTTPException) lists what _http_post raises when it gets no usable answer. The token exchange, run registration and the event upload in flush_once all 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_credential applies _SENDABLE_CREDENTIAL to the collector's access_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-token then 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 with ValueError.

Documentation

  • README, "Delivery and persistence": how an unreadable saved login is handled, which other errors do not count as one, and that an answer that is not valid HTTP or ends early counts as a network failure.
  • README, telemetry-token exit codes: row 5 names the visible-ASCII rule for access_token, and row 7 says a failed OIDC token exchange exits 7 and why.
  • CollectorSession's docstring and the comment on TelemetryRunRecord.local_only_reason describe 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 --spool and #36: #36 was closed unmerged, so that command does not exist.

Behaviour that differs from main outside the three fixes

telemetry-token with HF_OIDC_RESOURCE set, when the OIDC exchange fails with an OSError such as HfHubHTTPError, now exits 7 ("an unexpected HfHubHTTPError occurred") instead of 3. On main its own except (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; on main, OIDCError and httpx errors already did.

Tests

Test Before its fix After
test_an_unreadable_saved_login_keeps_runs_pending_until_it_can_be_read (not readable by this user, not text): runs TelemetryModule.tick() with this run and an earlier one in the event queue file On main: raised out of tick(). At round 1's local-only fix: both runs local-only 2 passed
test_delivery_resumes_once_the_saved_login_can_be_read Failed: the run was local-only Passed
test_a_registered_run_loses_the_unreadable_login_reason_once_it_can_be_read: the login cannot be read when the exchanged token is renewed Failed at review round 2: the delivered run kept huggingface_login_unreadable Passed
test_a_collector_outage_after_the_login_can_be_read_is_retried_at_shutdown: the login can be read again, the exchange answers 503 Failed at review round 2: waiting_for_credential stayed true Passed
test_a_saved_login_that_cannot_be_read_raises_an_error_without_its_details (PermissionError, IsADirectoryError, an OSError for a symbolic link loop, UnicodeDecodeError) Existing behaviour. Without the branch for an OSError that names a file, the two OSError cases fail; with the error raised inside the except block, all four fail (context kept) 4 passed
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) 3 failed: each became UnreadableLoginError 3 passed
telemetry-token exit codes, new case "OIDC token exchange that fails" Exit 3 Exit 7
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): runs TelemetryModule.tick() 4 failed: IncompleteRead and BadStatusLine raised out of tick() 4 passed
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 character 3 failed: exit 0 instead of 5 Passed
test_a_collector_token_that_cannot_be_sent_is_retried_like_an_outage: runs TelemetryModule.tick() With the exchange's token check removed: ValueError raised out of tick() Passed

Locally, at 8ea9c54: uv run --no-sync pytest -m "not collector": 830 passed. tools/check-quality.sh and tools/check-changelog.sh pass.

🤖 Generated with Claude Code

anth-volk and others added 18 commits October 29, 2026 21:01
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
anth-volk marked this pull request as ready for review October 29, 2026 18:47
@anth-volk
anth-volk merged commit f97b8b9 into main Oct 29, 2026
18 of 20 checks passed
@anth-volk
anth-volk deleted the claude/telemetry-unreadable-login-malformed-answer branch October 29, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep the telemetry module running after an unreadable saved login or a malformed collector answer, and refuse a collector token with a space

1 participant