Skip to content

fix: hold the session to its wall-clock budget - #1171

Merged
xiaofei-zheng merged 65 commits into
mainfrom
fix/1146-time-budget-enforcement
Aug 19, 2026
Merged

fix: hold the session to its wall-clock budget#1171
xiaofei-zheng merged 65 commits into
mainfrom
fix/1146-time-budget-enforcement

Conversation

@zoroyihan7

@zoroyihan7 zoroyihan7 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Refs #1146. Also fixes #1167.

Scoped to #1146's item 3 (in-flight enforcement) plus the admission gate; items 1, 2, 4 and 5 of that issue are not all closed here, so this does not auto-close it.

A run given a 3h budget spent 3h55. The budget was consulted between ticks and
nowhere else, so the only question ever asked was "should the next tick start?"
-- never "may this action start", "how long may it run", or "is it still worth
waiting for". An explore could be granted four hours inside a three-hour
session and nothing would stop it.

Four defences, each covering what the one before it cannot.

Admission. An action whose expected cost cannot fit the budget that is left
never starts. Refused before a task row exists, so the refusal stays out of the
failure ledgers -- a reaped attempt used to teach the KB that the action fails
when all that happened was the run ran out of time. Fit is judged on expected
cost (p50), not the p75 tail: judging on the tail abandons minutes that would
have been used more than half the time, and overruns are what the later layers
are for. The closing actions stay startable on an empty budget.

Timeout clamping. Action and variant grants are clamped to the budget that
is left, so no single grant can exceed the session. The stack rebench never
consulted the budget at all -- the same hole, second instance.

Session reaper. run_with_session_kill gains a session deadline on its own
channel, checked in every phase and never suspended. The separate channel is
the point: the soft deadline retires at eval start by design, because it judges
whether a variant is abnormally slow, and that boundary is meaningless for "is
the run out of time". An accuracy eval starting a minute before the session
ends used to run to completion.

In-flight cancellation. The backstop for an action already running whose
executor takes no deadline, or that is not spending its time in a subprocess.
The handles lived only in the pump's frame, so the sole caller able to cancel an
action was the frame already blocked awaiting it; they now live on the
dispatcher. This is also what makes SIGTERM work: it only set the stop event
before, and since the pump does not return until everything it dispatched has
finished, shutdown latency was the remaining runtime of the longest action --
after which teardown closed the database out from under work still using it.

Spending the budget early is the other way to lose it

Field data from two sessions on unrelated models -- different quantization,
different PRELUDE composition, one baseline-dominated and one split with a
TraceLens roofline -- showed the four layers above would not have saved either
run. Neither overran at the tail. Both spent roughly three quarters of a
three-hour budget (73.8% and 72.8%) in PRELUDE, whose declared share is 3%, and
both reached FRAMEWORK_AGENT with about 47 minutes against its 108-minute entry
threshold. They were unrecoverable the moment preparation ended.

Nothing enforced the share because exit_normal_prelude tests whether a
baseline landed and nothing else, so the phase ran to whatever its contents
happened to cost. PRELUDE now decides before it spends: prelude_can_afford
bounds an optional arm by the tighter of its own share and what the
optimization phases have reserved, and two arms consult it -- the initial
roofline/profile (81 minutes in one run, 45% of the session, and known-degraded
at second zero because the vLLM version patch had failed) and the baseline's
measured round (59 minutes in the other, to move the anchor 0.2%; dropping it
leaves the single-round baseline the codebase already supports rather than a
new half-measured state). Both estimates come from what the session itself
measured, because the catalog's p50s are calibrated on small models and a guard
reading them would have admitted both arms.

time_exhausted_during_prelude also finally gets a producer: it was in the
terminal-reason vocabulary and in the report's glossary, but nothing ever
assigned it. And PRELUDE's normal exit now states whether the budget it leaves
behind still funds one benchmark round.

