Skip to content

release: Merge main into v0.1.x-branch - #1072

Merged
sputn1ck merged 153 commits into
v0.1.x-branchfrom
kon/sync-main-to-v0.1.x
Jul 30, 2026
Merged

release: Merge main into v0.1.x-branch#1072
sputn1ck merged 153 commits into
v0.1.x-branchfrom
kon/sync-main-to-v0.1.x

Conversation

@sputn1ck

Copy link
Copy Markdown
Member

Summary

Merge main at da2f5441 into v0.1.x-branch at 7cbef62b through a dedicated integration branch.

This intentionally makes every commit reachable from the recorded main tip an ancestor of the release branch, while preserving the release line's version state. GitHub may show 151 main-side commits because the branches have diverged; this PR does not recreate those commits or cherry-pick them individually.

Strategy

  1. Branch from v0.1.x-branch.
  2. Merge the recorded main tip with a two-parent merge commit.
  3. Resolve duplicated backport conflicts in favor of the evolved main implementations.
  4. Regenerate sqlc output from the merged SQL sources.
  5. Preserve the release version at 0.1.0; bump to 0.1.1 separately.
  6. Merge this PR using a merge commit, not squash or rebase, so the ancestry relationship is retained.

Result

  • Merge commit: 60399d48
  • Release parent: 7cbef62b
  • Main parent: da2f5441
  • Both recorded tips are ancestors of the PR head.
  • The final tree matches main except for build/version.go.
  • 14 conflicts were resolved; they were primarily overlapping release backports and generated SQL output.

Validation

  • make sqlc
  • make fmt-changed
  • make lint-local
  • make unit timeout=30m
  • make systest
  • SYSTEST_TIMEOUT=20m make systest db=postgres
  • make tidy-module-check
  • make commitmsg-lint range="origin/main..HEAD"
  • Verified both parent tips are ancestors of HEAD.
  • Verified the only tree difference from da2f5441 is build/version.go.

levmi and others added 30 commits July 17, 2026 10:15
In this commit, we add a RefreshFeeEstimate message to
RefreshVTXOsResponse so a dry_run refresh can carry an advisory,
itemized operator-fee preview for the selected VTXOs. Today the
refresh path never surfaces the fee anywhere: the binding amount is
set by the server-issued JoinRoundQuote at seal time and
auto-accepted against the client's MaxOperatorFeeSat cap, so the
user only learns the charge afterwards via the fee history.

The per-outpoint rows carry the amount and remaining lifetime the
daemon resolved for each VTXO (removing the manual amount /
remaining-blocks input the standalone fees estimate command
requires) plus the operator's liquidity / on-chain share / margin
breakdown. Rows are deliberately NOT waiver-adjusted so their
components always sum to their total; the selection-level
free_refresh_eligible flag and zeroed total express the
free-late-refresh waiver instead, mirroring the operator's
all-or-nothing seal-time rule from lumos#675. estimate_error keeps
dry_run usable as a validity probe when the operator or chain
height is unavailable: the preview still returns and the caller is
told the numbers are absent rather than zero.

The daemon-side population of the new field lands in the next
commit. Part of #986.
In this commit, we populate the new RefreshVTXOsResponse fee
estimate on the dry-run path. The daemon resolves every selected
VTXO to its full descriptor, computes each remaining lifetime from
the chain tip, and fetches the operator's itemized quote through
the existing EstimateFee proxy, deduped on (amount, remaining
blocks). The remaining-blocks figure is clamped to 1 for expiring
VTXOs because the operator treats zero as "price the full
sweep-delay lifetime".

The explicit-outpoint path now looks targets up in the VTXO store
instead of only parsing strings: an unknown or non-live outpoint
surfaces as InvalidArgument instead of echoing back as a plausible
preview, mirroring the LiveState filter the --all path already
applies — the real refresh can never execute either, and dry_run
is the validity probe the CLI consent prompt trusts. Explicit
selections are also deduped (order-preserving) so a repeated
outpoint can neither double-count in the estimate nor register a
doomed duplicate forfeit pair with the wallet.

