irtt-server now has sensible zero-argument defaults: no --bind binds the wildcard IRTT port on both address families ([::]:2112 then 0.0.0.0:2112) where wildcard reply-source selection is supported. Any explicit --bind fully replaces the defaults; repeated explicit binds are unchanged. No protocol, measurement, or session behavior changed otherwise.
The root README is reduced to a project landing page (~190 lines, down from 411), including a factual comparison against upstream IRTT v0.9.2.
Detailed CLI behavior moved into manpage-shaped docs: docs/irtt-rs.md, docs/irtt-client.md, docs/irtt-tui.md, docs/irtt-server.md.
Every published crate (irtt-rs, irtt-client, irtt-server, irtt-stats, irtt-proto) now has its own crates.io README instead of inheriting the root README.
Rust usage moved toward docs.rs/rustdoc and runnable Cargo examples/ targets for irtt-client, irtt-server, and irtt-stats (irtt-proto intentionally has none). docs.rs rustdoc example scraping enabled for irtt-client/irtt-server.
## Summary
- `irtt-server` now has sensible zero-argument defaults: no `--bind` binds the wildcard IRTT port on both address families (`[::]:2112` then `0.0.0.0:2112`) where wildcard reply-source selection is supported. Any explicit `--bind` fully replaces the defaults; repeated explicit binds are unchanged. No protocol, measurement, or session behavior changed otherwise.
- The root README is reduced to a project landing page (~190 lines, down from 411), including a factual comparison against upstream IRTT v0.9.2.
- Detailed CLI behavior moved into manpage-shaped docs: `docs/irtt-rs.md`, `docs/irtt-client.md`, `docs/irtt-tui.md`, `docs/irtt-server.md`.
- Every published crate (`irtt-rs`, `irtt-client`, `irtt-server`, `irtt-stats`, `irtt-proto`) now has its own crates.io README instead of inheriting the root README.
- Rust usage moved toward docs.rs/rustdoc and runnable Cargo `examples/` targets for `irtt-client`, `irtt-server`, and `irtt-stats` (`irtt-proto` intentionally has none). docs.rs rustdoc example scraping enabled for `irtt-client`/`irtt-server`.
## Test plan
- [x] `cargo fmt --all -- --check`
- [x] `cargo test --workspace --locked`
- [x] `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings`
- [x] `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --locked`
- [x] `git diff --check`
- [x] `cargo check -p irtt-rs --no-default-features` (and again with `client`/`server`/`tui` features individually)
- [x] New examples build under their required features and exit cleanly with no reachable server (no hangs/panics)
- [x] `cargo package --list` per publishable crate confirms each crate's own README and examples are included
- [x] Fresh independent review pass against a 20-point checklist; one gap found (ServerFill negotiation docs had no new home) and fixed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `a4dc933184`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Client::send_probe is caller-paced, but this loop calls it again immediately after each receive instead of waiting for next_send_deadline(). With a reachable low-latency server, recv_once() returns immediately and the example sends probes at roughly RTT speed rather than the configured 200 ms interval, producing invalid pacing measurements and potentially exhausting the server's burst allowance.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Wait for the blocking client's send deadline**
`Client::send_probe` is caller-paced, but this loop calls it again immediately after each receive instead of waiting for `next_send_deadline()`. With a reachable low-latency server, `recv_once()` returns immediately and the example sends probes at roughly RTT speed rather than the configured 200 ms interval, producing invalid pacing measurements and potentially exhausting the server's burst allowance.
Useful? React with 👍 / 👎.
When any echo reply is lost or rate-limited, recv().await remains pending indefinitely, so the loop never reaches poll_timeouts(), rechecks the two-second run completion, or closes the session. This is readily triggered because the preceding caller-paced sends also run without waiting for next_send_deadline() and exhaust the default server burst allowance; the documented runnable example then hangs rather than finishing. Drive the receive together with the next send/timeout deadline instead of awaiting it unconditionally.
**<sub><sub></sub></sub> Bound the asynchronous receive wait**
When any echo reply is lost or rate-limited, `recv().await` remains pending indefinitely, so the loop never reaches `poll_timeouts()`, rechecks the two-second run completion, or closes the session. This is readily triggered because the preceding caller-paced sends also run without waiting for `next_send_deadline()` and exhaust the default server burst allowance; the documented runnable example then hangs rather than finishing. Drive the receive together with the next send/timeout deadline instead of awaiting it unconditionally.
AGENTS.md reference: [AGENTS.md:L78-L78](https://github.com/Lochnair/irtt-rs/blob/a4dc9331848f9fd7c80b8ca128378b2954b8ac2d/AGENTS.md#L78-L78)
Useful? React with 👍 / 👎.
Require one statistics collector per managed target
A managed session may contain multiple targets, but this instruction reads as though their ClientEvent values can all be fed into one StatsCollector. The collector has one packet-count and sequence/IPDV namespace, while each target starts its sequence numbers at zero; combining a managed event stream therefore merges unrelated measurements and pairs probes from different targets, corrupting the resulting snapshot. Document that callers must route each ManagedEvent::Client to a separate collector keyed by its target, as the CLI does.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Require one statistics collector per managed target**
A managed session may contain multiple targets, but this instruction reads as though their `ClientEvent` values can all be fed into one `StatsCollector`. The collector has one packet-count and sequence/IPDV namespace, while each target starts its sequence numbers at zero; combining a managed event stream therefore merges unrelated measurements and pairs probes from different targets, corrupting the resulting snapshot. Document that callers must route each `ManagedEvent::Client` to a separate collector keyed by its target, as the CLI does.
Useful? React with 👍 / 👎.
Fixed: the loop now checks next_send_deadline() before calling send_probe(), so probes are paced at the configured interval instead of as fast as the loop spins. See a981487.
Fixed: the loop now checks `next_send_deadline()` before calling `send_probe()`, so probes are paced at the configured interval instead of as fast as the loop spins. See a981487.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: the receive wait is now bounded with tokio::select! against a sleep to the next send/timeout deadline instead of awaiting recv() unconditionally, and send_probe() is only called once its deadline is due. See a981487.
Fixed: the receive wait is now bounded with `tokio::select!` against a sleep to the next send/timeout deadline instead of awaiting `recv()` unconditionally, and `send_probe()` is only called once its deadline is due. See a981487.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: added a paragraph clarifying that StatsCollector is scoped to a single target and a multi-target managed session needs one collector per target, since feeding multiple targets into one collector would pair up unrelated probes. See a981487.
Fixed: added a paragraph clarifying that `StatsCollector` is scoped to a single target and a multi-target managed session needs one collector per target, since feeding multiple targets into one collector would pair up unrelated probes. See a981487.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `a981487ed0`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
The new Instant::now() + client.probe_timeout() fallback does not represent the next pending probe timeout. Once the send schedule finishes, if an early probe was lost but later probes replied, recv() and this sleep can both remain pending until a full timeout from the current instant, so poll_timeouts() classifies the loss and completes the run substantially late (up to about two extra seconds with this example's configuration). Wake at the earliest pending timeout, or poll on a short bounded cadence if that deadline is not publicly exposed.
**<sub><sub></sub></sub> Poll at the oldest probe's timeout**
The new `Instant::now() + client.probe_timeout()` fallback does not represent the next pending probe timeout. Once the send schedule finishes, if an early probe was lost but later probes replied, `recv()` and this sleep can both remain pending until a full timeout from the current instant, so `poll_timeouts()` classifies the loss and completes the run substantially late (up to about two extra seconds with this example's configuration). Wake at the earliest pending timeout, or poll on a short bounded cadence if that deadline is not publicly exposed.
AGENTS.md reference: [AGENTS.md:L78-L78](https://github.com/Lochnair/irtt-rs/blob/a981487ed097a78c9a039e5f5aa4b31deb038f63/AGENTS.md#L78-L78)
Useful? React with 👍 / 👎.
Fixed: replaced the Instant::now() + probe_timeout() fallback with a fixed 50ms poll cadence, since AsyncClient doesn't expose the earliest pending probe's own deadline. This bounds how late a lost probe can be classified to the poll interval instead of a full probe_timeout() from whenever the loop happened to run. See 17422e0.
Fixed: replaced the `Instant::now() + probe_timeout()` fallback with a fixed 50ms poll cadence, since `AsyncClient` doesn't expose the earliest pending probe's own deadline. This bounds how late a lost probe can be classified to the poll interval instead of a full `probe_timeout()` from whenever the loop happened to run. See 17422e0.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `17422e0dc3`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Even with send pacing corrected, this hard deadline expires after three seconds while the example retains the default four-second probe_timeout. Therefore, if any probe reply is lost, not even the first probe can reach its timeout before the loop exits; close() then transitions the session to closed and discards the pending probes without emitting the EchoLoss events this example is set up to print. Base the safety deadline on the run duration plus client.probe_timeout(), or otherwise keep polling until pending probes are classified.
**<sub><sub></sub></sub> Wait long enough to classify lost probes**
Even with send pacing corrected, this hard deadline expires after three seconds while the example retains the default four-second `probe_timeout`. Therefore, if any probe reply is lost, not even the first probe can reach its timeout before the loop exits; `close()` then transitions the session to closed and discards the pending probes without emitting the `EchoLoss` events this example is set up to print. Base the safety deadline on the run duration plus `client.probe_timeout()`, or otherwise keep polling until pending probes are classified.
AGENTS.md reference: [AGENTS.md:L78-L78](https://github.com/Lochnair/irtt-rs/blob/17422e0dc3900cbc300bdd79d9e6bc66af3d9ed0/AGENTS.md#L78-L78)
Useful? React with 👍 / 👎.
Separately from the now-bounded receive branch, a persistent send_probe() error (for example, the interface going down after Open) is discarded here. Failed sends intentionally retain the prepared probe without advancing the schedule, so next_send_deadline() remains overdue and is_run_complete() remains false; this loop consequently retries every 50 ms forever instead of reporting the error or exiting. Surface the error and terminate the example rather than converting it to an empty event list.
**<sub><sub></sub></sub> Stop retrying after a probe send error**
Separately from the now-bounded receive branch, a persistent `send_probe()` error (for example, the interface going down after Open) is discarded here. Failed sends intentionally retain the prepared probe without advancing the schedule, so `next_send_deadline()` remains overdue and `is_run_complete()` remains false; this loop consequently retries every 50 ms forever instead of reporting the error or exiting. Surface the error and terminate the example rather than converting it to an empty event list.
AGENTS.md reference: [AGENTS.md:L78-L78](https://github.com/Lochnair/irtt-rs/blob/17422e0dc3900cbc300bdd79d9e6bc66af3d9ed0/AGENTS.md#L78-L78)
Useful? React with 👍 / 👎.
When a finite run uses --format csv, tsv, or jsonl, no final summary is printed: OutputFormat::prints_summary() returns true only for table output, and StreamOutput::print_summary() enforces that condition. Saying a summary is “always printed” therefore gives structured-output users the wrong contract and contradicts the format documentation immediately below; qualify this statement as applying only to table output.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Qualify final summaries as table-only**
When a finite run uses `--format csv`, `tsv`, or `jsonl`, no final summary is printed: `OutputFormat::prints_summary()` returns true only for table output, and `StreamOutput::print_summary()` enforces that condition. Saying a summary is “always printed” therefore gives structured-output users the wrong contract and contradicts the format documentation immediately below; qualify this statement as applying only to table output.
Useful? React with 👍 / 👎.
Fixed: qualified both mentions of the final summary to table output specifically, matching the OUTPUT section's existing note that CSV/TSV/JSONL never print one. See 86468f0.
Fixed: qualified both mentions of the final summary to table output specifically, matching the OUTPUT section's existing note that CSV/TSV/JSONL never print one. See 86468f0.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: send/receive/timeout-poll errors are now matched explicitly and surfaced via eprintln + break instead of being discarded by unwrap_or_default(), so a persistent send failure reports an error and exits rather than retrying every 50ms forever. See 86468f0.
Fixed: send/receive/timeout-poll errors are now matched explicitly and surfaced via eprintln + break instead of being discarded by unwrap_or_default(), so a persistent send failure reports an error and exits rather than retrying every 50ms forever. See 86468f0.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: the safety deadline is now RUN_DURATION + client.probe_timeout() instead of a fixed 3s guess, so a lost reply near the end of the run can actually reach its timeout and be classified/printed as EchoLoss before the loop gives up and closes. See 86468f0.
Fixed: the safety deadline is now `RUN_DURATION + client.probe_timeout()` instead of a fixed 3s guess, so a lost reply near the end of the run can actually reach its timeout and be classified/printed as EchoLoss before the loop gives up and closes. See 86468f0.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `86468f0ce9`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
On an IPv4-only host, such as a Linux container with IPv6 disabled, binding [::]:2112 fails. Since serve passes this generated pair to the all-or-nothing ServerSet::bind, the subsequent IPv4 wildcard is never served, so the newly advertised zero-argument invocation exits even though IPv4 wildcard serving is supported. Construct the implicit defaults so an unavailable address family does not prevent startup on the available family, while retaining all-or-nothing behavior for explicit binds.
**<sub><sub></sub></sub> Allow zero-argument startup without IPv6**
On an IPv4-only host, such as a Linux container with IPv6 disabled, binding `[::]:2112` fails. Since `serve` passes this generated pair to the all-or-nothing `ServerSet::bind`, the subsequent IPv4 wildcard is never served, so the newly advertised zero-argument invocation exits even though IPv4 wildcard serving is supported. Construct the implicit defaults so an unavailable address family does not prevent startup on the available family, while retaining all-or-nothing behavior for explicit binds.
AGENTS.md reference: [AGENTS.md:L58-L60](https://github.com/Lochnair/irtt-rs/blob/86468f0ce904844c2eb4f6d852eb47c35dd0fccf/AGENTS.md#L58-L60)
Useful? React with 👍 / 👎.
This docs.rs command does not actually scrape any of the three client examples because the package has dev-dependencies and none of its [[example]] targets explicitly opts in. Running the configured cargo doc -Zunstable-options -Zrustdoc-scrape-examples reports: “Rustdoc did not scrape the following examples because they require dev-dependencies” and instructs adding doc-scrape-examples = true; consequently the published API documentation will omit the example usage this metadata is intended to add.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Opt the client examples into rustdoc scraping**
This docs.rs command does not actually scrape any of the three client examples because the package has dev-dependencies and none of its `[[example]]` targets explicitly opts in. Running the configured `cargo doc -Zunstable-options -Zrustdoc-scrape-examples` reports: “Rustdoc did not scrape the following examples because they require dev-dependencies” and instructs adding `doc-scrape-examples = true`; consequently the published API documentation will omit the example usage this metadata is intended to add.
Useful? React with 👍 / 👎.
When the UDP send starts failing after Open, for example after the interface goes down, unwrap_or_default() converts the ClientError into an empty event list. A failed send retains the scheduled probe without advancing the schedule, so this loop silently retries until its safety deadline and then returns from main successfully, presenting an incomplete run with no diagnostic. Handle the error and terminate/report it as the Tokio example does.
**<sub><sub></sub></sub> Propagate blocking probe-send failures**
When the UDP send starts failing after Open, for example after the interface goes down, `unwrap_or_default()` converts the `ClientError` into an empty event list. A failed send retains the scheduled probe without advancing the schedule, so this loop silently retries until its safety deadline and then returns from `main` successfully, presenting an incomplete run with no diagnostic. Handle the error and terminate/report it as the Tokio example does.
AGENTS.md reference: [crates/irtt-client/AGENTS.md:L83-L87](https://github.com/Lochnair/irtt-rs/blob/86468f0ce904844c2eb4f6d852eb47c35dd0fccf/crates/irtt-client/AGENTS.md#L83-L87)
Useful? React with 👍 / 👎.
The canonical serialized column names are rd_us and sd_us: selecting either long spelling is accepted by the parser, but CSV/TSV headers and JSONL keys are still emitted using Column::name(), which returns the abbreviated spelling. Describing receive_delay_us and send_delay_us as the columns and the emitted names as aliases gives structured-output consumers the wrong schema; reverse this wording so the documented keys match the actual output.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Document the canonical delay column names**
The canonical serialized column names are `rd_us` and `sd_us`: selecting either long spelling is accepted by the parser, but CSV/TSV headers and JSONL keys are still emitted using `Column::name()`, which returns the abbreviated spelling. Describing `receive_delay_us` and `send_delay_us` as the columns and the emitted names as aliases gives structured-output consumers the wrong schema; reverse this wording so the documented keys match the actual output.
Useful? React with 👍 / 👎.
Fixed: the implicit zero-argument default now retries with just the surviving address if one of [::]:2112/0.0.0.0:2112 fails to bind (identified via ServerSetError::ListenerSetup's addr), instead of failing the whole invocation. An explicit --bind list keeps the ordinary all-or-nothing behavior — the fallback only applies when --bind was not given. Added unit tests for the decision logic and documented the fallback in docs/irtt-server.md. See 68e5f36.
Fixed: the implicit zero-argument default now retries with just the surviving address if one of `[::]:2112`/`0.0.0.0:2112` fails to bind (identified via `ServerSetError::ListenerSetup`'s `addr`), instead of failing the whole invocation. An explicit `--bind` list keeps the ordinary all-or-nothing behavior — the fallback only applies when `--bind` was not given. Added unit tests for the decision logic and documented the fallback in docs/irtt-server.md. See 68e5f36.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: send/receive/timeout-poll errors are now matched explicitly and surfaced via eprintln + break instead of unwrap_or_default(), matching the tokio_client fix from the previous round. See 68e5f36.
Fixed: send/receive/timeout-poll errors are now matched explicitly and surfaced via eprintln + break instead of unwrap_or_default(), matching the tokio_client fix from the previous round. See 68e5f36.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: added doc-scrape-examples = true to all three irtt-client example targets. Confirmed with cargo +nightly doc -p irtt-client --all-features --no-deps -Zunstable-options -Zrustdoc-scrape-examples — the "did not scrape" warning is gone. irtt-server needed no change since it has no dev-dependencies and wasn't affected. See 68e5f36.
Fixed: added `doc-scrape-examples = true` to all three irtt-client example targets. Confirmed with `cargo +nightly doc -p irtt-client --all-features --no-deps -Zunstable-options -Zrustdoc-scrape-examples` — the "did not scrape" warning is gone. irtt-server needed no change since it has no dev-dependencies and wasn't affected. See 68e5f36.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: reversed the wording so rd_us/sd_us are documented as the emitted column names, with receive_delay_us/send_delay_us as the longer selector aliases, matching Column::name(). See 68e5f36.
Fixed: reversed the wording so `rd_us`/`sd_us` are documented as the emitted column names, with `receive_delay_us`/`send_delay_us` as the longer selector aliases, matching `Column::name()`. See 68e5f36.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `68e5f36a3a`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
The project does implement a server-side payload-fill policy: echo replies honor the session's negotiated fill, including the fixed fallback behavior documented later in this change. What is absent is an operator-configurable server fill policy, so listing the unqualified capability among things this project does not implement can mislead readers into thinking the server cannot generate negotiated payload fills at all; qualify this entry as configurable policy.
**<sub><sub></sub></sub> Qualify the missing server-fill capability**
The project does implement a server-side payload-fill policy: echo replies honor the session's negotiated fill, including the fixed fallback behavior documented later in this change. What is absent is an operator-configurable server fill policy, so listing the unqualified capability among things this project does not implement can mislead readers into thinking the server cannot generate negotiated payload fills at all; qualify this entry as configurable policy.
AGENTS.md reference: [AGENTS.md:L60-L60](https://github.com/Lochnair/irtt-rs/blob/68e5f36a3a9815bb20610a5bdbac8964a2d81e24/AGENTS.md#L60-L60)
Useful? React with 👍 / 👎.
On hosts where IPv6 sockets default to dual-stack, this retry can still fail when the IPv4 default listener is unavailable. The initial two-address bind makes [::]:2112 IPv6-only, but after the IPv4 bind fails—for example because another process occupies only 0.0.0.0:2112—retrying [::]:2112 alone no longer enables IPV6_V6ONLY, so it also tries to claim the occupied IPv4 port and exits. Preserve the family isolation used by the original pair so zero-argument startup can actually continue on the available IPv6 family.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Keep the surviving IPv6 listener IPv6-only**
On hosts where IPv6 sockets default to dual-stack, this retry can still fail when the IPv4 default listener is unavailable. The initial two-address bind makes `[::]:2112` IPv6-only, but after the IPv4 bind fails—for example because another process occupies only `0.0.0.0:2112`—retrying `[::]:2112` alone no longer enables `IPV6_V6ONLY`, so it also tries to claim the occupied IPv4 port and exits. Preserve the family isolation used by the original pair so zero-argument startup can actually continue on the available IPv6 family.
Useful? React with 👍 / 👎.
Exempt --columns default from the all-events claim
An explicit --columns default selection does not show all event rows: OutputConfig::new deliberately treats it like an omitted column option, sets default_table_rows, and continues hiding echo_sent rows in table output. Qualify this claim to custom or all selections so users do not expect --columns default to expose events that remain suppressed.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Exempt `--columns default` from the all-events claim**
An explicit `--columns default` selection does not show all event rows: `OutputConfig::new` deliberately treats it like an omitted column option, sets `default_table_rows`, and continues hiding `echo_sent` rows in table output. Qualify this claim to custom or `all` selections so users do not expect `--columns default` to expose events that remain suppressed.
Useful? React with 👍 / 👎.
When a run is interrupted, the implementation suppresses the “no managed target completed successfully” error via the !interrupted condition, so, for example, interrupting a continuous run whose only target never opened exits with status 0. This sentence currently says such a run exits nonzero and contradicts the preceding interrupted-run exception; qualify the no-success condition as applying only to uninterrupted runs.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Qualify the no-success exit-status condition**
When a run is interrupted, the implementation suppresses the “no managed target completed successfully” error via the `!interrupted` condition, so, for example, interrupting a continuous run whose only target never opened exits with status 0. This sentence currently says such a run exits nonzero and contradicts the preceding interrupted-run exception; qualify the no-success condition as applying only to uninterrupted runs.
Useful? React with 👍 / 👎.
When --min-interval exceeds one quarter of --idle-timeout, negotiation caps the session interval below the configured minimum and the rate limiter replenishes at that shorter negotiated interval, not at --min-interval. Describing the configured floor as the refill cadence gives operators the wrong effective rate policy for this supported configuration; mention that the negotiated interval becomes the cadence when the idle cap wins.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Document the capped-interval refill cadence**
When `--min-interval` exceeds one quarter of `--idle-timeout`, negotiation caps the session interval below the configured minimum and the rate limiter replenishes at that shorter negotiated interval, not at `--min-interval`. Describing the configured floor as the refill cadence gives operators the wrong effective rate policy for this supported configuration; mention that the negotiated interval becomes the cadence when the idle cap wins.
Useful? React with 👍 / 👎.
Fixed: added ServerSet::bind_with_family_context, which decides IPV6_V6ONLY against a caller-supplied context instead of the addresses actually being bound (bind() is now this method using the addresses as their own context, so its existing behavior is unchanged). The CLI's fallback now passes the original two-address default as context, so a surviving lone IPv6 listener stays IPV6_V6ONLY instead of reverting to dual-stack. Added a test that reproduces the dual-stack collision without the context and confirms it's avoided with it. See 17ef443.
Fixed: added `ServerSet::bind_with_family_context`, which decides IPV6_V6ONLY against a caller-supplied context instead of the addresses actually being bound (`bind()` is now this method using the addresses as their own context, so its existing behavior is unchanged). The CLI's fallback now passes the original two-address default as context, so a surviving lone IPv6 listener stays IPV6_V6ONLY instead of reverting to dual-stack. Added a test that reproduces the dual-stack collision without the context and confirms it's avoided with it. See 17ef443.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: clarified that --columns default is equivalent to omitting the option and keeps echo_sent hidden for table output; only a different explicit selection exposes it. See 17ef443.
Fixed: clarified that `--columns default` is equivalent to omitting the option and keeps echo_sent hidden for table output; only a different explicit selection exposes it. See 17ef443.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: qualified the no-success exit condition to uninterrupted runs, with an explicit example (interrupting a continuous run whose only target never opened still exits 0). See 17ef443.
Fixed: qualified the no-success exit condition to uninterrupted runs, with an explicit example (interrupting a continuous run whose only target never opened still exits 0). See 17ef443.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: clarified that the reply-allowance refill cadence tracks the negotiated interval, which can be reduced below --min-interval by the quarter-idle-timeout cap. See 17ef443.
Fixed: clarified that the reply-allowance refill cadence tracks the negotiated interval, which can be reduced below --min-interval by the quarter-idle-timeout cap. See 17ef443.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: qualified the entry to specifically call out the missing capability as an operator-configurable fill policy (like upstream's --fill/--allow-fills), while noting this project's server does honor negotiated fill requests with a fixed fallback pattern. See 17ef443.
Fixed: qualified the entry to specifically call out the missing capability as an operator-configurable fill policy (like upstream's --fill/--allow-fills), while noting this project's server does honor negotiated fill requests with a fixed fallback pattern. See 17ef443.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `17ef443573`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Make the IPv6 family-context test capability-aware
On an IPv4-only host—the exact environment the new default-bind fallback is intended to support—bind_with_family_context(...).await.unwrap() still fails because no IPv6 socket can be created. Conversely, on a host where lone IPv6 sockets are already IPv6-only, this assertion fails because the first bind legitimately succeeds despite the IPv4 occupant. Detect and skip unsupported/non-dual-stack environments or test the option-selection logic without assuming the host's IPv6 defaults.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Make the IPv6 family-context test capability-aware**
On an IPv4-only host—the exact environment the new default-bind fallback is intended to support—`bind_with_family_context(...).await.unwrap()` still fails because no IPv6 socket can be created. Conversely, on a host where lone IPv6 sockets are already IPv6-only, this assertion fails because the first bind legitimately succeeds despite the IPv4 occupant. Detect and skip unsupported/non-dual-stack environments or test the option-selection logic without assuming the host's IPv6 defaults.
Useful? React with 👍 / 👎.
Limit the finite-mode median promise to cumulative snapshots
When StatsConfig::finite() is combined with rolling_count or rolling_time, the rolling snapshots still report no medians because rolling::snapshot deliberately builds its CoreStats with SampleMode::RunningOnly. Since the README introduces both cumulative and rolling snapshots, saying every metric gets an exact median gives rolling-window users the wrong API contract; qualify this as applying to cumulative metrics with samples and note that rolling metrics remain running-only.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Limit the finite-mode median promise to cumulative snapshots**
When `StatsConfig::finite()` is combined with `rolling_count` or `rolling_time`, the rolling snapshots still report no medians because `rolling::snapshot` deliberately builds its `CoreStats` with `SampleMode::RunningOnly`. Since the README introduces both cumulative and rolling snapshots, saying every metric gets an exact median gives rolling-window users the wrong API contract; qualify this as applying to cumulative metrics with samples and note that rolling metrics remain running-only.
Useful? React with 👍 / 👎.
Qualify the structured-output all-columns statement
With a command such as --format csv --columns event,seq --verbose, structured output contains only the explicitly selected columns, because parse_columns returns that custom list. Structured formats merely default to all columns; saying they already include every column misdocuments supported custom-column output. Clarify that --verbose has no effect on structured formats regardless of whether their default or a custom selection is used.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Qualify the structured-output all-columns statement**
With a command such as `--format csv --columns event,seq --verbose`, structured output contains only the explicitly selected columns, because `parse_columns` returns that custom list. Structured formats merely default to all columns; saying they already include every column misdocuments supported custom-column output. Clarify that `--verbose` has no effect on structured formats regardless of whether their default or a custom selection is used.
Useful? React with 👍 / 👎.
Exclude unknown irtt- names from dispatcher fallthrough*
If the dispatcher is copied or symlinked as an unrecognized name such as irtt-custom, dispatch_from_argv returns an unknown-applet error before examining the client/tui/server subcommand. Thus irtt-custom server does not fall through as the phrase “any other name” promises, and this directly contradicts the preceding sentence; qualify the fallthrough as applying only to names that do not start with irtt-.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Exclude unknown irtt-* names from dispatcher fallthrough**
If the dispatcher is copied or symlinked as an unrecognized name such as `irtt-custom`, `dispatch_from_argv` returns an unknown-applet error before examining the `client`/`tui`/`server` subcommand. Thus `irtt-custom server` does not fall through as the phrase “any other name” promises, and this directly contradicts the preceding sentence; qualify the fallthrough as applying only to names that do not start with `irtt-`.
Useful? React with 👍 / 👎.
Fixed: added a small capability probe (host_ipv6_defaults_to_dual_stack) and skip the test wherever it can't actually demonstrate the collision — no local IPv6 support at all, or IPv6 sockets that are already IPv6-only by default. See 428e46d.
Fixed: added a small capability probe (`host_ipv6_defaults_to_dual_stack`) and skip the test wherever it can't actually demonstrate the collision — no local IPv6 support at all, or IPv6 sockets that are already IPv6-only by default. See 428e46d.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: clarified that --verbose has no effect on structured formats regardless of whether their default or a custom --columns selection is in use, since a custom selection can itself be a subset. See 428e46d.
Fixed: clarified that --verbose has no effect on structured formats regardless of whether their default or a custom --columns selection is in use, since a custom selection can itself be a subset. See 428e46d.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: scoped the exact-median claim to cumulative snapshots and noted rolling-window snapshots are always running-only (SampleMode::RunningOnly in rolling::snapshot_window), regardless of which StatsConfig produced them. See 428e46d.
Fixed: scoped the exact-median claim to cumulative snapshots and noted rolling-window snapshots are always running-only (SampleMode::RunningOnly in rolling::snapshot_window), regardless of which StatsConfig produced them. See 428e46d.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: corrected the fallthrough description — only irtt-rs itself, or a name that doesn't start with irtt- at all, falls through to subcommand parsing; any other irtt-* name is rejected before argv[1] is examined. Added the irtt-custom example. See 428e46d.
Fixed: corrected the fallthrough description — only irtt-rs itself, or a name that doesn't start with irtt- at all, falls through to subcommand parsing; any other irtt-* name is rejected before argv[1] is examined. Added the irtt-custom example. See 428e46d.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair
(Migrated from github.com)
left a comment
Copy link
Copy source
The implicit default-bind fallback should be narrowed before merge. The zero-argument convenience default may degrade to one address family only when the other family is genuinely unavailable on the host. AddrInUse, permission failures, wildcard source-selection/setup failures, and unknown listener setup errors should remain fatal. Explicit --bind stays all-or-nothing.
Please prefer simplifying the design over preserving retry machinery introduced solely for prior review comments. With a narrow family-unavailable predicate, ServerSet::bind_with_family_context() and its collision test likely become unnecessary again.
The implicit default-bind fallback should be narrowed before merge. The zero-argument convenience default may degrade to one address family only when the other family is genuinely unavailable on the host. `AddrInUse`, permission failures, wildcard source-selection/setup failures, and unknown listener setup errors should remain fatal. Explicit `--bind` stays all-or-nothing.
Please prefer simplifying the design over preserving retry machinery introduced solely for prior review comments. With a narrow family-unavailable predicate, `ServerSet::bind_with_family_context()` and its collision test likely become unnecessary again.
This fallback is currently too broad: any ListenerSetup failure for one of the implicit defaults causes a retry with the other family. Please only fall back when the underlying bind error specifically means that address family is unavailable/unsupported on this host. AddrInUse, permission errors, wildcard source-selection/setup failures, and unknown failures should abort startup. If fallback is restricted that way, the bind_with_family_context() retry path should no longer be needed, since we should never retry merely because the sibling family's port is occupied.
This fallback is currently too broad: any `ListenerSetup` failure for one of the implicit defaults causes a retry with the other family. Please only fall back when the underlying bind error specifically means that address family is unavailable/unsupported on this host. `AddrInUse`, permission errors, wildcard source-selection/setup failures, and unknown failures should abort startup. If fallback is restricted that way, the `bind_with_family_context()` retry path should no longer be needed, since we should never retry merely because the sibling family's port is occupied.
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `304d322607`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
On an IPv6-only host or sandbox where creating an AF_INET socket is unavailable, this assertion fails even though the new fallback path deliberately supports losing either member of the implicit default pair. The test checks an operating-system capability rather than a crate invariant, so remove it or skip it when IPv4 is unavailable instead of making the workspace test suite require IPv4.
**<sub><sub></sub></sub> Make the capability test tolerate IPv4-less hosts**
On an IPv6-only host or sandbox where creating an `AF_INET` socket is unavailable, this assertion fails even though the new fallback path deliberately supports losing either member of the implicit default pair. The test checks an operating-system capability rather than a crate invariant, so remove it or skip it when IPv4 is unavailable instead of making the workspace test suite require IPv4.
AGENTS.md reference: [AGENTS.md:L105-L112](https://github.com/Lochnair/irtt-rs/blob/304d322607e8572cb53a42b714ee08518ff02a60/AGENTS.md#L105-L112)
Useful? React with 👍 / 👎.
When an operator presses p, toggle_pause only suppresses scheduled rendering; the managed client continues sending probes, draining events, and advancing statistics and graph history. Calling this simply “Toggle pause” can make users believe network probing has stopped, so label it as a display/redraw pause.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Describe `p` as pausing only the display**
When an operator presses `p`, `toggle_pause` only suppresses scheduled rendering; the managed client continues sending probes, draining events, and advancing statistics and graph history. Calling this simply “Toggle pause” can make users believe network probing has stopped, so label it as a display/redraw pause.
Useful? React with 👍 / 👎.
Fixed: removed the test — it was checking an OS capability, not a crate invariant, and would have wrongly failed the whole point of the fallback on an IPv6-only host. Folded into the relevant commit as part of a squash/rebase cleanup of this PR's history.
Fixed: removed the test — it was checking an OS capability, not a crate invariant, and would have wrongly failed the whole point of the fallback on an IPv6-only host. Folded into the relevant commit as part of a squash/rebase cleanup of this PR's history.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: reworded to "Toggle display pause (probing continues; only redraws stop)", matching toggle_pause's actual effect on scheduled rendering only. Folded into the relevant commit as part of a squash/rebase cleanup of this PR's history.
Fixed: reworded to "Toggle display pause (probing continues; only redraws stop)", matching toggle_pause's actual effect on scheduled rendering only. Folded into the relevant commit as part of a squash/rebase cleanup of this PR's history.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `fd792a55dd`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Describe the refill cadence as the shorter interval
With an ordinary configuration where the negotiated interval exceeds the configured floor—for example, a 1 s negotiated interval and the default 10 ms minimum—this says allowance refills every second, but RateState::new uses configured.min(params.interval_ns) and therefore refills every 10 ms. The previous thread's documentation fix overcorrected the capped-interval case; the current implementation is fresh evidence that the rule is the shorter of the configured minimum and negotiated interval. Document that formula so operators do not infer a substantially stricter rate limit.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Describe the refill cadence as the shorter interval**
With an ordinary configuration where the negotiated interval exceeds the configured floor—for example, a 1 s negotiated interval and the default 10 ms minimum—this says allowance refills every second, but `RateState::new` uses `configured.min(params.interval_ns)` and therefore refills every 10 ms. The previous thread's documentation fix overcorrected the capped-interval case; the current implementation is fresh evidence that the rule is the shorter of the configured minimum and negotiated interval. Document that formula so operators do not infer a substantially stricter rate limit.
Useful? React with 👍 / 👎.
The purported full list omits a supported shared option: running irtt-tui --help reports --length <LENGTH> as “UDP payload length,” because TuiArgs flattens the same CommonClientArgs as the stream client. Users relying on this manual can therefore miss that the TUI can request a specific probe payload size; include --length or avoid describing the list as complete.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Include `--length` in the shared-option list**
The purported full list omits a supported shared option: running `irtt-tui --help` reports `--length <LENGTH>` as “UDP payload length,” because `TuiArgs` flattens the same `CommonClientArgs` as the stream client. Users relying on this manual can therefore miss that the TUI can request a specific probe payload size; include `--length` or avoid describing the list as complete.
Useful? React with 👍 / 👎.
Fixed: rewrote to document the actual formula (min(configured floor, negotiated interval)), including the ordinary case where the floor is normally shorter, since the previous wording overcorrected into implying the negotiated value always wins. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
Fixed: rewrote to document the actual formula (min(configured floor, negotiated interval)), including the ordinary case where the floor is normally shorter, since the previous wording overcorrected into implying the negotiated value always wins. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: added --length to the shared-option list. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
When --min-interval exceeds one quarter of --idle-timeout, such as 5s with an 8s timeout, negotiation caps the interval to 2s and RateState::new refills at that shorter negotiated interval. The live --help output still calls the configured 5s floor the refill cadence, even though the manual was corrected to document the shorter-of formula, so operators are given the wrong effective rate policy.
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
https://github.com/Lochnair/irtt-rs/blob/630ed3daacd485ad7b63d50a319ca3965652a526/crates/irtt-app/src/cmd/server/args.rs#L60
**<sub><sub></sub></sub> Describe the effective refill cadence in server help**
When `--min-interval` exceeds one quarter of `--idle-timeout`, such as `5s` with an `8s` timeout, negotiation caps the interval to `2s` and `RateState::new` refills at that shorter negotiated interval. The live `--help` output still calls the configured `5s` floor the refill cadence, even though the manual was corrected to document the shorter-of formula, so operators are given the wrong effective rate policy.
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
If the socket fails while sending the final close—for example, the interface goes down after the last successful probe—Client::close() returns an error and deliberately leaves the session open for retry, but this if let Ok silently discards it. The example then exits without a diagnostic while the remote session remains allocated until idle expiry; the Tokio example has the same pattern, so both should surface their final cleanup failure just as they now surface send, receive, and timeout-poll failures.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Report close failures in the client examples**
If the socket fails while sending the final close—for example, the interface goes down after the last successful probe—`Client::close()` returns an error and deliberately leaves the session open for retry, but this `if let Ok` silently discards it. The example then exits without a diagnostic while the remote session remains allocated until idle expiry; the Tokio example has the same pattern, so both should surface their final cleanup failure just as they now surface send, receive, and timeout-poll failures.
Useful? React with 👍 / 👎.
Detect families that cannot bind, not only missing socket domains
On Linux systems where IPv6 is disabled through interface/sysctl configuration, creating an AF_INET6 datagram socket can still succeed while binding [::]:2112 fails with EADDRNOTAVAIL. This probe therefore reports the family as available, so implicit_default_fallback rejects the fallback and the zero-argument server exits instead of serving the usable IPv4 default. Fresh evidence relative to the earlier IPv4-only-host finding is that the replacement helper tests only Socket::new and never the wildcard bind capability that actually failed.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Detect families that cannot bind, not only missing socket domains**
On Linux systems where IPv6 is disabled through interface/sysctl configuration, creating an `AF_INET6` datagram socket can still succeed while binding `[::]:2112` fails with `EADDRNOTAVAIL`. This probe therefore reports the family as available, so `implicit_default_fallback` rejects the fallback and the zero-argument server exits instead of serving the usable IPv4 default. Fresh evidence relative to the earlier IPv4-only-host finding is that the replacement helper tests only `Socket::new` and never the wildcard bind capability that actually failed.
Useful? React with 👍 / 👎.
Fixed: address_family_available now actually binds the wildcard address of the family on an ephemeral port instead of only constructing a bare socket, so administratively-disabled-per-interface IPv6 (constructible AF_INET6 socket, every bind EADDRNOTAVAIL) is now correctly detected as unavailable. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
Fixed: address_family_available now actually binds the wildcard address of the family on an ephemeral port instead of only constructing a bare socket, so administratively-disabled-per-interface IPv6 (constructible AF_INET6 socket, every bind EADDRNOTAVAIL) is now correctly detected as unavailable. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Fixed: both examples now match on client.close()'s result and eprintln the error instead of silently discarding it via if-let-Ok, matching how send/receive/timeout-poll failures are already surfaced. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
Fixed: both examples now match on client.close()'s result and eprintln the error instead of silently discarding it via if-let-Ok, matching how send/receive/timeout-poll failures are already surfaced. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
irtt-servernow has sensible zero-argument defaults: no--bindbinds the wildcard IRTT port on both address families ([::]:2112then0.0.0.0:2112) where wildcard reply-source selection is supported. Any explicit--bindfully replaces the defaults; repeated explicit binds are unchanged. No protocol, measurement, or session behavior changed otherwise.docs/irtt-rs.md,docs/irtt-client.md,docs/irtt-tui.md,docs/irtt-server.md.irtt-rs,irtt-client,irtt-server,irtt-stats,irtt-proto) now has its own crates.io README instead of inheriting the root README.examples/targets forirtt-client,irtt-server, andirtt-stats(irtt-protointentionally has none). docs.rs rustdoc example scraping enabled forirtt-client/irtt-server.Test plan
cargo fmt --all -- --checkcargo test --workspace --lockedcargo clippy --workspace --all-targets --all-features --locked -- -D warningsRUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --lockedgit diff --checkcargo check -p irtt-rs --no-default-features(and again withclient/server/tuifeatures individually)cargo package --listper publishable crate confirms each crate's own README and examples are included🤖 Generated with Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
a4dc933184ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.Client::send_probeis caller-paced, but this loop calls it again immediately after each receive instead of waiting fornext_send_deadline(). With a reachable low-latency server,recv_once()returns immediately and the example sends probes at roughly RTT speed rather than the configured 200 ms interval, producing invalid pacing measurements and potentially exhausting the server's burst allowance.Useful? React with 👍 / 👎.
@@ -0,0 +1,133 @@//! Low-level Tokio client example (`tokio` feature).When any echo reply is lost or rate-limited,
recv().awaitremains pending indefinitely, so the loop never reachespoll_timeouts(), rechecks the two-second run completion, or closes the session. This is readily triggered because the preceding caller-paced sends also run without waiting fornext_send_deadline()and exhaust the default server burst allowance; the documented runnable example then hangs rather than finishing. Drive the receive together with the next send/timeout deadline instead of awaiting it unconditionally.AGENTS.md reference: AGENTS.md:L78-L78
Useful? React with 👍 / 👎.
@@ -0,0 +8,4 @@This crate does not open sessions or drive sockets. Feed it the `ClientEvent`stream produced by an `irtt-client` `Client`, `AsyncClient`, or managedsession via `StatsCollector::process`, and read back a `Snapshot`.A managed session may contain multiple targets, but this instruction reads as though their
ClientEventvalues can all be fed into oneStatsCollector. The collector has one packet-count and sequence/IPDV namespace, while each target starts its sequence numbers at zero; combining a managed event stream therefore merges unrelated measurements and pairs probes from different targets, corrupting the resulting snapshot. Document that callers must route eachManagedEvent::Clientto a separate collector keyed by its target, as the CLI does.Useful? React with 👍 / 👎.
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.Fixed: the loop now checks
next_send_deadline()before callingsend_probe(), so probes are paced at the configured interval instead of as fast as the loop spins. See a981487.🤖 Addressed by Claude Code
@@ -0,0 +1,133 @@//! Low-level Tokio client example (`tokio` feature).Fixed: the receive wait is now bounded with
tokio::select!against a sleep to the next send/timeout deadline instead of awaitingrecv()unconditionally, andsend_probe()is only called once its deadline is due. See a981487.🤖 Addressed by Claude Code
@@ -0,0 +8,4 @@This crate does not open sessions or drive sockets. Feed it the `ClientEvent`stream produced by an `irtt-client` `Client`, `AsyncClient`, or managedsession via `StatsCollector::process`, and read back a `Snapshot`.Fixed: added a paragraph clarifying that
StatsCollectoris scoped to a single target and a multi-target managed session needs one collector per target, since feeding multiple targets into one collector would pair up unrelated probes. See a981487.🤖 Addressed by Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
a981487ed0ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,133 @@//! Low-level Tokio client example (`tokio` feature).The new
Instant::now() + client.probe_timeout()fallback does not represent the next pending probe timeout. Once the send schedule finishes, if an early probe was lost but later probes replied,recv()and this sleep can both remain pending until a full timeout from the current instant, sopoll_timeouts()classifies the loss and completes the run substantially late (up to about two extra seconds with this example's configuration). Wake at the earliest pending timeout, or poll on a short bounded cadence if that deadline is not publicly exposed.AGENTS.md reference: AGENTS.md:L78-L78
Useful? React with 👍 / 👎.
@@ -0,0 +1,133 @@//! Low-level Tokio client example (`tokio` feature).Fixed: replaced the
Instant::now() + probe_timeout()fallback with a fixed 50ms poll cadence, sinceAsyncClientdoesn't expose the earliest pending probe's own deadline. This bounds how late a lost probe can be classified to the poll interval instead of a fullprobe_timeout()from whenever the loop happened to run. See 17422e0.🤖 Addressed by Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
17422e0dc3ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.Even with send pacing corrected, this hard deadline expires after three seconds while the example retains the default four-second
probe_timeout. Therefore, if any probe reply is lost, not even the first probe can reach its timeout before the loop exits;close()then transitions the session to closed and discards the pending probes without emitting theEchoLossevents this example is set up to print. Base the safety deadline on the run duration plusclient.probe_timeout(), or otherwise keep polling until pending probes are classified.AGENTS.md reference: AGENTS.md:L78-L78
Useful? React with 👍 / 👎.
@@ -0,0 +1,133 @@//! Low-level Tokio client example (`tokio` feature).Separately from the now-bounded receive branch, a persistent
send_probe()error (for example, the interface going down after Open) is discarded here. Failed sends intentionally retain the prepared probe without advancing the schedule, sonext_send_deadline()remains overdue andis_run_complete()remains false; this loop consequently retries every 50 ms forever instead of reporting the error or exiting. Surface the error and terminate the example rather than converting it to an empty event list.AGENTS.md reference: AGENTS.md:L78-L78
Useful? React with 👍 / 👎.
@@ -0,0 +1,224 @@# irtt-clientWhen a finite run uses
--format csv,tsv, orjsonl, no final summary is printed:OutputFormat::prints_summary()returns true only for table output, andStreamOutput::print_summary()enforces that condition. Saying a summary is “always printed” therefore gives structured-output users the wrong contract and contradicts the format documentation immediately below; qualify this statement as applying only to table output.Useful? React with 👍 / 👎.
@@ -0,0 +1,224 @@# irtt-clientFixed: qualified both mentions of the final summary to table output specifically, matching the OUTPUT section's existing note that CSV/TSV/JSONL never print one. See 86468f0.
🤖 Addressed by Claude Code
@@ -0,0 +1,133 @@//! Low-level Tokio client example (`tokio` feature).Fixed: send/receive/timeout-poll errors are now matched explicitly and surfaced via eprintln + break instead of being discarded by unwrap_or_default(), so a persistent send failure reports an error and exits rather than retrying every 50ms forever. See 86468f0.
🤖 Addressed by Claude Code
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.Fixed: the safety deadline is now
RUN_DURATION + client.probe_timeout()instead of a fixed 3s guess, so a lost reply near the end of the run can actually reach its timeout and be classified/printed as EchoLoss before the loop gives up and closes. See 86468f0.🤖 Addressed by Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
86468f0ce9ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
On an IPv4-only host, such as a Linux container with IPv6 disabled, binding
[::]:2112fails. Sinceservepasses this generated pair to the all-or-nothingServerSet::bind, the subsequent IPv4 wildcard is never served, so the newly advertised zero-argument invocation exits even though IPv4 wildcard serving is supported. Construct the implicit defaults so an unavailable address family does not prevent startup on the available family, while retaining all-or-nothing behavior for explicit binds.AGENTS.md reference: AGENTS.md:L58-L60
Useful? React with 👍 / 👎.
@@ -30,0 +36,4 @@[[example]]name = "tokio_client"required-features = ["tokio"]doc-scrape-examples = trueThis docs.rs command does not actually scrape any of the three client examples because the package has dev-dependencies and none of its
[[example]]targets explicitly opts in. Running the configuredcargo doc -Zunstable-options -Zrustdoc-scrape-examplesreports: “Rustdoc did not scrape the following examples because they require dev-dependencies” and instructs addingdoc-scrape-examples = true; consequently the published API documentation will omit the example usage this metadata is intended to add.Useful? React with 👍 / 👎.
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.When the UDP send starts failing after Open, for example after the interface goes down,
unwrap_or_default()converts theClientErrorinto an empty event list. A failed send retains the scheduled probe without advancing the schedule, so this loop silently retries until its safety deadline and then returns frommainsuccessfully, presenting an incomplete run with no diagnostic. Handle the error and terminate/report it as the Tokio example does.AGENTS.md reference: crates/irtt-client/AGENTS.md:L83-L87
Useful? React with 👍 / 👎.
@@ -0,0 +1,224 @@# irtt-clientThe canonical serialized column names are
rd_usandsd_us: selecting either long spelling is accepted by the parser, but CSV/TSV headers and JSONL keys are still emitted usingColumn::name(), which returns the abbreviated spelling. Describingreceive_delay_usandsend_delay_usas the columns and the emitted names as aliases gives structured-output consumers the wrong schema; reverse this wording so the documented keys match the actual output.Useful? React with 👍 / 👎.
Fixed: the implicit zero-argument default now retries with just the surviving address if one of
[::]:2112/0.0.0.0:2112fails to bind (identified viaServerSetError::ListenerSetup'saddr), instead of failing the whole invocation. An explicit--bindlist keeps the ordinary all-or-nothing behavior — the fallback only applies when--bindwas not given. Added unit tests for the decision logic and documented the fallback in docs/irtt-server.md. See 68e5f36.🤖 Addressed by Claude Code
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.Fixed: send/receive/timeout-poll errors are now matched explicitly and surfaced via eprintln + break instead of unwrap_or_default(), matching the tokio_client fix from the previous round. See 68e5f36.
🤖 Addressed by Claude Code
@@ -30,0 +36,4 @@[[example]]name = "tokio_client"required-features = ["tokio"]doc-scrape-examples = trueFixed: added
doc-scrape-examples = trueto all three irtt-client example targets. Confirmed withcargo +nightly doc -p irtt-client --all-features --no-deps -Zunstable-options -Zrustdoc-scrape-examples— the "did not scrape" warning is gone. irtt-server needed no change since it has no dev-dependencies and wasn't affected. See 68e5f36.🤖 Addressed by Claude Code
@@ -0,0 +1,224 @@# irtt-clientFixed: reversed the wording so
rd_us/sd_usare documented as the emitted column names, withreceive_delay_us/send_delay_usas the longer selector aliases, matchingColumn::name(). See 68e5f36.🤖 Addressed by Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
68e5f36a3aℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -1,26 +1,75 @@# irtt-rsThe project does implement a server-side payload-fill policy: echo replies honor the session's negotiated fill, including the fixed fallback behavior documented later in this change. What is absent is an operator-configurable server fill policy, so listing the unqualified capability among things this project does not implement can mislead readers into thinking the server cannot generate negotiated payload fills at all; qualify this entry as configurable policy.
AGENTS.md reference: AGENTS.md:L60-L60
Useful? React with 👍 / 👎.
@@ -36,7 +37,29 @@ async fn serve(shutdown_requested: &AtomicBool,On hosts where IPv6 sockets default to dual-stack, this retry can still fail when the IPv4 default listener is unavailable. The initial two-address bind makes
[::]:2112IPv6-only, but after the IPv4 bind fails—for example because another process occupies only0.0.0.0:2112—retrying[::]:2112alone no longer enablesIPV6_V6ONLY, so it also tries to claim the occupied IPv4 port and exits. Preserve the family isolation used by the original pair so zero-argument startup can actually continue on the available IPv6 family.Useful? React with 👍 / 👎.
@@ -0,0 +1,224 @@# irtt-client--columns defaultfrom the all-events claimAn explicit
--columns defaultselection does not show all event rows:OutputConfig::newdeliberately treats it like an omitted column option, setsdefault_table_rows, and continues hidingecho_sentrows in table output. Qualify this claim to custom orallselections so users do not expect--columns defaultto expose events that remain suppressed.Useful? React with 👍 / 👎.
When a run is interrupted, the implementation suppresses the “no managed target completed successfully” error via the
!interruptedcondition, so, for example, interrupting a continuous run whose only target never opened exits with status 0. This sentence currently says such a run exits nonzero and contradicts the preceding interrupted-run exception; qualify the no-success condition as applying only to uninterrupted runs.Useful? React with 👍 / 👎.
@@ -0,0 +1,261 @@# irtt-serverWhen
--min-intervalexceeds one quarter of--idle-timeout, negotiation caps the session interval below the configured minimum and the rate limiter replenishes at that shorter negotiated interval, not at--min-interval. Describing the configured floor as the refill cadence gives operators the wrong effective rate policy for this supported configuration; mention that the negotiated interval becomes the cadence when the idle cap wins.Useful? React with 👍 / 👎.
@@ -36,7 +37,29 @@ async fn serve(shutdown_requested: &AtomicBool,Fixed: added
ServerSet::bind_with_family_context, which decides IPV6_V6ONLY against a caller-supplied context instead of the addresses actually being bound (bind()is now this method using the addresses as their own context, so its existing behavior is unchanged). The CLI's fallback now passes the original two-address default as context, so a surviving lone IPv6 listener stays IPV6_V6ONLY instead of reverting to dual-stack. Added a test that reproduces the dual-stack collision without the context and confirms it's avoided with it. See 17ef443.🤖 Addressed by Claude Code
@@ -0,0 +1,224 @@# irtt-clientFixed: clarified that
--columns defaultis equivalent to omitting the option and keeps echo_sent hidden for table output; only a different explicit selection exposes it. See 17ef443.🤖 Addressed by Claude Code
@@ -0,0 +1,224 @@# irtt-clientFixed: qualified the no-success exit condition to uninterrupted runs, with an explicit example (interrupting a continuous run whose only target never opened still exits 0). See 17ef443.
🤖 Addressed by Claude Code
@@ -0,0 +1,261 @@# irtt-serverFixed: clarified that the reply-allowance refill cadence tracks the negotiated interval, which can be reduced below --min-interval by the quarter-idle-timeout cap. See 17ef443.
🤖 Addressed by Claude Code
@@ -1,26 +1,75 @@# irtt-rsFixed: qualified the entry to specifically call out the missing capability as an operator-configurable fill policy (like upstream's --fill/--allow-fills), while noting this project's server does honor negotiated fill requests with a fixed fallback pattern. See 17ef443.
🤖 Addressed by Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
17ef443573ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
On an IPv4-only host—the exact environment the new default-bind fallback is intended to support—
bind_with_family_context(...).await.unwrap()still fails because no IPv6 socket can be created. Conversely, on a host where lone IPv6 sockets are already IPv6-only, this assertion fails because the first bind legitimately succeeds despite the IPv4 occupant. Detect and skip unsupported/non-dual-stack environments or test the option-selection logic without assuming the host's IPv6 defaults.Useful? React with 👍 / 👎.
@@ -0,0 +1,38 @@# irtt-statsWhen
StatsConfig::finite()is combined withrolling_countorrolling_time, the rolling snapshots still report no medians becauserolling::snapshotdeliberately builds itsCoreStatswithSampleMode::RunningOnly. Since the README introduces both cumulative and rolling snapshots, saying every metric gets an exact median gives rolling-window users the wrong API contract; qualify this as applying to cumulative metrics with samples and note that rolling metrics remain running-only.Useful? React with 👍 / 👎.
@@ -0,0 +1,224 @@# irtt-clientWith a command such as
--format csv --columns event,seq --verbose, structured output contains only the explicitly selected columns, becauseparse_columnsreturns that custom list. Structured formats merely default to all columns; saying they already include every column misdocuments supported custom-column output. Clarify that--verbosehas no effect on structured formats regardless of whether their default or a custom selection is used.Useful? React with 👍 / 👎.
@@ -0,0 +1,64 @@# irtt-rsIf the dispatcher is copied or symlinked as an unrecognized name such as
irtt-custom,dispatch_from_argvreturns an unknown-applet error before examining theclient/tui/serversubcommand. Thusirtt-custom serverdoes not fall through as the phrase “any other name” promises, and this directly contradicts the preceding sentence; qualify the fallthrough as applying only to names that do not start withirtt-.Useful? React with 👍 / 👎.
Fixed: added a small capability probe (
host_ipv6_defaults_to_dual_stack) and skip the test wherever it can't actually demonstrate the collision — no local IPv6 support at all, or IPv6 sockets that are already IPv6-only by default. See 428e46d.🤖 Addressed by Claude Code
@@ -0,0 +1,224 @@# irtt-clientFixed: clarified that --verbose has no effect on structured formats regardless of whether their default or a custom --columns selection is in use, since a custom selection can itself be a subset. See 428e46d.
🤖 Addressed by Claude Code
@@ -0,0 +1,38 @@# irtt-statsFixed: scoped the exact-median claim to cumulative snapshots and noted rolling-window snapshots are always running-only (SampleMode::RunningOnly in rolling::snapshot_window), regardless of which StatsConfig produced them. See 428e46d.
🤖 Addressed by Claude Code
@@ -0,0 +1,64 @@# irtt-rsFixed: corrected the fallthrough description — only irtt-rs itself, or a name that doesn't start with irtt- at all, falls through to subcommand parsing; any other irtt-* name is rejected before argv[1] is examined. Added the irtt-custom example. See 428e46d.
🤖 Addressed by Claude Code
The implicit default-bind fallback should be narrowed before merge. The zero-argument convenience default may degrade to one address family only when the other family is genuinely unavailable on the host.
AddrInUse, permission failures, wildcard source-selection/setup failures, and unknown listener setup errors should remain fatal. Explicit--bindstays all-or-nothing.Please prefer simplifying the design over preserving retry machinery introduced solely for prior review comments. With a narrow family-unavailable predicate,
ServerSet::bind_with_family_context()and its collision test likely become unnecessary again.@@ -36,7 +37,29 @@ async fn serve(shutdown_requested: &AtomicBool,This fallback is currently too broad: any
ListenerSetupfailure for one of the implicit defaults causes a retry with the other family. Please only fall back when the underlying bind error specifically means that address family is unavailable/unsupported on this host.AddrInUse, permission errors, wildcard source-selection/setup failures, and unknown failures should abort startup. If fallback is restricted that way, thebind_with_family_context()retry path should no longer be needed, since we should never retry merely because the sibling family's port is occupied.💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
304d322607ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
On an IPv6-only host or sandbox where creating an
AF_INETsocket is unavailable, this assertion fails even though the new fallback path deliberately supports losing either member of the implicit default pair. The test checks an operating-system capability rather than a crate invariant, so remove it or skip it when IPv4 is unavailable instead of making the workspace test suite require IPv4.AGENTS.md reference: AGENTS.md:L105-L112
Useful? React with 👍 / 👎.
@@ -0,0 +1,83 @@# irtt-tuipas pausing only the displayWhen an operator presses
p,toggle_pauseonly suppresses scheduled rendering; the managed client continues sending probes, draining events, and advancing statistics and graph history. Calling this simply “Toggle pause” can make users believe network probing has stopped, so label it as a display/redraw pause.Useful? React with 👍 / 👎.
Fixed: removed the test — it was checking an OS capability, not a crate invariant, and would have wrongly failed the whole point of the fallback on an IPv6-only host. Folded into the relevant commit as part of a squash/rebase cleanup of this PR's history.
🤖 Addressed by Claude Code
@@ -0,0 +1,83 @@# irtt-tuiFixed: reworded to "Toggle display pause (probing continues; only redraws stop)", matching toggle_pause's actual effect on scheduled rendering only. Folded into the relevant commit as part of a squash/rebase cleanup of this PR's history.
🤖 Addressed by Claude Code
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
fd792a55ddℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,261 @@# irtt-serverWith an ordinary configuration where the negotiated interval exceeds the configured floor—for example, a 1 s negotiated interval and the default 10 ms minimum—this says allowance refills every second, but
RateState::newusesconfigured.min(params.interval_ns)and therefore refills every 10 ms. The previous thread's documentation fix overcorrected the capped-interval case; the current implementation is fresh evidence that the rule is the shorter of the configured minimum and negotiated interval. Document that formula so operators do not infer a substantially stricter rate limit.Useful? React with 👍 / 👎.
@@ -0,0 +1,83 @@# irtt-tui--lengthin the shared-option listThe purported full list omits a supported shared option: running
irtt-tui --helpreports--length <LENGTH>as “UDP payload length,” becauseTuiArgsflattens the sameCommonClientArgsas the stream client. Users relying on this manual can therefore miss that the TUI can request a specific probe payload size; include--lengthor avoid describing the list as complete.Useful? React with 👍 / 👎.
@@ -0,0 +1,261 @@# irtt-serverFixed: rewrote to document the actual formula (min(configured floor, negotiated interval)), including the ordinary case where the floor is normally shorter, since the previous wording overcorrected into implying the negotiated value always wins. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
🤖 Addressed by Claude Code
@@ -0,0 +1,83 @@# irtt-tuiFixed: added --length to the shared-option list. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
🤖 Addressed by Claude Code
💡 Codex Review
https://github.com/Lochnair/irtt-rs/blob/630ed3daacd485ad7b63d50a319ca3965652a526/crates/irtt-app/src/cmd/server/args.rs#L60
Describe the effective refill cadence in server help
When
--min-intervalexceeds one quarter of--idle-timeout, such as5swith an8stimeout, negotiation caps the interval to2sandRateState::newrefills at that shorter negotiated interval. The live--helpoutput still calls the configured5sfloor the refill cadence, even though the manual was corrected to document the shorter-of formula, so operators are given the wrong effective rate policy.ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.If the socket fails while sending the final close—for example, the interface goes down after the last successful probe—
Client::close()returns an error and deliberately leaves the session open for retry, but thisif let Oksilently discards it. The example then exits without a diagnostic while the remote session remains allocated until idle expiry; the Tokio example has the same pattern, so both should surface their final cleanup failure just as they now surface send, receive, and timeout-poll failures.Useful? React with 👍 / 👎.
@@ -218,6 +218,40 @@ impl ServerSet {}On Linux systems where IPv6 is disabled through interface/sysctl configuration, creating an
AF_INET6datagram socket can still succeed while binding[::]:2112fails withEADDRNOTAVAIL. This probe therefore reports the family as available, soimplicit_default_fallbackrejects the fallback and the zero-argument server exits instead of serving the usable IPv4 default. Fresh evidence relative to the earlier IPv4-only-host finding is that the replacement helper tests onlySocket::newand never the wildcard bind capability that actually failed.Useful? React with 👍 / 👎.
@@ -218,6 +218,40 @@ impl ServerSet {}Fixed: address_family_available now actually binds the wildcard address of the family on an ephemeral port instead of only constructing a bare socket, so administratively-disabled-per-interface IPv6 (constructible AF_INET6 socket, every bind EADDRNOTAVAIL) is now correctly detected as unavailable. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
🤖 Addressed by Claude Code
@@ -0,0 +1,110 @@//! Runtime-free blocking client example.Fixed: both examples now match on client.close()'s result and eprintln the error instead of silently discarding it via if-let-Ok, matching how send/receive/timeout-poll failures are already surfaced. Folded into the relevant commit as part of this PR's squash/rebase cleanup.
🤖 Addressed by Claude Code