Repository navigation
Conversation
Every emitter service delivered every pending run in the host's shared spool and judged each against its own Hugging Face credential, so a build started without a login (or with one the collector refused) permanently made every other build's queued run local-only. The collector gives a run to the Hugging Face user who registers it first, so a service now delivers its own run, and another producer's run only once that producer's service has exited: each service holds an advisory flock on a per-producer lease file beside the spool, which the kernel drops on any exit. A run with no lease file (an older checkout) is adopted only after 15 idle minutes. An answer about the caller's credential (none, refused, not the run's owner) decides only the caller's own run; an adopted run goes local-only only for an answer about the run itself. A refused credential is exchanged once, and the shutdown drain waits only for the build's own run. No spool migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A delivery pass lists the pending runs once, then waits on the collector for seconds. Meanwhile another build's service can make its own run local-only and exit, freeing its lease, and the pass then adopted that run and uploaded it under its own login. The two-service test caught it. The run's state is now read again while it is held. Tests: a deterministic case for that interleaving; the property test can now run one service while another's request is in flight, and checks that nothing about a run reaches the collector after it goes local-only (the shrunk counterexample is pinned as an example). The two-service test waits for build B's first pass instead of sleeping, and fails on origin/main with build A's run local-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Microcosm's main has no leases until PolicyEngine/microcosm#1177 merges, so the README, changelog and comments now say from when the two packages' services recognise each other's leases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Finding from the microcosm-emitter port of this PR (PolicyEngine/microcosm-emitter#5), reproduced there; the code is identical here at A run passed over under a login the collector refused is never offered again by the same service after its login changes. Reproduction (two services):
The run is not lost, since another service can still adopt it, but this one never does. The property test misses it because its final liveness check starts fresh rescuers, which have no pass-over memory. Fix in the emitter, commit The same commit has two more changes that apply here:
Two claims in this PR's body were also flagged as broader than the tests: the two-process test's 1.5 s sleep does not show B made a second pass, and "right after a collector answer" was checked against any earlier answer, not the latest. This PR should take the same fix before it merges. 🤖 Generated with Claude Code |
Review round 1 (GPT-6.1 Sol, at 662fd4a) reproduced four defects. Each has a test that fails on 662fd4a and passes now. 1. A service that could not lock its own lease still served. The first lock error ended the attempt, and the attempt had already created the lease file. Other services read a lease file nobody holds as an exited producer's, so they adopted the live run and registered it under their own login. Now the attempt is repeated until its deadline, and a service that still has no lease does not start: the build goes on without telemetry and says why. A platform with no advisory locks is unchanged. 2. A run passed over under a login the collector refused was never offered again after the login changed. With the service's own run local-only and every other pending run passed over, nothing read the login, so the pass-over check kept comparing against the old one. Each pass with passed-over runs now notes the credential its requests would use. A session the collector issued stays in use until it expires, as for the own run. 3. The delivery's per-run memory was keyed by "run_id:producer_id". Both ids may contain colons, so two runs could share a key: a refusal for one passed the other over, and registering one counted for the other. The key is now the id pair. 4. leases._lock left its descriptor open, and the lock held, when the inode check itself raised. Tests: the property test's last two services now start under generated logins, make a pass, and only then become Alice and Bob with their sessions expired, so recovery by a service that was refused first is checked. It also generates run ids the collector answers with 409. A local-only decision is checked against the deciding service's latest collector answer, not any earlier one. The fake clock the session tests need replaces time.monotonic in the delivery during each flush. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests the microcosm-emitter port has and this branch lacked: - an adopted run whose events the collector refuses with 403 (another user owns it) is passed over, not made local-only, and its owner delivers it; - the lease directory, file name, digest and modes are pinned to one id pair, since services from different versions of either package must agree on them to see each other's live runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports two fixes from PolicyEngine/microcosm#1177 (1c9bd671b), whose review found them in the code this PR copies. Each has a test that fails on 07b2c36 and passes now. 1. A service that could not lock its own lease still served. hold() gave up on the first lock error, after os.open(O_CREAT) had already made the lease file, and other services read a lease file nobody holds as an exited producer's: they adopted the live run at once and registered it under their own login. hold() now repeats an attempt that errs until its deadline. A delivery that still has no lease raises OwnLeaseUnavailableError where advisory locks exist, so TelemetryModule.initialize prints why and the host exits before readiness; the build goes on without telemetry. A platform with no advisory locks is unchanged. 2. The delivery's per-run memory (_registered, _passed_over, _warned_denied) was keyed by "run_id:producer_id". Both ids may contain colons, so two runs could share a key: a refusal for one passed the other over, and registering one counted for the other. The key is now the (run_id, producer_id) pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lost run again Three follow-ups to the settled-rejection rule in the telemetry emitter service's collector delivery. A refused registration printed the "credential was not accepted as a PolicyEngine organization member" warning, because `_make_local_only` defaulted to it. That was false for a 409 (the owner registering with other metadata), a 422 and any other settled 4xx, and for a 403 on a registration or on events, where the collector had accepted the login and refused it the run. A rejected registration now names its HTTP status, a 403 names the Hugging Face user, and the message no longer has a default. A batch was bounded by count only. A full batch of the largest events (non-ASCII messages, details at their limit) is over the collector's 1 MiB request limit, which it answers with 413 for the whole request, and that made the run local-only. A batch is now the longest run of leading events whose request body fits, measured with the serialization `_http_post` sends. An event too large to send alone is sent alone, for the collector to refuse. A 404 for a run's events means the collector does not have a run this service registered. The service now registers the run again and sends the same batch on the next pass; a second 404 with no batch acknowledged in between makes the run local-only, as before. Stacked on #1177 (1c9bd67). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Every build on a host shares one telemetry spool (
~/.cache/microcosm/telemetry/events.sqlite3), and every emitter service's delivery worker walked every pending run in it (CollectorDelivery.flush_onceoverspool.pending_runs()). For each run it read its own process's Hugging Face credential. With none, it called_make_local_only(registration, missing_huggingface_credential), which permanently setsupload_state = local_onlyon that run, another build's run included. The same applied to a token exchange the collector refused (401/403), a registration refused with 403 and an events batch refused with 403.So one build started without a credential (a CI shell, a terminal with no
HF_TOKEN, another login) permanently disabled hosted telemetry for every other build's queued events. An independent check on 2026-10-09 reproduced it at 7f23594: two producers share a spool, and oneflush_onceby build B's worker, with no credential, left('build-a', 'local_only', 'missing_huggingface_credential').How the collector authorizes (read for this change)
From PolicyEngine/calibration-diagnostics @ 3afb94d,
telemetry-service/telemetry_collector/(app.py,auth.py,storage.py,models.py):POST /v1/auth/huggingface/exchange: the HF token is checked againstwhoami-v2. An invalid token gets 401, a non-member ofpolicyenginegets 403, and an unavailable HF gets 503. Success returns a JWT whosesubis the HF user id.POST /v1/runs: the first registration of a run id records the caller asowner_hf_id. A registration by another HF user gets 403 ("Run belongs to another Hugging Face user"). The owner with different metadata gets 409.POST /v1/runs/{run_id}/events: an unknown run gets 404, checked before the owner. An owner mismatch or an unregistered producer gets 403. A batch that fails the schema gets 422. A body over 1 MiB gets 413 from a middleware that runs before the route.RunRegistrationisextra="forbid", so no local bookkeeping can ride in the stored registration (an older checkout delivering it would get 422).^[A-Za-z0-9][A-Za-z0-9._:-]{0,191}$, so both may contain colons.So a run belongs to whoever registers it first. Delivering another producer's run with your own credential has three outcomes:
Decision: who delivers which run, with whose credential
Liveness: per-producer advisory leases, no spool migration
flockon<spool>.leases/<sha256(run_id NUL producer_id)>.lockwhen its delivery is built inmain(), before the service is ready. Its run therefore never has an event without a live lease. The service holds the lease for its whole life, and the kernel drops it on any exit, SIGKILL included.main()prints one line and exits 1, and the build's emitter falls back to its disabled handle, as for any service that does not become ready. The reason is that a failed attempt has already created the lease file, and a lease file nobody holds is how other services recognise an exited producer.ORPHAN_IDLE_SECONDS(15 min) without an update. Live services append a heartbeat every 60 s by default.upload_statevalues and reason strings are unchanged.Change
leases.py(new):ProducerLeases(hold,try_acquire,sweep),ProducerLeaseandOwnLeaseUnavailableError.collector.py:CollectorDelivery(spool, registration, …)now knows its own run and holds its lease. The constructor raisesOwnLeaseUnavailableErrorif it cannot take it (except on a platform withoutfcntl).flush_onceclaims each run first (_claim): the own run always; another run only under its lease, or under the idle fallback.run_has_deliverable) before sending anything. A pass reads its list of pending runs once, then can wait seconds on the collector. Meanwhile a run's own service may have made the run local-only and exited, freeing its lease. Without this check the pass adopted that run and uploaded it under its own login. The two-process test below found this._make_local_only(…, caller_dependent=…)is the one gate. Registration 403 is now split from 409 and the other settled 4xx._note_current_credential), so a run passed over under one login is offered again once the login changes. A session the collector issued stays in use until it expires, as it does for the own run onmain, so the new login counts from then._registered,_passed_over,_warned_denied) is keyed by the(run_id, producer_id)pair.mainkeys the first and last by"run_id:producer_id", which two runs can share.runtime._drainwaits only for the build's own run (EventSpool.run_has_deliverable). Before, every shutdown could wait out its full drain while another build had queued events, because those events are no longer this service's to deliver.main.pybuilds the delivery before the service runs and closes it afterwards.spool.pygainsrun_has_deliverableandlast_updated.Invariants
Each is checked by
test_telemetry_foreign_runs.py, within the fake collector's model of the hosted one. Invariants 1 to 5 are also checked after every step of a Hypothesis property test (150 examples in CI) over 2–4 running builds plus up to 30 random steps:The model leaves out lock failures, ids with colons, oversized bodies (413) and real retry timing; the first two have their own tests below.
main()with this change holds its lease whenever it is serving, because it does not start without one. Two kinds of service have no lease and are protected only by the 15-minute idle rule, which is a heuristic: one from an older checkout, and one on a platform withoutfcntl.run_registration_rejected, and a run whose batches get 422 goes local-only withcollector_rejected_events. The property test checks this with two last services that start under generated logins, make a pass, and then become alice and bob, the only owners in the model.test_telemetry_emitter.pycases pass with two changes only: the new constructor argument, and the drain test's fake spool answering the scoped query.Deterministic tests:
a:b/canda/b:c): a refusal for one does not pass over the other, and registering one does not count for the other;main()returns 1, prints its one line and never serves;build-alocal-only formissing_huggingface_credential, which is the reported bug.Review
Round 1 (GPT-6.1 Sol, at
662fd4acb) returned REQUEST_CHANGES with four should-fix findings, each executed by the reviewer.1c9bd671baddresses them, and615d84f89adds two tests the microcosm-emitter port already had (an adopted run's events 403 is passed over; the lease file layout is pinned). Each finding has a test that fails on662fd4acband passes now (checks/r1_new_tests_fail_on_662fd4acb.txtin the evidence folder).maindoes for the own run, and a test pins it.("a:b", "c")and("a", "b:c")shared the key"a:b:c", so Bob's 403 for the first suppressed his deliverable second run. The memory is keyed by the id pair now._registeredhad the same key onmain, with a worse effect once runs are adopted (the second run's registration is skipped, its events get 404 and the run goes local-only), so it is fixed too._locknow closes the descriptor first.Also from its notes:
Mutation check
23 runtime mutants were injected through a pytest plugin that patches module globals, so no tracked file was edited. Each ran on
615d84f89against both touched test files, and each was killed by the tests that target it: every listed test failed on an assertion or on the exception it expects, and no run had a setup or collection error. The 12 round-1 mutants are joined by 11 for the review fixes.no_gate,no_recheck,no_own_leaseanddrain_everythingwere rewritten for the code as it now is.original_bugno_gate_make_local_onlygate (main's version)no_claimgate_all_foreignconflict_caller_dependentno_recheckno_passed_over[403, 403, 403] != [403]), session wait-out, colliding idsno_credential_memono_credential_refreshrefresh_ignores_session[403, 403] != [403])no_own_leaseown_lease_optional662fd4acb's lease code: give up on the first error, then serve lease-less (finding 1)start_without_leasehold_gives_up_on_errorclose_keeps_leasealways_idledrain_everythingmain's drain over every runAttributeError, because their fake spool now answers only the scoped querysweep_heldtrust_replaced_fileunchecked_lease_leakjoined_pass_over_keysrun_id:producer_id(finding 3)joined_registered_keyswarn_for_foreignThe gate and the claim are independent protections. The e7 regression test fails only when both are removed (
original_bug); each one alone is caught by its own tests.Verification
Local runs are on macOS with Python 3.14.7 (free-threaded build), on a heavily loaded host. Each is labelled with the head it ran on;
615d84f89changes only tests after1c9bd671b.615d84f89).test_telemetry_foreign_runs.pyandtest_telemetry_emitter.py: 70 passed, 1 deselected. The deselected test istest_subprocess_exchanges_ambient_token_and_delivers_events: importing the service takes 5 to 6 s on this host, past that test's 3 s startup budget (unchanged by this PR; CI runs it).ruff checkandruff format --checkare clean on the touched files, andtools/ci_test_plan.py verifypasses.662fd4acb's source). Eight cases fail there: login change (outsider, expired), session wait-out, colliding ids (pass-over and registration), refused lock at startup, unchecked lease, and the property test's second@example. Three tests were written after that run: a service that cannot take its lease does not start (two cases) and the own lease waits out refused locks. Theown_lease_optionalmutant restores662fd4acb's lease code, and all three fail under it (below).main's code (1c9bd671b). The two-process test ran with its service subprocesses ona224dc110's source: it fails with{'build-a': 'missing_huggingface_credential'} != {'build-a': 'pending'}. On this branch it passes.1c9bd671b; the property test is unchanged in615d84f89). 2,000 examples passed, 0 failed, 54 invalid. Share of examples reaching each path:Limits and follow-ups
flockaltogether, no service with this change starts.fcntl. There are no leases there. A service adopts only idle runs and has no exclusion. The service already needs Unix sockets, so no supported platform lacksfcntl._claim, the_make_local_onlygate, the credential memo and note), so whichever lands second rebases; the property test catches a dropped invariant. A rebase must keep one rule in mind: a secondCollectorDeliveryfor the same run in one process now raises unless the first was closed.Evidence (design note, logs, mutant plugin and results, review prompts and reports) is in a local folder,
~/reviews/microcosm-telemetry-foreign-local-only-2026-10-09/.axiom: n/a: telemetry infrastructure
🤖 Generated with Claude Code