<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/transport, branch 0.15</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.15</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.15'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-23T22:10:41Z</updated>
<entry>
<title>fix(hub): offer ceilings that bound a member without failing an ordinary one</title>
<updated>2026-09-23T22:10:41Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-23T22:10:41Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2d657f40ebd697e4332c95d7a57bbb292ff46012'/>
<id>urn:sha1:2d657f40ebd697e4332c95d7a57bbb292ff46012</id>
<content type='text'>
A phone on 4G found one of five groups missing from Search on half its
visits, and all but one on a fifth of them. Every offer that reached the node
was answered in about a second; the hub refused the others with 429 — 93 of
them in half an hour on meshbay.org, all from that phone, all to nodes that
were up. Two ceilings did it. Three pending offers per account was exactly
what one Search page dialled at once, so the first tile or reconnection beside
the sweep was refused. Thirty offers a minute per address and per node was
spent by four or five reloads of a page whose groups share one node.

Sized now against an account with twenty groups on three devices:

- 32 offers pending per account, across devices and nodes.
- A budget per account and per node, a burst of 120 refilled at two a
  second. This is what bounds a member's cost to one machine (H6), and it
  leaves their other nodes alone. Counted by account, because a mobile
  carrier puts many subscribers behind one IPv4 address.
- 600 a minute per address and per node, as a coarse guard in front of
  authentication only.

Both refusals carry Retry-After, which the browser now honours. The node's
own ceiling on peer sessions is unchanged; its comment no longer quotes the
old per-account number.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: drop the invitation-link plan from the tree</title>
<updated>2026-09-23T15:46:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-23T15:46:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=5bce0acad6d10f9b952874f1359406b4eae3a8f9'/>
<id>urn:sha1:5bce0acad6d10f9b952874f1359406b4eae3a8f9</id>
<content type='text'>
The plan was a working document; what it decided lives in
MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md. Code and tests now
cite those instead.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node): invitation links — a code bound to no account until redeemed</title>
<updated>2026-09-23T15:31:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-23T15:31:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d30e95b2ce1ffe9dc4655855406f2784b5f7af34'/>
<id>urn:sha1:d30e95b2ce1ffe9dc4655855406f2784b5f7af34</id>
<content type='text'>
New invite kind "link": member of one group, once, never operator, not
spendable by an active member, capped at 20 per group, cancellable by
handle. Signed ops invite_link_create / invite_cancel, loopback routes,
and the known-device join path now accepts a link code. Adds the plan,
docs/invite-links.md.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: the node signs its handshake challenge (MNP 3.4)</title>
<updated>2026-09-23T15:14:26Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-23T15:14:26Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=339cb427f886a0177014126bb684335837eff067'/>
<id>urn:sha1:339cb427f886a0177014126bb684335837eff067</id>
<content type='text'>
node_pk in handshake_challenge is now signed over the channel binding
and both nonces, so a client can check the node key before a join
rather than only at the ack. Both transports; the browser and the QUIC
client refuse a wrong signature and treat an absent one as an older node.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>perf(hub): bound video read-ahead by bytes, not by seconds</title>
<updated>2026-09-20T22:21:50Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-20T22:21:50Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f7d33c299ae0d0c4f406779b8f5324d80637adc8'/>
<id>urn:sha1:f7d33c299ae0d0c4f406779b8f5324d80637adc8</id>
<content type='text'>
A browser limits bytes, so a bound in seconds had to be sized for the
highest-bitrate file and every ordinary one then held a fraction of what
the same buffer would have taken. Floored at the old 90 s so nothing
pulls less than before, and walked up rather than declared. A refused
append now waits for an eviction instead of retrying on every tick.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>style: the 98 ruff could not fix, so the linter is a signal again</title>
<updated>2026-09-19T12:39:38Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T12:39:38Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9e7b75bb0f6f6649fb00f2dc97059e90b7d52875'/>
<id>urn:sha1:9e7b75bb0f6f6649fb00f2dc97059e90b7d52875</id>
<content type='text'>
The pass before this applied ruff's own fixes. These are the ones needing a
decision, and the point of doing them is that `ruff check .` now passes: a
linter reporting 98 known-acceptable findings reports nothing, because the next
real one arrives invisible.

**Lines over 100 (70).** Mostly wrapped where they stood. Two exceptions: the
aligned trailing comments in `protocol.py`'s message table were shortened rather
than wrapped, because wrapping one row of a table breaks the table; and in
`models.py` the column comments moved above their columns for the same reason.

**Imports below the first statement (14).** `csam.py` kept its FastAPI imports
under a section header halfway down the file; two node tests had a constant and
a `pytestmark` wedged between two import blocks. Moved, not suppressed.

**Bindings nothing reads (4).** Three in tests, where the call stays and only the
name goes — `_user(client, "listener")` is there to create the user, not to
return one. The fourth was in `revocation.py` and was not a lint finding at all:
`_connect_and_listen` opened an httpx stream to the WebSocket URL, did `pass`,
and then opened the real connection through the `websockets` library. One
pointless request per connect, left over from before that library was used
directly. Removed, and `httpx` with it.

**`l` as a name (4)**, **semicolons (6)** in the POC spikes, and the rest.

2893 passed, the same count as the two commits before it.

