maxPayloadBytes

open val maxPayloadBytes: Int?

The largest payload a single broadcast or sendTo may carry, or null when this seam cannot tell.

null means unknown, not unbounded — it is the honest answer from a fabric with no frame ceiling it can name, and a caller must treat it as "no guidance", never as "any size is fine". A non-null value is a promise: a payload of that size or smaller will not be refused by this seam for being too big. (It may still fail for every other reason a send can fail.)

Why the contract needs this at all

A framed fabric rejects an oversize frame — :kuilt-stream's framed() throws FrameTooLargeException — and every layer that wraps a payload before handing it down spends some of that ceiling on its own header. Without a published limit those two facts meet only at run time, in the failure: a payload that fits when sent directly overflows once a decorator wraps it, and the caller learns the difference from a fabric-level error it had no way to anticipate (#2047). Publishing the number lets a wrapper subtract its own cost and hand the caller a bound it can actually respect.

What a decorator owes

A Seam that decorates another and adds bytes reports the delegate's limit less its own overhead, floored at zero — the idiom inner.maxPayloadBytes?.let { (it - cost).coerceAtLeast(0) }. Subtract unconditionally, even when the overhead is only paid on some routes: a limit that moves with routing is a TOCTOU trap, because the route can change between the caller's check and its send. A stable, conservative bound is the only useful one.

A decorator that adds no bytes delegates unchanged. Leaving the default in place is safe but lossy — it discards a bound the fabric underneath does know.

An implementation that publishes a limit should refuse an over-budget payload with PayloadTooLarge rather than letting a fabric-level error out, subject to each method's own contract: sendTo is addressed and reports, broadcast is best-effort and drops.

Publishing IS enforcing, and the check is per link (#2069)

The in-tree fabric seams pre-check every send. They check the connection's us.tractat.kuilt.core.fabric.Connection.maxFrameBytes, not this value: a link's ceiling is fixed for the life of the link, whereas this aggregate moves, so checking here would leave a check-then-send window a tightening could slip through — and on a mesh it would refuse a payload the addressed link could carry. A decorator with no connection under it has no such alternative and checks this value; the difference is what it can see, not what it owes.

What that replaced is worth stating, because it is the failure mode a published-but-unchecked budget produces anywhere: LinkSeam.sendTo enqueued and returned success, and its write loop — which cannot tell an oversize frame from a dead wire — then tore the whole seam down asynchronously, after the caller had been told the send was accepted; MeshSeam.sendTo routed the same failure into removePeer, evicting a healthy recipient as though its link had died. Publishing a number nothing checks is worse than publishing none.

The TCK now holds every fabric that publishes a number to it at both edges. Still open under #2069: the decorators of #2058 that delegate to a bounded seam while reporting null.

NwSeam is where the distinction was drawn, and it is worth keeping. It enforced a 16 MiB ceiling and refused above it while publishing nothing — deliberately, because publishing is a promise to carry, not merely to refuse, and its receive path dropped bytes under a multi-chunk burst, so a frame that size never completed. A fabric may enforce a bound it cannot yet promise; those are different claims, and only the second one this value makes. The withholding ended with #2134, which gave the receive path real backpressure — so NwSeam now publishes its ceiling, and the TCK's payloadOfExactlyTheBudgetIsCarried (the case that found the defect) is what holds it to it.

A reading, not a lease

This value may move. A mesh reports the minimum across its live links, so a peer attaching over a tighter transport lowers it. Read it per send; a caller that reads once and trusts the value for a whole batch can be refused part-way through, correctly.