Skip to content

v20.3.0: adopt persist v41.2.0 + leviculum v0.25.0, the #568 dial detach, #573 pyo3 hybrid, leviculum#52 rotation seal, #575 quorum - #574

Merged
emooreatx merged 7 commits into
mainfrom
fix-573-pyo3-hybrid-envelope
Sep 5, 2026
Merged

v20.3.0: adopt persist v41.2.0 + leviculum v0.25.0, the #568 dial detach, #573 pyo3 hybrid, leviculum#52 rotation seal, #575 quorum#574
emooreatx merged 7 commits into
mainfrom
fix-573-pyo3-hybrid-envelope

Conversation

@emooreatx

@emooreatx emooreatx commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

The whole cut in one PR. Closes #573. Addresses #568 (second mechanism).

Additive at every public surface — two substrate adopts, one transport fix, one FFI fix.

Adopts

persist v41.0.0 → v41.2.0. Two minors, both currency. All four ABI constants unchanged across both, verify stays v14.2.0, so the >=41,<42 wheel floor holds. v41.1.0 (#807) fixes list_widening_candidates; v41.2.0 (#810) adds rejected to the CIRISLens TaskStatus vocabulary with a SQLite V136 rebuild. Edge drives none of it — no TaskStatus, no lens tables, no lens features enabled. v41.2.0's tag derefs to 52f6592, verified green at job level upstream before tagging.

leviculum v0.24.0+ciris.1 → v0.25.0+ciris.1. This one unbreaks our wheels. The upstream catch-up (+92) brought a BLE interface declaring bluer/dbus unconditionally, both pulling libdbus-sys, which builds on neither macOS nor Windows. leviculum's changelog names the consumer:

CIRISEdge's darwin and win_amd64 wheels resolve this crate as a git dependency and would have failed at build time.

Also carries leviculum#63 (below) and leviculum#52, which is addressed in its own section.

#568 — a round no longer destroys the link it paid for

The v20.2.0 fix addressed the missing-signer race. Re-measured: 181 s and 210 s, unmoved. The owner's key was on the peer at 31 s, so the key was never the bottleneck. This is a second, independent mechanism.

DEFAULT_ROUND_TIMEOUT is 10 s. A pathed peer gets LINK_ESTABLISH_TIMEOUT = 30 s to establish. The dial ran inline in the round's future, so a cold dial could never finish inside the round that started it — arithmetic, not load — and abandoning the round dropped the half-built link. Every retry began cold; the peer converged only when it dialled us. The observed shape matches exactly: timeout at +10 s, next completed round ~150 s later = five 30 s cadence ticks.

This is the #532 establish:identify gap one layer up. #532 stopped N coordinators discarding each other's links; nothing stopped a round discarding its own.

The fix. The dial runs in a spawned task holding a cloneable DialCtx, so abandoning a round abandons the wait, not the link. The task completes and publishes into reusable_dialed_linkdial_and_identify's existing "PUBLISH LAST" step — so the next round takes the fast reuse path.

Two deliberate choices worth review:

The three moved functions (dial_and_identify, reusable_link_to, path_table_snapshot) moved verbatim onto DialCtx — field names match, so no body changed — with thin delegates left behind so no existing call site moved.

Measurement, not inference

ladder.owner_binding_converged now records leviculum#63's retry_queued / retry_queue_cap / retry_dropped_total beside elapsed_ms. A slow convergence with a flat queue and one with dropped_total climbing are different bugs that read identically as a stopwatch value — and leviculum reported that exact pattern from the live canonical: "the retry queue climbed past its warning threshold and began discarding traffic while the log read as quiet."

Tests

test pins
the_dial_ctx_shares_the_transports_link_pool pointer identity on the shared maps — if dial_ctx() ever handed out copies, a detached dial would publish into a map nobody reads and the fix would look applied while changing nothing
a_cold_dial_cannot_fit_inside_a_round the inequality, with a failure message saying why the detach is not a workaround for it

Not claimed: that 181/210 s is fixed. The tests pin the mechanism; only a mesh run measures the outcome. One run each was not a regression call, and it is not a fix call either.

leviculum#52 — the rotation seal named the wrong hazard

Audited (subagent) against leviculum's finding that seal retires the old IFAC key for inbound, so a zero-dwell rotation strands in-flight traffic — ~50% packet loss upstream, drops_ifac incrementing exactly once each time.

Edge is the API surface, not a driver. It wraps the three phases and re-exports them to Python; nothing in edge sequences them — no activate → seal pair in src/, tests/, or src/bin/. Edge cannot commit the defect itself. Two things it told the operator were wrong:

  • The docstring named the wrong hazard. "Call after the convergence window" is about membership. Sealing also retires the old key for inbound, rejecting packets already on the wire from members that did re-key. Following the instruction correctly still stranded traffic — worse than no guidance, because it read as complete. Both seal docstrings rewritten.
  • Seal breaks inbound only, so the gauge points the other way. leviculum consults the alternate key only in verify_ifac; apply_ifac never reads it. A node's seal rejects old-masked traffic arriving from peers (upstream saw drops_ifac at the relay). retry_queued is this node's outbound backlog — zero licenses its peers to seal, not itself. The true precondition (every peer at zero) is unobservable from one node; under lockstep rotation each node polling its own gauge approximates it. Now on PyEdge, documented as a fleet proxy, never a measurement of what this node's own seal will reject. The retry queue is the long pole: masked before enqueueing, re-sent verbatim, count-capped (1024), no time bound.

A default — a conservative choice, not a derivation. DEFAULT_IFAC_ROTATION_DWELL = RESOURCE_TRANSFER_TIMEOUT (120 s), exported as default_ifac_rotation_dwell_ms(). Residency has no time bound, so nothing to derive from; 120 s reuses edge's committed answer to how long traffic can legitimately remain in flight. An earlier draft justified it by transfer duration and leviculum#62 — wrong (old-masked bytes stop at activate; #62 is receiver-side reassembly), caught by the audit and corrected before merge. 10 s was proposed; 10 s is shorter than every in-flight window edge already asserts. The safe default — ejecting a compromised member is a reason to go lower on purpose.

Two clocks, not to be harmonized. install → activate waits on key distribution (DEFAULT_CONVERGENCE_WINDOW, 300 s — activating early only degrades stragglers). activate → seal waits on packet drain (DEFAULT_IFAC_ROTATION_DWELL, 120 s — the silent one). Both docstrings and the constant now cross-reference each other so a later "cleanup" equalising them has to argue past the doc. Residency is bounded by neither a clock nor any deterministic quantity: no TTL, head-of-line, airtime-paced, and the 1024 cap bounds depth.

No seal_after_drain helper. It would have to invent a never-drains policy, and both answers are the operator's: block forever and the excluded member stays admitted; time out and seal and the hazard returns, buried in a function whose name promises it can't.

No test, stated plainly. Nothing sequences the IFAC trio, so zero coverage and nothing to regress from. The activate_next → seal_rotation pair in scope_addressing.rs is the address table — in-memory with an injected Instant, structurally blind to a drain hazard. Per upstream the natural test is a ~50% flake; a real test is a soak.

No prior art: upstream Reticulum's IFAC is static config with no rotation ceremony. The three-phase rotation is a leviculum fork invention, and 500 ms is its loopback floor.

#575 — the AV-28 quorum assertion reports while the trust root transitions

Matches CIRISPersist#809 on the same registry and the same guard. Downgraded to ::warning::; re-ran the #571 fault matrix to prove the change moves exactly one case — absent holders, missing charter root, unparseable protocol, old-shape-none-deployed and unrecognised shapes all still red the lane. Tracked in #575, paired with persist's #809 so the two restore together.

#573 — the pyo3 envelope helper signs hybrid

build_signed_inbound_envelope hard-coded None for the PQC half, so since v19.0.0 it could not build a single envelope on the wheel that ships it. Nothing here caught it because its Rust twin had been moved to hybrid — the two codepaths the doc calls identical had silently diverged, and the only caller of the broken one is Python.

pqc_seed_bytes is taken, not derived: a derivation convention has to be known identically by whoever registers the ML-DSA pubkey and whoever signs with it, and when they disagree the only symptom is a verify refusal at the far end with nothing pointing at the cause. derive_ml_dsa_65_pubkey_base64 closes that loop from the same code that signs.

Unblocks CIRISConformance's test_230_intake_gate and test_520_wire_vocabulary::test_tier1_and_opaque_variants_accepted.

Verification

  • cargo tree -ione copy each of persist 41.1.0, verify trio 14.2.0, leviculum 0.25.0
  • clippy -D warnings clean: pyo3-full --all-targets, three transport combos, the mesh-harness binary, the test-anchor lane
  • 1461 lib tests pass at 81c18ac; integration suite re-running (1867 passed at 757f97a; exit status captured from cargo, not a filtered pipe)

🤖 Generated with Claude Code

…ld any envelope since v19.0.0 (#573)

Additive: one new optional argument and one new static method. No existing call
signature changes.

`Edge.build_signed_inbound_envelope` exists so a harness can hand
`dispatch_inbound_bytes` a verifiable envelope without reverse-engineering the
wire format. Since v19.0.0 it could not build a single envelope on the wheel
that ships it:

  RuntimeError: build_signed_inbound_envelope: sign_envelope failed: … signer
  node-… has no ML-DSA-65 (PQC) half — every signature is the FULL Ed25519 +
  ML-DSA-65 hybrid, no fallback (CIRISEdge#425)

v19.0.0 made every signature the full hybrid — correctly — and this surface was
not moved with it. It hard-coded `None` for the PQC half, and its doc justified
that with two claims that were both false on this wheel: that software-only
signers don't carry a PQC half, and that the verify pipeline's Ed25519Fallback
policy makes the field optional. The policy no longer decides whether a PQC half
is required. The signer does, and it refuses at source.

The Rust twin the doc names as its counterpart
(tests/trust_short_circuit.rs::FedKey::local_signer) HAD been moved. That is why
nothing here caught it: the two codepaths the doc calls identical had silently
stopped being identical, and the only caller of this one is Python.

THE FIX. `pqc_seed_bytes` (optional, 32 bytes) builds the PQC half and signs the
full hybrid. The seed is TAKEN, not derived from `seed_bytes`: a convention
("same seed", "seed with byte 0 flipped") must be known identically by whoever
REGISTERS the ML-DSA pubkey and whoever SIGNS with it, and when the two disagree
the only symptom is a verify refusal at the far end with nothing in it pointing
at the cause — a bill this codebase has already paid twice.

`derive_ml_dsa_65_pubkey_base64` (new, static) returns the matching
pubkey_ml_dsa_65_base64 for the federation_keys row from the same code that
signs, so registering and signing cannot drift.

The argument stays OPTIONAL so the classical-only refusal stays reachable and
loud: omitting it fails at sign_envelope naming the missing half, which is the
correct answer and not a silent downgrade.

Signer construction moved into `software_hybrid_signer` so the tests drive the
function the wheel calls rather than a copy — the copy drifting IS the bug.
Three tests: the hybrid signature is present, classical-only refuses with the
exact token CIRISConformance keys its imperative xfail on, and the derived
pubkey equals the one the signer signs with.

The false doc paragraph is deleted rather than corrected; it documented the
defect.

Surfaced by CIRISConformance#91. Unblocks test_230_intake_gate (3 tests) and
test_520_wire_vocabulary::test_tier1_and_opaque_variants_accepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
@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.

…lives its round (#568)

Additive at every public surface. Two substrate adopts and one transport fix,
riding with the #573 FFI fix already on this branch.

## Adopts

persist v41.0.0 -> v41.1.0. All four ABI constants unchanged, verify stays
v14.2.0, so the >=41,<42 wheel floor holds. Its fix (#807) is in
list_widening_candidates, which edge drives none of — taken for currency.

leviculum v0.24.0+ciris.1 -> v0.25.0+ciris.1. leviculum#64 unbreaks OUR wheels:
the upstream catch-up (+92) brought a BLE interface declaring bluer/dbus
unconditionally, both pulling libdbus-sys, which builds on neither macOS nor
Windows. leviculum's changelog names the consumer — CIRISEdge's darwin and
win_amd64 wheels resolve this crate as a git dependency and would have failed
at build time. Also carries leviculum#63 (below) and leviculum#52's rotation
dwell note (edge drives rotation; worth an audit, not changed here).

## CIRISEdge#568 — a round no longer destroys the link it paid for

The v20.2.0 fix addressed the missing-signer race. Re-measured: 181 s and
210 s, unmoved. The owner's key was on the peer at 31 s, so the key was never
the bottleneck. Second, independent mechanism.

The scheduler abandons a round at DEFAULT_ROUND_TIMEOUT (10 s); a pathed peer
gets LINK_ESTABLISH_TIMEOUT (30 s) to establish. The dial ran INLINE in the
round's future, so a cold dial could never finish inside the round that started
it — arithmetic, not load — and abandoning the round DROPPED the half-built
link. Every retry began cold; the peer converged only when it dialled us.
Observed shape matches: timeout at +10 s, next completed round ~150 s later =
five 30 s cadence ticks.

This is the #532 establish:identify gap one layer up. #532 stopped N
coordinators discarding each other's links; nothing stopped a round discarding
its own.

FIX: the dial runs in a spawned task holding a cloneable DialCtx, so abandoning
a round abandons the WAIT and not the LINK. The task completes and publishes
into reusable_dialed_link (dial_and_identify's existing PUBLISH LAST step), so
the next round takes the fast reuse path.

DialCtx is a context struct rather than a Weak<Self> because
ReticulumTransport::new returns Self: a self-handle needs installing at every
construction site, and a site that forgot would silently lose the detach. The
three moved fns (dial_and_identify, reusable_link_to, path_table_snapshot)
moved VERBATIM onto it — field names match, so no body changed — with thin
delegates left behind so no existing call site moved.

The dial-gate permit is acquired INSIDE the task: held by the caller, it would
release on cancellation while the dial it gates still ran, leaving #532's
single-flight property true only for callers that survive.

MEASUREMENT: ladder.owner_binding_converged now records leviculum#63's
retry_queued / retry_queue_cap / retry_dropped_total beside elapsed_ms. A slow
convergence with a flat queue and one with dropped_total climbing are different
bugs that read identically as a stopwatch value — leviculum reported that exact
pattern from the live canonical ("the retry queue climbed past its warning
threshold and began discarding traffic while the log read as quiet").

TESTS: the_dial_ctx_shares_the_transports_link_pool pins the load-bearing
property by pointer identity — if dial_ctx() ever handed out copies, a detached
dial would publish into a map nobody reads and the fix would look applied while
changing nothing. a_cold_dial_cannot_fit_inside_a_round pins the inequality and
says in its failure message why the detach is not a workaround for it.

NOT CLAIMED: that 181/210 s is fixed. The tests pin the mechanism; only a mesh
run measures the outcome. One run each was not a regression call and is not a
fix call either.

Verified: cargo tree -i shows ONE copy each of persist 41.1.0, the verify trio
14.2.0, leviculum 0.25.0. clippy -D warnings clean on pyo3-full --all-targets,
three transport combos, the mesh-harness binary and the test-anchor lane.
1461 lib + 1867 integration tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
@emooreatx emooreatx changed the title fix(20.3.0): the pyo3 envelope helper signs hybrid — it could not build any envelope since v19.0.0 (#573) v20.3.0: adopt persist v41.1.0 + leviculum v0.25.0, the #568 dial detach, and the #573 pyo3 hybrid fix Sep 5, 2026
emooreatx and others added 3 commits September 5, 2026 15:45
…nsitions

Matches CIRISPersist#809, which made this call first on the same registry and
the same guard. Its reasoning applies here unchanged: CIRISRegistry v3.0.0 went
live 2026-09-04 and holders and authorizations move during that migration, so a
tag run blocked on a mid-migration count is a release blocked on somebody else's
deploy window.

Edge has a specific reason to weigh that heavily. v20.2.0 shipped with NO GitHub
Release at all because this same pre-flight refused a registry that was healthy
(#571). Losing a release to a registry state edge does not control has already
happened once.

Say plainly what it costs, because a check that cannot fail is a report: for the
length of the transition this leg CANNOT catch a registry serving a bundle whose
quorum has collapsed. What still fails in that case is the registration step
that follows — this leg is early warning, and this half of it is warning only.

The downgrade moves EXACTLY ONE case, re-verified against the #571 fault matrix:

  live bundle, quorum met          pass -> pass
  quorum NOT met (1 of 2/3)        FAIL -> pass + ::warning::
  no holders                       fail -> fail
  no charter_root_key_id           fail -> fail
  unparseable consensus_protocol   fail -> fail
  old shape, deployed steward      pass -> pass
  old shape, none deployed         fail -> fail
  unrecognised third shape         fail -> fail

An absent or unrecognisable root still reds the lane; only a countable but short
quorum is waved through.

CIRISEdge#575 tracks restoring it, and says the two repos should restore
together — two siblings enforcing different thresholds against one registry
means one can release while the other cannot, for reasons neither repo's code
explains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…e observable never reached Python (leviculum#52)

Audit of leviculum#52 against edge. Edge is exposed as the API SURFACE, not as
a driver: it wraps the three IFAC rotation phases (ifac_install_next /
ifac_activate_next / ifac_seal_rotation) and re-exports them to Python, but
nothing in src/, tests/ or src/bin/ sequences activate -> seal. Edge cannot
commit the zero-dwell defect itself. It hands it to an operator.

TWO REAL DEFECTS, both in what edge tells that operator.

1. THE DOCSTRING NAMED THE WRONG HAZARD. Both wrappers said "call after the
   convergence window". That window is about MEMBERSHIP — every member holding
   the new key. But sealing also retires the old key for INBOUND, so it rejects
   packets already on the wire under the old mask, and those were sent by the
   members that DID re-key. An operator who followed the instruction correctly
   still stranded traffic. Upstream measured a zero-dwell rotation losing its
   packet in ~50% of runs with the peer's drops_ifac incrementing exactly once.

   Worse than no guidance, because it reads as complete: it documents the
   INTENDED exclusion and is silent on the accidental one.

2. THE ONE OBSERVABLE NEVER REACHED PYTHON. retry_queue_gauges (added this cut
   for #568) had exactly one caller, src/bin/edge_node.rs. So the operator
   holding the one call that can strand traffic could not see the one thing
   edge is able to see, and had to guess the whole window blind. Now exposed on
   PyEdge.

WHAT THE DOCS NOW SAY. Four things carry the retired mask: this node's retry
queue, its socket buffer, bytes in flight, and the peer's receive buffer. Edge
observes exactly one. retry_queued == 0 is NECESSARY, NOT SUFFICIENT — stated
as such, because a reader who takes it as sufficient has been given a new way
to be wrong. Its worth is collapsing the wait from "however long anything could
take" to one link-latency of slack, defensible per medium. leviculum's 500 ms
is quoted as what upstream calls it: a loopback floor, not a deployment value.
And the dwell is PER-NODE from that node's own activate, so a fleet need not
seal in lockstep.

NO HELPER, DELIBERATELY. A seal_after_drain() that polls the gauge must invent
a policy for the queue never draining, and both answers belong to the operator,
not to edge: block forever and the member being excluded stays admitted
indefinitely; time out and seal anyway and the hazard returns, now buried in a
function whose name promises it cannot happen. Which is right depends on why
the rotation is happening.

NO TEST, AND SAYING SO. Nothing in edge sequences the IFAC trio, so there is
zero coverage and nothing to regress from. scope.rotation and
conformance.rotation_frame_loss drive the MLS epoch advance and edge's
scope-address table, not IFAC; the activate_next -> seal_rotation pair in
scope_addressing.rs is the address table, pure in-memory with an injected
Instant, and cannot detect a drain hazard in either mechanism. Per upstream's
own evidence the natural test is a ~50% flake, so a single green run would
prove nothing anyway — a test worth having here has to be a soak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…rotation seal (leviculum#52)

persist v41.1.0 -> v41.2.0. Tag derefs to 52f6592, verified green at job level
upstream before tagging. All four ABI constants unchanged, verify stays v14.2.0,
>=41,<42 floor holds. Its fix (#810) adds `rejected` to CIRISLens TaskStatus
with a SQLite V136 rebuild — edge has no TaskStatus, no lens tables, and enables
none of the lens features. Currency only. cargo tree -i: ONE copy.

DEFAULT_IFAC_ROTATION_DWELL = RESOURCE_TRANSFER_TIMEOUT (120 s), exported to
Python as default_ifac_rotation_dwell_ms(), referenced from both seal
docstrings. ANCHORED, NOT CHOSEN: 10 s was proposed, and 10 s is shorter than
all four of this file's in-flight windows (5 / 30 / 30 / 120 s). Edge already
asserts a resource transfer can legitimately be in flight for two minutes, and
since leviculum#62 a single logical delivery spans many segments over that
whole window under the key being retired — a shorter dwell guillotines a
transfer edge itself still considers healthy.

It is the SAFE default, not the right number for every rotation: ejecting a
compromised member is a reason to override downward on purpose and accept the
drops. That decision depends on why the rotation is happening, which is why it
is a constant to deviate from and not a helper that decides.

No prior art to borrow: upstream Reticulum's IFAC is static per-interface
config with no rotation ceremony — the three-phase rotation is a leviculum fork
invention (leviculum#52). leviculum's own 500 ms is a loopback floor upstream
explicitly disclaims as a deployment value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
@emooreatx emooreatx changed the title v20.3.0: adopt persist v41.1.0 + leviculum v0.25.0, the #568 dial detach, and the #573 pyo3 hybrid fix v20.3.0: adopt persist v41.2.0 + leviculum v0.25.0, the #568 dial detach, #573 pyo3 hybrid, leviculum#52 rotation seal, #575 quorum Sep 5, 2026
emooreatx and others added 2 commits September 5, 2026 16:30
…was justified by the wrong mechanism

Two corrections from the leviculum#52 audit, both verified against the pinned
source before editing.

1. SEAL BREAKS INBOUND ONLY, SO retry_queued POINTS THE OTHER WAY.
   leviculum consults the alternate key in exactly one place, verify_ifac —
   the inbound path. apply_ifac (outbound) never reads it. So a node's seal
   changes only what THAT node accepts, and what it rejects is old-masked
   traffic arriving FROM PEERS. Upstream's failing run showed drops_ifac at
   the relay: the relay's seal rejecting a member's in-flight output.

   retry_queued counts THIS node's OUTBOUND backlog. Reaching zero therefore
   licenses this node's PEERS to seal — not this node. The true precondition
   for a node sealing is "every peer's retry_queued is zero", which no single
   node can observe. Under lockstep rotation (the only mode edge supports,
   since nothing sequences the phases) every node polling its own gauge to
   zero approximates that fleet condition, and the local reading is a
   reasonable proxy. It is NOT a measurement of what this node's own seal
   will reject: a local zero while one congested peer still holds 800
   old-masked packets is a green light that means nothing. All four docstrings
   (both seals, the gauge accessor, the pyo3 gauge) now say so.

   The earlier text told the operator to poll their own gauge to zero and
   then seal — a procedure that would have passed on the very node whose
   seal was about to drop packets.

2. THE 120 s ANCHOR: RIGHT NUMBER, WRONG STATED REASON.
   The earlier doc claimed leviculum#62 made a whole resource transfer
   old-masked at once, so the drain window was a transfer's 120 s duration.
   Wrong on both counts. Old-masked bytes stop being GENERATED at each peer's
   activate — every packet dispatched after it is new-masked, and receivers
   accept those because they installed the new key in phase 1. A transfer
   straddling activate is part old, part new; its duration is not the drain
   window. And #62 is receiver-side reassembly; it says nothing about when
   the sender masks.

   What actually bounds the window is retry-queue residency, and that has NO
   time bound: leviculum masks before enqueueing (transport.rs:423-448 pushes
   the wrapped bytes on BufferFull), re-sends queued bytes verbatim
   (driver/mod.rs:5205-5228), caps the queue by COUNT (1024), and drains it
   only as fast as the interface unblocks. There is no principled number to
   derive. 120 s stays — conservative, and conservative is correct for a safe
   default — but it is now stated as a CHOICE that reuses edge's committed
   answer to "how long can traffic legitimately remain in flight", not as a
   derivation from a mechanism that does not exist.

Both corrections also applied to the release notes and PR body. Verified from
the checkout: no alt branch in apply_ifac; retries.push(SendRetry{data:
send_data}) after apply_ifac; retry.data re-sent verbatim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…onized (leviculum#52, closes the audit)

A rotation has two dwells measuring different things, and this repo carries a
constant for each:

  install -> activate   key DISTRIBUTION   DEFAULT_CONVERGENCE_WINDOW  300 s
  activate -> seal      packet DRAIN       DEFAULT_IFAC_ROTATION_DWELL 120 s

Activating early is not a safety failure — upstream keeps the old key
accept-only, so a straggler's outbound still lands while its inbound degrades
until it upgrades (driver/mod.rs:2823-2826). Sealing early is the silent one.
The pre-fix prose collapsed both into "the convergence window", which is how
the hazard was mis-framed. Both seal docstrings and the dwell constant now
distinguish them and cross-reference each other, so a later cleanup that
notices two dwell constants and equalises them has to argue past the doc.

Residency sharpened to what is checkable: the retry queue is a bare
VecDeque<Vec<u8>> with no age, TTL or expiry; drain_retry_queues is
head-of-line and rate-gated by next_slot_ms; a packet leaves only by send,
cap-overflow eviction of the oldest, disconnect, or interface removal. The
cap bounds DEPTH, not time. RESOURCE_TRANSFER_TIMEOUT is cited as the longest
window this file already treats as survivable in-flight, not as a claim about
masking duration.

Audit closed: the subagent reports nothing else outstanding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
@emooreatx
emooreatx merged commit 905a50e into main Sep 5, 2026
26 checks passed
@emooreatx
emooreatx deleted the fix-573-pyo3-hybrid-envelope branch September 5, 2026 23:00
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