Repository navigation
release/v1.1.4: cherry-pick resume fixes (#1859, #1870, #1871, #1879) and round server teardown (partial #1606) - #1874
Conversation
) Cherry-pick of #1859 onto release/v1.1.4, as the prerequisite of #1871 (which changes the owner-scope code this adds) and of #1870's regression test (which cancels the sweep through confirm_scope_gone). Applied cleanly. Adds `hyperloom recover --confirm-scope-gone OWNER_SCOPE --confirmation-reason TEXT`: under the session lock and in one transaction it cancels the unfinished tasks holding a lease in that scope and releases their execution and GPU leases, and refuses the current scope, an unobservable current scope, any unexpired lease of another scope, and tasks spanning two scopes. local_owner_scope moves unchanged to hyperloom.common.owner_scope. (cherry picked from commit 680b374) Co-Authored-By: Claude <noreply@anthropic.com>
…out one (#1870) Cherry-pick of #1870 onto release/v1.1.4. `hyperloom recover` cancels the tasks of a vanished execution scope directly in the task ledger, so a conc_sweep cancelled that way never records last_conc_sweep. A resume does not re-enter the current phase, so a session resumed in SWEEP had no sweep in flight and no outcome to exit on: it idled until the grant the lost sweep held ran out, and the sweep was never measured. Before a resumed process's first phase decision, SWEEP with no recorded conc_sweep outcome now re-fires its entry hook, once. Conflicts resolved: - src/hyperloom/orchestrator/phases/machine.py: main's context next to the new _release_holds_no_task_will_settle helper includes _drain_for_transition, which landed on main after the release cut. Only the new helper is taken; _drain_for_transition is not brought to the release. - scripts/code_metrics_baseline.txt: the release's baseline for MachinePhase.advance_phase_if_needed is 192 (main's was 156). The pick shortens the function by 4 lines on this branch too, so the entry is 188. (cherry picked from commit c831742) Co-Authored-By: Claude <noreply@anthropic.com>
…PID namespace (#1871) Cherry-pick of #1871 onto release/v1.1.4. The owner scope recorded on leases was `<boot_id>:<pid-namespace inode>`; a container started on a node after another one exited can repeat that inode, so `recover --confirm-scope-gone` refused the lost container's scope as the current environment. The scope now adds the start time of the namespace's PID 1; two-part scopes of this boot and namespace are still treated as this environment by the resume guard and reaper, and confirming one checks that no holder that started before its lease still runs here. Conflict resolved: - src/hyperloom/inference_optimizer/session/resume_guard.py: on main, #1840 split _inspect into _scan_leases/_scan_gpu_leases/_scan_tasks/ _scan_open_rounds before #1871 changed one line in _scan_leases. The release keeps the unsplit _inspect; the same one-line change (match the lease's scope with same_environment) is applied there. The refactor is not brought to the release. Every other file applied cleanly. (cherry picked from commit 25ea91f) Co-Authored-By: Claude <noreply@anthropic.com>
Code metrics gate: PASSED with waived findings (
|
| Dimension | Limit | New | Worse | Baseline out of date | Baseline entries | vs base |
|---|---|---|---|---|---|---|
| Cyclomatic complexity | fails > 20 | 0 | 0 | 0 | 122 | +0 |
| Cognitive complexity | fails > 30 | 0 | 1 | 0 | 323 | +0 |
| Function length (lines) | fails > 80 | 0 | 2 | 0 | 477 | +0 |
| Nested block depth | fails > 5 | 0 | 0 | 0 | 29 | +0 |
| Module length (lines) | warns > 800, fails > 1200 | 0 | 2 | 0 | 55 | +0 |
| Duplicated lines per file | fails > 0 | 0 | 0 | 0 | 120 | +0 |
| Dead code (vulture) | fails > 0 | 0 | 0 | 0 | 2 | +0 |
| Check | Findings | Effect |
|---|---|---|
| Comments | 0 | |
| English only | 0 | |
| Production code imports test code | 0 | |
| Repeated literals | 0 | |
| A1 Private name of another package | 0 | |
| A8 Flag argument | 0 | |
| A9 Tuple return | 0 | |
| A10 Raw vocabulary comparison | 0 | |
| A11 Hidden global write | 0 | |
| A12 Pure forwarder | 0 | |
| A12 Single-implementation abstraction | 0 |
Worse than the baseline (waived by the baseline-raise label): 5
| Unit | Dimension | Baseline | Now |
|---|---|---|---|
src/hyperloom/orchestrator/actions/executors/_grid_runner.py:863 run_grid |
Cognitive complexity | 222 | 228 |
src/hyperloom/orchestrator/actions/executors/_grid_runner.py:863 run_grid |
Function length (lines) | 1073 | 1076 |
src/hyperloom/orchestrator/actions/executors/baseline.py:2309 BenchmarkRunExecutor._run_once |
Function length (lines) | 682 | 683 |
src/hyperloom/orchestrator/actions/executors/_grid_runner.py:1 |
Module length (lines) | 2258 | 2274 |
src/hyperloom/orchestrator/actions/executors/baseline.py:1 |
Module length (lines) | 4253 | 4292 |
How to fix
- New or worse: split the unit (extract a function, a class, a module) until it is back under the limit, or no worse than its recorded value. The baseline cannot absorb a new violation: CI refuses any baseline entry added or raised relative to the base branch.
- Baseline out of date: something improved. Run
python scripts/code_metrics.py --update-baselineand commitscripts/code_metrics_baseline.txt; the command only lowers or removes entries. - Override: the
baseline-raisePR label waives new, worse and baseline-growth findings and the failing design checks (they stay listed here). Use it only with the reason in the PR description; it waives nothing else. - Tools for a local run:
pip install ruff==0.16.2 complexipy==8.0.1 vulture==2.16andnpm install -g jscpd@5.4.1(or pointCODE_METRICS_JSCPDat another jscpd command, such asnpx --yes jscpd@<version>).
Already over at the base (backlog, not this change's): 4
| Unit | Dimension | Base | Now |
|---|---|---|---|
src/hyperloom/inference_optimizer/cli/parser.py:1 |
Module length (lines) | 1214 | 1202 |
src/hyperloom/inference_optimizer/cli/parser.py:178 _build_parser |
Function length (lines) | 1038 | 1025 |
src/hyperloom/orchestrator/actions/executors/baseline.py:3371 BenchmarkRunExecutor._run_single_benchmark |
Function length (lines) | 580 | 580 |
src/hyperloom/orchestrator/loop/writeback.py:1 |
Module length (lines) | 4809 | 4807 |
33 findings in files this change does not touch
Outside this change (informational): 33
| Unit | Dimension | Baseline | Now |
|---|---|---|---|
src/hyperloom/orchestrator/enablement/recipe/projections.py project_launch_evidence |
Cognitive complexity | absent | 36 |
src/hyperloom/orchestrator/state/shared_state.py resolve_graded_comparison |
Cognitive complexity | absent | 35 |
src/hyperloom/common/launch_log_evidence.py _server_args_from_log |
Cognitive complexity | 48 | 49 |
src/hyperloom/orchestrator/actions/executors/explore.py ExploreExecutor.__call__ |
Cognitive complexity | 369 | 372 |
src/hyperloom/orchestrator/loop/dispatcher.py DispatcherCollaborator._spawn_fitting_queued |
Cognitive complexity | 110 | 111 |
src/hyperloom/orchestrator/state/_shared_state/render.py _RenderMixin._format_variant_line |
Cognitive complexity | 36 | 40 |
src/hyperloom/common/launch_log_evidence.py _server_args_from_log |
Cyclomatic complexity | 22 | 23 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py materialize_config_with_envs |
Cyclomatic complexity | 110 | 111 |
src/hyperloom/inference_optimizer/cli/__init__.py _run_optimize |
Function length (lines) | 940 | 946 |
src/hyperloom/inference_optimizer/cli/bootstrap.py _seed_shared_state |
Function length (lines) | 155 | 157 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py materialize_config_with_envs |
Function length (lines) | 922 | 928 |
src/hyperloom/orchestrator/actions/executors/explore.py ExploreExecutor.__call__ |
Function length (lines) | 1124 | 1133 |
src/hyperloom/orchestrator/loop/dispatcher.py DispatcherCollaborator._spawn_fitting_queued |
Function length (lines) | 242 | 243 |
src/hyperloom/orchestrator/loop/dispatcher.py DispatcherCollaborator.run_task_registered |
Function length (lines) | 109 | 111 |
src/hyperloom/orchestrator/phases/prelude.py PreludePhase._settle_warm_replay |
Function length (lines) | 388 | 390 |
src/hyperloom/orchestrator/policy/gate.py PolicyGate._validate_specialist_dispatch |
Function length (lines) | 91 | 93 |
src/hyperloom/orchestrator/prompts/prompt_builder.py build_orchestration_prompt |
Function length (lines) | 139 | 142 |
src/hyperloom/orchestrator/prompts/specialist_prompt_builder.py _section_identity |
Function length (lines) | 94 | 95 |
src/hyperloom/orchestrator/specialists/runner.py SpecialistRunner._prepare |
Function length (lines) | 192 | 200 |
src/hyperloom/orchestrator/state/shared_state.py resolve_graded_comparison |
Function length (lines) | 100 | 101 |
src/hyperloom/inference_optimizer/cli/__init__.py |
Module length (lines) | 2569 | 2740 |
src/hyperloom/orchestrator/actions/executors/explore.py |
Module length (lines) | 1905 | 1914 |
src/hyperloom/orchestrator/loop/coordinator.py |
Module length (lines) | 1429 | 1432 |
src/hyperloom/orchestrator/loop/dispatcher.py |
Module length (lines) | 1503 | 1551 |
src/hyperloom/orchestrator/loop/intent_router.py |
Module length (lines) | 1548 | 1556 |
...and 8 more; the job log lists every row.
Verdict computed by the gate scripts in
/home/runner/work/_temp/code-metrics-gate, not by this tree's copy.
Thresholds and sources: [tool.hyperloom.code_metrics] in pyproject.toml. Tools: complexipy 8.0.1, jscpd 5.4.1, ruff 0.16.2, vulture 2.16.
chaojhou
left a comment
There was a problem hiding this comment.
PR #1874 -- release/v1.1.4: cherry-pick resume fixes (#1859, #1870, #1871)
What it does: On release/v1.1.4 a session whose container is gone cannot be resumed: its leases name an owner scope that no longer exists, and the only release path, recover --confirm-stopped, refuses tasks with a recorded scope. This PR cherry-picks the three main fixes that close this -- hyperloom recover --confirm-scope-gone (#1859), re-dispatching a lost conc_sweep when a resume lands in SWEEP (#1870), and an owner scope that tells two containers on one node apart (#1871) -- applying #1871's one-line scope check to the release's unsplit _inspect and leaving main's refactors behind.
Blocking issues: 1
- [X5] The new
--confirm-scope-gonerecovery step is missing from the operator docs this branch ships [verified]
Problem: the flag is new on this base (absent from release/v1.1.4 at 60b5770) and no doc names it.docs/reference/operations.md"Disaster recovery / Scenario A: pod was OOM-killed mid-session" -- the scenario this PR exists for -- still tells the operator to relaunch with--resume-from, and saysrecoveronly rebuilds the breakdown artifact (operations.md:278); by this PR's own description that resume is refused until the vanished scope is confirmed. src/hyperloom/inference_optimizer/cli/recover.py:29
Impact: an operator following the v1.1.4 disaster-recovery page after a lost container hits the resume guard's refusal, and the step that resolves it is discoverable only from that refusal message (resume_guard.py:196-197) or--help. The same page on main has the same gap.
Action: add thehyperloom recover --session-dir S --confirm-scope-gone OWNER_SCOPE --confirmation-reason TEXTstep to Scenario A -- when to use it instead of--confirm-stopped, and what it refuses -- and update the other places that still describerecoveronly as a breakdown rebuild (operations.md:144crash-recovery row,operations.md:214exit codes, therecoversubcommand help atparser.py:1186-1187), in this PR and the same change on main.
Checked: per-file interdiff of each pick against its main merge commit's first-parent diff (#1859 680b374 -> 21695dc, #1870 c831742 -> 330d9bc, #1871 25ea91f -> 123ad18): identical except the baseline numbers (192 -> 188) and the indentation of the one same_environment line in _inspect, as the description states; the owner-scope comparisons in resume_guard.py / resource_lock.py on the release head against main (same set, same kinds); every local_owner_scope importer on the release (the old import path is kept by resource_lock.py:16); the new machine helper's dependencies (release_finished_roofline_gate and resume_unsettled_sweep only, nothing from _drain_for_transition); open PRs on release/v1.1.4 (none besides this one); the docs for hyperloom recover on the head and on main; an independent reader pass over this card, the diff and the merge base | Ran: the interdiff script and git grep sweeps on both branches; no local tests (CI green at the head: 12 test shards, both coverage jobs, code-metrics, pr-hygiene, import-linter) | SKIPPED: V4 -- openprs.txt "# PARTIAL:" lines for #1032, #1154, #1276 and #1675 (only their first 100 files compared); none of them targets this base -- gh pr list --base release/v1.1.4 lists only this PR | Base: 60b5770 | Head: 123ad18
Scenario A of the disaster-recovery runbook still told the operator only to relaunch with --resume-from and described recover only as a breakdown rebuild, while a resume after a lost pod is refused until the vanished owner scope is confirmed (#1859). - operations.md Scenario A: a step for a blocked resume, with the recover --confirm-scope-gone command, what it does and does not do, what it refuses, and when --confirm-stopped applies instead. - operations.md: the crash-recovery lifecycle row, the recover exit codes and the --resume-from / recover summary line. - operator-scripts.md: the recover entry. - recover subcommand help: names the cleanup confirmations. Docs and help text only; no behaviour change. Co-Authored-By: Claude <noreply@anthropic.com>
|
Thanks. Addressed in 6c3c2cd (docs and help text only, no behaviour change):
Same change on main: #1880. |
…on environment A node loss during an integrate left pending_integrate at phase "ready" with the operator's stash recorded in a tree that lived in the dead container. On resume in a new container the task carried the operator's stopped confirmation with an empty result, so recovery held the sentinel as an unknown outcome, and a rollback would have refused anyway because the stash is absent from the new checkout: every resume stopped with patch_recovery_incomplete. - An integrate attempt now records an identifier in the per-worktree git directory of the tree it mutates (tree_instance). On resume, a different identifier at the same path means another checkout: the tree-side obligations (reset the candidate out, hand the stash back) went with the old tree, in every phase short of restored or accepted. The sentinel is marked restored with tree_replaced, artifact targets outside the tree are still restored from their preimages, and the stash that was not restored is reported. A matching identifier keeps today's behaviour, so a stash dropped or pruned from the same tree still refuses. - A task whose last outcome is an operator confirmation that it stopped, with no kept result on the bus and no stack row, now proceeds to rollback instead of being held as an unknown outcome. An accepted window is still held. - A sentinel written before identifiers existed is judged replaced only when the task was confirmed stopped, the recorded stash object is not in the tree's object store at all, and nothing of the candidate is in the tree: every patch still applies forward cleanly (an applied candidate, or one edited over by hand, keeps the refusal) and every artifact target inside the tree holds its recorded preimage. (cherry picked from commit fd2ab7a) Co-Authored-By: Claude <noreply@anthropic.com>
Resume settled a crashed integrate_patch window from its task row only when the window carried a patch or an artifact. A candidate that changed only environment variables or server arguments has neither, so a resume after its KEEP was recorded on the task row, but before it reached the bus, cleared the sentinel as stale and the accepted candidate never reached the stack. Every window now has its task row read. A confirmed KEEP in the accepted phase is replayed into the stack, the same as for a patch candidate; a window with nothing in the tree is otherwise cleared as before, since there is nothing to roll back or hold. (cherry picked from commit a436bd4) Co-Authored-By: Claude <noreply@anthropic.com>
…l backport of #1606) In a concurrency sweep whose rounds each boot their own server through an AgentX client (aiperf_client.sh / mlperf_agentic_client.sh), a round that hits the benchmark cap is killed as magpie_timeout. The client dies, but the server it booted leads its own session and is re-parented away from the client, so reaping the round's process tree never reaches it. The leftover server keeps the port and nearly all GPU memory: the next rounds either measure against it (their own server fails to bind) or fail at engine start for lack of memory, until the sweep ends. The stop marker still said "tree reaped". This is a partial backport of #1606 (cherry picked from commit c9818d5, server-teardown part only): - agentx_workload: AGENTX_SERVER_PIDFILE, the pidfile both client scripts already write. - _grid_runner: the variant teardown runs after every round (not only auto-warmup rounds), including a multi-node warm-up stop, and also stops the server agentx_server.pid names; a stopped round's record names any server that outlived SIGKILL. - _server_lifecycle: stop_agentx_servers / reap_recorded_servers; the reaper treats a zombie leader as gone while still reaching live ranks in its group, refuses to signal its own process group, confirms the SIGKILL and keeps a survivor's pidfile. Not backported from #1606: the GEAK metric axis, the GEAK hand-off GPU wait (free_gpus_for_handoff and its rocm_smi helper), the extend-budget cap, the GEAK budget reservation, the baseline executor's cleanup call and the code-metrics baseline update. Those are behaviour changes or features, not this bug. Tests: #1606's TestRoundServerTeardown and orphan-reaper tests, plus the timed-out-round regression test from #1894 (test_timed_out_round_stops_its_server_before_the_next_round, run for a sglang and a vllm server cmdline). The new test fails on the parent of this commit (the server is still alive when the next round starts) and passes with it. Co-Authored-By: Claude <noreply@anthropic.com>
|
Pushed Test result (Python 3.10.22):
@chaojhou could you take another look? |
E2E: resume after sandbox loss, on head 5b9f212Re-ran the real resume end-to-end test on Interruptions (Kubernetes Eviction API on the session's own sandbox pod):
Verified
Not covered
Pre-existing issue seen (not introduced by this PR; same code on main) 🤖 Generated with Claude Code |
chaojhou
left a comment
There was a problem hiding this comment.
PR #1874 -- release/v1.1.4: cherry-pick resume fixes (#1859, #1870, #1871, #1879)
What it does: On release/v1.1.4 a session whose container is lost cannot be resumed: its leases name an owner scope that no longer exists (and a restarted container on the same node can repeat that scope), a session resumed in SWEEP idles with no sweep in flight, and the sentinel an interrupted integrate left for the lost tree blocks every resume. This PR cherry-picks the main fixes (#1859 hyperloom recover --confirm-scope-gone, #1871 a three-part owner scope, #1870 SWEEP re-dispatch on resume, and #1879, still open on main, which adds a source-tree identity and replays env-only KEEPs) with their runbook docs, and partially backports #1606 so that run_grid stops the server an AgentX client booted after every round.
Blocking issues: 2
- [C2] The partial #1606 backport stops a killed round's AgentX server only in
run_grid; #1606's same teardown in the baseline executor is left out for a reason that does not hold [verified]
Problem:stop_agentx_servershas a single production caller on this head,_teardown_variant_server(_grid_runner.py:2054). #1606 also calls it from the baseline executor's round cleanup (c9818d5baseline.py:2740; its commit message: "so does the baseline executor's cleanup"). On this head the baseline executor runs the same AgentX client (baseline.py:2470-2476) throughrun_with_session_kill(baseline.py:3613); on a single nodeMAGPIE_RUN_PHASEstaysfull(it is set toclientonly for multi-node,baseline.py:3428), so the client boots the server itself and records it inagentx_server.pid(aiperf_client.sh:132). Neither the round cleanup (baseline.py:2735-2741) nor_pre_start_cleanupreads that file; only the session-widereap_orphaned_serversdoes, from the roofline preflight and at coordinator boot and shutdown. The description lists "the baseline executor's cleanup call" as not taken under "unrelated behaviour changes ... Only its server-teardown part is taken", but that call is the same server teardown. src/hyperloom/orchestrator/actions/executors/_server_lifecycle.py:325
Impact: in a single-node AgentX session, a baseline round killed at its cap or cancelled leaves its setsid'd server holding the port and nearly all GPU memory until the next roofline action or the end of the session, so the rounds after it hit the same port-bind failures and engine-start OOMs this commit fixes for sweep rungs.
Action: add #1606's_lifecycle.stop_agentx_servers(output_dir)to the baseline executor's round cleanup (baseline.py:2735-2741), or keep it out and state in the description that a killed AgentX baseline round still leaves its server running on v1.1.4, and why that path is out of scope. - [X2] The title and the description's summary describe a resume-only PR, but the head also ships the partial #1606 sweep-teardown backport [verified]
Problem: the title names only #1859, #1870, #1871 and #1879, but the head carries d09c9e1 ("fix(sweep): stop the server a timed-out round's client booted (partial backport of #1606)"), which changes round teardown in_grid_runner.py,_server_lifecycle.pyandagentx_workload.py. The opening paragraph still says "Nothing else is picked: no refactors or unrelated fixes", and the observable-effect section lists only resume effects. src/hyperloom/orchestrator/actions/executors/_grid_runner.py:1594
Impact: the body has its own section on the sweep fix, but the parts that summarise the PR -- the title, the opening paragraph and the observable-effect section -- leave it out or contradict it. The release cut aggregates PR descriptions into the GitHub release (AGENTS.md, "Every change states its observable effect in its own PR description"), so the v1.1.4 notes would present this PR as resume fixes only and omit the behaviour it changes in every sweep and explore round: teardown after every round,server_survived_teardown:<pid>warnings, and a 30 s SIGKILL confirmation in the orphan reaper.
Action: retitle to name the sweep teardown (for example "release/v1.1.4: cherry-pick resume fixes (#1859, #1870, #1871, #1879) and sweep server teardown (partial #1606)"), and correct the opening paragraph, the observable-effect section and the linked references to include it.
SKIPPED: V4 -- PARTIAL overlap for #1032, #1154, #1276, #1675, #1869 (each cut at its first 100 files), overlap there unchecked; all target main, and gh pr list --base release/v1.1.4 lists only this PR
Checked: per-file interdiff of every pick against its source (first-parent diffs of 680b374, c831742 and 25ea91f; #1879's head commits fd2ab7a and a436bd4; #1606 c9818d5): identical except the resolutions the description states and, for #1606, the parts it leaves out; #1879's head is still a436bd4 (OPEN) and its resume functions are byte-identical on this head; every symbol the picks and the backport call exists at the base; stop_agentx_servers callers on this head vs c9818d5; the baseline executor's AgentX path (prepare_agentx_runtime, run_with_session_kill, MAGPIE_RUN_PHASE) and the client scripts' agentx_server.pid; the earlier card's [X5], fixed by 6c3c2cd (operations.md:144, :214-215, :263-302, :320-322; operator-scripts.md:194-198; parser.py:1186-1187) | Ran: interdiff and function-compare scripts; git grep at 60b5770, d09c9e1 and c9818d5; python ..\pr_tools\c901.py 60b57706e d09c9e1df <14 src files> (ruff C901 counting; ruff is not installed here): no added or raised function over 20; gh run list --commit d09c9e1d (latest run of all 9 workflows green); gh pr list --base release/v1.1.4; findings traced statically, no local tests | Base: 60b5770 | Head: d09c9e1
…d (partial backport of #1606) An AgentX client boots its server setsid'd, outside the round's process tree, and records it in agentx_server.pid. On a single node the baseline executor runs that client too (MAGPIE_RUN_PHASE stays full), and its single-round cleanup never read the pidfile, so a baseline round killed at its cap or cancelled left its server holding the port and the GPUs until the next session-wide reap. The single-round cleanup now calls _lifecycle.stop_agentx_servers on the round's output directory, the same call #1606 adds on main. A test runs a single baseline round whose fake client leaves a setsid'd server behind and checks it is stopped after both a completed and a cancelled round; it fails without the fix. Also explain the empty FileExistsError branch in ensure_tree_instance (CodeQL "empty except"): the identifier another creator linked first stands and is read back. Co-Authored-By: Claude <noreply@anthropic.com>
|
Thanks. Both addressed in 1. [C2] baseline executor teardown. Taken. The single-round cleanup in 2. [X2] title and summary. Retitled to "release/v1.1.4: cherry-pick resume fixes (#1859, #1870, #1871, #1879) and round server teardown (partial #1606)". The opening paragraph now lists the two concerns (resume fixes; sweep and baseline server teardown) and drops "Nothing else is picked"; the teardown section covers both commits and no longer lists the baseline cleanup call as not taken; the pick table has a #1606 row ( CodeQL "empty except" ( |
PR #1874 -- release/v1.1.4: cherry-pick resume fixes (#1859, #1870, #1871, #1879) and round server teardown (partial #1606)What it does. A session on Blocking issues: none. Checked, since the value of a backport review is whether the picks are faithful and the partial one is self-contained:
Ran: LGTM |
xiaofei-zheng
left a comment
There was a problem hiding this comment.
Approving on the review posted above: no blocking issues. All five picks verified byte-identical to their main originals apart from the documented baseline numbers and the one docstring cross-reference; the partial #1606 backport is self-contained and its pidfile writer already exists on this branch; the new docs match the code they describe.
Two things for
release/v1.1.4:release/v1.1.4needs (hyperloom recoverfollowed byoptimize --resume-fromon the original session). Each one is required: without it that resume fails or stalls. No refactors are picked. One docs commit documents the new recover step for operators (below). Resume a crashed integrate: discharge a tree lost with its execution environment, replay an accepted env-only candidate #1879 (merged on main asd77c808bb) is picked on top: once the steps above let a session resume, a crashedintegrate_patchwindow is the next thing that stops it.hyperloom recover --confirm-scope-gone(#1859). When the container that ran a session is gone, the leases it held name an execution scope that no longer exists. Until now the only way out was--confirm-stoppedone task at a time, which refuses tasks with a recorded owner scope, so the resume guard kept refusing the session.recover --confirm-scope-gone OWNER_SCOPE --confirmation-reason TEXTnow cancels the unfinished tasks of that vanished scope and releases their execution and GPU leases, under the session lock and in one transaction. It refuses the current scope, an unobservable current scope, any unexpired lease of another scope, and tasks that span two scopes. Why required:release/v1.1.4does not have the command (git merge-base --is-ancestor 680b3742 origin/release/v1.1.4fails), and without it a session whose container is gone cannot be resumed at all; #1870's regression test and #1871 also build on it.Re-dispatch the conc_sweep when a resume finds SWEEP without one (#1870).
recovercancels a vanished scope's tasks directly in the task ledger, so a conc_sweep cancelled that way never records its outcome. A resume does not re-enter the current phase, so a session resumed in SWEEP had no sweep in flight and nothing to exit on: it idled until the grant the lost sweep held ran out, and the sweep was never measured. Before a resumed process's first phase decision, SWEEP with no recorded conc_sweep outcome now re-fires its entry hook, once. Why required: without it a session interrupted in SWEEP resumes and then sits idle instead of finishing.Owner scope unique per container (#1871). The owner scope recorded on leases was
<boot_id>:<pid-namespace inode>. A container started on a node after another one exited can repeat that inode, sorecover --confirm-scope-gonerefused the lost container's scope as "the current execution environment" and the resume guard kept refusing. The scope now also includes the start time of the namespace's PID 1. Two-part scopes from this boot and namespace are still treated as this environment by the resume guard and reaper, and confirming one checks that no holder process that started before its lease is still running here. Why required: without it, a resume on the same node after a container restart is refused every time.Resume a crashed integrate (#1879, two commits). Once a vanished container's scope is confirmed gone, a session whose container was lost during an
integrate_patchstill cannot resume.state.jsonkeepspending_integrate(phaseready, the operator's stash, the source tree's HEAD) for a tree that lived in the lost container, and the task's last outcome is the operator's stop confirmation with an empty result. Resume held that sentinel as an unknown outcome, and a rollback would have refused anyway because the recorded stash is absent from the new checkout: every resume stopped withpatch_recovery_incomplete, with no way forward short of editingstate.json.recovery.tree_instance). On resume, a different identifier at the same path means another checkout: the sentinel is markedrestoredwithtree_replaced, artifact targets outside the tree are restored from their preimages, and the stash that was not restored is reported (unrestored_stash_oid), never popped into the new tree. A task whose last outcome is an operator stop confirmation, with no KEEP on the bus and no stack row, now rolls back instead of being held; anacceptedwindow is still held. A sentinel written before identifiers existed (every sentinel a v1.1.4 session wrote before this pick) counts as a replaced tree only when the task was confirmed stopped, the recorded stash object is not in the tree's object store at all, every patch of the candidate still applies forward cleanly and none reverse-applies, and every artifact target inside the tree holds its recorded preimage; otherwise it refuses as before. That inference can misread the same tree with its stash dropped and pruned in two cases, both recorded in thetree_replaceddocstring: the candidate already taken out by hand (nothing is lost), and an in-tree artifact whose content is back at its preimage but whose file mode the candidate changed (the mode stays as the candidate set it).acceptedphase is replayed into the stack (replayed_pending_integrate), anything else with nothing in the tree is cleared as before, never held.Why required: a release user whose container is lost mid-integrate cannot resume at all after the fixes above (the first commit), and a resume can silently drop a candidate it had already accepted (the second). Both defects are in v1.1.4: the reproductions below fail on this branch before the pick and pass after it.
Sweep and baseline: stop the server a round's AgentX client booted (partial backport of #1606). Two commits on top of the resume picks:
d09c9e1df(sweep / explore rounds inrun_grid) and384eb89d1(the baseline executor's single-round cleanup).What. In a concurrency sweep whose rounds each boot their own server through an AgentX client (
aiperf_client.sh/mlperf_agentic_client.sh, per-round restart), a round that hits the benchmark cap is killed asmagpie_timeout. The client dies, but the server it booted leads its own session and is re-parented away from it, so reaping the round's process tree never reaches it. Onrelease/v1.1.4,run_gridonly tears down a server it started itself (auto-warm-up lifecycle), so that leftover server keeps the port and nearly all GPU memory for the rest of the sweep: the next rungs fail to bind the port and measure against the leftover, and once it goes idle the later rungs fail at engine start for lack of memory (magpie_nonzero_invalid_measurement). The stop marker still records "tree reaped".Why it is a bug, not a feature. Every rung after a timed-out one reports a number that is not its own (measured against another rung's server) or no number at all. Nothing in the sweep is meant to keep a timed-out round's server alive; the client scripts already record its pid in
agentx_server.pidfor exactly this teardown, and the release simply never reads it.Partial backport scope. #1606 (
c9818d50e) bundles this fix with unrelated behaviour changes. Only its server-teardown part is taken, three production files, with-x-style reference in the commit message:common/agentx_workload.py:AGENTX_SERVER_PIDFILE._grid_runner.py: the variant teardown runs after every round (measured, failed, timed out, cancelled, warm-up stop, multi-node warm-up stop), not only auto-warm-up rounds, and also stops the serveragentx_server.pidnames; a stopped round's marker andnonfatal_warningsname any server that outlived SIGKILL._server_lifecycle.py:stop_agentx_servers/reap_recorded_servers; the reaper treats a zombie leader as gone while still reaching live ranks in its group, refuses to signal its own process group, confirms the SIGKILL and keeps a survivor's pidfile.baseline.py(384eb89d1): the single-round cleanup calls_lifecycle.stop_agentx_servers(output_dir), the same line fix(geak): make an AgentX hand-off to GEAK end in a result the session can keep. #1606 adds on main (baseline.py:2740atc9818d50e). On a single node the baseline executor runs the same AgentX client (MAGPIE_RUN_PHASEstaysfull, so the client boots the server and records it inagentx_server.pid), and without this a killed or cancelled baseline round left that server up until the next session-wide reap.Not taken: the GEAK metric axis, the GEAK hand-off GPU wait (
free_gpus_for_handoffand therocm_smihelper it needs; it has no caller without the KERNEL-phase change), the extend-budget cap, the GEAK budget reservation and #1606's code-metrics baseline update.Tests. #1606's
TestRoundServerTeardownand orphan-reaper tests, plus the timed-out-round regression test from #1894 (TestRoundServerTeardown::test_timed_out_round_stops_its_server_before_the_next_round, run with a sglang and a vllm server cmdline). Python 3.10.22:5b9f21250, production files unchanged): the new test fails for both cmdlines withassert [True] == [False](the first round's server is still alive when the second round starts); an independent reproduction written outside the PR fails the same way.test_grid_runner_behavior_lock.py+test_orphan_server_reaper.py: 38 passed._server_lifecycle/run_grid(43 files): 2251 passed, 4 skipped, 2 failed; the same 2 (test_coverage_gap_units.py::test_credentials_validate_and_reset_claude_config,test_integrate_patch_executor.py::test_enablement_zero_patch_round_does_not_erase_prior_accepted_work) fail on5b9f21250on this host as well, so neither comes from the commit.ruff check/ruff format --check(0.16.2) on the five touched files: clean.384eb89d1:test_baseline_warmup_double_run.py::test_baseline_single_round_stops_the_server_its_agentx_client_booted(a completed and a cancelled round, each leaving a setsid'd server withagentx_server.pid) fails ond09c9e1df(the server is still alive) and passes with the commit.test_baseline*.py+test_grid_runner_behavior_lock.py+test_orphan_server_reaper.py+test_source_tree_identity_unit.py: 358 passed. The same commit adds an explanatory comment to the emptyexcept FileExistsErrorinsource_tree_identity.ensure_tree_instance(CodeQL "empty except"); no behaviour change.--base-ref 5b9f21250: the only findings arerun_grid(cognitive complexity 222 -> 228, length 1073 -> 1076) and_grid_runner.pymodule length (2258 -> 2274), from the teardown now running on every exit path. fix(geak): make an AgentX hand-off to GEAK end in a result the session can keep. #1606 carried the same growth on main underbaseline-raise; this PR takes that label for the same reason rather than reshapingrun_gridon a release branch away from main.Pick list, in order (
git cherry-pick -x -m 1for the merged PRs, plaingit cherry-pick -xfor #1879's two commits; each commit keeps its(cherry picked from commit ...)trailer):680b374289328ca36a189798e70b9006861b46c221695dc29c831742d37b18310fe3ba8cb387fa116e43d9e31330d9bc5d25ea91feacbd20769ce252249f8994876dd4ebfa123ad1849fd2ab7a546992447ec2b592fb2ab39b31d706ce7(PR commit; merged on main byd77c808bb)f25d8a41fa436bd4e675cfe4cda06fcf1c124270698b128ed(PR commit; merged on main byd77c808bb)5b9f21250c9818d50e909f322a81b5c7259f1b1e0efa62a7bd09c9e1df,384eb89d1Base:
release/v1.1.4at60b57706e.Conflicts and how they were resolved:
src/hyperloom/orchestrator/phases/machine.py: on main, the context next to the new_release_holds_no_task_will_settlehelper includes_drain_for_transition, which landed on main after the release was cut. Only the new helper is taken;_drain_for_transitionis not brought to the release.scripts/code_metrics_baseline.txt: the release's baseline forMachinePhase.advance_phase_if_neededis 192 (main's was 156). The pick shortens the function by 4 lines here too, so the entry is 188.src/hyperloom/inference_optimizer/session/resume_guard.py: on main, refactor(optimizer): bring the optimizer and scripts under the complexity ceiling #1840 split_inspectinto_scan_leases/_scan_gpu_leases/_scan_tasks/_scan_open_roundsbefore Tell apart two containers that repeat one boot and PID-namespace inode in the owner scope #1871 changed one line in_scan_leases. The release keeps the unsplit_inspect, and the same one-line change (match the lease's scope withsame_environment) is applied there. The refactor is not brought to the release. Every other file applied cleanly.src/hyperloom/inference_optimizer/session/resume_guard.py: on main the newconfirmed_stopped_scopesits next to_confirmation_entry/_write_confirmation, a split of_confirm_task_stopped_in_transactionthat is not on the release. The release keeps the unsplit function, andconfirmed_stopped_scopeis added unchanged after it; only its docstring names_confirm_task_stopped_in_transactionas the writer. The evidence it reads (reason,cleanup_confirmed,outcome,confirmed_scope_gone) is what the release writes.scripts/code_metrics_baseline.txt: the release's entries forIntegratePatchExecutor._stage_apply(521) andintegrate_patch.py(4886) differ from main's. The pick shortens them here too, so the entries are 515 and 4881, measured on this branch. The new import line inintegrate_patch.pyapplied cleanly.Docs for the recover step (review request). One commit on top of the picks, docs and help text only, no behaviour change:
docs/reference/operations.md, Disaster recovery / Scenario A: a new step 4 for a resume that exits 2 withresume blocked: ...: thehyperloom recover --session-dir S --confirm-scope-gone OWNER_SCOPE --confirmation-reason TEXTcommand, what it cancels and releases, what it refuses (current scope, unobservable current scope, an unexpired lease of another scope, a task holding execution leases in two scopes, a two-part scope whose holder still runs here, a ledger without owner scopes, a held session lock), what happens if theruntime/pod_history.jsonlappend fails, and when--confirm-stoppedapplies instead (legacy empty-scope ownership, one task at a time).docs/reference/operations.md: the crash-recovery lifecycle row, thehyperloom recoverexit codes (2 also on a refused confirmation; 0 also when there was nothing to confirm) and the--resume-from/recoversummary line.docs/reference/operator-scripts.md: therecoverentry.recoversubcommand help inparser.py: names the cleanup confirmations (same two lines, so_build_parserdoes not grow).The same docs change for main is #1880.
No resume pick brought in unrelated changes. The #1859 edits to
parser.pyandresource_lock.pymove the recover flags next to their handler and movelocal_owner_scopeunchanged intohyperloom.common.owner_scope, which #1871 then extends.Cherry-picks of #1859, #1870, #1871 and #1879 (all merged to main), and a partial backport of #1606 (server teardown only).
The picked tests come from the PRs:
test_resume_scope_gone.py(#1859, extended by #1871), fourTestASweepCancelledByRecoverIsRedispatchedOnResumetests intest_resume.py(#1870), andcommon/tests/test_owner_scope.py(#1871). Everything below was run on this branch with Python 3.10.22:Per file (collected): test_owner_scope 14, test_resume 40, test_resume_scope_gone 30, test_resume_guard 133, test_sweep_phase_auto 101, test_conc_sweep 121, test_conc_sweep_ceiling 7, test_conc_sweep_session_deadline 38, test_cli_resume_launch_shape 22, test_resume_budget_objective 7, test_preflight_resume_framework 36, test_geak_resume_recovery 7, test_cli_conc_sweep_default 23 (579 total = 577 passed + 2 skipped).
The two hand-resolved hunks were checked to be load-bearing on this branch: reverting the
same_environmentline in the release's_inspectfailstest_resume_scope_gone.py::test_resume_after_an_in_place_upgrade_still_sees_its_own_two_part_lease, and removing theresume_unsettled_sweep()call from the new machine helper fails two of the #1870 tests intest_resume.py.#1879 picks (Python 3.10.22):
test_coordinator_async_batch2_unit.py(166, including the six new resume scenarios) andtest_source_tree_identity_unit.py(19) pass, as dotest_resume_guard.pyandtest_resume_scope_gone.py(348 passed across the four files). The wider run over every test file that mentionspending_integrate, the resume guard, tree identity or writeback (69 files): 3452 passed, 7 skipped, 17 failed; the same 17 fail on the release base6c3c2cd8don this host (nohttpxmodule, nopatchbinary), so none comes from the picks. Two independent reproductions written outside the PR, run on this branch and on6c3c2cd8d: a lost container's tree re-cloned at the same path (with an identifier and with a legacy sentinel) now discharges and restores the artifact outside the tree, where the base refuses; the same tree with an intact stash now rolls back normally, where the base holds it; the same tree with its stash pruned, or with the candidate edited by hand, still refuses on both; an environment-only and a server-args-only KEEP now reach the stack, where the base clears them (13 passed here, 6 failed on the base).Docs commit:
python -m hyperloom recover --helprenders the new subcommand help;recover --confirm-scope-gone <this environment's scope>printsERROR: the named owner scope is the current execution environmentand exits 2, and combining it with--forceexits 2, as the docs say;test_resume_scope_gone.py30 passed;ruff check/ruff format --checkonparser.pyclean.Code-metrics gate, run with the gate scripts taken from the release base:
python code_metrics.py --root . --base-ref 60b57706e-> PASSED (11 files judged, 0 new, 0 worse, 0 stale baseline entries). For the #1879 picks,--base-ref 6c3c2cd8d(this branch before them) -> PASSED (8 files judged, 0 new, 0 worse, 0 stale entries).ruff checkandruff format --check(0.16.2) on the touched Python files: clean.Re-qualification: resume and SWEEP paths, resume of a session interrupted during an integrate, a concurrency sweep with a timed-out rung, and a single-node AgentX baseline round that is cancelled or hits its cap.
A real resume E2E on this branch (head
5b9f21250, two pod evictions,recover --confirm-scope-gonethenoptimize --resume-from) is posted as a PR comment; the teardown commits are not on the resume path.None crossed by the resume picks; the code-metrics gate passes against the release base for them. The sweep teardown commit grows
run_gridas #1606 did on main and carriesbaseline-raise(see its section). With it the PR changes 1146 production Python lines, over the 1000-line diff budget (the resume picks alone were under it); the PR carriessize-exceptionbecause the sweep fix was asked to ship in this release PR rather than a separate one, and it is a separate commit that can be reviewed on its own (git show d09c9e1df). The production diff is about 600 lines for #1859-#1871 because it carries #1859 whole, which the other two fixes depend on. #1879 puts its new logic in a new module (actions/executors/source_tree_identity.py) andTaskRegistry.find, sowriteback.pyandintegrate_patch.pydo not grow.Not a refactor. Main-only refactors (#1840's
_inspectsplit,_drain_for_transition) were deliberately left out.On v1.1.4 an operator can confirm a vanished container's scope with
hyperloom recover --confirm-scope-goneand resume the session, including on the same node after a container restart. A session resumed in SWEEP measures its sweep instead of idling until the grant runs out. A session whose container was lost during an integrate resumes once its tasks are confirmed stopped; the resume report carriesrolled_back_pending_integratewithtree_replaced: trueand, when there was one,unrestored_stash_oid(the operator's uncommitted changes in the lost checkout are not recovered, and the report says so). An environment-only candidate whose KEEP reached the task row is promoted on resume instead of dropped. Before, such sessions could not be resumed, or resumed and then stalled or lost an accepted candidate.In every sweep, explore and baseline round, the server an AgentX client booted is now stopped when the round ends, however it ended (measured, failed, timed out at the cap, cancelled, warm-up stop). The teardown reads
agentx_server.pidunder the round's own workspace, where$RESULT_DIRpoints by default; a round whoseparams.result_diroverride points outside its workspace is not covered, as on main; the session-wide orphan reap still finds it when that directory lies under the session'sruns/, as the failure-recovery reference recommends. Before, a round killed at its cap or cancelled left that server holding the port and nearly all GPU memory: later rungs measured against the leftover server or failed at port bind or engine start, and the stop marker still said "tree reaped". Operators may now see aserver_survived_teardown:<pid>warning in a round'snonfatal_warningsand the stop marker when a server outlives SIGKILL; the orphan reaper waits up to 30 s to confirm a SIGKILL and keeps a survivor's pidfile for the next reap.no. New leases record a three-part owner scope; two-part scopes written by earlier versions are still recognised. New integrate sentinels carry one extra field (
recovery.tree_instance), which older code ignores.no: two concerns, both approved for this release. Resume on release/v1.1.4, and the round server teardown (partial backport of #1606, its own commits
d09c9e1dfand384eb89d1, reviewable on their own).No, the root cause is in Hyperloom.
🤖 Generated with Claude Code