Skip to content

Queue Hugging Face staging uploads with one commit per repository per cycle - #23

Merged
anth-volk merged 23 commits into
mainfrom
feat/huggingface-staging-queue
Oct 21, 2026
Merged

anth-volk merged 23 commits into
mainfrom
feat/huggingface-staging-queue

Conversation

@anth-volk

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

Copy link
Copy Markdown
Contributor

Fixes #19

Third of four stacked pull requests (base: #22). Moves the transport of Microcosm's staging uploads into a new huggingface emitter module behind the interface. Microcosm keeps building and validating its staging documents and bundles; the emitter writes the bytes it is given, with the build's own credential. Nothing in Microcosm calls this yet (H2a and H2b switch it over).

What is ported

Microcosm code (41209b38c) Interface call Emitter behaviour
staging.py (contract version 1) and staging_v2.py uploads under runs/<run_id>/, through staging_storage.HuggingFaceDatasetStorage and BestEffortUploadSession stage_run_files(StageRunFiles(...)), run_files_receipt, read_back_run_files Same paths and bytes. Version 1's latest_staging.json is written at the root, and its run summary is merged into the repository's runs.json with Microcosm's ordering and formatting (staging._write_json).
staging_dataset.stage_bundle, used by uk_runtime/rowwise_staging.py and the Orrery evidence bundle stage_dataset_bundle(StageDatasetBundle(...)), dataset_bundle_receipt Same paths (staged/<run_id>/<file>), one commit pinned to the head it checked, the same commit message, and the same idempotency: a matching repository staged_manifest.json is already_staged with its own commit; a different one is REMOTE_DIFFERS and never overwritten. Each file is handed over with its role (dataset, calibration_diagnostics, build_manifest, staged_manifest, checksums, ...).
rowwise_staging.preflight_staged_dataset and HuggingFaceDatasetStorage.credential_can_write check_write_access Whether a credential is present, the repository is reachable, and the Hub reports a write scope.

Not ported: staging_dataset.fetch_bundle and tools/fetch_uk_staged_dataset.py (downloads, not hand-overs).

How the transport works

  • Durable queue in its own hand-over database (SQLAlchemy ORM only), a SQLite file next to the spool database (events.sqlite3 → events.handovers.sqlite3) with its own Alembic history (handovers_20261020_01). The shared spool database stays at 20261008_02, which every released emitter accepts (see the second round of review fixes below). staged_run_files keeps the newest handed-over bytes per repository path; staged_run_deliveries keeps per-run counts and the pending runs.json entry; repository_commit_schedules spaces commits, backs off and leases each repository to one writer on the machine; handover_jobs holds bundles (and, in the next pull request, release changes) with leases, retries and receipts, like graph_publication_jobs; handover_files holds the bundle files copied at hand-over. Rows are kept after delivery and nothing deletes them yet, except the copied bundle bytes, which are deleted once the hand-over settles.
  • Run files are copied, not referenced. The module reads each handed-over file and stores its bytes before answering, so the build may rewrite it immediately. Bytes equal to those last committed are not written again.
  • One Hugging Face commit per repository per cycle. Each module tick (on the host's worker thread) commits, for each repository with run files due, every pending path in one commit; bundle uploads run on a worker thread of the module's own. An unforced hand-over is due one interval after the repository's last commit (the build's upload interval, for example Microcosm's 300 seconds for UK runs); a forced one (final states, artifacts) at the next cycle. Either way, two commits from this machine to one repository, of run files or bundles, are at least 10 seconds apart; only the last commit at shutdown ignores that spacing, and other machines' commits are not counted. Before, each file was its own upload_file commit, up to eight per upload cycle, each triggering the dataset repository's webhook. Each delivery leases its repository first, so this machine never commits to one repository twice at once.
  • Best-effort for staging. Requests answer within seconds or report unavailable; nothing raises into the build. A failed commit keeps everything queued and backs off (2 s doubling up to 5 minutes), replacing BestEffortUploadSession's pause after three consecutive failures. Without a credential, work stays queued and one warning is printed. On shutdown the module commits every repository's pending run files once more, before it waits for any bundle upload; only a repository a bundle upload holds at that moment can miss that commit, if the upload outlasts the shutdown period. Warnings carry only error type names, never Hub messages.
  • Concurrent writers. A runs.json merge commits with parent_commit; if another build commits first, the index is read and merged again. A bundle commit that loses a race is checked again (it may now be already_staged) and retried.
  • Bundles: small files copied, large files verified. Before answering, the module copies the bundle's files smallest first, each up to 32 MiB and up to 64 MiB per hand-over, after checking each one's size and SHA-256 (FILE_SIZE_DIFFERS, FILE_DIGEST_DIFFERS); the receipt's copied_files names them, and those are uploaded as handed over. The worker re-hashes every other file before uploading; changed bytes are refused (PRESERVED_BYTES_CHANGED), missing files wait (PRESERVED_FILES_MISSING).
  • Leases. Repository and job leases last 120 seconds and are renewed every 30 seconds while a delivery runs, so a long upload keeps them and a killed emitter blocks the repository for at most 120 seconds.
  • Credential. Uploads use HfApi(token=…) with the token found in the same order as telemetry (core/auth.py), read again every cycle.
  • Shared path rules (microcosm_emitter_interface.identifiers) are checked by both the interface and the module; staging never writes under releases/.

Behaviour differences to note

  • runs.json: Microcosm started from an empty index on any download error, which could overwrite other runs. The module starts empty only when the Hub answers HTTP 404 for the file; every other error, including huggingface_hub's LocalEntryNotFoundError for connection errors, timeouts and 429/5xx responses, retries the cycle.
  • read_back_run_files of a run with no file this emitter committed (a refused hand-over, or another spool database) is failed with NO_COMMITTED_FILES, never passed.
  • Bundles: REPOSITORY_UNAVAILABLE and UPLOAD_FAILED were final after one attempt; they are now retried from the queue (the receipt keeps the last code while pending).
  • Delivery counts are reported per run as files attempted and committed, and commits attempted and made, so Microcosm's staging delivery summary (upload_attempts, upload_successes) can be filled from the emitter's receipt in H2a.

Release

Three added fragments: merging releases the next minor version of both distributions.

Validation

Every test uses an in-memory Hugging Face API (tests/support/fake_hugging_face.py): no network, no real repositories.

Commit Red (tests first) Green
cd31388 Add the durable Hugging Face hand-over queue and repository calls tests/huggingface/test_hub.py, test_queue.py: 29 of 29 fail (module missing). Modified tests/telemetry/test_persistence.py (new head revision and tables), run without the migration: 2 failed, 1 passed 32 passed; re-run: 32 passed
04f9805 Stage run files with one Hugging Face commit per repository per cycle tests/huggingface/test_run_files.py, tests/interface/test_staging_client.py: 38 of 38 fail. Fixture tests with the new fixtures expected: 8 failed, 30 passed 38 passed; fixtures 38 passed; with test_package_names.py 80 passed; check-artifacts.sh exit 0; re-run: 80 passed
473b576 Stage dataset bundles with each file's role through the queue tests/huggingface/test_bundles.py, tests/interface/test_bundle_client.py: 36 of 36 fail. Fixture tests: 5 failed, 36 passed 36 passed; fixtures 41 passed; re-run of tests/huggingface and tests/interface: 156 passed

For the next steps

  • I2 (collector delivery): handover_jobs is keyed by hand-over and destination (today only huggingface), so the same hand-over can get a collector row without a new table; the bundle hand-over already carries each file's role.
  • Jobs are claimed only for kinds this emitter delivers, so a job queued by a newer emitter sharing the database waits for it.

Overlap with open pull requests

Max's #5–#7 edit README.md, including the "Delivery and persistence" section, where this changes one sentence about migrations. #5 edits pyproject.toml and uv.lock.

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; none contacts a real repository.

Commit Finding Red (tests first) Green
09cf78b Build staging and bundle messages inside the client's error handling #22's fix carried up: stage_run_files, run_files_receipt, read_back_run_files, stage_dataset_bundle and check_write_access built their messages before the error handling. (When this branch was restacked, these call sites were first adapted to the new message-builder argument without changing behaviour; this commit makes them lazy.) tests/interface/test_client_errors.py: 4 failed, 12 passed 39 passed with the staging and bundle client tests
bdd659c Start runs.json empty only when the Hub reports the file missing is_absent matched any EntryNotFoundError/FileNotFoundError subclass, including LocalEntryNotFoundError, so a transient error wiped runs.json. tests/huggingface/test_hub.py, test_run_files.py: 5 failed, 42 passed (with real huggingface_hub error classes) 47 passed
e67f60e Copy a bundle's small files into the queue when it is handed over Microcosm's full_build_cli (at 41209b38c) rewrites the build manifest, sha256sums.txt and the national build record right after stage_dataset returns, so the queued upload failed with PRESERVED_BYTES_CHANGED. test_bundles.py, test_persistence.py: 5 failed, 2 errors, 24 passed 31 passed; with the interface bundle, fixture and error tests: 76 passed
d13e186 Never pass a read-back of a run with no committed files _read_back_run_files returned passed with files: 0. test_run_files.py: 2 failed, 30 passed 43 passed with test_staging_client.py
6a277f7 Renew repository and job leases during deliveries; shorten them to 120 s A 900-second lease outlived a killed emitter. tests/huggingface/test_leases.py: 3 failed tests/huggingface: 93 passed
04a612a Describe the runs.json merge, commit spacing and row retention exactly README wording on runs.json and the 10-second spacing; HandoverQueue's docstring said rows are kept only until delivered. Documentation only: no red step —

Re-run after the last commit, every test file this pull request adds or changes: 182 passed. ruff check and ruff format --check pass.

Finding 9: the design, and what Microcosm's H2b must do

Design (emitter side). The bundle's small files are copied into the hand-over database when the build hands the bundle over, as run files are; only files too large to copy (the datasets, which Microcosm never rewrites) are referenced by path and re-hashed before upload. The copy is checked against the declared size and SHA-256 when it is taken, so the upload carries exactly the bytes Microcosm verified with StagedDatasetBundle.from_manifest and listed in its sidecars. tests/huggingface/test_bundles.py::test_files_microcosm_rewrites_after_staging_are_uploaded_as_handed_over follows full_build_cli's order: hand over, rewrite the manifest atomically as replace_manifest does, append to sha256sums.txt as refresh_sha256sums_entry does, rewrite the build record, then run the cycle; the commit carries the handed-over bytes. That is also what Hugging Face held before, because the synchronous stage_bundle uploaded before those rewrites.

What H2b must do (uk_runtime/rowwise_staging.stage_dataset, used by _close_attempt and _close_national_attempt):

  1. Call client.stage_dataset_bundle(StageDatasetBundle(...)) where stage_bundle(bundle, storage=..., prefix=UK_STAGED_DATASET_PREFIX) is called today: after write_sidecars, inside stage_dataset, so before replace_manifest, refresh_sha256sums_entry and the national build-record rewrite. Do not rewrite any handed-over file before the call returns.
  2. Hand over every bundle file with its role and the SHA-256 and size it has on disk at that moment: dataset, calibration_diagnostics, build_manifest (rowwise_candidate_manifest.json), staged_manifest, checksums (sha256sums.txt) and, in national builds, the build record as its own output role (for example build_record).
  3. After the call, check that each file the close step rewrites (the manifest, sha256sums.txt, build_record.json) is in the receipt's copied_files. If one is not (a file above the copy limits, or an emitter that does not copy), wait for the hand-over to settle before rewriting it.
  4. Keep every file not in copied_files unchanged and in place until the receipt settles (uploaded, already_staged or failed). On a machine that ends with the build (Modal), wait for that before the build exits (wait_seconds, or poll dataset_bundle_receipt): the queue lives in the machine's hand-over database, and a host started later elsewhere cannot finish it.
  5. Map the receipt to staged_dataset's delivery record: revision from the receipt; pending is not yet staged; refused carries its code. Keep the preflight (preflight_staged_dataset) through check_write_access.

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; none contacts a real repository.

Commits (red → green) Finding Red (tests first) Green
3330414 → 2709caf Keep hand-overs in their own database so the shared spool stays released Migration 20261020_03 upgraded the shared spool database (by default ~/.cache/microcosm/telemetry/events.sqlite3). Released emitters 0.2.0 to 0.3.0, and #21's 0.4.0, refuse an Alembic revision they don't know, so a Microcosm build pinned to 0.2.1 on that machine would start with no telemetry and no Orrery queue. tests/test_queue_compatibility.py, tests/huggingface/test_queue.py, tests/interface/test_staging_client.py: 6 failed, 25 passed (CommandError: Can't locate revision identified by '20261020_03') 31 passed; 188 across this pull request's changed test files
5071bcc → 904c1c0 Upload bundles on their own worker so other commits never wait All repositories' commits ran one after another on the module's single worker thread, and the host runs the commit at shutdown on that thread after the current cycle. So a long bundle upload delayed the staging repository's run-file commits, the shutdown commit never ran if the build exited mid-upload, and a requested read-back answered pending. tests/huggingface/test_delivery_workers.py (a host runtime with a 2-second shutdown period, and a bundle commit held on an event): 3 failed, 1 passed. Run files never reached the staging repository; the runtime reported LocalServiceError: Service worker failed (TimeoutError); the read-back answered pending. The passing test checks the held bundle still lands once released. 4 passed; 192 across this pull request's changed test files
cfcb2f6 → 0393d26 Reach the five-minute retry cap, recommit re-handed bytes, guard waits Review code-quality items: retry_delay capped at 256 s, so MAX_RETRY_SECONDS (300) and the README's "up to five minutes" never applied; bytes equal to the last committed ones, handed over again while other bytes for the path were being committed, were left uncommitted; _wait_until_settled indexed receipt["handover_id"] outside the error handling. test_queue.py, test_client_errors.py: 3 failed, 34 passed 37 passed

Design: a separate hand-over database (finding on the shared spool). The five tables this stack adds (staged_run_files, staged_run_deliveries, repository_commit_schedules, handover_jobs, handover_files; #24 and #27 add none) live in their own SQLite file with their own Alembic history. core/handover_models.py holds them on their own declarative base, core/handover_alembic holds their one migration (renamed handovers_20261020_01, so neither history accepts the other's file), and core/migrations.py applies either history through one function. The migration was unreleased, so it was moved and renamed, not kept for compatibility. The path is explicit: HuggingFaceSettings(handover_database_path=…) names the file; by default it is events.handovers.sqlite3 next to the spool database. The settings refuse the spool file itself (invalid_configuration), and the module opens only that file. The unreleased configuration fixture and its digest changed with it. Tests: a spool database at 20261008_02, opened by this branch's EventSpool with the module started on the same spool_path, still resolves in a migration history built from only the two released migration files and stays at 20261008_02; a new hand-over database starts empty and migrates on first open (alembic check passes); a spool database is refused as a hand-over database. The installed-interface smoke check confirms the wheel creates the file.

Design: bundle uploads on their own worker (finding on a slow upload). The host gives each module one worker thread and runs close (the commit at shutdown) on it only after the current tick returns, so nothing that can take minutes may run inside the tick. DeliveryWorker (huggingface/service/workers.py) runs one kind of delivery cycle on a thread of its own and can tell an upload in progress from an idle wait. The module starts one for dataset bundles; the tick commits run files only. close stops the workers, commits every repository's pending run files that no worker is uploading to, gives an upload in progress until the shutdown deadline, then commits that repository's run files too if time remains. A repository's lease still keeps two writers off it, so run files for the repository a bundle is uploading to wait for that upload. #24's release changes and #27's collector submissions get their own workers through the same class. background_workers=False keeps every delivery on the tick, for tests that drive the module by hand. The README's shutdown sentence now says which repository can miss the shutdown commit.

Re-run after the last commit, every test file this pull request adds or changes: 196 passed. tools/check-artifacts.sh exit 0; ruff check, ruff format --check and tools/check-changelog.sh pass.

Not changed here: copied bundle bytes kept while a job stays pending (retention is undecided); run-file commits recorded without rechecking the repository lease (the commit has landed, so recording it is right); the module's single request thread for read-backs and write-access checks.

Fix round 2 (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, against the in-memory Hub.

Commits (red → green) Finding Red (tests first) Green
092b5d8 → 2a752c4 Start no bundle upload once shutdown begins After close called the bundle worker's stop, the cycle in progress still leased, claimed and uploaded the other repositories on its list. When such an upload outlasted the shutdown period, the second shutdown commit was skipped, so the run files of a repository whose upload had finished in time missed it, which the README says cannot happen. tests/huggingface/test_delivery_workers.py: 1 failed, 4 passed (AssertionError: an upload to the second repository started after shutdown began) 5 passed
a20a596 → 8d5a589 Check the commit spacing again once a repository is leased The 10-second spacing between this machine's commits to a repository was checked only when a cycle listed the repository. With bundles on their own worker, the host's thread and the worker could commit to one repository back to back. (#24 already rechecked it; this makes #23 correct on its own.) test_delivery_workers.py: 2 failed, 5 passed (the staging bundle was committed less than 10 seconds after its run files; the staging run files were committed less than 10 seconds after the bundle) 7 passed; 199 across this pull request's changed test files
0e810f2 Say how to repair a spool database an earlier build moved to 20261020_03 The repair note below, also in the README. Documentation only: no red step —

Design: no new upload once shutdown begins. DeliveryWorker now passes its stop check (stop_requested) to each cycle. The job cycle checks it before each repository on its list and again once that repository is leased, so after close calls stop the bundle worker starts no new upload. Only an upload already under way can then hold a repository during the commit at shutdown, which is the one exception the README names; that repository's run files get the second shutdown commit when the upload finishes in time. Cycles that no worker runs use never_stop.

Design: the spacing after the lease. Once a repository is leased, _claim_job claims no job and run_files_due reports no run files due until MIN_COMMIT_SPACING_SECONDS have passed since this machine's last commit to it. The helpers (_spacing_elapsed, commit_spacing_elapsed, _run_files_allowed) are the ones #24 uses, where release changes are exempt. The commit at shutdown still ignores the spacing.

Repair for a developer machine that ran an earlier, unreleased build of this branch. Before the hand-over database existed, this branch's migration 20261020_03 moved the shared spool database to that revision. No migration history contains that revision now, so on such a machine every emitter, this one and the released ones, refuses the spool database at startup (Can't locate revision identified by '20261020_03'), and builds run without telemetry or hand-overs. Nothing repairs it automatically. Stop every Microcosm build and emitter on the machine, then delete the spool database: $XDG_CACHE_HOME/microcosm/telemetry/events.sqlite3, or ~/.cache/microcosm/telemetry/events.sqlite3 when XDG_CACHE_HOME is unset (or the spool_path the build set), together with events.sqlite3-journal beside it if that file exists. Deleting it loses everything queued in it and not yet delivered: telemetry events not yet sent to the collector, graph publication jobs not yet published, and the run files and dataset bundles the earlier build handed over and had not committed. The next emitter creates a new spool database at 20261008_02. To keep the queued telemetry events and graph publication jobs instead, reset the revision with sqlite3 <spool file> "UPDATE alembic_version SET version_num = '20261008_02'": the earlier build added only the five hand-over tables, which every emitter then ignores, so their undelivered hand-overs are lost either way.

Re-run after the last commit, every test file this pull request adds or changes: 199 passed. tools/check-artifacts.sh exit 0; ruff check, ruff format --check and tools/check-changelog.sh pass.

Fix round 3 (2026-10-20)

Each code fix is a commit that adds a test failing because of the bug, then a commit with the code that leaves that test unchanged. Only the test files this pull request adds or changes were run, against the in-memory Hub. This section replaces the round-2 statements that the bundle worker "starts no new upload" and the round-2 repair command.

Commits (red → green) Finding Red (tests first) Green
7ccbf7a → ebf18c9 Give back a job claimed after shutdown began instead of uploading it The cycle checked for shutdown before each repository and once the repository was leased, but not after it claimed the job, so one upload could still start after shutdown began. tests/huggingface/test_delivery_workers.py: 1 failed, 7 passed (AssertionError: a bundle upload started after shutdown began, once its job was claimed) 8 passed
817d017 Guard the spool repair to revision 20261020_03 and drop its tables The repair note's UPDATE had no WHERE, kept the earlier build's tables, said "five tables", and said every emitter is affected. Documentation only: no red step —

Design: a check after the claim. _deliver_repository now checks for shutdown once more after it claims a job, immediately before the upload begins. A job claimed after shutdown began is given back with the new HandoverQueue.release_job, which clears its lease without counting an attempt, so it stays pending and due for the next emitter. The test begins shutdown from inside the job claim, after both earlier checks, so it covers this check alone. The README now states the exact guarantee: the worker checks before each repository, once the repository is leased, and once the job is claimed; during the commit at shutdown a repository can be held only by an upload that passed that last check before shutdown began, or, for the few database operations it takes to give it back, by a lease taken just before.

Repair note (replaces the round-2 note). On a developer machine that ran an earlier, unreleased build of this branch, the spool database is at revision 20261020_03, which no migration history contains now. An emitter started with the telemetry or orrery module, this one or a released one, refuses it at startup; an emitter started with only the huggingface module never opens the spool database and is not affected. After stopping every build and emitter on the machine, either delete the spool database (and events.sqlite3-journal beside it), losing everything queued in it, or keep the queued telemetry events and graph publication jobs with:

sqlite3 -bail <spool file> <<'SQL'
BEGIN;
-- Stop here, changing nothing, unless the file is at the earlier build's revision.
CREATE TEMP TABLE repair_guard (revision TEXT NOT NULL CHECK (revision = '20261020_03'));
INSERT INTO repair_guard VALUES ((SELECT version_num FROM alembic_version));
DROP TABLE IF EXISTS handover_files;
DROP TABLE IF EXISTS handover_jobs;
DROP TABLE IF EXISTS repository_commit_schedules;
DROP TABLE IF EXISTS staged_run_deliveries;
DROP TABLE IF EXISTS staged_run_files;
UPDATE alembic_version SET version_num = '20261008_02' WHERE version_num = '20261020_03';
COMMIT;
SQL
  • The tables dropped are the four or five the earlier migration created (handover_files only in its later form), checked against that migration at every earlier head (f438369, 473b576, 04a612a); it created no index. No spool revision on any branch of the stack or in any release uses these names, so dropping them does what that migration's own downgrade() did.
  • Unless the file's revision is 20261020_03, the guard row is refused, -bail stops sqlite3 at that error, and the open transaction is rolled back. So the command changes nothing in a spool database at any other revision, nor in the hand-over database (events.handovers.sqlite3), whose tables have the same names. I checked this with sqlite3 on scratch files at 20261020_03, at a later revision, at the hand-over revision 20261020_01, and with an empty version table.
  • No repair code is added: the repository's rules exclude schema repair in code.

Re-run after the last commit, every test file this pull request adds or changes: 200 passed. tools/check-artifacts.sh exit 0; ruff check, ruff format --check and tools/check-changelog.sh pass.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #25 October 19, 2026 21:21
@anth-volk
anth-volk force-pushed the feat/huggingface-staging-queue branch from f438369 to 473b576 Compare October 20, 2026 13:00
anth-volk added a commit that referenced this pull request Oct 20, 2026
The hand-over tables (staged run files, run deliveries, repository
schedules, jobs and copied files) move from the shared spool database to
a separate SQLite file with its own Alembic history:

- core/handover_models.py holds them on their own declarative base, and
  core/handover_alembic holds their one migration, renamed
  handovers_20261020_01 so neither history accepts the other's file;
- core/migrations.py applies either history through one function;
- the spool database's history ends at 20261008_02 again, the revision
  every released emitter (0.2.0 to 0.4.0) knows.

HuggingFaceSettings now sends handover_database_path: by default
events.handovers.sqlite3 next to the spool, or a file the build names,
never the spool file itself (invalid_configuration). The module opens
only that file. The unreleased configuration fixture and its digest
follow, the installed-interface smoke check confirms the wheel creates
the file, and the README and changelog fragment describe it.

Green: 31 passed (the three red files); 188 passed across #23's changed
test files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the feat/huggingface-staging-queue branch from 04a612a to 0393d26 Compare October 20, 2026 17:47
anth-volk added a commit that referenced this pull request Oct 20, 2026
The host gives each module one worker thread, and runs the module's
close (the commit at shutdown) on it only after the current tick
returns. Bundle uploads ran inside that tick, so a long one held up
every other repository's commits, the shutdown commit and read-backs.

- workers.DeliveryWorker runs one kind of delivery cycle on a thread of
  its own. It can tell an upload in progress from an idle wait, so
  close knows when the queue is safe to release. Later kinds (release
  changes, collector submissions) use the same class.
- HuggingFaceDelivery splits its cycle: run_file_cycle commits run
  files only, and job_cycle(kinds) delivers jobs of those kinds only.
  run_cycle still does both on the calling thread.
- HuggingFaceModule starts a dataset-bundle worker. The host's tick
  commits run files only. close first commits every repository's
  pending run files that no worker is uploading to, then gives an
  upload in progress until the deadline, and commits that repository's
  run files after it if time remains. Leases in the hand-over database
  still keep two writers off one repository.
- background_workers=False keeps every delivery on tick, for tests
  that drive the module by hand.

The README's statement that the module commits pending files once more
at shutdown now says which repository can miss that commit and why.

Green: 4 passed; 192 passed across #23'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 #23 review's code-quality notes:

- retry_delay capped its exponent at 8, so retries never waited the
  300 seconds MAX_RETRY_SECONDS and the README promise;
- bytes equal to the last committed ones, handed over again while other
  bytes for the path were being committed, were marked not pending and
  their copy cleared, so the repository kept the older commit's bytes;
- the interface's wait for a bundle to settle indexed the receipt's
  handover_id outside its error handling.

Red: 3 failed, 34 passed. "assert [] == [b'X']"; "At index 3 diff:
256.0 != 300.0"; "KeyError: 'handover_id'".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
The #23 fix review found that after `close` calls the bundle worker's
`stop`, the cycle already in progress still leases, claims and uploads
the remaining repositories on its list. When the upload to a second
repository outlasts the shutdown period, `wait` returns false, the second
shutdown commit is skipped, and the first repository's run files miss it
although that repository's upload finished in time; the README says they
are committed.

The test queues bundles for two repositories before the worker starts,
holds the first upload, begins shutdown, then releases it after half a
second. It asserts that no upload to the second repository starts and
that the first repository's run files land at shutdown.

Red: 1 failed, 4 passed (`AssertionError: an upload to the second
repository started after shutdown began`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
The #23 fix review found that the 10-second spacing between this
machine's commits to one repository is checked only when a cycle builds
its list of due repositories. Since bundles moved to their own worker,
the host's thread can commit to a repository on the worker's list (or
the other way round) while the worker uploads elsewhere, and the second
commit then follows at once.

Two tests, each with the clock standing still, make the other thread's
commit from inside the first commit of a cycle:

- a bundle cycle lists the private and staging repositories, and the
  staging run files are committed during the private upload: the staging
  bundle must wait for the spacing;
- a run-file cycle lists both repositories, and the staging bundle is
  committed during the private run-file commit: the staging run files
  must wait for the spacing.

Red: 2 failed, 5 passed (`AssertionError: the staging bundle was
committed less than 10 seconds after its run files`; `AssertionError:
the staging run files were committed less than 10 seconds after the
bundle`).

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 #23 found one window left after the stop checks:
the bundle cycle checks for shutdown before each repository and once the
repository is leased, but not after it claims the job. A shutdown that
begins between the post-lease check and the claim still starts that
upload. The new test begins shutdown from inside the job claim and
expects no upload, and the job left pending with its lease released and
its attempt count unchanged.

Red: 1 failed, 7 passed. The failure is the stated reason:
`AssertionError: a bundle upload started after shutdown began, once its
job was claimed`.

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 #23 asked for three changes to the README's repair
note for a spool database an earlier, unreleased build moved to
`20261020_03`:

- The revision reset now runs in one transaction that first inserts the
  file's revision into a temporary table whose check accepts only
  `20261020_03`. With `sqlite3 -bail`, any other revision (a later spool
  revision, or the hand-over database, whose tables have the same names)
  stops the command with nothing changed. The `UPDATE` also carries
  `WHERE version_num = '20261020_03'`.
- The same transaction drops the earlier build's tables, as that
  migration's own `downgrade()` did. Checked against the migration file
  at every earlier head (`f438369`, `473b576`, `04a612a`): it only
  created `staged_run_files`, `staged_run_deliveries`,
  `repository_commit_schedules`, `handover_jobs` and, from `04a612a`'s
  form on, `handover_files`, with no index. No spool revision at any
  branch of the stack or any release uses these names.
- The note says four or five tables, and that an emitter started with
  only the `huggingface` module never opens the spool database.

Documentation only; no red step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
`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>
anth-volk added a commit that referenced this pull request Oct 20, 2026
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
anth-volk force-pushed the feat/huggingface-staging-queue branch from 817d017 to 67fa198 Compare October 20, 2026 22:47
Base automatically changed from feat/emitter-interface-package to main October 21, 2026 19:03
@anth-volk
anth-volk force-pushed the feat/huggingface-staging-queue branch 2 times, most recently from 67fa198 to bd78514 Compare October 21, 2026 19:03
@anth-volk
anth-volk marked this pull request as draft October 21, 2026 21:35
anth-volk and others added 8 commits October 22, 2026 01:58
- Migration 20261020_03 adds four tables to the spool database:
  staged_run_files (the newest handed-over bytes per repository path),
  staged_run_deliveries (per-run counts and the pending runs-index
  entry), repository_commit_schedules (commit spacing, backoff and a
  per-repository lease) and handover_jobs (one row per hand-over and
  destination, with leases, retries and a receipt, like graph jobs).
- `HandoverQueue` records hand-overs without touching the Hub: bytes equal
  to those last committed are not pending, unforced hand-overs wait for
  their interval after the repository's last commit, forced ones keep a
  10-second spacing, and failures back off.
- `DatasetRepository` ports the calls of Microcosm's
  HuggingFaceDatasetStorage that the emitter needs, including
  credential_can_write, and recognises parent-commit conflicts (HTTP 409
  or 412). `hugging_face_api()` uses the build's credential in the same
  order as core/auth.py.
- Tests use an in-memory Hugging Face API (tests/support).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new `huggingface` emitter module takes over the transport of
Microcosm's staging writers (staging.py, staging_v2.py):

- `stage_run_files` copies the handed-over files' bytes into the queue
  before answering. They are written at `<prefix>/<run_id>/<path>` with
  the bytes the build wrote; staging contract version 1 also writes
  `latest_staging.json` and merges its run summary into the repository's
  `runs.json` with Microcosm's ordering and formatting.
- Each module tick makes at most one commit per due repository, with the
  newest bytes of every pending path. Unforced hand-overs wait for the
  build's upload interval after the last commit; forced ones go at the
  next cycle, at least 10 seconds after the previous commit.
- Failures keep files queued and back off; without a credential one
  warning is printed. Close commits pending files once more.
- A `runs.json` merge commits with `parent_commit` and, on a conflict,
  reads the index again and merges again.
- `read_back_run_files` compares the Hub's copies with the committed
  bytes.
- Requests are checked with the interface's shared path rules; staging
  never writes under `releases/`.
- Protocol fixtures for the new messages; the installed-wheel check
  starts the module without a credential.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transport of Microcosm's staging_dataset.stage_bundle (used by
uk_runtime/rowwise_staging.py and the Orrery evidence bundle) moves to
the emitter as a durable job:

- `StageDatasetBundle` names each file with its role (dataset,
  calibration_diagnostics, build_manifest, staged_manifest, checksums,
  ...), SHA-256 and size; exactly one staged_manifest is required.
- The worker re-hashes preserved files and refuses changed bytes, then
  writes every file under `<prefix>/<run_id>/` in one commit pinned to
  the head it checked, with Microcosm's commit message.
- A matching repository inventory is `already_staged` with its own
  commit; a different one is `REMOTE_DIFFERS` and is never overwritten.
- REPOSITORY_UNAVAILABLE and UPLOAD_FAILED are retried with backoff; a
  commit that loses a race is checked and tried again.
- `check_write_access` ports the staged-dataset preflight.
- Jobs are filtered by the kinds this emitter delivers, so a job queued
  by a newer emitter sharing the database waits for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The staging calls still built their messages before the client's error
handling: an unconvertible `run_index_entry`, a bundle file whose path
cannot be resolved, or an invalid repository or run identifier given to
`run_files_receipt`, `read_back_run_files` or `check_write_access` raised
into the build. `stage_dataset_bundle`'s unavailable receipt also rebuilt
the message, so it raised again.

Each call now passes a message builder that the client runs inside its
error handling, as the telemetry and Orrery calls do since the previous
pull request; a failure returns the call's unavailable result and records
`invalid_arguments` in `last_failure`. The unavailable bundle receipt has an
empty `handover_id` when the files cannot be converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`commit_run_files` started `runs.json` empty whenever `hub.is_absent`
matched the download error, and `is_absent` matched any error class named
`EntryNotFoundError` or `FileNotFoundError` in the error's hierarchy.
huggingface_hub's `LocalEntryNotFoundError` subclasses both, and
`hf_hub_download` raises it for connection errors, timeouts and 429 or 503
responses when the file is not cached. A transient error therefore
committed an index without every other run.

`is_absent` now holds only for an entry-not-found error that carries the
Hub's HTTP 404 response, and never for `LocalEntryNotFoundError`, a missing
revision or repository, or a local `FileNotFoundError`. Any other error
fails the commit, keeps the run files and index entry queued, and retries
after the backoff. Tested with real huggingface_hub error classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Microcosm's `full_build_cli` (at 41209b38c) rewrites three bundle files
right after `stage_dataset` returns: the build manifest
(`replace_manifest`), the checksum listing (`refresh_sha256sums_entry`)
and, in national builds, the build record. With the queued upload the
worker re-hashed those files and failed the hand-over permanently with
`PRESERVED_BYTES_CHANGED`.

- Before answering, the module copies the bundle's files smallest first,
  each of at most 32 MiB and together at most 64 MiB, after checking that
  each holds its declared size and SHA-256 (`FILE_SIZE_DIFFERS`,
  `FILE_DIGEST_DIFFERS`). The copies are stored with the job in a new
  `handover_files` table (added to the unreleased revision `20261020_03`)
  and named in the receipt's `copied_files`.
- The worker uploads the copied bytes as they were handed over, and still
  re-hashes and refuses changed bytes of the larger files it uploads from
  their paths (the datasets, which Microcosm does not rewrite).
- The copies are deleted once every destination of the hand-over has
  settled.

A test follows Microcosm's order: hand over, rewrite the manifest atomically,
append to the checksum listing and rewrite the build record, then run the
cycle; the commit carries the handed-over bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_read_back_run_files` returned `passed` with `files: 0` for a run without
committed rows, for example after a refused hand-over or when the run was
staged through another spool database. Microcosm's
`validate_staging_delivery` rejects a passed read-back with zero uploads.
Such a read-back is now `failed` with the error code `NO_COMMITTED_FILES`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every repository and job lease lasted 900 seconds and was never renewed, so
an emitter that was killed during a delivery kept the next emitter on the
same machine from committing to that repository for up to 15 minutes.

Leases now last 120 seconds. While a delivery runs, `HeldLeases` renews the
repository lease and the claimed job's lease every 30 seconds on a
background thread, and stops before the job's attempt is recorded and the
repository is released, so both use the renewed lease. A long upload keeps
its leases; a killed emitter's lease expires within 120 seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 15 commits October 22, 2026 01:58
Documentation only:

- README: `runs.json` starts empty only when the Hub reports it missing;
  other download errors retry the commit (Microcosm started empty on any
  error). The 10-second spacing applies to every commit from this machine
  to a repository, of run files or bundles, forced or not, except the last
  commit at shutdown; it does not count other machines' commits.
- `HandoverQueue`: rows are kept after delivery and nothing deletes them,
  except the copied hand-over bytes once the hand-over settles. The earlier
  docstring said records were kept only until delivered.
- `StageRunFiles` and `MIN_COMMIT_SPACING_SECONDS` say the same about the
  interval and spacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Released emitters (0.2.0 to 0.4.0) refuse a spool database at a revision
they don't know. Opening this branch's spool, or starting the Hugging
Face module on it, moved the shared database to 20261020_03, so a build
pinned to 0.2.1 on the same machine would start with no telemetry and no
Orrery queue.

The tests ask for the hand-over tables in a separate database next to
the spool, with its own migration history, and for HuggingFaceSettings
to name that file explicitly and refuse the spool file itself.

Red: 6 failed, 25 passed. The compatibility test fails with
"CommandError: Can't locate revision identified by '20261020_03'".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hand-over tables (staged run files, run deliveries, repository
schedules, jobs and copied files) move from the shared spool database to
a separate SQLite file with its own Alembic history:

- core/handover_models.py holds them on their own declarative base, and
  core/handover_alembic holds their one migration, renamed
  handovers_20261020_01 so neither history accepts the other's file;
- core/migrations.py applies either history through one function;
- the spool database's history ends at 20261008_02 again, the revision
  every released emitter (0.2.0 to 0.4.0) knows.

HuggingFaceSettings now sends handover_database_path: by default
events.handovers.sqlite3 next to the spool, or a file the build names,
never the spool file itself (invalid_configuration). The module opens
only that file. The unreleased configuration fixture and its digest
follow, the installed-interface smoke check confirms the wheel creates
the file, and the README and changelog fragment describe it.

Green: 31 passed (the three red files); 188 passed across #23's changed
test files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All repositories' commits ran one after another on the module's single
worker thread, and the commit at shutdown ran on that thread after the
current cycle. While a bundle upload to one repository was in progress,
run files for another repository waited, the shutdown commit never ran
before the host gave up, and a requested read-back answered pending.

The tests run the module in a host runtime with a 2-second shutdown
period and an in-memory Hub whose bundle commit blocks on an event.

Red: 3 failed, 1 passed. Run files never reached the staging repository;
the runtime reported "LocalServiceError: Service worker failed
(TimeoutError)" at shutdown without the staging commit; the read-back
answered "pending". The passing test checks that the blocked bundle
still lands once released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host gives each module one worker thread, and runs the module's
close (the commit at shutdown) on it only after the current tick
returns. Bundle uploads ran inside that tick, so a long one held up
every other repository's commits, the shutdown commit and read-backs.

- workers.DeliveryWorker runs one kind of delivery cycle on a thread of
  its own. It can tell an upload in progress from an idle wait, so
  close knows when the queue is safe to release. Later kinds (release
  changes, collector submissions) use the same class.
- HuggingFaceDelivery splits its cycle: run_file_cycle commits run
  files only, and job_cycle(kinds) delivers jobs of those kinds only.
  run_cycle still does both on the calling thread.
- HuggingFaceModule starts a dataset-bundle worker. The host's tick
  commits run files only. close first commits every repository's
  pending run files that no worker is uploading to, then gives an
  upload in progress until the deadline, and commits that repository's
  run files after it if time remains. Leases in the hand-over database
  still keep two writers off one repository.
- background_workers=False keeps every delivery on tick, for tests
  that drive the module by hand.

The README's statement that the module commits pending files once more
at shutdown now says which repository can miss that commit and why.

Green: 4 passed; 192 passed across #23's changed test files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the #23 review's code-quality notes:

- retry_delay capped its exponent at 8, so retries never waited the
  300 seconds MAX_RETRY_SECONDS and the README promise;
- bytes equal to the last committed ones, handed over again while other
  bytes for the path were being committed, were marked not pending and
  their copy cleared, so the repository kept the older commit's bytes;
- the interface's wait for a bundle to settle indexed the receipt's
  handover_id outside its error handling.

Red: 3 failed, 34 passed. "assert [] == [b'X']"; "At index 3 diff:
256.0 != 300.0"; "KeyError: 'handover_id'".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- retry_delay lets its exponent grow until MAX_RETRY_SECONDS (300 s)
  applies, as the README says.
- A path handed over again with the bytes last committed, while other
  bytes for it are pending, keeps those bytes; when the in-progress
  commit is recorded and the row's latest bytes differ from what it
  committed, the row is pending again, so the next commit restores the
  newest bytes.
- The interface waits for a hand-over to settle only when its receipt
  names a hand-over ID, and otherwise returns the receipt as answered.

Green: 37 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #23 fix review found that after `close` calls the bundle worker's
`stop`, the cycle already in progress still leases, claims and uploads
the remaining repositories on its list. When the upload to a second
repository outlasts the shutdown period, `wait` returns false, the second
shutdown commit is skipped, and the first repository's run files miss it
although that repository's upload finished in time; the README says they
are committed.

The test queues bundles for two repositories before the worker starts,
holds the first upload, begins shutdown, then releases it after half a
second. It asserts that no upload to the second repository starts and
that the first repository's run files land at shutdown.

Red: 1 failed, 4 passed (`AssertionError: an upload to the second
repository started after shutdown began`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`DeliveryWorker` now calls its cycle with `stop_requested`, which turns
true when `close` calls `stop`. The job cycle checks it before each
repository on its list and again once that repository is leased, so after
shutdown begins the bundle worker starts no new upload. Only an upload
already under way can then hold a repository during the commit at
shutdown, which is the one exception the README names; that repository's
run files get the second shutdown commit when the upload finishes in time.

Cycles that no worker runs (the host's tick, and modules driven by hand)
use `never_stop`.

Green: 5 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #23 fix review found that the 10-second spacing between this
machine's commits to one repository is checked only when a cycle builds
its list of due repositories. Since bundles moved to their own worker,
the host's thread can commit to a repository on the worker's list (or
the other way round) while the worker uploads elsewhere, and the second
commit then follows at once.

Two tests, each with the clock standing still, make the other thread's
commit from inside the first commit of a cycle:

- a bundle cycle lists the private and staging repositories, and the
  staging run files are committed during the private upload: the staging
  bundle must wait for the spacing;
- a run-file cycle lists both repositories, and the staging bundle is
  committed during the private run-file commit: the staging run files
  must wait for the spacing.

Red: 2 failed, 5 passed (`AssertionError: the staging bundle was
committed less than 10 seconds after its run files`; `AssertionError:
the staging run files were committed less than 10 seconds after the
bundle`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A developer machine that ran an earlier, unreleased build of this branch
has its spool database at revision `20261020_03`, which no migration
history contains now, so every emitter there refuses to start. The README
now names the file to delete (and its rollback journal), says what
deleting it loses, and gives the revision reset that keeps the queued
telemetry events and graph publication jobs. No repair code is added for
an unreleased revision.

Documentation only; no red step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #23 found one window left after the stop checks:
the bundle cycle checks for shutdown before each repository and once the
repository is leased, but not after it claims the job. A shutdown that
begins between the post-lease check and the claim still starts that
upload. The new test begins shutdown from inside the job claim and
expects no upload, and the job left pending with its lease released and
its attempt count unchanged.

Red: 1 failed, 7 passed. The failure is the stated reason:
`AssertionError: a bundle upload started after shutdown began, once its
job was claimed`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_deliver_repository` now checks for shutdown once more after it claims
a job, immediately before the upload begins. A job claimed after
shutdown began is given back with the new `HandoverQueue.release_job`,
which clears its lease without counting an attempt, so the job stays
pending and due for the next emitter.

The README, the `workers` module docstring and the delivery docstrings
now state the exact guarantee: the worker checks before each repository,
once the repository is leased, and once the job is claimed. During the
commit at shutdown a repository can be held only by an upload that
passed that last check before shutdown began, or, for the few database
operations it takes to give it back, by a lease taken just before.

Green: 8 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #23 asked for three changes to the README's repair
note for a spool database an earlier, unreleased build moved to
`20261020_03`:

- The revision reset now runs in one transaction that first inserts the
  file's revision into a temporary table whose check accepts only
  `20261020_03`. With `sqlite3 -bail`, any other revision (a later spool
  revision, or the hand-over database, whose tables have the same names)
  stops the command with nothing changed. The `UPDATE` also carries
  `WHERE version_num = '20261020_03'`.
- The same transaction drops the earlier build's tables, as that
  migration's own `downgrade()` did. Checked against the migration file
  at every earlier head (`f438369`, `473b576`, `04a612a`): it only
  created `staged_run_files`, `staged_run_deliveries`,
  `repository_commit_schedules`, `handover_jobs` and, from `04a612a`'s
  form on, `handover_files`, with no index. No spool revision at any
  branch of the stack or any release uses these names.
- The note says four or five tables, and that an emitter started with
  only the `huggingface` module never opens the spool database.

Documentation only; no red step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the feat/huggingface-staging-queue branch from bd78514 to 8fa62eb Compare October 21, 2026 21:58
@anth-volk
anth-volk marked this pull request as ready for review October 21, 2026 22:30
@anth-volk
anth-volk merged commit 04ae70a into main Oct 21, 2026
10 checks passed
@anth-volk
anth-volk deleted the feat/huggingface-staging-queue branch October 21, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move Microcosm's Hugging Face staging uploads into the emitter, with one commit per repository per cycle

1 participant