Repository navigation
Conversation
`microcosm-emitter status --spool <path>` must print each run's upload state, the reason it waits or is local-only, and its undelivered event count as JSON, with no network request, leaving the file byte for byte unchanged and creating nothing beside it, even when its events are older than the 7-day retention that opening a spool with `EventSpool` enforces. A file that is not a spool database exits 7. Fails: there is no `status` command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It opens the spool database in SQLite's read-only mode, refuses a file
without a known spool revision (exit 7), and prints
`{"runs": [{run_id, producer_id, upload_state, reason,
undelivered_events}]}`. It never opens the spool with `EventSpool`,
which migrates it and deletes events past the retention period, so
looking at a copy cannot lose the events it holds. It makes no network
request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`microcosm-emitter deliver --spool <path>` must deliver a copy whose run a build on Modal registered with a given token, from another directory, with the operator's own Hugging Face login of the same user: the collector accepts the second registration and stores every event. It must deliver events older than the 7-day retention, ignore a build token left in the shell, print the copy's status and exit 0. Without a login, with a refused login or with an unreachable collector, it must exit 3, 4 or 5 and leave the copy byte for byte unchanged, rather than make its runs local-only. The fake collector now takes telemetry: registrations owned by the token's Hugging Face user, events stored by ID, and a way to refuse every token issued so far. Fails: there is no `deliver` command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It exchanges this machine's Hugging Face login first, ignoring any collector token given to builds in the environment; without a login, with a refused one, or with an unreachable collector it exits 3, 4 or 5 before opening the spool, so the copy's runs never become local-only for want of a login. It then opens the spool without pruning (`EventSpool(..., prune_on_open=False)`), so an event leaves the copy only when the collector acknowledges it, however old it is, and delivers until nothing is pending or `--time-limit-seconds` (120 by default) passes (exit 5). It prints the copy's status as `status` does. The collector accepts the run again from the same Hugging Face user who registered it on the other machine, and the spool records no paths for telemetry, so a copy delivers from any directory. The message a build prints when its given token stops working now names this command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds `status --spool` and `deliver --spool` to the README section on builds without a Hugging Face credential, with their exit codes and why neither prunes the copy, and an `added` changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an operator other than the one who launched the build runs `deliver --spool` on the copy, the collector answers the run's registration with 403: the run belongs to the Hugging Face user who registered it. `deliver` must then leave the copy unchanged, so the run stays pending, name the run and say that another Hugging Face user owns it, and exit 8. The launching operator's later `deliver` on the same copy must still deliver every event. Fails: `deliver` makes the run local-only with `run_registration_rejected`, prints the build's organization-membership warning and exits 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collector answers 403 to a run's registration or events when another Hugging Face user registered the run. `deliver --spool` reused the build's handling, which makes the run local-only: run on the copy by an operator other than the one who launched the build, it excluded the run from upload for good, blamed organization membership and exited 0, and the launching operator's later `deliver` sent nothing. `CollectorDelivery` now routes both 403 answers through `_forbidden`, which a build still answers by making its run local-only, with the same reason and warning as before. `deliver_spool` uses a subclass that records the run instead, writes nothing to the copy, sends nothing more for that run, and stops once only such runs are pending. The constructor is unchanged. `deliver_spool` returns a `SpoolDelivery` (a `Literal` outcome and the refused runs) instead of a bare string. `deliver` prints the copy's status, names the refused runs and says that the operator who launched the build should deliver them, and exits 8, or 5 if the time limit also passed with other events pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`status --spool` and `deliver --spool` must both exit 7 for a file that is not a spool database, whether it holds other content or nothing, and leave it and its directory unchanged. `deliver` is run with a working login and collector, so the test reaches the code that opens the file. The status test's own non-spool check moves here. Fails: `deliver` exits 0 on a zero-byte file, having written the spool schema into it, and on other content exits 7 but leaves a migration lock file beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`deliver --spool` opened the file with `EventSpool`, whose migration refuses an unversioned database only when it already has tables. On a zero-byte or table-less file it wrote the spool schema, found nothing to deliver and exited 0, while `status` exited 7 on the same file; on other content it left a migration lock file beside it. `deliver_spool` now first reads the file in SQLite's read-only mode and checks its revision, as `spool_status` does, before the login exchange and before opening it for writing. Both share one read-only helper, and the mapping from database, migration and file errors to `UnreadableSpoolError` is written once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `deliver --spool` must leave the modes of the copy and its directory as they were (0755 and 0644 in the test). - Both commands must report a spool database this user may not read as a permission error, not as a file that is not a spool database. - `status`: every socket connection now fails the test, and the operator has a login, so an attempt to deliver would not stop for want of one before reaching the network. - `deliver` without a usable login: the copy is now a real copy in a directory of its own, whose listing and modes must stay unchanged. An unreachable collector writes nothing, so only the lock file that opening the copy for writing leaves would show that it was opened before the login was checked. Fails: `deliver` sets the directory to 0700 and the file to 0600, and both commands report an unreadable file as "not a spool database". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `EventSpool(path, as_copy=True)`, which replaces `prune_on_open=False`, opens an existing copy without creating it or changing its mode or its directory's: a build's own spool is still made private (0600 and 0700) and pruned on open. `deliver` uses it, so a `--spool` path in a shared or home directory keeps its permissions. - Both commands check this user's permissions first: `status` needs to read the file, and `deliver` also to write it and create files in its directory, for SQLite's rollback journal. SQLite reports a missing permission only as a file it cannot open, which `status` printed as "not a spool database". `UnreadableSpoolError` now says whether permission was denied, and the message says so (still exit 7). - The command-line module maps `UnreadableSpoolError` to exit 7 and its message in one place, `_spool_command`, for both commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's section on a copied spool now says that `deliver` checks the file before the login, changes no permissions, leaves the migration lock file beside the copy, leaves a run another Hugging Face user registered pending and exits 8 for it, and does not send runs already local-only. Exit 7 now also covers a file this user lacks permission for. The changelog fragment and `deliver`'s help say the same in brief. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-spool Brings in #34's round-one changes, up to 25d40c4. Conflicts: - README.md: #34's rewritten bullets on given tokens, followed by this branch's section on a copied spool. - src/microcosm_emitter/cli.py: both sides' imports, codes and messages. `telemetry-token` uses #34's `exchange_huggingface_credential` and its exit 7 for unexpected failures, with this branch's `--development-collector-url` argument and `CREDENTIAL_FAILURES` name. Both sides use exit 7 for different failures; the following commits give the spool errors their own code. #34 removed `CollectorSession`'s `environment` parameter and `exchange_telemetry_token`, which this branch used: - `deliver_spool` now builds `CollectorSession(origin)`. That session reads a given token from the environment, so `deliver` now uses a build's token left in the shell; the next commit stops that. The existing test fails: 1 failed, 24 passed in the two command test modules (`assert 5 == 8`). - The copied-spool tests obtain the build's token with `exchange_huggingface_credential`, and set the given-token variables with #34's `given_token_variables`, only while the build's session is created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#34 removed `CollectorSession`'s `environment` parameter, through which `deliver_spool` kept the session from reading MICROCOSM_COLLECTOR_TOKEN and its two companion variables. Since the merge, a build's token left in the operator's shell was sent instead of the operator's login: the run stayed pending with `collector_token_expired` until the time limit, and `deliver` exited 5. `CollectorSession` now takes `use_given_token`, true by default. With false, which only `deliver_spool` passes, it reads none of the three variables and exchanges this machine's Hugging Face credential. Before: 1 failed, 24 passed (`assert 5 == 8`). After: 25 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#34 merges first, and its `telemetry-token` exits 7 for any failure it does not name. `status --spool` and `deliver --spool` also exit 7 for a file they cannot use as a spool database, so the two meanings share a code. The spool file errors move to 9, the next code neither command uses. Fails: 6 failed, 5 passed (`assert 7 == 9`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The commands' exit codes are now members of one `ExitCode` table in `cli.py`, marked `@unique`, which refuses two members with one value when the module is imported. A new code therefore cannot take a meaning another command already gives it, as exit 7 did when #34's unexpected failures met this branch's spool file errors. The table also reserves 1 (the host's start failure, and Python's code for an uncaught exception) and 2 (argparse's usage error). `status --spool` and `deliver --spool` exit 9 for a file they cannot use as a spool database; 7 keeps #34's meaning, and `deliver` keeps 8 for a run another Hugging Face user registered. The README says so. Before: 6 failed, 5 passed (`assert 7 == 9`). After: 11 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`deliver_spool` declared its own `LoginFailure` literal for three of the session's answers, from before #34 typed them. `SpoolDelivery` now has the outcome `login_failed` and carries the session's answer in `login`, typed with #34's `SessionResult`. Any answer other than `ok` stops `deliver` before it opens the file, where only the three named answers did. `deliver` maps the answer to its exit code and message with the same `CREDENTIAL_FAILURES` table as `telemetry-token`. No behaviour changes for the answers a session without a given token or a scope gives: 25 passed in the two command test modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-spool Brings in #34's round 2 fixes up to ca37144. Two files conflicted; both sides are kept: - cli: #36's ExitCode table and messages, with #34's unsendable-credential entry (exit 4), unreadable-login message (exit 3) and ExchangeFailure check. CREDENTIAL_FAILURES is typed by the exchange's failure reasons and the session's answers, which telemetry-token and deliver look up. - README: #34's corrected paragraph on a token that stops working, followed by #36's section on a copied spool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deliver checks this machine's login once, before it opens the copy. When the collector then answers 401 and exchanging the login again is refused, the inherited CollectorDelivery makes the copy's run local-only, as a build would, and deliver exits 0. The run must stay pending, and deliver exit 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_CopiedSpoolDelivery now answers the session itself instead of inheriting the build's handling: for any answer other than ok or unavailable, it records why, sends nothing more and leaves every run not yet delivered pending. deliver_spool returns login_failed with that reason, and deliver exits with its code (4 for a refused login) and says the runs stay pending. SpoolDelivery.login becomes login_failure, typed by the three failures deliver reports (LoginFailure); any other session answer, which its session does not give, counts as unavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#34 makes a Hugging Face credential that an HTTP header cannot carry count as refused, and never sends it. deliver gets that answer from its session as rejected, so it exits 4 but says the collector refused the credential, which never reached the collector. It should print telemetry-token's message for an unsendable credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CollectorSession now answers unsendable for a Hugging Face credential that an HTTP header cannot carry, instead of rejected. The telemetry delivery and collector submissions treat it exactly as rejected, as before. deliver reports it as its own login failure, so it exits 4 with the message telemetry-token prints: the credential was not sent, and why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Today an error neither command names escapes cli.main, so Python exits 1 with a traceback, whose text can quote a credential in deliver. Both must exit 7 and print only the error's type, as telemetry-token does, leaving 1 to a host that could not start. The login-failure test also checks that every failure deliver_spool reports has an exit code and a message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An error that status or deliver does not name now prints one line with its type, saying for deliver that unacknowledged events stay in the copy, and exits 7, as telemetry-token does, instead of a traceback and Python's exit 1. The exit-code table now leaves 1 to a host that could not start, lists an unreadable saved login under 3 and an unsendable credential under 4, and names all three commands under 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An emitter stopped during a commit leaves a journal beside the spool database that SQLite must roll back before anyone reads it, which a read-only connection cannot do. Today both commands exit 9 and call the file not a spool database: status must say what is wrong and still change nothing, and deliver must recover and deliver the copy, as it did before its read-only check, without writing to it before the check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the read-only check fails because a journal beside the copy holds a write that did not finish (SQLITE_READONLY_ROLLBACK), deliver_spool copies the file and its journal into a temporary directory only this user may open, lets SQLite roll the write back there, and checks the revision of the result. Only if that passes, and the login works, does it open the file itself, which rolls the same write back, and deliver it. The file is never written before then. status still never writes: it exits 9 and says that the journal holds a write that did not finish, rather than that the file is not a spool database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llector Nothing covered the time limit. With every telemetry request answered 503 and --time-limit-seconds 1, deliver exits 5 with the run's two events pending and prints the status JSON. The limit bounds the whole delivery, so its message must name the limit rather than always blame an unavailable collector, as it does today. The fake collector can now answer every telemetry request with a chosen status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The time limit's help, its message and the exit-code table said it applied while the collector was unavailable, but it bounds the whole delivery. The message now names the limit and both causes, and the help gives the default. The table's exit 8 also no longer says every other run was delivered: one can instead be excluded for a 409 or another permanent 4xx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's copied-spool section now covers a copy left with an unfinished write, a login that fails partway, an unsendable login, exit 7 for unexpected errors and the time limit bounding the whole delivery, and says exit 1 is only for a host that could not start. The changelog fragment names the same behaviour. One CollectorSession docstring is rewrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lite3 The repository's boundary test refuses an import of the SQLite driver in the emitter's source, which 3fa23f4 added for SQLITE_READONLY_ROLLBACK's number. UnreadableSpoolError now compares the driver exception's sqlite_errorname with that code's name instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-spool Brings #34 up to a6cf212: the unsendable credential's own telemetry warning, the corrected wording for an unsendable credential, and the tests for the submissions refusal and ExchangedToken's repr. Conflicts: - core/auth.py: both sides added the session answer `unsendable`; kept #34's comment, which says how each caller handles it, including Orrery's credential_unsendable. - cli.py: kept #36's ExitCode table and CREDENTIAL_FAILURES, with #34's wording for the unsendable message and the comment above it. Also resolved: - telemetry/service/collector.py merged without a conflict but with #36's `result in ("rejected", "unsendable")` ahead of #34's own `unsendable` branch, so the new warning would never print; the rejected branch is `result == "rejected"` again. - test_copied_spool checks the unsendable message's new wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With neither HF_TOKEN nor HUGGINGFACE_TOKEN set and a saved Hugging Face login this user cannot read, deliver must exit 3 and say the saved login could not be read, as telemetry-token does, and leave the copy unchanged. It exits 7, reporting an unexpected PermissionError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deliver's session now reads this machine's Hugging Face login through _this_machines_login, which turns the OSError or UnicodeDecodeError that huggingface_hub raises for a saved login it cannot read into UnreadableLoginError, carrying only the error's type. deliver catches it, before it opens the copy or partway through delivery, prints the message telemetry-token prints for the same case and that runs not yet delivered stay pending, and exits 3, as the ExitCode table documents. The copy is not changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… refused A copy with a revision this emitter does not know, and a journal beside it holding an unfinished write: deliver rolls the write back only in its temporary copy, refuses the result with exit 9, and leaves the database and its journal byte for byte as they were. The temporary directory is deleted. It passes; without the check of the recovered copy, deliver rolls back the real file and deletes its journal, and the test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… login The Orrery module records the session's answer as credential_<answer>, so a graph job whose Hugging Face credential cannot be sent as a bearer token waits with credential_unsendable, not credential_rejected. A comment where the code is recorded now lists the credential codes, beside the session comment, README and changelog that #34 updated. The README and changelog also say that deliver stops, leaving the copy unchanged, for a saved login it cannot read (exit 3), and the remaining comments that said a space is something "an HTTP header cannot carry" now say the credential contains characters that cannot be sent as a bearer token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 29, 2026 16:11
Contributor
Author
|
Closing without merging. We decided not to deliver a Modal build's leftover telemetry after the build: that needs a collector outage at the end of the build or a rejected token, since the token outlasts the stage, and recovering it took more steps than late telemetry for a finished build is worth. |
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 #35
Stacked on #34. The base is
claude/modal-telemetry-token; GitHub retargets this PR tomainonce #34 merges. This branch contains #34 up to a6cf212, through the merge commits 85ee23d, 98dcbab and c99c53e (see "Merges of #34"). This repository's pull request workflow runs for every pull request, so CI runs here too: all 10 checks pass on 132be00.Summary
A build on Modal leaves undelivered events in its spool database when its given collector token expires, is refused, or the collector is unavailable (#34 keeps those runs
pending). The Modal runner will copy that spool to the runs volume. This PR adds the two commands that report on and deliver such a copy on the operator's workstation.microcosm-emitter status --spool <path>prints{"runs": [{"run_id", "producer_id", "upload_state", "reason", "undelivered_events"}]}, sorted by run and producer.reasonis the run'slocal_only_reasoncolumn: why alocal_onlyrun is excluded, or why apendingone waits (collector_token_expired,collector_token_rejected). It makes no network request and does not change the file (see "Pruning").microcosm-emitter deliver --spool <path> [--time-limit-seconds N]delivers the copy's pending runs with this machine's own Hugging Face login, then prints the same JSON and exits.statusdoes: this user's permissions, then, in SQLite's read-only mode, that it is a spool database of a revision this emitter knows (exit 9 otherwise, file unchanged). It then exchanges the login, and never uses a collector token given to builds (CollectorSession(origin, use_given_token=False)). Without a login, with a saved login it cannot read, with a refused or unsendable login or with an unreachable collector, it exits before opening the file for writing. Otherwise the existing delivery code would make every run in the copy permanentlylocal_onlywithmissing_huggingface_credentialorhuggingface_credential_rejected.deliversends nothing more, leaves every run not yet delivered pending, and exits 3 or 4._CopiedSpoolDeliveryanswers the session itself for this, instead of inheriting the build's handling, which made those runslocal_onlyand exited 0.deliverchecks a recovered copy first, then rolls the write back and delivers;statusexits 9 and says why.delivercan also be repeated. The test drives this end to end: a token fromexchange_huggingface_credential, a container spool that registers the run and delivers one event before the collector refuses the token, then a copy in another directory delivered bydeliver.deliver, that run stays pending in the copy, unchanged, so the launching operator can still deliver it.delivernames the run on standard error, says that the operator who launched the build should deliver it, and exits 8. The same test runsdeliverwith another operator's login first, then with the launching operator's, which delivers every event.local_only, as in a build. Open Keep one build's telemetry login from deciding other builds' runs #5 to Say what the collector refused, fit requests to its limit, register a lost run again #7 change the build's handling of collector refusals. Runs alreadylocal_onlyin the copy are not sent.EventSpool(path, as_copy=True), which neither creates the file nor sets the 0600 and 0700 modes that a build's own spool gets. Opening it runs the spool migrations, which bring a copy an older emitter wrote up to date and leave a lock file beside it (events.migration.lock).deliver --spool.Exit codes
Every command's exit codes are now one table,
ExitCodeincli.py, marked@unique: importing the module fails if two members share a value, so a new code cannot take a meaning another command already gives it. The table across this PR and #34:telemetry-token: the token is printed.status: printed.deliver: nothing it may deliver is pendinghosttelemetry-token,statusanddeliverexit 7, not Python's 1, for an unexpected error--spoolthat is not an existing filetelemetry-token,deliverdeliver: the copy is unchanged, or, when the login is gone at an exchange partway through delivery, the runs not yet delivered stay pendingtelemetry-token,deliverdeliver: as for 3telemetry-token,deliverdeliver, also: the time limit, which bounds the whole delivery, passed with events still pendingtelemetry-tokenlifetime_seconds(#34)telemetry-token,status,deliverdeliverstatus,deliverstatusneeds to read it;deliveralso to write it and to create files in its directory), or it is not a spool database, or one with a revision this emitter does not know (written by a newer release).statusalso: the journal beside the copy holds a write that did not finish. The message says whichWhen the time limit passes and a run belongs to another user,
deliverexits 5 and prints both messages. A login failure partway takes precedence over both:deliverprints the login's message and that the runs stay pending, and exits 3 or 4.deliver_spoolreports a login failure asLoginFailure(missing,unsendable,rejectedorunavailable), and maps any other session answer, which its session cannot give, tounavailable;CREDENTIAL_FAILUREShas a code and a message for each, and a test checks that. A saved login that huggingface_hub cannot read (OSErrororUnicodeDecodeError, with neitherHF_TOKENnorHUGGINGFACE_TOKENset) raisesUnreadableLoginError, at the start or partway;deliverprints the messagetelemetry-tokenprints for it, with only the error's type, and exits 3.Merges of #34
85ee23d merges #34's branch at 25d40c4 with a merge commit. Conflicts:
cli.py: both sides' imports, codes and messages.telemetry-tokenkeeps Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34'sexchange_huggingface_credentialcall and its exit 7 for unexpected failures, with this PR's--development-collector-urlargument and itsCREDENTIAL_FAILUREStable.The commits after it:
CollectorSession'senvironmentparameter, through whichdeliverignored a given token. After the merge,deliversent a build's token left in the operator's shell instead of the login.CollectorSessionnow takesuse_given_token(true by default);deliverpasses false.ExitCodetable above.SpoolDelivery.logintook Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34'sSessionResult. Round 2 replaced that withlogin_failure, typed by the four failuresdeliverreports (536a49b, b32cbf3). 85ee23d, not 30afcac, adapted the tests to Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34: they obtain the build's token withexchange_huggingface_credentialand set the given-token variables with Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34'sgiven_token_variables.98dcbab merges #34's branch at ca37144 (#34's round 2 fixes) with a merge commit. Conflicts:
cli.py: this PR'sExitCodetable and messages, with Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34's unsendable-credential entry (exit 4), unreadable-login message (exit 3) andExchangeFailurecheck.CREDENTIAL_FAILURESis typed by the exchange's failure reasons and the session's answers, whichtelemetry-tokenanddeliverlook up.After it, b32cbf3 makes the session answer
unsendablerather thanrejectedfor a credential that cannot be sent as a bearer token, so thatdeliverprints #34's message for it rather than saying the collector refused it.c99c53e merges #34's branch at a6cf212 (#34's round 3 fixes) with a merge commit. #34 now makes the session answer
unsendableitself, so that the telemetry module can print its own warning for it. Conflicts:core/auth.py: both sides addedunsendableto the session's answers; Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34's comment is kept, as it says how each caller handles it.cli.py: this PR'sExitCodetable andCREDENTIAL_FAILURES, with Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given #34's corrected wording for the unsendable message and the comment above it.Also resolved in the merge:
telemetry/service/collector.pymerged without a conflict, but with this PR'sresult in ("rejected", "unsendable")ahead of #34's newunsendablebranch, which would have hidden #34's warning; therejectedbranch matches onlyrejectedagain.test_copied_spoolchecks the unsendable message's new wording.Orrery's receipts. The Orrery module records the session's answer as
credential_<answer>, so a graph job whose Hugging Face credential cannot be sent recordscredential_unsendablerather thancredential_rejected, and is retried with backoff, as before. The telemetry module still makes the runlocal_onlywithhuggingface_credential_rejected, now with its own warning, and collector submissions still endfailedwithCREDENTIAL_REJECTED. The session's comment, the README and the changelog (from #34), and a comment where the Orrery queue records the code (132be00), say so.Relation to #5
Open #5 states the same rule for builds that share one spool: an answer about the caller's credential (no credential, a refused exchange, registration 403, events 403) must not make another producer's run
local_only. It does this by changingCollectorDelivery's constructor to take the build's own registration and hold a lease for it, and by adding acaller_dependentargument to_make_local_only.This PR does not change the constructor.
CollectorDeliverynow sends both 403 answers through one method,_forbidden, which a build still answers by making its runlocal_only, with the same reason and warning as before.deliver_spooluses a subclass whose_forbiddenrecords the run instead, writes nothing, and sends nothing more for it, and whose_collector_tokenstops delivery, writing nothing, when this login is missing or refused. When #5 is brought up to date withmain, itscaller_dependent=Truecalls for the two 403 answers go inside_forbidden, anddeliver_spoolmust decide which registration it passes as its own, since every run in a copy is another process's.Pruning: what
EventSpooldoes to a copy, and what I decidedEventSpool.__init__runs the spool migrations and thenprune(). That deletes events older than 7 days (by the time they were queued in the container), then the oldest events beyond 100 MiB, then runs older than 7 days that have no events left. Opening a copy that way 8 days after the build would delete exactly the events it was copied to keep, before anything is sent.statusnever opens the copy withEventSpool. It opens it in SQLite's read-only mode (core.database.create_read_only_spool_engine), checks that its revision is one this emitter knows, and reads the runs through the ORM. Nothing is migrated, pruned, chmod-ed or created beside it. The test checks the file's SHA-256 and the directory listing are unchanged, with events aged past 7 days, while every socket connection fails the test and a login is set.deliveropens the copy withEventSpool(path, as_copy=True), and never callsprune(). An event leaves the copy only when the collector acknowledges it, however old it is. It still migrates an older revision forward, as any emitter would. The test ages the copy's events to 8 days and checks all of them reach the collector, and that the copy and its directory keep modes 0644 and 0755.A copy left with an unfinished write
An emitter stopped during a commit leaves a journal beside the spool database (
events.sqlite3-journal) holding the write that did not finish. SQLite rolls it back the first time a connection that may write reads the file; a read-only connection refuses the file instead (SQLITE_READONLY_ROLLBACK). Round 1's read-only check therefore madedeliverrefuse such a copy with exit 9, where it had recovered it before.deliver: when the read-only check fails for that reason, it copies the file and its journal into a temporary directory that only this user may open, lets SQLite roll the write back there, and checks the revision of the result. Only when that passes, and the login works, does it open the copy itself, which rolls back the same write, and deliver it. The copy is never written before then, and the temporary files are deleted. I chose this over opening the copy read-write at once (SQLite'smode=rw), which would roll the write back before anything had shown the file to be a spool database. It costs one extra copy of the file, and only for such a copy.statusstill never writes, and does not recover the write elsewhere either. It exits 9 and says that the journal holds a write that did not finish, thatstatuschanges nothing, and thatdeliverrecovers it; before, it said the file was not a spool database.sqlite_errorname; the emitter's source imports no database driver (tests/test_boundaries.py).emitter#27's submission rows on Modal
The tracker asked whether emitter#27's collector submission rows and their status checks would be lost when a Modal container stops. They can't exist there, so this PR builds nothing for them:
stage_dataset_bundlehand-over, and only when the module configuration setscollector_submissionstotrue(COLLECTOR_SUBMISSIONS_ENABLED_BY_DEFAULTisFalse).HuggingFaceSettings.configuration()returns onlyhandover_database_path, and Microcosm starts the emitter only through the interface (microcosm#1410'stelemetry_emitter.pypassesHuggingFaceSettings()).maincallsstage_dataset_bundlenowhere yet. Under the decision that Modal stays compute-only, a Modal run's bundle is staged and submitted from the workstation after its outputs are copied back (tracker 6.2 · 3), with the operator's login.artifacts:submithas no credential, so even a configuration that switched submissions on could not submit from Modal.deliveralso leaves graph publication jobs in the copied spool alone, and the hand-over database beside it. Their rows name files by container paths, and staging copied-back outputs is tracker 6.2 · 3.Not in this PR: runs already
local_onlyfor a missing credentialA container that has neither a given token nor a Hugging Face credential, for example a build that opts out of obtaining a token, makes its runs
local_onlywithmissing_huggingface_credentialbefore any collector request.deliverdoes not send such runs, and exits 0 if nothing else is pending. Whetherdelivershould return those runs to pending is decided together with Microcosm's runner PR (tracker 6.4 · 3), which decides whether that opt-out exists. Nothing is built for it here.Environment variables
No new environment variables.
deliverdeliberately ignores #34'sMICROCOSM_COLLECTOR_TOKEN,MICROCOSM_COLLECTOR_TOKEN_EXPIRES_ATandMICROCOSM_COLLECTOR_ORIGIN(use_given_token=False), so a build token left in the operator's shell is never used; the test sets them to an expired token to check this. It reads the operator's Hugging Face credential as every emitter does (HF_TOKEN, thenHUGGINGFACE_TOKEN, thenhf auth login's file). No variable chooses where it sends: the origin isPRODUCTION_COLLECTOR_URL, or a loopback--development-collector-url.Not verified
storage.register_runonmain, not exercised against a deployment: a second registration of the same run by the same user with the same metadata gets 201, and one by another user gets 403. The fake collector intests/support/fake_collector.pyfollows that rule._forbiddenmethod as a registration 403, but no test reaches it. Once the collector has accepted this login's registration of a run, the login owns the run and the producer is registered, so neither of the collector's reasons for an events 403 applies.deliver's exit 3 for a saved login that becomes unreadable partway through delivery is not tested; it raises the sameUnreadableLoginErroras at the start, from the session's credential read.os.access. Run as root, it permits every file, so the permission test assumes a non-root user, as on GitHub's hosted runners.Tests
Red, then green (Python 3.14, macOS, locally):
status --spoolreads without changes or networkinvalid choice: 'status')deliver --spooldelivers a copy from another machine with the operator's login, without pruning; a missing or refused login leaves it unchangedinvalid choice: 'deliver')deliverby another operator leaves the run pending, unchanged, and exits 8; the launching operator then delivers itassert 0 == 8: the run becamelocal_onlywithrun_registration_rejected, with the organization-membership warning)deliverexited 0 on the zero-byte file; on other content it left a migration lock file beside it)deliverkeeps the copy's modes; both commands report an unreadable spool database as a permission errordeliverignores a given token left in the shell (two command test modules)assert 5 == 8: the session counted the given token as expired, 30 s early, and answeredtoken_expiredwithout sending it; the run was kept pending withcollector_token_expireduntil the time limit)telemetry-token's unexpected failures (7)assert 7 == 9)deliverexits 4assert 0 == 4: the run becamelocal_onlywithhuggingface_credential_rejected)delivernames an unsendable login as such, and exits 4statusanddeliverexit 7 for an unexpected error and print only its type; every login failure has a code and a messageValueErrorescapedcli.main)statusexits 9, says so and changes nothing;deliverchecks a recovered copy without writing to the copy, then delivers itstatussaid "not a spool database";deliveralso exited 9, checked by hand)test_boundaries)import sqlite3)deliverexits 3 for a saved login it cannot read, and leaves the copy unchangedassert 7 == 3: it reported an unexpectedPermissionError)a1ecf7d also strengthens two assertions that passed already.
statusnow runs with a login and with every socket connection failing the test.deliverwithout a usable login now checks that the copy's directory listing and modes are unchanged, which would show the lock file left by opening the copy before the login is checked; an unreachable collector writes nothing, so the digest alone could not. With temporary edits, reverted, both assertions failed for the defect they target.30afcac changes no behaviour: the two command test modules pass (25 passed). 0f3fbe2 also corrects
--time-limit-seconds's help, which said the limit applied only while the collector was unavailable, and the exit-8 description. 8038401 updates the README and the changelog fragment. In round 3, c34ae02 tests thatdeliverleaves a copy and its journal byte for byte unchanged, and deletes its temporary directory, when the recovered copy fails its check (a revision this emitter does not know); it held already, and fails with the check of the recovered copy removed. 132be00 documentscredential_unsendableand the exit 3, and corrects the remaining wording for an unsendable login.At the head of this branch (132be00), locally:
uv run --no-sync pytest -m "not collector"passed in full (828 passed, 9 deselected), andtools/check-quality.sh,tools/check-changelog.shandtools/check-artifacts.shpassed. The collector job (tools/check-collector.sh, a pinned collector with PostgreSQL) was not run locally.Follow-up
#37 tracks problems older than this PR and #34. One reaches
deliver: a malformed collector answer during delivery (http.client.HTTPException) ends it with exit 7.Release
The fragment is
added. If #34 merges first and releases 0.11.0, merging this releases 0.12.0 of both distributions to PyPI.🤖 Generated with Claude Code