Changelog
View Source[Unreleased]
Added
raxol acp --agent <app>[:<id>]: a Raxol agent for ACP editors. Eachsession/newstarts the named app (one of:mcp_spawnable_apps) with its journal bound to the real path of the request'scwd. A prompt is one turn of the agent; its tool calls are announced with their arguments, and a call to asensitiveaction asks throughsession/request_permission. With:<id>a later run continues the journal, one session per server process. See Editor ACP.raxol_agent:Raxol.Agent.McpTools.start_owned/4andstop_owned/2. Start and stop an allowlisted app for another owner under theagent_spawnrules; theagent_*tools do not answer for such an agent.raxol_agent: agents as MCP tools. Underraxol mcpandmix mcp.server,agent_spawn,agent_list,agent_ask,agent_status,agent_stopand the resourceraxol://agent/<id>/journalstart and drive agents this server started, each with a private, locked journal kept per project root (Raxol.Agent.McpTools).agent_spawnis refused on Windows (#1299).raxol_agent::mcp_spawnable_apps,:mcp_project_root,:mcp_exposed_agents,:mcp_tools. The appsagent_spawnmay start (default: the reference counter), the project root a journal is bound to (default: the working directory), agents started elsewhere that the tools may also reach, and where the tools register (true,false, or unset for a node booted for MCP).raxol mcp: Raxol apps as MCP tools over stdio.raxol mcp [--expect-version X] [--no-derived] [--bench-dump PATH]serves the headless tools on stdin/stdout through theuserdevice, and exits 0 at end of input after cancelling in-flight calls, giving agent journal writers 500 ms to flush and then writing the--bench-dumpfile (:erlang.term_to_binary(%{session_id => model}), its own 500 ms budget, a temporary file renamed into place). One predicate inpackages/raxol_cli/config/runtime.exs(RAXOL_MODE=mcp, or the Burrito binary run asraxol mcp) boots:raxolin:mcpmode withRaxol.CLI.MCP.authorize/3(every tool exceptraxol_startgiven apath; static tools only under--no-derived) andauthorize_read/3(the listing methods, andresources/read,resources/subscribeandresources/unsubscribeof the sessions it runs, minus the tool-listing session resources under--no-derived), without the harness tools, with the CLI version asserverInfo.version, and with every later application's group leader on stderr. The CLI's logger writes to stderr, and the release'svm.argssets-kernel standard_io_encoding unicodeso the wire is byte-exact underLC_ALL=C. It refuses (exit 1) a version mismatch, a VM the predicate did not boot, aRAXOL_MODEnaming another mode and a setRAXOL_CLI_INSTALL_DIR; unknown arguments exit 64. A VM the predicate booted runs no otherraxolcommand. A newmcp-smoke.ymlbuilds the linux-amd64 binary on pull requests touchingraxol_cli,raxol_mcp, rootlib/, the CLI Dockerfile ormix.lock, and drives it throughnode npm/bin/launcher.js mcp.raxol_agent: agent turn contract and a reference agent.Comm.call(agent, {:acp_prompt, text, approver}, timeout)asks a session for one turn, answered{:ok, text, usage};approver(an arity-3:tool_authorizer) authorizes that turn's tool calls and is consulted in the turn's own process (started with the newComm.run_turn/3), never inline inupdate/2, so a blocked ask holds up only that turn. The session stands in as the caller, so{:acp_cancel, ref}kills the turn's process instead of dropping its reply, answers the prompt{:error, :cancelled}, and, sent withComm.call, replies:okonly once that process is dead. A turn is also cancelled when its caller goes down or itsComm.calltimes out, so a prompt needs an explicit timeout:Comm.call/2refuses one with{:error, :timeout_required}without sending it (other messages keep the 5 s default). A prompt or call-form cancel sent withComm.send(with or withoutfrom:) is handled like itsComm.callform, and a prompt envelope without a pid caller and a reference ref is dropped without reachingupdate/2or the logs. A turn whose process crashes or is killed answers{:error, :turn_failed}, and every turn of a standalone session whose agent goes down answers{:error, :agent_down}with its process stopped (the session knows its agent is down by its own monitor, so a forged:DOWNnaming the agent's pid changes nothing); a turn that ends without its reply (cancelled or failed) reachesupdate/2as{:acp_cancel, ref}. The agent sees a ref the session made for the turn, never the caller's, so the caller's ref cancels a turn but cannot answer it, start its work or message its process; an inlineComm.replywhile the turn's process runs kills that process first, andupdate/2sees the cancel.{:error, :cancelled}does not roll back a change the turn already sent to the agent. Turn failures are logged by shape (exception module or leading atom), never content. The session finds the turn a:DOWNbelongs to without a scan, so many turns ending at once cost linear time.Comm.call/4takesref:(a fresh reference) so a caller knows the ref to cancel;Comm.send_turn/3messages a running turn. This{:acp_cancel, ref}is unrelated to the bare:acp_cancelatom that the ACP stack sends to aTurnRunnerprocess.Raxol.Agent.Reference.Counteranswers the contract withincrement,get, a sensitivereset, andhold, which blocks untilreleaseor a cancel, and applies a change only from a turn it still tracks (the ref is its only credential) and forgets a denied hold.raxol_agent:Raxol.Agent.Backend.Anthropic.Wire(ADR-0042).Wireis the pure Anthropic Messages codec moved out ofRaxol.Agent.Backend.HTTP:build_request/2returns{url, headers, body}(system messages joined intosystem,x-api-keyomitted when no key is set, a non-empty:toolslist passed through),parse_response/2returns the text ortool_useresponse contract or the keys-only{:error, marker}, andapi_version/0is theanthropic-versionheader.Backend.HTTPnow builds and parses every Anthropic request through it, with identical requests and responses; the caller still resolves themax_tokensdefault (AI_MAX_TOKENS).raxol_broker: an in-process Robinhood MCP server, and throttle handling for the read-only session (#1175).Raxol.Broker.MCP.Fakeserves the recorded tool list and a scripted scenario (quotes, positions, alerts, review warnings, rejected symbols, order states, idempotentplace_*), injects faults (HTTP status, malformed body, slow, closed, held, any transport error), gates on the bearer like Robinhood, and gives an isolated session throughclient_opts/2.Raxol.Broker.MCP.Clientretries 429/502/503/504 for calls with jittered backoff, within the caller's deadline and without letting retries open the session's breaker. Its connect retries any transient failure (a 5xx or throttle, a dial, read or deadline timeout, a transport or DNS failure) at most:retriestimes per run, one handshake per attempt, and after a failed run answers calls locally until a doubling cooldown ends.raxol_brokernow depends onplugdirectly. Inraxol_mcp,Raxol.MCP.Clienttakesfail_fast: true(a failed connect or handshake answersawait_ready/2at once, including a caller that arrives while the client is still closed, while the retry still runs) andRaxol.MCP.CircuitBreaker.failure_threshold/1is public. Seepackages/raxol_broker/CHANGELOG.md.raxol_broker: tool catalog, generated read tools and a capture task (#1174).Raxol.Broker.Tools.Catalogclassifies every tool from the frozen capture (priv/robinhood/tools_list.json, now carrying a provenance record) as:read,:review,:write(explicit allowlist:place_*_order,cancel_*_order,replace_*_order, alert create/update) or:unknown.review_for/1pairs aplace_*tool with itsreview_*/preview_*tool.reconcile/1diffs a livetools/listagainst the capture: changed review or write tools become:unknown(logged at:error), changed read tools stay readable (logged at:warning). The executor reconciles on every port connect and refuses a review or order tool that is not:review/:writefor the session with{:error, {:tool_refused, tool, class}}, before anything is sent. The read-onlyRaxol.Broker.MCP.Clientreplaces its static allowlist with the catalog.mix raxol.broker.gen.tools [--check]generatesRaxol.Broker.Tools.<Family>modules (one function per read tool, with@spec, argument validation frominputSchemaviaRaxol.Broker.Tools.Schema); a test fails when the checked-in files are stale.mix raxol.broker.capture_toolswrites a fresh capture from a signed-in session. The checked-in capture is Robinhood's live list of 2026-10-06 (76 tools): 50:read, 3:review(review_equity_order,review_option_order,preview_crypto_order), 8:write, and 15:unknownmutators (watchlists, scans,exercise_option,delete_alert, ...).raxol_mcp'sRaxol.MCP.Clienttool maps now carryannotations.raxol_broker: the Executor, with mandatory review and idempotent place (#1178).Raxol.Broker.Executor.run/3journals intent, context, pre-review verdict, review, post-review verdict,placingand the order response, in that order. Review warnings turn ALLOW into a parked ASK answered withapprove/3,decline/3orclose/3; approval re-runs the policy against fresh journal counters. The policy comes from the required:policystart option, not the caller's context, and the daily window is a rolling 24 hours from the executor's clock that no caller input moves. One executor per journal: only an executor process may claim it withRaxol.Broker.Journal.claim/1, only the claimant may write any group record, the journal takesplacingonly after a review and an ALLOW or approval, the journal checks the claimant before it reads anything a group write carries and rebuilds the intent and context from plain data before encoding them, and each Writer call has a deadline (:write_deadline, default 60 s;Raxol.Agent.Journal.FileStore.append_many/3gains a timeout): a Writer that misses it is killed and the journal stops with{:writer_stalled, ms}rather than report an error for a write that could still land, and recovery reads what reached disk. The executor stops with{:journal_down, reason}when the journal goes down so its supervisor restarts it and it claims again; itsinittouches only the journal, and the order session connects afterwards, answering{:error, :port_not_ready}until it is up and reconnecting with backoff when it drops, so a down endpoint, or a session client that dies while connecting, cannot exhaust the supervisor's restart budget; a port that drops between review and place closes the group asport_not_readywithout writingplacing. No caller-supplied code runs in the executor process or the journal: intents and contexts passRaxol.Broker.Plain(pattern matching only, so no caller Enumerable, Inspect or String.Chars implementation runs) and are rebuilt byIntent.normalize/1andPolicy.Context.normalize/1in the executor, the journal andPolicy.evaluate/2. Decimals are capped at a 40-digit coefficient and an exponent in -40..40, the clock's result must normalize to an ISO UTCDateTime, and external terms are only inspected withstructs: false, enforced by a structure test. A place to a session client that is already dead is recorded as not sent (:failed).Raxol.Broker.Supervisorrefuses an executor bound to another journal with{:journal_mismatch, given}, the executor's child spec waits up to the place timeout plus 5 s (or:shutdown) for an in-flight order on shutdown, and a journal stall beyond that is recovered ascrash_outcome_unknown, and timeouts are capped at 4294_000_000 ms. OnlyRaxol.Broker.Executor.Placenames an order tool, and only with an HMACReviewReceiptbound to the whole intent and the group, verified under a key that exists only in the executor's own process. The guarantee is that no path through public APIs sends an order tool without the review pipeline; code that deliberately writes into the executor's private process state is outside the threat model. An intent id with a liveplacingis never sent again, and each place carries aref_idderived from it. Once an order is sent the result is `{:ok, }, withjournaled: falseif the response could not be recorded. Dry run only, against a session markedsandbox: trueoffrobinhood.com; armed mode waits for #1179. Seepackages/raxol_broker/CHANGELOG.md`.raxol_agent: Robinhood browser sign-in, andraxol_broker: encrypted token store and refreshing MCP session (#1173).Raxol.Agent.Auth.Flow.run(:robinhood, opts)returns a%Raxol.Agent.Auth.Credential{}; it is a separate clause and NOT inFlow.providers/0, so ACP clients,raxol loginand the TUI never offer it.Raxol.Agent.Auth.Robinhoodpins the issuer, authorization, token and registration endpoints and the resource. RFC 9728/8414 discovery is fetched and checked against the pins ({:error, :metadata_mismatch}on any difference); dynamic client registration must echo the exact redirect URI and thenoneauth method; the code exchange andrefresh/2are form posts through the:http_fnseam (default Req withredirect: false, retry: false). Errors are a closed set such as{:token_rejected, status, :invalid_grant}, never a response body.refresh/2adopts the rotated refresh token.expires_atcomes fromexpires_inand isnilwhen the server sends none.Raxol.Agent.Auth.Loopback.open/1takeshost: :ip(redirect URIhttp://127.0.0.1:<port>/callback); the default stayslocalhostfor OpenRouter. The newawait/3ends the wait only for a GET on the callback path whosestatematches in constant time with no repeated parameter, requiresissto equal the issuer ({:error, :iss_mismatch}), and drops the provider'serror_description.await/2is unchanged.Raxol.Agent.Auth.Credential:Inspectredacts the access token, refresh token anduser_uuid; there is noJason.Encoder, onlydump/1/load/1for the encrypted store.raxol_brokeraddsRaxol.Broker.CredentialStore(AES-256-GCM envelope, key in 1Password or the macOS keychain),Raxol.Broker.Login, andRaxol.Broker.MCP.Client(single-flight refresh on 401 or expiry, read-only tool allowlist atcall/3). Seepackages/raxol_broker/CHANGELOG.md.raxol_mcp: an HTTP spec may pinera: :legacy | :modern, which skips theserver/discoverprobe at connect and after a rejected session (any other value is{:error, :invalid_era}). Robinhood's trading endpoint answers the probe with a plain-text 400, which is correctly not era evidence, so an unpinned client never connected; the broker pins:legacy. A 401 at connect or handshake no longer reconnects:Raxol.MCP.Clientstays:closedwith{:connect_failed, reason}and answersawait_ready/2at once, instead of re-sending the same rejected headers every backoff interval until the shared per-origin breaker opened.
raxol_brokerstarts as a fail-closed pre-alpha package (#1172).Raxol.Broker.PolicyFileparses a boundedbroker.policy.exsas data rather than evaluating Elixir, then validates every policy field and requires explicit positive per-order and daily notional caps;ask_timeoutis limited to 1..4_294_967_295 ms and invalid UTF-8 is a parse error. Loading rejects symlinks and refuses a file that fails the owner, mode, and parent-directory rules ofRaxol.Agent.OperatorFile.trusted?/1({:untrusted_file, path, reason}).PolicyFile.new/2builds the complete restrictive policy from the two required caps.mix raxol.broker.initpublishes it atomically, mode 0600, without replacing an existing destination; prompting is limited to an interactive terminal, accepts the trimmed entered line, fails on EOF without writing, and is covered through the deterministic process shell used by tests. Decimal 3 avoids EEF-CVE-2026-32686.- FX market data with a peg verdict (ADR-0040).
raxol_web3gainsRaxol.Web3.FX:FX.Sleuthreads Sleuth's fiat-stablecoin surface (Bearer key only, never in a URL orinspect/1; 15 requests a minute; 60 s cache; every figure aDecimal; symbols canonical toRaxol.Payments.Assets),FX.Chainlinkreads the rate of record byeth_call(its RPC URLs, which often carry a provider key, never render throughinspect/1) (EUR from Base with Ethereum fallback, CHF from Ethereum, USD exactly 1; feed identity,heartbeat * 1.1freshness and the Base sequencer checked first; a wrong feed address is blocked, not failed over), andFX.Qualityjudges each asset against that rate as:ok,:suspect(more than 100 bps off, or no price),:no_rateor:yield_bearing, never from Sleuth's own deviation or stale FX rates. Served as theweb3_fx_stables,web3_fx_stableandweb3_fx_corridorsMCP tools (Raxol.Web3.MCP.FXTools, read-only and sensitive) and as thefxagent tool (Raxol.Agent.Actions.FX).Raxol.Web3.Serialize.result/1now renders aDecimalas a string instead of throughinspect/1. raxol_agent: theweb3andfxtools are wired into real sessions. NewRaxol.Agent.Web3readsconfig :raxol_agent, :web3(router:enablesweb3,fx: [rpc_urls: ...]plusRAXOL_SLEUTH_API_KEYenablesfx) and puts:web3_router/:fx_sourceinto the console gateway's tool context. Until nowcontext[:web3_router]was set only in tests. Unconfigured, nothing changes;fxconfigured without the key refuses theraxol_agentboot and names the variable. The configuration is resolved once there and cached, so no context build re-reads it or raises.- Security:
Raxol.System.Updaterverifies each release's Sigstore provenance before installing (#1075). Until now a download was only checked againstSHA256SUMSfrom the same release, so anyone able to replace release assets could replace both. The newRaxol.System.Updater.Provenancechecks the release'sraxol-cli-attestation.sigstore.jsonnatively, with:public_key,:cryptoand Jason and noghorcosignat runtime:- The DSSE signature must verify with the Fulcio leaf key. The leaf must chain to a pinned Fulcio root at the Rekor integrated time, carry code signing and an SCT from a trusted CT log, and the integrated time must fall inside its validity.
- The Rekor
dsseentry must commit to this envelope and certificate. Its RFC 6962 inclusion proof must reach a checkpoint signed by the pinned Rekor key, and its signed entry timestamp must verify. - The certificate's issuer, source repository,
refs/tags/<tag>, SAN (the manifest'ssigner_workflowat that ref) and GitHub-hosted runner must match. The in-toto SLSA statement must name the asset with the downloaded digest. Manifestgainsprovenance: :required | :off(default:required),attestation_assetandsigner_workflow. A missing or failing attestation refuses the install ({:error, {:provenance_failed, reason}}) before anything is extracted or replaced, onself_update/2and ondownload_update/2+install_update/3alike.raxol updatereports the verification on success and explains a refusal.- The Sigstore public-good trusted root is compiled in from
priv/sigstore/trusted_root.json; itsREADME.mdrecords the source and how to refresh it. Verified against the realraxol-cli-v0.2.10release bundle (all four binaries) and the@raxol/cli@0.2.8npm provenance bundle. Releases before 0.2.10 carry no attestation, so the updater will not install them.
raxol_brokeraddsRaxol.Broker.Journal(write-ahead decision groups with aplacingrecord before every order call, crash recovery, ETS index fortoday_notional/1,orders_last_minute/1andrealized_pnl_today/1) andmix raxol.broker.replay --date YYYY-MM-DD. Seepackages/raxol_broker/CHANGELOG.md.
Fixed
raxol acp: a server whose launcher died stops.Raxol.Agent.ClientProtocol.ParentWatchbuilt its default action and never called it, so the orphaned BEAM kept its agents and journal locks. It now callsSystem.stop/1; the:on_orphanoption is replaced by:stop.raxol_agent_client_protocol: a turn that ends with a permission ask outstanding withdraws it.Sessionanswers the asker:cancelledand sends$/cancel_requestfor the client's request, assession/cancelalready did; the ask no longer waits out its timeout.raxol_agentkeeps local package dependencies out of its Hex lock. The Req 0.7.5 update no longer locksraxol_coreandraxol_mcpas published packages, preserving source-worktree dependency pins and passing the path-shadow guard.raxol_agenton Windows: a CRLFSKILL.mdparses.Skill.parse/1required LF after each---delimiter, so aSKILL.mdsaved with CRLF endings was:missing_frontmatter.Skills.Store.view/3now also refuses a drive-rooted path such as/etc/passwdon Windows (:volumerelative). A test parses a CRLF skill on every platform.raxol_agent:ThreadLog.Postgrexrange reads no longer come back empty. The list query builder numbered the optional clauses andLIMITindependently, solist_by_kind/4with the defaultto: :infinityandlist/3with a bounded:toboth put two parameters on$4; Postgres rejected the statement and the error was returned as{:ok, []}. Placeholders are now numbered from the parameter list itself. A failed read inlist/3,list_by_kind/4orlatest/2is now logged with the operation, table and thread (their behaviour callbacks have no error channel, so the return values are unchanged).Tests no longer blank
PATHinsideasync: truemodules.raxol_symphony'sSshTestsetPATHto""to proveSsh.exec/3refuses withoutssh, andraxol_payments'AnvilTestpointed it at a missing directory;PATHis VM-global, so async modules running alongside saw it too (CI caughtPortReaperTestcallingSystem.cmd(nil, ...)afterSystem.find_executable("bash")returnednil). Both tests moved, unchanged in meaning, intoasync: falsemodules, which ExUnit runs only after every async module has finished.raxol_agent:ThreadLog.Postgrexreads canonical event kinds in a fresh VM. Rows were decoded withString.to_existing_atom/1, so reading atool_call,state_snapshotor other canonical kind raisedArgumentErroruntil some loaded module happened to name the atom. Canonical kinds now decode through a literal map built from the newThreadEvent.canonical_kinds/0; a custom kind decodes to its atom if one exists and otherwise stays a string, without minting atoms. Same bug class as #1196. The:integrationsuite can now run (postgrexis a test-only dependency and tables are created one statement at a time); a regression test reads rows from a child VM that has not interned the kinds.raxol_agent: reading~/.raxol/providers.jsonno longer depends on which modules are loaded (#1196).Credentials.sanitize_entry/1turned the~w(op_ref model base_url)field names into atoms withString.to_existing_atom/1, which raised when no loaded module had named:op_refyet, sofetch/1,delete/1,put/2andload/0could crash depending on load order. The fields now map to literal atoms. A regression reads the store from a fresh BEAM where onlyCredentialsis loaded.raxol_agent:opchildren get/dev/nullfor stdin, not the BEAM's terminal.Credentials.run_executable/3relied on the port's:inoption to giveopEOF, but:inmakes the child inherit the BEAM's stdin, which under the TUI is the user's tty:opcould prompt on it and read keystrokes the TUI owns, and the stdin regression test hung to its deadline whenever the suite ran from a terminal. On Unix the child is now started through/bin/sh -c 'exec "$0" "$@" </dev/null'(executable and args as positional parameters, pid preserved for the deadline kill); Windows keeps the direct spawn.raxol_agent: reading~/.raxol/providers.jsonno longer depends on which modules are loaded (#1196). Theop_ref,modelandbase_urlfields were turned into atoms withString.to_existing_atom/1, which raised when no loaded module had named them yet, soCredentials.fetch/1,delete/1,put/2andload/0could crash depending on load order. The fields are now literal atoms. A regression reads all three fields from a fresh BEAM after checking that none of the atoms exist there yet.Security: spawned processes no longer get the TUI's terminal as stdin (#1199). Every spawner in
raxol_agent, andraxol_symphony's workspace hooks, opened its port with:in, believing it gives the child/dev/null. On Unix it does not: erts skips the stdindup2and the child inherits the BEAM's fd 0, which under the TUI is the user's tty. A shell directive's command could read keystrokes typed into the TUI and rewrite the terminal's modes; the same held for Symphony hooks and theopCLI, which prompted on the tty instead of using the desktop app. Shell spawners now prefix their script withexec </dev/null;opruns through the newRaxol.Agent.SpawnedPort.spawn_spec/2, which wraps it insh -c 'exec "$@" </dev/null'with the executable expanded to an absolute path, removesSHELLOPTS,BASHOPTS,PS4,BASH_ENVandENVfrom the wrapper's environment, and drops anything the wrapper prints beforeexec(bash-as-sh warns about an uninstalledLC_ALL, for one), so none of it can reach a secretop readreturns. Unix ports no longer pass:in, so a spawner that loses its redirect hangs its stdin test on every host rather than only under a terminal. Windows keeps:in, where erts does openNULfor an input-only port.raxol_agent:opis looked up only on absolutePATHentries. A relative entry (bin,., an empty element) resolves against the BEAM's working directory, so a cloned repository could ship abin/opthat received everyop readandop item create.Credentials.op_status/0now reports anopthat cannot be executed (exit 126/127, or a failed spawn) as:absentrather than asking forop signin, andrun_executable/3returns{:error, :op_spawn_failed | {:op_spawn_failed, reason}}instead of raising.raxol_web3: keyed RPC URLs no longer render throughinspect/1. Provider URLs usually carry the key (/v2/<key>,?api-key=), and handles reach agent tool contexts,Router.candidates/3and crash reports, which format a process's state whole.FX.Chainlinknow hidesrpc_urls,Backend.JSONRPCandBackend.Solanahideurl, andBackend.Blockscouthidesrpc_url, as each already hidhttp_opts. Error terms keep naming an origin byRaxol.Web3.Originid.raxol_web3: the Chainlink rate keeps the feed's eight decimals. It is built asanswer * 10^-8without division, so it renders as, say,1.13388500rather than1.133885. The value is the same (Decimal.equal?/2), but it is no longer==to a rate built by division.raxol_agent's refusal of a malformedrouter:names a struct by its module and a keyword list as a list.raxol_web3,raxol_payments: Sleuth text is stripped of anything invisible, and symbols fold in ASCII everywhere. The control/format filter missed variation selectors, which can hide arbitrary bytes in a name a model reads, and line separators, while dropping whole legitimate names over a soft hyphen. Text fields now have every invisible character removed (control and format characters, line and paragraph separators, variation selectors, Hangul fillers), andcorridorandpegCurrencymust be printable ASCII. The newRaxol.Payments.Assets.fold_symbol/1upper-cases in ASCII only, andAssetslookups,Prices.FXandFX.price_fn/3all use it, so a lookalike such asuſdcorEURıno longer folds onto a registered symbol.FX.Chainlink.route_id/1is the one route identity: the identity cache key sorts headers by name only, so two handles whose repeated routing headers differ in order no longer share a verified identity, and the rate-log state is kept per route and per reason class, so a{:rate_limited, ms}no longer logs every sweep.raxol_web3: Sleuth text is an allowlist, and identities are refused rather than stripped. The invisible-character denylist let through invisibles it did not name: Mongolian free variation selectors, the combining grapheme joiner, the Khmer inherent vowels and the braille blank all reached a model inname. Text fields (name,aliases,peg_mechanism,as_of) now keep only letters, numbers, punctuation, symbols, the ASCII space and combining marks on a kept character, so accents and the emoji either side of a joiner stay.symboland a pool'schain,dex,pair,baseandquotemust be printable ASCII or are refused (symbolas""), soEUR\u200BCis no longer merged intoEURC. The live API (2026-10-02, 300 assets) had no non-ASCII text, so nothing real is dropped today.raxol_web3:FX.Chainlink.route_id/1takes map headers and compares header names case-insensitively.:headersgiven as a map, whichRaxol.Web3.HTTPaccepts, raisedFunctionClauseErrorfromroute_id/1and the identity cache key. Names are now downcased before the stable sort, so[{"x-net", "base"}, {"X-Net", "impostor"}]and its reverse, which a gateway honouring the firstx-netroutes apart, no longer share a route id or a verified identity.raxol_agent:config :raxol_agent, :web3, router:must be aRaxol.Web3.Router.router: false(or any other value) loaded and offered theweb3tool over no router. It now refuses the boot with anArgumentErrornaming the setting and its shape.raxol_web3: smaller FX fixes.FX.Chainlink's identity cache key now digests the request headers in:http_optswith the URL, so a handle routed to another network by a header no longer reads a sibling's verified identity. A rate served by a fallback feed carries:fallback_from, the primary's failure.FX.price_fn/3logs a peg's rate state when it changes, not on every sweep: a{:blocked, _}refusal at warning, anything else at info, a fallback feed with the primary's reason, and the return to the primary. It folds symbols withString.upcase/1, asRaxol.Payments.Assetsdoes.FX.Sleuth: astablescall that sent no query reports a 400 as{:http, 400}; text fields carrying control or format characters (C0/C1, bidi overrides, zero-width) decode tonil;pegCurrencyis compared upper-cased; and a decimaldeviationBpsis held to the same 10^38 bound as an integer one.
raxol_web3: Sleuth lists are capped at decode, and no FX figure depends on the caller'sDecimalcontext. A 262 KB body of{}entries decoded to 87,000 assets and a 32 MB tool result, cached for 60 s.assetsnow stops at the requestedlimit(300 by default),topPoolsat 32,corridorsat 64 with 400 corridor assets in total, andfxRatesUsdat 64 rates keyed by a three- or four-letter code.deviationBpsis rounded in a pinned context, so a caller's trapping context no longer raises from inside the HTTP cache stage; the Chainlink rate is built asanswer * 10^-8rather than divided, so a caller's precision or exponent limit no longer rounds it or turns it intoInfinity; andFX.Qualityjudges a non-finite rate:no_rateinstead of:ok.raxol_agent: a:web3orfx:setting that is not a keyword list refuses the boot by name.config :raxol_agent, :web3, fx: true(or a map) stopped the boot with aFunctionClauseErrorfromKeyword.get/3. It is now anArgumentErrornaming the setting and its shape, never its value, which may hold the Sleuth key.raxol_web3: a Sleuth 400 names the argument that was sent. Every 400 was{:invalid_argument, "query"}, including onstable/2, which sends a symbol, andcorridors/1, which sends nothing. It is now"symbol"onstable/2and{:http, 400}oncorridors/1, where no argument can have been refused.raxol_web3:FX.price_fn/3compares symbols case-insensitively and says why a peg is unrated. Listings were grouped and lookups matched by exact symbol, so aeurclisting at $0.50 did not vetoEURC, andprice.("EURE")orprice.("eurc")reached the fallback, which could price a euro at par, whileRaxol.Payments.Assetstreats those symbols case-insensitively. Both now fold to ASCII upper case. A peg whose Chainlink read fails is now logged with the reason ({:blocked, :feed_mismatch}at warning level,:stale,:sequencer_downand the rest at info) instead of only showing up as unpriced legs.raxol_agent: a Sleuth key with a space or line break inside it refuses the boot with a namedArgumentError.Raxol.Agent.Web3matched{:ok, sleuth} = Sleuth.new(...), so a key thatFX.Sleuth.new/1now refuses stopped theraxol_agentboot with a bareMatchError. The error now says whether the key came fromRAXOL_SLEUTH_API_KEYorconfig :raxol_agent, :web3, fx: [sleuth_api_key: ...], and never includes the key.raxol_web3: three smallerFX.Sleuthfixes.stable/2refuses a symbol that does not start with a letter or digit, so".."can no longer send/api/mcp/fx/stables/..with the Bearer key (USDC.eis still looked up). A 403 is{:http, 403}, asRaxol.Web3.Backenddocuments it, instead of{:upstream_refused, :auth}, so a CDN challenge no longer reads as a bad key; it still counts as a fault of the source inMCP.FXTools.new/1trims the key and refuses one with a byte outside visible ASCII (a CR, LF or space) as{:invalid_argument, "api_key"}, rather than letting the transport refuse the header later with an error that names nothing.raxol_web3:FX.Qualitydraws its 100 bps line exactly, whatever the caller'sDecimalcontext. It compared the deviation after rounding it half-even to whole basis points, so 100.4 and 100.5 bps (and -100.5) were judged:ok; the next version divided under the process's 28-digit context, so a 34-digit price just past the line was:okand a caller's narrower context could flip a verdict or raise. The line is now|price - rate| * 100 > rate, with no division, in a pinned 200-digit context that is exact for every figure the decoder admits. Only thedeviation_bpsthe verdict carries is rounded, for display, in its own pinned context. A rate that is not positive is:no_rate.raxol_web3: a wrong Chainlink answer no longer pins{:blocked, :feed_mismatch}on every handle sharing its RPC host for a day, and a verified identity no longer vouches for another handle.FX.Chainlinkcached a feed'sdescription()anddecimals()for 24 h under a key of origin, proxy and selector, whatever they said, so one handle whose Base URL reached the wrong chain ("0x") blocked a correctly configured handle on the same host, and blocked its Ethereum fallback with it. An identity is now cached only when it is the expected one, under a key that adds the chain id and a truncated SHA-256 of the RPC URL (so a provider key in the path never enters it), and an answer that does not decode ("0x"from an address with no code) is{:decode_failed, :identity}, which fails over. Without the URL in the key, a healthy handle's cached identity let a sibling on another path skip its own check and read a rate from an impostor contract. A decodable but wrong identity is still blocked.raxol_web3: Sleuth's payload no longer decides which symbolsFX.price_fnreprices, or at which peg.price_fn/2is nowprice_fn/3, taking the registered%{symbol => peg}table (Raxol.Payments.Assets.fx_pegs/0, passed in byRaxol.Payments.Prices.FX). Only those symbols are answered, at the Chainlink rate for the registered peg; Sleuth's listing can only veto. A hostile listing ofETHwithpegCurrency: "EUR"used to price ETH at 1.13 instead of the fallback's 2500. A registered symbol that is absent, relabelled, listed twice with any listing not:ok, or missing because the snapshot failed isnil, never handed to the fallback.raxol_web3: a malformed or hostile Sleuth body no longer crashes the FX decoder or burns CPU.FX.Sleuthskips anyassets,topPoolsorcorridorsentry that is not a JSON object, numbers included (1.5decodes to a%Decimal{}struct, which%{}matches), and refuses a non-objectassetorfxRatesUsd;{"assets":[1]}and{"assets":[1.5]}raised fromstables/2andFX.price_fn/3. A figure isnilunless it has at most 38 significant digits and an exponent within ±30, checked on the struct before any arithmetic (deviationBps: 1e1000000, nine bytes of JSON, ran past a minute inDecimal.round/2). Under decimal 3, whichraxol_web3now requires likeraxol_payments, a number with more than 34 digits or an exponent past ±6_144 fails the whole body as{:decode_failed, :sleuth}instead. Every other field is typed and bounded at decode: text at most 256 bytes, counts non-negative integers below 10^9, at most 32 stringaliases, elsenil. A 200 that is not the documented envelope is never cached, so it is not replayed for 60 s. Symbols are canonicalized in ASCII only (MONERıUMis no longerEURe), andnew/1returns an error for a non-string:base_url.Serialize.result/1renders aDecimalwith a larger exponent in scientific form.raxol_agent_client_protocol:Transport.Pairedlost what reached a side before its owner was set. A side with no owner discarded peer frames and the peer's close, against theTransportbehaviour's T-REL clause.Connectionadopts its handle inhandle_continue/2, afterAgent.start_link/2andClient.start_link/2return, so a client that sentinitializeright after both sides started could reach the agent's side first: the request was dropped and the client got{:error, :timeout}(the agent test's end-to-end smoke flaked this way in CI). Frames and the close are now held untilset_owner/2, which delivers them in arrival order ahead of anything later, asTransport.Stdioalready did. New tests send, and close, before adopting (both failed before).Raxol.System.Updater.Statecrashed on Windows.UpdaterServerbuilt its default download and backup paths asSystem.get_env("HOME") <> "/.raxol/..."; Windows setsUSERPROFILE, notHOME, soinitraisedbadargand everyUpdater.Statecall failed (the master Windows test leg went red once #1094 made the API reachable). Both default-settings builders now usePath.expand("~/.raxol/..."), like the settings file already did. New tests start the server withHOMEunset and check the paths (both failed before).raxol_agent:Conversation.Logkept an empty subscriber set for every conversation ever subscribed to.unsubscribe/2and a subscriber's:DOWNdeleted the pid from the conversation'sMapSetbut kept the key, so the log'ssubscribersmap grew by one entry per conversation id for the life of the log (andunsubscribe/2on a conversation never subscribed to added one). A conversation's entry is now removed when its last subscriber leaves. The newlog_test.exstests failed before the fix (four empty sets left after unsubscribes; the killed subscriber's conversation left as an empty set) (#1080).raxol_agent:SessionStreamermonitored its subscribers on every subscribe and never let go. Duplicate subscribes and each subscribe/unsubscribe cycle added a monitor that lasted until the subscriber died, so a long-lived consumer cycling per-run sessions grew the streamer's monitor list without bound (3,000 after 1,000 cycles). It now keeps one monitor per subscriber pid and removes it when that pid leaves its last session.Security:
Raxol.System.Updaterinstalled downloads it never verified, from a repository it did not own. The self-updater was pinned tousername/raxol(andDeltaUpdatertoraxol/raxol), so whoever controlled those GitHub accounts decided whatself_update/2downloaded. It unpacked archives with the host'star/unzipand copied the result over the running executable; theverify_checksums: truesetting was never read. The delta path ran the patched binary (--version) before checking anything, andCorepassedapply_delta_update/2its arguments in reverse order (the delta path is now removed; see Removed). Now:- Every URL comes from
Raxol.System.Updater.Manifest, never from a release's ownbrowser_download_url. The default channel is theraxolCLI'sraxol-cli-v*releases onDROOdotFOO/raxol(raw binaries plusSHA256SUMS); apps set their own withconfig :raxol, :updater_manifest. Base URLs must behttps(plainhttponly on loopback), and a version must beX.Y.Z. - Each download must match its
SHA256SUMSentry before it is extracted or installed. A missing, duplicated or malformed entry means nothing is downloaded at all. - Archives are read with
:erl_tar/:zip, and any absolute,.., drive-letter, symlink or hard-link entry refuses the whole archive before anything is written. - The new binary is renamed into place from the same directory, and the one it replaces is kept for
rollback_update/1, which until now had nothing to restore. HTTPS verifies the certificate and hostname.
- Every URL comes from
Raxol.System.Updater.check_for_updates/1raised, and version checks never agreed. It matched{:ok, settings}against the bare settings map (WithClauseErroron every call), compared"2.7.0"with the tag"v2.7.0"so an update was always "available", and read the version throughMix.Project, which a release does not have. The automatic-check interval read string keys thatset_auto_check/1never wrote, so disabling checks did nothing. The installed version now comes from the manifest's application, versions compare withVersion, and the interval uses the atom keys that are actually stored.self_update/2also no longer falls back toSystem.argv()for "the executable", which could have overwritten a file named on the command line.Raxol.CLI.Commands.UpdateCmd.execute/1raised on any call without--help. It matched absent flags asfalse, butOptionParserreturnsnilfor them, so every call hitCaseClauseError.raxol_earn: a failed seller offer kept itsJobSessionrunning and its capacity slot.Raxol.Earn.Seller.Queuestarted a session for every:job_offered, and when the offering'sresolve_accept/2or the on-chainsetBudgetthen failed, it dropped the offer without stopping that session. The job was never tracked, so its later:job_expireddropped as:job_not_runningand nothing ever ended the session: withseller_max_active_jobs: 1, onesetBudgetRPC error made every later offer drop as:at_capacity. Those error branches now stop the session the offer started, so the slot is freed. A session the offer did not start (rehydrated byResync, or one that survived a Queue restart) is left running.raxol_cli: the appraxol newgenerates compiles and runs (#1102). The template useduse Raxol.UI, framework: :react, which builds a component and brings none of the view DSL into scope, and importedkey_matchfromRaxol.Core.Runtime.Application, which does not define it, so the new project failed to compile onkey_match("+"). It now usesuse Raxol.Core.Runtime.Applicationlike the examples and the quickstart, quits withDirective.stop()onqand on Ctrl+C (as themix raxol.newtemplates and now the quickstart skeleton do), and itsstart/0waits for the app to exit, sincemix run -e "MyApp.start()"halts the VM as soon as the call returns. Everything the template calls exists unchanged in raxol 2.6.0 and 2.7.0, so the generatedraxol ~> 2.6requirement stands. A new test generates the app, compiles it against this checkout and renders its first frame headlessly (it failed to compile before).raxol newalso refuses a name that cannot make a working project, before creating anything: the check matched$, which lets a trailing newline through, and now reserves the namesmix raxol.newreserves (raxol,mix, ...) and, asmix newdoes, any name whose module already exists (enumwould redefineEnum).
Changed
Breaking:
raxol_agent:Raxol.Agent.Contract.pump/2,3isRaxol.Agent.StreamPump.pump/2,3, and the old name is gone. Same arguments and results.Raxol.Agent.Contractis inraxol_journaland keeps only the event schema, its encoder andevidence_status/1.raxol_agent: a cancelled turn is stopped, then killed. Its process gets an exit signal with reason:shutdownand is killed only if still alive afterconfig :raxol_agent, :turn_cancel_grace_ms(default 500); a monitor of a turn that ends within the grace sees:shutdown, not:killed.Breaking:
raxol acpno longer serves the coding agent.--backend,--model,--harnessandHARBOR_ACP_REQUESTED_MODELare gone (an unknown argument exits 64);session/loadis not offered (loadSession: false); the Terminal Auth method is not advertised; asession/newcwdthat is not an existing directory named by an absolute path is refused.Raxol.Agent.ClientProtocol.Permission.authorizer/2announces every tool call with its arguments and must be built in the process that ends the turn, which closes the calls withsettle/3.Raxol.Agent.ClientProtocol.Budgetis held by the connection as a pid:check/0,record/1andspent/0give way tobegin_turn/1,record_usage/2andspent/1,start_link/1takes the environment, andRAXOL_MAX_TURNScounts a turn when it starts, whatever its outcome.raxol_agent_client_protocol:Transport.Stdio.start_self/1reads and writes theuserdevice, as bytes. A{:fd, 0, 1}port lost input that arrived before it opened under-noshell. It takes:inputand:outputdevices, no longer needs-noinput, and sets both toencoding: :latin1so the wire is UTF-8 whatever the locale.raxol_agent: every prompt turn is bracketed in the journal. A session journalsturn_startedand one ofturn_completed,turn_canceledorerroraround each{:acp_prompt, ...}turn, under a turn id of its own.raxol_agent: a derived session id ends in 12 hex digits, not an integer. A session started without:session_idjournals under<base>-<12 hex>.raxol_agent: the modules ADR-0042 keeps no longer reach into the coding harness. Breaking forraxol_agentusers: no behaviour changes, but a documented public name of the published package moves, so direct callers must update. The shell and network gatesshell_allow/2,shell_jail_allow/1,network_allow/1,sandbox_allow/2andjailed?/1move fromRaxol.Agent.Actions.Codeto the newRaxol.Agent.Policy.Grants(no alias left behind), and every caller (Directive.Executor,Actions.{FX,Web3,Fetch,WebSearch,Shell,Shell.Start,Code.Bash},Code.Hooks) asks there.Raxol.Agent.Auth.Flow's default OpenRouter:store_fnis nowRaxol.Agent.Auth.OpenRouter.store_key/2(the sameSetup.connect_key/3call with the same 120 s 1Password budget), soFlowno longer aliasesRaxol.Agent.Setup. ADR-0040 notes the moved FX gate.CI:
raxol_agent's package tests run on Windows. A newPackage Tests (raxol_agent, Windows)job runs the suite onwindows-2022, so the direct-spawn clause ofSpawnedPort.spawn_spec/3andCredentials.run_executable/3are exercised where they apply: four new:windows_onlytests runsort.exeagainst the NUL stdin erts gives an:inport (EOF, not a timeout), round-tripcmd /c echo, report a missing executable as{:op_spawn_failed, :enoent}and bound a longping. The package'stest_helpernow excludes:unix_only/:skip_on_windowson Windows (as the root suite does) and:windows_onlyelsewhere. Tests that need/bin/sh, shebang scripts, POSIX modes,$HOME,/procor/devare tagged:unix_only. It runs, and gatesCI Status, on platform-sensitive pull requests and nightly (see theplatform-tests.ymlentry above).raxol updateruns onRaxol.System.Updater.Raxol.CLI.Updatekept its own copy of release lookup,SHA256SUMSparsing, download, checksum and binary replacement. It now keeps only the command-line experience (flags, messages, exit codes, the daily prompt and itsRAXOL_NO_UPDATE_CHECK/~/.raxol/cli-update-check.jsoncache) and hands everything else to the library updater, so there is one updater with one set of guarantees. What changes for users: the replaced binary is now kept in~/.raxol/backups/previous_version,SHA256SUMSis parsed strictly (a malformed or duplicated line refuses the update), and asking for an unpublished version (--version 9.9.9) says "no such release".Raxol.System.Updater.check_for_updates/1takes a:versionoption to check one release instead of the newest.
Removed
raxol_agent: memory, self-improvement and the coding tools. Breaking. Removed:Raxol.Agent.Code.*,Memory.*,UserModel,SelfImprove,Curator,ContextCompactor,Compaction,Trajectory,Probe.*,Auxiliary,Fingerprint,SignalTrap,NativeHarness,LSPContext,Lsp.*,Shell.{Jobs,Pty},P,Setup.CLI,ClientProtocol.Login,Harness.{ClaudeCode,Cursor,GrokBuild,ToolExecutor,McpTools,McpToolConfig,StreamJson,SessionInbox,ToolClassifier}andActions.{Code,Fs,Shell,Workspace,Lsp,Anchor,SessionSearch,Vfs,Memory,Fetch,WebSearch,Task}.use Raxol.Agentno longer definesmemory_provider/0,memory_providers/0,self_improve/0orcompaction_config/0, andRaxol.Agent.Processno longer compactsmodel.history.Raxol.Agent.Supervisorno longer readsconfig :raxol_agent, :memory_provideror:curator. Skills stay (Raxol.Agent.Skills.Storeand the three skill tools); nothing here replaces memory.raxol_agent: modules nothing called. Breaking. Removed:Raxol.Agent.Turn,Conversation.Recorder,Interrupt,Steer,SnapshotandSnapshot.Persist,Tunnel(withFrameandLink),MetaandMeta.Registry,Authorization.{BlastRadiusGate,Hook,Server,McpAuthorizer},PolicywithPolicy.{Cache,Retry,Timeout}andPolicyApplier,CachewithCache.{Ets,Postgrex},ThreadLogRouter,PermissionHook,Command,Reattach(withFileReaderandTailer),Harness.SessionLane,EventForwarder,Stream.Event,McpClientandMcpClient.Message,SessionStreamServer,SessionKey,LlmPricesand theRaxol.Agent.TaskSupervisorchild ofRaxol.Agent.Supervisor.use Raxol.Agentno longer definesthread_log/0, andRaxol.Agent.thread_log_adapter/1,Telemetry.identifier?/1andTelemetry.max_identifier_bytes/0are removed. Also gone: the[:raxol, :agent, :policy, ...]and[:raxol, :agent, :snapshot, :persist_redacted_by_heuristic]events, theconfig :raxol_agentkeys:session_stream_token,:session_stream_require_auth,:session_stream_allowed_origins,:reattach_impl,:reattach_poll_msand:interrupt_kill_sh, andRAXOL_SESSION_STREAM_TOKEN.- For
Turn.run/3, callRaxol.Agent.Stream.react/2withactions: Mod.available_actions()andcontext: Raxol.Agent.Web3.put_context(%{skills: Raxol.Agent.Skills.provider_context(Mod.skills_provider())}), leaving:skillsout when the agent has no provider. Nothing records the turn. - For
PermissionHook, declaresandbox/0withSandbox.Shell,SendAgentandAsync, whichSandboxHookenforces. Its prompter has no replacement. - For
McpClient, useRaxol.MCP.Client. - For
Authorization.HookandMcpAuthorizer, callAuthorization.Engine.evaluate/4from your ownCommandHookor:authorizerfunction. Policy.Grants,Authorization.{Engine,Policy,Verdict,Labels},Sandbox.{Shell,SendAgent,Async},Conversation.LogwithStore,Store.ETSandItem,ThreadLogwith its Ets and Postgrex adapters (Raxol.Agent.Schedulertakes one as:thread_log), andStrategy.{Direct,ReAct}stay.
- For
raxol_agent: the:claude_native,:grok_native,:cursorand:codexbackends. Breaking.Backend.{Native,ClaudeCode,Cursor,GrokBuild,Cli}are removed with the catalog's:nativeand:reservedkinds andCatalog.by_kind/1;Selector.select/1answers{:error, {:unknown_backend, id}}for those ids. Use an HTTP backend such as:anthropic.config :raxol_agent, :native_probeis not read, andBackend.Resolverno longer returns the:subscriptionsource. Auto-detection no longer finds an installed vendor CLI: a surface that resolves a provider itself (the gateway agent handler, the scheduler, the console) and finds none answers from the Mock backend. A pinned removed id answers from Mock too, andRaxol.Agent.Streamlogs a warning.Raxol.Agent.BenchmarkProfilekeeps the turn and cost caps only (RAXOL_MODEL,RAXOL_PROFILEandRAXOL_TRAJECTORY_PATHare not read), and the[:raxol, :agent, :cost, ...]and[:raxol, :agent, :budget, :halt, ...]telemetry events are no longer emitted.raxol_agent: the ACP coding-agent turn runner.Raxol.Agent.ClientProtocol.TurnRunner(withdetect_executor/0),Raxol.Agent.AcpStreamAdapter,mix raxol.acp,Raxol.Agent.ClientProtocol.Serve.apply_requested_model/2, and the telemetry events[:raxol, :agent, :acp_turn_runner, :interrupt_failed]and[:raxol, :agent, :acp_turn_runner, :journal_failed].bin/raxol-acpnow takes--agent.raxol_agent: the ACP coding-agent turn runner.Raxol.Agent.ClientProtocol.TurnRunner(withdetect_executor/0),Raxol.Agent.AcpStreamAdapter,mix raxol.acp,Raxol.Agent.ClientProtocol.Serve.apply_requested_model/2, and the telemetry events[:raxol, :agent, :acp_turn_runner, :interrupt_failed]and[:raxol, :agent, :acp_turn_runner, :journal_failed].bin/raxol-acpnow takes--agent.packages/raxol_cli/tbench/harbor-patch.sh, which patched a harness that drove the removed mode, goes with it.raxol_agent:Raxol.Agent.SystemPrompt. Nothing called it, soRAXOL_BONDED_PROMPTandconfig :raxol_agent, :bonded_prompt_pathconfigured nothing.
Security
raxol_paymentsandraxol_earnrequiredecimal ~> 3.0(was~> 2.0), so no consumer can resolve a decimal with EEF-CVE-2026-32686;raxol_paymentsparses amounts from payment servers and config. For consumers: decimal 3's default context is decimal128, precision 34 (was 28) withemax: 6_144/emin: -6_143and over/underflow signalled, andDecimal.new/1,parse/1andcast/1reject strings of more than 34 digits or with an exponent past 6_144. The package locks, and those ofraxol_consoleandraxol_symphony, which build on them, move to decimal 3.1.1 (and req 0.7.4 in the two packages).raxol_payments: x402 and MPP atomic amounts are bounded to a uint256. A challenge whose amount is wider (more than 78 digits, or above 2^256 - 1) is rejected at parse time as{:invalid_amount, _}, checked before the string is parsed; no such amount can be signed. Under decimal 3, an amount string of 35 to 78 digits raisedDecimal.Errorout ofReq.AutoPay(x402 throughAssets.to_human/2, MPP throughMPP.amount/1), andSettlementLedgercrashed recording one, losing its table; they now convert through the integer (Assets.to_decimal/1) and reach the policy and budget gates, which refuse them when aSpendingPolicyand ledger are configured. Without the bound, a hostile server's header-sized amount could detach the payments telemetry logger or raise fromon_confirm, since decimal 3'sto_stringstops at 6_178 digits. New tests failed before.
[2.8.0] - 2026-11-13
Added
raxol_journal: the session journal and the event contract as a package.Raxol.Agent.Journal,Raxol.Agent.Journal.FileStorewith itsReaderandWriter,Chain,Tip, the checkpoint records andRaxol.Agent.Contract(Event,encode_line/1,sanitize_payload/1,evidence_status/1) ship inpackages/raxol_journal, which depends on Jason and:telemetryand on no Raxol package. Module names, the on-disk layout,RAXOL_SESSIONS_DIRand the[:raxol, :agent, :journal, ...]telemetry events are unchanged.raxol_agentandraxol_brokerdepend on it; the rootraxoldoes not. It replaces the journal insideraxol_agent2.7.0, which defines the same modules, and must not be combined with it. The frozen golden corpora ship in itspriv/golden/. Every module is also reachable asRaxol.Journal.*, which delegates to theRaxol.Agentname.Raxol.Journal.Test.FaultJournal,Raxol.Journal.Test.ForeignBeamandRaxol.Journal.Test.MutationAnchorare helpers for ExUnit suites. Seepackages/raxol_journal/README.md.config :raxol, :code_reloader.falsekeepsRaxol.Dev.CodeReloaderfrom starting in:dev, for a process whose stdout carries a protocol: a recompile prints there.bin/raxol-acpsets it.- Scoreboard: deterministic performance counters on every CI run.
mix raxol.scoreboard(Raxol.Scoreboard) drives the playground table and flex-layout demos through the rendering engine's own frame stages in-process and counts the ANSI frame bytes the engine writes (a terminal with synchronized output also receives a 16-byte wrapper per frame, not counted) and the BEAM reductions spent, for the first frame and per keystroke of a ten-key script, and merges the buffer gate's memory-per-cell rows (bench/core/buffer_gate.exs --json PATH). The stages are shared rather than copied: the engine's theme resolution and border-beam pass move intoRaxol.Core.Runtime.Rendering.Pipeline, andBackends.terminal_frame/2builds the frame payload thatrender_to_terminal/2writes; plugin transforms, which come from the runtime plugin manager, are not applied. A test drives a real engine through the same keys and requiresRaxol.Scoreboard.frames/2to produce its bytes frame by frame, so the two cannot drift. Repeated runs agree within a few hundredths of a percent for reductions and exactly for bytes and memory, so pull requests can be compared with master on shared runners where wall-clock timings cannot; records carry the full Elixir and OTP versions (29.0.3, not29), because a toolchain bump moves reductions. Metric ids, plain-English labels, units, which direction is better and each metric's noise tolerance are inpriv/scoreboard/metrics.json, now packaged withraxol. CI's buffer-gate job becomesBuffer gate + scoreboardand uploads the record as thescoreboardartifact. The newscoreboard.yml(workflow_run, never executing pull request code, actions pinned to commits) keeps one comment on the open pull request whose head is the tested commit. It compares against the nearest recorded first-parent ancestor of the base commit CI merged with, only when that commit is on the PR's base branch per the API, never a newer record, and says how many commits earlier the record is when it is not the base commit itself; metrics the PR adds are listed as not compared. It appends each master record toscoreboard/records.jsonlon thegh-pagesbranch, every waiting publish job runs in turn (queue: max), andlatest.jsonand the shields.io badges follow the newest recorded commit on master rather than the last job to finish, removing badges for metrics no longer headlined;release.ymladds the change since the previousv*tag to the release notes. The counters report and do not gate; the buffer gate's budgets still do. - Scoreboard: replayed frames must reproduce their screen, a keystroke-vs-redraw ratio, and a noise tolerance that matches CI. Before measuring,
mix raxol.scoreboardreplays every scenario frame throughRaxol.Terminal.Emulatorand requires each cell's character and style to equal the screen the frame was rendered from (Raxol.Scoreboard.replay_mismatch/1); a mismatch fails the CI job, so the frame-byte metrics can only improve by sending less for the same screen, never by sending a broken one.Raxol.Scoreboard.frames/2now returns{payload, screen}pairs. New headline metrictable.keystroke.work_ratio, a keystroke's reductions over the first frame's: an incremental renderer sits far below 1, and the table demo measures 0.98. The reduction metrics' noise tolerance goes from 0.1% (0.2% for the ratio) to 0.5%: the first live PR comment reported changes of 0.1-0.2% on a change that touches no rendering code, and two CI runs of the same commit (#1288, attempts 1 and 2) differed by up to 0.2%. Bytes and memory stay at zero tolerance. - Scoreboard: scenario screens are pinned. Every scenario frame's screen (each cell's character and full style, serialized stably like
Raxol.RATE) has a fingerprint inpriv/scoreboard/screens.refs, andmix raxol.scoreboardrefuses to measure, failing the CI job, when a screen changed, has no reference, or is no longer drawn (Raxol.Scoreboard.check_screens/0). The replay check proves a frame's bytes draw its screen; the pin proves it is the same screen, so a demo that draws less cannot show up as fewer bytes. An intended visual change toTableDemoorFlexLayoutDemorunsmix raxol.scoreboard --blessand commits the file in the same pull request. - Scoreboard: history from before the scoreboard.
scripts/scoreboard-backfill.shmeasures older master commits with today's scoreboard. It checks each commit out in a scratch worktree and copies the scoreboard over it, addingBackends.terminal_frame/2(the first half of that commit's ownrender_to_terminal/2). It keeps a record only when the copied harness reproduces that commit's rendering engine byte for byte (test/raxol/scoreboard_test.exs, run against that commit) and every scenario screen matchespriv/scoreboard/screens.refs. The manualscoreboard-backfill.ymlruns it over master's first-parent history in parallel slices, on the toolchain CI records master with, and appends the records togh-pages;latest.jsonand the badges keep following the newest commit. The first commit that draws today's screens is #1133, which fixed the dim attribute that v2.6.1 and v2.7.0 drop, so no release is comparable and none gets a record. For the releases,bench/core/scoreboard_legacy.exsfeeds today's cells to each release's terminal backend and reports the bytes next to how many cells came out different (table inbench/README.md). The gh-pages write inscoreboard.ymlmoves to.github/scripts/scoreboard-publish.sh, shared by both workflows, and now rewrites the branch's README on every publish. - Scoreboard: deliberate regressions it must catch, and the render path's screen memory. Each file in
bench/scoreboard_canaries/is a patch with aRegression:line and anExpect-worse:orExpect-refused:line.scripts/scoreboard-canaries.shmeasures the checkout, then applies, measures and reverts each patch, and fails unless the scoreboard flags every one with the classification the pull request comment uses (newscoreboard.mjs classify), or if a patch no longer applies. Nine canaries: every frame a keyframe, a 4-byte redundant reset per row, rows encoded twice (+1.7% work per keystroke), rows rebuilt twice on every write, a field added to either cell struct, a diff that leaves out a changed row, the encoder dropping dim, and a table demo that draws fewer rows. The last three must make the scoreboard refuse to measure.scoreboard-canaries.ymlruns them fromci-unified.yml, gatingCI Status, when a change touches the scoreboard's own files or a file a canary patches, and fromnightly.ymlevery night. Writing the cell-field canary showed that a field added to the render path's%Raxol.Terminal.Cell{}moved no metric: the buffer gate's memory rows measureRaxol.Terminal.Buffer, whose cells are%Raxol.Terminal.Buffer.Cell{}. New metrictable.screen.heap_bytes_per_cellis the memory of the screen buffer the engine holds after the table script, per cell (90.08 bytes). raxol_core2.8.0:Raxol.Core.Runtime.Plugins.DependencyResolver.find_cycle/2(#1262). A depth-first walk of a%{node => [successor]}graph from a start node that returns the cycle path, starting and ending with the same node ([:a, :b, :a]), ornil.Raxol.Plugins.Lifecycle.Dependenciesnow delegates its ordering and cycle detection toDependencyResolver, so rootraxolrequiresraxol_core ~> 2.8; everyraxol_coredependent moves to~> 2.8with it.raxol_journal: hash-chained journals (#1177).Raxol.Agent.Journal.FileStore.open(session, chain: true)creates a journal whose records carryprev_hashandhash(SHA-256 of the record's canonical JSON: keys sorted by byte order at every depth, no whitespace,hashremoved). One chain spans every segment; the first record links to 64 zeros.- The mode is fixed at creation (
"chain": trueinmeta.json). A chained journal stays chained whatever later openers pass;chain: trueon an existing unchained journal is{:error, :unchained_journal}. Chained appends refuse floats ({:error, {:float, path}}), and each line on disk must be its record's canonical JSON (Jason withescape: :jsonpassed explicitly; a test pins the exact bytes and hash of a record with control, DEL, C1 and non-ASCII characters). - The
meta.jsonflag is not the only marker: atip_hashinHEADor aprev_hash/hashon the first record also makes a journal chained, so removing the flag is reported as damage ({:damaged, 1},{:broken, 1}, appends refused) instead of silently turning the checks off. HEADanchors the tip hash after each datasync. On a chained journal a missing or cut record at or below the anchored offset is damage, not a torn tail; an unsynced line above it is still healed. A Writer that starts on a broken chain refuses every append with{:error, :damaged}and leavesHEADalone.- New
Raxol.Agent.Journal.verify/1(andFileStore.verify/1, plus read-sideFileStore.verify_session/2)::ok,{:broken, offset}, or{:error, :unchained}.status/1now returns{:damaged, offset}instead of bare:damagedfor every journal;read/2andread_records/2refuse a broken chain like any interior corruption.FileStore.append_many/2appends several events in one Writer call. FileStore.open(session, private: true)creates the session directories 0700 and every file 0600 from its first byte, and refuses a symlinked, foreign-owned, or group- or other-writable base or session directory with{:error, {:untrusted_dir, {path, reason}}}. The journal checks owner and mode itself.- Replay no longer slows superlinearly: the reader walks segments without building a per-line entry list, checks each chained record with one encode, and sizes the heap up front instead of copying the growing record list on every full collection (50k chained records: about 1.0 s CPU, down from 2.3 s, with 7 major collections instead of 71).
- The default
schema_versionis1.2.0. Unchained journals change only in that stamp. The 1.2.0 golden corpus is frozen as a chained journal; the 1.0.0 and 1.1.0 corpora are unchanged and still replay.
- The mode is fixed at creation (
raxol_speech:Raxol.Speech.Recognizertakesload_model: false(#1117). The recognizer then starts without loading a model, and so without fetching one from the Hugging Face Hub:available?/0isfalseandrecognize/1returns{:error, :bumblebee_not_available}, as in a build without Bumblebee. The default (true) is unchanged.- In
raxol_mcp,Raxol.MCP.Clienttakesfail_fast: true(a failed connect or handshake answersawait_ready/2at once, including a caller that arrives while the client is still closed, while the retry still runs) andRaxol.MCP.CircuitBreaker.failure_threshold/1is public. raxol_mcp'sRaxol.MCP.Clienttool maps now carryannotations.raxol_mcp: an HTTP spec may pinera: :legacy | :modern, which skips theserver/discoverprobe at connect and after a rejected session (any other value is{:error, :invalid_era}). Robinhood's trading endpoint answers the probe with a plain-text 400, which is correctly not era evidence, so an unpinned client never connected; the broker pins:legacy. A 401 at connect or handshake no longer reconnects:Raxol.MCP.Clientstays:closedwith{:connect_failed, reason}and answersawait_ready/2at once, instead of re-sending the same rejected headers every backoff interval until the shared per-origin breaker opened.
Changed
The framework line is 2.8.0, and
raxol_journaljoins it.raxol,raxol_core,raxol_terminal,raxol_mcp,raxol_liveview,raxol_plugin,raxol_sensorandraxol_journalare 2.8.0, and every requirement between them is~> 2.8.raxol_speech,raxol_telegramandraxol_watchare 0.3.0.raxol_gatewayis 0.2.0 and is published on its own. Updateraxoland itsraxol_*siblings together:raxol2.7.0 accepts the 2.8.0 siblings but loses its derived MCP tools with them, so a project that stays onraxol2.7.0 pinsraxol_core,raxol_terminal,raxol_mcp,raxol_liveview,raxol_pluginandraxol_sensorto~> 2.7.0.raxol_agentandraxol_paymentsleave the Hex release train.Raxol.Release.PackageCheck.public_packages/0lists neither, so a root tag publishes neither, andmix raxol.release.checkcovers them only with--all. Both require the runtime packages at~> 2.8.raxol_core's package shipstest/support/boundary_vectors. The path and terminal-text conformance vectors are in the Hex tarball, so a dependant's tests can read them from the dependency.Breaking: the runtime packages no longer name the agent packages. Main
raxol,raxol_core,raxol_terminal,raxol_liveview,raxol_mcp,raxol_plugin,raxol_sensor,raxol_speech,raxol_telegram,raxol_watch,raxol_gatewayandraxol_journalhold no reference toraxol_agent,raxol_payments,raxol_broker,raxol_earn,raxol_web3,raxol_console,raxol_cliorraxol_agent_client_protocol: no module, application atom or dependency inlib/,config/ormix.exs, except the rows of the release catalog,lib/raxol/release/package_check.ex, which classifies every directory underpackages/.scripts/check_package_boundaries.exsenforces it, with no allowlist.Raxol.Gateway.Handler.Agentships inraxol_console, andraxol_gatewaydrops its optionalraxol_agentdependency. An app that used the handler withraxol_gatewayandraxol_agentalone addsraxol_console, whose application starts nothing until a Console package is configured.raxol_earnandraxol_paymentscome with it as dependencies, and the:raxol_earnapplication starts its base supervisor: no listener, and no seller, buyer or accounting child by default. Withoutraxol_console, a gateway givenhandler: {Raxol.Gateway.Handler.Agent, opts}does not start:Raxol.Gateway.SessionRouterstops with{:handler_not_available, Raxol.Gateway.Handler.Agent}.init/2no longer returns{:error, :raxol_agent_not_loaded}.raxol_gatewayis 0.2.0 andraxol_consolerequiresraxol_gateway ~> 0.2, so it does not resolve with a gateway that still defines the handler.Raxol.MCP.AgentBridgeships inraxol_agent, notraxol_mcp.actions_to_mcp_tools/2is unchanged.meta_tools/0and itsagent.list,agent.sendandagent.get_modeltools are removed: they droveRaxol.Headlesssessions, whichraxol_list,raxol_send_keyandraxol_get_modelserve.raxol_mcpis 2.8.0 andraxol_agentrequiresraxol_mcp ~> 2.8:raxol_mcp2.7 defines the module too, and lacksRaxol.MCP.TreeWalker.action_name?/1andclean_spec/2, which the bridge calls.raxol_agent2.7.0 does not define the module, so a project onraxol_mcp2.8.0 andraxol_agent2.7.0 has noRaxol.MCP.AgentBridge.Raxol.Agent.Scheduler.Delivery.gateway/1is removed. Give the scheduler a:deliverclosure, asRaxol.Console.Scheduler.Wiringdoes:fn target, rendered -> Raxol.Gateway.Delivery.deliver(adapters, {:target, target}, rendered) end.Raxol.Applicationno longer startsRaxol.Agent.Supervisor; the:raxol_agentapplication starts it in every startup mode. Only:fullchanges: the supervisor was a child ofRaxol.Supervisor, started before the MCP tree, and is now a child ofRaxolAgent.Supervisor, started after:raxol. A node that loadsraxol_agentwithout starting its application has no agent supervisor until it startsRaxol.Agent.Supervisoritself. In:mcpand:fulltheagent_*MCP tools register where they did: withconfig :raxol_agent, :mcp_toolsunset, only on a node whose startup mode is:mcp(raxol mcp,mix mcp.server). A registry run in any other mode, or one a host passes toRaxol.Agent.Supervisor.start_link/1, now needsmcp_tools: true.mcp_tools: truenow registers them in any mode that runs the MCP registry,:fullincluded, andfalsein none. The setting is read once, whenRaxol.Agent.McpToolsfirst starts, so it belongs inconfig/*.exs. The tools answer to whateverconfig :raxol, :mcp_authorizerthe node sets.- The root
config/runtime.exsno longer readsXOCHI_SOLVER_*,XOCHI_PULL_REQUIRE_SOLVER_PINor the accounting variables, and no longer configures:raxol_paymentsor:raxol_earn. Theraxolrelease carries neither application, so no release changes. A release that signs Xochi origin pulls reads the pin withconfig :raxol_payments, Raxol.Payments.Protocols.Xochi.origin_pull_pin_env_config!()(new);packages/raxol_earn/config/runtime.exssets the accounting keys, as before.
raxol_journal:writer.lockholds the writer's pid and a start token. A holder counts as alive only if it is a BEAM started at that time; a stale lock is reclaimed by one claimer at a time.raxol_mcp: one connection may hold at most half the callback workers.Raxol.MCP.Servertakes:max_in_flight_per_connection(default half of:max_in_flight, at least 1; the server refuses to start if it exceeds:max_in_flight). A connection's call past its share is answeredserver_busywhile other connections keep the rest, so one stdio client running many slow calls (the oh-my-pi coding agent, OMP, overraxol mcp) leaves room for an SSE client. The share is per connection id:handle_message/2callers share the stdio transport's:defaultid and its slots, and several connections together can still fill the server (two at the default share do). Both counts are kept and checked inside the server process when a worker starts. With the default 32 workers, one connection now runs at most 16 calls at once (was 32).Raxol.MCP.Serveralso takes:server_version, theserverInfo.versionit answersinitializewith (defaultRaxolMcp.version/0);Raxol.MCP.Supervisorpasses it through andRaxol.Applicationsets it fromconfig :raxol, :mcp_server_version.Breaking: derived MCP tools are named per session.
- Derived tools are
<session>.<widget>.<action>(was<widget>.<action>) and each session's meta-tool is<session>.discover_tools(wasdiscover_tools), which searches only that session. Two sessions rendering the same widget used to register one tool name: the later session's tool replaced the earlier one's, the earlier session never got it back, and stopping either removed it for both.Raxol.MCP.TreeWalker.derive_tools/2requires:session_idin its context;Raxol.MCP.FocusLens.filter/2requires:session_idin:focusedand:hovermodes anddiscover_tools_spec/2takes the session id. With nothing focused,FocusLens.filter/2now answers that session's tools, then itsdiscover_tools, then the unscoped tools, cut to:max_tools(it answered the first:max_toolsof every tool), anddiscover_toolsno longer lists unscoped tools. Breaking for anything that calls derived tools by name;Raxol.MCP.Testhelpers name the session themselves. raxol_startrefuses the session idsraxolandagent.raxolis this server's key in an OMP config, and OMP strips an exact<server>_prefix from a tool name once, so a sessionraxolwould reach OMP under another session's tool names;agent.prefixes the agent bridge's tools.raxol_startandRaxol.Headless.start/2also refuse, and stop, a new session any of whose initial tools the registry refuses (a session"S"holds the client-side names of a later session"s", and the reverse), naming each refused tool but not its holder, which can be another session's tool:start/2answers{:error, {:name_collisions, names}}, so a client's session can no longer leave a later Elixir caller's session running with every tool refused under{:ok, id}.Raxol.Headless.name_collisions/1andRaxol.MCP.ToolSynchronizer.name_collisions/1answer a session's tools refused for a name collision on a later render, with their holders.- A session id is now the first segment of every tool name, so
Raxol.Headless.start/2checks its id withRaxol.MCP.TreeWalker.id?/1, and aRaxol.MCP.ToolSynchronizerstarted directly for a session whose id is not 1-32 characters of[A-Za-z0-9_-]derives no tools, itsdiscover_toolsincluded, registers none of itsraxol://session/<id>/...resources, and is warned about once: a dotted idt.v2would otherwise appear inside sessiont'sdiscover_toolsand focus lens, and a control character would reachtools/listandresources/list.FocusLensmatches a session (and a widget) on the exact.segment. The session also warns for a derived tool whose name, with OMP'smcp__raxol_raxol_prefix, exceeds OMP's 64 characters (OMP lists such a tool under a hashed name). That warning and each widget refusalTreeWalkerreports are given once per session, also for a widget that leaves the view and comes back; the session remembers the 256 most recently derived, so per-item ids keep its memory bounded. Raxol.MCP.Registry.register_tools/2refuses a tool whose client-side name (Raxol.MCP.Registry.omp_name/1: OMP's lowercasing, each run outside[a-z0-9_]to_, runs of_collapsed and trimmed,"tool"when nothing is left) is already held by a differently named tool, sos.x_y.clickands.x-y.clickno longer reach OMP as one tool; it registers the rest and returns{:error, {:name_collision, [{refused, holder}]}}, whichRaxol.Headless.McpTools.register/1,Raxol.MCP.AdaptiveTools.register/1(it answered:okregardless), the agent harness's and the Telegram guardian'sregisterpass on;Raxol.Applicationlogs the headless, adaptive and agent harness tools' refusals at boot instead of dropping them.register_all/2returns that error and registers nothing (no tools, resources or prompts), checking the batch against itself and the table first. A headless session leaves a refused tool out, itsdiscover_toolsincluded, offers it again on each render, and logs the refusals when they change. A batch holding a tool whose:nameis not a binary is refused whole with{:error, {:invalid_tool_name, name}}, and unregistering such a name is a no-op; computing its client-side name had crashed the registry and with it the MCP supervisor.- A call to
<session>.<widget>.<action>or<session>.discover_toolswhose session is not inRaxol.Headless.list/0answers anisErrorresult naming the session and pointing toraxol_list, not JSON-RPC-32601: OMP shows the names it cached from its previous run. Headless makes that call through the newRaxol.MCP.Serveroption:unknown_tool_reply(a function from an unknown tool name to reply text ornil;Raxol.MCP.Supervisorpasses it through), whichRaxol.Applicationsets toRaxol.Headless.McpTools.unknown_tool_reply/1. A server without it, such as an embedder's, answers every unknown name-32601, and so does a name no session could derive or whose session is the reservedraxoloragent.
- Derived tools are
A headless session's MCP tool set is sticky, and every derived tool acts on its Component as it is now.
- A session's tool set is every tool its renders have derived so far, its
discover_toolsincluded, keyed by name, provider and a hash of the listed definition (description, input schema, annotations). A Component that leaves the view keeps its tools until the session stops, so a modal opened, dismissed and reopened sendsnotifications/tools/list_changedonce instead of on every change; a tool is registered again when its provider or definition changes. Stopping a session unregisters the whole set and sends onelist_changed, and starting one sends one. - A derived tool resolves its Component by id at call time, in the app's
view/1of the dispatcher's current model, after checking the dispatcher is alive. A tool used to keep the node it was derived with for as long as its name stayed registered, so a Button'sclicksent a staleon_click, a Checkbox'stogglesent the same value twice, andget_value,get_selectedandget_tabsanswered stale state. When widgets share an id, the call acts on the first in the view of the tool's kind that offers the action. A Component that is absent, no longer offers the action (disabled) or is of another type answers anisErrorresult naming it, which never counts toward the tool's circuit breaker (each derived tool registers a:fault?classifier). The server names the widget only for the reply TreeWalker gives (Raxol.MCP.TreeWalker.widget_unavailable?/1: an id passingid?/1and one of those three reasons); any other tool answering{:widget_unavailable, _, _}gets a fixed sentence. A call that dispatched messages syncs the session's tool set before it answers.Raxol.UI.Components.Table'sget_rowsanswers the rows in the order the node's:sort_by/:sort_directionshow them, and the TableDemo node carries them, sosortthenget_rowscomes back sorted. Raxol.MCP.Registry.register_tools/3(besidederived:),register_resources/3andregister_prompts/3takeowner: pid, andunregister_tools/3andunregister_resources/3takeowner: pidto remove only what that owner still holds. The registry monitors owners, removes what a dead owner still holds and sends onelist_changedto its:server(new start option, defaultRaxol.MCP.Server; a value that is no server name is{:error, {:invalid_server, server}}); registering a name again moves it to the new owner or to none, and registrations without an owner are never removed automatically. A session'sRaxol.MCP.ToolSynchronizerowns its tools and resources and, stopping, unregisters only those it still owns; the process runningRaxol.Application.start/2owns the static headless, docs and adaptive registrations (Raxol.Headless.McpTools.register/2,Raxol.Headless.DocsResource.register/2andRaxol.MCP.AdaptiveTools.register/2take the options). Unregistering also deletes the entry's circuit-breaker row.Raxol.MCP.ToolSynchronizertakes:server(itslist_changedtarget, defaultRaxol.MCP.Server) and:debounce_ms(defaultconfig :raxol_mcp, :tool_synchronizer, debounce_ms:, else 50;0syncs each render as it arrives, whichconfig/test.exssets).Raxol.Headless.start/2takes:serverand passes it on. Like:registry, a server passed explicitly must be a runningRaxol.MCP.Server, checked without messaging it; anything else is{:error, {:invalid_server, server}}, so a process of another kind is never sent the notifications.- Registering a tool, resource or prompt clears its circuit-breaker row too, so a call that finished after its tool was unregistered, and recorded its result then, leaves nothing to the name's next registration.
- A derived call into a session whose dispatcher has exited answers an
isErrorresult that the session is not running, pointing toraxol_list. It names the session only when the id passesRaxol.MCP.TreeWalker.id?/1, since any tool may answer{:error, {:session_not_running, id}}. - There is no per-session cap: a view that gives each item its own widget id grows the session's set with every item it renders (
docs/features/MCP.md).
- A session's tool set is every tool its renders have derived so far, its
Breaking: headless session ids are binaries, and
raxol_startrequires a checked id.Raxol.Headlesssession ids are binaries only: every per-session function guardsis_binary/1,start/2returns{:ok, binary}and refuses an:idthat is not 1-32 characters of[A-Za-z0-9_-](the widget-id rule ofRaxol.MCP.TreeWalker) with{:error, {:invalid_session_id, id}}, so no id with invalid UTF-8, a newline or a bidi character reaches a client throughraxol_listor session-id completion. The default id is the underscored module name as a binary ("table_demo"); a character outside the rule becomes_, and a name over 32 characters becomes its first 23,_and 8 hex digits of the module's hash. Breaking for Elixir callers passing atom ids or ids outside that rule (dotted, spaced, over 32 characters).start/2takes:registry(defaultRaxol.MCP.Registry, when it is running) for the session's tools, an atom, pid,{:global, name}or{:via, module, name}. A registry passed explicitly must be a runningRaxol.MCP.Registry, checked from the process's initial call without messaging it: any other value, a name no process has, or a process of another kind is{:error, {:invalid_registry, registry}}. The check is of identity, not responsiveness: aRaxol.MCP.Registrythat cannot reply passes it. The newsettle/1returns once the session's dispatcher and rendering engine have caught up and its tool set has the frame's tools (Raxol.MCP.ToolSynchronizer.flush/2is the last of those barriers; it takes a reply from the session's rendering engine first, so the frame's view tree has arrived before it applies).Raxol.MCP.Testuses it instead of sleeping (:settle_msis gone), and its default session id is lowercase.raxol_startrequires anidmatching^[a-z][a-z0-9]{0,6}$and refuses any other, never substituting the module name. Breaking for MCP clients that omittedid. Its refusals no longer count toward its circuit breaker, so a run of bad calls cannot make it unavailable to every client.raxol_screenshot,raxol_send_key,raxol_get_modelandraxol_stoptake the id as given: no MCP-supplied id becomes an atom.raxol_stopfor an id with no running session is an error rather than a report that it stopped.completion/completefor a tool'sidargument lists the running headless sessions to a caller the tool authorizer would let callraxol_list(any caller when there is no authorizer), and none for a non-string value. It matched{:ok, sessions}againstRaxol.Headless.list/0, which returns a bare list, so it always offered none.
Breaking: derived MCP tools are annotated, carry no app text, and
raxol_startstarts only listed modules.- Every built-in action that changes app state (
click,type_into,clear,toggle,select,select_row,select_node,select_tab,sort,expand,collapse,dismiss,confirm,seek,play,pause,scroll_to) is nowsensitive: true, andRaxol.MCP.TreeWalkerkeeps a tool spec'ssensitive: trueanddestructiveHint: trueon the derived tool; every other annotation key or value (title,readOnlyHint,sensitive: false) is dropped, since a client acts on a provider's claim to be safe. Breaking for embedders with no authorizer (Raxol.MCP.SupervisororRaxol.MCP.Serverwithout:authorizer): those calls are now refused withsensitive_tool_unguarded; reads still run. Breaking for the same embedders:raxol_get_modelis nowsensitiveas well, since it returns the whole model and an app keeps its secrets there (a masked input's plaintext included), so it is refused unguarded likeraxol_start,raxol_send_keyandraxol_stop. The Server's boot refusal scans static tools only, so an unguarded Server restarted while sessions have derived sensitive tools registered still boots, and those calls stay refused attools/call. A tool is derived by origin, not by a dotted name or by a key in its tool map: onlyRaxol.MCP.ToolSynchronizerregisters a session's tools as derived (Raxol.MCP.Registry.register_tools/3withderived: true;Raxol.MCP.Registry.static_tools/1lists the others), so a static tool with a dotted name, such as theagent.<name>toolRaxol.MCP.AgentBridgemakes of an Action, or a tool map carryingderived: true, still refuses the boot. A network deployment needs a function authorizer, since derived names change with the view and a name allowlist cannot list them, and SSE connections share sessions: every connection reaches every headless session, and the connection id is the client'smcp-session-id.Raxol.MCP.AgentBridge.actions_to_mcp_tools/2annotates an Action declaredsensitive: trueassensitive, so its tool is refused without an authorizer. The bridge cleans an Action's description and input schema asTreeWalkercleans a provider's, and an Action whose name is not a valid action name, or whose specTreeWalkerwould refuse, derives no tool, with a warning. - Derived tool descriptions are static per provider and action. Modal titles, button and checkbox labels, tab and scrubber names, and widget ids no longer appear in
tools/list, only in tool results.TreeWalkerstrips control, format (bidi, zero-width, BOM, tag), line/paragraph separator and private-use characters, variation selectors (U+FE00-FE0F, U+E0100-E01EF), U+034F, the Hangul fillers (U+115F, U+1160, U+3164, U+FFA0) and U+2800 from any provider's description and from every string and every key in itsinputSchema(titles, enums, defaults, examples, property names); invalid UTF-8 is replaced first, and an atom is cleaned as its name. Breaking for third-party providers: a spec whose description is neither a string nor an atom, whoseinputSchemaholds a term JSON cannot encode (a tuple, a struct, a pid, a function, an improper list, a key that is not a string or atom), whose map has two keys equal once cleaned (:amountand"amount"), or whoseinputSchemanests maps and lists deeper than 32 levels or holds more than 10,000 values derives no tool, with a warning; before, one such spec made every client'stools/listunencodable, let one property replace another, or held up every render while it was cleaned. The scrubber'sseekno longer states its range in the description or schema;get_positionreports it. ATextInputwith amask_charderives noget_value(itshandle_tool_call/3refuses one), asPasswordFieldderives none, and its value no longer reaches theraxol://session/<id>/widgetsresource (itsa11y_node/1reports none) or the.../contextresource (which drops thevalueof a password field or a node with amask_char; that resource still includes the model projections the app declares inmcp_resources/0). The layout engine now draws a masked:text_input(amask_charon the node or in its:attrs) as one mask character per grapheme of its value, soraxol_screenshot, the terminal and the LiveView DOM show the mask rather than the value. - Breaking: a provider widget whose id is not 1-32 characters of
[A-Za-z0-9_-]derives no tools, and an action whose name is not a lowercase letter followed by up to 31 of[a-z0-9_]derives no tool, since both become part of the listed tool name.TreeWalker.derive_tools/2logs a warning naming each;TreeWalker.derive_with_refusals/2returns them instead, andRaxol.MCP.ToolSynchronizerlogs each when it enters the view and keeps only the current render's, so a view with per-item ids does not grow its state. raxol_startstarts a module only if it is onRaxol.Headless.McpTools.startable/0:config :raxol, :headless_startablewhen set, which replaces the default, and otherwise the playground demos exceptRaxol.Playground.Demos.ReplDemoandRaxol.Playground.Demos.BeamDashboardDemo, which sets the VM-wide:scheduler_wall_timeflag (default_startable/0, pinned in a test). Every entry must be a loadable module: an invalid value makesRaxol.Headless.McpTools.register/1raise at boot, and one set at runtime makesraxol_startanswer with the config error without opening its circuit breaker. Breaking for anyone starting their own app over MCP: list it there (withdefault_startable/0too, to keep the demos). An unlisted name is refused before resolution and never becomes an atom. The list does not bind thepathargument, which a deployment enables withRAXOL_HEADLESS_PATH_ROOT.Raxol.Headless.start/2called from Elixir (Raxol.MCP.Test,Raxol.Recording.Video) is unaffected.
- Every built-in action that changes app state (
raxol_mcp: the stdio transport no longer serializes requests, and honorsnotifications/cancelled.Raxol.MCP.Transport.Stdioused to block on each request until the server answered, so one slowtools/callheld up every line behind it. Its reader now sends each request in read order with the newRaxol.MCP.Server.request/3(:gen_server.send_request/2) and writes replies as they arrive; only the transport writes stdout, and methods the server answers inline keep their order.notifications/cancelledcancels the named request through the newRaxol.MCP.Server.cancel/2, which kills its callback worker; a cancelled request is answered{:reply, nil}, so nothing is written for it. Atools/callparked for elicitation stays outstanding underrequest/3(its reply is the call's eventual response, notnil), so a cancel drops it while it waits for the answer and kills the tool once an approval started it. Calls parked for an elicitation answer hold no callback worker and so are capped apart from:max_in_flight: a connection may have at most:max_parked_elicitations(default 32) parked, and the server at most:max_parked_elicitations_total(default 256, eight connections' full share) across every connection. Both can be passed toRaxol.MCP.Supervisor, and a per-connection cap above the total refuses to start. Past the per-connection cap an ASK is answered with theauthorization_requireddeny, detailtoo many elicitations pending. On a full server an ASK from a connection holding fewer parked calls than the connection holding the most closes that connection's oldest parked call with the same deny, detailtoo many elicitations pending on this server, and parks in its place; any other ASK gets that deny. So one client spreading its calls over several SSE streams can no longer hold every other client's approvals; a client with more elicitation-capable connections than the total can still keep a newcomer's call from lasting. On end of input the transport still writes every reply already produced (methods the server answers inline, such asinitializeandping, and worker-run requests that finished first), cancels every request still running in a callback worker (tools/*,resources/*,prompts/*,completion/complete,logging/setLevel) or waiting on an elicitation answer, which gets no reply, and stops:normalonce nothing is outstanding. The server monitors everyrequest/3caller and cancels its requests if it exits, and the transport's reader is linked to it and killed when it stops, so a killed or crashed transport leaves no worker running and no reader consuming stdin. The transport traps exits: an abnormal exit of a linked process stops it with that reason, and its starting process exiting stops it. A connection whose subscriber goes away also has the tools its elicitation approvals started killed. A request that reuses the id of one still in flight is answered-32600("duplicate request id in flight") and not run, and any request whoseparamsis neither an object nornull(treated as absent) is answered-32602instead of crashing the server. Likewise a request whosemethodis not a string is answered-32600(such a notification is dropped), andlogging/setLevelanswers alevelit does not know with-32602. Client values the server writes into a message (method, level, tool, prompt and resource names, an elicitation answer'saction) are rendered withinspect, so error messages now quote them; a non-string one used to crash the server, which since this change also stops every stdio transport.logging/setLevelis now gated by:read_authorizerlike the other read surfaces, and like them runs in a callback worker, so its reply no longer keeps its place among inline replies. A read allowlist, including the production defaultmcp_allowed_read_methods, must name it before a client can set the level. Stdio requests now run concurrently and share the server's:max_in_flightcap with every other connection, so a stdio client can be answeredserver_busy. The transport monitors the server: if it dies, with or without requests pending, the transport stops with{:server_down, reason}(a server restarted under the same name knows nothing of its subscription), andmix mcp.serverrun where noRaxol.MCP.Serveris running (insidepackages/raxol_mcp) now exits 1 with{:server_down, :noproc}at start instead of exiting 0. Ahandle_message/2,3orrequest/3message must beRaxol.MCP.Protocol.decode/1output (atom top-level keys); a request with string top-level keys is answered-32600instead ofnil.CI: macOS and Windows run only for platform-sensitive changes, and gate when they do. The macOS root suite, Windows root suite and Windows
raxol_agentpackage suite move fromci-unified.ymlinto the reusableplatform-tests.yml. Thechangesjob calls a change platform-sensitive when it touchesmise.toml, anymix.exsormix.lock, the termbox2 NIF or the workflow itself, or changes an Elixir file whose content on either side of the change branches on the OS (:os.type(),:win32,win32_), calls aPlatform.helper, or carries an OS-scoped test tag (windows_only,unix_only,skip_on_windows); reading both sides catches a deleted OS module and a removed@tag :skip_on_windows. Pull requests and pushes to master are classified the same way (branch protection does not require a PR to be current with master, so a merged commit can be an untested combination), andCI Statusthen requires the legs to pass. Before, every pull request paid for all three legs and none could failCI Status, and the macOS leg's runner queue was what stretched a ~4-minute gate to ~11 minutes of wall time.nightly.ymlruns the Windows legs every night; its matrix already covers macOS. Integration tests, which ran onlib//mix.*changes without gating, now gate too.CI: Dialyzer runs nightly only, Credo per pull request only. The advisory per-PR Dialyzer job is gone (it could not fail a merge), and so is nightly's
plt-prebuild, whose only purpose was to warm that job's PLT. Nightly'scredo-ratchetduplicated the blocking per-PR Credo ratchet on the same toolchain and is gone; the per-PR job is renamed from "Credo Analysis (advisory)" to "Credo Ratchet".CI: a red nightly keeps one issue current instead of opening a new one per night. Only the scheduled run reports. It comments on the open
nightly-buildissue this workflow opened (skipping pull requests and human-filed issues), or opens one, and closes older duplicates; it closes the issue only when every nightly job succeeded, so a cancelled run or a skipped job leaves it open.CI: nightly no longer swallows failures.
mix test --only stress || trueand--only edge_cases || truematched no tests at all and are removed.mix test --only integration --only slow || echohid a real failure on every run: three:benchtests inlist_scorer_test.exsasserted wall-clock ceilings on a shared runner (the 16 ms keystroke row measured 32.9 ms). Their timings now live inbench/features/list_scorer_benchmark.exs, a report, and the step fails on any test failure. The@max_score_graphemesclamp those timings guarded gets a deterministic test instead: a match past the cap is missed and one inside it is found, which fails if the clamp is removed.Flaky tests.
Raxol.Recording.RecorderTestcounted events on the globally registered recorder, which also receives frames and keystrokes from any live session another test left running (a third event failed CI on 2026-10-20); the counting tests now use an unregistered recorder.Raxol.Dev.CodeReloaderTestwaited 3 s for a real Mix recompile, which macOS nightly runners outlasted; the wait is now a 60 s hang bound.Breaking: plugin dependency ordering and plugin event handlers (#1262).
raxol_core:DependencyResolver.resolve/1andresolve_incremental/2treat a plugin that depends on itself as a cycle ({:error, {:cycle, _}}); it used to be skipped. The sort is stable: plugins that become ready together keep their input order, where they used to follow queue order.{:cycle, ids}now names the cycle as a path that starts and ends with the same id ([:a, :b, :a]) instead of listing every plugin stuck behind it in map order.Raxol.Plugins.EventHandler.handle_mouse_event/3dispatches to plugins and returns{:ok, manager, :propagate | :halt}or{:error, reason}; it was a stub returning{:ok, manager}.handle_resize/3now dispatches the resize to plugins instead of returning the manager unchanged.
CI:
raxol_terminal,raxol_liveview,raxol_plugin,raxol_sensor,raxol_speechandraxol_watchjoin the per-PRpackage-testsmatrix (#1117). Nothing in per-PR CI ran their suites, soraxol_terminalchanges such as #1101 and #1110 shipped on local evidence alone. Each cell runsdeps.get --check-locked,compile --warnings-as-errors, the suite and the format check; theraxol_terminalcell also installstmux, because without a multiplexer its real-pty tests print "skipped" and pass. Four packages joined clean.raxol_speech's non-liveRecognizertests loadedopenai/whisper-tinyin their setup, which with an empty Bumblebee cache (every CI run) downloaded about 160 MB from huggingface.co, and a failed fetch left no model, so the tests took a different path depending on the network; they now start the recognizer withload_model: false, and only the:stt_livetest loads the model.raxol_terminalneeded three repairs, all in its tests: deadtest/supporthelpers that called modules only the root suite has (ten warnings) are removed; the full-screen ^C fixture now clearsSKIP_TERMBOX2_TESTSas well asMIX_ENV, sinceRaxol.Terminal.Env.test?/0reads both and with it set the Driver never turnedISIGoff, which failed four tests on every nightly leg after #1130; and itstest_helpersets:terminal_test_modeas the root one does, soIntegration.init/0no longer calls the realtb_init(), which failed without a terminal and, run from one, left it raw. Nothing was quarantined.Raxol.Headless.send_key/3andsend_resize/3return once the app'supdate/2has handled the event. Both cast the event to the dispatcher and returned while it was still queued, so:oksaid nothing about whenupdate/2would take it, and callers slept after them: the suites did,send_key_and_screenshot/3slept 50 ms inside the session manager, andRaxol.Recording.Video.capture_clip/2slept 40 ms per event. ThroughRaxol.Headlessitself a followingget_model/1orscreenshot/1already saw the key (the model read queues behind the cast, and the render asks the dispatcher after it), so those sleeps covered an order that held by delivery rather than by contract; nothing ordered whatupdate/2does outside the dispatcher's mailbox. The event now goes as a call the dispatcher answers after focus navigation, bubbling andupdate/2, so the model, the nextscreenshot/1orget_buffer/1(raxol_send_keyincluded), and anythingupdate/2did synchronously reflect it when the call returns. Commandsupdate/2returns (Tasks, intervals) are started, not awaited. The wait runs in the caller's process:Raxol.Headlessonly resolves the session id, andscreenshot/1,get_buffer/1,get_model/1andsend_message/2now also call the session's processes from the caller, so a slowupdate/2or render holds up only its own calling process, and anupdate/2that callsRaxol.Headlessfor another session no longer waits on itself. Fromupdate/2, a call on the app's own session (whose dispatcher is the process runningupdate/2) answers{:error, :called_from_own_update}instead of exiting:calling_selfor stalling 5 s on a render. The isolation is per process:Raxol.MCP.Serverruns every tool call in its one process, so over MCP a slow or wedged session still holds every client'sraxol_send_key,raxol_screenshotandraxol_get_modelfor up to 5 s each, as it did before.send_key(id, key, wait: false)returns once the key is queued, with noupdate/2guarantee.send_key/3takes:timeout(default 5000 ms;send_resize/3uses the default); a timeout only ends the wait, andupdate/2may still handle the event afterwards. A dispatcher that dies on the event or does not answer in time is{:error, {:dispatch_failed, class}}for that caller rather than an exit.classis the exit reason's atom (for example:timeout,:noproc,:killed,:shutdown), else:unknown; the reason itself is logged, never returned, because a dispatcher killed by a linked process that raised exits with the exception, whose message (a URL, a token)raxol_send_keywould otherwise hand to the model.Raxol.MCP.Registrykeeps exception messages out of tool results the same way. New tests hold the dispatcher suspended and fail ifsend_key/3orsend_resize/3returns before it runs, or if killing it mid-call costs another session its answer (all three failed before), araxol_send_keytest fails if the exception's message reaches the tool result, and tests block one session'supdate/2and check that another session'sget_model/1andscreenshot/1answer meanwhile, thatupdate/2can callRaxol.Headless.list/0, and thattimeout:answers{:dispatch_failed, :timeout}(each failed while the call ran inside the session manager); the sleeps aftersend_key/3andsend_resize/3in the suites, and thesend_message/2fence in the generated-app helper, are gone.Tests: the
ProcessComponentowner-exit test names the process that died first (#1253).start_link/1can return{:ok, pid}and the monitor taken right after it deliver:noproc, and the test could only report a timeout. It now monitors the owner and the component before anything else, checks both are alive and that no DOWN arrived during start, then asserts the owner's:normalexit and the component's in that order on distinct refs, and releases the owner on exit so a failed assertion no longer leaks it. The cause of the flake is still open.
Removed
The coding harness is removed (ADR-0042). Breaking. In
raxol2.8.0 this removes the SSH code server, the twoRaxol.Applicationfunctions below, theharness_*MCP tools andbin/raxol; the command and task changes reach users with the nextraxol_cliandraxol_agentreleases. Gone from theraxolcommand:code,p/-p,agent,loginandsetup; bareraxolprints the help. Gone with them:mix raxol.code,mix raxol.p,mix raxol.inspect,mix raxol.setup,bin/raxol,bin/raxol-code, the SSH code server (RAXOL_SSH_CODE*,Raxol.Application.ssh_code_budget/0andhttp_client_available?/0), theharness_*MCP tools andconfig :raxol, :mcp_harness_tools.raxol doctorprints the install header, the connected providers and next steps, not the resolved configuration, and the daily update prompt fires onraxol playgroundonly. For a coding agent use OMP, which drives Raxol apps and agents throughraxol mcp; an editor reaches a Raxol agent throughraxol acp --agent. A provider is connected withRaxol.Agent.Setup.connect_ref/3or a provider environment variable (Credentials).raxol: harness modules nothing called. Breaking. Removed:Raxol.Harness.LiveSessionDriver,StreamCadence,CadencePolicyandStallDetector, andRaxol.UI.Components.Harness.Picker, with the[:raxol, :harness, :stream_cadence, :overflow]event.Raxol.Harness.Surfacestays, withRaxol.UI.Harness.OverlayPickeras its picker.Raxol.Harness.EventBoundaryand theRaxol.Harness.SessionLanebehaviour stay becauseraxol_agent2.7.0 calls them.Docs and site:
docs/features/CODING_AGENT.md,MEMORY.mdandSELF_IMPROVEMENT.md, andraxol.io/coding-agent. Skill authoring is indocs/guides/SKILL_AUTHORING.md; the ACP and MCP commands are onraxol.io/agents.Raxol.System.Updaterand everyRaxol.System.Updater.*module leaveraxol(ADR-0043).raxol_cli, their only caller, now has them asRaxol.CLI.UpdaterandRaxol.CLI.Updater.*and reads the channel override fromconfig :raxol_cli, :updater_manifest. Breaking forraxolusers: the modules andpriv/sigstore/trusted_root.jsonno longer ship in theraxolpackage, andraxolno longer lists:inetsand:sslinextra_applications(the updater was their only user).raxol_symphonyleaves this repository (ADR-0043). The tracker-driven coding-agent orchestrator, never published to Hex, is frozen with its history at DROOdotFOO/raxol-symphony (archived, CI off), its raxol dependencies git-pinned toce40937c4; it returns only as a Hex consumer of the runtime and agent repositories. Removed here with it:packages/raxol_symphony/,docs/features/SYMPHONY.mdand its ExDoc extra, the package-test matrix cell and itsinotify-toolsstep, its release-check catalog entry, singleton-guard roots and allowlist rows, and its.gitignoreline.Dead CI scaffolding. The
coveragejob (if: false) and the threecoverage-*artifact uploads that fed only it (the test runs never wrote coverdata),codecov.yml, the composite actions in.github/actions/(no workflow used them), andscripts/ci/build_and_test.shandci_validate_structure.sh(no callers).CI benchmarks that reported constants.
regression-testing.ymlcompared--jsonoutput thatbench/suites/parser/parser_benchmark.exshardcoded (# Mock: 1.25μs) and thatbench/suites/terminal/buffer_benchmark.exsandcursor_benchmark.exsproduced without calling Raxol (they timed:timer.sleep(0)stand-ins), so baseline and current were always equal and the job could not fail; it cost about 27 runner-minutes per PR.performance-tracking.ymlwrote fixed values (3.3us,0.9ms,2.8MB) when its run produced no results, which it never did, and fed them to a benchmark history and dashboard. Both workflows, the two simulated benchmarks,scripts/analyze_{performance,memory}_regression.exs, andpr-comment.yml(it waited for apr-numberartifact no workflow uploads, so it never posted) are deleted. Performance figures in the docs that came from those constants or from estimates are removed; the measured 2026-08-07 frame-budget run andbench/core/buffer_gate.exs, still a blocking CI job, remain.parser_benchmark.exsnow runs undermix runas documented; itsMix.installcall made it exit withMix.install/2 cannot be used inside a Mix project.Plugin modules with no callers (#1262).
Raxol.Plugins.Manager.Cells,Raxol.Plugins.Manager.Hooks,Raxol.Plugins.Manager.State,Raxol.Plugins.Manager.Events(a duplicate ofRaxol.Plugins.Manager) andRaxol.Plugins.Lifecycle.Validation. Breaking forraxol_terminalusers:Raxol.Terminal.Plugin.DependencyResolveris removed; useRaxol.Core.Runtime.Plugins.DependencyResolverfromraxol_core.Raxol.Core.Theming.ThemeRegistry.hex_to_rgb/1,hex_to_rgba/1andrgb_to_hex/1, andconvert_color/2's lenient parsing. MovingThemeRegistryfrom mainraxol'slib/intoraxol_corecut its delegation toRaxol.Utils.ColorConversion, whichraxol_corecannot see. The three helpers, public inraxol2.7.0 and unused in this repository, are gone.convert_color/2now parses exactly#rrggbb(either case) and raisesFunctionClauseErrororArgumentErroron anything else; 2.7.0 read#fff,#rgba,#rrggbbaaand a missing or doubled#as colours, and returned{0, 0, 0}for other strings and for non-strings such as:red.nilstill returnsnil. Breaking forraxolusers who called the helpers or passed other colour forms toconvert_color/2: callRaxol.Utils.ColorConversion.hex_to_rgb/1andrgb_to_hex/1(mainraxol) instead, or normalise to#rrggbbfirst. Every colour in the registry's own themes is#rrggbband converts exactly as before.raxol_terminalmode plumbing that duplicatedModeManager/ModeTypes. ModulesRaxol.Terminal.ModeState,Raxol.Terminal.ModeHandler,Raxol.Terminal.Commands.ModeHandlerandRaxol.Terminal.Modes.Handlers.MouseHandler;Raxol.Terminal.ModeManager.get_manager/1,update_manager/2,mode_set?/2,get_set_modes/1,reset_all_modes/1,save_modes/1andrestore_modes/1(all were no-op stubs); theRaxol.Terminal.Emulator.mode_statefield (also onEmulatorLite) andEmulator.update_insert_mode/2/update_auto_wrap_mode/2, which wrote to it and were never read; and theModeTypesrows for DEC-private 80 and standard 80/132 described above.Raxol.Core.ConnectionPoolwaiting queue: thewaitingstate field, the checkin branch that handed a connection to a waiter, and the:waitingkey instats/1. Nothing ever joined the queue: an exhausted pool has always returned{:error, :timeout}at once, and still does, now documented as a refusal rather than a wait.Raxol.System.Updaterbinary-delta updates (#1076):Raxol.System.DeltaUpdaterand itsDeltaUpdaterSystemAdapterBehaviour/DeltaUpdaterSystemAdapterImpl, thebspatchstep, the:use_deltaoption, andRaxol.CLI.Commands.UpdateCmd's--no-delta/--delta-info. Nothing published deltas, and none would pay off. Burrito appends an xz-compressed payload, so 98% of the bytes differ between the 0.2.8 and 0.2.10 macOS binaries, and absdiffpatch is 14.3 MB against a 15.6 MB full download. Also removed: theverify_checksumsupdate setting (verification is no longer optional) andRaxol.System.Updater.Validation.get_platform/0(seeManifest.host_platform/0). TheUpdaterfunctions now take an options keyword, documented on the module.Servers that nothing started, and their only clients (#1094). No call into these servers could succeed: nothing in
raxolstarted them, so each API call exited:noproc(or, for casts, did nothing), and their only in-repo callers were each other.Raxol.Performance.AutomatedMonitorandmix raxol.perf.monitor(Mix.Tasks.Raxol.Perf.Monitor). The task booted the app, which never starts the monitor, so every subcommand exited:noproc; and eachmixrun is a fresh VM, sostart,statusandstopcould never share a monitor. The four events it measured are not emitted anywhere.Raxol.Core.ErrorPatternLearner,Raxol.Core.ErrorPatternLearner.PersistenceandRaxol.Core.ErrorPatternLearner.Predictor. Their callers were the three modules below.Raxol.Core.ErrorRecovery.RecoverySupervisor(itshandle_child_exit/3recovery path had no caller),Raxol.Core.ErrorRecovery.RecoveryWrapper(started only byRecoverySupervisor), andRaxol.Core.ErrorRecovery.DependencyGraph, a pure helper used only byRecoverySupervisor.Raxol.Core.ErrorRecovery.ContextManageris removed too (below).Raxol.Telemetry.events/0drops the[:raxol, :error_recovery, :circuit_break],[:raxol, :error_recovery, :degradation]and[:raxol, :error_recovery, :restart]events, which onlyRecoverySupervisoremitted.Raxol.Core.ErrorReporter. Nothing started it or called it.Raxol.Core.ServerRegistry. Nothing started it, andget_server/1looked names up in aRaxol.Registrythat nothing starts either. It also leaves the singleton allowlist.
Servers nothing could reach, and their callers (#1094). Each used
BaseManager, whosestart_link/1registers no name unless given one, and nothing started it under the module name its API calls, so no call to any of them could succeed: every one exited:noproc.raxol_terminal:Raxol.Terminal.IO.IOServerandRaxol.Terminal.Rendering.RenderServer(only ever started unnamed, from inside IOServer). With them goRaxol.Terminal.Integration.Config.update_renderer_config/2,Raxol.Terminal.Integration.State.update_renderer_config/2and theIntegration.State:iofield.raxol_terminal:Raxol.Terminal.Window.Registryand its only caller,Raxol.Terminal.Window.Manager.Operations, which nothing called.Raxol.Terminal.Window.Manageris unaffected: it runs onWindowManagerServer.- Root:
Raxol.UI.Components.Terminal.Emulator, an IOServer wrapper nothing used. raxol_core:Raxol.Core.GlobalRegistry(includingGlobalRegistry.RegistryBehaviour), and with itRaxol.Terminal.Session.count_active_sessions/0.Terminal.Sessionno longer tries to register itself there, which failed and logged "Failed to register session" on every session start.raxol_core: the GenServer half ofRaxol.Core.Runtime.Plugins.PluginCommandManager:start_link/1,register_commands/3,unregister_commands/1,get_commands/0,get_plugin_commands/1anddispatch_command/2. Its one caller, the plugin-load path inLifecycleHelper, also passed the plugin's state map where a command list belonged, which a running server would have rejected. The pureinitialize_command_table/2andupdate_command_table/2stay.
Dead UI and theming APIs behind servers nothing started (#1094).
Raxol.UI.Universal, whichuse Raxol.UIimported: the macrosuse_action/2,3,transition/2,3andrender_universal_slot/1,2, andprovide_context/2,use_context/1,2,use_theme/0,with_theme/2,provide_slot/2,handle_universal_event/1,2,subscribe_to_events/1,2andanimate/2,3. None could work: they expanded to modules that do not exist (Raxol.Actions,Raxol.Transitions), called aStateManagementServer.get_slot/1that was never defined, registered in aRaxol.Eventsregistry nothing started, or called the UI state server, which nothing started on that path and which has no clause for those messages.animate/2,3only sent{:animate_property, property, value}messages that nothing in Raxol handles; useRaxol.Animation.Helpers.animate/2.use Raxol.UInow brings in only the selected framework.Raxol.UI.State.Management.StateManagementServer.set_context/2,get_context/1,2andset_slot/2, whose only caller wasRaxol.UI.Universal; the server never handled their messages.Raxol.UI.Theming.PaletteRegistryand theRaxol.UI.Theming.Colorsfunctions that called it:register_custom_palette/2,unregister_custom_palette/1,list_custom_palettes/0andget_custom_palette/1. Nothing started the registry, so every call exited:noproc; its persistence only logged.Raxol.Themes(apply_theme/1,get_current_theme/0,load_theme/1,list_themes/0,register_theme_callback/1,2). Nothing started it, and it could not be started under its name (init_manager/1matched only:ok), so every call exited:noproc. UseRaxol.UI.Theming.Theme.apply_theme/1andTheme.current/0.
Code with no callers (#1107).
- Root:
Raxol.Core.Renderer.RendererManagerandRaxol.Core.Runtime.Events.Handler, twoEventManagerconsumers that no library code started or called. raxol_core:Raxol.Core.Events.Manager(a delegating alias ofEventManager),Raxol.Core.Events.Subscription(keyboard, mouse, window, timer and custom subscription wrappers overEventManager.subscribe/2) andRaxol.Core.Events.EventManager.EventManagerServer, which nothing started. Breaking forraxol_coreusers: all three were public, documented modules of the published package, so code that calls them no longer compiles and the nextraxol_corerelease carries the break. UseRaxol.Core.Events.EventManagerdirectly.raxol_core: the plugin load path that only called itself:LifecycleManager.load_plugin/8andunload_plugin/6,Discovery.load_plugin/8andunload_plugin/6,LifecycleHelper.load_plugin/8,load_plugin/3andunload_plugin/6, and the helpers only they reached:Raxol.Core.Runtime.Plugins.PluginUnloader,PluginErrorHandler.handle_load_error/2,PluginValidator.validate_plugin/4, andPlugins.StateManager.initialize_plugin_state/2andupdate_plugin_state_legacy/3(with theirRaxol.Core.Behaviours.StateManagercallbacks). Plugins load throughPluginManager.LifecycleManager.reload_plugin/2stays, though it is unreachable as well: its only caller,PluginReloader, is called only fromFileWatcher.handle_debounced_events/3andLifecycleHelper.reload_plugin_from_disk/8, and nothing outside that chain (LifecycleManager,Discovery,LifecycleHelper,PluginReloader,FileWatcher) calls into it.PluginValidator,Security.BeamAnalyzer,Security.CapabilityDetectorandCommandRegistry.unregister_plugin_commands/2lost their last library caller here too; they stay becausedocs/plugins/GUIDE.mddocumentsCapabilityDetectorfor plugin authors. Nothing on the livePluginManager/PluginLifecycleload path validates a plugin, and thePluginValidatormoduledoc now says so.- Root:
Raxol.Core.ErrorTemplates, whose one caller was the removedErrorReporter, andRaxol.Core.ErrorRecovery.ContextManager, which only its own test used.
- Root:
Raxol.Core.Metrics.Aggregator.record/3(#1120). It only passed its arguments on toMetricsCollector.record_metric/4as a:custommetric, and since #1106 nothing in the repository calls it. Record throughRaxol.Core.Metrics.record/3orMetricsCollector.record_metric/4; aggregation rules read whatever the collector holds.raxol_terminal:Raxol.Terminal.Escape.Parsers.CSIParserCachedandRaxol.Terminal.CellCached, andbench/suites/core/performance_improvements_benchmark.exs(#1120). Neither module cached anything:warm_cache/0returned:ok, andparse/1,new/2,batch_new/1andmerge_styles/2calledCSIParser.parse/1,Cell.new/2andMap.merge/2directly. The benchmark, their only caller, timed those against the same uncached calls, printedRaxol.Performance.ETSCacheManager.stats/0(every table at 0 entries afterwarm_cache, since nothing wrote to them) and then a fixed summary of improvements it never measured ("60-80%" for CSI parsing, "40-60%" for cells, rendering "30-50% faster"). Breaking forraxol_terminalusers: both were public modules of the published package. CallCSIParser.parse/1andCell.new/2, which is what they did.Raxol.Headless.send_key_and_screenshot/3's:wait_msoption,raxol_send_key'swait_msargument andRaxol.Recording.Video.capture_clip/2's:event_settle_msoption. Each set a sleep after a key so its update could land;send_key/3now guarantees that forupdate/2itself (see Changed). The sleeps also gave a fast asynchronous command the key started (a Task, a directive that answers with{:command_result, _}) time to land before the capture, and nothing waits for those now: their results show in a later screenshot or frame.wait_mswas also unbounded: it reachedProcess.sleep/1inside the session manager, so oneraxol_send_keycall could stall every headless session for as long as it asked. Any of them passed now is ignored like any other unknown option or argument.raxol_terminal:Raxol.Terminal.Buffer.Writer.log_char_write/4and the debug line it wrote (#1031).write_char/5called it for every cell, and it logged the constant"[Buffer.Writer] Writing char ": the character, position and style were commented out of the message. The runtime draws each frame cell by cell throughScreenBuffer.write_char/5, so at:debugevery frame logged one line per cell that said nothing, which is theBuffer.Writerstream test logs were full of. Nothing else called it. Breaking forraxol_terminalusers who called it: it was public and documented.Raxol.Plugins.ClipboardPlugin(#1261). Nothing in the repository used it. Breaking forraxolusers: it was a public, documented module of the published package.Raxol.Core.Runtime.Plugins.PluginManager.load_plugin(:clipboard_plugin)resolved that id to this module by naming convention and now returns{:error, :module_not_found}.Public
raxolmodules and functions with no callers. Breaking forraxolusers who called them:Raxol.Core.Plugins.Core.ClipboardPlugin,Raxol.Core.Plugins.Core.ClipboardPluginBehaviour,Raxol.Core.Plugins.Core.NotificationPlugin,Raxol.UI.ComponentTest,Raxol.UI.IntegrationTest,Raxol.UI.PerformanceTest,Raxol.UI.VisualTest,Raxol.CLI.Commands.UpdateCmdandRaxol.Sandboxare removed, as are fifteen public functions ofRaxol.Plugins.Lifecycle.Initialization(initialize_plugin/2,initialize_plugin_with_config/3,initialize_all_plugins_with_configs/2,prepare_plugin_for_manager/3,create_plugin_struct/3,validate_plugin_module/1,validate_plugin_state/2,validate_required_fields/2,validate_field_types/2,check_api_compatibility/1,get_api_version/1,get_default_config/1,get_merged_config/2,get_plugin_metadata/1,get_plugin_name/1) andRaxol.Plugins.Lifecycle.ManagerUpdate.update_manager_state_with_plugin_state/3andupdate_manager_with_plugin_config/3. None has a replacement.
Fixed
raxol_mcp:Client.await_ready/2after a rejected credential waited out its budget. A client whose handshake got a 401 answered the callers already waiting, but one that arrived afterwards waited its whole timeout and got{:not_ready, :closed}. It now gets{:connect_failed, reason}at once.raxol mcpandmix mcp.servercorrupted non-ASCII text without a UTF-8 locale.Raxol.MCP.Transport.Stdioread and wrote through the device's encoding. WithLANGunset orLC_ALL=C,éin a request arrived aséand a reply containing it was not valid JSON; under a UTF-8 locale one invalid byte from the client switched the device to latin1 for the rest of the session. The transport now reads and writes bytes.raxol_journal: a journal's first open reads<cwd>/.git/HEADonly when it is a regular file of at most 512 bytes. A FIFO there blocked the agent supervisor for every later start and stop, and a device or a symlink was read to its end.Security: current Hex advisories are fixed across every lockfile.
josemoves to 1.11.13 to pick up its October 2026 cryptographic hardening,plug_cryptois pinned at 2.2.1, and the root lock moveshackneyto 4.9.1 and Tidewave to 0.9.2. The 20-project lockfile audit passes.Nightly's REPL sandbox test passes on every supported Elixir version. Elixir 1.17 and 1.18 omit the documented time-unit alternatives from
Time.utc_now/1's typespec. The spec-derived guard test now supplies those four known literals instead of misclassifying safe calls as unguarded.Nightly: four tests no longer fail on timing or on what another test left behind.
Raxol.UI.RendererTest(async) registered a theme and calledTheme.init/0, which changes the default theme every other test renders with. A test that renders twice and compares saw the two halves under different themes:Raxol.ScoreboardTest(2026-11-10, 11-11) andRaxol.CrossTerminal.ModalOverlayTest(2026-11-08, Windows). It now passes its theme to the renderer without registering it.EngineProcessComponentsTestwaited on its own monitor of a failing component, which can report the exit before the engine has handled its own. It now waits on the engine taking that exit.Raxol.Core.Runtime.LifecycleTestleft a running Lifecycle to shut down after the test, and its plugin manager still held thePluginLifecyclename whenPluginSystemTeststarted (2026-11-08). The test stops it and waits.- The 5000-block memory test in
t13a_surface_test.exshas 300 s rather than 180 s. Its drive is quadratic in the event count: the integration lane finished in 131 to 184 s on nights it passed, and the test ran past 180 s on the others (2026-11-08, 11-11). 300 s is what nightly gives the other CPU-bound harness suites.
Derived MCP write tools reach the app's
update/2instead of crashing the Dispatcher. A derived tool's message ({:sort, id, column, direction}from a Table'ssort, and the rest) was cast as{:dispatch, msg}into the full event path, which folds it throughupdate/2and then readsevent.type, so the first write call killed the session's Dispatcher.Raxol.MCP.TreeWalkernow calls the Dispatcher's new{:dispatch_tool_message, msg}clause, which takes the raw path toupdate/2(still emitting the durable:app_updateevent), and the tool answers onceupdate/2has folded the message; a dead or unresponsive Dispatcher is an error result, and so is anupdate/2that raises or returns an invalid result (:update_failed). A timed-out call (5 s) may still be applied afterwards;Raxol.MCP.ToolProvideranddocs/features/MCP.mdsay so.Raxol.MCP.ToolProviderdocuments each provider's message. A Button'sclicktool sends the node'son_clickmessage as captured when the tool was derived (it sent a click%Event{}the Bubbler routed from the focused element, not the button), and is an error result whenon_clickis missing or a function. A Table'ssortis an error result for a direction other than"asc"/"desc"or a column the node does not declare (key or label, ignoring case).Tabs.mcp_tools/1no longer raises on string tabs, andget_tabsreads the DSL's:active(or the component's:active_index).Components.text_input/1keeps:id. The Table, SelectList, Modal, TextInput and Tabs playground demos now derive their tools (playground_table.sort,lang.select,confirm.dismiss,input.type_into,tabs.select_tab, ...) and handle the messages; TableDemo's stars are numbers (still shown as39k), itssorttakes a column key or label in any case, and TabsDemo draws the DSLtabsbar.Raxol.Headlessisolates each session'sToolSynchronizerand teardown. Synchronizers are unlinked and monitored, so one synchronizer exit ends only its session; Headless and every other session continue. Registry synchronization during startup and all session teardown now run in monitored workers rather than the Headless GenServer: a synchronizer blocked registering with or unregistering from an unresponsive Registry cannot blocklist/0, screenshots, or other sessions. Concurrentstop/1callers join the same teardown and receive their replies only after it finishes; a stop racing startup cancels that start and also waits for cleanup. The session remains listed until its synchronizer is down, and the Registry's owner monitor remains the authority that removes tools and resources after a kill.ToolSynchronizernow also monitors the concrete Registry process it registered with. If a named Registry is replaced, the old process's exit stops the synchronizer and the existing monitored teardown ends that session instead of leaving it listed against an empty replacement. Synchronizers still monitor their optional:owner, which Headless sets to itself. A failed synchronizer start answers{:error, {:tool_synchronizer_failed, class}}only after owner cleanup and stops the Lifecycle. Abnormal teardown workers have a bounded retry and a terminal forced-cleanup error instead of retrying forever. Headless traps app-init failures and bounds whole-manager shutdown, including sessions in startup or teardown, killing sessions that exceed the shutdown budget rather than leaving their Lifecycles running.Tests: an approved stdio elicitation worker now reports why it died early (#1318). The cancellation regression monitors the worker and immediately asserts it is alive before sending
notifications/cancelled. The blocking worker traces its exit reason, and the test traces:erlang.exit/2calls from the MCP Server, so a recurrence reports both the reason and whether the Server sent it instead of only showing a later monitor reason of:noproc.Security:
Raxol.REPL.SandboxrefusesCalendar.put_time_zone_database/1(#1237).Calendaris on the:strictallowlist, so the call passed both sandboxed levels with any argument, including a module computed from data. It sets:elixir's:time_zone_databasefor the whole node, which neither the evaluation timeout nor the heap cap undoes, so one anonymousReplDemosubmission made every laterDateTimezone operation in every process dispatch to the module it chose. It is now on the denied list for:standardand:strict, in call, piped, captured and:"Elixir.Calendar"forms;Calendar.get_time_zone_database/0and the rest ofCalendarstay allowed.:strictkeeps the setting out of reach;:standardrefuses the function by name only, andApplication.put_env(:elixir, :time_zone_database, m)still writes it there.Security:
Raxol.REPL.Sandbox:strictrefuses a module argument that is called back (#1238).raise m, path: "x",reraise m, [], [],DateTime.now("Europe/Paris", m),Date.new(2020, 1, 1, m),Calendar.compatible_calendars?(m, Calendar.ISO),Map.from_struct(m)and~D[2020-01-01 M]each passed:strictwithmminted from data (String.to_existing_atom/1) and ranm.exception/1, the time zone database or calendar callbacks,m.__struct__/0, orM.parse_date/1while the sigil expanded;raise Protocol.UndefinedError, protocol: m, value: 1ranm.__protocol__/1when the evaluator formatted the error.raise/reraisenow take a string literal or one of 15 audited exception modules (ArgumentError,KeyError,RuntimeError, ...), written out, with attributes that are a string (where the exception has a:messagefield) or a written-out keyword list of the exception's own fields; on Elixir 1.17 an unknown key went toIO.warnand the node's stderr. Those fields are written out per exception rather than read from the struct, and a test fails when an Elixir release adds one nobody has audited. Every calendar argument of an allowlisted function (53 function/arity pairs, from their specs) takesCalendar.ISOand every time zone database argumentCalendar.UTCOnlyTimeZoneDatabase, plus only the unit or format atoms that slot's own spec lists; a calendar sigil may only nameCalendar.ISO, by Kernel's own suffix rule, which a test expands against a probe calendar so the copy cannot drift, and a sigil macro called with anything but sigil syntax (Kernel.sigil_D("2020-01-01 M", [])) is refused rather than left to Kernel's macro to reject.Map.from_struct/1takes only a map or struct written out: given a module it also wrote a deprecation throughIO.warnto the node's stderr on Elixir 1.18 and later, 880 KB from onefor _ <- 1..2000, do: Map.from_struct(Date);Map.delete(s, :__struct__)does the same job for a held struct. Piped,Kernel.-qualified and captured forms are refused alike. At:strict, a variable, call,<>or sigil as the exception is refused, re-raising a rescued exception included; interpolate a computed message (raise "#{msg}"). Any map with a:calendarkey remains a route to the same callbacks, a plain map literal included (Date.leap_year?(%{calendar: m, year: 2020})), as do struct literals, updates,Map.put/3and a struct held in araiseattribute (raise BadBooleanError, operator: %Date{..., calendar: m}) (#1282; the class closes with #1231).Nightly Credo no longer restores stale compiled path dependencies. Dependency sources retain lockfile fallback reuse, while
_buildis cached separately for the exact commit, OS, architecture, resolved OTP/Elixir versions and Mix environment, with no fallback. This prevents cachedraxol_core2.7.0 metadata from failing the current~> 2.8requirement before compilation.Four package back-references no longer need boundary exceptions.
raxol_agentrepresents the reserved Codex backend without namingraxol_symphony; accessibility changes reach the root colour system through the existing core event; andRaxol.Core.BufferandRaxol.PubSubnow ship fromraxol_coreandraxol_liveview, respectively.raxol_terminalworks without mainraxolon the code path.Raxol.Terminal.Config.Capabilities.optimized_config/0,Raxol.Terminal.Theme.Manager.load_from_registry/2andlist_registry_themes/0, andRaxol.Terminal.Script.ScriptServerwithauto_load: trueeach called a module that lives in the rootlib/, behind a@compile {:no_warn_undefined, ...}whose comment claimed a runtime guard that did not exist, and raisedUndefinedFunctionErrorwhenever the package ran on its own.Raxol.System.EnvironmentAdapterBehaviourandEnvironmentAdapterImplmove intoraxol_terminal(nothing in mainraxolused them);Raxol.Core.Theming.ThemeRegistrymoves intoraxol_core(see Removed for the API it loses), which goes to 2.8.0, andraxol_terminalnow requiresraxol_core ~> 2.8, since 2.7.0 has noThemeRegistry; the script server keeps auto-loaded scripts in its own state, whereget_scripts/1can see them, instead of writing them intoRaxol.Core.StateManagerwhere nothing read them. In the other direction,Raxol.Core.UserPreferences.default_preferences/0no longer probesRaxol.Terminal.Config.DefaultswithCode.ensure_loaded?/1:raxol_terminalshipsdefault_terminal_preferences: {Raxol.Terminal.Config.Defaults, :generate_default_config, []}in its application env, so every project that has it, standalone or a Hex consumer ofraxol, gets the full terminal section;config :raxol_core, :default_terminal_preferences(a map or an{m, f, a}returning one) overrides it, and without either the section is%{}. Any other value, or an{m, f, a}that is not exported or returns something else, logs a warning and gives%{}instead of raising.scripts/check_package_boundaries.exsnow runs in CI: it parses every file underpackages/*/liband fails on any module reference (call,apply/3, anyno_warn_undefinedform, atom, module or process name) into a tree the package'smix.exsdoes not declare for:prod; the existing ones are listed inscripts/.package-boundaries-allowlist, which a pull request may only shrink. See the boundary rule indocs/PACKAGES.md.CI: every Dependabot Hex bump failed the lock path-shadow guard, and a Hex install of
raxol_agenttook the consumer's own project as:raxol. Eachraxol_dephelper in the root and packagemix.exsfiles swapped a sibling's path dep for the Hex form whenever the sibling directory was absent. Dependabot resolves a bump in a sandbox holding onlymix.exsandmix.lock, so every bump lockedraxol_core,raxol_liveview,raxol_mcp,raxol_plugin,raxol_sensorandraxol_terminalas Hex packages, andscripts/check_lock_path_shadows.exsfailed the PR (#1220, #1247, #1248, #1249, #1252). The same check maderaxol_dep(:raxol, "~> 2.7", "../..")a path dep inside a consumer'sdeps/raxol_agent(a parent directory always exists), and likewise inraxol_cli,raxol_gateway,raxol_symphonyandraxol_telegram. The helpers now hand out the Hex form only for a publish (HEX_BUILD=1) and inside a Hex install, which Hex marks by unpackinghex_metadata.configbesidemix.exs; anywhere else a sibling is a path dep whether or not its directory exists, which Mix warns about and resolves around.test/mix/sibling_deps_test.exsevaluates every Dependabot project from that sandbox and from a Hex install.Process components start asynchronously, tear down with
unmount/1, and are not restarted every frame.Raxol.Core.Runtime.ProcessComponentran the component'sinit/1andmount/1insideGenServer.init/1, and the rendering engine starts every component through the sharedRaxol.DynamicSupervisorwith an:infinitycall, so one slowinit/1held up every engine in the VM. They now run in the component's process afterstart_link/1has returned, and the owner (:parent_pid, now required, Breaking for directstart_link/1callers; it defaulted toself(), which understart_childis the supervisor) gets{:process_component_ready, pid}once both have run. The engine draws[label: starting]until then, and requests a frame on the ready message so the placeholder is replaced without waiting for the app's next event.init/1returns are no longer laundered into empty state:{:ok, state}and a bare map are state,{:error, reason}stops the process withreason, anything else with{:bad_init_return, value}.start_link/1loads the module first and refuses one it cannot load as{:error, {:module_not_available, module, reason}}with no process started; the optional callbacks are found withfunction_exported?/3, which does not load code, so an unloaded module rendered as the bare[label]placeholder. New optionalunmount/1, the mirror ofmount/1, gets the live state when the component stops for any reason (the process traps exits, so a supervisor:shutdownreaches it too; a linked process's exit now stops the component with that reason instead of being ignored) and before a re-init on a props change withoutupdate_props/2; an exception in it is logged and never changes the exit reason.format_status/1redacts state and props in crash reports and:sys.get_status/1. The engine monitors each component and, instead of starting a failed one afresh on every frame, draws[label: crashed](orfailed to start) for a run of frames that doubles with each failure in a row (1, 2, 4 … 64) before the next start; a ready message resets the run, and a node that leaves the view during it is forgotten.Security: every lockfile pins
decimal3.1.2 (CVE-2026-97853, GHSA-6c27-994x-c52f). Below 3.1.2,Decimal.round/3builds its result at the full requested scale before applying the context precision, so one call with a largeplacesallocates gigabytes (-50_000_000is about 5.5 GB). The root,weband ten package locks moved from 3.1.1 to 3.1.2, the security-only patch; 3.2.0's rounding and signalling changes are left for their own upgrade.raxol_broker,raxol_earn,raxol_paymentsandraxol_web3now requiredecimal ~> 3.1 and >= 3.1.2, so a published package can no longer resolve a vulnerable release.Raxol.Core.CircuitBreaker: a breaker that left OPEN no longer half-opens on a stale timer. Opening scheduled{:timeout, :half_open}and nothing cancelled it, so afterreset/1(or after a call half-opened the breaker early) the timer still fired and moved a CLOSED or HALF-OPEN breaker to HALF-OPEN, zeroing its counts. Each open period now tags its timeout with a token, and a timeout from an earlier period is ignored. Found as a Windows CI flake in thereset/1test, which now also waits pastopen_timeout.Raxol.Terminal.Image.display/2refuses an unsupported or unknown protocol before reading or resizing the source. It used to load the file and resize it through ImageMagick (two external process spawns) and only then return{:error, :unsupported_protocol}, so every first render of animageelement in a headless or agent session paid for work it could not show: 378 ms locally, and over 5 s on the Windows runner, inside the rendering engine and pastRaxol.Headless's 5 s call timeout ({:error, {:session_unavailable, :timeout}}inViewDslConformanceTest).RainbowThemePluginstops rotating when told to. The rotation handler rescheduled without keeping the new timer, so after the first tickrainbow stop(andon_unload) cancelled a timer that had already fired and rotation ran on; a tick queued across a stop orspeedrestart started a second chain. Each tick now carries the token of the timer that scheduled it and a stale one is ignored.BundledPluginTimerTestobserves the rotation as a tracedhandle_event/2call on the runtime instead of sampling a colour index that flips every millisecond, which a fixed-interval poll on Windows' 15.6 ms clock could read as unchanged every time.Security:
Raxol.REPL.Sandboxrefuses the remote spellings ofdefmodule,defimpl,defprotocol,useand~w. Each is aKernelmacro, andKernelis required in every evaluation env, soKernel.defmodule(Zzz, do: ...)defined a module,Kernel.defimpl(String.Chars, for: Reference, do: ...)replaced a protocol impl for the whole node,Kernel.use(Bitwise)ran a__using__/1andKernel.sigil_w("#{x}", [?a])minted an atom from runtime data, each:okat:standardand:strictwhile its bare form (defmodule,use,~w(#{x})a) was refused. The four macros are now on the denied list for both levels, in alias,:"Elixir.Kernel"and piped forms, andKernel.sigil_w/2is judged by the~wrule.Security:
Raxol.REPL.Sandbox:strictrefusesIO.streamandIO.binstreamat every arity. Each returns an%IO.Stream{}whosedevicefield is what itsEnumerableandCollectableimpls hand to:io, and a struct update rewrites that field with no device in anyIOcall:Enum.into(["x"], %{IO.stream() | device: :code_server})andEnum.to_list(Map.put(IO.binstream(), :device, name))sent an io request to the named process at:strict, past the device rule below. Both builders, their:stdioforms and their captures are now refused;IO.gets/1andIO.puts/1still read and write the evaluation's own device.Security:
Raxol.REPL.Sandbox:strictrefuses__-prefixed functions of allowlisted modules. The defstruct-generatedMod.__struct__/1writes every pair of its keyword argument,:__struct__included, soURI.__struct__(kv)(and its piped,:"Elixir.URI", and captured forms) passed:strictand built a forged struct, reopening the hole #1235 closed. Remote macros such asRegex.__import_pattern__/1(expands to:re.import/1) passed the same way. Any call or qualified capture of a__-prefixed name is now refused at:strict;:standardis unchanged.Security:
Raxol.REPL.Sandbox:strictrefusesIOcalls that write anywhere but the evaluator's capture.IOhands any atom, pid or tuple device to:io, which sends an io request to that process (a read waits on its reply), soIO.write(Raxol.Payments.Wallets.Op, "x"),IO.puts(:logger, "x"),IO.read(:code_server, :line)andIO.stream(dev, :line)passed:strict, the levelReplDemoserves anonymously, whileKernel.send(Raxol.Payments.Wallets.Op, :x)was refused;IO.write(:stderr, "x")andIO.warn("x")passed too and wrote to the node's real stderr, past the output capture. Every device arity (read/2,binread/2,gets/2,getn/2,3,write/2,binwrite/2,puts/2,inspect/3,stream/2,binstream/2) now needs:stdioor:standard_iowritten as a literal, including when a pipe supplies it (dev |> IO.write("x"),Kernel.|>(:n, IO.puts("x"))) or the module is the atom:"Elixir.IO", and those arities are refused captured (&IO.write/2);IO.warn/1,2is refused in every form. Behavior change at:strict:IO.write(:stderr, x)andIO.warn(x)are now refused.IO.puts(x),IO.inspect(x, label: "a"),x |> IO.inspect()andIO.puts(:stdio, x)still work;IO.getn(a, b)is allowed only with a literal count or:eofsecond, or:stdio/:standard_iofirst.Security:
Raxol.REPL.Sandboxrefuses syntax that mints atoms from runtime data. Atoms are never collected, so neither the evaluation timeout nor the heap cap bounds them, and filling the atom table kills the node.~w(#{x})a, whichKernel.sigil_w/2expands toString.to_atom/1over the runtime split, and computed aliases such asx.Foo,%{x.Foo => 1}andInteger.to_string(i).Foo, which concatenate a newElixir.-prefixed atom, all returned:okat:strictand:standard; both levels now refuse them (and__MODULE__.Foo).~w(a b)a, uninterpolated or non-atom word lists and literal aliases are unaffected.Security:
Raxol.REPL.Sandbox:strictrefuses sorters that could name a runtime module (same class as #1238).Enum.sort/2,Enum.sort_by/3,Enum.max/min/min_maxand their_byforms, andList.keysort/3callmodule.compare/2when the sorter is a module, soEnum.sort(l, m),Enum.sort(l, {:asc, m}),Enum.max(l, m, fn -> nil end),List.keysort(l, 0, m), the pipedl |> Enum.sort(m)and the capture&Enum.sort/2were:okat:strictand dispatched to whatevermheld. The sorter must now be written out as a function or capture,:asc/:desc, a whitelisted module or a direction tuple of those;Enum.sort(dates, Date)andEnum.sort(dates, {:desc, Date})stay allowed, and a sorter-hiding capture is refused.Raxol.REPL.Sandbox.check/2emits no parser warnings and rejects unknown levels. Parsing ran with the defaultemit_warnings: true, so submitted source wrote the tokenizer's warnings to the node's stderr before any refusal: 12 KB ofMap.'put'();produced 328 KB. It now parses withemit_warnings: false. An unknown level (:Strict,:bogus) checked nothing and returned:okforSystem.cmd("id", []); it now raisesArgumentError. The only caller already validated its level, so this was latent. The duplicate, unreachableimport/alias/require/useclause is gone.Security:
Raxol.REPL.Sandbox:strictrefuses forged structs (#1235). A map whose__struct__key names a module is protocol-dispatched to that module, som = %{__struct__: File.Stream, path: p, ...}; Enum.into(["hi"], m)passed:strict, the levelReplDemoserves anonymously, and wrote the file;Enum.count/1, string interpolation,Map.put/3,Map.new/1,struct/2and%File.Stream{}reached other modules' impls the same way. #1230 had refused only a forged map written directly as theEnum.intotarget. The key is now refused where a map is built: a:__struct__or computed key in a map literal or an access path, a struct literal orstruct/2outsideURI,Date,Time,DateTime,NaiveDateTime,RangeandMapSet, aMap.put-family call with a non-literal key,Map.new/1,Map.from_keys/2,Enum.into/Stream.intointo a map from a computed input,for ... into:anything but a list orMapSet, andMap.new/2,Map.map/2,Map.merge/3,Map.intersect/3. Behavior change at:strict:Enum.into(l, %{}),Map.new(l),for ... into: %{}andMap.put(acc, k, v)with a computedkare now refused;%{m | a: 1}and literal-key builders still work. Keys that exist only at runtime in an unlisted form can still forge; the OS-isolated peer node (#1231) closes the class.- A bare name piped into
struct/struct!(File.Stream |> struct, whichKernel.|>expands tostruct(File.Stream)) is checked as that call;Access.values/0is refused outside a path too, since its access fun rewrites a planted__struct__value; a key is judged by its top-level form, so string, interpolated, tuple, list and map keys are allowed and nested map keys no longer cost quadratic time; and a refused computed%expr{}no longer echoes its expression, which made 9.6 KB of nested input produce 2.7 GB of messages. A piped call is judged once, as the call the pipe rebuilds, so a piped form passes exactly when its direct form does (URI |> struct(host: "x"),[a: 1] |> Enum.into(%{})and[:a] |> Map.from_keys(0)were refused because the right-hand node was also checked alone with its arguments one place off), and a qualified capture of a forge or__-prefixed function (&Map.put/3,&URI.__struct__/1) gives one refusal, from the capture, not a second, wrong one ("its key cannot be seen") from the argument-less call inside it. Refusals of a computed map key, a one-key writer (Map.put-family,Access.key/1,2),Map.new,Enum.into/Stream.intoandfor ... into:name what still works at:strict: a computed key wrapped in a string or tuple ("#{k}",{k}),Enum.frequencies/1,Enum.group_by/2,3, and collecting into a list orMapSet;Enum.into/Stream.intoandfor ... into:give one refusal per site that names only targets that pass (a literal list orMapSet.new()), where the string-target refusal used to recommend a map orMap.new(), which the forge rule then refused.docs/features/REPL.mddescribes the shallow key rule. Every forge rule entry now has tests of its own in each call form: deleting:update_inor:struct!from the Kernel forge calls,Timefrom the struct-literal allowlist,:findfrom the list selectors or the alias case from the plain-key rule failed no test before.
- A bare name piped into
Raxol.REPL.Sandbox.check/2returns a syntax error instead of raising on a strayendor invalid UTF-8. The parser splits a strayend's message into a{prefix, suffix}tuple, which the"Syntax error: #{message}"interpolation raised on, andCode.string_to_quoted/1raises on invalid UTF-8;ReplDemothen logged a stack trace per Enter and kept the input. Both now come back as{:error, ["Syntax error: ..."]}at every level, with the parser's token included in the message as Elixir prints it.Globs under a directory no longer read the directory's own name as pattern.
Path.wildcard(Path.join(dir, "*.md"))compilesdirtoo, so a directory namedapp [v2]matched nothing,app{a,b}matchedappa/andappb/instead, and on Windows every\separator was an escape. The directory part is now escaped (Raxol.Utils.Glob.under/3in the root app,Raxol.Agent.Glob.under/3inraxol_agent, a private equivalent inraxol_coreandraxol_earn). Affected:Skills.Store(skipped every skill under such a root),Raxol.Release.PackageCheck(its provenance walk saw no files, dotfiles included, in such a checkout),Config.Loader,Headless.DocsResource,Benchmark.Storage,mix raxol.convert.base_manager,mix raxol.gen.specs,Telemetry.Invariants.scan_lib!/1and the ACP console package reader. Patterns that are themselves user globs (mix raxol.mutation,files:entries) keep their syntax.raxol_liveview: connected mounts no longer create permanent atoms.TEALivenow starts each per-client lifecycle unregistered instead of deriving a registered atom from the LiveView pid. A connected-mount regression test verifies the lifecycle has no registered name.raxol_liveview: keydown events no longer synchronously call the lifecycle process. A connectedTEALivemount resolves and caches the dispatcher once; each browser key event casts directly to it. A dead or unresponsive lifecycle therefore cannot crash or stall the LiveView callback. Regressions cover dead and blocked lifecycle processes plus successful delivery through the cached dispatcher.raxol_mcp: slow callbacks no longer block every MCP client.Raxol.MCP.Servernow runs tool, resource, prompt, completion, and authorization callbacks in linked, monitored workers while the shared router continues handling pings, notifications, connection cleanup, and elicitation timers. Callback concurrency is bounded by:max_in_flight(default 32), execution by:callback_timeout_ms(default five minutes), and a finitehandle_message/4timeout cancels its worker instead of merely abandoning it. Approved elicitation tools use the same worker path. The ETS circuit breaker now counts concurrent failures atomically and admits exactly one half-open recovery probe.raxol_terminal: emulator writes preserve wide-cell pairs.ScreenBuffer.Operations.write_char/5now uses the same canonical row placement as the renderer: overwriting either half of a CJK/emoji cell clears the other half, and a wide glyph at the final column becomes a styled blank instead of painting past the buffer edge. Regressions cover both overwrite directions and the final-column boundary.raxol_gateway: failed adapter sends no longer become phantom conversation history.Raxol.Gateway.Sessionrecords:gateway_outonly after delivery succeeds. Adapter{:error, reason}returns, exceptions, and exits now log context and emit[:raxol_gateway, :session, :delivery_failed]without crashing the chat session. Integration regressions use the real conversation log to verify only inbound history remains after both returned and raised delivery failures.The rendering engine survives a busy dispatcher and coalesces render bursts. Asynchronous
:render_framecasts arriving during a render now collapse into one trailing frame, preserving the latest dispatcher state without replaying every intermediate frame. Dispatcher exits and the configurable:dispatcher_timeoutnow skip a frame instead of crashing the linked engine and terminating the whole TUI. Regressions hold the dispatcher to verify a 21-request burst renders twice and verify timeout recovery through the live GenServer API.The virtual filesystem preserves its node-parent invariant.
Raxol.Commands.FileSystem.mkdir/2andcreate_file/3now return{:error, :not_a_directory}when the parent path is a file. Previously both returned{:ok, fs}, changed the file'schildrenfield fromnilinto an improper list, and inserted an unreachable child that became orphaned when the parent file was removed. A public-API regression covers both node types, and a state-machine property checks that every successfully created non-root node has a directory parent.Workflow fan-out reports real execution progress instead of checkpoint addresses.
Raxol.Workflow.Runtimenow counts user-node executions separately from checkpoint step allocation. A four-node two-branch graph that returnednodes_executed: 2_000_002now returns4; concurrent branch tasks report their local progress to the parent, while the existing million-step branch strides continue preventing checkpoint-key collisions. Retries still count once after settling, and successful, interrupted, and failed nodes retain the existing progress semantics.raxol_mcp: static headless apps expose derived tools before startup returns.Raxol.MCP.ToolSynchronizernow pulls the initialized model and derives the app's first view synchronously during startup, then retains the existing telemetry-driven updates for later renders. Previously the rendering engine could emit its only frame before the synchronizer attached, leaving a quiet app with onlydiscover_tools. A publicRaxol.Headless.start/2regression repeats startup ten times and requires the button-derived tool immediately on every return.MCP focus now reaches adaptive command-concentration learning without treating hover as focus.
Raxol.MCP.ToolSynchronizeremits:pane_focuswith the:pane_idfield consumed byRaxol.Adaptive.BehaviorTracker, so the next:command_issuedevent carries the same identifier in:focused_pane. Mouse hover now uses a distinct:widget_hoverbehavior event and[:raxol, :mcp, :hover_changed]telemetry instead of impersonating keyboard focus. End-to-end regressions cover focus propagation, command annotation, and hover isolation.Git integration tests isolate repository policy and Git commands run once. Plugin test repositories now create a deterministic
mainbranch, disable commit signing locally, and fail immediately when setup fails instead of cascading from an unbornHEAD.GitIntegrationPluginexecutes each command once and keeps stderr separate so warnings cannot corrupt successful stdout parsing; failed commands return their stdout or exit status while Git writes diagnostics to stderr.Security: the terminal emulator bounds what its output byte stream can make it allocate, loop over or buffer. That stream is written by programs, remote peers and replayed
.castfiles, and a few bytes carrying a large number were enough:CSI 50000000 @(11 bytes) and a sixel!3000000~(14 bytes) each ran a process past a 64 MB heap, and an unterminated escape sequence or string grew for as long as the stream did. Where a standard terminal bounds a valueraxol_terminalnow follows it; elsewhere the cap is documented where it is applied:- Control sequence parameters (CSI and DCS) keep xterm's limits while they arrive: 30 parameters (
NPARAM), each clamped to 65535 (MAX_I_PARAM); past the last one digits accumulate into it, as in xterm. A million-digit parameter no longer builds a bignum, nor a million parameters a list. Intermediate bytes stop at 15, as in libvterm. - ICH (
CSI Ps @) inserts at most up to the right margin. - OSC and DCS strings stop at 20,000 bytes (xterm's
maxStringParsedefault for builds without sixel or ReGIS; with them xterm allows 600,000), and sixel data at the newRaxol.Core.Defaults.max_image_payload_bytes/0(4 MiB). A longer string is discarded, skipped to its terminator and never dispatched. - Sixel decoding keeps only the pixels the screen can show from the cursor, and at most the new
Raxol.Core.Defaults.image_size_ceiling/0(the local terminal ceiling: 4096 x 4096, 1,048,576 pixels) for any other caller. Repeats, raster attributes and colour registers are read without bignums or unbounded lists, and data with many unknown bytes or commands parses in linear time (both were quadratic). Each sixel is its own image: the previous one's pixels are no longer drawn again at the next one's cursor (which also brought a cleared image back). Drawing is batched per row with one cell per colour; one full-screen image at 400x100 needed over 64 MB of heap, and 200 full-screen images at 512x256 took 143 s (now 21 s). - The tracked command line (
current_command_buffer, and the history entries made from it) keeps at most 4096 bytes, a canonical-mode tty line, and each chunk is scanned without a list of its graphemes (~64 bytes of heap per input byte). - The head-of-chunk DECSTBM prescan in
Emulator.InputProcessing, which parsed anyCSI Pt;Pb rat the start of a chunk as an unbounded integer and was always overridden by the real handler, is removed. ESC H(HTS) andCSI Ps Jwith a mode other than 0-3 crashed the emulator; HTS now keeps one stop per column and an unknown ED mode is ignored.- Sequences real programs send no longer crash
process_input. OSC 7, 4, 10, 11, 17, 19, 51, 52 and OSC 1337CurrentDir=/RemoteHost=wroteEmulatorfields that never existed (so, for one, neovim's startup OSC 11 query killed the session). OSC 7 and 1337 now set the newcurrent_directoryandremote_hostfields. OSC 4, 10, 11, 17 and 19 are ignored, sets and queries alike, since the emulator keeps no colours to set or report; programs that get no reply fall back to their defaults. OSC 52 and OSC 51 are ignored: a query is never answered, so output cannot read the pilot's clipboard or selection, and a set has no clipboard to go to. ICH, DCH, IL, DL and ECH read a count of 0, or one with colon subparameters, as 1 (ECMA-48) instead of crashing. - A byte that is not UTF-8 shows as U+FFFD and parsing carries on, as in xterm, in time linear in the input. It used to throw away everything the chunk had changed and the rest of it, so one bad byte could hide output from the pilot. A character split between chunks is joined. The command-line scan steps by codepoint, as OTP's grapheme breaking raises on some invalid UTF-8.
- Sixel images are drawn at the cursor's column and row; the cursor's row and column were swapped, so an image landed transposed and was clipped to the wrong extent. The decoded pixel map is dropped once the image is on the screen (it held up to a million entries, ~36 MB at 4096x256).
- BEL is counted in the new
Emulator.bell_countfield instead of forkingtput belfor every 0x07 byte (about 2.7 ms each, with the output captured so nothing rang: 200 KB of BEL stalled a session for 524 s). The count only grows (RIS keeps it) and is there for a renderer to act on, comparing it with the last value it saw; nothing in raxol rings it yet. - DECSC (
ESC 7) keeps one saved state per screen, main and alternate, as xterm does, and DECRC restores it without consuming it. EachESC 7pushed a full copy ontostate_stack, never trimmed (100,000 of them held 16 MB). The alternate-screen modes save and restore through the same slot:CSI ? 1049 hthenESC 7thenCSI ? 1049 l, orCSI ? 1049 h/? 1047 hthenESC 8(smcup followed by sc/rc, routine for ncurses) raised KeyError on mismatched saved-state shapes. Leaving 1047/1049 restores only the main screen's DECSC slot, as in xterm, not the scroll region saved on entry (margins are not part of the saved cursor) and no longer theCSI sslot, which a program'sCSI son the alternate screen overwrote; a 1047 exit with nothing saved leaves the cursor where it is.CSI salso fills its screen's DECSC slot, as in xterm, soESC 8restores it. DECRC with nothing saved on its screen (as on a fresh alternate screen) homes the cursor, turns origin mode and all SGR attributes off and resets the character sets, including a locking shift, as in VT510 and xterm; it was a no-op.CSI ? 1048 lrestores only the cursor, as before. - An alternate-screen switch and IL/DL cost in proportion to the rows they change, and IL/DL keep the buffer's row count when the scroll region runs past its last row (after
Emulator.resize/3, or at a size past the cell ceiling, the emulator's height and the buffer's differ). ED with the cursor past the buffer's last column keeps the row's width instead of crashing. Clearing a screen shares one blank row instead of building every cell, and IL/DL split the row list inside the scroll region (count clamped to it) instead of converting the whole screen to a map and back. PerCSI ? 1049 h CSI ? 1049 l: 2.1M reductions to 5.4K at 512x256, 18M to 14K at 4096x256 (a 10-toggle run went from 4.4 s and +1.1 GB to 1 ms). PerCSI 256 L: 437K to 1.8K reductions at 512x256, 3.2M to 5.8K at 4096x256. Inserted and deleted rows are now blankCells rather than bare maps. - OSC 7 is parsed as a
file://host/pathURI:current_directoryholds the percent-decoded path andremote_hostthe host, only when the path is absolute and both are valid UTF-8 without C0 or C1 control characters (OSC 1337CurrentDir=/RemoteHost=likewise); anything else is ignored. Both remain the writer's claim. - Malformed or unknown sequences (unknown CSI and OSC, unknown ED/EL modes, stray C1 bytes, bad sixel colours, unterminated strings, unhandled DECRQSS, RIS) log at
:debug. At:infoor above the output stream could otherwise set the log volume, a warning or error per sequence. - Breaking for
raxol_terminalusers, dead code removed:Raxol.Terminal.Colors;Raxol.Terminal.Commands.OSCHandler.ColorParser,.FontParser,.HyperlinkParserand.SelectionParser;Raxol.Terminal.ANSI.TerminalState.save_state/2,restore_state/1,apply_restored_data/3,count/1,empty?/1andclear_state/1; theRaxol.Terminal.ANSI.Behaviours.TerminalStatebehaviour; and theconfig :raxol, :terminal_state_imploverride that swapped it in, which is now ignored (the alternate-screen modes always save and restore through DECSC/DECRC). Nothing in the repository used them after this change.
Still bounded but costly: sixel work per image grows with the screen area it covers (about 105 ms for a full-screen image at 512x256), and a sixel-painted cell carries its own style.
New tests in
packages/raxol_terminal/test/raxol/terminal/emulator/(output_bounds_test.exs,cursor_save_bell_test.exs,screen_cost_bounds_test.exs) failed before, andtest/property/emulator_adversarial_property_test.exsfeeds random bytes, assembled escape sequences and a vocabulary of real ones (alternate screens, DECSC/DECRC, charsets, DECSTBM, SGR, OSC, DCS) throughEmulator.process_input/2, checking it never raises and keeps the screen's shape; on the code before these fixes it finds the alternate-screen crashes within a few dozen runs.- Control sequence parameters (CSI and DCS) keep xterm's limits while they arrive: 30 parameters (
Security: a session's terminal size is capped, and a network client's more tightly than the pilot's. Nothing bounded it: a screen buffer is a full width x height grid allocated up front at ~536 bytes a cell, so an SSH client's pty-req or window change, or a
raxol_startMCP call, asking for 100000x100000 asked the VM for ~5.4 TB, and the public SSH playground made that reachable from the network. The kept buffer is not the only cost: every keystroke and resize draws a full frame at the session's size, and that cost grows faster than the cell count (one styled full-screen frame measured 44 ms / +33 MB at 200x60, 0.9 s / +247 MB at 512x256, and 220 s / +1.4 GB at 4096x256).Raxol.Core.Defaults(raxol_core) now holds two ceilings, applied by the newRaxol.Core.Utils.Validation.clamp_terminal_size/3andvalidate_terminal_size/3:- The local ceiling,
max_terminal_width/0andmax_terminal_height/0(4096) andmax_terminal_cells/0(1,048,576), sized for the pilot's own terminal (two 8K panels side by side at a 6x12 px cell are 921,600 cells). It is the backstop every buffer and engine clamps to. - The remote ceiling,
max_remote_terminal_width/0(512),max_remote_terminal_height/0(256) andmax_remote_terminal_cells/0(131,072), for sizes a network client chooses. A full-screen terminal on a 4K display at 8x16 px cells is 480x135.
Per surface:
raxol_terminal:ScreenBuffer.new/3,ScreenBuffer.resize/3,ScreenBuffer.Core.new/3andCore.resize/3clamp to the local ceiling, so no path allocates a grid past it.Emulator.set_dimensions/3(andCoordinator.validate_dimensions/2, which allowed 1000x1000) andSafeEmulator.resize/3(10000x10000) now refuse past it with{:error, :dimensions_too_large}, andRaxol.Recording.Indexclamps a.castheader to it (it clamped to 1000x1000).- The rendering engine clamps its size to the local ceiling at start and on every
{:update_size, _}.Lifecycleclamps its:width/:heightoptions and the dispatcher clamps every resize event, so the app'sinit/1andupdate/2are told the size it is drawn at. These sizes are the pilot's own (the local terminal,Raxol.start_link/2, in-processRaxol.Headless), so they are clamped rather than refused. An oversized:width/:heightoption logs a warning, as does an oversized resize event once per crossing into the ceiling (not on every SIGWINCH while it stays there); on a TTY, though, the Terminal Driver silences the Logger for the session so nothing is drawn over the screen, so an oversized local terminal renders at the ceiling without a visible warning. - SSH clamps the client's pty-req and window-change sizes to the remote ceiling, or to the new
Raxol.SSH.Serveroptionmax_terminal_size: {columns, rows}(within the local ceiling, or the server refuses to start): a larger window renders at that size and the session stays up. A 0 column or row count takes the default 80x24, so no app is told a width of 0. A window change also resizes the session now:Raxol.SSH.Sessionsent it as a:windowevent, which onlyupdate/2saw, so the engine kept drawing at the pty-req size. - The
raxol_startMCP tool refuses a width or height past the remote ceiling with an error naming the range, and its input schema states the bounds. - An app's resize clause that raises, throws or exits is now caught, logged and the model kept, as
update/2failures on every other message already were; before, it crashed the dispatcher, and with it the session (for SSH, a client's chosen size could do this).
New tests over each constructor, the engine, a headless session, the MCP tool, the dispatcher and a real SSH client (pty-req and window change at 4294967295x4294967295, and at 0x0) failed before.
- The local ceiling,
raxol_core,raxol_symphony,raxol_earn: the secret scrub reaches the remaining third-party spawns and can be configured. Symphony's Codex runner and workspace hooks and the earn console's benchmarked agent packages inherited the node's whole environment; they now go throughRaxol.Core.ChildEnv.port_env/1too.XOCHI_AUTH_TOKENjoins the unset set.config :raxol_core, Raxol.Core.ChildEnvtakesextra_secrets:(for example a customWallets.Envenv_var:) andpass:. A raxol node started from a raxol shell or MCP server boots withfx:from its ownfx: [sleuth_api_key: ...]; its boot error points there first, and mentionspass: ["RAXOL_SLEUTH_API_KEY"]only as global, since it hands the key to every child the parent spawns (shells, MCP servers, Symphony runners, the earn bench). A caller'sfalseis passed to the port as Port's own unset marker instead of the string"false".raxol_agent,raxol_mcp: spawned processes no longer inherit raxol's secrets. Every child raxol starts for a model or a third party got the node's whole environment, so a prompt-injected command in a session holding thefxtool couldprintenv RAXOL_SLEUTH_API_KEY. The newRaxol.Core.ChildEnv.port_env/1unsetsRAXOL_SLEUTH_API_KEY,RAXOL_SHARE_SECRET,RAXOL_SESSION_STREAM_TOKEN,RAXOL_WALLET_KEYandRAXOL_ACP_AGENT_PRIVATE_KEYin the child unless the caller passes one explicitly, and is used by directive commands and MCP stdio servers, each with a test that a raxol secret in the node's environment does not reach the child. Provider API keys are not scrubbed, since a user's own commands may rely on them. This stops inheritance, not every read: a same-user process can still read the node's launch environment (ps -E,/proc/<pid>/environ).raxol_core: a malformedRaxol.Core.ChildEnvconfig raises by name.pass: [nil]or an atom name inpass:was accepted and matched nothing, and a string instead of a list, an atom inextra_secrets:or a map config crashed the spawn with an unrelated error. The config is now checked whenever a child is spawned: it must be a keyword list andpass:/extra_secrets:lists of variable-name strings, or the spawn raises anArgumentErrornamingconfig :raxol_core, Raxol.Core.ChildEnvand the key.raxol_core,raxol_symphony: Symphony'sgitcalls in a workspace no longer see raxol's secrets. The review contract'sgit diffand the evidence subject'sgit config/git rev-parseran in an implementer's workspace with the node's whole environment, so adiff.externalorcore.fsmonitorcommand the agent set in that repository's config could readRAXOL_SLEUTH_API_KEY. The newRaxol.Core.ChildEnv.cmd_env/1isport_env/1forSystem.cmd/3(nilunsets), and both modules use it; the diff also runs asgit -c core.fsmonitor=false -c diff.external= diff --no-ext-diff --no-textconv, which leaves a normal repository's output unchanged.raxol_agent_client_protocol'sTransport.Stdio.start_spawn/3cannot depend onraxol_core; its doc now says the agent inherits the whole environment and callers must scrub it through:env.Plugins now execute stateful callbacks in one stable process per loaded plugin.
PluginLifecyclestarts an isolatedPluginRuntimebelow a dynamic supervisor; initialization, events, filters, commands, hooks, state access, and timer messages are serialized there.self()remains valid for the loaded lifetime, unload removes the process and state, and resource accounting includes the stable runtime alongside auxiliary tasks.The root
Raxol.Pluginsfacade now uses the canonical stable plugin runtime. Loading throughRaxol.Plugins.Managerstarts the same supervisedPluginRuntimeused byRaxol.Core.Runtime.Plugins; root input, output, mouse, resize, cell, command, lifecycle, and timer callbacks execute serially there. Manager structs retain metadata and configuration but no callback-state snapshots or duplicate loaded-plugin map. Unload runs stateful cleanup in the runtime, removes the process, and reload creates a fresh process identity.Raxol.Headless.screenshot/1andget_buffer/1could return a blank frame, or one at the host terminal's size, right afterstart/2. Both drew the frame with oneRendering.Enginecall (:render_frame_sync) and read it back with a second (:get_buffer), so a cast that reached the engine in between ran first. One always could:Lifecycleinit casts{:set_rendering_engine, _}to the dispatcher, which may not have handled it whenstart/2returns, and the dispatcher answers it with an{:update_size, _}cast that replaces the engine's buffer with a blank one. A runner slow to schedule the dispatcher therefore got a blank first screenshot (examples_render_test.exsfailed "rendered a blank first frame" on the Windows leg), as could anyraxol_screenshotmade straight afterraxol_start. The new:render_frame_sync_buffercall renders and replies{:ok, buffer}with the frame it drew, andHeadlessreads its frames through it;:render_frame_syncstill replies:ok, so the gateway's per-event render barrier copies no buffer. Until that{:update_size, _}landed, the engine was also the size:io.columns/0and:io.rows/0(orstty size) reported, not the requested one, in every environment, so on a VM attached to a terminal an early frame came back at the terminal's size. In:agent,:liveview,:ssh,:telegramand:gatewaythe engine now starts at the:width/:heightoptions, the size the dispatcher corrected it to anyway. Newheadless_test.exstests failed before: one queues a resize at the engine between a screenshot's render and its read (the screenshot was blank), the other starts a session at 50x12 while the server's group leader answers like a 132x43 terminal (the frame was 132x43). The retry loops tests had added around the first frame, and the sleeps before a first screenshot, are gone.Raxol.Core.Metrics.MetricsCollectorlost records made on the same clock tick. Entries were keyed{type, name, monotonic_microseconds}, so a second record on the same tick overwrote the first. Windows' coarse clock made that routine (the Aggregator tests saw mean 30.0 for 10/20/30) and a tight loop hits it anywhere. The key's time slot is now{timestamp, unique_integer}, which still sorts by time. A new test records 500 values in a loop and gets all 500 back (it lost some before) (part of #1120).Raxol.Core.Metrics.AlertManagerapplies itsdefault_cooldownanddefault_severityoptions (#1120). A rule without its own:cooldownor:severitytook them from the module's built-in defaults (300 s,:warning), not from the options the manager was started with, sostart_link(default_cooldown: 0)still held every repeat alert for five minutes. Rules now take them from the running manager's options, andstart_link/1checks them as it already checkedcheck_interval: adefault_cooldownthat is not a non-negative integer number of seconds, or adefault_severityoutside:info,:warning,:errorand:critical, returns{:error, {:invalid_option, key, value}}(anilor"300"cooldown would otherwise let each rule fire once and never again, and a non-atom severity crashed the first notification). In the test support helper,setup_metrics_test/1started its unnamed manager withdefault_cooldown: :timer.seconds(5)(5000 s under the seconds unit) andmax_rules: 10, which no metrics server reads, although no rule is ever added to that manager; it now passes neither, and the aggregator no longer getsmax_ruleseither. The helper'screate_test_rule/4,create_test_alert/4andcreate_test_chart/3are removed: nothing called them and none could work (the alert helper called the registeredAlertManagername, which the setup never registers, with a:timer.minutes(5)cooldown of 300000 s; the rule helper senttime_window:where the Aggregator readswindow:; the chart helper passed a metric name string whereVisualizer.create_chart/3merges a map). A new test starts the manager withdefault_cooldown: 0, default_severity: :criticaland checks twice: it got one:warningalert before and gets two:criticalones now; the new rejection tests started a manager for every invalid value before.Raxol.Core.Metrics.Aggregatorno longer creates atoms fromgroup_bykeys, and takes atom keys (#1120). Its grouping fell back toString.to_atom(key)whenever a metric lacked the key as given, so every string key missing from some metric's tags minted an atom, and an atom key missing from a metric's tags raisedArgumentErrorand crashed the aggregator.AlertManagerhad its own lookup, which usedString.to_existing_atom/1but never tried an atom key's string form. Both now group throughMetricsCollector.tag_value/2, next tonormalize_tags/1: it tries the key as given, then the other form (String.to_existing_atom/1for a string,Atom.to_string/1for an atom), and givesnilwhen neither is present. New tests failed before: an Aggregator rule grouped by[:region]crashed on a metric tagged%{"region" => ...}or without the tag, a string key no tag used existed as an atom afterupdate_aggregation/1, and anAlertManagerrule grouped by[:component]over%{"component" => ...}tags saw one ungrouped mean (55.0 instead of 90.0).A
row/columndoblock with one child renders that child (#1114). A block with a single expression evaluates to that element, not a list, and therow/2andcolumn/2macros (with options, e.g.column style: %{gap: 1} do text("x") end, and theRaxol.View.Elementsforwards) stored it aschildrenunwrapped, so the layout engine drew an empty container. Every View container macro (row,column,box,flex,split,split_layout) and the function forms now wrap the block with the internal helperView.children_from_block/1(List.wrap/1:nilfrom anifwithoutelseis no children, a list stays as is). The macros call it fully qualified anduse Raxol.Core.Runtime.Application/use Raxol.Viewdo not import it, so an app's ownchildren_from_block/1still compiles.split_layout :dashboardwith one child raisedFunctionClauseError, andpanel do ... endignored its block entirely; both render the child now. The layout engine also lays out any container given one bare child map as a one-element list: before,:flexdropped it and:row/:columnraisedBadMapError. The newview_single_child_test.exstests failed before (empty output, or the exceptions above).raxol_terminal:Raxol.Terminal.Integration.Rendererreported every termbox call as failed outside test mode (#1105). It matched0from:termbox2_nif.tb_set_cursor/2,tb_clear/0andtb_present/0, which return:ok, somove_cursor/3(andIntegration.move_cursor/3) returned{:error, {:set_cursor_failed, :ok}},clear_screen/1returned{:error, {:clear_failed, :ok}}, andrender/1failed at its cursor step.set_title/2andset_config_value/3with:titlematched{:ok, "set"}, but the NIF returns the charlist{:ok, ~c"set"}, so the title was sent to the terminal yet never stored and an error was logged. Each now matches the NIF's actual success value, and the:termbox2_nif.tb_set_title/1andtb_set_position/2docs name it. Those three NIFs discard termbox's own status, so the renderer's{:set_cursor_failed, _},{:clear_failed, _}and{:present_failed, _}branches could never match and are removed. The Renderer docs now say thatclear_screen/1,move_cursor/3, showing the cursor and the cursor and present steps ofrender/1cannot report a termbox failure: they return:okeven when termbox rejects the call, for example beforeinit_terminal/0. The newintegration_renderer_test.exsruns the real-terminal branch against the loaded NIF; its three tests failed before. Two more fixes close #1119.Integration.move_cursor/3returned the renderer's:okin place of the state, although the README passes its result on toIntegration.clear/1; it now returns the state with the moved cursor. Hiding the cursor calledtb_set_cursor(-1, -1), which termbox clamps to (0, 0) and shows the cursor there, and showing it did nothing.set_cursor_visibility/2andset_config_value/3with:cursor_visiblenow hide throughtb_hide_cursor/0and show by placing the cursor at the cursor manager's position, andrender/1no longer places (and so re-shows) a hidden cursor.tb_hide_cursor/0returns termbox's status, so a rejected hide is logged as{:hide_cursor_failed, code}and leaves the state unchanged. Both new tests failed before (move_cursor/3returned:ok; the hide was recorded although termbox rejected it).box/panelwithout astyle:map, andcontainer/1, render instead of blanking the screen (#1122).Box.new/1(behindboxandpanel) defaultedstyleto[], and the cell renderer'sStyleProcessormerged it withMap.merge/2, sobox padding: 1 do ... end,box(border: :single),panel do ... endand any keyword style (style: [border: :double]) failed every frame with{:cell_rendering_error, %BadMapError{}}and nothing was drawn.Box.new/1now builds a style map (keyword and atom-list styles are normalized withStyleInheritance.ensure_style_map/1), and its documented top-level:border,:padding,:fg,:bg,:border_bgand:background_clipoptions are folded into it as shorthands, sobox(border: :single, bg: :blue)draws its frame and fill asdocs/core/RENDERING.mdshows (astyle:entry still wins).Flex.row/column/containernormalize their style the same way, sorow style: [gap: 2]keeps its gap. A style list entry that is neither a{key, value}pair nor an atom (e.g.style: ["bold"]) is skipped instead of failing the layout of the whole frame.StyleProcessorand the layout engine accept keyword ornilstyles on hand-built element maps.container/1emittedtype: :container, which the layout engine dropped with "Unknown or unhandled element type"; it now lays out as a column, gapless unlesscontainer(gap: n)orstyle: %{gap: n}asks for one, which also coversSelectList's:containertree.examples/advanced/color_system_demo.exrendered a blank screen before and renders now.Raxol.View.Elementsgains apanel/2macro, soUI.panel title: "x" do ... endcompiles (it called an undefinedpanel/2and rendered nothing).Raxol.Core.Renderer.Viewdeliberately has no such macro:use Raxol.Core.Runtime.Applicationimports View, and apps define their ownpanel/2helpers (as the cookbooks show), which an imported macro would shadow. TheRaxol.Core.Runtime.Applicationmoduledoc example, which used that form, now usespanel(title: ..., children: [...])and compiles and renders. The 12 newview_default_style_render_test.exsheadless tests all failed before (blank screen,BadMapError, the dropped:container, or the undefinedElements.panel/2).Every View DSL constructor draws what it is given (#1129). A table test over every public constructor of
Raxol.Core.Renderer.View,Raxol.View.ElementsandRaxol.View.Components(checked against their exports, so a new constructor without a row fails) renders each headlessly and checks its text and documented styles. It found:box/paneltitle:never drawn (the positioned box dropped it);border/2,border/3,border_wrap,wrap_with_borderand the*_borderhelpers,scroll/2,scroll_wrap,shadow/1,checkbox/2,progress,list,select,radio_group,textarea,tabs,modal,inputandComponents.button/1drawing nothing (no layout clause);block_border/simple_borderraising;checkbox(label: ...)andbutton(label: ...)(the component gallery's form) crashing the frame; an emptytext_input()in a row or column crashing the frame;Elements.label/2missing;text(style: [fg: :red])losing its keyword pairs;text/2's:align/:wrap,container/1's:scrollableandView.new/2's box never read; adoblock holding a nested list ([header, for ...]) failing the whole frame;split_pane's side-by-side divider drawn as a horizontal row over the right pane; the checked checkbox drawn as[[OK]]. The declarative widgets lay out throughRaxol.UI.Layout.ViewNodes;shadow/1takes:children.progresstreats avalueormaxthat is not a number as 0 (an empty bar), truncates a floatwidth, uses the default for any other non-integer, and clamps the bar so it and the label it draws (5%to100%) fit the space it is laid out in, so bad input no longer fails the frame or allocates a bar of any requested size; in a row or column it is sized by what it draws, not by its barwidth, so the bar keeps its length whatever the value. Alist/select/radio_group/tabsitem may also be a keyword list, drawn by its:label; one that is not a string, a{label, value}pair, a%{label: ...}map, a[label: ...]keyword list orString.Chars(a record, any other list) is not drawn (logged once per node at debug, without its contents), so records passed as items never put their fields on screen or in an error log, and anilitems/options/tabsdraws as empty. A box border of:bolddraws heavy glyphs (┏━┓),:block(fromblock_border/2) solid█, and:dashedits dashed set, instead of single lines;:simplestill draws single.border/2,border/3,border_wrapandwrap_with_bordertake every stylebox(border:)draws (:heavy,:dashed_fine,:asciiincluded), and still raise on an unknown one. Text scrolled past the frame's left or top edge draws the part still on screen (a wide character straddling column 0 is dropped) instead of vanishing;shadow/1paints only the strips its content does not cover, so the content keeps its own background; a box title of wide (CJK) characters advances by display width and is truncated by it.Elements.label/2andComponents.label/2take the content first;Raxol.Core.Renderer.Viewgets nolabel/2, becauseuse Raxol.Core.Runtime.ApplicationimportsViewand an app's ownlabel/2must keep compiling. A second test starts every TEA app inexamples/headlessly and requires a non-blank first frame.:dim,:reverseand:strikethroughreach every surface (#1132).Backends.transform_cells_for_update/1kept only:bold,:underlineand:italicof a cell's attributes, so tab and list selection highlights and everystyle: [:dim]drew plain on the terminal, SSH, LiveView and headless paths. Cell attributes now map to the buffer's names in one table (:dimbecomes the buffer's:faint); the ANSI renderer emits SGR 2, 7 and 9 for them after its existing codes, so styles without them produce the same bytes as before, and the LiveView bridge swaps colours for:reverseand halves the foreground for:faint, mixingcurrentColorfor text in the default colour (itsinheritis not a colour, socolor-mix()with it was dropped and the text drew at full strength).CheckboxandScrubberhad the same three-attribute allow-list for their own styles and now keep every text attribute. Telegram (plain<pre>text) and VS Code (ignores cells) still show none.A process component runs in one process for as long as it is in the view (#1132).
Rendering.Enginestarted a new, unnamedProcessComponentunderRaxol.DynamicSupervisoron every frame and never stopped one, so each process component leaked a process per frame and lost its state each frame. Each rendering engine now owns its components: it keys them by:id, or by position in the view, so same-module siblings no longer share the colliding default id"pc-<module>"; it reuses the live process across frames, sends changed props through the newProcessComponent.update_props/2(a component that exportsupdate_props(props, state)keeps the state it returns; any other is re-initialised from the new props withinit/1, as each frame did before, so a component like theFileListWidgetexample needs no props handler), stops components that leave the view, and stops all of them when it stops. A component that crashes draws a[<label>: crashed]placeholder for that frame and is started afresh on the next.ProcessComponentis nowrestart: :temporaryand stops when the engine that owns it exits, and two sessions of the same app no longer share components.tabledraws its documented:border(#1132). Nothing read the option: the renderer drew only text, and the layout reserved an empty row for a header separator it never drew and padded widths by rules that matched nothing drawn. A table now draws a frame in its border style (default:single) and a rule under the header,border: :nonedraws neither and takes no space for them, and the layout measures exactly what is drawn, so the next sibling starts just past the table.Raxol.View.Components.table/1now passes:borderon as well; it dropped the option, so its tables were always framed. A table that followed another child in the same box no longer blanks the frame (its layout replaced the accumulated siblings with an empty map).View.new/2's:positionand:z_indexare honoured (#1132). No container read them. A view withposition: {x, y}is now placed at that offset from the content origin of the container that lays it out and takes no space in its flow; a higherz_indexdraws over a lower one where views overlap, in any child order.Smaller render-path fixes (#1132).
BarChartbuilt a decreasing0..-1range for a bar with no full blocks, which Elixir deprecates and warned about at render; its ranges now step explicitly.examples/advanced/commands.exscalled the undefinedDirective.spawn/1and waited for a message the runtime never sends; it usesDirective.spawn_task/1and matches{:command_result, ...}.examples/components/accessibility/accessibility_demo.exdrew nothing (its struct had no:form_dataand its root node type named no module) and listened for key messages the runtime does not send; it renders, and Tab/Shift+Tab, Enter and Space act through{:focus_changed, old, new}and%Event{}keys.examples_render_test.exsnow also fails an example that calls an undefined function or module, which the compiler only warns about, and drops its known-blank list.raxol_terminal: quitting a Raxol app undermix runexited 1 and left the terminal in the alternate screen (#1115). On a TTY the Terminal Driver reads keys by tracing OTP's prim_tty reader (:user_drv_reader), butTermboxLifecycle.cleanup_terminal/1sent that reader a:shutdownexit on quit.user_drvtreats any abnormal reader exit as a crash and stops, taking every group leader with it, so the restore that followed (mouse and focus modes off, autowrap, cursor, leaving the alternate screen) raised:terminated. The Driver died mid-cleanup, andGenServer.stop/3carried the exit through the Lifecycle to the process that calledRaxol.start_link/2.qinexamples/getting_started/counter.exsexited 1 with that stack trace. Cleanup now only stops tracing the reader, which stays withuser_drv, so the restore is written and the app exits 0. A newtermbox_lifecycle_test.exstest failed before the fix (the reader was dead after cleanup).raxol_terminal:Raxol.Terminal.Integration.write/2,handle_input/2andupdate_config/2exited:noproc(#1094). Each calledRaxol.Terminal.IO.IOServerby module name, and nothing started one under that name, so the README exampleIntegration.write(Integration.init(), "Hello, World!")exited.Integration.State.update/2had the same call behind arescue, which does not catch exits, andState.render/1called the equally unstartedRenderServerwhenever a window was active. Those calls are gone along with both servers (see Removed).write/2now returns the state, as the README documents; before, it returned""even with a server running, because IOServer never produced any output.State.render/1returns the state without looking up (and lazily starting) the window manager, andIntegration.Main.write/2replies:oklike the process's other calls instead of{:ok, ""}. The newintegration_api_test.exscallswrite/2,handle_input/2andupdate_config/2with no servers running; all three exited:noprocbefore.Lazily started singletons no longer die with the process that first called them (#1094).
Raxol.Core.Utils.GenServerHelpers.ensure_started/2(raxol_core) ran astart_linkfor all ten of its callers, so the server was linked to whichever process called first: a render process, an SSH or LiveView session, a Task, an ExUnit test. When that process exited abnormally the server went with it, taking all its state (RBAC roles,Storedata, the user context, updater state, the i18n locale and its:raxol_i18ntable, the colour theme, gestures, UX features, protocols), and the next call quietly started an empty one. A crash in the server also killed that first caller. And two first callers racing each other both ranstart_fun, so the loser got{:error, {:already_started, pid}}and raisedMatchError.ensure_started/2now treats{:already_started, _}as success, and its documented contract is thatstart_funstarts the process unlinked, asRaxol.Animation.StateManageralready does. All ten callers now useGenServer.start/3orAgent.start/2:Raxol.Protocols,Raxol.RBAC,Raxol.Animation.Gestures,Raxol.Core.ColorSystem,Raxol.Core.UXRefinement,Raxol.Security.UserContext,Raxol.Style.Colors.System,Raxol.System.Updater.State,Raxol.UI.State.Storeandraxol_core'sRaxol.Core.I18n. The two colour-system modules, which start oneColorSystemServerunder one name, both go through the tolerant path.Raxol.RBAC.start_link/1stays linked, for use in a supervision tree. New tests kill the first caller ofRaxol.RBACand ofStoreand check that the server keeps its pid and its state (before the fix it went down with:killed). A third test registers the name betweenensure_started/2'swhereisand its own start (before the fix this raisedMatchError). The colour-system and accessibility tests now stop the lazily startedColorSystemServerandI18nServeraround each test, because those servers now outlive the test that started them.Quitting a full-screen app: Ctrl+C opened the BEAM BREAK menu, shutdown logs printed after quit, and a child crashing in
terminate/2aborted the teardown (#1128). (1)raxol_terminal: the Terminal Driver ranStty.raw!/0beforestart_stdin_reader/1, whose prim_tty reinit writes prim_tty's own raw mode, the termios saved at VM boot minus ICANON and ECHO. ISIG came back on, so ^C raised SIGINT and the node hung in the BREAK menu with the terminal still in the alternate screen. Raw mode is now applied after the reinit, so ^C reaches the app as%{key: :char, char: "c", ctrl: true}; themix raxol.newtemplates and the examples already bind it toDirective.stop(), and it now quits with exit 0 and the terminal restored. The first Ctrl+C still goes to the app, sinceupdate/2has no "unhandled" result to fall back on. A second one within a second of it, with no other key between them, makes the Driver send the Lifecycle:quit_runtime, the messageDirective.stop()sends, so an app that does not bind Ctrl+C can still be quit through the normal teardown. prim_tty also writes its raw mode again on SIGCONT (the node stopped and continued, as withkill -STOPandkill -CONT), which turned ISIG back on for the rest of the session. The Driver now takes-isigback on SIGCONT with the InlineDriver's verify-then-assert loop, moved toRaxol.Terminal.Driver.IsigGuardso both drivers share it. (2) The Driver's cleanup set the Logger level to:debuginstead of the level it found at init; it now restores that level. The Lifecycle stops the Driver first, so everything the rest of its teardown logged (about 7[info]/[debug]lines) printed on the restored terminal. When the session ran with Logger at:none,Lifecycle.terminate/2now drops log events until the teardown is done (Shutdown.quietly/3). It drops only the events of the Lifecycle and the children it stops, through a primary:loggerfilter of its own per teardown. A separate process owns the filter and removes it when the teardown returns or the Lifecycle dies, so a Lifecycle killed mid-teardown cannot leave logging off, and one teardown finishing cannot lift another's silence. The filter id is an atom, which is never collected, so a teardown takes the first of:raxol_lifecycle_teardown_0,_1, ... not already installed: there are only as many ids as there were teardowns at once, not one more per session. Only a Lifecycle that owns a Terminal Driver goes silent, sinceTERMINAL_LOG_LEVEL=nonealso leaves Logger at:none, SSH and LiveView sessions included. The Driver's cleanup now restores the level in anafter, so a terminal write that raises (:terminatedonce stdio has gone) no longer leaves logging off for the node. A Driver killed on the stop timeout, or crashing before its cleanup, never restores it, so the Lifecycle notes the level found before it started the Driver and restores it after the teardown, before the failure is reported. (3)Shutdown.stop_process/2usedrescue, which does not catch the exit fromGenServer.stop/3when the process crashes interminate/2, so the Lifecycle's teardown stopped at that child and the Lifecycle died with the crash, taking down itsstart_linkcaller. The exit is now caught and the teardown continues. A child that overruns the stop timeout is killed, sinceGenServer.stop/3leaves it running. Each failure is logged with the child's label, pid and reason once the teardown is done and the terminal restored, and a quit that would have ended:normalends{:shutdown, {:teardown_failed, failures}}instead, somix runexits non-zero. New tests failed before the fix: a real-pty-isigtest for the full-screen Driver (^C never decoded,isig_off=false), the Logger level aftercleanup_terminal/1(:none), the Lifecycle's teardown log (six lines), a Lifecycle whose driver raises interminate/2(the linked test process was killed), real-pty tests for a double ^C (both reached the app) and for ^C after SIGSTOP/SIGCONT (isig_off=false, no output),quietly/3tests for a killed teardown (its filter stayed installed), two concurrent teardowns (the first to finish lifted the other's silence) and a bystander process (its events were dropped too), and Lifecycle tests for a driver that fails to stop on a TTY (exit:normal), one that hangs interminate/2(still running after the teardown), and drivers that crash before restoring the level or are killed on the stop timeout (Logger left at:noneand the failure not reported);quietly/3run 50 times in a row (50 filter ids), a teardown with no Driver at:none(silenced), andcleanup_terminal/1raising (level left at:none).Raxol.Core.Events.EventManageris supervised, and starts under its name as a bare child (#1094). No startup mode started it, yetSubscription.events/1(andsubscribe_to_events/1) in a TEA app'ssubscribe/1, the component{:subscribe, events}command,KeyboardShortcuts.init/0,KeyboardNavigator.init/0andPlugins.API.subscribeall call it by name, so each exited:noproc. Theraxol_coreREADME's bareRaxol.Core.Events.EventManagerchild did not help, becauseBaseManagerregisters no name unless given one.EventManager.start_link/1now registers under its module name unless:nameis passed, andRaxol.Applicationstarts it in:full,:mcpand:minimal(the raxol.io gallery runs TEA apps in:minimal). In:testit is a:transientchild replacingtest_helper.exs's unsupervised start, since tests stop it withEventManager.cleanup/0and start their own. A newraxol_coretest starts it as a bare child and receives a subscribed event (the subscribe exited:noprocbefore); booting each mode and callingSubscription.start(Subscription.events([:x]), %{pid: self()})returned{:ok, {:events, _}}instead of exiting.Raxol.set_theme/1changes the theme Raxol renders with (#1104). It wrote the:themeapplication env key, which onlyRaxol.current_theme/0read;Raxol.Style, the modal renderer,Theming.SelectorandAccessibility.ThemeIntegrationreadRaxol.UI.Theming.Theme.current/0(the:current_themekey), so the new theme never reached a render.set_theme/1now goes throughTheme.apply_theme/1andcurrent_theme/0returnsTheme.current/0. It also accepts the id of a registered theme.Theme.apply_theme/1given an id that is not registered (other than:default) now returns{:error, :theme_not_found}and leaves the theme alone; before, it silently applied the default theme. Themix raxol.newconfig template now names:current_theme; aconfig :raxol, :theme, ...line is no longer read. The newtest/raxol_test.exstests failed before the fix (Theme.current/0still returned the default theme; an unknown id returned:ok). The theme is node-global (one:current_themefor every SSH and LiveView session on the node), andset_theme/1takes aThemestruct or an atom id: a string or plain map raisesFunctionClauseError. Becauseset_theme/1now takes effect,Raxol.set_accessibility/1no longer calls it unconditionally (it reset the theme to the default on any call without:high_contrast, andhigh_contrast: trueapplied the plain dark theme): it touches the theme only when:high_contrastis given,trueapplyingTheme.adjust_for_high_contrast/1to the current theme andfalserestoring the theme it replaced (unless another was set meanwhile).Theme.create_high_contrast_variant/1(behindadjust_for_high_contrast/1) no longer raisesBadMapErroron a theme withnilvariants, which everyTheme.new/1anddefault_theme/0theme has.Rendering.Engine, which draws every TEA frame (terminal, SSH, LiveView, headless), looked its theme up by the dispatcher's theme id and never readTheme.current/0, so the theme still did not reach a TEA app: while that id is the default it now rendersTheme.current/0(which, before anyset_theme/1, is the theme registered under the default id, as the Engine rendered before, else the built-in default), and an id chosen by the app or the user's preferences renders the theme registered under it (Theme.current/0if none is).high_contrast: trueraisedFunctionClauseErroron a theme with a colour that has no RGB components (a named colour such as:green, a 256-colour index, a bad hex);create_high_contrast_variant/1now keeps such colours as they are. Andhigh_contrast: trueafter a theme was set while it was on returned:okwithout touching the new theme; it now raises that theme's contrast.Choosing a theme in
Raxol.UI.Theming.Selectorcrashed (#1118). A click on a theme in the open list calledTheme.apply_theme/1with the theme'sname, a display string such as"default", butapply_theme/1takes a theme struct or a registered id, so the click raisedFunctionClauseError. It now applies the listed theme struct itself. New tests click a theme through the selector'shandle_event/3and checkTheme.current/0, the collapsed list and the:on_selectcallback (both raised before).Raxol.UI.Theming.Selector's keyboard selects a theme. The open list's footer saysEnter: Select, but Enter and Space only kept the highlight where it was: neither applied the theme nor closed the list, and on the closed selector no key opened it, so a theme could be chosen only with a click. On the closed selector Enter or Space now opens the list; in the open list they apply the highlighted theme, pass its name to:on_selectand close the list, as a click does. On an empty list they only close it. Escape still closes without changing the theme. The new keyboard tests failed before (the list never opened).Subscription.events/1delivers to the app'supdate/2(#1103). EventManager sends each match to the subscribing Dispatcher as{:event, event_type, event_data}, and the Dispatcher had no clause for it: it logged "Dispatcher received unhandled info message" and dropped the event, so a TEA app whosesubscribe/1returnedSubscription.events/1(orsubscribe_to_events/1) never saw one. The Dispatcher now passes the message toupdate/2unchanged. The docs now say which events arrive: only those dispatched throughEventManager(theme, accessibility and keyboard-navigation events). Terminal input still reachesupdate/2as%Event{}structs and is not routed throughEventManager, so the old[:key_press, :mouse_click]example is gone. They also say thatEventManageris node-global: every session on the node whose app subscribes to a type gets every event of it, whichever session dispatched it, so:screen_reader_announcement,:activateand:dismisscarry other sessions' announcement text and focus ids. A new dispatcher test dispatches throughEventManagerand asserts thatupdate/2gets the event, then stops getting it once the model drops the subscription. It timed out before the fix. Only an event type that a running subscription lists is passed on: an event still queued when the model drops its subscription, or an{:event, type, data}sent by any other process, is dropped with a debug log. Declarations that list the same type share oneEventManagersubscription, so each event reachesupdate/2once; two overlapping declarations used to deliver it twice. Dropping a subscription whileEventManageris down no longer crashes the Dispatcher (EventManager.unsubscribe/1exited:noproc). Known limitation: delivery stops ifEventManagerrestarts, because the subscription is not re-established. Tests for the stale, forged and overlapping cases and the:noproccrash failed before.ComponentManagerdelivers subscribed events to its components (#1121). A component that returned{:command, {:subscribe, types}}got anEventManagersubscription for the manager, but the manager had no clause for the{:event, type, data}messagesEventManagersends, so the first matching event crashed it withFunctionClauseError. It could not have routed them either: the message carries no subscription ref, and every subscribe started its ownEventManagersubscription, so overlapping subscriptions got every event once per subscription. The manager now holds oneEventManagersubscription per event type with the set of components that list it, started when the first component subscribes and stopped when the last one leaves, and passes each event once to every listing component'supdate/2as{:event, type, data}, through the same path asComponentManager.update/2(returned commands are processed). An event for a type no component lists is dropped with a debug log.update/2may return{:ok, new_state}, asRaxol.UI.Components.Base.Componentdeclares, for every message the manager passes it: the manager answered it with:invalid_component_returnonComponentManager.update/2calls, dropped it for scheduled messages, and stored:okas the component's state for a broadcast. An invalid return from a broadcast or scheduled message now keeps the component's state and logs the component and the message kind but not the data (a broadcast crashed the manager withMatchError; a scheduled one was dropped silently). A component whoseupdate/2raises or returns anything else keeps its state and the other subscribers still get the event; the warning names the component and the event type but not the event data, which can be another session's (the crash warning quoted it). Contract change:{:unsubscribe, sub_id}is replaced by{:unsubscribe, event_types}, because the component was never told its subscription id; the old form now logs "Unknown component command". Unmounting removes the component from every type. Subscribing whileEventManageris down, not answering in time, or stopping during the call logs a warning and leaves the component unsubscribed from that type instead of crashing the manager and every mounted component with it (the{:ok, sub_id} = Subscription.start(...)match exited:noproc; any exit from the call is now caught). Commands returned frommount/1orhandle_event/3lost their effect on the manager's state, so a subscription made there was started but never recorded; they now take effect, anddispatch_event/1reads each component's current state, so a{:broadcast, _}from one component'shandle_event/3is not overwritten when a later component handles the same event. The moduledoc states the contract, including the known limitation that subscriptions are not re-established ifEventManagerrestarts. The new tests failed before the fix: the delivery, mount, unsubscribe, unmount and forged-event tests crashed the manager withFunctionClauseError, overlapping and repeated subscriptions madeEventManagersend two copies of one event, thehandle_event/3subscription never delivered, an{:ok, state}reply was rejected (and stored as the state:okfor a broadcast), a failing component's warning carried the event data (and a bad return was not logged), a component handling the event after the broadcasting one lost the broadcast, and subscribing withEventManagerdown exited:noproc, orshutdownwhen it stopped during the call.Raxol.UI.State.Store,Raxol.Security.UserContextandRaxol.Core.ColorSystemreach their servers again (#1080). Their servers useBaseManager, whosestart_link/1registers no name unless it is given one, yet each public API calls the server as__MODULE__. The lazy starts (ensure_server_started/0) passed no name,ServerRegistrystarted the UI server as:ui_state_server, andRaxol.Core.ColorSystem(behindRaxol.Core.get_theme/0,set_theme/1and core init) calledColorSystemServerwithout starting it at all, so every call exited:noproc.StateManagementServer,ContextServerandColorSystemServernow register under their module name unless:nameis passed,ServerRegistryuses that name, andCore.ColorSystemstarts the server on first use. New tests call each public API with no server running (each exited:noprocbefore) and check thatStoreuses a supervised server instead of starting a second one.Store.get_state/0returns the whole store (#1080). The default empty path reachedget_in(store, []), which raisesFunctionClauseError, so asking for the whole store crashed the store server. An empty path now returns the store; the first-useStoretest caught it once the naming fix let the call through.Raxol.Profiler,DevHints,mix raxol.bench.advancedand the recovery context manager reach their servers (#1080). The same unnamedBaseManagerstart:Raxol.Profiler.enable/0and the:performance_monitoringchild startedRaxol.Performance.Profilerunnamed, sodisable/0,report/1and every profile run exited:noproc; the dev child{DevHints, []}leftDevHints.enabled?/0false, so hints andstats/0were off;mix raxol.bench.advancedcalledSuiteRegistry.start_link()and then the registry by name;RecoverySupervisorstartedContextManagerunnamed and then called it by name. Each server now registers under its module name unless:nameis passed. A new test per server starts it the way production does and calls the API (each exited:noproc, or reported hints off, before). #1107 has since removedContextManager, and its test with it, because nothing else used it (see Removed).raxol_core:ErrorRecoveryworks in minimal mode (#1080).RAXOL_MODE=minimalstarts{Raxol.Core.ErrorRecovery, [mode: :minimal]}with no name, so everywith_circuit_breaker/3call exited:noproc.ErrorRecoverynow registers under its module name unless:nameis passed; a new test starts it that way and runs a circuit breaker (it exited:noprocbefore).raxol_terminal: the terminal cache andSync.Systemreach their servers (#1080).Raxol.Terminal.SupervisorstartsRaxol.Terminal.Cache.Systemwith options and no name, and the:terminal_syncfeature starts{Raxol.Terminal.Sync.System, []}, soAnimationCache,ScrollManagerand theSync.SystemAPI exited:noproc. Both now register under their module name unless:nameis passed; new tests start each without a name and use the API (each exited:noprocbefore).The UI state server drops a dead component's hook state (#1080).
StateManagementServer's:DOWNhandler deleted the pid fromcomponent_idsand then looked the pid up there to find which hook states to purge, so the purge never ran andhook_stateskept every dead component's entries. It now reads the component id first; a new test kills a component and assertsget_hook_state/2returnsnil(it returned the old value before).UserContext.ContextServerforgets a dead caller's monitor (#1080).monitorsis keyed by pid, but the:DOWNhandler deleted it by monitor ref, so one entry stayed for every process that ever set a user or context. It now deletes by pid; a new test lets a caller exit and assertsmonitorsis empty (it still held the dead pid before).raxol_core: a high-contrast preference change reaches the color system (#1080).Raxol.Core.Accessibility.PreferenceManager.maybe_notify_color_system/2checkedProcess.whereis(Raxol.Style.Colors.System.Server), a module that does not exist, so it never notified the runningColorSystemServer. It now looks upColorSystemServer; a new test flips:high_contrastand asserts the server'sget_high_contrast/0follows (it stayedfalsebefore).Raxol.Animation.Gesturesexited:noprocon every call, and its server kept entries for dead processes.GestureServer's API callsGenServer.call(__MODULE__, ...), but its BaseManagerstart_link/1registered no name unless one was passed, soGestures.ensure_server_started/0started an unnamed server andGestures.init()exited:noproc.GestureServer.start_link/1now registersGestureServerby default (a:namestill overrides it). Behind that, onlyinit_gestures,register_handlerandtouch_downmonitored the caller, so a process whose first call wastouch_move,touch_uporupdate_animationsgot an entry nothing removed when it died. Every write now goes through one path that monitors the pid. The new tests failed before each fix:Gestures.init()exited:noproc, then a killedtouch_move-only process kept its entry (#1080).Raxol.Performance.CycleProfilerkept dead subscribers and notified duplicates twice.subscribe/1appended the caller to a list with no monitor and no dedup, so a subscriber that exited stayed in the list for the profiler's lifetime, and a process that subscribed twice got each{:slow_cycle, _}twice. A repeated subscribe is now a no-op, and each subscriber is monitored and dropped on:DOWN. Both new tests failed before the fix (#1080).Raxol.Style.Colors.HotReloadkept dead subscribers, and oneunsubscribe/0did not undo twosubscribe/0calls. Subscribers were a list with no monitor, so an exited subscriber stayed until the server stopped, and a duplicate subscribe added a second entry: each change arrived twice, and after oneunsubscribe/0the process still got notifications. Subscribes are deduplicated, each subscriber is monitored and dropped on:DOWN, andunsubscribe/0removes the monitor. Both new tests failed before the fix; the "multiple subscribers" test now uses a second process instead of subscribing the same one twice (#1080).raxol_core:EventManagerkept pid handlers after the pid died.register_handler/3accepts a pid target and:DOWNalready removed a dead pid's handler rows, but onlysubscribe/2ever monitored anyone, so the handler rows of a pid that never subscribed stayed in the ETS bag for the manager's lifetime (get_handlers/0still listed a killed pid). Pid handlers are now monitored too, with one monitor per pid instead of one persubscribe/2call (#1080).A headless session whose app exits now stops its tool synchronizer.
Raxol.Headlessdropped the session on the lifecycle's:DOWNwithout stopping itsRaxol.MCP.ToolSynchronizer, which is linked to Headless rather than to the app, so the synchronizer kept its[:raxol, :runtime, :view_tree_updated]telemetry handler and the session's MCP tools andraxol://session/<id>/*resources for a sessionlist/0no longer reported. The:DOWNpath now stops it the waystop/1does; a new test kills the lifecycle and asserts the synchronizer exits and its handler and resources are gone (it timed out waiting for the synchronizer before).Security: anonymous code execution through the playground REPL demo (#1045). The HTTPS gallery serves every catalog entry at
/demos/:demowith no auth, andRaxol.Playground.Demos.ReplDemoevaluated whatever a browser typed. Two fixes:Raxol.REPL.Sandboxnow refusesimport,alias,requireanduseat:standardand:strict(import System; cmd("id", [])andalias :os, as: Enum; Enum.cmd(~c"id")both returned:okbefore, because every other clause decides safety from a module name and those four forms decide which module a name reaches), and the demo no longer evaluates at all unless the deployment opts in. The opt-in is now a single predicate,Raxol.Core.Boundary.Evaluation.exposed?/0(RAXOL_REPL_EXPOSED=trueorconfig :raxol_core, :repl_exposed, true), read by both halves of the rule a node that evaluates submitted code does not hold signing keys: the demo before it evaluates, andRaxol.Payments.Deployment.assert_signing_isolated!/0before a signing node boots. They previously read different application keys (:raxoland:raxol_payments), so configuring the demo's key turned anonymous evaluation on while a co-located signing node still booted.config :raxol_payments, :repl_exposedis still honoured by the boot assertion so an existing signing deployment does not silently start booting, but it does not enable the demo.mix raxol.replpasseslocal_operator: trueand keeps working without the flag, which is accepted only atenvironment: :terminal, so a served app cannot claim it over SSH or LiveView. Sandboxed code that names its modules in full is unaffected;:none, the local-terminal level, is unchanged.Raxol.REPL.Sandboxis a mitigation, not a trust boundary: at:strictthe bare-name capture forms ofapplyandspawnstill pass the checker, so the deployment flag is what keeps an anonymous caller away from the evaluator.Cell text is blanked, not deleted, at the terminal emitters.
Raxol.Terminal.RendererandRaxol.Core.Rendererdropped a cell they could not emit, which pulled every cell to its right one column left for the rest of the row (box borders, table columns, the approval line) with noCSI 2 Kto repaint it. A disallowed cell now becomes a space through the newRaxol.Core.Boundary.TermText.sanitize_cell/1, whichRaxol.Core.Runtime.Rendering.Backends.sanitize_char/1also delegates to, so the cell-write boundary and the emitters share one convention and C1 controls and raw invalid-UTF-8 bytes are covered at both. A 200x60 frame renders in 1.6 ms against 4.9 ms for the per-cell text sanitize it replaces (1.3 ms with no confinement at all).Bidi overrides and other format characters are stripped at every sink.
TermTextstripped only C0/DEL/C1/ESC, soU+202A-U+202E,U+2066-U+2069,U+200B,U+200E,U+200F,U+00AD,U+2028,U+2029,U+FEFFand theU+E0000-U+E007Ftag characters reached the terminal through the transcript tree while the app chrome stripped them: an approval line could be reversed (Trojan Source, CWE-451) on the same screen as a footer that could not.TermText.strip_codepoint?/2is now the one deny set, andRaxol.Harness.Surface.ViewTextuses it instead of its own list. The wider set costs nothing on ordinary text: printable ASCII short-circuits before the scanner consults it, so 200 lines of 1100 plain bytes sanitize in 2.95 ms against 2.96 ms for the narrower set it replaces.OSC 8 links are restricted to
http,httpsandmailto. A Markdown link an LLM wrote reached the hyperlink sinks with its scheme unchecked, so afile:///etc/hoststarget, or anx-apple.systempreferences:one, became clickable.TermText.sanitize_url/1drops any other scheme (and a scheme-less target) at the layout link attribute and at both renderers; the label text still renders.raxol_terminal: an OSC 8 param could replace the URL.URI.encode/1leaves;and:intact, and both are structural inESC ] 8 ; params ; URI ST, so a tooltip containing either split the param list and a terminal could parse a different URI. Params are now encoded down to the unreserved set.report_progress/2also clamps out-of-range values instead of emitting an OSC 9;4 with an empty percentage.The hyperlink plugin no longer strips the stream it is scanning.
Raxol.Plugins.HyperlinkPlugin.handle_output/2ran an untrusted-text sanitize over the whole output chunk, so any chunk mentioning a URL lost its SGR runs, cursor moves and CRs. Only the captured URL is confined now.raxol_terminal:CSI ? 80 h,CSI 80 handCSI 132 hwiped the screen.Raxol.Terminal.Modes.Types.ModeTypesregistered DEC-private 80 and standard 80/132 as column-width modes, and onceModeProcessorstarted dispatching from the registry every one of them reached a handler that replaced both screen buffers with empty ones. None is a real mode (CSI ? 80is DECSDM in xterm; standard 80/132 do not exist). The three rows are removed, soModeTypes.lookup_private(80),lookup_standard(80)andlookup_standard(132)returnniland the sequences are ignored.CSI ? 3 h/l(DECCOLM) remains the only column-width switch;:deccolm_80survives only as the name of its reset state.raxol_terminal: a malformedCSI h/CSI lparameter raised.\e[?1;h(empty slot) and\e[?4:3h(colon subparameter) reachedString.to_integer/1and crashed the emulator. Non-integer parameters are now skipped and the well-formed ones still apply.raxol_terminal:CSI ? 1049 hno longer filled theCSI s/CSI uslot. xterm defines 1049 as save-as-DECSC / restore-as-DECRC; the save and restore now live inRaxol.Terminal.Modes.Handlers.ScreenBufferHandler, so\e[5;10H\e[?1049h\e[1;1H\e[ureturns the cursor to row 5, column 10 again.Dev endpoint: a moving probed port forced recompiles and broke
--no-compileboots.Raxol.Endpointrecorded the entireRaxol.Endpointconfig as compile-time environment, including theport:thatconfig/dev.exsprobes for a free socket on every config evaluation. Whenever the probe landed on a different port, Mix marked the module stale andmix compileaborted with a compile-environment mismatch. Only:tidewave_project_evalis recorded now.dev_bind_loopback?also treats an IPv4-mapped loopback bind (::ffff:127.0.0.1) as loopback, matching Tidewave's ownis_local?/1, so that bind no longer silently unmounts MCP.Playground listener: three "hardened" limits were looser than Cowboy's own defaults.
request_timeoutwas 10_000 against Cowboy's 5_000,max_header_value_length8_192 against 4_096, andmax_reset_stream_rate{100, 10_000}against{10, 10_000}, which is the CVE-2023-44487 Rapid Reset guard. Every limit is now at or below the upstream default and carries its rationale.inactivity_timeoutno longer cuts idle long-polls (it was 10_000, exactly the transport'swindow_ms, so an idle poll died with no response), andmax_connectionsis 1_000 to match the fly.io proxy's per-machine connection hard limit instead of stopping at 500.web/mix.locknow resolves cowboy 2.19.0 / ranch 2.3.0 / phoenix 1.8.14 / phoenix_live_view 1.2.12, so the deployed playground runs the same adapter the rest of the repo is locked to.raxol_terminal: a carriage return cost three cursor round trips with debug logging off.Raxol.Terminal.ControlCodes.handle_cr/1hoistedCursor.Manager.get_position/1out of itsLogger.debugcalls, so withemulator.cursorheld as a pid every CR byte made three synchronousGenServer.calls (seven when it resolved a pending wrap) at any log level. The lookups now sit inside the macro argument, leaving the one callmove_to/3actually needs (three across a wrap).raxol_terminal: the CSI intermediate parser logged the rest of the input chunk. Its debug line inspected the whole remaining binary, which can carry an OSC 52 clipboard payload; it now reports the byte count.Raxol.Terminal.Input.CoreHandlerhad the same shape and got the same treatment.TERMINAL_LOG_LEVEL=warnno longer refuses to boot. The release config maps that variable onto the node-wideLoggerlevel, and its allowlist omitted the"warn"spelling that Logger still accepts, so a previously harmless setting raised anArgumentErrorat startup.Raxol.Core.ConnectionPool.transaction/3leaked every connection and serialized the pool. The callback ran inside the pool GenServer and the statedo_checkin/2returned was thrown away, so each successful transaction left its connection marked busy (a size-1 pool refused the second transaction with{:error, :timeout}), and one slow callback blocked every other checkout andstats/1call. The callback now runs in the calling process between acheckout/2and acheckin/2, and the pool monitors whoever holds a connection, so a holder that exits without checking in gives it back.raxol_gateway: a failed Discord WebSocket upgrade leaked its TCP socket. WhenMint.WebSocket.upgrade/4returned{:error, conn, reason},MintTransport.connect/2dropped the openconn, and because the gateway socket reconnects on error, it leaked one socket per attempt. The connection is now closed withMint.HTTP.close/1before the error returns.raxol_gateway:SessionRouter.route/3reported:okfor events cast into a dead session. The router tracks sessions by pid and learns of a death from a:DOWNthat queues like any other message, so aroute/3call read ahead of that:DOWNfound the dead pid, cast the event into it and replied:ok: the event was gone and the caller was told it had been accepted. A tracked pid that is no longer alive is now dropped on the spot (its queued:DOWNtaken for the:downtelemetry, the monitor flushed) and replaced through the normal start path, so the event reaches a fresh session orroute/3returns the refusal. The per-key cooldown still applies, so a restart within:cooldown_ms(5 s by default) of the dead session's start is{:error, :rate_limited}rather than a crash-loop bypass.start_session/2gets the same treatment instead of returning the dead pid.handoff/3had the same flaw, returning a dead destination pid, and it started destinations with no:max_sessionsor cooldown check, so withmax_sessions: 1a handoff opened a second session. It now starts the destination through the same gates and returns{:error, :max_sessions}or{:error, :rate_limited}like any other start, leaving the source session as it was.CI: a docs-only pull request could never merge.
CI Status, the only required check onmaster, lives inci-unified.yml, whosepull_requesttrigger hadpaths-ignoreforweb/**,docs/**,assets/**,**.mdand.github/**. A pull request touching only those paths never started the workflow, so the check never reported and the PR satBLOCKEDwith every other check green (#1077). The filter now lives in achangesjob: a docs-only diff skipssetupand everything after it, andCI Statuspasses unless a job that did run failed or was cancelled. The diff base is also the event'spull_request.base.shainstead oforigin/<base_ref>, which madegit diffexit 128 when the base branch was deleted during a retarget.Raxol.Core.ConnectionPooloverflow connections were permanent.create_overflow_connection/2added each overflow connection toconnections.overflowand nothing removed it; on checkin it joinedavailable, so the pool grew topool_size + max_overflowfor good, the overflow gate never admitted another connection, anddisconnect_fnwas never called on one. Overflow connections are now transient, as in poolboy: checking one in (or its holder exiting) disconnects it and frees its overflow slot (a size-1 pool withmax_overflow: 1reportedavailable: 2, overflow: 1after two checkins; it now reportsavailable: 1, overflow: 0).raxol_gateway:Pairingkept every unconfirmed code and every requester forever.request_code/2added topendingandlast_request, but onlyconfirm/2removed a code and nothing ever removed alast_requestentry. Every Discord, Telegram or email sender who asked for a code and never confirmed it stayed in both maps for the life of the gateway (21 codes and 21 requesters after 21 requests with a zero TTL and cooldown). Each request now sweeps cooldowns that have run out, and codes more than one TTL past expiry. The extra TTL means a late confirm still gets:expiredrather than counting as a failure.raxol_mcp:initializeunder invented session ids grew the server without bound.Raxol.MCP.Serverkept each connection's advertised capabilities until that connection's subscriber exited, andTransport.SSEtakes the connection id from any client-suppliedmcp-session-id, so a client that POSTedinitializeunder a new id each time and never opened a stream left one permanent entry per request (50 POSTs, 50 entries). The map is now capped by the new:max_client_capabilitiesstart option (default 1024). Past the cap the oldest entry without a live subscriber goes first, and a live one only when every entry has a subscriber; each eviction emits[:raxol, :mcp, :server, :client_capabilities_evicted]. An evicted connection keeps working, but an ASK denies instead of prompting until it sendsinitializeagain. Entries for subscribed connections are still removed when stdio or the SSE stream ends.Raxol.Performance.Cacheadmin calls and opt-in style caching reach the cache manager (#1094).Raxol.ApplicationstartsETSCacheManagerwith no name ({ETSCacheManager, []}in test mode,hibernate_after:only under:performance_monitoring), soCache.stats/0,clear/1andclear_all/0exited:noproc, and thecache_available?guard inThemeResolverandStyleProcessorwas always false:cache: trueorconfig :raxol, :theme_resolver/:style_processor, cache_enabled: truesilently resolved without the cache, and bothclear_cache/0functions did nothing.ETSCacheManagernow registers under its module name unless:nameis passed.clear_cache/1with a name that is not one of its six caches returned the raw name as a table and crashed the table owner, wiping every cache; it now returns{:error, :unknown_cache}. The test isolation helper stops clearing the nonexistent:theme_cache(theme entries live in:style), and the performance-improvements bench accepts an already started manager. New tests:stats/0andclear_all/0exited:noproc, a cachedresolve_styles/4left a:miss, andCache.clear(:theme_cache)crashed the server before.Raxol.Core.Metrics.init/1andRaxol.Core.start_application/2no longer raise, andclear_metrics/0survives without an aggregator (#1094).init/1calledAggregator.init/1andAlertManager.init/1, the GenServer callbacks, whose{:ok, state}fell through thewithas aWithClauseError; sincestart_application/2runs it during core init, every call raised. It now starts only the collector: the Aggregator and AlertManager stay opt-in, user-started servers as the metrics README says.clear_metrics/0called the Aggregator from a linked Task, so with none running the:noprocexit killed the caller; it now skips the Aggregator when none is running. New tests:init([])raisedWithClauseErrorandclear_metrics/0killed the test process before.Raxol.Core.Metrics.Aggregatoraggregates recorded metrics and survives its update timer (#1094). The timer sent{:aggregate, id}, which no clause handled, so every running Aggregator crashed withFunctionClauseError60 s after start; it also ignored theupdate_intervaloption.update_aggregation/1and the periodic update passedMetricsCollector.get_metrics/2's{:ok, list}straight to aggregation, so every call crashed the server withProtocol.UndefinedError; the tests mocked a bare list and hid it. The timer now sends the handled message everyupdate_intervalseconds, the result is unwrapped, and a rule with no recorded metrics aggregates to[]instead of raising. The tests now record through the real collector; they crashed before, and the periodic-update test saw no timer message within 3 s.Raxol.UI.Theming.Colors.convert_to_palette/2exited:noprocfor any palette it did not know (#1094). Every palette name other than:xterm256,:xterm,:basic,:linux,:macand:windowswas looked up inRaxol.UI.Theming.PaletteRegistry, a server nothing ever started, soconvert_to_palette({255, 0, 0}, :no_such_palette)exited:noprocinstead of reaching its intended fallback. An unknown palette now falls back to the xterm 256-colour palette; new tests convert a colour and a theme map with an unknown palette and compare against:xterm256(both exited:noprocbefore).The example
RainbowThemePlugincrashed on every colour rotation (#1094).rotate_color/1, reached from therainbow nextcommand,handle_rainbow_next/2and the auto-rotate timer, calledRaxol.Themes.apply_theme/1, a server nothing started (and which could not be started under its name), so each rotation exited:noproc. The plugin, whichdocs/plugins/README.mdtells users to copy, now sets the colour as the:foregroundand:accentofRaxol.UI.Theming.Theme.current/0and applies it withTheme.apply_theme/1, the process-free theme thatRaxol.Style, the modal renderer and the theme selector read. A new test rotates through the palette and asserts the current theme's colours (it exited:noprocbefore).mix raxol.newstandalone apps no longer start insidemix compile(#1102). Without--sup,lib/<app>.exended with top-level code that started the app and waited for it, andmix compileevaluates every file underlib/, so compiling the project (and somix test, the generated CI and--install) launched the TUI inside the compiler, which did not return until the app quit. That code is now the module'sstart/0, and the generated README and instructions run it withmix run -e "MyApp.start()"; the quickstart's skeleton moves the same way. Theblanktemplate's view, thetodotemplate's empty-list message and thedashboardtemplate's request bar drew nothing, because acolumnorrowwhose block is a single element renders no children; each now passes a list. A new test generates every template with and without--sup(plus--ssh --liveview), compiles each project'slib/and checks its first frame headlessly: before, the four standalone projects ran code at compile time,blank --supdrew an empty screen andtodo --supan empty list box.mix raxol.newalso refuses, before creating anything, an app name with a trailing newline (its check matched$, asraxol new's did) and, asmix newandraxol newdo, a project whose module already exists:mix raxol.new agentgenerateddefmodule Agent, andenum, like--module Enum, redefinedEnum.assets/tapes/workspace/counter.exscompiles (#1102). The counter shown in the MCP-client recordings had the sameframework: :reactdefect as theraxol newtemplate, and itsupdate/2returned a bare model. It now usesRaxol.Core.Runtime.Application, returns{model, commands}and handles the+,-andqkeys its view advertises, and quits on Ctrl+C; a new test compiles and renders it. Theraxol_mcpRaxol.MCP.ResourceProviderexample, a TEA app, usedframework: :reacttoo and now uses the same app API.Raxol.Core.Metrics.record/3records each metric once (#1106). It calledMetricsCollector.record_metric/4and thenAggregator.record/3, which passes through to the same collector, so every value was stored twice and counts and sums overrecord/3data came out doubled. It now records through the collector only. The new test found two entries for onerecord/3call before the fix.Raxol.Core.Metrics.Aggregatorgroups metrics recorded with keyword-list tags (#1106). Agroup_byrule read each metric's tags withMap.get/2, so tags recorded as a keyword list (theMetrics.record/3form) raisedBadMapErrorand crashed the aggregator. Tags are now normalized to a map first throughMetricsCollector.normalize_tags/1, which is now public. The new test crashed the aggregator before the fix.AlertManager'sgroup_byhad the sameMap.get/2on tags and crashedAlertManagerat its next check, losing every rule and all alert state; it now uses the same normalization (the first bullet of #1120, fixed here). A new test groups keyword-tagged metrics through an alert check; it crashedAlertManagerwithBadMapErrorbefore.Raxol.Core.Metrics.AlertManagerhonours itscheck_intervaloption (#1106).schedule_checkalways used the 60-second default, so a manager started with a shorter or longer interval still checked once a minute. The option (in seconds, as documented) is now used; the test helper that passed:timer.seconds(1), i.e. 1000 s, now passes1. The new test waited 3 s for a check atcheck_interval: 1and got none before the fix. Because the value is now used, it is validated at start: anything but a positive integer makesstart_link/1return{:error, {:invalid_option, :check_interval, value}}. Before,0spun a busy loop of checks and a negative or float value crashedinitwithbadarg; a new test covers0,-1and0.5.mix raxol.new --supapps start (#1116). The generatedmix.exsnamed no application callback, and without--sshthe supervisor had no children, somix run --no-halt, the documented way to run a--supapp, ran nothing.mix.exsnow hasmod: {MyApp.Application, []}, and the application runsMyApp.Appunder its supervisor with the options inconfig :my_app, :raxol. The TUI runs under a process that stops the VM--no-haltkeeps up once the TUI exits, with status 0 when the user quit and 1 when it failed; stopping the application (as a test may) stops only the TUI and leaves the VM running.mix teststarts the application too, so the generated config runs the TUI headless in the test env. Under IEx the application leaves the TUI unstarted and says to run it withmix run --no-halt:iex -S mixwould otherwise pass every key typed into the TUI to IEx, which evaluates it. With--ssh, the application serves the app over SSH with the options inconfig :my_app, :ssh: the generated child passed no authentication, so the fail-closedRaxol.SSH.Serverwould have refused to start, and it now configures anonymous access on loopback with explicit limits in the dev env only; undermix testit takes port 0, keeps its host key in_buildinstead of~/.raxol/ssh_keysand admits no keys, and elsewhere it refuses to start until authentication is configured.MyApp.start/0starts the application instead of calling its callback a second time. New tests start the generated application from itsmix.exsand config and draw its first frame, check the SSH variant listens, and, in a VM of its own, check the exit status when the TUI quits or fails and that stopping the application leaves the VM running; they failed before on the missingmod:.--moduleis checked to be an alias before anything is written: a name likeMy Appused to fail formatting with an empty project directory left behind.mix raxol.newoutput passesmix format --check-formatted(#1116). The generated--ciworkflow runs that check, and it failed onconfig/config.exs(blank lines at the end), the app module of three of the four templates,live.exand the todo test. The generator now formats every.ex/.exsfile it writes with the formatter defaults the generated.formatter.exsleaves in place, which holds every template to it whatever the module name's length. A new test runsmix format --check-formattedon the generated tree for each template with each combination of--sup,--sshand--liveview; all 32 failed before. Theraxol new(raxol_cli) project gets the same check, which it already passed.--moduleis checked before anything is written too: a value that is not an alias (My App), which failed to format with files already on disk, is refused, as are the bareElixir, the prefix of every alias, and any module that already exists (--module Raxol,--module Application).The
mix raxol.newcounter's key hint matches its keys (#1116). It said'='/'-'and bound=, while the generator's instructions said+. It now binds both+and=(unshifted+), labels the button+and says'+'/'-'. Theraxol newtemplate already bound the+its hint names.mix raxol.new --sshgenerates an SSH server that starts (#1116). The generatedMyApp.SSH.start/1calledRaxol.SSH.Server.serve/2with only a port, and the server fails closed without authentication, so it refused to start ({:ssh_auth_required, ...}). Without--supit also servedMyApp.App, a module that only--supgenerates; the LiveView bridge had the same wrong module.start/1now serves the app's real TEA module with the options inconfig :my_app, :ssh, which every--sshproject now gets: explicit limits, anonymous access on loopback in the dev env only, and in the test env port 0, a host key kept in_buildand no admitted keys. Without--supthe generator's instructions now saymix run --no-halt -e "MyApp.SSH.start()", because plainmix run --no-haltstarts nothing there. The unusedssh_subsystem_fwupdependency, a Nerves firmware-update package that nothing generated referenced, is no longer added tomix.exs. A new test calls the generatedSSH.start/0under the project's test config and checks that it listens and serves the generated TEA module, as does the--sup --sshtest (the server loads its app only when a client connects, so it starts just the same over a wrong module); before, it exited with:ssh_auth_required.CI:
mix deps.get --check-lockedfailed the day a transitive dependency published a release, always in the root,raxol_symphonyandraxol_telegramlocks. Those lockfiles still held Hex entries for siblings the project takes by path (raxol_core,raxol_liveview,raxol_mcpand others), written whenever a lock was resolved in Hex mode. On every resolve Hex unlocks each path dependency by name together with every package its old lock entry lists as a child, recursively, so the staleraxol_liveviewentry freedphoenix_live_view,igniter,req,finchandmintfrom the lock and each one moved to its newest release as soon as it appeared. That is what #1048, #1060, #1071, #1089, #1146 and #1163 each refreshed a lock for (phoenix twice, phoenix_template, hpax, mint, finch). The 26 stale entries are gone from those three lockfiles and fromraxol_mcp,raxol_speech,raxol_terminalandraxol_watch, where a staleraxol_corelefttelemetryunpinned. The resolved versions do not change.scripts/check_lock_path_shadows.exsnow fails CI on any such entry, naming the lockfile, the entries and the--fixcommand that removes them. It runs before--check-lockedin thesetupjob and in everypackage-testscell, and over every lockfile in the Hex Advisories job.scripts/check-lockstep-deps.shno longer advises refreshing a committed lock withHEX_BUILD=1 mix deps.get.A
Raxol.Headlesscall racing an app's exit no longer takes down the session manager.screenshot/1,get_buffer/1,get_model/1,send_key/3,send_message/2,send_resize/3andsend_key_and_screenshot/3asked the session'sLifecyclefor its processes with an unguardedGenServer.callmade inside theRaxol.Headlessserver. An app that had just quit (a key whoseupdate/2returnsDirective.stop()) left a window before the server handled theLifecycle's:DOWN, and a call in that window exited:noprocand killed the server, orphaning every other session it held. The lookup now runs in the caller (see Changed) and answers{:error, :not_found}, which is what the caller gets once the:DOWNhas been handled; the:DOWNhandler still drops the session. ALifecyclethat is alive but does not answer is{:error, {:session_unavailable, class}}. The calls that follow it, the render at the engine and the model read (andsend_message/2's fence) at the dispatcher, answer the same way when the session ends between the lookup and the call; they exited the caller before. New tests hold the server until the lookup is queued, kill theLifecycleso its:DOWNqueues behind it, and check thatscreenshot/1andsend_key/3answer:not_foundwith the server alive (both exited before); others suspend the engine or dispatcher until the caller's call is queued, end the session, and check thatscreenshot/1,get_buffer/1,get_model/1andsend_message/2answer:not_found(all four exited before).raxol_terminal: a chunk holding invalid UTF-8 cost ~100 us of logging at any log level (#1031).Raxol.Terminal.Parser.States.GroundStateanswers an invalid byte with{:error, :unhandled_input, emulator, parser_state}, andTerminalParser.parse_chunk/3logged that result withinspect/1throughRaxol.Core.Runtime.Log.error/1, a function, so the whole emulator was dumped before any level check. With the Logger at:emergency, a peer sending<<0xFF>>one chunk at a time cost 98.8 us per byte against 2.4 us for"a"; the line now names only the reason, and the byte cost 1.9 us. Invalid bytes no longer take that path at all: they render as U+FFFD (see the emulator bounds entry). TheModeManagerwrite path #1031 reported (253 us perCSI ? 25 h, 45.8 us per input byte against 2.7 us for plain text) was already fixed by #1039 (3.9 us per call, 0.97 us per byte). A regression test fails if either path dumps the emulator at a disabled level; the ModeManager and mode-definition dumps #1031 named stay covered by #1039'sKernel.inspecttrace test inmode_manager_test.exs.Every input event and every frame dumped the app's model, view or event into debug messages at any log level (#1031).
Raxol.Core.Runtime.Log.debug/1is a function, so its argument is built before the level check. Per frame, the Dispatcher's:get_render_contextand the rendering engine's:render_frameeach inspected the whole model, and the engine inspected the view tree and every positioned element; per event, the Dispatcher inspected the event (twice with the app's debug mode on) and the commandsupdate/2returned; per agent message, the message. Over SSH that is once per key of a paste. The model, view and element dumps are gone (the lines keep the theme id and the view's type; the element count was already logged), and the event, command and agent-message lines use theLogger.debug/1macro, which skips them when:debugis off. For an app whose model holds 500 rows, with the Logger at:emergency: a key throughRaxol.Headless.send_key/3went from 116 us to 37 us, and a synchronous 120x40 frame from 6.5 ms to 3.1 ms.
Security
- raxol.io (
web/) no longer ships req 0.5.17, decimal 2.4.1, swoosh 1.19.8, mint 1.7.1 or hpax 1.0.3.web/mix.lockmoves to req 0.7.4 (EEF-CVE-2026-49755, a decompression bomb, HIGH; EEF-CVE-2026-49756, multipart header injection, LOW), decimal 3.1.1 (EEF-CVE-2026-32686, unbounded-exponent DoS, MEDIUM), swoosh 1.28.1 (EEF-CVE-2026-54893, LOW), and mint 1.11.0 / hpax 1.1.0, the HTTP client under Req, Finch and Swoosh (mint 1.7.1 carries more than a dozen advisories, several HIGH, among them EEF-CVE-2026-91043 and -82728; hpax 1.0.3 carries EEF-CVE-2026-58226). The 26 entries nothing in web's dependency tree uses any more (postgrex, hackney, ecto, ex_cldr and others) are dropped, somix hex.auditinweb/no longer reports their advisories either. It now lists only cowlib 2.20.0's EEF-CVE-2026-43966 and -43969: there is no fixed cowlib release, and neither vulnerable function is called anywhere in the tree (#921). - The package lockfiles no longer carry fixable Hex advisories. Nothing audited
packages/*/mix.lock, and several held HIGH ones:raxol_agent: cowboy 2.17.0 -> 2.19.0 (EEF-CVE-2026-65624), cowlib 2.18.0 -> 2.20.0 (EEF-CVE-2026-59248, HPACK/QPACK integer DoS, HIGH; -43971), mint 1.10.1 -> 1.11.0 (EEF-CVE-2026-91043, HTTP/2 header-list memory exhaustion, HIGH; -92103, -94194).raxol_cli,raxol_console: cowlib 2.19.0 -> 2.20.0 (EEF-CVE-2026-43971), phoenix_live_view 1.2.8 -> 1.2.12 (EEF-CVE-2026-64941), mint 1.9.3 -> 1.11.0 (EEF-CVE-2026-91043 and -82728, both HIGH; -82672, -82729, -92103, -94194), with cowboy 2.19.0 and phoenix 1.8.15 alongside.raxol_earn,raxol_payments: phoenix 1.8.8 -> 1.8.15 (EEF-CVE-2026-56811, unbounded channel joins per transport, HIGH; -56812), phoenix_live_view 1.2.4 / 1.2.3 -> 1.2.12 (EEF-CVE-2026-58228,<.link>scheme-validation XSS; -64941), cowboy 2.16.1 -> 2.19.0 (-65624), cowlib 2.17.1 -> 2.20.0 (-59248, HIGH; -43971).raxol_gateway: cowboy 2.17.0 -> 2.19.0, cowlib 2.18.0 -> 2.20.0, phoenix_live_view 1.2.7 -> 1.2.12 and mint 1.9.3 -> 1.11.0, clearing the same advisories as above.raxol_mcp,raxol_web3: mint 1.10.1 -> 1.11.0 (-91043, HIGH; -92103, -94194).raxol_speech: progress_bar 3.0.0 -> 3.1.0, which no longer requires decimal, so decimal 2.4.1 (EEF-CVE-2026-32686) leaves the lock.raxol_watch: eleven entries pigeon 2.1.0 no longer uses are dropped, among them hackney 1.17.1 and its six advisories (EEF-CVE-2026-47071, TLS upgrade without timeout, HIGH; -47069, -47075, -47076; GHSA-9fm9-hp7p-53mf, GHSA-vq52-99r9-h5pw).
- CI:
mix hex.auditin every project with a lockfile. The newHex Advisoriesjob insecurity.ymlrunsmix deps.get --check-lockedandmix hex.auditin the root,web/and eachpackages/*/that has amix.lock(found by glob), reports every failing project rather than stopping at the first, and fails the Security Scanning workflow; it is not a required check, so a newly published advisory turns runs red without blocking unrelated merges. It replaces the root-onlymix hex.auditstep, which ran undercontinue-on-error, and aweb/-only change now triggers the workflow. cowlib 2.20.0's EEF-CVE-2026-43966 and -43969 have no fixed release and neither vulnerable function is called (#921), so the ten projects that lock cowlib acknowledge exactly those two withhex: [ignore_advisories: [...]]inmix.exs.mix hex.auditlists them as ignored; an entry that no longer matches anything fails the job, so it is removed in the PR that makes it stale. - The scheduled Security Scanning run can file its critical-findings issue. The workflow granted only
contents: readandsecurity-events: write, soCreate issue for critical findingsgot a 403 on every scheduled failure and never filed one;security-reportnow hasissues: write. The new Hex Advisories job is one of the failures that files it. - The lockfiles move past five advisories published between 2026-10-30 and 2026-11-06, and the cowlib acknowledgement is gone. In the root,
web/and the eight packages that lock a web server: cowboy 2.20.0 -> 2.21.0 (EEF-CVE-2026-115701, unbounded buffering of pipelined HTTP/1.1 data, HIGH), cowlib 2.21.0 -> 2.22.0 (EEF-CVE-2026-113431, HPACK amplification through an unbounded decoded header list, HIGH) with ranch 2.3.0 -> 2.4.0 alongside, and phoenix_html 4.3.0 -> 4.4.0 (EEF-CVE-2026-117645, attribute injection through unvalidated attribute names inattributes_escape/1, MEDIUM), whichraxol_liveviewalso takes. The root lock also moves ssl_verify_fun 1.1.7 -> 1.1.9 (EEF-CVE-2026-117666, partial-label wildcard certificates matching across a label boundary, CRITICAL) and ecto 3.14.2 -> 3.14.3 (EEF-CVE-2026-118391, unbounded query-cache growth, HIGH). cowlib 2.22.0 also fixes EEF-CVE-2026-43966, so thehex: [ignore_advisories: ...]entry leaves all tenmix.exsfiles (#921).
[2.7.0] - 2026-09-09
Added
- Coding agent: durable sessions, replay, and rewind (#819). Each session writes an offset-addressed journal under
~/.raxol/sessions/<id>/, so--continue/--resume <id>restore the model context and the scrollback together.--replay <id>prints a transcript straight from the journal (--to-offset Nstops at an offset) viaRaxol.Agent.Code.Replay, and/rewindreturns a live session to an earlier turn.Raxol.Agent.Code.Inspectionbacks bothmix raxol.inspectand the TUI's/inspect: one snapshot of provider resolution,.raxol/config.json,.raxol/hooks.json,.mcp.json, skills roots, and the session store, recording env variable names and never their values. - Multi-tenant SSH coding agent (#819).
mix raxol.code --ssh --ssh-tenants DIRserves the TUI to many users from one daemon. Each tenant authenticates against its ownDIR/<user>/ssh/authorized_keysand gets its own cwd jail, session store, journal base, and spending identity (ssh:<user>) throughRaxol.Agent.Code.Tenant; the jail disables the shell tool, which a path sandbox cannot confine. Hosted deployment (RAXOL_SSH_CODE=true, port 2223) refuses to serve unlessRAXOL_SSH_CODE_TENANTSnames a tenants root,RAXOL_SSH_CODE_BUDGET_USDsets a positive per-tenant cap, and both raxol_agent and an HTTP client are in the build. /shareread-only session links (#819).Raxol.Agent.Code.ShareTokenmints an HMAC-SHA256 token binding a session id, its tenant scope, and a 24-hour expiry; a host mountsRaxol.Agent.Code.ShareLiveat/share/:tokento replay the journal and follow the session live. RequiresRAXOL_SHARE_SECRET; a token minted for one tenant cannot be replayed against another's tree.- ACP stdio surface (#819).
mix raxol.acp, the packagedraxol acp, and thebin/raxol-acpshim serve the coding agent over the Agent Client Protocol on stdio, for editors that spawn an agent themselves (Raxol.Agent.ClientProtocol.Serve+StdioAgent). Turns run the read-only toolset. - The harness as MCP tools (#819).
Raxol.Agent.Harness.McpToolsregistersharness_start_session,harness_send_prompt,harness_read_transcript, andharness_list_sessions. Sessions share the TUI's store, so a session started over MCP resumes withmix raxol.code --resume <id>. Read-only toolset, one unlinked worker per turn. .mcp.jsonservers in the coding TUI (#819).Raxol.Agent.Code.McpLoaderturns declared servers into approval-gatedmcp__<server>__<tool>tools. A janitor process owns every started client and monitors the session, so clients and their OS subprocesses die on any termination path.- LLM spend on the payments ledger (#819).
Raxol.Agent.Code.CostLedgermeters each turn intoRaxol.Payments.Ledgerwhen the host wires a ledger and a spending policy, so LLM spend and agent payment spend draw on one budget.Raxol.Agent.LlmPricesprices well-known hosted models whenRAXOL_COST_PER_MTOK_IN/_OUTare unset. Raxol.Agent.Code.Launcher(#819): flag parsing, provider resolution, and session resolution for the coding TUI, with no Mix calls, somix raxol.code,bin/raxol-code, and the packagedraxol codecannot drift.SECURITY.md(#819): private vulnerability reporting through GitHub security advisories, the supported release line, and the pre-alpha packages that carry no support commitment.Money-path telemetry (ADR-0036).
Raxol.Agent.Code.Appemits[:raxol, :agent, :cost, :priced](measurementscost_usd,input_tokens,output_tokens; metadatasourcein:env | :reported | :scoped_table | :flat_table,backend,model,session_id,turn_id,kind,ledger?) for every metered provider call,[:raxol, :agent, :cost, :unpriced]when billed tokens could not be priced (witharmed?saying whether a halt follows), and[:raxol, :agent, :budget, :halt, :unpriced]/[:raxol, :agent, :budget, :halt, :over_limit](metadatalimit) /[:raxol, :agent, :budget, :halt, :ledger_unreachable]when a running turn is stopped, split by remedy: a price, a policy decision, a process to repair. A sub-agent round is metered under its parent turn'sturn_id.Raxol.Agent.LlmPrices.turn_cost/4returns{:ok, cost, source};turn_cost_usd/4is unchanged and delegates to it. All five events are classified:operationalinRaxol.Agent.Telemetry. Identifiers are correlation attributes, not metric labels; no prompt, argument or credential reaches metadata.docs/development/RELEASE_CHECKLIST.md: the ordered publish sequence for the Hex train, written for someone holding the credentials and no other context. Carries the verified package/version table (twelve published, six not), the publish order forced by inter-package requirements, the gates that must be green, the per-release CHANGELOG-dating and tagging bookkeeping, and the manual operator gates forraxol_gateway,raxol_symphony, andraxol_earnwith the reason each one cannot be rehearsed offline.- First CHANGELOGs for
raxol_gateway(0.1.0) andraxol_earn(0.2.0), both linked from their package metadata and shipped in their tarballs.
Changed
Raxol.LiveView.TEALiverenders one DOM node per screen row (raxol_liveview). Its:terminal_htmlassign is replaced by:rowsand:container_attrs, which is breaking for anything that read the old assign or overroderender/1against it. The screen was one dynamic, so LiveView resent all of it on every frame: measured 2.8 KB per frame at 62x13 and 7.0 KB at 60x34, against 0.3 KB for a one-row change at either size.TerminalBridge.buffer_to_rows/2andhtml_to_rows/2produce the rows, andcontainer_attrs/1andaria_mode_of/1are public so a caller owning the<pre>cannot drift from the semanticsbuffer_to_html/2emits for the same:aria_mode.- Headless sessions now require the full TEA callback contract (
init/1,update/2, andview/1). Modules that exported onlyview/1are refused as:not_a_raxol_applicationinstead of starting a session that cannot process events. - Spending budgets halt a running turn (#819) instead of only the next prompt, and a model with no price fails closed once a ledger and policy are wired (an unpriced model bills real tokens while the ledger records $0.00).
- The web release carries the agent stack (#819):
raxol_agent,raxol_payments, andreqare release dependencies. Without them the hosted coding agent could not have started or made an LLM call, andRaxol.Applicationnow declines to serve the surface rather than standing up a broken one. - Dependency refresh in
raxol_symphonyandraxol_telegramlockfiles, carried in the telemetry change because the newmix deps.get --check-lockedCI gate found both locks stale (every locked version satisfied every constraint, so nothing had caught it). Nomix.exsconstraint changed.raxol_symphony: mint 1.9.3 -> 1.10.0, phoenix 1.8.9 -> 1.8.13, phoenix_live_view 1.2.8 -> 1.2.11, phoenix_pubsub 2.2.0 -> 2.3.0, postgrex 0.22.3 -> 0.22.4, ranch 2.2.0 -> 2.2.1, req 0.7.2 -> 0.7.4, telemetry_metrics 1.1.0 -> 1.2.0.raxol_telegram: cowboy 2.17.0 -> 2.18.0, cowlib 2.18.0 -> 2.19.0, phoenix_live_view 1.2.7 -> 1.2.11, req 0.6.3 -> 0.7.4, plus the same mint, phoenix, phoenix_pubsub, ranch and telemetry_metrics moves. bin/raxol,bin/raxol-codeandbin/raxol-acplaunch throughmix run --no-start --no-compile --no-deps-checkwith stdin closed on the compile step and-noinputset for the ACP shim, so no Mix chatter reaches the protocol pipe and the ACP peer's VM does not claim fd 0 from the transport.bin/raxoldrops the deprecated--harnessflag.- DeepSeek is a registered backend (
:deepseek,DEEPSEEK_API_KEY, defaultdeepseek-v4-flash), which is what makesRaxol.Agent.LlmPrices' backend-scoped cache-tier and peak-clock table (ADR-0035) reachable from a real session; the rows shipped keyed to an id no configuration could name. - The 256-color cube resolves to xterm's ramp, changing 208 of 216 indices.
Formats.ansi_to_rgb/1(lib/raxol/style/colors/formats.ex),SixelPalette.calculate_rgb_cube_color/1andTerminalBridge.color_256_to_rgb/1(packages/raxol_liveview) each computed the 6x6x6 cube asn * 51, giving levels 0/51/102/153/204/255 instead of xterm's 0/95/135/175/215/255. Only the two endpoints agreed, so the same buffer rendered different colors in LiveView than in the terminal renderer. All three now go throughRaxol.Core.Colors.Ansi256.to_rgb/1. This is a VALUE change, not a refactor: downstream snapshot tests over emittedrgb(...)CSS, golden Sixel fixtures and recorded asciinema artifacts will all diff. - Publishing is part of landing this change, not a follow-up. Every install snippet in the READMEs,
docs/getting-started/QUICKSTART.md,docs/PACKAGES.mdandweb/priv/static/skill.mdnow says~> 2.7, andscripts/check-lockstep-deps.shrequires prose andmix.exsto agree, so the doc half cannot be staged separately. Until the family is on Hex, every documented install failsmix deps.get, andweb/priv/static/skill.mdis served to LLM agents, so the unresolvable constraint propagates into generated projects. TheRaxol.Core.Telemetry.Invariantsblocker that motivated the bump is itself cleared by it:raxol_terminal,raxol_paymentsandraxol_earnallusethat module and all now requireraxol_core "~> 2.7", which cannot resolve araxol_corewithout it.
Removed
These are public modules of published packages, so their removal is breaking and the family's 2.7.0 minor bump carries it. Each was dead or duplicated at the point of removal, and no shipped code referenced any of them.
raxol_terminal: four unreferenced renderers.Raxol.Terminal.Rendering.{GPUAccelerator, GPURenderer, LigatureRenderer, OptimizedStyleRenderer}(2,175 lines). None was reachable from the render pipeline;Raxol.Terminal.Rendereris the only renderer the emulator uses.raxol_terminal: three forks of live modules.Raxol.Terminal.ANSI.SGRProcessorduplicatedRaxol.Terminal.ANSI.SGR.Processorover a plain map with its owndefault_style/0carrying a:dimkey theTextFormattingstruct does not have, soESC[2mraisedKeyErroron theEmulator.{ANSIHandler,CommandHandler}paths; both now callSGR.Processor, which operates on the structemulator.styleactually is.Raxol.Terminal.Buffer.CharsetandRaxol.Terminal.Commands.CSIHandler.ModeHandlerswere likewise superseded (byANSI.CharacterSetsandCSIHandler.ModeProcessor) with no remaining callers.raxol_core:Raxol.Core.ErrorHandler. Superseded byRaxol.Core.ErrorHandling; its test moved with it (error_handler_test.exs->error_handling_test.exs).- Two benchmark suites with no subject.
bench/suites/validation/verify_optimization.exsandbench/suites/parser/sgr_comparison.exscomparedSGRProcessoragainstSGRProcessorOptimized, a module that does not exist anywhere in the tree; both had been failing at runtime before this change. The four repairable suites now callSGR.Processor.process_params/2. raxol: the top-levelPluginRunnerandMockTerminal. Both were unnamespaced public modules of the publishedraxolpackage; they are nowRaxol.Plugins.Testing.StubRunnerandRaxol.Plugins.Testing.StubTerminal. A consumer referencing either name by its old spelling breaks.raxol_terminal:Renderer.get_content/2's pid clause. The clause matching a buffer-manager pid and returning{:error, :deprecated_buffer_manager}is gone with no catch-all behind it, so an external caller still passing a pid now gets aFunctionClauseErrorrather than the error tuple. Match on the buffer directly.
Fixed
raxol --versionis a real top-level flag. The packaged CLI now prints its release version and build stamp, then exits successfully instead of treating--versionas an unknown command.A new
raxol_coremodule shipped under a constraint that resolved without it.Raxol.Core.Colors.Ansi256was added toraxol_core, butraxol,raxol_terminalandraxol_liveviewall depended onraxol_core "~> 2.6", which Hex resolves to the published 2.6.0, a version without the module. A consumer installing from Hex would have hitUndefinedFunctionErroron any 256-color render (Formats.ansi_to_rgb/1,SixelPalette,TerminalBridge.color_256_to_rgb/1). Adding public API is a minor bump, so the framework family moves to 2.7.0 and the constraints to"~> 2.7", which cannot resolve araxol_corewithout the module. Neither guard could have caught this:scripts/check-lockstep-deps.shcompares onlyX.Y, andRaxol.Release.PackageCheckrequires the constraint to be exactly"~> major.minor"of the sibling, so a patch-level API addition is invisible to both by construction.The 2.6.x family was split across two patch versions (
raxolandraxol_terminalat 2.6.1, the rest at 2.6.0), which is what allowed a dependent to resolve a sibling older than the code it was built against. The framework line (raxol,raxol_core,raxol_terminal,raxol_agent,raxol_mcp,raxol_liveview,raxol_plugin,raxol_sensor) is unified at 2.7.0.Three package tarballs shipped no licence.
raxol_gateway,raxol_cli, andraxol_consoledeclaredlicenses: ["MIT"]but had noLICENSE.mdon disk and none infiles:, somix raxol.release.checkfailed all three and a published tarball would have carried a licence claim with no licence text. All three now ship the repository MIT licence.raxol_agent_client_protocolcould not be packaged at all: its description was 341 characters against Hex's 300-character limit, somix hex.buildaborted. Trimmed to the protocol and the transport surface; the resumable-session vendor extension is described in the package README.Published docs for the 0.x packages linked their source at unrelated code.
raxol_speech,raxol_telegram,raxol_watch,raxol_gateway,raxol_earn, andraxol_symphonysetsource_ref: "v#{@version}", but the barevX.Y.Ztags are the rootraxolversion line, wherev0.2.0is raxol from 2025. All six now use a package-scoped<package>-v<version>ref, and pushing that tag before publishing is a step indocs/development/RELEASE_CHECKLIST.md.raxol_payments0.2.0 is already published with the defect; the checklist records it and the docs-only repair.raxol_earnadvertised a release candidate that was never cut. Its Hex description still credited the retired v1 memo model, and its README's status line and installation snippet said0.2.0-rc.0and{:raxol_earn, "~> 0.2-rc"}against amix.exsat 0.2.0.raxol_symphony's CHANGELOG and RUNBOOK had gone stale against the tree. The 0.2.0 entry was written in July and predates the worker host pool, durable paused runs, Codex runner auth, the review stage, workspace confinement, and the retry and evidence fixes; it also still claimed runner pauses are unsupported in:graph_parallel, which they no longer are. The RUNBOOK pointed at aSPEC.mdthat does not exist in this repository, and its publish step did not mention thatraxol_telegramandraxol_watchmust reach Hex at 0.2.0 beforeHEX_BUILD=1 mix deps.getcan resolve.Journal reads healed a torn tail (#819). A read-side scan truncated live segments and permanently damaged a session; healing now belongs to the owning writer alone (
Raxol.Agent.Journal.FileStore.Reader.resume_scan/1).Per-session Lifecycles owned the VM-singleton plugin manager (#819). The
:ssh,:telegram,:agent,:liveview, and:gatewayenvironments start no plugin manager, so one disconnect can no longer kill a concurrent session.Raxol.SSH.Session.lifecycle_opts/4is the opts merge seam.raxol acpnever exited on peer disconnect (#819): exit 1 on the mix path, and a resident BEAM per editor session on the packaged one. A clean peer disconnect now exits 0.Raxol.Agent.SessionStreamerretained event history forever (#819). History is a live replay buffer tied to subscriber presence: it is reclaimed when the last subscriber leaves, andrelease/2retires a per-run session id at the call site.Sub-agent LLM spend never reached the ledger (#819), and turns were priced from the configured model rather than the billed one, so budgets read $0.00.
Raxol.System.PortCommandleft children running on timeout (#819). A timed-out command is killed; where the port is its own process group leader, the whole group is signalled so descendants die with it.Phoenix dev code reloading raised on every request in
web/: Phoenix 1.8 drives the reloader through Mix's compiler-listener API, andweb/mix.exsdid not declarelisteners: [Phoenix.CodeReloader]. Theweb-deploy-checkworkflow now boots the dev environment for real (RAXOL_DEV_SMOKE=1, watchers off, port fromPORT) and requires one page to render, the only place this class of breakage is observable./usageand the spending ledger priced the same money two ways. The panel summed the session's tokens and priced the total through the flat table with the last billed model, so a provider-reported cost (grok'stotal_cost_usd, an ACP peer'susage_update) or a cache split showed asunknown modelwhile the ledger charged the real figure. Each turn is now priced exactly as it was metered and turns nothing could price are counted, not hidden. On the flat table a cache-read token (Anthropic'scache_read_input_tokens) is billed at the input rate;rates/2callers never billed it at all, and the correction is upward on purpose.A hostile usage figure could crash or silently skip metering. An ACP peer's
usage_updatewith acost.amountpast what a float can carry crashedRaxol.Agent.AcpStreamAdapter(and its embedder through the link); the same figure in a token count abortedRaxol.Agent.Code.App's fold before the ledger record, the cost event and the halt, and the dispatcher logged and carried on. Every usage reader now bounds counts and amounts atRaxol.Agent.BenchmarkProfile.max_count/0and reads anything past it as absent.Harness.GrokBuildstamps its cost as%{amount, currency: "USD"}; a bare number with no currency is no longer read as a cost.Cancelled, errored and superseded ACP turns lost their spend. The adapter advanced its cumulative-cost anchor at every turn boundary, but only a completed bracket carries usage, so the money a cancelled turn spent could never be billed by any later turn. The anchor now advances only when a bracket bills, and a
:cost_anchorstart option seeds it when attaching to a session with history (a restart orsession/load), which otherwise billed the whole history to one turn.A token-less ACP turn priced at $0.00 disarmed the fail-closed halt. The measured omp frame carries
used/size/costand no token split, so when the adapter drops the per-turn cost (cumulative went down, currency changed) the gate saw zero tokens and zero dollars and called the turn free. A usage map carryingsession_costnow counts as billed, so it is unpriced, not free.Symphony review preflight accepted pairs dispatch could never run: two kinds of one vendor validated and then parked every issue as
:awaiting_human; a disabled review leftimplementer_kindunvalidated, sonoopcrashed at dispatch andreviewrecursed into itself; andReview.select_reviewer/3escalated acandidate_kindslist that named only the other vendor.
Security
grep/globfollowed symlinks out of the workspace sandbox (#819) and returned file contents with no approval. Containment is decided on the real path of every walked entry (Raxol.Agent.Actions.Fs.realpath/1), and a symlink that resolves outside the root is skipped.- Policy telemetry emitted the wrapped operation's argument (ADR-0036).
Raxol.Agent.PolicyApplierputparams(an arbitrary term, and for a cache policy keyed on a prompt, the prompt) into the metadata of[:raxol, :agent, :policy, :applied],:cache_hitand:cache_miss, andRaxol.Agent.ThreadLogRouterpersisted every metadata key to the durable thread log. Theparamskey is removed (a handler readingmetadata.paramsnow getsnil); every policy event carriesparams_digest, 64 bits of a keyed HMAC-SHA256 over the argument's external term format, which joins the events of one call without carrying content. Callers that need correlation identifiers on these events passPolicyApplier.apply/4's newmetadata:option, enforced byRaxol.Agent.Telemetry.identifier?/1(atom keys; atom, number, or binary values of at most 64 bytes) with a refusal that names the offender by shape, never by content (a struct handed over as the map is refused by its name, not enumerated into the message); Symphony's runner passes itsturnandissue_idthat way.ThreadLogRouterno longer mirrors event metadata into the persisted entry: it keepssession_id,turn_id, theTraceContextids, and the keys a host names inattach/4's:metadata_keys, each only when its value is an identifier, and drops the rest without raising so a bad value cannot detach the audit handler. - Policy and sandbox telemetry carried error reasons, cache keys and shell commands verbatim (ADR-0036).
reasonon[:raxol, :agent, :policy, :retry_attempt]/:retry_exhaustedwas the wrapped operation's error term (a provider error can echo the request or carry a response struct with headers);keyon:cache_hit/:cache_misswas the user'skey_fnresult, which may be the argument; and[:raxol, :agent, :sandbox, :denied]carried the full shell command line or the whole malformed tool payload inreason, whichThreadLogRouterpersisted as the:sandbox_denyaudit entry. All are now bounded before emission byRaxol.Agent.Telemetry.bound/1, which keeps identifiers and the shape of a term (depth 3, width 8) and replaces every other leaf with a shape tag such as{:redacted, :binary, 4096};ThreadLogRouterbounds every payload it persists as a backstop. A shell denial is emitted as{:shell_denied, mode, program}with acommand_digest, whereprogramis the first token only for a simple command (Raxol.Agent.Sandbox.Shell.simple_command?/1) and the tag:non_simpleotherwise: an env-assignment prefix (PGPASSWORD=... psql) makes the first token the secret itself, on exactly the commands a list mode always denies, so it never leaves the process;modeis bounded too, so a predicate sandbox emits{:redacted, :function}. A malformed-payload denial is emitted as its tag; the caller's{:deny, reason}still carries the full reason.params_digestis now an HMAC-SHA256 under a per-VM random key rather than an unkeyed hash, so an audit log cannot be used to confirm a guessed argument; it joins the events of one operation within a run and, deliberately, not across runs. It is computed once perPolicyApplier.apply/4and carried on every event of that call, including:retry_*and:timeout. /resumeand--resumeaccepted an unvalidated session id (#819), which let/transcriptwrite into another tenant's workspace. Both validate the id, and/transcriptis containment-checked like/export.
[2.6.1] - 2026-08-08
Fixed
- Benchmark regression machinery is real (#805):
--regressiondetects (5% median gate, persisted baselines),--comparediffs the previous snapshot, the summary and dashboard report measured values, and the mix-task rendering suite benches the real buffer/tree-diff pipeline instead of a stub. The comparison suite's memory row useserts_debug.size. - benchee HTML reports no longer auto-open in any suite (#810, #812):
System.cmd("xdg-open", ...)crashed headless Linux runs and opened browsers mid-test on macOS. - Cloud metrics export is real (#805): OTLP/HTTP JSON, Datadog v1 series, and Prometheus pushgateway senders replace
:okstubs, with async monitored export, ingestion-time validation, label escaping, and api_key redaction. - postgrex 0.22.4 (EEF-CVE-2026-66838, SQL injection via the
:commentoption) (#812).
Changed
- README and QUICKSTART document the headless from-source path (#810, #815): C-toolchain prerequisite, non-interactive Hex bootstrap, the full test-exclusion set (
SKIP_TERMBOX2_TESTS=true), RATE,MIX_HOME/HEX_HOMEfor read-only sandboxes, and the PostgreSQL requirement of unexcluded integration suites. - Performance tables republished from a measured run (#813): single-provenance numbers (commit, hardware, command), emulator-ingest rows labeled as the full parse+apply path distinct from the sub-microsecond lexer, and the cross-framework ANSI row removed as lexer-vs-ingest comparisons mislead.
Deprecated
Raxol.UI.VisualTest,Raxol.UI.ComponentTest,Raxol.UI.IntegrationTest,Raxol.UI.PerformanceTestare deprecated and scheduled for removal in 3.0. They have no callers; no replacement is planned.
Removed
Raxol.UI.ThemeResolverCached(delegating shim) removed. UseRaxol.UI.ThemeResolver.<fn>(..., cache: true), a one-to-one replacement documented in that module.
[2.6.0] - 2026-07-11
Added
- LiveView accessibility: ARIA roles + announcement live region (#431). The LiveView terminal surface now emits ARIA roles and an
aria-liveregion so screen readers announce state changes. - Accessibility projection + MCP a11y fields (#428). The model exposes an accessibility projection; MCP screenshots carry a11y fields describing focus, roles, and announcements.
- OpenRouter backend harness with app attribution (#414).
Backend.Selector's:openrouterharness targets OpenRouter (OpenAI-compatible) and attaches app-attribution headers (HTTP-Referer, X-OpenRouter-Title, X-OpenRouter-Categories) so Raxol appears on openrouter.ai/rankings. raxol_payments:recipient_addresson the XochiQuoteRequest(#416), and stealth fields surfaced in the ACP Xochi deliverable so buyers can settle to a stealth meta-address.- Property tests for the money-adjacent paths (#392/#393):
router_property_test(trust-score totality / monotonicity / clamp / tier_override bounds),ledger_release_property_test(Ledger.release/4refund netting),jsonrpc_nonce_test(concurrent-send nonce distinctness),job_session/telemetry_test(pins the[:raxol, :acp, :job_session, :transition]cross-package contract withraxol_symphony), and an exhaustiveJobSession.Status.validate/2matrix.
Changed
- Toolchain: Elixir 1.20 / OTP 29, zero warnings (#398).
raxol_payments: MPP amounts pinned to atomic integer units (#413); Xochi Tron deposit-route quotes verified.- Web surface hardening (#427/#418/#426): web deploy gained a CI gate, health check, SSH, and warnings-as-errors; the raxol.io agent surface was tightened; the landing demo is interactive (forwards keydown).
Fixed
raxol_earn: EOA nonce race inProviderAdapter.JSONRPC(#392).send_calls/3fetched the pending nonce inline, so two concurrent sends for one EOA signed the same nonce and the RPC silently dropped one (fund loss on retry). Nonce assignment now routes through the mailbox-serializedRaxol.Earn.Wallet.NonceServer; a failed send resyncs to re-fetch and re-fill the gap. The SCA/UserOp path is unaffected.raxol_payments: negative trust score crashed the router (#393).Raxol.Payments.Routerclamped only the upper trust-score bound, so a negative:trust_scorehitPrivacyTier.from_trust_score/2(no negative clause) and raisedFunctionClauseErroron the settlement path. Now clamped to[0,100]at the boundary.raxol_payments: x402 auto-pay budget leak and value binding (#403); refunded status now handled and the session budget reconciled on refund (#402);Ledgerreservation tags are bounded with a TTL sweep (#407).raxol_earn: write-confirmation correctness. EOA transactions confirm the receipt before reporting write success (#412); a reverted UserOp is rejected as a failed SCA write (#409);accept_request(#408) and seller delivery (#406) are idempotent.- Web release stability: fixed a raxol.io release boot crash (#424), restored web PubSub + SSH for minimal-mode Raxol (#425), set ssh/public_key to
:permanent(#420), and excluded nested build artifacts from the Docker context (#422). - CI: de-flaked two timing-sensitive property tests (#432) (parser performance budget, process-isolation link race) that failed seed-dependently on the nightly and macOS matrices.
Security
raxol_payments: wallet private key guarded from crash-report leak (#404). Keys are wrapped so they cannot surface inInspectoutput or crash reports.- Dependencies: pruned orphaned lockfile deps, dropping the Tesla CVEs (#415); dropped the earmark dependency (#396).
Removed
raxol_earn: v1 memo model retired (seller-stack migration Phases 1-4, #385/#390/#388/#389/#391). DeletedRaxol.Earn.Job.{Server, Supervisor, Registry, Workflow, StateMachine, MemoType, FeeType, Store},Raxol.Earn.ContractClient(+Onchain+InMemory), the memoRaxol.Earn.Directives + their executor,Raxol.Earn.Onchain.LogDecoder, and theacp_version/ACP_VERSIONswitch. The v2 hook/event model (Raxol.Earn.JobSession+Raxol.Earn.HookClient->AgenticCommerceV3viaRaxol.Earn.ProviderAdapter) is now the only runtime.raxol_earngraduated0.2.0-pre.0 -> 0.2.0. Supersedes ADR-0016/0017. Seepackages/raxol_earn/MIGRATION_V2.md.
[2.5.0] - 2026-07-07
Fixed
- Layout engine: dynamic-children dropped from
column(opts, do: var): when callers wrote the inline keyword-list formcolumn(style: ..., do: items)Elixir matched it againstdef column/1instead ofdefmacro column/2, and the:dokey rode through into the layout function unread.Flex.column/1then sawchildren: []and rendered an empty container. Fix:Raxol.Core.Renderer.View.promote_do_to_children/1pops:doand re-keys it as:children(viaList.wrapso single-child blocks work too);View.column/1,View.row/1,View.box/1all forward through it. Restored rendering for CheckboxDemo, TextAreaDemo, RadioGroupDemo. - Layout engine: chart widget types dropped at layout step:
Components.chart/1returns a:boxelement whose:typeis overridden to one of:line_chart,:bar_chart,:scatter_chart,:heatmapso MCP'sToolProvidercan discover them. The layout engine had no clauses for those types; they hit the catch-all warning and the element disappeared. Fix: added oneprocess_element/3clause that rewrites the chart types to:boxand forwards. Restored LineChart, ScatterChart, BarChart, CursorTrail, Heatmap demos. - Layout engine:
style: %{position: {x, y}}offset ignored on:textelements: chart cells emitted byRaxol.UI.Charts.ViewBridgecarry their own relative position so each row lands at the right column. The:textlayout clause usedspace.x,space.yunconditionally and stacked all cells at the same coordinate. Fix: extract{dx, dy}fromstyle.positionand add to the space origin. - Layout engine:
:text_inputconstructor shape mismatch:Components.text_input/1builds the element with top-level:value/:placeholderkeys, but the layout clause required them inside:attrs. Element fell to the catch-all and dropped. Fix: added a clause that rewrites the new-DSL shape into the:attrs-shaped form and forwards.
Added
test/property/layout_completeness_property_test.exs: 3 properties locking down the layout-element-drop bug shape. Generates view trees (text / flex / box / chart) and asserts every:textcontent string in the input appears in the positioned-elements output. Sabotage-verified: comment out the chart-type clause and both relevant properties fail.test/property/promote_do_to_children_property_test.exs: 8 invariants for the helper above. Includes one property documenting that[do: nil]is intentionally indistinguishable from[](becauseKeyword.pop/2can't disambiguate).test/property/demo_update_totality_property_test.exs: one property per Catalog demo (30 total). For each demo, folds random sequences of up to 30 mixed key events (chars, special keys,:tick, modifier combos) throughupdate/2and asserts no exception plus correct{model, commands_list}shape. Catches state-machine edge cases that example tests don't reach (negative cursors, list overflows, unhandled tick states).test/raxol/playground/demo_render_test.exs: end-to-end render regression test that mounts every Catalog demo throughRaxol.Headless, asserts both text-screenshot content lines and buffer cell paint coverage, and drives demos through documented key sequences (@key_drives) to reach state where the canonical widget content is visible. Caught the four framework bugs above on its first run.raxol_symphonyPhases 0-14: Elixir/OTP port of OpenAI Symphony. Tracker-driven coding-agent orchestrator with two runner backends (Runners.RaxolAgentdefault;Runners.CodexPort-based JSON-RPC for parity with upstream Symphony Elixir) and six surfaces (terminal dashboard, LiveView, MCP tools +symphony://runsresource, Telegram per-issue session router, Watch push, JSON/api/v1/*). Workflow hot-reload viaWorkflowStore, Linear (GraphQL) + GitHub Issues (state labels) trackers, evidence framework (Evidence.GitHubCI/PR comments,Evidence.Complexitycloc/SLOC fallback,Evidence.Recordingcast scan), in-run asciinema capture (Evidence.Capturewrites<workspace>/.raxol_symphony/run-<attempt>.castper dispatch whenrecording.enabled: true). 399 tests, 0 failures. Pre-alpha, path-dep.raxol_earnv0.1 (engineering complete): First Elixir/OTP-native Virtuals Agent Commerce Protocol implementation. Job lifecycle (Job.{Server, StateMachine, Memo, Store, Supervisor, Registry}), EIP-712 typed-data memos viaRaxol.Payments.EIP712, on-chain client (ContractClient.Onchainover Req JSON-RPC, EIP-1559 typed-tx signing, Yellow-Paper RLP, log decoder forcreate_jobjob_id extraction), Seller stack (Backend.InMemory+Queue+Runtime+Supervisor, opt-in via:seller_enabled),Wallet.NonceServerfor serialized nonce assignment,mix raxol_earn.benchsandbox-graduation harness. Optional DETS-backedJob.Storedurability. 256 tests, 0 failures. Pre-alpha, path-dep. External-blocked: real ACP contract ABIs,Wallet.SCA,Seller.Backend.WebSocket.Raxol.Payments.Wallet.sign_hash/1: new behaviour callback for EIP-1559 transaction signing (sign-precomputed-digest semantics).Wallets.EnvandWallets.Opboth implement.Workflow Graph (
Raxol.Workflow.*, ADR-0015): LangGraph-style stateful workflow primitive in main raxol: a declarativeGraphbuilder with six-step structural validation, synchronousCompiled.invoke/3plusasync_invoke/3andstream_events/3, a durableCheckpoint.Saver(Ets/Dets/Postgrex adapters),Workflow.interrupt/1human-in-the-loop withCompiled.resume/4, andfailure_policy: :retry | :compensate(reverse-order saga rollback).Workflow concurrency (ADR-0019):
Graph.add_channel/3(typed reducers) andGraph.add_join/4(barrier); multi-node parallel branches viaTask.async_stream(parallelism :: pos_integer | :branches), per-branch pause/resume, cancellation cascade, per-branch trace spans, andbranch_idcheckpoint/telemetry metadata. Real consumers: an ACP cross-chain settlement example and a Symphony parallel-candidatesGraphAdapter.- Effect system (
Raxol.Agent.Directive): struct-based directives (Async/Shell/SendAgent/Schedule/Spawn/Stop) with anExecutorprotocol replacing the closed Command tuple set; a CloudEvents v1.0 envelope (Event.to_cloud_event/2);TraceContextcausation_idcorrelation. - Agent-stack substrate (ADR-0020):
Raxol.Agent.Cache,ThreadLog(append-only audit), declarativePolicy.{Retry, Timeout, Cache}viaPolicyApplier, and theSandboxprotocol withSandbox.Chaingating. Symphony's RaxolAgent runner adopts every primitive (tracker cache, thread-log audit, per-turn policies and sandboxes); a Session-backed sibling runner ships. - Operator-flow contract (ADR-0018): a first-class paused-run substrate end-to-end (Workflow Saver -> ACP Job -> Symphony Orchestrator -> MCP tools -> TUI/LiveView/Telegram/Watch);
Raxol.Symphony.PauseReasonconsolidates reason formatting and is mechanically enforced via apause_reasons/0callback. - raxol_earn Workflow migration (ADR-0016):
Job.Server's GenServer state machine replaced by a Workflow-backed implementation (Job.Workflow, a 10-node graph);:via_workflowflipped to the default. - Self-improving agent loop (
raxol_agent): skills as filesystem procedural memory, an after-turn curation reviewer, and a Curator.Raxol.Agent.Skillparses/renders agentskills.ioSKILL.md;Raxol.Agent.Skills.Storeis aBaseManagerdisk index with usage telemetry persisted viaCore.Stores.Dets, a managed root plus read-onlyexternal_dirs(default~/.agents/skills), and archive/state/pin;skills_list/skill_view/skill_manageActions.Raxol.Agent.SelfImprove.after_turn/3spawns an isolated, unlinked reviewer on an auxiliary model that writes durable memory andcreated_by: :agentskills;Raxol.Agent.Curatoragesactive -> stale -> archivedwith interval-plus-idle gating, dry-run, andtar.gzbackup/rollback. Newuse Raxol.Agentcallbacksskills_provider/0andself_improve/0. - Memory provider stack, full-text recall, dialectic user model (
raxol_agent):Raxol.Agent.Memory.Stackcomposes N memory providers (fan-out writes, merge + rerank + dedup reads);Raxol.Agent.Memory.SessionSearchis a BM25-lite inverted index over raw conversation items (session_searchreturns messages, not summaries);Raxol.Agent.UserModelderives a per-user dialectic block on an auxiliary model, injected into the last user message via the new optionalbuild_user_context/1callback. Newmemory_providers/0callback. Raxol.Agent.Turn: the turn driver that assembles memory/skills/user-context/session-search context from an agent module's callbacks, records each turn to aConversation.Log, and fires the after-turn self-improvement effects. Wires the loop end-to-end.raxol_gatewaypackage (pre-alpha): one daemon connecting many chat platforms through a sharedGateway.Adaptercontract.Routekeys sessionsagent:main:{platform}:{chat_type}:{chat_id};SessionRouter+Sessionrun one process per chat under aDynamicSupervisorwith idle/cooldown/max-session limits;Pairingissues DM codes and decidesauthorize/2;Deliveryresolves four outbound destinations (direct, home, cross-platform, explicit"platform:chat_id"target); per-chat history plusSessionRouter.handoff/3move a conversation across platforms with its history intact.Adapter.InMemoryis a reference adapter.- Symphony opt-in self-improvement:
Raxol.Symphony.Runners.RaxolAgentfires theAgent.Turnafter-turn hook (skills curation, memory, user-model refresh, session index) on each turn whenagent.moduledeclares it, in both the workflow and simple run paths, without touching event forwarding, pause detection, policies, or sandboxes. - ADRs 0021-0028: Hermes-extraction Tier 1 (self-improving agents, memory provider stack, unified messaging gateway) and Tier 2 (execution backends + hibernation, cronjob, execute_code, delegate_task, auxiliary-model routing) decision records.
- Idempotent crash recovery for cross-chain payment Actions (
raxol_payments):Raxol.Payments.Checkpoint, a nil-safe behaviour for a durable in-flight-intent store injected viacontext[:checkpoint], withderive_key/1for a stable idempotency key. Three stores:Checkpoint.ETS(process-crash-durable),Checkpoint.ContextStore(backed byRaxol.Agent.ContextStore, so a deployed agent's in-flight intent rides the same store that backs its crash recovery), and nil (disabled, the default).ExecuteXochiIntentandExecuteRelayTransfercheckpoint the dispatched intent before submit and resume it on a re-run after a crash (poll-before-re-sign, no second spend authorize) instead of re-quoting and signing twice; the relay rail keys on the logical payment (not the client-mintedtransfer_id) so a resume reuses the same id and an idempotent broadcaster dedupes a retried deposit. Drops the checkpoint on definite failure; protocol-tagged keys (:xochi/:relay) so a shared store can't collide. The ZERO cockpit crash beat (examples/agents/zero_system.exs) models the contract end-to-end.:live_xochiand:live_relaykill-and-resume gates. 24 new tests + the live variants; 0 failures. - Auxiliary-model routing (
raxol_agent, ADR-0028):Raxol.Agent.Auxiliaryroutes background tasks (curation, user-model derivation, future titling/triage) to a cheap model per task kind instead of the frontier model.resolve/2maps a task kind to its slot'sExecutorConfig;resolve_chain/2adds the slot's fallback chain terminating at the primary executor;select/2walks the chain throughBackend.Selectorwith an injectable availability predicate. Config is anauxiliary:slot map keyed by task kind with adefault_auxcatch-all.SelfImproveandUserModelconsult the resolver when no explicit backend/model is set, keeping explicit config as an override; withauxiliary:unset every kind resolves to the primary executor.
Changed
Raxol.Agent.Memory.Store.EtsDETS: migrated the hand-rolled:detsopen/persist/delete/clear/sync/close to the sharedRaxol.Core.Stores.Detshelper. Behaviour-preserving; the existing store tests pass unchanged.
[2.4.0] - 2026-04-14
Added
- Phase 14B: Xochi Integration: Xochi as default agent-facing protocol for cross-chain payments. Cash-positive with tier-based fees (0.10-0.40%). Riddler solves intents behind the scenes.
Raxol.Payments.Xochi.Client: HTTP client for Xochi intent API (quote, execute, status, history)Raxol.Payments.Xochi.Schemas: 5 typed structs (QuoteRequest, QuoteResponse, ExecuteRequest, ExecuteResponse, IntentStatus)Raxol.Payments.Protocols.Xochi: full intent flow:get_quote/2->execute/3(EIP-712 wallet signing) ->poll_status/3Raxol.Payments.Riddler.Client: Commerce API client (B2B/direct solver access, not default)Raxol.Payments.Router: cross-chain routes to:xochi, privacy to:xochi, same-chain stays:x402
- Phase 14C: PXE-Bridge Integration: Aztec Private eXecution Environment as settlement target for high-trust privacy tiers.
Raxol.Payments.Pxe.Client: JSON-RPC 2.0 client (aztec_createNote, aztec_getVersion, /status)Raxol.Payments.Pxe.Schemas: CreateNoteParams, CreateNoteResult, HealthStatusRaxol.Payments.PrivacyTier: Glass Cube model (6 tiers: open, public, standard, stealth, private, sovereign), attestation gating, downgrade logic- Router settlement routing with trust-score-aware privacy depth
- Phase 14D: Stealth Settlement: Full ERC-5564/ERC-6538 in
Xochi.Stealth(~300 LOC).- ECDH stealth address derivation (secp256k1)
- View tag scanning (256x speedup, 1:256 false positive rate)
- Domain-separated key derivation from EVM signature
- Meta-address encode/decode (st:eth:0x format)
- 44 tests (32 unit + 12 e2e), stress-tested 500 round-trips at 0% failure
- Phase 14E: ZKSAR + Trust Tiers: Zero-knowledge attestation verification and trust scoring.
Raxol.Payments.Zksar: 6 ZK proof type verification (type, expiry, issuer, structure), batch verify, JSON parsingRaxol.Payments.Zksar.TrustScore: diminishing-returns aggregation:score = sum(weight_i / ln(rank + 1)), capped at 100- PrivacyTier attestation requirements per tier, downgrade logic
- Router attestation-aware routing with
trust_score_for/1
- AutoPay wiring:
Backend.HTTPaccepts:req_pluginsfor transparent HTTP 402 handling.Raxol.Payments.Req.AgentPlugin.auto_pay/1builds the closure. - Riddler solver wiring (ADR-0005): 9 Xochi endpoints, fee policy (5 tiers + privacy premiums), stealth/ERC-4337 settlement, ZKSAR attestation, EIP-712 typed data.
- raxol_liveview package: TerminalBridge, TEALive, TerminalComponent, 5 themes, CSS asset. 37 tests.
- raxol_plugin package:
use Raxol.Pluginmacro, API facade, Manifest, Testing helpers, generator. 50 tests. - Hex publishing: All packages at v2.4.0 with LICENSE.md, README.md, package metadata, version-constrained path deps. Publishing order: raxol_sensor + raxol_core -> raxol_terminal/mcp/plugin/liveview -> raxol -> raxol_agent -> raxol_payments.
Changed
- Deprecated
Protocols.Riddler: now delegates to Xochi internally - All sub-packages bumped to v2.4.0 (raxol_payments stays at 0.1.0)
- Path deps now include version constraints (
~> 2.4) for Hex compatibility
Fixed
- Dashboard demo:
status_dot/1used identical character for all scheduler load levels; now uses distinct ASCII indicators per threshold - String.to_atom on external input: Replaced 6 unsafe catch-all
String.to_atom(s)in schema parsers with explicit clauses +:unknown/nilfallback - Duplicated
maybe_put/3: Extracted toSchemas.put_non_nil/3shared helper - Dialyzer specs: Tightened
{:error, term()}to actual error tuple shapes inmetrics.ex - QuoteRequest validation: Added
validate/1with eth address format checks - Attestation filtering: PrivacyTier now filters attestations by
valid: true
[2.3.2] - 2026-03-30
Added
- Headless session manager (
Raxol.Headless): GenServer managing non-interactive TEA app instances in:agentenvironment. Start from module or .exs file, take text screenshots, send keystrokes, read model state. Used by MCP tools and future test harness. - MCP tools for Claude Code: 6 tools (
raxol_start,raxol_screenshot,raxol_send_key,raxol_get_model,raxol_stop,raxol_list) injected into Tidewave at dev startup. Tidewave MCP proxy script for stdio transport. - ADR-0012: MCP as Rendering Target: Architecture decision formalizing MCP as a first-class rendering target. Automatic tool derivation from widget tree via ToolProvider behaviour, focus lens with mouse tracking, context tree as MCP resources, agent-MCP symmetry, multi-surface cockpit vision (terminal, MCP, Telegram, speech, watch). Category theory foundations for design and property-based test invariants.
- Playground resize handling: Fixed resize event propagation in playground demos.
Changed
- Updated
docs/core/ARCHITECTURE.mdwith MCP rendering target section - Updated
CLAUDE.mdwith raxol_mcp package, dependency graph, and consolidated namespace - Updated
IDEAS.mdwith MCP architecture, functor family, multi-surface cockpit, category theory - Updated
docs/adr/README.mdwith ADR-0012 and new "AI & MCP" category - Dispatcher and Engine refactored under 800 LOC each
- Credo strict: 0 issues (was 373, resolved via function extraction and pattern matching)
[2.3.0] - 2026-03-27
182 commits, 1,030 files changed, net -39K LOC (89,793 insertions, 129,283 deletions).
Added
- Distributed swarm subsystem: libcluster integration with gossip, EPMD, DNS, and custom Tailscale strategy. CRDTs (LWW-Register, OR-Set) for shared state. Node health monitoring, seniority-based leader election, bandwidth-aware message routing. 11 modules, 2,100+ lines.
- AI agent framework:
use Raxol.Agentfor TEA-based agents with OTP supervision. Agent discovery via Registry, typed inter-agent messaging, headless or rendered. Three command types: async (SSE streaming), shell (Port), inter-agent. Team supervision viaAgent.Team. Backend.HTTP streams across Anthropic, OpenAI, Ollama, Kimi, and Lumo (U2L encryption). Tiered fallback detection. - Interactive playground: 28 demos across 8 categories (input, display, feedback, navigation, overlay, layout, visualization, effects). Search, filter by category/complexity, help overlay. Charts as View DSL widgets. SSH serving with
mix raxol.playground --ssh. - Time-travel debugging:
Raxol.start_link(MyApp, time_travel: true)snapshots everyupdate/2cycle into a circular buffer. Step back/forward, jump to any point, restore state. Recursive map diffing. Zero cost when disabled. - Session recording and replay: Asciinema v2 format. Capture with timestamps, replay with pause/seek/speed controls. Stream replay.
- Sandboxed REPL:
Code.eval_stringwith spawn_monitor timeout, IO capture via group_leader swap, persistent bindings. Three sandbox levels: unrestricted, standard, strict (whitelist-only, safe for SSH). Available asmix raxol.repl. - Nx/Axon adaptive ML: Optional Nx vectorized fusion, Axon MLP recommender for layout, FeedbackLoop training. Gated behind
Code.ensure_loaded?. - Plugin system: Phase 1 mission plugin system (4 modules) with lifecycle management
- AGI Cockpit Console: Phase 2 cockpit with uptime panel, dependency checker, crash recovery, state machine
mix raxol.new: Project generator with 4 templates (basic, phoenix, ssh, agent)mix raxol.demo: 4 built-in runnable demosmix raxol.check: Unified quality gate (format, compile, credo, dialyzer, security, test)- Benchmark suite: Comparative benchmarks against Ratatui, BubbleTea, and Textual
- 746+ new tests across 21 modules (357 for core, 233 for runtime, 156 for plugins, 84 for converter/keyboard/engine/shutdown/initializer/scheduler)
- Mouse click hit testing for buttons
- Agent semantic view tree
- Unified image facade + View DSL
image/1 - Event capture phase (W3C-style dispatch), event bubbling, style inheritance through component tree
- HEEx parser for terminal compilation
- Sixel real dithering + image cache
- CODE_OF_CONDUCT.md, SECURITY.md
Fixed
- 3 real bugs found via test coverage: converter pipe BadMapError, keyboard CaseClauseError on quit/debug, unreachable ctrl_c/ctrl_q clauses
- 6 flaky CI tests (Registry race conditions, ETS cleanup, JIT warmup timing, behavior_tracker race)
- Windows CI (GPG path separators, IOTerminal backend)
- Input parser multi-byte sequences + mouse tracking
- REPL sandbox hardening, capped eval history
Changed
- Deleted ~35K lines of dead code (CQRS, EventSourcing, Pipeline, stale stubs)
- Phoenix is now an optional dependency
- All examples modernized to TEA pattern
- Zero credo warnings, zero dialyzer regressions
- Refactored large files into focused submodules (CSI handlers, sensor, adaptive, orchestrator)
- 6,484 tests passing, 0 failures, 74 excluded by environment
[2.2.0] - 2026-03-22
Added
- Widget examples for Viewport, CodeBlock, MarkdownRenderer, MultiLineInput
- Modal test file (22 tests) and Terminal test file (16 tests) verified
Changed
- Rewrote
examples/apps/todo_app.exfrom broken LiveView/HEEx to TEA pattern - Hex package readiness:
mix hex.buildsucceeds (1.8MB) - 14 deps marked optional (Ecto, PostGres, bcrypt, mogrify, contex, HTTPoison, ex_cldr, phoenix_ecto)
- Excluded compiled
.so/.obinaries from hex package - Moved esbuild/dart_sass to dev-only
- Replaced
Ecto.UUID.generate()withUUID.uuid4()in emulator struct - Updated package description
[2.1.0] - 2025-02-27
Added
ScreenBuffer.Scroll Module - Complete scroll operations for terminal buffer
- Scroll region management (set/get scroll regions)
- Basic scroll operations (scroll_up/down with configurable line counts)
- Region-specific scrolling (scroll within defined regions)
- Scrollback buffer management (save, clear, get with limits)
- Scroll position tracking for viewing history
- VT100 index operations (IND/RI for cursor-triggered scrolling)
Core.Performance Module - ETS-backed performance statistics tracking
- Frame timing and FPS calculation
- Memory usage monitoring
- Sample-based metrics with configurable limits
- Non-blocking initialization (works without GenServer)
Fixed
Dialyzer Errors - Resolved all dialyzer warnings in plugin modules
- plugin_supervisor: Explicit handling of Task.Supervisor return values
- beam_analyzer: Precise error types for analysis_result
- capability_detector: Tightened policy and capabilities types
- Added documented ignore patterns for safe supertype specs
Orphaned Tests Cleanup - Removed test files for non-existent modules
- Deleted adaptive_framerate_test.exs (module removed in 9f27ae77)
- Deleted monitor_test.exs (module never existed)
- Deleted gpu_renderer_test.exs (module never existed)
- Deleted render_server_test.exs (module never existed)
- Tagged termbox2 NIF loading test with :docker for CI compatibility
Changed
- Test Suite - Improved test stability and reduced flaky failures
- 4669 tests passing with 0 failures
- Removed ~750 lines of orphaned test code
[2.0.1] - 2025-12-04
Added
Plugin Visualization Integration - Complete Sixel rendering in UI components
- Implemented
create_sixel_cells_from_buffer/2to bridge plugin and terminal Sixel rendering - Integrated with
Raxol.Terminal.ANSI.SixelGraphics.process_sequence/2for native processing - Added pixel buffer to Cell grid conversion with palette-to-RGB color mapping
- Implemented
CI Root Cause Analysis Documentation (2025-12-06)
- Documented three distinct root causes affecting nightly builds with ready-to-apply solutions
Fixed
- Test Type Warnings - Removed unreachable pattern matching clauses in DCS handler tests
- Nightly Build CI/CD Pipeline Stabilization (2025-12-06)
- Fixed Erlang :cover module crashes on NIF beam files (OTP 27.2/28.2)
- Fixed Hex archive OTP version conflicts
- Fixed macOS performance test timing issues
- Fixed Elixir 1.19.0 LiveComponent lifecycle
- Result: 43% -> 93% CI success rate
CI/CD workflow fixes
Fixed failing GitHub Actions workflows and updated to latest Elixir/OTP versions.
Documentation
- Sixel Graphics Rendering - Documented complete Sixel implementation
- Clarified that full Sixel rendering pipeline is implemented and tested
- Updated integration test with proper assertions for Sixel pixel rendering
- Removed outdated TODO comments suggesting Sixel was incomplete
- Core rendering complete: Parser, Graphics module, DCS Handler, Buffer operations, Cell support
- All Sixel tests passing (parser, graphics, integration)
Changed
- Updated to Elixir 1.19.0 and OTP 28.2 across all CI workflows
ci-unified.yml- Main CI pipelinenightly.yml- Nightly builds with test matrix (1.17.3/1.18.3/1.19.0 + OTP 27.2/28.2)regression-testing.yml- Performance and memory regression testsperformance-tracking.yml- Performance benchmarksrelease.yml- Release workflow
Fixed
Audit.LoggerTest - Confirmed all 28 tests passing (verified 2025-12-05)
- Previous concern about
:events_must_be_listerror was resolved - Full test coverage for audit logging system at
test/raxol/audit/logger_test.exs
- Previous concern about
Window Server Tests - Re-enabled and fixed all 15 tests (verified 2025-12-05)
- Added
update_configimplementation towindow_manager.exandwindow_manager_server.ex - Re-enabled 4 test describe blocks: window splitting, operations, focus, configuration
- Updated test assertions to match actual implementation behavior
- All window management functions verified working (split, resize, move, title, etc.)
- Added
Scroll Server Tests - Re-enabled and fixed all 16 tests (verified 2025-12-05)
- Added
scroll_regionfield toRaxol.Terminal.Buffer.Scrollstruct - Implemented
set_scroll_region/3andclear_scroll_region/1functions - Re-enabled scroll region test describe blocks
- Full scroll buffer functionality verified
- Added
Meta-Package Configuration - Configured root raxol package as meta-package (2025-12-05)
- Added path dependencies to
apps/raxol_core,apps/raxol_plugin,apps/raxol_liveview - Root package now serves as convenient meta-package for users wanting all features
- Individual packages remain independently publishable for modular adoption
- Updated package description to clarify meta-package purpose
- Verified all package READMEs use GitHub links correctly
- Tested independent compilation of all packages
- Ready for Hex.pm publishing with documented workflow
- Added path dependencies to
Code Duplication Cleanup - Removed duplicate files between root and apps (2025-12-05)
- Removed 4 duplicate files from root
lib/raxol/core/:box.ex,buffer.ex,renderer.ex,style.ex - Files now exist only in
apps/raxol_core/lib/raxol/core/where they belong - Root package correctly depends on apps packages via path dependencies
- Prevents module naming conflicts when users install meta-package
- All tests passing after cleanup, compilation clean with zero warnings
- Removed 4 duplicate files from root
Command History Integration - Fully integrated command history tracking (2025-12-05)
- Added
history_bufferfield to Emulator struct for tracking command history - Initialized HistoryBuffer in all emulator constructor functions (basic and full)
- Implemented automatic command tracking in
Emulator.process_input/2 - Commands accumulate in
current_command_bufferand are added to history when newline is encountered - Empty commands are correctly ignored (not added to history)
- Integration test now fully validates history functionality
- History accessible via
Raxol.Terminal.HistoryManagerAPI
- Added
Theme System Implementation - Completed theme loading and accessibility integration (2025-12-05)
- Terminal Handlers: Implemented proper theme loading in
lib/raxol/handlers/terminal_handlers.ex- Uses
Raxol.Themes.load_theme/1to load themes from predefined names or JSON files - Converts basic theme structure to full theme structure with all required fields
- Supports "default", "dark", "light", and "high_contrast" predefined themes
- Proper error handling for theme loading failures
- Uses
- High Contrast Accessibility: Implemented theme system integration in
lib/raxol/ui/accessibility/high_contrast.ex- Integrates with
Raxol.UI.Theming.ThemeManager.update_palette/2 - Applies all theme colors to the UI system (background, foreground, accent, etc.)
- High contrast themes now properly update the system palette
- Includes logging and graceful error handling
- Integrates with
- Terminal Handlers: Implemented proper theme loading in
LOW PRIORITY Enhancements - Completed stubbed features and tests (2025-12-05)
- Git Integration Diff Rendering: Implemented colorized diff view in example plugin
- Executes
git diffand parses output - ANSI color formatting: green for additions, red for deletions, cyan for hunks
- Bold file headers, dimmed meta information
- Proper line truncation and padding for terminal width
- Error handling for failed git operations
- Executes
- StateManager Delete Function: Enabled existing delete functionality
- Function was already implemented but test was commented out with TODO
- Enabled test assertions for
delete_state/2 - Verified support for both simple and nested key deletion
- Works with ETS and process storage strategies
- CharsetManager Single Shift: Implemented VT100 single shift character set support
- Added
single_shiftfield to CharsetManager struct - Implemented
apply_single_shift/2with guard clause for :g2 and :g3 - Implemented
get_single_shift/1to return current shift state (nil when inactive) - Added
clear_single_shift/1helper to clear shift after processing one character - Updated
reset_state/1to properly reset single_shift field - Supports VT100 SS2 (ESC N) and SS3 (ESC O) escape sequences
- Added
- Rainbow Theme Plugin Command System: Implemented proper command registration
- Added
get_commands/0callback for plugin framework integration - Implemented command handlers: rainbow_start, rainbow_stop, rainbow_speed, rainbow_palette, rainbow_next, rainbow_help
- Each handler returns
{:ok, state, message}or{:error, message, state}tuple - Commands automatically registered/unregistered by plugin framework
- Updated documentation for register_commands/unregister_commands stubs
- Added
- Git Integration Diff Rendering: Implemented colorized diff view in example plugin
Nightly Build Workflow (
.github/workflows/nightly.yml)- Added test exclusions (
--exclude slow --exclude integration --exclude docker) to prevent CI timeouts - Added
timeout-minutes: 20andcontinue-on-error: trueto Dialyzer steps - Added fallback for integration tests that may not exist
- Added test exclusions (
Regression Testing Workflow (
.github/workflows/regression-testing.yml)- Fixed benchmark commands that incorrectly used
--jsonflag (not supported) - Changed output capture from JSON to text with
tee - Removed invalid
command -vchecks for mix tasks - Simplified memory benchmark execution
- Fixed benchmark commands that incorrectly used
Memory Benchmark Scripts (
bench/memory/*.exs)- Removed
Mix.install()blocks that conflict with project dependencies when run viamix run - Fixed
terminal_memory_benchmark.exs,plugin_memory_benchmark.exs,load_memory_benchmark.exs - Scripts now rely on project dependencies from mix.exs
- Removed
[2.0.0] - 2025-10-05
Major Release - Modular Architecture
Modular package architecture for incremental adoption: core modules, LiveView integration, Spotify plugin showcase, documentation overhaul, package split, and feature additions.
Package structure
Four independent packages now available:
raxol_core(v2.0.0): Buffer primitives, zero dependencies, <100KBraxol_liveview(v2.0.0): Phoenix LiveView integrationraxol_plugin(v2.0.0): Plugin framework with behavior APIraxol(v2.0.0): Meta-package including all packages
Phase 1: Core Modules
- Raxol.Core.Buffer: Terminal buffer operations (<1ms for 80x24)
- Raxol.Core.Renderer: Pure functional rendering with diff calculation
- Raxol.Core.Box: Box drawing with multiple border styles (single, double, rounded, heavy, dashed)
- Raxol.Core.Style: ANSI color and style management
- Documentation: BUFFER_API.md, GETTING_STARTED.md, ARCHITECTURE.md
- Examples: hello_buffer, box_drawing
Phase 2: LiveView Integration
- Raxol.LiveView.TerminalBridge: Buffer to HTML conversion with caching
- Raxol.LiveView.TerminalComponent: LiveComponent for embedding terminals
- Event handling: keyboard, mouse, paste, focus/blur
- Five themes: Nord, Dracula, Solarized Dark/Light, Monokai
- Performance: 1.24ms avg rendering (60fps target achieved)
- CSS grid layout with accessibility features
Phase 3: Spotify Plugin Showcase
- Full Spotify Web API integration with OAuth 2.0
- Six operational modes: auth, main, playlists, devices, search, volume
- Complete keyboard controls and modal navigation
- API client with request/oauth2 dependencies
- Authentication flow and token management
- Documentation: SPOTIFY.md with setup guide
- Four working examples with integration patterns
Phase 4: Documentation Overhaul
- Getting Started: QUICKSTART.md (5/10/15 min tutorials)
- Core Concepts: CORE_CONCEPTS.md, MIGRATION_FROM_DIY.md
- Cookbook: 5 practical guides (LiveView, VIM, Performance, Commands, Theming)
- Feature docs: VIM, Parser, Search, Filesystem, Cursor Effects
- 75% documentation reduction via DRY consolidation (5000+ lines -> 1250 lines)
- Beginner-friendly with clear migration paths
Phase 5: Modular Package Split
- Independent packages with clear dependency boundaries
- Path-based dependencies for monorepo development
- Each package fully documented with README, LICENSE, CHANGELOG
- All packages compile independently with zero warnings
- Ready for Hex.pm publishing
Phase 6: Feature Additions
- VIM Navigation: hjkl movement, gg/G jumps, search (/ ?), word movement (w b e), visual mode
- Command Parser: tokenization, tab completion, history navigation, aliases, fuzzy search
- Fuzzy Search: fzf-style matching with scoring, highlighting, case-sensitive/insensitive modes
- Virtual Filesystem: ls, cat, cd, pwd, mkdir, rm with absolute/relative paths
- Cursor Effects: rainbow, comet, minimal presets with smooth interpolation
Test results
- Total: 2147 tests (2147 passing, 0 failing, 49 skipped)
- Test pass rate: 100%
- Property tests: 58/58 passing
- Zero compilation warnings with --warnings-as-errors
Performance
- Parser: 0.17-1.25μs (Phase 0 baseline maintained)
- Core buffer operations: <1ms for 80x24 buffers
- LiveView rendering: 1.24ms avg (60fps achieved)
- VIM navigation: <1μs per movement
- Command parser: ~5μs per parse/execute
- Fuzzy search: ~100μs for 1000-line buffer
- Filesystem operations: ~10μs per command
- Cursor effects: ~7μs per update
Breaking changes
- None - v2.0 packages are additive
- Existing v1.x code continues to work with root package
- New modular packages available for incremental adoption
- See MIGRATION_FROM_DIY.md for upgrade strategies
Migration Path
- Current v1.x users: No changes required, continue using root package
- New v2.0 users: Choose packages based on needs
- Minimal:
{:raxol_core, "~> 2.0"}(just buffers) - Web: Add
{:raxol_liveview, "~> 2.0"}(LiveView integration) - Full:
{:raxol, "~> 2.0"}(everything included)
- Minimal:
Documentation Added
- Core: BUFFER_API.md, GETTING_STARTED.md, ARCHITECTURE.md, PHASE1_COMPLETION.md
- Getting Started: QUICKSTART.md, CORE_CONCEPTS.md, MIGRATION_FROM_DIY.md
- Cookbook: LIVEVIEW_INTEGRATION.md, VIM_NAVIGATION.md, PERFORMANCE_OPTIMIZATION.md, COMMAND_SYSTEM.md, THEMING.md
- Features: VIM_NAVIGATION.md, COMMAND_PARSER.md, FUZZY_SEARCH.md, FILESYSTEM.md, CURSOR_EFFECTS.md, feature README.md
- Plugins: SPOTIFY.md with OAuth setup guide
Examples Added
- Core: 01_hello_buffer/, 02_box_drawing/
- LiveView: 01_simple_terminal/ with event handling
- Plugins: counter.exs, 4 Spotify examples with integration patterns
Benchmarks Added
- Core: buffer, renderer, style, box, comprehensive
- Features: vim, parser, fuzzy, filesystem, cursor, comprehensive
- LiveView: rendering benchmark with 60fps validation
[1.19.0] - 2025-10-01
Added
- Distributed Session Support: Complete multi-node session management system
- DistributedSessionRegistry with consistent hashing for optimal session distribution
- Session affinity support (cpu_bound, memory_bound, io_bound, network_bound)
- Node discovery and heartbeat monitoring for cluster health
- Load balancing with automatic rebalancing during topology changes
- SessionReplicator with configurable replication strategies (immediate, eventual, quorum, best_effort)
- Vector clock-based conflict-free replicated data types (CRDTs)
- Anti-entropy mechanisms for replica drift correction
- Performance-aware replication with load monitoring
- SessionMigrator with multiple migration strategies (hot, warm, cold, bulk)
- Automatic node evacuation during maintenance or failures
- Intelligent failover with dependency-aware restart ordering
- Migration rollback capabilities for failure recovery
- DistributedSessionStorage with multi-backend support (ETS, DETS, Mnesia)
- Automatic data sharding with configurable shard counts
- Data compression and encryption at rest capabilities
- Write-ahead logging for durability guarantees
- Comprehensive test framework with 50+ test scenarios and fault injection
Performance
- Session location: < 1ms (consistent hashing)
- Hot migration time: < 500ms for typical sessions
- Replication sync: < 5ms (vector clock-based)
- Storage operations: < 10ms (multi-backend optimized)
- Failover time: < 2s (automatic with replicas)
- Memory overhead: < 5MB for 10,000+ sessions across cluster
Technical Components
- DistributedSessionRegistry: Session location with consistent hashing
- SessionReplicator: Multi-node replication with vector clocks
- SessionMigrator: Live migration with 99.9% availability preservation
- DistributedSessionStorage: Multi-backend storage with persistence
- DistributedSessionTestHelper: Comprehensive testing framework
[1.18.0] - 2025-09-29
Added
- Enhanced Error Recovery: Comprehensive self-healing system
- RecoverySupervisor with adaptive restart strategies
- ContextManager with TTL-based state preservation (< 1ms access)
- DependencyGraph with intelligent restart ordering
- EnhancedPatternLearner with ML-based strategy recommendation
- RecoveryWrapper for transparent process enhancement
Performance
- Context retrieval: < 1ms (ETS-backed)
- Recovery decision time: < 5ms (pattern-based)
- Graceful degradation: < 100ms activation
- Memory overhead: < 2MB for 1000+ contexts
[1.17.0] - 2025-09-25
Changed
- IO.puts/inspect Migration: Migrated 524+ raw IO calls to structured logging
- 37 files migrated to centralized logging system
- Context-aware replacement strategies by module type
- Smart message-level detection (error, warning, info)
- Zero compilation errors maintained
Added
- Automated migration tools for systematic conversion
- Log.console for debug/test modules
- Log.log_inspect with label support
[1.16.0] - 2025-09-20
Changed
- Logger Standardization: Centralized logging across 144 files
- 733 Logger calls standardized with enhanced functionality
- Module-aware logging with automatic context detection
- Performance timing with built-in execution measurement
- Environment-aware console logging
Added
- Bulk Logger Migrator tool
- Alias Syntax Fixer for import conflicts
- Structured context with metadata enrichment
[1.15.2] - 2025-09-15
Changed
- Module Naming Cleanup: Removed "unified"/"comprehensive" qualifiers
- 25 files renamed with descriptive, specific names
- Updated all imports, aliases, and references across 170+ files
- Zero breaking changes, all functionality preserved
Examples
unified_registry.ex->global_registry.exunified_config_manager.ex->config_server.exunified_collector.ex->metrics_collector.ex
[1.15.0] - 2025-09-10
Added
- BaseManager Migration Complete: 170 modules migrated (100%)
- SSH Session module (final migration)
- Standardized error handling and supervision
- Consistent functional patterns throughout
- Zero compilation warnings achieved
Summary
- Wave 1-22 completed across versions v1.7.5 to v1.15.0
- Better OTP supervision tree integration
- Reduced boilerplate significantly
[1.5.4] - 2025-09-26
Major Release - Code Consolidation & BaseManager Pattern
Added
BaseManager Pattern: Unified behavior for GenServer-based modules
- Advanced BaseManager pattern adoption across 22+ modules
- Consistent lifecycle management and error handling
- Standardized init, call, cast, and info handlers
- Enhanced state management and configuration handling
TimerManager Integration: Centralized timer management system
- Unified timer intervals and scheduling across core modules
- Reduced timer pattern duplication throughout codebase
- Improved timer lifecycle and cleanup handling
Fixed
Zero Compilation Warnings: Achieved strict error checking compliance
- Resolved all @doc and @impl attribute conflicts
- Fixed unused variable warnings across all modules
- Enhanced pattern matching in CSI handlers
- Added proper mock implementations for testing
Enhanced Test Coverage: Improved to 99.8% (1632/1635 tests passing)
- Tagged performance tests appropriately
- Resolved environment-dependent test failures
- Fixed BaseManager conversion issues
Performance
Ultra-fast Operations: Parser 0.17-1.25μs | Render 265-283μs
- Maintained Memory Efficiency: <2.8MB per session
- Exceeded Frame Rate: 3,500+ FPS capability
[1.4.1] - 2025-09-16
Major Release - Zero Warnings & Enhanced Developer Experience
Added
Automated Type Spec Generator:
mix raxol.gen.specs- Tool for automatic type specification generation- Intelligent type inference based on function/argument naming patterns
- Supports dry-run, interactive, and backup modes
- Handles guard clauses and pattern matching correctly
- Generated 12,000+ type specs across the codebase
- Full integration with Dialyzer for validation
Unified TOML Configuration System:
Raxol.Config- Centralized configuration management- TOML-based configuration with environment-specific overrides
- Runtime configuration updates without restarts
- Automatic validation and hot-reload capabilities
- GenServer-based with comprehensive API
- Support for development, test, and production environments
Enhanced Debug Mode:
Raxol.Debug- Four-level debugging system- Debug levels:
:off,:basic,:detailed,:verbose - Performance profiling with time_debug and inspect_debug
- Process state dumping and debug breakpoints
- Automatic performance monitoring (memory, run queue)
- Export debug sessions to JSON for analysis
- Component-specific debugging capabilities
- Debug levels:
Changed
Compilation Quality: Achieved ZERO compilation warnings (reduced from 88)
- Fixed all undefined function warnings (49 -> 0)
- Resolved unused variable warnings (15 -> 0)
- Fixed Logger.warn deprecation warnings
- Created missing modules and corrected all API references
- Full
--warnings-as-errorscompliance
Test Suite Excellence: 2134 tests total, 99.8% pass rate - All critical issues resolved
- Fixed TestBufferManager compilation error
- Fixed plugin system JSON encoding issues (5 failures resolved)
- Fixed character set timeout issue
- Fixed MouseHandler test failures (URXVT button decoding, drag detection)
- Fixed EraseHandler integration with UnifiedCommandHandler
- Fixed cursor save/restore struct field access bugs (position vs x/y)
- Added EmulatorLite support to command executor pattern matching
- Fixed nil handling in history tracking for minimal emulators
- Resolved all test stability issues and parallel execution conflicts
Compilation Quality: ZERO compilation warnings achieved
- Full ElixirLS support restored, clean compilation with
--warnings-as-errors - All behaviour callbacks implemented correctly
- Fixed all StateManager, BufferManager, and EventManager references
- Full ElixirLS support restored, clean compilation with
World-Class Benchmarking Infrastructure: Complete overhaul from 21 to 11 core modules
- New Benchmark Config Module: Profile-based configuration with environment awareness
- Statistical significance testing (95% confidence level)
- Dynamic threshold calculation based on historical variance
- Environment-specific adjustments (CI/local)
- Comprehensive metadata tracking (Git SHA, system info, versions)
- Enhanced
mix raxol.bench: Production-ready benchmarking- Performance regression detection with configurable thresholds
- Interactive HTML dashboard with Chart.js visualization
- Comprehensive suites: parser, terminal, rendering, memory, concurrent
- Baseline metrics storage and comparison
- Advanced Analytics: P50-P99.9 percentile tracking, outlier detection, trend analysis
- Competitor Comparison Suite: Direct benchmarking against Alacritty, Kitty, iTerm2, WezTerm
- Benchmark DSL: Idiomatic Elixir macro-based benchmark definitions
- New Benchmark Config Module: Profile-based configuration with environment awareness
Mix Task Consolidation: Organized task structure
mix raxol- Main command with helpmix raxol.check- All quality checksmix raxol.test- Enhanced test runnermix raxol.bench- Production-ready benchmarking with dashboardmix raxol.mutation- Refactored with functional patterns (no if/else)
Technical Debt Resolution: Major infrastructure improvements completed
- Removed deprecated terminal config backup files
- Consolidated duplicate buffer operations and configuration managers
- Refactored event system to use :telemetry exclusively with TelemetryAdapter
- Updated all dependencies to latest stable versions
- Implemented comprehensive protocol system (5 core protocols)
- Added connection pooling and circuit breakers for external services
- Standardized error handling patterns across modules
- Created DevHints system for real-time performance monitoring
- Added performance profiling tools with
mix raxol.perf
Performance & architecture
Performance Metrics: Parser 3.3μs/op | Memory <2.8MB | Render <1ms
- Code Reduction: 722 lines removed, 150+ duplicate patterns eliminated
- Module Consolidation: 43+ modules consolidated, state management unified (16->4 managers)
- Documentation: Updated README.md and CLAUDE.md with accurate commands
[1.3.0] - 2025-09-12
Codebase Consolidation (Phases 1-4) - COMPLETE
Test Helper Consolidation: Unified test infrastructure
- Consolidated 3 test helper modules into single
test/support/unified_test_helper.ex - Removed 195 lines of duplicate code while enhancing functionality
- Migrated all tests to use unified helper with zero breaking changes
- Eliminated duplicate test_helper.exs files
- Consolidated 3 test helper modules into single
Hook Implementation Consolidation: Single comprehensive hook system
- Merged 3 hook implementations into unified
Raxol.UI.Hooks.Functional - Reduced codebase by 527 lines while adding functionality
- Enhanced from 6 + 2 stubs to 8 fully implemented hooks
- Implemented task-based execution with timeout controls
- Achieved zero try/catch blocks with pure functional patterns
- Added
use_contextanduse_asyncimplementations
- Merged 3 hook implementations into unified
Repository Cleanup: Improved project organization
- Added .gitignore entries for cache, coverage, and log directories
- Archived 4 obsolete scripts replaced by mix tasks
- Removed duplicate test_helper.exs from platform_specific/
- Cleaned up inconsistent file locations
Extended DRY Consolidation: Major structural improvements
- Moved 21+ test helpers from
lib/raxol/test/totest/support/raxol/ - Created 4 common behaviors (StateManager, EventHandler, Lifecycle, Metrics)
- Flattened excessive directory nesting (reduced from 7+ to max 5 levels)
- Eliminated 150+ duplicate manager/handler/server patterns
- Clarified module responsibilities with enhanced documentation
- Moved 21+ test helpers from
Summary
- Total Lines Reduced: 900+ lines eliminated through consolidation
- Code Quality: All compilation warnings resolved, critical performance issues fixed
- Test Coverage: 98.7% maintained with enhanced test infrastructure
- Architecture: Cleaner, more maintainable codebase with consistent patterns
[1.2.1] - 2025-09-11
Code Quality Sprint (Phase 6-7) - COMPLETE
Critical Issues Fixed: All compilation errors and warnings resolved
- Fixed syntax error in
examples/snippets/advanced/commands.exs - Resolved TypeScript import errors across
examples/snippets/typescript/ - Achieved zero compilation warnings status
- Fixed all high-priority Credo issues
- Fixed syntax error in
Performance Improvements: 15+ performance optimizations completed
- Replaced inefficient list appending with proper accumulator patterns
- Optimized apply/3 usage across codebase
- Improved memory efficiency in hot paths
- Performance targets met: Parser 3.3μs/op, Render <1ms, Memory 2.8MB baseline
Code Duplication Reduction: Eliminated 5 major duplication patterns
- Created
Raxol.Utils.MapUtilsfor shared stringify_keys functionality - Consolidated duplicate implementations across modules
- Reduced duplication instances from 46 to 41
- Fixed audit module code duplication
- Created
TypeScript Support: Created missing core modules
- Added performance, events, and renderer core modules
- Created visualization and dashboard component modules
- Enhanced TypeScript example completeness
Linter Analysis Results:
- Critical Issues: [FIXED] All fixed
- High Priority Performance: [FIXED] 15+ fixed
- Code Duplication: [FIXED] 5 patterns eliminated
- Remaining: 700+ minor optimizations (low impact, deferred)
Documentation and Project Organization
Major Documentation Consolidation: Significantly improved project organization and reduced duplication
- Consolidated 5 scattered
examples/directories into single organized structure - Removed duplicate VS Code extension directory (archived
extensions/vscode/) - Streamlined main README from 280 to 128 lines (54% reduction)
- Created comprehensive documentation hub at
docs/README.md - Consolidated benchmark documentation and removed redundant example snippet READMEs
- Moved release notes to organized
docs/releases/directory
- Consolidated 5 scattered
Script Organization: Cleaned up scripts directory with new categorical structure
- Organized scripts into
ci/,dev/,testing/,quality/,db/,visualization/subdirectories - Updated
dev.shand documentation references to new script locations - Created comprehensive scripts README with usage examples
- Organized scripts into
Package Preparation: Optimized for Hex.pm release
- Updated package files list in
mix.exsto include consolidated structure - Validated package build with
mix hex.build - All required files (LICENSE.md, README.md, CHANGELOG.md) present and updated
- Updated package files list in
Space Savings: Removed approximately 15-20KB of redundant documentation
- Eliminated duplicate files and directories
- Improved DRY compliance across all documentation
- Enhanced navigation and discoverability
[1.2.0] - 2025-09-10
Sprint 28: Process-Based Test Migration - COMPLETE
Test Suite Stabilization: Fixed remaining process-based test failures
- Fixed
CommandHelper.safe_execute/1error handling to properly match error tuples - Updated
Web.SupervisorTestto handle already-running supervisor processes - Fixed
KeyboardShortcutsTestmodule alias syntax error - Resolved
performance_optimization_test.exscompilation error (duplicate ExUnit.run) - All core tests now passing (excluding keyboard shortcuts needing API updates)
- Fixed
Termbox2 NIF Testing: Enhanced termbox2 NIF test coverage
- Created comprehensive
Termbox2LoadingTestto verify NIF loading without TTY - Verified all termbox2 functions are properly exported
- Confirmed NIF compilation and shared library generation
- Added tests for priv directory structure and C source files
- All 9 termbox loading tests passing
- Created comprehensive
Dialyzer Integration Fix: Resolved compilation errors
- Fixed import conflict in
Mix.Tasks.Raxol.Dialyzer(removed duplicate Mix.Shell.IO import) - Dialyzer tasks now compile and run successfully
- Fixed import conflict in
Security Scanning Integration: Added comprehensive security tooling
- Integrated Sobelow for Phoenix security analysis
- Added mix_audit for dependency vulnerability checking
- Created
mix raxol.securitytask for unified security scanning - Checks for hardcoded secrets, insecure configurations, and file permissions
- All dependency vulnerability checks passing
Sprint 27: Technical Debt Elimination - COMPLETE
Process Dictionary Migration: Complete elimination of Process dictionary usage (20 files)
- Migrated all Process.get/put/delete calls to use
Raxol.Core.Runtime.ProcessStore - Updated test files to use ProcessStore for cross-process test synchronization
- Modified animation framework to use ProcessStore for test compatibility
- Updated demo/example files to use ProcessStore patterns
- Enhanced documentation to show ProcessStore as recommended approach
- Made tests async-safe by removing Process dictionary dependencies
- Migrated all Process.get/put/delete calls to use
Configuration System Consolidation: Unified configuration management
- Confirmed removal of redundant generated config files (dev_generated.exs, test_generated.exs, prod_generated.exs)
- Validated that main environment files (dev.exs, test.exs, prod.exs) contain all necessary configuration
- Eliminated duplication between generated and manual config files
- Maintained standard Elixir configuration patterns
Development Scripts Organization: Streamlined scripts directory
- Archived 20 unused development scripts into organized subdirectories:
scripts/archived/deprecated-by-dev-sh/- 6 scripts replaced by unified dev.sh toolscripts/archived/sprint-refactoring/- 12 scripts from module refactoring sprintsscripts/archived/old-experiments/- 2 experimental testing scripts
- Updated DEPRECATED.md with comprehensive archival documentation
- Preserved all active development tools while cleaning main scripts directory
- Archived 20 unused development scripts into organized subdirectories:
Sprint 25: Final Test Suite Resolution - COMPLETE
InputBuffer Module Implementation: Complete implementation of missing InputBuffer functionality
- Fixed module name conflicts causing compilation errors
- Implemented all 39 required functions (append, prepend, clear, size, etc.)
- Added proper overflow handling for truncate, wrap, and error modes
- Fixed error message formatting to match test expectations
- All 39 InputBuffer tests now passing (was 6 critical failures)
Major Test Issues Resolved: Fixed all remaining Sprint 25 test failures (13/13 complete)
- ColorSystem high contrast accessibility integration
- Mouse input integration tests (click/selection modes)
- UI rendering pipeline parameter validation fixes
- Interlacing mode CSI handler mappings
- InputBuffer module functionality complete
Sprint 26: Technical Debt & Codebase Cleanup - COMPLETE
Repository Organization: Comprehensive cleanup and maintenance
- Removed crash dump files (6.8MB storage freed)
- Archived Sprint 23 refactoring scripts to
scripts/archived/sprint23-refactoring/ - Created
bench/archived/for historical benchmark data - Cleaned temporary testing artifacts from root directory
Documentation & Standards: Established comprehensive development guidelines
- Created
docs/development/NAMING_CONVENTIONS.mdwith complete module naming standards - Documented Sprint 22-23 refactoring patterns (
<domain>_<function>.ex) - Verified no duplicate JSON libraries (Jason as primary)
- Applied consistent code formatting across entire codebase
- Created
Technical Debt Reduction: Addressed major backlog items
- Audited Process dictionary usage (20 files, mostly test compatibility)
- Organized 99 development scripts for better maintainability
- Eliminated development clutter and improved repository structure
- Enhanced code consistency and developer experience
Previous API Fixes (from earlier 1.2.0 work)
GenServer Delegation Corrections: Fixed parameter passing issues in FocusManager and KeyboardShortcuts modules
- Fixed all 16 FocusManager API functions to properly pass server parameter to GenServer calls
- Fixed all 14 KeyboardShortcuts API functions to properly pass server parameter to GenServer calls
- Resolves
GenServer.whereis/1errors where component IDs were being interpreted as server names - Restores intended functionality for focus management and keyboard shortcuts in production
Duplicate Filename Prevention System: Comprehensive tooling to detect and prevent duplicate filenames
- Added standalone script (
scripts/quality/check_duplicate_filenames.exs) with severity classification and rename suggestions - Added Mix task (
lib/mix/tasks/raxol.check.duplicates.ex) with configurable options and strict mode - Added Credo integration (
lib/raxol/credo/duplicate_filename_check.ex) for existing linting workflow
- Added standalone script (
Usage
# Check for duplicate filenames
mix raxol.check.duplicates
# With rename suggestions
mix raxol.check.duplicates --suggest-fixes
# Strict mode (fails on duplicates)
mix raxol.check.duplicates --strict
# Run as part of Credo checks
mix credo
Impact
- Test Suite Excellence: 100% of major test issues resolved across all sprints
- Code Quality: Comprehensive naming conventions and organization standards
- Repository Health: Clean structure with archived historical artifacts
- Developer Experience: Clear documentation and development guidelines
- Technical Debt: Systematic cleanup of backlog items for maintainability
[1.0.1] - 2025-08-11
Changed
- [SECURE] Security Validation Complete: Zero vulnerabilities confirmed via Snyk security scanning
- [DOCS] Documentation Links Fixed: Updated README with correct paths to generated docs and HexDocs
- [PERF] Performance Documentation Updated: Confirmed all targets exceeded with 3.3μs parser operations
- [PREP] Release Preparation: Fixed broken links, validated security, documented performance achievements
- Updated package documentation and performance benchmarks
- Added professional release notes for v1.0.0
- Improved test infrastructure stability
- Enhanced NIF loading reliability
v1.0 Launch Milestones Achieved
- [SECURE] Zero Security Vulnerabilities: Comprehensive security scan passed
- [DOCS] All Documentation Links Working: README and docs fully functional
- [FAST] Performance Targets Exceeded: 30x better than target (3.3μs vs 100μs)
- [ARCH] Multi-Framework Architecture: Terminal UI framework supporting React, Svelte, LiveView, HEEx, and raw terminal
[1.0.0] - 2025-08-11
Sprint 5 - Critical Architectural Fixes
ETS Table Race Conditions - FIXED
- Created
Raxol.Core.CompilerStatemodule with thread-safe ETS management - Replaced all direct
:etscalls with safe wrappers - Eliminated "table identifier does not refer to an existing ETS table" errors
- Achieved stable parallel compilation without race conditions
- Created
Property Test Improvements - MAJOR PROGRESS
- Fixed Store.update arithmetic errors with proper error handling
- Fixed Button.new API usage and style merging issues
- Fixed TextInput.handle_input to append text correctly
- Fixed tree_size calculation using integer division
- Fixed Store naming conflicts using System.unique_integer
- Reduced property test failures from 10+ to just 1
- Achieved 99.6% overall test pass rate (1406/1411 tests)
NIF Build Automation - FIXED
- Integrated termbox2_nif with elixir_make for automatic compilation
- Fixed NIF loading path resolution to check :raxol priv directory first
- Updated Makefile to copy NIF to main app priv directory
- NIF now builds automatically during
mix compile
CLDR Compilation Optimization - IMPROVED
- Optimized CLDR configuration for development environment
- Reduced to single locale and provider in dev mode
- Disabled documentation generation for faster builds
- Compilation time reduced from timeout-prone to ~25 seconds
Multi-Framework architecture - Complete
First Terminal Framework Supporting 5 UI Paradigms
- React-style components with hooks and state management
- Svelte-inspired reactive system with compile-time optimization
- Phoenix LiveView integration for real-time updates
- HEEx templates for server-side rendering
- Raw terminal control for maximum performance
Universal Features Across All Frameworks
- Actions system works with any framework
- Transitions and animations unified across paradigms
- Context API for cross-framework communication
- Slot system for component composition
- No vendor lock-in - switch frameworks anytime
Fixed - 2025-08-11
Critical NIF Loading Issues
- Fixed termbox2_nif load failure caused by
:code.priv_dir/1returning{:error, :bad_name} - Added robust fallback path resolution for NIF library loading
- Resolved Path.join/2 FunctionClauseError preventing terminal functionality
- Fixed termbox2_nif load failure caused by
UI Component API Completeness
- Added
Button.handle_click/1for button interaction handling - Added
TextInput.handle_input/2with validation support for controlled input - Added
TextInput.handle_cursor/2for cursor position management - Implemented
Flexbox.new/1,render/1, andcalculate_layout/1for flexbox layouts - Implemented
Grid.new/1,render/1, andcalculate_spacing/1for grid layouts - Added
Store.update/3alias for state management consistency
- Added
Property Test Compatibility
- Fixed 10+ property test failures in UI component testing suite
- Ensured text input validation filters invalid characters correctly
- Fixed grid spacing calculation to return proper tuple format
- Resolved flexbox child layout calculation issues
Known issues - To Be Addressed
- 54 compilation warnings remaining (mostly unused variables in Svelte modules)
- Property tests still showing some failures in component composition
- Some undefined function references in Svelte actions need resolution
[1.0.0] - 2025-08-10
Added
Svelte-Style Component System
- Complete Svelte-inspired framework bringing compile-time optimization to terminals
- Actions system with
use:directive (tooltip, clickOutside, focusTrap, draggable, autoSave, lazyLoad) - Reactive stores with automatic dependency tracking and derived values
- Reactive declarations using Svelte's
$:syntax with automatic re-execution - Transitions and animations (fade, scale, slide, fly, draw) with 60 FPS animation engine
- Context API for component communication without prop drilling (ThemeProvider, AuthProvider)
- Slot system for advanced component composition with named and scoped slots
- Template compiler with AST analysis, static content inlining, and buffer operation optimization
- Built-in components: Modal, Tabs, DataTable with slot customization
- Advanced dashboard demo showcasing all Svelte features
Property-Based Testing Suite
- Parser property tests with 10 comprehensive test properties
- Core system property tests for Buffer and Terminal state
- StreamData integration for generative testing
- Performance scaling verification tests
Demo Recording Infrastructure
- Interactive demo recording script (scripts/visualization/demo_videos.sh)
- 6 demo categories: Tutorial, Playground, VSCode, WASH, Performance, Enterprise
- Asciinema integration with GIF conversion support
- Professional demo showcase documentation
Enterprise Audit Logging System
- Comprehensive event types: authentication, authorization, data access, security, compliance, terminal operations, privacy
- Tamper-proof storage with cryptographic signatures and event encryption
- Real-time threat detection: brute force, privilege escalation, data exfiltration, reconnaissance
- Compliance reporting: SOC2, HIPAA, GDPR, PCI-DSS with automated violation detection
- SIEM integration: Splunk, Elasticsearch, IBM QRadar, Azure Sentinel
- Multiple export formats: JSON, CSV, CEF, LEEF, Syslog (RFC 5424), PDF, XML
- Full-text search with inverted indexing and configurable retention policies
Enterprise Encrypted Storage System
- Master key encryption with PBKDF2 key derivation (100,000 iterations)
- Data encryption keys (DEK) with automatic rotation and versioning
- Key encryption keys (KEK) for secure key wrapping and HSM support
- Multiple algorithms: AES-256-GCM, ChaCha20-Poly1305, AES-256-CBC, AES-256-CTR
- Transparent file and database encryption with streaming support for large files
- Ecto custom types for encrypted database fields with searchable encryption
- Compliance profiles: PCI-DSS, HIPAA, GDPR, SOX with automatic policy enforcement
- Comprehensive audit logging for all encryption operations
Developer Experience Revolution
- Interactive tutorial system with 3 comprehensive guides and GenServer-based runner
- Component playground with 20+ components, live preview, and code generation
- Professional VSCode extension (2,600+ lines) with IntelliSense, syntax highlighting, and live preview
- Sub-5-minute onboarding with comprehensive tooling ecosystem
Modern UI Framework
- CSS-like animation system with transitions, keyframes, and spring physics
- Layout engines: CSS Flexbox, CSS Grid with responsive design and breakpoints
- State management: Context API, Hooks system, Redux store, reactive streams
- Component composition: Higher-Order Components, render props, compound components
- Developer tools: Hot reloading, component preview, props validation, debug inspector
Changed
Performance Milestones Achieved
- Memory per session: 2.8MB (44% better than 5MB target)
- Created Raxol.Minimal for <10ms startup with 8.8KB footprint
- Memory efficiency score: 125.6/100 (exceeded maximum)
- Rendering: 1.3μs simple components, 0.48ms full screen
- Animation: 971K FPS max, 99.5% smoothness
Performance Breakthrough
- Parser performance: 30x improvement (648μs -> 3.3μs per operation)
- EmulatorLite architecture: GenServer-free parsing path
- SGR processor: 442x speedup using pattern matching optimization
- All tests migrated to optimized architecture
Test Suite Excellence
- Maintained 100% test pass rate (1751/1751 tests passing)
- Added comprehensive test coverage for audit and encryption systems
- Enhanced component lifecycle testing
Fixed
macOS CI Runner Issues
- Resolved Docker availability issues on macOS runners
- Configured local PostgreSQL for macOS CI
- Added platform-specific CI configuration
- Created DockerHelper for conditional test execution
CI/CD System
- Made termbox2_nif dependency optional for broader compatibility
- Fixed code formatting across all modules
- Improved driver resilience with conditional native dependency loading
- Simplified CI workflow and fixed codecov integration
Completed (Moved from TODO)
Documentation Milestones
- Comprehensive 660+ line API.md reference with 100% public API coverage
- Professional documentation (removed emojis, reduced verbosity, added YAML frontmatter)
- Created comprehensive CONTRIBUTING.md guide
- Reduced documentation redundancy (40% improvement)
- 9 Architecture Decision Records (ADRs) for key design choices
- WASH-style system documentation for session continuity
Development Tools
- VSCode extension packaged (raxol-1.0.0.vsix, 32KB) ready for marketplace
- Interactive tutorial system with 3 comprehensive guides
- Component playground with 20+ components and live preview
Test Suite Excellence
- 100% test pass rate (2681+ tests all passing)
- Fixed all performance tests with robust expectations
- Implemented GenServer cleanup patterns across test suite
- Fixed CSI Handler, LiveView tests
Code Quality
- Zero compilation warnings (100% reduction from 227)
- Replaced all stub implementations with working code
- Fixed all critical TODO/FIXME items
- Fixed module alias references
Impact
- Enterprise Ready: Production-grade audit logging and encryption for regulated industries
- World-Class Performance: Sub-millisecond parser operations suitable for high-throughput applications
- Developer Experience: Framework-level tooling matching React/Vue ecosystem expectations
- Security & Compliance: Meeting requirements for healthcare, finance, and government deployments
[0.9.0] - 2025-01-26
Added
- Complete Terminal Feature Implementation
- Mouse Handling: Full mouse event system with click, drag, and selection support
- Tab Completion: Advanced tab completion with cycling, callback architecture, and Elixir keyword support
- Bracketed Paste Mode: Complete implementation with CSI sequence parsing (ESC[200~/ESC[201~)
- Column Width Changes: Full DECCOLM support for 80/132 column switching (ESC[?3h/ESC[?3l)
- Sixel Graphics: Already comprehensive with parser, renderer, and graphics manager
- Command History: Multi-layer history system with persistence and navigation
Changed
- Test Suite Improvements
- Achieved 100% test pass rate (1751/1751 tests passing)
- Fixed terminal mode classification issues
- Improved mode manager test accuracy
- Enhanced test coverage for all new features
Fixed
- Technical Debt Resolution
- Documented 12 compilation warnings as false positives from dynamic apply/3 calls
- Fixed failing test in ModeManager.lookup_standard for correct mode classification
- Resolved terminal mode specification compliance (DEC private vs standard modes)
Impact
- Production Ready: Raxol terminal framework is now feature-complete
- Full VT100/ANSI Compliance: Complete terminal emulation with modern features
- 100% Test Coverage: Comprehensive testing ensures reliability
- Enterprise Features: Mouse, history, completion, graphics all fully operational
[0.8.1] - 2025-08-09
Added
Complete Component Lifecycle Implementation
- Added mount/unmount hooks to all 23 UI components
- Implemented @impl annotations for proper callback tracking
- Created comprehensive lifecycle documentation
API Documentation
- Added 76+ @doc annotations to Raxol.Terminal.Emulator
- Enhanced documentation for Parser and core modules
- Achieved 100% documentation coverage for public APIs
Changed
- Performance Improvements
- Removed all debug output from parser (27 debug statements eliminated)
- Cleaned up test output for better readability
- Profiled parser performance (identified 648 μs/op baseline)
Fixed
Compilation Warnings
- Reduced warnings from 52 to 15 (71% reduction)
- Fixed unreachable clause warnings in cursor functions
- Added pattern matching to distinguish struct types
- Removed unused module aliases
Test Suite
- Achieved 99.3% test pass rate (1742 tests passing, 0 failures)
- Fixed test output clarity by removing debug logging
- 13 intentionally skipped tests for unimplemented features
Technical Debt Reduction
- Standardized component lifecycle across entire UI framework
- Improved code consistency with proper @impl annotations
- Enhanced maintainability with comprehensive documentation
[0.8.0] - 2025-07-25
Added
- Phase 8: Release Process Streamlining (COMPLETED)
- Simplified
burrito.exsconfiguration (127->98 lines, 23% reduction) - Added standardized mix aliases for release tasks:
mix release.dev- Development buildsmix release.prod- Production buildsmix release.all- All platform buildsmix release.clean- Clean build directoriesmix release.tag- Create version tags
- Enhanced release script with build summaries and artifact manifests
- Streamlined version management with safety checks and validation
- Improved error handling and user feedback throughout release process
- Simplified
Changed
Release Configuration Optimization:
- Extracted common configurations into module attributes (
@base_steps,@common_config,@package_meta) - Eliminated code duplication between dev/prod profiles
- Consolidated platform-specific settings for better maintainability
- Unified package metadata across distribution formats
- Extracted common configurations into module attributes (
Release Script Enhancements:
- Added comprehensive build result tracking and reporting
- Implemented JSON manifest generation for build artifacts
- Enhanced git tagging with duplicate detection and clean working directory validation
- Improved cross-platform executable name handling
Fixed
- Release Process Reliability:
- Fixed potential issues with duplicate git tags
- Enhanced error reporting for failed builds
- Improved platform detection and build consistency
- Added proper validation for release prerequisites
Impact
- 23% reduction in release configuration complexity
- Unified release workflow across all platforms (macOS, Linux, Windows)
- Enhanced artifact tracking and build management
- Safer and more reliable version tagging process
- Improved developer experience with clear, standardized commands
[0.7.0] - 2025-07-15
Added
Refactored Buffer Server Architecture:
- Introduced
BufferServerRefactoredwith modular, high-performance design - Added
ConcurrentBuffer,MetricsTracker,OperationProcessor, andOperationQueuemodules - Improved buffer management, batch operations, and performance metrics
- Comprehensive documentation and type specs for all new modules
- Introduced
Comprehensive Test Coverage:
- Added integration and unit tests for all new buffer modules
- Enhanced test coverage for concurrent operations, metrics, and damage tracking
- Improved test reliability and isolation
Fixed
Event Handler and State Restoration:
- Fixed event handler to properly pass emulator to handlers
- Corrected terminal state restoration logic and fixed KeyError
- Added cursor-only restoration for DEC mode 1048
Debug Output Cleanup:
- Removed excessive debug output from renderer and tests
- Cleaned up verbose IO/puts statements across terminal modules
Changed
- General Code and Documentation Improvements:
- Updated and harmonized code formatting across all modules
- Improved documentation and roadmap
- Miscellaneous bug fixes and code cleanups
Removed
- Obsolete debug scripts and legacy code
Next Focus
- Continue performance optimization
- Address any remaining test edge cases
- Further improve documentation and code quality
[0.4.2] - 2025-06-11
Added
Terminal Buffer Management Refactoring:
- Split
manager.exinto specialized modules:State- Buffer initialization and state managementCursor- Cursor position and movementDamage- Damaged regions trackingMemory- Memory usage and limitsScrollback- Scrollback buffer operationsBuffer- Buffer operations and synchronizationManager- Main facade coordination
- Improved code organization and maintainability
- Enhanced test coverage
- Better error handling
- Clearer interfaces
- Split
Plugin System Improvements:
- Implemented Tarjan's algorithm for dependency resolution
- Enhanced version constraint handling
- Added detailed dependency chain reporting
- Improved error handling and diagnostics
- Optimized dependency graph operations
Component System Enhancements:
- Harmonized API for all input components
- Improved theme and style prop support
- Enhanced lifecycle hooks
- Better accessibility integration
- Comprehensive test coverage
Performance Infrastructure:
- New
Raxol.Test.PerformanceHelpermodule - Performance test suite for terminal manager
- Event processing benchmarks
- Screen update benchmarks
- Concurrent operation benchmarks
- New
Documentation Improvements:
- Completed comprehensive guides
- Enhanced API documentation
- Improved architecture documentation
- Added migration guide
- Updated component documentation
Changed
Test Infrastructure:
- Replaced
Process.sleepwith event-based synchronization - Enhanced plugin test fixtures
- Improved error handling
- Better resource cleanup
- Clear test boundaries
- Replaced
Terminal Command Handling:
- Standardized error/result tuples
- Improved error propagation
- Enhanced command handler organization
- Better test coverage
Component System:
- Migrated to
Raxol.UI.Componentsnamespace - Improved theme handling
- Enhanced style prop support
- Better lifecycle management
- Migrated to
Deprecated
- Old event system
- Legacy rendering approach
- Previous styling methods
Raxol.Terminal.CommandHistory(Replaced with new command system)
Removed
- Redundant
Raxol.Core.Runtime.Plugins.CommandsGenServer - Redundant clipboard modules
Raxol.UI.Components.ScreenModesmodule- Direct
:meckusage from test files
Fixed
SelectList Mouse Focus Bug:
- Fixed focus update on mouse clicks
- Improved test coverage
- Enhanced mouse interaction handling
Accessibility Tests:
- Fixed color suggestion tests
- Improved test reliability
- Enhanced accessibility coverage
Test Suite:
- Resolved compilation errors
- Fixed helper function scoping
- Improved test organization
Next Focus
- Address remaining test failures
- Complete OSC 4 handler implementation
- Implement robust anchor checking
- Document test writing guide
- Continue code quality improvements
[0.4.0] - 2025-05-10
Added
- Initial public release
- Core terminal functionality
- Basic component system
- Plugin architecture
- Testing infrastructure
[0.5.0] - 2025-06-12
Added
Progress Component:
- New
Raxol.UI.Components.Progressmodule with multiple progress indicators:- Progress bars with customizable styles and labels
- Spinner animations with multiple animation types
- Indeterminate progress bars
- Circular progress indicators
- Comprehensive test coverage for all progress variants
- Full documentation with examples and usage guidelines
- New
Documentation Overhaul:
- Major updates to all component documentation in
docs/components/ - Improved structure, navigation, and cross-references
- Added mermaid diagrams and comprehensive API references
- Expanded best practices and common pitfalls sections
- Major updates to all component documentation in
Test Suite Improvements:
- Enhanced test coverage and organization
- Updated test fixtures and support files
- Improved reliability and maintainability
Code Style and Formatting:
- Applied consistent formatting across all Elixir source files
- Improved code readability and maintainability
Utility Scripts:
- Added scripts for code maintenance and consistency
Changed
- Updated guides and general documentation for clarity and completeness
- Refined documentation links and fixed broken references
Removed
Obsolete migration and test consolidation guides
Native Dependency Management:
- Removed vendored
termbox2C source fromlib/termbox2_nif/c_src/termbox2 - Now uses the official termbox2 as a git submodule
- Developers must run
git submodule update --init --recursivebefore building - Updated build and documentation to reflect this change
- Removed vendored
[0.6.0] - 2025-01-27
Added
Comprehensive Documentation Renderer:
- Markdown to HTML conversion with Earmark integration
- Table of contents generation with anchor links
- Search index creation with metadata extraction
- Code block extraction and processing
- Full documentation rendering with metadata and navigation
- Graceful fallbacks when dependencies aren't available
Window Event Handling:
- Complete window event processing in terminal driver
- Resize event handling with dimension updates
- Title and icon name change processing
- Proper logging and error handling for window events
Shared Helper Modules:
Raxol.Core.Runtime.ShutdownHelperfor graceful shutdown logicRaxol.Core.Runtime.GenServerStartupHelperfor startup patternsRaxol.Core.Runtime.ComponentStateHelperfor state managementRaxol.Benchmarks.DataGeneratorfor benchmark data generationRaxol.Core.StateManagerfor shared state patternsRaxol.Terminal.Scroll.PatternAnalyzerfor scroll analysisRaxol.EmulatorPluginTestHelperfor test setup
Changed
Major Code Quality Improvements:
- Eliminated all duplicate code across the codebase
- Reduced software design suggestions from 44 to 0
- Improved modularity and maintainability
- Enhanced code organization and structure
Refactored Components:
- Removed duplicate character operations in favor of char_editor
- Extracted shared event handling in button component
- Unified scroll region logic into single helper
- Delegated duplicate auth functions to public auth.ex
- Removed embedded CharacterHandler in text_input
- Created shared color parsing delegation
- Unified component state update patterns
Test Suite Improvements:
- Created shared test helper for emulator plugin tests
- Removed duplicate cursor manager tests
- Eliminated duplicate notification plugin test file
- Improved test organization and maintainability
Removed
- Legacy and Duplicate Modules:
lib/raxol/terminal/input_manager.ex(legacy version)lib/raxol/core/cache/unified_cache.ex(duplicate of system cache)lib/raxol/terminal/buffer/char_operations.ex(duplicate functionality)test/raxol/plugins/notification_plugin_test.exs(duplicate of core version)test/raxol/terminal/cache/unified_cache_test.exs(duplicate tests)
Fixed
- All TODO Items:
- Implemented window resize processing
- Implemented window event handling
- Implemented comprehensive documentation renderer functionality
- No more TODO comments in the codebase
[0.5.2] - 2025-01-27
Added
Enhanced Demo Runner:
- Command Line Interface: Added comprehensive command line argument support to
scripts/bin/demo.exs--list: List all available demos with descriptions--help: Show detailed usage information and examples--version: Display version information--info DEMO: Show detailed information about a specific demo--search TERM: Search demos by name or description- Direct demo execution:
mix run bin/demo.exs form
- Interactive Menu Improvements:
- Categorized demo display (Basic Examples, Advanced Features, Showcases, WIP)
- Enhanced navigation with keyboard shortcuts
- Better error handling and user feedback
- Similar demo suggestions for typos
- Auto-discovery: Automatic detection of available demo modules in
Raxol.Examplesnamespace - Error Handling: Robust validation and error reporting for demo modules
- Performance Monitoring: Demo execution timing and monitoring capabilities
- Configuration Support: Optional configuration file support for customizing demo behavior
- Command Line Interface: Added comprehensive command line argument support to
Documentation Updates:
- Updated README with comprehensive demo usage instructions
- Added demo examples to Quick Start guide
- Enhanced documentation with interactive demo capabilities
Changed
- Demo Script Architecture:
- Refactored
scripts/bin/demo.exsfor better maintainability - Improved code organization with dedicated modules
- Enhanced user experience with better feedback and error handling
- Refactored
Fixed
- Demo Discovery: Resolved issues with demo module loading and validation
- User Experience: Improved error messages and help text clarity
Next Focus
- Continue enhancing demo system with additional features
- Add more comprehensive demo examples
- Implement demo recording and playback capabilities
- Enhance configuration and customization options
[0.5.1] - 2025-01-27
Added
Terminal System Major Enhancements:
- Comprehensive refactoring of terminal buffer, ANSI, plugin, and rendering subsystems
- Enhanced ANSI state machine with improved escape sequence handling
- Better sixel graphics support and parsing
- Improved mouse tracking and window manipulation
- Enhanced buffer management with unified operations
- Better character handling and clipboard integration
- Comprehensive test coverage for all terminal subsystems
Core System Improvements:
- Enhanced metrics aggregation and visualization
- Improved performance monitoring and system utilities
- Better UX refinement with accessibility integration
- Enhanced color system and theme management
- Improved application lifecycle management
UI Component Updates:
- Updated base component lifecycle management
- Enhanced input field components (multi-line, password, select)
- Improved progress spinner and layout engine
- Better rendering pipeline and container management
- Enhanced test coverage for UI components
Testing Infrastructure:
- Expanded test suite with improved coverage
- Enhanced test fixtures and support scripts
- Better plugin test organization and reliability
- Improved mock implementations and test helpers
- Added comprehensive test documentation
Documentation and Configuration:
- Added compilation error plan and critical fixes reference
- Enhanced plugin test backups and documentation
- Updated configuration and application startup
- Improved plugin events and metrics collector
- Better script organization and maintenance
Changed
Code Quality:
- Applied consistent formatting across all Elixir source files
- Improved code readability and maintainability
- Enhanced error handling and validation
- Better separation of concerns in terminal subsystems
- Standardized API patterns across components
Performance:
- Optimized terminal operations and buffer management
- Improved rendering pipeline efficiency
- Enhanced memory management and resource cleanup
- Better concurrent operation handling
Fixed
Terminal Operations:
- Fixed ANSI sequence parsing edge cases
- Improved buffer scroll region handling
- Enhanced cursor positioning accuracy
- Better window state management
- Fixed sixel graphics rendering issues
Test Reliability:
- Resolved test compilation errors
- Fixed mock implementation inconsistencies
- Improved test isolation and cleanup
- Enhanced test data management
Removed
- Obsolete UI component files
- Redundant test fixtures
- Unused configuration options
[0.5.2] - 2025-01-27
Added
Enhanced Buffer Manager Compression:
- Implemented comprehensive buffer compression in
Raxol.Terminal.Buffer.EnhancedManager - Added multiple compression algorithms:
- Simple compression for empty cell optimization
- Run-length encoding for repeated characters
- LZ4 compression support (framework ready)
- Threshold-based compression activation
- Style attribute minimization to reduce memory usage
- Performance metrics tracking for compression operations
- Automatic compression state updates and optimization
- Implemented comprehensive buffer compression in
Buffer Compression Features:
- Cell-level compression with empty cell detection
- Style attribute optimization (removes default attributes)
- Run-length encoding for identical consecutive cells
- Configurable compression thresholds and algorithms
- Memory usage estimation and monitoring
- Compression ratio tracking and statistics
Changed
- Buffer Management:
- Enhanced memory efficiency through intelligent compression
- Improved performance monitoring for buffer operations
- Better memory usage optimization strategies
Fixed
- Code Quality:
- Resolved TODO items in buffer compression implementation
- Improved code maintainability and documentation
[0.5.3] - 2025-01-27
Fixed
Enhanced Buffer Manager:
- Implemented buffer eviction logic in
Raxol.Terminal.Buffer.EnhancedManager - Added automatic pool size management to prevent memory overflow
- Resolved TODO item for buffer eviction implementation
- Improved memory management efficiency
- Implemented buffer eviction logic in
Plugin resource budgets now constrain supervised plugin work.
PluginSupervisortracks each task by plugin ID and startsResourceBudgetin its supervision tree; the monitor measures live process memory, ETS ownership, process count, and BEAM reduction share instead of returning zeroes. Manifest limits now reach the registry,:warnemits telemetry,:throttleblocks new event/filter/hook work until usage recovers, and:killterminates active tasks and unloads the plugin. End-to-end regressions cross zero-sized limits with a real supervised task for all three actions.