All notable changes to Milady are documented here. Format is Keep a Changelog.
- Companion UI — VRM engine, animations, themes, i18n: 3D VRM avatar engine with animation system (idle, wave, dance, retargeting), companion view with chat integration, bubble emotes, theme switching, full i18n support (en, zh-CN), and plugin logos for installed ElizaOS plugins. Includes BSC wallet trading panel, wallet portfolio display, and trading profile modal. (#812) thanks @Dexploarer
- Release workflow documentation (WHYs):
docs/build-and-release.mdnow has a "Release workflow: design and WHYs" section explaining strict shell, retry assertions,@electron/asarcrash dump, find/read safety, DMG path robustness, node-gyp artifact removal, size report scope, single Capacitor build step, and packaged DMG E2E timeout/dump. Why: Future maintainers and agents need to know why these choices exist so they don't "fix" or remove them incorrectly. - Multi-destination streaming registry: Replace single
destination?withMap<string, StreamingDestination>+activeDestinationId, enabling runtime switching between streaming targets viaPOST /api/streaming/destination. NewgetActiveDestination()helper provides backward-compatible fallback for single-destination configs. (#788) thanks @Dexploarer - pump.fun streaming plugin:
@milady/plugin-pumpfun-streamingadds pump.fun RTMP destination using the sharedcreateStreamingPlugin()factory. (#788) thanks @Dexploarer - X/Twitter streaming plugin:
@milady/plugin-x-streamingadds X/Twitter RTMPS destination using the shared factory. (#788) thanks @Dexploarer - RTMP URL validation:
getCredentials()inplugin-streaming-basenow throws early with a clear error when RTMP URL is not configured. (#788) thanks @Dexploarer - Plugin resolution and NODE_PATH (doc):
docs/plugin-resolution-and-node-path.mdexplains why we setNODE_PATHin three places so dynamic plugin imports resolve from CLI, desktop dev, and direct eliza load. Why: Prevents "Cannot find module '@elizaos/plugin-...'" when entry is underdist/or cwd is a subdir. - Electron startup resilience: The desktop app now keeps the API server running when the agent runtime fails to load (e.g. missing native module like
onnxruntime_binding.node). Why: Without this, a single load failure would throw, the outer catch would tear down the API server, and the renderer would get no port and show only "Failed to fetch" with no error message. Keeping the server up and settingstate: "error"with port preserved lets the UI connect and show "Agent unavailable: …" with the actual error. Seedocs/electron-startup.mdand WHY comments inapps/app/electron/src/native/agent.ts— do not remove the try/catch and.catch()guards as "excess" exception handling. - Regression tests for startup resilience:
apps/app/test/electron-ui/electron-startup-failure.e2e.spec.tsnow has two tests: (1) failed runtime keeps API server alive and recovery on retry, (2) failedeliza.jsload (e.g. missing native binding) preserves port and no server teardown. Why: A failing test is strictly stronger than documentation for preventing regressions; if someone removes the guards, CI fails.
- Node.js CI timeouts: Test and other CI workflows no longer rely on
actions/setup-node@v4for Node. On Blacksmith runners we useuseblacksmith/setup-node@v5(colocated cache); on GitHub-hosted runners we pinactions/setup-node@v3and setcheck-latest: false. We also add Bun global cache (~/.bun/install/cache) andtimeout-minutesto test, release, nightly, benchmark-tests, and publish-npm. Why: v4’s slow post-action and nodejs.org downloads caused frequent timeouts; Blacksmith’s action avoids the download on their runners; v3 + no check-latest uses toolcache and avoids the regression. Caching and timeouts keep runs fast and bounded. Seedocs/build-and-release.md"Node.js and Bun in CI: WHYs". - Release workflow hardening:
.github/workflows/release.yml— (1) Job default shell set tobash -euo pipefailso steps fail on first error. (2) Allbun installretry loops now run the same install command once after the loop so the step fails if all retries failed. (3) Crash dump usesnpx @electron/asar listinstead of deprecatedasar. (4) Bundle dist and node-gyp removal usefind -print0andwhile IFS= read -r -d ''for paths with special characters. (5) DMG path for packaged E2E usesfind+stat -f(newest by mtime) instead ofls -t. (6) Size report usesif [ -d ... ]guards and includesmilady-dist. (7) Single "Build Capacitor app" step for all platforms. Why: Reproducible, fail-fast builds and diagnosable failures; seedocs/build-and-release.md. - Packaged DMG E2E: CDP wait timeout is 240s in CI (120s locally); on CDP wait or connect failure we dump app stdout/stderr before throwing. Why: CI can be slower; longer timeout reduces flakiness; log dump makes timeouts debuggable.
- Coding agent is core, not optional:
@elizaos/plugin-coding-agentremains inCORE_PLUGINSso it is always auto-loaded. Why: Required for PTY/coding flows; optional would mean it is not in the default load set. - NODE_PATH for plugin resolution:
scripts/run-node.mjssetsNODE_PATHfor the spawned child sodist/eliza.jscan resolve@elizaos/*.src/runtime/eliza.tsprepends repo rootnode_moduleson load when not already set (dedupe). Electron: packaged uses ASARnode_modules; dev walks up from__dirnameto find monorepo root (no fixed depth). All callModule._initPaths()so Node re-reads NODE_PATH. Why: Dynamicimport("@elizaos/plugin-...")only works when Node knows where to look; seedocs/plugin-resolution-and-node-path.md. - NFA routes: optional @milady/plugin-bnb-identity:
/api/nfa/statusand/api/nfa/learningsnow lazy-load the plugin via dynamicimport()and fall back when the package is missing (empty Merkle root, empty entries). Tests mock the plugin so they pass without the workspace package. Why: Core can be built and tested without the plugin; installs or CI that don’t have the plugin still get a working API. An ambient declaration insrc/types/optional-plugin-modules.d.tslets TypeScript resolve the dynamic import even whenpackages/is excluded from the build. - CI / Mac binary build: Plugin and dependency copy for the Electron bundle is now derived automatically from each copied
@elizaospackage'spackage.jsondependencies (seescripts/copy-electron-plugins-and-deps.mjs). Why: A curated list was a maintenance burden and caused silent failures when new plugin runtime deps were added. Walking the dependency graph ensures we copy everything plugins need; we skip known dev/renderer-only packages (e.g. typescript, lucide-react) to avoid bloat. macOS x64 builds run root and Electron installs underarch -x86_64so native modules get x64 binaries on Intel Macs. Whisper universal binary is built in release; electron test jobs no longer usecontinue-on-erroron every step; Bun install cache andverify-build.sharch detection added.
- Windows release build (plugin-bnb-identity prepare): The plugin’s build script ran
npx tscfor declaration emit; on Windows CI this resolved to the joke npm packagetscinstead of the TypeScript compiler, so the prepare step failed. Why: We now usenpx -p typescript tscso the compiler is taken from thetypescriptpackage explicitly. See comment inpackages/plugin-bnb-identity/build.ts. - Release workflow size-report step (Linux/macOS): The
du | sort | headpipelines could exit with 141 (SIGPIPE) and, underbash -euo pipefail, the step exited before ther=$?check ran, failing the job. Why: We wrap each pipeline in a subshell and use( pipeline ) || r=$?so 141 doesn’t trigger errexit, then explicitly allow 0 or 141. We also redirectsortstderr to avoid "Broken pipe" noise. Seedocs/build-and-release.mdand the step comment inrelease.yml. - Intel Mac desktop app: Packaged DMG could fail with "Cannot find module .../darwin/x64/onnxruntime_binding.node" because CI runs on arm64 runners and was shipping arm64 native binaries. Why: Native Node addons (e.g. onnxruntime-node) are built for the install host's arch; installing and building under
arch -x86_64(Rosetta) produces x64.nodefiles so the Intel DMG works. - Electron agent startup: If
eliza.jsfailed to load (e.g. due to the above), the whole startup threw and the outer catch closed the API server. Why: We now isolate failures (.catch()on eliza import, try/catch aroundstartEliza()), keep the API server up, and setstate: "error"with port preserved so the renderer can display the error instead of "Failed to fetch". - Plugin resolution (
@elizaos/plugin-coding-agentand others): Dynamicimport("@elizaos/plugin-*")fromdist/eliza.jsormilady-dist/eliza.jsfailed with "Cannot find module" because Node's resolution did not reach repo rootnode_modules. Why: We setNODE_PATHin three places (eliza.ts on load, run-node.mjs for the CLI child, Electron agent for dev/packaged); seedocs/plugin-resolution-and-node-path.md. - Bun +
@elizaos/plugin-coding-agent: Underbun run dev, the plugin failed to load with "Cannot find module … from …/src/runtime/eliza.ts" even though the package was installed. Why: The published npm package hasexports["."].bun = "./src/index.ts"; that path exists only in the upstream dev workspace, not in the tarball. Bun's resolver picks the"bun"condition first and does not fall back to"import"when the file is missing. We patch the package'spackage.jsoninscripts/patch-deps.mjs(postinstall) to remove the deadbun/defaultconditions so Bun resolves via"import"→./dist/index.js. See "Bun and published package exports" indocs/plugin-resolution-and-node-path.md.
See Releases for version history.