跳到正文

MeshChatX 将于 2027 年 1 月 1 日结束生命周期。

更新日志

来自 MeshChatX 仓库的版本说明,最新在前。未发布内容会留在顶部,直到正式发货。

v5.0.0

2026-12-12 [unreleased]

Changed

  • CI: cut GitHub Actions down to what a fork needs. Pushes and PRs to master run CI (lint, tests, localization, Linux build check, E2E) and the Security workflow. Every other workflow is manual (workflow_dispatch) or tag driven: Android build, Native build, Build Linux packages, Docker (tags only), Benchmarks, Mutation testing, QA endurance.
  • CI: removed the issue housekeeping bots (issue triage, issue commands, commit references, stale sweeps), the daily Testing release chain (nightly.yml), the daily Android emulator smoke run, and the scheduled DAST, SBOM re-scan, Rekor monitor, and OpenSSF Scorecard jobs. Tagging nightly-* still produces a Testing prerelease through build-release.yml.

Breaking

  • Forks that relied on the removed scheduled workflows need to dispatch runs manually or keep the files from the 4.10.x tree.
  • Android versionCode for 5.0.0 computes as 50000, below the 50007 shipped with 4.10.7. Devices on 4.10.7 cannot upgrade to the 5.0.0 APK in place; uninstall first or bump versionCode before shipping.

v4.10.7

2026-10-11 [released]

Added

  • Traffic tools: sustained high traffic now raises a warning in the log and on the Traffic page, naming the component (LXMF, RRC, NomadNet, crawler) or the wire total when the load is announces, path requests, or forwarding. Warnings are rate limited so a flood cannot flood the log.
  • Traffic tools: active transfers now show with progress, covering RNCP file transfers and file sync. Their bytes are metered as they move, so the component table names them instead of hiding them in the residual row.
  • Traffic tools: hovering the chart shows the upload and download rates at that point in time and which component was driving them. Both series now share one vertical scale so their heights compare honestly.

Fixed

  • Relay chat: delivered messages no longer show "not delivered to hub" after a restart or reconnect. Echo confirmations are now persisted, a late echo restores a failed row instead of counting it as a repeat, and the client asks the hub which sends it already relayed on rejoin (T_SYNC/T_SEEN) so delivered rows heal without resending.
  • Relay chat: messages sent to a room the client has not joined are now confirmed by a direct hub receipt instead of always expiring as failed.
  • Profile: clearing the display name no longer claims an auto-save and then brings the old name back. The field keeps what you are typing, an empty name sent by any client resets to the default name, and leaving the field empty restores the saved name with a notice.
  • Relay chat: the available rooms section remembers whether it was collapsed or expanded when you navigate away and come back. Mount time auto-selection no longer overwrites the saved layout before it is restored.
  • Relay chat: unreachable hubs no longer spray mesh wide path requests while the reconnect backoff grows. Each hub gets a small burst for the first attempt, then at most one request per minute.
  • Settings: the auto announce interval and the propagation auto sync interval now clamp to a one minute minimum instead of accepting sub-minute values that turn announces and mailbox syncs into mesh wide storms.
  • Relay chat: the log now names the hub when a send budget blocks, warns when a message keeps getting redelivered, and reports how many held messages a rejoin resent.
  • Messages: the log warns when a destination has exhausted its auto-resend budget and messages need a manual retry.
  • File sync: the announce interval setter now enforces the same one minute floor the loader and the UI already use, so an API request can no longer set a sub-minute announce storm.
  • Traffic tools: the hover guide and dots on the activity chart are round again on wide windows. They now render outside the stretched chart area instead of being squished by it.
  • Remote execute: listener sessions now surface their address even when quiet mode suppresses rnx log output. The manager derives the rnx.execute destination hash from the identity file instead of waiting for a log line that may never come.
  • Tools: rnsh and rnx are out of alpha. Both are covered by live network tests that run a real listener and initiator over a loopback Reticulum link, including the frozen app launcher path.
  • NomadNet browser: multi-line form fields on pages now accept and submit line breaks again. The bundled micron WASM renderer ignored the field row count and drew a single-line box, so posts submitted through forms arrived as one long line. Lines carrying multi-row fields now render through the JS parser on any WASM version, and the bundled renderer is updated to v1.3.0 which draws the textbox correctly on its own.

v4.10.6

2026-10-09 [released]

Fixed

  • Nomad and Micron pages: restored the bundled Roboto Mono Nerd Font, so Nerd Font glyphs and icons on pages such as rngit render again instead of showing as boxes.
  • Settings: Roboto Mono Nerd Font is selectable as the UI font again.
  • CI: the wheel packaging check no longer treats the bundled Nerd Font as a leftover, so Android and Linux release builds pass with it.
  • Packages: RPM and Alpine APK assets build again. The Linux release job installs Go and TinyGo for the full builds those formats run, and the release log now lists the final asset set so a skipped format cannot pass unnoticed.

v4.10.5

2026-10-09 [released]

Fixed

  • Flatpak CDN: the release purge now reaches the Bunny API under the CI egress allowlist, and an unreachable purge no longer fails the whole release.
  • CI: the Flatpak OSTree publish is best effort in the release pipeline. A CDN failure no longer fails the release run, since the GitHub, PyPI, and rngit assets are already complete.

Notes

  • v4.10.4 is skipped due to a CI/CD issue with the Flatpak CDN purge and tags are immutable.

v4.10.4

2026-10-09 [released]

Changed

  • Python and desktop packages no longer ship old Vite chunks, the Nerd Font, or the browser Tailwind script. Release builds start from a clean frontend tree.
  • Standalone docs pages use a small static stylesheet instead of the old browser Tailwind file.
  • Nomad and Micron monospace UI uses JetBrains Mono / system fonts. An existing Roboto Mono Nerd Font setting switches over automatically.
  • Map and network visualiser WASM builds require TinyGo.
  • Relay chat: redelivered copies now show as an x2, x3 badge on the original message instead of folding in silently. Counts survive restarts and hub reconnects, and a relay preference turns the badge off.

