Every captured commit receives deterministic security triage and a separate communication-quality score. Security candidates and broader second-pass signals receive full-patch Ollama analysis.
Message quality measures whether a commit identifies its scope, purpose, rationale, testing, and supporting references. It does not change the security-severity score.
This commit changes how the Lightning networking code handles oversized encrypted messages. Previously, certain conditions would cause the program to crash with a panic. Now the code returns errors instead, which is a defensive improvement…
panic-to-error conversion for oversized message encryption/decryptiondenial-of-service hardening against oversized peer messagesdebug_assert retained to preserve test coverage of invariant violations
This commit simply updates the 'repository' web links in 15 package metadata files from GitHub to a self-hosted Forgejo instance. It does not change any program code, build logic, dependencies, or security behavior. There is no security is…
This commit fixes a bug in the Lightning Dev Kit where, after a disconnection, a node could fail to retransmit a 'splice_locked' message to a peer that was still waiting for transaction signatures. Without this retransmission, the two peer…
Protocol state desynchronization between channel peers after reconnectionMissing retransmission of splice_locked for 0-conf splice channelsPotential channel unusability or stuck splice negotiation
This commit fixes a small accounting bug in how the Lightning wallet estimates the size (and therefore transaction fee) of a special Bitcoin transaction that sweeps funds back to the user after a channel closes. The old code always assumed…
debug assertion failure possible in development/testing buildstransaction weight/fee estimate overestimation up to 3 WUconstant replaced with per-descriptor length computation
This commit changes the project's internal code-review workflow. It stops automatically assigning a human reviewer when a pull request is opened; instead, contributors must manually click a button to request a reviewer after first addressi…
This commit fixes a bug in LDK's Lightning channel reconnection logic after a splice (a way to resize a channel's on-chain funds). If one peer had already received the splice signatures but the other had not, and then they disconnected and…
Protocol-state inconsistency on reconnection after splice signature exchangePotential channel stall/force-close due to quiescence not being exited before commitment updateFuzzer-discovered edge case in Lightning splicing retransmission
This commit adds a new option for Lightning invoice creators to explicitly tell payers not to use multi-path payments (MPP) when paying an invoice. It does not change any enforcement rules; it only changes what feature bits are advertised …
New API surface for feature advertisement controlExplicit documentation that the method does not enforce single-HTLC receipt, shifting enforcement responsibility to callersNo removal or weakening of existing validation logic
This commit adjusts the project's continuous integration (CI) test script to pin an older version of a build-time helper crate called `jobserver` when using older Rust compilers. It is a build compatibility fix, not a security patch, and d…
This commit only adds new test code. It exercises how a newer version of LDK exchanges saved channel data with the older LDK 0.2 release when a channel has a pending splice. There is no change to production logic, no bug fix, and no securi…
This commit removes the 'Option' wrapper from several HTLC amount fields, making them required instead of optional. It is a cleanup/refactoring change that simplifies the code by assuming the amount is always known. The commit message fram…
Removal of Option wrapper for financial amount fieldsSerialization format change from optional to required TLV fieldsLoss of backward compatibility with older serialized monitor/channel state
This commit is a code cleanup inside the project's test suite. It replaces a helper function with several hard-to-read positional arguments (like bare `false` and `None`) with a 'builder' pattern that names each option. This makes the test…
This commit is a feature addition, not a vulnerability fix. It extends rust-lightning's BOLT 12 payment support so that when a wallet pays a BOLT 12 invoice, the paid invoice is saved through retries and restarts and is later exposed in th…
New BOLT 12 payer proof feature: persists paid invoice across retries/restarts and exposes it in Event::PaymentSentPayer signing key re-derived from invoice payer metadata rather than storing extra key materialAdds end-to-end test for proof creation, verification, and bech32 round-trip
This commit is a code cleanup (refactor) in the Lightning Dev Kit library. It moves existing payer key-derivation logic into shared helper functions so that future 'payer proof' features can reuse the same code. The change does not appear …
Refactor only: moves existing key derivation/verification logic into helpers without changing algorithmsAdds new public API `Bolt12Invoice::derive_payer_signing_keys` for payer proof key recoveryNo mention of vulnerability, bug, CVE, security fix, or exploit in commit title/message
This commit changes a CI workflow for the rust-lightning project. It stops trying to push new fuzz test inputs directly to a corpus repository from automated test runs, and instead uploads them as a temporary artifact that a separate sched…
This commit adds partial support in the Lightning Dev Kit node software for receiving and temporarily holding multi-part trampoline payments, then deliberately rejects them once all parts arrive because full outbound forwarding is not yet …
New trampoline forward handling path accumulates MPP parts before rejectingDebug assertion guards first-HTLC failure in MPP mergeTODO comment flags possible MPP inconsistency in next_node_id across trampoline parts
This commit removes a redundant 32-byte shared secret field from an internal data structure used when forwarding trampoline payments in the Lightning Dev Kit. The developers realized the secret was already stored inside each previous hop's…
Removes redundant secret field from in-memory/persisted stateChanges TLV serialization layout for HTLCSource::TrampolineForwardBreaking persistence change acknowledged by commit author
This commit changes the project's automated reviewer-assignment workflow to stop using a long-lived secret token and instead request a short-lived authentication token from the Forgejo CI service. This is a security-hardening improvement: …
Removal of long-lived repository secret from CI workflowAdoption of OIDC-based short-lived token for API authorizationWorkflow runs in pull_request_target context with no code checkout
This commit only adds new fuzz-testing commands to an existing test harness. It lets the fuzzer temporarily block and then re-enable the local node's own signing operations during simulated channel failures. There is no change to productio…
This commit hardens the project's automated build and test scripts by replacing loose version tags like 'actions/checkout@v4' with exact commit hashes served from a specific domain. This prevents a compromised or renamed third-party action…
CI/CD supply-chain hardeningAction reference pinning to immutable commit hashUse of explicit action mirror URL
This commit fixes a logic bug in the Lightning Dev Kit's channel splicing code. Previously, the code wrongly assumed that no unrelated monitor update could be pending when a splice `tx_signatures` message arrived while the channel was quie…
Assertion relaxation in state-machine handling of splice `tx_signatures`Race condition between unrelated HTLC preimage monitor update and splice signature exchangePotential panic or protocol stall due to overly strict debug assertion
Expand any commit for its author, full message, clarity score, changed files, triage signals, analysis, and source link.
Lower-priorityQueue a splice on a channel with an inherited splice until it locksby Jeffrey Czyz · 1127a38a · Jul 9, 2026 · 2 filesMessage 73 · AdequateTriage 0Details
Commit message · Jeffrey Czyz
Queue a splice on a channel with an inherited splice until it locks
A pending splice negotiated before an upgrade from a prior LDK version (e.g. 0.2) comes back without its feerate or our contribution: 0.2 persists neither and drops the odd TLVs that carry them. Without them the inherited splice cannot be RBF'd.
Rather than refuse to splice the channel, leave the RBF feerate floor unset so the new splice is queued and begins as a fresh splice once the inherited splice locks -- the same path taken whenever a contribution cannot replace the pending candidate via RBF, such as on a zero-conf channel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73/100 · AdequateMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
Compute the minimum splice RBF feerate via PendingFunding
splice_channel derived the pending splice's minimum RBF feerate by mapping a standalone helper over the prior round's feerate, shadowing the helper's name with the resulting local. Encapsulate the derivation as PendingFunding::min_rbf_feerate, mirroring the FundingTemplate accessor it feeds, and keep the formula as an associated function for the call sites that derive the prior feerate differently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73/100 · AdequateMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
A channel may have splice attempts in progress: a contribution we have committed but not yet begun negotiating, one under negotiation with the counterparty, and any negotiated transactions (the original splice and any RBF replacements) waiting on confirmations. This state was only observable through events and the broadcaster's TransactionType::InteractiveFunding, neither of which can be queried on demand.
Add an optional splice_details field to ChannelDetails. Every splice or RBF round on the channel that has not yet locked is reported as a candidate, each carrying our contribution to it (if any) and a status giving the stage it has reached, from a contribution awaiting quiescence, through negotiation, to a signed transaction awaiting confirmations. This also reports the single candidate that has confirmed, with its confirmation progress and whether we have sent splice_locked for it, and the txid of any splice_locked received from the counterparty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
With rust-bitcoin `0.32.4` release, verifying a script is a witness program delegates to `Script::witness_version`, This makes adding `Script::is_witness_program` check alongside a witness_version check redundant.
68/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
Include per-HTLC amounts in `PaymentForwarded` locators so callers can account for each channel independently when a forward uses multiple incoming or outgoing HTLCs.
AI-assisted-by: OpenAI Codex
68/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
Lower-priorityCorrect CHANGELOG dates on 0.2.1 and 0.2.2by Matt Corallo · 253018ca · Jul 8, 2026 · 1 fileMessage 45 · ThinTriage 0Details
Commit message · Matt Corallo
Correct CHANGELOG dates on 0.2.1 and 0.2.2
45/100 · ThinMessage clarity
✓ Descriptive subject✓ Names a concrete action or component! No meaningful explanatory body
Monitor event draining expects BumpTransaction events, which the harness must mine, plus SpendableOutputs and DiscardFunding events, which it intentionally ignores because it does not model an external wallet. Make that whitelist explicit by panicking on any other monitor event instead of silently ignoring it.
80/100 · StrongMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Explains rationale or failure mode
Store splice contributions with their negotiated candidates
PendingFunding tracked our splice contributions in a compact list implicitly aligned to the tail of the negotiated candidates, with the in-flight negotiation round's contribution as the implicit last entry. Every consumer had to re-derive this positional relationship, which is easy to get wrong -- e.g., attributing an in-flight round's contribution to a completed counterparty-only candidate.
Instead, store each candidate's contribution with the candidate itself and give the in-flight round's contribution its own field, making such misattribution unrepresentable. The contributions still form a suffix of the candidates -- once a round includes our contribution, every subsequent round carries it forward (possibly feerate-adjusted) so the splice intention is never lost -- which is now asserted when a round completes.
Serialize this so a single (non-RBF) pending splice stays loadable by LDK 0.2 while RBF is refused loudly. 0.2 predates per-candidate contributions, the in-flight contribution, and the last-negotiated feerate, so writing any of them in an even (required) TLV would make 0.2 refuse even a single splice it can otherwise operate. The legacy TLV 3 therefore carries only the first candidate's funding -- the single-splice view 0.2 reads -- while the full candidate list, the in-flight contribution, and the feerate go in odd TLVs that 0.2 skips. An even gate TLV is written only when there is more than one negotiation round (RBF), so 0.2 loads single splices and refuses RBF, which it cannot operate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Explains rationale or failure mode
Require `htlc_value_satoshis` in [pending] `HTLCUpdate`s
In 0.0.100 we started tracking the amounts being claimed in `OnchainEvent::HTLCUpdate` and then also in `MonitorEvent::HTLCUpdate`'s `HTLCUpdate`. It was always set, but stored as an `Option` to support further downgrade. Because these objects time out after `ANTI_REORG_DELAY` (6) blocks, there's not really much reason to keep supporting backwards compatibility to upgrade with such objects without an amount.
In 0.0.115, we started providing the amount in `PaymentForwarded`. For whatever reason, despite the event only being generated in cases where we had amounts, the field was added as an `Option`.
Still, in 0.0.118 we started generating them from both off-chain and on-chain claims. For off-chain claims it was always set, but for claims which originated from on-chain claims, the amounts came from the `MonitorEvent::HTLCUpdate` and thus were always an `Option`. If we no longer care about `MonitorEvent::HTLCUpdate`'s without a claim amount, we no longer need to worry about `Event::PaymentForwarded` either. Thus, we make it required here as well.
85/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Explains rationale or failure mode
Why it was queued
update trust
AI analysis · Low 25/100
This commit removes the 'Option' wrapper from several HTLC amount fields, making them required instead of optional. It is a cleanup/refactoring change that simplifies the code by assuming the amount is always known. The commit message frames this as removing legacy downgrade support, not as fixing a security bug. There is no direct evidence in the diff of an exploitable vulnerability.
Use a builder for sign_interactive_funding_tx arguments
The signing helper had accumulated several boolean/option parameters beyond the two nodes, so call sites passed opaque positional `false`s and bare `None`s whose meaning was unclear without consulting the signature.
Replace the two overloaded functions with a single `sign_interactive_funding_tx` taking a `SignInteractiveFundingTxArgs` builder, mirroring `PassAlongPathArgs`: `new(initiator, acceptor)` defaults to a first-attempt splice on a confirmed channel with no acceptor contribution, and each non-default behavior is opted into by a named method (`zero_conf`, `with_acceptor_contribution`, `replacing`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78/100 · AdequateMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Names security-relevant behavior explicitly
Why it was queued
signing boundary
AI analysis · Informational 15/100
This commit is a code cleanup inside the project's test suite. It replaces a helper function with several hard-to-read positional arguments (like bare `false` and `None`) with a 'builder' pattern that names each option. This makes the tests easier to read and maintain, but it does not change any production behavior or fix a security bug.
Lower-priorityFilter stale closed hop sends in chanmon fuzzby Joost Jager · 53b399c4 · Jul 7, 2026 · 1 fileMessage 78 · AdequateTriage 5Details
Commit message · Joost Jager
Filter stale closed hop sends in chanmon fuzz
Hop sends can route over an open SCID while LDK's non-strict forwarding still selects a parallel channel that the harness has already tracked closed but a node still lists. Skip those API sends until the stale listing clears, while keeping the existing open-id checks for fully dropped closed channels.
78/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification
When stale message events are delivered after a channel close, the recipient can respond with the expected STFU warning instead of the control error path. Treat that warning as expected for channels the harness already tracks closed, while documenting that stale events should still be delivered so handlers exercise their normal error paths.
78/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification
ln: persist the paid BOLT 12 invoice and build payer proofs
Carry the paid `Bolt12Invoice` through the outbound payment so it survives restarts, and surface it as a `PaidBolt12Invoice` on `Event::PaymentSent` so the payer can build a payer proof. The payer signing key is re-derived from the invoice's own payer metadata, so no extra key material is stored.
`PaidBolt12Invoice` now lives in `offers::payer_proof`; existing async payment tests and a test helper are updated to construct it via the new API. Adds an end-to-end test that pays a BOLT 12 offer and builds + verifies a payer proof from the resulting `Event::PaymentSent`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification
Why it was queued
signing boundary
AI analysis · Low 25/100
This commit is a feature addition, not a vulnerability fix. It extends rust-lightning's BOLT 12 payment support so that when a wallet pays a BOLT 12 invoice, the paid invoice is saved through retries and restarts and is later exposed in the PaymentSent event as a PaidBolt12Invoice. The wallet can then use that object to build a cryptographic 'payer proof' that selectively discloses invoice fields to prove to a third party that it paid. The payer signing key is re-derived from data already in the invoice, so no extra secret key storage is needed. There is no indication in the commit that this fixes a security bug; it is new functionality with tests.
Move the invoice/refund payer key derivation logic into reusable helpers so payer proofs can derive the same signing keys without duplicating the metadata and signer flow.
85/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Uses a recognizable type or scope✓ Provides detailed explanatory context
Why it was queued
secret or key materialsigning boundarysigning or wallet path
AI analysis · Informational 17/100
This commit is a code cleanup (refactor) in the Lightning Dev Kit library. It moves existing payer key-derivation logic into shared helper functions so that future 'payer proof' features can reuse the same code. The change does not appear to fix a security bug; it reorganizes existing logic and adds a new public method to re-derive a payer's signing keys from invoice data. No vulnerability or exploit is described in the commit itself.
Extend the BOLT 12 merkle module with selective-disclosure support: build the full merkle tree from a TLV stream, compute the omitted-TLV markers and the minimal set of missing hashes for omitted subtrees, and reconstruct the merkle root from a partial disclosure. These are the primitives a payer proof is built on.
Co-Authored-By: Rusty Russell <rusty@rustcorp.com.au> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: OpenAI Codex <codex@openai.com>
73/100 · AdequateMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
Add the `payer_proof` module: `PayerProof`/`UnsignedPayerProof`, the `PayerProofBuilder` (with selective disclosure and a derived-key path), bech32 `lnp` encoding, and parse-time verification, implementing the payer proof extension to BOLT 12 (https://github.com/lightning/bolts/pull/1295). Also exposes the offer/invoice TLV-type constants and an invoice-bytes accessor used to build proofs, and a `Sha256` `Writeable`/`Readable` impl for the proof hashes.
Co-Authored-By: Rusty Russell <rusty@rustcorp.com.au> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: OpenAI Codex <codex@openai.com>
86/100 · StrongMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification✓ Links an issue, advisory, or supporting reference
Why it was queued
defensive validation
Security candidateUpload new fuzz corpus entries as a short-lived CI artifactby Matt Corallo · b9f55b6c · Jul 5, 2026 · 1 fileMessage 83 · StrongInformational 15Details
Commit message · Matt Corallo
Upload new fuzz corpus entries as a short-lived CI artifact
Fork-PR runs get no credentials from Forgejo — neither secrets nor authorized-integration identity tokens — so the fuzz job cannot push new corpus entries to the corpus repo from CI. Instead, clone the corpus from this Forgejo instance (rather than the GitHub copy, so new entries are detected against the repo they will land in), stage the new entries plus any SIG* crashes like the GitHub workflow does, and upload them as an `hfuzz-corpus` artifact with a two-day retention. The ldk-fuzzing-corpus repo's nightly job sweeps these artifacts into a corpus pull request and deletes them once processed.
Unlike the GitHub workflow's version, the crash-staging loop here uses the `rust-lightning/<target>` prefix the corpus entries are actually staged under (upstream checks the wrong path, so no crash file is ever picked up there), and it stages crashes for targets that produced no new corpus entries rather than only creating the target directory as a side effect of staging corpus files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification
Why it was queued
fuzzing or regression evidencecredential or privilege state
AI analysis · Informational 15/100
This commit changes a CI workflow for the rust-lightning project. It stops trying to push new fuzz test inputs directly to a corpus repository from automated test runs, and instead uploads them as a temporary artifact that a separate scheduled job later collects. This is a workflow reliability and credential-handling improvement, not a security fix or vulnerability.
Add upgrade test for legacy post-close monitor update persistence
60/100 · AdequateMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Mentions testing or verification! No meaningful explanatory body
AI review queuedFix assign-reviewer's auth by setting the authorized integration audienceby Matt Corallo · 055d334d · Jul 3, 2026 · 1 fileMessage 55 · ThinTriage 12Details
Commit message · Matt Corallo
Fix assign-reviewer's auth by setting the authorized integration audience
55/100 · ThinMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Names security-relevant behavior explicitly! No meaningful explanatory body
Why it was queued
signing or wallet pathsecond-pass: security-sensitive path
AI review queuedci: don't double-assign reviewers, support manual assignment runsby Matt Corallo · 24651a1a · Jul 3, 2026 · 1 fileMessage 90 · StrongTriage 12Details
Commit message · Matt Corallo
ci: don't double-assign reviewers, support manual assignment runs
The assign-reviewer workflow blindly picked a random reviewer every time it ran, so a re-run (or any future non-opened trigger) could request review from a second person even when someone was already on the PR. Teach it to inspect the PR state first:
* Anyone already requested as a reviewer or who has submitted a review is never picked. The author self-reviewing (commenting on their own PR) doesn't count. * On automatic runs (including re-runs), skip assignment entirely if anyone from the REVIEWERS pool has already reviewed or been requested; reviews from people outside the pool are ignored.
Also add a workflow_dispatch trigger taking a PR number so a reviewer can be assigned manually. Manual runs skip the "someone is already on it" check and always add a new (not-yet-involved) reviewer if an eligible candidate remains.
This should let us fully emulate the old bot's second-reviewer assignment logic fully via the action. A first reviewer can go hunt in the actions page and trigger a second assignment (but we'll add a UI element in the PR page above merge to trigger this in a nice UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Uses a recognizable type or scope✓ Provides detailed explanatory context✓ Names security-relevant behavior explicitly
Why it was queued
signing or wallet pathsecond-pass: security-sensitive path
Extract the all-node broadcaster drain into a helper and use it from the finish-time mempool cleanup loop. This leaves relay behavior unchanged while giving cleanup paths a shared relay primitive.
78/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification
Route BumpTransaction events through the wallet-backed handler and drain raw ChainMonitor events during normal event processing. This matches the background processor path and lets anchor commitments and claim transactions enter the harness mempool from fuzz opcodes as well as final cleanup.
During settle_all, alternate event processing, relay, and mining until tracked force-closed channels no longer report claimable balances. The bounded loop catches stuck on-chain cleanup instead of leaving broadcasts or claims unresolved.
78/100 · AdequateMessage clarity
✓ Descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification
Why it was queued
fuzzing or regression evidence
Lower-priorityfuzz: sync reloaded monitors from their own best blockby Joost Jager · 3d9a10d2 · Jul 3, 2026 · 1 fileMessage 83 · StrongTriage 5Details
Commit message · Joost Jager
fuzz: sync reloaded monitors from their own best block
A node's channel monitors can be persisted at different heights, so on reload they are not all at the same chain tip. Driving them to the tip through the shared ChainMonitor from the oldest monitor's height replays blocks that monitors already ahead have seen, which they interpret as a reorg. That reorg discards force-close claims registered at a later height, leaving the closed channel with no transaction to broadcast.
Sync each monitor to the tip from its own best block instead, and sync the manager separately, matching LDK's per-listener startup contract.
83/100 · StrongMessage clarity
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification