<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/docs/MESHBAY_NODE_PROTOCOL.md, 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-23T16:16:44Z</updated>
<entry>
<title>docs: qualify what an invitation link changes about the code</title>
<updated>2026-09-23T16:16:44Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-23T16:16:44Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1f6b67c7bfc8078991a4607bb0419aea15bfd54c'/>
<id>urn:sha1:1f6b67c7bfc8078991a4607bb0419aea15bfd54c</id>
<content type='text'>
The security claims table, §3.4's property 2, the protocol's stated
limits, the quickstart and the user guide's defaults now say where a
link's code differs: bound to its account only when redeemed, and held
by the hub when the inviter asks it to mail.

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>feat(node): per-file upload ceiling is an operator setting, default 8 GB</title>
<updated>2026-09-18T23:49:56Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-18T23:02:05Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c2c49182e6d4f1f1ef3ed3ba5e8699912f9a27bd'/>
<id>urn:sha1:c2c49182e6d4f1f1ef3ed3ba5e8699912f9a27bd</id>
<content type='text'>
Was a 4 GB constant in webrtc_server.py, the same on a small board and on
a machine holding a library. Now max_upload_gb in node.toml, on the Node
page and via `meshbay-node transfers max-size`, read from the transport
context per chunk so a change reaches an upload already running.

MESHBAY_DESIGN.md §6.4; §15.3 records a defect found beside it.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: state what is built, and what the packaged install has answered</title>
<updated>2026-09-18T12:59:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-18T12:59:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=880a4273a2ffa2aa86737a3cfb64fac07adc09c8'/>
<id>urn:sha1:880a4273a2ffa2aa86737a3cfb64fac07adc09c8</id>
<content type='text'>
The two documents disagreed with the code they describe. §15.2 still listed
playlists and Chromecast subtitles as not built, both shipped and tested; the
protocol document's header said wire version 3.2 beside its own §13 saying 3.3,
which is §12's "a number restated away from its definition is a number that will
drift" happening to the document that states the rule.

The packaged install is no longer an open question: the artefacts have been taken
to a running hub and node on Ubuntu 26.04 (.deb), Fedora 44 (.rpm) and Windows 11
(.exe). What is left of D11 is signing the binary, which is a different question
from whether it installs.

The migration row overstated its own risk. Nothing replays the revisions behind
the first packaged release — they were development no installation ever ran — so
the exposure is the next migration, not the chain.

The disk-I/O row gains the consequence that was measured rather than reasoned: a
root waking from standby stalls the loop long enough for a client's WebRTC
connection to time out, on one node with one member. `refresh_availability` is
named beside the two chunk paths, because a stat on a sleeping disk is what wakes
it while reading nothing.

A blank line had also split §15.3's table in two.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): measure where a seek lands instead of predicting it</title>
<updated>2026-09-17T14:18:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T14:18:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=0fed6786fe304195df66776787c5b5b82d321bcf'/>
<id>urn:sha1:0fed6786fe304195df66776787c5b5b82d321bcf</id>
<content type='text'>
The previous fix read ffprobe's key frames and took the last one at or before
the request. It was wrong twice, and a viewer felt the difference: subtitles
went from 5 s early to 2–3 s late.

Matroska's Cues index only some keyframes, so an index seek backs off to an
indexed one that the frame list does not single out. And the landing point
moves with **which streams are mapped**, because the container is positioned
where every mapped stream has data — on the reported title, a seek to 4913.7 s
landed at 4909.863 with video alone and at 4907.236 with the second audio
track mapped beside it. The frame scan gave the first number; the stream
delivered the second; the gap was 2.65 s, and the measured audio displacement
in the served stream was 2.65 s.

So the node asks ffmpeg instead: the same seek, the same mapping, one copied
frame under `-copyts`, and the answer read back off the result. 0.06–0.07 s,
cheaper than the scan it replaces. The `-ss` argument stays at the request, so
the bytes served are exactly the ones served before — only the number naming
them changes. The probe runs after the audio track is resolved, because it
cannot be right before that is known.

An answer after the request, or further before it than any real keyframe gap,
is discarded in favour of the old label: a number wrong by seconds beats a
fabricated one.

Found by decoding the served stream and locating its first frame in the
source, which put it at 4907.213 s against an announced 4909.863 s. The test
does the same thing rather than comparing the announced number against a
second reading of the same probe.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>fix(node): a seek reports where the picture begins, not where it was asked</title>
<updated>2026-09-17T13:50:55Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T13:50:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3dcce17f9c616c8528a93bc625eb462d7fffb8f9'/>
<id>urn:sha1:3dcce17f9c616c8528a93bc625eb462d7fffb8f9</id>
<content type='text'>
Copied video can only start on a keyframe, so `-ss t` with `-c:v copy`
delivers the keyframe at or before `t`. The node reported `t` anyway and the
client builds `SourceBuffer.timestampOffset` out of that number, so everything
downstream believed the picture stood a few seconds further along than it did.

