Back to list
Skywire Development Update — September 4, 2026
The desk stopped being a page the hypervisor happened to serve and became the hypervisor UI itself — on the visor-attached hypervisor, on the standalone hv serve, and on the documentation site, whose root now greets with a terminal that has just run skywire --help. The docs moved into the binary that documents them, so the command reference is the cobra tree walked live on every request rather than a copy that can drift, and it renders with no visor running at all. Underneath the surface work, the transport discovery’s per-transport metrics feed — the only dead feed on the deployment — was traced to a single object a fraction over CXO’s 16MB limit, which is refused at Put time and therefore kills the whole feed rather than one window; it was gzipped, chunked, and rebuilt as immutable per-day leaves. A production dmsg server was found restarting roughly every eighty seconds, and the reward server’s statistics pages were found pulling 24MB over dmsg to compute three numbers TPD publishes in 138 bytes.
Skywire: The Desk Is the Hypervisor UI
4491 makes the desk the default surface — the native hypervisor serves it at / with the Angular dashboard opening as a window inside it — and repairs the regression left by retiring the JS browse engine, whose panel had been the desk chrome’s only implementation, leaving the desk hanging at “starting the desk…”. 4492 replaces that stopgap with the chrome from the 0magnet/desk library and makes loopback addressing explicit, fixing a real hazard along the way: the retired engine had routed 127.0.0.1 and localhost through the page’s vnet port table, the Go browser port never grew that branch, and a hypervisor URL therefore fell through to the clearnet transport, where a proxy exit would have dialed it against that exit’s own localhost. Then everything moved into tabs.
4493 makes the dashboard the browser’s first tab, which took two fixes: netscrape styled its host element with a wholesale cssText write that destroyed the geometry its window manager had given it, so the tab strip rendered under the title bar and the drag handler swallowed every click meant for a tab; and a page framed under a /vnet/<port>/ prefix has its base href rewritten to that prefix, so a /dashboard path normalized back to the root and served a desk inside a desk. 4494 turns terminals into panes of one window, giving every window its strip at creation even when it will only ever hold one pane, because retrofitting one later means moving live DOM out from under a mounted pane and neither an iframe nor an xterm survives being reparented.
4495 names tabs after their host, and 4496 gives the chrome favicons fetched through the transport and inlined as data URIs, titles read from the page’s own document, history buttons that gray out when there is nowhere to go, and the keys a browser is expected to answer to. 4498 makes hv serve agree with the visor-attached hypervisor, which it never had, since it builds its own mux and had never gone through the shared handler. 4499 fixes the /desk redirect, which sent an absolute / and so escaped the vnet prefix onto the outer server’s root — a different visor entirely, silently; http.Redirect cannot express the relative form, because it resolves the target server-side against the request path and turns it straight back into the absolute one, which is why the existing test kept passing while the behavior was wrong.
4522 then retires /dashboard altogether, the framed root already serving it, and 4519 moves wasm variant selection from a query string to /wasm/<variant>/ paths, since a query string cannot select bytes on a static host and the blob and its loader are toolchain-specific and must travel as a pair.
Skywire: Docs Served by the Binary That Documents Them
4527 adds skywire doc serve, putting the live cobra tree at the root and 762KB of embedded prose beneath it. The command reference costs nothing to carry and cannot drift because it is the binary’s command tree; only prose travels, and three of the embed’s corrections came from measuring rather than reading — two prose directories are gitignored and staged at docs-build time, so they are absent when go build runs; the file list has to be hand-enumerated because a glob sweeps in the 528 generated command pages, and the first hand list silently missed five files; and a whole-directory embed of the examples took 551KB of built wasm, forty percent of the total. The reason for serving rather than linking is measurable too: in the browser visor the nested browser has no transport until someone starts a visor, and on the deployed site the docs tab sat on a placeholder while the address bar read the docs URL.
4524 is the step before it, letting the desk’s browser render same-origin pages natively instead of transcoding them into a sandboxed frame with their scripts stripped — matched on exact origin, never a bare prefix a sibling origin could satisfy — and giving the desk shell the framing guard that natively rendered docs pages make reachable, since they execute their own scripts and would otherwise mount a desk inside the desk’s browser without end.
4532 opens the docs as a tab beside a terminal that runs the commands they describe, and carries three fixes only a browser could find: doc serve had bound with net.Listen, which serves fine on a workstation and binds nothing reachable in a tab, the case the command exists for; the loader now claims the loopback however it is spelled and hands netscrape a source separate from the URL, so a tab reads vnet:8002 while rendering natively out of the service-worker path, where before the canonical spellings resolved only through the transcoding transport and a tab addressed the documented way silently rendered wrong; and the docs tab no longer waits on the hypervisor, since gating it there meant no docs wherever no visor starts, which is exactly the docs site. On the site itself, 4485 regenerated the command reference, taught skywire doc to scrub the generating host’s username, home and working directory so regenerations neither leak nor churn on whoever ran them, corrected a batch of prose against the code, and mounted the playground as a persistent drawer under Material’s instant loading; 4488 fixes what instant loading broke, since it rewrites head content on navigation and dropped the drawer’s injected style element, leaving the body-appended panel in static flow at the document bottom; 4490 then removes the drawer entirely and makes the site root the desk surface; and 4487 updates the glossary for the retired JS engine.
Skywire: The Feed One Object Killed
cli visor cxo status showed tpd-metrics as the only dead feed on the deployment — zero paths, never synced, timing out waiting for a Root — while its three siblings synced normally against the same peer, and TPD’s log said why once per publish attempt: the object was too large. 4508 establishes the mechanism, which is worse than losing a window, since CXO refuses an oversized object at Put time and the feed therefore never gets a Root at all; raising the limit would have been the wrong fix, because CXO holds whole objects in RAM on the subscriber side too and those subscribers include visors running in browser tabs.
4509 replaces that PR’s degradation with compression — the metrics publisher was the only one of TPD’s publishers that did not gzip — carrying a 16.8MB one-day window of 79,257 records in a single 4.0MB leaf with nothing dropped, and splitting across part leaves anything that still will not fit; 4516 then fixed the split’s arithmetic, which computed the part count from the raw JSON size while the budget applied to the gzipped bytes, so production was publishing a seven-day window as fifteen parts where four would carry it, each surplus leaf another object a subscriber must fetch before the window is usable.
4515 changes the shape rather than the size: the publisher had written three overlapping windows as one leaf each, so content-addressing bought nothing — one changed byte made a multi-megabyte object new, and the thirty-day window went back over the wire in full every thirty minutes though twenty-nine of its days could not have changed. One immutable leaf per calendar day means a settled day hashes identically forever, windows are assembled reader-side, and the store is asked for sixty transport-days per cycle instead of 102; liveness and latency are current state rather than day-scoped facts and ride only on the current day’s leaf, since carrying them everywhere would make every past leaf mutable again. Two subscriber-side fixes finish it.
4520 gives the metrics feed the large-feed first-sync timeout, a classification that had been backwards — all-transports is around 679KB in practice and had the generous bound, while the network-wide aggregate now spanning twenty-odd leaves sat on the 10-second default, timing out with zero cached bytes and retrying from scratch every cycle while TPD published without error. 4525 fixes the deeper trap: a caller finding the cache empty cannot distinguish an empty feed from a first fill still in flight, and returning the miss immediately also releases the reference, so the grace-period teardown closes the subscriber mid-fill and the next attempt starts from nothing — a feed whose first sync outlasts one caller’s patience never becomes readable through that path however often it is retried. Holding the reference across a bounded wait, the same visor returns a 7.1MB hit carrying 20,493 valid records.
Skywire: Numbers Published at the Size They Are
4526 adds the feed those bulk datasets should never have been standing in for. Every network-level number worth charting had been obtainable only by downloading 2.4MB or 16.8MB and reducing it locally to a value TPD already holds in 138 bytes, so a new tpd-stats feed publishes the network aggregate and the version stats — both under a kilobyte, both gzipped, republished every twelve seconds off caches the HTTP handlers already serve from.
The per-key rollup is deliberately left off at 38KB gzipped, two orders of magnitude larger, because including it would push the whole feed onto the large-feed timeout and defeat the point of a feed a dashboard holds continuously. The publisher also refuses to stamp a known artifact into every chart built on it: TPD’s aggregate is readable while it refills after a restart, so each body carries an observation time, a completeness flag and a confidence verdict, and a sample below ninety percent of the trailing fifteen-minute peak does not overwrite a standing complete one — bounded at five minutes rather than held indefinitely, since a genuine network-wide drop is indistinguishable from a refill at sample time and a feed that froze forever on the first big decline would be worse than one that publishes it honestly marked. The reward site got the corresponding treatment.
4531 takes the summary’s three counts from the 138-byte aggregate instead of reducing a 24MB body that was failing outright with EOF, keeps bandwidth as a separate best-effort fetch because that figure is the min()-verified one mirroring the reward calculation rather than TPD’s cumulative, and prints “unavailable” on failure instead of a zero, which reads like a real measurement. 4534 rebuilds the charts: two had been fetching the tens-of-megabytes per-transport body when TPD already publishes the 2.7KB reduction they want with per-day latency alongside, and the version-history page was 2.1MB and 8.1 seconds because its chart was 14,417 absolutely-positioned one-pixel divs and every request re-parsed the whole history directory — now inline SVG at 71.5KB for 684 days, with the two Chart.js CDN tags gone and a missing measurement breaking the line rather than being drawn as a zero.
4535 then restores something the reward UI used to be: it has always looked like a terminal, and for a long time it was one, so rather than hand-writing HTML that resembles a terminal, one renderer emits ANSI — printed to a terminal it is the TUI, passed through an ANSI-to-HTML filter it is the page — with network counters, per-day bandwidth and latency, visors online at five-minute resolution, transports by type, and version adoption colored by how far behind each build is, in a 15KB fragment whose data costs 138 bytes plus 2.7KB plus 300. Failure is per-section by construction: each source is fetched independently and a failure recorded rather than returned, so the page renders what succeeded and names what did not.
Skywire: dmsg Servers, Entries, and Identities Nobody Needed
A production dmsg server had been restarting roughly every eighty seconds — 3,317 restarts against 44 days of host uptime, 176 panic hits in two hours, all the same stack — and every restart dropped every session on it. 4506 traced it to the once-a-minute throughput task, which iterates the session map and reads each session’s noise nonces; that state is only assigned when a handshake completes, so a session is reachable through the snapshot both before its handshake has installed any and after it has been torn down, and the panic was on a background goroutine where nothing could recover it. Unreadable sessions are now skipped rather than recorded as zero, which would make the counter appear to run backwards and be read by the delta arithmetic as a uint64 overflow, and periodic tasks run through a recover, since nothing served depends on them and a server that stops reporting throughput is enormously preferable to one that dies. The finding also makes point-in-time fleet checks meaningful again: that host is up for most of an eighty-second sawtooth, so a probe of all nine servers would have reported nine of nine healthy.
4505 found the discovery’s official-server allowlist maintained by hand alongside the deployment seed list and drifted — seven of the nine seeded servers plus two that no longer exist — and because the discovery downgrades anything neither allowlisted nor carrying the auth passphrase, the live servers 03f57e7cf26c0764c5ab659a606add056ddf8bfad4f5bc7e8613cad05e5f228adf and 0326978f5a53aff537dbb47fed58b1f123af3b00132d365f1309a14db4168dcff7 were being served to clients as community and skipped outright by anything configured for official servers; the allowlist now falls back to the seeded keys. 4521 fixes a related blindness, the discovery entry advertising a version string that stops at the release tag while /health on the same host reports the commit, so all nine production servers advertised an identical string and there was no way to see which had picked up a fix.
Two changes then attack the discovery’s write volume from opposite ends, both measured against 913 clients and roughly 38 entry posts per second with 1.8% rejected on the sequence check: 4503 has a client that loses a session prefer re-dialing the server it just lost, since reconnecting leaves the delegated set identical and publishes nothing, bounded at two retries and not yet verified under fleet load; 4511 closes the read-modify-write race itself, where a get followed by a sequence-incrementing put had only the put guarded, so the startup publish, the update tick and a session nudge could each read the same sequence and all but one be rejected at the cost of a full Noise handshake — guarded now by a per-entity semaphore taken across the whole cycle, deliberately a channel rather than a mutex, because acquisition has to honor the caller’s context and a plain mutex here is exactly what wedged dmsg-only services before. Three more changes removed identities that had no business existing.
4501 deletes the server-side tpviz API from hv serve along with the dmsg client backing it: that process serves a page, the visor the page is about runs in the tab with its own client, and the API had arrived whole as a 404-avoidance patch minting a fresh keypair on every process start and discarding the cleanup function, leaving 660 abandoned identities in fourteen days on the production host. It was removed rather than gated, because a flag would leave a mesh client one config line away from a static file server. 4502 fixes the same shape in the reward server, which derives a public key from its configured secret and listens on it, then started a second client from a fresh keypair — shadowing the identity it had just derived — purely to read services the first client already reaches.
4530 fixes the third instance in tp viz, where installing the fallback discovery publishes an entry as a side effect and a restart loop mints a new key each time; seeding the service keys directly leaves the client dialable and publishing nothing, verified live as reaching the deployment while remaining absent from all 931 discovery entries.
Skywire: A Boot Deadlock, Wasted Bytes, and a Lint Job Gating Nothing
4486 fixes an AB-BA deadlock that could wedge the entire transport layer from boot until restart, taking the RPC port, the hypervisor UI and every transport operation with it: accepting a transport holds the manager’s mutex while reading six packet handlers under their own locks, and every handler setter held its handler lock across the manager’s, so an inbound transport accepted inside the init window deadlocked against a module installing its handler.
It was caught live on the dev visor with a pprof goroutine dump showing the accept parked on one handler lock while the setter holding it waited on the manager’s. 4483 fixes a router pathology visible only on quiet routes: gap-free traffic was acknowledged only by the periodic SACK service every fifteen seconds or so, far past the sender’s loss-detection horizons, so trickle traffic had every frame re-sent up to its backoff cap — about 2,500 spurious retransmits on one idle route overnight — and Karn’s rule then starved the ack-delay estimator of samples, so the sender could never learn its real feedback delay. A one-shot 100ms delayed ack after clean in-order delivery coalesces bursts into one SACK and gives the loss detector honest samples, at a cost of at most one extra small SACK per 100ms under bulk traffic.
4489 removes a 2.4x waste in range-split, where a single twenty-second deadline covered both the SOCKS handshake and the entire multi-megabyte chunk body, so on any route sustaining less than about a chunk per twenty seconds a chunk could not mathematically complete: every attempt read a few megabytes, timed out, was discarded and refetched, with the sequential rescue carrying the file — 48.5MB received for a clean 20MB download at 158KB/s. The wasm VPN client from the previous day gained its operational half, with 4481 adding dial retry, because the first route dial after a cold boot routinely times out while transports are still forming, and auto-selection from service discovery with direct peers first, failed candidates cooled for ten minutes, and handshake-plus-netstack-up as the honest probe since a zombie server cannot fake the address assignment; 4482 adds the wrap-everything mode, routing the page’s default clearnet path through the tunnel while the session runs, with a VPN failure falling back to the proxy chain so pages never hang on a dead tunnel.
4477 compiles the Go browser into the visor binary as a surface of the page-realm instance already loaded there for its shell and netview roles, rather than shipping a second module with its own duplicate Go runtime, and retires the JS browse engine — which was not merely a browser but the visor’s whole mesh-browse UI framework, so the taskbar, panes, deep-linking and real-origin isolated browsing go with it, and the rest of the day’s desk work is what rebuilds them. Finally, three fixes made CI mean something again: 4523 corrects four British spellings that had turned the lint job red on develop for every run, which also turned the linux, darwin, windows and android jobs red regardless of their tests, so a genuinely broken pull request looked exactly like a clean one; 4507 drops ten superseded pseudo-version lines failing the module integrity check; and 4528 clears the two remaining lint findings and a CodeQL allocation-size-overflow alert open since August 25 — not exploitable in practice, since it would need a nine-exabyte slice, but enough to redden the check on every pull request touching that call path.