Skip to content

fix(native): keep storage carrier commands alive through host stalls - #356

Merged
roodboi merged 15 commits into
nextfrom
wf/g13-config-networks-flake-b17
Oct 21, 2026
Merged

roodboi merged 15 commits into
nextfrom
wf/g13-config-networks-flake-b17

Conversation

@roodboi

@roodboi roodboi commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Refs HACK-1281

Summary

native-config-networks fails when a healthy storage-bearing hack exec (or up/restart) is refused by the storage carrier after its Docker effect. The refusal leaves carrier intent uncertain. By design, down --recover then 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:

  • a 1-second process identity read (ps);
  • a 3-second post-exit settlement budget.

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

  • Mechanism, replayed on hosted CI with real Docker. A temporary commit (fbf5cf6, reverted in 055ecce) stopped the CLI process for 1.2 s of every 2 s during 20 saved exec commands on shards 1 and 2.
    • On the pre-fix carrier (run 39643040488), both stalled shards failed with the original signature. Shard 1 (job 119371288508) failed on the first stalled exec: hack:exec exit 1, E_COMPOSE_FAILED, stage storage-helper-cleanup, reason probe-timeout. Shard 2 (job 119371288299) failed on the 13th: stage storage-helper-create, reason probe-timeout. In both, saved state showed storageWitnessesPending, cleanup down --recover exited 1 with stop-incomplete, and the scenario printed "Cleanup also failed" and "Fixture command must exit 0".
    • On this fix (fc4095b, run 39644645624), both stalled shards passed all 20 stalled execs and the whole scenario. All four shards passed.
  • The same replay in a local Linux container. Running the real recorded transport 600 times under the same 1.2 s / 2 s stalls gave 5 probe-timeout refusals before the fix and 0 after. With 3.5 s stalls over 300 runs, there were 2 refusals before (one probe-timeout, one probe-budget) and 0 after.
  • The historical failures match this signature. I found eight natural native-config-networks failures 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%).
    • Each one is "Fixture command must exit 0" followed by "Cleanup also failed", which is the replayed signature.
    • They happen at varied points in the run (88 s to 33 min into phase 2, and inside the ingress phase), which fits a per-command event rather than one phase.
    • The rate rises with CI concurrency: about 1.0% in quiet hours (fewer than 8 runs per hour), 1.7% in middle hours and 4.5% in busy hours (20 or more runs per hour). That fits host contention.
  • What is not proven. The original failing commands cannot be recovered, because those runs uploaded no captures. A natural stall monitor ran on all six sampled shards (about 4 runner-hours and 7,700 product-shaped ps reads). 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

  • Carrier fix (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 as probe-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 (docs/reference/native-compose-storage-xattr-carrier.md). Documents the new budgets and why they must outlast a multi-second host stall.
  • Diagnostics (tests/e2e/native-config-networks-acceptance.ts). These close the gaps from the review:
    • An incomplete saved stop issues no refusal boundary, so its stage and reason stay null. The command diagnostic now adds a closed message label for exact fixed product sentences: stop-incomplete, operation-failed, and native-state, native-busy, native-uncertain and native-stale. Any other text, or a changed sentence, gives null.
    • A command the runner killed (timeout) or could not return now logs its class, role, sequence and duration, plus a closed runner outcome. Before this, it logged nothing.
    • No output, argv or path is printed.
  • Earlier commits on this branch are kept: the closed refusal boundary in E_COMPOSE_FAILED detail and the human summary line, and the classified probe-timeout, probe-cancel and probe-budget carrier causes.

Validation

  • New regression tests (tests/native-compose-storage-command-record.test.ts), each proven by a mutation:
    • A child identity read stalled for 1.2 s still settles. Fails with the 1 s bound restored.
    • A 3.5 s post-exit stall still settles. Fails with the 3 s budget restored.
    • A settlement past the budget still refuses as probe-budget and 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.
  • Fixture tests (tests/native-networks-fixture.test.ts), each proven by a mutation:
    • An incomplete stop is named stop-incomplete, from both JSON and human output, and changed text gives null. Fails with no fixed-message classification, and fails with no human-line classification.
    • A real runner timeout is labelled runner: "timeout". Fails with the outcome always set to other.
    • The wiring of that timeout diagnostic inside the scenario's execute is only exercised by the Docker scenario.
  • Focused suites: bun test on the fixture, closed-error-output, effect-diagnostics, command-record, docker-io, tests/cli-spawn-env-isolation.test.ts and 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.
  • Full suite: hosted test passed on fc4095b. A local macOS bun run test (Bun 1.4.2, load average about 30) failed in files unrelated to this change, including native-authored-project-start, global-command (in the combined run) and native-compose-storage-fence-cost. native-authored-project-start and native-compose-storage-fence-cost fail the same way with the pre-fix carrier source. global-command passes when run alone. These are local environment failures, not regressions.
  • gitleaks with .gitleaks.toml on the full branch diff: no leaks.
  • Hosted e2e on the fixed code (run 39644645624, head fc4095b): all four shards passed 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.
  • Final head 055ecce (run 39654287962): all 16 checks passed, including test and docker-e2e shards 1-4.

Open gates

  • The historical failures are attributed by matching signature and correlation, not by a recovered command. See "What is not proven" above.
  • The stability evidence is the stall A/B plus a few clean hosted runs. It is not a long natural sample.
  • HACK-1281 in Linear has not been updated from this unit.

🤖 Generated with Claude Code

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>
@linear-code

linear-code Bot commented Oct 19, 2026

Copy link
Copy Markdown

HACK-1281

roodboi and others added 3 commits October 19, 2026 13:23
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>
@blacksmith-sh

This comment has been minimized.

roodboi and others added 6 commits October 19, 2026 14:48
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>
@blacksmith-sh

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>
@roodboi roodboi changed the title test(e2e): diagnose and fix native-config-networks command flake test(e2e): add closed failure evidence for native-config-networks Oct 20, 2026
@roodboi roodboi changed the title test(e2e): add closed failure evidence for native-config-networks fix(native): name closed refusal boundaries in native command failures Oct 20, 2026
roodboi and others added 3 commits October 21, 2026 13:17
…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>
@blacksmith-sh

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 roodboi changed the title fix(native): name closed refusal boundaries in native command failures fix(native): keep storage carrier commands alive through host stalls Oct 21, 2026
@roodboi
roodboi marked this pull request as ready for review October 21, 2026 20:28
@roodboi
roodboi merged commit f2278c7 into next Oct 21, 2026
16 checks passed
@roodboi
roodboi deleted the wf/g13-config-networks-flake-b17 branch October 21, 2026 20:29
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)
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