Back to list
Skywire Development Update — September 23, 2026

Skywire Development Update — September 23, 2026

The twenty-third opened a new design axis for the mux campaign — a session’s shape, how many tunnels it holds and how many legs each one carries — and spent the rest of the day building a converger that can actually move a session toward a target shape and correcting every place that move could silently fail on the way. Away from the mux, four separate mechanisms were found tearing down perfectly healthy voice calls for no real reason, a retransmit kept landing back on the exact leg that had just been cut, and the browser build’s CXO-backed state stores were given working in-memory substitutes so a wasm visor and skychat no longer fail at construction. A dmsg CLI tool’s config knob for the embedded Wisp server turned out not to reach the config file at all, in three separate ways, and the visor’s own log file was found silently losing every INFO, WARN and ERROR line to a rotation race that let DEBUG win every time.

Skywire: Calls That Nothing Was Wrong With

5091 chased a reported pattern of voice calls between a phone and a PC disconnecting on their own, and found four separate mechanisms with the same shape: something decided a perfectly healthy thing was broken and tore it down. A call’s media rides one dmsg stream on a server session shared with everything else that visor is doing, so a background liveness check closing that session on a failed new-stream probe hung up a call that had been relaying fifty packets a second in both directions for sixteen minutes — the reaper was acting on a weaker signal than the one that actually proved the session alive. A session that has read bytes since the last tick is now spared regardless of probe failures, and a separate self-probe recovery path that closed every session on two missed self-dials is now withheld for exactly the same reason.

Two more fixes were platform-specific. An Android network-change watcher had been comparing a phone’s entire address set and re-dialing every session whenever one merely gained an address, which happens routinely as IPv6 privacy extensions mint new temporary addresses — only a removed address should mean anything moved, and now only a removed one does.

A caller hanging up before the callee answered left the other end ringing out its full timeout because nothing was actually reading the signaling connection during the ring; it now does, and a call also survives its underlying transport dying and being re-dialed, closing a window where the two ends’ own reconnect attempts could miss each other by the twenty-four seconds a dead TCP connection can take to actually report failure. Every exit path now logs why a call ended rather than a bare “call ended,” which is what let the last three of these four be diagnosed in seconds instead of hours. 5116, 5117 and 5118 cleared the lint debt and duplicate helpers this large a change left behind when merged alongside the same week’s pool-role work, restoring both branches to a single, compiling set of helpers.

Skywire: A Retransmit That Kept Landing on the Leg That Lost It

5114 found a multi-leg download that could wedge permanently after cutting its busiest leg, reproducing one to three times in six runs: every retransmit of the sequence blocking the reorder frontier went out on the cut leg itself, because the retransmit picker chose purely by measured latency with no memory of which leg a frame had just failed on — and the cut leg had been the fastest one, so it was deterministically picked again every single time. Retransmits now exclude the leg a frame was last sent on, falling back to an unrestricted pick only when no other leg exists, and twenty-four consecutive test runs that used to wedge one to three times now complete cleanly. 5115 added the rig smoke run proving it on real hardware to the test plan.

Skywire: The Mux Shape Axis Gets a Vocabulary and a Target

5139 laid out the design: a session to one exit is a multiset of tunnels, each with its own leg count, and that pair is the session’s shape — 4x1 is pure stream-level striping, 1x4 pure packet-level multiplexing, 2x2 is what the router already does by default. The total number of chains a session holds is conserved across any move between those extremes, and the doc identifies the four primitive moves along that axis, noting that two of them already existed while the other two were only half built.

5141 gave the shape a parseable grammar and a live knob, mux.shape, defaulting to auto so an operator who never touches it sees exactly today’s behavior reported back rather than a a new target silently diverging from it. 5142 built the converger itself: once per arbiter tick, for each app-and-exit session, it compares the measured shape against the target and makes at most one move toward it — compose, decompose, promote or park — never dialing and never closing purely for a shape reason, and backing off cleanly whenever the peer declines a move rather than forcing it.

Skywire: Making a Shape Move Actually Survive the Network

5136 found every leg re-home failing identically on the live rig because the router’s own packet-type switch had no case for the re-home packet at all, dropping the request, its acknowledgment and its commit as an unknown type at the first intermediate hop or even at the far edge’s own router — the emulated tests never caught this because their harness dispatches a frame straight to the owning group, skipping the exact gate that was broken. 5153 then found that even a correctly dispatched re-home or split request couldn’t cross an unpinned relay in the live fleet, since a relay only forwards a packet type its own build recognizes and the fleet is heterogeneous by design; the fix wraps both requests inside an ordinary data-carrying mux control frame that any relay already knows how to forward blindly by route ID, with its own reserved sequence band so it never touches the reorder frontier or a SACK.

5144 made a failed re-home attributable to a specific hop for the first time, since nothing had been counting what a relay forwarded versus dropped and an initiator’s only visibility was a bare “no ack” — a relaying hop now counts forwards and drops by name, and a real bug surfaced while building that instrumentation: the transport read loop had been comparing the re-home packet’s type byte against the wrong constant and logging every single one as malformed framing.

5152 closed the last gap in the same area: a split or re-home the exit never actually acknowledges was retried on the same leg every single arbiter tick, each attempt taking that leg’s chain out of the tunnel’s active stripe set for the whole ack timeout — a session stuck at an uneven shape for two minutes on the live rig while throughput cratered by a factor of ten. A chain that fails to ack is now remembered and backed off by transport, doubling up to twenty times, and the converger prefers a leg with a better acknowledgment history before trying another.

Skywire: Keeping Composed Tunnels and Pool Legs Disjoint

5137 found that when an app’s two active tunnels to the same exit both wanted to compose a leg in the same arbiter tick, they could be offered — and both take — the identical standby tunnel’s first hop, defeating the entire point of having two tunnels since one dead transport would then cut a leg in both at once; the arbiter now runs one round per session, folding each tunnel’s take back into the held set before the next tunnel is even offered the pool. 5133 found two related ways the standby pool itself could double up on a first hop: a diversity-relaxation window was sized far too small for a client holding hundreds of transports, so free first hops sat outside it entirely, and concurrent pool fills at startup could each see the same transport as free before either one claimed it; first hops are now claimed under a lock for the duration of a dial, so a losing concurrent dial excludes the hop and re-picks rather than doubling it.

5138 gave a released pool leg somewhere to go other than simply closing: when the arbiter releases a leg after a load episode ends, that chain now splits back into a standalone standby route group on the same transport rather than being torn down outright, so refilling the pool after a load spike no longer costs a fresh setup-node dial every single time. 5128 fixed a status-page log line that always blamed “the exit not honoring the mirrored active set” even when nothing was actually wrong, appending that clause only when the signature genuinely indicated a problem.

Skywire: An Active Tunnel Follows Its Own Shape

5130 found the pool arbiter never actually widening an active tunnel in the first place, because its width target had been fixed at whatever a client dialed with and a role-reporting proxy client always dials one leg per tunnel; a new pool.active_width knob, default two, now gives such a tunnel a real target to widen toward under load. 5135 extended that to the idle case: packet-level multiplexing exists for privacy and for surviving a cut leg rather than raw throughput, so an active tunnel is now composed up to its target width whether or not it is actually under load, accepting a small throughput cost in exchange for never having to set a fresh route up mid-transfer after a cut.

5146 closed the gap where a tunnel the router had promoted to active still carried no application traffic, since the yamux session that actually carries streams belongs to the dialing app rather than the router — the app’s active-tunnel count now follows the same per-tunnel snapshot it already polls for capacity priors, draining a surplus tunnel of new work before it is parked rather than dropping anything already in flight. 5154 found a real regression this chain of persistence had exposed: a per-app mux-width override of zero is not “nothing requested,” it is the documented zero-hop direct shortcut a tunnel deliberately falls back to when its route group cannot be built — stamping a width onto that fallback dial re-triggered the exact route setup that had just failed, which is how a persisted width from an earlier test run could leave a later proxy session stuck reporting “Starting…” for its entire timeout.

Skywire: Naming the Far End Correctly

5155 found every re-home and split failure message on a dialing client naming the local visor itself as “the far end,” because the helper deciding which side of a route descriptor is the peer branched on the wrong field — the one thing those log lines exist to report had been reporting the wrong key the entire time. 5157 and 5158 found the same confusion reaching the CLI and the shape converger’s own session bookkeeping: proxy status printed a route’s destination as the local visor’s own key, and a mux.shape target set for two different exits was being converged and reported as if it were one shared session, since the session key used to bucket them was derived from the same always-local field. One helper now answers “who is the peer” everywhere a route group needs to know, used consistently by the CLI, the arbiter, the converger and the move ledger.

