Skip to content

Revision History

All notable changes to Muxit are documented here, grouped by version.

v0.31.0 — May 8, 2026

Features

  • node build.js publish now produces both linux-x64 and win-x64 archives by default on either host OS. Archive creation moved from platform-specific shell-outs (tar / Compress-Archive / zip) to a single archiver-based helper that explicitly sets the +x bit on muxit, createdump, and *.so* files in the linux tarball — so cross-host builds (e.g. linux-x64 from a Windows dev box) now ship the executable bit correctly. Override with MUXIT_PUBLISH_RID if you only want one platform.

Bug Fixes

  • Dev console can now save manifests for Python drivers. Saving a Python driver's manifest from the Drivers tab returned unknown driver: python/<name> because the dev console's driver resolver only listed C# and JS sources; Python drivers are now included so save / build / registry-entry / publish endpoints all resolve them.
  • Dev console Release tab: Build release archive no longer makes the log box vanish. The release panel was repainting 500ms after the build started, which destroyed the log element while the build was still streaming — looking like the operation crashed silently. The refresh now waits for the build's exit event. Help text clarified to reflect that the local build targets the host OS only (cross-platform matrix runs on tag push).

v0.30.0 — May 8, 2026

Features

  • Linux now has a one-line installer and a runtime dependency check.

    End-users on Ubuntu / Debian can install with:

    bash
    curl -fsSL https://raw.githubusercontent.com/muxit-io/muxit/main/install.sh | bash

    The installer (installer/install.sh):

    • Detects the distro, runs sudo apt install ffmpeg libayatana-appindicator3-1 xdg-utils with confirmation.
    • Adds the user to the dialout group if needed (serial-port access).
    • Downloads the latest muxit-linux-x64-v*.tar.gz from GitHub Releases and verifies the SHA-256 against checksums.txt.
    • Extracts into ~/.local/share/muxit/ (per-user, no sudo for the install itself; workspace/ is preserved across reinstalls).
    • Symlinks ~/.local/bin/muxit so plain muxit works on $PATH.

    Re-running the same command upgrades to the latest release in place.

    Independently, the server itself now probes its runtime dependencies at startup on Linux and prints a non-fatal System dependencies section in the boot banner:

    ── System dependencies ─────────────────
      ✓ ffmpeg (Onvif IP-camera RTSP streaming)
      ⚠ libayatana-appindicator3-1 missing — system tray icon unavailable.
        Fix: sudo apt install libayatana-appindicator3-1
      ✓ xdg-open (auto-open browser at startup)
      ⚠ user 'matthijs' is NOT in 'dialout' group — serial drivers will
        fail with EACCES on /dev/ttyUSB*.
        Fix: sudo usermod -aG dialout matthijs && newgrp dialout

    So users who skip the installer (manual tarball + extract) still get told exactly what's missing and how to fix it.

    New install guide at /docs/guides/install-linux covers the one-liner, manual install, all four supported package managers (apt / dnf / pacman / zypper), and uninstall. macOS, dnf-native support, .desktop launcher, udev rules and systemd-user service are deliberately scoped out for v1 — see docs/follow-up-linux-installer.md in the dev repo for the punch list.

  • Linux x64 release archives are now published alongside the Windows build. The release tag (v*) workflow now runs on a matrix (windows-latest + ubuntu-latest) and uploads two assets per release: muxit-win-x64-v<version>.zip and muxit-linux-x64-v<version>.tar.gz. The Linux tarball preserves the executable bit on the muxit binary because it is created on the Linux runner.

    build.js publish now defaults to the host platform's RID and accepts an MUXIT_PUBLISH_RID env override so a single dev machine still produces a relevant archive. Packaging is .tar.gz for linux-* / osx-* RIDs and .zip for win-*.

    Linux runtime requirements

    shimat's manylinux build of libOpenCvSharpExtern.so ships a static FFmpeg without network protocols (no rtsp://, no http://) and a GStreamer backend that registers but is non-functional. Both VideoCapture(rtspUri, FFMPEG) and VideoCapture(pipeline, GSTREAMER) return IsOpened()=false on Linux for any RTSP URL. Verified with the diagnostic at tools/rtsp-probe/run.sh — five different in-process approaches all fail.

    The Onvif driver therefore delegates RTSP capture on Linux/macOS to the system ffmpeg binary: spawn ffmpeg -i rtsp://… -f mjpeg pipe:1, parse JPEGs from stdout, decode each one with Cv2.ImDecode (which works fine in the same wrapper). On Windows the in-process VideoCapture(rtspUri, FFMPEG) path is unchanged — there shimat's runtime ships a full FFmpeg with RTSP support.

    For Onvif on Linux you therefore need ffmpeg on $PATH:

    bash
    sudo apt install -y ffmpeg

    That's the whole story — no GStreamer packages required. TestDevice's synthetic video stream and any future Webcam / Vision driver work without ffmpeg (they use the in-process OpenCV pipeline directly, which is fine because they don't open network streams).

    Optional:

    • xdg-utils — lets the server auto-open the browser on startup. Without it, the server prints the URL and you open it yourself.
    • libayatana-appindicator3-1 — system-tray icon. Without it, the server starts normally but with no tray.

    The server itself (web UI, scripts, non-video drivers) runs without any of the above.

    Vendor-SDK drivers (Avantes, Fairino) remain Windows-only and are not exercised on the Linux build leg of CI.

v0.29.0 — May 8, 2026

Features

  • Welcome screen now shows system info instead of a tier comparison table. The Free vs Pro feature table is gone — the home page still shows your current license tier badge, plus a new System panel with the server port, hostname (e.g. muxit.local), Bonjour/mDNS state, remote-access state, the active AI provider, and (for Muxit AI) credit usage.

v0.28.0 — May 8, 2026

Features

  • System tray now has a "View Logs" entry that opens a live log viewer. The viewer is served at /logs.html and streams entries from the in-memory ringbuffer over Server-Sent Events with level + substring filters, pause and download. Console.WriteLine / Console.Error.WriteLine calls are now teed into ServerLogger, so startup banners are visible too. Each instance has its own port, so multiple muxit instances on the same machine each surface their own logs without cross-talk.

    Two on-disk files are kept per workspace under %LOCALAPPDATA%\Muxit\logs\<hash>\ (Windows), ~/Library/Application Support/Muxit/logs/<hash>/ (macOS), or ~/.local/share/Muxit/logs/<hash>/ (Linux):

    • startup.log — overwritten on every clean exit, so a screenshot is always available even if the HTTP server is down.
    • last-crash.log — written from the unhandled-exception handler. When present, the tray gains a one-shot "Open Last Crash Log" entry that opens it in the OS default text editor.

    Nothing is ever appended — exactly two files per workspace, both overwritten in place.

v0.27.2 — May 7, 2026

Bug Fixes

  • Restart Server and Switch Workspace now work in the published muxit executable. Previously these actions wrote a marker file and stopped the server, relying on the dev-mode Node launcher (start.js) to respawn the process — which left users running muxit.exe with a stopped server and no automatic restart. The server now respawns itself when no parent launcher is present.

v0.27.1 — May 7, 2026

Bug Fixes

  • AI assistant no longer asks for confirmation twice on numeric writes. The system prompt now tells the model to call the tool first and let the safety gate prompt the user, instead of pre-confirming in chat and then triggering the gate's prompt on top.

v0.27.0 — May 7, 2026

Features

  • Windows release archives are now versioned (muxit-win-x64-v<version>.zip). The release zip includes the version in its filename so downloads from the GitHub Releases page are self-identifying. The auto-updater (SelfUpdater/UpdateChecker) and the PowerShell installer match the asset by muxit-win-x64*.zip prefix, so existing installs continue to find the latest release without changes.

v0.26.0 — May 7, 2026

Features

  • Imprint, Privacy Policy, and Terms & License Agreement are now published at muxit.io/legal/imprint, /legal/privacy, and /legal/terms. The site footer carries persistent links to all three. The in-app workspace acknowledgement bumps to v1.1.0 (forces re-acceptance) and now references the full Terms and Privacy Policy directly. The privacy policy describes the licence machine-fingerprint precisely (machine name, OS user name, up-to-three sorted MAC addresses, and an OS-installation identifier — all hashed locally before transmission), clarifies that MCP-with-your-own-AI-key is available on every tier including Free, and removes the analytics paragraph: muxit.io currently runs no website analytics at all. The references to the EU Online Dispute Resolution platform have been removed from both the imprint and the terms because the European Commission discontinued the platform on 20 July 2025.
  • Multi-instance support: one running muxit per workspace, with a tier cap. The same workspace can no longer be opened twice — the second instance exits with the holder's PID — so concurrent muxit processes always own distinct workspaces. The new concurrent-instances license feature caps how many workspaces a user can run at once on the same machine (Free 1, Maker 2, Pro 5, Lab/Enterprise unlimited); the cap is enforced via a per-process lock file on Windows (%LOCALAPPDATA%\Muxit\locks\Instance-N.lock) and a Unix-domain socket on Linux/macOS, both of which the OS releases on process death regardless of which thread originally acquired them. Licences are now stored per-machine (under ~/.muxit/ or %LOCALAPPDATA%\Muxit\), so a single activation covers every workspace on the same computer; on first launch the migration scans every catalogued workspace for a .license that actually contains a key (paired with its signed .license-proof) and copies that one up. Launching muxit without --workspace when more than one workspace is registered opens a browser-based workspace picker. If the cap is already reached, the picker shows a tier-aware banner up front (Multi-instance limit reached. Your <Tier> tier allows up to N concurrent muxit instances…) and disables every workspace button instead of redirecting to a dead origin. Port collisions auto-bump to the next free port for the current run only — the workspace's server.json is no longer rewritten, so a solo relaunch returns to the configured port. Deactivating a licence now sticks across restarts: the server-signed proof is deleted alongside the local .license, and the Deactivate button is visible whenever any non-free tier is active even if the cached key is missing. The active workspace name is shown in the tray menu (Muxit — <name> (port N)) and next to the wordmark in the GUI titlebar.
  • Python connector support is now front-and-centre in the docs and on the website. The marketing site's "How it works" page lists Python as a fourth driver format alongside SCPI / JS / DLL, and the makers and professionals pages pitch the "drop a .py file in workspace/python/ and a connector calls its functions" path. In the docs, getting-started gains a new "I already have a Python script that talks to my device" path, system-requirements documents the optional Python 3.10+ dependency, and a new Python Connectors guide covers the generic Python driver — properties, init / shutdown, per-script venvs, common pitfalls — without making readers pick through the full SDK reference.
  • AI-assisted SCPI connector authoring is now its own license feature. Added scpi-authoring (defaults to Maker tier, matching prior behavior) so the authoring workflow — probe_scpi_device, validate_connector_config, search_manual, read_manual_pages, and the rest of the SCPI authoring tool set — can be moved between tiers in AppSettings.cs without touching the rest of the chat surface. The intent classifier now skips the SCPI authoring group entirely when the feature is blocked, so blocked tiers no longer pay for the SCPI system-prompt fragment or the OpenRouter web-search plugin on irrelevant turns.
  • Window title now shows the active workspace. The browser/Electron tab title and the server's console window (Windows taskbar tooltip) both read muxit-io [WorkspaceName], making it easy to tell multiple muxit.io instances apart at a glance.

Bug Fixes

  • Pricing tiers now match what's actually shipped. Pro-tier concurrent-instances was capped at 5 in AppSettings, but the pricing page promises unlimited — bumped Pro to -1 so the cap matches the marketing. Autonomous agents are now flagged (in development) on the pricing card and comparison table since the agents feature is still Disabled = true in code. Added Startup scripts and Video recording rows to the pricing card and comparison table so the silent Maker-tier gates on those features are visible to users.
  • Pricing and AI docs no longer falsely claim ai(prompt, image) requires Pro. The script-side ai() global only checks the "ai" license feature (Maker tier), so passing a base64 JPEG to it from a script works on Maker — that was already true in the code, but the docs and pricing card said otherwise. The Pro-tier "Vision AI" benefit is reframed around what's actually gated: the agentic vision tools in the Chat Panel and MCP (take_snapshot, identify_objects, locate_object, OpenCV trackers, spatial mapping). Maker = explicit ai(prompt, image) calls from your scripts; Pro = the AI assistant decides when to look.
  • Security: close N1–N4 from the 2026-05-07 follow-up audit. License-proof verification now binds the signed envelope to the local machine fingerprint — copying a paid user's .license-proof to another machine no longer elevates that machine's tier (audit N1). JS and Python drivers can no longer opt out of safety gates: meta.requiresSafetyGates: false (JS) and META["requiresSafetyGates"] = False (Python) are ignored with a stderr warning, since neither tier has a per-driver signing path that would prevent a hand-dropped driver from bypassing every limit, confirm and audit row (audit N2). The MUXIT_PROXY_BASE_URL and MUXIT_DISABLE_INSTANCE_LIMIT environment-variable bypasses are now gated on MUXIT_DEV=1, so production binaries silently ignore them — without that gate, an end user could redirect license-flow calls to a dead URL (defeating the B2 trial ledger) or bypass the per-tier concurrent-instances cap by setting one variable (audits N3, N4). Built-in drivers that opt out via the C# class override (Webcam, FileAccess, Onvif, MqttBridge) and DLL drivers that opt out via a signed assembly attribute are unaffected — those paths continue to honour declared opt-outs.

v0.25.47 — May 5, 2026

Cleanup — drop legacy HMAC license-cache reader

The one-release migration helper from v0.25.46 has done its job. Removed MuxitServer/Licensing/LegacyHmacReader.cs and the legacy-format detection branch in LicenseManager.Cache.cs:LoadFromDisk — the cache loader is now a plain JSON read. Anyone still on a build older than v0.25.46 who upgrades through this version will see "license invalidated, please re-activate" on first launch and need to paste their key once. Acceptable since the public release happens after this version.

v0.25.46 — May 5, 2026

Security — server-signed license cache (audit B1 + B2)

The local .license cache used to be HMAC-signed with a key derived from a hidden salt file plus MachineName/UserName — every input the user could read, so anyone with workspace access could forge TierId="lab" with full entitlements (audit B1). Trials lived in the same file, so deleting it granted a fresh trial (audit B2).

  • Tier is now gated by a server-signed proof. A new .license-proof file stores an RSA-SHA256 envelope returned by POST https://api.muxit.io/v1/license/sign. On startup the proof is verified against an embedded RSA-4096 public key (MuxitServer/Licensing/LicensePubKey.cs) and, if its valid_until is still in the future, its tier overrides the local .license. Missing / tampered / expired → user is demoted to free until the next online re-validation.
  • .license is now plaintext JSON. It still holds local bookkeeping (StartupCount, LastSeen, FingerprintComponents, customer info, BaseLicenseKey and BaseInstanceId for re-validation), but editing it can no longer escalate tier — the proof is the source of truth.
  • Trials moved to a server-side ledger. LicenseManager.StartTrial now calls POST /v1/license/trial-start with the machine fingerprint; the proxy holds an idempotent (fingerprint, feature_id) record. Deleting the workspace no longer grants a fresh trial. StartTrial is async now (Task<bool>).
  • Soft cutover for existing installs. A read-only LegacyHmacReader still loads the old HMAC cache once on startup so cohort-1 customers don't see a "license invalidated" prompt; the next online re-validation upgrades them to the new format and deletes the legacy .license-salt file. Lifespan: one release.
  • New keypair, separate from driver signing. Generated by tools/generate-license-signing-key.csx (PKCS#8 PEM, not .NET XML — Web Crypto in the worker imports PEM natively). Private half lives only in the Cloudflare Worker secret store; same key is never used to sign DLLs.
  • MUXIT_PROXY_BASE_URL env-var override added to SignedLicenseClient and TrialClient for staging / local-wrangler-dev testing without touching source.
  • Companion changes ship in MUXITPROXY (signing endpoint, trial-start endpoint, trial_starts D1 table, LICENSE_SIGNING_PRIVATE_KEY worker secret).
  • Crypto roundtrip can be verified offline via dotnet script tools/verify-license-signing.csx.

v0.25.45 — May 4, 2026

Connector configs: c parameter replaced by driver closure variable

Custom methods, getters, and setters in connector configs no longer receive the driver proxy as a leading argument. Instead, the driver is in scope as the closure variable driver. Function signatures now match the script-side call exactly:

js
methods: {
  start:  { fn: () => driver.startPolling() },
  rampTo: { fn: (target) => { driver.voltage = target; } },
}
properties: {
  voltage: {
    get: () => driver.voltage,
    set: (v) => { driver.voltage = v; }
  },
  power: () => driver.voltage * driver.current,
}

This removes the asymmetry between how a script calls a connector (psu.rampTo(5)) and how the connector declared the method ((c, target) => …), and gets rid of the cryptic c parameter.

Breaking change. Configs still using the old (c) => c.… shape will fail at runtime — V8 throws because c is no longer bound. All bundled connectors, driver template stubs, and docs have been updated to the new form. Update any pre-release config in workspace/connectors/.

v0.25.44 — May 4, 2026

Python connector init runs in the background — UI shows live status

A multi-minute pip-install + model-load on connector activate was blocking the synchronous InitAsync path. Even with last commit's 30-minute timeout, the user got a frozen UI while torch downloaded — and a single bad driver could hang the whole connectors panel for everyone else trying to enable theirs. Replaced with a fire-and-forget background init + live status broadcast.

Server side (MuxitServer/Drivers/PythonDriverHost.cs)

  • InitAsync now returns immediately after the synchronous catalog / interpreter pre-checks. The actual venv setup, pip install, subprocess launch, and init RPC run on a Task.Run worker (RunInitInBackgroundAsync) with a per-instance CancellationTokenSource.
  • New PythonInstanceState enum: Loading | Ready | Error | Cancelled, plus a PythonInstanceStatus record stored in _instanceStatus. Updated at every phase boundary (Preparing virtual environment...Installing requirements...Starting subprocess...Initialising driver...Ready). Latest pip / venv output line is mirrored into the message field too, so the UI gets a flowing progress string.
  • New EnsureReady(instanceId, verb) helper. GetPropertyAsync, SetPropertyAsync, ExecuteActionAsync call it before dispatching — calls against a still-loading connector throw a clear "Python connector 'foo' is not ready (loading): Installing requirements... Cannot execute action 'speak' until initialisation completes." instead of either silently waiting or timing out arbitrarily.
  • ShutdownAsync now cancels the in-flight init token before tearing down. Disabling the connector mid-install kills the running pip subprocess (via proc.Kill(entireProcessTree: true) in PythonVenvManager.RunStreamingAsync's new OperationCanceledException branch) instead of leaving torch's wheel-builder children running. ShutdownAllAsync includes loading instances too.
  • The init RPC's own timeout is back to a sane 5 minutes — pip install (which has no timeout) is now in the background, so the RPC only covers the model-load step inside the user's init().

Wire layer (MuxitServer/WebSocket/EventBridge.cs)

  • New connector.status rebroadcast from the EventBus to all WS clients: { connector, driver, state, message }. The existing driver.python.install event continues to carry per-line pip output for the server log; connector.status is the higher-level state machine the UI binds to.

UI (web-ui/src/hooks/useServer.js, web-ui/src/components/ConnectorBrowser.jsx)

  • useServer adds a connectorStatus state map keyed by connector name; subscribes to connector.status messages and updates the entry on every transition.
  • ConnectorCard reads its entry from server.connectorStatus[name] and renders a one-line status badge below the header when state ≠ ready: a spinning gear + the latest message for loading, a red ✗ + error message for error, a muted ⊘ + reason for cancelled. Disappears once the connector is ready, leaving the normal card layout.

User-visible effect

  • Activating python-chatterbox now returns instantly. The connector card immediately shows ⚙ Installing: Creating venv at python/.venvs/chatterbox, then a flow of [pip] Collecting torch ... lines, then Loading chatterbox model on cpu (first run downloads ~2GB; subsequent runs are cached)..., then disappears when the connector is ready. Other connectors can be enabled / used in parallel; nothing blocks on chatterbox's install.
  • Disabling the connector mid-install kills pip and the model download cleanly. Re-enabling starts fresh.

v0.25.43 — May 4, 2026

Three fixes that turned the chatterbox first-run from "broken" to "working but slow"

A user activating the python-chatterbox example connector hit:

Failed to init connector 'python-chatterbox': Python driver did not respond to 'init' within 60000ms

…after seeing only Installing requirements from chatterbox.requirements.txt (first run can take a while) and then no further log output for a minute. Three independent issues were combining:

  • Init RPC timeout was 60s. Way too short for any ML driver — chatterbox's ChatterboxTTS.from_pretrained() downloads several GB of model weights from HuggingFace and warms them into RAM, easily 5–15 minutes on a fresh machine. Bumped to 30 minutes in PythonDriverHost.InitAsync. Pip install (which precedes init) already had no timeout, so this just brings init in line. Genuinely runaway processes still get caught; users who want to interrupt earlier can disable the connector.
  • Pip's progress bar was invisible. pip install writes its download progress with \r (no newline), and .NET's Process.OutputDataReceived only fires on \n. So between download-step lines (which can be several minutes apart for a 2 GB wheel), the user saw absolute silence and assumed pip had hung. Added --progress-bar off to the pip invocation in PythonVenvManager.PipInstallAsync so each download step prints as a flushed line.
  • The chatterbox example script was silent during init(). Even with pip output flowing, the first from chatterbox.tts import ChatterboxTTS plus the model load can take another minute or two. The example now prints stderr heartbeats (Loading chatterbox library..., Loading chatterbox model on cpu (first run downloads ~2GB; subsequent runs are cached)..., Chatterbox ready (sample rate 24000 Hz, ...)) via a small _say() helper that goes through sys.stderr so it can't collide with the JSON-RPC channel. The host's stderr pump forwards them verbatim with the [connector-name] stderr: prefix.

Net effect for a user activating the chatterbox connector for the first time: continuous progress output throughout the multi-minute first install + model load, instead of the previous "log line, silence, timeout error" experience.

v0.25.42 — May 4, 2026

Chatterbox TTS example for the generic Python driver

Adds a fourth example to the workspace template demonstrating the heavy-dependency case for the Tier 2 generic Python driver:

  • workspace/python/chatterbox.py + chatterbox.requirements.txt — TTS via chatterbox-tts.
  • workspace/connectors/python-chatterbox.js — connector exposing speak(), speakWithVoice(), speakAs(), plus sampleRate / device / outDir properties.

The flow is the same as the existing http-probe example, just with a much larger first-install cost: activating the connector for the first time creates workspace/python/.venvs/chatterbox/, pip-installs torch + transformers + chatterbox into it (a couple of GB, several minutes — output streams to the server log so you can watch progress), then loads the model. Subsequent activations skip pip and start in a few seconds; the loaded model stays in memory across speak() calls.

Each call writes a WAV file under workspace/data/chatterbox/ and returns the absolute path. Pipe that path into a player, an <audio> element, or any downstream connector that handles audio playback. The companion docstrings populate the API Reference tab so users see usage hints (device config, voice cloning via voice parameter, output filename overrides) without leaving the driver page.

This commit is not smoke-tested in CI — installing torch from the chatterbox dependency tree takes too long for the test runner. The plumbing it relies on (per-script venv + Python tier dispatch) is covered by the existing http-probe and counter / hello tests.

v0.25.41 — May 4, 2026

Driver detail page surfaces the manifest details + Python tier label

Two follow-ups to the driver page after testing the Python driver in-app:

  • Tier badge for Python drivers no longer shows "Unknown". The two driver-detail components in the web UI (DriverDetailPage.jsx for the standalone Drivers panel, DriverDocViewer.jsx for the in-editor doc viewer) had their tier label maps hard-coded for tiers 0/1/3 only. Both now include 2: 'Python'. The "trust badge" ladder on DriverDetailPage similarly gains a Community (Python) arm.
  • API Reference tab now renders manifest details as a "Documentation" section. The earlier change wired details through driver.schema for the in-editor viewer, but the user-facing Drivers panel uses marketplace.detail which wasn't including the field. Added details = localEntry?.Details to that handler's response, then rendered it as a top-of-tab Documentation section using the existing DetailsMarkdown component (now exported from DriverDocViewer). Imports the existing driver-doc.css so the markdown styling (paragraphs, lists, code blocks) applies in the standalone page too.

Effect for the Python driver: clicking it in the Drivers panel now shows the badge Community (Python) instead of Unknown, and the API Reference tab leads with the ~4 KB markdown documenting layout, the requirements.txt lifecycle, examples, and conventions — instead of just the Connector Template snippet. Same plumbing benefits any driver (DLL / JS / built-in) whose manifest declares a details field.

v0.25.40 — May 4, 2026

Driver doc viewer surfaces long-form details

The driver detail page in the Drivers panel previously had room only for a one-line description and the property/action tables. Drivers like the new generic Python one needed more — how requirements.txt works, where scripts live, what conventions to follow — and there was nowhere to put it without bloating the always-visible description (which feeds into the AI system prompt).

Three coordinated changes:

  • manifest.json gains an optional details field. Markdown, rendered as a collapsible "Show details" section in DriverDocViewer.jsx directly under the description. Threaded through all three tiers: DriverManifestDriverEntry.Details → the driver.schema WebSocket response.
  • Per-action and per-property details are now sent in the schema response (the descriptor records always carried the field; the WS handler just wasn't including it). The existing DetailsDisclosure component already rendered them when present, so the change immediately unlocks more useful entries on every driver.
  • The generic Python driver's introspection now splits docstrings into description + details. The first line of a function's docstring becomes its action's description (always-on, in the AI prompt); the rest becomes details (markdown, surfaced on demand). Same split for properties via the get_<x> docstring. So writing a normal Python docstring on def head(url): automatically populates both fields in the UI.

Concrete payoff: the Python driver's manifest now ships a ~4 KB markdown details block covering layout, the requirements.txt lifecycle, examples, and limitations — visible by clicking "Show details" on the driver page. The shipped example scripts (hello.py, counter.py, http-probe.py) gain richer docstrings so each action / property has both a one-liner and a paragraph of context.

The details field is optional; existing drivers (built-ins, JS, DLL packages) keep rendering exactly as before until their manifests add the field.

v0.25.39 — May 4, 2026

Fix: connector calls on the generic Python driver were rejected

connector("python-hello").greet("world") was throwing Driver has no property or action 'greet' because the connector proxy authorises calls against the driver's catalog-time META, and the generic Python driver's META is intentionally empty (its actions are dynamic — they come from whichever user script the connector points at).

Fixed by mirroring the per-instance schema mechanism the DLL tier already had for GenericScpi: after init() returns, the host now sends a new schema RPC to the Python driver subprocess. The generic driver responds by introspecting the loaded user module:

  • top-level callables (excluding init, shutdown, and underscore-prefixed names) → actions, with the function's first docstring line as the description
  • paired get_<x> / set_<x> → R/W property <x>
  • unpaired get_<x> → read-only, unpaired set_<x> → write-only

Typed Driver subclasses get a sensible default — schema() falls back to the values declared in META — so existing typed drivers don't change.

PythonDriverHost.GetDriverMetaForInstance(instanceId) exposes the per-instance schema; DriverRegistry.GetDriverMeta consults it first for Python instances and falls back to the catalog-level meta for instances that haven't completed init yet. The schema is cleaned out on ShutdownAsync so a re-activate gets a fresh introspection.

After this, c.greet({ name }) from a connector method body lands on the driver's execute("greet", {...}) and dispatches through to module.greet(name="...") as intended.

v0.25.38 — May 3, 2026

Python tier — examples by connector, one driver to install

The two scaffolding-style Python drivers introduced alongside the tier (drivers/py/example/ and drivers/py/example-with-deps/) are removed. They duplicated what the generic muxit/python driver now does, and shipping three Python .muxdriver packages where one will do made the marketplace listing confusing.

In their place, workspace-template/python/ now ships three working user scripts and workspace-template/connectors/ ships matching connectors so a freshly seeded workspace boots with usable Python examples:

  • hello.py + python-hello.js — minimal, no dependencies. Single greet(name) action.
  • counter.py + python-counter.js — adds state; demonstrates get_<x>() / set_<x>(value) properties and init(config).
  • http-probe.py + python-http-probe.js — adds requests as a runtime dependency; the sibling http-probe.requirements.txt triggers the per-script venv flow on first activation.

workspace-template/python/README.md documents the conventions (one file per script, optional init / shutdown, get_<x> / set_<x> properties, <stem>.requirements.txt for deps).

lib/muxit-menu/index.js updated so python/ is part of the workspace seed (option 17), the empty-workspace dir creation list (option 16), and the workspace status report (option 20).

The driver-side regression test (drivers/py/python-runner/) and the muxit_driver SDK are unchanged — the deleted drivers were only example payloads, not part of the platform.

v0.25.37 — May 3, 2026

Generic Python driver — write functions, skip the boilerplate

A new built-and-shipped Python driver (drivers/py/python-runner/, manifest id muxit/python) lets a connector point at any .py file in workspace/python/ and exposes that file's top-level functions as RPC actions / properties. Users who want to glue an existing Python library to a Muxit connector no longer have to subclass Driver or wire up a JSON-RPC dispatch — they drop a script with bare functions, optionally a sibling <name>.requirements.txt, and reference it from the connector's config.script.

python
# workspace/python/chatterbox.py
def init(config): ...
def speak(text): ...      # ← connector().speak({ text }) lands here
def get_status(): ...     # ← connector().status read
js
{ driver: "Python", config: { script: "chatterbox" }, methods: { ... } }

Per-script venvs at workspace/python/.venvs/<stem>/ give clean dependency isolation between scripts (one with torch, one with pyserial, no collisions). First activation creates the venv and pip-installs the requirements with line-by-line output streaming to the server console. Subsequent activations check the SHA-256 of the requirements file against a stamp and skip pip when nothing changed (~150ms warm start). Migration to a typed Driver subclass is mechanical when a script outgrows the generic dispatch.

Plumbed through end-to-end: DriverRegistry.CreateInstanceAsync injects _workspacePath into the config dict for Tier 2 drivers (mirroring the built-in driver path), so the generic driver can resolve workspace/python/<script>.py reliably regardless of what the user wrote in the connector config.

New "Two ways to use Python" + "Generic Python driver" sections in docs-site/reference/driver-sdk-python.md.

v0.25.36 — May 3, 2026

Python driver debugging helpers

Two small, no-overhead additions to the Python driver SDK so a problem in a Tier 2 driver is a few minutes to track down instead of "stare at stderr and guess."

  • Tracebacks in error responses. When a driver method raises, the JSON-RPC error sent to the host (and on to the script / dashboard / AI) now includes a compact location excerpt — first line is the exception, then up to 3 frames of the user's own code (SDK frames are filtered out). Looks like KeyError: 'unknown property: nope'\n at get() in mydriver.driver.py:62. The full traceback still goes to stderr / server console as before.
  • Optional debugger attach via MUXIT_PYTHON_DEBUG. Setting the env var to a port number on the driver's process makes the SDK call debugpy.listen(("127.0.0.1", port)) at startup and wait for a client before init. Attach VS Code's "Python: Attach" launcher and step through your driver. No-op when the env var isn't set; degrades to a one-line stderr hint if debugpy isn't installed (add it to requirements.txt to use). pdb.set_trace() continues to not work — the subprocess' stdin is busy with JSON-RPC.

New "Debugging" section in docs-site/reference/driver-sdk-python.md covers the three escalation paths: read the error response, run the driver standalone with --scan / --instance, attach debugpy. Plus a "Common pitfalls" subsection ((non-json stdout), module-level heavy imports, hangs during pip install, missing python3-venv on Debian).

v0.25.35 — May 3, 2026

Features

  • Python drivers auto-install their dependencies. A driver that ships a requirements.txt next to its .driver.py now gets a per-driver virtual environment (.venv/) created in its package cache dir on the first connector activation. Pip runs against that venv and the driver subprocess is launched with the venv's python, so import torch (or anything else inside init()) resolves to a clean per-driver install. New MuxitServer/Drivers/PythonVenvManager.cs handles the lifecycle: hashes requirements.txt into .venv/.muxit-req-hash so subsequent activations skip the install when nothing changed; streams pip's stdout/stderr line-by-line to the server console and to a new driver.python.install EventBus event so the UI can show progress while the user waits. Different drivers can pin conflicting versions of the same package without interfering — each lives in its own venv. Cached venv start time is ~150ms; first install runs as fast as pip + the network can manage (~2s for requests, several minutes for torch).
  • Example driver with deps. drivers/py/example-with-deps/ (PyHttpProbe) demonstrates the flow with a small requests dependency. Validates + builds + runs end-to-end via the new venv plumbing.

Known limitations

  • Native wheels (torch, opencv) sometimes need system-level build tools or libraries; pip failures surface in the driver's init error with the full transcript above. Adding a "diagnose missing system deps" pass is on the roadmap.
  • On Debian/Ubuntu the system Python may not include the venv module by default (apt install python3-venv once if you see python -m venv failing).
  • Loose .driver.py files dropped into workspace/drivers/ (no .muxdriver packaging) don't get a managed venv — they use the system interpreter directly. This is the dev workflow; packaged drivers are the user-facing path.

v0.25.34 — May 3, 2026

Features

  • Python driver tier (Tier 2). Drivers can now be authored in Python — a new .driver.py extension alongside the existing .driver.js and .dll tiers. Each active connector spawns its own Python subprocess; MuxitServer talks to it over line-delimited JSON-RPC on stdin/stdout (init / get / set / execute / shutdown). Drivers subclass muxit_driver.Driver and call run(YourDriverClass). Wired through end-to-end: DriverTier.Python = 2 (MuxitServer/Drivers/DriverTypes.cs), PythonDriverHost + PythonDriverInstance for subprocess + IPC, PythonRuntimeManager to detect a usable interpreter (MUXIT_PYTHON env var → python3python, requires ≥ 3.10), DriverPackageStore recognises *.driver.py packages, manifest schema accepts tier: 2, node drivers.js build packages Python drivers (auto-vendoring the muxit_driver SDK into every .muxdriver so installed drivers don't need anything on PYTHONPATH). Use it for Python-only ecosystems (numpy, torch, instrument SDKs); JS remains the right choice for serial/TCP instruments.
  • Python interpreter detection is optional. If no usable Python is found at startup, the server keeps booting; Python drivers (if any are installed) surface a warning at scan time instead of failing the run. Set MUXIT_PYTHON to a full interpreter path to override probing. See docs-site/reference/driver-sdk-python.md for the SDK reference, manifest, and wire protocol.
  • Example Python driver. drivers/py/example/ ships a deps-free counter+greeter driver as a starting point and as the regression target for the JSON-RPC bridge.

Known limitations

  • Python drivers are free-only — premium signing isn't implemented for the Python tier yet (PYTHON_PREMIUM_FORBIDDEN validator rule).
  • Runtime dependencies declared in a driver's requirements.txt are bundled into the .muxdriver. Auto-installation into a per-driver venv landed in v0.25.35.

v0.25.33 — May 4, 2026

CSP: allow 'wasm-unsafe-eval' for the opus-decoder polyfill

The dashboard's strict Content-Security-Policy was blocking WebAssembly.compile() (script-src 'self' blob: doesn't permit WASM compilation), so the opus-decoder polyfill threw on load and the dashboard went silent on insecure-context connections.

Both the docs CSP and the SPA CSP now include 'wasm-unsafe-eval' in their script-src directive. This is a much narrower permission than 'unsafe-eval' — it allows WebAssembly.compile/instantiate only, not arbitrary eval() or string-to-code conversion. No other directive changed.

v0.25.32 — May 4, 2026

Audio renderer: pass channel count into the WASM Opus decoder

Fix for "no sound when accessing the dashboard remotely (e.g. http://192.168.x.x:8765)": the opus-decoder constructor defaults to 2 channels, but the encoder produces mono. A mono Opus packet decoded by a 2-channel decoder fails silently and returns samplesDecoded: 0, which the renderer was discarding without any log output — so the dashboard just went quiet.

  • WASM polyfill now gets { sampleRate, channels } from the wire payload. WebCodecs already configured channels correctly; the WASM path was missing it.
  • Better diagnostics. When the decoder returns 0 samples, log the configured channels / sampleRate and any decoder errors once per stream so a future regression is visible in DevTools.
  • The reason this only manifested on remote connections: WebCodecs is gated to secure contexts (HTTPS or localhost). Connecting from another machine uses http://<lan-ip> which is not a secure context, so window.AudioDecoder is undefined and the renderer fell through to the WASM polyfill path. Local browsers loading via localhost never hit it.

v0.25.31 — May 4, 2026

Audio renderer: jitter buffer + mobile fallback

Two fixes for the browser-side audio path.

  • Cracking on desktop, fixed. ServerAudioRenderer now schedules buffers ~100 ms ahead of ctx.currentTime (the jitter buffer) instead of letting decode latency push nextStartTime into the past. The previous behaviour was the classic Web Audio streaming bug — async decode let currentTime overtake the cursor, the Math.max(now, …) clamp left a silent gap at every buffer boundary, and you'd hear a click ~50 times a second. Underruns are now detected and recovered by resetting the cursor with a fresh 100 ms lookahead.
  • 48 kHz AudioContext. Created with { sampleRate: 48000 } so decoded 48 kHz buffers don't get implicitly resampled to the device's native rate (typically 44.1 kHz on macOS/iOS). Falls back to the default-rate constructor on browsers that throw on the options arg.
  • Opus on mobile / older browsers. The renderer now lazily imports the opus-decoder WASM polyfill when AudioDecoder (WebCodecs) is missing. Covers iOS Safari < 16.4, in-app WebViews, and other browsers where WebCodecs isn't shipped or doesn't support raw Opus. The polyfill bundle is only fetched on browsers that need it; WebCodecs hosts pay nothing extra.
  • The PCM16 fallback path goes through the same shared scheduler, so it benefits from the jitter buffer too.

v0.25.30 — May 4, 2026

Audio encoding moves into the host

The Opus encoder (Concentus) moved out of the AudioSynth driver and into a single shared MuxitServer.Audio.AudioStreamEncoder. Drivers that emit audio now hand the host raw float PCM through a new SDK property and the host owns codec, framing, pacing, EventBus emission, and the stop-frame. This is the right shape for the upcoming TTS connectors (chatterbox, cosyvoice, OpenAI TTS, ElevenLabs) — they can plug in without bundling their own copy of Concentus, and a future swap to binary WS frames stays a single-place change.

  • SDK addition. IConnectorDriver gains Func<float[], AudioFrameInfo, CancellationToken, Task>? AudioStreamEmitter, alongside the existing string StreamEmitter. New value type Muxit.Driver.Sdk.AudioFrameInfo(int SampleRate, int Channels) with a convenience AudioFrameInfo.Mono48k.
  • Host addition. New MuxitServer.Audio.AudioStreamEncoder wrapping Concentus. DriverRegistry.InitBuiltInAsync and DllDriverHost.InitAsync wire driver.AudioStreamEmitter to it alongside the existing string StreamEmitter wiring.
  • drivers/Muxit.Driver.AudioSynth bumped to 1.4.1. Removed the Concentus PackageReference and the in-driver Opus encoding code; StreamSamplesAsync now just awaits the host emitter.
  • Wire format unchanged. Browser still sees the same JSON envelope ({ op, data, sampleRate, channels, format: "opus", frameSize }) and ServerAudioRenderer is untouched.
  • JS drivers (Tier 1) still go through the legacy string emitter — the audio emitter is C#-only for now.

v0.25.29 — May 4, 2026

Audio streams are now Opus-encoded by default

The audio stream wire format switches from base64 PCM16 to base64 Opus packets (32 kbit/s VBR, 20 ms frames at 48 kHz mono). The driver-side encoder is Concentus — pure managed C# with no native deps, so the self-contained muxit-win-x64.zip release stays single-binary clean. On the wire, sustained audio drops from ~1 Mbit/s (base64 PCM16) to ~100–200 kbit/s.

  • AudioSynth driver bumped to 1.4.0. New stream chunk shape: { "op": "chunk", "data": "<base64 opus packet>", "sampleRate": 48000, "channels": 1, "format": "opus", "frameSize": 960 }. Internal ToneGenerator now renders at 48 kHz so the encoder gets samples at one of Opus's native input rates without resampling. Server-side NAudio playback (output: "server" | "both") is unchanged.
  • web-ui/src/audio/ServerAudioRenderer.js decodes Opus packets via the browser-native WebCodecs AudioDecoder (Chrome 94+, Safari 16.4+, Firefox 130+ — universal in 2026). Decoded AudioData is copied into a Web Audio AudioBuffer and scheduled on the same per-connector nextStartTime cursor as before, so the rest of the pipeline is unchanged. The legacy PCM16 path (format: "pcm16", pcm field) remains as a fallback for any emitter that can't link Concentus.
  • docs/audio-streaming.md updated: the remaining migration step is now binary WS frames (when JSON+base64 envelope overhead becomes the bottleneck or when video / file-transfer streams want the same binary pipeline). Until then, JSON-wrapped Opus stays as the simpler thing that works.

v0.25.28 — May 4, 2026

Fixes

  • Remote browser tabs hung on "disconnected" after a server restart. A remote client stores its login token in sessionStorage, but the server's session table is in-memory — restarting the server invalidates every previously-issued token. The browser can't read the 401 returned by a failed WebSocket upgrade, so useServer would happily keep retrying with the dead token forever and the user had to do a hard refresh to recover. The fix is two-sided: /api/auth/status now reports sessionValid when the caller presents an X-Auth-Token, and both App.jsx (on mount) and useServer.connect() (before each WebSocket attempt) verify a stored token via that endpoint. When the server says the token is dead, the client clears sessionStorage, drops the cached token in api-shim.js, and dispatches muxit-login-required so the login page shows up automatically. __muxitSetAuthToken(null) now also removes the sessionStorage entry, so the api-shim cache and storage stay in sync.

v0.25.27 — May 3, 2026

Audio is now a first-class driver stream

Server-side drivers can now stream audio to every connected dashboard, alongside video and other stream kinds. AudioSynth gains a multi-mode output config (server | browser | both, default browser) that picks where the synthesised audio actually plays. Cross-platform fix as a side effect: AudioSynth used to be Windows-only via NAudio; in browser mode it now works on Linux and macOS hosts because the audio is rendered server-side and decoded in the browser via the Web Audio API. This is also the unifying primitive for upcoming server-side TTS connectors (chatterbox, cosyvoice, OpenAI TTS, ElevenLabs) — they will plug into the same path with no new infrastructure on the client side.

  • AudioSynth output config. New connector field with three values:
    • browser (default) — every connected dashboard plays the audio. Multi-client by construction (broadcast through the existing per-client stream subscription model). Cross-platform.
    • server — original v1.2 NAudio path, unchanged. Still Windows-only.
    • both — runs the two paths in parallel; the local-output half fails silently on non-Windows hosts so the streaming half always works.
  • Wire format. Per-chunk JSON in the data field of stream.data: { "op": "chunk", "pcm": "<base64-pcm16>", "sampleRate": 44100, "channels": 1, "format": "pcm16" }. A { "op": "stop" } frame is emitted on cancellation so dashboards drop their queued audio. Real-time pacing (~50 ms chunks) so a client can schedule chunks back-to-back without running its jitter buffer dry.
  • web-ui/src/audio/ServerAudioRenderer.js — singleton on the dashboard side. Lazily creates an AudioContext, primes it on the first user gesture, decodes incoming PCM16 frames to AudioBuffers, and schedules them per-connector with a nextStartTime cursor so chunks for the same source play seamlessly. Different connectors play independently and may overlap. Not TTS-specific; any future driver that emits an audio stream goes through this single playback path.
  • web-ui/src/hooks/useServerAudio.js — auto-subscribes to every connector that declares an audio stream (via schema.streams). Mounted in Shell.jsx and MobileShell.jsx.
  • web-ui/src/hooks/useServer.js — emits a per-chunk muxit-stream-data window event in addition to the React-state path. Audio chunks need every frame; the existing requestAnimationFrame coalescing on streamData is right for video (only the latest matters) but wrong for continuous audio.
  • script.say() is unchanged — still routes through the browser's Web Speech API via the FIFO TtsController. For server-side voices the canonical pattern is explicit: connector('chatterbox').speak("..."). No implicit re-routing of say(), no global config field that would change its behaviour invisibly.
  • TODO documented: raw PCM16-in-base64 trades bandwidth for simplicity (~700 kbit/s per active stream). Migration plan to Opus + binary WS frames + MSE lives in docs/audio-streaming.md. The JSON wrapper keeps format as a discriminator so adding format: "opus" later is forward-compatible.

v0.25.26 — May 3, 2026

Text-to-speech rewrite

The dashboard's TTS pipeline was rewritten around a single shared controller (web-ui/src/tts/TtsController.js) with a strict FIFO queue and a pluggable provider abstraction. Previous symptoms — "TTS sometimes works, sometimes doesn't, and clicking the speaker button releases a buffer of accumulated speech" — came from useTTS (chat) and useScriptSpeech (script say()) writing directly to window.speechSynthesis without coordination, plus a gesture gate that accumulated arbitrary callbacks until any user click drained them all at once. The new controller serialises every utterance through one worker, primes the speech engine on the first user gesture once and never queues callbacks outside the job FIFO, so a late click can never release a backlog.

  • web-ui/src/tts/TtsController.js — singleton with FIFO queue, single async worker, gesture priming, per-source filtered stop() (stop({ source: 'chat' }) cancels chat without stopping a script say()), and provider switching at runtime.
  • web-ui/src/tts/providers/BrowserProvider.js — Web Speech API wrapper conforming to a small speak(job) → Promise / cancel() / getVoices() contract. Future providers (chatterbox, cosyvoice, OpenAI, ElevenLabs) plug in without touching consumers.
  • web-ui/src/hooks/useTts.js replaces useTTS.js; useScriptSpeech.js now routes through the controller. ttsGesture.js is deleted — the gate logic lives inside the controller and no longer queues callbacks.
  • SettingsEditor/VoiceSection.jsx "Test" button now plays through the controller too, so it shares the same priming and queue with the rest of the system.

Emotion hint on say()

Scripts can now pass an emotion hint: say("Calibration done!", { emotion: "excited" }). The hint flows through script.say WebSocket events (new emotion field, null when omitted) and is applied by the active TTS provider. The browser provider approximates with rate/pitch tweaks for a small built-in vocabulary (excited, happy, sad, whisper, serious); server-side providers added in a later release will honour the field natively.

v0.25.25 — May 2, 2026

Features

  • ONVIF driver gains software digital zoom. New writable properties digitalZoom (1.0–16.0), zoomX and zoomY (each -1..1) crop a 1/zoom × 1/zoom window of the live frame and rescale it back to the camera's native resolution with bilinear interpolation, so the live video stream and any active recording keep their nominal resolution while showing the cropped region. Implemented as OnvifDriver.ApplyDigitalZoom, called per frame after ApplyFlips and before the recorder + JPEG encode — both the continuous capture loop and the snapshot() paths (live + cold-open) pick it up. The (zoomX, zoomY) offset is normalized against the available pan range at the current zoom level (±1 puts the crop edge flush with the image edge), so the zoomed window can never extend outside the source frame regardless of zoom level — no clamping logic needed downstream. Coordinates apply on the displayed image, after flipH / flipV, so they always match what dashboards and AI vision pipelines see. Useful for AI tasks where you want to inspect a region without driving optical PTZ — software zoom takes effect on the next frame, not after a multi-second move-and-settle. Stored as volatile float so reads/writes from the capture thread and ws-handler thread don't need a lock. The starter template gains zoomReset / zoomCenter2x example methods to demonstrate the property syntax.

v0.25.24 — May 2, 2026

Startup log cleanup

Boot output was a wall of unrelated lines (built-in driver names, half-related warnings, network diagnostics that ran before Kestrel had bound). Same content, now grouped under ── Built-in drivers ──, ── Security ──, ── Network ──, ── Extension drivers ──, ── Services ── (and ── Licensing ── when there's something to report). Built-in drivers collapsed to one line listing their names. The "Driver registry: N drivers" tail is now N extension driver(s) loaded · N total in registry, printed under the extension scan. Auth-token / editor-mode / HTTPS / scan results route through a single StartupBanner helper (MuxitServer/Logging/StartupBanner.cs) for consistent ✓ / ⚠ / ✗ prefixes.

Fixes

  • MailKit security advisory (NU1902 / GHSA-9j88-vvj5-vhgr). Bumped MailKit from 4.7.1.1 to 4.8.0 in MuxitServer.csproj. The warning was also being re-printed at every node start.js server because start.js ran an explicit dotnet build and then dotnet run, which does its own implicit incremental build and re-prints the same NuGet warnings. start.js now passes --no-build to dotnet run whenever the explicit build succeeded.
  • Capability scan never worked for any free driver. DllDriverLoader.AuditCapabilitiesDetailed built its MetadataLoadContext resolver from the .NET runtime dir + the driver's own dir, but Muxit.Driver.Sdk.dll lives in the host's AppContext.BaseDirectory — so every scan failed with Could not find assembly 'Muxit.Driver.Sdk' and the audit reported "scan failed" instead of capabilities. Added AppContext.BaseDirectory to the resolver paths; AudioSynth / Midi / GenericScpi now report their actual capability set (FileSystem / Network / Process / …).
  • Auth token "denied" then "written" contradiction. On Windows, File.WriteAllText fails with Access denied when overwriting an existing file that has the Hidden attribute set — which is the attribute we set on the previous run. The catch printed a Warning, then we unconditionally printed Auth token written to: …, so every restart claimed both. Program.cs now clears the Hidden flag before writing and only prints the success line when the write actually succeeded.
  • False "No TCP listener detected on port N" warning. NetworkDiagnostics.GetBannerSummary() was called inline before app.RunAsync(), so the listener probe ran before Kestrel had bound — every boot reported a phantom "Kestrel may have failed to bind" hint. Moved the diagnostics + the Listening: / WebSocket: lines into lifetime.ApplicationStarted.Register so they print after the bind has actually happened. Same callback also kicks off Phase 2 (extension scan), so ── Network ── appears before ── Extension drivers ──.
  • Cryptic "Method not found" for SDK-drift drivers. A driver compiled against an older Muxit.Driver.Sdk (e.g. before the Details parameter was added to PropertyDescriptor) failed loading with the raw MissingMethodException: Void Muxit.Driver.Sdk.PropertyDescriptor..ctor(System.String, System.String, System.String, System.String, System.String). DllDriverHost.ScanAsync now catches that case explicitly and prints ✗ <file>.dll — incompatible Muxit SDK (rebuild and repackage with the current SDK).

v0.25.23 — May 2, 2026

Features

  • ONVIF driver pushes a snapshot stream per snapshot() call. The driver now declares a second stream channel, snapshot, and emits the same base64 JPEG on it that snapshot() returns to the caller — both code paths (live RTSP grab and cold-open temp connection). Bind a Canvas widget to <connector>:snapshot (mode image) and you see exactly what the AI/vision pipeline received, with no polling overhead and no extra hardware traffic. Solves the most common "the AI's description doesn't match what's on screen" debugging problem: the snapshot can lag the live view by 0.5–1.5 s due to the camera's H.264 GOP / B-frame settings, and the widget now makes that lag visible. Returning the value through StreamEmitter was preferred over a lastSnapshot property because polling a 200–500 KB string at the default 1-second cadence would burn ~500 KB/s of WS traffic for no gain — streams only push when the value is actually produced.

v0.25.22 — May 2, 2026

Features

  • ONVIF driver gains absolute PTZ + status read. Added ptzMoveAbsolute({ pan, tilt, zoom }) for driving the camera to a specific position (vs. ptzMove's relative nudge), and ptzStatus for reading the current { pan, tilt, zoom, panTiltState, zoomState } — the move-state fields are useful for waiting until an absolute move has settled before issuing the next one. Implementation lives in OnvifClient.AbsoluteMoveAsync / GetStatusAsync; both go to the existing _ptzUrl SOAP endpoint and reuse the same WS-Security envelope as the rest of the PTZ surface. Cameras that don't implement absolute PTZ (some lower-end Profile S devices) return a SOAP fault — script accordingly with gotoPreset or relative moves as fallback. Coordinate space is camera-specific; most ONVIF cameras normalize each axis to [-1, 1], but a handful use degrees or raw step counts — a quick ptzStatus call after a known position discloses what your camera returns. The misleading "continuous-move velocities" wording on ptzMove is also corrected — RelativeMove is a one-shot delta, the camera stops on its own, no ptzStop needed.

Dashboards

  • Text Display widget supports multiline values and per-widget alignment. Strings containing \n now render across multiple lines (was: collapsed to a single line by the default white-space: normal). The widget config panel gains an align toggle (Left / Center / Right) so multiline values can be left-aligned for log-style output without affecting other Text Display widgets on the same dashboard. Mobile view picks up the same fix — root cause was that dashboard.css was only imported by the desktop entry, so the new white-space: pre-wrap rule was missing on mobile; MobileShell now imports dashboard.css too.

  • Dashboard editor stops squishing widgets. Switching into edit mode used to shrink the grid canvas by ~420 px (palette + config panel widths), changing every widget's aspect ratio. The grid now keeps its live-mode width while editing — the center pane scrolls horizontally if the panels would otherwise overlap the canvas. Single change in DashboardEditor.jsx; the existing overflow: auto on the center pane handles the scrollbar.

v0.25.21 — May 1, 2026

Fixes

  • AI tool flow restored after upstream strict-mode change. The Muxit cloud proxy / OpenRouter started rejecting the follow-up turn after every tool call with validation_error: Invalid request: Invalid input — every chat that needed a tool went unrecoverable on the second LLM call. Diagnosed via a new MUXIT_AI_DEBUG=1-gated dump of the full upstream error body (#411): the OpenRouter generation log showed the first call (returning tool_calls) succeeded with status 200, and the follow-up call was rejected by the validator before reaching generation. The only structural difference between the two calls was the assistant message we appended carrying content: null together with tool_calls. OpenAI's public spec still allows that combination, but a strict-mode rollout late April 2026 along the proxy → OpenRouter → upstream path no longer accepts it. Fix in AiSession.AddAssistantMessage (MuxitServer/AI/AiSession.cs): emit content: "" instead of content: null whenever tool_calls is present. Empty string is universally accepted, including by the local-LLM chat-template bridges (LM Studio Gemma, llama.cpp) the original null was already working around. (#412)

Diagnostics

  • AiService.Streaming logs the full upstream error body when MUXIT_AI_DEBUG=1. One _logger.Info line, runs before the structured-error parsing so the full body is captured even when extraction succeeds. Zero production-log noise; saved hours on the next upstream API change. (#411)

v0.25.20 — May 1, 2026

Security

Second hardening pass against the 2026-04-30 audit. Closes the three remaining sprint-sized items that round out the "if anything goes wrong, it doesn't stay quiet" theme.

  • Logger and EventBridge redaction. ServerLogger.Log already ran SecretRedactor.RedactMessage on its input, but script.output, agent.log, script.say, and connector.error events emitted straight onto the bus and bypassed it. A user script that did log.error(\Bearer leaked-token-…`)or a connector load failure that quoted rawapiKey: "…"landed in the browser console verbatim.EventBridgenow wraps each leak-prone field through the redactor at broadcast time.AuditLogrunsSecretRedactor.RedactJsonon thedetailstree before serializing, so bothworkspace/logs/audit.jsonland theaudit.log` WS event lose secret-named cleartext (knowing "user X wrote field 'apiKey'" suffices for forensics). (audit U16, #406)

  • Tamper alarm on safety.audit.enabled flip. A single config flag silently disabled the entire audit trail; nothing in the existing flow recorded the disable event itself. AuditLog now subscribes to config.changed, compares to a thread-safe _lastKnownEnabled, and on every flip writes a stderr WARNING + a structured line to a separate workspace/logs/audit-meta.jsonl that ignores the enabled flag. Startup with audit off produces an immediate audit-disabled-at-startup entry; runtime flips produce audit-disabled / audit-enabled entries plus an audit.tamper event for any future UI banner. Gated on IsLicensed so free-tier servers (which never had audit) stay silent. (audit U9, #407)

  • Per-WS rate limit on hardware/AI/marketplace calls. A leaked auth token (or runaway dashboard) could fire thousands of connector.call / scripts.start / ai.chat / marketplace.install per second. MessageRouter now enforces a sliding 1-second window per WebSocket on the gated set; over-budget returns WS_RATE_LIMITED with a slow-down message. Read-only / metadata calls (state.subscribe, connector.list, connector.schema, safety.get, license.get, …) are intentionally not gated — legit dashboards burst through them on connect. Default 50/sec per client, override via security.rateLimit.callsPerSec in server.json (clamped to ≥ 5 so a misconfiguration can't brick the server). (audit A9, #408)

Reverts

  • U5 — AI confirmation user-turn gate (#402) reverted (#410). Reverted while diagnosing a chat-breaking validation_error: Invalid input after every tool call. Subsequent investigation traced the actual cause to an unrelated upstream OpenAI/OpenRouter strict-mode rollout that rejected content: null on assistant messages with tool_calls — fixed in v0.25.21 (#412). U5's logic itself was never proven faulty, only untested against the AI flow. Re-attempt tracked in the audit follow-up — needs a stub-LLM provider in the test harness before re-merging so a real regression can be told apart from another upstream coincidence.

The remaining audit items (B1/B2 server-side license signing, U1 secret vault, U11/U12/U13/U14/U15, A2A8, plus U5 re-attempt) are tracked in internal-docs/operations/audits/2026-04-30-followup.md.

v0.25.19 — May 1, 2026

Security

Hardening pass following the 2026-04-30 internal security audit (internal-docs/operations/audits/2026-04-30-security-audit.md). Seven coupled fixes that close the immediate "trust boundaries cross the DLL layer" and "browser-side cross-origin / secret leakage" classes.

  • Premium driver signature is now mandatory at load time. DllDriverHost.ScanAsync performs the DriverSignature.IsOfficialDriver check before DllDriverLoader.Load runs Activator.CreateInstance, so an unsigned DLL in a premium-category package never gets to execute its constructor in the host process. Skipped DLLs surface via the driver.diagnostics event (PREMIUM_NOT_SIGNED, hard severity) so the Extensions panel still shows the rejection. lib/driver-manager/build.js now fails the build instead of silently shipping unsigned premium packages — set MUXIT_ALLOW_UNSIGNED_PREMIUM=1 only for local dev builds. (audit B3/B4, #399)

  • RequiresSafetyGates=false only honoured on signed drivers. DllDriverLoader.Load previously read [assembly: RequiresSafetyGates(false)] from any DLL and propagated it straight through ConnectorInstance so the safety gate became a no-op. Now the opt-out is gated on DriverSignature.IsOfficialDriver; an unsigned DLL that requests it gets the flag forced back to true plus a stderr warning. Closes the "any DLL bypasses every limit/confirm/audit row" path. (audit B5/U8, #398)

  • connector.schema no longer echoes secret-named driverConfig values. Fields whose key matches the existing SecretRedactor.SecretKeyPattern (apiKey, password, token, bearer, credential, auth_key, private_key, secret) come back as { value: null, secret: true, hasValue: bool }. Editing still works through the per-key connector.driverConfig.set endpoint, which only takes the single key being changed. UI work to render the new shape as "Set / Replace" is a follow-up. (audit U3, #396)

  • WebSocket upgrade rejects cross-origin requests. Browsers will happily open a WS from any page to ws://127.0.0.1:8765 as long as the auth token is present. WebSocketHandler.HandleAsync now compares Origin against http(s)://<Request.Host.Value>; non-browser clients (CLI, MCP, Python) don't send Origin and stay gated by the auth-token check alone. Closes the realistic DNS-rebinding / token-exfil-via-XSS attack vectors. (audit U6, #397)

  • Editor open-with extension whitelist. /api/editor/open-with previously called Process.Start with UseShellExecute=true on Windows and no extension check, so a workspace script that dropped a .bat/.cmd/.vbs/.lnk could trigger arbitrary code execution as the server user once the user clicked "open". The handler now whitelists document / image / video / audio / plain-text extensions only; everything else returns 415. macOS / Linux paths switch from string interpolation to ProcessStartInfo.ArgumentList (defence in depth). RevealInExplorer got the same ArgumentList treatment. Editor mode remains opt-in (security.editorMode=true). (audit U7, #400)

  • AiDebugDump output is redacted. MUXIT_AI_DEBUG=1 writes the assembled chat-request body (including connector instructions, Authorization: Bearer … headers in the messages array, and any sk-… tokens the LLM reflected back) to workspace/logs/ai-last-request.json — exactly the file users grab when reporting bugs. The dump now deep-clones via parse, runs a new SecretRedactor.RedactJson walker on the clone, and stamps "redacted": true so it's obvious the file has been sanitised. RedactJson is reusable for the upcoming logger / event-bridge pass (audit U16). (audit U2, #401)

  • AI confirmation key requires a fresh user turn.Reverted in #410 during a chat-breaking outage. Subsequently traced (v0.25.21) to an upstream strict-mode rollout, not to U5 itself; U5's logic was never proven faulty, just never tested end-to-end. Original intent was to tag each pending entry with _userMessageCount at record time so AI can't auto-confirm its own retries within a single turn. Re-attempt blocked on stub-LLM test harness. (audit U5, #402)

  • Default-deny cross-connector access for connector configs. A connector config running in V8 could call connector("any-name") and reach into any other loaded connector — read state, invoke methods, exfiltrate via emit(). Pre-deploy this is fine because the user installs every connector themselves; once a marketplace exists it becomes a supply-chain vector. The opt-in is now explicit:

    js
    export default {
      driver: "...",
      allowedConnectors: ["mqtt", "psu"],
      ...
    }

    Self-reference is always allowed (composite/wrapper convention). Scripts under workspace/scripts/ go through ScriptHost / ConnectorBridge directly and are unaffected — every demo / template script that calls connector("test-device") keeps working without any opt-in. Workspace-template connector configs don't cross-call today, so no migration. (audit U4, #403)

The remaining audit items (B1/B2 server-side license signing, U1 secret vault, U16 logger redaction, A4 driver process isolation, et al.) are tracked in internal-docs/operations/audits/2026-04-30-followup.md.

v0.25.18 — April 30, 2026

Features

  • WebhookNotifier notify() now attaches images. Pass image as either a base64 string (with or without a data: prefix) or an absolute file path — the camera drivers' snapshot() output (base64 JPEG) flows through unchanged. For ntfy.sh the driver switches to PUT-binary mode (image bytes as the request body, message text moves to the Message header) so the push notification renders the snapshot inline. For Discord the driver sends multipart/form-data with payload_json + files[0] and the embed references the file via attachment://<filename> so it shows inside the embed. Generic flavor base64-encodes the image into the JSON body. Slack incoming webhooks can't upload files — the driver drops the image and emits a warn delivery event. Pattern (MuxitServer/Drivers/BuiltIn/WebhookNotifierDriver.cs:144-200):

    javascript
    connector("webhook-ntfy").notify("Robot status", "Voor inspectie", "info", {
        image: connector("webcam").snapshot(),   // base64 JPEG
    });
  • Three more notify() args: click (URL — ntfy/Discord make the notification clickable), tags (ntfy emoji shortcodes like "warning,robot"), and a new config-level markdown: true flag for ntfy that adds the Markdown: yes header so the body renders as markdown in the ntfy app. The flag is ignored when an image is attached because ntfy doesn't render markdown alongside attachments.

  • WebhookNotifier and EmailNotifier surfaced on the public marketplace page (website/src/pages/marketplace.astro) under built-ins. One-liner each, naming the concrete destinations (ntfy / Discord / Slack for the webhook; Gmail App Password / Resend / M365 / local relay for e-mail) so the marketing surface matches what ships out of the box.

Documentation

  • E-mail without typing your password. The "raw password in config" footgun was the most-cited friction point on the EmailNotifier — users (rightly) push back on dropping their Google password into a workspace file. The connector template (workspace-template/connectors/email.js) is now a four-path pick-list with all blocks commented except Gmail App Password: alongside Gmail it shows Microsoft 365, Resend (API key as password), and a no-auth local relay. Header and ai.instructions make explicit that password is never the user's real e-mail password — it's an App Password, an API key, or no credential at all. The Notifications guide (guides/notifications.md) gains a "E-mail without typing your password" table covering Gmail / M365 / Resend / SendGrid / Mailgun / Postmark / Brevo / SES / local relay, and explains why a transactional API service is usually the right call for any lab sending more than a handful of mails a day.
  • ntfy "raw JSON" pitfall documented in both the WebhookNotifier reference and the Notifications guide. The fix is almost always one of three things: calling post() instead of notify() (post bypasses flavor reshaping), flavor: "generic" against an ntfy URL, or expecting markdown without the markdown: true config flag. The webhook-ntfy connector template gains a header note steering users at notify() and an example block that shows tags, click URL, and image attachment in their idiomatic forms.

v0.25.17 — April 30, 2026

Features

  • Two new built-in drivers ship outbound notifications. Long-running scripts and threshold breaches can now reach the user out-of-band — the existing real-time WebSocket plumbing only helps when the browser tab is open. Both drivers live in the Communication group alongside MqttBridge and follow the same registration shape (MuxitServer/Core/MuxitHost.cs:69-70).
    • EmailNotifier (MuxitServer/Drivers/BuiltIn/EmailNotifierDriver.cs) sends SMTP via MailKit — handles STARTTLS, implicit-SSL, plain transports, and SASL auth out of the box, so Gmail App Passwords, Microsoft 365, and local relays all work without per-provider plumbing. The send action takes { to, subject, body, html?, cc?, bcc?, from?, attachments? }; when both body and html are supplied the message goes out as multipart/alternative with the plain version as fallback. Connection is opened, used, and closed per send — fine for the typical "few mails per run" rate. RequiresSafetyGates = true, so the first send to a new recipient triggers a confirmation prompt; subsequent sends to the same address pass through.
    • WebhookNotifier (MuxitServer/Drivers/BuiltIn/WebhookNotifierDriver.cs) POSTs to a fixed URL. A flavor config key reshapes the body for popular targets so scripts call a uniform notify(title, body, level) regardless of where the message lands: ntfy sends text/plain + Title/Priority headers (level → default/low/high/urgent); discord builds a webhook embed with color per level (info=blue, warn=orange, error=red, success=green); slack builds a text + colored attachment; generic posts JSON { title, body, level, timestamp } for n8n / Zapier / Make / custom backends. post(payload) bypasses flavor reshaping for full control. RequiresSafetyGates = false because the URL is fixed at config time — there's no runtime target choice to gate.
  • Four workspace-template connector starters ship with the new drivers (workspace-template/connectors/email.js, webhook.js, webhook-ntfy.js, webhook-discord.js), each preloaded with the right flavor and a placeholder URL or credential block. Drop in your value and the connector is live on next workspace reload.
  • Two demo scripts (workspace-template/scripts/demo/15-notifications.js, 16-threshold-alert.js) show the canonical patterns: the first runs a fake long operation, sends a short webhook push, then mails an AI-generated summary; the second polls a property with hysteresis-based debounce so a sustained high reading produces one alert, not one per sample (re-arms after the value drops 5 units below the threshold).
  • New documentation: Notifications & Alerts guide walks through channel selection, the three core use cases (run finished, threshold breach, hardware fault), severity-level mapping per flavor, and safety-gate behavior. Per-driver references at drivers/email-notifier.md and drivers/webhook-notifier.md cover properties, actions, streams, and config options. Sidebar entries added under Guides and Built-in Drivers (docs-site/.vitepress/config.mts).

v0.25.16 — April 30, 2026

Fixes

  • Editor running-line highlight now actually moves with the script. The Phase-2 execution-line tracer was effectively dead end-to-end — three independent bugs lined up to make it never produce a visible highlight: (1) __traceCallSite() in SandboxScript.cs walked new Error().stack looking for <anonymous>:LINE:COL and picked the first frame whose adjusted line was ≥ 1, but SandboxScript.InitCode is loaded via a separate engine.Execute(...) call so its frames also rendered as <anonymous> — the regex matched the helper's own new Error() line (~line 31 of InitCode) before ever reaching the user frame. Fix: evaluate the wrapped user script with DocumentInfo("muxit:user") (MuxitServer/Scripts/ScriptEngine.cs) so user frames render as muxit:user:LINE:COL, and key __traceCallSite and StackLineRegex / AdjustStackLines off that source name. The regex tolerates a [temp-N] suffix that ClearScript appends for transient docs in some versions. (2) Both effects in EditorGroup.jsx (the Monaco decoration and the subscribeTrace opt-in) gated on a strict ^scripts/.+\.js$ regex, but the workspace explorer hands the editor paths like workspace/scripts/foo.js. Neither effect fired, so the trace subscriber count on the server stayed at zero, __host.TraceEnabled returned false, and __traceCallSite short-circuited before reporting a single line. Both effects now use isScript() (the project's own predicate, accepting any */scripts/*.js), and the highlight key derives from activeFile.name to match the name the Run button passes to server.runScript. (3) subscribeTrace() in useServer.js was a no-op when issued before the WebSocket finished opening (wsRef.current was still null), and it never re-sent on reconnect. ws.onopen now resends the subscribe whenever traceSubRef.current > 0. docs/clearscript-quirks.md quirk #9 updated to record the InitCode-vs-user-script frame disambiguation gotcha.

Documentation

  • MCP setup instructions corrected for end users. The previous AI guide (guides/ai.md) and the in-app Settings → MCP panel (web-ui/src/components/SettingsEditor/McpSection.jsx) shipped three misleading snippets: (1) the Claude Code block claimed to be "already configured in .claude/.mcp.json" — wrong path (the actual project file is .mcp.json at the repo root) and wrong scope (it only applies when Claude Code is launched from inside this source checkout, not from a user's lab workspace); (2) the Claude Desktop snippet used dotnet run --project /path/to/MuxitServer which is a from-source invocation an end user with the published muxit binary can't run; (3) the ChatGPT block told users to paste http://127.0.0.1:8765/mcp directly into the connector dialog, but ChatGPT's connector UI rejects loopback / private-IP addresses as "unsafe url" and requires a publicly reachable HTTPS endpoint with a valid TLS cert — pasting the local URL just produces an Error creating connector. unsafe url toast every time. All three blocks now distinguish installed Muxit from source checkout: Claude Code shows a claude mcp add muxit -- /path/to/muxit --mcp --workspace … command for installed builds and keeps the existing dotnet run snippet as the source-tree variant; Claude Desktop's snippet now points at the absolute path of the installed muxit binary; ChatGPT documents the public-HTTPS-tunnel workaround (Cloudflare Tunnel one-liner) and labels it as a constraint of ChatGPT, not Muxit. Both the docs page and the Settings UI panel were updated to stay in sync.

  • ChatGPT-via-tunnel security warning. Because Muxit's HTTP middleware (MuxitServer/Program.cs:786-865) treats every loopback request as authenticated and a tunnel daemon (cloudflared / ngrok) running on the same host reaches Muxit over loopback, naively exposing the local /mcp endpoint over a public tunnel publishes the full hardware surface — read sensors, write properties, run scripts — with no credentials. The optional security.remoteAccess password gate is bypassed for loopback callers and uses a custom X-Auth-Token header that ChatGPT's connector can't send anyway, so it doesn't close the gap. Both the AI guide's ChatGPT section and the Settings UI's ChatGPT panel now lead with a stark warning and recommend acceptable mitigations: Tailscale Funnel restricted to your tailnet, Cloudflare Access in front of cloudflared, or short-lived per-session tunnels — and steer users with no tunnel infrastructure toward Claude Desktop / Claude Code, which connect locally over stdio with no exposure.

  • AI features tier table footnote. The capability matrix at the top of guides/ai.md advertised "Claude Desktop / Claude Code / ChatGPT" as the three supported MCP clients with no caveat; the ChatGPT-only public-HTTPS limitation is now flagged with an asterisk and a small footnote pointing at the new MCP Server section, and the What you get on Free paragraph now reads "ChatGPT (with a tunnel — see below)" so the marketing copy doesn't oversell the out-of-the-box ChatGPT experience.

v0.25.15 — April 29, 2026

Features

  • ONVIF driver gains flipH / flipV orientation toggles. Both are new config options and live R/W properties on the driver, so cameras mounted upside-down or facing a mirror can be corrected without re-opening the RTSP stream. Flips are applied in ApplyFlips() (MuxitServer/Drivers/BuiltIn/OnvifDriver.cs) immediately after decode and before recorder feed + JPEG encode, so both the live video stream and any active .mp4 recording reflect the flip. Implementation uses OpenCvSharp's Cv2.Flip with FlipMode.Y (horizontal mirror), FlipMode.X (vertical), or FlipMode.XY (180° rotation when both are set). No restart needed when toggling at runtime — the capture loop reads the volatile flags each iteration. Documented in docs-site/reference/drivers/onvif.md.

Changes

  • ONVIF capture pipeline tightened for lower latency. Two driver-side changes for the cameras whose 1–2 s end-to-end delay was bottlenecking the dashboard view: (1) OPENCV_FFMPEG_CAPTURE_OPTIONS now also sets max_delay=0 and reorder_queue_size=0, disabling FFmpeg's frame-reordering and demuxer-delay buffers (the rest — nobuffer, low_delay, framedrop=1, analyzeduration=0, probesize=32768 — was already there). (2) After opening VideoCapture, the driver now calls _capture.Set(VideoCaptureProperties.BufferSize, 1) to ask the FFmpeg backend for a single-frame internal buffer; backends that don't honor the request silently ignore it. The remaining 0.5–1.5 s of glass-to-screen delay is dominated by the camera's own H.264 encoder (GOP length, B-frames) and the browser's MJPEG decode pipeline — the ONVIF driver reference now has a "Latency" section documenting the camera-side knobs (substream profile, shorter GOP, disable B-frames, wired LAN) that move the needle further.

v0.25.14 — April 29, 2026

Features

  • Dashboard knob widget now handles fractional values and direct entry. Previously the knob always rounded display to one decimal (current.toFixed(1)) and used a step-rounding pass that left float noise (Math.round(0.15 / 0.05) * 0.05 = 0.15000000000000002), so a knob configured min:0, max:2.6, step:0.05 looked broken. The widget now derives display precision from step (e.g. step:0.05 → 2 decimals, step:1 → 0 decimals), strips post-rounding float noise via toFixed(decimals), and renders the value as an editable input — click it to type a specific value and press Enter (or click out) to commit; Escape cancels. Out-of-range or off-step entries are clamped/quantized on commit. Implemented in web-ui/src/components/dashboard/widgets/KnobWidget.jsx with new decimalsFromStep() and quantize() helpers and matching styles in web-ui/src/styles/dashboard.css.

  • Numeric config fields (min, max, step, duration, yMin, yMax, sendValue, lineWidth, maxLines) now accept decimal entry. The widget config panel previously parsed every keystroke through Number(val), which meant typing 0.05 was eaten alive: after 0. the parser collapsed it to 0 and re-rendered the input, swallowing the in-progress decimal point. A new NumericField component (web-ui/src/components/dashboard/WidgetConfigPanel.jsx) keeps the in-progress text in local state and only commits a parsed number on blur or Enter. Escape reverts; invalid input falls back to the last good value. The fix unblocks fractional knob configuration end-to-end (the widget supports it; the config panel was the bottleneck).

  • Dashboard editor exposes grid columns, row height, and layout-compaction mode in the right panel. A new "Dashboard" section (web-ui/src/components/dashboard/WidgetConfigPanel.jsx DashboardSection) sits at the top of the right-side editor panel and is visible whether or not a widget is selected. Columns is a 1–24 slider (default 12), Row height is a number input (px), and Layout toggles between Auto-stack (compact) (default — items pull up to fill whitespace, the previous behavior) and Free placement (preserves whitespace above widgets so users can leave intentional gaps). Free-placement mode swaps in noCompactor from react-grid-layout; new widgets dropped while in this mode get placed at the bottom of the existing layout (otherwise y:Infinity wouldn't resolve under noCompactor). All three settings persist in the dashboard JSON.

  • Subtle gridlines while the dashboard is in edit mode. A new .dashboard-grid.show-gridlines style (web-ui/src/styles/dashboard.css) paints faint 1px lines at every column and row boundary using two repeating-linear-gradient backgrounds. The --grid-col-step and --grid-row-step CSS variables are computed in JS from the current containerWidth, gridCols, rowHeight, and the grid's actual margin/padding, so the lines align with cell boundaries exactly and shift live as the columns slider changes or the editor pane resizes. Lines are off in live (non-edit) mode.

v0.25.13 — April 29, 2026

Features

  • Editor word wrap is now toggleable from the View menu. Previously the Monaco editor was hard-coded to wordWrap: 'off', so long lines (CSV rows, log dumps, single-line connector configs) ran off-screen and required horizontal scrolling. The View menu now has a Word Wrap entry (also bound to Alt+Z) that flips the editor between 'on' and 'off'. The setting persists in the muxit-layout-state localStorage key alongside the other layout preferences, applies live across all editor groups, and is reflected with a checkmark in the View dropdown. Documented in guides/keyboard-shortcuts.md.

Changes

  • The free tier is now limited to one active SCPI (GenericScpi) connector. The overall connector budget is unchanged (free=5, maker=10, pro+=unlimited), but a new scpi-connectors feature in the license registry sub-limits how many of those slots may be SCPI instruments — free=1, maker and above unlimited. Enforced in three places in MuxitServer/Connectors/ConnectorManager.cs: ResolveEnabledSet skips additional SCPI configs at startup (the first SCPI connector wins; the rest land in the disabled list with a Not enabled (SCPI limit: 1 on Free tier) reason), AddConnector rejects programmatic creation past the limit with CONN_LIMIT_REACHED, and SetEnabledConnectorsAsync validates the count when the user changes the enabled set from the UI. Match is by driver name (GenericScpi, case-insensitive) — connectors built on the JS serial-monitor driver or any TCP/serial transport that happens to send SCPI bytes are not affected, by design (the limit is on the convenience driver, not on raw byte traffic). Existing paid tiers are unchanged.

Documentation

  • Free vs Maker AI capabilities are now spelled out. The AI guide (guides/ai.md) leads with a new AI features by tier section: a side-by-side comparison table covering MCP, Chat Panel, voice, ai(), AI-assisted SCPI authoring, Vision AI, Local LLM, and Autonomous Agents, followed by three short narrative blocks — What you get on Free (MCP plus your own AI client is a complete workflow for a one-person lab), What Maker unlocks (the things that live inside the Muxit window — chat, voice, ai() in scripts, AI-assisted SCPI, 500 credits/month), and Beyond Maker (Vision, Local LLM, Agents at Pro). The Script API reference (reference/script-api.md) had ai() mis-listed as Pro-tier — corrected to Maker, with a vision caveat for the ai(prompt, image) form (still Pro) and a pointer to MCP for Free users. The AI-assisted SCPI guide (guides/ai-scpi-authoring.md) now opens with a tier note that points Free users to two equally good paths: hand-authoring with the declarative GenericScpi schema, or driving the same probe_scpi_device / validate_connector_config / write_connector_config tools from their own AI client over MCP. Marketing-side, the maker page restructures AI you actually use into a clear Free → Maker progression and reframes Local LLM as a Pro-tier feature, and the pricing page (website/src/pages/pricing.astro) now sells MCP on the Free card as a real workflow — "point Claude Desktop, Claude Code, or ChatGPT at Muxit and your AI client drives your hardware, runs scripts, and drafts connectors. Your provider, no Muxit credits." The Maker card emphasises that built-in chat and voice live in the Muxit window (no external client to juggle), and the AI credits — how they work info block was rewritten as AI on Free vs paid plans, presenting MCP and Muxit AI as two first-class paths instead of burying MCP as a footer afterthought.

v0.25.11 — April 28, 2026

Documentation

  • Script API docs and examples now lead with the .file(path) handle pattern. The Script Guide (guides/scripts.md), Script API Reference (reference/script-api.md), the FileAccess driver reference's "Example Connector" / "Example Script" sections (reference/drivers/file-access.md), the UI tour's CSV-charts blurb (getting-started/ui-tour.md), and the AI script-authoring system prompt (MuxitServer/AI/Prompts/script-api.md) were all written before v0.25.10 added handles, so every example still showed the path-on-every-call form (files.writeText({ path: 'temps.csv', content: ... }), files.appendText({ path: 'temps.csv', content: ... })). They now demonstrate the handle pattern as the recommended path: const csv = files.file('temps.csv'); csv.write(header); csv.append(row);. Standalone actions are still documented in the FileAccess action table (and still work — positional args, object-arg, both unchanged) but the worked examples no longer teach the noisier form. The default workspace connector's writeArray helper (workspace-template/connectors/fileaccess.js) was also rewritten on the handle (c.file(args.path).write(...)) so the example custom method matches the docs.

v0.25.10 — April 27, 2026

Features

  • FileAccess driver gains a path-bound file(path) handle for compact write/read syntax. Repeating the path on every call (fa.writeText({ path: "runs/log.csv", content: line })) was the most-cited papercut in the script API, especially in append loops. The new file(path) action returns a FileHandle host object whose methods bind the path once: const f = fa.file("runs/log.csv"); f.write(header); f.append(row); f.read(); f.exists; f.size; f.info(); f.delete(); f.rename("runs/done.csv"). The handle is a CLR object surfaced via ClearScript [ScriptMember] attributes, so the path-binding closures live on the .NET side and survive the script ↔ connector V8 boundary cleanly — pure-JS handles would have been stripped by V8Interop.NormalizeJsValue's function-discarding traversal. All operations delegate to the parent driver's existing path-resolution, size-limit, and workspace.file.changed plumbing, so handle calls obey the same sandbox rules as the standalone actions. Methods are synchronous (f.write(...) returns "OK", no await) to match the rest of the connector proxy. The handle's rename updates itself in-place so subsequent calls operate on the moved file. Default connector config (workspace-template/connectors/fileaccess.js) and the FileAccess template advertise the new pattern in ai.instructions so the AI prefers it. Standalone actions (writeText, readText, appendText, …) are unchanged and still accept positional args via MergePositionalArgs. Reference: docs-site/reference/drivers/file-access.md "File Handles" section.

Features

  • System prompts now have a minimal profile for local LLMs with 4–8K context windows. A new ai.promptProfile config key ("standard" default, "minimal" opt-in) controls how much of the platform context ships in the system message and tool-definition list. In minimal mode:

    • Connected Devices collapses to one line per connector (- name (driver) — instructions) plus a single hint to call get_connector_schema(name) when properties/actions are needed.
    • Decision-tree fragment (decision-tree.md / decision-simple.md, ~1.5 KB) is skipped — short tool-description summaries already steer tool choice for the small models that need this profile.
    • Safety-confirmation fragment swaps from the 3.3 KB worked-examples version to a 0.6 KB rule sheet (safety-confirmation-minimal.md). The chat gate still parks numeric writes and script-authoring; the model just learns the protocol from the short version. Skipped entirely when the active safety mode is off.
    • Tool descriptions in the OpenAI function-calling list collapse to the first sentence of each tool's entry in tool-descriptions.md. PromptRegistry.GetSectionShort cuts at the first ./!/? that's followed by either end-of-string or whitespace + an uppercase letter, so decimals (1.5) and abbreviations (e.g.) don't trip it. ToolRegistry.GetDefinitions(groups, minimal: true) flips the lookup; AiService reads the profile from config and passes the flag through. Saves several KB across the ~50 registered tools — for the worst offenders (run_script, run_code, probe_scpi_device, write_dashboard, read_manual_pages) the first sentence is what the model needs to call them; recovery / argument detail is something it can rediscover from failures or the JSON schema.

    On a typical 5-connector lab the system message drops from ~13 KB to ~1.5 KB and the tool-definitions list drops from ~14 KB to ~3 KB. The model trades occasional tool round-trips for ~7× total shrink — right tradeoff for Ollama / LM Studio backends, wrong tradeoff for cloud models, hence opt-in. Documented in docs-site/reference/configuration.md and docs-site/guides/ai.md.

Changes

  • MCP tool-result JSON keeps non-ASCII characters literal. MuxitServer/Mcp/McpJsonOptions.{Default,Indented} now set Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, so em-dashes (), units (°, µ, Ω), +, <, and similar safe punctuation appear as one literal byte instead of expanding to a six-byte \uXXXX escape. Affects every MCP tool result the AI sees (notably list_connectors, get_connector_schema, read_property results that quote device strings) and the device-schema JSON the agent-mode prompt embeds via SystemPromptBuilder.BuildForAgent. The "unsafe" name refers to embedding JSON in HTML/JS, which we never do for tool results — only the LLM consumes them. Saves a few percent on text-heavy results and several KB on schemas with many separators.

v0.25.8 — April 25, 2026

Bug Fixes

  • Hitting the model's context window now shows a friendly chat message with a "Start new chat" button instead of a raw HTTP error. The streaming SSE error parser in AiService.Streaming.cs now classifies upstream errors against a marker list (context_length_exceeded, prompt_too_long, request too large, context window, exceeds the context, …) and throws a MuxitException with the new AI_CONTEXT_LENGTH_EXCEEDED error code. The web client's useChat reads err.code (set by muxit-client from the server error envelope) and tags the message with errorCode: 'AI_CONTEXT_LENGTH_EXCEEDED'; ChatPanel renders an inline Start new chat button next to the error so the user can clear the bloated session in one click. Detection covers OpenAI's error.code = "context_length_exceeded", Anthropic's prose message, OpenRouter's upstream_status body, plus the plain-text 4xx that older Ollama builds return without a JSON envelope. Previously this surfaced as Error: AI service error (invalid_request_error) (upstream 400): ... and broke TTS, scrolling, and the loading indicator in confusing ways.
  • Muxit AI test connection no longer 502s with "anthropic/claude-haiku-4 is not a valid model ID". OpenRouter retired the bare 4.0 Anthropic slugs in favour of the dated 4.5 IDs, so the proxy was rejecting every request that used the shipped default. AiService.DefaultModelId, the FALLBACK_MODELS / DEFAULT_FAVORITE_MODELS / DEFAULT_MODEL constants in the web UI, the AI_PROVIDERS[muxit].defaultModel, the ModelCache fallback list, and the doc references all move to anthropic/claude-haiku-4-5 and anthropic/claude-sonnet-4-5. Test config in tests/fixtures/test-server.json updated to match.
  • Switching AI providers now refreshes the model list and picker. useModels was only fetching once per mount, so flipping ai.provider from Muxit to Ollama / LM Studio left the chat header dropdown showing OpenRouter cloud slugs that the local backend rejects. The hook now takes a providerKey (the active provider id) and re-fetches whenever it changes; SettingsEditor and ChatPanel pass config.ai.provider through. The chat-header ModelDropdown also receives activeProviderId: on a local provider it lists everything from /models whose provider matches (Ollama / LM Studio / openai-compatible), so the picker only ever offers models the active backend can actually serve. Stale cloud favourites still live in ai.favoriteModels for when the user swings back to Muxit, but they no longer surface against a local endpoint.
  • Local LLMs that emit tool calls as JSON in content now actually trigger the tool. Qwen3, Hermes-3, and gemma chat-template bridges sometimes ignore the OpenAI tool_calls field and return the call as JSON in the assistant message body — {"tool": "read_property", "arguments": {...}} (or wrapped in <tool_call>...</tool_call>, or fenced in a json `` block). Previously this rendered as raw JSON in the chat and the user saw nothing happen. AiService.Streaming.ExtractEmbeddedToolCalls is a post-stream fallback (only runs when no structured tool call landed) that recognises all three shapes, accepts name/tool and arguments/parameters/input field aliases, and promotes the payload to a real tool-call entry the loop executes. Streaming text deltas whose first non-whitespace char is { or <tool_call> are also held server-side until the extractor decides — if it isn't a tool call after all, the buffer flushes through ai.delta so prose still streams; if it is, the JSON never reaches the chat.

v0.25.7 — April 25, 2026

Bug Fixes

  • Multi-positional args from scripts no longer create empty files named Microsoft.ClearScript.V8.V8ScriptItem+V8Array. A script call like connector("fileaccess").writeText(path, content) was forwarded across the V8 bridge as a single ClearScript ScriptObject (a V8 array, not a CLR object?[]) — ConnectorManager.MergePositionalArgs only checks for object?[], so it fell through to the scalar-wrap branch and produced {path: <V8Array>}. The driver's ArgString(args, "path", "") then .ToString()'d the V8Array, yielding the type-name string, and WriteText happily wrote an empty file at that bogus path. The single-arg object form writeText({ path, content }) worked because of a downstream normalization, but the same fragility was waiting on every multi-positional call — rename(from, to), appendText(path, content), writeBinary(path, content), and any extension driver action with two or more ArgDescriptor entries. The v0.25.2 positional-args fix only covered the WebSocket path (where JSON deserialization already produces CLR types); the V8 path was still broken. Three layered fixes: (1) ConnectorBridge.CallAsync now runs V8Interop.NormalizeJsValue on the incoming args, recursively converting V8 arrays → object?[] and V8 objects → Dictionary<string, object?> before they reach MergePositionalArgs. (2) MergePositionalArgs throws VALIDATION_INVALID_VALUE when more positional arguments are passed than the action declares, naming the action's expected signature instead of silently dropping the extras. (3) Muxit.Driver.Sdk.DriverConfig.ArgString/ArgInt/ArgDouble/ArgBool reject dictionary, collection, and ClearScript ScriptObject values up-front with "Argument 'X' expected Y, got Z", so even if marshalling regresses the driver fails loudly instead of writing files named after CLR class names. Numeric-to-string coercion (writeText({ path: 42, ... })"42") is preserved.

v0.25.6 — April 25, 2026

Features

  • Script and connector errors now point at the right line. script.done carries a structured error object — { message, line, column, stack, details } — instead of a flat string. Line numbers refer to the user's source: the wrapper preamble that ScriptEngine.cs injects to make top-level await and main()-style scripts both work prepended 3 lines, and every <anonymous>:N:M reference is now adjusted by ScriptEngine.WrapperPreambleLines before it leaves the server (ScriptEngine.AdjustStackLines). ConnectorLoader.TransformImports keeps line counts stable too — stripped import statements are replaced with newline-equal padding instead of being collapsed, so V8's reported lines line up with the user's .js. Connector load failures broadcast a new connector.error message with the same line/column shape.
  • The web UI marks the offending row. EditorGroup.jsx calls monaco.editor.setModelMarkers against the active script or connector file when its name appears in useServer's new scriptErrors / connectorErrors maps. The squiggly carries the V8 stack as the hover message; markers clear automatically on the next run start. BottomPanel renders an Lnn chip next to error log lines that have line info — clicking it opens the file (muxit-open-file event → Shell.handleFileOpen) and reveals the line via editor.revealLineInCenter / editor.setPosition.
  • The editor highlights what a running script is parked on. A new script.line broadcast (with paired script.line.subscribe / script.line.unsubscribe ref-counted on the server) reports the current user-line whenever a script blocks on delay, await connector.x, ai, ask.*, say, or stream. The trace is gated by ScriptHost.TraceEnabled so the in-script __traceCallSite() helper short-circuits when no dashboard is watching; throttled to one event per 50ms per script and de-duplicated on the prior line. EditorGroup paints a Monaco line decoration (soft-amber background plus a ▶ glyph) for the active file's running line. Useful for lab automation where a script can sit on await delay(30000) or a slow await psu.voltage = 12 for a long time.

v0.25.5 — April 25, 2026

Features

  • Muxit AI can now talk to a local LLM instead of the Muxit cloud proxy. A new ai.provider config selects between muxit (the existing managed proxy, default), ollama (local Ollama daemon, default http://localhost:11434/v1), lmstudio (LM Studio's local server, default http://localhost:1234/v1), and openai-compatible (any other server exposing /chat/completions — vLLM, llama-server, …). Per-provider settings live under ai.providers.<id>.{baseUrl,apiKey,model} so switching back doesn't lose the previous endpoint. Settings → AI Services now shows a provider picker with a per-provider Test connection button (powered by a new config.test_provider WebSocket message) that probes the endpoint, validates auth, and reports how many models the server has loaded. The chat UI, the ai() script global, the agent inference loop, and the AI vision object detector all transparently route through the active provider, so the same dashboards / scripts / agents work whether you're on the cloud or fully offline. Vision uses the OpenAI image_url content block, which works against multimodal Ollama models (llava, qwen2-vl, llama3.2-vision) without changes. Local providers gate behind a new Local LLM Pro feature (local-llm in the registry); the Muxit cloud path still rides on the existing ai feature, so Free/Maker tiers behave exactly as before.

Architecture

  • AI/LLM call sites consolidated behind ILlmProvider. MuxitServer/AI/AiService.cs, MuxitServer/AI/AiService.Streaming.cs, MuxitServer/Scripts/ScriptHost.Ai.cs, MuxitServer/Agents/AgentInstance.cs, and MuxitServer/AI/ObjectDetector.cs previously each built their own HttpRequestMessage against the hardcoded Muxit proxy URL with the license key as bearer. They now all go through MuxitServer/AI/Providers/LlmProviderFactory, which returns a cached MuxitProvider (cloud) or OpenAiCompatibleProvider (Ollama / LM Studio / openai-compatible) based on ai.provider. The streaming SSE parser, tool executor, safety gate, session store, and WebSocket message shapes are unchanged — adding a new provider only requires implementing ILlmProvider. License gates moved into the providers themselves: MuxitProvider enforces the ai feature, OpenAiCompatibleProvider enforces local-llm. ModelCache now merges the active local provider's /models response with the OpenRouter cloud list so the model picker shows what's actually loaded on Ollama / LM Studio.

v0.25.4 — April 24, 2026

Features

  • The AI always confirms numeric and scripted actions before running them. A new AI confirmation column was added to the safety matrix (SafetyPolicy.LevelBehavior.AiConfirm, the SafetyChip dialog, and the safety.custom.aiConfirm field in server.json). Every built-in level — Observe, Assisted, Active, Unrestricted — ships with Ask every time; only a Custom row may set it to Allow. While on, the AI is blocked from executing two classes of tool call without the user first approving them in plain text: write_property / call_action whose arguments contain any number (the AI restates each value with its unit — "setting voltage to 15 V, current limit to 0.35 A — confirm?"), and write_script / run_code (the AI summarizes the script's plan — connectors, ranges, step sizes, expected duration — before authoring or executing). Numbers the user just spoke are still echoed back, because voice input drops units and "15" on its own is ambiguous. Simple parameterless or boolean-only calls like power_on or reset continue to execute immediately — the gate only catches writes that carry numbers and script-authoring tools. The legacy per-level confirm_unknown_values / confirm_all / off mapping is gone; the column now drives every AI tool-call prompt. See the Safety Guide for the new flow.

Changes

  • The AI stops walking loops in chat. The decision-tree, decision-simple, and safety-confirmation prompt fragments now carry a mandatory "never walk a loop in chat" rule: anything that implies three or more device writes of the same property (a sweep, a range, "for each", "every N ms", "measure at X values", "log every step") must be authored with write_script or run_code and executed — not stepped through with repeated write_property calls. Before this, a 35-step current sweep produced 35 per-step confirmation round-trips; now the same request produces one kind: "script" plan confirmation and the script runs with per-step writes going through the hardware safety gate directly. The ClassifyIntentByKeywords classifier and MentionsScriptAuthoring gate were both extended so sweep/step/range phrasing activates the Scripts toolgroup and loads the script-api fragment even when the user doesn't say "script" or "loop" outright.
  • Identical tool-call retries no longer re-arm the confirmation gate. BuildConfirmationKey now canonicalizes the args JSON before hashing — object keys are sorted and numeric leaves are round-trip-formatted, so {"value":0} and {"value":0.0} collide to the same pending-confirmation marker. The old raw-string comparison let the LLM trip the gate a second time just by re-emitting the same call with different number formatting, producing "Setting current_set to 0 A (repeat) — confirm?" loops.

v0.25.3 — April 24, 2026

Bug Fixes

  • Typos in connector/driver property names now throw instead of silently returning a function stub. Previously, a script line like log.info(psu.voltage) — where the real property is measured_voltage — evaluated to a ghost function (the Proxy's generic dispatch closure) and string-interpolated into logs as a garbage function body, hiding the typo. The V8 Proxy get traps for (a) the script-sandbox connector(), (b) cross-connector access inside connector configs, and (c) the per-config __driverProxy used as c in custom methods, now branch three ways: known property → read value, known action → callable function, unknown → throw new ReferenceError("Connector 'X' has no property or method 'Y'") (or "Driver has no property or action 'Y'" for the driver proxy). The script-sandbox Proxy now also pulls bridge.ActionNames alongside PropertyNames to tell valid actions apart from typos; the other two already tracked both sets. set traps are unchanged — writes to unknown names already throw via ConnectorManager.CallAsync's ConnMethodNotFound path.

v0.25.2 — April 24, 2026

Bug Fixes

  • Positional args now work when calling driver actions from scripts. Previously a script like connector('fileaccess').writeText('data.csv', 'hello') failed with Error: Path cannot be empty — the script-bridge proxy forwards the call as {method: 'writeText', args: ['data.csv', 'hello']}, and ConnectorManager.MergePositionalArgs only converted an array to a named-arg dict when its last element was already a trailing options dict. Pure positional calls (and single non-dict scalars like readText('data.csv')) fell through unchanged, so FileAccessDriver.WriteText read ArgString(args, "path", "") off a bare array/string and got the empty default. MergePositionalArgs now also handles the pure-positional and single-scalar cases by mapping them onto the action's declared ArgDescriptor names, so writeText(path, content){path, content} and readText("x.csv"){path: "x.csv"}. The object-literal form (writeText({ path, content })) keeps working exactly as before.

v0.25.1 — April 24, 2026

Bug Fixes

  • TTS no longer replays the last reply on page refresh, and survives the browser's autoplay block. Browsers reject speechSynthesis.speak() with error: "not-allowed" until the tab receives a user gesture (autoplay policy) — previously this silently tore the speech chain down, stranded queued progressive chunks, and on a subsequent gesture suddenly flushed the whole backlog, which looked like "TTS was buffering and now it's firing everything at once." Both AI chat TTS (useTTS) and script-say() TTS (useScriptSpeech) now route every synth.speak() through a shared priming gate (ttsGesture.whenReady) that defers the call until the next click/keydown/touchstart; a not-allowed error re-arms the gate and retries the same chunk on the next gesture instead of terminating the chain. On page refresh, the chat auto-speak cursor is seeded from restored history so the last assistant reply isn't re-narrated — toggling TTS off then on still replays it explicitly, switching sessions via the chats panel resets the cursor, and new sessions / cleared chats drop it back so the next reply speaks. Two extra footguns were closed along the way: finish() now also drops the progressive backlog (so a hard error can't leave chunks that a later call would drain), and speakNext/speak/speakProgressive read voice/rate/pitch from refs so their useCallback identities are stable — otherwise the async voiceschanged event was re-identifying them, which re-fired the ChatPanel auto-speak effect and occasionally kicked a duplicate speak.

v0.25.0 — April 23, 2026

Features

  • Drivers can opt out of the safety gate. Drivers with no path to physical hardware and no destructive actions now declare requiresSafetyGates: false — connectors of that driver bypass the safety gate entirely (no limit checks, no confirmations, no rate caps, no audit rows). C# DLLs declare it at the assembly level via [assembly: RequiresSafetyGates(false)]; JS drivers set meta.requiresSafetyGates: false; built-ins override bool RequiresSafetyGates => false; on the driver class. Default is true, so every existing driver keeps its current behaviour — this is a pure opt-out. Built-in Webcam, FileAccess, and Onvif ship opted out. TestDevice keeps the flag on so it can serve as the canonical example for exploring the safety system (limits, confirms, simulate, audit) against a no-hardware target. The flag is surfaced in drivers.list and driver.schema WebSocket responses as requiresSafetyGates. Orthogonal to per-connector safety.overrideLeveloverrideLevel pins a gated connector to a specific level; requiresSafetyGates: false turns the gate off for the whole driver.

v0.24.5 — April 23, 2026

Changes

  • Serial connectors are three lines shorter. The built-in serial JS drivers (Gcode, SerialMonitor, Plotter, SerialProbe) now accept plain port and baudRate fields in their connector config and build the serial transport themselves. The old pattern — a bogus import { createSerialTransport } from "../../shell/src/transports/serial.js" (the path never existed; the loader stripped it with a regex), a pair of top-level const port = …; const baudRate = …; lines, and a hand-rolled _transport: createSerialTransport(port, { baudRate }) entry — is replaced by a two-field config block ({ port: "COM3", baudRate: 115200 }). Optional serial knobs (delimiter, timeout, dataBits, parity, stopBits) sit alongside. SerialProbe's exportConnector emits the new shape. The config._transport field still works as an undocumented escape hatch for non-serial transports (e.g. redirecting the Plotter driver at a TCP simulator via createTcpTransport) and for back-compat with existing workspaces — no C# host changes were needed; the cleanup is entirely in the four driver JS files, their templates, and the in-tree workspace-template connectors.

v0.24.4 — April 23, 2026

Changes

  • The legacy per-service AI safety mode is gone. Settings → AI Services no longer shows a Safety Mode radio (Confirm / Trust), and ai.safetyMode has been removed from the server.json schema and docs. The setting was already dead code — AiService.GetSafetyMode() derived tool-call prompt behaviour from the global SafetyGate.CurrentLevel (see v0.23.0), so the per-service config was accepted and silently ignored. Unknown config keys are preserved on disk, so any ai.safetyMode still in an existing server.json is harmless; removing it keeps the file tidy. Tool-call approvals are now controlled exclusively by the safety level picker in the StatusStrip SafetyChip — see the Safety Guide for the level-to-behaviour mapping.

v0.24.3 — April 23, 2026

Changes

  • The "auto" AI model is gone. It was confusing — users assumed it triaged simple requests to a cheap model and escalated only when needed. In reality the proxy did a static tier-based pick (Haiku for Free, Sonnet for Pro), which silently raised cost on Pro accounts. ai.model now defaults to anthropic/claude-haiku-4-5 (a single concrete ID for every tier) and the model picker in the Chat header and Settings → AI no longer offers an Auto option. Existing configs with "model": "auto" keep working: AiService.NormalizeModelId maps the legacy sentinel to the new default, and the UI's currentModel fallback now mirrors AiService.DefaultModelId so the picker always shows the model the server will actually call. Pin a different model by setting ai.model to any OpenRouter ID (e.g. anthropic/claude-sonnet-4-5).

v0.24.2 — April 23, 2026

Features

  • Restart Server is reachable without digging into Settings. The system-tray icon menu gained a Restart Server entry between Open GUI and Quit, and the Titlebar's File menu now has a Restart Server... item below Settings. Both use the same mechanism as the existing Settings → Server button (/api/server/restart for the UI, WorkspaceManager.WriteRestartFile() + IHostApplicationLifetime.StopApplication() for the tray), so the launcher's restart marker auto-relaunches the process. The File-menu action asks for confirmation before firing since menu items are easier to misclick than the Settings button.

Bug Fixes

  • Restart now takes ~2s instead of 20–30s. The generic host's ShutdownTimeout was defaulting to 30s, so every restart path (tray, Settings, /api/server/restart) had to wait for Kestrel to drain idle WebSocket subscribers before the launcher's restart marker could fire. HostOptions.ShutdownTimeout is now capped at 2s — long enough for in-flight HTTP requests to finish, short enough that stale WS connections don't hold up a relaunch. Clients reconnect on their own once the new process is listening.

v0.24.1 — April 23, 2026

Bug Fixes

  • Settings → Server tab no longer crashes on open. The Remote Access subsection referenced a closure variable (licenseHook) that wasn't in its scope, so every attempt to open the tab threw a ReferenceError at render time and was silently swallowed by React's ErrorBoundary, leaving the tab non-functional. The license prop now threads through SettingsEditorServerSectionRemoteAccessSubSection, and the remote-access tier gate renders correctly.

v0.24.0 — April 22, 2026

Features

  • Five-tier licensing lands in the server. The compile-time tier table in AppSettings.cs now matches the published pricing: Free → Maker → Pro → Lab → Enterprise, with ranks 0–4 and Lemon Squeezy product IDs wired for the four paid tiers. Count limits are raised in line with the marketing page (Free now gets 5 connectors / 3 scripts / 5 extension drivers; Maker gets 10 connectors and unlimited scripts and drivers; Pro gets unlimited connectors). AI, MQTT broker, remote access, startup scripts, and video recording move from Pro down to Maker; Vision AI and autonomous agents stay at Pro; SafetyLevel.Custom and the new Safety Policy Engine live on Lab. Dashboards are unlimited on every tier. The Settings → Subscription Plan Comparison table now renders all five tiers dynamically from the server's feature registry (boolean features show a tick from each feature's requiredTierId upward; count-limited features inherit through the tier rank), and the "Pro" badge / upgrade copy across the UI is driven by the blocked feature's blockedReason rather than hard-coded strings, so future tier moves don't need a UI patch.
  • Safety Policy Engine is a Lab-tier feature. The per-connector safety: { limits, confirm, rate, interlock, safeState, overrideLevel } block is now gated by the new safety.policy-engine license feature. SafetyGate keeps every connector's block parsed as before, but wraps lookups in a tier check: on Maker/Pro the block is ignored (with a one-time console warning per connector) and the global SafetyLevel policy table applies unchanged, so downgrades never silently remove safety — they fall back to a coarser but still-enforced policy. On Lab and Enterprise the block is fully honoured.

v0.23.0 — April 22, 2026

Features

  • Graduated safety levels with an audit trail. Every write and action across the server now flows through a new SafetyGate that resolves to a single safety levelObserve (simulate every write/action so nothing reaches hardware), Assisted (real writes; ask once on large changes, ask every destructive action), Active (real writes; only destructive actions ask once), Unrestricted (everything passes; typed phrase to enable), plus a Custom row where the user picks an outcome per column. The level picker lives in the StatusStrip SafetyChip; applying a change is a single click in the dialog (no second confirmation — the dialog itself is the confirmation surface), with Unrestricted additionally requiring typing I UNDERSTAND inside the dialog. Once granted, a level stays active until the user picks a different one. Connector configs can add an optional safety: { limits, confirm, rate, interlock, overrideLevel } block — limits.min/max hard-block out-of-range writes at every level except Unrestricted, largeChangeFraction (default 25% of range) catches fat-finger deltas, confirm: { reset: "always" | "typed" } flags an action destructive so the level's destructive-action column applies, and overrideLevel pins a single connector to a specific level independent of the global. Blocked, simulated, and confirmed operations are appended to workspace/logs/audit.jsonl as flat {ts, event, caller, details} records — the record shape is deliberately extensible so a future phase can log every write without schema migration. Every origin (script:<name>, ai:<sessionId>, dashboard:<clientId>, agent:<name>, mcp:<id>, system:<subsystem>) is stamped onto the audit line. License gating is per-level: each level is a separate license feature (safety.level.observe, safety.level.assisted, safety.level.active, safety.level.unrestricted, safety.level.custom), and the audit trail is its own feature (safety.audit). Default ship config gives the two low-risk levels (Observe, Unrestricted) to free and the three lab-safety levels (Assisted, Active, Custom) plus the audit trail to pro, but every tier's allow-list is config-driven — future tiers (Maker, Lab, Enterprise, …) can mix and match any subset by bumping RequiredTierId on the relevant features in AppSettings.cs, no code changes in the SafetyGate. The chip and dialog are visible on every tier; locked levels appear in the dialog with a badge and disabled radio. A workspace moved to a tier that doesn't expose the persisted level is clamped to the lowest-risk allowed level on startup (falling back to Unrestricted only if the tier exposes no levels at all — a pathological config that keeps writes working until the config is fixed). The AI user-value confirmation is not gated by any of this: ai.safetyMode gains a third level — confirm_unknown_values (the new default) — which only prompts when a tool-call argument carries a numeric value the user did not actually say; existing confirm configs migrate to confirm_all automatically so no one loses existing prompts. The Chat Panel's pending-approval card now labels each argument as user-spoken (green) or AI-chosen (orange) so it is obvious why the confirmation is being asked.

v0.22.1 — April 21, 2026

Features

  • AI asks before it acts on complex requests — the default chat behavior no longer rushes to call tools on the first sentence of a multi-step request. For script authoring, agent setup, and other automation that touches real hardware, the AI now runs a short clarification exchange first (which device, what range, what step size, what timing, where to save data, what safety limits), offers sensible options inline, restates the plan in one sentence, then acts. Simple reads, setting changes, and questions still resolve directly without a round-trip. The change lives in MuxitServer/AI/Prompts/identity-fallback.md and a new "Before authoring" section in script-api.md, and the matching guidance was mirrored into the seeded workspace-template/config/instructions.md so fresh workspaces get the new defaults too.

v0.22.0 — April 21, 2026

Features

  • Workspace EULA acknowledgement + Sandbox mode — every workspace must now have an accepted EULA on file before Muxit will let any driver perform a hardware write. On startup the server checks {workspace}/.muxit/EULA-ACCEPTED.md against the canonical EULA bundled with the server (SHA256 of the body above the footer separator, LF line endings, UTF-8 no BOM). If the file is missing, corrupt, tampered, or outdated the workspace enters Sandbox mode: reads and polling keep working, but every property write and action call is refused at DriverRegistry with error code EULA_NOT_ACCEPTED, and script.run() refuses to launch. The web UI shows a blocking acceptance modal that renders the full EULA, asks for an identifier (name or email), and requires an explicit checkbox; accepting writes the acceptance file atomically and appends an entry to .muxit/eula-history.jsonl for the audit trail. Headless installs (--cli, --mcp, CI) accept via the new muxit eula accept --user <id> subcommand or the MUXIT_EULA_ACCEPT=1 / MUXIT_EULA_USER=… env-var pair. Minor/major EULA bumps require re-acceptance; patch bumps do not. See docs-site/guides/workspaces.md for the full flow.

v0.21.0 — April 21, 2026

Features

  • AI can now generate and edit dashboards. Three new tools — list_dashboards, read_dashboard, write_dashboard — let the Chat Panel AI (and any MCP client such as Claude Desktop / Claude Code) create .dashboard.json files directly in the active workspace. Ask "build me a dashboard for the PSU and the robot" and the AI calls list_connectors + get_connector_schema to discover valid bindings, then writes a complete JSON layout with gauges, sliders, charts, and action buttons wired to real device properties. Follow-ups like "add a chart of voltage" round-trip through read_dashboardwrite_dashboard so the existing layout is preserved. Nested sub-folders (e.g. robots/arm-1.dashboard.json) work for AI and MCP just like the HTTP API. File operations share the same validation rules as the HTTP dashboard API (filename must end with .dashboard.json, content must parse as JSON, no path traversal, sub-folders must already exist), so hand-edited and AI-edited dashboards stay interchangeable. Intent routing picks up the keywords dashboard, widget, gauge, panel, layout so the group only loads when relevant.

v0.20.0 — April 20, 2026

Features

  • SCPI driver speaks USBTMC — the GenericScpi driver now supports a third transport alongside TCP/LXI and Serial: USBTMC (USB Test & Measurement Class), the USB-native protocol used by modern bench instruments with a front-panel USB-B port (Rigol DS1000Z/MSO5000, Keysight DSO-X, Siglent SDG/SSA/SDS, Owon, most R&S gear). Set transport: "usbtmc" in the connector config and provide vendorId / productId (hex or decimal) — an optional serial filter disambiguates multiple of the same model. The driver wraps every SCPI line in a USBTMC DEV_DEP_MSG_OUT BBB header, implements the two-step REQUEST_DEV_DEP_MSG_IN + multi-transfer reassembly for reads, issues INITIATE_CLEAR on connect and INITIATE_ABORT_BULK_IN on drain recovery — enough of the spec for reliable SCPI round-trips against every mainstream instrument. Uses libusb-1.0 via LibUsbDotNet (native binaries auto-bundled into the .muxdriver package for Windows). Requires WinUSB bound to the device via Zadig. See docs-site/reference/drivers/scpi.md for setup details.

v0.19.0 — April 20, 2026

Features

  • Built-in AI is now a Pro feature. The Chat Panel, the script ai() global, vision AI (identify_objects / locate_object), ai.credits, and the "test AI" button in Settings all now require a Pro subscription (or active Pro trial). Free-tier users who try these get a clean AI_REQUIRES_PRO error instead of consuming credits against the Muxit AI proxy. The Chat Panel renders an upgrade CTA in place of the input box when AI is blocked. What stays free: MCP (bring your own AI client — Claude Desktop, Claude Code, ChatGPT), say() (browser TTS), ask.* dashboard prompts, and every non-AI feature. Agents were already Pro-gated; this just closes the remaining entry points to Muxit-hosted AI.

v0.18.1 — April 19, 2026

Bug Fixes

  • Default script template and docs no longer show trailing cleanup after while (script.running) loops. Pressing Stop cancels delay() and calls V8 Interrupt(), which terminates execution synchronously — so statements after the main loop (and finally blocks) were never reliably running, despite examples like log.info("script finished") suggesting otherwise. The built-in "new script" template, the sinewave demo, the scripts guide, the first-script walkthrough, the script API reference, and the AI script-writing prompt have all been updated to drop the misleading trailing cleanup and to spell out that Stop is a hard-kill. The stop behavior itself is unchanged — this is a pure docs/template alignment so scripts are written to be safe-to-abort at every iteration rather than depending on cleanup code that never runs.

v0.15.1 — April 19, 2026

Bug Fixes

  • Network diagnostics no longer leak LAN topology to remote callersGET /api/diagnostics/network now returns the full report (interface IPs, Windows Firewall rule list, PowerShell fix commands, elevation flag) only to loopback callers. Authenticated remote clients — including a client using a leaked session token — get a redacted summary: interface counts, firewall profile booleans, and mDNS counters, but no addresses or rule names. The Settings → Server → Network Diagnostics panel now loads via HTTP (instead of WebSocket) so the loopback check governs the UI too; the WebSocket diagnostics.network handler always returns the redacted view because WebSocket handlers don't currently see peer IP. The startup console banner likewise drops the LAN-address list and Administrator flag, keeping only counts and firewall findings — safer when server logs are collected to shared files.

v0.18.0 — April 18, 2026

Features

  • Smaller drivers fill in the on-demand details layer — the Gcode, Plotter, AudioSynth, and SerialMonitor drivers now populate Details on the props/actions whose one-liners were hiding non-obvious behavior. Highlights: Gcode documents the GRBL status enum values, the send / home / reset / hold distinction around buffer flushing and step loss, and streamFile's line-count-based progress (which doesn't wait for motion completion); Plotter documents the canvas-API angle convention (0 = +x, CCW), lineTo chaining polylines until penUp breaks them, and the draw batch's non-atomic error behavior; AudioSynth documents volume/waveform applying to the next playback (not in-progress), the playMelody shorthand grammar with number-as-rest, and that overlapping playback calls replace rather than layer; SerialMonitor documents that port/baudRate are transport-owned sentinel values and that fast bursts need the output stream rather than lastLine polling. This completes the first pass of the details rollout — every shipped driver (except the intentionally minimal Template and the not-yet-stable Avantes) now carries extended markdown on its subtle props and actions.

v0.17.0 — April 18, 2026

Features

  • Extension drivers fill in the on-demand details layer — the premium Midi, Scpi, Vision, and Fairino extension drivers now populate Details on their most semantically dense props/actions. Highlights: Midi documents channel 1-16 vs wire-level 0-15, velocity-0 = Note Off, sendSysex auto-framing of F0/F7, and learn timeout behavior; GenericScpi documents query retry-once + timeout scope, settleMs applied to writes only, and readErrors iterating until the SCPI "No error" sentinel; Vision documents the color/contour tracker param schemas and calibrateColor HSV margins (±10 hue, ±40 sat/val); Fairino documents the 6-element joint/TCP layout with mixed mm/deg, null-vs-zero semantics, moveJ arc vs moveL straight-line paths, blending with blendT / blendR, singularity risk, stop flushing the queue vs pause preserving it, and disable letting the robot fall under gravity. As before, Details only flows through on-demand surfaces (doc disclosure, editor hover, get_*_schema) so the upfront AI prompt cost is unchanged.

v0.16.0 — April 18, 2026

Features

  • Built-in drivers fill in the on-demand details layer — the new Details field added in v0.15.0 is now populated across every built-in driver: Webcam, Onvif, MqttBridge, FileAccess, and TestDevice. The one-line descriptions stay the same (and the AI system prompt summary stays the same size), but opening a driver's doc page or hovering a method in the script editor now surfaces parameter enums, clamp ranges, side effects, and failure modes that the one-liners hid — e.g. Onvif's PTZ speed sign convention, FileAccess's write-size enforcement timing, MqttBridge's QoS levels and retain-flag semantics, Webcam's silent backend-dependent brightness/contrast normalization. The extra markdown only travels through on-demand surfaces (driver doc disclosure, editor hover popup, get_connector_schema / get_driver_schema) so the upfront token cost is unchanged.

v0.15.0 — April 18, 2026

Features

  • Driver descriptors gain an on-demand details layerPropertyDescriptor and ActionDescriptor now carry an optional markdown Details field alongside the existing one-line Description. The short description stays in the upfront AI system prompt and IntelliSense summary line (so per-request token cost is unchanged); the extended markdown travels only through on-demand surfaces: the driver doc page's new "Show details" disclosure, the script editor's hover/autocomplete popup, and the full-schema payloads of get_connector_schema / get_driver_schema. Authors put parameter enums, side effects, truncation caps, and failure modes in Details — everything a user or AI agent would otherwise have to discover through trial and error. The SerialProbe driver now demonstrates the pattern with extended docs on its five subtle actions (scanBaud, hypothesize, analyzeFrames, stimulus, exportConnector); other drivers fill in Details opportunistically.
  • Network diagnostics panel for "my device can't connect" triage — Settings → Server → Network Diagnostics now renders a live report: bind address/port, Kestrel listener state, network interfaces with per-interface mDNS advertise/skip reasons, mDNS query/answer counters, a UDP 5353 conflict probe (warns when Apple Bonjour / Windows mDNSResponder / Avahi is already bound), and — on Windows — the active Windows Firewall profile, whether any enabled rule covers the listening port, and any stale muxit Block rules overriding an Allow. The same report prints as a startup console banner and is exposed over HTTP (GET /api/diagnostics/network) and WebSocket (diagnostics.network), so a phone or laptop that can't reach the server can still curl it. The UI shows copy-paste PowerShell fix commands (add allow rule, remove stale block rules, set profile to Private); the diagnostics layer is strictly read-only and never touches firewall state. Backed by MuxitServer/Core/NetworkDiagnostics.cs, Http/DiagnosticsApi.cs, and WebSocket/Handlers/DiagnosticsHandler.cs.
  • SerialProbe port / baud configurable at runtime — the driver now configures its serial port and link-layer settings at session time via actions (listPorts, openPort, closePort, setBaudRate, setPort), so the AI can try different configurations without editing the connector config. scanBaud accepts an explicit portPath + reopenAt policy, exportConnector refuses to run without an active port (so the generated config captures the confirmed portPath + baudRate), and the starter template + example connector no longer pin a port — the AI picks one via listPorts / openPort at session start. The driver now also exposes five terminal-friendly streams — traffic (unified RX/TX with arrow prefix), rxHex, rxText, txHex, txText — alongside the existing frames / events; raw-hex output previously went through an un-wired emitter and never reached terminal dashboards. The legacy preconfigured config._transport path still works for backward compat.
  • Dev Console: driver registry publication status — the Dev Console Drivers tab now shows, per driver, whether the latest built .muxdriver matches what's published to the public muxit-drivers registry, with a direct action to publish or re-publish from the button row. Release routing in lib/driver-manager/publish.js targets the public muxit-io/muxit-drivers repo instead of the development repo.

Changes

  • Verbose AI request/response logging is now opt-in — the AI service no longer floods the server log with full proxy request bodies, SSE event dumps, or tool-call summaries by default. Set MUXIT_AI_DEBUG=1 before starting the server to re-enable the full firehose while diagnosing prompt or tool-format issues.

v0.14.0 — April 18, 2026

Features

  • Declarative SCPI schema keeps connector configs light — the GenericScpi driver now reads an optional config.scpi: { properties: {...}, methods: {...} } block and auto-synthesizes the SCPI boilerplate. An entry like voltage: { cmd: "VOLT", type: "float", unit: "V" } becomes a full read/write property (get sends VOLT?, set sends VOLT 5), and save: { cmd: "*SAV", arg: "int" } becomes c.save(3) → sends *SAV 3. A trailing ? on the command marks the property read-only. Top-level properties/methods with the same name still win, so vendor quirks can stay hand-written while the regular 80% shrinks to a single-line declaration. The bk-psu.js template loses ~40 lines of repeated parseFloat/${Number(v)}/.trim() boilerplate. Under the hood, DllDriverHost.GetDriverMetaForInstance now returns live per-instance schema to ConnectorInstance so declared properties reach the connector pipeline (DLL drivers that want per-config schema in the future get this for free). The SCPI driver is bumped to v1.2.0.

v0.13.0 — April 17, 2026

Features

  • Open file tabs refresh automatically on FileAccess writes — a .csv log being appended to from a running script now updates its chart (or text) view live, no close-and-reopen needed. FileAccess actions (writeText, appendText, writeBinary, deleteFile, rename) emit a new workspace.file.changed WebSocket event with the workspace-relative path; the editor debounces rapid events (~150 ms) and reloads any open tab pointing at that path. Tabs with unsaved edits are skipped so in-progress work isn't clobbered.
  • Script API prompt and docs steer the AI away from require('fs') — the AI script-sandbox prompt, the Script Guide, and the Script API Reference now include a dedicated "Working with Files" section that shows the FileAccess connector pattern (connector('files').writeText({ path, content }), appendText, readText, …) and explicitly calls out that require, import, and Node.js filesystem APIs are not available. Cuts down on AI-generated scripts that try require('fs') and fail at runtime.

v0.12.1 — April 17, 2026

Bug Fixes

  • AI chat TTS no longer gets stuck behind script speech — after the v0.12.0 split, the chat's progressive TTS only kick-started when the global speechSynthesis queue was idle, so any in-flight (or just-finished) script say() utterance left the chat chunks queued forever and the toggle felt broken. The chain now starts whenever its own iterator isn't running, regardless of what useScriptSpeech is doing on the shared queue — the browser still plays the utterances in order. Re-enabling the chat speaker after a response has already arrived now also speaks that last response, instead of silently waiting for the next one.

v0.12.0 — April 17, 2026

Features

  • Independent script speech toggle — script say() output now has its own speaker button (🔊) in the top status strip, decoupled from the AI chat TTS toggle. Mute the AI without silencing your scripts (or vice versa). The new toggle also makes say() audible while the chat panel is closed — previously the auto-speak only ran when the chat sidebar was the active panel. Persisted as voice.tts.scriptEnabled (default on).

v0.11.0 — April 17, 2026

Features

  • CSV files open as charts — opening a .csv or .tsv file from the File Explorer now renders it as an interactive line chart (powered by the existing uPlot dependency). The parser auto-detects the delimiter (,, ;, or tab), whether the first row is a header, and each column's type (timestamp, number, or text). By default the first timestamp column becomes the X-axis and every numeric column becomes a Y-series; both can be changed from the controls bar. A toggle in the tab's control strip switches between chart view and the raw Monaco text view, so the files produced by FileAccess.appendText(...) in measurement scripts are now immediately inspectable without wiring up a dashboard widget. Files over 100 000 rows are capped on render (with a banner) to keep the chart responsive. No new dependencies.

v0.10.0 — April 17, 2026

Features

  • AI no longer hangs on long-running scriptsrun_script and run_code now accept wait_seconds (default 15, max 120) and return status: "running" with a run_id, recent output, and a next_seq cursor if the script doesn't finish within the window. The script keeps running; the assistant stays responsive. Three new tools — get_script_status, get_script_output (with since_seq cursor), and wait_for_script — let the AI poll progress, fetch incremental log output, or block again with a bounded wait. Pass wait_seconds: 0 for known background scripts (monitors, watchers) to kick off and return immediately. Per-script output is retained for about 5 minutes after completion so polls that arrive just after the script exits still see the final result and log tail.

v0.9.1 — April 17, 2026

Bug Fixes

  • Midi driver release-ready — shipped with its own first-class template.js (the old MidiDriver.Template.cs C# string has been removed), registered in the documentation sidebar and the Built-in Drivers index, and given accurate manifest tags (["midi", "audio"] — a new midi tag was added to the controlled vocabulary so it's discoverable in the Extensions panel). The driver source and behaviour are unchanged.

Features

  • Generic SCPI driver 1.1.1 — consolidates the earlier 1.1.0/1.1.2 pair into a single coherent release:
    • Stale-response framing — every transaction drains inbound bytes before sending, auto-resyncs on query timeouts or empty responses, and retries once with a known-clean RX buffer. Fixes the "values off by one" / NaN flicker on slow instruments.
    • settleMs option — optional post-write pause held inside the transport lock, so a set-then-query sequence reads back the freshly applied setpoint on instruments that need a few ms to commit.
    • debug TX/RX stream — when debug: true, every transaction's command and response is emitted on the scpi stream (for Terminal-widget live tails) and written to server stdout.
    • Cleaner defaults & internals — TCP host default is now the RFC 5737 placeholder 192.0.2.100, the unused IScpiTransport interface was removed in favour of the existing abstract base, and the *IDN? fallback now only swallows TimeoutException / InvalidOperationException instead of every Exception.
    • New reference pageScpi Driver documents every property, action, config option, the framing/recovery model, and a BK Precision example connector.

v0.9.0 — April 16, 2026

Features

  • Guided tour via the AI chat — new Help → Guided Tour menu in the title bar with one-click starter prompts ("Install a Driver", "Create a Connector", "Drag Properties into a Script", "Build a Dashboard", plus a general overview). Each entry pins the AI chat open and sends a pre-written question so the assistant walks you through the task. Starter prompts are not a gate — you can ask for a tour in your own words and get the same behaviour.
  • AI docs-reading tools — three new tools (list_docs, read_doc, search_docs) give the AI direct access to every page of this documentation site. Pages are embedded into the server binary at build time so they're available offline and always match the running version. The chat picks them up automatically when your question includes phrases like how do I, walk me through, tour, tutorial, getting started, or what is — expect the assistant to quote the relevant guide back instead of guessing. Dev-mode reload is available via MUXIT_DOCS_DEV=1, mirroring the prompt fragment hot-reload. Backed by a new DocsRegistry and DocsToolHandlers.

v0.8.0 — April 16, 2026

Features

  • New driver: SerialProbe — AI-assisted serial protocol reverse-engineering. A Tier 1 bundled JavaScript driver that captures raw bytes in labelled windows, auto-detects framing (lines-lf / lines-crlf / stx-etx / fixed:N), runs local CRC-16/Modbus and XOR-trailing checks, correlates stimulus/response pairs, and exports a working connector config when a hypothesis is confirmed. The AI never sees raw traffic — only summaries returned by local analysis — so a full session stays cheap. See reference/drivers/serial-probe.
  • Raw-byte streaming on SerialTransportsetRawMode(true) + onRawData(hex, timestampMs) bypass line buffering and deliver per-chunk hex with timestamps. portPath and baudRate are now accessible from JS, enabling drivers like SerialProbe to spawn temporary transports for baud-rate scans.

v0.7.0 — April 16, 2026

Features

  • New MIDI extension driver — live input/output for any USB or virtual MIDI device, per-channel accept-lists, input→output pass-through, internal 24-PPQN clock with start/stop/continue transport, Standard MIDI File playback and recording (sandboxed to workspace/midi/), panic (All Notes Off + All Sound Off + Reset All Controllers on every channel, with local activeNotes bookkeeping cleared), and a learn action that resolves the next incoming message for binding physical knobs/pads. Incoming messages are normalized to { type, channel, note?/controller?/…, tsMs } and published on three streams: events (everything), note (just Note On/Off), and clock (24-PPQN ticks, opt-in via emitClockStream: true). Built on DryWetMIDI and ships as muxit-midi-v0.1.0.muxdriver — install via the Extensions panel. Full reference: Midi Driver.
  • AudioSynth extension driver — new free tier-3 driver that plays tones, musical notes, and short melodies on the server host's default audio output via NAudio. Four waveforms (sine / square / triangle / sawtooth), volume control, click-free attack/release envelopes, and replace-not-queue playback semantics so a new sound always cleanly replaces the previous one. Actions: playTone(frequency, duration), playNote(note, duration), playMelody(notes) (shorthand "C4:200,E4:200" or [{note,duration}, …]), beep(), stop(). Properties: volume, waveform, playing. Ships with a workspace-template/connectors/audio.js example wiring up chime(), alert(), and fanfare() helpers. See AudioSynth reference — useful for lab-side "experiment complete" cues, CNC/robot feedback tones, and safety alerts.
  • New driver tagsaudio, synth, and notification added to the controlled tag vocabulary (and lib/driver-manager/schema.js) so audio-class drivers are discoverable in the Extensions panel.

v0.6.0 — April 16, 2026

Features

  • Centralised AI instructions and tool registry — every AI-facing prompt fragment (script API guide, decision tree, agent identity, per-autonomy blurbs, TTS rules, tool descriptions) now lives as markdown under MuxitServer/AI/Prompts/ and is loaded at runtime by a new PromptRegistry. A new ToolRegistry owns the authoritative tool list and dispatch; per-group handler classes under MuxitServer/AI/ToolHandlers/ replace the 1,040-line monolithic AiToolExecutor. Chat and agent system prompts are composed by a shared SystemPromptBuilder from these fragments, eliminating the parallel implementations in AiService.BuildSystemPrompt and AgentInstance.BuildAgentSystemPrompt.
  • Dev-mode prompt hot-reload — setting MUXIT_PROMPT_DEV=1 makes PromptRegistry read fragments from the source tree on every lookup, so prompt edits are visible without rebuilding the server.

Bug Fixes

  • Consistent script-writing guidance — resolved contradictions between write_script/run_code tool descriptions (which showed await dev.prop()) and the Script API guide (which said "Do NOT use await"). Canonical text now lives once in Prompts/script-api.md and is referenced from everywhere else.
  • Robot-specific rules scoped to the robot section — the workspace-template/config/instructions.md rule "do not add delays in the script" previously appeared to apply to all scripts; it is now inside the ### robot device subsection where it belongs, and the generic Script API guide keeps delay() as the recommended loop pacing tool for non-robot devices.
  • Edit connector visibility from the Connectors panel — expanded connector cards have a pencil (✎) icon that enters an edit mode. In that mode every property, action, and stream gets an eye-icon toggle and each section header gets hide all / show all shortcuts; hidden items appear greyed-out so they can be toggled back on. Both driver-sourced items and custom methods / properties defined in the connector's own methods / properties blocks can be hidden or exposed (previously only driver items). Changes are written back to the connector's .js source (the top-level hide: […] array, clearing any expose: whitelist to keep the file consistent) and the connector is hot-reloaded — the AI picks up the new surface on the next turn, no server restart required.
  • Edit primitive driver-config from the Connectors panel — edit mode also reveals a Config section that lists each driver-config key with an inline editor (text / number / checkbox). Committing a value surgically rewrites the matching literal in the .js source (either at the top level or inside the nested config: { … } block, whichever already holds the key) and hot-reloads only that connector. Non-primitive entries (expressions, transports, objects) are shown read-only with a tooltip explaining that the file must be edited directly. Framework-injected internals (anything whose key starts with _, e.g. _workspacePath) are never exposed.
  • Auto-open Config on load error — when a connector fails to initialize (wrong serial port, unreachable host, ...) the card automatically expands into edit mode with the Config section visible, so the problem can be fixed without opening the file. A failed reload keeps the connector in the enabled list with its InitError set, so the Config panel stays reachable for the retry.
  • Compact pill switch for enable/disable — the per-connector checkbox is replaced with a small toggle-pill to avoid the visual collision with the pencil/checkmark icons elsewhere in the header. Property rows show the R/W access label inline again (previously only in the tooltip).
  • New WebSocket endpointsconnector.visibility.set and connector.driverConfig.set drive the new UI. connector.schema gains an optional includeHidden: true flag and its response now includes driverConfig, hiddenItems, and visibilityMode; streams are now returned as [{name, hidden}] objects (the UI already handled both forms).

v0.5.1 — April 15, 2026

Features

  • License status on the premium driver detail page — when a premium driver (e.g. Fairino) is installed, the driver detail page now shows a clear license banner: "License active", "Trial — N day(s) left", or "License required" with an actionable message. marketplace.detail carries new licensed, licenseStatus, licenseMessage, and trialDaysRemaining fields for locally-loaded premium drivers.

v0.5.0 — April 15, 2026

Features

  • Premium driver management in the Dev Console — new Premium tab provides a single source of truth for premium drivers (currently Fairino, Vision, Avantes). Each row joins the driver's manifest.json with its build / sign / package state and its Lemon Squeezy product binding. Inline editor for the commerce block writes back to drivers/<name>/manifest.json so the binding ships inside the signed .muxdriver. Optional Push to LS mirrors manifest storefront copy to the Lemon Squeezy product via the v1 REST API (requires LEMON_SQUEEZY_API_KEY env var on the dev console).
  • Manifest commerce block — driver manifests now carry an optional commerce block (lemonSqueezyProductId, lemonSqueezyVariantId, price, storefront.{tagline,longDescription,images}) for premium drivers. At server boot, DllDriverHost reads the product ID and feeds it into LicenseManager.RegisterDynamicFeature(...) so a customer's Lemon Squeezy activation can be matched back to this driver. Validator emits new rules (COMMERCE_MISSING_FOR_PREMIUM, COMMERCE_LS_PRODUCT_MISSING, COMMERCE_LS_PRODUCT_FORMAT, COMMERCE_PRICE_INVALID, COMMERCE_FREE_FORBIDDEN).
  • node drivers.js premium-status CLI — new headless command reports the build / sign / commerce readiness of every premium driver. --strict exits non-zero on any outstanding issue, suitable as a release gate.

v0.4.2 — April 15, 2026

Bug Fixes

  • Installed drivers appear immediately after install — the server now broadcasts drivers.changed over WebSocket after a marketplace install or uninstall, so the Extensions panel's Installed tab refreshes without needing a reconnect.
  • Premium badge in Available tab — the marketplace.search response now includes the category field, and the Extensions panel renders a Premium tag next to premium drivers on the Available tab (not just the Installed tab).

Features

  • Version selector for drivers with multiple registry entries — when the registry exposes several versions of the same driver id, the Available tab now renders a single card with a version dropdown instead of one card per version. marketplace.install accepts an optional version field so clients can pin a specific version; omitted, the server installs the highest-versioned entry.

v0.4.1 — April 14, 2026

Bug Fixes

  • Wake word changes apply instantly — updating the wake word phrase in Settings → Voice now restarts the background listener immediately instead of requiring a page refresh.
  • Microphone selection applies instantly — picking a different input device while hands-free listening is active now reopens the capture stream with the new device, no refresh needed.

Features

  • "Saved — applied immediately" indicator in Settings — every change to Voice, AI Services, and AI Behavior settings now shows a transient confirmation badge so users can see the change took effect without refreshing the page.

v0.4.0 — April 14, 2026

Breaking Changes

  • Removed DEBUG-only license bypassAppSettings.DevMode, the surrounding #if DEBUG compile-time switch, and the license.dev_simulate WebSocket endpoint have all been deleted. License enforcement now applies uniformly to every build configuration, closing the risk that a mis-shipped Debug binary would silently disable tier / count / entitlement gates and expose a backdoor that could forge any subscription tier. Side effect: 8 of the 9 build warnings emitted by dotnet build MuxitServer -c Release (all CS0162 Unreachable code detected) are gone, plus a related CS8602 nullable-reference warning in Program.cs.

v0.3.2 — April 14, 2026

Bug Fixes

  • Fix AI script prompt to teach canonical property syntax (db55b4e)
  • Fix Hardware pane polling pile-up on state updates (303b1a1)
  • Fix Hardware pane writing to devices on mount (49b3e2e)
  • Fix SCPI driver stale-response framing + add settleMs / resync (31e2b14)
  • Fix GUI page flickering on driver info / marketplace panes (7919baa)
  • Fix Scpi manifest entryPoint drift + add validator rule to catch class (52c31bc)
  • Fix marketplace install on Windows + route premium drivers correctly (7e23aa8)
  • Fix dev console crash on boot — stale MUXIT_SERVER_URL reference (faf845e)
  • Fix Overview/Diagnostics tabs not refreshing when MuxitServer comes up (804ae1a)
  • Fix dashboard Knob widget first-quarter unresponsiveness (f28d33d)
  • Fix outdated await/function-call patterns in docs and examples (25b3589)
  • Fix DeepEquals numeric type coercion causing phantom serial writes (ae33d8d)
  • Fix serial overload from knob and improve SCPI log accuracy (b48f460)
  • Fix BK-PSU serial overload, property types, and store updates (cf39a4f)
  • Fix PSU polling timeout blocking other connectors (d0e67b6)
  • Fix dashboard Knob widget first-quarter unresponsiveness (93709bb)
  • Fix outdated await/function-call patterns in docs and examples (90ae0de)
  • Fix DeepEquals numeric type coercion causing phantom serial writes (63b11f5)
  • Fix serial overload from knob and improve SCPI log accuracy (de753ac)
  • Fix BK-PSU serial overload, property types, and store updates (9db1d0f)
  • Fix PSU polling timeout blocking other connectors (776a8c2)
  • Fix docs site CSP errors by using path-conditional security policy (0cfe29e)
  • Fix ai() script command returning empty response (6eabb38)
  • Fix log copy formatting: use tab-separated fields instead of line breaks (e3b0403)
  • fix: preserve InitializeAsync wrapper for MCP stdio mode (5addd16)

Features

  • Self-pace Hardware pane polling with setTimeout chain (c67fd82)
  • SCPI driver 1.1.2: also write TX/RX to stdout when debug=true (1cdfb28)
  • Rename sidebar tabs, add create shortcuts, fix Connectors show-all crash (e3baab9)
  • Bump SCPI driver to 1.1.1 for version verification (5d8c590)
  • debugging SCPI (8cd8894)
  • Flatten workspace/drivers and keep .muxdriver packages intact (95aa67c)
  • fixed docs build error; fix PSU connector (2765ef1)
  • Extract Vision into a premium extension; drop muxit-vision static feature (8c7fc96)
  • Expose EventBus to drivers via an SDK-level IDriverHost (67cbc84)
  • Dev console: rename pass + clickable sources + promote/extract refactor (7178573)
  • Dev console UI cleanup: collapsible driver cards + Overview clarity (1618e7f)
  • Phase 6c: deployment simulation — local mocks for GitHub + driver registry (5718ad5)
  • Phase 6b: driver authoring — per-driver manifest editor in dev console (270a629)
  • Phase 6a: expanded per-tab docs + new Release pipeline tab (2b2ce8a)
  • Proxy MuxitServer HTTP + WS through dev console to bypass CORS (8da35cf)
  • Auto-discover MuxitServer host:port instead of hard-coding 127.0.0.1:8765 (ed73a02)
  • Add Driver/Feature Overview tab + DEBUG tier simulator to Dev Console (0ff217f)
  • Replace in-server Dev Tools panel with a standalone Dev Console (Phase 4) (eada670)
  • Revert "Add Dev Tools panel skeleton with Workspace + Diagnostics tabs (Phase 4)" (bb3aeb3)
  • Add Dev Tools panel skeleton with Workspace + Diagnostics tabs (Phase 4) (c36710a)
  • Add server-side driver validator and surface diagnostics in UI (Phase 3) (5a6b1bd)
  • Reorganize muxit.js menu and split driver build out of build.js (Phase 2) (985ec91)
  • Codify driver manifest schema and structured validator (Phase 1) (f06fd90)
  • Gate SCPI TX/RX stream behind debug config option (7adf534)
  • Throttle property writes to prevent knob/slider flooding serial devices (cdc6b0a)
  • Add SCPI command/response stream to PSU dashboard for debugging (395d24a)
  • updated manifests (7ddea2a)
  • added manifests for avantes, fairino, scpi drivers (dd9b3a7)
  • Gate SCPI TX/RX stream behind debug config option (5fae791)
  • Decouple workspace from git — use extensions for driver development (b760d92)
  • Throttle property writes to prevent knob/slider flooding serial devices (0b131e2)
  • Add SCPI command/response stream to PSU dashboard for debugging (b634499)
  • Add clean URL rewriting for docs so help tab links work (b814c86)
  • Add AI Chat toggle to View menu, sort sidebar tabs to match icon order (a873bc5)
  • Add mobile/desktop view switcher, pin AI chat to secondary sidebar (9af64d2)
  • fixed robot enable state (befc1f8)
  • robot dashboard (016547a)
  • test script (e354dd2)

Other

  • refactor: split startup into core + background phases for instant GUI loading (cae93d6)

v0.3.2 — April 14, 2026

Bug Fixes

  • Hardware pane no longer writes to the device on mount — the desktop Hardware pane (ConnectorBrowser) polled writable properties by sending args: [] in its connector.call WebSocket message. An empty array is not null, so the server routed the call as a SET with an empty-array payload, which JS coerces to 0 for numbers and true for booleans — silently reprogramming instruments the moment you expanded a connector card (e.g. writing VOLT 0 / CURR 0 / OUTP ON to a power supply). Fixed on both sides: the UI now omits the args field for reads, and ConnectorManager.CallAsync normalises an empty args array to null so no future caller can hit this trap.

v0.3.1 — April 14, 2026

Bug Fixes

  • SCPI driver: stale-response framing fixGenericScpi over TCP and Serial no longer mis-attributes a late reply from a prior (timed-out) transaction to the next query. The transport now drains inbound bytes at the top of every transaction, owns its own line buffer on TCP (removing the hidden StreamReader buffer), and enters a self-healing "resync" state on timeout / empty-response so the cascade that made voltage reads collapse to 0 after a few iterations is closed out. The connector-level parseFloat hazard that rendered NaN / object [0] in the Hardware pane is addressed by a new num() guard in bk-psu.js and by the driver rejecting empty SCPI responses outright.
  • SCPI driver: echo stripping — instruments with local-echo enabled are handled automatically (the driver peels one echo layer before returning the response).

Features

  • SCPI driver settleMs option — hold the transport lock for N ms after every non-query write, so a VOLT 5 / VOLT? round-trip reads back the freshly applied setpoint without needing delay() in the connector.
  • SCPI driver: shared transport base — TCP and Serial transports now share ScpiTransportBase, collapsing ~130 lines of duplicated locking / pacing / framing into one place.

v0.3.0 — April 14, 2026

Features

  • Flat workspace/drivers/ layout — the free/ and premium/ subdirectories are gone. Installed drivers now sit as single .muxdriver package files directly under workspace/drivers/; the server extracts each package transparently into a hidden workspace/drivers/.cache/<id>@<version>/ on load. The category in each package's manifest.json is the sole source of truth for premium enforcement — a DLL's location on disk no longer matters.
  • Package-native install/uninstall — installing a driver now copies the .muxdriver file into workspace/drivers/ untouched; uninstalling deletes it. Stale cache entries are garbage-collected automatically on the next scan.

Breaking Changes

  • Any existing workspace/drivers/free/ or workspace/drivers/premium/ contents are no longer scanned. Reinstall drivers via the Extensions panel.
  • Editor API now blocks all writes under drivers/ (previously only blocked drivers/free/ and drivers/premium/). Installs must go through the Extensions panel.
  • node drivers.js sign now signs each premium driver's bin/publish/ DLL (pre-package). Re-run node drivers.js build afterwards to repackage.

v0.2.0 — April 6, 2026

  • Add revision history page accessible from Help menu and documentation sidebar
  • Bump version from 0.1.0 to 0.2.0

v0.1.0 — April 2, 2026

Initial development release.

Bug Fixes

  • Fix V8 OOM crash from sinewave script tight loop (#188)
  • Fix AI Service section hidden when credits fetch fails (#187)
  • Fix Muxit AI proxy integration — system prompt format, tools format, and error messages (#183)
  • Fix await delay() in connector configs resolving instantly (#182)
  • Fix scripts stuck in _running after stop, blocking new script starts (#179)

Features

  • Align docs styling with muxit.io and fix MQTT docs (#186)
  • Improve license key masking to show first 8 + last 4 chars (#185)
  • Add oscilloscope widget with phosphor glow and persistence effects (#184)
  • Add AI service card to license UI with keys, credits, and test button (#181)
  • Add smart tool filtering to reduce AI costs and improve cheap model reliability (#180)
  • Redesign license plan comparison to dynamic column grid (#178)

Muxit — Hardware Orchestration Platform