Revision History
All notable changes to Muxit are documented here, grouped by version.
v0.31.0 — May 8, 2026
Features
node build.js publishnow 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 singlearchiver-based helper that explicitly sets the+xbit onmuxit,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 withMUXIT_PUBLISH_RIDif 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:
bashcurl -fsSL https://raw.githubusercontent.com/muxit-io/muxit/main/install.sh | bashThe installer (
installer/install.sh):- Detects the distro, runs
sudo apt install ffmpeg libayatana-appindicator3-1 xdg-utilswith confirmation. - Adds the user to the
dialoutgroup if needed (serial-port access). - Downloads the latest
muxit-linux-x64-v*.tar.gzfrom GitHub Releases and verifies the SHA-256 againstchecksums.txt. - Extracts into
~/.local/share/muxit/(per-user, no sudo for the install itself;workspace/is preserved across reinstalls). - Symlinks
~/.local/bin/muxitso plainmuxitworks 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 dialoutSo 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-linuxcovers 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 — seedocs/follow-up-linux-installer.mdin the dev repo for the punch list.- Detects the distro, runs
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>.zipandmuxit-linux-x64-v<version>.tar.gz. The Linux tarball preserves the executable bit on themuxitbinary because it is created on the Linux runner.build.js publishnow defaults to the host platform's RID and accepts anMUXIT_PUBLISH_RIDenv override so a single dev machine still produces a relevant archive. Packaging is.tar.gzforlinux-*/osx-*RIDs and.zipforwin-*.Linux runtime requirements
shimat's manylinux build of
libOpenCvSharpExtern.soships a static FFmpeg without network protocols (nortsp://, nohttp://) and a GStreamer backend that registers but is non-functional. BothVideoCapture(rtspUri, FFMPEG)andVideoCapture(pipeline, GSTREAMER)returnIsOpened()=falseon Linux for any RTSP URL. Verified with the diagnostic attools/rtsp-probe/run.sh— five different in-process approaches all fail.The Onvif driver therefore delegates RTSP capture on Linux/macOS to the system
ffmpegbinary: spawnffmpeg -i rtsp://… -f mjpeg pipe:1, parse JPEGs from stdout, decode each one withCv2.ImDecode(which works fine in the same wrapper). On Windows the in-processVideoCapture(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:bashsudo apt install -y ffmpegThat'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.htmland streams entries from the in-memory ringbuffer over Server-Sent Events with level + substring filters, pause and download.Console.WriteLine/Console.Error.WriteLinecalls are now teed intoServerLogger, 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 runningmuxit.exewith 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 bymuxit-win-x64*.zipprefix, 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-instanceslicense 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.licensethat actually contains a key (paired with its signed.license-proof) and copies that one up. Launchingmuxitwithout--workspacewhen 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'sserver.jsonis 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 theDeactivatebutton 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
.pyfile inworkspace/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 genericPythondriver — 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 inAppSettings.cswithout 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-instanceswas capped at 5 inAppSettings, but the pricing page promises unlimited — bumped Pro to-1so the cap matches the marketing. Autonomous agents are now flagged (in development) on the pricing card and comparison table since theagentsfeature is stillDisabled = truein 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-sideai()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 = explicitai(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-proofto 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) andMETA["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). TheMUXIT_PROXY_BASE_URLandMUXIT_DISABLE_INSTANCE_LIMITenvironment-variable bypasses are now gated onMUXIT_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-prooffile stores an RSA-SHA256 envelope returned byPOST 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 itsvalid_untilis still in the future, itstieroverrides the local.license. Missing / tampered / expired → user is demoted tofreeuntil the next online re-validation. .licenseis now plaintext JSON. It still holds local bookkeeping (StartupCount,LastSeen,FingerprintComponents, customer info,BaseLicenseKeyandBaseInstanceIdfor 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.StartTrialnow callsPOST /v1/license/trial-startwith the machine fingerprint; the proxy holds an idempotent(fingerprint, feature_id)record. Deleting the workspace no longer grants a fresh trial.StartTrialis async now (Task<bool>). - Soft cutover for existing installs. A read-only
LegacyHmacReaderstill 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-saltfile. 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_URLenv-var override added toSignedLicenseClientandTrialClientfor staging / local-wrangler-dev testing without touching source.- Companion changes ship in MUXITPROXY (signing endpoint, trial-start endpoint,
trial_startsD1 table,LICENSE_SIGNING_PRIVATE_KEYworker 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:
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)
InitAsyncnow returns immediately after the synchronous catalog / interpreter pre-checks. The actual venv setup, pip install, subprocess launch, and init RPC run on aTask.Runworker (RunInitInBackgroundAsync) with a per-instanceCancellationTokenSource.- New
PythonInstanceStateenum:Loading | Ready | Error | Cancelled, plus aPythonInstanceStatusrecord 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,ExecuteActionAsynccall 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. ShutdownAsyncnow cancels the in-flight init token before tearing down. Disabling the connector mid-install kills the running pip subprocess (viaproc.Kill(entireProcessTree: true)inPythonVenvManager.RunStreamingAsync's newOperationCanceledExceptionbranch) instead of leaving torch's wheel-builder children running.ShutdownAllAsyncincludes 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.statusrebroadcast from the EventBus to all WS clients:{ connector, driver, state, message }. The existingdriver.python.installevent continues to carry per-line pip output for the server log;connector.statusis the higher-level state machine the UI binds to.
UI (web-ui/src/hooks/useServer.js, web-ui/src/components/ConnectorBrowser.jsx)
useServeradds aconnectorStatusstate map keyed by connector name; subscribes toconnector.statusmessages and updates the entry on every transition.ConnectorCardreads its entry fromserver.connectorStatus[name]and renders a one-line status badge below the header when state ≠ready: a spinning gear + the latest message forloading, a red ✗ + error message forerror, a muted ⊘ + reason forcancelled. Disappears once the connector is ready, leaving the normal card layout.
User-visible effect
- Activating
python-chatterboxnow returns instantly. The connector card immediately shows⚙ Installing: Creating venv at python/.venvs/chatterbox, then a flow of[pip] Collecting torch ...lines, thenLoading 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 inPythonDriverHost.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 installwrites its download progress with\r(no newline), and .NET'sProcess.OutputDataReceivedonly 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 offto the pip invocation inPythonVenvManager.PipInstallAsyncso each download step prints as a flushed line. - The chatterbox example script was silent during
init(). Even with pip output flowing, the firstfrom chatterbox.tts import ChatterboxTTSplus 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 throughsys.stderrso 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 exposingspeak(),speakWithVoice(),speakAs(), plussampleRate/device/outDirproperties.
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.jsxfor the standalone Drivers panel,DriverDocViewer.jsxfor the in-editor doc viewer) had their tier label maps hard-coded for tiers 0/1/3 only. Both now include2: 'Python'. The "trust badge" ladder onDriverDetailPagesimilarly gains aCommunity (Python)arm. - API Reference tab now renders manifest
detailsas a "Documentation" section. The earlier change wireddetailsthroughdriver.schemafor the in-editor viewer, but the user-facing Drivers panel usesmarketplace.detailwhich wasn't including the field. Addeddetails = localEntry?.Detailsto that handler's response, then rendered it as a top-of-tab Documentation section using the existingDetailsMarkdowncomponent (now exported fromDriverDocViewer). Imports the existingdriver-doc.cssso 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.jsongains an optionaldetailsfield. Markdown, rendered as a collapsible "Show details" section inDriverDocViewer.jsxdirectly under the description. Threaded through all three tiers:DriverManifest→DriverEntry.Details→ thedriver.schemaWebSocket response.- Per-action and per-property
detailsare now sent in the schema response (the descriptor records always carried the field; the WS handler just wasn't including it). The existingDetailsDisclosurecomponent 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 becomesdetails(markdown, surfaced on demand). Same split for properties via theget_<x>docstring. So writing a normal Python docstring ondef 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, unpairedset_<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. Singlegreet(name)action.counter.py+python-counter.js— adds state; demonstratesget_<x>()/set_<x>(value)properties andinit(config).http-probe.py+python-http-probe.js— addsrequestsas a runtime dependency; the siblinghttp-probe.requirements.txttriggers 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.
# workspace/python/chatterbox.py
def init(config): ...
def speak(text): ... # ← connector().speak({ text }) lands here
def get_status(): ... # ← connector().status read{ 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 calldebugpy.listen(("127.0.0.1", port))at startup and wait for a client beforeinit. 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 ifdebugpyisn't installed (add it torequirements.txtto 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.txtnext to its.driver.pynow 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'spython, soimport torch(or anything else insideinit()) resolves to a clean per-driver install. NewMuxitServer/Drivers/PythonVenvManager.cshandles the lifecycle: hashesrequirements.txtinto.venv/.muxit-req-hashso subsequent activations skip the install when nothing changed; streams pip's stdout/stderr line-by-line to the server console and to a newdriver.python.installEventBus 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 forrequests, several minutes for torch). - Example driver with deps.
drivers/py/example-with-deps/(PyHttpProbe) demonstrates the flow with a smallrequestsdependency. 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
venvmodule by default (apt install python3-venvonce if you seepython -m venvfailing). - Loose
.driver.pyfiles dropped intoworkspace/drivers/(no.muxdriverpackaging) 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.pyextension alongside the existing.driver.jsand.dlltiers. 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 subclassmuxit_driver.Driverand callrun(YourDriverClass). Wired through end-to-end:DriverTier.Python = 2(MuxitServer/Drivers/DriverTypes.cs),PythonDriverHost+PythonDriverInstancefor subprocess + IPC,PythonRuntimeManagerto detect a usable interpreter (MUXIT_PYTHONenv var →python3→python, requires ≥ 3.10),DriverPackageStorerecognises*.driver.pypackages, manifest schema acceptstier: 2,node drivers.js buildpackages Python drivers (auto-vendoring themuxit_driverSDK into every.muxdriverso installed drivers don't need anything onPYTHONPATH). 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_PYTHONto a full interpreter path to override probing. Seedocs-site/reference/driver-sdk-python.mdfor 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_FORBIDDENvalidator rule). - Runtime dependencies declared in a driver's
requirements.txtare 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 useshttp://<lan-ip>which is not a secure context, sowindow.AudioDecoderis undefined and the renderer fell through to the WASM polyfill path. Local browsers loading vialocalhostnever 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.
ServerAudioRenderernow schedules buffers ~100 ms ahead ofctx.currentTime(the jitter buffer) instead of letting decode latency pushnextStartTimeinto the past. The previous behaviour was the classic Web Audio streaming bug — async decode letcurrentTimeovertake the cursor, theMath.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-decoderWASM polyfill whenAudioDecoder(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.
IConnectorDrivergainsFunc<float[], AudioFrameInfo, CancellationToken, Task>? AudioStreamEmitter, alongside the existing stringStreamEmitter. New value typeMuxit.Driver.Sdk.AudioFrameInfo(int SampleRate, int Channels)with a convenienceAudioFrameInfo.Mono48k. - Host addition. New
MuxitServer.Audio.AudioStreamEncoderwrapping Concentus.DriverRegistry.InitBuiltInAsyncandDllDriverHost.InitAsyncwiredriver.AudioStreamEmitterto it alongside the existing stringStreamEmitterwiring. drivers/Muxit.Driver.AudioSynthbumped to 1.4.1. Removed the Concentus PackageReference and the in-driver Opus encoding code;StreamSamplesAsyncnow justawaits the host emitter.- Wire format unchanged. Browser still sees the same JSON envelope (
{ op, data, sampleRate, channels, format: "opus", frameSize }) andServerAudioRendereris 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 }. InternalToneGeneratornow 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.jsdecodes Opus packets via the browser-native WebCodecsAudioDecoder(Chrome 94+, Safari 16.4+, Firefox 130+ — universal in 2026). DecodedAudioDatais copied into a Web AudioAudioBufferand scheduled on the same per-connectornextStartTimecursor as before, so the rest of the pipeline is unchanged. The legacy PCM16 path (format: "pcm16",pcmfield) remains as a fallback for any emitter that can't link Concentus.docs/audio-streaming.mdupdated: 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, souseServerwould 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/statusnow reportssessionValidwhen the caller presents anX-Auth-Token, and bothApp.jsx(on mount) anduseServer.connect()(before each WebSocket attempt) verify a stored token via that endpoint. When the server says the token is dead, the client clearssessionStorage, drops the cached token inapi-shim.js, and dispatchesmuxit-login-requiredso 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
outputconfig. 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
datafield ofstream.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 anAudioContext, primes it on the first user gesture, decodes incoming PCM16 frames toAudioBuffers, and schedules them per-connector with anextStartTimecursor so chunks for the same source play seamlessly. Different connectors play independently and may overlap. Not TTS-specific; any future driver that emits anaudiostream goes through this single playback path.web-ui/src/hooks/useServerAudio.js— auto-subscribes to every connector that declares anaudiostream (viaschema.streams). Mounted inShell.jsxandMobileShell.jsx.web-ui/src/hooks/useServer.js— emits a per-chunkmuxit-stream-datawindow event in addition to the React-state path. Audio chunks need every frame; the existingrequestAnimationFramecoalescing onstreamDatais 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 FIFOTtsController. For server-side voices the canonical pattern is explicit:connector('chatterbox').speak("..."). No implicit re-routing ofsay(), 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 keepsformatas a discriminator so addingformat: "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 filteredstop()(stop({ source: 'chat' })cancels chat without stopping a scriptsay()), and provider switching at runtime.web-ui/src/tts/providers/BrowserProvider.js— Web Speech API wrapper conforming to a smallspeak(job) → Promise / cancel() / getVoices()contract. Future providers (chatterbox, cosyvoice, OpenAI, ElevenLabs) plug in without touching consumers.web-ui/src/hooks/useTts.jsreplacesuseTTS.js;useScriptSpeech.jsnow routes through the controller.ttsGesture.jsis 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),zoomXandzoomY(each-1..1) crop a1/zoom × 1/zoomwindow of the live frame and rescale it back to the camera's native resolution with bilinear interpolation, so the livevideostream and any active recording keep their nominalresolutionwhile showing the cropped region. Implemented asOnvifDriver.ApplyDigitalZoom, called per frame afterApplyFlipsand before the recorder + JPEG encode — both the continuous capture loop and thesnapshot()paths (live + cold-open) pick it up. The(zoomX, zoomY)offset is normalized against the available pan range at the current zoom level (±1puts 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, afterflipH/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 asvolatile floatso reads/writes from the capture thread and ws-handler thread don't need a lock. The starter template gainszoomReset/zoomCenter2xexample 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
MailKitfrom 4.7.1.1 to 4.8.0 inMuxitServer.csproj. The warning was also being re-printed at everynode start.js serverbecausestart.jsran an explicitdotnet buildand thendotnet run, which does its own implicit incremental build and re-prints the same NuGet warnings.start.jsnow passes--no-buildtodotnet runwhenever the explicit build succeeded. - Capability scan never worked for any free driver.
DllDriverLoader.AuditCapabilitiesDetailedbuilt itsMetadataLoadContextresolver from the .NET runtime dir + the driver's own dir, butMuxit.Driver.Sdk.dlllives in the host'sAppContext.BaseDirectory— so every scan failed withCould not find assembly 'Muxit.Driver.Sdk'and the audit reported "scan failed" instead of capabilities. AddedAppContext.BaseDirectoryto the resolver paths; AudioSynth / Midi / GenericScpi now report their actual capability set (FileSystem / Network / Process / …). - Auth token "denied" then "written" contradiction. On Windows,
File.WriteAllTextfails withAccess deniedwhen 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 printedAuth token written to: …, so every restart claimed both.Program.csnow 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 beforeapp.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 + theListening:/WebSocket:lines intolifetime.ApplicationStarted.Registerso 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 theDetailsparameter was added toPropertyDescriptor) failed loading with the rawMissingMethodException: Void Muxit.Driver.Sdk.PropertyDescriptor..ctor(System.String, System.String, System.String, System.String, System.String).DllDriverHost.ScanAsyncnow 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
snapshotstream persnapshot()call. The driver now declares a second stream channel,snapshot, and emits the same base64 JPEG on it thatsnapshot()returns to the caller — both code paths (live RTSP grab and cold-open temp connection). Bind a Canvas widget to<connector>:snapshot(modeimage) 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 throughStreamEmitterwas preferred over alastSnapshotproperty 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), andptzStatusfor 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 inOnvifClient.AbsoluteMoveAsync/GetStatusAsync; both go to the existing_ptzUrlSOAP 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 withgotoPresetor 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 quickptzStatuscall after a known position discloses what your camera returns. The misleading "continuous-move velocities" wording onptzMoveis also corrected —RelativeMoveis a one-shot delta, the camera stops on its own, noptzStopneeded.
Dashboards
Text Display widget supports multiline values and per-widget alignment. Strings containing
\nnow render across multiple lines (was: collapsed to a single line by the defaultwhite-space: normal). The widget config panel gains analigntoggle (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 thatdashboard.csswas only imported by the desktop entry, so the newwhite-space: pre-wraprule was missing on mobile;MobileShellnow importsdashboard.csstoo.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 existingoverflow: autoon 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 newMUXIT_AI_DEBUG=1-gated dump of the full upstream error body (#411): the OpenRouter generation log showed the first call (returningtool_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 carryingcontent: nulltogether withtool_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 inAiSession.AddAssistantMessage(MuxitServer/AI/AiSession.cs): emitcontent: ""instead ofcontent: nullwhenevertool_callsis present. Empty string is universally accepted, including by the local-LLM chat-template bridges (LM Studio Gemma, llama.cpp) the originalnullwas already working around. (#412)
Diagnostics
AiService.Streaminglogs the full upstream error body whenMUXIT_AI_DEBUG=1. One_logger.Infoline, 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.Logalready ranSecretRedactor.RedactMessageon its input, butscript.output,agent.log,script.say, andconnector.errorevents emitted straight onto the bus and bypassed it. A user script that didlog.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.enabledflip. A single config flag silently disabled the entire audit trail; nothing in the existing flow recorded the disable event itself.AuditLognow subscribes toconfig.changed, compares to a thread-safe_lastKnownEnabled, and on every flip writes a stderr WARNING + a structured line to a separateworkspace/logs/audit-meta.jsonlthat ignores the enabled flag. Startup with audit off produces an immediateaudit-disabled-at-startupentry; runtime flips produceaudit-disabled/audit-enabledentries plus anaudit.tamperevent for any future UI banner. Gated onIsLicensedso 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.installper second.MessageRouternow enforces a sliding 1-second window per WebSocket on the gated set; over-budget returnsWS_RATE_LIMITEDwith 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 viasecurity.rateLimit.callsPerSecinserver.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 inputafter every tool call. Subsequent investigation traced the actual cause to an unrelated upstream OpenAI/OpenRouter strict-mode rollout that rejectedcontent: nullon assistant messages withtool_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, A2–A8, 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.ScanAsyncperforms theDriverSignature.IsOfficialDrivercheck beforeDllDriverLoader.LoadrunsActivator.CreateInstance, so an unsigned DLL in apremium-category package never gets to execute its constructor in the host process. Skipped DLLs surface via thedriver.diagnosticsevent (PREMIUM_NOT_SIGNED, hard severity) so the Extensions panel still shows the rejection.lib/driver-manager/build.jsnow fails the build instead of silently shipping unsigned premium packages — setMUXIT_ALLOW_UNSIGNED_PREMIUM=1only for local dev builds. (audit B3/B4, #399)RequiresSafetyGates=falseonly honoured on signed drivers.DllDriverLoader.Loadpreviously read[assembly: RequiresSafetyGates(false)]from any DLL and propagated it straight throughConnectorInstanceso the safety gate became a no-op. Now the opt-out is gated onDriverSignature.IsOfficialDriver; an unsigned DLL that requests it gets the flag forced back totrueplus a stderr warning. Closes the "any DLL bypasses every limit/confirm/audit row" path. (audit B5/U8, #398)connector.schemano longer echoes secret-nameddriverConfigvalues. Fields whose key matches the existingSecretRedactor.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-keyconnector.driverConfig.setendpoint, 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:8765as long as the auth token is present.WebSocketHandler.HandleAsyncnow comparesOriginagainsthttp(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-withextension whitelist./api/editor/open-withpreviously calledProcess.StartwithUseShellExecute=trueon Windows and no extension check, so a workspace script that dropped a.bat/.cmd/.vbs/.lnkcould 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 toProcessStartInfo.ArgumentList(defence in depth).RevealInExplorergot the sameArgumentListtreatment. Editor mode remains opt-in (security.editorMode=true). (audit U7, #400)AiDebugDumpoutput is redacted.MUXIT_AI_DEBUG=1writes the assembled chat-request body (including connector instructions,Authorization: Bearer …headers in the messages array, and anysk-…tokens the LLM reflected back) toworkspace/logs/ai-last-request.json— exactly the file users grab when reporting bugs. The dump now deep-clones via parse, runs a newSecretRedactor.RedactJsonwalker on the clone, and stamps"redacted": trueso it's obvious the file has been sanitised.RedactJsonis 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
_userMessageCountat 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 viaemit(). 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:jsexport default { driver: "...", allowedConnectors: ["mqtt", "psu"], ... }Self-reference is always allowed (composite/wrapper convention). Scripts under
workspace/scripts/go throughScriptHost/ConnectorBridgedirectly and are unaffected — every demo / template script that callsconnector("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. Passimageas either a base64 string (with or without adata: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 theMessageheader) so the push notification renders the snapshot inline. For Discord the driver sendsmultipart/form-datawithpayload_json+files[0]and the embed references the file viaattachment://<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 warndeliveryevent. Pattern (MuxitServer/Drivers/BuiltIn/WebhookNotifierDriver.cs:144-200):javascriptconnector("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-levelmarkdown: trueflag for ntfy that adds theMarkdown: yesheader 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 aspassword), and a no-auth local relay. Header andai.instructionsmake explicit thatpasswordis 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 ofnotify()(post bypasses flavor reshaping),flavor: "generic"against an ntfy URL, or expecting markdown without themarkdown: trueconfig flag. The webhook-ntfy connector template gains a header note steering users atnotify()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
Communicationgroup alongsideMqttBridgeand 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. Thesendaction takes{ to, subject, body, html?, cc?, bcc?, from?, attachments? }; when bothbodyandhtmlare supplied the message goes out asmultipart/alternativewith 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. Aflavorconfig key reshapes the body for popular targets so scripts call a uniformnotify(title, body, level)regardless of where the message lands:ntfysendstext/plain+Title/Priorityheaders (level →default/low/high/urgent);discordbuilds a webhook embed with color per level (info=blue, warn=orange, error=red, success=green);slackbuilds atext+ colored attachment;genericposts JSON{ title, body, level, timestamp }for n8n / Zapier / Make / custom backends.post(payload)bypasses flavor reshaping for full control.RequiresSafetyGates = falsebecause 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 rightflavorand 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()inSandboxScript.cswalkednew Error().stacklooking for<anonymous>:LINE:COLand picked the first frame whose adjusted line was ≥ 1, butSandboxScript.InitCodeis loaded via a separateengine.Execute(...)call so its frames also rendered as<anonymous>— the regex matched the helper's ownnew Error()line (~line 31 of InitCode) before ever reaching the user frame. Fix: evaluate the wrapped user script withDocumentInfo("muxit:user")(MuxitServer/Scripts/ScriptEngine.cs) so user frames render asmuxit:user:LINE:COL, and key__traceCallSiteandStackLineRegex/AdjustStackLinesoff that source name. The regex tolerates a[temp-N]suffix that ClearScript appends for transient docs in some versions. (2) Both effects inEditorGroup.jsx(the Monaco decoration and thesubscribeTraceopt-in) gated on a strict^scripts/.+\.js$regex, but the workspace explorer hands the editor paths likeworkspace/scripts/foo.js. Neither effect fired, so the trace subscriber count on the server stayed at zero,__host.TraceEnabledreturned false, and__traceCallSiteshort-circuited before reporting a single line. Both effects now useisScript()(the project's own predicate, accepting any*/scripts/*.js), and the highlight key derives fromactiveFile.nameto match the name the Run button passes toserver.runScript. (3)subscribeTrace()inuseServer.jswas a no-op when issued before the WebSocket finished opening (wsRef.currentwas still null), and it never re-sent on reconnect.ws.onopennow resends the subscribe whenevertraceSubRef.current > 0.docs/clearscript-quirks.mdquirk #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.jsonat 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 useddotnet run --project /path/to/MuxitServerwhich is a from-source invocation an end user with the publishedmuxitbinary can't run; (3) the ChatGPT block told users to pastehttp://127.0.0.1:8765/mcpdirectly 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 anError creating connector. unsafe urltoast every time. All three blocks now distinguish installed Muxit from source checkout: Claude Code shows aclaude mcp add muxit -- /path/to/muxit --mcp --workspace …command for installed builds and keeps the existingdotnet runsnippet as the source-tree variant; Claude Desktop's snippet now points at the absolute path of the installedmuxitbinary; 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/mcpendpoint over a public tunnel publishes the full hardware surface — read sensors, write properties, run scripts — with no credentials. The optionalsecurity.remoteAccesspassword gate is bypassed for loopback callers and uses a customX-Auth-Tokenheader 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/flipVorientation 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 inApplyFlips()(MuxitServer/Drivers/BuiltIn/OnvifDriver.cs) immediately after decode and before recorder feed + JPEG encode, so both the livevideostream and any active.mp4recording reflect the flip. Implementation uses OpenCvSharp'sCv2.FlipwithFlipMode.Y(horizontal mirror),FlipMode.X(vertical), orFlipMode.XY(180° rotation when both are set). No restart needed when toggling at runtime — the capture loop reads the volatile flags each iteration. Documented indocs-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_OPTIONSnow also setsmax_delay=0andreorder_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 openingVideoCapture, 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 configuredmin:0, max:2.6, step:0.05looked broken. The widget now derives display precision fromstep(e.g.step:0.05→ 2 decimals,step:1→ 0 decimals), strips post-rounding float noise viatoFixed(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 inweb-ui/src/components/dashboard/widgets/KnobWidget.jsxwith newdecimalsFromStep()andquantize()helpers and matching styles inweb-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 throughNumber(val), which meant typing0.05was eaten alive: after0.the parser collapsed it to0and re-rendered the input, swallowing the in-progress decimal point. A newNumericFieldcomponent (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.jsxDashboardSection) 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 innoCompactorfrom react-grid-layout; new widgets dropped while in this mode get placed at the bottom of the existing layout (otherwisey:Infinitywouldn'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-gridlinesstyle (web-ui/src/styles/dashboard.css) paints faint 1px lines at every column and row boundary using tworepeating-linear-gradientbackgrounds. The--grid-col-stepand--grid-row-stepCSS variables are computed in JS from the currentcontainerWidth,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 toAlt+Z) that flips the editor between'on'and'off'. The setting persists in themuxit-layout-statelocalStorage 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 newscpi-connectorsfeature 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 inMuxitServer/Connectors/ConnectorManager.cs:ResolveEnabledSetskips additional SCPI configs at startup (the first SCPI connector wins; the rest land in the disabled list with aNot enabled (SCPI limit: 1 on Free tier)reason),AddConnectorrejects programmatic creation past the limit withCONN_LIMIT_REACHED, andSetEnabledConnectorsAsyncvalidates the count when the user changes the enabled set from the UI. Match is by driver name (GenericScpi, case-insensitive) — connectors built on the JSserial-monitordriver 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) hadai()mis-listed as Pro-tier — corrected to Maker, with a vision caveat for theai(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 declarativeGenericScpischema, or driving the sameprobe_scpi_device/validate_connector_config/write_connector_configtools 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'swriteArrayhelper (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 newfile(path)action returns aFileHandlehost 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 byV8Interop.NormalizeJsValue's function-discarding traversal. All operations delegate to the parent driver's existing path-resolution, size-limit, andworkspace.file.changedplumbing, so handle calls obey the same sandbox rules as the standalone actions. Methods are synchronous (f.write(...)returns"OK", noawait) to match the rest of the connector proxy. The handle'srenameupdates 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 inai.instructionsso the AI prefers it. Standalone actions (writeText,readText,appendText, …) are unchanged and still accept positional args viaMergePositionalArgs. Reference:docs-site/reference/drivers/file-access.md"File Handles" section.
Features
System prompts now have a
minimalprofile for local LLMs with 4–8K context windows. A newai.promptProfileconfig key ("standard"default,"minimal"opt-in) controls how much of the platform context ships in the system message and tool-definition list. Inminimalmode:- Connected Devices collapses to one line per connector (
- name (driver) — instructions) plus a single hint to callget_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 isoff. - Tool descriptions in the OpenAI function-calling list collapse to the first sentence of each tool's entry in
tool-descriptions.md.PromptRegistry.GetSectionShortcuts 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;AiServicereads 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.mdanddocs-site/guides/ai.md.- Connected Devices collapses to one line per connector (
Changes
- MCP tool-result JSON keeps non-ASCII characters literal.
MuxitServer/Mcp/McpJsonOptions.{Default,Indented}now setEncoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, so em-dashes (—), units (°,µ,Ω),+,<, and similar safe punctuation appear as one literal byte instead of expanding to a six-byte\uXXXXescape. Affects every MCP tool result the AI sees (notablylist_connectors,get_connector_schema,read_propertyresults that quote device strings) and the device-schema JSON the agent-mode prompt embeds viaSystemPromptBuilder.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.csnow classifies upstream errors against a marker list (context_length_exceeded,prompt_too_long,request too large,context window,exceeds the context, …) and throws aMuxitExceptionwith the newAI_CONTEXT_LENGTH_EXCEEDEDerror code. The web client'suseChatreadserr.code(set bymuxit-clientfrom the server error envelope) and tags the message witherrorCode: 'AI_CONTEXT_LENGTH_EXCEEDED';ChatPanelrenders an inline Start new chat button next to the error so the user can clear the bloated session in one click. Detection covers OpenAI'serror.code = "context_length_exceeded", Anthropic's prosemessage, OpenRouter'supstream_statusbody, plus the plain-text 4xx that older Ollama builds return without a JSON envelope. Previously this surfaced asError: 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, theFALLBACK_MODELS/DEFAULT_FAVORITE_MODELS/DEFAULT_MODELconstants in the web UI, theAI_PROVIDERS[muxit].defaultModel, theModelCachefallback list, and the doc references all move toanthropic/claude-haiku-4-5andanthropic/claude-sonnet-4-5. Test config intests/fixtures/test-server.jsonupdated to match. - Switching AI providers now refreshes the model list and picker.
useModelswas only fetching once per mount, so flippingai.providerfrom Muxit to Ollama / LM Studio left the chat header dropdown showing OpenRouter cloud slugs that the local backend rejects. The hook now takes aproviderKey(the active provider id) and re-fetches whenever it changes;SettingsEditorandChatPanelpassconfig.ai.providerthrough. The chat-headerModelDropdownalso receivesactiveProviderId: on a local provider it lists everything from/modelswhoseprovidermatches (Ollama / LM Studio / openai-compatible), so the picker only ever offers models the active backend can actually serve. Stale cloud favourites still live inai.favoriteModelsfor 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
contentnow actually trigger the tool. Qwen3, Hermes-3, and gemma chat-template bridges sometimes ignore the OpenAItool_callsfield 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 ajson `` block). Previously this rendered as raw JSON in the chat and the user saw nothing happen.AiService.Streaming.ExtractEmbeddedToolCallsis a post-stream fallback (only runs when no structured tool call landed) that recognises all three shapes, acceptsname/toolandarguments/parameters/inputfield 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 throughai.deltaso 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 likeconnector("fileaccess").writeText(path, content)was forwarded across the V8 bridge as a single ClearScriptScriptObject(a V8 array, not a CLRobject?[]) —ConnectorManager.MergePositionalArgsonly checks forobject?[], so it fell through to the scalar-wrap branch and produced{path: <V8Array>}. The driver'sArgString(args, "path", "")then.ToString()'d the V8Array, yielding the type-name string, andWriteTexthappily wrote an empty file at that bogus path. The single-arg object formwriteText({ 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 moreArgDescriptorentries. 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.CallAsyncnow runsV8Interop.NormalizeJsValueon the incoming args, recursively converting V8 arrays →object?[]and V8 objects →Dictionary<string, object?>before they reachMergePositionalArgs. (2)MergePositionalArgsthrowsVALIDATION_INVALID_VALUEwhen 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/ArgBoolreject dictionary, collection, and ClearScriptScriptObjectvalues 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.donecarries a structurederrorobject —{ message, line, column, stack, details }— instead of a flat string. Line numbers refer to the user's source: the wrapper preamble thatScriptEngine.csinjects to make top-levelawaitand main()-style scripts both work prepended 3 lines, and every<anonymous>:N:Mreference is now adjusted byScriptEngine.WrapperPreambleLinesbefore it leaves the server (ScriptEngine.AdjustStackLines).ConnectorLoader.TransformImportskeeps 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 newconnector.errormessage with the sameline/columnshape. - The web UI marks the offending row.
EditorGroup.jsxcallsmonaco.editor.setModelMarkersagainst the active script or connector file when its name appears inuseServer's newscriptErrors/connectorErrorsmaps. The squiggly carries the V8 stack as the hover message; markers clear automatically on the next run start.BottomPanelrenders anLnnchip next to error log lines that have line info — clicking it opens the file (muxit-open-fileevent →Shell.handleFileOpen) and reveals the line viaeditor.revealLineInCenter/editor.setPosition. - The editor highlights what a running script is parked on. A new
script.linebroadcast (with pairedscript.line.subscribe/script.line.unsubscriberef-counted on the server) reports the current user-line whenever a script blocks ondelay,await connector.x,ai,ask.*,say, orstream. The trace is gated byScriptHost.TraceEnabledso 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 onawait delay(30000)or a slowawait psu.voltage = 12for 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.providerconfig selects betweenmuxit(the existing managed proxy, default),ollama(local Ollama daemon, defaulthttp://localhost:11434/v1),lmstudio(LM Studio's local server, defaulthttp://localhost:1234/v1), andopenai-compatible(any other server exposing/chat/completions— vLLM, llama-server, …). Per-provider settings live underai.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 newconfig.test_providerWebSocket message) that probes the endpoint, validates auth, and reports how many models the server has loaded. The chat UI, theai()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 OpenAIimage_urlcontent 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-llmin the registry); the Muxit cloud path still rides on the existingaifeature, 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, andMuxitServer/AI/ObjectDetector.cspreviously each built their ownHttpRequestMessageagainst the hardcoded Muxit proxy URL with the license key as bearer. They now all go throughMuxitServer/AI/Providers/LlmProviderFactory, which returns a cachedMuxitProvider(cloud) orOpenAiCompatibleProvider(Ollama / LM Studio / openai-compatible) based onai.provider. The streaming SSE parser, tool executor, safety gate, session store, and WebSocket message shapes are unchanged — adding a new provider only requires implementingILlmProvider. License gates moved into the providers themselves:MuxitProviderenforces theaifeature,OpenAiCompatibleProviderenforceslocal-llm.ModelCachenow merges the active local provider's/modelsresponse 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 confirmationcolumn was added to the safety matrix (SafetyPolicy.LevelBehavior.AiConfirm, the SafetyChip dialog, and thesafety.custom.aiConfirmfield inserver.json). Every built-in level — Observe, Assisted, Active, Unrestricted — ships withAsk every time; only a Custom row may set it toAllow. 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_actionwhose arguments contain any number (the AI restates each value with its unit — "setting voltage to 15 V, current limit to 0.35 A — confirm?"), andwrite_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 likepower_onorresetcontinue to execute immediately — the gate only catches writes that carry numbers and script-authoring tools. The legacy per-levelconfirm_unknown_values/confirm_all/offmapping 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, andsafety-confirmationprompt 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 withwrite_scriptorrun_codeand executed — not stepped through with repeatedwrite_propertycalls. Before this, a 35-step current sweep produced 35 per-step confirmation round-trips; now the same request produces onekind: "script"plan confirmation and the script runs with per-step writes going through the hardware safety gate directly. TheClassifyIntentByKeywordsclassifier andMentionsScriptAuthoringgate were both extended so sweep/step/range phrasing activates the Scripts toolgroup and loads thescript-apifragment even when the user doesn't say "script" or "loop" outright. - Identical tool-call retries no longer re-arm the confirmation gate.
BuildConfirmationKeynow 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 ismeasured_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 V8Proxygettraps for (a) the script-sandboxconnector(), (b) cross-connector access inside connector configs, and (c) the per-config__driverProxyused ascin 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 pullsbridge.ActionNamesalongsidePropertyNamesto tell valid actions apart from typos; the other two already tracked both sets.settraps are unchanged — writes to unknown names already throw viaConnectorManager.CallAsync'sConnMethodNotFoundpath.
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 withError: Path cannot be empty— the script-bridge proxy forwards the call as{method: 'writeText', args: ['data.csv', 'hello']}, andConnectorManager.MergePositionalArgsonly 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 likereadText('data.csv')) fell through unchanged, soFileAccessDriver.WriteTextreadArgString(args, "path", "")off a bare array/string and got the empty default.MergePositionalArgsnow also handles the pure-positional and single-scalar cases by mapping them onto the action's declaredArgDescriptornames, sowriteText(path, content)→{path, content}andreadText("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()witherror: "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 everysynth.speak()through a shared priming gate (ttsGesture.whenReady) that defers the call until the nextclick/keydown/touchstart; anot-allowederror 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), andspeakNext/speak/speakProgressiveread voice/rate/pitch from refs so theiruseCallbackidentities are stable — otherwise the asyncvoiceschangedevent 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 setmeta.requiresSafetyGates: false; built-ins overridebool RequiresSafetyGates => false;on the driver class. Default istrue, 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 indrivers.listanddriver.schemaWebSocket responses asrequiresSafetyGates. Orthogonal to per-connectorsafety.overrideLevel—overrideLevelpins a gated connector to a specific level;requiresSafetyGates: falseturns 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 plainportandbaudRatefields in their connector config and build the serial transport themselves. The old pattern — a bogusimport { createSerialTransport } from "../../shell/src/transports/serial.js"(the path never existed; the loader stripped it with a regex), a pair of top-levelconst 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'sexportConnectoremits the new shape. Theconfig._transportfield still works as an undocumented escape hatch for non-serial transports (e.g. redirecting the Plotter driver at a TCP simulator viacreateTcpTransport) 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.safetyModehas been removed from theserver.jsonschema and docs. The setting was already dead code —AiService.GetSafetyMode()derived tool-call prompt behaviour from the globalSafetyGate.CurrentLevel(see v0.23.0), so the per-service config was accepted and silently ignored. Unknown config keys are preserved on disk, so anyai.safetyModestill in an existingserver.jsonis 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.modelnow defaults toanthropic/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.NormalizeModelIdmaps the legacy sentinel to the new default, and the UI'scurrentModelfallback now mirrorsAiService.DefaultModelIdso the picker always shows the model the server will actually call. Pin a different model by settingai.modelto 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/restartfor 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
ShutdownTimeoutwas 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.ShutdownTimeoutis 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 aReferenceErrorat render time and was silently swallowed by React's ErrorBoundary, leaving the tab non-functional. Thelicenseprop now threads throughSettingsEditor→ServerSection→RemoteAccessSubSection, 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.csnow 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.Customand 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'srequiredTierIdupward; count-limited features inherit through the tier rank), and the "Pro" badge / upgrade copy across the UI is driven by the blocked feature'sblockedReasonrather 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 newsafety.policy-enginelicense feature.SafetyGatekeeps 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 globalSafetyLevelpolicy 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
SafetyGatethat resolves to a single safety level — Observe (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 StatusStripSafetyChip; applying a change is a single click in the dialog (no second confirmation — the dialog itself is the confirmation surface), with Unrestricted additionally requiring typingI UNDERSTANDinside the dialog. Once granted, a level stays active until the user picks a different one. Connector configs can add an optionalsafety: { limits, confirm, rate, interlock, overrideLevel }block —limits.min/maxhard-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, andoverrideLevelpins a single connector to a specific level independent of the global. Blocked, simulated, and confirmed operations are appended toworkspace/logs/audit.jsonlas 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) tofreeand the three lab-safety levels (Assisted, Active, Custom) plus the audit trail topro, but every tier's allow-list is config-driven — future tiers (Maker, Lab, Enterprise, …) can mix and match any subset by bumpingRequiredTierIdon the relevant features inAppSettings.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.safetyModegains 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; existingconfirmconfigs migrate toconfirm_allautomatically 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.mdand a new "Before authoring" section inscript-api.md, and the matching guidance was mirrored into the seededworkspace-template/config/instructions.mdso 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.mdagainst 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 atDriverRegistrywith error codeEULA_NOT_ACCEPTED, andscript.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.jsonlfor the audit trail. Headless installs (--cli,--mcp, CI) accept via the newmuxit eula accept --user <id>subcommand or theMUXIT_EULA_ACCEPT=1/MUXIT_EULA_USER=…env-var pair. Minor/major EULA bumps require re-acceptance; patch bumps do not. Seedocs-site/guides/workspaces.mdfor 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.jsonfiles directly in the active workspace. Ask "build me a dashboard for the PSU and the robot" and the AI callslist_connectors+get_connector_schemato 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 ofvoltage" round-trip throughread_dashboard→write_dashboardso 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 keywordsdashboard,widget,gauge,panel,layoutso the group only loads when relevant.
v0.20.0 — April 20, 2026
Features
- SCPI driver speaks USBTMC — the
GenericScpidriver 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). Settransport: "usbtmc"in the connector config and providevendorId/productId(hex or decimal) — an optionalserialfilter disambiguates multiple of the same model. The driver wraps every SCPI line in a USBTMCDEV_DEP_MSG_OUTBBB header, implements the two-stepREQUEST_DEV_DEP_MSG_IN+ multi-transfer reassembly for reads, issuesINITIATE_CLEARon connect andINITIATE_ABORT_BULK_INon drain recovery — enough of the spec for reliable SCPI round-trips against every mainstream instrument. Useslibusb-1.0viaLibUsbDotNet(native binaries auto-bundled into the.muxdriverpackage for Windows). Requires WinUSB bound to the device via Zadig. Seedocs-site/reference/drivers/scpi.mdfor 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 cleanAI_REQUIRES_PROerror 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 cancelsdelay()and calls V8Interrupt(), which terminates execution synchronously — so statements after the main loop (andfinallyblocks) were never reliably running, despite examples likelog.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 callers —
GET /api/diagnostics/networknow 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 WebSocketdiagnostics.networkhandler 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
detailslayer — theGcode,Plotter,AudioSynth, andSerialMonitordrivers now populateDetailson the props/actions whose one-liners were hiding non-obvious behavior. Highlights:Gcodedocuments the GRBL status enum values, thesend/home/reset/holddistinction around buffer flushing and step loss, andstreamFile's line-count-based progress (which doesn't wait for motion completion);Plotterdocuments the canvas-API angle convention (0 = +x, CCW),lineTochaining polylines untilpenUpbreaks them, and thedrawbatch's non-atomic error behavior;AudioSynthdocumentsvolume/waveformapplying to the next playback (not in-progress), theplayMelodyshorthand grammar with number-as-rest, and that overlapping playback calls replace rather than layer;SerialMonitordocuments thatport/baudRateare transport-owned sentinel values and that fast bursts need theoutputstream rather thanlastLinepolling. This completes the first pass of thedetailsrollout — every shipped driver (except the intentionally minimalTemplateand the not-yet-stableAvantes) now carries extended markdown on its subtle props and actions.
v0.17.0 — April 18, 2026
Features
- Extension drivers fill in the on-demand
detailslayer — the premiumMidi,Scpi,Vision, andFairinoextension drivers now populateDetailson their most semantically dense props/actions. Highlights:Mididocuments channel 1-16 vs wire-level 0-15, velocity-0 = Note Off,sendSysexauto-framing of F0/F7, andlearntimeout behavior;GenericScpidocuments query retry-once +timeoutscope,settleMsapplied to writes only, andreadErrorsiterating until the SCPI"No error"sentinel;Visiondocuments thecolor/contourtracker param schemas andcalibrateColorHSV margins (±10 hue, ±40 sat/val);Fairinodocuments the 6-element joint/TCP layout with mixed mm/deg, null-vs-zero semantics,moveJarc vsmoveLstraight-line paths, blending withblendT/blendR, singularity risk,stopflushing the queue vspausepreserving it, anddisableletting the robot fall under gravity. As before,Detailsonly 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
detailslayer — the newDetailsfield added in v0.15.0 is now populated across every built-in driver:Webcam,Onvif,MqttBridge,FileAccess, andTestDevice. 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
detailslayer —PropertyDescriptorandActionDescriptornow carry an optional markdownDetailsfield alongside the existing one-lineDescription. 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 ofget_connector_schema/get_driver_schema. Authors put parameter enums, side effects, truncation caps, and failure modes inDetails— everything a user or AI agent would otherwise have to discover through trial and error. TheSerialProbedriver now demonstrates the pattern with extended docs on its five subtle actions (scanBaud,hypothesize,analyzeFrames,stimulus,exportConnector); other drivers fill inDetailsopportunistically. - 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
muxitBlock 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 byMuxitServer/Core/NetworkDiagnostics.cs,Http/DiagnosticsApi.cs, andWebSocket/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.scanBaudaccepts an explicitportPath+reopenAtpolicy,exportConnectorrefuses to run without an active port (so the generated config captures the confirmedportPath+baudRate), and the starter template + example connector no longer pin a port — the AI picks one vialistPorts/openPortat session start. The driver now also exposes five terminal-friendly streams —traffic(unified RX/TX with arrow prefix),rxHex,rxText,txHex,txText— alongside the existingframes/events; raw-hex output previously went through an un-wired emitter and never reached terminal dashboards. The legacy preconfiguredconfig._transportpath still works for backward compat. - Dev Console: driver registry publication status — the Dev Console Drivers tab now shows, per driver, whether the latest built
.muxdrivermatches what's published to the publicmuxit-driversregistry, with a direct action to publish or re-publish from the button row. Release routing inlib/driver-manager/publish.jstargets the publicmuxit-io/muxit-driversrepo 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=1before 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 likevoltage: { cmd: "VOLT", type: "float", unit: "V" }becomes a full read/write property (get sendsVOLT?, set sendsVOLT 5), andsave: { cmd: "*SAV", arg: "int" }becomesc.save(3)→ sends*SAV 3. A trailing?on the command marks the property read-only. Top-levelproperties/methodswith the same name still win, so vendor quirks can stay hand-written while the regular 80% shrinks to a single-line declaration. Thebk-psu.jstemplate loses ~40 lines of repeatedparseFloat/${Number(v)}/.trim()boilerplate. Under the hood,DllDriverHost.GetDriverMetaForInstancenow returns live per-instance schema toConnectorInstanceso 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
.csvlog being appended to from a running script now updates its chart (or text) view live, no close-and-reopen needed.FileAccessactions (writeText,appendText,writeBinary,deleteFile,rename) emit a newworkspace.file.changedWebSocket 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 thatrequire,import, and Node.js filesystem APIs are not available. Cuts down on AI-generated scripts that tryrequire('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
speechSynthesisqueue was idle, so any in-flight (or just-finished) scriptsay()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 whatuseScriptSpeechis 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 makessay()audible while the chat panel is closed — previously the auto-speak only ran when the chat sidebar was the active panel. Persisted asvoice.tts.scriptEnabled(default on).
v0.11.0 — April 17, 2026
Features
- CSV files open as charts — opening a
.csvor.tsvfile 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 byFileAccess.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 scripts —
run_scriptandrun_codenow acceptwait_seconds(default 15, max 120) and returnstatus: "running"with arun_id, recent output, and anext_seqcursor 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(withsince_seqcursor), andwait_for_script— let the AI poll progress, fetch incremental log output, or block again with a bounded wait. Passwait_seconds: 0for 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 oldMidiDriver.Template.csC# string has been removed), registered in the documentation sidebar and the Built-in Drivers index, and given accurate manifest tags (["midi", "audio"]— a newmiditag 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" /
NaNflicker on slow instruments. settleMsoption — 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.debugTX/RX stream — whendebug: true, every transaction's command and response is emitted on thescpistream (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 unusedIScpiTransportinterface was removed in favour of the existing abstract base, and the*IDN?fallback now only swallowsTimeoutException/InvalidOperationExceptioninstead of everyException. - New reference page — Scpi Driver documents every property, action, config option, the framing/recovery model, and a BK Precision example connector.
- 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" /
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 viaMUXIT_DOCS_DEV=1, mirroring the prompt fragment hot-reload. Backed by a newDocsRegistryandDocsToolHandlers.
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. Seereference/drivers/serial-probe. - Raw-byte streaming on SerialTransport —
setRawMode(true)+onRawData(hex, timestampMs)bypass line buffering and deliver per-chunk hex with timestamps.portPathandbaudRateare 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/continuetransport, Standard MIDI File playback and recording (sandboxed toworkspace/midi/),panic(All Notes Off + All Sound Off + Reset All Controllers on every channel, with localactiveNotesbookkeeping cleared), and alearnaction 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), andclock(24-PPQN ticks, opt-in viaemitClockStream: true). Built on DryWetMIDI and ships asmuxit-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 aworkspace-template/connectors/audio.jsexample wiring upchime(),alert(), andfanfare()helpers. See AudioSynth reference — useful for lab-side "experiment complete" cues, CNC/robot feedback tones, and safety alerts. - New driver tags —
audio,synth, andnotificationadded to the controlled tag vocabulary (andlib/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 newPromptRegistry. A newToolRegistryowns the authoritative tool list and dispatch; per-group handler classes underMuxitServer/AI/ToolHandlers/replace the 1,040-line monolithicAiToolExecutor. Chat and agent system prompts are composed by a sharedSystemPromptBuilderfrom these fragments, eliminating the parallel implementations inAiService.BuildSystemPromptandAgentInstance.BuildAgentSystemPrompt. - Dev-mode prompt hot-reload — setting
MUXIT_PROMPT_DEV=1makesPromptRegistryread 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_codetool descriptions (which showedawait dev.prop()) and the Script API guide (which said "Do NOT use await"). Canonical text now lives once inPrompts/script-api.mdand is referenced from everywhere else. - Robot-specific rules scoped to the robot section — the
workspace-template/config/instructions.mdrule "do not add delays in the script" previously appeared to apply to all scripts; it is now inside the### robotdevice subsection where it belongs, and the generic Script API guide keepsdelay()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/propertiesblocks can be hidden or exposed (previously only driver items). Changes are written back to the connector's.jssource (the top-levelhide: […]array, clearing anyexpose: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
.jssource (either at the top level or inside the nestedconfig: { … }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
InitErrorset, 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/Waccess label inline again (previously only in the tooltip). - New WebSocket endpoints —
connector.visibility.setandconnector.driverConfig.setdrive the new UI.connector.schemagains an optionalincludeHidden: trueflag and its response now includesdriverConfig,hiddenItems, andvisibilityMode; 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.detailcarries newlicensed,licenseStatus,licenseMessage, andtrialDaysRemainingfields 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.jsonwith its build / sign / package state and its Lemon Squeezy product binding. Inline editor for thecommerceblock writes back todrivers/<name>/manifest.jsonso 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 (requiresLEMON_SQUEEZY_API_KEYenv var on the dev console). - Manifest
commerceblock — driver manifests now carry an optionalcommerceblock (lemonSqueezyProductId,lemonSqueezyVariantId,price,storefront.{tagline,longDescription,images}) for premium drivers. At server boot,DllDriverHostreads the product ID and feeds it intoLicenseManager.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-statusCLI — new headless command reports the build / sign / commerce readiness of every premium driver.--strictexits 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.changedover 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.searchresponse now includes thecategoryfield, 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.installaccepts an optionalversionfield 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 bypass —
AppSettings.DevMode, the surrounding#if DEBUGcompile-time switch, and thelicense.dev_simulateWebSocket 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 bydotnet build MuxitServer -c Release(allCS0162 Unreachable code detected) are gone, plus a relatedCS8602nullable-reference warning inProgram.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 sendingargs: []in itsconnector.callWebSocket message. An empty array is notnull, so the server routed the call as a SET with an empty-array payload, which JS coerces to0for numbers andtruefor booleans — silently reprogramming instruments the moment you expanded a connector card (e.g. writingVOLT 0 / CURR 0 / OUTP ONto a power supply). Fixed on both sides: the UI now omits theargsfield for reads, andConnectorManager.CallAsyncnormalises an empty args array tonullso no future caller can hit this trap.
v0.3.1 — April 14, 2026
Bug Fixes
- SCPI driver: stale-response framing fix —
GenericScpiover 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 hiddenStreamReaderbuffer), and enters a self-healing "resync" state on timeout / empty-response so the cascade that made voltage reads collapse to0after a few iterations is closed out. The connector-levelparseFloathazard that renderedNaN/object [0]in the Hardware pane is addressed by a newnum()guard inbk-psu.jsand 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
settleMsoption — hold the transport lock for N ms after every non-query write, so aVOLT 5/VOLT?round-trip reads back the freshly applied setpoint without needingdelay()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 — thefree/andpremium/subdirectories are gone. Installed drivers now sit as single.muxdriverpackage files directly underworkspace/drivers/; the server extracts each package transparently into a hiddenworkspace/drivers/.cache/<id>@<version>/on load. Thecategoryin each package'smanifest.jsonis 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
.muxdriverfile intoworkspace/drivers/untouched; uninstalling deletes it. Stale cache entries are garbage-collected automatically on the next scan.
Breaking Changes
- Any existing
workspace/drivers/free/orworkspace/drivers/premium/contents are no longer scanned. Reinstall drivers via the Extensions panel. - Editor API now blocks all writes under
drivers/(previously only blockeddrivers/free/anddrivers/premium/). Installs must go through the Extensions panel. node drivers.js signnow signs each premium driver'sbin/publish/DLL (pre-package). Re-runnode drivers.js buildafterwards 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
_runningafter 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)