5156 fixed an emulated test that had been failing only on the darwin lane, because every leg in the emulator runs inside one process and genuinely does share a scheduling bottleneck the way real hardware rarely does at that scale.

Skywire: Threading a Wisp Config Knob All the Way Through

5131 added the autoconfig flags to actually turn on the embedded Wisp server, after finding that config gen had read a WISP environment variable since the server first landed but nothing could ever write it, and on the wasm visor the config regenerates on every load, so a hand-edited setting could never survive anyway. 5132 found that fix incomplete: the flag reached autoconfig but never the generator’s own environment-variable registry, so the setting still silently vanished, hardest of all in the browser build where configuration generation re-enters in the same process and reads its defaults before the edit is even visible.

5140 and 5143 each found a different flavor of the identical timing problem — a flag’s default computed once at package initialization rather than when the config file’s path is actually known, which only a browser build’s single-process re-entry could expose. 5145 and 5149 added the debug tracing that pinned exactly where in that chain the section was being lost, and 5150 found the actual final loss: a hand-maintained mirror struct behind the config’s JSON unmarshaling had simply never been given fields for the Wisp section or the persisted per-app settings, so every single config load silently discarded both no matter how correctly they had been written moments earlier.

Skywire: One Writer for the Log File

5147 found the visor’s own log file losing every INFO, WARN and ERROR line to a rotation race: the logging hook had been given one separate rotating writer per severity level, all six pointed at the identical path, so whichever level generated the most volume — always DEBUG — won the race to rotate the file first and left the others writing into an inode already renamed out from under them. Passing one shared writer for every level fixes it outright. 5148 found the identical bug, introduced the identical way, in the per-app log file instead. 5151 fixed both new tests failing their own cleanup step on Windows, since the underlying writer has no explicit close and a temp directory still holding an open file handle refuses to be removed.

Skywire: Working State Stores for the Browser Build

5121 found every CXO-backed publisher the visor itself owns — stats, the transport-discovery leaf, service-discovery registration — constructed with an on-disk data directory that simply errors out under GOOS=js, so those publishers never came up at all and transport registration silently fell back to a slower HTTP path with nothing explaining why. An in-memory store now stands in under js for every one of them, with the wiring state and its reason exposed directly in visor state rather than an omitted section. 5124 found the same failure in skychat’s own pairing and group managers, the two remaining CXO stores still hardcoded to disk, fixed the same way.

Skywire: What a Closed Connection Actually Means

5123 found that anything which signals the end of its data by closing its write side rather than sending a length — an FTP data connection, an old-style HTTP response, a plain pipe through nc — simply hung forever through skysocks, because the underlying session type offered only a full close and the half-close signal from the origin was silently dropped on the floor. 5126 found the mirror-image bug while verifying that fix: a splice helper tore down both directions the instant either one’s copy returned, so a client that closes its own write side early, which is a normal and common pattern, had its still-open download direction killed along with it — an empty reply with no error at all, which is a worse failure than the hang it replaced, since it looks exactly like success.

Skywire: Small Fixes to the Hypervisor UI, and Mobile’s License

5119 fixed the hypervisor’s ports handler panicking outright on the wasm visor whenever a dmsg session was carried over skynet, since that transport reports an address with no colon in it and the handler assumed one would always be there to split on. 5120 fixed the same node list showing a bare question mark for any skynet-carried dmsg session rather than a real carrier label. 5127 gave the Android app its first LICENSE file, dual-licensing it under AGPL-3.0 with a paid commercial option for anyone who wants to keep their own changes private, which is also what makes an F-Droid listing possible in the first place since F-Droid only publishes free and open-source software; the release pipeline now stamps a separate fdroid-v* tag only once F-Droid’s own build and update checks pass against the tagged commit. 5134 fixed that new F-Droid check crashing on its first real run, because one of F-Droid’s own tools assumed it was running inside F-Droid’s own metadata repository rather than a plain checkout of this one.