Fixed

  • Relay chat: restarting the app no longer reposts your recent messages to every configured hub and room. Rows saved while a send was still unconfirmed loaded back as failed and were retried on rejoin, so delivered messages arrived again at staggered times. Only sends that fail in the current session auto-retry now, and replayed text that looks like a hub command is skipped.
  • Relay chat: retried messages reuse the original envelope id and timestamp instead of minting new ones, so receivers that dedupe by id collapse a retry with the original copy.
  • Messages: resending a failed message now keeps its original title and timestamp, so a resend that rebuilds the same payload carries the same message hash and dedupes at receivers and propagation nodes instead of arriving as a copy stamped at retry time.
  • Relay chat: the compose box floats over the room history with a light frosted look, same idea as the conversation composer. The scroll-to-bottom control sits above it.
  • Relay chat: Enter or Send while a message is already going out is ignored, so one press cannot post the same line twice.
  • Flatpak install from the MeshChatX remote failed after the last release. The CDN index is kept in sync with the package files again so flatpak install can pull the app.

v4.10.3

2026-10-08 [released]

Changed

  • CI now targets master only. The dev branch is retired, so every master commit still publishes the rolling :dev Docker image while :latest moves only on release tags and manual runs. The Android Gradle egress allowlist now includes jitpack.io.