`meshbay_node/revocation.py` is worth a decision separately: 154 lines that
nothing imports, superseded by `hub_client.maintain_ws`'s `on_revocation`. This
commit only stopped it failing the linter.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>style: ruff's own fixes, mechanically applied</title>
<updated>2026-09-19T12:24:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T12:24:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=86188385cbdae1ee90c1dca7a7b9db2edef1ecd4'/>
<id>urn:sha1:86188385cbdae1ee90c1dca7a7b9db2edef1ecd4</id>
<content type='text'>
`ruff check .` had gone unrun long enough to report 568 errors, which is the
same as having no linter: the next real finding would have been invisible in the
noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the
98 it cannot fix are the next commit.

What actually changed: import sorting (225), imports nobody used (87, none of
them a re-export — no `__init__.py` is touched, which was the one way this could
have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69)
and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12
this project requires, `Optional[X]` to `X | None` (24), and f-strings with
nothing to interpolate (19).

Checked rather than assumed: every module in the three packages still imports,
and the suite is 2893 passed — the same count, test for test, as the merge
before it.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Merge origin/main: the operator's upload ceiling beside the disk-thread work</title>
<updated>2026-09-19T12:01:38Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T12:01:38Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d2495a2c4b89fbbfc18cefec83ae96cabdd745e2'/>
<id>urn:sha1:d2495a2c4b89fbbfc18cefec83ae96cabdd745e2</id>
<content type='text'>
One conflict, in §15.3's open list, and it was two changes agreeing rather than
disagreeing: this side removed the rows for the third-party search bound and the
node-announcement bound because both are now built (AV27, AV28), while the other
side kept them and added a new one. Resolved by keeping what is genuinely still
open — per-device revocation having no CLI — and leaving the two closed.

`webrtc_server.py` merged without conflict but the two sides met inside one
function: `_upload_chunk` gained the operator's `max_upload_gb` ceiling from
there and the per-group lock and `off_disk` calls from here. Read back rather
than trusted: the operator's ceiling now sits inside the critical section that
keeps chunk ordering, and the unlink beside it goes to the disk thread with
everything else.

2893 passed. The twelve `test_sticky_header.py[firefox]` setup errors are the
open Firefox on this machine, as before.
</content>
</entry>
<entry>
<title>fix(node): the node decides which files it will transcode, not the player</title>
<updated>2026-09-19T11:45:56Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T11:45:56Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f8223293a211a87c92b1fed80f5ca53660f6b26c'/>
<id>urn:sha1:f8223293a211a87c92b1fed80f5ca53660f6b26c</id>
<content type='text'>
`BROWSER_INCOMPATIBLE_AUDIO_EXTS` named the two formats no browser decodes and
was read by nothing. The player asked for `.wma` and `.mpc` and no others, and
`music-player.js` called itself "kept in sync with the node's" constant — so the
entire restriction lived in the caller, and a member's own message is not the
caller.

What that let through: this converts a whole file and holds a transcode slot
shared with video streaming while it runs. One message naming a two-hour film
spends minutes of the operator's CPU and a slot every other viewer is queued
behind. `AUDIO_TRANSCODE_MAX_BYTES` catches the result, after the work is done;
only the extension catches the work.

The client comment now says what it is — an optimisation that saves asking for a
refusal — rather than implying it is the rule.

Two tests: a film is refused before ffmpeg is reached, and a WMA still passes,
because a gate that refuses everything is not a gate.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): bound what a transcode may produce, and read ffmpeg's output off the loop</title>
<updated>2026-09-19T11:24:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T11:24:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=eca01c7f970d2d3ab2298f934da9776fe01179c6'/>
<id>urn:sha1:eca01c7f970d2d3ab2298f934da9776fe01179c6</id>
<content type='text'>
Two things about the same three functions, which write to a temp file with ffmpeg
and then read it back.

**The read was on the event loop.** These files are ffmpeg's own, under
`tempfile.mkstemp` on the system disk, so they are not a group root and there is
no spun-down platter to serialise against — which is why they go through
`asyncio.to_thread` and not `roots.off_disk`. But a whole transcode read inline is
still tens of megabytes of blocking read while nothing else in the node is served.
The AST guard now covers the module with no exemption at all, and a second check
refuses a direct call to the reading helper: passed to `to_thread` it appears in
the syntax tree as a name, called inline it appears as a call.

**The audio transcode had no size ceiling**, where the subtitle path beside it
has had one all along. The bound is the media cache's rather than memory's:
`put_thumb` writes one SQLite row, and the store is 512 MB with least-recently-used
eviction, sized for thumbnails, posters and short transcodes. At 192 kbit/s a
three-hour source is ~260 MB — one row that evicts most of the cache to fit and is
evicted again by the next few thumbnails. Not a size this store can hold usefully.

64 MiB, about forty-five minutes: past any track, any single piece, most sets.

It takes away nothing that worked. `AUDIO_TRANSCODE_TIMEOUT_SECS` is 120, so a
source long enough to reach this was already liable to be killed mid-transcode;
what changes is that the refusal now names the limit it met and the size that met
it. Serving audio of that length properly means streaming the conversion instead
of buffering it, which is a different feature — recorded in §9.8 rather than left
as an implied promise.

The stat comes before the read, so an oversized result costs a stat rather than
the read and the memory behind it.

Twelve `test_sticky_header.py[firefox]` setup errors again: Firefox is still open
on this machine, and its `[chrome]` half passes.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
