fix(supervisor): make a watchdog restart resumable, bounded, and sized by the backend it interrupts - #1462
Conversation
bbf6ffa to
52daeab
Compare
|
Finding 3 is real and worth fixing. Finding 2 is fixed in the wrong direction, and 1. One bit, one signal. "Was this SIGTERM the supervisor's?" is one bit, and 2. Derive the terminal markers from the monitor, don't enumerate them by hand. 3. Don't borrow 4. Say where the brake is. With 5. Finding 2: fix the root cause, not the detector. 6. Collapse what's left. One 7. Split the PR. The description says "Findings 2 and 3 only"; the diff carries
Finally: no CHANGELOG entry, and |
Carry the watchdog restart on the signal itself, bank the interrupted leg on its own timestamp, and stop after three resumable attempts. Co-authored-by: Cursor <cursoragent@cursor.com>
546234b to
f47b9d5
Compare
A backend's call_timeout_s caps the silence between streamed messages, so reading it as a turn ceiling cancelled healthy agents and left their retry budget unreachable. Give the turn its own ceiling above the longest legal one, widen the stall floor to hold a whole tick of them, and count a turn cancelled at that ceiling as a crash so an advancing tick cannot hide it. Co-authored-by: Cursor <cursoragent@cursor.com>
A constant reactor ceiling is only right for the defaults it was written against: every input to a legal turn -- the per-attempt timeout, the attempt count, the backoff -- is an environment override, so raising one made the constant cancel turns the backend still calls healthy. Each backend now reports the longest turn its own configuration allows, and the tick reads that; a backend that declares no budget is left to the session bound rather than capped at a guess. The stall floor goes back to its own argument instead of being pinned to three copies of that constant. The watchdog's restart budget is banked before the signal goes out, since the coordinator's stop path can outlive the supervisor and a restart spent only afterwards reads as never spent. The terminal ask is no longer counted as a restart it did not perform. Also: the monitor imported a stop-reason module that does not exist and silently fell back to a subset missing 16 terminal reasons; leg_ended_ts is Coordinator-owned like stop_ts, since the next leg banks phase time up to it; a SIGTERM crossing the watchdog's SIGHUP is re-read at the last moment so an operator's stop stays terminal; and a resumable leg no longer writes the session's close-out package or prints a final summary. Co-authored-by: Cursor <cursoragent@cursor.com>
Use an explicit total reactor deadline and stage progress so idle budgets cannot cancel healthy turns or make the watchdog fire inside legal work. Preserve terminal signal priority and resumable recovery across persistence failures, while falling back to a terminal stop when the restart budget cannot be recorded. Co-authored-by: Cursor <cursoragent@cursor.com>
| #: role turns at their five-minute cap, plus a retry each. | ||
| _TICK_STALL_FLOOR_SEC: float = 1800.0 | ||
| #: The shortest default stall window covers one reactor turn and one poll. | ||
| _TICK_STALL_FLOOR_SEC: float = DEFAULT_REACTOR_TURN_TIMEOUT_SEC + DEFAULT_POLL_SEC |
| #: role turns at their five-minute cap, plus a retry each. | ||
| _TICK_STALL_FLOOR_SEC: float = 1800.0 | ||
| #: The shortest default stall window covers one reactor turn and one poll. | ||
| _TICK_STALL_FLOOR_SEC: float = DEFAULT_REACTOR_TURN_TIMEOUT_SEC + DEFAULT_POLL_SEC |
Co-authored-by: Cursor <cursoragent@cursor.com>
What this fixes
Two failures the supervisor could not tell apart, and the machinery that got in
the way of telling them apart.
A wedged coordinator is restarted, not ended. The supervisor sends
SIGHUP,and the coordinator reads the signal number off the drain to classify it. A
resumable stop records the leg boundary in
leg_ended_tsand writes nostop_reason, nostop_tsand no terminal artifacts, so the monitor resumes thesame session. The restart budget lives in
runtime/supervisor/status.jsonand isbanked before the signal goes out -- the coordinator's stop path can outlive
the supervisor, and a restart spent only afterwards reads to the next leg as
never spent. After three restarts the next watchdog stop is
SIGTERMand followsthe normal terminal path; that terminal ask is not counted as a restart it never
performed.
A hung turn cannot hold the tick open.
_await_within_session_boundboundedinline steps by the session, so one awaited reactor turn could consume the
whole run. Each role turn now carries a wall-clock ceiling -- but read off the
backend that owns it, not from a constant. A backend's
call_timeout_sis anidle budget between streamed messages, and every input to a legal turn (that
timeout, the attempt count, the backoff) is an environment override, so any fixed
number is only right for the defaults it was written against. Each backend
reports the longest turn its own configuration allows; one that declares no
budget is left to the session bound rather than capped at a guess. A turn
cancelled at its ceiling is recorded as a crash, because the tick advances either
way and the crash count is the only channel left that can end a session wedged on
one role.
Also in this PR
hyperloom.orchestrator.phase_state, whichdoes not exist, and silently fell back to a subset missing 16 terminal stop
reasons -- so a finished session could be relaunched. It now reads the real
vocabulary and says on stderr when it cannot.
leg_ended_tsis Coordinator-owned (CORE_STATE_FIELDS) likestop_ts: thenext leg banks phase time up to it, so a forged value bills a phase for time it
never ran.
SIGTERMthat crosses the watchdog'sSIGHUPis re-read at the last momentbefore the stop is acted on, so an operator's stop stays terminal.
final summary; the close-out steps are collapsed behind one guard.
1800s) instead of being pinnedto three copies of a reactor constant, and the env-var reference matches it.
Test plan
pytest src/hyperloom/orchestrator/ src/hyperloom/inference_optimizer/tests/ src/hyperloom/agents/ -n auto— 16028 passed; the 19 failures reproduce unchanged on the base commit (local gateway/credential env, missinghypothesis).ruff check src/andruff format --checkclean.