# FIXP lifecycle and step diagnostics review — 2026-10-13

Scope: the C++, Java, .NET and Rust FIXP runners, the .NET direct protocol API,
and Rust acceptor payload views. Worktree `codex-review-fixp-state-1013`; the
main checkout is used only for fast-forward pulls. Three parallel agents worked
on separate flavours and cross-reviewed one another's changes. Root coordinated
all builds, baseline reproduction and publication.

## Confirmed defects and changes

- All four direct initiators left their runner active after a hard transport
  read error, although the socket protocol had already disconnected. They now
  stop liveness and become terminal. The first error retains its existing
  `SessionError` return and raw read diagnostics; later calls perform no I/O.
  Rust single and multi acceptors also stop their clocks on EOF/read error.
- .NET's direct `OnSequencedApplication` API applied negotiated flow rules before
  requiring establishment. An unsequenced application could be accepted, and an
  idempotent gap could mutate the frontier, outside an established session.
  Lifecycle admission now precedes flow rules, matching the other flavours.
- C++ and .NET initiators retained unfinished finalization acknowledgements
  across replacement connections. An early peer Terminate on the new connection
  could permanently finalize the logical session using the old acknowledgement.
  New admitted connections reset unfinished finalization; completed markers and
  preflight refusal behaviour are preserved.
- .NET replacement `Start` reopened its store before retiring the previous
  established owner, and a successful replacement retained the old clocks during
  negotiation. Admitted replacement now retires the old state before store
  acquisition. Basic preflight refusals still leave a healthy owner intact.
- Rust multi-acceptor payload lookup used the most recently serviced peer instead
  of the peer which produced the supplied copied step. Steps now retain their
  producing peer. Both acceptor accessors also use checked range arithmetic.
- C++ and .NET runner heartbeat diagnostics were cleared by subsequent inbound
  processing. One pump now retains all its successful/partial outbound bytes,
  the latest applicable outbound template and the last actual transport result.
  Buffered dispatch preserves the heartbeat's result; an actual empty read
  replaces it with zero. Callback replacement keeps the replacement's diagnostics.
- Follow-up review found C++ multi-acceptor internal retirement dropped the
  preceding heartbeat when a required response failed; terminal outcomes now
  retain that accounting while explicit callback replacement stays isolated. A
  separate diagnostic-owner tag distinguishes internal retirement from a failed
  nested replacement or sibling send, including preflight refusals; those nested
  operations do not inherit the retired connection's heartbeat. Whole-listener
  closure also invalidates that owner, including when a subsequent Listen fails
  its replay-store admission before I/O.
  C++ initiator flow-violation replies also contribute their bytes and actual
  transport result.
- .NET replay previously overwrote the inbound request with each outbound record.
  The existing response scratch step now preserves the complete inbound metadata,
  total successful/partial writes and latest actual result across the group.
  Gap responses, flow-refusal/violation replies and partial application/Terminate
  sends retain the same diagnostics. Local encode refusal preserves the preceding
  actual transport result.
- Follow-up coverage confirmed pre-write gap failures in all four flavours.
  Automatic notifications retain the current pump's last transport result through
  preparation and durable-provider refusal. C++, Java and Rust also carry it
  across pending-gap control dispatch and scratch clearing. An actual write,
  including zero or negative results, still replaces the seed; a new pump starts
  with its own read/buffered result.
- Java initiator response scratch survived public reconfiguration. An automatic
  gap notification refused before preparation could copy a previous connection's
  response bytes/template into the new pump. The response scratch is now cleared
  before that refusal, retaining the current pump's transport result.

The Rust `AcceptorRunnerStep` gains the public field `application_peer_index`.
External exhaustive struct literals/patterns need to name it or use a default
update/rest pattern. There are no such external literals in this repository.
Payload validity still ends at that peer's next pump or reset; servicing or
sending to another peer does not shorten it.

## Regression evidence

The first baseline used unchanged production sources. Tests exercise both SOFH
layouts, terminal reads during establishment/Terminate wait, reconnect admission,
durable store reopen failure/standby/exhaustion, copied peer steps, overflow
refusal, heartbeats with buffered/read input, nested sends and callback replacement.
Protocol lifecycle rows use the public direct state API; the rejection setup
does not claim to be a complete wire handshake. Runner lifecycle fixtures use
encoded public handshakes.

| Original baseline | Failures | Passing controls | Skipped/ignored |
|---|---:|---:|---:|
| C++ initiator terminal/reconnect cases | 32 | 20 | 0 |
| C++ heartbeat diagnostic cases | 168 | 60 | 0 |
| .NET lifecycle/protocol/native rows | 70 | 68 | 0 |
| .NET heartbeat/native rows | 62 | 19 | 0 |
| Java terminal-input/native rows | 8 | 9 | 0 |
| Rust terminal-input methods | 5 | 2 | 0 |
| Rust payload ownership/range methods | 5 | 1 | 0 |

C++ baseline fixtures made 1,016 and 4,312 checks respectively. Rust methods
iterate over the two layouts and additional lifecycle/range scenarios. Native
load guards passed; these failures were assertions reproducing the bugs, not
compilation or missing-library failures.

A saved intermediate implementation reproduced the follow-up response/replay
issues: 32 of 336 C++ heartbeat cases failed (6,670 checks, 40 failures), and
72 of 300 .NET lifecycle/protocol/heartbeat/native rows failed. Native guards
passed and no rows were skipped. Final cross-review then reproduced failed
callback ownership in 48 of 440 C++ cases. The first .NET cross-review attempt
found a nullable annotation compile error in the new replay helper; that error
was corrected to match the validating socket API and is not counted as a
regression assertion. The corrected cross-review baseline ran 456 C++ cases
(64 failed, 392 passed; 10,394 checks, 104 failures) and 191 .NET rows (16 failed,
175 passed, zero skipped/errors, native guard passed). All 16 managed failures
were the expected last-transport-result assertions.

The expanded 600-case C++ baseline failed in 130 cases: the same 64 callback
cases and 66 additional gap-diagnostic cases. Its other 470 cases passed, with
13,214 total checks and 170 failed checks. The gap subset includes 48 durable
provider refusals across all three runner roles, 48 exhausted-sequence cases and
48 actual-send controls. The Rust counterpart ran 144 tests: 44 expected
failures and 100 passing controls, with none ignored. Java ran 144 counterpart
rows plus its native guard: 48 expected failures and 97 passes, zero skips/errors.
The first Java run had four additional fixture assertion failures: the single
acceptor intentionally retains its primary store on sequence exhaustion. The
fixture was corrected and rerun; the corrected counts above exclude that mistake.
Stale Surefire XML from an earlier selection was excluded from totals and retained
separately; subsequent runs clear this session's report directory first.

A supplemental Java response-scratch fixture initially used a Sequence gap on a
None local flow, which is rejected before notification dispatch. That was a
fixture error, not evidence of the scratch bug. Its corrected trigger is peer
FinishedSending with an outstanding idempotent gap, which reaches automatic
notification through finalization. With only the scratch-clear change reverted,
8 of its 12 rows failed with predecessor bytes, while the 4 empty-scratch controls
passed. The native guard passed; no rows skipped or errored.

The final C++ Close/Listen reproduction ran the expanded 604-case heartbeat
fixture against the otherwise fixed sources: 2 failed and 602 passed. Both
failures were stale heartbeat bytes after whole-listener closure followed by
store-admission refusal; successful-reopen controls passed in both layouts.

The durable-provider fixtures use an in-memory implementation of the public
configured-store interface. They verify provider `Save` refusal before transport
I/O, rather than simulating a filesystem failure.

## Qualification

All 66 build/test stages passed on the fixed candidate `09fe99e69`.

| Selected checks | Development | Open | Skipped/ignored |
|---|---:|---:|---:|
| C++ FIXP programs | 12 passed | 12 passed | 0 |
| .NET FIXP tests plus native guard | 1,358 passed | 1,271 passed | 0 |
| Java FIXP tests plus native guard | 3,157 passed | 3,028 passed | 0 |
| Rust four integration fixtures | 163 passed | 163 passed | 0 |
| Rust FIXP unit tests | 378 passed | 328 passed | 0 |

The two new C++ fixtures (52 lifecycle cases and 604 heartbeat cases, 14,334
checks together) also passed ASan/UBSan and open C++11 builds. Both Java and
.NET native guards passed in both editions. The Rust unit filter intentionally
excludes non-FIXP unit tests (1,111 development / 999 open); those are outside
this qualification, not ignored FIXP tests. Open edition counts exclude private
source tests.

The first C++
candidate launch ran from the repository root; two existing table-driven tests
could not find their CSV fixtures. This was a harness working-directory error,
not a production assertion. The runner was corrected to execute in `engine/cpp`
and the C++ matrix rerun. The 604-case heartbeat and 52-case lifecycle fixtures
already passed before that correction.

Selected FIXP suites are used; this review does not claim a full repository-wide
test run or measured latency improvement.

All new production work is bounded and allocation-free on the pump path. The
C++ multi-acceptor tracks diagnostic ownership with two scalar fields and stamps
them when an operation owns the step. Stopping terminal pumps
eliminates invalid repeated reads and heartbeats on a failed connection.

The reviewed production files were restored byte-for-byte after each temporary
baseline. All 25 changed engine paths retained their qualified SHA-256 hashes;
`qualified-input-hashes.json` also records the complete engine tree. The final
export scan checked those 25 paths with zero findings. The final rebase onto
`300e9a521` preserved the complete qualified engine tree
`30b5780ca5b8e74992838b7fed4f0ddd28bccc65`; upstream changes were confined
to benchmark/operations files. Source verification passed again after rebasing.

Logs, commands, source hashes, baseline snapshots, a verified source-history
Git bundle and test reports are retained
under `~/archive/libhft/reviews/2026-10-13-codex-review-fixp-state-1013/` before
session worktrees/build outputs are removed. BEELINK2 builds use
`LIBHFT_BUILD_ROOT`, the shared host lease and a foreign-heavy-process guard,
with at most 16 jobs and one heavy job from this session at a time.
