Skip to content

Commit 768df0f

Browse files
argszeroEMRG Evolution
andauthored
emrg: the agent registry is agents.yml, migrated in place (#2522)
* emrg: the agent registry is agents.yml, migrated in place * emrg: step 0 keeps answering while the register is mid-move --------- Co-authored-by: EMRG Evolution <emrg@argszero.dev>
1 parent 57bef57 commit 768df0f

49 files changed

Lines changed: 556 additions & 174 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎emrg/__main__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def _build_parser() -> argparse.ArgumentParser:
108108
)
109109
rant_parser.add_argument(
110110
"-p", "--project",
111-
help="Target project (from projects.yml). Omit for emrg itself.",
111+
help="Target project (from agents.yml). Omit for emrg itself.",
112112
default=None,
113113
)
114114
rant_parser.add_argument(

‎emrg/client/widgets.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ def _format_tool_input(arguments: dict) -> list[str]:
660660
class ProjectSelector(Widget):
661661
"""Interactive project picker — arrow-key navigation with highlight.
662662
663-
Renders a list of projects from projects.yml with the selected one in
663+
Renders a list of projects from agents.yml with the selected one in
664664
reverse video. Used by /rant when invoked without @project.
665665
"""
666666

‎emrg/config.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,66 @@ def logs_dir() -> Path:
221221
return config_dir() / "logs"
222222

223223

224+
#: The register's name before an Agent became the record of the directory a
225+
#: session runs in (#2521). Spelled here and nowhere else in the package: the
226+
#: move below is what retires it.
227+
_LEGACY_AGENTS_NAME = "projects.yml"
228+
229+
230+
def agents_file(base: Path | None = None) -> Path:
231+
"""Returns the path of the agent registry (`~/.emrg/agents.yml`).
232+
233+
**The one derivation of that path** (#2521): every reader and writer of
234+
the register asks this rather than spelling `config_dir() / <name>` again —
235+
the daemon, the scheduler, the rant tool's candidate-name reader, the
236+
sandbox's protected-files list and `scripts/check-memory-rotation.py` —
237+
which is the treatment :func:`logs_dir` already gives the log directory.
238+
239+
A pure path: asking where the register lives opens and moves nothing.
240+
:func:`migrate_legacy_registry` is the one call that moves it, from the one
241+
process that owns it.
242+
243+
``base`` is the caller's own data directory, for a reader whose directory
244+
is not the host's (the rant tool's tests point it at a temp directory).
245+
"""
246+
directory = config_dir() if base is None else base
247+
return directory / "agents.yml"
248+
249+
250+
def migrate_legacy_registry(base: Path | None = None) -> bool:
251+
"""Move a register still at the pre-rename name onto the new one.
252+
253+
Returns whether this call moved it. ``Path.rename`` within one directory is
254+
a single filesystem operation, so the register is wholly at the old name or
255+
wholly at the new one and never half of each: the count of entries, every
256+
field and ``last_active`` travel with the file because nothing is read or
257+
rewritten to carry them.
258+
259+
Called from the daemon's startup and nowhere else — the one process that
260+
owns the register — and not from :func:`agents_file`, so that asking for
261+
the path never mutates a host's state. A test suite constructs the objects
262+
that read the register but never runs that startup, which is exactly the
263+
separation that keeps a run of the suite from renaming the host's file
264+
under a daemon that is still reading the old name.
265+
266+
Nothing moves once the new name exists (an older daemon that re-created the
267+
legacy file leaves both, and the new name governs), a register that never
268+
existed is not created, and a move that cannot happen — a read-only home, a
269+
permission boundary, a concurrent caller that got there first — answers
270+
``False`` and leaves the legacy file exactly where it is.
271+
"""
272+
directory = config_dir() if base is None else base
273+
target = agents_file(directory)
274+
legacy = directory / _LEGACY_AGENTS_NAME
275+
try:
276+
if target.exists() or not legacy.exists():
277+
return False
278+
legacy.rename(target)
279+
return True
280+
except OSError:
281+
return False
282+
283+
224284
def config_path() -> Path:
225285
"""Returns the config file path."""
226286
home = Path.home()

‎emrg/gui/main.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ vision = false
908908
return { ok: true, path: p };
909909
});
910910

911-
// P5 slice 2:删除项目 = 只删 projects.yml 条目(保留磁盘数据)+ 关闭该项目已打开会话
911+
// P5 slice 2:删除项目 = 只删 agents.yml 条目(保留磁盘数据)+ 关闭该项目已打开会话
912912
// 受保护项目不可删除(内置 project emrg / 内置 task emrg-task → 演化依赖,删了悬空);
913913
// `.emrg` 是 _touch_project 历史记录**非内置 → 可删**(再次访问重新注册)。
914914
// 返回 { ok, removed, closed: [sids] };renderer 收到后负责切换激活会话。

