Back to list
Development Update — August 2
The headline today is a one-line divisor fix that retroactively repairs a fleet-wide reward regression: continuously-up visors that had been scoring ~30% daily uptime — below the 75% reward bar — snap back to ~100% the moment the reward server re-reads its stored counts, no fleet update required. Around it, apps got a way to reach a user who isn’t looking at them (a visor-global notification hub with an SSE stream, an OS-notify sink, and a hv notify bridge for headless nodes), a visor learned to quiesce itself without stopping the process, the wasm-visor’s clearnet proxy became a first-class configurable app, and skychat’s broadcast channels plus a mobile-app skeleton landed.
Skywire: The Reward-Uptime Divisor
3667 fix(reward-uptime): score visor uptime against the 5-min heartbeat cadence is the day’s most consequential change. Daily visor uptime — which gates rewards at ≥75% — is computed from the per-day heartbeat count as count / expectedHeartbeatsPerDay, and that divisor was 960 (86400/90), the ~90-second transport re-registration cadence. But a visor’s dedicated presence heartbeat fires every 5 minutes (tickDuration in init_services.go), so a fully-up non-hub visor only lands 288 heartbeats/day and scored 288/960 = 30.00% — under the bar — while transport hubs read 100% only because their frequent re-registration also calls RecordHeartbeat. This was the stable v1.3.88+ regression that clustered roughly half the fleet at ~30%; #3619’s earlier timeline backfill didn’t help because the reward percentage reads count, not the timeline bitmap. The fix scores visor uptime with expectedVisorHeartbeatsPerDay = 86400/300 = 288 (transport uptime keeps the genuine 90s figure). Because the daily percentage is derived from the stored count at read time, correcting the divisor retroactively fixes every day still in Redis — no backfill, no fleet update — and the ~30% cluster maps straight to ~100%.
Skywire: A Visor-Global Notification Hub
3666 Notification Hub gives apps a way to reach a user who isn’t watching them. Until now skychat was the only app that notified, and it posted straight to the host OS — which every other app would have had to duplicate, and which only worked when the visor and the user were the same desktop. The hub (pkg/visor/notifyhub.go) splits the problem: the app decides whether an event deserves attention, the visor decides where it can land, routing each notification down a fixed priority chain and stopping at the first tier that can deliver — an attached UI holding a fresh capability lease → subscribed host apps on the SSE stream → the host OS via pkg/osnotify → dropped (headless, the normal case for most of the fleet, not an error). Suppression is derived per-publish, never cached: a stale “a subscriber is attached” flag plus a dead connection would mute the desktop permanently. Publish never blocks — slow subscribers lose events rather than back-pressure the publishing app’s RPC serve goroutine. The app-facing seam (pkg/app/appserver/notify.go, one Notify/NotifyOrLog call) is deliberately transport-free so appserver keeps compiling for GOOS=js/wasm and TinyGo; all net/http/os/exec concerns live on the visor side. The ingress gateway overwrites the App field from the calling proc’s own identity so no app can publish under skychat’s name, and notification bodies (routinely untrusted peer text) are never logged. A live-only SSE stream (GET /api/notifications/stream) is registered outside the /api group so its 30s middleware.Timeout can’t sever a long-lived feed, and its write deadline is re-armed per write with a 20s ping so a vanished client is observable. skywire cli hv notify subscribes to a visor’s stream and posts each event to this machine’s notification centre — because a containerised/headless visor has no desktop session and a UI browsed over the LAN isn’t a secure context, so the browser’s Notification API is unavailable there too; while it’s connected the visor stops posting its own OS notifications so nothing doubles. skychat and skydex-client were moved onto the hub, and the skychat UI’s real notification bug got fixed (the pref defaulted ON but permission was only ever requested on an off→on flip of an already-on switch, so a fresh browser profile was never prompted and every notification was silently dropped). Folded into the same series is a bbolt fix: WithBatch now takes Cache.mx before bbolt’s writer lock, closing a publish/fill deadlock where the two lock orders could cross.
Skywire: Quiescing the Visor, and the Proxy-as-App
3659 feat(visor): Suspend/Resume RPC lets an operator or the systray pause a node without privilege to control the service-manager unit — the privesc-free alternative to systemctl stop/start. Suspend tears down every network subsystem (transports, routing, apps, dmsg, autoconnect, AR/TPD registration, the reward heartbeat, and the dmsg/transport-bound RPC surfaces) while keeping the local CLI RPC listener alive and the process running: the node earns nothing, routes nothing, and drops off dmsg discovery but stays controllable on CLIAddr. Mechanically, NewVisor wraps the module ctx in a cancelable child (networkCtx/networkCancel); Suspend cancels it so ctx-honoring goroutines unwind before their listeners close, then walks the closeStack in reverse LIFO order running every closer except the kept local-RPC entries, and resets the one-shot readiness channels (stun/dmsgTracker reset their sync.Once). Resume re-runs the module graph to bring it back online. It’s local-only by design — a suspended visor is unreachable over dmsg, so the hypervisor-proxied API returns an explanatory error. 3661 feat(wasm-visor): model skysocks-client-lite as a configurable app is Phase 1 of turning the wasm-visor’s clearnet proxy into an app-like object: a proxy-instance registry with a built-in default instance named skysocks-client (matching the native launcher app), an auto-random exit selector that picks a public proxy exit from service discovery over dmsg after transports come up (so the iframe browser and skycoin-web wallet clearnet paths work with no hand-entered exit), and a surfacing in the shared Apps tab via selfAppStates() in the native AppState shape with live status and start/stop riding the existing self-app control path. New JS hooks proxyInstances() and setProxyExit(id, pkHex) back the forthcoming ⚙ picker.
Skywire: Broadcast Channels and the First Phone
skychat’s channels became a first-class broadcast medium — an immutable kind, admin-only topology, chunked backfill, request-only files, and an opt-in discovery catalog — alongside a one-start-menu redesign with skychat:// addresses and QR share/scan. Delete-for-everyone (3668) now works on DMs, carrying wire ids through history so the store is pruned on both ends, with matching CXO/pair retractions. And the first cut of a native Android app landed: a skywire-mobile lite multicall core with a semver-safe buildinfo stamp, an app skeleton (theme, splash, bottom bar, apps hub), Skycoin brand assets, and an on-device core service with a Connect flow and log viewer — the scaffold the per-app phone screens will hang off.