Fixed

  • I2P / SIGPIPE: losing the I2P router (or any broken peer socket) no longer kills the Linux backend. The process now ignores SIGPIPE at startup so keepalive writes that return EPIPE reach Reticulum exception handlers and can reconnect when SAM returns. Electron also retains the signal name when a child exits with a null code, so signal-terminated backends are reported instead of silently dropped (fixes #137).
  • NomadNet crash-tab: Electron no longer shows dual vertical scrollbars on micron-parser-js pages. The crash-tab document had overflow-x: hidden on both html and body with height: 100%, which computes overflow-y: auto on both and nests two scrollports. html is now non-scrolling and body is the sole vertical scroller.
  • Relay chat: the channel list (/list) now populates on hubs whose reply exceeds the link MDU. rrcd splits oversized notices into one NOTICE envelope per line, so the header was parsed as an empty list and the room lines landed as stray system notices. The client now reassembles the burst into one notice before parsing.
  • Messages: the conversation composer now floats over the message list instead of holding a normal-flow band at the bottom, so it no longer shows as a background strip while scrolling up. The scroll-to-bottom button floats above it, and the pill uses a subtle backdrop blur. The empty pane in split view no longer shows the drop-hint overlay over the compose input.
  • CI: harden-runner now enforces egress blocking on Linux runners with an explicit allowed-endpoints list built from observed run traffic.

v4.10.2

2026-10-07 [released]

Fixed

  • Contacts: the empty-state box no longer sits flush against the search bar separator.

  • Themes: toggle switches now have a border outline in the off state so they stay visible on near-black themes.

  • Windows: shared-instance RPC replies are read through the connection dispatch path instead of os.read, fixing bad file descriptor errors that broke LXMF delivery, interface stats, and interface management on Windows builds when connected to an external rnsd.

  • Messaging: outbound sends that fail the path wait are now persisted as failed messages instead of being dropped, so they resend automatically when the peer announces and can be resent manually. The propagation-node fallback now defaults on for new configurations and is a safe no-op when no propagation node is set.

  • Maps: user-configured tile servers, Nominatim endpoints, and extra CSP sources are no longer stripped from the Content Security Policy in privacy mode. Only the shipped public defaults stay gated. Scheme-less and subdomain-template ({s}, {r}) tile URLs are normalized so OpenLayers and the CSP match real hosts.

  • NomadNet: page translation now works with the sandboxed page renderer. The shell asks the renderer frame for the page text over the frame channel instead of reading an empty shell container, so the translate button opens the translator with real page content.

  • NomadNet: the page shell no longer can show its own scrollbar next to the rendered frame's scrollbar. The container only scrolls for state banners now, which removes the second slider on platforms with always-on scrollbars.

  • NomadNet: serving nodes no longer re-hash the whole source image on every /media request. The hash is memoized by file stat. Failed conversions are marked and skipped for an hour instead of respawning the encoder on every request, which removes the sustained CPU spikes on image-heavy pages.

  • Desktop: every popout window (call, nomad tab, translator, paper message, and the rest) now runs in its own renderer process where the API allows it, so a crash there cannot take the whole app down. A crashed child window is destroyed instead of lingering as a dead frame. The main window's crash recovery dialog is unchanged.

  • NomadNet: rendered pages that use the full-bleed shell (.html, .md, .txt) no longer overflow the viewer by the header and toolbar height. The content container carried a stray min-h-full that beat its min-h-0, so the rendered frame ran ~54px past the bottom of the pane and its scrollbar hung below the page boundary, reading as the extra-slider artifact.

  • Network visualiser: leaving the page during engine setup no longer orphans a live graph instance per visit. The init chain now checks for unmount after every await and destroys anything created in flight.

  • Network visualiser: the loading overlay now has a cancel button. Stopping aborts in-flight fetches, halts physics and batch work, and leaves a fresh load one click away on the refresh button.

  • Telemetry: tracked peers that never answer are no longer polled every interval forever. Each unanswered request doubles the wait up to a daily cap, and after a bounded number of misses the peer is skipped until a telemetry response re-arms it or tracking is re-toggled.

  • Messaging: automatic resends no longer retry feature-only deliveries (commands, reactions, telemetry) to peers that never sent one back, so Sideband and other LXMF clients are not spammed with payload types they cannot parse.

  • Contacts: shared address output now uses the bare 32-hex hash when no public key is known, since not every LXMF client parses an lxmf:// URI. lxma:// output is unchanged when a public key is available.

  • Android: Bluetooth RNode interfaces (BLE and classic) added or imported through the UI now write the Android-native Reticulum keys at save time, so the live attach takes the Bluetooth path instead of treating a bt:// or ble:// port as a dead USB serial device. Leftover Bluetooth keys are also stripped when an interface is switched back to serial or TCP, since the Android implementation lets stale ble_* settings override the port.

  • Android: the RNode form now asks for Bluetooth permission when the BLE or Bluetooth transport is enabled, instead of relying on the bundled startup prompt that can be dismissed and never re-shown.

  • Android (build): the bundled Reticulum wheel now guards paired-device enumeration against devices with no name and denied Bluetooth permissions, so one bad bonding record or a missing grant no longer kills RNode Bluetooth bring-up.

  • Relay chat: the hide join/part preference now applies. A setup-time read shadowed the setting's lookup, so presence lines kept rendering and the toggle wrote the old value back to storage.

  • Tools: the traffic page refresh button no longer flashes on every background poll. The spinner now only runs for manual refreshes.

  • Tools: the traffic page RRC hint now counts connected hubs by the real status constant instead of a string comparison that always read zero.

Added

  • Interfaces: bulk config imports now attach imported interfaces live instead of requiring a restart. The restart banner only appears when live attach could not bring one up.
  • Health: the monitor now tracks file descriptor counts, warning when usage nears the limit and when the count climbs steadily across the check window, catching slow descriptor leaks early.
  • Diagnostics: new Traffic tool shows live upload and download rates per interface and per component, per-peer byte attribution, announce and propagated splits, a rolling activity chart, and plain-language hints explaining what is driving the wire load.
  • Interfaces: I2P community presets can now be quick-added like other presets. They run through the normal add-interface checks, so transport mode and the single-I2P rule still apply.
  • Interfaces: the community presets list now has a filter dropdown for TCP, Backbone, I2P, Yggdrasil, and other types.
  • Messages: the message translate bar now has an auto-translate toggle per conversation. The chosen language pair is remembered per contact, and when auto-translate is on, new incoming messages are translated on arrival and the newest loaded messages are translated when the conversation opens.
  • Maps: the tile server URL now accepts TileJSON endpoints (URLs ending in .json). Tile templates, attribution, zoom range, bounds, relative tile URLs, and scheme:tms are honored, and a bad document falls back to the next provider like a failed tile server.
  • Translator: the pack catalog now shows both directions of a language pair as a single bidirectional entry, and downloading it installs every direction present in the catalog. A search field filters the catalog by language or pair code.
  • Updated Python deps: rns 1.5.7, lxst 0.5.4 floor.
  • Messages: inbound messages that arrive while a conversation is open now enter with a short fade-and-rise instead of appearing instantly. History, outbound messages, and reduced-motion settings are unaffected.
  • Tools: shared tool page headers are slimmer so tool content gets more vertical space.

v4.10.1

2026-10-04 [released]

Fixed

  • Messages: the mobile attachment popup lived outside the paperclip button's click-outside boundary, so taps inside the menu counted as outside clicks. Touch devices closed the menu before the tap could fire any item, and on desktop the add image accordion collapsed the menu instead of expanding. The popup now renders inside the bound element.

Added

  • Relay chat: the composer is now a multiline field that grows with content up to 160px. Enter sends, Shift+Enter inserts a newline, and Escape, ArrowUp message recall, and Tab nick completion behave as before.
  • Interfaces: discovered interface settings gained Max Stored Interfaces plus Mark Unknown After, Mark Stale After, and Delete After day fields, so retention of discovered interfaces is configurable instead of fixed at Reticulum defaults.
  • Interfaces: the discovery list can be exported as a zip of the Reticulum interface store and imported again on another install, supporting offline backup and sharing of discovered interfaces. Import validates entries and keeps old archives in the retention window instead of letting them expire immediately.
  • Interfaces: the discovered list is now paginated at 48 per page with selectable page sizes, so large discovery stores stay responsive.

v4.10.0

2026-10-03 [released]

Added

  • Interfaces: per-interface runtime attach, detach, and reload using the RNS 1.5.5 interface management API. New POST endpoints /reticulum/interfaces/attach, /detach, and /reload, plus a Restart Interface action on attached interfaces in the Interfaces page.
  • Interfaces: interface edits, enables, disables, deletions, and bitrate changes now apply live against the running Reticulum stack instead of always requiring a full RNS restart. The restart banner only appears when a live apply is refused.
  • Interfaces: discovery settings expose the RNS 1.5.5 autoconnect_unverified_implementations toggle, and discovered interface cards show the announcing implementation name and version.
  • CI: a backend test leg now runs the full suite on Python 3.15 (beta) in advisory mode.
  • Sidebar: configurable filter chips on the conversations and announces lists. The funnel menu exposes every filter, a pencil button opens an editor modal to show, hide, and reorder chips, chips can also be dragged directly in the bar, and the layout persists per browser. New filters include Favourites on conversations and Direct, Nearby, Pinned, and Blocked on announces.
  • Fonts: Inter, JetBrains Mono, IBM Plex Sans, and Space Grotesk are bundled and selectable in appearance settings with live preview.
  • Docs: relative image links in in-app documentation now render, and image and clip assets ship with the docs bundle. A declarative guide capture system (tests/ui/guide) produces annotated WebP screenshots and animated clips for documentation.
  • Deployment: Helm chart, Ansible role and playbook, Proxmox template build, a devcontainer, and a standalone Firecracker microVM start.sh.
  • Tooling: UI metrics tracking (test:ui:metrics) records perf, heap, and Lighthouse results per git revision and fails on regressions against a local baseline.
  • Screenshots: the UI screenshot catalog now captures a desktop/mobile x light/dark matrix as WebP at 2x device scale, and guide screenshots support reusable annotation overlays (boxes, spotlights, blurs, arrows, badges, captions) plus a CDP/ffmpeg recorder for animated clips.

Changed

  • Sync messages button is pinned to the top nav bar by default with a stacked mail and sync-state icon

  • Updated Python deps: rns 1.5.6, cryptography 50.0.2, wasmtime 49, miniaudio 1.71, numpy 2.5.3 on Python 3.12+, setuptools 84

  • Backend threads are named for attribution in py-spy and top -H

  • Python 3.15 readiness: event-loop policy calls replaced by get_running_loop(), and the atheris dev dependency is gated to interpreters that have wheels (up to 3.14 on Linux x86_64).

  • Config: update_config now applies a generic pass over every registered config field, so newly added settings can no longer be silently dropped when no explicit handler exists. get_config_dict serializes registered fields generically behind a denylist instead of a hardcoded key list.

  • UI: hardcoded color buttons migrated to semantic action tokens (primary, success, danger, warning) so they follow theme presets, and sidebar chrome uses the raised-surface token so panels separate from the canvas on flat themes.

  • Search inputs use a compact pill variant in sidebars and on settings, call, docs, and debug pages.

Fixed

  • Config: ui_font_family, ui_custom_font_name, nomad_* and other newer settings now round-trip through GET /config instead of silently reverting, and multi-megabyte custom font data serves on demand via /api/v1/app/custom-font.
  • Icons: LxmfUserIcon badges render their glyph again. Percentage padding was measured against the card width instead of the icon and collapsed the SVG to zero size, leaving empty colored circles on bot and contact rows.
  • Docs: ![alt](src) markdown no longer renders as a stray bang plus a link, unsafe image sources are dropped, and relative image paths resolve under /meshchatx-docs/ in-app.
  • UI: the Banished page screenshot path no longer exposes the skeleton grid. Loaded-state markers gate capture.

Security

  • Docs: _safe_href strips ASCII whitespace and control characters before scheme checks so payloads like java\tscript: can no longer reach the page as javascript: links or images, and URLs containing & are no longer double entity-encoded in rendered links.

v4.9.3

2026-09-30 [released]

Changed

  • Reticulum Network Stack updated to 1.5.5, which adds runtime interface attach/detach/reload management, publishes implementation name and version in discovery announcements, and fixes IFAC publishing edge cases.
  • Tutorial: the Back/Skip/Next row is fused back into the bottom of the step content in both modal and page modes instead of a separate pinned footer bar.
  • Visualiser: the radial view is replaced by a cluster view that groups peers into lobes around the interface that announced them, so the layout shows where traffic actually arrives from. Stored radial preferences upgrade to cluster.

Fixed

  • Visualiser: WebGL live layout settles again. Tick damping and the per-step speed cap were tuned so nodes spread out and come to rest instead of ringing at equilibrium.
  • Map: dragging the settings window stays smooth now (backdrop blur is disabled for the drag) and the coordinates readout no longer re-renders the whole page on every pointer event.
  • NomadNet: image size hints now follow upstream semantics. A bare w=/h= number is a character cell count (columns and rows) instead of pixels, w=n renders at the image's native size, and percent values stay relative to the page as upstream intends. Matches markqvist's NomadNet ImageWidget behavior.
  • NomadNet: the w=/h= spec now sizes the loaded image itself instead of only the invisible placeholder box, so w=50 renders 50 columns wide as in upstream NomadNet.
  • NomadNet: image alignment is applied. a=c centers and a=r right-aligns whole-line images, and whole-line images default to centered like the upstream ImageWidget.
  • NomadNet: a loaded image drops the placeholder border and padding and shows the bare image, and the reserved placeholder space is released so pages no longer keep dead space at the bottom.
  • NomadNet: the page shell no longer keeps its own scrollbar next to the rendered frame's scrollbar. The container was always scrollable by its own padding and showed a second slider on platforms with always-on scrollbars.
  • Issue triage: the welcome comment no longer asks reporters for logs or a screenshot when the issue body already contains an attachment or a filled-in logs section.

Security

  • Plugin install and preview paths now jail wasm backend entries under the plugin tree, bound extracted zip content by real bytes and file count instead of declared size, reject symlink members, and keep fallback wasm stubs out of the integrity-checked tree. A failed enable() rolls back to a clean disabled state.
  • RRC hub sessions that never send HELLO are reaped on a timeout so a peer cannot pin session slots, stale link-close callbacks no longer clear a live session's state, and a room-less forced-leave error preserves local room history instead of wiping it.
  • Reticulum config snapshots returned by the API redact secrets before serving, matching the redaction used by the live config view.
  • File sync incoming-file tracking expires and clears on link close so a dead link cannot pin a path against re-request forever.
  • Docs archive import streams members with a real-byte cap and skips symlink entries before anything reaches disk.

Fixed

  • Forwarding: replies sent to a forwarding alias now route back to the original sender. The mapping was keyed by the alias identity hash while replies arrive addressed to the alias destination hash, so the reply path never matched.
  • Messages: auto-resend no longer reclaims a malformed message forever. The attempt budget is spent before attachment parsing, so a bad base64 field cannot loop a resend.
  • Messages: local retention purge keys on when the row was written locally instead of the sender-controlled LXMF timestamp, so a peer can no longer age a message out instantly or keep it forever.
  • Telephone: an outgoing call that stalls mid-dial now times out and hangs up instead of leaving the phone stuck in Calling until teardown, and an incoming ring during a pending outbound call is surfaced instead of silently dropped.
  • Voicemail: the auto-answer timer is bound to the link instance it was scheduled for, the blocklist fails closed when lookup errors, and greeting recording writes to a temp file that only replaces the live greeting on success.
  • Web audio bridge reattaches after an LXST pipeline reconfiguration (answer, profile switch, loudspeaker toggle) instead of leaving mic and speaker paths silently disconnected, and per-client sends coalesce so one slow socket no longer stalls the whole 60fps feed.
  • Geo WASM: MGRS formatting for single-digit zones is correct, OLC shorten/recover no longer panics on edge-case codes, and NaN or non-finite coordinates cannot crash the runtime.
  • Visualiser WASM: the WebGL pick radius holds a screen-space minimum so clicks still land at high zoom, scene handlers validate argument types instead of calling Float on non-numbers, coincident layout nodes get a deterministic separation nudge, and duplicate edges are deduplicated.
  • Relay chat: sent messages now show a gray "sending" hint until the hub relays the message back. If the echo never arrives, or the link drops first, the line switches to "not delivered to hub" with a retry action that resends under a new envelope id.
  • Relay chat: message and sidebar context menus show icons, and the message menu gains "Message user" (opens a direct conversation through the peer's LXMF address) and "Copy user hash".
  • Context menus across the app use a pointer cursor on interactive items.
  • NomadNet: a live announce no longer re-sorts its row to the top of the announces list, so clicking a node no longer makes the sidebar reshuffle under the cursor.
  • Relay chat: a "new messages" divider marks where unread history starts when a room opens, pressing ArrowUp on an empty composer recalls your last sent message, a byte counter appears near the hub's message limit, failed sends retry once automatically on rejoin, and hubs show their measured link latency next to the status line.
  • Context menus support Escape to close and arrow-key navigation between items.
  • RRC: hub reconnect after a restart no longer stalls on a dead link. The connect worker now waits for both the identity and a live path before creating the link, since a link request sent with no known path is dropped silently and only the establishment timeout would recover it. Path requests retry inside the connect window instead of a single shot.
  • RRC: a link stuck PENDING on a dead path no longer parks the hub in CONNECTING for minutes. An establishment watchdog tears it down after a bounded window and lets reconnect backoff retry instead.
  • Live e2e now covers backend restarts (warm storage reconnect within an SLA), link flaps, and network partitions through a control-file chaos proxy between the peer and the backend.
  • Nightly QA workflow runs a soak harness that samples threads, fds, RSS, path table and interface byte rates under a steady mesh workload and fails on any rising leak slope, plus opt-in live backend tests and mutation runs on the highest-risk modules.
  • Backend tests gained a Hypothesis state machine for the RRC hub connect lifecycle and property-based codec/parser tests for the wire format.
  • Shared-instance RNS calls can no longer wedge the web server. The shared RPC recv had no deadline, so a stalled rnsd blocked whatever thread asked, including the aiohttp loop serving /interface-stats, /path-table, path probe, blocklist and telephone endpoints. Route handlers now run those calls in a thread when a shared instance is in use, and every shared-instance RPC recv carries a 10s deadline.
  • RNode Flasher: the page 500s in dev mode because Vite refuses dynamic imports of public assets, and the fallback code imported vendor modules without any SRI check. Vendor bundles now load through a hash-verified blob URL path shared by all flasher scripts, and the dead web-serial polyfill reference (the file was never shipped) is removed.
  • RRC: reconnect backoff can no longer overflow after days of retries (the exponent is capped), a dead link reporting close twice no longer double-counts the backoff, and a connect that crosses a manual disconnect no longer installs its link anyway. A connect attempt epoch now drops links from superseded workers.
  • RRC: hub error text can no longer delete room history files. Only explicit remove/clear actions erase the archive. History files now compact to the retention window instead of growing without bound.
  • RRC: pending-delivery echoes age out on message reads, not only on inbound packets. Active-room tracking normalizes room names so mixed-case paths cannot split unread state or history.
  • Removed dead code: the bot_propagation re-export shim, the never-wired lifecycle deferred_network module, two unused frontend API wrappers, the unused FormSubLabel component, a dead RRC ping path (send_ping was never called, so pong tracking and its bookkeeping were inert), the trusted-publisher OO store cluster, unused config fields, and roughly two dozen unreachable DAO, plugin, sandbox, and diagnostics helpers.
  • Map coordinate format now persists: the PATCH handler and config export were wired (the setting was previously silently dropped).
  • API hardening from OpenAPI contract fuzzing: malformed JSON bodies (arrays or nulls in string fields) no longer 500 on the RRC join-room, hub-create, room key/topic, archive, and LXMF send endpoints. They now fail 400 with a named reason. The lxmf send error response also gained the standard error/code/message fields.
  • RRC room names containing lone-surrogate unicode are rejected at normalize time instead of exploding mid-encode inside CBOR.
  • RNPath tool: a remote query no longer fires without a management identity (it returned a guaranteed 400 on every poll and spammed the console), and the search box is clamped so oversized input cannot overflow the HTTP request line.
  • Testing: an OpenAPI spec now ships under docs/openapi.yaml covering 79 API operations, exercised live by Schemathesis against a loopback backend in CI, plus an atheris coverage-guided fuzz suite over the RRC envelope codec and parsers, golden CBOR wire captures pinned against decode/round-trip drift, deterministic replay of recorded RRC sessions, a SIGKILL crash-consistency suite that kills the backend mid-write and asserts storage integrity on restart, a shared-instance e2e that runs the backend against a real rnsd, and scripted exploratory personas (impatient, adversarial input, data-heavy seeding) over all routes.
  • Testing: axe-core accessibility audit runs across all routes with a baseline ratchet so new serious or critical violations fail while documented debt is tracked to zero. Visual regression screenshots for key routes can be generated per environment through a dedicated Playwright config. A diff-cover gate requires 80% coverage on changed backend lines in pull requests.
  • RRC sessions can now record every routed envelope to JSONL for deterministic replay in tests.
  • Health monitor now samples Linux IO stall pressure (/proc/pressure/io avg10). Sustained high IO pressure warns in the log and over the websocket, throttles the background ratchet persist worker until pressure clears, and reports recovery.
  • Shared-instance RPC: the deadline now also covers the authkey handshake (a stalled rnsd rpc_loop could park the connect path forever) and the whole frame read, not just the first byte. Every remaining route handler that still called a blocking shared-instance RPC on the event loop now offloads it, including rnstatus/rnpath/rnprobe/trace tools, path probe, blocklist writes, telephone dialing, and the reticulum hot-reload shutdown sequence. RPC timeouts surface as 503 instead of a generic 500, and a deadlined packet-metric lookup can no longer skip the message state update it precedes.
  • Command palette: typing with peers or contacts lacking names no longer crashes filtering, and pressing arrows on a query with no matches crashed the render path because the highlight move indexed an empty result set, and null entries in peer or contact lists could throw while building results.
  • IO pressure handling hardens: the gate releases when PSI disappears mid-run or the monitor stops, ratchet writes held during pressure flush synchronously at shutdown, and a dropped re-queue signal can no longer orphan pending ratchet entries.
  • Live e2e covers RRC room history across a backend restart and before_seq pagination, plus an exploratory interaction crawl (buttons, menus, command palette) on every route and an adb-based mobile UI crawl when a device is attached.
  • E2E hardening: the live peer no longer self-terminates at 10 minutes (soak runs were measuring a dead mesh), storm destinations now announce the real rrc.hub aspect, the soak slope oracle uses the correct slope standard error and fails on too few samples or peer death, the stack script frees ports with escalation and kills Vite on cleanup, and fault-injection specs restore chaos modes in finally blocks so one failure cannot poison the suite.
  • RNode flasher: the dead load-polyfill action and a standalone-page import of a file that was never shipped are removed, SRI verification failures log loudly instead of being swallowed, vendor loading deduplicates concurrent runs, and insecure contexts surface a clear error instead of silent crypto.subtle failures.
  • Live e2e now covers an adversarial announce storm (40 synthetic hub announces), a second live peer for cross-peer isolation checks, and an exploratory route crawl that fails on console errors or blank pages. The e2e stack also frees its ports from stale leftovers before starting.
  • Live e2e now also asserts interface byte counters stay bounded during idle announce windows and real chat flows, catching unbounded announce or path-request churn before it becomes a traffic bill.
  • Telephony: dialing by identity hash no longer resolves a wrong identity. The announce fallback rebuilt the peer identity from its public key through a private-key API, producing a constant wrong hash, so calls by identity hash could never find a path.
  • Backend: fixed a self-deadlock in AsyncUtils.run_async where a done-callback could fire inline while the futures lock was held, freezing the main event loop and wedging the server under sustained announce traffic. A live Playwright mesh suite now covers real LXMF delivery and relay-hub connect, join, and echo over a local TCP link.
  • LXMF: a transient blocklist or contact lookup error can no longer bounce inbound long messages with a false REJECTED state. The pre-transfer policy only rejects on a positive verdict, matching the 4.8.8 unknown-peer rule.
  • Health monitor now probes the main event loop and dumps all thread stacks to the log if the loop stops answering, and SIGUSR1 dumps stacks on demand, so silent server wedges self-diagnose.
  • Relay chat: auto-connect now waits for the first interface to come online before sending path requests, so hubs connect quickly after a restart instead of burning a full path-request window.
  • Contacts context menu dismisses consistently on Escape, and the announce sidebar no longer corrupts first-seen timestamps or custom names from slim live announce payloads. Announce list pagination no longer skips server rows when live nodes arrive mid-browse.
  • Sandbox: seccomp denylist now prefers the seccompy backend (pure Python, no libseccomp needed) and falls back to libseccomp via ctypes when it is absent.
  • Crawler: re-queued tasks keep their retry budget instead of resetting to zero, in-flight tasks count toward the per-node page cap, a successful crawl clears the previous skip reason, and a node's own page destinations are never queued for self-crawl.
  • Page nodes write pages and hosted files atomically and open served files with O_NOFOLLOW so a swapped symlink cannot redirect a served path after the jail check.
  • Media conversion holds a process-wide lock around the temporary-directory and environment window so concurrent conversions cannot clobber each other's temp settings.

v4.9.2

2026-09-28 [released]

Added

  • Network visualiser gains a radial view mode that pins nodes on deterministic hop rings around the local node, plus screen-space label decluttering so dense zoomed views stay readable.
  • Settings self-test gains a CBOR roundtrip check that exercises the RRC codec's encode, decode, and stream replay paths.
  • CI performance suite measures cold-load FCP, LCP, SPA route transition latency, API round-trip, and post-mount heap per page with per-page budgets, plus a heap-growth spec that fails when a page leaks listeners, timers, or nodes across mount/unmount cycles.
  • NomadNet: local page nodes rescan their pages and files directories on a timer and on each incoming link, so content dropped in externally is served without a restart and removed content stops answering. Matches upstream NomadNet's page_refresh_interval behavior.
  • Backend fault-injection suite covers malformed RRC wire traffic, reconnect timer storms, hub store corruption, page-node rescan failures, crawler garbage inputs, link-cache teardown errors, and callback exceptions.

Security

  • HTTP-set command_plugins_path is jailed under storage so remote callers cannot point the plugin loader at arbitrary Python files.
  • Reticulum rpc_key and interface secrets are served only to loopback or authenticated callers, and a redacted GET round-trips through PUT via a sentinel so non-privileged edits cannot erase real values.
  • auth_session_epoch is stamped into session cookies and checked on HTTP middleware and the WebSocket upgrade, so password, auth-toggle, and setup changes revoke outstanding sessions. Session secret files are restricted to 0600.
  • Translation-pack extraction is bounded by per-member, total, and file-count caps with streamed copies.
  • /api responses send Cache-Control: no-store, the static auth bypass is scoped to actual asset suffixes, and remote markup rendered into the main document loses id/name attributes so archived pages cannot clobber window globals.

Fixed

  • Messages: opening a raw outbound message no longer 404s once it leaves router memory (delivered or post-restart). The paper URI endpoint rebuilds the signed lxm:// URI from the stored row, preserves the original timestamp so the message hash and ingest deduping hold, verifies the repacked hash against the stored row, and skips the request entirely for inbound messages, which can never be signed by the local sender.
  • Messages: resend preserves title, reply quotes, reactions, app extensions, and correctly decomposed telemetry, and other sessions are told when the old failed row is deleted.
  • Messages: cancel validates the hash, reaches forwarding-alias routers, and reconciles rows stuck in generating/outbound/sending to cancelled after a restart instead of returning ok while nothing changed.
  • Messages: startup recovery fails sent+direct rows that could never advance after restart, and a same-hash inbound delivery can no longer demote an outbound row to incoming or repoint its peer.
  • Messages: hash lookups normalize case across message, attachment, spam, and announce paths.
  • Messages: queued sends snapshot peer and composer state so a cleared composer or peer switch cannot send into the wrong conversation, pending placeholders dedupe correctly, inbound duplicates are dropped, and audio decode is serialized with contexts closed on unmount.
  • Messages: paper URI generation and URI ingest over WebSocket were fire-and-forget. A failed send or dropped reply left buttons disabled or a spinner forever. Sends now check the result, toast on failure, and bound the wait with a timeout.
  • Android: bridge-backed buttons (WiFi Aware grant, nearby permission, native RNode flasher) were silently dead because the injected bridge sat inside a Vue reactive proxy. The bridge now marks itself non-reactive and binds methods to the raw instance.
  • Android: release APKs kept stripping org.meshchatx.locallink.* because the classes are only reached via jclass() from Python, so WiFi Aware stayed disabled in minified builds. R8 keeps them and a dex check gates the build.
  • Android: the native RNode flasher no longer shows a duplicate title hidden behind the status bar.
  • Reticulum: a second AutoInterface bind failure left a zombie singleton that killed every in-process restart. Recovery now releases interface sockets and resets singleton state before rebinding, a colliding interface is rejected at add time, and a guidance notice reports auto-disabled interfaces.
  • NomadNet: image loading now parses the NomadNet 1.4 whole-line image syntax (paren links with w/h/a/s/k/profile fields and percent widths) and sends the key field the /media protocol requires. Verified live against rns.recipes.
  • NomadNet: page and file download events are correlated by request id so stale transfers cannot feed replacement entries, and archive navigation performs full teardown.
  • NomadNet: requesting an unknown page or file from a local node opened a doomed link to the node itself. Local serving now resolves only canonical /page, /file, and /media paths, and the download handlers fail fast instead, reporting archive availability for pages.
  • Relay chat: prefs and drafts are scoped per identity, so hide-join/part and ignore state apply once the identity hash resolves, hub switches reset room state, the composer clears before send to end double-send and lost-text races, and debounced config writes survive identity switches.
  • Relay chat: hub auto-reconnect retried on a 60 s backoff cap forever, so a large set of unreachable hubs produced a constant stream of link and path requests. Backoff now doubles per failure up to a 15 minute ceiling with jitter, startup connects are staggered, path requests dedupe per hub, and a fresh hub announce resets the backoff so a recovering hub reconnects quickly. Manual connects reset the counter for an immediate retry.
  • Service worker: subframe requests are excluded from the shell navigation strategy and only shell documents or extensionless SPA routes use the fallback slot, fixing the stale-cache stuck Loading page. Updates reload exactly once, WebTransport falls back to WebSocket on session death, and a version-mismatched backend triggers a reload after reconnect.
  • Backend: websocket_broadcast from foreign loops forwards onto the owning client's loop, the self-test endpoint runs off the event loop so its own probes cannot deadlock, and the Windows AppContainer probe wait is bounded.
  • Backend: deleting an identity tears down its live context first, telemetry per destination is capped, and in-flight propagation-node tasks are cancelled on shutdown.
  • Electron: protocol links are stripped from backend argv and delivered to the renderer after load, certificate-error bypass is scoped to the local backend, DevTools shortcuts and hardware permissions are gated, and the Electron runtime version invalidates stale caches on upgrade.
  • UI: context menus dismiss on click-off and re-right-click again, popup carets stay off rounded corners, and the map drawing toolbar stays pinned at the top on xl screens.
  • UI: stuck-state follow-through in MiniChat (send failures now toast and fresh timestamps render), ContactsPage lxma import, NomadNet archives spinner, and the archived-pages flush result toast.
  • CI: alpine APKs are built without fpm and verified by actually installing them in an apk-tools container.
  • Locale files gained the missing aware/nearby permission strings in all 15 locales.
  • Backend: a wedged SQLite pool could leave every API call answering 503 until a manual restart. When WAL or SHM files get unlinked under open connections, every statement fails with a disk I/O error and per-connection retries never recover. The provider now spots failures that persist on fresh connections, resets the whole connection pool at once, and if that is not enough restarts the process. Restart attempts are bounded, so a permanently broken store keeps serving retryable 503s instead of crash-looping.
  • Relay chat: a malformed hub or client envelope could raise inside a packet handler and break session processing. Client and server dispatch now contain handler errors per packet, and the announce-reset and path-request rate limits use never-fired sentinels so the first retry is not skipped on hosts with low uptime.
  • HTTP: the safe file response raises a real error instead of asserting when aiohttp hands back no writer.
  • CI: Docker builds install pnpm 12 through npm since the pinned node image's corepack cannot shim its native binary, and the dev container does the same.
  • CI: the shared Node setup keys its corepack cache by runner architecture, so an arm64 pnpm binary can no longer be restored onto the x64 macOS build runner.

Changed

  • The Landlock sandbox now uses landlockpy instead of the custom ctypes plumbing. The enforced filesystem policy is unchanged and Android is unaffected.
  • Relay chat's CBOR codec moved from cbor2 to cborx, a zero-dependency RFC 8949 implementation with an optional compiled fast path. Canonical wire encoding is unchanged, decode rejects trailing bytes instead of ignoring them, and Android packages cborx through a Chaquopy recipe instead of the cbor2 wheel.
  • Visualiser layout spacing widened to match node size, WASM and JS paths gained LOD color, NaN guard, edge-filtering, and dead-scene fallback parity, and visualiser.wasm was rebuilt.
  • UI lighthouse, performance, and heap suites run against the production bundle instead of the Vite dev server, and service workers are unregistered before audits so scores cannot be nulled by a controlled navigation.
  • Backend: the eight per-identity periodic loops (auto-announce, propagation sync, crawler, auto-backup, telemetry, retention, flood cooldown, auto propagation selection) now share one background event loop instead of one thread each. Backups, retention sweeps, and announce table reads run in worker threads so they cannot stall the shared loop. This cuts several threads and thread-local database connections per identity.
  • Backend: debug log writes to SQLite are batched into one transaction per flush, the retention sweep runs at most every ten minutes instead of every five seconds, and MESHCHAT_LOG_DB=0 disables database logging entirely. On SD-card installs this removes a steady stream of small writes.
  • Backend: after identity setup finishes, startup objects are frozen out of the cyclic garbage collector's scan set, and the periodic cleanup calls malloc_trim so freed memory returns to the OS instead of sitting in allocator arenas.
  • Docker images and the Raspberry Pi installer set MALLOC_ARENA_MAX=2 and OPENBLAS_NUM_THREADS=1, and the Pi guide documents both plus MESHCHAT_LOG_DB=0 for low-memory and SD-card deployments.
  • Backend: the LXST telephony stack, numpy, and its audio backends no longer load at startup. The web audio bridge sits behind a lazy proxy that constructs it on first call, and telephone and voicemail resolve their LXST symbols on demand. Text-only installs keep tens of MB of memory and the OpenBLAS worker threads out of the process. If the audio stack fails to initialize or a bridge call keeps raising, the proxy disables audio for the session instead of letting errors reach the messaging paths.

v4.9.1

2026-09-21 [released]

Added

  • Android Nearby: hotspot, WiFi Direct, WiFi Aware, and NFC tap link nearby phones with no router. Peers join through Auto Interface or the bundled AwareInterface.
  • Android satellite readiness: the app declares constrained-data optimization so the OS can route mesh traffic on a satellite attach. Nearby shows satellite state and a low-bandwidth hint.
  • OIDC sign-on for the web UI (Authentik, Keycloak, Pocket ID, and compatible providers) via Authorization Code + PKCE. Set it under Settings, Authentication, or with MESHCHAT_OIDC_* variables. OIDC can require login without a local password.
  • UI heap-profile spec fails CI if a catalog page leaves listeners, timers, or heap above baseline after unmount.
  • Android GPS location source shares your position through the native LocationManager bridge, so location works even when the WebView geolocation API fails. Pick it under Settings, Location.
  • Top navigation bar is customizable. Pin, reorder, or remove section buttons under Settings, Appearance, and NomadNet joins the default set.
  • Reticulum config editor keeps versioned snapshots on every save and can restore an older config.
  • Contact shares carry the display name and LXMF icon (name plus colors), so the card renders like a real contact.
  • Toasts can carry an action button. Errors like a missing propagation node or disabled location now offer a Configure shortcut that jumps to the right settings section.
  • Screenshot tooling: task screenshots captures desktop and mobile views of every page with seeded demo data.

Security

  • Messages: user text could forge markdown placeholders and inject anchors or tokens into linkified URLs. Placeholders now use a per-render nonce, and linkified URLs stop at bracket tokens.
  • Map exchange: the KML sanitizer now matches namespaced, attribute, and CDATA href forms, and treats malformed remote URLs as unsafe.
  • Docs: uploaded Reticulum documentation HTML no longer shares the app origin. The docs iframe and /reticulum-docs/ responses are CSP-sandboxed.
  • Authentication: the public setup endpoint no longer sets a local password on an OIDC-only deployment.

Fixed

  • Windows desktop: the AppContainer child no longer dies at loader init (0xC0000142) when the LPAC token lacks window station access. A failed sandbox still falls back to an unsandboxed backend in auto mode.
  • Desktop: a crashed renderer now offers relaunch, relaunch without GPU acceleration, or quit instead of leaving a dead window. Crash minidumps are kept locally under the Crashpad folder, and GPU process deaths are logged.
  • Desktop: a crash-looping GPU process (which takes the window's renderer down with it) now triggers an automatic fallback, the app disables hardware acceleration once via the disable-gpu marker and relaunches.
  • Packaged builds: .gitkeep placeholders are no longer hashed into backend-manifest.json, so a dropped marker cannot block onboarding.
  • Relay chat: hosted hub announce intervals show hours and days, and accept values like "6h" or "1d".
  • Messages: reopening a conversation no longer shows a stale first page that misses messages sent while the pane was closed.
  • NomadNet: the path-finder menu hides on small screens, and the mobile sidebar gains a URL entry row.
  • Relay chat: Host, Bots, and Search tabs collapse into the overflow menu on narrow screens.
  • Android: a failed Chaquopy start or missing WebView no longer closes the app with no message. Startup and renderer crashes show a readable error.
  • NomadNet: closing a node opened from the announce list returns to that list instead of a new Favourites tab. The sidebar reopens on the last used tab.
  • NomadNet: leaving the browser and coming back no longer reloads the page. Scroll position and field contents survive.
  • Relay chat: backing out of a room opened from Search or Discovery returns to that view.
  • Reverse proxy: trusted proxies can pass X-Forwarded-Proto and X-Forwarded-Port, so TLS-terminating ingress no longer gets 403 on /ws upgrades.
  • Docker: the entrypoint prepends meshchatx when argv starts with a flag, so docker run image --flag and Kubernetes args no longer crash su-exec.
  • Landlock: parent directories of --ssl-cert and --ssl-key are granted as read roots, so TLS files outside the usual roots load.
  • Logging: the Python logger also writes to stdout, so request-handler tracebacks show up in docker logs and kubectl logs.
  • WebSocket: the cost limiter covers binary and invalid JSON frames, and idle timeout drops dead or flooding clients. The telephone audio socket uses the same gating.
  • Authentication: turning auth_enabled off no longer wipes the local password hash while OIDC still enforces login.
  • Settings: an empty OIDC client secret no longer overwrites the stored secret, and an invalid issuer URL is rejected before any keys persist.
  • Backend: concurrent reload_reticulum calls are serialized, and recovery clears a dead instance instead of reusing it.
  • Backend: transport-thread RNS callbacks wake asyncio safely, dropped coroutines are closed, and the memory log handler drops its database handle at identity teardown.
  • Voicemail: greeting text is passed to espeak on stdin, so a leading dash is not treated as a flag.
  • Startup: leftover ratchet files with non-hex names are swept before RNS init, so they do not retrigger the corrupted-ratchet path on every boot.
  • Map: telemetry, announce, and interface coordinates are range-checked before projection, and marker updates stop after the map is torn down.
  • Messages: conversation and relay timelines cap retained items, unmount no longer cancels accepted sends, and MiniChat no longer races local appends against its fetch.
  • Frontend lifecycle: live transport cleans up sockets on destroy and mid-connect mode changes, failed WASM injections can be retried, and a stale NomadNet started event cannot resurrect a cancelled download.
  • Boot: the splash waits for the initial route chunk (4 second cap) before fading, so the shell does not jump onto an empty view.
  • NomadNet: page images no longer stick on Loading when a burst of file downloads was rate-limited. Errors now fail the matching download.
  • Themes: outbound, failed, and waiting message bubbles now take their colors from the active theme instead of a fixed blue. Settings pickers show the theme-resolved color until you override it.
  • Settings: the Reticulum Stack panel no longer leaks into Simple mode or unrelated tabs.
  • Android: location sharing no longer fails when browser geolocation is unavailable. Native GPS is used when selected, with a clear prompt when permission is missing.
  • Messages: shared contacts no longer show the raw payload above the card, and the mobile composer moves attachments into the input with a tidy picker.
  • Relay chat: search, cleanup, options, and leave collapse into a menu on mobile so the members button still fits.
  • Interface cards no longer leave a gap above the title on mobile, and the map info chip no longer touches the zoom controls.
  • Map: switching between online and offline no longer flashes a blank map, offline uses cached tiles when present, and the last view is restored on restart.
  • Voicemail works without espeak: record or upload a greeting even when text-to-speech is unavailable.
  • LXMFy bots get their configured icon colors seeded up front instead of showing a white avatar until first reply.
  • Popup menus show a caret pointing back at the button that opened them.
  • Mobile shows one toast at a time, and settings sections collapse to icon buttons on small screens.
  • Service worker: HEAD probes on hashed assets no longer reject the fetch, fixing the emoji picker data load.
  • Windows: the AppContainer backend sandbox is off by default again after loader-init crashes on some systems. Set MESHCHAT_APPCONTAINER=1 to require it, or auto to enable it with an unsandboxed fallback.
  • Desktop: Chromium GPU and logging flags injected by a crash-fallback relaunch no longer leak into backend arguments, which could break the Windows backend spawn under a Job Object.
  • NomadNet: page and file downloads in flight resume on the new socket after a websocket reconnect instead of spinning forever, and a resend that lands on a dead socket fails the download cleanly.
  • NomadNet: requests that never get a response now fail on a watchdog timeout instead of hanging, and dead links are evicted so the next attempt reconnects.
  • Tutorial: navigation buttons stay pinned below the scroll area in page mode, and the bootstrap-only toggle follows the backend default of off.
  • Map: location search sits inline in the header on desktop and tablet, and the first-run tooltip anchors to the map tools button.

Changed

  • Install docs cover reverse proxies, Kubernetes, and previously undocumented flags and MESHCHAT_* variables.
  • Long sessions bound per-view state (announces, map markers, NomadNet pages, MiniChat, toasts) and clean up timers and streams on unmount.
  • Bump rns to 1.5.4 and lxst to 0.5.3.
  • Hardcoded palette colors across calls, contacts, toasts, voice notes, and docs controls now use semantic theme tokens that follow the active theme.
  • Headless self-check gains a Windows AppContainer launch probe so loader-init failures show up in --self-check and CI.
  • CI checks the packaged Electron backend tree against backend-manifest.json before staging is pruned.
  • GHCR container images publish zstd-compressed OCI layers for faster pulls on modern runtimes. Docker Hub stays on gzip for compatibility.