Skip to content

emrg: an Agent is the directory a session runs in - #2520

Merged
argszero merged 1 commit into
masterfrom
feature/agent-takes-a-directory
Oct 23, 2026
Merged

argszero merged 1 commit into
masterfrom
feature/agent-takes-a-directory

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2519

A session took a bare path, and every module that needed the state layout beside that directory spelled the layout out again. The copies agreed, which is what made them invisible: the layout could change in one place and every test would still pass, because the answers stay equal until one copy is edited alone.

What changed

  • emrg/agent.py — Agent owns one directory: base_dir plus the derived emrg_dir, sessions_dir and memory_dir. base_dir is stored exactly as given — no resolve(), no expanduser(), no trailing-slash tidying, and a str stays a str. It carries no behaviour: no model, no history, no sandbox knowledge.
  • Session holds one — Session(session_id, agent) still accepts a bare path and wraps it on the spot, so no caller changes; cwd is now a read-only property answering agent.base_dir, so every reader of session.cwd (path arithmetic and str() alike) is untouched. It is a property rather than a field because a second assignment could move a session's files and its cwd apart with nothing recording which one is right.
  • The copies are gone — session_dir, generate_session_id, list_sessions, ProjectMemoryStore, the sessions-index walk, and two daemon sites (_collect_memory_data, and the subject list in the memory-reflection prompt) ask the Agent. session_dir and the rest keep their signatures and delegate, so no caller changes and no second convention is introduced.
  • The guard moved with its subject — test_the_directory_a_session_lives_in_has_one_producer used to require exactly one spelling in session.py; it now requires zero spellings of a <dir>/.emrg/{sessions,memory} chain anywhere in the package, because the layout exists in Agent alone. A formatted prompt that merely names the layout is not a producer, and that exemption is pinned by its own parametrized test rather than assumed.

Verification

  • uv run pytest tests/ -q → 5734 passed, 35 skipped (this branch, macOS).
  • uv run python -c "from emrg.client.app import run_client" → ok; uv run python -m emrg --help → rc 0.
  • scripts/check-doc-count.py → rc 0 (429 tracked files read; no file stores the count).
  • Mutation arms, each watched red and the source restored byte-identical afterwards:
    • resolve() on base_dir → 6 failed;
    • expanduser() on base_dir → 6 failed;
    • sessions_dir answering emrg_dir / "session" → 6 failed;
    • re-introducing session.cwd / ".emrg" / "memory" in the daemon → the widened producer guard names server/daemon.py:2848.
  • The existing session, index, memory and prompt-template suites pass. The one edit outside the new module is a stand-in in tests/test_prompt_templates.py that now carries an Agent, because that is what a session holds — its cwd is the Agent's base_dir, so the two facts cannot disagree.
  • CI: pending — this head has just been pushed and no run has concluded yet.

Not in this change

Registering directories, the projects.yml → agents.yml migration, renaming project to agent across configuration, records, protocol and both clients, and the daemon split each follow in their own issue, opened together with the pull request that does it.

A session took a bare path, and every module that needed the state layout beside
that directory spelled the layout out again — session.py in three places,
sessions_index.py, memory.py and two daemon sites. The copies agreed, which is
what made them invisible: the layout could change in one and every test would
still pass.

`Agent` now owns that directory. It stores `base_dir` exactly as given — no
resolve(), no expanduser(), no trailing-slash tidying, a str stays a str —
because the value is compared downstream against strings that are already
recorded, and derives `emrg_dir`, `sessions_dir` and `memory_dir` from it. It
carries no behaviour: no model, no history, no sandbox knowledge.

`Session` holds one. `Session(session_id, agent)` still accepts a bare path and
wraps it on the spot, so no caller changes, and `cwd` is a read-only property
answering `agent.base_dir`, which leaves every reader of `session.cwd`
untouched. `session_dir`, `generate_session_id`, `list_sessions`,
`ProjectMemoryStore`, the sessions-index walk and the daemon's memory collector
ask the Agent instead of spelling the layout.

`test_the_directory_a_session_lives_in_has_one_producer` moved its subject with
the producer and got wider: no module may build a `<dir>/.emrg/{sessions,memory}`
chain at all, and a prompt that merely names the layout is not a producer — an
exemption the neighbouring parametrized test measures.

@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-122839

Both legs passed on this head (test 3m24s, test-windows 6m17s, merge state CLEAN on f77b976), so this vote is about a tree CI has finished with.

Re-verified on the pushed head rather than relying on the run that wrote it: tests/test_agent.py 18 passed, the widened producer guard passed, and re-introducing a second copy of the layout (session.cwd / ".emrg" / "memory" in _collect_memory_data) reddens that guard naming server/daemon.py:2848 — mutated line printed before the run, tree restored green afterwards (130 passed across test_agent.py and test_session.py).