‎emrg/server/__main__.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import traceback
66
from logging.handlers import RotatingFileHandler
77

8-
from emrg.config import logs_dir
8+
from emrg.config import logs_dir, migrate_legacy_registry
99
from emrg.logfiles import migrate_legacy_logs
1010
from emrg.server.daemon import DaemonExit, _non_regular_kind, run_server
1111
from emrg.server.logcontext import session_label
@@ -195,6 +195,13 @@ def main() -> None:
195195
# whatever the crash log keeps as the low-level sink.
196196
try:
197197
config = load_config()
198+
# The agent register's move from its pre-rename name runs here and only here
199+
# (#2521): this is the one process that owns the file, and it runs before the
200+
# server object below asks `emrg.config.agents_file()` for the path. It is
201+
# placed after `load_config()` so a startup whose configuration is broken
202+
# fails without touching the host's register at all, and it sits in no path
203+
# derivation — asking where the register lives must never move it.
204+
migrate_legacy_registry()
198205
result = asyncio.run(run_server(config.llm))
199206
except KeyboardInterrupt:
200207
# SIGINT delivered at the event-loop poll point escapes the main

‎emrg/server/daemon.py‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from emrg.config import (
4040
LlmConfig,
4141
SandboxConfig,
42+
agents_file,
4243
config_dir,
4344
logs_dir,
4445
find_model_entry,
@@ -868,7 +869,7 @@ def __init__(self, llm_config: LlmConfig) -> None:
868869
# (session_id → (text, injected_ms)) so unchanged context is not
869870
# re-sent (context_refresh_interval_ms gating).
870871
self._context_snapshots: dict[str, tuple[str, float]] = {}
871-
self._projects_log = runtime_dir / "projects.yml"
872+
self._projects_log = agents_file(runtime_dir)
872873
self._rants_log = runtime_dir / "rants.jsonl"
873874

874875
# ── Phase 2 broadcast model (protocol-contract §2.6) ──
@@ -1248,7 +1249,7 @@ def _rebuild_sessions_index(self) -> None:
12481249
"""Backfill the global cross-project session index at startup.
12491250
12501251
Rant 2026-08-13T16:42:22: sessions created before this feature (or in
1251-
projects not registered in projects.yml) would otherwise be invisible
1252+
projects not registered in agents.yml) would otherwise be invisible
12521253
to other projects. Best-effort — failures are logged at debug level
12531254
and never crash the daemon.
12541255
"""
@@ -2103,7 +2104,7 @@ async def _broadcast_all(self, data: dict, exclude=None) -> None:
21032104
# ── Project tracking ─────────────────────────────────────
21042105

21052106
def _touch_project(self, cwd: str) -> None:
2106-
"""Record a project as active in ~/.emrg/projects.yml.
2107+
"""Record a project as active in ~/.emrg/agents.yml.
21072108
21082109
Used by the evolution cycle to discover which projects have
21092110
recent user activity and analyze their sessions for improvement ideas.
@@ -3456,7 +3457,7 @@ async def _process_message(
34563457

34573458
elif msg_type == "remove_project":
34583459
# P1 GUI multi-session (rant 2026-08-10T15:07:19): drop a
3459-
# projects.yml entry by name (disk session data preserved).
3460+
# agents.yml entry by name (disk session data preserved).
34603461
name = msg.get("name", "").strip()
34613462
if not name:
34623463
await self._send(ws, {
@@ -6684,14 +6685,14 @@ async def _handle_list_sessions(
66846685
async def _handle_list_projects(
66856686
self, ws
66866687
) -> None:
6687-
"""Read projects.yml and return all project entries.
6688+
"""Read agents.yml and return all project entries.
66886689
66896690
Ordered by each project's latest session activity (created_at desc,
66906691
parallel scan) — P6 of the GUI multi-session rant (2026-08-10T15:07:19)
66916692
so the open/new-session dialogs show the most recently active projects
66926693
first.
66936694
6694-
No evolution-workspace filter (rant 2026-08-07T10:48:00): projects.yml
6695+
No evolution-workspace filter (rant 2026-08-07T10:48:00): agents.yml
66956696
only contains explicitly registered entries, and on packaged installs
66966697
the emrg project's only path IS ~/.emrg/evolution/emrg — filtering it
66976698
hid emrg from /rant entirely.
@@ -6738,17 +6739,17 @@ async def _latest_session_at(entry: dict) -> str:
67386739
for (p, at), repo in zip(ordered, repos)
67396740
]
67406741
except (yaml.YAMLError, OSError):
6741-
logger.exception("Failed to read projects.yml")
6742+
logger.exception("Failed to read agents.yml")
67426743
await self._send(ws, {
67436744
"type": "projects_list",
67446745
"projects": projects,
67456746
})
67466747

67476748
async def _handle_remove_project(self, name: str, ws) -> None:
6748-
"""Remove a project entry from projects.yml by name.
6749+
"""Remove a project entry from agents.yml by name.
67496750
67506751
P1 of the GUI multi-session feature (rant 2026-08-10T15:07:19):
6751-
deletes only the projects.yml entry — on-disk session data under
6752+
deletes only the agents.yml entry — on-disk session data under
67526753
<path>/.emrg/sessions/ is preserved (a later _touch_project
67536754
re-registers the project). Mirrors _touch_project's read path and
67546755
atomic_write_yaml. Responses:
@@ -6774,7 +6775,7 @@ async def _handle_remove_project(self, name: str, ws) -> None:
67746775
"type": "project_removed",
67756776
"removed": False,
67766777
"name": name,
6777-
"error": "failed to read projects.yml",
6778+
"error": "failed to read agents.yml",
67786779
})
67796780
return
67806781
if not isinstance(data, list):
@@ -6803,10 +6804,10 @@ async def _handle_remove_project(self, name: str, ws) -> None:
68036804
"type": "project_removed",
68046805
"removed": False,
68056806
"name": name,
6806-
"error": "failed to write projects.yml",
6807+
"error": "failed to write agents.yml",
68076808
})
68086809
return
6809-
logger.info("remove_project: removed %s from projects.yml", name)
6810+
logger.info("remove_project: removed %s from agents.yml", name)
68106811
await self._send(ws, {
68116812
"type": "project_removed",
68126813
"removed": True,

‎emrg/server/evolution_prompt.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Read what others landed, why, and whether it needs follow-up. Use `FETCH_HEAD`,
127127
**Coverage is not negotiable; depth is.** Every cycle asks about **every** project whether anything changed — step 0 reads no content, and it is the step that is never skipped. How far into a project the cycle then goes is decided by evidence. Nothing here shrinks §1: every step of §1–§4 is still executed, and R10's rule — a cycle saves rounds by asking for several things at once, never by asking for less — is not relaxed by a word of it.
128128

129129
```bash
130-
cat ~/.emrg/projects.yml
130+
cat ~/.emrg/agents.yml
131131
# step 0, per project: what under its memory is newer than its bookmark time?
132132
find <project>/.emrg -newermt '<bookmark-time>' \
133133
\( -path '*/memory/*.md' -o -name 'MEMORY.md' \) 2>/dev/null | head

‎emrg/server/promote_prompt.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Run this loop every round; never stop at "executed":
3232
---
3333

3434
### Current State
35-
- Promotion target: {{ project.name }}({% if project.description %}{{ project.description }}{% else %}see projects.yml for description{% endif %})
35+
- Promotion target: {{ project.name }}({% if project.description %}{{ project.description }}{% else %}see agents.yml for description{% endif %})
3636
- Project path: `{{ project.path }}`
3737
- Session ID: `{{ session_id }}`
3838

@@ -63,7 +63,7 @@ Durable facts — `channel accounts`, `blog posts`, `blog drafts`, `banned list`
6363

6464
#### 0.2 Read the project config
6565

66-
Read the full config of `{{ project.name }}` from `~/.emrg/projects.yml` (path, name, description, custom fields) as context. The `project` variable in the template (name/path/description and other fields) is already injected into this prompt.
66+
Read the full config of `{{ project.name }}` from `~/.emrg/agents.yml` (path, name, description, custom fields) as context. The `project` variable in the template (name/path/description and other fields) is already injected into this prompt.
6767

6868
#### 0.3 Confirm promotion channel availability
6969

@@ -307,7 +307,7 @@ here, because a second copy of a rule is a copy that can disagree. So there is
307307
nothing for this prompt to write by hand — and no reason to open the file at all,
308308
not even to read it.
309309

310-
- `project` = the promoted project's registered short name (as in `~/.emrg/projects.yml`); the rant is then handled by that project's evolution task. The tool warns when the name is not registered.
310+
- `project` = the promoted project's registered short name (as in `~/.emrg/agents.yml`); the rant is then handled by that project's evolution task. The tool warns when the name is not registered.
311311
- Deduplicate before submitting: `submit_rant(action="list", project="{{ project.name }}")` and skip feedback that is already queued.
312312
- Each message notes the source (channel + link) so the evolution task can trace back.
313313

0 commit comments

Comments
 (0)