Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ EMRG is a self-evolving AI agent architecture experiment. Python implementation,
- `__main__.py` — CLI entry (`emrg`, `emrg server`, `emrg rant`, `emrg update`)
- `protocol.py` — Communication protocol (TaskRequest, TaskResponse, ToolStart, ToolEnd, ServerPong, EvolutionLog, InstanceIdentity)
- `config.py` — Config loading (`~/.emrg/config.toml`, Python 3.11+ tomllib)
- `connect.py` — IPC connection (WebSocket over TCP loopback, token auth via `emrgd.port`)
- `connect.py` — IPC connection (WebSocket over TCP loopback, token auth via `emrgd.token`)
- `memory.py` — Memory system (ProjectMemoryStore, SessionMemoryStore, MemoryFile, MemoryIndex)
- `session.py` — Session management (Session CRUD, history persistence, compact/clear)
- `emrg/server/` — Server (WebSocket daemon, EMRG's living core)
Expand DownExpand Up@@ -115,7 +115,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
## Test Commands

```bash
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"`
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ vision = true
┌─────────────┐ WebSocket (ws://) ┌──────────────┐
│ emrg TUI │ ◄─────────────────────► │ emrgd │
│ (client) │ TCP loopback + auth │ (daemon) │
│ │ token (emrgd.port) │ │
│ │ token (emrgd.token) │ │
│ • Chat │ │ • LLM loop │
│ • Markdown │ │ • Tools │
│ • ToolCards│ │ • Evolution │
Expand Down
16 changes: 8 additions & 8 deletions emrg/_stop_all.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -329,17 +329,17 @@ def ws_graceful_shutdown(port: int, token: str, timeout: float = 3.0) -> bool:
def stop_daemon() -> None:
"""Stop the daemon: ws shutdown → pid file → SIGTERM/taskkill /F → poll.

Also removes ``~/.emrg/emrgd.port`` once the daemon pid is confirmed dead
Also removes ``~/.emrg/emrgd.token`` once the daemon pid is confirmed dead
(the daemon itself removes it on graceful shutdown; a force-killed daemon
cannot, so we clean it up — the next daemon start re-asserts both files).
"""
port_path = config_dir() / "emrgd.port"
token_path = config_dir() / "emrgd.token"
# Fixed-port shutdown (rant 2026-08-19T08:05:21): the daemon always
# listens on _EMRGD_PORT; the port file only supplies the auth token. If
# the file is missing/stale, fall through to the pid + cmdline paths.
# listens on _EMRGD_PORT; the token file only supplies the auth token
# (single line, rant 2026-08-20T14:32:52). If the file is missing/stale,
# fall through to the pid + cmdline paths.
try:
text = port_path.read_text(encoding="utf-8").split()
token = text[1] if len(text) == 2 else ""
token = token_path.read_text(encoding="utf-8").strip()
except (OSError, ValueError):
token = ""
if token and ws_graceful_shutdown(_EMRGD_PORT, token):
Expand DownExpand Up@@ -371,13 +371,13 @@ def stop_daemon() -> None:
for pid in _scan_windows_python_emrg(os.getpid()):
_kill_pid_windows(pid)

# Port file cleanup: the daemon removes it on graceful shutdown; a
# Token file cleanup: the daemon removes it on graceful shutdown; a
# force-killed daemon cannot, so remove it once the pid is confirmed gone
# (the next daemon start re-asserts both files).
daemon_gone = pid is None or not _pid_alive(pid)
if daemon_gone:
try:
port_path.unlink()
token_path.unlink()
except OSError:
pass

Expand Down
34 changes: 18 additions & 16 deletions emrg/connect.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@

The daemon listens on the fixed port ``127.0.0.1:EMRGD_PORT`` (56031, rant
2026-08-19T08:05:21 — fixed-port bind exclusivity is the single-instance
admission) and writes its auth token to ``~/.emrg/emrgd.port``
(``port\\n token``, mode 0o600). Clients read that file for the token, and
admission) and writes its auth token to ``~/.emrg/emrgd.token``
(single-line token, mode 0o600). Clients read that file for the token, and
connect to the fixed port.
Clients then send
a first-frame auth message; the daemon confirms with ``auth_ok`` before the
Expand All@@ -33,13 +33,13 @@
logger = logging.getLogger(__name__)

# ── Connection identifier ───────────────────────────────────────
# Port/token file lives at ~/.emrg/emrgd.port (port\n token, mode 0o600)
# Auth token file lives at ~/.emrg/emrgd.token (single-line token, 0o600)
CONNECT_ID = "emrgd"

# Fixed daemon port (host rant 2026-08-19T08:05:21): the daemon binds a FIXED
# loopback port so kernel-level bind exclusivity (EADDRINUSE) is the single-
# instance admission — no PID file to forge/delete, no race window. The port
# file still carries the auth token; the port itself is now a constant.
# instance admission — no PID file to forge/delete, no race window. The
# token file only carries the auth token; the port itself is a constant.
# Keep in sync with emrg._stop_all._EMRGD_PORT (that module is pure stdlib).
EMRGD_PORT = 56031

Expand All@@ -53,17 +53,19 @@ class AuthError(Exception):


def get_server_path() -> str:
"""Return the path of the daemon port/token file."""
return str(config_dir() / f"{CONNECT_ID}.port")
"""Return the path of the daemon auth token file."""
return str(config_dir() / f"{CONNECT_ID}.token")


async def connect_to_server():
"""Connect to the emrgd server over WebSocket.

Reads the auth token from ``~/.emrg/emrgd.port``, connects to the FIXED
daemon port ``ws://127.0.0.1:<EMRGD_PORT>`` (rant 2026-08-19T08:05:21 —
the port is a constant; the file only carries the token), sends the
first-frame auth message and waits for the ``auth_ok`` confirmation.
Reads the auth token from ``~/.emrg/emrgd.token`` (single line, rant
2026-08-20T14:32:52 — the file carries ONLY the token), connects to the
FIXED daemon port ``ws://127.0.0.1:<EMRGD_PORT>`` (rant
2026-08-19T08:05:21 — the port is a constant; the file only carries the
token), sends the first-frame auth message and waits for the
``auth_ok`` confirmation.
Returns the connected WebSocket object (single ws — no
``(reader, writer)`` tuple anymore).

Expand All@@ -72,7 +74,7 @@ async def connect_to_server():
ConnectionRefusedError / OSError / FileNotFoundError: daemon not running.
"""
port_path = Path(get_server_path())
_, token = port_path.read_text(encoding="utf-8").split()
token = port_path.read_text(encoding="utf-8").strip()
# proxy=None: loopback connections must never go through a system proxy.
# websockets 17 defaults proxy=True and reads the OS proxy settings — when a
# Windows system proxy is enabled (e.g. 10.10.0.28:6501 for HN/Reddit access),
Expand DownExpand Up@@ -100,19 +102,19 @@ async def connect_to_server():


def cleanup_server() -> None:
"""Remove the daemon port/token file on shutdown."""
"""Remove the daemon auth token file on shutdown."""
port_path = Path(get_server_path())
if port_path.exists():
port_path.unlink()
logger.debug("removed port file: %s", port_path)
logger.debug("removed token file: %s", port_path)


def is_server_running_sync(timeout: float = 2.0) -> bool:
"""Synchronous health-check probe (for client startup).

Blocking TCP connect to the FIXED daemon port ``127.0.0.1:EMRGD_PORT``
(rant 2026-08-19T08:05:21). No port-file read: the fixed port is the
ground truth, so a missing/stale ``emrgd.port`` never makes the probe
(rant 2026-08-19T08:05:21). No token-file read: the fixed port is the
ground truth, so a missing/stale ``emrgd.token`` never makes the probe
report "not running" while a daemon is actually alive (the dual-instance
root cause). Real auth happens on the first frame of a real connection.
"""
Expand Down
Loading
Loading