Repository navigation
release(40.1.0): leviculum v0.29 — known-destinations backpressure, off-lock resource assembly, and transport counters that finally reach production - #810
Conversation
There was a problem hiding this comment.
💡 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".
| note_packet_dropped( | ||
| ctx.metrics, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| let evictions = self.node.known_destination_evictions(); | ||
| if let Some(m) = self.metrics.as_ref() { | ||
| m.set_known_destination_evictions(evictions); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
…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
There was a problem hiding this comment.
💡 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".
| for transport in &self.transports { | ||
| transport.attach_metrics(self.metrics.clone()); | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
…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
There was a problem hiding this comment.
💡 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".
| fn attach_metrics(&self, metrics: crate::observability::EdgeMetrics) { | ||
| let _ = self.metrics.set(metrics); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
…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
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
| for transport in self.inner.transports() { | ||
| transport.refresh_metrics(); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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).
…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
…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
There was a problem hiding this comment.
💡 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".
| let metrics = self | ||
| .transports | ||
| .iter() | ||
| .find_map(|t| t.attached_metrics()) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| 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 } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
05c3ef9 to
95a3bdf
Compare
…(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
There was a problem hiding this comment.
💡 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".
| pub fn is_same_bag(&self, other: &Self) -> bool { | ||
| Arc::ptr_eq(&self.envelopes_sent_total, &other.envelopes_sent_total) |
There was a problem hiding this comment.
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 👍 / 👎.
…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
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
…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
…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>
…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
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
…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
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
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.82a9d601, main CI green): includes 0.28 (upstream +164, leviculum#49 retain-aware LRU identity-cap eviction, no-resurrect flush) and 0.29 (leviculum#71 off-lock resource assembly, Codex round-four resource fixes). No public fn signature edge compiles against changed.transport_packets_droppedandknown_destination_evictions(per transport, summed) inEdgeMetricsBundle, PyO3metrics_snapshot()and UniFFI counters; the eviction gauge refreshes on the announce tick and on every snapshot read.ReticulumTransport::with_metricshad no production caller, so Announce intake is unbounded: 10,460 transport_destinations and 8,494 announced_peers for ~700 registered keys, with nothing that ever removes a row #530announce_intake_evictionsand The announce rides the link, and attribution never waits on the directory — two-stage announce receipt, first-seen never dropped, link_before_binding (root cause of #624 / CIRISServer#609) #627link_before_bindingread zero in the field since they shipped.EdgeBuilder::buildnow attaches the Edge's bag to every transport before any listener exists (coversspawn_background_listeners,Edge::run, and hosts callinglistenthemselves), adopts a bag a transport was built with, and refuses (EdgeError::Config) transports built with different bags.Transport::{attach_metrics, attached_metrics, refresh_metrics}(defaulted),ReticulumTransport::known_destination_evictions(),EdgeMetrics::is_same_bag;set_known_destination_evictionstakes a source id (new in this release, no prior holders).Gates on
95a3bdf(+0104e49: FFI suite 17/17, #809 lib tests, CI clippy ×3) (CI feature set 1 unless noted): clippy-D warningson 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