Skip to content

fix(transport): the responder closes idle inbound links and counts every departure (#853, main port of v40.0.10) - #854

Merged
emooreatx merged 4 commits into
mainfrom
fix/853-inbound-reap-main
Oct 8, 2026
Merged

emooreatx merged 4 commits into
mainfrom
fix/853-inbound-reap-main

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Main port of v40.0.10 (release-40.0.10 dc5b296), cherry-picked without the release-only files. Closes #853.

Field read. The canonical on 0.5.224 (edge v40.0.8) grew ~95 inbound links per 5 min with the pool gauges at zero: #819's reap is initiator-only, and about a fifth of inbound links are never closed by older peers' pools. No events were lost and there is no implicit cap (max_links unset; the 1,024 mirror envelope only logs).

Change.

  • Direction per link from leviculum's own LinkEstablished { is_initiator }, so an inbound link is known before it is identified; direction on link_list() (UDL regenerated).
  • A responder-side idle reap of inbound links (inbound_idle_bound, default 120 s, 0 disables), exempting links with a transfer in either direction (incl. sender-side Resource progress), LXMF serve activity within one bound, and the 30 s identify window. Closes go through the node's authoritative link state.
  • inbound_links / outbound_links gauges and inbound_ / outbound_link_closed_by_reason with every departure token: idle_expired, pool_idle_expired, pool_full, local_teardown, normal, peer_closed, timeout, stale, invalid_proof, channel_exhausted, blackholed, other, vanished.
  • A reconciliation pass each tick against node.link_is_established, counting links the node dropped without an event as vanished (2 s grace so a queued LinkClosed isn't misfiled).
  • Differences from the tag: the LXMF exemption is bounded (one idle bound after the last LXMF activity; witness (f) fails both for-life and removed); hold_link_open is dropped (no caller; feat(transport): the A/V spine on the live node (#805 item 4, #720) #813 tracks A/V links through LinkPlane::Av).

Not projected to PyO3/UniFFI yet on main: the P0 bundle machinery (#848) isn't here, so the new gauges and reason families live in EdgeMetrics / EdgeMetricsBundle until #848 lands.

Conflict to carry, whichever of this and #813 merges second: #813 routes A/V frames to deliver_av_frame, bypassing attribute_and_deliver, the only writer of link_last_inbound_at. A peer-opened A/V link carrying media would read as idle and be reaped 120 s into a call. Fix: stamp link_last_inbound_at in deliver_av_frame (preferred) or treat LinkPlane::Av as busy.

Witness tests/inbound_reap_853.rs (two loopback nodes): (a) a quiet dialed link is reaped and the dialer self-heals on its next send; (b) a link mid-receive survives; (c) bound 0 disables; (d) gauges by direction; (e) a silent close is counted vanished; (f) the LXMF bound. Mutation runs: (a)(b)(e) fail with the reap + reconciliation removed; (f) fails both with the exemption for-life and removed.

Gates: fmt; clippy --all-targets -D warnings on CI's three sets; lib filtered 84; integration 57; FFI 30; 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK

…nk departure is counted (#853)

Main port of v40.0.10 (971a605 + dc5b296), with two design changes.

- Link direction from LinkEstablished { is_initiator }; inbound_links /
  outbound_links gauges; EdgeLinkInfo.direction in the UDL and the
  regenerated bindings.
- Responder-side idle reap on the pool tick (inbound_link_idle_bound,
  default 120 s, 0 disables), node-authoritative teardown; busy:
  link_in_flight, inbound transfers, outbound ships, sender-side
  resource progress, LXMF serve activity, the attribution handshake.
- {inbound,outbound}_link_closed_by_reason with one token per way a
  link leaves, and reconcile_vanished_links pruning links the node no
  longer holds as `vanished`.

Changed from the release line:
- The LXMF serve exemption is BOUNDED: a link is busy for one inbound
  idle bound after its last LXMF activity (a request, its response
  handed to the link), then the normal rule applies. A sender-side
  resource still making progress (e.g. a large LXMF response) is busy
  too. Witness (f): fails with the exemption made for-life and with it
  removed.
- hold_link_open / release_link_hold removed: no A/V caller on main,
  and #813 tracks link ownership through LinkPlane::Av + AvSink.
- Main has no P0 telemetry (#820) yet, so the new metrics are in
  EdgeMetrics / EdgeMetricsBundle only; their PyO3 / UniFFI flattening
  arrives with #820's port.

Gates: fmt; clippy --all-targets -D warnings on CI's three sets; lib
filtered 819/853/link/reap/metrics/observability 84 passed (84 under
ffi-uniffi); 6 integration files 57 passed; links_ffi +
peer_mutation_ffi + witness under ffi-uniffi 30 passed; bindings regen
matches; 0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T20:47:32.031782Z d58e06f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cb17f4081

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. 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".

Comment thread src/transport/reticulum.rs Outdated
Comment on lines +3673 to +3677
inbound_link_idle_expired(
now_ms,
*at,
last_inbound.get(id).copied(),
self.node.get_remote_identity(id).is_some(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset the idle clock after outbound link use

When a peer-opened link has received no inbound frame for the bound but is still carrying periodic outbound reverse-path traffic, this predicate continues aging solely from last_inbound. The busy sets only defer reaping while a resource send is currently in progress, and Channel-first sends are not represented there at all, so the next tick can close an actively used link immediately after or even during an outbound send. For an initiator-only/NATed peer, the fallback outbound dial is unavailable, leaving subsequent pushes undeliverable until that peer transmits and redials; successful outbound resource and Channel activity should therefore refresh the link's idle timestamp.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38bf07f. A successful outbound send on a link now restarts its idle clock. That covers a Channel-first frame fully sent (the scoped lease and both reverse-path arms) and a Resource whose completion future resolved in ship_resource_on_link. Each stamps link_last_outbound_at, which is kept separate from link_last_inbound_at so the #353 reverse-path selector still reads only inbound liveness. The idle predicate starts the clock at max(last inbound, last outbound, establishment). Witness a_link_this_node_keeps_sending_on_is_not_reaped_854: B sends one frame and then only listens, and A keeps answering on that link via send_on_reply_path_only for three bounds. The link stays open, and it is reaped only after A stops. With the stamp disabled, A's reply path is gone under its own sends (Unreachable: no live reply path).

Comment thread src/transport/reticulum.rs Outdated
Comment on lines +3666 to +3669
let due: Vec<LinkId> = candidates
.into_iter()
.filter(|id| !busy.contains(id))
.filter(|id| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude vanished suspects from idle teardown

When the peer has already closed an idle link but its LinkClosed event is still queued behind this timer arm, reconciliation deliberately marks the link as a suspect and waits for the grace period, yet this selection still treats it as due because the local mirrors remain and there is no node.link_is_established(id) check. link_teardown_as then removes the direction record and counts idle_expired; when the queued event is finally handled it cannot count the real peer_closed reason. This defeats the new grace period and corrupts departure telemetry, so links the node already considers gone must be left for the event or vanished reconciliation path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38bf07f. Idle selection now filters on node.link_is_established. A link the node already closed is left to its queued LinkClosed (counted under the real reason), or after VANISH_GRACE to reconciliation's vanished. It is never counted as our idle_expired. Witness the_idle_reap_skips_links_the_node_no_longer_holds_854: an inbound link in edge's books, far past the bound but not held by the node, survives three passes with idle_expired = 0, then counts once as vanished. Without the filter, the second pass tears it down as idle_expired.

Comment on lines +5514 to +5520
forget_link_direction(
&self.link_direction,
self.metrics.get(),
self.metrics_source,
&link_id,
cause,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve reconciliation tracking until close cleanup finishes

If a locally initiated or reaper-initiated close has its LinkClosed event dropped during control-plane saturation, this eagerly removes the direction record and the function subsequently removes the link from established_links, so reconcile_vanished_links can no longer discover it. The local teardown path does not remove link_to_peer_key_id, link_last_inbound_at, or inbound_resource_active; those are cleared only by the now-lost event's LinkBook::forget, causing the per-link maps to grow across repeated reaps. Either perform the complete LinkBook cleanup eagerly or retain one of the reconciliation records until the close event or grace-period cleanup runs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38bf07f by doing the complete cleanup eagerly. link_teardown_as counts the close under its cause before close_link (so the following LinkClosed adds nothing), then runs the full LinkBook::forget right away. That covers mirror, established_at, attribution, last-inbound and last-outbound stamps, dialed dest + identity pool, in-flight, inbound transfer marks and plane, plus the LXMF entry, pool stamp and scoped pool. A lost LinkClosed leaves nothing behind. Witness a_local_teardown_forgets_the_link_everywhere_854: every per-link map is empty after a teardown, and the close is counted once as local_teardown. With the old partial teardown, attribution, the stamps and the transfer mark remain.

Comment on lines +3663 to +3665
let established_at = self.link_established_at.lock().await.clone();
let last_inbound = self.link_last_inbound_at.lock().await.clone();
let now_ms = u64::try_from(chrono::Utc::now().timestamp_millis().max(0)).unwrap_or(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for queued inbound activity before reaping

Because the reaper runs inside the same listen loop that consumes NodeEvents, these snapshots can be stale whenever the timer branch wins while an inbound frame or ResourceTransferStarted/ResourceProgress event is already queued. The frame has reached the node, but neither last_inbound nor the busy maps has been updated yet, so a link that is actively receiving at the idle boundary can be selected and closed before its activity event is handled. Under event backlog this is not limited to an exact timer tie; the reaper should consult authoritative node activity or defer candidates with pending receive activity rather than relying only on event-loop mirrors.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38bf07f, using deferral rather than node state. leviculum 0.27's std ReticulumNode has no per-link last-inbound: link_stats carries none, and core's last_inbound_secs also counts keepalives, which would keep every link alive. So a link is closed only by the second consecutive pass that finds it idle with an unchanged (last inbound, last outbound) snapshot. A frame or ResourceProgress queued behind the timer arm is drained by the loop before the next tick, and its stamp or busy mark makes the link no longer due. Witness a_link_is_reaped_only_on_the_second_idle_observation_854 drives the passes itself. The first idle pass only notes the link, a frame landing between passes saves it, and two idle unchanged passes close it. With a single observation, the first pass closes it.

emooreatx and others added 2 commits October 8, 2026 14:26
…nner (#853)

inbound_exits_are_instrumented_or_marked failed the v40.0.10 tag run on
the bare `return;` after the swallow_link_closed test seam in the
LinkClosed arm. The seam is empty in production and the swallowed close
is still counted as `vanished` by reconciliation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
1. Outbound use restarts the idle clock. A successful outbound send on a
   link (a Channel-first frame fully sent on the scoped lease and both
   reverse-path arms; a Resource whose completion future resolved) stamps
   link_last_outbound_at, kept apart from link_last_inbound_at so the #353
   reverse-path selector still reads only inbound liveness. The idle clock
   is max(last inbound, last outbound, establishment). An initiator-only
   peer's link this node keeps answering on is no longer reaped under it.
2. Idle selection skips links the node no longer holds, so a queued
   LinkClosed is never pre-empted and misfiled as idle_expired; those wait
   for their event or reconciliation's `vanished`.
3. A local teardown counts the close under its cause, closes at the node,
   then runs the full LinkBook::forget eagerly (plus LXMF, pool stamp,
   scoped pool): a lost LinkClosed leaks no attribution, stamps or
   transfer marks.
4. Two consecutive idle observations. leviculum 0.27's std handle does
   not surface a link's last-inbound (link_stats has none; core's
   last_inbound_secs counts keepalives), so a link is closed only by the
   second pass that finds it idle with the same (last inbound, last
   outbound) snapshot; activity queued behind the timer lands between.

Witnesses, each failing before its fix (mutation runs, file restored):
- (g) a_link_this_node_keeps_sending_on_is_not_reaped_854: without the
  outbound stamp, A's reply path is gone under its own sends.
- the_idle_reap_skips_links_the_node_no_longer_holds_854: without the
  node filter, idle_expired is counted for a link the node had closed.
- a_local_teardown_forgets_the_link_everywhere_854: without the eager
  forget, attribution/stamps/transfer marks remain.
- (h) a_link_is_reaped_only_on_the_second_idle_observation_854: with one
  observation, the first pass closes the link.

Gates: fmt; clippy --all-targets -D warnings on CI's three sets; lib
filtered 819/853/854/link/reap/metrics/observability/inbound_exits 87
passed (87 under ffi-uniffi); 6 integration files 59 passed; links_ffi +
peer_mutation_ffi + witness under ffi-uniffi 32 passed; 0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

… speak (#853)

From the Datum link-storm trace (storm-v4006): ~890 `link identified`
and ~825 PeerClosed per 10 min from one v40.0.6 initiator, and no
responder line at the canonical's RUST_LOG=info.

- No PoolFull close on release: release_link_after_transfer no longer
  trims the pool to the idle cap, and the reaper applies no cap either;
  pooled lanes die only by the idle bound. pool_full is retired (always
  0, kept for its series); idle_cap is accepted but not enforced.
- LinkClaim: a pool lane's transfer claim is an RAII guard released on
  Drop, so a send cancelled mid-ship (the responder's 60 s reply
  timeout) no longer retires its lane.
- A pooled lane closing ChannelExhausted or Stale logs a throttled WARN
  with running counts (the starved-responder signature).
- Responder round lifecycle at INFO (started; both completion paths
  with duration_ms; reply failures stay WARN), with peer, kind and the
  arrival link; counter responder_rounds_total{kind, outcome}.
- responder_link_up_seconds{stage: bundle_gate, announce_intake,
  owner_binding} from LinkEstablished on a peer-dialled link, and
  responder_link_up_total{ok, bundle_refused, timeout, link_gone}.
- EdgeMetrics only on main (accessors responder_rounds_total() /
  responder_link_up_seconds() / responder_link_up_total()); the bundle
  and both bindings carry them when #848 lands. FixedHistogram /
  HistogramSnapshot come over verbatim from the release line.

Witnesses: a_burst_closes_nothing_on_release_and_the_bound_retires_it_853
(14 concurrent sends, cap 1), a_cancelled_send_gives_its_lane_back_853,
each_responder_round_outcome_increments_its_token_once_853,
a_responder_link_up_lands_in_a_bucket_and_counts_its_outcome_853.
Fail-before: with the release trim + reaper cap restored the burst
witness fails (12 lanes closed pool_full); with LinkClaim's Drop a no-op
the cancel witness fails (the lane stays claimed).

Gates: fmt; clippy --all-targets -D warnings on CI's three sets; lib
filtered 819/853/854/link/reap/metrics/observability/inbound_exits 89
passed (89 under ffi-uniffi); 6 integration files 60 passed
(link_pool_819 3 runs green); links_ffi + peer_mutation_ffi + witness
under ffi-uniffi 32 passed; 0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d58e06f6e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. 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".

Comment on lines +3639 to +3643
let candidates = self
.link_direction
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.inbound_ids();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recover links whose establishment event was dropped

When control-plane saturation drops a LinkEstablished event, this link is never inserted into link_direction, so it can never enter the inbound reaper's candidate set. The ControlPlaneOverflow arm explicitly acknowledges that establishment events may be missed, but later LinkIdentified/traffic does not reconstruct the direction, and reconciliation only examines the same direction map plus established_links, which the missed event also populates. Consequently, a peer-opened link in this scenario can remain live and uncounted indefinitely—the exact growth this reaper is intended to stop—so recovery needs to discover authoritative node links or establish bookkeeping from subsequent per-link events.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #859 (1419d93). New recover_unknown_link runs on a link's next per-link event (LinkIdentified, or any frame through attribute_and_deliver). If the node holds the link but edge has no direction record for it, the link is recorded INBOUND unless this node dialled it (dialed_link_dest is written at every own dial), added to the established mirror, given now as its establishment so the idle clock starts there, and has its plane classified. It is counted in recovered_links_total. Witness a_link_whose_establish_event_was_lost_is_recovered_and_reaped_854: a test seam drops the next inbound LinkEstablished; B's frame then rebuilds the bookkeeping, and the link is counted and reaped once idle. Without the recovery the witness fails. Limit: leviculum 0.27 still can't list its links, so a link that never carries another event stays invisible until it closes.

Comment on lines 6565 to +6568
let outcome = send_fragments_on_channel(&self.node, &link_id, &fragments).await;
if outcome.complete() {
// CIRISEdge#853 (Codex on #854) — a sent frame is a use of the link.
note_outbound_use(&self.link_last_outbound_at, link_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark Channel sends busy before awaiting them

With a configured short inbound idle bound, a reverse-path Channel send can begin after the first idle observation and still be awaiting pacing when the next reaper tick runs; Channel transmission can take up to the 15-second frame budget, while the tick can be as short as 250 ms. This timestamp is written only after the whole frame succeeds, and unlike the Resource path no busy guard covers the await, so the unchanged second observation can close the link mid-send. Fresh evidence beyond the earlier fix is that the final code still places note_outbound_use after send_fragments_on_channel(...).await in both reverse-path Channel arms; mark the link busy for the duration of those awaits.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #859 (1419d93). Every Channel-first send that rides a link goes through send_channel_marked, which holds an OutboundShipGuard (the Resource path's busy mark) for the whole await. That covers both reverse-path arms and the scoped lease. A reply waiting on the Channel window is therefore busy to the inbound reaper, not only once it has completed. Witness a_reply_waiting_on_the_channel_holds_its_link_854: the first idle pass notes the link, then a reply is held 3 s inside the send by a test delay. The second pass runs mid-send and closes nothing, and the reply completes. With the guard removed, the second pass closes the link.

Comment on lines +9279 to +9282
// CIRISEdge#853 — the first application frame on a link a peer dialled
// ends its link-up (a no-op for a link not being timed).
if let Some(m) = ctx.metrics {
m.responder_link_up_end(link_id.into_bytes(), crate::observability::LINK_UP_OK);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record owner binding before ending link-up tracking

For an owner-binding push, this ends and removes the pending link-up timer before the frame reaches OwnerBindingCarveOut::admit_frame, where the newly added LINK_UP_STAGE_OWNER_BINDING observation is recorded. Because responder_link_up_stage ignores links absent from the pending map, every successfully consumed owner binding produces no owner-binding latency sample at all. Defer the ok end until downstream link-up stages have been recorded, or otherwise preserve the start time through owner-binding admission.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #859 (1419d93). The transport no longer ends the link-up on the first frame. note_link_up_frame runs at the sink and ends it ok only for an ATTRIBUTED frame. An unattributed frame, which may be the sibling's owner-binding push, leaves it open. After recording LINK_UP_STAGE_OWNER_BINDING, the carve-out ends it ok itself. An unattributed frame that is not an owner binding leaves the link-up to time out. Witness an_unattributed_frame_leaves_the_link_up_for_the_owner_binding_stage_854 fails when the frame ends the link-up regardless of attribution.

Comment on lines +368 to +372
if link.is_some() {
*self
.last_inbound_link
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = link;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the logged link when traffic changes transport

When an existing responder coordinator later receives a frame over HTTPS or another non-Reticulum transport, link is None, but this conditional leaves the previous Reticulum link ID stored. All subsequent responder lifecycle logs therefore attribute those rounds to a stale link rather than the documented -, which is especially misleading during transport failover investigations. Assign the option unconditionally so None clears the prior link.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #859 (1419d93). deliver_inbound_framed_on now assigns the link unconditionally, so a frame with no link (HTTPS, any non-Reticulum transport) clears it, and the lifecycle lines read -. Witness a_frame_without_a_link_clears_the_logged_link_854 fails with the old conditional assignment.

tracing::debug!(peer = %peer, ?kind, "responder driver ending (channel closed)");
break;
};
let began = *round_began.get_or_insert_with(std::time::Instant::now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset timing when the responder rebinds to a new round

If an initiator abandons an incomplete round and sends a frame with a new round ID, drive_round_step_framed resets the responder session and rebinds it, but this local timer remains anchored to the abandoned round. When the replacement round completes, its duration_ms therefore includes all time spent waiting in the prior round, corrupting the new lifecycle diagnostic precisely in packet-loss and retry scenarios. Detect the round-ID change before stepping and restart round_began when the coordinator rebinds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #859 (1419d93). New responder_round_clock restarts the round timer whenever a frame names a round id different from the one the timer is anchored to, i.e. when the coordinator rebinds to a new round. Same-round frames and legacy (round 0) frames leave it alone. Witness the_round_clock_restarts_when_the_responder_rebinds_854: after 45 s spent in an abandoned round, the replacement round's start is its own first frame. Without the restart the witness fails.

Comment on lines +10159 to +10163
.reusable_dialed_link
.lock()
.await
.get(&dest)
.is_some_and(|pool| pool.contains(&link_id)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include scoped lanes in pooled-close diagnostics

When a pooled scoped link closes with ChannelExhausted or Stale, this predicate checks only reusable_dialed_link, while scoped lanes are stored in reusable_scoped_link. Such a close is therefore treated as non-pooled and never reaches note_starved_pool_close, suppressing the warning and running counters for one of the two pool families even though the new diagnostic is documented for pooled lanes generally. Check both pools before forgetting the link.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #859 (1419d93). The pooled check is now link_is_pooled, which scans both reusable_dialed_link and reusable_scoped_link, so a scoped lane closing ChannelExhausted/Stale reaches note_starved_pool_close. Witness a_scoped_lane_counts_as_pooled_854 fails when only the identity pool is consulted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant