From 0b149ca0308e91d85cae58f14204385dd419dc5c Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:35:15 -0700 Subject: [PATCH] fix(process): wait timeout result reads as status, not failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit process(action='wait') hitting its window returned status='timeout' with a terse note — models read it as an error and re-issued identical waits (process is the #1 exact-duplicate tool call in production: 511 dupes in a 400k-msg window; wait is 57% of all process actions). The timeout result now carries: - process_running: true — machine-readable 'this is a status, not a failure' - an explicit note: 'Wait window of Ns elapsed — the process is still running. This is not an error. Uptime: Ms.' plus the right next step: when notify_on_complete is set, 'you will be notified on exit — do more work instead of waiting again'; otherwise a pointer to notify_on_complete for next time. - the clamp note (requested > max) now composes with the status note instead of replacing it. Exited/interrupted results are unchanged. --- tests/tools/test_process_wait_clarity.py | 64 ++++++++++++++++++++++++ tools/process_registry.py | 27 ++++++++-- 2 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 tests/tools/test_process_wait_clarity.py diff --git a/tests/tools/test_process_wait_clarity.py b/tests/tools/test_process_wait_clarity.py new file mode 100644 index 0000000000000..d5a61ed10761c --- /dev/null +++ b/tests/tools/test_process_wait_clarity.py @@ -0,0 +1,64 @@ +"""Tests for process wait timeout-result clarity (not-an-error semantics).""" + +import pytest + +from tools.process_registry import ProcessRegistry + + +@pytest.fixture +def registry(tmp_path, monkeypatch): + monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes")) + return ProcessRegistry() + + +def _spawn_sleeper(registry, notify=False): + session = registry.spawn_local("sleep 30", cwd="/tmp", task_id="t-waitclar") + session.notify_on_complete = notify + return session.id + + +class TestWaitTimeoutClarity: + def test_wait_timeout_marks_process_running(self, registry): + sid = _spawn_sleeper(registry) + try: + r = registry.wait(sid, timeout=1) + assert r["status"] == "timeout" + assert r["process_running"] is True + assert "not an error" in r["timeout_note"] + assert "Uptime" in r["timeout_note"] + finally: + registry.kill_process(sid) + + def test_wait_timeout_suggests_notify_when_unset(self, registry): + sid = _spawn_sleeper(registry, notify=False) + try: + r = registry.wait(sid, timeout=1) + assert "notify_on_complete=true" in r["timeout_note"] + finally: + registry.kill_process(sid) + + def test_wait_timeout_defers_to_notify_when_set(self, registry): + sid = _spawn_sleeper(registry, notify=True) + try: + r = registry.wait(sid, timeout=1) + assert "you will be notified on exit" in r["timeout_note"] + finally: + registry.kill_process(sid) + + def test_clamped_wait_keeps_clamp_note_and_running_semantics(self, registry, monkeypatch): + monkeypatch.setenv("TERMINAL_TIMEOUT", "1") + sid = _spawn_sleeper(registry) + try: + r = registry.wait(sid, timeout=600) + assert r["status"] == "timeout" + assert "clamped" in r["timeout_note"] + assert "not an error" in r["timeout_note"] + assert r["process_running"] is True + finally: + registry.kill_process(sid) + + def test_exited_process_unaffected(self, registry): + session = registry.spawn_local("true", cwd="/tmp", task_id="t-waitclar") + r = registry.wait(session.id, timeout=10) + assert r["status"] == "exited" + assert "process_running" not in r diff --git a/tools/process_registry.py b/tools/process_registry.py index fdea8f4762ec5..29af51f000680 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -1598,11 +1598,32 @@ class ProcessRegistry: "status": "timeout", "command": session.command, "output": strip_ansi(session.output_buffer[-1000:]), + # A wait window elapsing is NOT a failure — 511 exact-duplicate + # process calls in a production window show models re-issuing + # identical waits after misreading this result as an error. + "process_running": True, } - if timeout_note: - result["timeout_note"] = timeout_note + uptime = time.time() - session.started_at if session.started_at else None + base_note = ( + f"Wait window of {effective_timeout}s elapsed — the process is " + "still running. This is not an error." + ) + if uptime is not None: + base_note += f" Uptime: {int(uptime)}s." + if session.notify_on_complete: + base_note += ( + " notify_on_complete is set: you will be notified on exit — " + "do more work instead of waiting again." + ) else: - result["timeout_note"] = f"Waited {effective_timeout}s, process still running" + base_note += ( + " Poll again later or use terminal(background=true, " + "notify_on_complete=true) next time for automatic notification." + ) + if timeout_note: + result["timeout_note"] = f"{timeout_note}. {base_note}" + else: + result["timeout_note"] = base_note return result def kill_process(