Repository navigation
emrg: an Agent is the directory a session runs in - #2520
Conversation
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
left a comment
There was a problem hiding this comment.
✅ 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
left a comment
There was a problem hiding this comment.
✅ 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— base5098e733, landing tree7bb5f3615df5, 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
left a comment
There was a problem hiding this comment.
✅ 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— base5098e733, landing tree7bb5f3615df5, 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 (
test3m24s,test-windows6m17s), merge state
MERGEABLE/CLEAN, andcheck-issue-links.pyreads this PR and its issue asokboth 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.
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—Agentowns one directory:base_dirplus the derivedemrg_dir,sessions_dirandmemory_dir.base_diris stored exactly as given — noresolve(), noexpanduser(), no trailing-slash tidying, and astrstays astr. It carries no behaviour: no model, no history, no sandbox knowledge.Sessionholds one —Session(session_id, agent)still accepts a bare path and wraps it on the spot, so no caller changes;cwdis now a read-only property answeringagent.base_dir, so every reader ofsession.cwd(path arithmetic andstr()alike) is untouched. It is a property rather than a field because a second assignment could move a session's files and itscwdapart with nothing recording which one is right.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_dirand the rest keep their signatures and delegate, so no caller changes and no second convention is introduced.test_the_directory_a_session_lives_in_has_one_producerused to require exactly one spelling insession.py; it now requires zero spellings of a<dir>/.emrg/{sessions,memory}chain anywhere in the package, because the layout exists inAgentalone. 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).resolve()onbase_dir→ 6 failed;expanduser()onbase_dir→ 6 failed;sessions_diransweringemrg_dir / "session"→ 6 failed;session.cwd / ".emrg" / "memory"in the daemon → the widened producer guard namesserver/daemon.py:2848.tests/test_prompt_templates.pythat now carries anAgent, because that is what a session holds — itscwdis the Agent'sbase_dir, so the two facts cannot disagree.Not in this change
Registering directories, the
projects.yml→agents.ymlmigration, renamingprojecttoagentacross 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.