Skip to content

fix(native): keep interrupted restarts recoverable under concurrent ps - #645

Open
roodboi wants to merge 5 commits into
nextfrom
claude/hack-1954-restart-recovery
Open

roodboi wants to merge 5 commits into
nextfrom
claude/hack-1954-restart-recovery

Conversation

@roodboi

@roodboi roodboi commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A native hack up or hack restart could fail with provider_busy while hack ps or hack logs polled the project. A failed restore could then leave a state that no public command recovered from. This PR fixes both.

Root cause (reproduced with a synthetic project)

hack ps and hack logs run graph inspect, which holds the exclusive provider lease for a short time. Several mutating steps took that lease without waiting:

  1. Retained-start eligibility (graph retained-preflight) failed up before anything started.
  2. Restore admission (restore_inputs) failed the foreground restore of a retained run.
  3. The restore's pre-admission cleanup in serve_input then also took the lease without waiting. On provider_busy it returned early, so the process exited with its foreground publication (owner.json, control.sock) still bound.

After step 3, up refused with graph_retained_startup because the publication was not retired. down reported the project stopped, since the receipt was stopped-data-retained. doctor --native-cleanup inspect found nothing pending, and down --recover was refused for native image projects. A trace of the failing process confirmed this exact chain.

The repro: a stopped retained run, then hack up while a lock holder keeps the provider lease (standing in for a polling ps). On 5.0.0-next.11 this leaves exactly the stuck state described above.

Changes

Wait for the lease with a bound. Only acquisition is retried. Nothing is admitted while a step waits, and when the bound expires the step returns the usual provider_busy refusal.

  • Retained-start eligibility waits up to 5 s (Lock::acquire_existing_waiting), the same as runtime startup.
  • Restore admission waits within its restore budget and stops waiting when the restore is canceled (Engine::connect_until), the same as graph startup.
  • The foreground's pre-admission cleanup uses connect_cleanup_wait.
  • Owned cleanup and recovery (connect_cleanup_wait) now wait up to 30 s instead of 5 s. A foreground owner that gives up on its cleanup strands its publication and services.
  • Inspections inside up, restart, down and recovery retry busy for 15 s. ps and logs keep their 2 s budget and still report a busy runtime.

The refusal names the next step. When a foreground publication remains, retained startup now reports one of these:

  • graph_foreground_owner_running: stop the owner with Ctrl-C or hack down.
  • graph_foreground_owner_exited: the owner exited while its services were running; run hack down --recover.
  • graph_foreground_owner_exited_unadmitted: the owner exited before its restore was admitted. This is reported, but nothing retires it (see Gaps).

The classification grants no authority.

hack down --recover works for native image projects.

  • A new read-only command, graph foreground-owner --run-id RUN --json, returns the receipt phase, the SHA-256 of the exact receipt and the publication state. It takes no lease and contacts no guest.
  • For an exited owner of a ready graph, down --recover calls the existing same-boot graph recover-live-owner --expect-receipt <sha>. That command revalidates owner death, endpoints and inventories, stops the services with data retained and retires the publication. Down hooks run as for a normal down.
  • If a down --recover was interrupted after the runtime committed its cleanup intent, graph foreground-owner also returns live_owner_recovery: the original receipt digest of that unfinished recovery. A retry resumes the recovery by that digest, then takes the stopped path (host processes, owed journaled hooks, finalization) without replaying a stop.
  • A running, uncertain or unadmitted owner (one with no recovery intent) is refused before any hook or stop.
  • If the frontend (hack up or hack restart) that owned the run also exited, down --recover prints the exact hack restart --recover-frontend --expect-finalization-attempt <attempt> step and does not wait 150 s for a dead frontend.
  • Plain hack down now names hack down --recover when the foreground owner cannot be reached.
  • Frontend recovery now accepts a project with no lifecycle state file, which is how the lifecycle runtime already reads it. Before this, recovery refused every project that had never recorded lifecycle processes.

Validation

