Repository navigation
fix(native): keep storage carrier commands alive through host stalls - #356
Merged
Merged
Conversation
native-config-networks fails intermittently with only "Fixture command must exit 0; captures retained", and its private captures are not uploaded, so the failing command and product refusal cannot be identified from hosted logs. Each failing fixture command now logs a fixed diagnostic: command class (hack verb or docker verb/action), checkout role, sequence, expected and actual exit, and the product's closed error code and effect-refusal stage/reason when present. A failed phase also logs a census of exact owned containers (service, storage carrier, fixture) by closed engine state. No argv values, output text or paths are printed. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosted sampling for HACK-1281 only; reverted before review. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The failing-command diagnostic now carries its duration, and a cleanup failure after an original failure logs its fixed assertion sentence (or withholds it) plus a second owned-object census. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosted sampling for HACK-1281 only; reverted before review. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Hosted sampling for HACK-1281 only; reverted before review. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k census A non-canonical project container (for example a Compose recreate temporary) blocks every native command, including down --recover, with an ownership refusal. The failure census now reports such containers apart from canonical services by closed kind only. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native command that failed with a plain error (for example a storage carrier refusal during a witness-bearing exec) mapped to the fixed E_COMPOSE_FAILED text without the closed stage/reason its owner had issued, and a command without --json never printed that boundary at all. Hosted native-config-networks failures therefore could not say which owner refused. The generic E_COMPOSE_FAILED translation now carries detail.native_compose_effect_refusal when the registry holds an issued diagnostic; the code and text are unchanged. Without --json, a coded error prints one more fixed line, "Refusal boundary: <stage> (<reason>); values omitted.", only for a known stage and reason pair read as own data. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… evidence The failing-command diagnostic read a human error code only from stderr as "ERROR E_X:", which no logger backend prints (clack writes "■ E_X: " to stdout), so every failing hack exec reported code null. It now reads the leading closed code and the closed refusal boundary line from either stream, ANSI stripped, for the clack, gum and console formats. In-container probe scripts now exit with closed codes for every failure (rejected or timed-out fetch, unreadable or unwritable marker), so a hack exec that exits 1 is the product, not an uncaught probe error. The failure census also logs each started checkout's saved stopped/pending operation and storage/hook pending flags. No values, text or paths. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosted sampling for HACK-1281 only; reverted before review. A third iteration crossed the 150-minute job budget, so a new iteration starts only within 55 minutes. Shards 1 and 2 add two CPU spinners and an fsync loop. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A storage carrier command refusal collapsed into one plain error, so its issued boundary reason was always "unclassified": a timed-out or cancelled child, an exhausted publication budget or a settlement that outlasted its three-second post-exit budget looked identical to a real identity mismatch. The command runner now keeps a closed cause with the same fixed refusal (probe-timeout, probe-cancel or probe-budget), and every carrier boundary reports it before the generic reason. No check, deadline, retention or recovery rule changes: an over-budget settlement still leaves the original command unsettled, as the new focused test replays with a slow parent sync. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Removes the hosted HACK-1281 sampling from tests/e2e/run.ts (every shard running only native-config-networks, repeated iterations and background CPU/fsync load on shards 1 and 2). Shard selection and a single runScenarios pass are restored exactly as on the merge base. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re evidence A failing `down --recover` that stays incomplete issues no refusal boundary, so its diagnostic printed E_COMPOSE_FAILED with a null stage and reason. A command the runner killed at its budget printed no diagnostic at all. The command diagnostic now adds a closed `message` label for an exact fixed product sentence (stop-incomplete, operation-failed, native-state, -busy, -uncertain, -stale); any other or changed text yields null. A command the runner could not return logs its class, role, sequence, duration and a closed runner outcome (timeout, output-bound, group-unconfirmed, capture, other). No output, argv or path is printed. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosted sampling for HACK-1281 only; reverted before review. Every shard runs only native-config-networks under a host-stall monitor (event-loop lag and a product-shaped `ps` identity read every two seconds; numbers only). Shards 1 and 2 stop the CLI process 1.2 s of every 2 s during the first 20 saved exec commands after the created-recovery phase, replaying a host stall. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A recorded storage carrier command read process identity with a fixed one-second bound and settled within three seconds after exit. A host stall of the CLI longer than that resumes with the timer already due, so a healthy command refused after its daemon effect (probe-timeout or probe-budget). The refusal leaves the carrier intent uncertain: the next startup, exec or restart refuses, and `down --recover` stays incomplete by design. This is the native-config-networks failure signature in HACK-1281 (a product command exits 1, then cleanup also fails, with a created carrier and storage witness work pending). Identity reads now share the enclosing handshake or settlement budget, and the post-exit settlement budget is a finite 15 seconds. No check, record, retention, recovery rule or authority changes. An exhausted budget still refuses as probe-budget; the race also observes the abandoned work so its rejection cannot escape as an unhandled process error. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Reverts fbf5cf6. Hosted results are recorded in the PR: on the pre-fix carrier (run 39643040488) both stalled shards failed with the HACK-1281 signature; on fc4095b (run 39644645624) both stalled shards passed all 20 stalled exec commands and all four shards passed. tests/e2e/run.ts and the scenario's stall hooks are back to their reviewed state. Refs HACK-1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 21, 2026 20:28
This was referenced Oct 21, 2026
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…hecks locally (#381) ## Root cause - **HACK-1290:** `package.json` pins `packageManager: bun@1.4.2`, and `mise.toml`, `.tool- versions`, every workflow and the runtime images use 1.4.2. Nothing checked the Bun that actually ran local gates, though. A Bun ahead of mise's shims on `PATH` (here mise's `latest` 1.4.3) skips the checkout's `mise.toml`. A Bun 1.4.2 macOS SIGSTOP difference made a test pass locally on 1.4.3 and then fail in CI. - **HACK-1294:** the compiled-bundle CLI checks (`scripts/check-config-compiler-cli.ts`, `scripts/check-native-{env,host,routing,endpoint,process,acquisition}-plan-cli.ts`, `scripts/check-native-config-import-cli.ts`) ran only inside CI's macOS "Config compiler" job. No local command ran them, so a behavior change passed every local gate and failed CI. ## Fix - New `scripts/check-bun-version.ts` (`bun run check:bun-version`). It compares `Bun.version` with the exact `packageManager` pin. On a mismatch it exits 1 with the fix: `mise x bun@<pin> -- bun run <script>`. Setting `HACK_ALLOW_BUN_VERSION_MISMATCH=1` (exactly `1`) continues with a warning for a deliberate experiment. - The guard runs first in root `test`, `check` and `typecheck` and in `packages/cli` `test`, `check` and `typecheck`. `turbo.json` passes the override through via `globalPassThroughEnv`, so package tasks honor it. CI already installs 1.4.2, and the `.hack` toolchain image installs from `mise.toml`, so both pass unchanged. - New `bun run check:native-plan-cli` (`scripts/check-native-plan-clis.ts`). It runs the eight compiled-bundle checks one at a time, in CI order, and stops at the first failure. It refuses to start without `dist/hack` and `dist/hack-config-compiler`, and tells you how to build them. - `AGENTS.md` and `.ai/skills/hack-repo-verify/SKILL.md` document the guard, the override and the new command. These are contributor-only files. Consumer guidance (`src/agents/instruction- source.ts`) did not change, so no plugin regeneration or render fingerprint is needed. ## Tests - `tests/bun-version-guard.test.ts` covers four things. First, the match, mismatch and override verdicts (only `"1"` overrides) and the rejection of inexact pins. Second, the real script exits 1 on a mismatch and 0 with the override, run in a temp copy with an isolated `HOME`. Third, `mise.toml`, `.tool-versions` and every literal workflow `bun-version` agree with `packageManager`. Fourth, every entry point starts with the guard, and turbo passes the override through. - `tests/native-plan-cli-wiring.test.ts` fails when any `scripts/check-native-*-plan-cli.ts`, the config-import check or the config-compiler check is missing from the local command or from a CI step. It also checks that the runner refuses to start without the built bundle. - Mutation-proven: each of these 8 mutations made at least one test fail, and the restored code passes 8/8. - Always match. - Treat any truthy override as `1`. - Drop the exit code on a mismatch. - Remove one check from the list. - Remove the guard from root `test`. - Remove the guard from CLI `typecheck`. - Remove the turbo pass-through. - Ignore missing prerequisites. ## Evidence (macOS arm64, local) - Plain `bun` is 1.4.3: - `bun run check:bun-version` exits 1 with the message. - `bun run test` stops before turbo. - `HACK_ALLOW_BUN_VERSION_MISMATCH=1 bun run typecheck --force` warns at the root and inside `@hack/cli:typecheck`, which proves the pass-through, and both tasks succeed. - `mise x bun@1.4.2 -- bun run check:bun-version` exits 0. - `bun run build`, `bun run build:config-compiler` (rustc 1.97.1), then `mise x bun@1.4.2 -- bun run check:native-plan-cli` all pass. All eight checks pass, including acquisition (198 checks) and config import (49 checks). - Bun 1.4.2 gates: - `bun run --cwd packages/cli typecheck`: 0 errors. - `bun run --cwd packages/cli check`: 0 errors (existing warnings only). - `bun run check`: passes. - `bunx ultracite check` on the changed files: clean. - `bun run test` (full suite, Bun 1.4.2, host load average 50 to 130 from parallel agents, 2h07m): the guard passed and turbo ran. 6785 tests passed and 1197 failed across 120 files. All 5 new tests passed in the suite. - I reran each of the 120 failing files serially with `--timeout 60000`. 99 files passed. 21 files still fail, all `native-compose-*` / `native-authored-*` source-CLI fixtures. Most fail with the fixture's inner 15 s `SIGKILL` (exit 137) or `private-uncertain` refusals. - Baseline A/B with `origin/next`'s `package.json`, `packages/cli/package.json` and `turbo.json` restored on the same host gave identical results. `native-compose-warm-up- command`: same exit 137. `native-compose-down-command` plus `native-compose-storage-fence- cost`: 1 pass / 17 fail in both runs. - Direct `bun test` does not run any code this PR changes, so I count these failures as host and environment failures, not as caused by this change. I did not get a fully green local suite on this host. - gitleaks 8.30.1 over `origin/next..HEAD`: no leaks. ## Remaining gaps - Raw `bun test <paths>` and `bun index.ts` do not run the guard. Only the `bun run` entry points do; this is documented. - `check:native-plan-cli` covers the CLI checks only. CI's "Config compiler" job also lints those scripts, runs their transport tests, runs `build:release` and validates the schema corpus in Python. The docs say to lint changed scripts explicitly. - This head (`d9d6376d`) is based on `ec223cdb`, the commit I ran the full suite against. `next` has since gained four unrelated commits (#353, #340, #344, #356). None of them touches these files, and the head merges cleanly (`git merge-tree`). The focused tests also pass after rebasing onto `2f434217`. - Hosted CI on this head has not been observed yet. - Found while testing, not fixed here: `tests/setup-migrate-command.test.ts` resets `root = ""` in `afterEach`. A CLI call that outlives its timed-out test then passes `root: ""` to `privateCliEnv`. That resolves to the checkout, so `isolatedDockerClientEnv` writes `docker- client/config.json` into the repository root. I saw this during the loaded full run. `privateCliEnv` should refuse an empty or relative `root`. Refs HACK-1290 Refs HACK-1294 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…lisher (#382) ## Root cause `private_control_stops_only_matching_publisher_and_preserves_replaced_path` (and three other tests in `packages/runtime-core/tests/stream_relay_contract.rs`) picked a port with `free_loopback_port()`. That helper bound `127.0.0.1:0`, read the port, and dropped the listener before the publisher bound it. Another test running in parallel could take the port in that window. The publisher's `bind()` then failed, it exited **73** (`EX_CANTCREAT`, the stream relay's "frontend could not be bound" code in `guest/stream-relay.c`) without a readiness line, and the helper reported `stage: "readiness EOF"`. That matches CI run 39284084093, job 118164984179. I reproduced it locally on unmodified `next` (ec223cd): 3 of 300 parallel runs of the contract binary failed in this test with `PublisherStartupFailure { stage: "readiness EOF", status: unix_wait_status(18688) }`, which is exit 73. ## Fix The publisher now binds the port itself, so no released port is ever handed to it. - `stream-relay --publish 0` (exactly `"0"`) binds a loopback port that the kernel chooses. It reports that port as `ready <port>\n`, read back with `getsockname` after the listener and the optional control socket are bound. - The change is backwards compatible: - Explicit ports (1-65535) and `--publish-unix` still write the bare `ready\n` line. - `00`, `--publish-unix 0` and relay-mode port `0` are still refused with 64. - The only production caller (`provider/publisher.rs`) passes an explicit port and is unchanged. - The contract is documented in the comment block above `main` in `guest/stream-relay.c`. - Test changes: - Every test that used the released probe now publishes on port 0 (`ephemeral_publisher`) or holds the port it needs for the whole test. - `free_loopback_port()` is removed. - The exit-73 retry loop in `control_startup_refuses_occupied_and_nonprivate_paths_without_retaining_publisher_resources` is removed: its TCP leg now always reaches the control path. - `try_frontend_publisher` reads one bounded readiness line of at most 12 bytes. It accepts `ready\n`, or `ready <canonical nonzero port>\n` for port 0. ## Tests The new test is `ephemeral_publication_reports_its_bound_port_and_explicit_ports_keep_bare_ready`. It checks that: - the exact-`0` form is the only one admitted. Refusals are bounded by a 3 s deadline, so a wrongly admitted publisher fails the test instead of hanging it. - two port-0 publishers report distinct ports, and each port is the publisher's live listener (`AddrInUse` on bind, connect succeeds). - explicit ports keep the bare `ready\n` line. This leg still needs a released probe to choose a concrete port, so it retries at most 3 times, and only on the publisher's confirmed bind refusal (`readiness EOF` with exit 73). Mutations, each run against the contract suite: | Mutation in `stream-relay.c` | Result | | --- | --- | | Port 0 prints the bare `ready` line | killed: 4 tests fail with `readiness marker` | | Reported port is the bound port + 1 | killed: 3 tests fail (AddrInUse/connect asserts) | | Any all-zero string (`00`) selects an ephemeral port | killed: `command was admitted instead of refused` | | Explicit TCP ports print `ready 1` | killed: explicit-port leg fails with `readiness marker` | ## Evidence All on an M-series macOS host under heavy shared load (load average 26-90). - **Repeated parallel runs** of the `stream_relay_contract` test binary (`--test-threads 16`, several processes at once): | Build | Runs | Port-race failures (exit 73 / readiness EOF) | | --- | --- | --- | | base `next` | 100 (4 at a time) + 200 (8 at a time) | 3 | | this branch | 100 (4 at a time) + 400 (8 at a time) | 0 | Every other failure on both builds is the unrelated `EINVAL` in `Relay::connect` described under remaining gaps. - `cargo +1.97.1 fmt --check`: clean. - `clippy --all-targets --all-features -D warnings` (Zig 0.15.2): clean. - `bun run check:local`: clean. - `bun run test:local`: pass. - All-features `cargo test` (CI's command): 1270 passed, 2 failed. The 2 failures were `graph_timeout` in `provider::graph::native::runtime::tests::two_*`, which do not use the relay. All 3 `two_*` tests pass when rerun serially. - `bun run --cwd packages/cli typecheck`: 0 errors. `check`: 0 errors (existing warnings only). - Full `bun run test` with Bun 1.4.2 (`mise x bun@1.4.2`): did not pass on this host. The parallel run hit load average 30-90 and had 1270 timeout and fixture-cascade failures across 118 files. Rerunning each of those files serially with `--timeout 60000` passed 98 of them. 20 files still fail: the `native-compose-*` family plus `native-authored-project-start`, `-recovery` and `-process-incarnation`. Their errors are `Completed-job worktree acceptance refused`, `storage.enroll` stalls of 31 s or more, and fixture-settlement cascades. The TypeScript suite does not read or run either changed file; the only TS mention is the `native- stream-relay` feature-name string in `native-candidate-authored-artifact.test.ts`. Two of these files (`native-compose-runtime-metadata-command`, `native-compose-storage-fence-cost`) fail the same way with the base `next` relay sources restored (1 pass, 6 fail). I am treating these as host and worktree failures, not regressions. Hosted CI is the authority here, and `next` has since landed #356 ("keep storage carrier commands alive through host stalls"). - gitleaks 8.30.1 over `origin/next..HEAD`: no leaks. ## Remaining gaps - Hosted CI on macOS (Candidate core) has not run on this head yet. - Another flake in the same file, outside HACK-1284 and not fixed here: `Relay::connect` calls `set_read_timeout(...).unwrap()` after connecting. On macOS, `setsockopt` returns `EINVAL` once the relay has already closed the accepted socket, as it does for the excess connection or an unavailable target. This failed `connection_limit_refuses_excess_without_disrupting_existing_streams` and `occupied_socket_is_not_unlinked_and_unavailable_target_fails_connection` in about 1% of parallel runs, on both base and this branch. - `provider/publisher.rs`'s macOS test fixture uses the same released-probe pattern. That file is outside this unit and is reported separately. - The `releases_port` check (`connect` fails after stop) could still in theory see another process's listener on a reused ephemeral port. This was not observed. Refs HACK-1284 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…nd strict-default bounds (#378) ## Root cause This change adds tests only. HACK-1305 asks for tests that pin three Compose startup readiness bounds that #362 (HACK-1279) added in `src/lib/native-compose-ownership.ts`: 1. A readiness timeout never becomes success. 2. The transient-flag term behaves as documented. 3. The strict default holds. The #362 tests did not pin these bounds. I ran 10 source mutants that break them against `tests/native-compose-ownership.test.ts` and `tests/native-process-policy-initial-trace.test.ts` (77 tests). **None of the 10 was reliably killed** (table below). The three "kills" in that run were replay-driver timeouts, not assertions (see Findings). The tests exposed no product defect, so production code is unchanged. ## Fix New `tests/native-compose-readiness-bounds.test.ts` (7 tests): - **The fake Docker serves one scripted scan per inspect call.** Each query's first scan and recheck are therefore exact. `retry` (the only selected on-failure service) is `member`, `absent-running`, `absent-restarting`, `placeholder-running` or `placeholder-restarting`. `web` is always a healthy ID member. - **A virtual readiness clock.** The poll interval (`Bun.sleep`) returns at once. The Nth poll moves `Date.now` past the deadline. The clock only moves between queries, so each query's own probe deadline is unaffected. This fixes the number of in-loop observations, and the inspect count shows whether the settled query ran. A harness-control test checks this for 1 and 3 observations. What each test pins: | Bound | Test | | --- | --- | | (1) Timeout never becomes success | Last in-loop observation is case 2 (running, absent), then the settled check reads `retry` rejoined and ready: the result is `null`, the settled query ran (8 inspects), and `observed` is never called. The same holds for a case-1 running recheck placeholder. An owned but never-ready observation times out as `null`, without a settled query, and the last unready observation is not returned. | | (2) Transient-flag term | Each disjunct of `first.transient \|\| second.transient \|\| recheck placeholders` is tested alone: first-scan running absent, recheck running absent, and a recheck placeholder for a `restarting` container. Each alone runs the settled check, which refuses the unchanged shape with the original predicate and exact inspect count. Restarting-only shapes (absent in both scans, or a first-scan restarting placeholder) time out `null` after exactly one query, with no settled query. The flag follows the **last** in-loop observation: transient then restarting runs no settled query, and restarting then transient runs one. | | (3) Strict default | Reads that do not opt into `transient` membership stay strict. For every race shape (case 2, a first-scan running placeholder, and case-1 running and restarting recheck placeholders), `settled`, strict and saved reads refuse with the original predicate (`live-endpoint-membership` or `network-member-id`), while the startup default keeps the shape unready. `settled` still admits the `restarting` shapes from before #362, which strict and saved refuse. An unselected service is never admitted. | ## Mutations All mutants are in `src/lib/native-compose-ownership.ts`. The "existing" column is the 77 tests in the two #362 test files. | Mutant | Bound | New file | Existing tests | | --- | --- | --- | --- | | M1: settled query's owned observation is returned | 1 | killed | survived | | M2: timeout returns the last unready observation | 1 | killed (2 fail) | survived* | | M3: drop `first.transient` | 2 | killed | survived | | M4: drop `second.transient` | 2 | killed | survived | | M5: drop the recheck-placeholder term | 2 | killed | survived* | | M6: every `restarting` observation counts as transient | 2 | killed (2 fail) | survived* | | M7: transient flag latches (`\|\|=`) instead of following the last observation | 2 | killed | survived | | M15: first-scan placeholders also count as transient | 2 | killed | survived | | M16: no settled check | 1, 2 | killed (3 fail) | not run (#362 M5) | | M8: settled refuses a `restarting` member | 3 | killed | survived | | M9: settled refuses a `restarting` first-scan placeholder | 3 | killed | survived | | M12: settled query passes `transient` | 3 | killed (2 fail) | not run (#362 M3) | | M13: startup default membership is `settled` | 3 | killed (3 fail) | not run | | M10: `transientMembership` default is `true` | 3 | survived (equivalent) | — | | M14: strict read opts into `transient` | 3 | survived (equivalent) | — | | M11: drop `result.restarting &&` from the wait's latch | 2 | survived (equivalent) | — | \* Under the M2, M5 and M6 runs, one HACK-1279 trace replay test failed with `Initial process- policy replay unavailable` after 7–20 s, at a load average of about 35–180. These mutants cannot change those replays: M2 only edits the wait, and M5 and M6 only change the `transient` field, which `observeNativeComposeStartupOwned` ignores. The failures are replay-driver timeouts (the driver's 5 s default exec budget), not kills. The three survivors are equivalent: - **M10 and M14.** The strict default is guarded three times. Strict and saved reads pass an empty on-failure selection, so `restartingServices.has(...)` fails. `requireLiveMember` also requires `mode === "startup"`, and `networkMembers` requires `opts.startup`. Flipping the default alone cannot change any outcome. This is the same as M6 in #362. The new tests pin the observable bound instead: every non-transient read refuses every race shape. - **M11.** `result.transient` implies `result.restarting`. Every disjunct requires an admitted container: a transient-admitted running member is added to `restarting`, and a claimed recheck placeholder owner is never an ID member, so `requireLiveMember` admits it too. ## Evidence - Bun 1.4.2 via `mise x bun@1.4.2`, after `bun install --frozen-lockfile`. No lockfile changes. - `bun test tests/native-compose-readiness-bounds.test.ts`: 7 pass on this Mac, at a load average of up to about 180. - `bun run --cwd packages/cli typecheck`: clean. `bun run --cwd packages/cli check`: 0 errors, 34 warnings, the same as the base. `ultracite check` on the new file is clean. Root `tsc` reports no diagnostics in the new file. - `gitleaks dir` (8.30.1, repository config) over a `git log -p origin/next..HEAD` export: no leaks. - Full suite on the M3 (macOS, Bun 1.4.2) at exact head `f2aec257`, run with other units' suites in parallel (load average about 100–160): 7548 pass, 126 skip, 33 fail, 7 errors in 541 files (70 min). The new file passed. - Serial reruns on the M3 with `--timeout 60000` (load average about 11–26): - These files now pass: `env-key-argument-redaction-cli`, `native-authored-project-oneoff`, `native-authored-project-exec`, `native-compose-proxy-probe`, `native-project-branch-cli`, `registry-touch-worktrees`, `portable-bootstrap-env-refusal`, `prerelease-install`, `native- compose-warm-up-command` and `project-env-config`. The new `native-compose-readiness-bounds` file also passes (7/7). - These files still fail: `native-compose-retained-storage-command` (5/5), `native-compose- storage-fence-cost` (4/4), `native-compose-adoption-generation` (13/114, all at their own 20–30 s test timeouts), `native-authored-process-incarnation` (1/10) and `native-authored- project-recovery` (2/18). - None of these failures can come from this PR. It adds one test file and changes no source or helper, so every failing file runs the same code as base. Both storage files also fail on the M3 at the pre-#356 base `ec223cdb` (retained-storage 3/5, fence-cost 4/4). The `process- incarnation` and `no-host` recovery failures also show in other units' M3 logs on base. - Hosted CI on this PR is green: `test`, all four `docker-e2e` shards, `linux-process-lifetime` and `secret-scan`. - Not captured: the 7 suite-level errors and the first few failure names in the full run. The M3 command keeps only the last 60 lines of output. ## Gaps - The command-level mapping in `src/lib/native-compose-command.ts` is not pinned here. A `null` readiness gives `value: code || 1` and `outcome: "uncertain"`. Pinning it needs the native authored-project start harness, and `tests/native-authored-project-start.test.ts` already fails on base. - The abort guard (`opts.ownership.signal?.aborted` skips the settled query) is not pinned. Aborting deterministically between the last observation and the latch check needs a probe hook. - The docker-e2e scenario was not run locally. These are unit-level bounds against a fake Docker. ## Findings (not fixed here) - The trace replay driver in `tests/e2e/native-process-policy-initial-replay.ts` uses a 5 s default `timeoutMs`. Under host load it fails with `Initial process-policy replay unavailable` on tests that the source change cannot affect (see the footnote above). Severity: low (flake). - The wait's doc comment says the settled check runs only when the last observation was unready "only because a `running` on-failure member" was missing or placeholdered. The code also runs it for a `restarting` container whose `ep-` key first appears in the recheck scan, which then refuses with `network-member-id` at the deadline. The `transient` field's own doc ("an admitted container depended on a shape that a `settled` query refuses") matches the code, and the new tests pin the code. Severity: low (doc precision). - **Storage-backed source CLI startup fails on the M3 (macOS).** Repro: on the M3, run `bun test --timeout 60000 ./tests/native-compose-retained-storage-command.test.ts`. It fails at `ec223cdb` and at `f2aec257`. Expected: the fake engine's cold `up --detach --json` exits 0. Actual: it exits 1 after about 34 s with `E_CONFIG_INVALID`, `native_compose_effect_refusal: { stage: "storage-helper-start", reason: "unclassified" }`. `native-compose-storage-fence- cost.test.ts` fails 4/4 the same way. Hosted Linux CI passes. This is possibly a known flake. Severity: medium, because it hides the storage regression tests on macOS hosts. Refs HACK-1305 🤖 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.
Refs HACK-1281
Summary
native-config-networksfails when a healthy storage-bearinghack exec(orup/restart) is refused by the storage carrier after its Docker effect. The refusal leaves carrier intent uncertain. By design,down --recoverthen stays incomplete, so the fixture reports "Cleanup also failed".The carrier used two fixed bounds that do not survive a host stall of the CLI process:
ps);A stalled process resumes with its timer already due, so the command times out even though the child finished in time. This PR keeps those bounds finite but stall-tolerant. It also fixes the diagnostics gaps the verifier found.
Root cause and evidence
execcommands on shards 1 and 2.hack:execexit 1,E_COMPOSE_FAILED, stagestorage-helper-cleanup, reasonprobe-timeout. Shard 2 (job 119371288299) failed on the 13th: stagestorage-helper-create, reasonprobe-timeout. In both, saved state showedstorageWitnessesPending, cleanupdown --recoverexited 1 withstop-incomplete, and the scenario printed "Cleanup also failed" and "Fixture command must exit 0".probe-timeoutrefusals before the fix and 0 after. With 3.5 s stalls over 300 runs, there were 2 refusals before (oneprobe-timeout, oneprobe-budget) and 0 after.native-config-networksfailures on shard 1 since 2026-10-12, not four. They are 7 natural failures plus the load-sampled run, across 331 completed runs (about 2%).psreads). It saw no stall of 250 ms or more in a read, and an event-loop lag of at most 250 ms. Those runs were in a quiet CI period. Stall-induced refusal is therefore the mechanism that reproduces the signature on CI and that this PR removes. It is not a direct observation of the historical cause. If a failure with this signature recurs on this head, the diagnostics below will name the step, stage and reason.What changed
src/lib/native-compose-storage-witness-docker.ts). Identity reads now share the remaining handshake or settlement budget instead of a fixed 1 s bound. The post-exit settlement budget is a finite 15 s (SETTLEMENT_BUDGET_MS). No check, record, retention, recovery rule or authority changes. An exhausted budget still refuses asprobe-budget.bounded()now observes abandoned work, so a rejection after the budget has run out cannot escape as an unhandled process error. The over-budget test exposed this bug.docs/reference/native-compose-storage-xattr-carrier.md). Documents the new budgets and why they must outlast a multi-second host stall.tests/e2e/native-config-networks-acceptance.ts). These close the gaps from the review:messagelabel for exact fixed product sentences:stop-incomplete,operation-failed, andnative-state,native-busy,native-uncertainandnative-stale. Any other text, or a changed sentence, givesnull.runneroutcome. Before this, it logged nothing.E_COMPOSE_FAILEDdetail and the human summary line, and the classifiedprobe-timeout,probe-cancelandprobe-budgetcarrier causes.Validation
tests/native-compose-storage-command-record.test.ts), each proven by a mutation:probe-budgetand stays unsettled, using a monotonic clock jump with no real wait. Fails with the budget raised to 60 s, and fails without the abandoned-work catch.tests/native-networks-fixture.test.ts), each proven by a mutation:stop-incomplete, from both JSON and human output, and changed text givesnull. Fails with no fixed-message classification, and fails with no human-line classification.runner: "timeout". Fails with the outcome always set toother.executeis only exercised by the Docker scenario.bun teston the fixture, closed-error-output, effect-diagnostics, command-record, docker-io,tests/cli-spawn-env-isolation.test.tsand e2e-shard tests: 135 pass, 0 fail.bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors and 34 warnings. The two complexity warnings in the carrier file existed before this PR.testpassed on fc4095b. A local macOSbun run test(Bun 1.4.2, load average about 30) failed in files unrelated to this change, includingnative-authored-project-start,global-command(in the combined run) andnative-compose-storage-fence-cost.native-authored-project-startandnative-compose-storage-fence-costfail the same way with the pre-fix carrier source.global-commandpasses when run alone. These are local environment failures, not regressions.gitleakswith.gitleaks.tomlon the full branch diff: no leaks.native-config-networks. The two stalled shards took 3,200 s and 2,675 s, and the two unstalled shards took 2,190 s and 2,430 s. Run 39643040488 shards 3 and 4 (pre-fix, unstalled) also passed.testand docker-e2e shards 1-4.Open gates
🤖 Generated with Claude Code