That was a wrong label while only the scrubber read it — it is recorded as
such, low priority, in the design document. Subtitles made it a wrong answer:
their cues carry the source's own absolute timestamps, so the mismatch put
every line on screen before it was spoken. Reported from real use on an H264
title, where seeking to 600 s, 2650 s and 5000 s lands on keyframes 0.82 s,
1.56 s and 4.64 s earlier.

The copy path now resolves the request to that keyframe, seeks to it, and
reports it. The bytes delivered do not change — ffmpeg lands on the same frame
either way — only the number that names them. The look-up reads the thirty
seconds before the request and cost 0.12–0.51 s on that title, which is the
price already costed in §15.3 and never paid. Re-encoded video is untouched:
it can begin exactly where it is asked to, and does.

Two details worth their lines. The keyframe is passed back to ffmpeg at six
decimals, because rounding a keyframe's own timestamp down puts it before the
frame it names and selects the previous one — the same fault again, smaller.
And the ffprobe window ends past the request, since an interval whose end is
the request never emits a keyframe sitting exactly on it: the resume position
is the one place a viewer asks for the same instant twice, and it would have
been answered a whole GOP early.

The test decodes the first frame served and matches it against the source
frame at the position announced, rather than comparing `start` to an expected
number — both sides of that comparison would be reading the same ffprobe and
would agree by construction.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>feat: embedded subtitles in the video player (MNP 3.3)</title>
<updated>2026-09-17T11:39:23Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T11:39:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ad4ca3229002997934ccb5b2eaeb553c13b8888f'/>
<id>urn:sha1:ad4ca3229002997934ccb5b2eaeb553c13b8888f</id>
<content type='text'>
MSE decodes no in-band text track, so a subtitle cannot ride inside the
fragmented MP4 the player is fed. The node extracts one track whole, converts
it to WebVTT and caches it under its own hash; the client pulls that blob
through the ordinary file_req/chunk path and hangs a &lt;track&gt; on the video
element — the same indirection as a TMDB poster or an audio transcode, which
is what makes a film's subtitles extracted once in the life of the file rather
than once per viewing. Whole-file also makes the cues absolute, so a seek and
an audio-language change both leave the track untouched.

**The ordinal counts every subtitle stream, including the ones never listed.**
Only text codecs are offered: a bitmap track (PGS, VOBSUB — about a fifth of a
real library) has no path to WebVTT without OCR, and one extracted anyway
yields a header with no cues, which is a menu entry that shows nothing and
reports no error. Numbering the survivors of that filter would give a
PGS/SRT/SRT file the ordinals 0 and 1 for its text tracks and `-map 0:s:0`
would then extract the PGS — the same trap `AudioTrack.ordinal` exists for,
one level deeper. A fixture whose first subtitle stream cannot be decoded pins
it, and the handler checks membership of the probed list, never a range.

Additive and MINOR: the selector is drawn from `subtitle_tracks` in the node's
own `stream_init` and from no version number, so `subtitle_req` is never sent
to a peer that would not answer it. The floor stays at 3.0.

Also here: a failed extraction never touches playback, a superseded reply
cannot install its blob over a newer choice, and `_languageName` is shared
with the audio labels — lifted by both label harnesses, since a lift that
names one function stops covering the rule the moment logic moves out of it.

Tests: 9 node (tracks told apart by the words in the extracted cues, not by
tags), 10 client. Full suite green: 1545 node/common, 1252 hub.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>fix(node): a seek left the audio a GOP behind the picture</title>
<updated>2026-09-17T09:23:36Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T09:23:36Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=677775266217b08044fe734be78f0e1bc874d708'/>
<id>urn:sha1:677775266217b08044fe734be78f0e1bc874d708</id>
<content type='text'>
-ss before -i cannot trim copied video, which must begin on a keyframe, but
accurate_seek did trim the re-encoded audio to the exact request. Every seek
on a copied stream therefore opened with a GOP-wide hole in the audio and ran
a GOP out of sync afterwards — 9.979s on a real film with a 10s keyframe
interval. Accurate seeking is now off wherever video is copied, and stays on
where it is re-encoded, which is the only path that could already begin where
it was asked to.

Every timestamp was correct throughout, which is why nothing caught it; the
tests assert on decoded audio and on frames compared against the source.

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