Skip to content

release(40.1.0): leviculum v0.29 — known-destinations backpressure, off-lock resource assembly, and transport counters that finally reach production - #810

Merged
emooreatx merged 8 commits into
mainfrom
feat/809-item3
Oct 6, 2026
Merged

emooreatx merged 8 commits into
mainfrom
feat/809-item3

Conversation

@emooreatx

@emooreatx emooreatx commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Release PR for v40.1.0 (MINOR from v40.0.3; persist v53.1.4 and verify v19.0.0 unchanged; no wire change). Rebased onto v40.0.3. Issue #809; notes in docs/RELEASE_NOTES.md § v40.1.0.

Gates on 95a3bdf (+ 0104e49: FFI suite 17/17, #809 lib tests, CI clippy ×3) (CI feature set 1 unless noted): clippy -D warnings on CI's three feature sets; lib suite in thirds and 16 integration/transport suites — 19 runs, 2051 passed, 0 failed (blob_federation_e2e, family_files_wire_736, delivery_receipts_738, self_dag_field_path_717, chat_message_federates, public_group_reach_762, group_record_reach_758, membership_first_contact_756, first_contact_ladder_659, relay_roster_752, peer_mutation_ffi, reticulum_loopback, links_ffi, link_plane_728, reticulum_av42, transport_http_hardening).

Not here: #809 item 2 (retain pins from persist is_load_bearing), next MINOR.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

@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: 28d9d29e41

ℹ️ 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 +8829 to +8830
note_packet_dropped(
ctx.metrics,

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 Attach packet-drop accounting to the Edge metrics bag

In the production PyO3 initialization path, ReticulumTransport::new is wrapped in Arc without calling with_metrics, while EdgeBuilder::build later creates a separate EdgeMetrics; therefore ctx.metrics is None here and every PacketDropped only emits the debug log. Consequently, the newly added transport_packets_dropped field in edge.metrics().snapshot() remains zero for normal production runtimes regardless of actual drops. The transport and Edge need to share the same metrics handle before the listener starts.

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.

Confirmed, and bigger than stated: ReticulumTransport::with_metrics had no production caller at all (only tests), so ctx.metrics was None on every real node — the pre-existing #530 announce_intake_evictions and #627 link_before_binding have read zero in production since they shipped, not just these two. Fixed in c53a80e: the transport's slot is a OnceLock (first handle wins, so a bag a test attached through with_metrics is kept), Transport gains an attach_metrics default, and Edge::spawn_background_listeners attaches its bag to every transport before spawning listen — the per-event EventCtx reads the slot on each event, so the first event already sees it. Witness: the_edge_attaches_its_metrics_to_every_transport_before_listening_809 (tests/peer_mutation_ffi.rs) builds an Edge with a recording transport and asserts one increment through the attached handle is one in edge.metrics().snapshot(), and that build alone does not attach — the listener spawn does.

Comment thread src/transport/reticulum.rs Outdated
Comment on lines +4360 to +4362
let evictions = self.node.known_destination_evictions();
if let Some(m) = self.metrics.as_ref() {
m.set_known_destination_evictions(evictions);

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 Refresh the eviction gauge without requiring an explicit read

The only production path that mirrors the node's eviction count is this accessor itself, and there are no callers of ReticulumTransport::known_destination_evictions() elsewhere in the repository; EdgeMetrics::snapshot() merely loads the cached atomic. Thus a node can evict destinations continuously while ordinary metrics snapshots keep reporting zero unless a caller first invokes this otherwise redundant transport getter. Poll the node from an existing lifecycle/readback path or update the mirror when evictions occur so snapshot reads are current.

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.

Confirmed. In c53a80e the gauge refreshes on two production paths: the announce tick mirrors node.known_destination_evictions() on its cadence (a brief node lock), and Transport::refresh_metrics (new trait default; ReticulumTransport implements it) is called for every transport by both binding snapshots right before they read. Witness: the_uniffi_snapshot_carries_both_pressure_signals_and_refreshes_first_809 — a transport reporting 42 evictions reads 42 from metrics_snapshot() with no prior transport-specific getter.

Comment thread src/observability.rs
Comment on lines +1879 to +1883
pub transport_packets_dropped: u64,
/// CIRISEdge#809 / leviculum#49 — identity-cap evictions in leviculum's
/// known-destinations store, mirrored at the last transport readback.
/// Zero with room; climbing = at cap, unretained LRU destinations going.
pub known_destination_evictions: u64,

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 Expose the new counters through the binding snapshots

These fields are added to the Rust-only EdgeMetricsBundle, but neither PyEdge::metrics_snapshot nor the UniFFI metrics_snapshot projects transport_packets_dropped or known_destination_evictions into its returned dictionary/counter map. Therefore Python and UniFFI operators cannot observe either new signal at all—even after their collection paths are fixed—despite those being the repository's public metrics readbacks. Add both counters to each binding projection and its API contract/tests.

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.

Confirmed. c53a80e projects both: PyO3 metrics_snapshot() gets the scalars transport_packets_dropped and known_destination_evictions (test metrics_snapshot_carries_both_pressure_signals_809, src/ffi/pyo3.rs); UniFFI gets transport.packets_dropped_total and transport.known_destination_evictions in counters (test in tests/peer_mutation_ffi.rs). Both read the same bag the transports now count into.

emooreatx added a commit that referenced this pull request Oct 5, 2026
…es itself, and both signals reach the bindings (Codex round one on #810)

Three findings, one class — a counter nobody in production could read:

1. `ReticulumTransport::with_metrics` had no production caller, so the
   listener's `ctx.metrics` was `None` on every real node: not only the
   two new counters but #530 `announce_intake_evictions` and #627
   `link_before_binding` have read zero in production since they
   shipped. The slot is now a `OnceLock` (first handle wins, so a bag a
   test attached is kept); `Transport` gains `attach_metrics` /
   `refresh_metrics` defaults; `Edge::spawn_background_listeners`
   attaches its bag to every transport BEFORE spawning `listen`.
2. The eviction gauge only moved when someone called the transport
   getter. The announce tick mirrors it on its cadence and
   `refresh_metrics` mirrors on demand; both binding snapshots refresh
   every transport before reading.
3. Neither binding projected the new fields. PyO3 `metrics_snapshot()`
   carries `transport_packets_dropped` / `known_destination_evictions`;
   UniFFI carries `transport.packets_dropped_total` /
   `transport.known_destination_evictions`.

Witnesses (fail on the pre-fix code): an Edge built with a recording
transport proves the attached handle and `Edge::metrics()` are one bag
and that attach happens at the listener spawn; the UniFFI snapshot
carries both keys and refreshes the gauge itself (42 reported → 42 read
with no prior getter); the PyO3 dict carries both scalars.

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

@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: c53a80e07f

ℹ️ 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/edge.rs Outdated
Comment on lines +4295 to +4297
for transport in &self.transports {
transport.attach_metrics(self.metrics.clone());
}

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 Attach metrics in the Edge::run lifecycle

This attachment only covers spawn_background_listeners; nodes using the documented full Edge::run lifecycle start every transport listener directly without calling attach_metrics. For a normally constructed ReticulumTransport, its OnceLock therefore remains empty, so PacketDropped events and the refreshed destination-eviction value never reach Edge::metrics() on those nodes. The fresh evidence beyond the earlier PyO3-path issue is the independent listener spawn in Edge::run; attach the metrics bag there as well, before spawning its listeners.

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.

Confirmed — and there was a third site you didn't name: edge_node's spawn_inbound calls listen itself. Fixed the class in ba3d434 rather than patching sites: EdgeBuilder::build now creates the bag and attaches it to every registered transport (reticulum_transport(..) also pushes into transports) before returning the Edge, so no listener spawn — spawn_background_listeners, Edge::run, or a host's own — can precede it. The per-site attach is removed. Witness the_edge_attaches_its_metrics_to_every_transport_at_build_809 asserts the attach with no listener spawned, and fails with the build-time attach removed.

emooreatx added a commit that referenced this pull request Oct 5, 2026
…stener can exist (Codex round two on #810)

c53a80e attached the Edge's bag in `spawn_background_listeners` only.
Two other sites start a transport's listener: `Edge::run` (Codex) and
hosts that call `listen` themselves — `edge_node`'s `spawn_inbound`
does. Fix the class, not the sites: `build` creates the bag and attaches
it to every registered transport (including one registered through
`reticulum_transport`, which pushes into `transports`) before returning
the Edge, so no spawn site can precede it.

Witness: `the_edge_attaches_its_metrics_to_every_transport_at_build_809`
asserts the attach with NO listener spawned; fails with the build-time
attach removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

@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: ba3d434273

ℹ️ 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 +6104 to +6105
fn attach_metrics(&self, metrics: crate::observability::EdgeMetrics) {
let _ = self.metrics.set(metrics);

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 Replace a preattached metrics bag when building an Edge

When a caller constructs the transport with the public with_metrics(Some(...)) API before passing it to EdgeBuilder, this OnceLock::set fails silently because the slot is already populated. The listener then records packet drops and eviction/link counters into the caller-supplied bag, while Edge::metrics() and both binding snapshots read the new bag created by EdgeBuilder::build, recreating the zero-metrics problem for that supported construction path. Ensure Edge attachment becomes authoritative, or have the builder reuse the already attached bag.

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.

Confirmed. Fixed in 14a4a28 by having the builder reuse the bag (your second option): EdgeBuilder::build now adopts the first transport's already-attached bag (new Transport::attached_metrics, default none; ReticulumTransport returns its slot) as the Edge's bag, and attaches it to every transport. A replace isn't possible without dropping the caller's own handle, and reuse keeps both the caller and the Edge reading one bag. If two transports were built with different bags, the one that can't be re-pointed gets a WARN naming the split, so it is never silent (EdgeMetrics::is_same_bag compares the shared Arcs). Witness the_edge_adopts_a_bag_the_transport_was_built_with_809 fails when the builder mints a fresh bag.

emooreatx added a commit that referenced this pull request Oct 6, 2026
…ound three on #810)

A caller who builds `ReticulumTransport::with_metrics(Some(bag))` and
hands it to `EdgeBuilder` kept counting into `bag` (the slot is set
once, first handle wins), while `build` minted a second bag that
`Edge::metrics()` and both binding snapshots read: zero again. `build`
now ADOPTS the first already-attached bag (new `Transport::
attached_metrics`, default none) and attaches it to every transport; a
transport holding a different bag cannot be re-pointed, and that split
is a WARN naming it, never silent (`EdgeMetrics::is_same_bag`).

Witness: `the_edge_adopts_a_bag_the_transport_was_built_with_809`;
fails when the builder mints a fresh bag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 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-06T03:26:07.589678Z 0104e49 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: 14a4a28158

ℹ️ 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/ffi/pyo3.rs
Comment on lines +2476 to +2477
for transport in self.inner.transports() {
transport.refresh_metrics();

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 Aggregate evictions from all transports

When an Edge contains multiple ReticulumTransport instances, which repeated EdgeBuilder::transport calls permit, this loop refreshes every transport into the same EdgeMetrics atomic. Each refresh uses set_known_destination_evictions to store that node's local cumulative value, so the last transport overwrites all earlier values—for example, nodes reporting 10 and 0 evictions produce a snapshot of 0—and periodic announce refreshes can even make this cumulative metric move backward. Aggregate the per-transport values or retain separate per-transport gauges; the equivalent UniFFI refresh loop has the same problem.

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.

Confirmed. Fixed in 05c3ef9. The gauge is now one entry per transport instance: a process-unique metrics_source is assigned to each ReticulumTransport at construction, and the entries are summed on read. set_known_destination_evictions(source, value) stores into that transport's own entry, so a refresh never overwrites another transport's count, and the total is monotonic as long as each node's count is. Both the PyO3 and UniFFI loops read the sum, so the one change fixes both. Witness: evictions_from_several_transports_are_summed_not_overwritten (10 + 0 reads 10, not 0; after 10 + 3, reads 13).

emooreatx added a commit that referenced this pull request Oct 6, 2026
…ting (Codex round four on #810)

An Edge may hold several Reticulum transports; each refresh stored its
own node's cumulative count into ONE atomic, so the last refresh won
(nodes at 10 and 0 read 0) and the cumulative gauge could move backward.
`EdgeMetrics::known_destination_evictions` is now one entry per
transport instance (a process-unique `metrics_source` assigned at
construction), summed on read; `set_known_destination_evictions` takes
the source. Fixes the PyO3 and UniFFI snapshot paths alike, since both
read the sum.

Witness: `evictions_from_several_transports_are_summed_not_overwritten`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx and others added 6 commits October 5, 2026 21:54
…49 known-destinations backpressure, #71 off-lock assembly)

Pin leviculum-core / -std / -lxmf to v0.29.0+ciris.1 (82a9d601). No
signature edge compiles against changed (leviculum's parse: 1479 → 1530
pub fns, none removed or altered); the new `ResourceError` variants
reach edge's `Other` arm and `NodeEvent::PacketDropped` the wildcard.

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

- `EdgeMetrics::transport_packets_dropped`: one increment per
  `NodeEvent::PacketDropped` whatever its `DropReason` (duplicate, no
  path, IFAC, announce replay / rate limit / over-max-hops); the reason
  rides a DEBUG line, not WARN — per-packet WARN would be attacker-paced
  noise (#460). `note_packet_dropped` is split out of `handle_event` so
  it is unit-testable without an event-loop context.
- `EdgeMetrics::known_destination_evictions`: a mirror (`store`, never an
  increment) of leviculum's identity-cap eviction count, read live by the
  new `ReticulumTransport::known_destination_evictions()` readback
  (leviculum#49: a rising value = at cap, unretained LRU destinations
  going; a retained destination never counts).

Both land in `EdgeMetricsBundle`. Tests: every drop counted once for any
reason and surfaced in the snapshot; two readbacks of the same count do
not read as double.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…es itself, and both signals reach the bindings (Codex round one on #810)

Three findings, one class — a counter nobody in production could read:

1. `ReticulumTransport::with_metrics` had no production caller, so the
   listener's `ctx.metrics` was `None` on every real node: not only the
   two new counters but #530 `announce_intake_evictions` and #627
   `link_before_binding` have read zero in production since they
   shipped. The slot is now a `OnceLock` (first handle wins, so a bag a
   test attached is kept); `Transport` gains `attach_metrics` /
   `refresh_metrics` defaults; `Edge::spawn_background_listeners`
   attaches its bag to every transport BEFORE spawning `listen`.
2. The eviction gauge only moved when someone called the transport
   getter. The announce tick mirrors it on its cadence and
   `refresh_metrics` mirrors on demand; both binding snapshots refresh
   every transport before reading.
3. Neither binding projected the new fields. PyO3 `metrics_snapshot()`
   carries `transport_packets_dropped` / `known_destination_evictions`;
   UniFFI carries `transport.packets_dropped_total` /
   `transport.known_destination_evictions`.

Witnesses (fail on the pre-fix code): an Edge built with a recording
transport proves the attached handle and `Edge::metrics()` are one bag
and that attach happens at the listener spawn; the UniFFI snapshot
carries both keys and refreshes the gauge itself (42 reported → 42 read
with no prior getter); the PyO3 dict carries both scalars.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…stener can exist (Codex round two on #810)

c53a80e attached the Edge's bag in `spawn_background_listeners` only.
Two other sites start a transport's listener: `Edge::run` (Codex) and
hosts that call `listen` themselves — `edge_node`'s `spawn_inbound`
does. Fix the class, not the sites: `build` creates the bag and attaches
it to every registered transport (including one registered through
`reticulum_transport`, which pushes into `transports`) before returning
the Edge, so no spawn site can precede it.

Witness: `the_edge_attaches_its_metrics_to_every_transport_at_build_809`
asserts the attach with NO listener spawned; fails with the build-time
attach removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…ound three on #810)

A caller who builds `ReticulumTransport::with_metrics(Some(bag))` and
hands it to `EdgeBuilder` kept counting into `bag` (the slot is set
once, first handle wins), while `build` minted a second bag that
`Edge::metrics()` and both binding snapshots read: zero again. `build`
now ADOPTS the first already-attached bag (new `Transport::
attached_metrics`, default none) and attaches it to every transport; a
transport holding a different bag cannot be re-pointed, and that split
is a WARN naming it, never silent (`EdgeMetrics::is_same_bag`).

Witness: `the_edge_adopts_a_bag_the_transport_was_built_with_809`;
fails when the builder mints a fresh bag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…ting (Codex round four on #810)

An Edge may hold several Reticulum transports; each refresh stored its
own node's cumulative count into ONE atomic, so the last refresh won
(nodes at 10 and 0 read 0) and the cumulative gauge could move backward.
`EdgeMetrics::known_destination_evictions` is now one entry per
transport instance (a process-unique `metrics_source` assigned at
construction), summed on read; `set_known_destination_evictions` takes
the source. Fixes the PyO3 and UniFFI snapshot paths alike, since both
read the sum.

Witness: `evictions_from_several_transports_are_summed_not_overwritten`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

@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: 05c3ef964f

ℹ️ 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/edge.rs Outdated
Comment on lines +8350 to +8354
let metrics = self
.transports
.iter()
.find_map(|t| t.attached_metrics())
.unwrap_or_default();

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 Reconcile every preattached metrics bag

When an Edge is built with multiple transports that were each configured through with_metrics(Some(...)) using different bags, this adopts only the first bag, and attach_metrics cannot replace the other transports' OnceLock values. Those transports therefore continue recording packet drops, announce evictions, and related counters into bags that Edge::metrics() and both binding snapshots never read; emitting a warning does not prevent the returned metrics from silently undercounting a configuration supported by repeated EdgeBuilder::transport calls. Either reject mismatched bags during build or make the Edge's selected bag authoritative for every transport.

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.

Confirmed. I took your first option in 0104e49: EdgeBuilder::build now refuses transports built with different bags, returning EdgeError::Config("transports were built with different metrics bags …"), instead of adopting the first and warning. Making the Edge's bag authoritative would silently stop the caller's own handle from receiving counts, which is the same undercount moved somewhere else. One shared bag across every transport, or one bag plus bag-less transports, still builds and is adopted. Witness: build_refuses_transports_with_different_metrics_bags_809, which fails with the adopt-and-warn behaviour restored.

Comment thread Cargo.toml
Comment on lines +892 to +893
leviculum-core = { git = "https://github.com/CIRISAI/leviculum", tag = "v0.29.0+ciris.1", version = "0.29", optional = true }
leviculum-std = { git = "https://github.com/CIRISAI/leviculum", tag = "v0.29.0+ciris.1", version = "0.29", optional = true }

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 attribution for late resource completions

With v0.29's new ordering where a receiver-side ResourceCompleted may arrive after its link has gone stale, a completed non-bootstrap replication resource can now be discarded. The earlier LinkClosed arm removes both link_to_peer_key_id and dialed_link_dest (src/transport/reticulum.rs:8660-8667), so attribute_and_deliver emits the late bytes with source_key_id=None; route_replication_frame then takes SkippedNoSourceKeyId and drops ordinary CRPL traffic (src/edge.rs:5394-5408). Retain sufficient closed-link attribution until pending completions have drained, or carry the authenticated peer identity on the completion event, so fully received replication payloads are not lost under this newly permitted ordering.

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.

Checked at the pin, and this ordering can't happen. At leviculum 82a9d601, remove_link (leviculum-core/src/node/link_management.rs:513–540, the path every teardown goes through, before LinkClosed is emitted) removes the link's pending off-lock assembly from assembling_resources and emits ResourceFailed { error: LinkClosed, is_sender: false } for it. complete_resource_assembly (node/mod.rs:1910) only concludes a resource whose entry is still waiting, so a late assembly result on a closed link finds nothing and emits no ResourceCompleted. A completion that does conclude is pushed to the same FIFO event queue before any later LinkClosed, and edge's event loop handles it while link_to_peer_key_id / dialed_link_dest still hold the link. So a completion is always attributable, or the resource fails as LinkClosed; it is never late and unattributed. The window v0.29 opened is a completion on a link that went Stale but isn't closed yet, and edge's attribution maps are only cleared at LinkClosed. leviculum's teardown tests don't yet pin the close-during-assembly case, so I've asked leviculum to add that test rather than adding a defensive copy of the attribution in edge.

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.

Follow-up: leviculum now pins this invariant in its own CI, in leviculum PR #75 (test-only). a_link_closed_during_assembly_fails_the_resource_and_drops_the_late_result asserts it with an assembly job already taken, both for a local close_link and for a close sent by the peer mid-assembly: exactly one receiver-side ResourceFailed{LinkClosed}, ordered before LinkClosed, and no ResourceCompleted from the late complete_resource_assembly. It was mutation-checked: removing the assembling_resources take in remove_link fails both variants.

…ff-lock resource assembly, and transport counters that finally reach production

Version 40.1.0, evidence TSV regenerated, notes in docs/RELEASE_NOTES.md
§ v40.1.0. MINOR from v40.0.3: persist v53.1.4 and verify v19.0.0
unchanged; leviculum v0.29.0+ciris.1 (82a9d601); additive API only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
@emooreatx emooreatx changed the title feat(809): adopt leviculum v0.29.0+ciris.1 — known-destinations backpressure, off-lock assembly, and the two pressure signals in the metrics snapshot release(40.1.0): leviculum v0.29 — known-destinations backpressure, off-lock resource assembly, and transport counters that finally reach production Oct 6, 2026
…(Codex round five on #810)

95a3bdf adopted the first pre-attached bag and only WARNed when another
transport held a different one, so `Edge::metrics()` silently undercounted
that configuration. `EdgeBuilder::build` now returns
`EdgeError::Config("... different metrics bags ...")` for it; one shared
bag, or a mix of one bag and bag-less transports, still builds.

Witness: `build_refuses_transports_with_different_metrics_bags_809`;
fails with the adopt-and-warn behaviour restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

@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: 0104e49265

ℹ️ 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/observability.rs
Comment on lines +1237 to +1238
pub fn is_same_bag(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.envelopes_sent_total, &other.envelopes_sent_total)

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 Compare all metric stores before accepting a shared bag

EdgeMetrics exposes each backing Arc publicly, so a caller can clone a bag and replace one field (for example, transport_packets_dropped or known_destination_evictions) before preattaching it to another transport. This method still declares those partially shared bags identical because only envelopes_sent_total is compared; EdgeBuilder::build then adopts the first bag while the second transport's OnceLock keeps writing the replaced field, silently undercounting the Edge snapshot. Fresh evidence beyond the prior split-bag thread is this single-field identity check, which lets the newly added rejection be bypassed without constructing wholly unrelated bags.

Useful? React with 👍 / 👎.

@emooreatx
emooreatx merged commit 642e9ef into main Oct 6, 2026
20 checks passed
emooreatx added a commit that referenced this pull request Oct 6, 2026
…Edge::metrics()

open_av_link reads the transport's attached bag (OnceLock, #810) instead
of the pre-#810 Option. The live-node witness now builds its transports
with no with_metrics and an Edge over each through EdgeBuilder, and
asserts the av_plane counters land in Edge::metrics().snapshot() on both
sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx added a commit that referenced this pull request Oct 6, 2026
metrics_snapshot carries every EdgeMetrics::av_plane entry as
av_plane.<label>, beside #810's transport.* counters, so the A/V plane is
visible to mobile operators as it is in the Rust snapshot and the pyo3
dict. Contract test the_uniffi_snapshot_carries_the_av_plane_ledger_805
(fails with the projection removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx added a commit that referenced this pull request Oct 6, 2026
…metrics() of an Edge built normally (#771 on #810)

A fetches the link-less W's chunk from B over the wire. B's door serves
it through the legacy walk, and the walk is counted in
b.edge.metrics(), the one bag EdgeBuilder::build settled on. B's Edge is
built normally, and its wired source is a wrapper that forwards
attach_metrics.

The rebase onto #810 had auto-merged a second EdgeMetrics::new() for the
chunk source ahead of #810's metrics block. The source now attaches the
bag #810 settles on. Reintroducing the second bag fails this assertion
with left 0, right 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx added a commit that referenced this pull request Oct 7, 2026
…aw-and-evict witnesses (#771) (#812)

* feat(blob_swarm): the serve gate judges DAG membership by persist's chunk→manifest link (#771)

Persist v53.1 (#979) writes a chunk→manifest relation (V176) at seal and
promote. PersistBlobChunkSource now asks it instead of keeping its own:

- The #766 per-file membership cache is deleted, and so is edge's stream
  walk for every DAG persist has linked. `Engine::dag_contains_chunk`
  answers membership. A linked DAG that does not hold the chunk answers
  ChunkNotInNamedDag, whatever any row's pointer names. Before this a row
  referencing X whose pointer named Y's stream made the gate serve Y's
  chunk under a request naming X (persist I485's shape, on edge).
- The named DAG's own fold (persist `binding_state`) is read first. A
  withdrawn DAG's chunk answers Withdrawn on every door, armed or bare,
  warm or cold, even when the chunk is shared with a live DAG. Before
  this an unarmed door served the shared chunk under the withdrawn name.
- A plaintext DAG, which persist does not link, is still judged by its
  clear manifest.
- A sealed DAG with no link (sealed or pulled before persist v53.1.0, never
  re-promoted) keeps the pre-#771 stream reading, uncached, as a legacy
  fallback until persist backfills its link.
- The revocation register stays armed and is documented at the serve site.
  It is the only refusal for a withdrawal persist's fold cannot see (a
  reference by BlobPointer only).

Witnesses in self_dag_field_path_717: the forged pointer (fails on main:
"bytes" where "ChunkNotInNamedDag" is expected); A withdraws X and B's
door refuses every chunk Withdrawn on the armed, warm and cold doors and
over the wire, then B's eviction removes the manifest and every chunk X
holds alone, and keeps the chunk shared with a live DAG plus all of that
DAG (fails on main: ["Withdrawn", "bytes", "bytes"] for the shared chunk);
and a link-less withdrawn DAG, where persist's gap is measured and the
register is the only refusal (fails with the register check disarmed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

* test(family_files_wire_736): leg (d) asserts by name that F's blob store holds no family bytes (CIRISConstitution#141)

At the non-member forwarder F, both the inline file and the DAG file are
checked. F has no blob head, F's open is refused, persist relates no chunk
to the manifest, and no chunk sha that P1 sealed is held. A mutant that
copies the family blob rows into F's store fails here, on F's head for the
inline file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

* feat(blob_swarm): key the legacy DAG walk on "no relation AND chunk_dag", and count it (#771)

The lead's conditions and persist's contract (CIRISPersist#994):

- The legacy stream walk is reached only when persist holds NO relation
  for the named manifest (`chunks_of_manifest` empty) AND the manifest's
  head is a `chunk_dag`. A linked DAG that lacks the chunk refuses it, so
  "not a member" never falls through to the walk. A whole blob also
  answers empty, and it never reaches the walk.
- Every use of the walk is counted in
  `EdgeMetrics::blob_serve_legacy_dag_walks`. It is projected in the pyo3
  snapshot as `blob_serve_legacy_dag_walks` and in the uniffi snapshot as
  `blob.serve_legacy_dag_walks_total`. Each manifest is named once at
  INFO, bounded by LEGACY_LOGGED_CAP. The sunset comment cites
  CIRISPersist#994, and notes that manifests this node cannot open stay
  link-less after that backfill.
- `BlobChunkSource::attach_metrics` is a default no-op, the same shape as
  the transports' hook in CIRISEdge#809. `EdgeBuilder::build` hands it the
  edge's own bag. `PersistBlobChunkSource` keeps the bag in a OnceLock,
  and `with_metrics` sets it for a source built outside the builder.
  Every wrapper forwards the hook: edge_node's RowScopedChunkSource and
  the test fixtures.

Witnesses in self_dag_field_path_717:
- A linked DAG refuses a non-member chunk with zero walks, through a
  direct door and through the bag the builder attached. With the
  linked-non-member return removed, it fails: walks left 1, right 0.
- A link-less sealed DAG serves every chunk through the walk, and each
  serve is counted.
- A whole blob named as the DAG is refused with zero walks. With the
  chunk_dag head check removed, it fails: left 1, right 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

* fix(blob_swarm): an unreadable binding state fails closed; the sunset shrinks with #994 (#771 review)

The named DAG's binding_state is read before its link. If that read
errored, the gate used to warn and go on to the link, so a DAG that might
be withdrawn could be judged a Member on the link alone. It now answers
NotMember (ChunkNotInNamedDag), as every other unreadable read in the gate
does.

Witness in self_dag_field_path_717: B's federation_attestations table is
renamed away for one request, through B's SQLite writer, and then put
back. A live linked DAG's chunk is refused ChunkNotInNamedDag, and it is
served before and after. The warn-and-continue mutant fails with
left "PolicyDenied", right "ChunkNotInNamedDag". The old path did not
serve here only because persist's own chunk door failed on the same read.

The sunset comments now say the legacy walk shrinks with CIRISPersist#994.
They no longer say it goes when #994 lands: manifests this node cannot
open stay link-less after the backfill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

* test(self_dag_field_path_717): the legacy-walk counter lands in edge.metrics() of an Edge built normally (#771 on #810)

A fetches the link-less W's chunk from B over the wire. B's door serves
it through the legacy walk, and the walk is counted in
b.edge.metrics(), the one bag EdgeBuilder::build settled on. B's Edge is
built normally, and its wired source is a wrapper that forwards
attach_metrics.

The rebase onto #810 had auto-merged a second EdgeMetrics::new() for the
chunk source ahead of #810's metrics block. The source now attaches the
bag #810 settles on. Reintroducing the second bag fails this assertion
with left 0, right 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
emooreatx added a commit that referenced this pull request Oct 8, 2026
…Edge::metrics()

open_av_link reads the transport's attached bag (OnceLock, #810) instead
of the pre-#810 Option. The live-node witness now builds its transports
with no with_metrics and an Edge over each through EdgeBuilder, and
asserts the av_plane counters land in Edge::metrics().snapshot() on both
sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx added a commit that referenced this pull request Oct 8, 2026
metrics_snapshot carries every EdgeMetrics::av_plane entry as
av_plane.<label>, beside #810's transport.* counters, so the A/V plane is
visible to mobile operators as it is in the Rust snapshot and the pyo3
dict. Contract test the_uniffi_snapshot_carries_the_av_plane_ledger_805
(fails with the projection removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx added a commit that referenced this pull request Oct 8, 2026
…Edge::metrics()

open_av_link reads the transport's attached bag (OnceLock, #810) instead
of the pre-#810 Option. The live-node witness now builds its transports
with no with_metrics and an Edge over each through EdgeBuilder, and
asserts the av_plane counters land in Edge::metrics().snapshot() on both
sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
emooreatx added a commit that referenced this pull request Oct 8, 2026
metrics_snapshot carries every EdgeMetrics::av_plane entry as
av_plane.<label>, beside #810's transport.* counters, so the A/V plane is
visible to mobile operators as it is in the Rust snapshot and the pyo3
dict. Contract test the_uniffi_snapshot_carries_the_av_plane_ledger_805
(fails with the projection removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant