Releases

What shipped in each Octowright release, newest first — the curated notes the daemon prints on upgrade.

These are the same notes Octowright prints to your terminal the first time a new version starts, and the same ones octowright_status() hands your agent. They are the “why it matters” version of each release. For the exhaustive record — every fix, every commit — read CHANGELOG.md.

  1. v0.21.0 current

    2 notes

    Release notes have headlines now

    The what's-new banner used to print five ~500-character paragraphs into your daemon log. Every curated highlight now carries a short title alongside its body, so the banner is five scannable lines and the full text still reaches the agent through octowright_status. All 118 entries back to 0.7.0 were backfilled, so the archive reads the same way as this release does.

    Highlights are data, one file per version

    They moved out of a Python dict literal into src/octowright/upgrade/highlights/<version>.json, read at import. Cutting a release now ADDS a file instead of editing the top of a shared one, so two release branches cannot conflict over it. If you consume octowright_status()["upgrade"]["highlights"] programmatically, note it changed from a list of strings to a list of {title, body} -- join entry["body"] where you used to join the string.

    GitHub release v0.21.0

  2. v0.20.0

    5 notes

    WebSocket traffic can be read back

    WebSocket traffic can be read back. Every frame a page sent or received has always been captured to a sidecar, and nothing could ask for it -- so a real-time app left its most interesting traffic on disk. browser_websocket_messages and browser_websocket_summary are the read pair. The capture was also recording EMPTY payloads and had been from the start: playwright-python hands the handler the payload itself, where only Node's API wraps it in an object, so every frame persisted with no content in it.

    A failed request says why it failed

    A failed request now says why it failed. Recorded rows carried url, method and status and no body, so a 409 was recoverable only as its status code -- and the refusal reason already on the wire ({"detail": "component_allocation_required"}) is usually the whole diagnosis. Failed same-origin bodies are kept and attached to macro failures.

    Typing into a canvas keeps its shifted characters

    Typing into a canvas keeps its shifted characters. page.type() dispatches the right key payload but never holds Shift down, which a DOM input reads and a canvas app does not -- so on a BMC console echo TYPE=Ab*: arrived as echo type=ab8;, silently. Pass key_mode="keys" to press physical keys with Shift genuinely held.

    engine_health stops blaming your engine for your typo

    engine_health stops calling your engine broken when YOU sent a bad request. A file:// url left chromium reporting {"outcome": "error", "error": "ValueError"} -- byte-identical to a genuinely broken engine, because only the class name is kept. It was read as one and cost about an hour. Refused requests are now classified by type and counted separately, at octowright_status()["pool"]["refusals"].

    A hostile route pattern can't stall every browser

    A hostile route pattern can no longer stall every browser at once. A URL glob compiles to a regex inside the shared Node driver, and five wildcards against a 129-character URL measured 18 seconds -- from an eighteen-character pattern, in a driver every session shares. Patterns are now bounded before they are forwarded.

    GitHub release v0.20.0

  3. v0.19.4

    3 notes

    A dead terminal is torn down, not just forgotten

    A terminal whose connector dies is now actually cleaned up, not just forgotten. Eviction dropped the session from the registry without closing it -- and that registry holds the only reference -- so a dropped SSH connection or an exited shell leaked its transport or PTY child and its recording handle, and shutdown could no longer reach the session to tidy up. It stopped appearing in the dashboard while quietly keeping everything it held.

    SSH key auth works

    SSH key auth works. Terminal launches emitted client_key_path where the connector expects client_key, so keys were silently ignored and the connection fell through to whatever else was available.

    A terminal that died badly says so at lookup

    A terminal that died badly no longer answers like one that never existed. Every terminal_* call failed at pool lookup with 'no terminal session <id>' for a session you just watched work; the error now names the connector and how it ended.

    GitHub release v0.19.4

  4. v0.19.3

    3 notes

    An unpaired dashboard tells you how to get in

    An unpaired dashboard now tells you how to get in. The corner badge's dashboard and recording links carry no pairing code -- they cannot, since a code is single-use with a 60s TTL and the script holding one would run in the page where every site could read it -- so clicking them landed on a dashboard asserting 'No live sessions.' while sessions were running, promising a retry it had already cancelled, with the only accurate message a toast that vanished after 3.5s. It is now a blocking page that names what happened and hands you the commands.

    Dashboard pairing stops expiring while you use it

    Dashboard pairing stops expiring while you are using it. The 8-hour window was absolute from the moment you paired, so a dashboard you had been watching all day died mid-use. It now slides on use -- the open tab's own event stream keeps it alive -- under a hard 7-day ceiling.

    doctor detects a second daemon

    octowright doctor detects a SECOND daemon. check_daemon only ever reports on the one leader the lockfile names, so a systemd- or launchd-started daemon that skipped the election lock answered unrecorded while doctor called the deployment clean. The new check probes every port a leader of this deployment could hold, because which of the two the lockfile records is a race rather than a property.

    GitHub release v0.19.3

  5. v0.19.2

    3 notes

    doctor checks your followers

    octowright doctor now checks your FOLLOWERS. A follower is a subprocess its MCP client owns and it survives a leader restart by design, so upgrading octowright and restarting the daemon updates the leader and nothing else -- every connected client keeps running whatever it spawned until that client reconnects. Found live with followers two releases behind a current leader, driving browsers, while doctor reported all-PASS. It compares against the RUNNING daemon's version and warns rather than fails, because the fix is per client, not per machine.

    Dead followers stop counting as stale

    Dead followers are no longer counted as stale. octowright_status reported 8 followers 'running older code'; the two investigated were both already-exited processes, so acting on the count meant chasing ghosts. Exited followers are now filtered out and reported separately as dead_follower_count.

    One HTTP client instead of two

    One HTTP client instead of two. httpx 0.x and httpx2 both shipped, and both log the same request line under different logger names -- which put doctor --json one import away from emitting output no parser could read. Everything is on httpx2 now, including the SSRF DNS-pinning transport, whose pin was re-verified end to end rather than assumed from a green test run.

    GitHub release v0.19.2

  6. v0.19.1

    1 note

    octowright restart works again

    octowright restart works again. 0.19.0 made it hold the leader-election lock across kill -> spawn -> confirm, but it spawned the daemon without --daemon-mode, so the child ran leader election and blocked on the lock its own parent was holding. Restart waited out its whole health budget, printed 'daemon did not become healthy' and exited 1, and the daemon then started about ten seconds later. Every restart on 0.19.0 reported failure while actually working, slowly -- upgrade if you are on it.

    GitHub release v0.19.1

  7. v0.19.0

    2 notes

    doctor names a wedged coreaudiod

    octowright doctor now names a wedged coreaudiod, which is the cause behind a WebKit that cannot load any page. WebKit's GPU process calls into CoreAudio on every startup; when the HAL stops answering, that call never returns, WebKit's own watchdog kills the GPU process after about three seconds, relaunches it, and it hangs again -- so every navigation dies with no crash report anywhere. Diagnosed live: the engine probe could only say 'failed at goto', which sends you into WebKit, the wrong place. The new audio:coreaudio check costs 0.15s, runs even under --skip-engines, and prints the remedy (sudo killall coreaudiod) on the same line.

    restart can't create the split-brain it recovers from

    octowright restart can no longer create the split-brain it recovers from. It was the one spawner that never took the leader-election lock, so killing the leader let every follower's respawn race it: two healthy daemons, one port-walked to a bumped port -- and restart printed 'daemon healthy' and exited 0 because its health probe also accepts the lockfile endpoint. It now holds the lock across kill -> spawn -> confirm.

    GitHub release v0.19.0

  8. v0.18.0

    5 notes

    A browser that stops answering can't hang you forever

    A browser that stops answering no longer hangs the caller forever. evaluate, title, content and the context setup calls Playwright gives no timeout of its own are now bounded (30s by default, OCTOWRIGHT_UNBOUNDED_CALL_TIMEOUT_SECONDS). This is ON by default because the alternative is not a slow call, it is a coroutine that never returns: a real run wedged for 12.6 hours against a broken WebKit with page.on('crash') silent, because a target that merely stops replying never crashes.

    doctor tells you whether the machine or octowright is broken

    octowright doctor tells you whether the machine is broken or octowright is. It drives each engine through launch, page, goto and evaluate using raw Playwright and no octowright code, and names the step that failed -- so a bad WebKit reads as 'engine:webkit failed at goto' in seconds instead of an afternoon in the launch pipeline. It also reports the daemon, orphaned drivers and browsers, and storage permissions; --fix reaps only processes whose parent is already gone.

    browser_a11y_dragdrop drives keyboard drag-and-drop

    browser_a11y_dragdrop drives the drag-and-drop that browser_drag cannot. Accessible widgets usually implement the keyboard WAI-ARIA pattern -- grab with a key, move with keys, drop with a key -- which a synthetic mouse sequence never triggers. One atomic attempt per call, exactly one verify_* required, and a failed drop releases the grab instead of leaving the widget stuck in a state that is indistinguishable from never having grabbed at all.

    octowright_status says which engine is broken

    octowright_status now says which engine is broken. Per-engine launch health records the last outcome for chromium, firefox and webkit separately, so 'WebKit is broken on this machine, Chromium is fine' is one status call rather than an hour of a 12.6-hour incident. An engine never launched is absent rather than reported healthy -- 'no data' and 'fine' are different answers.

    An unresponsive target has a name and a notification

    An unresponsive target now has a name, a notification and a status record. It is its own crash scope rather than being folded into renderer crashes, and it deliberately does not auto-recover: the target may still be executing, and force-replacing a page that is merely slow makes things worse. Surfaced at octowright_status()['crash']['unresponsive_recent'].

    GitHub release v0.18.0

  9. v0.17.0

    5 notes

    Terminal sessions are a plugin, not core

    Terminal sessions are a plugin now, not part of core. PTY/SSH/telnet moved to the octowright-terminal distribution and core carries no terminal code at all -- no terminal package, no uterm import, no hardcoded scenario branch, no xterm renderer in the dashboard bundle. Install the terminal dependency group and set OCTOWRIGHT_PLUGINS=terminal to get the terminal_* tools back. Nothing loads by default, because a transitive dependency must not be able to extend a browser-driving daemon on its own.

    Anything can be a session kind

    Anything can be a session kind now. A distribution declares an octowright.session_kinds entry point and gets MCP tools, a scenario adapter, a capability profile, a contained artifact store, and its own dashboard renderer -- without a line of it landing in core's bundle. A plugin that cannot satisfy the contract it claims is refused at load with the offending method named, rather than failing halfway through someone's scenario and reading as a scenario bug.

    cleanup stops eating your macro artifacts

    octowright cleanup no longer eats your macro artifacts. They live under the recordings root, so the age-based sweep pruned any artifact whose files had not been touched lately -- hand-written critical points and their whole verification history, gone quietly. Age says a recording is disposable; it says the opposite about an artifact, because the one that stops changing is the one that keeps passing.

    persona delete asks first

    octowright persona delete asks first. It was taking a name and removing the tree -- live session cookies, localStorage and IndexedDB for every site that persona ever logged into, a stronger credential than the typed password we already protect. It now names what it is about to destroy; --yes skips the prompt for scripts.

    Verifying an artifact twice gives the same answer

    Verifying a macro artifact twice gives the same answer twice. A result_status check declares its expected run status under status and the evaluation reported its verdict under the same key, so the first verify rewrote "ok" to "passed" in the manifest and the second one compared the run against "passed" and failed. The verdict also reaches summary.md now -- that section was written before verification could run, so it was unreachable in production.

    GitHub release v0.17.0

  10. v0.16.4

    3 notes

    timeout_ms works on a CSS-selector click or fill

    timeout_ms actually works on a CSS-selector click or fill. It was accepted by macro lint, saved by the dashboard editor, forwarded on the ARIA path -- and popped before the selector path, which ran on the 15s default. A failing click cost 15s every time and the obvious mitigation was a no-op. browser_click/browser_fill had the same hole, so an agent had no working knob either.

    browser_list reports the headers each browser sends

    browser_list now reports what extra HTTP headers each browser is actually sending. extra_http_headers used to be write-only -- it went into Playwright's context and nothing could read it back -- so a client adopting an already-running browser could not tell a current run tag from a stale one. The three scopes (launch, page, per-URL injection) are reported separately rather than merged, because a flattened map would make a page-scoped token look browser-wide. Credential-named values are always redacted.

    The ty type gate could never fail

    The ty type gate could never fail. It filtered ty's output with startswith('error['), but concise-format lines start with the file path -- so it matched nothing and passed no matter what ty found, while its 154-entry baseline sat stale and disagreed with CI (which enforces zero). Nothing slipped through, because CI never used the script; the two just quietly disagreed.

    GitHub release v0.16.4

  11. v0.16.3

    5 notes

    Launch headers can be scoped to URL globs

    Launch headers can be scoped to URL globs: browser_launch(extra_http_headers_urls=[...]). Context-level headers ride EVERY request including cross-origin subresources, and on Chromium that makes them CORS-preflighted -- a CDN or font host that doesn't echo Access-Control-Allow-Headers rejects them outright and the page never finishes rendering. Scoping moves them onto context routes that still follow popups and new tabs.

    Injected headers follow popups and new tabs

    browser_inject_headers is a CONTEXT route now, so it follows popups and tabs opened later instead of dying at the page boundary. One caveat worth knowing: browser_mock_route is a page route, page routes are evaluated ahead of context ones, and a fulfilling handler ends the chain -- so a mock on an overlapping pattern silently wins in EITHER order. Both install sites now warn.

    browser_network_requests can prove a header applied

    browser_network_requests can prove a header applied: pass include_headers=True. It's off by default because a header map is ~7x the size of the row it rides on, which turned an unfiltered read of an ordinary page from ~6.6k tokens into ~45k. The same call had no row cap at all and could return the whole 5000-entry buffer; it now pages 200 at a time with returned/truncated in the payload.

    The what's-new notice actually reaches you

    If you are reading this, the what's-new notice works -- which it did not on the last release. 0.16.2 said this was fixed; it fixed five named test modules and added a guard that lists them by name, so a sixth one nobody had listed kept marking the version seen during make ci and eating the notice before you could see it. The isolation now lives in conftest, where it covers every spawned daemon instead of the ones somebody remembered.

    Captures, goldens and macros are locked to 0700

    Captures, goldens and macros are locked to 0700, governed by the same OCTOWRIGHT_RECORDINGS_PRIVATE knob recordings already use. They hold page text, accessibility trees and now request headers, but sat world-readable while recordings and profiles were locked. The directory is the control because atomic writes preserve an existing file's mode -- a golden first written at 0644 keeps 0644 forever, and no later save heals it.

    GitHub release v0.16.3

  12. v0.16.2

    4 notes

    /api/health reports the version actually running

    /api/health now reports the version the daemon is actually RUNNING. It used to read package metadata off disk on every request, so right after an upgrade it reported the new version while the daemon was still executing the old code -- the one question you ask it after deploying was the one it could never answer. When an upgrade is sitting on disk unadopted, the response now says so separately as installed_version, which is your cue to restart.

    The /new-tab status strip tells the truth too

    The /new-tab status strip tells the truth too: its version came from the same on-disk read, and its commit hash ran git rev-parse HEAD in whatever directory the daemon was launched from, at request time -- so switching branches under a running daemon changed the commit it claimed while the running code did not change.

    Live-daemon tests stop eating your upgrade notice

    This what's-new notice can reach you. Five live-daemon test modules spawned a real daemon without isolating the config dir, so every make ci run wrote the marker in your real ~/.config and marked the version already seen -- the notice was consumed before an upgrade ever happened.

    octowright_status reports the daemon's running version

    octowright_status() now reports the daemon's running version, and tells you when connected followers are on older code and which action fixes it (their client reconnecting -- a daemon restart cannot).

    GitHub release v0.16.2

  13. v0.16.1

    4 notes

    Browsers can carry custom HTTP headers, at three scopes

    Browsers can carry custom HTTP headers now, at three scopes. browser_launch(extra_http_headers=...) covers every request the browser makes including popups and subresources; browser_set_extra_http_headers covers one page and can change mid-run, for a token you only get after logging in; browser_inject_headers covers just the URLs matching a pattern. All three replay in macros and export to a script.

    A failed console.assert counts as an error

    A failed console.assert now counts as an error. Every engine reports it under its own 'assert' level, and the classifier only knew 'error' -- so the one line naming a broken invariant was neither counted nor included in the console tail a macro failure ships.

    The dashboard's console Warn filter works again

    The dashboard's console Warn filter works again. It matched the raw level against 'warn' while every engine emits 'warning', so choosing Warn showed an empty list.

    octowright_status tells you which followers are stale

    octowright_status() now tells you which followers are running old code. A follower survives a daemon restart by design, so restarting the daemon never deploys follower-side fixes -- and nothing reported the skew, since a follower identified itself by pid alone. The bridge summary now carries leader_version, follower_versions and stale_follower_count.

    GitHub release v0.16.1

  14. v0.16.0

    7 notes

    The dashboard asks who you are

    The dashboard now asks who you are. Binding to loopback stops the internet, not the machine — any other local process could list your live browsers, read recordings (typed input, URLs, console), pull video, and watch your screencast. Run octowright dashboard for a single-use link, or ask your agent for it. Set OCTOWRIGHT_DASHBOARD_REQUIRE_PAIRING=off for the old flow.

    Passwords stopped leaking into accessibility snapshots

    Passwords stopped leaking into accessibility snapshots. Playwright renders a text control's value as its accessible name and the tree cannot say type=password, so a filled login form came back as - textbox: hunter2 from snapshot, brief, captures, goldens, and the dashboard — and clicking a password field wrote it into the JSONL recording, which is the file redaction exists to protect.

    Persona profiles are owner-only

    Persona profiles are owner-only now. Firefox and WebKit wrote cookies.sqlite 0644 inside an 0755 tree, so on a shared host another local user could copy a logged-in session straight off disk — a stronger credential than the password already protected at 0600.

    SSRF checks follow redirects

    SSRF checks follow redirects. The policy only ever inspected the URL you asked for, so a public page answering 302 Location: http://169.254.169.254/... reached the metadata service and the read tools handed back the body.

    Four more SSRF and credential holes closed

    Four more holes closed: a trailing dot or a fullwidth digit walked past the SSRF host check, a leading control byte defeated the file:// deny-list, a rejected leader URL was returned anyway, and a macro could expand a credential into a navigate URL.

    A failed macro tells you why

    A failed macro now tells you why. The payload reported the symptom ("timed out waiting for #save") while the line explaining it (net::ERR_NETWORK_CHANGED) sat unread in the console buffer -- you had to open the raw JSONL to find it. Errors are picked first, so a chatty page cannot bury the useful line.

    CI can ask whether the daemon is up

    CI can ask if the daemon is up: octowright serve --wait-ready prints the URL and exits 0/1 instead of every workflow hand-rolling a lockfile poll. On Windows the daemon now actually detaches (the POSIX flag was a silent no-op there), a slow cold start can be given more time with --ready-timeout, and a failed spawn quotes the daemon log instead of pointing at an empty one.

    GitHub release v0.16.0

  15. v0.15.1

    2 notes

    A stale session stops haunting octowright_status

    A stale session that outlived its daemon no longer haunts octowright_status forever. The cleanup only asked whether the recorded pid was still alive, and the OS recycles pids — one entry here was kept for four weeks because its dead daemon's pid had become a shell. It now checks that the process is actually an octowright daemon.

    A tail summary says when it covers only part of the file

    A tail recording summary says when it only covers part of the file. 0.15.0 bounded the read so a huge recording could not be pulled into the daemon whole, which quietly made event_count describe one window rather than the recording — so it read as a total when it was not.

    GitHub release v0.15.1

  16. v0.15.0

    5 notes

    The bridge stops double-reporting a failed call

    The follower bridge no longer hands your client a second frame for a request it already failed. A tool call that timed out, then finished on the leader anyway, used to deliver BOTH the bridge error and the real response under one JSON-RPC id — and a late progress ping could arrive carrying a bridge-internal token your client never asked for.

    A long recording no longer loads whole into the daemon

    Opening a long-lived session's recording in the dashboard no longer reads the whole file into the daemon at once. Recordings are unbounded by default, so a busy session could pull gigabytes into the process that owns every live browser; reads are now windowed (OCTOWRIGHT_TAIL_MAX_BYTES, 8 MiB) and page on the cursor they already returned.

    SSRF canonicalizes a URL the way the browser will

    SSRF blocking now canonicalizes a URL the way the browser will. Backslashes and stray tabs/newlines let http:/\169.254.169.254/ read as harmless to the guard and as cloud-metadata to Chromium, so block-private said ALLOWED on the one request it exists to stop.

    macro_lint stopped refusing saves it had no business refusing

    macro_lint stopped refusing saves it had no business refusing — a prefilled ?email= signup link, a 32-digit order id, an ignored screenshot field and text: "" are no longer errors. It also catches the magic-link and reset tokens its own docs promised, which the old path scan could never match.

    restart actually sweeps the browsers it owns

    octowright restart actually sweeps the browsers it owns. It enumerated the daemon's children AFTER signalling it, by which point they had reparented and nothing was found.

    GitHub release v0.15.0

  17. v0.14.4

    3 notes

    text_exact and label_exact join role_exact

    click_by/fill_by/get_text_by gained text_exact and label_exact flags mirroring role_exact — matching stayed substring-by-default the whole time, this just adds an opt-in for exact instead of leaving you to hand-craft a selector.

    macro_lint catches an impossible field before replay

    macro_lint now catches a field a macro action can't actually accept before you replay it, instead of a live TypeError days later.

    restart --help says what the sweep actually kills

    restart --help says plainly what the browser sweep actually kills (every Playwright browser on the machine, protected or not), and macro_lint stopped warning on ordinary navigate URLs.

    GitHub release v0.14.4

  18. v0.14.3

    2 notes

    A security review closed seven findings

    A full security review closed out seven findings across the codebase: exported macro scripts can no longer be turned into executable code via a crafted recorded field, launching a custom browser executable now requires an explicit opt-in, macro repair/lint/healing stopped echoing literal credentials back at you, the takeover config write and the viewport-pill control binding are hardened against symlink-swap and unauthenticated-page-script attacks respectively, and file-upload targeting no longer defaults to the daemon's own working directory.

    The rate limiter stops lumping every client together

    The leader's new-session rate limiter no longer lumps every headerless MCP client into one shared bucket — a noisy direct client that skips the follower handshake can no longer 429 an unrelated client's legitimate new session.

    GitHub release v0.14.3

  19. v0.14.2

    5 notes

    Every session serializes its own actions

    Every browser session now serializes its own actions through a per-session FIFO gate: a manual action can no longer interleave mid-macro, and a closing session rejects new work instead of racing it. Live gate state is visible per-session and, all at once, from octowright_status and the dashboard.

    The viewport mismatch badge stopped crying wolf

    The 'fixed mismatch' viewport badge stopped crying wolf: it compared the whole OS window against the viewport with a chrome allowance too small for real browsers, so it flagged every headed session from the moment it launched. It now measures the real chrome and only warns on genuine drift — including a resize on a fluid session, which previously turned off drift detection without saying so.

    Dashboard pairing protects the control plane end to end

    Opt-in dashboard pairing now protects the browser-facing control plane end to end: a one-use fragment becomes an origin-scoped bearer for APIs, SSE, WebSockets and protected media, and cloned tabs must pair independently. It stays off by default; the loopback daemon and same-user 0600 lockfile remain the trust boundary, and remote exposure still requires its separate explicit opt-in.

    Pairing expires everywhere it should

    Dashboard pairing now expires everywhere it should: already-open SSE and WebSocket streams close when their bearer expires, while protected recordings keep Range requests and progressive playback instead of buffering the complete video in memory.

    Lifecycle and concurrency gaps closed across the pool

    The correctness review closes lifecycle and concurrency gaps across browser launch/close, persona deletion, bridge snapshots, idempotent dispatch, scenario and terminal teardown, saturated recording discovery, replay classification, and dashboard degradation. Frontend installs/builds are now lockfile-deterministic and dependency-audited too.

    GitHub release v0.14.2

  20. v0.14.1

    2 notes

    A persistent profile survives a bad browser death

    A persistent profile no longer breaks when its browser dies badly or when a cache/temp cleanup runs. Chromium leaves an in-use marker pointing at a process that is gone, and every later launch of that persona failed with 'profile is already in use' until the files were deleted by hand — taking its saved logins out of service. Octowright now clears that marker itself, and only when the process that left it is confirmed dead.

    Switching tabs records the tab you switched to

    Switching tabs records the tab you switched to. A concurrent switch could make an earlier one report the wrong page's URL, which then landed in the recording and followed through to replay and export.

    GitHub release v0.14.1

  21. v0.14.0

    2 notes

    Octowright runs on the MCP 2.0 Python SDK

    Octowright now runs on the MCP 2.0 Python SDK. Installs had started breaking outright — the SDK's 2.0 release removed the module octowright imported, and the dependency had no upper bound, so a fresh install pulled a version the daemon could not start on. If you pin mcp yourself, you now need 2.0 or newer.

    The long-call keepalive survived the SDK upgrade

    The keepalive that stops long tool calls from looking like a dropped connection survived the upgrade — MCP 2.0 changed how request metadata reaches a tool, in a way that would have switched the keepalive (and duplicate-call protection) off silently rather than loudly.

    GitHub release v0.14.0

  22. v0.13.10

    4 notes

    The live preview follows the tab you switch to

    The dashboard's live preview now follows the tab you switch to. It used to keep streaming whichever page was active when you opened it, and the tab it left kept encoding frames forever.

    A live preview no longer freezes silently

    A live preview no longer freezes silently. If the stream can't be re-attached — the page it was casting crashed and couldn't be replaced, or the session closed while you were watching — the socket now closes and the panel drops to screenshot polling instead of showing a frame that never updates.

    A background-tab crash stops breaking your preview

    A crash in a background tab stops breaking the preview of the tab you're actually watching.

    Screenshot fallback backs off when the server is slow

    Screenshot fallback keeps one request in flight and backs off when the server is slow, so a sluggish screenshot endpoint no longer aborts every frame while still doing all the work.

    GitHub release v0.13.10

  23. v0.13.9

    3 notes

    One macro works against every deployment

    One macro now works against every deployment. A persona's default_url becomes the browser context's base_url, so browser_navigate('/orders') resolves per persona — launch the same macro as a different persona to replay it against a local stack, staging or production instead of keeping a divergent copy per environment. Library callers with no persona can set LaunchOptions.base_url directly. Absolute URLs and existing macros are untouched.

    Replaying a recording stops inventing failures

    Replaying a recording no longer invents failures. Passive rows the recorder emits — websocket frames, dialog/download outcomes — were classified nowhere and each counted as an error, so one captured library reported 608 bogus failures on every run. The strip-list is now derived from the recorder instead of hand-mirrored, and a test fails on any new unclassified event.

    switch_frame and get_text_by replay for real

    switch_frame and get_text_by replay for real. Both were recorded but performed by nothing, so a macro that entered an iframe or asserted on a value silently did neither.

    GitHub release v0.13.9

  24. v0.13.8

    1 note

    The leader defends itself against a session storm

    The leader now defends itself against a follower reconnect/session storm — the failure that could balloon the shared daemon to many GB of RAM and starve real tool calls until every connected client looked broken. A follower churning MCP sessions is now rate-limited (429) and the session table is capped, so one misbehaving or outdated client can't take everyone else down. On by default, tunable via OCTOWRIGHT_MCP_MAX_SESSIONS / OCTOWRIGHT_MCP_NEW_SESSION_MAX. Deploys with a single daemon restart.

    GitHub release v0.13.8

  25. v0.13.7

    2 notes

    A daemon restart stops disconnecting every client

    A daemon restart no longer disconnects every MCP client at once. Followers retry an unresponsive leader for a recovery window that was 15s — shorter than a real restart (20-30s+) — so on every restart they all gave up and exited simultaneously, breaking octowright across all clients. The window is now 180s, so followers wait out a normal restart and reconnect to the new leader transparently (tunable via OCTOWRIGHT_BRIDGE_LEADER_RECOVERY_WINDOW_SECONDS).

    Each BrowserPool can have its own recordings root

    Embedders can now give each BrowserPool its own recordings root: BrowserPool(recordings_dir=...) routes a pool's per-launch artefacts (log, video, HAR, downloads) to a distinct directory, so several pools in one process no longer collide on one recordings tree.

    GitHub release v0.13.7

  26. v0.13.6

    1 note

    Split-brain daemons closed in both directions

    Split-brain daemons are closed in both directions. A dying leader no longer leaves two daemons racing on different ports — the election lock is now held until the replacement is confirmed up, so followers adopt the new leader instead of forking a rival. And octowright restart now recovers from an existing split-brain by reclaiming the canonical port from the rival leader (found by its listening socket, not by guessing from its command line).

    GitHub release v0.13.6

  27. v0.13.5

    1 note

    The reaper stops killing Chromium's crash handlers

    Fixes the orphan-browser reaper silently killing Chromium's crash handlers every housekeeping cycle: chrome_crashpad_handler lives inside the browser bundle and detaches to ppid 1, so the reaper matched it as an orphaned browser and SIGKILLed both handlers of every live browser once a minute — which freed nothing and disabled crash reporting for perfectly healthy sessions. Crash-reporter helpers are now spared. Also refreshes all locked dependencies, clearing three mcp security advisories (mcp 1.27.1 -> 1.28.1) with no octowright behavior change.

    GitHub release v0.13.5

  28. v0.13.4

    1 note

    Recorded mock_route replay matches what was recorded

    Fixes recorded mock_route/unmock_route replay: the recorder and replayer disagreed on the route pattern's field name, so any macro or recording using route mocking failed on replay with a TypeError. Recorded route mocks now replay correctly.

    GitHub release v0.13.4

  29. v0.13.3

    1 note

    The real cause behind 'Octowright disconnected'

    Fixes the real cause behind repeated 'Octowright disconnected' reports: the leader process leaking memory over multi-day uptime (seen as high as 18.8GB RSS). A new housekeeping reaper terminates leader-side sessions the instant a follower's OS process is confirmed dead — not by guessing from idle time, so it can never drop a client that's just being quiet — plus automatic cleanup of orphaned bridge-state tmp files.

    GitHub release v0.13.3

  30. v0.13.2

    1 note

    Release tooling only -- no behavior changed

    Release-tooling fix only — no octowright behavior changed. The PyPI/TestPyPI publish workflow now matches the trusted-publisher setup on both registries, so releases actually reach PyPI again.

    GitHub release v0.13.2

  31. v0.13.1

    1 note

    No more silent mid-conversation disconnects

    No more silent mid-conversation disconnects: the idle-session reaper added in 0.12.1 is now OFF by default (it was killing live, wanted sessions during completely normal pauses) — opt in with OCTOWRIGHT_MCP_SESSION_IDLE_SECONDS on a shared/CI host that wants bounded memory.

    GitHub release v0.13.1

  32. v0.13.0

    1 note

    Headed browsers protect themselves

    Headed browsers protect themselves: a browser launched headed (so you can watch it) now refuses a reflex browser_close by default — an agent needs force=True to close it. Headless/CI browsers are unaffected. Opt out with OCTOWRIGHT_PROTECT_HEADED=0.

    GitHub release v0.13.0

  33. v0.12.1

    1 note

    Leader memory stays bounded

    Leader memory stays bounded: abandoned MCP sessions (left behind by a reconnect storm) are now reaped instead of piling up forever — a leak that could grow the daemon to gigabytes of RAM with zero live browsers. Tune or disable with OCTOWRIGHT_MCP_SESSION_IDLE_SECONDS (default 300s).

    GitHub release v0.12.1

  34. v0.12.0

    3 notes

    No false 'disconnected' on slow tool calls

    No more false 'Octowright disconnected' on slow tool calls: the leader now heartbeats progress for every in-flight call, so a slow-but-alive browser action keeps its bridge deadline alive instead of timing out — a genuinely wedged leader still fails fast.

    Proactive notifications reach the default daemon setup

    Proactive notifications reach you in the default daemon setup: crash / recovery / driver-death / session-closed now stream to stdio clients (Codex, Claude Code) over the leader's new /api/mcp-events channel, not just in inline mode.

    The bridge no longer storms or splits

    The bridge no longer storms or splits: a leader that instantly ends a session is backed off instead of hot-looped into a transport storm, and a respawn can't spin up a second competing daemon beside a healthy one.

    GitHub release v0.12.0

  35. v0.11.0

    3 notes

    Cheaper browsing loops

    Cheaper browsing loops: new compact browser and HTTP-first discovery tools help agents find links, fields, and page outlines before paying for full snapshots or raw dumps.

    Bounded summaries by default

    Bounded summaries by default: console, network, downloads, captures, and text reads now surface concise next actions so agents can drill in only when needed.

    Live preview is more resilient

    Live preview is more resilient: the dashboard falls back from screencast streaming to snapshot polling when a browser cannot provide live frames.

    GitHub release v0.11.0

  36. v0.10.1

    2 notes

    Survives a client compaction freeze

    Survives a compaction freeze: when your MCP client (Codex/Claude) pauses to compact and freezes Octowright's follower, the bridge no longer times out and dies on resume — it detects the suspension, keeps in-flight calls alive, and re-handshakes the leader cleanly instead of stranding on a half-initialized session. No more 'Octowright timed out' after a compaction.

    Terminal connectors enumerate in canonical order

    Terminal connectors now enumerate in the canonical ssh, telnet, pty order; the terminal_launch kind arg and behavior are unchanged.

    GitHub release v0.10.1

  37. v0.10.0

    4 notes

    Terminals, alongside browsers

    Terminals, alongside browsers: the optional octowright[terminal] extra adds in-process PTY / SSH / telnet sessions that record to the same JSONL and show up in the dashboard with a live, read-only xterm.js screen — new terminal_* tools and scenario participants.

    Browsers self-heal

    Browsers self-heal: a crashed renderer is replaced in place (not a broken reload) and a dead shared Playwright driver rebuilds itself instead of bricking the pool. Crashes, driver deaths, and lost sessions now surface as MCP notifications + incident records, with a health verdict in octowright_status.

    Hardened by default

    Hardened by default, configurable where it changes behavior: the loopback /mcp transport requires a capability token, recordings are written 0600, navigation can block SSRF to internal/cloud-metadata hosts, downloads are contained, and credentials are scrubbed from traces and selector-less sinks. See the OCTOWRIGHT_* knobs in the docs.

    Runs on Windows now

    Runs on Windows now: a real Win32 RSS reader for the memory governor + telemetry, cross-platform paths, and security bumps for cryptography / starlette / python-multipart / msgpack / pydantic-settings.

    GitHub release v0.10.0

  38. v0.9.1

    3 notes

    No more mid-session disconnects

    No more mid-session disconnects: the idle watchdog is now OFF by default, so the daemon stays up across client blips instead of auto-exiting and tearing down your browsers. Opt back into auto-quit with OCTOWRIGHT_IDLE_GRACE on CI/shared hosts.

    Reconnects are seamless

    Reconnects are seamless: a detached daemon reliably stays alive after a client disconnects (no more fragile inline-mode fallback), and --keep-alive now actually reaches the daemon it's meant to govern.

    No more orphaned serve processes

    No more orphaned octowright serve processes: a follower hard-exits when its MCP client closes stdin, instead of lingering and reconnecting forever.

    GitHub release v0.9.1

  39. v0.9.0

    4 notes

    Crashed browsers are caught

    Crashed browsers are caught: a renderer crash (Aw, Snap) pushes a browser_crashed notification and a clear 'crashed — relaunch' message instead of an opaque failure.

    Bridge blips stop double-running side effects

    Bridge blips no longer double-run side-effectful calls: an in-flight tool call is safely auto-resumed after a reconnect (leader-side idempotency), so a browser_launch interrupted mid-flight resumes as one browser, not two.

    Long macros don't spuriously time out

    Long macros don't spuriously time out: macro_run streams progress per step (which keeps the bridge alive), and a failure tells you exactly which steps already landed.

    Quieter, cleaner telemetry

    Quieter, cleaner telemetry: the per-call OpenTelemetry error that spammed the daemon log is gone, and HTTP metrics now export over OTLP (the /api/metrics scrape endpoint is removed — point a collector at the process).

    GitHub release v0.9.0

  40. v0.8.0

    4 notes

    Self-healing macros

    Self-healing macros: macro_repair_apply rewrites a brittle CSS selector into its semantic click_by/fill_by (from the role/label/text captured at record time) and saves it in place — no hand-editing JSON.

    Snapshots follow you into iframes

    Snapshots follow you into iframes: after browser_switch_frame, browser_snapshot and browser_brief show the frame you're in, not the parent page (capture / golden / read_markdown too).

    Disconnect-aware

    Disconnect-aware: if octowright's MCP server drops, you're told to reconnect it in your client instead of silently opening a browser it can't drive, inspect, or record.

    This what's-new notice itself

    This 'what's new' notice itself — shown once on the first run after an update, and in octowright_status.

    GitHub release v0.8.0

  41. v0.7.0

    6 notes

    Launches open instantly on a local /new-tab page

    Launches open instantly on a local /new-tab page (Otto + a live status strip) instead of hitting the network — works fully offline.

    Cmd+T lands on /new-tab in every engine

    Cmd+T (Ctrl+T) lands on /new-tab across Chromium, Firefox, and WebKit.

    No-argument launches auto-name and persist a profile

    No-argument launches auto-name the browser and persist a profile from your git repo / username / .octowright config — no more random instance IDs.

    browser_each fans one action across every browser

    browser_each fans one action (navigate/resize/evaluate/wait_for/screenshot) across every browser at once.

    The dashboard moved to port 6286

    Dashboard moved to port 6286 ('OCTO'); octowright restart now rebinds in ~2s.

    browser_click and browser_fill take ARIA locators

    browser_click / browser_fill take ARIA locators (role/label/text/test_id) directly — the separate _by tools are gone.

    GitHub release v0.7.0