Skip to content

emrg: the agents.yml self-heal reads one register and writes another #2547

Closed

Description

@argszero

What was observed

TaskScheduler._ensure_emrg_agent_entry (emrg/server/scheduler.py) performs a read-modify-write on the register of agents:

reg = AgentRegistry(agents_file(config_dir()))
row = next((e for e in reg.entries() if e.get("name") == "emrg"), None)
if row is None:
    reg.upsert("emrg", target)

agents_file() names where the register belongs. Every reader in the package now asks register_for_reading() instead, which answers where it is: the canonical agents.yml when that file exists, and the pre-rename name (projects.yml) while that is the one holding it. This function asks the first for both halves, so its read and its write can address two different files.

That state is reachable. migrate_legacy_registry() answers False and leaves the file exactly where it is when the move cannot happen — a read-only home, a permission boundary, a concurrent caller that got there first. Startup then carries on, the scheduler is constructed, and this runs with the register still at the old name. AgentRegistry._read() maps FileNotFoundError to [], so:

  • the emrg row reads as absent (or its recorded path reads as dead) and the upsert runs;
  • the write creates a new agents.yml holding that one row;
  • from the next call on, register_for_reading() prefers agents.yml, so every other entry — 33 of them on the host this was measured against — disappears from every reader at once;
  • projects.yml is left behind, complete and unread.

Every step is silent. The function reports success, and the register it produced is the one the readers will now believe.

Why the suite does not catch it

All five _ensure_emrg_agent_entry tests write either a canonical agents.yml or no file at all. None creates the legacy name, so the state above is not reachable from the suite, and the requirement "the write lands in the file the read came from" is named nowhere.

What the fix must satisfy

  • One path for both halves. Resolve the register once through register_for_reading() and hand that same path to AgentRegistry, so the write lands in the file the read was taken from.
  • Decline when that path is not the canonical name. A write must not create a second register beside the real one: when the two names disagree, log and return, leaving the move to migrate_legacy_registry — the one operation that moves it, from the one process that owns it.
  • A fresh installation still self-heals. With neither name present, register_for_reading() answers the canonical path, so the seed is unchanged.
  • A canonical register behaves exactly as before. The four existing tests — add the entry, preserve a dev path, repair a stale path, preserve other entries — stay green untouched.
  • A test names the state. With only the legacy name present, carrying an emrg row whose path is dead (the shape that reaches the write), no agents.yml appears, the legacy file is byte-identical afterwards, and every entry it held is still readable through register_for_reading().

Activity

  1. argszero commented on Oct 23, 2026

    @argszero
    OwnerAuthor

    Handled by #2548

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions