Repository navigation
Submit staged bundles' calibration diagnostics and Orrery inputs to the collector - #27
Merged
Merged
Conversation
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 13:00
8bfcbdd to
14c0240
Compare
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
2 times, most recently
from
October 20, 2026 13:33
5948c03 to
b7c7422
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 17:47
28dcec2 to
19cac1b
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 20, 2026
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 18:47
19cac1b to
e463794
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that `close` waits for every worker, the collector worker included, before the second shutdown commit. When a collector upload is still running at shutdown, that wait lasts until the deadline and the second commit is skipped, so the run files of the repository a bundle upload held are not committed although that upload finished in time. On Modal they are lost with the container. The test follows the review's case: a described bundle's collector upload is held, a bundle upload to the staging repository is held for half a second, run files for the staging repository are handed over, and the module closes with a three-second deadline. The staging run files must land. Red: 1 failed, 21 passed (`AssertionError: the staging run files missed the second shutdown commit`), in two runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that every upload stops after `upload_time_limit(size)`, which assumes at least 2.1 Mbit/s: a large file on a slower link (399 MiB at 1.5 Mbit/s, for example) is stopped on every attempt, starts again from zero, and is never submitted. The fake collector can now give each connection a small receive buffer and read an upload's body slowly, and the client tests use a client with a small send buffer, so the client's sends wait for the fake to read. Two tests on a 2.5 MiB file: - the fake reads 64 KiB every 20 ms (about a second in all), each send makes progress well within a 1-second stall threshold, and any limit on the upload's total time derived from the file's size is set as short as the client allows (`monkeypatch.setattr(..., raising=False)`, so the test still runs once such a limit is gone): the upload must complete; - the fake reads nothing: the upload must fail with `UPLOAD_FAILED` after the 0.5-second stall threshold. This one passes today, because the per-send write timeout already detects a stall. Red: 1 failed, 41 passed (`AssertionError: an upload that kept making progress was stopped`; the outcome was `('pending', 'UPLOAD_FAILED')` after about 0.4 s, at the second 1 MiB piece), in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 20:01
e463794 to
423139b
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that `close` waits for every worker, the collector worker included, before the second shutdown commit. When a collector upload is still running at shutdown, that wait lasts until the deadline and the second commit is skipped, so the run files of the repository a bundle upload held are not committed although that upload finished in time. On Modal they are lost with the container. The test follows the review's case: a described bundle's collector upload is held, a bundle upload to the staging repository is held for half a second, run files for the staging repository are handed over, and the module closes with a three-second deadline. The staging run files must land. Red: 1 failed, 21 passed (`AssertionError: the staging run files missed the second shutdown commit`), in two runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that every upload stops after `upload_time_limit(size)`, which assumes at least 2.1 Mbit/s: a large file on a slower link (399 MiB at 1.5 Mbit/s, for example) is stopped on every attempt, starts again from zero, and is never submitted. The fake collector can now give each connection a small receive buffer and read an upload's body slowly, and the client tests use a client with a small send buffer, so the client's sends wait for the fake to read. Two tests on a 2.5 MiB file: - the fake reads 64 KiB every 20 ms (about a second in all), each send makes progress well within a 1-second stall threshold, and any limit on the upload's total time derived from the file's size is set as short as the client allows (`monkeypatch.setattr(..., raising=False)`, so the test still runs once such a limit is gone): the upload must complete; - the fake reads nothing: the upload must fail with `UPLOAD_FAILED` after the 0.5-second stall threshold. This one passes today, because the per-send write timeout already detects a stall. Red: 1 failed, 41 passed (`AssertionError: an upload that kept making progress was stopped`; the outcome was `('pending', 'UPLOAD_FAILED')` after about 0.4 s, at the second 1 MiB piece), in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The round-2 review of #27 (NEW BUGS 1) found that over HTTPS the upload's 60-second write timeout does not measure progress. CPython's `ssl` does no partial writes, and its write timeout covers each whole call, so the timeout covers each whole 1 MiB piece the client sends. A link slower than about 140 kbit/s therefore fails partway and starts again from zero, though the upload keeps making progress. The fake collector gains `tls_uploads`: it signs upload URLs for a second loopback server that speaks TLS, as Cloud Storage does, while the collector stays plain HTTP. The server has no certificate: both sides use only anonymous TLS 1.2 cipher suites, so no key material is generated or committed. The new test reads uploads at about 1 MiB/s, sets the stall threshold to 0.5 s and uploads 1.5 MiB: the link keeps making progress, but a 1 MiB piece takes about a second to write. Red: 1 failed, 42 passed, the same in three runs. The failure is the stated reason: `an HTTPS upload that kept making progress was stopped` (`('pending', 'UPLOAD_FAILED')`). With the threshold raised to 5 s the same test passes on this code, so the TLS connection works and the failure is the write timeout on the first piece. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The round-2 review of #27 (NEW BUGS 2) found that the collector row's one-hour lease is never renewed during an attempt. Every emitter of a user on one machine shares the hand-over database by default, so once an attempt outlasts the lease, a second emitter claims the row and uploads the same file in parallel, and the first attempt's result is dropped. The new tests shorten the lease to 1 s (and a renewal interval, which the code before the fix does not have, to 0.2 s), and run a second collector delivery on the same hand-over database with a client that records each submission and sends nothing: - while the first emitter's upload is held for two and a half leases, the second emitter must claim nothing; once released, the first emitter's result must be recorded; - a guard: after shutdown abandons the held upload, the second emitter must be able to claim the row within three leases. It passes before the fix (nothing renews the lease) and must keep passing after it, so renewal has to stop at shutdown. `described_bundle_with_a_held_upload` now returns the hand-over it stages, so a test can read its receipt. Red: 1 failed, 24 passed. The failure is the stated reason: `a second emitter claimed the collector row while its upload was running`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An error queueing a bundle's collector row is printed once (its type only), and the bundle's answer is its Hugging Face receipt, which is already queued. - Collector settings the module cannot use (a non-loopback development URL today) switch collector submissions off with one warning; the Hugging Face module starts as usual. - An unexpected exception during a submission is recorded as a pending attempt (LOCAL_ERROR) that keeps earlier submissions' results, so the row is retried after the usual backoff instead of staying leased for an hour. Green: 21 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #27 fix review found that `close` waits for every worker, the collector worker included, before the second shutdown commit. When a collector upload is still running at shutdown, that wait lasts until the deadline and the second commit is skipped, so the run files of the repository a bundle upload held are not committed although that upload finished in time. On Modal they are lost with the container. The test follows the review's case: a described bundle's collector upload is held, a bundle upload to the staging repository is held for half a second, run files for the staging repository are handed over, and the module closes with a three-second deadline. The staging run files must land. Red: 1 failed, 21 passed (`AssertionError: the staging run files missed the second shutdown commit`), in two runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`close` now waits for the collector worker separately: 1. it stops every worker; the collector cycle now receives the stop check and claims no further hand-over once shutdown begins; 2. it makes the first shutdown commit, waits until the deadline for the Hugging Face workers only (bundles and release changes), and makes the second shutdown commit when they stopped in time; 3. only then does it wait for a collector submission in progress, for at most `COLLECTOR_SHUTDOWN_WAIT_SECONDS` (2 s) and never past the deadline. Collector work therefore never uses up the time the Hugging Face shutdown commits need, and a busy collector worker no longer holds the process for the whole shutdown period. A submission still running is abandoned with the process, as before; its row is retried once its lease expires. The HTTP client and the queue stay open while any worker still runs. Two tests come with the fix: the bounded collector wait, and a collector cycle that claims nothing once stop is requested. The README's shutdown sentence for collector submissions now says this. Green: 24 passed (`tests/submissions/test_delivery.py`); every test file this branch adds or changes: 159 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #27 fix review found that every upload stops after `upload_time_limit(size)`, which assumes at least 2.1 Mbit/s: a large file on a slower link (399 MiB at 1.5 Mbit/s, for example) is stopped on every attempt, starts again from zero, and is never submitted. The fake collector can now give each connection a small receive buffer and read an upload's body slowly, and the client tests use a client with a small send buffer, so the client's sends wait for the fake to read. Two tests on a 2.5 MiB file: - the fake reads 64 KiB every 20 ms (about a second in all), each send makes progress well within a 1-second stall threshold, and any limit on the upload's total time derived from the file's size is set as short as the client allows (`monkeypatch.setattr(..., raising=False)`, so the test still runs once such a limit is gone): the upload must complete; - the fake reads nothing: the upload must fail with `UPLOAD_FAILED` after the 0.5-second stall threshold. This one passes today, because the per-send write timeout already detects a stall. Red: 1 failed, 41 passed (`AssertionError: an upload that kept making progress was stopped`; the outcome was `('pending', 'UPLOAD_FAILED')` after about 0.4 s, at the second 1 MiB piece), in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A signed upload is one `PUT` that stores nothing unless it completes, so a limit on its total time only makes a slow upload start again from zero. The client no longer has one: `upload_time_limit`, `chunks_until`, `UploadTimeLimitError`, `MIN_UPLOAD_SECONDS` and `MIN_UPLOAD_BYTES_PER_SECOND` are gone. Stalls are still detected by `UPLOAD_TIMEOUT`'s write timeout, which httpcore applies to each send to the connection: a send that has had no byte accepted for 60 seconds fails, and the attempt stays `pending` with `UPLOAD_FAILED`. Connecting and waiting for the answer keep their 60-second limits. Files are still sent in 1 MiB pieces with an explicit `Content-Length`. The collector cycle's ten-minute budget already only stops new submissions from starting; its comment now says it never stops one in progress. The lease comment now says an attempt can outlast its one-hour lease, which is not renewed during an attempt. `test_an_upload_stops_once_it_outlasts_its_time_limit` tested the removed functions and is removed. The README and the changelog fragment describe stall detection instead of the size-based limit. Green: 42 passed (`tests/submissions/test_client.py`); every test file this branch adds or changes: 160 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #27 (NEW BUGS 1) found that over HTTPS the upload's 60-second write timeout does not measure progress. CPython's `ssl` does no partial writes, and its write timeout covers each whole call, so the timeout covers each whole 1 MiB piece the client sends. A link slower than about 140 kbit/s therefore fails partway and starts again from zero, though the upload keeps making progress. The fake collector gains `tls_uploads`: it signs upload URLs for a second loopback server that speaks TLS, as Cloud Storage does, while the collector stays plain HTTP. The server has no certificate: both sides use only anonymous TLS 1.2 cipher suites, so no key material is generated or committed. The new test reads uploads at about 1 MiB/s, sets the stall threshold to 0.5 s and uploads 1.5 MiB: the link keeps making progress, but a 1 MiB piece takes about a second to write. Red: 1 failed, 42 passed, the same in three runs. The failure is the stated reason: `an HTTPS upload that kept making progress was stopped` (`('pending', 'UPLOAD_FAILED')`). With the threshold raised to 5 s the same test passes on this code, so the TLS connection works and the failure is the write timeout on the first piece. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`UPLOAD_CHUNK_BYTES` drops from 1 MiB to 64 KiB, the size httpx itself reads files in. Each piece is one send under the 60-second write timeout. Over HTTPS, where CPython's `ssl` writes a piece whole under one timeout, the slowest link that never fails now averages 65,536 bytes per 60 seconds over each piece: 1,093 bytes a second, about 8.7 kbit/s, instead of about 140 kbit/s. Over plain HTTP nothing changes: a send still fails only after 60 seconds in which no byte was accepted. The comment at `UPLOAD_TIMEOUT`, the cycle budget's comment, the README and the changelog fragment now state that exact rule and rate instead of "an upload that keeps making progress is never stopped, however slow the link". One comment in `test_client.py` that named the old 1 MiB piece size now describes the file size without it; the new test is unchanged. Green: 43 passed, the same in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #27 (NEW BUGS 2) found that the collector row's one-hour lease is never renewed during an attempt. Every emitter of a user on one machine shares the hand-over database by default, so once an attempt outlasts the lease, a second emitter claims the row and uploads the same file in parallel, and the first attempt's result is dropped. The new tests shorten the lease to 1 s (and a renewal interval, which the code before the fix does not have, to 0.2 s), and run a second collector delivery on the same hand-over database with a client that records each submission and sends nothing: - while the first emitter's upload is held for two and a half leases, the second emitter must claim nothing; once released, the first emitter's result must be recorded; - a guard: after shutdown abandons the held upload, the second emitter must be able to claim the row within three leases. It passes before the fix (nothing renews the lease) and must keep passing after it, so renewal has to stop at shutdown. `described_bundle_with_a_held_upload` now returns the hand-over it stages, so a test can read its receipt. Red: 1 failed, 24 passed. The failure is the stated reason: `a second emitter claimed the collector row while its upload was running`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A collector row's lease drops from a fixed hour to 120 seconds (`SUBMISSION_LEASE_SECONDS`) and is renewed every 30 seconds (`SUBMISSION_LEASE_RENEWAL_SECONDS`) while an attempt runs, through the same `HeldLeases` mechanism the Hugging Face jobs use. `HeldLeases` gains `for_job`, which holds a job without a repository lease, and an optional stop check: renewal ends at `stop` or once shutdown has begun. So: - another emitter on the machine, which shares the hand-over database, never claims the row while an upload runs, however long it takes; - the attempt finishes with the renewed lease value, so its result is recorded instead of dropped; - an attempt abandoned at shutdown, or an emitter that was killed, frees the row within two minutes instead of an hour. `_attempt` now returns an `AttemptResult`, and `run_cycle` stops the renewal before it records that result. The Hugging Face leases are unchanged: they are built without the stop check. The lease comment, the README and the changelog fragment describe the renewed two-minute lease instead of the fixed hour. Green: 25 passed, the same in three runs; `test_leases.py`, `test_delivery_workers.py` and `test_client.py`: 54 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #27 (CODE QUALITY 3) found that `submit` fetches the collector token once, before the uploads, and completes the submission with it afterwards. An upload longer than the token's life (an hour) therefore gets 401 at completion and waits for the next attempt, with a misleading `COLLECTOR_UNAVAILABLE` receipt meanwhile. The fake collector now issues tokens with a lifetime the test chooses (`token_lifetime_seconds`, 900 s by default) and answers an expired one with 401, as the collector does. The new test gives tokens one second, holds the upload for a second and a half, and expects the submission to be accepted, with a second exchange for the completion. Red: 1 failed, 43 passed. The failure is the stated reason: `the completion after a long upload used an expired token` (`('pending', 'COLLECTOR_UNAVAILABLE')`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`submit` now calls `_token()` once more after the uploads and before the completion request. The session hands back the same token while it is valid and exchanges for a new one only when it is about to expire, so a submission whose uploads outlast the token (an hour) completes in the same attempt instead of getting 401 and a `COLLECTOR_UNAVAILABLE` receipt until the next one. The README says so. Green: 44 passed, the same in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed the new HTTPS slow-upload test on both macOS jobs, though it passed on Ubuntu and locally. Measuring each piece's write showed why: a loopback TCP connection on macOS grows its receive buffer to about 280 KiB whatever `SO_RCVBUF` the listening socket or the accepted connection is given. The buffer then absorbs several 64 KiB pieces at once, and the next piece waits up to about 0.3 s for the window to open, close to the test's 0.5 s threshold. That is the kernel's buffering, not the client's piece size, so the test depended on the machine. The fake's TLS upload server now listens on a Unix socket, which keeps the buffer it is given. The test client sends `https://` requests over that socket (an httpx mount with `uds=`), and the upload URLs still name `https://127.0.0.1`, so the client's checks on upload URLs are the same. Each 64 KiB piece now takes a steady 70 to 80 ms against the 0.5 s threshold. Checked both ways, with the test as it stands here: with `UPLOAD_CHUNK_BYTES` temporarily set back to 1 MiB, the test fails three times out of three with `an HTTPS upload that kept making progress was stopped`; with 64 KiB it passes three times out of three. It also passes under Homebrew's Python 3.14 (OpenSSL 3.6.5), which the macOS CI jobs use for 3.14. `test_client.py`: 44 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After completion is accepted, the contract (collector docs/artifact-submissions.md)
says to poll GET /v1/submissions/{id} until the status is final, then read
GET /v1/submissions/{id}/receipt. The fake collector gains both endpoints,
answering as collector#12's intake_api.py does, and a finish() that plays
the worker. The tests expect the hand-over to wait in awaiting_final_status,
the checks to back off from 10 seconds to 10 minutes, the receipt and the
collector's rejection to be stored, a lost submission to be created again,
and the checks to resume in the next emitter on the machine.
Red: tests/submissions, 44 failed, 63 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An accepted completion no longer settles the collector row. Each accepted
submission waits as awaiting_final_status in the hand-over queue, and the
row's leases, attempts and retries drive status checks: GET
/v1/submissions/{id} 10 seconds after acceptance, each later wait twice the
last, up to ten minutes. Once the status is succeeded, rejected or failed,
the client reads GET /v1/submissions/{id}/receipt, checks that it names the
submission and carries its receipt_sha256, and the row stores it with the
collector's rejection. The hand-over then settles as succeeded, rejected or
failed. A 404 submission_not_found creates the submission again; outages,
a missing intake and 401 or 403 answers follow the contract's table. Status
checks read no file, so they work after the build's files are gone.
Green: tests/submissions, 107 passed (three runs); tests/huggingface,
tests/interface and tests/test_boundaries.py, 374 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejected or failed submission must say so where a person looks: the emitter host shares the build's standard error, so one line there, naming the hand-over, the kind, the submission and the code, reaches the build's log. A success prints nothing. Red: tests/submissions/test_delivery.py, 3 failed (no line printed); the guard that a success prints nothing passes before and after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a collector hand-over settles, the delivery writes one line for each of its submissions whose status is rejected or failed: the kind, the hand-over, the submission ID and the code (the collector's rejection code, or the emitter's own code for a refusal). The emitter host is the build's child process and shares its standard error, so the line reaches the build's log. finish_job now says whether it recorded the attempt, so only the holder of the row's lease reports. Green: tests/submissions, 111 passed (three runs); tests/huggingface and tests/test_queue_compatibility.py, 168 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collector's recovery brings every job to a final status within three attempts or dispatches of about an hour each. A submission still not final a day after acceptance, or one whose status checks get no answer for a day, must be recorded as timed_out and reported, with its last check on the limit itself. A guard checks that an emitter started days later still stores a final status the collector holds. Red: tests/submissions, 2 failed (checks continued, the last at 87,240 s instead of 86,400 s), 112 passed; the guard passes before and after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A submission still not final FINAL_STATUS_TIME_LIMIT_SECONDS (a day) after completion accepted it, whether the collector keeps answering queued or does not answer, becomes timed_out with FINAL_STATUS_TIMED_OUT, and is reported on standard error like a rejection. The last check is scheduled on the limit itself, and the limit applies only after that check, so an emitter started days later still stores a final status the collector holds. The collector's recovery brings every job to a final status within three attempts or dispatches of about an hour each, so a day leaves a wide margin. Green: tests/submissions, 114 passed (three runs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter answers collector_submission_receipt, but the interface had no typed call for it, so a build could not see a rejected submission. A new protocol fixture records the query, its digest joins the released list, and the tests expect EmitterClient.collector_submission_receipt to answer at once with each state (awaiting_final_status, then rejected with the collector's reason), and None when submissions are off or the emitter did not load the module. The emitter already accepts the fixture. Red: tests/interface and tests/submissions, 4 failed (no such call), 310 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmitterClient.collector_submission_receipt(handover_id) sends the collector_submission_receipt query recorded in the new fixture and returns the emitter's latest record at once, never waiting for a status check. The interface describes that record: CollectorSubmissionReceipt sums up the hand-over (pending, awaiting_final_status, succeeded, rejected, failed, timed_out or skipped), and each CollectorSubmission carries the collector's status, its rejection (code and message) and its receipt. The call reports under its own kind, collector_submission, so an older emitter that refuses the query does not silence warnings for staging calls. Green: tests/interface and tests/submissions, 314 passed (two runs); tests/test_boundaries.py, tests/test_package_names.py and tests/huggingface, 181 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README now says what follows completion (status checks with their backoff and one-day limit, receipts with the collector's reason, lost submissions created again, resumption in the next emitter), that builds never wait for it, and how the outcome shows: the hand-over's state and each submission's rejection and receipt through collector_submission_receipt, plus one line on standard error. The interface README lists the new call, and the changelog fragment covers it. It refers to the merged contract instead of collector#6 at 673df3d. Documentation only: no test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… awaiting A status or receipt request answered 403 submission_owned_by_another_user means only this token's Hugging Face user cannot read the submission; the collector has it and may already have finished it. The check must keep the submission awaiting its final status, as it does for a refused token, rather than record it as failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A status check answered 403 submission_owned_by_another_user recorded the submission as failed and stopped checking, although the collector holds it and may have finished it; only this token's Hugging Face user cannot read it. It now stays awaiting its final status, as for a refused token, so an emitter with the submitting user's token can still store its receipt, and otherwise it times out after a day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 7671af0, a status or receipt check answered 403 submission_owned_by_another_user keeps the submission awaiting its final status. When every check until the one-day limit gets that answer, the submission becomes timed_out with FINAL_STATUS_TIMED_OUT, and the stored record and the standard-error message no longer say that the checks were made with another Hugging Face user's token. Before 7671af0 that cause showed as COLLECTOR_REFUSED_403. The timed-out submission must keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 7671af0 a status or receipt check answered 403 submission_owned_by_another_user keeps the submission awaiting its final status. When the checks go on being refused that way until the one-day limit, the submission became timed_out with FINAL_STATUS_TIMED_OUT, so the stored record and the standard-error message no longer said that the token belonged to another Hugging Face user than the one who submitted it, although the collector has the submission and may have finished it. A last check refused that way now keeps COLLECTOR_REFUSED_403 as the timed-out submission's code. The client returns the code through the new OWNED_BY_ANOTHER_USER constant, which the time limit compares against. README and the interface's CollectorSubmission description say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 #26
Its stack (#25: #21 → #22 → #23 → #24) has merged, so the base is
main. It implements the emitter side of the collector's artifact intake: PolicyEngine/microcosm-collector#6 (the contract) and PolicyEngine/microcosm-collector#12 (the endpoints and worker).Update 2026-10-27: status checks and receipts
Why. The submission contract, merged as microcosm-collector's
docs/artifact-submissions.md, says that once completion is accepted the client pollsGET /v1/submissions/{submission_id}until the status is final, then readsGET /v1/submissions/{submission_id}/receipt. This pull request treated a 200 or 202 answer to completion as the end of its job. Decision (Anthony, 2026-10-27, tracker5.2 · 1): the emitter checks each submission's status until it is final and fetches its receipt, in its background queue, so builds never wait for it. collector#12, which serves both endpoints, merged on 2026-10-27 and deploys as 0.5.0 with intake switched off.What changed.
queuedorprocessing) or 200 (a final status) no longer settles the hand-over'scollectorrow in the hand-over SQLite database. Each submission becomesawaiting_final_statusin the row's receipt, and the row stays pending, so the existing leases (two minutes, renewed every 30 seconds), attempts and retries drive the checks.timed_out(FINAL_STATUS_TIMED_OUT, orCOLLECTOR_REFUSED_403when the last check was refused because the token belongs to another Hugging Face user than the one who submitted it). The last check falls on the limit itself, and an emitter that starts days later checks once more before applying it. The collector's recovery brings every job to a final status within three attempts or dispatches of about an hour each.succeeded,rejectedandfailed. On any of them the client reads the receipt, keeps it only when it names the submission and itsreceipt_sha256matches (SHA-256 of the contract's canonical JSON), and the row stores it with the collector'srejection(code,message).submission_not_foundfrom the status or the receipt creates the submission again, from the copies kept at hand-over (they stay until the row settles). A 409submission_not_finalfrom the receipt (an operator returned it toqueued) keeps checking. Outages, 401, a 403 without acodeand a collector without intake are handled as for submitting. A refused Hugging Face token, or a 403submission_owned_by_another_user(the next emitter on the machine runs with another Hugging Face user's token), keeps the checks going until the time limit, since the submission is already in. Status checks read no file.stage_dataset_bundleand every receipt query answer from the database at once, and at shutdown a check in progress gets at most the existing 2 seconds.EmitterClient.collector_submission_receipt(handover_id), with the new fixturehuggingface-collector-submission-receipt.json(digest recorded; released fixtures unchanged), and theTypedDictsCollectorSubmissionReceipt,CollectorSubmissionandCollectorRejectioninmicrocosm_emitter_interface.submissions. The call reports failures under its own kind,collector_submission, so an older emitter that refuses the query does not silence warnings for staging calls.HandoverQueue.finish_jobnow returns whether it recorded the attempt, so only the holder of a row's lease reports its outcome.How a rejected submission shows up.
client.collector_submission_receipt(handover_id)answers at once. A rejected bundle hasstatus: "rejected"anderror_codeset to the collector's code, such ascontent_mismatch,stored_object_mismatchorobject_missing; the matching entry insubmissionshasstatus: "rejected",rejection: {"code": ..., "message": ...}and the collector'sreceipt. The hand-over's status isrejectedif any submission was rejected, elsefailed(for exampleworker_error), elsetimed_out, elsesucceeded. An entry that isfailedwithout areceiptis one the emitter could not submit, with the emitter's own code.rejected,failedortimed_out, for exampleMicrocosm collector submission rejected: calibration-diagnostics for hand-over <id> (submission sub-…), content_mismatch. The hand-over's collector_submission_receipt has the details.The build starts the emitter host as a child process that inherits its standard error, so the text appears in the build's log when the build is still running; otherwise it appears in the log of the next build whose emitter finishes the checks.Commits (red, then green).
40a419cfirst mergesmain(the 0.7.0 release commit) with a merge commit.96109fd→20e6dc7tests/submissions: 44 failed, 63 passed (nocheck_status; receipts still saidsubmitted)tests/huggingface,tests/interface,tests/test_boundaries.py: 374 passed5add52b→e940cf7tests/submissions: 3 failed (nothing written), 108 passed; the guard that a success writes nothing passes before and aftertests/huggingface,tests/test_queue_compatibility.py: 168 passed81b4938→53c1769tests/submissions: 2 failed (checks continued, the last at 87,240 s instead of 86,400 s), 112 passed; the guard that a final status found days later is still stored passes before and after37d4f19→33ea2actests/interface,tests/submissions: 4 failed (no such call), 310 passedtests/test_boundaries.py,tests/test_package_names.py,tests/huggingface: 181 passed351eaafbeb09c3→7671af0(review)submission_owned_by_another_userhad recorded the submission asfailed(COLLECTOR_REFUSED_403) and stopped checking, although the collector holds it and may have finished ittests/submissions/test_client.py: 2 failed, 66 passedtests/submissions: 118 passed5bf5351→b09dfa6(re-review)submission_owned_by_another_user, the submission becametimed_outwithFINAL_STATUS_TIMED_OUT, so the stored record and the standard-error message no longer showed that another user's token was used. It now keepsCOLLECTOR_REFUSED_403tests/submissions/test_delivery.py: 2 failed (FINAL_STATUS_TIMED_OUTinstead ofCOLLECTOR_REFUSED_403), 44 passedtests/submissions: 120 passedTesting. At the head:
tests/submissions,tests/interface,tests/huggingface,tests/test_boundaries.py,tests/test_queue_compatibility.pyandtests/test_package_names.py, 501 passed atb09dfa6.tools/check-quality.sh(ruff check,ruff format --check),tools/check-changelog.shandtools/check-artifacts.shpass; the repository defines no type checker. The fake collector (tests/support/fake_collector.py) gained bothGETendpoints, answering as collector#12'sintake_api.pydoes, andfinish(), which plays the worker. No test reaches a real collector or the network beyond loopback.Cross-check against collector#12's code (not committed). A scratch script served microcosm-collector
main'screate_appover loopback, with the collector's own test stand-ins for its database, bucket and worker queue, and ran its realworker.run_job. Through this client: a submission was accepted asqueued, thensucceededafter the worker ran, with a receipt whosereceipt_sha256matched and whoseresultwas{"processed_by": "intake-only", "verified_files": 2}; bytes changed after completion gaverejectedwithcontent_mismatch; a stored object with another recorded SHA-256 gave completion 200rejected, then a receipt withstored_object_mismatchandjob: null; an unknown ID gaveSUBMISSION_NOT_FOUND; and a collector with intake switched off gaveCOLLECTOR_INTAKE_NOT_ENABLED.Deployment verification. No workflow, Dockerfile, Terraform, packaging or release script changed, and no new code reads an environment variable. Unverified, and a gap on Modal: the hand-over database defaults to
~/.cache/microcosm/telemetry/events.handovers.sqlite3(or underXDG_CACHE_HOME). On Modal that is inside the container unless it points at a Volume, and I found no Microcosm code that does so. A status check still pending when a Modal container stops is then lost with the container's database; the collector still holds the submission and its receipt.Summary
The
huggingfacemodule now also sends what a staged dataset bundle names to the collector, after the bundle's Hugging Face commit. Microcosm needs no separate call for it. Off by default.CollectorProvenance). A bundle hand-over doesn't say which country or build it is, or whether its data is licensed, and the collector needs all three.StageDatasetBundletherefore takes an optionalcollector=CollectorProvenance(country_code, build_id, licensed_data, release=None), checked with the collector contract's rules.submissions/planning.py, by role only; files are never read or interpreted):calibration_diagnosticsfile gives acalibration-diagnosticssubmission, with anybuild_manifest,release_manifestanddemographicsfile.orrery_schemaplusexecution_overlaygive anorrery-inputssubmission, with anyorrery_upstreamandorrery_evidencefiles, mapped to the collector's role names. The interface exports these bundle role names.collectorrow inhandover_jobs(one row per hand-over and destination, so no new table) waits for itshuggingfacerow (WAITING_FOR_HUGGING_FACE). A bundle Hugging Face refuses isskipped.submissions/client.py):artifacts:submitcredential.CollectorSessiongains an optionalscope; telemetry sessions still send{}. A scoped session uses a token only when the answer says"scope": "artifacts:submit".Host(412 means already stored), then complete it. An instruction whoseHostdiffers from its URL's host is refused.https://storage.googleapis.com, or to loopback when a development collector is configured.docs/artifact-submissions.md; updated on 2026-10-27, see the next section):pending, retried with the queue's backoff: outages, 408/429/5xx, a 409 at completion, and a 404submission_not_foundat completion (the next attempt creates the submission again).pendingfor an hour, with no request at all to that collector meanwhile (COLLECTOR_INTAKE_NOT_ENABLED): an exchange answered 200 without theartifacts:submitscope (a collector released before intake) or 404, and any other 404 from a submission path, including one to create. The token is discarded.code(the token's scope does not allow the request) is followed by one fresh exchange and retry; only a second such refusal is final.failedwithout a receipt: the emitter's own refusals (including 403submission_owned_by_another_user) and changed bytes, with the emitter's code.skipped: a bundle Hugging Face refused, or whose folder is longer than the collector's 512 characters (COLLECTOR_FOLDER_TOO_LONG).awaiting_final_statuswhile the emitter checks its status, then the collector's finalsucceeded,rejectedorfailedwith its receipt, ortimed_outafter a day. This replaces the earliersubmitted. Read it throughEmitterClient.collector_submission_receipt.collector_submissions: true, which no interface release sends.development_collector_urlpoints at a loopback collector. Switching production builds on is a separate emitter change that flips this default, so this release changes nothing for running builds.huggingface-stage-dataset-bundle-with-collector.json) carries a described bundle. Released fixtures are unchanged.Not in this PR
publish_release). The same mechanism would submit a release's calibration diagnostics with release provenance, which the calibration-tree worker needs to record each release publication. It is a follow-up, kept out to keep this bounded.hand_over_orrery_inputs, the graph publication queue to microcosm-runs) is unchanged. Its role set has no execution overlay, so the collector gets the Orrery inputs from the bundle instead. O1 (microcosm#1295) already registersgraph.execution.jsonas a build output.Retry-Afterand the 1 MiB response cap, as before.Commits (red, then green)
tests/support/fake_collector.py).Each commit's own test files were run again after committing.
Testing
tests/interface/test_collector_provenance.py,tests/submissions/andtests/interface/test_protocol_fixtures.py, 66 passed. They use temporary migrated databases, the in-memory Hub, synthetic credentials and loopback servers only.tools/check-quality.shandtools/check-changelog.shpass.core/auth.py,huggingface/service/module.py,requests.py,bundles.py). CI covers them.Deployment verification
No workflow, Dockerfile or release script changes. New and reused settings:
collector_submissions(module configuration)HuggingFaceSettings.configurationsends onlyhandover_database_path, so it is off in every build.development_collector_url(module configuration)_development_collector_urlcheck).PRODUCTION_COLLECTOR_URLconstantHF_TOKEN,HUGGINGFACE_TOKENor the savedhf auth logintokenhuggingface_token(#24), and sent only to the collector's token exchange.Coordination
bundles.py,__init__.py,huggingface/service/module.py,requests.py,tests/interface/test_protocol_fixtures.py). It was restacked on their review fixes on 2026-10-20 (below).Restack and collector contract fixes (2026-10-20)
Restacked on the review fixes to #21–#24, then changed to match collector#6 (contract,
673df3d) and collector#12 (intake,30dbd1b). Each commit red, then green; only the test files this pull request adds or changes were run, with loopback services only.4598ed7Use the shared hand-over codes and public credential lookup after the restacktests/test_boundaries.py,tests/submissions: 2 failed, 10 errors, 38 passed9c5447fSubmit the bundle files copied at hand-over to the collectortests/submissions: 1 failed, 39 passedtest_bundles.pyb7c7422Send the described-bundle fixture through a typed call in the teststest_every_fixture_has_a_typed_call_that_produces_it.test_client_messages.py: 1 failed, 28 passed (as in CI)906cd4eUse a submit token only when the exchange grants that scopetests/submissions: 21 failed, 23 passed (most from the client's newclockargument)tests/telemetry/test_delivery.py42b65d2Create a submission again only for a 404 submission_not_foundtest_client.py: 5 failed, 26 passedtests/submissions: 49 passed517d1b6Fail a submission whose completion answers rejected or failedstored_object_mismatchwith 200rejected, and its status documents carryfiles_sha256andjob.0130888Exchange again for the submit scope after a 403 without a code62badd7Report an already-staged bundle only with the commit that added itlast_commitfails or reports nothing.test_bundles.py: 2 failed, 28 passedtests/submissionsaf1e35dSkip a collector submission whose folder is longer than 512 characterstest_delivery.py: 1 failed, 12 passedtests/submissions: 57 passedc3b6082Refuse an upload instruction whose Host header names another hostHost. SendingHostas given already worked (that test passed before).test_client.py: 1 failed, 39 passed28dcec2Describe the collector answers and decision D10's disclosure rulesItem 9 (the fake collector:
scope, errorcodes,files_sha256andjob,Host,stored_object_mismatchas 200rejected) came with items 1–4 and 7. Not done, as asked: polling the submission's status and fetching its receipt.Re-run after the last commit: every test file this pull request adds or changes, 148 passed; with
tests/test_boundaries.py,tests/huggingfaceandtests/telemetry/test_delivery.py, 167 passed.tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass. The earlier four commits' hashes changed in the restack.Review fixes (2026-10-20, first review of this pull request)
Each fix is a commit that adds a test failing because of the bug, then a commit with the code. Only the test files this pull request adds or changes were run, with loopback services only.
2ea1dd6→46012a2Submit to the collector on a worker of its own, with time limitstickran the collector cycle after the Hugging Face cycle on the module's one worker thread. Uploads had no write timeout (httpx.Timeout(60.0, write=None)), and uncopied files were re-hashed on that thread before every attempt. So a slow or stalled submission delayed the Hugging Face commits a build waits for and stopped the commit at shutdown.tests/submissions/test_delivery.py: 5 failed, 13 passed. With a collector upload held: run files never reached the staging repository; in a host runtime with a 2-second shutdown period,LocalServiceError: Service worker failed (TimeoutError);UPLOAD_TIMEOUT.writewasNone; the upload limit and the cycle budget did not exist.1701b3e→19cac1bKeep collector errors out of the Hugging Face answers and startuptest_delivery.py: 3 failed, 18 passedDesign (finding 7). This uses the mechanism from #23's fix:
DeliveryWorker, so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back.tickruns the collector cycle only when the module runs without workers (tests driven by hand). At shutdown a submission still running at the deadline is abandoned with the process, and the collector's HTTP client and the queue are released only once its worker has stopped.httpx.Timeout(60.0): a stalled upload fails within a minute.upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks, and the limit is checked before each one.MAX_CYCLE_SECONDS(ten minutes). With the limits on every request and upload, one cycle's length is bounded.Not done now: #29's
collector_originsetting (review finding 2). #27 still chooses the collector origin itself in_collector_delivery(the development URL, else the production constant), so once #29 and this pull request are both merged, a configuredcollector_originwould be ignored for collector submissions. The fix needs #29 merged tomainand carried up the stack first, since the resolver doesn't exist in this branch's history. It will be:configured_collector_origin(self.configuration), as the telemetry and Orrery modules do, and allow loopback uploads only when the resolved origin is the development URL;19cac1b, instead of stopping the module.Restack onto #23's and #24's fixes.
9a0dda7(submit described bundles) changed while it was rebased: the module keeps its collector wiring next to the new delivery workers, and the test fixtures usehandover_database_pathandbackground_workers=False. As before, two of this pull request's intermediate commits don't pass on their own (9a0dda7imports the old private credential lookup untilbdf9410; a submission test is fixed bydc66627); the branch head passes.Re-run after the last commit, every test file this pull request adds or changes: 156 passed; every test file the four stacked pull requests change, at this head: 480 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.Not changed here: the collector row's hour-long lease is not yet renewed during an attempt (an emitter killed mid-upload still blocks that row for up to an hour; unexpected errors no longer do);
Retry-After, the collector's error codes in receipts and the 1 MiB response cap (review items 3, 6, 7). Open decisions, unchanged: polling submission status and receipts, the interface role constants for H2c, andAGENTS.md's publishing rule.Fix round 2 (2026-10-20)
Restack onto #23's and #24's round-2 fixes. Every commit has a new SHA; the titles are unchanged. The sections above use the earlier SHAs:
9a0dda7is nowc4c46c5,bdf9410is4c8c360,dc66627is51aec26,2ea1dd6ise081c07,46012a2isf2c94a7,1701b3eisf901abdand19cac1biscdffe80. One change goes beyond replaying the commits: #23'sDeliveryWorkernow passes its stop check to each cycle, sof2c94a7builds the collector worker's cycle aslambda _stop_requested: self.collector_delivery.run_cycle(). Without that, the worker would fail on every cycle. The collector cycle uses the stop check fromf735a52on. As before, two intermediate commits don't pass on their own (c4c46c5imports the old private credential lookup until4c8c360; a submission test is fixed by51aec26).b30ec7e→f735a52Finish both shutdown commits before waiting briefly for the collectorclosewaited for every worker, the collector's included, before the second shutdown commit. A collector upload still running at shutdown used up the whole period, and the second commit, for the repository a bundle upload had held, was skipped even when that upload finished in time. On Modal those run files are lost.tests/submissions/test_delivery.py, the review's case: 1 failed, 21 passed (AssertionError: the staging run files missed the second shutdown commit), in two runsd838a03→e463794Stop only stalled uploads, never ones that keep making progressupload_time_limit(size), which assumes at least 2.1 Mbit/s. A large file on a slower link (399 MiB at 1.5 Mbit/s) was stopped on every attempt, started again from zero, and was never submitted.tests/submissions/test_client.py: 1 failed, 41 passed (AssertionError: an upload that kept making progress was stopped; the outcome was('pending', 'UPLOAD_FAILED'), at the second 1 MiB piece), in three runs. The stalled-upload test passes on the old code too: its write timeout already detected a stall.Design: shutdown (finding 6).
closestops every worker, and the collector cycle now claims no further hand-over once stop is requested.closethen:COLLECTOR_SHUTDOWN_WAIT_SECONDS(2 s) and never past the deadline.Collector work therefore never uses up the time the Hugging Face shutdown commits need, and a busy collector worker no longer holds the process for the whole shutdown period. A submission still running is abandoned with the process, as before, and its row is retried once its lease expires. The HTTP client and the queue stay open while any worker still runs.
Design: stall detection instead of a size-based limit (finding 7). A signed upload is one
PUTthat stores nothing unless it completes, so a limit on its total time only makes a slow upload start again from zero.upload_time_limit,chunks_until,UploadTimeLimitError,MIN_UPLOAD_SECONDSandMIN_UPLOAD_BYTES_PER_SECONDare gone.UPLOAD_TIMEOUT's write timeout, which httpcore applies to each send to the connection: a send that has had no byte accepted for 60 seconds fails the attempt (pending,UPLOAD_FAILED). Connecting and waiting for the answer keep their 60-second limits. Files are still sent in 1 MiB pieces with an explicitContent-Length.FakeCollector(receive_buffer_bytes=…)andhttpx.HTTPTransport(socket_options=…)), so the client's sends wait for the fake to read. The tests patchUPLOAD_TIMEOUTto keep the stall threshold under a second. The slow-upload test also shortens any size-based limit withmonkeypatch.setattr(..., raising=False), which has nothing to shorten after the fix.Process note (finding 8; no history rewrite). In the first round of fixes, the red commit
2ea1dd6(nowe081c07) fails at a setup step rather than for its stated reason: its two runtime tests used the manual clock, and the green commit46012a2(nowf2c94a7) also switched them to the real clock. With the manual clock, a collector row claimed before its bundle's Hugging Face commit is rescheduled 2 seconds ahead on a clock that never moves, so one of those tests fails withAssertionError: no upload startedinstead. The tests as they stand at46012a2do fail for the stated reasons against the unfixed code. Those commits are not rewritten. This round's red commits,b30ec7eandd838a03, were each run before their fix and fail for their stated reasons.Re-run after the last commit, every test file this pull request adds or changes: 160 passed; every test file #23, #24 and this pull request change, at this head: 430 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.Fix round 3 (2026-10-20)
Restack onto #24's round-3 fixes. Every commit has a new SHA, the same title and the same patch (
git range-diffshows=for all 23). The sections above use the round-2 SHAs:c4c46c5is now8941474,4c8c360is57c0136,51aec26is4451d7f,e081c07isc64d661,f2c94a7is394c9b5,f901abdis3380529,cdffe80is4296daa,b30ec7eis24062f1,f735a52is3f63781,d838a03isf1e40f6ande463794isdc59b69. As before,8941474does not pass on its own until57c0136.c489ad7→8ecd153Send uploads in 64 KiB pieces so HTTPS stalls are measured per piecessldoes no partial writes. A link slower than about 140 kbit/s failed partway and started again from zero.tests/submissions/test_client.py: 1 failed, 42 passed (an HTTPS upload that kept making progress was stopped, outcome('pending', 'UPLOAD_FAILED')), in three runs. With the threshold raised to 5 s the same test passes on that code, so the failure is the write timeout on the first piece.99fa0e3→6e8d912Renew the collector row's lease while an attempt runstests/submissions/test_delivery.py: 1 failed, 24 passed (a second emitter claimed the collector row while its upload was running), in two runs. A guard (the row is free again within one lease after shutdown) passes before and after.a634cfb→423139bAsk for the collector token again just before completingCOLLECTOR_UNAVAILABLEuntil the next attempt.test_client.py: 1 failed, 43 passed (the completion after a long upload used an expired token, outcome('pending', 'COLLECTOR_UNAVAILABLE'))Before pushing, I amended the unpushed red commit
99fa0e3to drop a finalattempts == 1check that raced: the row can first be attempted, and recorded asWAITING_FOR_HUGGING_FACE, before the bundle's Hugging Face commit. The amended commit still fails for the stated reason.Design: 64 KiB pieces (replaces the round-2 wording "a send that has had no byte accepted for 60 seconds" and "files are still sent in 1 MiB pieces").
UPLOAD_CHUNK_BYTESis now 64 KiB, the size httpx itself reads files in. Each piece is one send under the 60-second write timeout. Over HTTPS, where a piece is written whole under one time limit, the slowest link that never fails averages 65,536 bytes per 60 seconds over each piece: 1,093 bytes a second, about 8.7 kbit/s. Over plain HTTP (a loopback development collector) a send still fails only after 60 seconds in which no byte was accepted. Nothing else changes on the wire. The code comment, the README and the changelog fragment state this rule and rate instead of "never stopped, however slow the link".tls_uploads: a second server for uploads that speaks TLS, as Cloud Storage does, while the collector stays plain HTTP. Both sides use only anonymous TLS 1.2 cipher suites, so no certificate or key is generated or committed. The fake reads uploads at about 1 MiB/s, the threshold is 0.5 s and the file is 1.5 MiB, so a 64 KiB piece takes about 75 ms and a 1 MiB piece more than a second.383a27f(Run the HTTPS slow-upload test over a Unix socket). The first CI run at423139bfailed this test on both macOS jobs (Ubuntu passed). Measured locally: a loopback TCP connection on macOS grows its receive buffer to about 280 KiB whateverSO_RCVBUFit is given, so it absorbs several pieces at once and the next piece waits up to about 0.3 s, close to the 0.5 s threshold. The upload server now listens on a Unix socket, which keeps the buffer it is given; the test client sendshttps://requests over it (an httpx mount withuds=), and the upload URLs still namehttps://127.0.0.1. Each piece then takes a steady 70 to 80 ms. With the test as it stands after this commit, settingUPLOAD_CHUNK_BYTESback to 1 MiB makes it fail three times out of three for the stated reason, and 64 KiB passes three times out of three, also under Homebrew's Python 3.14, which the macOS CI jobs use.Design: a renewed two-minute lease (replaces the round-2 "Known gap, unchanged" bullet). The collector row's lease is now 120 s (
SUBMISSION_LEASE_SECONDS), renewed every 30 s (SUBMISSION_LEASE_RENEWAL_SECONDS) while an attempt runs, through the sameHeldLeasesthe Hugging Face jobs use.HeldLeasesgainsfor_job, which holds a job without a repository lease, and an optional stop check: renewal ends atstop, or once shutdown has begun. The Hugging Face leases are built without the stop check, so they behave as before._attemptnow returns anAttemptResult;run_cyclestops the renewal, then records the result with the renewed lease value, so the attempt's result is recorded.Design: a fresh token before completion.
submitasks the session for the token once more after the uploads. The session returns the same token while it is valid and exchanges for a new one only when it is about to expire, so uploads that outlast the token (an hour) complete in the same attempt. The fake collector now issues tokens with a lifetime the test chooses and answers an expired one with 401.Re-run after the last commit, every test file this pull request adds or changes: 164 passed; every test file #23, #24 and this pull request change, at this head: 446 passed (five runs at
423139b, once more at383a27f).tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass. No workflow, Dockerfile, Terraform, packaging or release script changed, and no new code reads an environment variable.🤖 Generated with Claude Code