From 78b309d18efdd227c0efa42464988eaaf1483c16 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 9 Sep 2026 15:05:37 +0200 Subject: fix(node): the leaseless bound refused the music player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported the day MNP 3.0 shipped: playing a track answered "Too many files open at once without a transfer. Download this one instead of previewing it." §3.4.1's bound of two was reasoned about *viewers* — a photo viewer shows one photo, a preview modal one document, and the second is for prefetching the next. It forgot the music player, which warms a read-ahead window: `prefetchDepth()` returns 5 on Wi-Fi and 3 otherwise, so playing an album has six files in flight and the fourth was refused. Browsing a group is never subject to a transfer slot — that is a stated requirement, not a tuning parameter — and a constant nobody had checked against the client broke it. Twelve now: six for the music read-ahead at its widest, two for a photo viewer and its own prefetch in the same session, the rest as headroom. Generosity is cheap here and refusal is not — this is a fairness control among cooperating clients, not a security boundary, so a client that lies gets twelve files at a time instead of its member cap, bounded and audited, while refusing a legitimate read breaks the requirement outright. And the number is now derived rather than chosen: a test reads `prefetchDepth()` out of the shipped player and fails if the node's bound no longer covers it, so widening the client's read-ahead breaks the build instead of reaching a person. Checked by widening it: "the music player reads 21 files ahead and the node admits only 12". Three cases that hard-coded "two then refuse" now set their own limit — they are about the mechanism, and the shipped number moves with the client. Node suite 1210 passed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST --- .../meshbay-node/src/meshbay_node/transfers.py | 27 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) (limited to 'packages/meshbay-node/src') diff --git a/packages/meshbay-node/src/meshbay_node/transfers.py b/packages/meshbay-node/src/meshbay_node/transfers.py index 2185547..dd382b4 100644 --- a/packages/meshbay-node/src/meshbay_node/transfers.py +++ b/packages/meshbay-node/src/meshbay_node/transfers.py @@ -417,9 +417,28 @@ class TransferSlots: # takes a slot. # # But "not leased" cannot mean "unbounded", or a client that simply omits `tr` -# transfers outside every cap and the caps are decoration. Two, because a viewer -# looks at *one* file — one photo, one document — and the second is there so -# that prefetching the next photo stays possible. +# transfers outside every cap and the caps are decoration. +# +# **The number comes from what the shipped client legitimately does, and it was +# wrong once already.** §3.4.1 of ~/next/improve-downloads.md argued for two, +# reasoning about *viewers*: a photo viewer shows one photo, a preview modal one +# document, and the second is for prefetching the next photo. That reasoning +# forgot the music player, which warms a read-ahead window — `prefetchDepth()` +# in music-player.js returns 5 on Wi-Fi, 3 otherwise — so playing an album has +# six files in flight and the fourth was refused with `transfer_required`. +# Reported the day MNP 3.0 shipped, as "I try to play a track and it tells me to +# download it instead". +# +# Twelve: six for the music read-ahead at its widest, two for a photo viewer +# and its own prefetch running at the same time in the same session, and the +# rest as headroom for the next app that reads ahead. `test_leaseless_reads.py` +# derives the floor from music-player.js itself, so raising the client's +# prefetch without raising this fails rather than reaching a person. +# +# Generosity is cheap here and refusal is not. This is a fairness control among +# cooperating clients, not a security boundary — a client that lies gets twelve +# files at a time instead of its member cap, which is bounded and audited — +# whereas refusing a legitimate read breaks a stated requirement. # # Deliberately a count of files and not a byte budget: a RAW photo out of a # camera is 60-80 MB and is browsing, a 40 MB archive is a download, and no @@ -431,7 +450,7 @@ class TransferSlots: # as the cap itself. **This is a fairness control among cooperating clients**, # not a defence against a member determined to saturate a node's disk. The # answer to that member is `member revoke`. -MAX_LEASELESS_IN_FLIGHT = 2 +MAX_LEASELESS_IN_FLIGHT = 12 # A leaseless read has no "close" message, so it ends when the last chunk goes # out — or, when a viewer is closed mid-file and simply stops asking, when it -- cgit v1.2.3