A report the session can still write (#1167)

CLOSE reached for a report task by asking "was one enqueued?" when it needed
"is one runnable?", so a cancelled row came straight back and run_task's
queued -> running failed the registry's terminal check with IllegalTransition.
The session that hits this is the one whose report matters most, and it lost
final.md entirely.

This lands with the enforcement layers on purpose: cancelling in-flight work at
the deadline is the point of those layers, and it widens the set of ways a
report task can be sitting in cancelled when CLOSE arrives. A dead row now
falls through to a fresh enqueue under a suffixed idempotency key, and
_run_close_task reports a terminal row instead of handing it to run_task
again.

Two bugs found on the way

The new sentinel returncode first landed on _RAY_ACTOR_DIED_RC, which would
have had every Ray actor death read as a spent budget. The guard test written
to catch that then found a pre-existing overlap: AGENTX_PREFLIGHT_RETURNCODE
and _ACTOR_TIMEOUT_RC were both -912 and both leave _run_magpie through the
same return channel, so a hung actor was recorded as a missing aiperf. Fixed
here as well -- it is the same attribution problem these layers exist to solve.
The Ray code moved rather than the AgentX one, because downstream and every
record written so far already read -912 as the preflight failure.

A field run also crashed on AttributeError: _record_explore_variant_failures,
a _DELEGATED entry naming a method that no longer exists. The test added here
requires every delegated name to resolve on its collaborator; it found two more
stale entries (_resolve_issue_canonical, _wait_for_task_terminal), both
removed. These are now caught at test time rather than mid-session.

Test plan

  • test_session_time_budget.py -- 43 new tests: the fit rule, the usable
    budget accessor, the dispatcher gate, all three intent paths, the
    pre-dispatch backstop, the in-flight handles, the cancellation and its
    closing-action carve-out, the lane released on cancel, the task row
    landing terminal, and the pump / shutdown / Coordinator.stop triggers.
  • Session reaper and sentinel uniqueness guards in
    test_kill_spawned_server.py; clamping in test_explore_executor.py,
    test_grid_runner.py, test_framework_agent_executor.py and
    test_integrate_patch_coverage_unit.py.
  • PRELUDE spend policy in test_phase_state_machine.py (affordability
    bounds, the time-exhausted exit, a landed baseline outranking the guard,
    and the viability sentence on normal exit), the analysis arm in
    test_warm_replay.py, and the measured round in
    test_baseline_warmup_double_run.py.
  • Report-task state guard and the delegation-map check in
    test_close_phase_sequencer.py and
    test_coordinator_async_batch2_unit.py.
  • Full non-e2e suite: 13193 passed. The 16 failures reproduce unchanged on
    a clean tree (subprocess tests needing an installed package) or pass in
    isolation (cross-test pollution).

Inline actions

run_action_now abandons its future once the inline wait elapses -- the action
keeps running by design so the agent's turn is not held hostage -- which
abandoned the only handle on it. It now publishes its own task for the duration,
so the same cancellation reaches it. Its sync bridge also names cancellation
before its generic handler, which had been filing a deliberate stop as
errored.

@zoroyihan7
zoroyihan7 requested a review from a team as a code owner August 13, 2026 03:41
Comment thread src/hyperloom/inference_optimizer/tests/test_session_time_budget.py Fixed
Comment thread src/hyperloom/orchestrator/loop/coordinator_helpers.py Fixed
Comment thread src/hyperloom/orchestrator/loop/coordinator_helpers.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/test_session_time_budget.py Fixed
@zoroyihan7

Copy link
Copy Markdown
Contributor Author

Pushed two commits that extend this PR at the head end. The field data on #1146 — now two independent sessions — showed that stopping on time is necessary and not sufficient, and one of them showed that the enforcement layers here make an existing crash easier to hit.

8c2cdef94 — the CLOSE report guard (#1167), first on purpose

close.py reached for a report task by asking "was one enqueued?" when it needed "is one runnable?". Both reuse paths read task.state only in order to log it, so a cancelled row came straight back and run_task's queued -> running failed the registry's terminal check. The session that hits this is the one whose report matters most, and it lost final.md outright.

This has to land with the enforcement layers rather than after them: a1074febe adds an asyncio.CancelledError handler that transitions in-flight tasks to cancelled, which is exactly the point of the in-flight layer, and it widens the set of ways a report task can be sitting in cancelled when CLOSE arrives. Merging without the guard makes the crash easier to hit, not harder.

A dead row now falls through to a fresh enqueue under a suffixed idempotency key — shared by the report and session_breakdown steps rather than written twice — and _run_close_task reports a terminal row instead of running it, so no path can hand one to run_task again.

While adding the delegation entries this needed, I added the test that would have caught a field-run AttributeError on _record_explore_variant_failures (map entry present, method absent, crash only when the reap loop reached for it): every _DELEGATED name must resolve on its collaborator. It found two stale entries on main, _resolve_issue_canonical and _wait_for_task_terminal, both naming methods that no longer exist anywhere. Removed.

9433b3937 — PRELUDE stops spending the optimization phases' budget

The four layers already here bound the tail. Neither field session was lost in the tail.

PRELUDE's budget share is 3%, and nothing enforced it — exit_normal_prelude tests whether a baseline landed and nothing else, so the phase runs to whatever its contents cost. Two sessions with nothing in common (different model, quantization, and PRELUDE composition — one baseline-dominated, one split with a TraceLens roofline) spent 73.8% and 72.8% of a three-hour budget there, and both reached FRAMEWORK_AGENT with ~47 minutes against its 108-minute threshold. Apply this PR as it stood to either run and you get a session that honours its three hours and delivers a report about a baseline.

So PRELUDE now decides before it spends. prelude_can_afford bounds an optional arm by the tighter of PRELUDE's own share (40%) and what the optimization phases have reserved (50%), and two arms consult it:

  • the initial roofline/profile. In one run it took 81 minutes — 45% of the session — and its own log declared the result degraded at second zero, because the vLLM 0.26 patch had failed (see vLLM version specific patch application; use new profiler configs #1157).
  • the baseline's measured round. In the other it cost 59 minutes to move the anchor 0.2% (cold 514.4 vs hot 513.5 tok/s). Dropping it leaves the single-round baseline this codebase already supports, not a new half-measured state.

Both estimates come from what the session measured, not from the action catalog. That is the part I would most like a second opinion on: the catalog says baseline costs 5 minutes and roofline 8, while these runs measured 51 / 125 and 81, so a catalog-anchored guard admits both arms and the whole thing is decorative. The analysis arm boots its own server and runs the same benchmark under a profiler, so I use one measured baseline round as a floor on its cost; the measured round re-attaches to a hot server, so I use the warmup's own runtime as an upper bound on it.

Two smaller things in the same commit:

  • time_exhausted_during_prelude finally has a producer. It was in the terminal-reason vocabulary and in the report's glossary, but a search over src/ finds nothing that ever assigned it — the state machine had a word for this failure and no way to reach it.
  • PRELUDE's normal exit now records whether the budget it leaves behind still funds one benchmark round. That is the plain sentence neither field session ever got; instead each later phase declined in turn, each for its own local reason.

Not in this PR

The progress-visibility work (#1145, #1168) is going in separately — same root cause as each other, none with this one.

@zoroyihan7

Copy link
Copy Markdown
Contributor Author

/retest

The e2e run for 9433b3937 was cancelled 43s after it started, by the comment posted alongside the push — the same ci-e2e concurrency defect that has now eaten four runs across this PR and #1161. #1173 fixes it; until it lands, /retest is the only comment on a PR that is safe while a run is in flight, because it is the only one whose group membership is intended.

Verified the two new commits locally: 331 passed across test_close_phase_sequencer, test_phase_state_machine, test_baseline_warmup_double_run, test_warm_replay, test_coordinator_async_batch2_unit and test_session_time_budget; 179 passed on the two suites carrying the _DELEGATED guard; ruff clean on all five changed modules.

Comment thread src/hyperloom/orchestrator/actions/executors/baseline.py Fixed
@chaojhou

Copy link
Copy Markdown
Collaborator

Blocking items only. Everything below is verified at PR head against the code, not read off the description.

1. Layer 4 cancels the coroutine; the work keeps running

Every benchmark executor blocks in asyncio.to_thread, and a thread that has already started cannot be cancelled. cancel_inflight_actions gets a clean CancelledError on the await while run_with_session_kill runs on to its own timeout=.

                proc = await asyncio.to_thread(
                    run_with_session_kill,
                    cmd,
                    env=env,
                    cwd=str(output_dir),
                    timeout=timeout_sec,

Same shape at baseline.py:2811 and _grid_runner.py:1626, :1851, :1886. Consequences: Coordinator.stop() cancels, gathers a clean return, then closes SQLite — the exact thing the new docstring says it prevents, with the thread still running; the finally blocks release the GPU specialist lease and the lanes while the benchmark still owns the GPU; and SIGTERM latency is unchanged, since concurrent.futures.thread joins workers at interpreter exit. Where work does stop today is run_grid's local path, and only because layer 3 fires inside the thread — that is layer 3 doing layer 4's job.

The whole cancellation suite passes because _never_finishes is a bare await asyncio.sleep(3600), which no real action resembles. An executor doing await asyncio.to_thread(time.sleep, 30), asserting the thread is gone before cancel_inflight_actions returns, fails today and is the test this layer needs.

2. Layer 3 is dropped on the Ray serving path, which is the single-node default — and the clamp then manufactures false timeouts

_run_magpie returns through the lease and session_deadline_sec is not among the arguments:

        return serving_lease.run_session_kill(
            cmd,
            env=env,
            cwd=cwd,
            timeout=timeout_sec,
            soft_deadline_sec=soft_deadline_sec,

ServingLease.run_session_kill has no such parameter (_ray_serving.py:385-395), and it cannot trivially gain one: session_deadline_sec is a time.monotonic() instant, meaningless in the actor's process. The representation blocks propagation, not just the plumbing. With INFERENCE_OPTIMIZER_RAY_EXEC unset on a single node, the lease is created (_ray_backend.py:52-61), so this is the default path.

The failure is worse than "no reaper", because the clamp still applies. After a warmup that overran, _round_timeout_sec returns max(1, negative + 15) = 1 second (_grid_runner.py:1392-1393); the round is granted 1s, the lease converts the actor timeout into TimeoutExpired, and run_grid files it as a verdict about the variant:

                    status="failed",
                    error=f"timeout: {exc}",
                    error_class="magpie_timeout",

That is the "ledger full of spurious timeouts" the clamp's own docstring exists to prevent. The test pinning the clamp (test_grid_runner.py:1635) patches run_with_session_kill, so it only covers the local path.

3. baseline — the hole #1146 names as the largest — is covered by none of the four layers

BASELINE_DEFAULT_TIMEOUT_SEC = 7800  # WARM-start cap, 130 min
BASELINE_COLD_START_TIMEOUT_SEC = 9000  # COLD-start cap, 150 min (includes ~20 min cuda graph capture)

Admission admits it on a catalog cost_minutes_p50 of 5.0; _resolve_timeout (baseline.py:995-1074) never consults the budget; run_with_session_kill is called without session_deadline_sec; and it blocks in to_thread. session_deadline_sec appears nowhere in baseline.py. What the PR adds is a PRELUDE affordability gate on the second round only (baseline.py:2211-2231) — the 3941s MiniMax warmup that motivated the work, 36% of a 180-minute budget, stays unguarded. profile.py (PROFILE_DEFAULT_TIMEOUT_SEC = 14400, longer than the whole budget) is not in the file list at all, and #1146 names baseline and profile together as the two largest holes.

Relatedly, layer 1 is anchored on the static catalog, which this PR's own code documents as an order of magnitude low:

        Anchored on this session's own baseline round rather than the action
        catalog. The catalog's estimates (``baseline`` 5 min, ``roofline``
        8 min) are calibrated on small models: the two sessions that motivated
        this guard measured 51 and 125 minutes of baseline and an 81-minute
        roofline, so a catalog-anchored guard admits an arm it cannot pay for.

The PRELUDE guard uses measured cost; layer 1 uses the catalog, and would not have stopped either field run. SharedState.baseline_runtime_sec is already in scope there.


Two coordination notes, since these are decisions rather than defects. Closes #1146 overstates what lands: issue items 1 (one deadline), 2 (baseline/profile), 4 (report timeout and a measured close reserve) and 5 (an end-to-end assertion) are not addressed — Refs would be accurate. And this branch conflicts with #1177: both add an import at baseline.py:33 and insert into BaselineExecutor at 2206/2207, and both modify the same lines of sub_agent_runner.py. Against #1178 the shared files (baseline.py, machine_state.py) touch disjoint regions and should auto-merge.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

CI E2E report — ✅ Succeeded

item value
result ✅ Succeeded
model Qwen/Qwen3-0.6B (dense)
resources 1× GPU, TP=1
PR branch fix/1146-time-budget-enforcement
commit f2c241cbc32ee9f020858c118de289ebe0ea29d5
session_id 1813b9e8-dfdf-485a-9a35-1388161ef5f2
queue → dispatch 0s
run time 168m 14s
total 168m 14s

details

zoroyihan7 added a commit that referenced this pull request Aug 13, 2026
…still merges

The hook grew a progress report, so "pulse" stopped describing all of it and
it was renamed to match. But the name is a merge surface: #1171 adds a call
site of its own, in the branch that records a variant reaped by the session
budget as skipped rather than failed. Renaming here deletes the definition
that call site resolves to, in a region #1171 never touches, so git reports
no conflict and the merge lands a NameError on the reaped-variant path --
found only by lint or by running it.

Keep the name and say what it does in the docstring instead. Costs an
imprecise word; buys a merge that cannot break silently in either order.

Co-authored-by: Cursor <cursoragent@cursor.com>
@zoroyihan7

Copy link
Copy Markdown
Contributor Author

All three blocking items are fixed, plus a fourth defect that surfaced while verifying them and would have been the worst of the set. Both coordination notes are accepted; one of them turns out to be understated.

1. Layer 4 now stops the work, not just the coroutine — a52e39122

The test you specified came first and was red: an executor doing await asyncio.to_thread(run_with_session_kill, ["sleep", 30], ...), asserting the thread is finished before cancel_inflight_actions returns.

orchestrator/actions/cancel_channel.py carries a CancelScope (a threading.Event, first reason wins, listener count) published through a ContextVar. The dispatcher opens the scope before the action takes off; asyncio.to_thread copies the context, so the 0.5s poll loop in _communicate_with_soft_deadline sees it at the same point it checks the session deadline, kills the process group, and raises a distinct exception landing on ORCHESTRATOR_CANCELLED_RETURNCODE = -917. -916 was taken by the Ray actor timeout; the collision-guard test caught that on the first attempt.

Two additions beyond the sketch. run_with_session_kill registers as a listener before Popen, so cancel_inflight_actions can tell work that can hear it from work that cannot — the former gets a 10s grace window, the latter is cancelled immediately, instead of every asyncio.sleep coroutine costing a shutdown 10 seconds. And the five identical sentinel handlers in _subprocess_kill collapsed into one _ReapedByWatchdog base, so a new stop reason no longer means a sixth copy.

To prove red/green rather than assert it, I stubbed CancelScope.cancel to a no-op (equivalent to the pre-fix behaviour) and re-ran: test_the_work_is_over_before_the_cancel_returns fails with "the cancel returned while the thread was still running", and the attribution test fails on a missing returncode.

Scope limit, stated rather than hidden: a ContextVar does not cross a process boundary, so Ray-managed work is not reachable by this signal and still relies on the remaining budget passed to it. test_code_outside_an_action_finds_no_scope pins that. Carrying orchestrator cancel through the Ray boundary needs an actor-level control channel — a separate PR.

2. The Ray serving path carries the session clock — d78e0ccc0

You identified the real blocker: the representation, not the plumbing. A time.monotonic() instant means nothing in the actor's process. So the deadline is converted at the boundary — session_deadline_to_remaining_sec() on the caller side, session_remaining_to_deadline_sec() reconstructing a local monotonic deadline on the worker — and the parameter is named session_remaining_sec so nobody is tempted to pass an absolute instant across.

Worth recording why the suite was green: _should_use_ray_backend() returns False under pytest, so every existing test exercised the local path. Added coverage drives a mock actor and asserts the forwarded remaining seconds.

3. Baseline and profile are covered — 4bd6bad2e, and layer 1 in 4b8502144

Three things together, each harmful without the others: _run_single_benchmark resolves the deadline per round (a baseline runs up to three, and an overrun warmup has already spent what the later ones assumed); session_clamped_timeout_sec clamps the 130/150-minute hang backstop to what remains; and a stopped_by_the_run branch attributes the sentinel returncodes to session_time_exhausted / orchestrator_cancelled rather than server_init_dead. That third one matters as much as the first: a reaped round leaves evidence identical to a server that never came up — no workspace, no report, non-zero returncode — so without it the ledger records budget exhaustion as a verdict about the model.

The clamp and the classifier were hoisted out of the grid rather than copied; _round_timeout_sec's body became the shared helper and grid kept only its own log line. ProfileExecutor subclasses BaselineExecutor and overrides only config parsing and the trace pipeline, so it inherits all of it — but it is covered as its own test arm (4-hour default against a tight budget) rather than on the strength of "it should inherit".

On layer 1 being catalogue-anchored: fixed, and PRELUDE's rule is now the only definition. expected_action_cost_minutes() takes this session's measured baseline and uses it as a lower bound for actions that run benchmark rounds — a grid is never cheaper than one round, and only the catalogue knows how many rounds. Which actions those are is read from requires_lanes (benchmark_lane / profile_lane) rather than a name list. The test is the case you described: with 30 minutes left, a baseline priced at 5 catalogue minutes is admitted; after this session measures 51 minutes, the same call is refused and the message says 51.

4. The one that would have shipped silently: layer 1 was reading a field that no longer exists

main removed cost_minutes_p50 from ACTION_CATALOGUE in a708deca6 as unused. This PR added the first reader. Neither side is wrong alone; merged, getattr(meta, "cost_minutes_p50", 0.0) starts returning 0.0 and the admission gate admits everything regardless of budget — the gate that is item 1 of this PR, disabled by a merge. Reproduced at the merge commit: 8 red tests. fd99d0545 reads typical_runtime_min (which carries the same small-model values) through a helper, with a guardian test asserting no action in the catalogue prices out at zero.

Coordination

Closes #1146Refs #1146, updating the description now; items 1, 2, 4 and 5 of the issue are not all closed by this.

On the #1177 collision, your list is right and there is one more that git will not tell you about. #1177 renames _pulse_after_variant to _after_variant and rewrites all 13 call sites; this PR keeps the old name and adds a 14th, inside the new reaped-variant branch. The two regions do not overlap, so the merge is reported clean and lands a NameError on precisely the path that records a budget-reaped variant as skipped — this PR's headline behaviour, converted into a crash, in a tree where both branches' own suites are green. I merged the four branches in a scratch worktree to find it. It is fixed on #1177's side (2a131a4be, keeping the old name), so the merge is now safe in either order; the four textual conflicts remain and are all genuinely orthogonal. The integration tree runs 1101 passed / 0 failed and adds no new ruff finding over main.

Comment thread src/hyperloom/inference_optimizer/tests/conftest.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/conftest.py Fixed
ZhengGong-amd pushed a commit that referenced this pull request Aug 17, 2026
* fix(observability): let a long task say it is still working

A composite action — an explore grid, a baseline pair, a profile and its
roofline — is one task row that internally completes many units over hours.
Between dispatch and return it emitted nothing durable, so a healthy 80-minute
run and a wedged one left identical evidence. Field sessions read that as
"orchestration silent 8097s" while the run was in fact progressing normally,
and as a health probe demanding a restart of a server no phase had booted yet.

Executors now report each unit through an ambient reporter the runner binds to
the task, landing on the row as a heartbeat that moves ``updated_at``. Stall
detection consults it and withholds the accusation while dispatched work is
still moving; the server probe holds its fire when no server process is up.

Emitters: per variant in the grid runner, after the baseline warmup round, and
per roofline sub-step.

Refs #1145, #1168

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): bound and attribute the progress suppression

The heartbeat's consumer suppressed too much. A single running task that had
reported recently returned no symptoms at all, before the per-agent loop ever
ran: one busy explore grid vouched for the Coordinator, the critic and the
kernel agent alike, and a genuinely hung agent went unreported for as long as
anything on the box kept moving. Nothing was logged, so the near-miss left no
trace an operator could read afterwards.

Freshness also came from ``MAX(updated_at)`` over running rows, which a task
merely entering ``running`` advances — the suppression could fire without a
single heartbeat behind it.

Progress notes now name their owning agent, the probe reads them out of the
task history instead of trusting a column that moves for other reasons, and
the judgement moved into the per-agent branch: only an agent's own work speaks
for it. Work buys time, not immunity — past ``severity_high_after_s`` the
accusation fires regardless of how busy the machine looks — and a withheld
accusation is downgraded to LOW with its counter-evidence attached rather than
discarded, so "we would have accused X, but its work reported Ns ago" is
visible in the session.

Attribution is as narrow as the data supports: the ``tasks`` table has no
requester column and every row is dispatched by the Coordinator's
orchestration loop, so a heartbeat is stamped for that agent alone.

Refs #1145

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): report grid and baseline units before they block

The completion reports only fire once a unit has landed, so the very
failure this heartbeat targets - a first variant or a warmup round that
hangs for tens of minutes - still produced no evidence at all. Announce
each unit on entry, before the launch that can block, and keep the
completion reports as they were.

Entry reporting also covers the branches that reach neither the
completion report nor a finally, so `_after_variant` is left alone
rather than moved into a finally that would double-report.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): route every roofline sub-step through one reporter

Three of the five sub-step call sites were bare: the N26 re-analysis and
both halves of the multi-node compute-bound re-profile ran silently, and
each of them can take tens of minutes. Rather than paste the same report
block at each site, funnel every call through a single closure next to
the local imports, so a site added later cannot be the silent one.

Each call site keeps its own try/except and fail-soft semantics
unchanged; only the await is wrapped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): let the probe see a busy ray worker

``ray::IDLE`` is the name of a parked worker only; once a worker starts
running a serving actor Ray renames it after the actor class, so
``ray::ServingActor`` matched no pattern and the process disappeared
from the probe entirely.

Presence still says nothing about busy-versus-hung — it only stops the
probe reporting an empty machine while work is on it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(observability): let a long step prove it is alive from its own output

A start-of-unit report covers the first 300 seconds and nothing after,
so the 55-minute TraceLens analysis - one await, no units - still went
stale halfway through. A timer would have "fixed" it by vouching for
wedged processes, which is the exact failure the stall signal exists to
catch.

Drive the heartbeat from the child's own output instead: the subprocess
reader already walks stdout/stderr line by line, so it now tallies what
it reads, and a driver on the loop reports once per interval in which
that tally moved. A silent child produces no heartbeat and stays
accusable. The kernel-tool child is unbuffered so its lines arrive while
it works rather than in eight-kilobyte lumps.

Covers the TraceLens analysis (via every kernel tool subprocess) and the
profile / baseline benchmark round. The Ray-managed round runs its child
in a remote worker with no channel back for the callback, and is left on
the ceiling from the first commit in this series.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): stop the progress note from moving updated_at

``updated_at`` marks when a task entered ``running``, and three consumers
measure elapsed runtime from it: the R6 lease watchdog turned into an
inactivity timeout that a heartbeating task could never trip, the
``extend_lease`` remaining-budget math read time-since-last-note as
time-already-spent and handed lanes and GPUs more than the budget allowed,
and the Coordinator's own "Tasks in flight" line rendered an 80-minute task
as seconds old — this PR's goal running backwards in the one surface the
Coordinator reads. ``running()`` gets its least-recently-updated-first
meaning back for free.

The write had no reader left: the probe already reads freshness from the
progress notes on ``history`` rather than from the column. The docstring
argued for the behaviour and is rewritten to state the invariant instead,
matching the one ``extend_lease`` documents a few lines below.

Removing the bump restores the pre-PR reaper behaviour rather than changing
it. It does not close the pre-existing gap between the catalogue's
``lease_ttl_sec`` and observed composite runtimes (roofline 2700s vs a
3106s TraceLens step, baseline 4200s vs a 3941s warmup round plus its
measured round); that needs a TTL or extension policy decision and is
reported on the PR rather than papered over here.

Refs #1145

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): keep the variant hook's name so a sibling change still merges

The hook grew a progress report, so "pulse" stopped describing all of it and
it was renamed to match. But the name is a merge surface: #1171 adds a call
site of its own, in the branch that records a variant reaped by the session
budget as skipped rather than failed. Renaming here deletes the definition
that call site resolves to, in a region #1171 never touches, so git reports
no conflict and the merge lands a NameError on the reaped-variant path --
found only by lint or by running it.

Keep the name and say what it does in the docstring instead. Costs an
imprecise word; buys a merge that cannot break silently in either order.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): roll back a transaction the caller was cancelled out of

``asyncio.CancelledError`` derives from ``BaseException``, so the ``except
Exception`` guard around ``transaction()`` never saw it: a cancel landing on
one of the ``to_thread`` hops after ``BEGIN IMMEDIATE`` had already run left
the shared connection inside a transaction for the rest of the session, and
every later registry write failed with "cannot start a transaction within a
transaction". The heartbeat this branch adds is the first path that routinely
cancels a coroutine mid-write, once per long subprocess step.

Catch ``BaseException``, and roll back inline rather than on a worker thread —
the exception being handled is usually this task's own cancellation, and an
``await`` there can be cancelled in turn. A rollback that itself fails is
logged, never allowed to mask the original exception.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): stop the heartbeat driver cooperatively

Teardown used to hard-cancel the driver and swallow the result. Both halves
were wrong.

The cancel could land while the driver was inside a ``tasks`` row write, which
is exactly the case that leaves the coordinator's connection wedged. The driver
now polls a stop flag between ticks, so teardown asks it to finish the note it
is in and only cancels if it overruns a bounded grace — a wedged sink costs the
executor the grace window and nothing more.

``suppress(CancelledError)`` around the await could not tell the driver's own
cancellation from the enclosing task being cancelled during teardown, so an
outer cancel was absorbed and the step returned as though it had finished.
Nothing catches ``CancelledError`` on that path any more.

``interval_s`` now resolves its default at call time so the timescale can be
compressed under test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): report liveness from the three long paths still missing it

``_run_magpie`` is the longest single block in a session — up to a 7800s
variant timeout against a 300s suppression window — and it ran the benchmark
with no liveness callback, so a grid announced each variant on entry and then
went dark. Same shape in the baseline's discarded multi-node warmup pass.

Both now run under the same output-driven heartbeat the measured baseline round
already uses. In the grid all three Magpie passes (auto-warmup, multi-node
warmup, benchmark) go through one helper, so a pass added later cannot be the
one that reports nothing.

The Ray serving-lease branch is left as an explicit gap in both files: the round
executes inside an actor in another process and only its final ``(rc, stdout,
stderr)`` crosses back, so there is no local per-line event to hang a callback
on. A Ray-backed round reports on entry and stays quiet until it returns.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): bound how long a path may go unreported

The suite asserted that notes are emitted, never that consecutive notes on a
path stay closer together than the window a consumer waits before calling the
owning agent silent — which is how a benchmark with no liveness callback
survived a change whose whole purpose was to close that gap.

``ProgressCadence`` records each note against a compressed clock, so a test
second stands for ten production minutes and the assertions still read in the
numbers the window is configured with. Every long path now has a test that
fails if its callback is dropped: the grid variant benchmark, the baseline
round, the multi-node warmup pass and the kernel tool subprocess.

Two existing tests could not fail for the reason they existed:
``test_run_subprocess_counts_the_lines_its_child_emits`` counted nothing, only
that a callable had been passed, and now asserts one call per line the child
emits; ``test_a_variant_reports_before_it_blocks`` covered the entry marker and
nothing after it, so a cadence test now sits beside it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): let the configured server patterns decide what a server is

``is_server`` was computed against the module constant while the match list
came from ``config.server_process_patterns``, so a framework an operator added
to the documented knob was matched as a process yet flagged as not-a-server —
which silently disabled ``local_server_unreachable`` for exactly the
deployment that configured it. The probe config now carries the server subset
and the flag follows it.

An empty process list was also doing double duty: "nothing is running" and "we
could not find out". ``_sample_processes`` returns ``None`` for the second and
``SourceData`` carries ``local_processes_known``, so a missing or wedged ``ps``
no longer mutes an unrelated finding.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kernel): default the child's buffering instead of dictating it

Unbuffering a kernel tool keeps the heartbeat above it honest, but assigning
PYTHONUNBUFFERED overrode an operator who had set it deliberately. The three
launcher sites in multi_node/scripts already use setdefault; match them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): keep a pathological JSON blob inside its own sub-probe

_json_loads_or_none caught the decode errors but not RecursionError, so a
deeply nested history blob written by another process aborted the entire probe
tick instead of costing only the sub-probe that read it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(robustness): document the withheld agent_stall as its own row

The symptom table described agent_stall as medium/high only; a stall whose
dispatched work is still reporting is LOW and routes to send_message
(observation), which the ladder test now locks in.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): bound stall suppression by evidence freshness, not the clock

The 900s ceiling on a withheld agent_stall assumed a suppressed accusation
should eventually fire anyway. Measured work units break that assumption: a
single warmup runs 3941s and a TraceLens pass 3106s, so every healthy long
phase crossed the ceiling and alerted exactly as if nothing had reported.

Suppression now lasts as long as the counter-evidence stays fresh — the tick
after the work stops reporting accuses at full severity. Long waits are still
visible: the withheld note itself rises from LOW to MEDIUM past the threshold,
which is now operator-configurable via agent_stall_high_after_s.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): measure the cadence on a simulated clock, not the wall

The four cadence assertions compressed a 300s suppression window into 0.5s of
real time and then compared a real wall-clock gap against it, so every
scheduling delay in the test was multiplied by 600 and charged to the path
under test. The dominant term was not the heartbeat at all but the run-up
before the first note -- fixture setup, imports, the fake magpie workspace --
which on two cores under eight-way contention grew to 753/888/1035 production
seconds against a 300 budget.

Advance the clock only when the simulated child does a chunk of the work it
stands in for, so the timeline is a pure function of the lines emitted. The
run-up and the tail collapse to what they represent, the steady-state spacing
stays at the production ratio, and the assertion keeps failing when a liveness
callback is dropped or the tick is widened past the window.

Fake the kernel tool's child with the same helper the other three use: with a
real subprocess there is nothing in the test that can advance the simulated
clock. The real pump's line counting keeps its own coverage in
test_run_subprocess_counts_the_lines_its_child_emits.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): let a benchmark client vouch that a server was wanted

The process guard read "probed successfully, saw no server process" as "no
server was supposed to be running", which is true of the gap between two
variants and false of a server that crashed while its own load generator was
still sending requests into the closed port. That second snapshot is exactly
the one worth alerting on, and it was the one being suppressed: all targets
unreachable plus one is_server=False benchmark_serving process produced no
symptoms at all where base emitted a HIGH local_server_unreachable.

Suppress only when the probe answered, no server process is up and no
benchmark client is running. The client patterns are deliberately narrower than
the harness patterns the process probe matches -- the outer Magpie/InferenceX
harness is up across server launch and teardown too, so it stays evidence of
nothing, which is what keeps the idle-stretch guard intact.

Record benchmark_client_seen next to server_process_seen so the branch that
decided is legible in the alert, and give the suppressed case its own row in
the symptom table, which still promised that any target down always alerts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): keep the in-flight unit a fresher sibling was hiding

The snapshot kept only the newest heartbeat per agent, so with two units
dispatched concurrently -- one reporting, one quiet for hours -- the quiet one
left no trace at all and the withheld accusation rested entirely on the healthy
sibling. Only ``running`` hinted that a second unit existed, and nothing read
it.

Keep both ends of the range and name the quiet one in the evidence. Not the
oldest alone, and not "decline to withhold when some unit is quiet": a quiet
unit is not an agent fault, and quiet units are expected today -- a Ray-backed
baseline round has no local process to call a liveness callback from, so it
reports on entry and then not again until it returns, which either of those
readings would turn into an accusation on a healthy run. The stuck unit's own
backstop is the lease watchdog. What was missing was visibility, not severity.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(robustness): cover the quiet fallback's admission that nothing looked

Deleting local_processes_known=False from _QuietFallback left test_factory
fully green, which made the one line standing between "nothing looked" and
"nothing is running" the only unprotected decision in the module.

Assert it through the consumer it defends -- the guard that suppresses
local_server_unreachable when the process probe saw no server -- so the test
says why the flag exists rather than restating the assignment.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): keep the loop running while a cancelled write rolls back

The rollback added for cancelled ``transaction()`` bodies took ``_sync_lock`` on
the event-loop thread, and in the cancellation case it queued behind the very
worker the cancel had abandoned: that worker is parked inside its own ``BEGIN
IMMEDIATE``, holding the lock, for as long as another writer holds the database
— up to ``PRAGMA busy_timeout = 30000``. With a 3s external writer, the whole
orchestrator stopped for 2.55s, and it stopped during the shutdown or
budget-exhaustion window that issued the cancel, so a stalled loop ate the
cooperative-stop window it was supposed to be spending.

Run the rollback on a worker thread under ``asyncio.shield``. The worker queues
behind the abandoned one on ``_sync_lock``, which is the ordering the rollback
already relied on, and the shield keeps it uncancellable — a bare ``await`` in a
handler for this task's own cancellation is exactly how the connection stayed
wedged in a transaction. Awaited, not fired and forgotten: ``_async_lock`` stays
held until the connection is clean, so no later ``BEGIN IMMEDIATE`` can find a
transaction still open. Anything that goes wrong with the rollback itself — a
failing statement, a shut-down executor during teardown, a second cancel landing
on the wait — is logged and never masks the caller's original exception.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): pace the heartbeat tests on the driver, not the clock

Four of these tests asserted a note count reached after sleeping a few tick
intervals, which only holds if the loop was scheduled during that sleep. On the
2-vCPU CI runner, with two xdist workers and coverage tracing, it is not: under
comparable oversubscription three of them failed in every run, because the
driver had not yet taken the tick the assertion assumed.

Wait for the driver's own notes instead. A note is proof the driver got that
tick, so the wait is as long as the machine needs and the counts become exact
rather than lower bounds. A driver that is supposed to stay silent — the one
watching a child that went quiet, and the one whose step already returned — is
paced by a second heartbeat kept deliberately noisy, so the window it would have
reported in is intervals it actually got rather than intervals the test hoped
had gone by, and the assertion becomes the ordered sequence of who spoke. Every
wait is bounded so a driver that stops reporting fails in seconds.

Also drop ``assert elapsed < 1.0`` from the wedged-sink test: a teardown that
waited on the sink instead of cancelling it would still be inside the ``async
with``, never reaching the assertion, so the measured interval was carrying no
coverage that ``events == ["cancelled"]`` does not.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): wait out the rollback a second cancel used to abandon

``asyncio.shield`` protects the rollback, not the wait on it. A cancel landing on
that wait raised straight through the ``except BaseException`` written to keep the
caller's exception, and ``transaction()`` then ran on to its ``raise`` and released
``_async_lock`` with the rollback still queued on ``_sync_lock`` behind the worker
the first cancel abandoned -- fire and forget with the lock already gone, which is
the shape this rollback was moved off the loop to avoid rather than to adopt. The
next writer finds the connection inside a transaction nobody will close and every
write in the session fails with "cannot start a transaction within a transaction".

The same handler lost the cancellation outright. One cancel is enough: a
``record_progress`` body raises on its own -- a ``BEGIN IMMEDIATE`` past
``busy_timeout``, a ``history`` column that will not parse -- and the swallowed
cancel left the caller with the body's exception, which ``report_progress`` drops.
An action cancelled by the stop path then runs on as though nothing had happened
while the dispatcher's ``gather`` waits for it.

Keep the shielded future and resume the wait after every cancel, so the connection
is always clean before ``_async_lock`` is released, then re-raise the cancel that
arrived. Narrow the ``except`` to what a rollback can actually fail with -- a
statement error, or an executor refusing work during teardown -- so control flow
is no longer swallowed as though it were a database error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): report a step alive on its own output, not the server's log

``grew`` means "one of the resolved logs got longer", from any writer, and feeding
it to the liveness callback made the heartbeat the bare timer
``heartbeat_while_output_flows`` documents itself as never being. A child that
printed nothing for 3s was reported alive 5 times by a ``server.log`` an unrelated
thread appended to. In a real round that writer is the inference server, which
keeps logging while the benchmark client is wedged, and both vLLM and sglang write
an access line per request -- including the health probe the robustness agent
issues every standalone tick against the server it is monitoring. That closes a
loop where the monitor's own probe manufactures the evidence suppressing its own
stall accusation, and the wall-clock ceiling that used to backstop the case is
gone.

Report liveness on evidence about the round instead: a generation-progress marker,
which says tokens were flowing whoever logged them, or growth in the benchmark
body's own redirected stderr, which is output of this very child that never
reaches its pipe. Keeping the second is what stops the narrowing from silencing a
working round: the scriptable and bypass paths redirect the body's stderr to
``benchmark_stderr.log``, so for a whole round the pipe is empty and that file is
all there is. The stall watchdog keeps the broad "any new bytes" signal, which is
the right one for a gate that kills.

The scan now returns a named tuple rather than a fourth positional bool nobody
could read, which is also how the throwaway ``_saw_progress`` binding at the call
site goes away.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): name the variant that ran in its own progress note

``_pulse_after_variant`` inferred which variant it was reporting from the tail of
``results`` rather than from the index it was handed. A stop cause that ends the
batch records the round it stopped and then a not-run row for every later variant,
so the tail is the last variant in the grid and the terminal note for variant 1 of
3 reads ``label='c2', index=1, total=3``. The log line one frame away is right and
the ledger is right; only the progress note -- the artefact this heartbeat exists
to make honest, and the one a stall signal reads -- is wrong.

Locate the row by index instead, in a helper that says why the tail is not it, and
take the label from the grid entry where it cannot disagree. Extracting the note
also takes it out of a ``run_grid`` that is already far past the length worth
reading, and leaves ``_pulse_after_variant``'s signature alone so no call site has
to change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): bound the wedged-sink step so a regression fails, not hangs

The sink sleeps for an hour, so a teardown that waited on it instead of cancelling
it never leaves the ``async with`` and never reaches an assertion. With no timeout
plugin in this suite that blocks until the CI job is killed -- the failure mode the
grace window under test was added to prevent, which the test for it should be the
last place to reproduce. Run the step under ``asyncio.wait_for``: the regression now
fails in ten seconds where it previously hung indefinitely.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): take the rate, not the line, as proof of tokens

Narrowing the liveness callback to generation-progress markers left one writer
still able to vouch for a wedged child: some vLLM builds print ``Avg generation
throughput: 0.0 tokens/s`` on an idle engine, and an engine goes idle precisely
when the client driving it wedges. The most likely production shape of the defect
therefore survived the fix -- a server logging zeros suppresses the stall
accusation its own client has earned.

Read the value instead of the marker, reusing the parse the post-mortem
throughput estimate already runs over the same two frameworks' lines: the third
copy of that knowledge goes away, and a framework added to one parser can no
longer drift from the other. A line whose rate cannot be read counts as no
progress -- this evidence only ever suppresses an accusation, and one suppressed
by mistake is invisible, where a missing one is visible and still answerable from
the child's own redirected stderr.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): end the variant a failed remote restart abandoned

Every variant outcome reports a terminal note except one: a multi-node round whose
server never came back recorded its row and left, so the row a stall signal reads
stayed at ``started`` for the rest of the session while the variant was already
over. Two behaviour-lock tests pinned that omission as though it were a rule,
which is how it outlived the boundary the other thirteen paths share -- and unlike
a reaped round, nothing moves the task afterwards to make the stale row harmless.

Reach the boundary like every sibling does, so the note comes from
``_variant_progress_note`` and names the variant that ended rather than the tail
of the batch, and the failed restart gets the same bounded robustness tick as
every other variant boundary. The loop's 27 early exits were enumerated to
confirm this was the only gap: the one remaining exit that reports nothing is the
session-budget skip, which breaks before the variant is announced and so has no
row to close.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): let the evidence, not the wait, grade a withheld stall

The withheld note rose to MEDIUM once the agent had been silent past
severity_high_after_s, which is the wall-clock ceiling the previous commit set
out to remove, one rung lower: MEDIUM routes to alert(medium), so the measured
healthy runs it cited -- a 3941s warmup, a 3106s TraceLens pass -- alerted while
reporting a unit every tick.

Elapsed silence now grades only the accusation. A phase whose work keeps
reporting stays on the observation tier however long it runs, and the tick after
the reports stop accuses at full severity, which is what "bounded by evidence
freshness" was supposed to mean.

The withheld case gets its own symptom name so RCA can tell a healthy long
phase from an agent that really went quiet, and the evidence records the
freshness window that holds the accusation back. The ladder tier a withheld note
must not reach is pinned by a test of its own.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(robustness): visit the progress rows in both orders

The shipped test inserted the quiet unit first, so the freshest note always
arrived second and the branch that records the quiet end never ran: deleting it
left the whole suite green. Which order the rows arrived in was also SQLite's
choice, the running-task select having no ORDER BY, so the assertion rested on
unspecified behaviour either way.

Order the select by task_id and let the test choose which unit's note is folded
in first, so both directions of the fold are covered by the same case.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(state): bound the progress notes a task's history retains

Every note re-read the whole history blob, appended to it and rewrote it inside
a BEGIN IMMEDIATE on the connection every other writer serialises behind, with
no cap. 720 notes -- 12 hours at the 60s heartbeat -- left a blob north of
100 KB and tens of MB of cumulative row rewrites, and the robustness probe
re-parses that blob for every running task on every tick. The cost scaled with
session length, which is what the heartbeat exists for.

Retain the newest 120 notes, two hours of trail at that cadence and longer than
the longest measured work unit. Transitions are never dropped: consumers read
those positionally -- the last entry for a failure class, the newest
queued -> cancelled for a policy denial -- so only progress notes are retired.

Measured on this branch, 720 notes: blob 103 KB -> 17 KB, cumulative rewrites
35.6 MiB -> 10.9 MiB, and flat per note from there instead of growing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): tie a vouching benchmark client to this session

The client that overrides "no server process, so none was wanted" was found in a
whole-host ps snapshot, so on a shared node a co-tenant's benchmark_serving
vouched that this session's port should be answering, and an idle stretch came
back as a HIGH alert on somebody else's traffic.

The process probe now records each matched process's working directory, which is
the anchor a session already has: the harness is launched with its cwd inside
the session directory and its children inherit it. A client counts as this
session's when its cwd is under that directory, or when it names a path there on
its command line, for the launch paths that chdir elsewhere. With no session
directory configured there is nothing to compare against and the check stays
host-wide as before.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(robustness): say where the stall thresholds are actually set

The escalation knob was described as operator-configurable, but discover() reads
a fixed list of deployment-shape variables and this is not one of them, so it is
set in code like every other threshold on Config -- gpu_temp_warn_c, the disk and
shm percentages, its own sibling agent_stall_timeout_s.

Correct the claim rather than wire these two into discover(): none of the ~60
thresholds is environment-settable, and making the stall pair the exception would
buy one deployment knob at the price of an env variable per threshold and a doc
table that has to keep up. Where the boundary runs is now written down next to
the thresholds and under the environment table.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): run a grid variant inside the session that owns it

A benchmark client only overrides "no server process, so none was wanted" when it
can be tied to this session, and the tie is the working directory the harness's
whole subtree inherits. The baseline arm anchors Magpie to its own output dir for
that reason; the grid runner defaulted to the system temp directory, so an explore
/ sweep / conc_sweep / integrate_patch client carried no anchor of its own and a
server that died mid-variant read as the idle gap between two variants -- the one
snapshot the vouching rule exists to catch. Grid variants are where a server most
often dies mid-benchmark.

Each pass now runs from output_root, the per-task workspace under the session,
exactly as the baseline does. Nothing that reads a benchmark's results depended on
that directory being /tmp: Magpie re-roots the server itself, and workspace
selection scans the per-variant output dir while the leak harvest scans /workspace
plus $INFERENCE_OPTIMIZER_LEAK_ROOTS -- never the cwd. A relative result_dir now
resolves under the task workspace, which is where the baseline already resolves
one and where the salvage path can find what it produced.

The cwd override goes with it. No caller set it, and an override is the one knob
that could let the anchor drift back out of the session.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): tie a client to the session by directory, not by prefix

The cwd reading guarded the directory boundary; the command-line reading on the
next line was a bare substring test. A co-tenant benchmarking out of
<session>-retry -- a retry, a backup, or any sibling an operator names after ours
-- contains this session's path, so it still vouched that this session's port
should be answering. That is the false alarm the previous fix set out to remove,
narrowed from host-wide to prefix-wide rather than eliminated, and it is reachable
whenever the session directory is operator-supplied.

Both readings now go through one helper that compares a path a component at a time,
so the two cannot drift apart again, and the command line is read as tokens (plus
the value of a key=value token, so --result-dir=<path> still counts) rather than
searched for a substring, which is what makes a boundary applicable to it at all.

The launch-path matrix now also holds the case that hid the missing grid anchor: a
client with neither cwd nor command line under the session is indistinguishable
from a co-tenant's and must not vouch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(robustness): hold both spellings of --result-dir to the session boundary

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(grid): say why the session-rooted cwd is created up front

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make the heartbeat cancel tests name the exception they wait for

CodeQL read `await cancelled_task` inside pytest.raises as a statement
with no effect, and the log assertions after a raise it did not model
as unreachable. Catch CancelledError and ValueError explicitly so the
control flow is the test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Satisfy CodeQL on the heartbeat cancel tests without empty excepts

The previous try/except/else left an unreachable else, and a bare
`except CancelledError: pass` is an empty handler. One helper now
documents the success path, and the rollback-failure test goes back
to pytest.raises with the log assertions inside that block.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Stop CodeQL treating the rollback-failure asserts as dead

pytest.raises is not modelled as catching the raise, so the log
assertions after it were unreachable. Catch ValueError by assignment
instead, with no else-clause and no context manager for CodeQL to
misread.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/hyperloom/orchestrator/loop/coordinator.py Fixed
Comment thread src/hyperloom/orchestrator/loop/coordinator.py Fixed
zoroyihan7 and others added 9 commits August 18, 2026 03:30
An action was admitted on its own merits and only met the wall clock once it
was already running, so a 60-minute explore could start with 20 minutes left
and be reaped half-done -- spending the budget and yielding no measurement.
Worse, the reaped attempt landed in the failure ledgers, teaching the KB that
the action fails when all that happened was the run ran out of time.

Gate it before a task row exists, so nothing records a refusal as a failure.
Admission judges on expected cost (p50) rather than the p75 tail: judging on
the tail abandons minutes that would have been used more than half the time,
and the overruns are what the later defences are for. The closing actions stay
startable on an empty budget -- refusing those would strand the session with
nothing to show.

``SharedState.session_budget_usable_sec`` becomes the one number admission and
the grid deadline both read, so the two cannot disagree about how much budget
is left. A queued task re-runs the gate immediately before dispatch, since a
task can wait for a busy lane long enough for the budget to drain underneath
it.

Co-authored-by: Cursor <cursoragent@cursor.com>
``_run_magpie`` returns ``AGENTX_PREFLIGHT_RETURNCODE`` when the execution
boundary fails preflight and, thirty lines on, whatever the serving lease's
actor returned -- including ``_ACTOR_TIMEOUT_RC``. Both were -912, and both
reach their consumer as a bare returncode carrying no other tag, so a hung Ray
actor was recorded as a missing aiperf: an infrastructure timeout filed under a
setup mistake, in the ledgers the next run reads.

Move the Ray code rather than the AgentX one. Downstream and every record
written so far already read -912 as the preflight failure, so moving that side
would reinterpret history a second time.

The space is shared by two modules that were coordinating through a comment,
which is how the overlap survived. Say so where the codes are declared, and
name the guard test that now enumerates both modules and fails on reuse.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two holes let one action outlast the whole run. Action timeouts were derived
from the action alone, so explore could be granted four hours inside a
three-hour session; and once a subprocess was running, the only clocks watching
it were the soft deadline -- which retires at eval start, by design, because it
judges whether a variant is abnormally slow -- and the hard cap, which was the
unclamped grant. An accuracy eval starting a minute before the session ends
therefore ran to completion.

Clamp the grants to the budget that is left, and give
``run_with_session_kill`` a session deadline on its own channel. The separate
channel is the point: the eval-start boundary is meaningful for "is this
variant abnormally slow" and meaningless for "is the run out of time", so the
session deadline is checked in every phase and never suspended. It reaps the
tree with a returncode of its own rather than reusing the overtime kill, which
would have taught the KB that a variant is slow whenever a session happened to
end during it.

The stack rebench never consulted the budget at all -- the same hole, second
instance -- and a budget skip was filed as a failure with no cause attached.
Both now carry the session_time_exhausted attribution.

Co-authored-by: Cursor <cursoragent@cursor.com>
The earlier defences all act before or around a running action: admission
refuses what cannot fit, the clamp bounds the grant, and the subprocess reaper
stops the child trees that were handed a session deadline. Nothing reached an
action already under way whose executor takes no deadline, or that is not
spending its time in a subprocess at all -- and nothing reached any of them on
SIGTERM, which only set the stop event. Since the pump does not return until
everything it dispatched has finished, shutdown latency was the remaining
runtime of the longest action, and teardown then closed the database out from
under work still using it.

The handles were the obstacle: they lived only in the pump's frame, so the sole
caller able to cancel an action was the frame already blocked awaiting it.
Keep them on the dispatcher instead, retiring each as its action ends, and the
budget guard, the stop event and ``Coordinator.stop`` can all reach them.

Two details worth naming. A spent budget spares the closing actions, because
the reserve it trips on is held back precisely so they can run, and it does not
stop the queue scan -- that scan is what cancels the rows it can no longer fit.
And the runner now lands a cancelled task on a terminal state before
re-raising: ``CancelledError`` is not an ``Exception``, so it skipped the
handler that records failures, and the row would sit at ``running`` holding its
lanes and reading as live work to every phase gate until the TTL sweep.

Co-authored-by: Cursor <cursoragent@cursor.com>
``run_action_now`` hands the coroutine to the coordinator loop, waits out the
inline timeout, and then abandons the future -- the action keeps running by
design, so the agent's turn is not held hostage by it. But abandoning the future
abandoned the only handle on it, so nothing could stop it afterwards: not the
spent budget, not shutdown, and not the teardown that closes the database it is
still using. The blast radius was bounded (the inline whitelist admits only
lane-light actions, and admission still gates them) but the task row would sit
at ``running`` until the TTL sweep, and the run kept spending wall clock that
was already accounted for elsewhere.

Publish the coroutine's own task while the action runs, the same way the
dispatched path does, and the existing cancellation reaches it unchanged.

The sync bridge now names cancellation before its generic handler. It was
already caught there -- but reported as ``errored``, which reads as a fault in
the action rather than a deliberate stop. Both cancellation classes are caught
because whether ``concurrent.futures.CancelledError`` and
``asyncio.CancelledError`` are the same class varies by Python version, and on
the versions where they are, it is a ``BaseException`` that would escape the
bridge and take the agent's turn down with it.

Co-authored-by: Cursor <cursoragent@cursor.com>
CodeQL read ``TIME_BUDGET_EXEMPT_ACTIONS`` as unused. It is not -- the
dispatcher imports it for both the admission gate and the in-flight
carve-out -- but the module publishes an ``__all__`` that did not list it, so
the interface the module declares disagreed with the one it actually has, and a
reader (human or tool) that trusts the declaration concludes nobody uses it.
List both new names.

The test constant ``_EXPENSIVE_COST_MIN`` was genuinely unused: the assertion it
was written for spelled the cost out as a literal instead. Use it, so the
documented cost and the asserted message cannot drift apart.

Co-authored-by: Cursor <cursoragent@cursor.com>
CLOSE reached for a report task by asking "was one enqueued?" when it needed
"is one runnable?". Both reuse paths -- the ``closing_report_task_id`` the
wall-clock deadline path writes, and ``create_or_return_existing`` -- read
``task.state`` only to log it, so a cancelled row came straight back and
``run_task``'s ``queued -> running`` failed the registry's terminal check:

    IllegalTransition: cannot transition ... from 'cancelled' to 'running'

The session that hits this is the one whose report matters most, and it lost
``final.md`` entirely (#1167).

This lands ahead of the enforcement layers on purpose. Cancelling in-flight
work at the deadline is the point of those layers, and it widens the set of
ways a report task can be sitting in ``cancelled`` when CLOSE arrives -- so
without the guard the crash gets easier to hit, not harder.

A dead row now falls through to a fresh enqueue under a suffixed idempotency
key, shared by the report and session_breakdown steps rather than written
twice, and ``_run_close_task`` reports a terminal row instead of running it
so no path can hand one to ``run_task`` again.

Also adds the test that would have caught the AttributeError a field run hit
on ``_record_explore_variant_failures``: every ``_DELEGATED`` entry must
resolve on its collaborator. It found two stale entries
(``_resolve_issue_canonical``, ``_wait_for_task_terminal``), both naming
methods that no longer exist anywhere; removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
PRELUDE's budget share is 3%, and nothing enforced it: exit_normal_prelude
tests whether a baseline landed and nothing else, so the phase ran to whatever
its contents happened to cost. Two sessions on unrelated models -- different
quantization, different PRELUDE composition, one baseline-dominated and one
split with a TraceLens roofline -- spent 73.8% and 72.8% of a three-hour
budget in it, and both reached FRAMEWORK_AGENT with ~47 minutes against its
108-minute entry threshold. Landing on time would not have saved either run:
they were unrecoverable the moment PRELUDE ended.

So PRELUDE now decides before it spends. ``prelude_can_afford`` bounds an
optional arm by the tighter of its own share and what the optimization phases
have reserved, and two arms consult it:

- the initial roofline/profile, which in one run took 81 minutes -- 45% of the
  session -- and was already known-degraded at second zero because the vLLM
  version patch had failed;
- the baseline's measured round, which cost the other run 59 minutes to move
  the anchor 0.2%, and whose absence leaves the single-round baseline the
  codebase already supports rather than a new half-measured state.

Both estimates come from what this session measured, not from the action
catalog: the catalog's p50s (baseline 5 min, roofline 8 min) are calibrated on
small models, and a guard reading them would have admitted both arms.

``time_exhausted_during_prelude`` finally gets a producer. It was in the
terminal-reason vocabulary and in the report's glossary, but nothing ever
assigned it -- the state machine had a word for this failure and no way to
reach it. And PRELUDE's normal exit now states whether the budget it leaves
behind still funds one benchmark round, which is the plain sentence neither
field session ever got.

Extracts ``append_phase_evidence_row`` so the dropped-arm record and the CLOSE
step record share one definition of "append to the current phase record".

Co-authored-by: Cursor <cursoragent@cursor.com>
On a single node the Ray execution backend is the production default, and
the only reason every test on this branch exercised the local subprocess
path is the pytest guard inside ``_should_use_ray_backend``. So the session
reaper -- the defence that makes a run out of time say so -- was absent
exactly where the work actually runs.

Handing the reaper's ``session_deadline_sec`` straight to the actor would
have been worse than omitting it: it is a ``time.monotonic()`` instant, and
the actor is another process whose clock counts from its own unrelated
origin, so the number names an arbitrary moment there -- an immediate kill
or one that never fires, with nothing to distinguish the two. The deadline
therefore crosses as the seconds it has left and is re-anchored on the far
side, with a name that cannot be mistaken for the absolute form.

Without it the hard timeout is the last line, and it is deliberately set
slightly past the session deadline so the watchdog trips first; whichever
one fires writes the ledger's account of why the run stopped. On the Ray
path the cap won that race and the ledger blamed the variant for the
session running out of time, which is the attribution this branch exists to
correct. The accuracy eval made it worse: ``soft_deadline_sec`` retires at
eval start by design, so an eval starting near the end had nothing bounding
it but that same misattributing cap.

baseline.py is left alone. It has neither defence -- no clamp on its
timeout and no deadline on its subprocess -- and it also has no branch for
the session sentinel, so plumbing one in would only turn running out of
time into an unclassified crash. That needs its own design, not a keyword
argument.

Co-authored-by: Cursor <cursoragent@cursor.com>
zoroyihan7 and others added 18 commits August 18, 2026 03:47
… find out

The post-warmup gate keeps the session honest but not thrifty: it can only ask
its question once the warmup has already paid for the boot, the compile and the
graph capture. On a first baseline that is unavoidable -- there is nothing to
predict from, so a gate in front would either refuse every one of them or wave
every one through. From the second round on the figures exist, and so does an
anchor, which is what makes refusing free: the session keeps the number it
already had instead of spending a cold pass to be told it cannot keep the new
one.

So the round now faces a gate before the lease is taken, judged on a lower bound
built only from measurements rather than on an estimate, which is what makes
refusing on it sound: if the part the session can prove does not fit, the whole
round does not. The two passes are added separately because they buy different
things, and a session can hold the cold figure without the hot one -- a round
whose measured pass was dropped for budget promotes its cold number and has no
hot number to write. That session is the one most in need of the gate, so its
cold pass is still priced and only the unpriceable pass is left to the gate
after the warmup.

A refusal is the run stopping work, not the model failing a measurement, so it
carries ``session_time_exhausted`` and no returncode, and it finally gives
``StoppedByTheRun.never_started`` the consumer it was written for -- until now
every stop went through the ``interrupted`` wording, including the ones that had
not started.

Seven cases pin it, and each of five mutations -- the gate waved through, the
double run priced on one pass, the gate going inert on a half-measured session,
and the two result-shape slips -- is caught by exactly one of them.

Co-authored-by: Cursor <cursoragent@cursor.com>
…th one safe use

The gate in front of a round called its figure a lower bound and justified
refusing on it as sound. It is neither. The figure is an earlier round's full
wall-clock, and that round paid a one-time JIT compile on a cold kernel cache
which a later pass on the same signature does not pay again -- the executor's own
cache probe is built on exactly that difference and picks a cap 20 minutes
shorter when it finds the cache warm. So the figure over-predicts, and a gate
built on it refuses rounds that would have fitted.

Which is tolerable here, but only here, and for a reason worth writing down: the
two outcomes are not symmetric. Igniting a round that cannot finish costs a boot,
a compile and a capture for a number that must then be marked cold. Refusing one
that would have fitted costs a fresh anchor the session did not need, because the
anchor it measured earlier still stands and every later comparison is made
against that one. A caller that would end a session on this figure would be
trading the rest of the run against a systematic over-prediction, and the
docstring now says so rather than leaving the next reader to borrow the
justification.

Renamed accordingly, and the survival property the argument rests on is now
asserted instead of assumed: a refused round comes back with no throughput and no
warning, so there is nothing on it that promotion could put over the anchor.

Co-authored-by: Cursor <cursoragent@cursor.com>
… cold anchor

Two paths end a round after its warmup has already paid for the boot, the
compile and the capture, and both owe the same answer: keep the number, mark it
cold. Only one of them exists today. Naming it now means the second arrives as a
call rather than as a copy of these four lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
…pent benchmarking

A round's wall-clock is two things bought by different spenders. Every explore
variant boots its own server, so a variant costs the sum; a pass that re-attaches
to a server already up costs only the benchmark. Reported as one number, the
total is the only price available, and pricing a re-attaching pass at it refuses
work that fits comfortably.

The instant is recorded where it is already known -- the gate loop that latches
the ready marker for the from-ready soft deadline -- into a stamp beside the
round's server.log. A file rather than a wider return value because the reader is
not always the writer: on the Ray path the round runs inside an actor, whose
monotonic clock has its own origin and whose return would have to be widened
through every degraded branch to carry this. The caller already reads that same
directory's server.log to classify server deaths, so the channel is not new.

A stamp older than the round it is read for is reported as unknown rather than
clamped, which would price a cold round as though it had never booted.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ther it is a cold figure

The round's wall-clock alone cannot price later work. What a variant will spend
is a boot and a benchmark, and only the difference between the round's total and
the part of it that ran after the server was ready says what the boot costs. Both
figures are kept, because subtracting a stale one from a fresh total would report
a boot that never happened; the split is cleared the same way the warm figure is
when a later baseline does not carry one.

The marker is the other half. A baseline that had to keep its cold figure has a
denominator inflated by the boot, the first request's kernel compile and the
graph capture, and every variant read against it looks like an improvement over
a baseline that was never the baseline. That is a decision for the session, not
the round, so it is carried on the session -- and cleared by the next baseline
that does land a hot figure, so a leg resumed with a fresh clock is not held to
the earlier one's shortfall.

Co-authored-by: Cursor <cursoragent@cursor.com>
…g can use the result

Every budget gate priced its work at the baseline's whole cold round, which is a
boot plus a benchmark that also paid the first request's kernel compile. A variant
does not pay that compile and a re-attaching pass does not pay the boot at all, so
the session refused work that fitted: with a 900s cold round it declined variants
it had 750s of work left for. The pricing now names the parts -- boot, benchmark,
one further measured variant -- and every caller reads the same three functions,
so the executor cannot refuse a round the phase machine had just called affordable.

What the tighter numbers make safe is the harder rule they carry. A PRELUDE
baseline is not a result; it is the denominator later results are read against and
the anchor their overtime kill uses, so the clock must cover the round and one
variant to follow it. Refusing before ignition costs nothing, since the anchor the
session already has still stands. Refusing after the cold pass keeps that pass as
a marked cold anchor -- its GPU time is spent either way -- and a run's clock that
takes the hot pass mid-flight lands in the same place, rather than throwing away a
pass that ran to completion. A cold anchor is then not a finished preparation: the
session either measures a comparable baseline or stops with the figure marked,
instead of optimizing against a denominator that was never the baseline.

A re-baseline in a later phase is asked the narrower question. Its measurement is
the deliverable, so requiring a successor would refuse the round that validates
the run's own answer at the point in the budget where it is most likely to be the
last thing left.

Co-authored-by: Cursor <cursoragent@cursor.com>
…an be spent

CLOSE is the machine's only terminal phase, and a resumed leg loaded whatever
phase was persisted. Loading CLOSE meant staying there: the machine has no
transition out, and the run loop stops on a stop_reason rather than on the phase,
so the leg ticked to the end in a phase admitting only report, session_breakdown
and recover. Every stop taken on the promise of "resume with more budget" was
answered by a leg that spent the budget on none of the work it was resumed for.

Reopened at PRELUDE, the phase that works out where a session belongs: it exits on
its first evaluation when the anchor the run needs is already measured, and
measures one when it is not. A session that still cannot fund the work is not held
open by this -- the PRELUDE exits price the new clock and route it back, this time
against the budget it actually has.

The earlier leg's close_sequence_done goes with it. The flag means "the sequencer
already wrote the breakdown", and carried into a leg that then never reaches CLOSE
it silences the safety net that would have written one.

Co-authored-by: Cursor <cursoragent@cursor.com>
…d let a hot pass correct a cold anchor

Findings from an adversarial read of the cost-based gates.

A cold anchor holds PRELUDE open until a hot pass replaces it, and the rule
that keeps a later, lower re-baseline from displacing the anchor rejected
exactly that replacement -- so a retry that did land a hot pass, but measured
lower because the "cold" warmup was not really cold, left the marker set and
the session re-measured whole baseline rounds until the clock killed it. A hot
figure over a marked cold one is a correction, not a regression: the two are
not comparable, which is what the marker says.

The round price was rebuilt as boot-plus-hot-benchmark, dropping the compile
the cold pass pays and under-pricing the round by that much, while the gate
after the cold pass priced the same second pass at the post-ready segment. A
band of budgets was therefore admitted before ignition and refused for certain
afterwards, at the cost of a whole cold pass. Both now read what the session
measured: the round's first pass whole, and the hot pass from a hot pass.

Reading the measured total also gives a price to rounds with no boot boundary
to reconstruct from. Multi-node had lost both gates -- the shared pre-warmup
refusal was deleted and neither replacement reached it -- so a pair of passes
launched on a budget for one and the measured pass was reaped, leaving no
anchor. It is gated again, priced as the two passes it is.

Also: a prior attempt's nested workspace could latch this round's ready marker
and report a fifteen-minute boot as zero; the ready timestamp was tied to an
unrelated stall watchdog whose knob silently withdrew it; a missing timestamp
made the post-warmup gate demand two whole cold rounds at the one site where
refusing ends the session; and the exit glossary still described a share
mechanism this branch removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…or be re-measured

Two holes in the cold-anchor path, both on the resume leg it exists for.

The boot was derived by subtracting the ready stamp's wall-clock instant from
the caller's own. On the Ray path those are two clocks, possibly on two hosts,
and the difference between them was charged to the boot -- inflating it, and
making the budget gates refuse rounds that fit. The stamp now carries the boot
as a duration measured end to end inside the process that spawned the child,
and keeps the instant only to tell this round's stamp from an earlier one's. A
stamp missing the duration reads as no stamp at all: zero is a legitimate boot,
so it cannot also stand for "not recorded".

The singleton rule refuses a repeat baseline because it "re-measures a
reference the run already has". A marked cold anchor is the case where the run
does not have one, and it is a positive tput, so the rule refused the single
round that clears the mark. A session resumed on a fresh clock therefore
reopened at PRELUDE, declined to finish while the mark was set, declined to
close while the clock was healthy, and had no admissible way forward. The rule
now exempts a marked anchor -- after the authoring-round check, which is a
reason to wait whatever the anchor says.

Co-authored-by: Cursor <cursoragent@cursor.com>
The stamp's two fields answer two questions, and only one of them has a
safe default. A caller that omitted the boot would write a well-formed
stamp claiming the round came up instantly, which prices the whole round
as benchmark -- the one wrong reading the two-field format was added to
make impossible. Dropping the default turns that into a call-site error.

Co-authored-by: Cursor <cursoragent@cursor.com>
A multi-node variant runs two client passes, so reserving one leaves a
pass unfunded. Teaching the phase machine's pricing what shape the
cluster is costs more than the gap, which needs an enablement round
holding PRELUDE open past an anchor to reach at all. Noted where the
fallback is, so it reads as a bound rather than an oversight.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two live session_budget_usable_sec reads differ by microseconds, which
failed exact equality on py3.11 shard 4. Freeze elapsed time instead.
Also drop an unused started_unix and close the ServingLease fixture
with `with`, which is how the class is meant to be released.

Co-authored-by: Cursor <cursoragent@cursor.com>
Recover is not a closing action: it takes the server-lifecycle lane and
can hold CLOSE open past the wall clock, which is the overrun this
branch exists to stop.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ll close.

Co-authored-by: Cursor <cursoragent@cursor.com>
…reactor turn on a spent budget.

Co-authored-by: Cursor <cursoragent@cursor.com>
_run_reported_round from main and _mn_warmup_pass from this branch were left on one line, which is a SyntaxError.
Admission already refused the sweep, but last_conc_sweep stayed empty, so the LLM's skip_to_close was labelled robustness_escalated and CI exited 1 on a successful run.
@zoroyihan7
zoroyihan7 force-pushed the fix/1146-time-budget-enforcement branch from 5cec95b to 57c7b66 Compare August 18, 2026 06:34
Comment thread src/hyperloom/inference_optimizer/tests/conftest.py Fixed
Comment thread src/hyperloom/orchestrator/actions/executors/_grid_runner.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/conftest.py Fixed
Comment thread src/hyperloom/orchestrator/actions/executors/_grid_runner.py Fixed
@xiaofei-zheng
xiaofei-zheng merged commit 9ca2b60 into main Aug 19, 2026
28 checks passed
@xiaofei-zheng
xiaofei-zheng deleted the fix/1146-time-budget-enforcement branch August 19, 2026 03:50
ZhengGong-amd added a commit that referenced this pull request Aug 19, 2026
…lore-opt-17

~130 commits, dominated by the cooperative-cancellation and session-clock work.
It reaches the same conclusions this branch did from the other side -- "a round
the run stopped is not a baseline that failed", "a variant the run reaped is not
a variant that failed" -- so most of it merged clean. Five conflicts.

Reversed here, deliberately: the EXPLORE hours force-exit is back

fdadb65 removed DEFAULT_EXPLORE_FORCE_EXIT_HOURS_REMAINING because any run of
3h or less force-exited EXPLORE on its first tick, and argued the gate was
redundant once charge-back reserves the later phases' share. 07e23b9 found the
same defect and fixed it instead of removing it: _explore_hours_leavebehind_
applies disables the gate when the leave-behind is not strictly smaller than the
session, with disabled_leavebehind_covers_session in the evidence and 56 lines
of new tests.

Main's version is taken. It closes the failure this branch removed it for, it
belongs to a 130-commit effort by the owner of that machinery, and the removal
rested on redundancy rather than incorrectness -- so keeping it costs a
redundant check, while dropping it would delete another team's tested work
mid-merge. should_force_exit_explore and exit_normal_explore carry the
parameter again; a non-positive threshold still disables the gate.

The other two fixes in fdadb65 are untouched: normalize_budget_pct still
keeps an explicit 0.0, and the FRAMEWORK force-exit stays removed.

Other conflicts:

- _grid_runner __all__ takes main's three new stop-attribution exports;
  MULTI_NODE_DEFAULT_KEEP_THRESHOLD_PCT is not among them because 27ad677
  deleted the constant, and exporting it would break the module interface.
- request_handlers keeps main's vendor-playbook deploy-blocked guard and reads
  the stable ledger through _entry_by_kernel_id rather than the ordinal
  kernel_opt_attempts dict (2cb82ad).
- machine_state also takes main's SWEEP closeout helper, so skip_to_close no
  longer maps a refused conc_sweep onto robustness_escalated.
- test_phase_force_exit takes main's cases; one had spliced this branch's
  variable name onto main's new assertions.
- CHANGELOG keeps both sides' entries.

Also drops a duplicate _update_cumulative_gain_validated key this branch had
left in the Coordinator delegation table (same value, so inert, but ruff F601).

Not addressed here, and worth an issue against #1171: integrate_patch does not
consult stop_attribution on either side of this merge, so a session-budget stop
during the GEMM E2E validator files its remaining candidates as REVERT -- a
verdict about tuners that were never measured. Extending that subsystem into
integrate is the author's call, not a merge resolution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xiaofei-zheng pushed a commit that referenced this pull request Aug 25, 2026
* fix(observability): let a long task say it is still working

A composite action — an explore grid, a baseline pair, a profile and its
roofline — is one task row that internally completes many units over hours.
Between dispatch and return it emitted nothing durable, so a healthy 80-minute
run and a wedged one left identical evidence. Field sessions read that as
"orchestration silent 8097s" while the run was in fact progressing normally,
and as a health probe demanding a restart of a server no phase had booted yet.

Executors now report each unit through an ambient reporter the runner binds to
the task, landing on the row as a heartbeat that moves ``updated_at``. Stall
detection consults it and withholds the accusation while dispatched work is
still moving; the server probe holds its fire when no server process is up.

Emitters: per variant in the grid runner, after the baseline warmup round, and
per roofline sub-step.

Refs #1145, #1168

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): bound and attribute the progress suppression

The heartbeat's consumer suppressed too much. A single running task that had
reported recently returned no symptoms at all, before the per-agent loop ever
ran: one busy explore grid vouched for the Coordinator, the critic and the
kernel agent alike, and a genuinely hung agent went unreported for as long as
anything on the box kept moving. Nothing was logged, so the near-miss left no
trace an operator could read afterwards.

Freshness also came from ``MAX(updated_at)`` over running rows, which a task
merely entering ``running`` advances — the suppression could fire without a
single heartbeat behind it.

Progress notes now name their owning agent, the probe reads them out of the
task history instead of trusting a column that moves for other reasons, and
the judgement moved into the per-agent branch: only an agent's own work speaks
for it. Work buys time, not immunity — past ``severity_high_after_s`` the
accusation fires regardless of how busy the machine looks — and a withheld
accusation is downgraded to LOW with its counter-evidence attached rather than
discarded, so "we would have accused X, but its work reported Ns ago" is
visible in the session.

Attribution is as narrow as the data supports: the ``tasks`` table has no
requester column and every row is dispatched by the Coordinator's
orchestration loop, so a heartbeat is stamped for that agent alone.

Refs #1145

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): report grid and baseline units before they block

The completion reports only fire once a unit has landed, so the very
failure this heartbeat targets - a first variant or a warmup round that
hangs for tens of minutes - still produced no evidence at all. Announce
each unit on entry, before the launch that can block, and keep the
completion reports as they were.

Entry reporting also covers the branches that reach neither the
completion report nor a finally, so `_after_variant` is left alone
rather than moved into a finally that would double-report.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): route every roofline sub-step through one reporter

Three of the five sub-step call sites were bare: the N26 re-analysis and
both halves of the multi-node compute-bound re-profile ran silently, and
each of them can take tens of minutes. Rather than paste the same report
block at each site, funnel every call through a single closure next to
the local imports, so a site added later cannot be the silent one.

Each call site keeps its own try/except and fail-soft semantics
unchanged; only the await is wrapped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): let the probe see a busy ray worker

``ray::IDLE`` is the name of a parked worker only; once a worker starts
running a serving actor Ray renames it after the actor class, so
``ray::ServingActor`` matched no pattern and the process disappeared
from the probe entirely.

Presence still says nothing about busy-versus-hung — it only stops the
probe reporting an empty machine while work is on it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(observability): let a long step prove it is alive from its own output

A start-of-unit report covers the first 300 seconds and nothing after,
so the 55-minute TraceLens analysis - one await, no units - still went
stale halfway through. A timer would have "fixed" it by vouching for
wedged processes, which is the exact failure the stall signal exists to
catch.

Drive the heartbeat from the child's own output instead: the subprocess
reader already walks stdout/stderr line by line, so it now tallies what
it reads, and a driver on the loop reports once per interval in which
that tally moved. A silent child produces no heartbeat and stays
accusable. The kernel-tool child is unbuffered so its lines arrive while
it works rather than in eight-kilobyte lumps.

Covers the TraceLens analysis (via every kernel tool subprocess) and the
profile / baseline benchmark round. The Ray-managed round runs its child
in a remote worker with no channel back for the callback, and is left on
the ceiling from the first commit in this series.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): stop the progress note from moving updated_at

``updated_at`` marks when a task entered ``running``, and three consumers
measure elapsed runtime from it: the R6 lease watchdog turned into an
inactivity timeout that a heartbeating task could never trip, the
``extend_lease`` remaining-budget math read time-since-last-note as
time-already-spent and handed lanes and GPUs more than the budget allowed,
and the Coordinator's own "Tasks in flight" line rendered an 80-minute task
as seconds old — this PR's goal running backwards in the one surface the
Coordinator reads. ``running()`` gets its least-recently-updated-first
meaning back for free.

The write had no reader left: the probe already reads freshness from the
progress notes on ``history`` rather than from the column. The docstring
argued for the behaviour and is rewritten to state the invariant instead,
matching the one ``extend_lease`` documents a few lines below.

Removing the bump restores the pre-PR reaper behaviour rather than changing
it. It does not close the pre-existing gap between the catalogue's
``lease_ttl_sec`` and observed composite runtimes (roofline 2700s vs a
3106s TraceLens step, baseline 4200s vs a 3941s warmup round plus its
measured round); that needs a TTL or extension policy decision and is
reported on the PR rather than papered over here.

Refs #1145

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): keep the variant hook's name so a sibling change still merges

The hook grew a progress report, so "pulse" stopped describing all of it and
it was renamed to match. But the name is a merge surface: #1171 adds a call
site of its own, in the branch that records a variant reaped by the session
budget as skipped rather than failed. Renaming here deletes the definition
that call site resolves to, in a region #1171 never touches, so git reports
no conflict and the merge lands a NameError on the reaped-variant path --
found only by lint or by running it.

Keep the name and say what it does in the docstring instead. Costs an
imprecise word; buys a merge that cannot break silently in either order.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): roll back a transaction the caller was cancelled out of

``asyncio.CancelledError`` derives from ``BaseException``, so the ``except
Exception`` guard around ``transaction()`` never saw it: a cancel landing on
one of the ``to_thread`` hops after ``BEGIN IMMEDIATE`` had already run left
the shared connection inside a transaction for the rest of the session, and
every later registry write failed with "cannot start a transaction within a
transaction". The heartbeat this branch adds is the first path that routinely
cancels a coroutine mid-write, once per long subprocess step.

Catch ``BaseException``, and roll back inline rather than on a worker thread —
the exception being handled is usually this task's own cancellation, and an
``await`` there can be cancelled in turn. A rollback that itself fails is
logged, never allowed to mask the original exception.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): stop the heartbeat driver cooperatively

Teardown used to hard-cancel the driver and swallow the result. Both halves
were wrong.

The cancel could land while the driver was inside a ``tasks`` row write, which
is exactly the case that leaves the coordinator's connection wedged. The driver
now polls a stop flag between ticks, so teardown asks it to finish the note it
is in and only cancels if it overruns a bounded grace — a wedged sink costs the
executor the grace window and nothing more.

``suppress(CancelledError)`` around the await could not tell the driver's own
cancellation from the enclosing task being cancelled during teardown, so an
outer cancel was absorbed and the step returned as though it had finished.
Nothing catches ``CancelledError`` on that path any more.

``interval_s`` now resolves its default at call time so the timescale can be
compressed under test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): report liveness from the three long paths still missing it

``_run_magpie`` is the longest single block in a session — up to a 7800s
variant timeout against a 300s suppression window — and it ran the benchmark
with no liveness callback, so a grid announced each variant on entry and then
went dark. Same shape in the baseline's discarded multi-node warmup pass.

Both now run under the same output-driven heartbeat the measured baseline round
already uses. In the grid all three Magpie passes (auto-warmup, multi-node
warmup, benchmark) go through one helper, so a pass added later cannot be the
one that reports nothing.

The Ray serving-lease branch is left as an explicit gap in both files: the round
executes inside an actor in another process and only its final ``(rc, stdout,
stderr)`` crosses back, so there is no local per-line event to hang a callback
on. A Ray-backed round reports on entry and stays quiet until it returns.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): bound how long a path may go unreported

The suite asserted that notes are emitted, never that consecutive notes on a
path stay closer together than the window a consumer waits before calling the
owning agent silent — which is how a benchmark with no liveness callback
survived a change whose whole purpose was to close that gap.

``ProgressCadence`` records each note against a compressed clock, so a test
second stands for ten production minutes and the assertions still read in the
numbers the window is configured with. Every long path now has a test that
fails if its callback is dropped: the grid variant benchmark, the baseline
round, the multi-node warmup pass and the kernel tool subprocess.

Two existing tests could not fail for the reason they existed:
``test_run_subprocess_counts_the_lines_its_child_emits`` counted nothing, only
that a callable had been passed, and now asserts one call per line the child
emits; ``test_a_variant_reports_before_it_blocks`` covered the entry marker and
nothing after it, so a cadence test now sits beside it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): let the configured server patterns decide what a server is

``is_server`` was computed against the module constant while the match list
came from ``config.server_process_patterns``, so a framework an operator added
to the documented knob was matched as a process yet flagged as not-a-server —
which silently disabled ``local_server_unreachable`` for exactly the
deployment that configured it. The probe config now carries the server subset
and the flag follows it.

An empty process list was also doing double duty: "nothing is running" and "we
could not find out". ``_sample_processes`` returns ``None`` for the second and
``SourceData`` carries ``local_processes_known``, so a missing or wedged ``ps``
no longer mutes an unrelated finding.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kernel): default the child's buffering instead of dictating it

Unbuffering a kernel tool keeps the heartbeat above it honest, but assigning
PYTHONUNBUFFERED overrode an operator who had set it deliberately. The three
launcher sites in multi_node/scripts already use setdefault; match them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): keep a pathological JSON blob inside its own sub-probe

_json_loads_or_none caught the decode errors but not RecursionError, so a
deeply nested history blob written by another process aborted the entire probe
tick instead of costing only the sub-probe that read it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(robustness): document the withheld agent_stall as its own row

The symptom table described agent_stall as medium/high only; a stall whose
dispatched work is still reporting is LOW and routes to send_message
(observation), which the ladder test now locks in.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): bound stall suppression by evidence freshness, not the clock

The 900s ceiling on a withheld agent_stall assumed a suppressed accusation
should eventually fire anyway. Measured work units break that assumption: a
single warmup runs 3941s and a TraceLens pass 3106s, so every healthy long
phase crossed the ceiling and alerted exactly as if nothing had reported.

Suppression now lasts as long as the counter-evidence stays fresh — the tick
after the work stops reporting accuses at full severity. Long waits are still
visible: the withheld note itself rises from LOW to MEDIUM past the threshold,
which is now operator-configurable via agent_stall_high_after_s.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): measure the cadence on a simulated clock, not the wall

The four cadence assertions compressed a 300s suppression window into 0.5s of
real time and then compared a real wall-clock gap against it, so every
scheduling delay in the test was multiplied by 600 and charged to the path
under test. The dominant term was not the heartbeat at all but the run-up
before the first note -- fixture setup, imports, the fake magpie workspace --
which on two cores under eight-way contention grew to 753/888/1035 production
seconds against a 300 budget.

Advance the clock only when the simulated child does a chunk of the work it
stands in for, so the timeline is a pure function of the lines emitted. The
run-up and the tail collapse to what they represent, the steady-state spacing
stays at the production ratio, and the assertion keeps failing when a liveness
callback is dropped or the tick is widened past the window.

Fake the kernel tool's child with the same helper the other three use: with a
real subprocess there is nothing in the test that can advance the simulated
clock. The real pump's line counting keeps its own coverage in
test_run_subprocess_counts_the_lines_its_child_emits.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): let a benchmark client vouch that a server was wanted

The process guard read "probed successfully, saw no server process" as "no
server was supposed to be running", which is true of the gap between two
variants and false of a server that crashed while its own load generator was
still sending requests into the closed port. That second snapshot is exactly
the one worth alerting on, and it was the one being suppressed: all targets
unreachable plus one is_server=False benchmark_serving process produced no
symptoms at all where base emitted a HIGH local_server_unreachable.

Suppress only when the probe answered, no server process is up and no
benchmark client is running. The client patterns are deliberately narrower than
the harness patterns the process probe matches -- the outer Magpie/InferenceX
harness is up across server launch and teardown too, so it stays evidence of
nothing, which is what keeps the idle-stretch guard intact.

Record benchmark_client_seen next to server_process_seen so the branch that
decided is legible in the alert, and give the suppressed case its own row in
the symptom table, which still promised that any target down always alerts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): keep the in-flight unit a fresher sibling was hiding

The snapshot kept only the newest heartbeat per agent, so with two units
dispatched concurrently -- one reporting, one quiet for hours -- the quiet one
left no trace at all and the withheld accusation rested entirely on the healthy
sibling. Only ``running`` hinted that a second unit existed, and nothing read
it.

Keep both ends of the range and name the quiet one in the evidence. Not the
oldest alone, and not "decline to withhold when some unit is quiet": a quiet
unit is not an agent fault, and quiet units are expected today -- a Ray-backed
baseline round has no local process to call a liveness callback from, so it
reports on entry and then not again until it returns, which either of those
readings would turn into an accusation on a healthy run. The stuck unit's own
backstop is the lease watchdog. What was missing was visibility, not severity.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(robustness): cover the quiet fallback's admission that nothing looked

Deleting local_processes_known=False from _QuietFallback left test_factory
fully green, which made the one line standing between "nothing looked" and
"nothing is running" the only unprotected decision in the module.

Assert it through the consumer it defends -- the guard that suppresses
local_server_unreachable when the process probe saw no server -- so the test
says why the flag exists rather than restating the assignment.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): keep the loop running while a cancelled write rolls back

The rollback added for cancelled ``transaction()`` bodies took ``_sync_lock`` on
the event-loop thread, and in the cancellation case it queued behind the very
worker the cancel had abandoned: that worker is parked inside its own ``BEGIN
IMMEDIATE``, holding the lock, for as long as another writer holds the database
— up to ``PRAGMA busy_timeout = 30000``. With a 3s external writer, the whole
orchestrator stopped for 2.55s, and it stopped during the shutdown or
budget-exhaustion window that issued the cancel, so a stalled loop ate the
cooperative-stop window it was supposed to be spending.

Run the rollback on a worker thread under ``asyncio.shield``. The worker queues
behind the abandoned one on ``_sync_lock``, which is the ordering the rollback
already relied on, and the shield keeps it uncancellable — a bare ``await`` in a
handler for this task's own cancellation is exactly how the connection stayed
wedged in a transaction. Awaited, not fired and forgotten: ``_async_lock`` stays
held until the connection is clean, so no later ``BEGIN IMMEDIATE`` can find a
transaction still open. Anything that goes wrong with the rollback itself — a
failing statement, a shut-down executor during teardown, a second cancel landing
on the wait — is logged and never masks the caller's original exception.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): pace the heartbeat tests on the driver, not the clock

Four of these tests asserted a note count reached after sleeping a few tick
intervals, which only holds if the loop was scheduled during that sleep. On the
2-vCPU CI runner, with two xdist workers and coverage tracing, it is not: under
comparable oversubscription three of them failed in every run, because the
driver had not yet taken the tick the assertion assumed.

Wait for the driver's own notes instead. A note is proof the driver got that
tick, so the wait is as long as the machine needs and the counts become exact
rather than lower bounds. A driver that is supposed to stay silent — the one
watching a child that went quiet, and the one whose step already returned — is
paced by a second heartbeat kept deliberately noisy, so the window it would have
reported in is intervals it actually got rather than intervals the test hoped
had gone by, and the assertion becomes the ordered sequence of who spoke. Every
wait is bounded so a driver that stops reporting fails in seconds.

Also drop ``assert elapsed < 1.0`` from the wedged-sink test: a teardown that
waited on the sink instead of cancelling it would still be inside the ``async
with``, never reaching the assertion, so the measured interval was carrying no
coverage that ``events == ["cancelled"]`` does not.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): wait out the rollback a second cancel used to abandon

``asyncio.shield`` protects the rollback, not the wait on it. A cancel landing on
that wait raised straight through the ``except BaseException`` written to keep the
caller's exception, and ``transaction()`` then ran on to its ``raise`` and released
``_async_lock`` with the rollback still queued on ``_sync_lock`` behind the worker
the first cancel abandoned -- fire and forget with the lock already gone, which is
the shape this rollback was moved off the loop to avoid rather than to adopt. The
next writer finds the connection inside a transaction nobody will close and every
write in the session fails with "cannot start a transaction within a transaction".

The same handler lost the cancellation outright. One cancel is enough: a
``record_progress`` body raises on its own -- a ``BEGIN IMMEDIATE`` past
``busy_timeout``, a ``history`` column that will not parse -- and the swallowed
cancel left the caller with the body's exception, which ``report_progress`` drops.
An action cancelled by the stop path then runs on as though nothing had happened
while the dispatcher's ``gather`` waits for it.

Keep the shielded future and resume the wait after every cancel, so the connection
is always clean before ``_async_lock`` is released, then re-raise the cancel that
arrived. Narrow the ``except`` to what a rollback can actually fail with -- a
statement error, or an executor refusing work during teardown -- so control flow
is no longer swallowed as though it were a database error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): report a step alive on its own output, not the server's log

``grew`` means "one of the resolved logs got longer", from any writer, and feeding
it to the liveness callback made the heartbeat the bare timer
``heartbeat_while_output_flows`` documents itself as never being. A child that
printed nothing for 3s was reported alive 5 times by a ``server.log`` an unrelated
thread appended to. In a real round that writer is the inference server, which
keeps logging while the benchmark client is wedged, and both vLLM and sglang write
an access line per request -- including the health probe the robustness agent
issues every standalone tick against the server it is monitoring. That closes a
loop where the monitor's own probe manufactures the evidence suppressing its own
stall accusation, and the wall-clock ceiling that used to backstop the case is
gone.

Report liveness on evidence about the round instead: a generation-progress marker,
which says tokens were flowing whoever logged them, or growth in the benchmark
body's own redirected stderr, which is output of this very child that never
reaches its pipe. Keeping the second is what stops the narrowing from silencing a
working round: the scriptable and bypass paths redirect the body's stderr to
``benchmark_stderr.log``, so for a whole round the pipe is empty and that file is
all there is. The stall watchdog keeps the broad "any new bytes" signal, which is
the right one for a gate that kills.

The scan now returns a named tuple rather than a fourth positional bool nobody
could read, which is also how the throwaway ``_saw_progress`` binding at the call
site goes away.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): name the variant that ran in its own progress note

``_pulse_after_variant`` inferred which variant it was reporting from the tail of
``results`` rather than from the index it was handed. A stop cause that ends the
batch records the round it stopped and then a not-run row for every later variant,
so the tail is the last variant in the grid and the terminal note for variant 1 of
3 reads ``label='c2', index=1, total=3``. The log line one frame away is right and
the ledger is right; only the progress note -- the artefact this heartbeat exists
to make honest, and the one a stall signal reads -- is wrong.

Locate the row by index instead, in a helper that says why the tail is not it, and
take the label from the grid entry where it cannot disagree. Extracting the note
also takes it out of a ``run_grid`` that is already far past the length worth
reading, and leaves ``_pulse_after_variant``'s signature alone so no call site has
to change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(observability): bound the wedged-sink step so a regression fails, not hangs

The sink sleeps for an hour, so a teardown that waited on it instead of cancelling
it never leaves the ``async with`` and never reaches an assertion. With no timeout
plugin in this suite that blocks until the CI job is killed -- the failure mode the
grace window under test was added to prevent, which the test for it should be the
last place to reproduce. Run the step under ``asyncio.wait_for``: the regression now
fails in ten seconds where it previously hung indefinitely.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): take the rate, not the line, as proof of tokens

Narrowing the liveness callback to generation-progress markers left one writer
still able to vouch for a wedged child: some vLLM builds print ``Avg generation
throughput: 0.0 tokens/s`` on an idle engine, and an engine goes idle precisely
when the client driving it wedges. The most likely production shape of the defect
therefore survived the fix -- a server logging zeros suppresses the stall
accusation its own client has earned.

Read the value instead of the marker, reusing the parse the post-mortem
throughput estimate already runs over the same two frameworks' lines: the third
copy of that knowledge goes away, and a framework added to one parser can no
longer drift from the other. A line whose rate cannot be read counts as no
progress -- this evidence only ever suppresses an accusation, and one suppressed
by mistake is invisible, where a missing one is visible and still answerable from
the child's own redirected stderr.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(observability): end the variant a failed remote restart abandoned

Every variant outcome reports a terminal note except one: a multi-node round whose
server never came back recorded its row and left, so the row a stall signal reads
stayed at ``started`` for the rest of the session while the variant was already
over. Two behaviour-lock tests pinned that omission as though it were a rule,
which is how it outlived the boundary the other thirteen paths share -- and unlike
a reaped round, nothing moves the task afterwards to make the stale row harmless.

Reach the boundary like every sibling does, so the note comes from
``_variant_progress_note`` and names the variant that ended rather than the tail
of the batch, and the failed restart gets the same bounded robustness tick as
every other variant boundary. The loop's 27 early exits were enumerated to
confirm this was the only gap: the one remaining exit that reports nothing is the
session-budget skip, which breaks before the variant is announced and so has no
row to close.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): let the evidence, not the wait, grade a withheld stall

The withheld note rose to MEDIUM once the agent had been silent past
severity_high_after_s, which is the wall-clock ceiling the previous commit set
out to remove, one rung lower: MEDIUM routes to alert(medium), so the measured
healthy runs it cited -- a 3941s warmup, a 3106s TraceLens pass -- alerted while
reporting a unit every tick.

Elapsed silence now grades only the accusation. A phase whose work keeps
reporting stays on the observation tier however long it runs, and the tick after
the reports stop accuses at full severity, which is what "bounded by evidence
freshness" was supposed to mean.

The withheld case gets its own symptom name so RCA can tell a healthy long
phase from an agent that really went quiet, and the evidence records the
freshness window that holds the accusation back. The ladder tier a withheld note
must not reach is pinned by a test of its own.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(robustness): visit the progress rows in both orders

The shipped test inserted the quiet unit first, so the freshest note always
arrived second and the branch that records the quiet end never ran: deleting it
left the whole suite green. Which order the rows arrived in was also SQLite's
choice, the running-task select having no ORDER BY, so the assertion rested on
unspecified behaviour either way.

Order the select by task_id and let the test choose which unit's note is folded
in first, so both directions of the fold are covered by the same case.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(state): bound the progress notes a task's history retains

Every note re-read the whole history blob, appended to it and rewrote it inside
a BEGIN IMMEDIATE on the connection every other writer serialises behind, with
no cap. 720 notes -- 12 hours at the 60s heartbeat -- left a blob north of
100 KB and tens of MB of cumulative row rewrites, and the robustness probe
re-parses that blob for every running task on every tick. The cost scaled with
session length, which is what the heartbeat exists for.

Retain the newest 120 notes, two hours of trail at that cadence and longer than
the longest measured work unit. Transitions are never dropped: consumers read
those positionally -- the last entry for a failure class, the newest
queued -> cancelled for a policy denial -- so only progress notes are retired.

Measured on this branch, 720 notes: blob 103 KB -> 17 KB, cumulative rewrites
35.6 MiB -> 10.9 MiB, and flat per note from there instead of growing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): tie a vouching benchmark client to this session

The client that overrides "no server process, so none was wanted" was found in a
whole-host ps snapshot, so on a shared node a co-tenant's benchmark_serving
vouched that this session's port should be answering, and an idle stretch came
back as a HIGH alert on somebody else's traffic.

The process probe now records each matched process's working directory, which is
the anchor a session already has: the harness is launched with its cwd inside
the session directory and its children inherit it. A client counts as this
session's when its cwd is under that directory, or when it names a path there on
its command line, for the launch paths that chdir elsewhere. With no session
directory configured there is nothing to compare against and the check stays
host-wide as before.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(robustness): say where the stall thresholds are actually set

The escalation knob was described as operator-configurable, but discover() reads
a fixed list of deployment-shape variables and this is not one of them, so it is
set in code like every other threshold on Config -- gpu_temp_warn_c, the disk and
shm percentages, its own sibling agent_stall_timeout_s.

Correct the claim rather than wire these two into discover(): none of the ~60
thresholds is environment-settable, and making the stall pair the exception would
buy one deployment knob at the price of an env variable per threshold and a doc
table that has to keep up. Where the boundary runs is now written down next to
the thresholds and under the environment table.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): run a grid variant inside the session that owns it

A benchmark client only overrides "no server process, so none was wanted" when it
can be tied to this session, and the tie is the working directory the harness's
whole subtree inherits. The baseline arm anchors Magpie to its own output dir for
that reason; the grid runner defaulted to the system temp directory, so an explore
/ sweep / conc_sweep / integrate_patch client carried no anchor of its own and a
server that died mid-variant read as the idle gap between two variants -- the one
snapshot the vouching rule exists to catch. Grid variants are where a server most
often dies mid-benchmark.

Each pass now runs from output_root, the per-task workspace under the session,
exactly as the baseline does. Nothing that reads a benchmark's results depended on
that directory being /tmp: Magpie re-roots the server itself, and workspace
selection scans the per-variant output dir while the leak harvest scans /workspace
plus $INFERENCE_OPTIMIZER_LEAK_ROOTS -- never the cwd. A relative result_dir now
resolves under the task workspace, which is where the baseline already resolves
one and where the salvage path can find what it produced.

The cwd override goes with it. No caller set it, and an override is the one knob
that could let the anchor drift back out of the session.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(robustness): tie a client to the session by directory, not by prefix

The cwd reading guarded the directory boundary; the command-line reading on the
next line was a bare substring test. A co-tenant benchmarking out of
<session>-retry -- a retry, a backup, or any sibling an operator names after ours
-- contains this session's path, so it still vouched that this session's port
should be answering. That is the false alarm the previous fix set out to remove,
narrowed from host-wide to prefix-wide rather than eliminated, and it is reachable
whenever the session directory is operator-supplied.

Both readings now go through one helper that compares a path a component at a time,
so the two cannot drift apart again, and the command line is read as tokens (plus
the value of a key=value token, so --result-dir=<path> still counts) rather than
searched for a substring, which is what makes a boundary applicable to it at all.

The launch-path matrix now also holds the case that hid the missing grid anchor: a
client with neither cwd nor command line under the session is indistinguishable
from a co-tenant's and must not vouch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(robustness): hold both spellings of --result-dir to the session boundary

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(grid): say why the session-rooted cwd is created up front

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make the heartbeat cancel tests name the exception they wait for

CodeQL read `await cancelled_task` inside pytest.raises as a statement
with no effect, and the log assertions after a raise it did not model
as unreachable. Catch CancelledError and ValueError explicitly so the
control flow is the test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Satisfy CodeQL on the heartbeat cancel tests without empty excepts

The previous try/except/else left an unreachable else, and a bare
`except CancelledError: pass` is an empty handler. One helper now
documents the success path, and the rollback-failure test goes back
to pytest.raises with the log assertions inside that block.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Stop CodeQL treating the rollback-failure asserts as dead

pytest.raises is not modelled as catching the raise, so the log
assertions after it were unreachable. Catch ValueError by assignment
instead, with no else-clause and no context manager for CodeQL to
misread.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
xiaofei-zheng added a commit that referenced this pull request Aug 25, 2026
fix: hold the session to its wall-clock budget
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLOSE phase report generation crashes (IllegalTransition) when wall-clock watchdog cancels the report task before the sequencer runs it

4 participants