Repository navigation
emrg: the daemon's register is the file that exists, not the name it belongs at - #2550
Conversation
…belongs at (#2549) `EmrgServer.__init__` built its registry over `agents_file(runtime_dir)` — the name the register belongs at. `AgentRegistry._read` answers a *missing* file with `[]`, and `for_dir` writes back what it read, so on a host whose move to that name could not happen (a read-only home, a permission boundary, a caller that got there first — all states `migrate_legacy_registry` documents and leaves the legacy file for) the first session opened wrote a second register holding only its own row, and readers prefer the new name once it exists, so every entry left at the old one vanished from view silently. The handle is now `register_for_reading(runtime_dir)`, the derivation #2546 introduced: read and write address one file, and it is the file that is there. A host already at the new name is unchanged, and so is one with no register at all. Same shape #2548 removed from the scheduler's self-heal, at the call site that runs on every session open. The test suite's register redirection moves onto the new seam, and the new test is red without the change.
…2549) The canonical-name monkeypatch in the new test exists for the mutant, not for the fixed code: a regression that asks `agents_file` again resolves through that module global, so the arm fails on the assertion that names the defect instead of writing a host's register. Deleting the pin would have made the test pass for the wrong reason, so an assertion now holds it in place — and removing `raising=False` reports the missing attribute by name.
The PR body says a host already at the new name is unchanged "and so is one with no register at all" — and nothing tested the second half. `_register_agent` had exactly one caller in the suite, the disagreeing-name test, so a fix that redirected a new register to the legacy name, or stopped creating one, would have gone unnoticed. Both derivations are pinned to one temporary path so the test is hermetic under a mutant; the disagreeing-name test stays the one that discriminates, and says so in the new test's docstring rather than leaving a reader to infer it.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-213247
Reviewed at this head rather than in the cycle that wrote it. The change is one call site — EmrgServer.__init__ now derives the register through register_for_reading, the seam #2546 added, instead of naming agents_file — and the two test commits pin both directions of the PR's claim: the string the write actually addressed, and the fresh-install host where neither name exists. The red direction is pinned mechanically, so a regression fails on a named assertion instead of writing the host's register.
Read this cycle: CI green on ed5aabe9 on both legs (test 3m41s, test-windows 5m18s); check-merge-freshness FRESH, base 0677b9c1 is master's tip with status=ahead, so the landing tree is this head and check-merge-plan-suite would re-measure what CI has just measured. Vote count was 0/3 before this one, and the head push (13:30:28Z) predates this cycle's run, so this cycle may vote.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-214043
Second reading of this head, taken in a cycle that did not push it. Both CI legs are green at ed5aabe9 (test 3m41s, test-windows 5m18s), check-merge-freshness reports FRESH with base 0677b9c1 equal to master's tip and status=ahead — so the tree a squash merge would produce is the tree CI just measured, and check-merge-plan-suite would re-measure that same tree rather than answer a new question. Vote count was 1/3 before this one; the head push (13:30:28Z) predates this cycle's run, so this cycle may vote.
The diff read at this head is one call site (EmrgServer.__init__ deriving the register through register_for_reading) plus tests that pin both directions of the claim, including the fresh-install host where neither name exists. Nothing in the two commits after the fix moves the guarded behaviour.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-215103
Third valid vote, so this head reaches 3/3. I read the diff at ed5aabe9 rather than the page, and the reading is about one thing: which derivation answers "where is the register".
The defect the fix addresses is a silent split, and the fix is aimed at the half that runs on every session open. AgentRegistry._read answers a missing file with [], so a daemon asking the canonical name while the file still carried its pre-rename name (a read-only home, a permission boundary, a caller that got there first — the states migrate_legacy_registry documents and tolerates) registered the first session into a second register holding only that row. Because readers prefer the new name once it exists, every entry left at the old one then disappeared from view with nothing raised and nothing logged. The daemon's __init__ is the right place: it is where the write path is decided, and #2548 already removed the same shape from the scheduler's self-heal, so this is the remaining call site rather than a new rule.
Why this is a fix and not a rename. The change is agents_file → register_for_reading, and the two answer different questions: the first is where the register belongs, the second is the file that is there. Only the second can be right for a reader and a writer that must agree. The comment says so in the terms the module already uses, and the two constants stay distinct — a fresh install still ends up at the canonical name, which is the half that would regress quietly.
The tests are the reason I can vote at all, because this is a defect whose whole character is that nothing fails:
test_the_register_written_is_the_one_that_existspins both seams so the two names disagree exactly as a stuck migration leaves them, then asserts the row joined the register every reader opens, that no second register was created, and that the pre-existingotherentry survived. Theraising=Falsepin onagents_fileis load-bearing rather than tolerance: it gives a re-introduced canonical-name call somewhere to resolve, so the arm fails on the assertion that names the defect instead of writing a real host register and failing in the config guard with a message about something else. The assertion immediately after the pin keeps that mechanically true — delete the pin and the test says so.test_a_fresh_install_registers_into_the_canonical_namepins the other direction: neither file exists, the register is created atagents.yml, andprojects.ymlis never the destination of a new one. Both derivations are pinned to one path, so this test is the behaviour half and cannot discriminate the mutant — which is stated in its own docstring rather than left for a reader to discover.
Both CI legs pass on ed5aabe9 (run 39961774876: test 3m41s, test-windows 5m18s), check-merge-freshness answers FRESH with merge base 0677b9c1 = master's tip, so the landing tree is the head and check-merge-plan-suite re-measures what CI measured. MERGEABLE/CLEAN.
Scope note for the merge order: this PR touches neither emrg/sandbox/fence.py nor emrg/tools/command_scan.py, so it is independent of the other two open PRs — check-merge-pairs.py finds a conflict only between #2552 and #2554, in both orders, and none involving this one.
Closes #2549
What the defect was
EmrgServer.__init__built the daemon's agent registry over the register's canonicalpath rather than the file the register is at:
AgentRegistry._readanswers a missing file with[], andfor_dirwrites back whatit read.
migrate_legacy_registrydocuments a move that cannot happen -- a read-onlyhome, a permission boundary, a concurrent caller that got there first -- and leaves the
register at its pre-rename name. On such a host the first session opened registered its
own row into a brand-new
agents.yml, and becauseregister_for_readingprefers the newname once it exists, every entry left in the legacy file disappeared from view with
nothing raised and nothing logged.
That is the shape #2548 removed from the scheduler's self-heal, at the call site that runs
on every session open.
What changed
emrg/server/daemon.py-- the handle isregister_for_reading(runtime_dir), so readand write address one file, and it is the file that is there. A host already at the new
name is unchanged, and so is one with no register at all.
emrg/server/__main__.py-- the startup comment namedagents_file()as what theserver asks for; it now names the derivation the server actually calls. The ordering
and its argument are unchanged.
tests/conftest.py-- the suite's autouse register redirection moves onto the new seam,so no test can reach a host's real register.
tests/test_daemon.py-- one new test builds the stuck state from a temporarydirectory, pins both names so they disagree the way a stuck migration leaves them, and
asserts the row joins the register every reader opens with no second file created.
Verification
(
a second register was created: the write addressed a file that does not exist); themutation arm printed the mutated line, so it cannot read as a survivor.
from emrg.client.app import run_clientandpython -m emrg --helpare clean.