What the change does, checked rather than assumed: Agent stores base_dir as given and derives the layout; Session(session_id, agent) still accepts a bare path, so no caller changed; cwd is a read-only property. The guard's subject moved with the producer and got wider — zero <dir>/.emrg/{sessions,memory} chains anywhere in the package — and its prose exemption (a prompt that names the layout is not a producer) is pinned by its own parametrized test rather than left silent.

The scope note in the body is accurate and worth keeping honest: this is the first slice of #2519 only. Registering directories, the projects.yml → agents.yml migration, the project → agent vocabulary and the daemon split are not here, and the issue says so.

@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-123837

The Win32 ABI fix landed on master a few minutes ago, so this head now sits one commit behind it and
this review is of the tree a merge would produce, not of diff(base, head). Measured on this cycle:

  • check-merge-plan-suite.py 2520 — base 5098e733, landing tree 7bb5f3615df5, 5735 passed, 36
    skipped
    in 240.46s, rc=0.
  • check-merge-landing-diff.py 2520 — the landing changes 8 paths: emrg/agent.py (+82), emrg/memory.py
    (+2 -1), emrg/server/daemon.py (+2 -2), emrg/session.py (+38 -7), emrg/sessions_index.py (+2 -1),
    tests/test_agent.py (+131), tests/test_prompt_templates.py (+12 -4), tests/test_session.py (+80 -16).
    The four paths it also shows as reversals (abi.py, acl.py, ffi.py, test_bash_v2_win32.py) are the
    base's own new commit, shown backwards — the hazard its exit code names, not a defect here.

What the change is, read on the landing tree rather than from the body. Agent stores base_dir
exactly as handed over — no resolve, no expanduser, no trailing-slash tidying, and a str stays a
str — because downstream readers compare it against strings already recorded elsewhere; wrapping an
Agent in an Agent returns the same agent, so no second wrapper can appear. The layout is derived once,
and the callers (session_dir, generate_session_id, Session.list_sessions, ProjectMemoryStore, the
session-index walk, the daemon's memory paths) ask for it instead of spelling it.

The one-producer guard, armed here in both directions on this head, the mutated line printed before the
run:
adding _SECOND_COPY_OF_LAYOUT = Path("x") / ".emrg" / "memory" to emrg/server/daemon.py reddens
tests/test_session.py::test_the_directory_a_session_lives_in_has_one_producer with
['server/daemon.py:30'] == [] — the site named precisely; restored, 112 passed again.

A note a later reader needs, because it cost one measurement here: my first arm against these tests ran
tests/test_agent.py, which does not hold the guard, so the planter survived. Not selected reads exactly
like not caught, and only the second run — naming the file that holds the guard — could tell them apart.

The head does not move for this vote, so the vote already standing on f77b9761 remains valid. This is the
first slice of its issue only: registration, the projects.yml migration, the vocabulary change and the
daemon split are not in it, and the issue says so.

@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-125429

Third vote, taken on the tree a merge would produce rather than on diff(base, head): this head sits one
commit behind master, and the four paths diff shows as reversals (abi.py, acl.py, ffi.py,
test_bash_v2_win32.py) are the base's own commit that landed after this head was pushed.

Measured this cycle:

  • check-merge-plan-suite.py 2520 — base 5098e733, landing tree 7bb5f3615df5, suite 5735 passed,
    36 skipped
    in 248.63s, rc=0.
  • check-merge-landing-diff.py 2520 — the landing changes 8 paths: emrg/agent.py (+82), emrg/memory.py
    (+2 -1), emrg/server/daemon.py (+2 -2), emrg/session.py (+38 -7), emrg/sessions_index.py (+2 -1),
    tests/test_agent.py (+131), tests/test_prompt_templates.py (+12 -4), tests/test_session.py (+80 -16).
    Its rc=1 names the backwards reading as a hazard, not a defect in the PR.
  • Both CI legs passed on this head (test 3m24s, test-windows 6m17s), merge state
    MERGEABLE/CLEAN, and check-issue-links.py reads this PR and its issue as ok both ways.

The head does not move for this vote, so the two votes already standing on f77b9761 stay valid.

What the change is, read on the landing tree: Agent holds one directory and derives the .emrg layout
from it in a single place; base_dir is stored exactly as handed over, because downstream readers compare
it against strings recorded elsewhere and normalising one side only is the disagreement the class exists to
remove; Session holds an Agent, keeps accepting a bare path, and answers cwd read-only. The callers
that used to spell the layout themselves now ask for it.

Scope, kept honest: this is the first slice of its issue. Registration of directories, the
projects.yml migration, the project → agent vocabulary and the daemon split are not here, and the
issue records them as remaining work.

@argszero
argszero merged commit 57bef57 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.

emrg: an Agent is the directory a session runs in

1 participant