The free-late-refresh waiver (lumos#675) is applied locally: the
operator's EstimateFee prices every refresh as paid, so quoting a
free late refresh through it alone would over-quote. The daemon
already caches the advertised window in its operator terms and
knows a refresh selection is the pure one-for-one renewal shape the
waiver requires, so a selection fully inside the window previews
with an explicit zero total while the rows keep the ordinary paid
quote to show what the waiver saves.

The estimate is strictly best-effort and degrades all-or-nothing:
every quote is fetched and vetted (no negative or beyond-money-
supply values, no overflowing selection total) before any component
is written to a row, an unreachable operator or chain backend sets
estimate_error while the preview and the locally computed waiver
verdict survive, and the total uses explicit proto presence — it is
only set when meaningful, so a degraded estimate can never be
misread as a free refresh.

The dry-run branch also moves ahead of the wallet-ready gate,
matching the LeaveVTXOs ordering rule that pure-argument validation
and previews must not depend on wallet state (the H-5 fix's comment
already claimed this parity). A side effect is that an empty
selection=all dry run now honestly reports status "preview" instead
of "queued". The real refresh path still gates on wallet readiness
before queuing.

Part of #986.
In this commit, we render the dry-run fee estimate on the refresh
command. The itemized numbers stay in the JSON body on stdout;
stderr gains a short human-readable summary so an operator reading
the terminal sees the headline cost — or the free-refresh-window
verdict, or the degraded-mode warning — without stdout consumers
having to strip prose. Every wording branch repeats that the value
is advisory and the binding fee is set at seal time, and the
degraded branch explicitly says a fee still applies so a missing
estimate is never read as a free refresh. VTXOs below the
operator's minimum viable amount add a count-level warning,
mirroring the fees estimate command's below-dust handling.

A non-empty dry-run preview that carries no estimate at all can
only come from a daemon predating the feature, so the CLI warns
about that skew explicitly instead of silently dropping the
preview the flag help promises.

The fees estimate help now points refresh users at `ark vtxos
refresh --dry_run`, which resolves each selected VTXO's amount and
remaining lifetime automatically instead of requiring both by hand.

Part of #986.
In this commit, we stop dispatching a real refresh without consent.
Before this change the number a dry run can now surface was not
actionable: by the time a user read anything, the refresh was
queued, auto-joined, and priced by the auto-accepted seal-time
quote, with no cancel surface anywhere in between. The refresh
command now mirrors the leave --all consent pattern: --yes skips
the gate for scripted use, non-interactive stdin refuses to prompt
and directs the caller to --yes or --dry_run (so agents are never
blocked, per the issue's acceptance criteria), and only an
interactive TTY prompts.

The interactive prompt shows the advisory estimate first — fetched
through the same RPC in dry-run form — so the operator consents to
a number rather than a mystery fee, and the prompt goes to stderr:
stdout stays reserved for the JSON body, so a piped invocation can
never swallow the question and read as a hung command. A preview
rejected as InvalidArgument aborts outright (the real dispatch
would reject the same request shape); any other preview failure
degrades to prompting with an explicit "still charged the
seal-time fee" warning, so a broken estimate path never makes
refreshes unconfirmable and a missing estimate is never read as
free. An empty selection (refresh --all with no live VTXOs) skips
the prompt entirely — there is no fee to consent to, and warning
about a charge for a no-op would be false.

The schema registry entry gains the yes parameter and now names the
fee in its description, so schema consumers see the same contract
as the flag surface. getDaemonClient becomes a package-level
indirection (mirroring stdinIsTTY) so wiring tests can drive the
full command path against an in-process bufconn daemon and pin
that the gate runs before any dispatch: a refused invocation must
reach the daemon zero times.

Part of #986.
In this commit, we move the leave --all confirmation prompt from
stdout to stderr, matching the refresh confirmation gate and the
package's stream-split invariant: stdout is reserved for the JSON
body, diagnostics go to stderr. Before this change, piping the
command (e.g. into jq) on an interactive terminal swallowed the
prompt into the pipe — the command read as hung, and a user who
typed y blind fed prose into the JSON consumer.

Part of #986.
In this commit, we extend the refresh fee-consent contract to the
MCP surface. The MCP tool calls the daemon directly rather than
executing the cobra command, so the CLI gate never ran there: an
agent could queue a fee-incurring refresh with no warning, while
the schema registry — documented as the shared source of truth for
CLI commands and MCP tools — promised that a real refresh requires
yes. The tool description also never mentioned the fee.

The ark.vtxos.refresh tool now takes a yes acknowledgement: a real
(non-dry-run) call without it returns an immediate, actionable
error naming both the dry_run:true preview and the yes:true
acknowledgement path — nothing can block on MCP, matching the
CLI's non-interactive refusal — and the description now names the
seal-time fee and the preview-first flow. The dry-run preview
carries the same itemized fee estimate as every other surface.

Part of #986.
In this commit, we document the refresh-fee visibility surface. The
CLI guide's refresh section now names the operator fee and the
seal-time quote that binds it, describes the dry-run estimate
(per-outpoint amounts and lifetimes resolved by the daemon, the
free-refresh-window verdict, the degraded estimate_error mode), and
covers the interactive confirmation plus the non-interactive --yes
requirement, with examples updated to match.

The waveclicommands agent docs gain the refresh consent-gate
invariant — CLI prompt, non-interactive refusal, the MCP tool's yes
argument, and the absent-not-zero wire semantics of a degraded
total — alongside the existing leave --all and recovery escalate
prompt-refusal postures. The waved agent docs gain the dry-run
estimation invariant (pre-wallet-gate ordering, store-resolved
live-only outpoints, quote dedupe and validation, locally computed
waiver, degrade-only failure mode). The repo's own agent-facing
refresh examples in the waved skill and CONTEXT.md pick up
--dry_run/--yes so an agent following them is not stopped cold by
the new non-interactive refusal. Both members of each touched
CLAUDE/AGENTS pair receive the same edit; the waved pair's
pre-existing divergence on unrelated OOR text is left to the
doc-gardening workflow.

Part of #986.
In this commit, we fix the client-side operator fee reconciliation to
count locally owned outputs from the BUILT VTXOs rather than the intent
requests. Under the seal-time fee handshake, an intent's amount is the
pre-fee target: the server's quote shaves the operator fee off the leaf
at seal time, and the FSM intentionally no longer subtracts it client
side. Summing intent amounts therefore cancels the round's inputs
exactly, computeClientOperatorFee returns zero for every fee-charging
round, and the FeePaidMsg emission is silently suppressed — no
boarding_fee_paid or refresh_fee_paid row was ever landing in the fee
ledger.

The built ClientVTXO carries the sealed leaf value (the quote residual
extracted by leafNonAnchorAmount), so input minus output over the
sealed amounts yields the true fee. Foreign directed-send recipient
slots never materialize as owned VTXOs, so their intent amount remains
the only local record of their value and still counts as-is.

A new regression test pins the shape observed on regtest: a leave round
forfeiting 149,745 sats into a 10,000 sat leave plus a 139,204 sat
sealed change VTXO must book the 541 sat operator fee, and a fully
funded foreign recipient slot must leave that fee unchanged.
In this commit, we move the boarding fee leg's credit side from
vtxo_balance to wallet_balance so the chart of accounts nets correctly
against the amounts the round actually books. The boarding
vtxo_received leg carries the SEALED (post-fee) VTXO value under the
seal-time fee handshake, which means vtxo_balance already lands on the
true VTXO holding without any fee adjustment. Crediting the fee from
vtxo_balance on top of that would understate the VTXO layer by the fee;
crediting it from wallet_balance instead completes the gross wallet
outflow — deposit in, sealed value plus fee out — leaving every account
at its true balance.

Refresh fees keep their vtxo_balance credit: a refresh fee is carved
out of forfeited VTXO value, and the paired gross send/receive legs
cancel, so the fee leg is the only real movement there.

The fee_ledger doc's boarding and refresh walkthroughs are updated to
describe the sealed-amount legs and the per-flow credit accounts.
In this commit, we add migration 000015, which grows ledger_entries a
round_uuid TEXT column mirroring the raw 16-byte round_id BLOB in the
canonical lowercase UUID form that rounds.round_id and
vtxos.forfeit_round_id already store. The two subsystems historically
persisted the same identifier in different encodings, and no BLOB to
TEXT conversion exists in the SQL dialect subset shared by SQLite and
Postgres (hex() vs encode()), so nothing could join ledger rows against
the round-adjacent tables in plain SQL. The TEXT mirror closes that gap
once, for every present and future join.

New inserts stamp the column in the LedgerStoreDB adapter via
roundUUIDText, so no ledger actor or message change is needed. Existing
rows are converted by a Go post-migration step registered for version
15 — the string formatting is the part SQL cannot express portably —
wired into both store constructors through the previously dormant
makePostStepCallbacks machinery. The per-round backfill UPDATE guards
on round_uuid IS NULL, so a crash-interrupted run re-executes as a
no-op.

The generated sqlc code for the accounting queries is regenerated
alongside: the insert gains the new column, the ledger list queries
return it, and the two backfill helper queries land.
In this commit, we extend the ListVTXOsByStatus settlement join with the
forfeit round's operator fee. The query already LEFT JOINs the round
that forfeited each VTXO to surface the settling commitment txid and
confirmation height; a second LEFT JOIN against a fee-totals subquery
(SUM of boarding_fee_paid and refresh_fee_paid grouped by round_uuid,
keyed on forfeit_round_id) now rides along in the same single query, so
the read costs no extra roundtrip.

The figure is round-level: every VTXO forfeited in the same round
reports the same total, so consumers must attribute it once, never sum
it across VTXOs. It reads zero for fee-free rounds and for ledger rows
predating the round_uuid backfill.

vtxo.Settlement grows a FeeSat field populated by the by-status row
converter on both the full and light read paths, and the settlement
store test now proves the sum covers exactly the settling round's fee
event types — the vtxo_sent row in the same round and a fee booked
against an unrelated round contribute nothing.
In this commit, we add fee_sat to the VTXOSettlement message and
populate it in descriptorToProto from the settlement the store join now
carries. A FORFEITED VTXO's settlement thereby reports where the
forfeit round confirmed AND what the round cost, giving wallet-layer
consumers a single carrier for completing a cooperative-leave row with
its on-chain coordinates and fee. The field mirrors the store contract:
a round-level total repeated on every VTXO the round forfeited, zero
against fee-free rounds and ledgers predating fee-by-round
attribution.
In this commit, we complete the fee plumbing the activity surface was
missing: when a cooperative-leave EXIT row completes off its forfeited
source VTXO, the settlement now carries the forfeit round's operator
fee, and applyCooperativeLeaveForfeited stamps it onto the row's
fee_sat. The projection pass then persists it into the canonical
activity store, so both the live table and the durable row agree.

Sweep-all sends need one more step. A sweep's pending amount is the
gross drained balance with the fee still baked in — the binding fee is
unknown until the round seals — so displaying a fee next to that gross
amount would double-count it. The onchain request now records a
sweep_all marker (set by leaveEntryStub from the prepared intent), and
completion nets the settled fee back out of the amount. Every completed
EXIT thus reads the same way: amount is the value delivered to the
destination, fee is the cost on top, and their sum is the true outflow.
A bounded send's amount is already the exact destination value, so it
is left untouched.

Verified end-to-end on an arktest regtest topology: a 10,000 sat
bounded send completes as amount -10,000 / fee 541, and a sweep-all of
a 139,204 sat wallet completes as amount -138,816 / fee 388, with the
destination wallet receiving exactly those amounts on chain.
In this commit, we keep the newly emitted boarding_fee_paid ledger rows
out of the wallet activity view. The daemon's unified history typed
them 'boarding' all along, but the mapping was dead code while the
round actor's fee computation suppressed every fee row; with fee
emission fixed, each boarding round's fee leg surfaced as a phantom
PENDING DEPOSIT row for the fee amount (observed as a 255 sat
'ledger-N' deposit on regtest).

Only the wallet_utxo_created subtype is a user-facing deposit. The fee
already reaches the user through the real deposit row's fee_sat
attribution, so the accounting leg is classified out of the wallet
surface entirely.
In this commit, we bring the per-package agent docs in line with the
fee-attribution changes: the db package map documents migration 000015
and the round_uuid mirror column, the ledger doc describes the
per-flow fee credit accounts and the joinable round linkage, and the
swapwallet doc records the cooperative-leave EXIT fee stamping and
sweep-all amount netting invariant.
In this commit, we add a read path for the unilateral exit cost the
ledger already records: handleExitCost books an onchain_fee_paid leg
after the final sweep confirms, keyed by the exited VTXO's
outpoint-derived idempotency key. ExitIdempotencyKey exports that key
derivation, and LedgerStoreDB.GetConfirmedExitCost sums the fee legs
under it — the partial unique index scopes on (key, event_type,
accounts), so the send leg sharing the same key never contributes. An
exit that has not confirmed (or predates exit-cost accounting) reads
zero.
In this commit, we add exit_cost_sat to GetUnrollStatusResponse and
stamp it in the daemon on both status paths (live registry and the
persisted-job fallback) whenever a job reads COMPLETED. Unlike the
estimate breakdown a detailed probe projects via enrichExitFees, this
is the settled figure from the ledger's confirmed onchain_fee_paid exit
leg, so it needs no fee-rate estimation or lineage resolution and is
cheap enough for the plain per-row status lookups the activity surface
issues. The stamp is best-effort: a missing ledger store or a read
failure logs at debug and never fails the status query.
In this commit, we close the remaining FEE 0 gap on the activity
surface: a completed unilateral exit now carries the settled exit cost
the daemon reports on GetUnrollStatus. applyUnrollStatus stamps it onto
the row's fee_sat and nets it back out of the gross VTXO amount, so a
unilateral EXIT reads the same way as a completed cooperative leave:
amount is the value delivered on chain, fee is the cost on top, and
their sum is the gross VTXO value that left Ark custody. A zero cost
(old daemon, or an exit predating exit-cost accounting) leaves the row
exactly as before, preserving prior behavior for historical exits.
In this commit, we harden the EXIT completion paths against the ordering
gap between the status that completes a row and the ledger commit that
carries its fee. The forfeit status (VTXO actor) and the round's
FeePaidMsg (durable ledger actor) are independent fire-and-forget Tells
from the same round-actor turn, and the unroll path likewise only
guarantees the ExitCostMsg is enqueued before the terminal handoff. A
reconcile pass landing in that window would complete the row at fee 0,
durably project it, and drop the pending record — freezing the exact
fee-0 symptom this series eliminates.

Two guards close the window. First, a COMPLETE projection carrying a
zero fee retains its pending record for a bounded number of passes
(feeZeroClearGracePasses) before clearing, so the row stays derivable
long enough for a fee that commits milliseconds later to be re-read and
re-projected; genuinely fee-free rounds pay only a few redundant
decorations. Second, mergeActivityContext treats a recorded fee as
sticky: no producer legitimately moves a settled fee back to zero, so a
later fee-0 projection (racing pass, transient ledger read error)
restores the stored fee together with its coupled netted amount instead
of regressing the row to gross/fee-0.

The reconciler scenario test drives the race end to end: pass one
completes at fee 0 and retains the record, pass two observes the
late-committed fee, heals the stored row, and clears.
In this commit, we make the receive session fail fast when a same-Ark
p2p event arrives for a session whose route quote attached credit or
padded the expected vHTLC above the invoice amount. The direct p2p
vHTLC is funded entirely by the sender, so there is no output that can
carry the swap server's custodial credit top-up; such an event is
always the product of a server that took the p2p path against a
credit-bound intent.

Previously the acceptance check only compared the event amount against
the invoice amount, which still matches on a credit-attach plan (the
attached credit lives in the vHTLC padding, not the invoice amount).
The session would accept the event, switch to the in-ark rail (which
intentionally skips the out-swap ACK), and then poll for a sub-floor
vHTLC that can never be funded. On the production instance this
surfaced as an endless "script not registered for principal" poll while
the swap server held the incoming HTLC waiting on an ACK the session
would never send. Failing terminally with an explicit reason surfaces
the conflict immediately and abandons the session's per-hash mailbox
along with it: the envelope is never acked, but a failed session never
pulls that mailbox again, so nothing redelivers.

This is the client-side half of the fix; the server-side gate that
stops offering the p2p path for credit-bound intents lands in
swapdk-server.
wallet: attribute round operator fees to onchain withdrawal activity
In this commit, we add the wire surface for the wavelength#844
status reconcile: a client->server QueryRoundStatus RPC and the
ClientRoundStatusReport push event that answers it, carrying a
RoundLifecycleStatus classification (in-flight, broadcast,
confirmed, or dead).

A client that has already sent its per-VTXO forfeit signatures
cannot release the forfeit reservations on a round-failure
notification alone: the operator may hold fully-signed forfeit
txs, and a blind release risks a double-spend if the round's
commitment later confirms. The saving grace is that a forfeit tx
spends a connector output of the commitment, and the operator
persists a finalized round atomically with its VTXOs before the
commitment is ever broadcast. A ROUND_STATUS_DEAD answer (no live
FSM, no durable row) therefore proves the commitment can never
confirm, which is exactly the proof-of-death that makes the
release safe. The generated code is regenerated via make rpc.
In this commit, we fix wavelength#844: a round failure arriving
after the client's forfeit signatures have left the box no longer
strands the VTXO in Forfeiting for the life of the batch. The
releaseForfeitsOnFailure wrapper (the #653 fix) deliberately stops
at PartialSigsSentState, because past that point the operator may
hold fully-signed forfeit txs and a blind release risks a
double-spend. The result was that InputSigSentState had no release
path at all: the coin sat stranded until the #823 startup sweep or
batch expiry rescued it.

The fix is a status reconcile rather than a table edit. A
BoardingFailed that lands in InputSigSentState with forfeits at
stake now parks in the state (PendingFailure) while a
QueryRoundStatusOutbox probes the operator for the round's
authoritative lifecycle status. Only a dead answer, meaning the
round never finalized so its commitment can never confirm, fails
the round through releaseForfeitsOnFailure, which returns the
inputs to LiveState and retires the originating job on a
terminal-for-job code. Any other answer holds the reservations.

The same probe covers the lumos#618 silence door, where a crashed
operator never sends a failure at all: a status-reconcile timeout
(armed when the forfeit signatures are emitted, re-armed per
probe, re-armed on restart reload) fires in the silence and drives
the identical query. The timeout alone never releases; only the
operator's answer does. Boarding-only rounds keep the old
immediate-failure behavior, and a non-positive
StatusReconcileTimeout opts out entirely.

The timer only arms when forfeits are actually at stake, matching
the gate every consumer applies, and repeated unanswered probes
back off exponentially (capped at 16x the base window) so an
operator that predates the status RPC sees a bounded probe cadence
rather than a fixed-rate loop forever. The dead-answer release
also spells out its trust boundary in the code: the proof of death
is the operator's own self-report, sound against an
honest-but-faulty operator (the failure mode this reconcile exists
for), while the commitment confirmation watch stays registered so
a fraudulent later broadcast still surfaces as a detected conflict.
In this commit, we register the inbound dispatch route for
ClientRoundStatusReport, so the operator's answer to a
QueryRoundStatus probe reaches the round FSM as a
RoundStatusReported event through the same push-event path the
other round messages use. Without the route the daemon would drop
the report on the floor and the reconcile would spin on its retry
timeout forever.
In this commit, we close the restart gap in the status-reconcile
release. The reconcile keys every decision on Intents.Forfeits, but
that set only ever lived in memory: dbRoundToDomainRound and
reconstructInputSigSentState rebuilt boarding intents alone, so after
a restart a forfeit-bearing round looked boarding-only. The re-arm
guard in the actor's reload loop never fired, and even a hand-armed
timer would have released nothing, since the dead-answer path releases
the (empty) in-memory forfeit list. The strand the reconcile exists to
fix simply reopened across every restart.

The durable ground truth was already there: MarkVTXOForfeiting stamps
each Forfeiting VTXO row with the binding forfeit_round_id. We add a
ListForfeitingVTXOsByRound query over those rows and rebuild the
forfeit set in both reload paths. The rebuilt requests carry the
outpoint and amount with no custom spend paths, which is exactly
right: custom (caller-supplied) forfeit inputs never enter the wallet
store and their signing contexts die with the process, so everything
the query returns is a standard wallet forfeit for the release path.

The new TestRoundStoreReloadRebuildsForfeitSet pins the behavior
against a real store: commit an InputSigSent round, mark two VTXOs
Forfeiting against it, and require both reload paths to surface the
pair (and only the pair) as standard forfeits.
In this commit, we make the wallet activity feed explain the Ark
top-up transfer that funds a credit-backed pay. Paying a sub-dust
invoice routes through the swap server's credit account: the wallet
first funds the account with an OOR at the operator's dust floor, then
debits the invoice amount from the resulting balance. The ledger row of
that funding OOR previously surfaced as a bare outgoing transfer with
no connection to the payment (issue #989's "unrecognized outgoing
transfer ledger-46"), and the surplus that remained as credit was
invisible outside the Balance RPC's credit fields.

The credit registry summary now carries the delegated top-up OOR
session id and top-up amount from the durable operation record. The
history merger indexes pay operations by that session id (in both hex
orientations, so the lookup is independent of the recorded display
convention) and relabels the matching ledger row: the counterparty
becomes "credit" and the progress metadata carries a "credit_topup"
phase label plus the payment hash of the pay the top-up funded. The
amount is intentionally left untouched, since the row records the real
VTXO outflow; the surplus above the paid amount remains visible as the
wallet's credit balance, which Balance already surfaces through
credit_available_sat / credit_reserved_sat.
sdk/swaps+swapwallet: reject credit-bound in-ark events, label credit top-ups
round: release forfeit reservations via a status reconcile on round death
In this commit, we extend the wire surface for the credit-aware in-ark
settlement rail designed in swapdk-server#233. InArkHtlcEvent grows
requested_amount_sat and attached_credit_sat, mirroring
OutSwapHtlcEvent: a credit-shaped event carries the padded vHTLC
amount in amount_sat together with the invoice amount and the credit
portion, so the receiver can validate the full triplet against its
route quote instead of hard-rejecting any padded event.

RequestChannelIdRequest gains supports_in_ark_credit, letting the
receiver prove it understands credit-shaped in-ark events at route
registration time. The server records the capability on the receive
intent; without it, a credit-attach receive keeps the existing
fall-through behavior, so mixed fleets never see an event shape they
cannot validate.
In this commit, we teach the receive session to settle a credit-attach
receive over the swap-server-funded in-ark leg from swapdk-server#233.
A credit-shaped InArkHtlcEvent (one carrying requested_amount_sat and
attached_credit_sat) is validated against the session's route quote
triplet exactly like the out-swap acceptance path: the requested amount
must match the invoice, the attached credit must match the plan, and
amount_sat must equal the padded vHTLC amount. Any mismatch fails the
session terminally. Legacy direct p2p events keep the existing guards:
a credit-bound session still rejects them fast, since a sender-funded
vHTLC cannot carry the server's custodial credit.

The rail split also moves the ACK decision off the settlement type
alone: the legacy p2p rail keeps skipping the out-swap server ACK (the
sender funded before the event was published, so there is nothing to
gate), while a credit-shaped in-ark session sends it, because the swap
server funds the padded vHTLC only after the receiver durably accepted
the event. The claim policy is unchanged in shape: the event's sender
key (the swap server's funding key on this rail) fills the sender slot
and the Ark operator stays in the server slot, so funding validation
and the claim path work as they do for every other padded vHTLC.

RequestChannelID now always advertises supports_in_ark_credit, since
this client validates the credit shape; servers without the capability
field ignore it, and servers with it only publish credit-shaped events
to receivers that set it.
Roasbeef and others added 21 commits July 28, 2026 15:03
In this commit, we address two findings from review of the poll decay.

The config comment enumerated the events the fallback poll covers as
missed wakes, restarts, and external enqueues, and that list was
incomplete. A nacked message becomes eligible again when its retry delay
elapses, and nothing signals the wake channel at that moment, so the
poll is its only discovery path too. Under the old fixed cadence a retry
surfaced within a second of becoming due; under the decay it surfaces up
to the ceiling late. At-least-once delivery is unaffected and this is a
tradeoff we are taking knowingly, but it is real latency that was not
written down, and a deployment whose retry delays matter should size the
ceiling against them. The same paragraph now also states the
crash-recovered lease case, which is LeaseDuration plus up to the
ceiling.

We also clamp a decay that overflows. Doubling cur past MaxInt64 wraps
to a negative duration, which the existing "greater than ceiling"
comparison does not catch and which timer.Reset treats as
fire-immediately, turning the backoff into a tight spin against the
store. Reaching it needs a ceiling over roughly a hundred and forty
years, so nothing can hit this today, but the same argument that keeps
the redundant first branch in decay applies here: the method should be
correct on its own terms rather than only correct given the callers it
happens to have.

The overflow is pinned deterministically rather than left to the
property test. Widening the property's ceiling domain was the first
attempt and it did not work: reaching the overflow by repeated doubling
from a realistic floor takes more than forty decays, so the property
never got there and the mutant survived with the clamp removed. The
deterministic case steps cur to just past the halfway mark and kills it.
In this commit, we scope the non-transactional route skip to the
envelopes that actually reach the dispatch table.

The skip sits ahead of the kind switch, so it also caught a
KIND_RESPONSE whose route happened to be marked even when a live waiter
was registered for it. Such a response never resolves to a dispatcher
at all: dispatchBatch hands it to the waiter in memory and breaks. The
skip was therefore able to drop a response a caller was blocked on,
leaving it to time out on its own deadline.

Only the legacy path can present one, since splitIngressEnvelopes peels
waiter-backed responses off before the fold. It is also unreachable in
the current wiring, because marked routes carry operator-to-client
requests while waiters belong to client-to-operator RPCs, so the two
service and method spaces do not overlap. That was an unstated
invariant holding up a silent drop, which is not where we want one, so
resolvesToNonTxDispatcher now states the rule and a test holds it.

The new test bounds its wait rather than blocking on the future, so the
regression it guards shows up as a failure instead of a hang.
In this commit, we hold the wiring invariant the ingress hoist depends
on and that no type can express.

NonTxRoutes is trusted rather than checked: an EnvelopeDispatcher is an
opaque closure, so nothing downstream can tell a mux bridge from a
durable Tell. buildRPCDispatchers writes both maps through one helper
so the two cannot drift by hand, but that is a convention, and the cost
of breaking it is not a stall. A durable Tell hoisted out of the fold
commits ahead of the pull cursor, and the re-pull after a crash in that
window enqueues a second copy under a fresh ID that receiver-side dedup
cannot collapse.

So we assert the result instead of the convention: every marked route
resolves to a dispatcher, and never to one the EventRouter contributed.
Marking an OOR event route fails the test with the route named.
In this commit, we narrow the MaxInFlightUnary doc comment to match the
mechanism. It read as though the cap bounded every unary RPC the client
has outstanding, when it counts live UnaryFacade waiters in the response
registry and nothing else.

The durable egress paths do not register an in-memory waiter and are not
gated by admitUnary. That is the right scope rather than an oversight: a
durable response with no waiter left falls through to durable route
dispatch and is redelivered, while a live unary response with no waiter
is acked into the void, and it is the discarding this cap exists to
bound. Worth stating, since a reader who assumed the broader reading
would go looking for a bound on durable egress that is not there and
should not be.
actordelivery: retry serialization failures in the durable actor commit tx
…ency-keys

multi: mint one idempotency key per logical mailbox RPC
db: run read-only Postgres transactions at REPEATABLE READ
…dispatch

serverconn: keep inbound RPC dispatch out of the ingress write tx
baselib/actor: decay the idle mailbox poll instead of polling at a fixed rate
…y-gates

wavecli: gate raw money movement
Every expiry decision is derived from

	blocksRemaining = BatchExpiry - currentHeight

and BatchExpiry was trusted unconditionally. It is copied verbatim
from the wire by the incoming-VTXO handler, so a zero arriving from
the operator reads back as "expired by the entire height of the
chain" and classifies a brand-new VTXO as expired. Nothing
validated it at ingress and nothing guarded the arithmetic.

Add HasUsableBatchExpiry and reject three shapes: a non-positive
expiry, and an expiry earlier than the height the VTXO was created
at, since a VTXO cannot expire before it existed. CheckExpiry now
returns the new ExpiryStatusUnknown for those rather than
ExpiryStatusExpired.

Unknown is deliberately its own status rather than folding into
either extreme. Reporting "expired" surrenders live funds on the
strength of a corrupt field; reporting "safe" silently skips the
refresh a real deadline needs. LiveState holds the VTXO live and
warns, so a data fault neither retires the coin nor wedges the
actor on every block. The status is appended last so the existing
numeric values are unchanged, and the remaining call sites already
treat anything that is not Critical/Expired/NeedsRefresh as
inaction.

The incoming handler now drops an event carrying an unusable
expiry instead of materializing it. Dropping is the safer failure:
the wallet still re-derives the VTXO from ListVTXOsByScripts,
which reads the authoritative expiry off the server's round row,
whereas a poisoned expiry persists locally and is never rewritten.
CalculateCriticalThreshold sized the unilateral-exit window from
the commitment-tree depth and the CSV delay alone. It ignored
ChainDepth, the number of OOR checkpoint hops between the
commitment and the VTXO.

Those hops are not free. Each is a recovery transaction that must
confirm before the exit's final CSV even starts, and they are
strictly sequential because each checkpoint spends the previous
one. unroll already budgets fees this way, one recovery tx per
hop, so the time budget disagreed with the fee budget.

The threshold exists precisely so a client never has to race the
operator's sweep, and it was under-sized for exactly the deep OOR
chains that need the most room. Factor the sequential transaction
count into exitTxDepth: the deepest tree path, since parallel
ancestry fragments confirm concurrently and the worst branch sets
the pace, plus one transaction per OOR hop. A negative hop count
is treated as zero rather than being allowed to shorten the
budget.
Identify the witness path that actually spent each watched VTXO instead
of inferring intent from chain height. Operator batch sweeps can then
retire expired watches without being escalated as client fraud.
A round is checkpointed at input_sig_sent, the point of no return,
and can confirm long afterwards. The confirmation handler derives
each new VTXO's absolute batch expiry as

	confirmation_height + sweep_delay

but the delay lived only in the in-memory FSM state. A daemon
restart between checkpoint and confirmation rebuilt
InputSigSentState without it, so the resumed round computed an
expiry of confirmation_height + 0 and stamped every VTXO it
created with BatchExpiry == CreatedHeight. The wallet reads that
back as already expired, which retires a VTXO that was created
seconds earlier.

Add a sweep_delay column to the rounds table, carry the value on
round.Round, and restore it onto both the round record and the
FSM state. The upsert only adopts an incoming delay when it is
non-zero, since the value is fixed for the life of a round and a
later checkpoint must not clear what an earlier one recorded.

Rounds checkpointed before this migration have no recorded delay.
For those the confirmation path now leaves the expiry unstamped
rather than stamping a wrong one, and logs at error level. An
unstamped expiry classifies as ExpiryStatusUnknown, so the VTXO
stays live and spendable with only expiry monitoring disabled,
and the authoritative expiry is still recoverable from the
operator's indexer.
Keep expired value recoverable but outside the spendable set. After
chain catch-up, replay the synchronized tip through the ordinary refresh
and forfeit flow while preserving in-flight locks and restart recovery.

Co-authored-by: sputn1ck <kon@kon.ninja>
vtxoStatusToProto had no case for the new expired status, so an
expired VTXO was reported to clients as VTXO_STATUS_UNSPECIFIED.

Expiry is not terminal — the value is recovered by forfeiting the
VTXO in an ordinary round — so a wallet UI has to be able to tell
"expired, recoverable, not counted in spendable balance" apart
from "the daemon does not know what this is". Add
VTXO_STATUS_EXPIRED and map it in both directions.
Allow explicit and automatic expired refresh intents through the daemon.
Count quarantined value against the boarding limit and preview the exact
one-for-one recovery as fee-free.

Co-authored-by: sputn1ck <kon@kon.ninja>
client: Recover expired VTXOs through normal refresh
A confirmed boarding balance is split into VTXO pieces sized by the
operator's per-VTXO maximum, so a large deposit under terms like
MaxUserBalance / MaxVTXOAmount = 500 mints ~500 outputs in a single
round. Boarding signs one MuSig2 nonce per VTXO -- an O(N) step the
operator collects within a bounded round window -- so that fanout can
overrun the window. The round then fails "recoverable", the deposit
never converts, and the board-intent replayer re-issues the identical
oversized shape on every retry, stranding the deposit forever (#858).

Cap the per-round fanout at a configurable DefaultMaxVTXOsPerBoardRound
(128) in clampBoardingAmount: board at most the cap's worth of max-size
VTXOs and route the remainder to the existing change leave output, which
re-boards over successive rounds. 128 keeps the per-round signing well
inside a typical window even on the slowest serial-signing backend while
still boarding a large balance in a handful of rounds; operators with a
tighter window can lower it via WithMaxVTXOsPerBoardRound.

The cap is a deliberately conservative local assumption because the
operator does not advertise its nonce-collection deadline; a reactive
back-off that shrinks the target on a nonce-collection-timeout failure
is a natural follow-up.
wallet: cap per-round boarding fanout to bound nonce signing (#858)
Bring the v0.1.x release line to main commit da2f544 while preserving the release version.
Keep intentional merge topology when CI rebases a pull request onto
its target branch. This avoids replaying main as a flat series during
release-branch synchronization.
@sputn1ck
sputn1ck marked this pull request as ready for review July 30, 2026 11:50
Copilot AI review requested due to automatic review settings July 30, 2026 11:50
@sputn1ck
sputn1ck merged commit 81a8e37 into v0.1.x-branch Jul 30, 2026
41 of 42 checks passed
@sputn1ck
sputn1ck deleted the kon/sync-main-to-v0.1.x branch July 30, 2026 11:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR merges the recorded main tip into v0.1.x-branch via a two-parent merge commit to keep full ancestry while preserving the release-line version state. The resulting tree largely tracks main (with release-version preservation), bringing in a broad set of protocol, wallet/daemon, database, CLI, and operational improvements that have accumulated on main.

Changes:

  • Extend VTXO lifecycle modeling and RPC surface (new EXPIRED status; recoverable-vs-live store splits; additional settlement/exit-cost fields).
  • Improve reliability/operability across subsystems (unary in-flight admission control, keepalive for swap-server gRPC, fraud watcher sweep-provenance handling, durable actor idle polling backoff).
  • Update storage layer and tooling (new DB migrations and post-migration backfills; read-only Postgres isolation policy change; extensive wavecli UX/automation contracts: kebab-case flags + aliases, --request-json, bounded RPC contexts, prompt/consent gating).

Reviewed changes

Copilot reviewed 211 out of 211 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
waverpc/daemon.proto Add EXPIRED status and new fee/cost fields
waved/wallet_recovery_test.go Recovery indexer retry/idempotency tests
waved/wallet_ops_test.go Update store mock for new interface
waved/vtxo_status_expired_test.go RPC round-trip test for expired status
waved/rpc_server.go Map expired status; surface fees/exit-cost
waved/rpc_refresh_estimate_test.go Refresh estimate expectations for expired behavior
waved/mailbox_nontx_routes_test.go Assert non-tx route wiring invariants
waved/incoming_metadata.go Add retry seam + idempotency handling for pages
wallet/wallet.go Add max VTXOs per boarding round option
vtxo/states.go Introduce ExpiredState; track last-checked height
vtxo/messages.go Add manager reconcile-expiry message types
vtxo/manager_admission_test.go Test admission behavior for expired VTXOs
vtxo/interfaces.go Add expired status + recoverable listing + settlement fee
vtxo/incoming_handler.go Guard against unusable batch expiry
vtxo/incoming_handler_test.go Test dropping events with invalid expiry
vtxo/harness_test.go Mock store implements recoverable listing
vtxo/filter.go Exclude expired from pending balance sum
vtxo/filter_test.go Add expired coverage to pending balance test
vtxo/CLAUDE.md Document expired lifecycle invariants
vtxo/AGENTS.md Mirror expired lifecycle invariants documentation
vtxo/actor.go Restore expired state; rollback status logic
vtxo/actor_test.go Add expired catchup/rollback behavior tests
unroll/actor_test.go Update VTXO store mock interface
txconfirm/states.go Track confirm block hash through FSM states
txconfirm/messages.go Include confirmation block hash in TxConfirmed
txconfirm/fsm_types.go Add helpers for confirm block hash extraction
txconfirm/actor_test.go Update notifier call signature for block hash
systest/send_vtxo_test.go Preserve ArkProtocolVersion in mailbox replies
swapwallet/runtime.go Retain fee-zero completions briefly to heal lag
swapwallet/runtime_test.go Update leave entry stub usage
swapwallet/router.go Persist sweep-all marker on leave requests
swapwallet/reconciler_test.go Test fee healing after ledger lag
swapwallet/projector.go Prevent fee regression; bounded clear logic
swapwallet/normalize.go Extend leaveEntryStub; net exit cost on completion
swapwallet/normalize_test.go Update leaveEntryStub tests for new arg
swapwallet/errors.go Add ErrCreditReceiveUnavailable sentinel
swapwallet/errors_grpc.go Map new sentinel to gRPC + reason
swapwallet/errors_grpc_test.go Ensure sentinel mapping coverage
swapwallet/CLAUDE.md Document EXIT fee shaping (coop + unroll)
swapclientserver/service.go Add gRPC keepalive params for swap server
serverconn/unary_facade.go Add in-flight admission; idempotency key minting
serverconn/AGENTS.md Document non-tx dispatch invariants and fold rules
serverconn/actor.go Implement unary in-flight admission logic
sdk/wavewalletdk/errors.go Add SDK sentinel parity for credit receive unavailable
sdk/wavewalletdk/errmap.go Map new reason → SDK sentinel
sdk/wavewalletdk/errmap_roundtrip_test.go Extend reason/sentinel/code contract tests
rpc/wavewalletrpc/wallet.proto Add sweep_all; clarify comments
rpc/wavewalletrpc/failure_reasons.go Add CREDIT_RECEIVE_UNAVAILABLE reason
round/interfaces.go Persist per-round sweep delay field
round/fees_invariants_test.go Pin sealed-owned amount fee computation regression
README.md Publish MIT license badge + licensing text
oor/local_persistence_handler_test.go Update store fixture for recoverable listing
mailbox/rpc/CLAUDE.md Document Retry/NewIdempotencyKey semantics
mailbox/conn/response_registry.go Add waiter count for admission gating
mailbox/conn/response_registry_test.go Test waiter count + stale pruning
mailbox/conn/CLAUDE.md Document waiter count as operational metric
LICENSE Add MIT license text
ledger/handlers.go Correct boarding fee credit account; export exit key
ledger/handlers_test.go Update boarding fee posting expectations
ledger/CLAUDE.md Document round_uuid + fee account semantics
ledger/actor.go Update fee bookkeeping documentation
fraud/messages.go Add full spending tx + input index to spend msg
fraud/CLAUDE.md Document sweep-provenance escalation rules
fraud/AGENTS.md Mirror sweep-provenance escalation rules
fraud/actor.go Suppress escalation for proven operator sweeps
docs/index.md Add Postgres isolation doc link
docs/fee_ledger.md Update fee ledger accounting narrative
docs/dev_rpc_cli_builder.md Document kebab-case flags + aliases
Dockerfile Add GOTAGS build arg; build with tags
db/vtxo_store.go Add recoverable listing + settlement fee projection
db/sqlite.go Wire post-migration callbacks for SQLite
db/sqlc/schemas/generated_schema.sql Regenerate schema (round_uuid, sweep_delay, indexes)
db/sqlc/queries/vtxo.sql Add settlement fee subquery; add recoverable query
db/sqlc/queries/round.sql Persist sweep_delay; avoid clearing on later checkpoints
db/sqlc/queries/fee_accounting.sql Add round_uuid, exit cost query, backfill helpers
db/sqlc/models.go Add RoundUuid + SweepDelay to sqlc models
db/sqlc/migrations/000016_round_sweep_delay.up.sql Add rounds.sweep_delay column
db/sqlc/migrations/000016_round_sweep_delay.down.sql Drop rounds.sweep_delay column
db/sqlc/migrations/000015_ledger_round_uuid.up.sql Add ledger_entries.round_uuid + index
db/sqlc/migrations/000015_ledger_round_uuid.down.sql Drop round_uuid index/column
db/round_store.go Persist/restore sweep_delay through checkpoints
db/round_store_test.go Test sweep_delay round-trip + non-erasure
db/postgres.go Wire post-migration callbacks for Postgres
db/post_migration_checks.go Implement round_uuid backfill post-step
db/migrations.go Bump latest migration version to 16
db/ledger_store.go Write round_uuid on inserts; add confirmed exit cost lookup
db/interfaces.go Relax Postgres read-only isolation; export RandRetryDelay
db/interfaces_test.go Test isolation-level selection + readonly BeginTx
db/interfaces_postgres_test.go Assert server-applied tx isolation/read-only modes
db/CLAUDE.md Document migrations 15/16 and isolation policy
db/AGENTS.md Update migration version + sweep_delay notes
credit/registry.go Add dedicated receive admit timeout + logging
credit/registry_test.go Test receive-timeout fallback behavior
credit/config.go Add ReceiveAdmitTimeout + default value
cmd/wavecli/waveclicommands/wallet_table.go TTY detection + CLI error classification
cmd/wavecli/waveclicommands/wallet_password_test.go Add password input source tests
cmd/wavecli/waveclicommands/wallet_client.go Make dry-run preview a success-path JSON write
cmd/wavecli/waveclicommands/swap_removal_test.go Require send.prepare method in schema
cmd/wavecli/waveclicommands/send_result.go Update inspect command references
cmd/wavecli/waveclicommands/schema_registry_ark_observable.go Add join + watch bounds schema params
cmd/wavecli/waveclicommands/rpc_context.go Add per-RPC timeout context helper
cmd/wavecli/waveclicommands/rpc_context_test.go Test default timeout + disabling + watch bounds
cmd/wavecli/waveclicommands/prompt.go Centralize prompt/consent gating on stderr
cmd/wavecli/waveclicommands/prompt_test.go Test prompt I/O streams + CI/no-input behavior
cmd/wavecli/waveclicommands/oor_destination_test.go Update flag spelling expectations; root command wiring
cmd/wavecli/waveclicommands/json_input.go Rename request input flag to --request-json
cmd/wavecli/waveclicommands/json_input_test.go Test json vs request-json direction contract
cmd/wavecli/waveclicommands/flag_normalization.go Add snake_case → kebab-case normalization
cmd/wavecli/waveclicommands/flag_normalization_test.go Ensure canonical kebab-case; keep snake aliases
cmd/wavecli/waveclicommands/exit_codes.go Add confirmation-required exit code; refine classification
cmd/wavecli/waveclicommands/exit_codes_test.go Test new exit-code mappings (auth, args, consent)
cmd/wavecli/waveclicommands/devrpc/types.go Add RPCContext hook to devrpc config
cmd/wavecli/waveclicommands/devrpc/describe.go Emit canonical kebab-case flag paths
cmd/wavecli/waveclicommands/devrpc/describe_test.go Ensure schema uses kebab-case paths
cmd/wavecli/waveclicommands/devrpc/command.go Support --request-json + bounded RPC contexts
cmd/wavecli/waveclicommands/devrpc/command_test.go Update raw request-json test coverage
cmd/wavecli/waveclicommands/devrpc/CLAUDE.md Document bounded RPC contexts + canonical flags
cmd/wavecli/waveclicommands/devrpc/AGENTS.md Mirror bounded RPC contexts + canonical flags
cmd/wavecli/waveclicommands/cmd_vtxos_tty_test.go Add interactive stdin test helper; tighten TTY rules
cmd/wavecli/waveclicommands/cmd_vtxos_autojoin_test.go Update kebab-case flag names in docs/tests
cmd/wavecli/waveclicommands/cmd_unlock.go Add password-stdin; rename wallet-password-file; bound RPC ctx
cmd/wavecli/waveclicommands/cmd_sweep.go Add --yes gate; use bounded RPC ctx; request-json parsing
cmd/wavecli/waveclicommands/cmd_sweep_wallet.go Add --yes gate; use bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_send_test.go Improve confirmation error UX; dry-run preview JSON test
cmd/wavecli/waveclicommands/cmd_schema.go Expand schema help text for automation
cmd/wavecli/waveclicommands/cmd_recv.go Rename amt-hint flag; use bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_recovery.go Use bounded RPC ctx; centralize escalation prompt
cmd/wavecli/waveclicommands/cmd_oor.go Remove local normalization; use bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_mcp.go Add yes gate to MCP send tools
cmd/wavecli/waveclicommands/cmd_list.go Auto-json format with --json; TTY hint; bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_inspect.go Auto-json format with --json; bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_getinfo.go Use bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_fees.go Use bounded RPC ctx; update docs for dry-run spelling
cmd/wavecli/waveclicommands/cmd_exit.go Use bounded RPC ctx; update dry-run exit semantics
cmd/wavecli/waveclicommands/cmd_board.go Use bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_balance.go Use bounded RPC ctx
cmd/wavecli/waveclicommands/cmd_ark.go Update docs; use bounded RPC ctx
cmd/wavecli/waveclicommands/client.go Wrap macaroon load errors with stable prefix
cmd/wavecli/main.go Install signal-aware root context for cobra
cmd/wavecli/CLAUDE.md Update exit-code documentation
cmd/wavecli/AGENTS.md Mirror exit-code documentation
baselib/actor/durable_actor.go Add MaxPollInterval plumbing to durable actors
baselib/actor/durable_actor_test.go Test poll interval floor/ceiling plumbing
baselib/actor/CLAUDE.md Document idle poll floor/ceiling behavior
baselib/actor/AGENTS.md Mirror idle poll floor/ceiling behavior
.github/actions/rebase/action.yml Preserve merges when rebasing CI branch

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread waved/rpc_server.go
Comment on lines +4954 to +4962
cost, err := r.server.ledgerStore.GetConfirmedExitCost(ctx, outpoint)
if err != nil {
r.server.log.WarnS(ctx, "Confirmed exit cost lookup failed",
err,
slog.String("outpoint", outpoint.String()),
)

return
}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b449f29708

ℹ️ About Codex in GitHub

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

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

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vtxo/expiry.go
Comment on lines +237 to 239
treeDepthBuffer := exitTxDepth(vtxo) * c.TreeDepthMultiplier
csvBuffer := int32(vtxo.RelativeExpiry)
safeExitBuffer := treeDepthBuffer + csvBuffer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Saturate the exit-threshold arithmetic before narrowing

When an incoming VTXO advertises a sufficiently large positive ChainDepth, exitTxDepth returns math.MaxInt32, but multiplying that int32 by TreeDepthMultiplier and adding RelativeExpiry can overflow before the threshold is compared. With the defaults, MaxInt32*6+144 wraps negative, so the critical threshold falls back to 36 blocks instead of conservatively forcing an immediate exit; malformed or hostile indexer metadata can therefore make a deep OOR lineage wait until it no longer has time to unroll. Perform the multiply/add in a wider type and saturate the final result.

AGENTS.md reference: vtxo/AGENTS.md:L113-L117

Useful? React with 👍 / 👎.

Comment thread vtxo/manager.go
Comment on lines +710 to +713
_, becameExpired := transition.NewState.(*ExpiredState)
_, wasExpired := transition.PriorState.(*ExpiredState)
if !becameExpired || wasExpired {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh cached status when reconcile relives a VTXO

If a persisted Expired VTXO becomes live again at the current tip, such as after a reorg, ExpiredState.ProcessEvent persists Live but this condition immediately continues without updating liveDescriptors. initFraudWatcher subsequently consumes that snapshot, and fraud.shouldTrackDescriptor skips the still-Expired descriptor even though its actor and database row are now live, leaving an OOR coin without ancestry-spend watches until it is rematerialized. Update the cached descriptor for the Expired -> Live transition before continuing.

AGENTS.md reference: vtxo/AGENTS.md:L137-L142

Useful? React with 👍 / 👎.

Comment thread swapwallet/recv.go
},
}

r.runtime.projectAndEmit(context.WithoutCancel(ctx), entry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound the detached failed-receive projection

When credit admission fails while the caller context remains live, this removes every cancellation and deadline before synchronously entering projectAndEmit, which takes projectMu and performs database reads/writes. If the activity store blocks on a database lock or outage, canceling the Recv RPC cannot release the call or the mutex, so all other activity projections can remain wedged. Preserve the detached cleanup intent with an explicit finite timeout rather than an unbounded WithoutCancel context.

AGENTS.md reference: AGENTS.md:L75-L81

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants