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
2 changes: 1 addition & 1 deletion Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (573) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (575) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (93: 22 daemon_client + 22 app-commands + 24 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ EMRG doesn't just keep up — it catches up on its own.
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # install deps
uv run pytest tests/ -v # run tests (currently 573 items)
uv run pytest tests/ -v # run tests (currently 575 items)
uv run python -m emrg # launch TUI
# CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI

Expand Down
21 changes: 18 additions & 3 deletions emrg/server/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,13 @@ async def _run_evolution_cycle(self) -> None:
# Detect empty cycles: git HEAD unchanged → no work was done.
# A truncated cycle is NOT empty — the agent wanted to work but hit
# the tool-round cap; counting it would wrongly back off the handler.
# An aborted cycle (server error like "session busy", or an exception)
# is NOT empty either — the agent was blocked before reaching an NTE
# conclusion; counting it would also advance the idle-halt backoff.
git_head_after = self._get_git_head()
if (
not truncated
not error
and not truncated
and git_head_before and git_head_after
and git_head_before == git_head_after
):
Expand All @@ -681,22 +685,33 @@ async def _run_evolution_cycle(self) -> None:
else:
if self._empty_cycles > 0:
reason = "truncated cycle" if truncated else "git HEAD changed"
if error:
reason = f"aborted cycle ({error[:80]})"
logger.info(
"EvolutionHandler[%s]: %s, resetting empty streak",
self.name, reason,
)
self._empty_cycles = 0
self._save_saturation_state()

# Aborted cycles are not evolutions: no log file, no count. Writing
# them would inflate the evolution count (GUI growth card / toast,
# evolution_summary) with work that never happened — the "session
# busy" aborts observed when interactive sessions hold the daemon.
if error:
logger.warning(
"EvolutionHandler[%s]: cycle aborted (%s) — not counted as evolution",
self.name, error[:200],
)
return

cycle_ts = cycle_time.isoformat()
impact = [
f"evolution-cycle-{cycle_ts}-{'truncated' if truncated else 'complete'}",
f"tools-executed={tool_count}",
]
if truncated:
impact.append("truncated=max-tool-rounds")
if error:
impact.append(f"error={error[:200]}")

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
23 changes: 23 additions & 0 deletions tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,29 @@ def test_evolution_cycle_complete_unchanged_head_still_empty(tmp_path):
assert "truncated=max-tool-rounds" not in impact, impact


def test_evolution_cycle_aborted_error_not_counted(tmp_path):
"""Server error frame (e.g. 'session busy') → no evolution log, no count."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"error": "session busy"},
])
asyncio.run(handler._run_evolution_cycle())
assert "log" not in captured, "aborted cycle must not write an evolution log"
assert handler.evolutions == [], "aborted cycle must not append to evolutions"
assert handler._empty_cycles == 0, \
"aborted cycle must not advance the idle-halt backoff (agent never ran)"


def test_evolution_cycle_aborted_resets_empty_streak(tmp_path):
"""Aborted cycle resets a pre-existing empty streak (blocked ≠ NTE)."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"error": "session busy"},
])
handler._empty_cycles = 5
asyncio.run(handler._run_evolution_cycle())
assert "log" not in captured
assert handler._empty_cycles == 0, "abort resets the streak (not a real empty cycle)"


# ── Saturation halt auto-resume on upstream advance ───────────────
# The halt skips scheduled runs entirely, so a halted handler can never
# detect a HEAD change itself (only /trigger could resume it). If every
Expand Down
Loading