Repository navigation
fix(native): give storage proofs their own budget outside the start phase - #375
Merged
Merged
Conversation
…hase Native storage carriers took their deadline from HACK_COMPOSE_STARTUP_TIMEOUT_MS, and guard storage proofs consumed the start deadline of up/restart. On loaded hosted runners storage preparation alone exceeded a 15 s start budget (native-config-down-hooks) and a dozen build/spawn guard proofs exhausted the 90 s start budget (native-config-build). - HACK_NATIVE_STORAGE_TIMEOUT_MS (default 90 s, 1-600 s) bounds each storage carrier; it is never derived from the start budget. - Guard storage proofs during native up/restart extend the start deadline by their monotonic elapsed time instead of consuming it. - A refusal that finds its carrier budget exhausted reports the closed effect reason storage-budget, and the summary names the variable. - Unrecorded command group absence uses a monotonic deadline; a fake-clock test pins its probe-budget refusal (kills mutation M6). - Network acceptance: named 75-minute whole-run budget that gates command starts only, so it never kills an in-flight carrier command. - Build/down-hooks scenarios get measured command budgets and log closed per-command storage timings. Refs HACK-1324, HACK-1320, HACK-1321 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The slowest of 42 hosted passes (2113-3343 s) used 93% of the old 60-minute budget; 80 minutes keeps 1.44x that pass inside the 150-minute shard job. Refs HACK-1320 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR's own hosted run passed in 3494 s, 97% of the old 60-minute budget and 73% of the 80-minute one. 90 minutes keeps 1.55x the slowest of 44 measured passes and stays inside the 150-minute shard job. Refs HACK-1320 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uard exclusion Guard storage proofs extend the up/restart start deadline instead of consuming it, but their sum had no bound. The excluded total is now capped at 16 x HACK_NATIVE_STORAGE_TIMEOUT_MS or 1 hour, whichever is smaller: the deadline moves at most that far, a proof that would start past the cap is refused before it runs, and a proof that ends past it refuses the phase with a closed, value-free error whose effect reason is storage-budget. Add a command-level failure-injection test for the start.excludeStorage wiring. The Docker stand-in reads the CLI's own phase trace and delays the carrier dependency check only while a guard.storage span is open, so the guard proof outlasts a 3 s start budget. It fails with the wrap removed. Add unit tests for the cap, each failing under mutation, and document the per-proof bound, the phase cap and how the two variables interact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 22, 2026 13:34
This was referenced Oct 22, 2026
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…ough it, and support adopted projects (#392) ## Summary Two `hack doctor` fixes. - **HACK-1316:** doctor no longer follows a symlinked `.hack` or `.hack/.internal`. Before this fix, `doctor --fix` deleted a file outside the project. - **HACK-1311:** doctor now reports an adopted (`hack config adopt`) project correctly, and `doctor --fix` completes in one. Before this fix, doctor refused the project as native input, the `--fix` plan threw, and `--fix` with tracked generated files failed after it had already changed the Git index. ## Rework after review (commit `ff7f11e1`) The verifier found one blocking defect in the first version (`ffbc4ffd`): - **Defect.** In an active adopted project whose generated files are tracked by Git (for example a committed `.hack.secret.key`), `hack doctor --fix --no-interactive` exited 1 with `E_NATIVE_PROJECT_UNSUPPORTED`. It did this after `git rm --cached` had already run, so `.hack/.gitignore` was not written and the later fix steps (hackd, Docker networks, Caddy and the rest) never ran. - **Root cause.** `maybeUntrackGeneratedFiles` selected the adopted project, ran `git rm --cached`, and then called `ensureHackDirGitignore`. That writer calls `assertLegacyProjectDirectory`, which throws `ProjectInputSelectionError("native")` for an active adoption (`src/lib/project-input-selection.ts`). - **Fix.** For an adopted project, the step now changes only the Git index. It still adds a committed `.hack.secret.key` to the root `.gitignore`, which is not an adoption input. It leaves the adoption's `.hack/.gitignore` unchanged and says so in a closed note: `Removed N path(s) from the git index. Left .hack/.gitignore unchanged: an adopted Compose project's .hack/ belongs to 'hack config adopt'.` The plan line names the same change. Legacy projects behave exactly as before. - **Docs.** `docs/reference/native-compose-adoption.md` now matches the code: `--fix` leaves `.hack/.gitignore` unchanged and adds a committed key to the root `.gitignore`. - **Test gaps closed.** - The `findDoctorLegacyProjectContext` guards on the later `--fix` steps had no regression test. The new test runs the whole `--fix` flow with Docker reachable and `--migrate-env- config`, so the lifecycle, incomplete-runtime and env-migration steps run against the adopted project. - The writer's refusal before `mkdir` (HACK-1316) had no test. A new unit test covers it. ## Update: merge with `next` at `5c9cc82e` (commits `0d6aa587`, `51244fc9`) - `0d6aa587` merges `origin/next` at `5c9cc82e` (#375, #407, #412) with no conflicts. #407 changes the same `.hack/.gitignore` code in doctor: the repair helper now returns `{ changed, readOnly }` and the untrack step can leave a read-only file alone. After the merge, the adopted branch of the untrack step still skips the `.hack/.gitignore` writer, and only the legacy branch uses #407's read-only handling. - `51244fc9` is a one-line docs fix. `docs/reference/native-compose-adoption.md` listed every check that doctor skips for an adopted project except the `.hack/.gitignore` check. The code already skips it. - **The verifier's repro, rerun on the merged head.** It is the verifier's own scratch test: `retainedSourceBindFixture`, a real `git init`, `git add -f .hack.secret.key`, adopt (prepare and publish), then `runCli doctor --fix --no-interactive`, with shell `run` mocked. The output has no `E_NATIVE_PROJECT_UNSUPPORTED`. `git ls-files` is empty, `.hack/.gitignore` is not created, the closed note above is printed, the adoption is still `active`, and the later steps run (the global `daemon start` call and the closing `Re-run: hack doctor` note). - **Why that run still exits 1.** Doctor's exit code comes from the checks that run before `--fix`. This is existing behavior and is the same for legacy projects. The verifier's harness makes every non-Git `exec` fail, so `docker daemon` is an error, and the committed key is an error in the `generated files` and `env key` checks. The PR's `doctor-fix-adopted-project` test makes Docker reachable. Its `.hack/.env.state.json` case exits 0. Its key case exits 1 only because of the committed-key error. - **Mutations rerun on the merged head**, each killing its test for the intended reason: | Mutation | Result | | --- | --- | | Adopted untrack step writes `.hack/.gitignore` again (`ffbc4ffd` behavior) | `doctor-fix- adopted-project` 0/2, with `E_NATIVE_PROJECT_UNSUPPORTED` in the output | | `findDoctorLegacyProjectContext` no longer skips an adopted project (survived the first review) | `doctor-fix-adopted-project` 0/2, with `E_NATIVE_PROJECT_UNSUPPORTED` | | Writer's pre-`mkdir` refusal removed (survived the first review) | `compose-env-at-rest` 15/16; "the writer refuses a symlinked .hack before creating .internal behind it" fails | ## Root cause ### HACK-1316: symlinked `.hack/.internal` `src/lib/compose-env-at-rest.ts` checked that `.hack/.internal` was a real directory, but only inside `readReplaceablePrivateFile`, and it never checked `.hack`. When that read refused, `inspectEntry` called `lstat(path)` to choose between `absent` and `unverifiable`. That `lstat` followed the `.internal` link, found the outside `compose.env.override.yml`, and classified it as `unverifiable`. With `includeUnverifiable`, the `--fix` removal then ran `lstat`, `rename` and `unlink` through the same link, which deleted the outside file. The stale-aside sweep ran `readdir` on the linked directory before any symlink check, so it could also unlink `.remove`-shaped files in the outside directory. ### HACK-1311: adopted projects An active or pending adoption makes `inspectProjectInputsAtRoot` classify the root as `native`. Doctor's first step, `assertLegacyProjectDiscovery`, therefore threw `E_NATIVE_PROJECT_UNSUPPORTED`. Every project check and `buildDoctorRemediationPlanLines` call `findProjectContext`, which throws the same refusal. Every `--fix` step that writes legacy project files does too. ## Fix ### HACK-1316 (`src/lib/compose-env-at-rest.ts`) - One no-follow ownership check now covers both `.hack` and `.hack/.internal`. The writer and every read, removal and sweep use it. - A new `refused` state covers these cases: - a symlinked or non-directory `.hack` or `.hack/.internal`; - a symlinked or non-regular override. Doctor reports it with a closed, value-free message. A `refused` override is never eligible for removal. - Removal returns before the sweep when the directories are not real. It checks the directory identities again before the move and before each aside unlink. The aside listing runs only on the held directory. - The writer refuses a linked `.hack` or `.internal` before `mkdir`, so it creates nothing through the link. ### HACK-1311 (`src/commands/doctor.ts`, `src/lib/doctor-adopted-project.ts`) - Doctor selects an active or pending adoption first, with `selectLegacyComposeAdoptedRoot`, the selector that adopted `ps`, `logs` and the other adopted commands use. The legacy-discovery refusal runs only when no adoption is found. - **Checks for an adopted project:** - The `project` check names the adopted root. A pending adoption is a warning that asks for explicit recovery. - The env key, tracked generated files and Compose env at rest checks run against the adopted root. - Each check that reads adoption-owned inputs is listed as not checked. - Agent integrations use the adopted root, and sessions mux uses global settings. - **`--fix` for an adopted project:** - The plan says it leaves the adoption's inputs, retained containers and state alone, and it skips env migration. - Lifecycle, incomplete-runtime and env-migration repairs treat the project as having no legacy project. - Untracking generated files changes only the Git index (see the rework above). - The legacy project-check block moved unchanged into `runProjectChecks`, to stay under the complexity limit. The #365 compose backend features check and the #357 env-at-rest adopted fallback are unchanged. There are no CLI surface changes and no agent-guidance changes. ## Tests | Test | Proves | Mutation that kills it | | --- | --- | --- | | `doctor-fix-adopted-project` (new), 2 cases: a committed `.hack.secret.key` and a committed `.hack/.env.state.json` | Real `runCli doctor --fix --no-interactive --migrate-env-config` in an adopted fixture with a real `git init`. Docker is reachable through the mock, and Git runs only on the fixture with no global or system config. Each case checks:<ul><li>no `E_NATIVE_PROJECT` and no canary or key text;</li><li>the exact plan line and step note;</li><li>the closing notes, which prove every later step ran;</li><li>`git ls-files` is empty and the file is unchanged on disk;</li><li>the root `.gitignore` gets the key entry only in the key case, and `.hack/.gitignore` is not created;</li><li>the receipt is unchanged, the adoption is still active, and no stub engine command or project `run` call happens.</li></ul>Exit code: 0 for the env state file. For the key it is 1, because the `env key` check reports the committed key as an error before `--fix` runs. That is existing doctor behavior. | <ul><li>`ffbc4ffd` behavior (always write `.hack/.gitignore`): 0/2 pass, exit 1.</li><li>`findDoctorLegacyProjectContext` no longer skipping adopted projects: 0/2 pass, exit 1.</li><li>Plan line reverted to the legacy wording: 0/2 pass.</li></ul> | | `compose-env-at-rest`: "the writer refuses a symlinked .hack before creating .internal behind it" (new) | A link to an outside directory without `.internal` gets nothing created behind it, and the refusal is closed. | Writer's pre-`mkdir` refusal removed: fails (this mutation survived before). | | `doctor-fix-noninteractive`: "doctor --fix leaves a file behind a symlinked .hack/.internal untouched (HACK-1316)" | Real `runCli doctor --fix`: the outside file survives, and the plan and notes do not name it. | `origin/next`'s helper: fails with ENOENT, because the outside file was deleted. | | `compose-env-at-rest`: symlinked `.hack` and `.hack/.internal` (2 cases) | `refused` state, closed message, no repair planned, no asides removed with a stale-aged clock, writer refusal, outside contents byte-identical. | Old helper: both fail. Directory list without `.hack`: the `.hack` case fails. `inspectEntry` always `unverifiable`: the symlinked-override case fails. | | `doctor-adopted-project`: `doctor --json` and the `--fix` plan in an adopted project | Closed project message, the checks that run, the not-applicable list, the exact plan, the receipt unchanged, no container start or stop. | Adopted branch of `runProjectChecks` disabled: fails. Preflight forced back to legacy discovery: fails. | All tests use temporary directories only. They do not touch the real HOME, `~/.hack`, the Docker socket, the keychain or launchd. ## Evidence - **Toolchain.** Bun 1.4.2 (`mise x bun@1.4.2`), `bun install --frozen-lockfile`. `bun.lock` is unchanged. - **Local gates on the current head `51244fc9` (M5):** - `bun run --cwd packages/cli typecheck`: 0 errors. - Root `bun run check`: exit 0, including `privacy-check: ok` (34 existing warnings in other files). - Biome on the 7 changed source and test files: clean. - gitleaks 8.30.1: `gitleaks dir` over a `git log -p origin/next..HEAD` export: no leaks. - **Focused tests at `51244fc9`, each file run on its own with `--timeout 120000` (the M5 was under heavy load), all passing:** - `compose-env-at-rest` 16, `doctor-fix-noninteractive` 10, `doctor-adopted-project` 2, `doctor-fix-adopted-project` 2 - `native-compose-backend-features` 13, `native-compose-backend-features-cli` 15, `compose- env-at-rest-cli` 9 - `doctor-command` 28, `doctor-json` 3, `doctor-generated-files` 11, `doctor-env-key` 31, `doctor-utils` 8, `doctor-worktree` 6 - From #407 and #412, which touch the same code: `hack-gitignore` 14, `project-env-config` 58, `project-env-hydrate` 18, `compose-env-shared-references` 6 - The five doctor `--fix` and at-rest files also pass together in one process (41/41), which checks that the scoped module mocks do not interfere. - Under the same load, `doctor-fix-adopted-project` with Bun's default 5 s timeout failed in a fixture hook. It passes with the longer timeout. This is load, not a code change. - **Full suite on the M3: not run.** The runner for `91dae070` waited about 6.5 hours and never got the shared CI lock. I stopped it and queued a runner for `51244fc9`, which waited about 50 minutes. During that time the lock directory had existed since 12:37 with no suite process running on the M3, several other units were also waiting, and free space was about 57.6 GiB, under the 58 GiB floor. I did not touch the lock, since I could not prove who owned it. I stopped my runner. It created no worktree or ref. - **Hosted CI:** all 16 checks pass on the current head `51244fc9` (run 39839971608): the full- suite `test` job (39 min), all four `docker-e2e` shards, candidate core, config compiler, runtime state models, and secret scan. Earlier heads `0a94d881`, `a2470049` and `91dae070` also passed. ## Remaining gaps - **No M3 full suite.** It did not run because the shared lock was blocked and free space was under the floor (see Evidence). The hosted full-suite `test` job ran at `51244fc9` on ubuntu, not on macOS. - **Symlinked `.hack` now refused by the writer.** A Compose command that renders the env override now refuses a symlinked `.hack`, not only a symlinked `.internal`. This matches the existing `.hack/.env` rule. A project that relied on writing through a linked `.hack` will see a closed refusal. - **Small race window remains.** Directory identities are checked again before each move and unlink, not held as open descriptors. A same-user swap in that window remains, as already documented for the move-aside window. - **Other generated files can be committed again.** For an adopted project, `--fix` does not add the untracked generated files under `.hack/` to any ignore file. Only the key gets a root `.gitignore` entry. The note asks the user to keep them out of later commits. - **`--fix` with a committed key exits 1.** The exit code reflects the checks that ran before the fix, for legacy and adopted projects alike. The fix itself completes and removes the key from the Git index, which is what the tracked-key checks read. - **Pending adoption not tested.** The pending-adoption warning has no test: the fixture has no cheap pending state. - **Native backend skips adopted project checks.** With `HACK_RUNTIME_BACKEND=native`, native doctor still skips project checks for an adopted project. This is unchanged. Refs HACK-1316 Refs HACK-1311 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 24, 2026
…d exclude only carrier proofs (#421) ## Summary Fix unit G50: storage-budget diagnostics follow-ups after #375 (HACK-1361). ### (1) A policy refusal caught after the carrier deadline was reported as `storage-budget` Root cause, in `src/lib/native-compose-storage-witness-docker.ts`: - `nativeComposeStorageCarrierBudgetReason` returned `storage-budget` for any refusal once the carrier deadline had passed, without looking at the error's own observed reason. - Guard proofs run with original command records, so every helper command goes through `runNativeComposeStorageDockerCommand` with `originalCommand`. When its admission check (`assertAdmitted`, which re-inspects the helper through `checkNativeComposeStorageDockerCarrier`) refuses with a helper policy reason, the runner replaces that error with its fixed command refusal (`refuseCommand`). The replacement dropped the issued `storage-helper-policy` / `helper-*` boundary. The outer `invoke` catch then labelled it `storage-helper-start` with `storage-budget` if the deadline had passed (and `unclassified` otherwise). The human summary then told the operator to raise `HACK_NATIVE_STORAGE_TIMEOUT_MS` for a policy failure. Fix: - `nativeComposeStorageCarrierBudgetReason` now takes the error's issued reason (registry boundary, or the classified command cause) first. It reports `storage-budget` only for a deadline-shaped reason (`probe-timeout`, `probe-budget`, `storage-budget`, or no observed cause) when the deadline has expired and the carrier was not cancelled. Any other observed reason, such as `helper-host-policy`, is kept. - `refuseCommand` takes an optional source and copies its issued boundary onto the replacement refusal (first attach still wins). The runner keeps the first publication-stopping error (`publicationSource`) for the bounded-await and child-publication paths, and the outer `originalCommand` catch passes the caught error. Diagnostics only: no check, deadline, retention or recovery rule changes. ### (2) `start.excludeStorage` wrapped all of `storage.verify` The exclusion covered `assertFresh` (twice) and both `store.loadCurrent` reads as well as the carrier proofs. That non-carrier time also extended the start deadline, and the per-carrier bound does not limit it. I narrowed it: `NativeComposeCommandStorageVerifyOptions.excludeProof` now wraps only each enrolled volume's carrier proof (fresh carrier creation plus `verifyNativeComposeStorageXattrWitness`). `executePreparedGeneration` passes `start.excludeStorage` through it. The cap entry and exit checks now run per carrier proof, which matches the documented "a proof that would start at or past the cap is refused before it runs". ### (3) Comment precision The `NATIVE_STORAGE_EXCLUSION_MULTIPLE` comment no longer calls the uncapped sum "unbounded in practice". It now says the sum is finite, at most (5B+3) x V x S, and can reach hours. For example, 4 builds and 3 volumes allow 69 carriers: about 1.7 h at the 90 s default and 11.5 h at the 600 s maximum. The `createNativeComposeStartDeadline` doc and the reference docs say which span is excluded. ## Tests (each proven by mutation) - `tests/native-compose-storage-witness-docker.test.ts`: "a helper policy refusal caught after its carrier deadline keeps its policy reason". It uses a real `checkNativeComposeStorageDockerCarrier` host-policy refusal and a fake clock past the deadline. Mutation: drop the deadline-shaped precondition (the old behavior) -> `storage-budget` received, test fails. - `tests/native-compose-storage-command-record.test.ts`: "a helper policy refusal {before arming, after arming, after the stopped child is published} keeps its own reason after the carrier deadline". It uses the real recorded-command runner with a temp private directory, a child that is never resumed (marker absent), and a fake clock for the reason. Mutations: - outer catch without source -> all 3 fail; - settlement `refuseCommand` without `publicationSource` -> the child-published case fails; - `refuseCommand` without the copy -> all 3 fail. - Narrowed exclusion: the existing wiring test `tests/native-compose-start-storage-exclusion.test.ts` passes (about 103 s on this loaded Mac). Mutations, each killed (`up` exits 1, workload not admitted): - `excludeProof: start.excludeStorage` removed from the guard; - `verify` ignoring `options.excludeProof`. ## Evidence - Bun 1.4.2 (`mise x bun@1.4.2`), `bun install --frozen-lockfile`; no `bun.lock` change. - `bun run --cwd packages/cli typecheck`, root `bun run typecheck`, root `bun run check` (privacy-check ok): 0 errors. Biome on the changed files reports only 3 existing complexity warnings with unchanged scores (checked against the `origin/next` copy). - gitleaks 8.30.1 (`gitleaks dir` over the `git log -p origin/next..HEAD` export): no leaks. - Focused local run: 7 files, 282 pass and 1 timeout-only failure under load. The files are: storage command record, docker witness, docker IO, xattr owner, startup budget, effect diagnostics and build. The failure was `runEffect captures the original fresh carrier factory ...`, which hit the 5 s default timeout. It passes serially in 3.7 s with `--timeout 60000`. `tests/agent-instruction-source.test.ts` (reads the changed docs) passes. - M3 full suite at 9301b86: pending: queued behind the shared M3 CI lock (several units ahead); this section will be updated with the result. ## Gaps - The narrowed exclusion is proven by the command-level wiring test and two mutations. I did not add a unit test that pins `assertFresh` or the saved-state reads outside the excluded span. A full proof through `prepareNativeComposeCommandStorage` needs a real witness. - An untagged policy-shaped mismatch (for example a plain `refuse()` when a re-inspected carrier's `createdAt` or policy string changed) still has no observed reason. Past the deadline it is still reported as `storage-budget`. Tagging those checks is a separate change (see findings). - Hosted CI and native e2e scenarios are not run by this report. M3 is not a qualifying host for them. Refs HACK-1361 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix unit G30: native e2e and storage budgets (HACK-1324, HACK-1320, HACK-1321).
HACK-1324: storage preparation gets its own named budget
Root cause, confirmed on three hosted first attempts (#365 twice, #370):
(
Date.now() + resolveComposeStartupTimeoutMs()insrc/lib/native-compose-command-storage.ts).native-config-down-hooksrunswith
HACK_COMPOSE_STARTUP_TIMEOUT_MS=15000, so its first storage carrier had15 s for the dependency check, target observation and helper
create/policy/start. On loaded Blacksmith runners preparation alone takes
longer, and the scenario failed at 18.3-18.5 s with
storage-target,storage-helper-*,probe-timeout/probe-cancel/unclassified.native-config-build, about a dozen full storage proofs run in the buildand pre-spawn guards (
fresh()inbuildNativeComposeImages, thenrunNativeComposeOwnedFileChild). They consumed the 90 s start deadline, sothe spawn admission refused (
compose-child/private-uncertain, run39676161586) or the outcome was
E_STARTUP_INCOMPLETE(run 39663510147).Changes:
HACK_NATIVE_STORAGE_TIMEOUT_MS(resolveNativeStorageTimeoutMs, default90 s, range 1,000-600,000 ms) bounds each storage carrier. It is never derived
from the start budget. Default behavior on an unset host is unchanged (the old
start default was also 90 s).
createNativeComposeStartDeadline: guard storage proofs during a nativeup/restartare excluded from the start budget; their monotonic elapsed timeextends the start deadline. Overlaps count once, rejected proofs still count,
the deadline never moves earlier, and each excluded proof stays bounded by its
own carrier budget. Build and spawn admission read the live deadline.
up/restartstart deadline may not exceed 16 xHACK_NATIVE_STORAGE_TIMEOUT_MSor 1 hour, whichever is smaller (24 min at the 90 s default; 1 h from S = 225 s
up). The deadline moves at most that far; a guard proof that would start past
the cap is refused before it runs; one that ends past it refuses the phase with
a closed, value-free
NativeStorageTotalBudgetError(effect reasonstorage-budget, stageguard-storage). The phase ends within the startbudget + the cap + at most one in-flight guard proof.
storage-budget: a refusal that finds its carrier's owndeadline passed (not cancelled) names the budget; the human summary names
HACK_NATIVE_STORAGE_TIMEOUT_MS.Both scenarios now log per-command duration plus closed storage phase timings
from the passive phase trace, so hosted runs record measured preparation
durations.
No check, ownership fence, retention or recovery rule changed; per-probe caps
(10-15 s) are unchanged.
HACK-1320: networks whole-run budget
NATIVE_NETWORK_FIXTURE_SCENARIO_TIMEOUT= 90 min. 44 measured hostedpasses took 2113-3494 s (median ~2650 s); this PR's own run hit 3494 s, 97% of
the old 60-minute budget. 90 min keeps 1.55x the slowest pass; with cleanup and
two command bounds the hard bound stays inside the 150-minute shard job.
command keeps its full 180 s. Run 39431629301 failed because the remaining
whole-run budget clipped and SIGKILLed an in-flight
downcarrier command,leaving a created helper that saved
down --recovercorrectly refuses torepair.
HACK-1321: M6 and monotonic group absence
(
performance.now), namedUNRECORDED_GROUP_BUDGET_MS(3 s).probe-budgetrefusal on the no-record path.Release note
HACK_COMPOSE_STARTUP_TIMEOUT_MSin eitherdirection (a short start budget no longer shortens them, a long one no longer
lengthens them). Hosts that set
HACK_COMPOSE_STARTUP_TIMEOUT_MSshould alsoreview
HACK_NATIVE_STORAGE_TIMEOUT_MS, which now sets both the per-carrierbound and the whole-phase storage cap.
Evidence
Rework at 7daca0d (verifier gaps)
tests/native-compose-start-storage-exclusion.test.tsruns thereal source CLI
upwith a 3 s start budget. The Docker stand-in reads theCLI's own phase trace (stderr written to a file, so it is on disk before the
carrier spawns Docker) and delays the carrier dependency check (
image inspect) by 4 s only while aguard.storagespan is open. No call counting.About 43 s on a loaded Mac (the earlier prototype took about 80 s). Mutation:
with
start.excludeStorage(...)removed around the guard'sstorage.verify,the test fails (
upexits 1, the workload is never admitted); restored, itpasses.
tests/compose-startup-budget.test.ts(fake clocks). Each of fourmutations fails them: no post-proof check, no entry check, no
minon thedeadline extension, and the cap error not classified as
storage-budget.docs/cli.md,docs/reference/native-compose-commands.mdanddocs/reference/native-compose-storage-xattr-carrier.mddescribe the per-proofbound, the phase cap and how the two variables interact.
bun run check(privacy-check ok), biomeon the changed files (only an existing complexity warning in
native-compose-command.ts), gitleaks 8.30.1 overorigin/next..HEAD(no leaks).(
repository catalog > the checked-in .hack/tasks.json recipes are all reviewed). It passes locally at the same head and in hostedtest, and thisPR does not touch
.hack/or the task catalog. I could not rerun it on M3 nextbecause SSH to M3 failed (local agent signing error).
Measured storage durations (hosted shard 4, this branch, 9 attempts)
From the new per-command phase-trace lines:
storage.enroll, firstup)native-config-buildfirstupOld behavior on the slowest attempt: the 145 s of guard proofs alone exceed the
90 s start budget, and the 35 s cold enrollment exceeds the 15 s budget the
down-hooks scenario gave every carrier. New budgets: 90 s per carrier (2.6x the
slowest observed carrier work, 7.7x the slowest proof); build hack commands
300 s (1.7x the slowest
up); down-hooks hack commands 240 s.Hosted CI on this branch (11 attempts across 0af0fb6, 94ce188, cee65eb, 4e2b056)
The merged head 4e2b056 (next a25160d merged in) passed both of its full
runs (run 39721132414 attempts 1 and 2). Before this change, shard 4 failed on
first attempts of #365, #370, #374 and on next itself (39663080763,
39670689406).
Other attempt failures, all unrelated to these files:
tests/native-authored-project-start.test.tsfrontend cascade (also seen on 6+other branches and next),
tests/private-env-file.test.tssymlinked.envENOENT, and one runtime-core Rust test
(
rebind::tests::healthy_fixed_selection_is_noop_and_complete_shared_slot_refreshes_together).Gate vs PR shard-4 comparison
Same runner pool and labels (
blacksmith-4vcpu-ubuntu-2404, runner 2.337.0),identical shard composition and order, no leftover volumes before
native-config-build(end-of-job cleanup volumes are the failed scenarios'own retained fixtures). The failing jobs (including next push run
39663080763) ran unrelated earlier scenarios 1.3-1.9x slower, so the
difference is runner speed, not composition or state. The logs print no
docker infostorage-driver lines, so that dimension could not be compared.Mutation proofs (each killed by a focused test)
M6 (
groupAbsentdeadline -> plainrefuse()), M6b (wall-clock deadline),carrier deadline from the start budget, no start-deadline exclusion,
nested exclusion double-count, build deadline read once, carrier reason ignores
budget, budget reported over cancellation, xattr lifetime unmarked, effect
reason ignores mark, summary not naming the variable, networks command clipped
to the whole-run remainder.
Local and M3 gates
bun run typecheckandbun run check: 0 errors at 4e2b056.origin/next..HEAD: no leaks.10 of the 12 pass when their files rerun serially; the other 2
(
tests/native-authored-project-recovery.test.tsno-host death/recovery)fail the same way on plain next a25160d. None touch the changed code.
e2e proof.
Gaps
cannot isolate it, because at S = 1 s the per-carrier budget fires first.
within its own per-carrier bound, then the phase refuses.
bound; that genuine hang still leaves storage evidence for explicit recovery.
🤖 Generated with Claude Code