Native, on this host. These runs used a candidate built from this branch (debug hack-native with the bundle's features, plus this CLI) and synthetic single-service projects, each in its own private native home:

Scenario 5.0.0-next.11 This branch
Lease held from service start, across the restore provider_busy, publication left, up refuses graph_retained_startup restore waits, reaches ready; Ctrl-C cleanup confirmed; nothing left
Lease held from publication bind, then Ctrl-C cleanup timed out after 5 s; services left running, publication left ready, cleanup confirmed, up again works
Native owner SIGKILLed after ready (frontend survives) up, down and doctor refuse; no recovery up and down name hack down --recover; down --recover stops services and retires the publication
Native owner and frontend both SIGKILLed after ready no recovery down --recover stops services, prints the exact restart --recover-frontend step; that step and then up both reach ready
Owner SIGKILLed before restore admission generic refusal up and down --recover name the state; no supported recovery (gap)

Rust.

  • New tests: bounded acquire_existing_waiting; preflight waits out a brief holder; each remaining-publication state is classified with no state effect; an interrupted recovery is selected by its original receipt until its publication is retired.
  • RUSTUP_TOOLCHAIN=1.97.1 bun run check:local: clean.
  • bun run test:local: 1380 lib tests passed, 0 failed (110 ignored), and every integration target passed. A clippy run for the Linux target (aarch64-unknown-linux-musl, --all-targets -D warnings) is clean; before the cfg gate on acquire_existing_waiting, the same run reproduced CI's dead-code error.

TypeScript.

  • New tests: down --recover selection and refusals; resuming an interrupted recovery by its original receipt; the exact frontend step when the frontend has also exited (real exited child process); down names --recover; inspect busy budgets and abort; frontend recovery with absent and unsafe lifecycle state.
  • bun run --cwd packages/cli typecheck: only the pre-existing tests/fixtures/atomic-write-foreign-owner-worker.ts(39,25) error, which also fails on origin/next.
  • Biome on changed files: clean.
  • Full suite through the bounded runner: two bounded runs. The first run covered 415 of 660 files before the 2400 s watchdog fired: 5704 pass, 43 fail. The second covered the remaining 248 files to completion: 3687 pass, 44 fail. Every failing test also fails on a clean origin/next worktree in this environment. I reran each failing file there. They are in native-compose-* / native-vm-file-*, git-info-exclude, pre-commit-hook, runtime-turbo-inputs, test-config-compiler, native-ca-doctor-cli and bun-version-guard.
  • docs/reference/cli.md regenerated. docs/guides/native-candidate.md documents the recovery step and the lease waits.

Gaps

  • An owner that exits before its restore is admitted (for example SIGKILL during restore startup) still has no supported retirement. Its relay runtime may already have reserved dependency slots and started a relay owner before the publication was bound. Retiring it safely needs a proof that the attempt made no effects. That is a design decision, so this PR reports the state and retains data instead of inventing that authority. The lease fixes remove the lease-contention path into this state.
  • ensure-image still takes the lease without waiting. fix(runtime-core): wait briefly for a held provider lease at native startup #100 left it that way because its refusal may follow an image-cache effect.
  • Not verified with an installed candidate yet. The release-built bundle and installed hack-next check remain for the merge train.
  • The preserved stuck home from the trial is a separate install and was only inspected read-only. Its state matches the reproduced unadmitted-exit case, so this PR does not change it.

Refs HACK-1954

🤖 Generated with Claude Code

A `hack up` or `hack restart` could fail with `provider_busy` while
`hack ps` or `hack logs` polled, because the reads hold the exclusive
provider lease briefly and several mutating steps took it without
waiting. When the restore of a retained run hit that refusal, its
pre-admission cleanup also took the lease without waiting and exited
with the foreground publication still bound. From then on `up` refused
with `graph_retained_startup`, `down` reported the project stopped,
`doctor --native-cleanup inspect` saw nothing pending and
`down --recover` was refused.

Mutating steps now wait out a brief holder within a bound, and only
acquisition is retried:
- retained-start eligibility (5 s), as runtime startup already did;
- restore admission, within its restore budget and cancellation;
- the foreground's pre-admission cleanup and owned cleanup (30 s);
- inspections inside up, restart, down and recovery (15 s). ps and
  logs keep their 2 s budget and still report a busy runtime.

A remaining foreground publication now names the next step instead of
the generic refusal: a running owner (stop it), an owner that exited
while its services ran (`hack down --recover`), or an owner that exited
before its restore was admitted (reported; no supported retirement).

`hack down --recover` now works for native image projects. It selects
the run's exact receipt with the new read-only
`graph foreground-owner --run-id RUN --json` and stops an exited ready
owner's services through the existing same-boot `recover-live-owner`,
retaining data. If that run's frontend also exited, it prints the exact
`hack restart --recover-frontend --expect-finalization-attempt` step.
Frontend recovery now accepts a project that never recorded lifecycle
processes (no lifecycle state file), as the lifecycle runtime does.

Refs HACK-1954

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@linear-code

linear-code Bot commented Nov 7, 2026

Copy link
Copy Markdown

HACK-1954

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T20:20:41.135266Z fa765c2 Manual request
🔒 Security Review ✅ Completed 2026-11-07T20:22:58.408123Z fa765c2 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c672b4e1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/backends/native-project-down.ts Resolved
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 6c672b4e1f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…ck helper

`hack down --recover` refused a retry after an interruption past the
runtime's cleanup intent: the receipt was no longer `ready-observed`,
so the guard reported an owner that exited before its start was
confirmed and stranded the partially stopped graph and its stale
publication. `graph foreground-owner` now also returns
`live_owner_recovery`, the original receipt digest of a same-boot
dead-owner recovery whose intent exists but whose cleanup or publisher
retirement is unfinished. `down --recover` resumes that recovery by its
own selection, then takes the stopped path (host processes, owed
journaled hooks, finalization) without replaying a stop. An exit
before restore admission, with no recovery intent, still refuses.

`Lock::acquire_existing_waiting` is used only by macOS code and is now
gated like its callers, fixing the Linux clippy dead-code error.

Refs HACK-1954

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c46d34e0dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: c46d34e0dd

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

An interrupted `hack down --recover` could stop after both foreground
publisher paths were retired but before `finish_retirement` released the
run's dependency reservation. The resume selection then treated the
recovery as finished because the publication was absent, so a retry took
the already-stopped path and left the reservation behind; the next start
of a run with dependency bindings refused because a reservation for the
same run already existed.

The selection now stays resumable while either retirement effect remains:
the stale publication or the run's dependency reservation record. A retry
therefore re-enters `recover-live-owner`, whose completed-intent path
finishes retirement and releases the reservation.

Refs HACK-1954

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 0d8d9135dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0d8d9135dd

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Brings in #643 and #644, which also touch provider/lifecycle.rs and the
native candidate guide. clippy -D warnings and provider tests pass after
the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: fa765c20dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: fa765c20dc

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

This branch has not been deployed

No deployments
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.

1 participant