Repository navigation
Publish release builds side by side with releases.json as the only record of what is current - #24
Merged
Merged
Conversation
This was referenced Oct 19, 2026
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 20, 2026 13:00
ec71b7a to
3bc95d7
Compare
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 20, 2026 13:15
3bc95d7 to
ca43761
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
A release change that failed for any reason other than a broken rule stayed pending, was retried without limit and in backoff order, and was applied to whatever record it found. So an older promotion could land after a newer change from this or another machine and put back an older current build, and a change pending for days could land after a login. The tests cover the #24 review's case (another machine's newer change lands first), the same case with the newer change made on this machine and changing nothing, a change pending for two days before and after a login, and two guards: changes handed over in order all apply, and a plain publication queued before a newer change still lands. Red: 4 failed, 34 passed. The older SECOND build became current in both ordering cases ("assert 'uk-frs-calib...323Z-6cba5d71' == 'uk-frs-calib...443Z-d12ff9b3'"); the change pending for two days was "published"; the expired change stayed ("pending", "CREDENTIAL_MISSING"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 20, 2026 17:47
97a57f4 to
6bd4a45
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The mechanism, also described in releases.py and the README: - Expiry: a release change still pending an hour after its hand-over (RELEASE_CHANGE_EXPIRY_SECONDS) becomes refused with REQUEST_EXPIRED, also when no credential is present, so it never lands after a later login. - Order: an ordered change (a promotion, a withdrawal, or a publication that makes its build current) is refused with NEWER_CHANGE_LANDED when an ordered change requested after it has already landed on the repository's record. The writer learns that from this machine's queue, which also holds later changes that left the record unchanged, and from the title of the commit that last changed releases.json. Every writer commit's title now ends "[newest request <time>]", the newest request time of the ordered changes applied so far. - A publication that only adds a build is never refused for order and does not count as newer, because concurrent publications must both land (plan item 5). Its commit carries the recorded time forward. - A change that leaves releases.json unchanged makes no commit and reports the head, as the Hub would have done anyway. One case is left to the expiry alone: another machine's newer change that left releases.json unchanged records no time. Request times come from each machine's clock. Green: 40 passed in the release-writer tests, including two added with the fix that drive the recorded request time directly; 195 passed across #24'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 #24 review's code-quality notes: ReleasesRecord.parse accepted "schema_version": true or 1.0, because True == 1 == 1.0 in Python; and the writer never checked that a record it writes stays within the 4 MiB it accepts when reading, so one large write would make every later read refuse the record. Red: 3 failed, 29 passed. Both schema versions were accepted ("DID NOT RAISE ReleaseRecordError"); the oversized change gave no violation ("assert [] == ['RECORD_TOO_LARGE']"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- One helper builds the refusal for a list of broken rules: the first rule's code and every rule's message. The writer built it in two places before. - A new test covers a change whose parent commit loses every one of its MAX_PARENT_CONFLICT_ATTEMPTS: it stays pending with PARENT_CONFLICT and writes nothing. No behaviour change and no red step: the test guards existing behaviour, which the #24 review found untested. Green: 42 passed. 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 bundle worker and the host's thread each list due repositories and then deliver them in turn, so a list can be minutes old when a delivery starts. After the lease is taken, `_claim_job` now refuses to claim a job, and `run_files_due` reports no run files due, until `MIN_COMMIT_SPACING_SECONDS` have passed since this machine's last commit to the repository. The commit at shutdown still ignores the spacing. The helpers `_spacing_elapsed`, `commit_spacing_elapsed` and `_run_files_allowed` match the ones #24 uses, which also exempts release changes from the spacing. Green: 7 passed; every test file this branch adds or changes: 199 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
A release change that failed for any reason other than a broken rule stayed pending, was retried without limit and in backoff order, and was applied to whatever record it found. So an older promotion could land after a newer change from this or another machine and put back an older current build, and a change pending for days could land after a login. The tests cover the #24 review's case (another machine's newer change lands first), the same case with the newer change made on this machine and changing nothing, a change pending for two days before and after a login, and two guards: changes handed over in order all apply, and a plain publication queued before a newer change still lands. Red: 4 failed, 34 passed. The older SECOND build became current in both ordering cases ("assert 'uk-frs-calib...323Z-6cba5d71' == 'uk-frs-calib...443Z-d12ff9b3'"); the change pending for two days was "published"; the expired change stayed ("pending", "CREDENTIAL_MISSING"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The mechanism, also described in releases.py and the README: - Expiry: a release change still pending an hour after its hand-over (RELEASE_CHANGE_EXPIRY_SECONDS) becomes refused with REQUEST_EXPIRED, also when no credential is present, so it never lands after a later login. - Order: an ordered change (a promotion, a withdrawal, or a publication that makes its build current) is refused with NEWER_CHANGE_LANDED when an ordered change requested after it has already landed on the repository's record. The writer learns that from this machine's queue, which also holds later changes that left the record unchanged, and from the title of the commit that last changed releases.json. Every writer commit's title now ends "[newest request <time>]", the newest request time of the ordered changes applied so far. - A publication that only adds a build is never refused for order and does not count as newer, because concurrent publications must both land (plan item 5). Its commit carries the recorded time forward. - A change that leaves releases.json unchanged makes no commit and reports the head, as the Hub would have done anyway. One case is left to the expiry alone: another machine's newer change that left releases.json unchanged records no time. Request times come from each machine's clock. Green: 40 passed in the release-writer tests, including two added with the fix that drive the recorded request time directly; 195 passed across #24'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 #24 review's code-quality notes: ReleasesRecord.parse accepted "schema_version": true or 1.0, because True == 1 == 1.0 in Python; and the writer never checked that a record it writes stays within the 4 MiB it accepts when reading, so one large write would make every later read refuse the record. Red: 3 failed, 29 passed. Both schema versions were accepted ("DID NOT RAISE ReleaseRecordError"); the oversized change gave no violation ("assert [] == ['RECORD_TOO_LARGE']"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- One helper builds the refusal for a list of broken rules: the first rule's code and every rule's message. The writer built it in two places before. - A new test covers a change whose parent commit loses every one of its MAX_PARENT_CONFLICT_ATTEMPTS: it stays pending with PARENT_CONFLICT and writes nothing. No behaviour change and no red step: the test guards existing behaviour, which the #24 review found untested. Green: 42 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 20, 2026 18:47
6bd4a45 to
35481d4
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #24 fix review found that `417d651` compares request times across every release in a repository. A change to one release is then refused with `NEWER_CHANGE_LANDED` because a later change to another release landed first, and two publications that land at once and both make their build current lose one build, which breaks plan section 7, item 5. New tests, with a second release (`microcosm-uk-2024-25-dense`) in the same repository: - the review's test: a national promotion requested before a dense promotion that landed first must land; - the review's trigger A through the module's queue: a national promotion that failed once must land after a later dense promotion; - the review's trigger B: two publications to the two releases that both make their build current must both land and both be current; - two publications to one release that both make their build current must both be listed, with the newer request's build current, whichever lands first (the older-first case passes today); - a stale national promotion is still refused after the dense release's later commit (passes today: the original fix must hold). Red: 4 failed, 44 passed. Each failure is the refusal `('refused', 'NEWER_CHANGE_LANDED')` where `('published', None)` is expected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 20, 2026 20:01
35481d4 to
d1fa450
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
A release change that failed for any reason other than a broken rule stayed pending, was retried without limit and in backoff order, and was applied to whatever record it found. So an older promotion could land after a newer change from this or another machine and put back an older current build, and a change pending for days could land after a login. The tests cover the #24 review's case (another machine's newer change lands first), the same case with the newer change made on this machine and changing nothing, a change pending for two days before and after a login, and two guards: changes handed over in order all apply, and a plain publication queued before a newer change still lands. Red: 4 failed, 34 passed. The older SECOND build became current in both ordering cases ("assert 'uk-frs-calib...323Z-6cba5d71' == 'uk-frs-calib...443Z-d12ff9b3'"); the change pending for two days was "published"; the expired change stayed ("pending", "CREDENTIAL_MISSING"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The mechanism, also described in releases.py and the README: - Expiry: a release change still pending an hour after its hand-over (RELEASE_CHANGE_EXPIRY_SECONDS) becomes refused with REQUEST_EXPIRED, also when no credential is present, so it never lands after a later login. - Order: an ordered change (a promotion, a withdrawal, or a publication that makes its build current) is refused with NEWER_CHANGE_LANDED when an ordered change requested after it has already landed on the repository's record. The writer learns that from this machine's queue, which also holds later changes that left the record unchanged, and from the title of the commit that last changed releases.json. Every writer commit's title now ends "[newest request <time>]", the newest request time of the ordered changes applied so far. - A publication that only adds a build is never refused for order and does not count as newer, because concurrent publications must both land (plan item 5). Its commit carries the recorded time forward. - A change that leaves releases.json unchanged makes no commit and reports the head, as the Hub would have done anyway. One case is left to the expiry alone: another machine's newer change that left releases.json unchanged records no time. Request times come from each machine's clock. Green: 40 passed in the release-writer tests, including two added with the fix that drive the recorded request time directly; 195 passed across #24'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 #24 review's code-quality notes: ReleasesRecord.parse accepted "schema_version": true or 1.0, because True == 1 == 1.0 in Python; and the writer never checked that a record it writes stays within the 4 MiB it accepts when reading, so one large write would make every later read refuse the record. Red: 3 failed, 29 passed. Both schema versions were accepted ("DID NOT RAISE ReleaseRecordError"); the oversized change gave no violation ("assert [] == ['RECORD_TOO_LARGE']"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- One helper builds the refusal for a list of broken rules: the first rule's code and every rule's message. The writer built it in two places before. - A new test covers a change whose parent commit loses every one of its MAX_PARENT_CONFLICT_ATTEMPTS: it stays pending with PARENT_CONFLICT and writes nothing. No behaviour change and no red step: the test guards existing behaviour, which the #24 review found untested. Green: 42 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #24 fix review found that `417d651` compares request times across every release in a repository. A change to one release is then refused with `NEWER_CHANGE_LANDED` because a later change to another release landed first, and two publications that land at once and both make their build current lose one build, which breaks plan section 7, item 5. New tests, with a second release (`microcosm-uk-2024-25-dense`) in the same repository: - the review's test: a national promotion requested before a dense promotion that landed first must land; - the review's trigger A through the module's queue: a national promotion that failed once must land after a later dense promotion; - the review's trigger B: two publications to the two releases that both make their build current must both land and both be current; - two publications to one release that both make their build current must both be listed, with the newer request's build current, whichever lands first (the older-first case passes today); - a stale national promotion is still refused after the dense release's later commit (passes today: the original fix must hold). Red: 4 failed, 44 passed. Each failure is the refusal `('refused', 'NEWER_CHANGE_LANDED')` where `('published', None)` is expected. 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 #24 found that a withdrawal counts as a newer change when the writer decides whether a change to a release was overtaken, although a withdrawal can never change `current` (`withdraw_build` refuses the current build and `make_current` refuses a withdrawn one). The four new tests are the review's triggers: - a publication that makes its build current, requested before an unrelated withdrawal that landed first, must still become current; - a promotion requested before an unrelated withdrawal must land; - a withdrawal requested before an unrelated promotion must land; - on one machine, a promotion that failed once must land after a withdrawal of another build, handed over later, landed first. Red: 4 failed, 50 passed. Each failure is the stated reason: `a later withdrawal of another build kept the publication from becoming current`, `a promotion was refused for a later withdrawal of another build`, `a withdrawal was refused for a later promotion of another build`, and `a queued promotion was refused for a later withdrawal of another build`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 21, 2026 21:58
384bc5e to
ab3d28c
Compare
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 21, 2026 22:30
ab3d28c to
2db29df
Compare
`microcosm_emitter_interface.releases` holds the one record of which build of each release is current, and the rules every write follows: - Shape: schema_version 1, `default`, and for each release its `role`, `current` (a build or null) and each build's `published_at` and `withdrawn`; serialized in that order with releases and builds sorted. - Changes: `add_build` (optionally making it current), `make_current` and `withdraw_build`. A listed build cannot be published again, a release keeps its first role, a withdrawn build cannot become current and the current build cannot be withdrawn. - Checks: `check_record` (current names a listed, non-withdrawn build), `check_change` (existing build entries change only `withdrawn`), `check_repository` (every listed build has its folder and a release_manifest.json naming the same release and build) and `check_commit` (no file inside an existing build folder is added, changed or removed; no latest*.json is written; current does not change while any latest*.json exists). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter's `huggingface` module now owns every write to a dataset repository's releases.json (nothing calls it yet; Microcosm switches in a later change): - `PublishRelease` names the release, the build, whether to make it current, the release's role and each file with its role; it must include release_manifest.json. `PromoteBuild` and `WithdrawBuild` change only releases.json. - Each change reads releases.json and the file list at main's head, applies the change, runs the shared checks from microcosm_emitter_interface.releases and commits the new build folder and releases.json with parent_commit. On a parent-commit conflict it reads the new head and applies the change again. - No Hugging Face tags or branches, no current/ folder, no latest*.json; existing tags and files are untouched. release.py's staging branch, tag and pointer-file commit are replaced, not ported. - A retried publication that already landed reports published; preserved files are re-hashed; Hub failures stay pending and are retried. - `release_change_policy` is the extension point for who may promote. - Jobs no longer wait for the 10-second spacing, which applies to the frequent run-file commits only. - Protocol fixtures for the three release messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`publish_release`, `promote_build` and `withdraw_build` built their message before the client's error handling, and the unavailable receipt rebuilt it, so a release file whose path cannot be resolved raised into the build. The release calls now pass a message builder like every other call, and the unavailable receipt has an empty `handover_id` when the files cannot be converted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`PromoteBuild`, `WithdrawBuild` and `PublishRelease` derived their hand-over identifier from the change alone, and `HandoverQueue.enqueue_job` returns the stored receipt for a known identifier. Because the queue database persists across builds, these changes silently did nothing: promoting a build back after another was promoted (A, B, then A again), and retrying a withdrawal after another build was promoted. A refusal such as `POINTER_FILES_PRESENT` or `NOT_AUTHORIZED` also stayed final after its cause was gone. Every release request now carries a `request_id`: a new random one by default, or one the caller gives. The message and the hand-over identifier include it, so a new request is applied even when it repeats an earlier change, and only a resent request (the same `request_id` and change) is deduplicated. The emitter refuses a release change without a valid `request_id` (`INVALID_REQUEST`). The three unreleased release fixtures gain the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_already_published` refused a publication with `BUILD_FOLDER_EXISTS` when `make_current` was true and its build was listed with an identical manifest but was no longer current. That is what a retry finds after a commit that landed but raised an error, when another build was promoted in between. A build listed with a byte-identical `release_manifest.json` now always reports `published` with the commit that wrote it; when it is not current the receipt's message names the current build. Publishing never promotes an already published build. The fake Hub can now let a commit land and then raise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ReleasesRecord.parse` caught only `ValueError` and `UnicodeDecodeError`, and `check_repository` likewise. A deeply nested `releases.json` or release manifest makes `json.loads` raise `RecursionError`, which `apply_release_change` mapped to a pending `REPOSITORY_UNAVAILABLE` and retried forever. - Such a record is refused as `INVALID_RECORD`, and such a manifest as `MANIFEST_MISMATCH`; both are final. - A record larger than `MAX_RECORD_BYTES` (4 MiB) is refused as `INVALID_RECORD` before it is decoded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release writer's change to `HandoverQueue.due_repositories` let every job skip `MIN_COMMIT_SPACING_SECONDS`, not only release changes, so bundles could again commit in bursts, each starting the dataset repository's webhook. Only jobs of `UNSPACED_JOB_KINDS` (release changes) now skip the spacing; a repository due only for a bundle waits, and within the spacing the worker claims only a release change, so an earlier bundle job is not taken in its place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The module queues the validated change's own message (`PublishRelease.message()` and so on) instead of `dict(message)`, so keys the rules do not know never reach the queue or the hand-over identifier. The worker rebuilds the typed change from it (`release_change_from_message`), and `apply_release_change` takes the typed change. Release and bundle outcome statuses are `Literal` types. - `MAX_PARENT_CONFLICT_ATTEMPTS`, `UPLOAD_FAILED`, `REPOSITORY_UNAVAILABLE`, `PRESERVED_FILES_MISSING` and `PRESERVED_BYTES_CHANGED` are defined once, in `huggingface.service.hub`; `RELEASE_CHANGE` once, in the interface; file hashing once, in the queue. - The credential lookup is public (`core.auth.huggingface_token`), so the Hugging Face module no longer imports a private name. - `ReleaseChangePolicy` now receives the change and a `ReleaseCaller` (the user the emitter runs as and the build process that started it). The module applies it when the change is handed over, so a refusal (`NOT_AUTHORIZED`) is answered at once and never queued. Who may promote is still undecided; the default allows every change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A release change that failed for any reason other than a broken rule stayed pending, was retried without limit and in backoff order, and was applied to whatever record it found. So an older promotion could land after a newer change from this or another machine and put back an older current build, and a change pending for days could land after a login. The tests cover the #24 review's case (another machine's newer change lands first), the same case with the newer change made on this machine and changing nothing, a change pending for two days before and after a login, and two guards: changes handed over in order all apply, and a plain publication queued before a newer change still lands. Red: 4 failed, 34 passed. The older SECOND build became current in both ordering cases ("assert 'uk-frs-calib...323Z-6cba5d71' == 'uk-frs-calib...443Z-d12ff9b3'"); the change pending for two days was "published"; the expired change stayed ("pending", "CREDENTIAL_MISSING"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mechanism, also described in releases.py and the README: - Expiry: a release change still pending an hour after its hand-over (RELEASE_CHANGE_EXPIRY_SECONDS) becomes refused with REQUEST_EXPIRED, also when no credential is present, so it never lands after a later login. - Order: an ordered change (a promotion, a withdrawal, or a publication that makes its build current) is refused with NEWER_CHANGE_LANDED when an ordered change requested after it has already landed on the repository's record. The writer learns that from this machine's queue, which also holds later changes that left the record unchanged, and from the title of the commit that last changed releases.json. Every writer commit's title now ends "[newest request <time>]", the newest request time of the ordered changes applied so far. - A publication that only adds a build is never refused for order and does not count as newer, because concurrent publications must both land (plan item 5). Its commit carries the recorded time forward. - A change that leaves releases.json unchanged makes no commit and reports the head, as the Hub would have done anyway. One case is left to the expiry alone: another machine's newer change that left releases.json unchanged records no time. Request times come from each machine's clock. Green: 40 passed in the release-writer tests, including two added with the fix that drive the recorded request time directly; 195 passed across #24's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Python 3.13 and later getpass.getuser() raises OSError when no user name variable is set and the user ID has no password-database entry, as in a container run with an arbitrary user ID. The module looked the caller up for every release change, so every change failed. Red: 1 failed, 40 passed: "OSError: No username set in the environment". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release-change caller's user comes from getpass.getuser(), and from the numeric user ID when that raises, so a container run with an arbitrary user ID can still hand over release changes. The permission policy itself is unchanged: by default it still allows every change. Green: 41 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment on MIN_COMMIT_SPACING_SECONDS still said only the shutdown commit ignores the spacing, and the queue's docstring still called release changes a later kind of job. Both now match the code: release changes and the shutdown commit skip the spacing; run files and dataset bundles keep it. Documentation only; no red step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the #24 review's code-quality notes: ReleasesRecord.parse accepted "schema_version": true or 1.0, because True == 1 == 1.0 in Python; and the writer never checked that a record it writes stays within the 4 MiB it accepts when reading, so one large write would make every later read refuse the record. Red: 3 failed, 29 passed. Both schema versions were accepted ("DID NOT RAISE ReleaseRecordError"); the oversized change gave no violation ("assert [] == ['RECORD_TOO_LARGE']"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ReleasesRecord.parse accepts schema_version only as the integer 1. - check_change refuses a new record whose serialized size is above MAX_RECORD_BYTES (RECORD_TOO_LARGE). The writer runs check_change on every change, so it never writes a record readers would refuse. Green: 73 passed (the record tests and the release-writer tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One helper builds the refusal for a list of broken rules: the first rule's code and every rule's message. The writer built it in two places before. - A new test covers a change whose parent commit loses every one of its MAX_PARENT_CONFLICT_ATTEMPTS: it stays pending with PARENT_CONFLICT and writes nothing. No behaviour change and no red step: the test guards existing behaviour, which the #24 review found untested. Green: 42 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #24 fix review found that `417d651` compares request times across every release in a repository. A change to one release is then refused with `NEWER_CHANGE_LANDED` because a later change to another release landed first, and two publications that land at once and both make their build current lose one build, which breaks plan section 7, item 5. New tests, with a second release (`microcosm-uk-2024-25-dense`) in the same repository: - the review's test: a national promotion requested before a dense promotion that landed first must land; - the review's trigger A through the module's queue: a national promotion that failed once must land after a later dense promotion; - the review's trigger B: two publications to the two releases that both make their build current must both land and both be current; - two publications to one release that both make their build current must both be listed, with the newer request's build current, whichever lands first (the older-first case passes today); - a stale national promotion is still refused after the dense release's later commit (passes today: the original fix must hold). Red: 4 failed, 44 passed. Each failure is the refusal `('refused', 'NEWER_CHANGE_LANDED')` where `('published', None)` is expected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`417d651` compared request times across every release in a repository. Order is now kept per release: - The writer's commit titles end `[newest requests: <release>=<time>; ...]`, each release's newest request time among the ordered changes applied to it, carried forward by every writer commit. A release's time leaves the titles once it is two hours (`ORDER_WINDOW_SECONDS`, twice the expiry) older than the newest time listed: every change requested before it has expired by then, unless two machines' clocks differ by over an hour. Without that limit, repositories that give each build a new release would grow the titles without end. - This machine's own landed changes are grouped by release too. - A promotion or withdrawal is refused with `NEWER_CHANGE_LANDED` only when an ordered change to the same release, requested after it, has landed. The original order fix is unchanged within a release. - A publication that makes its build current and is overtaken in that way is no longer refused: it lists its build, leaves `current` as the newer change set it, and reports `published` with a message saying so. Two publications that land at once, to one release or to two, therefore both keep their builds, and the newer request's build is current (plan section 7, item 5). The `releases.json` schema and the one-hour expiry are unchanged. Another machine's newer change that leaves `releases.json` unchanged still records no time, so an older change to that release can land within the hour. Three earlier tests asserted the old single-time title suffix; they now assert the per-release suffix. Two tests come with the fix: the overtaken publication's message and title, and the window that drops old release times from titles. The README, the `ReleaseReceipt` docstring and the changelog fragment describe the per-release rule. Green: 50 passed (`tests/huggingface/test_release_writer.py`); every test file this branch adds or changes: 208 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #24 found that a withdrawal counts as a newer change when the writer decides whether a change to a release was overtaken, although a withdrawal can never change `current` (`withdraw_build` refuses the current build and `make_current` refuses a withdrawn one). The four new tests are the review's triggers: - a publication that makes its build current, requested before an unrelated withdrawal that landed first, must still become current; - a promotion requested before an unrelated withdrawal must land; - a withdrawal requested before an unrelated promotion must land; - on one machine, a promotion that failed once must land after a withdrawal of another build, handed over later, landed first. Red: 4 failed, 50 passed. Each failure is the stated reason: `a later withdrawal of another build kept the publication from becoming current`, `a promotion was refused for a later withdrawal of another build`, `a withdrawal was refused for a later promotion of another build`, and `a queued promotion was refused for a later withdrawal of another build`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`is_ordered` becomes `sets_current`: true for a promotion and for a publication that makes its build current, and false for a withdrawal. A withdrawal therefore records no request time in the commit title, does not count among this machine's landed changes (`_newest_current_changes_landed`), and is never refused with `NEWER_CHANGE_LANDED`. A promotion, or a publication that makes its build current, is overtaken only by a later change of the same release that also sets `current`. A withdrawal cannot conflict with such a change: `withdraw_build` refuses the current build and `make_current` refuses a withdrawn one, so whichever of a withdrawal and a promotion of the same build lands second is refused by those rules, and changes to different builds give the same record in either order. The commit-title suffix keeps its form. The module docstring, the `ReleaseReceipt` docstring, the README and the changelog fragment now say that withdrawals are not ordered. Green: 54 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #24 found that a publication that asked to become current but was overtaken gets `status: published` and `error_code: None`, and only a prose `message` says its build is not current. A caller cannot check this from a field. The new tests expect a `current` field on every release receipt: - the review's trigger: another machine's later promotion lands while this machine's make-current publication waits after a transient error; the publication's receipt must name the other build; - the published receipts of a publication made current, a plain publication, a promotion, a promotion that writes nothing and a publication of a build already published must each name the build that is current; pending and refused receipts carry `current: None`; - through the interface client, a published receipt names the build, and an `unavailable` receipt carries `current: None`. Red: 4 failed (2 in `test_release_writer.py`, 54 passed there; 2 in `test_release_client.py`, 18 passed there). Each failure is the stated reason: `the receipt does not say which build is current`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ReleaseReceipt` gains `current: str | None`. On a `published` receipt it is the release's current build in `releases.json` once the change was applied (or, when nothing needed writing, as the writer found it), and `None` when the release has no current build; on `pending`, `refused` and `unavailable` receipts it is `None`. A publication with `make_current=True` made its build current exactly when its receipt is `published` and `current` equals its build, so H2c can tell "published and current" from "published, not current" without reading `message`. The writer sets `ReleaseOutcome.current` on its three `published` paths (a commit, a change that needed no commit, and a build already published), and the delivery copies it into the receipt. The hand-over receipts the module answers with (pending, and refused at hand-over) and the interface's `unavailable_release_receipt` carry `current: None`. The `publish_release` docstring now states the one-hour expiry and the cases where a make-current publication is `published` but not current. The README and the changelog fragment describe the field. The protocol fixtures record only request messages and the answer's top-level `receipt` key, so no fixture describes receipt fields and none changed. The behaviour of an overtaken make-current publication (it adds its build and leaves `current` with the newer request's build) is unchanged; it awaits the user's acceptance. Green: `test_release_writer.py` 56 passed, `test_release_client.py` 20 passed; every test file this branch changes, plus #23's `test_delivery_workers.py`: 224 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #24 (CODE QUALITY 1) found that the two-hour limit on commit-title times is measured from the newest time in the title. One machine whose clock is more than two hours fast therefore drops every other release's time from the titles, turning off the order check across machines for those releases, and its own time refuses every later change to its release on other machines until real time catches up. The in-memory Hub can now date its commits with a clock of its own (`commit_clock`) and report that date from `get_paths_info`, as the Hub does; commits stay undated unless a test sets it. With the Hub's clock standing still, the new tests check: - after a machine three hours fast promotes the dense build, a stale national promotion is still refused with `NEWER_CHANGE_LANDED`; - after a machine three hours fast promotes a national build, a later national promotion from another machine lands; - a guard: a time one minute after the Hub's date still orders changes. Red: 2 failed, 57 passed. The failures are the stated reasons: `a fast clock on another release's change turned off the order check for this release` and `a fast clock blocked a later promotion of its release on another machine`. The guard passes before and after the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Request times in writer commit titles come from each machine's clock, but every change is requested before its commit. Two changes bound what one fast clock can do, without changing the `releases.json` schema or the one-hour expiry: - The writer now reads the Hub's date for the commit that last changed `releases.json` from the same `get_paths_info` answer as its title (`DatasetRepository.last_commit_title_and_date`; no extra request). A time in that title more than `CLOCK_TOLERANCE_SECONDS` (5 minutes) after that date is ignored, and is not carried into the next title. A machine whose clock runs fast therefore no longer refuses later changes to its release on other machines until real time catches up; its own change is ordered only by its own queue and the expiry. - The two-hour window that drops old times from the titles now ends at that Hub date instead of at the newest time listed, so no machine's clock decides which other releases' times are dropped. Every change requested before a dropped time is more than two hours older than a commit that has already landed, so it has expired, as before. When the Hub reports no date, both fall back to the earlier behaviour. Clock differences within the tolerance still allow the misordering of changes requested closer together than the difference, as before; the module docstring and the README say so. Green: `test_release_writer.py` 59 passed; every test file this branch changes, plus #23's `test_delivery_workers.py` and `test_hub.py`: 244 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/release-writer-releases-json
branch
from
October 22, 2026 12:57
2db29df to
7a47f9a
Compare
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 #20
Last of four stacked pull requests (base: #23). Adds the release writer: each build is published side by side, and one
releases.jsonper dataset repository is the only record of what is current. Nothing calls the writer yet; Microcosm switches to it in H2c.What changes
microcosm_emitter_interface.releases(shared, so Microcosm, the emitter and a scheduled repository check run the same code):schema_version1,default(a release ornull), and per release itsrole,current(a build ornull) and each build'spublished_atandwithdrawn, serialized in that order with releases and builds sorted.add_build(optionally making it current),make_current,withdraw_build.check_record:currentnames a listed build that is not withdrawn;defaultnames a listed release;check_change: existing build entries change onlywithdrawn(none removed);check_repository: every listed build has its folderreleases/<release>/<build>/and arelease_manifest.jsonnaming the same release and build;check_commit: no file inside an existing build folder is added, changed or removed, listed or not; no write creates alatest*.jsonfile;currentcannot change while anylatest*.jsonfile exists (the migration window before the old pointer files are deleted).The writer (
microcosm_emitter.huggingface.service.releases), queued as arelease_changejob:publish_release(PublishRelease(repository, release, build, release_role, make_current, files)): files with roles, includingrelease_manifest.json.promote_buildandwithdraw_buildchange onlyreleases.json.request_id, a new random one unless the caller gives one, and the emitter applies each request once: sending the same request again returns its receipt, while a new request is applied even when it repeats an earlier change (promoting a build back, or retrying a withdrawal or a refused change once its cause is gone). The module queues the validated request's own message, never extra keys.releases.jsonand the file list atmain's head, apply the change, run the checks, commit the new build folder andreleases.jsontogether withparent_commitset to that head. On a parent-commit conflict (HTTP 409 or 412), read the new head and apply the change again, up to five times; two concurrent publications both land.BUILD_FOLDER_EXISTS), except when the build is listed with a byte-identical manifest: that reportspublishedwith the commit that wrote it, even if another build was made current since (a retry after a commit that landed but raised). Publishing never promotes an already published build.releases.jsonabove 4 MiB or nested too deeply to decode is refused asINVALID_RECORD, and such a manifest asMANIFEST_MISMATCH; neither is retried.current/folder, no mutable root file, nolatest*.json; existing tags and files are untouched.release.py's staging branch, release tag and pointer-file commit are replaced by this layout, not ported.pendingand are retried, for at most an hour (REQUEST_EXPIRED); a promotion, withdrawal or make-current publication that a newer such change overtook is refused (NEWER_CHANGE_LANDED); broken rules arerefusedwith the rule's code and messages. Release changes run on a worker thread of their own (see Queue Hugging Face staging uploads with one commit per repository per cycle #23'sDeliveryWorker). Release jobs skip the 10-second spacing between commits; bundles and run files keep it (they all still make at most one commit per repository per cycle).Left open, with extension points (not decided here)
HuggingFaceModule(release_change_policy=...)takes a function of the typed change and aReleaseCaller(the user the emitter runs as and the build process that started it) that returns a refusal reason orNone. The module applies it when the change is handed over, so a refusal (NOT_AUTHORIZED) is answered at once and never queued. The default allows every change; nothing configures it yet.currentmay staynull;make_current=Falsepublishes without promoting.withdrawn.default(the writer never changes it; a new record starts withnull), P1's migration write (it setscurrentwhile the pointer files still exist, whichcheck_commitrefuses, so it needs its own one-time path or a migration mode), and withdrawing the current build (refused; promote another build first).release_roleis an addition to the plan's call: a new release needs its role forreleases.json.Release
Two
addedfragments: merging releases the next minor version of both distributions.Validation
All tests use the in-memory Hugging Face API; nothing touches a real repository.
244672aAdd the releases.json record and its publication checks to the interfacetests/interface/test_releases_record.py: 26 of 26 fail (module missing)ca43761Publish release builds side by side through the release writertests/huggingface/test_release_writer.py,tests/interface/test_release_client.py: 29 of 29 fail. Fixture tests with the three new release fixtures: 5 failed, 39 passedBefore the review fixes, across the stack, every test file it adds or changes: 261 passed.
bash tools/check-artifacts.shexit 0;ruff checkandruff format --checkpass.Review fixes (2026-10-20)
Each fix was written red, then green; only the test files this pull request adds or changes were run. All tests use the in-memory Hub.
f5c8dafBuild release messages inside the client's error handlingpublish_release,promote_buildandwithdraw_build.tests/interface/test_client_errors.py: 2 failed, 16 passedtest_release_client.py9ad61beApply each requested release change; deduplicate only resent requestsrequest_id; their digests are updated)75ba8e9Report a landed publication as published after another promotion_already_publishedrefused a landed publication whose build was no longer current.test_release_writer.py: 2 failed, 23 passed21ba3d6Refuse deeply nested or oversized release records instead of retryingRecursionErrorfromjson.loadsbecame a pendingREPOSITORY_UNAVAILABLE, retried forever.test_releases_record.py,test_release_writer.py: 5 failed, 51 passed1fc8fb9Keep the commit spacing for every job except release changesMIN_COMMIT_SPACING_SECONDS, so bundles could commit in bursts again.test_release_writer.py: 1 failedtests/huggingface: 121 passed97a57f4Queue typed release changes and pass the caller to the change policydict(message)queued; string statuses;RELEASE_CHANGE,MAX_PARENT_CONFLICT_ATTEMPTSandUPLOAD_FAILEDdefined twice or more;core.auth._huggingface_tokenimported; the policy had no caller identity.test_boundaries.py,test_release_writer.py: 7 failed, 36 passedtests/huggingface,tests/interface,tests/telemetry/test_delivery.py(its monkeypatches follow the renamedhuggingface_token): 334 passedRe-run after the last commit, every test file this pull request adds or changes: 183 passed.
ruff checkandruff format --checkpass. The other commits' hashes changed when this branch was restacked.Not built here, as asked: P1's one-time migration path, which
check_commitstill refuses while pointer files exist.Review fixes, second round (2026-10-20)
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. All tests use the in-memory Hub.
1287dd1→417d651Expire queued release changes and refuse ones a newer change overtookCREDENTIAL_MISSING,UPLOAD_FAILEDincluding 401/403,REPOSITORY_UNAVAILABLE,PARENT_CONFLICT) stayedpending, was retried without limit and in backoff order, and was applied to whatever record it found. So it could land after a newer change from this or another machine and put back an oldercurrent, or land days later afterhf auth login.tests/huggingface/test_release_writer.py: 4 failed, 34 passed. The older build became current in both ordering cases; the change pending for two days waspublished; the expired change stayedpendingwithCREDENTIAL_MISSING. The two passing tests guard that changes handed over in order all apply and that a plain publication queued before a newer change still lands.a314547→0cb0a28Name a caller without a user name by its numeric user IDgetpass.getuser()raises when no user name variable is set and the user ID has no password-database entry (a container run with an arbitrary user ID), so every release change came backunavailable. The policy itself is unchanged (allow-all by default).test_release_writer.py: 1 failed, 40 passed (OSError: No username set in the environment)327be59Say that release changes skip the commit spacingMIN_COMMIT_SPACING_SECONDSand the queue docstring's "and later releases".7233dc6→baab6edAccept only the integer schema version and keep written records readableReleasesRecord.parseaccepted"schema_version": trueor1.0; the writer never checked that a record it writes stays within the 4 MiB it accepts when reading (check_changenow refuses it withRECORD_TOO_LARGE).tests/interface/test_releases_record.py: 3 failed, 29 passedtest_release_writer.py6bd4a45Share the writer's refusal for broken rules; test running out of racesPARENT_CONFLICT).The mechanism for queued release changes (also in
releases.py's docstring and the README):RELEASE_CHANGE_EXPIRY_SECONDS) becomesrefusedwithREQUEST_EXPIRED. That holds without a credential too, so a later login never applies it.NEWER_CHANGE_LANDEDwhen an ordered change requested after it has already landed on the repository's record. The writer learns that from two places:releases.json. Every writer commit's title now ends[newest request <time>]: the newest request time of the ordered changes applied so far.releases.jsonunchanged makes no commit and reports the head, which is what the Hub would do anyway.releases.jsonunchanged (promoting the build that is already current) makes no commit, so it records no time; only the expiry bounds how late an older change can still land after it. Request times come from each machine's clock.Restack onto #23's fixes. Some of this pull request's commits changed while it was rebased onto the second round of #23 fixes:
630afd4(the release writer) adds a release-change worker beside Queue Hugging Face staging uploads with one commit per repository per cycle #23's bundle worker;93a08d0(commit spacing) merges with Queue Hugging Face staging uploads with one commit per repository per cycle #23's per-kind delivery cycles;handover_database_path, andbackground_workers=Falsewhere they drive the module by hand.Re-run after the last commit, every test file this pull request adds or changes: 200 passed.
ruff check,ruff format --checkandtools/check-changelog.shpass.Unchanged, as open decisions: who may promote or withdraw (the policy stays allow-all), the manifest's
buildfield, role names and file list (H2c), andAGENTS.md's publishing rule.Fix round 2 (2026-10-20)
Restack onto #23's round-2 fixes. Every commit of this pull request has a new SHA; the titles are unchanged. The sections above use the earlier SHAs:
630afd4is now9c9c8da,93a08d0isb8bc990,1287dd1is568fce8,417d651is42e5686,a314547is65f7242,0cb0a28is0b8b3a7,327be59is69a3f48,7233dc6ise5eb811,baab6edis7998111and6bd4a45is7788f61. Conflict resolutions:9c9c8da(the release writer) keeps Queue Hugging Face staging uploads with one commit per repository per cycle #23's spacing helpers, leaves out Queue Hugging Face staging uploads with one commit per repository per cycle #23's spacing check in_claim_jobbecause this commit lets every job skip the spacing (as it did before), and gives the release-change worker Queue Hugging Face staging uploads with one commit per repository per cycle #23's stop check;b8bc990(commit spacing) restores that check for every job except release changes;69a3f48merges its comment onMIN_COMMIT_SPACING_SECONDSwith Queue Hugging Face staging uploads with one commit per repository per cycle #23's.So the commits from
9c9c8datoebd4893fail #23's new test that a bundle waits for the spacing after a run-file commit, as the original630afd4let bundles skip it;b8bc990and the head pass.943913f→35481d4Keep release order per release; an overtaken publication keeps its build417d651compared request times across every release in a repository. A change to one release was refused because a later change to another release had landed first, and two publications that landed at once and both made their build current lost one build (plan §7, item 5).tests/huggingface/test_release_writer.py: 4 failed, 44 passed. Each failure is the refusal('refused', 'NEWER_CHANGE_LANDED')where('published', None)is expected: the review's test (a national promotion requested before a dense promotion that landed first); its trigger A through the module's queue; its trigger B (concurrent publications to two releases, both making their build current); and concurrent publications to one release when the newer one lands first. The two passing tests guard the older-first order and that a stale promotion is still refused after another release's later commit.Design: order per release.
[newest requests: <release>=<time>; ...]: for each release, the newest request time among the ordered changes applied to it, carried forward by every writer commit.releases.jsonis unchanged.NEWER_CHANGE_LANDEDonly when a promotion, a withdrawal or a publication that makes its build current, to the same release and requested after it, has landed. Within one release the original fix holds.currentas the newer change set it, and reportspublishedwith amessagesaying so. Two publications that land at once, to one release or to two, therefore both keep their builds, and the newer request's build is current.releases.json's shape. Finding each release's last writer commit in the history instead would read the repository's whole commit history on every change.The remaining one-hour window, left open. A newer change on another machine that leaves
releases.jsonunchanged (for example, promoting the build that is already current) makes no commit, so it records no request time. An older change to the same release, queued on another machine, can then still land within one hour of its own hand-over; after that it is refused withREQUEST_EXPIRED. Closing this needs a change to thereleases.jsonschema, which is your decision: the schema and the one-hour limit are unchanged here.Re-run after the last commit, every test file this pull request adds or changes: 208 passed.
ruff check,ruff format --checkandtools/check-changelog.shpass.Fix round 3 (2026-10-20)
Restack onto #23's round-3 fixes. Every commit has a new SHA and the same title. Only
9c9c8da(now5088f71) differs as a patch: Git merged #23's new README sentence about shutdown into it without a conflict. The sections above use the round-2 SHAs:aec7519is nowd2bf70a,9c9c8dais5088f71,6fb10ebisa534280,b1a9fffis69e49f5,538a378is701c891,ebd4893isc32cac9,b8bc990ise3b50b4,865fa7ais3e3a2a6,568fce8is43dbdfb,42e5686is901269b,65f7242is7c5f4d6,0b8b3a7isc97c065,69a3f48isf2bffe4,e5eb811is95f02d2,7998111is6980d95,7788f61is3b0182e,943913fis149f0d4and35481d4is5583e82. As before, the commits from5088f71toc32cac9fail #23's test that a bundle waits for the spacing after a run-file commit;e3b50b4and every later commit pass it. #23's new round-3 test passes at every commit of this pull request.357c8d5→22fce01Order only changes that set current; withdrawals are never overtakencurrent, though it can never changecurrent. A make-current publication requested before an unrelated withdrawal was listed but silently not made current, and promotions and withdrawals were refused with no conflict.tests/huggingface/test_release_writer.py: 4 failed, 50 passed:a later withdrawal of another build kept the publication from becoming current;a promotion was refused for a later withdrawal of another build;a withdrawal was refused for a later promotion of another build;a queued promotion was refused for a later withdrawal of another build(on one machine, through the queue)57bd90d→cd73066Name the release's current build in every release receiptstatus: publishedanderror_code: None; only the prosemessagesaid so.test_release_writer.py: 2 failed, 54 passed;tests/interface/test_release_client.py: 2 failed, 18 passed. Each:the receipt does not say which build is currenttest_delivery_workers.pydccca0b→d1fa450Measure the title window from the Hub's date; ignore future timestest_release_writer.py: 2 failed, 57 passed:a fast clock on another release's change turned off the order check for this release;a fast clock blocked a later promotion of its release on another machine. A guard (a clock one minute fast still orders changes) passes before and after.test_delivery_workers.pyandtest_hub.pyDesign: only changes that set
currentare ordered.is_orderedbecomessets_current: true for a promotion and for a publication withmake_current=True, false for a withdrawal. A withdrawal records no request time in the commit title, does not count among this machine's landed changes, and is never refused withNEWER_CHANGE_LANDED. It cannot conflict with a change tocurrent:withdraw_buildrefuses the current build andmake_currentrefuses a withdrawn one, so whichever of the two lands second for the same build is refused by those rules, and changes to different builds give the same record in either order. The title suffix keeps its form. This replaces the round-2 "Refusals" bullet above.Design: the receipt's
currentfield.ReleaseReceiptgainscurrent: str | None:publishedreceipt, the release's current build inreleases.jsononce the change was applied (or, when nothing needed writing, as the writer found it), andNonewhen the release has no current build;Noneonpending,refusedandunavailablereceipts.So H2c can check
receipt["status"] == "published" and receipt["current"] == request.buildto know its build became current, without readingmessage. The writer fills it on its threepublishedpaths (a commit, a change that needed no commit, a build already published); the module's hand-over receipts andunavailable_release_receiptcarryNone. Thepublish_releasedocstring now states this and the one-hour expiry. The protocol fixtures record only request messages and the answer's top-levelreceiptkey, so no fixture describes receipt fields, and none changed. The behaviour itself is unchanged and still awaits your acceptance (Decision 19): an overtaken make-current publication adds its build and leavescurrentwith the newer request's build; its receipt now says so incurrent.Design: a bound on what one fast clock can do. Every change is requested before its commit, and the Hub dates every commit with its own clock. The writer now reads that date from the same
get_paths_infoanswer as the title (DatasetRepository.last_commit_title_and_date; no extra request).CLOCK_TOLERANCE_SECONDS(5 minutes) after the Hub's date for that commit can only come from a fast clock. It is ignored and not carried into the next title; that machine's change is then ordered only by its own queue and the expiry.releases.json, not at the newest time listed, so no machine's clock decides which other releases' times are dropped. A change requested before a dropped time is more than two hours older than a commit that has already landed, so it has expired, as before.commit_clock.releases.jsonschema and the one-hour limit are unchanged. Clocks that differ by less than the tolerance can still land changes requested closer together than that difference in the wrong order, as before. This replaces the round-2 "Title size" bullet above.Re-run after the last commit, every test file this pull request adds or changes, plus every test file that uses the in-memory Hub (its support file changed): 333 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.🤖 Generated with Claude Code