Back to list
Skywire Development Update — September 26, 2026
The twenty-sixth centered on a fleet-wide discovery problem that had been quietly starving browser-tab visors of routes: transport discovery was failing to subscribe to most of the connections it held, dropping the transports it did see, and buckling under its own ingest load once those bugs were fixed and every visor started actually reaching it. Six separate fixes closed that chain end to end. In the browser desk, clearnet and status-page browsing inside netscrape were restored after an earlier refactor had quietly dropped the code path that made them work, and the hypervisor’s own enable flag gained a way to survive a tab reload instead of reverting itself. The visor’s underlying config file can now be read and edited directly over the API rather than only through the generated JSON that gets silently overwritten on every regenerate.
Skywire: Transport Discovery Catches Up With the Fleet
5196 found the root cause behind why a browser-tab visor’s transports were barely visible in discovery at all: the aggregator subscribed to every held connection in one serial pass, and a connection that never answers can hold that pass for up to its full minute-long response timeout — on a fleet-sized connection set, a pass routinely outlasted the ninety seconds a connection is allowed to sit idle unsubscribed, so most subscribe attempts landed on connections the node had already closed by the time their turn came, logged over six thousand times in twenty-five minutes against barely two hundred that actually succeeded. Each connection is now subscribed the instant it connects, on its own goroutine, with the periodic pass capped at sixty-four concurrent subscribes rather than one slow walk through all of them — a fix shared by every aggregator built on the same engine, so dmsg discovery, service discovery and the address resolver all got it at once.
5195 found a second, independent reason a tab’s transports kept disappearing even once they were seen: whenever a hub’s own targeted fetch failed, the aggregator fell back to re-applying that hub’s last known good list through a full reconcile, which deletes anything missing from it — including a transport the tab had opened since that list was captured. The throttle mark that deletion left behind then blocked the tab’s own fresh snapshot from re-registering the same transport for up to a hundred seconds, until the next failed fetch deleted it all over again. The cached re-apply now only registers and refreshes, never deletes, and a full reconcile that does remove a transport also forgets its throttle mark so the other side’s very next snapshot can re-register it immediately; visor state also gained a record of every announce attempt to discovery, since a failing one had left no trace outside trace-level logs.
With visors actually subscribing and staying registered, discovery’s own ingest pipeline started falling over under the new load. 5198 found a feed’s own fill-callback running inline on the node’s shared feeds loop, blocking every other connection’s subscribe or next-root delivery behind whatever that one feed’s own storage write happened to cost — on production transport discovery, a new connection logged as received and then took nearly two minutes to be fully established, timed out by the same idle watchdog the new subscribe fix was trying to beat. 5197 found that once every visor actually reached discovery, the telemetry they all publish on a shared ten-minute cadence arrived as one simultaneous burst that exhausted the Redis connection pool outright, logging over a hundred thousand deadline-exceeded failures in twelve minutes while the host swapped; concurrent ingest is now capped at eight dispatches with a waiting feed’s own fill goroutine providing the backpressure, and a shard whose processing time has run out simply stops rather than continuing to issue writes that are already guaranteed to fail.
5200 found that fix still left memory spiking to over three and a half gigabytes on every telemetry beat, because a feed’s drainer decoded a visor’s entire transport tree in one shot and then waited for a per-leaf ingest slot while holding the whole decoded tree in memory; one ingest slot is now held across an entire tree’s decode and dispatch instead of acquired per leaf, so at most eight trees are ever live in memory at once rather than an unbounded number waiting their turn.
5201 closed the last piece of the memory picture: a heap profile had found over a gigabyte of a production discovery node’s memory sitting in freshly re-encoded copies of the same roughly 3.5-megabyte snapshot object, encoded fresh for every one of the hundreds of visors requesting it; since the object is content-addressed, one encoding now serves every requester, cached and evicted oldest-first. 5194 closed the loop from the dialing side: a skynet dial from a tab had been failing intermittently because a single hung setup attempt could consume an entire thirty-second dial budget with nothing left for a retry, and the local route-finding snapshot it depends on — roughly sixteen thousand entries, costing nineteen seconds to fetch and index from inside a tab — was being rebuilt inline on whichever dial happened to need it next rather than kept warm in the background.
Skywire: Clearnet Browsing and the Desk’s Own Hypervisor
5203 and 5204 restored two browsing paths inside the desk’s own netscrape browser that an earlier refactor had quietly broken by removing the code that used to make them work: a plain clearnet page fell through to a direct page fetch the browser blocks outright as mixed content, and status.skysocks was sent to the resolving proxy, which has no route for it and served its own “connecting” interstitial forever. Both now go through the tab’s own local skysocks-client instead, and an HTTPS page specifically is fetched by the tab’s own hypervisor over its virtual network, verified against an embedded certificate bundle since a browser tab has no system trust store of its own to check against.
5205 found the setting that controls whether the tab’s own hypervisor exists at all could silently flip itself off on a reload, since autoconfig only re-asserts that flag when the generated config already has it on; a new flag records the intent directly in the durable config file instead, so one boot that happened to turn it off no longer keeps it off on every future reload.
5199 found several CLI commands — the load-test runner, parts of the proxy test path, the Wisp client’s own proxy mode — dialing a local SOCKS5 proxy directly on the loopback address, which inside a browser tab reaches the host’s own network rather than the tab’s virtual one and was refused outright; all of them now dial through the same virtual-network path the RPC client already uses, which is an ordinary direct dial on every native build and makes no difference there. 5202 fixed netscrape’s own browser state living in shared package-level variables rather than per window, so opening a second browser window on the desk made switching tabs in one window hide the other’s frames entirely, and closing a window could leave the survivor’s own tab switches silently updating the dead window’s address bar instead of its own.
Skywire: Editing the Config File Itself, Not Just Its Output
5206 gave the visor’s actual configuration file, the one autoconfig regenerates the JSON config from on every single run, a real API of its own — GET and PUT endpoints on the hypervisor, a matching CLI subcommand, and a visor API method, all addressing an edit by the same autoconfig flag name and routing it through the identical table and writer autoconfig --<flag> already uses. Editing only the generated JSON had never actually stuck, since the next autoconfig run, which fires on every package update and every single browser-visor reload, simply overwrites it from the conf file regardless of what the JSON said; now there is a durable place to make the same edit that survives exactly that regeneration, with the secret key itself left permanently unreadable and unwritable through this path.