Skip to content

emrg: the daemon's register is the file that exists, not the name it belongs at - #2550

Merged
argszero merged 3 commits into
masterfrom
fix/daemon-register-is-the-one-that-exists
Oct 23, 2026
Merged

argszero merged 3 commits into
masterfrom
fix/daemon-register-is-the-one-that-exists

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2549

What the defect was

EmrgServer.__init__ built the daemon's agent registry over the register's canonical
path rather than the file the register is at:

self._agents_log = agents_file(runtime_dir)

AgentRegistry._read answers a missing file with [], and for_dir writes back what
it read. migrate_legacy_registry documents a move that cannot happen -- a read-only
home, 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 because register_for_reading prefers the new
name 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 is register_for_reading(runtime_dir), so 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.
  • emrg/server/__main__.py -- the startup comment named agents_file() as what the
    server 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 temporary
    directory, 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

  • The new test is red without the change, on the assertion that names the defect
    (a second register was created: the write addressed a file that does not exist); the
    mutation arm printed the mutated line, so it cannot read as a survivor.
  • 509 tests pass locally across every file that builds a server or touches the register;
    from emrg.client.app import run_client and python -m emrg --help are clean.
  • CI is pending.

…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.
EMRG Evolution added 2 commits October 23, 2026 21:26
…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 argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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_exists pins 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-existing other entry survived. The raising=False pin on agents_file is 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_name pins the other direction: neither file exists, the register is created at agents.yml, and projects.yml is 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.

@argszero
argszero merged commit 9ab31a2 into master Oct 23, 2026
2 checks passed
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.

The daemon writes the agent register under the name it should have, not the one it has

1 participant