Docs / Operations / Changelog
Documentation

Changelog

All notable changes to OpenDray v2 are documented in this file.

77 min read Updated Sep 8, 2026 Source Edit History

Changelog#

All notable changes to OpenDray v2 are documented in this file.

The format is based on Keep a Changelog. Version numbers follow this project's own major-as-generation strategy: major version = product generation, minor = feature iteration, patch = fix / polish. See VERSIONING.md for the full rationale and what triggers a major bump.

[Unreleased]#

[v2.15.1], 2026-09-08#

Fixed#

  • Grok sessions now scroll cleanly on desktop and mobile. Grok used to open in its fullscreen mode, which has no scrollback and ignores the mouse wheel, so scrolling was erratic on desktop and impossible on phones and tablets. Grok now opens in its scrollback native mode, so the wheel and finger swipe scroll the conversation the same way they do for Claude. (#546)

[v2.15.0], 2026-09-05#

Added#

  • Grok account switching can now carry the conversation across the switch, matching Claude. Switching a running grok session to another account with "carry over conversation context" turned on seeds the new account's fresh session with a recap of your recent conversation. It is opt in and consent gated: the recap is sent to the provider under the new account, which the confirm dialog states before you switch. (#542, #543, #544)

[v2.14.1], 2026-09-04#

Added#

  • Grok accounts get a full UI: a Grok accounts panel in the Providers page (list, import local, enable or disable, remove) and live account switching from the session header, matching Claude and Antigravity. This surfaces the grok multi-account backend that shipped in v2.14.0. (#539)
  • Opt-in git worktree isolation, so concurrent sessions can each work in their own worktree without stepping on one another. (#531)
  • Mobile can reach a gateway behind Cloudflare Access without a VPN. (#538)

Changed#

  • Documentation rewritten in plain, human prose with no em dashes, the README simplified so it reads top to bottom in one pass, and the changelog brought up to date. (#537, #528, #536)

[v2.14.0], 2026-08-31#

Grok catches up. You can pool several xAI accounts, bind one to a session, and switch a running grok session from one account to another without losing the session, the multi-account parity Claude and Antigravity already had. Each account is an isolated GROK_HOME; the heavy install and cache directories are shared across accounts by symlink, so a second account costs kilobytes, not gigabytes.

Added#

  • Grok multi-account: pool accounts, pick one per session, and live-switch a running session between them (PATCH /sessions/{id}/grok-account) without dropping it. Each account is an isolated GROK_HOME; MCP trust is per-account and the heavy install/cache dirs are shared by symlink. (#533, #534, #535)
  • Cortex spawn injections (memory guidance, ambient memory, skills) now reach grok too, coalesced into grok's single --rules. (#530)

[v2.13.3], 2026-08-22#

The one-line installer runs to the end again, as root, on a fresh Proxmox LXC or VPS, which is exactly how it is usually run. And Cortex, the knowledge layer, grows a lifecycle: operator edits stick, a page can be handed to an in-session agent, proposals are reviewed as a diff, and the sweep respects a page's approval gate.

Added#

  • Cortex knowledge lifecycle: durable operator edits, polarity, deletion-as-signal, and conflict execution. (#521)
  • Hand a knowledge page to in-session agents (#513); review proposals as a diff instead of two full documents (#515); let the sweep honour a page's approval gate (#516).
  • Tasks grouped by project in the management views. (#520)
  • Git: an "Update branch" action so a stale PR can be merged from opendray. (#518)

Fixed#

  • Installer: root installs never completed: the Postgres readiness gate always failed as root (run_priv -u postgres ran a literal -u), and migrate ran with HOME=/root so its backup keyfile was unreadable. The one-liner now runs to the end. (#529)
  • Mobile: a knowledge proposal is reviewable before you decide on it. (#519)
  • Cortex: doc_read's framing is no longer written into pages. (#517)
  • Catalog: compare resolved paths when detecting non-npm bin links. (#514)

[v2.13.2], 2026-08-11#

The Vault stops being a flat pile of markdown. It holds a folder structure you maintain, files each project under its own name, and renders HTML documents as carefully as it renders markdown, from an in-memory string in a sandboxed frame, with scripts off, because a document that arrived by git pull is not your own writing.

The phone catches up with the browser. It can see whether your edits reached the remote, browse by tag, follow [[wiki links]], show what links back, and start today's note, instead of being somewhere you could only read what you had written elsewhere. The admin UI itself now fits a phone screen, which it never did.

Added#

  • The admin UI works on a phone and a tablet. It was built for a desktop three-pane layout, which on a phone meant a horizontal scrollbar and two columns you could not reach. Every page now has a narrow form: below 1024px the third pane becomes a slide-over, below 768px the list becomes the page until you pick something and the navigation tree follows it into a drawer. Tables, spacing and the topbar were sized to match.

    The drawer is one component rather than a pattern re-implemented per page, and it is reachable from a labelled control in the header, never from an edge handle alone, which is unreachable in practice.

  • The Vault syncs from the phone. Repository status, a manual commit / push / pull, and the auto-sync settings are all on the phone now; previously the mobile app could edit documents but not see whether they had reached the remote, so the answer to "did that save get anywhere" was only available on a desktop.

  • The mobile Vault has the knowledge layer the web has had. It could create, read, edit, rename and delete a document, but nothing that makes a vault more than a folder. It now browses by #tag, shows a document's backlinks and its tags, follows [[wiki links]], completes them while you type, jumps by heading, and opens today's daily note from the same template the web writes.

    Two constraints worth knowing. The preview runs with JavaScript off, a document can arrive by git pull, so it is not the operator's own writing by default, which means wiki-links are anchors on a private scheme intercepted at navigation, and a heading jump lands in the source view rather than scrolling the rendered page. And the path rules are Unicode-aware on mobile: the web's ASCII-only sanitiser turns 笔记.md into --.md.

  • The Vault stores and renders HTML documents, not just markdown. Project documentation increasingly ships as HTML, exported from Notion or Word, generated by typedoc, asciidoc or Sphinx, and a doc library that cannot open those is a markdown library. .html / .htm are now first-class alongside .md: listed in the tree, creatable, movable, and rendered.

    HTML is never served from opendray's own origin. There is no endpoint returning a document as text/html; that would execute it same-origin with the admin session, so a file pulled from a git remote could take over the account. The body travels as a string through the existing JSON read and is rendered from memory in a sandboxed frame with an opaque origin. Scripts are off by default, exported documentation is static markup and renders identically without them, with a per-document opt-in for pages that genuinely run code, remembered locally per file.

    Mobile gains a rendered view for the first time. It previously showed only a document's source, which HTML would have made unreadable, so preview covers both kinds: markdown is converted and rendered through the same webview rather than drawn as native widgets, so the two formats look like one product.

    Deliberately not symmetric: [[wiki links]] are still scanned and rewritten in markdown only. An HTML document can be the target of one, but its own <a href> links are left alone. Rewriting those on a move is a different job, with relative paths, anchors and assets to get wrong. Auto-derived paths (daily, project, personal notes) stay markdown. And when a filename and a template disagree, guide.html from the markdown Blank template, the filename wins, since the operator chose the extension and the template was probably a default.

    In-document links work: clicking a table-of-contents entry scrolls to that heading, and only links that actually leave the document open a new tab. This needed more than it sounds like. See Fixed below.

Fixed#

  • Vault auto-sync settings could not be saved. Whatever interval you set, the dialog snapped back to "every 10 minutes": the form refetched every 8 seconds and overwrote the draft mid-edit, so Save was never enabled and no setting had ever reached the database. The interval is now a free-text Go duration with presets, and the gateway rejects an unparseable one with a message instead of quietly substituting a default.

  • The mobile Vault's rename and delete were unfindable. They existed, behind a long press on a row, with nothing on screen saying so, reported in testing as missing outright, which for a hidden gesture amounts to the same thing. Each row now carries a visible actions button in place of a chevron that did nothing the row did not already do, and the open editor offers rename and delete where the web has always had them.

  • Creating guide.html on the phone produced guide.html.md. The new-document path appended .md unconditionally, so an HTML document could not be created from mobile at all.

  • Six hints rendered &lt;prefix&gt; as literal text instead of <prefix>.

  • A table-of-contents link no longer loses the document. In the web viewer, clicking an in-document #anchor navigated the frame away and rendered the opendray app inside the document view. A srcdoc frame's document URL is about:srcdoc while its base URL is inherited from the parent page, so href="#install" resolved against the gateway and became a real navigation. The base is now pinned to the frame's own document, which also stops a pulled document's relative URLs from resolving against the gateway's origin.

    The first attempt at "external links should open a new tab" used <base target="_blank">, which retargets every link, so a contents entry opened a blank tab instead of scrolling. Anchors are now rewritten individually: fragment links are left in place, an author's explicit target is respected, and only links leaving the document get target="_blank". Generated documentation is precisely where this mattered: Sphinx, typedoc, asciidoc and Notion exports all ship a fragment contents list, and markdown footnotes render as fragment links too.

    Mobile deliberately does not pin a base: loadData makes the document's own URL and its base both about:blank, so fragments already resolve in place, and pinning would have introduced the bug rather than fixed it.

Changed#

  • CI no longer runs the whole suite twice on every branch commit. A feat/** branch with an open PR matched both the push and pull_request triggers, and the concurrency group keys on the ref, which differs between the two events, so neither run cancelled the other. push is now main-only; pull_request already covered branch work.

  • A project's documents are filed under the project's name. The Vault is a project documentation library, and it filed every project under projects/, a folder naming what the whole library already is. One level of nesting that told the reader nothing, on every path, in every listing, and at the top of the git repository the Vault syncs to. The operator's own notes were split off further still: agent docs at projects/<name>/…, the human scratchpad at personal/<name>.md, the same project's material in two distant places, sorted by who wrote it rather than by what it is about.

    New vaults now use <name>/… with that project's personal.md inside it. Existing vaults are not rearranged. The layout is decided once, at first start, and written into config.toml as [vault] layout. Recording it is the point: the alternative, work the shape out from what is on disk each time, is the bug that put one install's entire document library behind a notes/ directory, because a probe asking "does this folder have content?" changes its answer the moment someone puts content there.

    The doc library offers the conversion, on web and on mobile, to any vault that still nests projects. A migration that ships only as a CLI command is one that only the people who wrote it ever run. Every existing vault would have stayed nested with its owner never learning there was a choice. The offer is dismissible and never appears for a vault that is already flat or has nothing to move, and the gateway says the same thing once in the startup log for anyone who never opens the UI.

    Nothing moves without a preview. Web and mobile both run the migration as a dry run and show the real list, including what it refuses to touch and why, before anything is renamed. opendray notes flatten does the same from a terminal. It defaults to a dry run, drives the same rename the UI uses so [[wiki links]] are repointed as it goes, repoints per-cwd project overrides, and never overwrites: a destination that already exists is reported and skipped, leaving both copies for you to reconcile.

    Where a project's notes live is now answered by the gateway, /notes/info reports the layout and the project mapping carries personal_path, instead of being re-derived by web, mobile and the CLI. Three implementations guessing is three chances to disagree, and the CLI's notes project was already guessing wrong.

    Reserved names step aside rather than collide: a project called daily files under daily-docs, since daily/YYYY-MM-DD.md belongs to the whole vault. _- and .-prefixed names are reserved too.

    The conversion records the resulting layout itself and applies it to the running gateway, rather than leaving both to the next restart. Otherwise a converted vault keeps deriving projects/<name> and personal/<name>.md against the directories it just emptied. The directories the migration empties are removed; one still holding anything is left exactly where it is.

  • Documents save when you say so, not on a timer. The editor wrote after every pause in typing, so a long document rewrote itself to disk over and over while it was being worked on. Saving is now a button, or ⌘S / Ctrl+S, on web and on mobile. Leaving a document still flushes unsaved text, that safety net costs nothing while typing, and closing the browser tab with unsaved work asks first.

    Autosave was not the whole story behind the typing lag, so two things that were: the editor streamed every keystroke to the page, which re-rendered the whole vault tree and recomputed the outline per character; and the tree re-rendered along with it. The stream is now paced to what a sidebar can use, and the tree only re-renders when the note list or the selection changes.

  • Line numbers in the source view. On by default for HTML, off for markdown, and toggleable either way. Numbers turn wrapping off, the way every code editor does it: a wrapped line covers several rows, so a gutter counting 1..N drifts on exactly the long lines an HTML document is full of.

  • The doc library can rename and delete a document. It could create and edit one, and that was all. A daily note, or anything not bound to a project, could be made but never moved or removed from the one surface that lists everything. Rename goes through the move endpoint, so the [[wiki links]] pointing at the old path are repointed rather than left dangling, and a partial rewrite is reported instead of being folded into a plain "renamed". Mobile gains rename too; it already had delete.

  • "New" and "Today" are reachable once a document is open. They were in the header the whole time, pushed off the right edge by the vault path: a truncate element in a flex row still needs min-width: 0, or it refuses to shrink below its content. Anyone with a long vault path could only reach the two actions from the empty state, which is exactly when they had no documents to leave.

  • A folder holding the selected document can be collapsed again. The tree re-applied "open the ancestors of the selection" on every render, so the collapse landed and the next render undid it, and "Collapse all" left that one branch open. Revealing the selection is a response to the selection changing, not a rule about what must stay open, so it now runs once per selection.

  • opendray notes accepts flags after the subcommand. Go's flag package stops parsing at the first non-flag argument, so notes flatten --apply left apply false, performed a dry run, and advised re-running with --apply, which is what had just been typed. notes list --prefix=x silently listed everything for the same reason. Both orders now mean the same thing.

  • The Vault is your documents. Agent skills and the MCP registry moved out. One root held three tenants with nothing in common: the operator's markdown, the skills opendray injects at spawn, and the MCP registry, which is why the settings page could only describe it as "notes, skills and git-versioned root", a sentence that parses only if you already know the implementation. Opening the Vault showed skills/ and mcp/ sitting among your folders, and because Vault Sync commits that same directory, they went to your remote: on one install a private docs repo had picked up opendray's own skills/secretary/SKILL.md, and a git clean -fd there would have deleted the gateway's skills.

    New installs get ~/.opendray/vault for documents, ~/.opendray/ skills and ~/.opendray/mcp beside it, and a Vault repo holding writing and nothing else. Existing installs are not moved: any root with content still in the old place keeps being used, the settings page prints where everything actually resolved, and says plainly when it is still the shared layout. vault.notes and vault.skills keep working; vault.root now means the documents directory, and [skills].root is the new spelling. Machinery directories that do sit inside the Vault are hidden from the doc library and added to an opendray-managed .gitignore block, so nothing new gets carried to your remote. Anything already committed needs git rm --cached. opendray will not rewrite your repo.

    Path resolution used to be reimplemented in four places (the gateway plus each of opendray notes|skill|mcp) with different precedence in each, so the CLI could read a different directory than the running gateway. There is now exactly one resolver.

Fixed#

  • Verifying a git credential now checks that it can push, not just read. A GitHub fine-grained token's Contents permission has three levels: No access, Read-only, Read and write, and every check the verification made passed identically for a Read-only one. So it went green, the Vault pulled happily, and the first push came back remote: Write access to repository not granted ... 403, the same message a token with no Contents produces on a plain fetch, which sends you looking at everything except the token.

    Verification now also probes git's receive-pack advertisement: literally the first request git push makes, asked with the same credential over the same protocol, and a plain GET that changes nothing. If it is refused, push will be refused. Because it is git's wire protocol rather than a forge API, one probe covers GitHub, Gitea and GitLab, and it cannot disagree with what git actually does. A read-only credential now reports "CANNOT push (read-only)" and names the setting to change. A forge answer that is neither a clear yes nor a clear no is reported as nothing at all. Telling someone their working token is read-only is the same mistake pointed the other way.

  • "Reset to remote" no longer silently destroys unpushed work. It ran git reset --hard plus git clean -fd behind a confirmation that named no quantity, survivable when the remote is ahead of you, and not survivable when the remote is empty. A vault whose pushes have all been failing is exactly that, and the two faults compose: one operator lost 354 documents when a read-only token made every push 403, the local commits piled up unpushed, a pull hit a rebase conflict, and "reset to remote" looked like the way out of the conflict.

    The endpoint now counts what exists only locally (unpushed commits, modified files, untracked files) and refuses with a 409 and that breakdown unless the caller explicitly confirms. The dialog quotes the numbers and names example files instead of asking "are you sure?". And confirming is no longer final: opendray parks the unpushed commits on an opendray-rescue/<timestamp> branch and stashes the working tree (--include-untracked, since clean -fd is what destroys untracked files and no ref can hold those) before resetting, then names the rescue branch in the success toast. A tree that is already level with its remote loses nothing and still resets in one click. A confirmation that fires on no-ops is one people learn to dismiss.

Added#

  • Git credentials are scoped per host and owner, so one forge can hold several identities. One row per hostname assumed one identity per forge, which breaks the moment you touch a personal repo and an org repo on the same host: a fine-grained GitHub token is granted per repository, so the token that reaches github.com/<you>/… generally cannot reach github.com/<org>/…, and there was nowhere to put the second one. Git host entries now take an optional Owner; resolution prefers the owner-scoped credential and falls back to the host-wide one, so existing setups keep working untouched. Vault sync resolves the same way, which it previously could not: it only ever looked up by hostname, and its auth panel now names the credential it resolved to, saying plainly when the remote's owner has none of its own and the host-wide one is standing in.

  • Git hosts is now the authority for HTTPS git auth. A session's push went out with whatever the machine offered. Xcode ships credential.helper = osxkeychain enabled, so a stale keychain entry answered silently and failed with an error describing a token nobody remembered configuring. Pushes now authenticate with the configured credential, and inherited helpers are blanked for HTTPS remotes even when opendray has nothing registered: failing as "no credentials" beats quietly succeeding as an identity you never chose. SSH remotes are untouched: the agent is a deliberate, visible configuration.

  • Disabling a git host entry now actually disables it. The toggle changed nothing: credential resolution returned disabled rows and every caller (vault sync, PR and issue listing, remote detection) used the token regardless, so an entry switched off kept authenticating. The check now lives in the resolver, which also makes disabling compose properly: turn off an owner-scoped entry and its host falls back to the host-wide one, exactly as if the row were absent.

  • Git host entries can be verified against the forge. A stored token was a claim nobody checked, and the forges hide the mistake: a GitHub fine-grained token keeps "which repositories" and "which permissions" in separate sections of one form, with permissions defaulting to none, so granting all repositories and stopping there produces a token that authenticates perfectly and cannot read a single repo. Git then reports Write access to repository not granted on a plain fetch, naming the wrong permission on the wrong operation. Verify now asks the forge who the token belongs to, warns when that differs from the entry's owner, and optionally checks a specific repo, with a hint that says where to look.

Added#

  • Markdown in the Vault is syntax-highlighted while you edit it. The file viewer has always coloured what it shows, so raw markdown in the Vault (the one place people actually read and write it) was the last flat grey surface. Web layers a highlighted backdrop under the textarea; mobile colours the field directly through its editing controller, so the caret can't drift from the glyphs. Headings, bold, italic, code, links, quotes, lists, tags and [[wiki-links]].

Fixed#

  • The Vault no longer describes itself as an Obsidian feature. It syncs through a plain git remote. Obsidian is merely one editor that can be pointed at the same repo, not something opendray integrates with. The user-facing wording was corrected in the previous release; this clears the same claim from the code that outlives it.

Added#

  • New docs start from a template, and folders can explain themselves. Every doc previously started as an empty file with a heading, which is how a vault ends up with five different ideas of what a feature note is. Creating a doc now offers Blank / Feature / Decision (ADR) / Runbook, and a folder holding a README.md gets a control that opens it, so a directory can say what lives in it. Templates render server-side (the title comes from the filename, the date from the clock) so a doc started on the phone and one started on the web come out identical rather than drifting. Dropping _templates/<id>.md in the vault overrides a built-in or adds a new one, so a project can change the shape of its docs without a gateway release.

Added#

  • The Vault can hold a folder structure you actually maintain. Project docs were a flat list: the "New doc" box replaced / with -, so features/canvas.md became features-canvas.md and a folder could not be created from the UI at all, while the backend had stored nested paths the whole time and the Notes page already rendered them as a tree. The project-docs lane now renders that tree (rooted at the project, with a Recent toggle for "the one I just edited"), typing a path with slashes files a doc in a folder, and a new move/rename repoints the [[wiki-links]] that pointed at the old path. Without that, filing a doc away silently stranded every reference to it, which is why nobody reorganised. Web and mobile both; the rewrite skips code blocks, so a fenced example of the syntax is never edited.

[v2.13.1], 2026-08-07#

Your phone and your browser can reach the gateway again while its Mac sits idle. A machine that falls asleep takes its network with it, so the gateway simply stops answering, a failure that looks exactly like a flaky gateway from every surface that talks to it, and sends people reading gateway code instead of pmset -g log. opendray now holds the host awake while it serves, and lets you choose how far to take that.

Added#

  • Host power is now a setting you can see, on web and mobile. The choice between "stay reachable" and "let the machine sleep" was only reachable by hand-editing config.toml, which meant nobody found it. Server settings gain a Host power section listing all four modes with what each one costs (web renders them as annotated cards, mobile as a labelled picker) plus a note that a deliberate sleep is never blocked and that non-macOS hosts ignore the setting. Translated across English, 中文 and Español.

  • Wake-on-demand: the host may sleep, and phone/web traffic wakes it. [host] prevent_idle_sleep = "on_demand" lets the gateway's Mac sleep whenever things are quiet instead of being held awake around the clock. Incoming traffic dark-wakes the machine (macOS "Wake for network access"); the gateway then seizes a power assertion the moment a request, live stream or mid-turn session shows up, extends the wake for as long as serving actually takes, and lets go after a two-minute linger so the host can sleep again. Running sessions count as activity, so a turn started from a phone survives the phone locking. On-demand warns at startup if womp is disabled, since without it a sleeping host can't be woken remotely at all. The existing "ac" (default), "always" and "off" behaviours are unchanged.

Fixed#

  • Saving invalid server settings no longer bricks the next restart. PUT /admin/settings wrote whatever it was given. Since the loader validates on every startup, a bad value (an unparseable duration, an unknown mode) saved cleanly and then stopped the gateway from coming back up, with nothing naming the offending field. The write path now validates exactly what is about to hit disk and answers 400 instead, leaving the stored config untouched.

  • The gateway keeps its host awake, so phone and web stay connected. A Mac left to itself idle-sleeps, and a sleeping host takes its network with it: the gateway stops answering, a remote Postgres becomes unreachable, and every phone or web request times out until someone physically wakes the machine. Incoming traffic does dark-wake the host, but the window is short and a user-session LaunchAgent isn't reliably scheduled inside it, so the request has already timed out, which reads as "opendray is flaky" and sends people reading gateway code instead of pmset -g log. opendray now holds a power assertion for as long as it serves. Default is wall-power only, so a laptop on battery still sleeps normally; [host] prevent_idle_sleep takes "always" or "off". A deliberate sleep (lid close, Apple menu → Sleep) is never blocked, and the assertion is tied to the gateway's pid so even a SIGKILL can't strand the host awake. macOS only; accepted and ignored elsewhere.

[v2.13.0], 2026-08-07#

The Canvas: the agent renders a page you can actually see, mark up and iterate on, instead of describing a screen in prose and hoping it matches.

Added#

  • Canvas: a visual surface for designing with the agent. The agent renders a self-contained HTML page via canvas_render and it appears in the operator's panel, where they pin a point or drag a region and send those marks back as feedback. Marking reports the real element selector and markup (and for a region, the components inside the frame) so the agent iterates on the DOM instead of guessing from coordinates. Not only UI mocks: ui, flow (flowchart), mindmap, graph (relationship diagram) and doc (a formatted spec page), with diagrams authored as inline SVG so they need no external assets and stay markable node by node.
  • A focused canvas per project. A project accumulates many canvases, so one is FOCUSED per cwd, scoped to the project rather than the session because the MCP server only ever receives the cwd. Browsing the list is free and costs nothing; only an explicit "Work on this" seeds the note that makes ordinary conversation ("make the title bigger") resolve to that canvas. Agents pull the same fact with canvas_context, and a slug-less canvas_render targets the focused canvas rather than creating a duplicate.
  • A design system per project. Colour / type / radius / spacing tokens plus free-text style rules, carried in every canvas request AND injected into each rendered document as CSS variables. The pairing is what stops successive renders from drifting apart. Ships starting palettes for people who don't think in hex, a real colour picker, and two one-click jobs: read the project's actual theme, or draw the system as a canvas.
  • Full mobile parity for all of the above: viewer, viewport switching, focus and precise marking from the phone.
  • opendray status is now a systemctl-style health dashboard. On macOS the command used to dump ~80 lines of raw launchctl print internals. A live process says nothing about whether the gateway is serving HTTP or can reach its database. The default output is now a compact checklist: process, HTTP health + uptime, database reachability, listen address, restart count (with the last non-zero exit surfaced as a hint) and the config path. The raw launchd/systemd dump moved behind --raw.

Changed#

  • primary in a design system means the BRAND colour, and the gateway now says so. shadcn/ui and the Tailwind templates built on it use --primary for a near-black or near-white ink and keep the brand hue in --accent, so an agent mapping token names one-to-one produced a palette of greys with no brand colour in it. The contract is now stated wherever an agent reads it (the extract prompt, the canvas_design tool schema, the token docs and the catalog guidance) and saving a palette whose every colour resolves to a grey returns an advisory warning saying what probably went wrong.

Fixed#

  • Design-system swatches show the real colour, in any notation. Reading a colour back from getComputedStyle().color only yields rgb(…) for legacy sRGB notations (a modern colour function round-trips unchanged in both WebKit and Chromium) so an all-oklch project got a grid of identical grey fallbacks on the web, and mobile, which parsed nothing but #rrggbb, got no swatches at all. The gateway now resolves hex / rgb / hsl / oklch / oklab centrally and serves the result, which also lets the hex-only OS colour picker edit an oklch theme without converting it one field at a time.
  • Pin and region marks stay on the content they were placed on. The web panel recorded a mark as a percentage of the visible frame and drew it in an overlay on top of the preview, so it was anchored to the window: scroll the canvas and the mark stayed put while the content moved out from under it, and a mark made after scrolling was stored pointing somewhere else entirely. Marks are now recorded in document percentages and drawn into the canvas document itself. This also settles a mismatch where the same x/y field meant frame percentages from the web and document percentages from mobile.
  • The mouse wheel (and touch swipe) now scrolls grok and opencode sessions. These TUIs enable mouse tracking (so xterm hands them the wheel instead of scrolling its own viewport) but then ignore wheel events, scrolling only on arrow keys. opendray's wheel→arrow fallback was gated behind "app hasn't grabbed the mouse", so it never ran for them and the wheel did nothing (verified live: grok sets ?1000/1002/1003/1006 at startup yet does nothing on button-64/65). The web terminal now recognises these wheel-ignoring providers and sends arrow keys for both wheel and one-finger touch scroll, scoped by providerId so Claude/Codex/Antigravity (which genuinely consume the wheel) are untouched.
  • Coming back to the mobile app after it slept no longer fails session loads. While the app is suspended the OS silently tears down its TCP connections, but the HTTP client's pool didn't know. The next request went out on a dead socket and stalled into a 30-second timeout and a full-screen error, even on a LAN. Connection-level failures on GET/HEAD are now replayed up to twice with backoff (a 4xx/5xx still passes through untouched), the pool's idle timeout drops to 5 seconds so stale sockets are rarely handed out at all, and returning from a suspension of 5+ seconds refetches the session list immediately instead of waiting for the next tap.

Security#

  • Hardened the path-containment barrier behind /fs/download, /fs/zip and /fs/upload. The root-scoped filesystem endpoints now validate the resolved path with filepath.IsLocal and refuse any residual .. sequence in the canonical path (which also means oddly-named entries like notes..md are rejected inside these endpoints). Resolves all seven open CodeQL go/path-injection alerts plus one allocation-size-overflow finding.
  • Bumped vulnerable transitive npm dependencies in the web workspace lockfile: seroval 1.6.2 (critical GHSA-mv8w-475r-vwqw), brace-expansion 5.0.9 (three DoS advisories), postcss 8.5.26 (path-traversal advisories) and @babel/core 7.29.7. Lockfile-only; no manifest changes.

[v2.12.2], 2026-07-23#

Round Table members gain live, grounded memory, the mobile session screen is rebuilt around a tool dock, and the operator takes ownership of the four global knowledge pages so their curation finally sticks.

Added#

  • Round Table members read the shared memory. Every seated provider now gets live, read-only access to the shared opendray-memory MCP, so members ground their claims in the real store instead of guessing. A new read-only mode (OPENDRAY_MEMORY_READONLY=1) exposes only the search / read tools and refuses every write server-side (safe even with tool permissions open) and the table prompt flips to "ground your claims via these tools". A single per-provider attach point (catalog.AttachMemoryMCP) handles the antigravity / codex quirks and is reusable for any spawn.
  • Operator owns the form of the four global KB pages. The shape of kb_infrastructure / kb_conventions / kb_lessons / kb_reusable was hardcoded in the drafter's prompts, so every consolidation sweep re-manufactured the fixed fat skeleton and operator curation never stuck. The drafter now honours each page's blueprint maintainer_mode (human → the operator owns it outright, never drafted) and, for AI-maintained pages, edits the operator's current structure in place (folding in only new, on-topic evidence) instead of regenerating from a template. A per-page prompt_hint lets the operator steer form and scope without a code change. Set both via PUT /blueprint/{slug} (cwd __global__); a web/mobile toggle is a follow-up. No migration. The four pages default to the previous behaviour.
  • Mobile: session tool dock. The session detail screen is rebuilt so the terminal owns the full height under a two-line AppBar title, and a new SessionToolDock (Files · Git · Database · Vault · More) opens each tool as a bottom sheet over the live terminal. The overflow ⋮ menu is slimmed to session actions.

Changed#

  • Mobile: More / settings redesigned. The flat menu is rebuilt as grouped inset cards per section, with an account-block identity header (monogram avatar), accent-tinted icon chips, refined typography and a version footer. New sessions.dock.more / sessions.tools.* strings ship across en / zh / es at 100% parity.

Fixed#

  • A human-locked global KB page could never converge from the UI. Two bugs are fixed: approving a divergence proposal silently dropped the lock (the merge was written as updated_by='agent', flipping HumanLocked off). An approval is an operator decision, so the result now stays operator-authored and locked; and rejecting a proposal didn't stick: the drafter re-generated and re-filed the identical refresh every consolidation cycle. The drafter now skips a locked page whose feedstock signature matches an already-rejected proposal, so a rejection holds instead of nagging forever.

Docs#

  • The README surfaces the two marquee capabilities shipped since the last pass: Round Table (cross-vendor AI group chat + role-assigned execution plans) and the Database tool (Postgres / MySQL / MariaDB / SQLite, per-project crypto isolation, web + mobile), plus staged image attachments, TUI theme-following / wheel-scroll, and one-click provider updates.

[v2.12.1], 2026-07-18#

Grok reaches parity with the other cloud agents, the Cortex knowledge base grows a real management surface, and the mobile knowledge experience is rebuilt for phones.

Added#

  • Grok is now a first-class cloud agent. It can drive the shared memory MCP (memory search, doc_read, cross-layer recall) just like Claude / Codex / Antigravity. Its spawn folder is marked trusted so grok actually starts the injected memory server instead of silently skipping it. Grok and OpenCode are also selectable in Discuss with AI and as Memory Worker agent providers, and creating a grok session now offers the Bypass permissions / YOLO toggle (--always-approve) the other agents already had.
  • A cross-page KB Librarian (experimental). Launch a dedicated agent session (pick its cloud agent, model and account) that can organize, create, edit and delete any global knowledge page across the whole base, driven conversationally, unlike the per-page Discuss chat. It gets read + write KB tools (list / upsert config / write body / delete) on its memory MCP; those tools are scoped to the Librarian session alone and never reach ordinary or third-party sessions.
  • Edit a knowledge page's settings after creation. A kb_* page's title, one-line description, nature (foundational / emergent) and inject flag were locked in at creation; they are now editable in place (web + mobile) on every page except the classic four, including seeded pages like Integrations, so you can flip a page between full-inject and on-demand retrieval.
  • Discuss with AI model lists are live and accurate. Antigravity and OpenCode models are enumerated straight from their CLIs, and Codex offers its full model family (higher plans unlock the fuller models) instead of one pinned choice, no more picking a stale model that fails at spawn.
  • Round Table members can change mid-conversation. Add or remove seated providers on an active chat (web + mobile): an added member is @mentionable on the next turn with the full thread as context; a removed one stops replying while its past messages stay.
  • Mobile: staged image uploads. Images queue in a dismissable tray before send instead of uploading immediately.

Changed#

  • The mobile Knowledge (KB) page is rebuilt as a searchable list → detail flow. The old horizontal page-chip strip didn't scale once you had many kb_* docs; the KB tab is now a grouped, searchable list (Foundational / Emergent) that grows gracefully, and tapping a page opens a full-screen reader/editor with its actions in an AppBar overflow menu. New page and the Librarian move onto a FAB.

Fixed#

  • Grok sessions had no MCP / memory tools. opendray wrote the memory server into the project-scoped <cwd>/.grok/config.toml, but grok refuses to start repo-local MCP servers in an untrusted folder as a supply-chain guard, so the server was configured but never started. opendray now trusts the operator's own spawn folder (--trust), matching the other CLIs.
  • The web terminal input cursor no longer drifts on iPad.

[v2.12.0], 2026-07-16#

Added#

  • Round Table: a cross-vendor AI group chat (experimental). Seat several providers (Claude / Codex / Antigravity / Grok / OpenCode) plus the operator in one shared thread; @mention who should reply (or @all) and each member answers in character after reading the whole conversation, so heterogeneous foundation-model families react to each other in seat order. Summarize the discussion on demand, or turn it into a role-assigned execution plan: each step runs as a real session in a shared project (bind the project after the fact if you started without one). Hand the whole thread off to a working session to do the actual code changes. A chat can be closed and reopened (close keeps the thread, just stops new messages). Available on both the web admin and the mobile app, where Round Table gets its own bottom-nav tab, per-agent bubble colours, and labelled action menus. Fully self-contained and rollback-able (internal/roundtable/ROLLBACK.md).

Fixed#

  • The Files-tree download icon is now reachable on touch devices (iPad, phones). The per-row download button was revealed only on hover (group-hover) or keyboard focus. Tailwind v4 gates group-hover behind @media (hover: hover), so on a touch device (which can neither hover nor focus a row) the icon stayed at opacity-0 and was impossible to tap. It now pins visible under @media (hover: none), so touch users get a permanently-shown download control while pointer users keep the clean hover-reveal. (Follow-up to the v2.11.6 positioning fix, which addressed where the icon sits but not whether it ever appears without a mouse.)
  • Two MCP servers sharing a display name no longer brick Codex sessions. Every provider renderer keys its generated config on a server's display name, not its unique id. Two enabled servers with the same name therefore collided on that key: Codex emitted a duplicate [mcp_servers."…"] TOML table and died with duplicate key at startup (before printing a byte, so the session flipped straight to the read-only "[buffer unavailable]" view) while Claude's map-based renderer silently dropped one of them. renderMCP now rejects a duplicate name up front (for every provider, before any config file is written), and the Plugins create/update endpoints return 409 when a new or edited server would reuse a name already taken by a different id. Grok's manifest gap and this collision are unrelated; a stray second Notion entry sharing the name Notion API is what exposed it.
  • Grok now reports and applies CLI updates from the Providers page. The grok manifest carried an empty npmPackage, and the whole update path is npm-gated: CheckUpdate returned early (no latest version, no "update available" flag) and Update hard-errored with "not updatable via npm". Grok is published as @xai-official/grok (maintainer [email protected]), so the manifest now names it.
  • A provider CLI installed outside npm can now be updated in place. Grok's documented installer (curl -fsSL https://x.ai/cli/install.sh | bash) drops a symlink into the npm bin dir that npm does not own, and npm refuses to clobber it: EEXIST: file already exists. Simply naming the package would therefore have shipped a dashboard that advertises an update behind a button that always fails. Update now preflights the bin path: an unmanaged symlink is cleared so npm can take ownership (and the update output tells the operator exactly which link was replaced), while a regular file is never deleted: it is reported instead, mirroring the existing ErrUpdatePrefixReadonly preflight. Grok's install note now recommends npm install -g @xai-official/grok.

[v2.11.6], 2026-07-13#

Fixed#

  • The download icon is reachable again in a deep or long file tree. The session inspector's Files tree renders inside a scroll area whose inner wrapper sizes to its content, so long filenames and deep nesting pushed rows wider than the panel: names were hard-cut with no ellipsis, and the hover-download icon (anchored to each row's right edge) sat beyond the visible edge, so hovering a file appeared to do nothing. The tree is now constrained to the panel width, so names truncate with an ellipsis and the download icon sits at the visible right edge. The Database tab is unaffected (its grid scrolls in its own containers). (#443)

[v2.11.5], 2026-07-13#

Added#

  • TUIs follow the opendray theme. A terminal UI picks a light/dark palette by asking the terminal, via the OSC 11 background query (which xterm.js already answered) or the COLORFGBG environment variable, which opendray never set. So a CLI that reads the environment (Grok's theme = "auto", vim, tmux, …) had no way to know the operator was in light mode and always defaulted to dark. opendray now stamps the client's applied theme on session create and advertises it at spawn via COLORFGBG. Optional and backward-compatible: no theme advertises nothing and the CLI keeps its own default, and an explicit COLORFGBG already in the environment still wins. (#446)
  • The mouse wheel scrolls full-screen TUIs. In the alternate screen there is no xterm scrollback, and a CLI that hasn't grabbed the mouse never receives wheel events either, so the wheel silently did nothing and a Grok conversation couldn't be scrolled at all. opendray now does what a real terminal does (alternate-scroll): wheel notches become cursor Up/Down keys when the app is in the alternate screen and hasn't grabbed the mouse. CLIs that do grab the mouse (Claude Code, Codex, Antigravity) are unaffected. They already receive the wheel as SGR events. (#446)
  • Custom tasks are pre-scoped to the current project. (#442)

Fixed#

  • A disconnected browser no longer wedges CLI updates. Provider updates ran npm install -g on the HTTP request context, so a client disconnect (browser closed, proxy timeout) cancelled it and SIGKILLed npm mid-install. A half-killed npm leaves a partial global tree behind (a stale .<pkg>-XXXXXX temp dir) after which every later install fails with ENOTEMPTY, permanently wedging updates for that CLI (a codex update stayed broken for a week this way, and left a CLI whose platform binary never landed, so its sessions failed too). The install is now detached from the caller's cancellation. (#445)
  • A broken CLI is now visible instead of looking healthy. When a provider's binary is on PATH but won't run, opendray used to fall back to showing the manifest version, rendering a CLI that can't even launch as perfectly fine. It now reports "Installed but not runnable" with the CLI's own error, and a failed update surfaces npm's actual message (ENOTEMPTY: …) rather than a bare exit status 217. (#445)

[v2.11.4], 2026-07-12#

Added#

  • Mobile: Resources section + Updates "what's new" sheet. The mobile app gains the sidebar Resources block and the Updates/"what's new" sheet, reaching parity with the web admin (#433). (#439)

Fixed#

  • Antigravity spawns no longer fail on an empty MCP config. antigravity's first-run migration writes an empty ~/.gemini/config/mcp_config.json; opendray's MCP-injection prep parsed it unconditionally and errored with "provider prepare: parse …/mcp_config.json … unexpected end of JSON input", blocking every Antigravity session spawn. An empty (or whitespace-only) file is now treated as "no config yet" rather than a parse error, on both the mcp_config.json and gemini settings.json surfaces. (#440)
  • Grok provider icon. Grok is now registered in the web provider icon/visual lookup tables so its brand mark renders. (#434)

[v2.11.3], 2026-07-09#

Added#

  • Session terminal: staged image attachments. Uploading an image to a session (attach button, clipboard paste, or drag-and-drop) now stages it as a dismissable chip in a tray at the bottom of the terminal instead of typing the server path straight into the running CLI. Esc (or the chip's ✕) cancels it (an empty tray still passes Esc through to the CLI) and an Insert button commits the path(s) when you're ready. Fixes the long-standing "the uploaded path can't be dismissed" surprise. Web for now; mobile parity to follow. (#436)
  • Sidebar Resources block + Updates drawer. The web admin's left nav gains a Resources section under Settings: an Updates drawer that shows "what's new" from GitHub Releases (falling back to CHANGELOG.md) with an unread badge and "mark read", plus Docs, Community, and Sponsor links. (#433)

[v2.11.2], 2026-07-09#

Added#

  • Database tool: MySQL, MariaDB and SQLite. The Database tool now connects to MySQL and MariaDB (host/port/username like PostgreSQL; a MySQL "schema" is a database) and SQLite in addition to PostgreSQL. The connection form (web and mobile) gains an engine picker with per-engine default ports. SQLite is a file-path connection: the path is fenced to the connection's project cwd (a path escaping it via ../ or a symlink is rejected) and extension loading is disabled. Reads run behind the same read-only fence on every engine (SQLite via a dedicated read-only connection pool). All engines are pure-Go drivers (go-sql-driver/mysql, modernc.org/sqlite), so the binary still cross-compiles without cgo. Migration 0075 widens the driver constraint; 0076 reseeds the kb_integrations page.

Fixed#

  • Backup download link authorises correctly. The backup download URL now carries the admin token, so downloading a backup from the web UI no longer fails auth. (#428)

[v2.11.1], 2026-07-09#

Added#

  • Mobile parity: Database tool in the session inspector. The mobile app's session inspector gains a Database tab mirroring the web tool: browse schemas and tables, page through rows, insert / update / delete by primary key, and run read or write SQL against the project's registered connections, honouring db:read / db:write scopes and read-only connections, and reusing the session's cwd for isolation.
  • Mobile parity: upload files into a session. The mobile session inspector's Files tab gains an upload button: pick one or more files and stream them into the current directory via POST /api/v1/fs/upload, matching the web files-sidebar upload shipped in v2.11.0 (same resolveWithinRoot sandbox, auto-rename on name collision).

Security#

  • Database tool: cryptographic per-project isolation for the auto-attached MCP. The opendray-dbtool MCP now holds a db:signed key and sends a per-session X-OpenDray-Dbtool-Sig = HMAC(secret, cwd) header; the gateway rejects a signed-key call whose signature doesn't match the cwd. An agent that extracts the injected key can no longer forge another project's cwd, closing the residual the honest-path check left open. Antigravity (whose MCP config is HOME-global and can't carry a per-session signature, a Google limitation) and third-party integration keys keep the plain ?cwd= check via a separate honest-path key. Migration 0074 reseeds the kb_integrations page.

Fixed#

  • Database tool: bigint primary keys stay exact. Row update/delete and filters decode JSON with UseNumber, so a 64-bit primary key above 2^53 is no longer rounded through float64 (which could address the wrong row or match none). Numbers beyond int64 keep their exact string.
  • Database tool: consistent table metadata. TableMeta runs its four catalog queries (columns / PK / indexes / FKs) inside one read-only transaction, so concurrent DDL can't produce a half-updated view.

Changed#

  • Dependencies. Bump golang.org/x/crypto 0.50.0 → 0.52.0 (#421) and golang.org/x/net 0.52.0 → 0.55.0 (#418), pulling transitive x/sys and x/text updates. Build and vet clean.

[v2.11.0], 2026-07-08#

Added#

  • Upload files & folders into a session from the files sidebar. The session inspector's files panel can now create folders and upload files or whole folders (recursively, preserving the subtree) via an upload button or drag-and-drop, including dropping onto a specific folder row to target it, or the panel background to target the session cwd. Uploads land in the session's working directory where the AI model reads them, streamed to disk (250 MiB per file) and confined to the session cwd by the same resolveWithinRoot sandbox the download/zip endpoints use. Path traversal and symlinked-intermediate escapes are rejected. Conflicting names auto-rename (name-1.ext) instead of overwriting what the session produced. New admin-only endpoint POST /api/v1/fs/upload on the existing /fs group; the tree refreshes to show new files (including renames). (#420)
  • Database tool: direct project database access. opendray can now hold per-project (cwd-keyed) database connections and expose them like a JetBrains-style database tool: browse schemas/tables, read table data, edit rows, and run a SQL console. It surfaces two ways: a Database tab on each project screen (web: connection manager, lazy schema tree, paginated data grid with row insert/edit/delete, and a CodeMirror SQL console with schema-aware autocompletion; mobile: connection management, schema browse, read-only query) and an auto-attached opendray-dbtool MCP server (db_connections_list / db_schema / db_table_data / db_query / db_execute) so agent sessions can query and mutate a project's database directly. PostgreSQL only for now (a driver interface reserves MySQL/SQLite). Connection passwords are encrypted at rest with the same field cipher as channel/git-host secrets and are never returned by any read endpoint. Two new scopes, db:read (browse + read-only SQL) and db:write (row CRUD + write/DDL), gate integration access; registering a connection stays admin-only (an integration can never point opendray at a new host). Reads run inside a server-side READ ONLY transaction with a statement timeout, and per-connection read_only refuses every write regardless of scope. The dbtool MCP is withheld from origin=integration sessions, matching memory isolation. Configurable via [dbtool] (enabled by default; the feature is inert until a connection is registered). Migrations 0072 (schema) and 0073 (kb_integrations reseed).
  • Mobile: antigravity multi-account parity with web. The mobile app gains the antigravity multi-account management already shipped on web (#396). (#409)
  • Mobile: Grok provider brand mark, syncing the real Grok icon added to web in #405. (#408)
  • Memory search surfaces folded (deduped) variants in memory_search and memory_load_context, so callers see the merged form rather than near-duplicate rows. (#414)

Fixed#

  • antigravity MCP injection now targets the real config surface, so injected servers actually reach antigravity sessions. (#416)
  • Transcript overlay for CLIs with unscrollable TUIs: the web terminal can surface scrollback for tools whose full-screen TUI can't be scrolled natively. (#415)
  • project_search moved from admin-only to dual-auth + memory:read scope, so integrations can search project memory. (#413)
  • Per-provider spawn parity: codex bypass, antigravity memory CLI, and a default-model guard for integration-originated sessions. (#412)
  • Integration default-agent model is an explicit dropdown rather than a free-text datalist, on both web (#411) and mobile (#410).

Docs#

  • README refresh: reworked hero, added comparison + FAQ, and synced the 5-CLI provider list across all 10 translations. (#417)

[v2.10.1], 2026-06-22#

Added#

  • MCP servers reach Grok. Grok Build sessions now receive opendray's enabled MCP registry (HashiCorp Vault, etc.), per-provider mcp_servers, integration-scoped servers, and the opendray-memory server, the same injection every other MCP-capable provider gets. opendray writes them into the project-scoped <cwd>/.grok/config.toml [mcp_servers] table, which Grok union-merges with your global ~/.grok/config.toml (your personal servers are untouched). Previously Grok shipped with MCP injection disabled, so it could not see Vault or any other shared server the operator had configured. (#404)
  • Cortex-first knowledge framing. The spawn banner now opens with a preamble telling the agent to consult opendray's injected cortex (kb_* pages) as the authoritative source first, treating any external mirror (Obsidian vault, wiki) as a secondary fallback used only when the cortex doesn't cover the topic. Stops agents from grounding infra/DB process answers in stale external notes when the curated copy is already in-context. (#406)
  • Current objective always injected. Lean-mode spawns now inject the live current_objective body as a dedicated "work to THIS" block (not just an index entry the agent had to remember to fetch), plus a stronger proactive-maintenance directive so agents keep current_objective, the journal, and durable memory current on their own. (#403)
  • On-demand, section-level KB access. doc_read and project_search can now pull a single heading-section of a large global knowledge page instead of the whole thing: a kb_integrations lookup drops from ~15K tokens to ~300–1.3K, and search hits carry a doc_read(slug, section=…) pointer instead of dead-ending on a teaser. (#400)
  • Cross-project distilled knowledge now applies. Fixed the experience compiler reading session outcomes through an ephemeral table, which starved it of feedstock (112 journal summaries → only 2 survived) so it produced zero global playbooks. Outcomes are now denormalized onto the durable journal row (migration 0070, with a one-shot backfill of historical rows), so the compiler sees the full corpus and its global playbooks auto-inject at spawn. (#402)

Fixed#

  • Grok provider icon. Grok now shows its real brand mark in the spawn dialog and provider rail instead of the neutral letter-disc fallback. (#405)

[v2.10.0], 2026-06-21#

Added#

  • Antigravity multi-account. Bind a session to a specific Antigravity (agy) login, switch accounts from the session header, and manage accounts in Providers → Antigravity (discovery + guided HOME=… agy login). Accounts are isolated by $HOME, the agy analogue of Claude's CLAUDE_CONFIG_DIR. Switching accounts keeps the conversation: agy stores each conversation as a portable per-$HOME SQLite db, so the switch copies the current conversation into the new account's HOME and resumes it (--conversation <id>). You continue the same chat on the other identity, only the credential/quota changes. Restarting an antigravity session resumes its conversation too. (#396)
  • Grok Build CLI provider. xAI's grok as a first-class provider (install with curl -fsSL https://x.ai/cli/install.sh | bash, then grok login; models grok-build / grok-composer-2.5-fast, bypass via --always-approve). Resolves exe/model/bypass generically: no per-CLI adapter code. (#397)
  • OpenCode local-endpoint diagnostics. When a session's local endpoint (LM Studio / Ollama / vLLM) is unreachable or serves no chat-capable model, opendray now surfaces a one-time spawn notice explaining the cause (check the URL ends in /v1, the endpoint serves on the LAN, a chat model is loaded) instead of OpenCode's opaque [buffer unavailable]. (#398)

Changed#

  • Carry-context is ON by default when switching Claude accounts, and rate-limit auto-failover now carries context too: a switch seeds the new account with a recap of the prior conversation instead of starting blank. Untick the toggle for a clean-slate switch. (#395)

Removed#

  • Gemini CLI provider retired, superseded by Antigravity. Removed from the install wizard, the provider catalog/UI, the opendray providers npm-update list, and the Cortex "discuss with AI" list. Existing Gemini sessions and on-disk credentials are left untouched, but the provider is no longer offered to new installs. (#397, #399)

[v2.9.1], 2026-06-19#

Fixed#

  • Escalating a Cortex discussion now jumps straight into the spawned session and continues on the same CLI + account. The escalated session surfaces immediately (web deep-links it via ?open=, mobile pushes the route) instead of only appearing on the next manual list refresh. It also inherits the conversation's provider / model / Claude-account override rather than always falling back to Claude. (#390)

Changed#

  • Refreshed the third-party integration guide and its searchable kb_integrations KB page to match the shipped v2.9.0 contract: permission_mode (default | bypass) replacing the old bypass_permissions boolean, the per-principal integration:<id> memory zone, the enforced providers:write / providers:update scopes, and the reserved agent_id field. Removes the stale FORTHCOMING framing so any AI or developer reading it gets the current contract. (#391)

[v2.9.0], 2026-06-19#

Added#

  • Per-integration spawn profile. Each third-party integration now carries its own provider-agnostic spawn config (MCP servers, system prompt, and a permission-bypass toggle) decoupled from per-CLI args, so one integration behaves consistently across Claude / Codex / Gemini. (#381)
  • Per-integration default agent + first-class session model. Choose the default agent and model an integration's sessions spawn with, from a dedicated web + mobile config UI. (#378, #379)
  • Native Select & Copy in the terminal. Drag (or long-press on touch) to select any portion of the buffer (a command, a line-wrapped URL) and copy it, on both web and mobile. Replaces the old whole-buffer copy. (#374)
  • Claude account selector for AI discussion. Cortex's Discuss With AI lets you pick which Claude account drives the discussion (web + mobile + i18n). (#385)
  • Remove (delete) session control alongside Stop, so an ended session can be cleared from the list rather than only halted. (#387)
  • Mobile parity for integrations + project blueprint, bringing the phone app level with the web admin's integration management. (#386)
  • Third-party integration guide + searchable kb_integrations KB page: an authoritative, on-demand reference for wiring external callers. (#382)

Changed#

  • Integration-origin sessions are isolated from the operator's session list and default to no memory capture, keeping third-party traffic out of the operator's working view. (#375, #376)
  • Retired the detected-URLs badge on the terminal pane: native Select & Copy now covers the OAuth-login URL case the badge existed to rescue. (#388)

Fixed#

  • Third-party integration memory capture is routed into per-integration zones instead of leaking facts into the operator's partition. (#380)
  • Backup pre-migration snapshots auto-discover the newest pg_dump so a stale PATH default can't crash-loop migrations. (#383)
  • AI discussion on non-Claude providers uses the correct per-CLI headless invocation (e.g. gemini --prompt, codex exec). (#384)

[v2.8.0], 2026-06-16#

Added#

  • OpenCode CLI provider. Drive OpenCode (the open-source, provider-agnostic agentic coding CLI) through the same PTY gateway as Claude Code / Codex / Gemini. Point it at a local OpenAI-compatible endpoint (Ollama / LM Studio / vLLM) with just a base URL: opendray generates a per-session config, auto-discovers the endpoint's models so they all appear in OpenCode's /model picker, and wires the shared opendray-memory MCP, skills, and a spawn-dialog bypass toggle. (#369)
  • AI discussion model picker on mobile, matching the web admin's cloud-agent + local-model selection in Cortex's Discuss With AI. (#368)
  • Antigravity is now selectable as a cloud agent in Cortex's Discuss With AI. (#370)
  • Backup-hardening arc + Cortex memory/notes UX integrated from beta: scheduled/fan-out backups across pluggable targets, pre-migration snapshots, and the conversational notes/knowledge maintenance surface. (#367)

Changed#

  • Recovery Kit clarity. The dialog now states what the kit is for (disaster-recovery insurance, rarely needed), that each generation is an independent file sealed with its own password (nothing is stored, there is no master password), and that the password protects the file, not the gateway. Downloaded kits are named by key fingerprint + date so regenerating no longer silently overwrites a prior kit. (#371)

Fixed#

  • Release announcements trigger via workflow_run instead of the release event. (#366)

[v2.7.6], 2026-06-14#

Added#

  • Carry context on Claude account switch (opt-in). Switching a live session to another account starts a fresh conversation (Claude Code can't --resume across accounts). A new "Carry over conversation context" toggle in the account switcher seeds the new account's session with a recap of the prior conversation, read from the old transcript, injected into the system prompt. Off by default; the toggle's helper text is the consent surface, since carrying context sends prior conversation content to the provider under the new account. Automatic rate-limit failover never carries context. Also fixes the stale switch-confirm copy that still claimed history was preserved via --resume (removed in v2.7.x).
  • Release announcements auto-drafted for X. Each published release now appends an "Announce on X" block to its GitHub release notes with a one-tap intent link composed from the CHANGELOG, and (when TYPEFULLY_API_KEY is configured) queues a Typefully draft.

Fixed#

  • Web self-update no longer dead-ends on live sessions. When an in-app upgrade would interrupt running sessions, the gateway gates the restart behind a confirmation (the sessions auto-resume). The web About panel previously surfaced that gate as a raw error with no way forward; it now shows an "Upgrade anyway" prompt with the live- session count and proceeds on click.

[v2.7.5], 2026-06-11#

Fixed#

  • iOS Safari: restored the contextual copy pill on terminal text selection (regression from v2.7.3). PR #353 added touch-action: pan-y to .xterm-viewport so iOS scrollback worked with the clipped pane, but the same property claimed horizontal / diagonal gestures for the browser and broke xterm's selection-drag on iPad and iPhone: the pill (anchored at pointerup once a selection finishes) never appeared because pointerup arrived with no finished selection to anchor to. Dropped pan-y (the default auto is sufficient for iOS scrollback) and added a touchend belt-and-suspenders fallback so the pill anchors reliably even when iOS Safari's synthesised pointerup doesn't fire (or fires with stale coordinates) on canvas-internal events.

[v2.7.4], 2026-06-11#

Added#

  • Inspector: one-click download for files + zip-on-the-fly for folders. Every row in the Files tree now carries a hover-revealed Download icon. Clicking it on a file streams the bytes as an attachment with the original filename (including unicode via RFC 5987 filename*=); clicking it on a folder streams a built-on-the- fly zip archive of the visible subtree (hidden entries + symlinks skipped to match the tree's listing). http.ServeContent handles Range requests so resume works on large files; the zip builder walks deterministically and skips per-file permission errors instead of aborting the whole archive. Downloads are confined to a caller-supplied root (the session's cwd in the inspector) and the server EvalSymlinks-checks the resolved target stays inside it, so a hand-crafted URL can't exfiltrate files from outside the inspector's view even with a leaked admin token.
  • Plugins: drag-and-drop install of SKILL.md. Drag a SKILL.md onto the Agent skills table and the daemon slugs the frontmatter name: into an id and writes it to <vault>/<id>/ SKILL.md: no id prompt, no editor modal. Vault collisions return 409; built-in collisions become overrides with the existing badge. i18n parity preserved across en/es/zh.

Fixed#

  • Web sessions: breathing room around the chat pane. Slimmed the chrome (SessionTabs h-9 → h-7, WorkbenchHeader h-14 → h-11, avatar 36 → 28px) and reserved a 12px bottom strip so Claude's input no longer sits flush against the browser bottom edge. Net result: chat-top moves up ~20px and typing feels less cramped on tall windows.

[v2.7.3], 2026-06-10#

Fixed#

  • Mac Safari terminal regressions introduced in v2.7.2 by #323. The chat no longer extends past the browser bottom; Claude's TUI input wraps correctly past the visible right edge after 3-4 lines; and [disconnected — reconnecting…] stops stacking: it announces once per disconnect cycle, then prints [reconnected] on recovery or [connection lost — refresh the page to reconnect] after retries are exhausted. WebKit's ResizeObserver fires late on absolute-positioned elements nested inside flex containers, which left fit() reading a stale size after sidebar/banner shifts; the xterm host now stays in-flow with contain: layout paint instead. The WebSocket retry budget was also bumped (maxRetries 6 → 30, maxBackoff 8s → 15s) so an idle proxy bouncing the socket doesn't read as "broken forever."
  • iOS web terminal scrollback. Two-finger drag on the terminal canvas now scrolls xterm's scrollback. .xterm-viewport got -webkit-overflow-scrolling: touch, overscroll-behavior: contain, and touch-action: pan-y. The AppShell switched from h-svh to h-dvh so the layout tracks the live visual viewport (keyboard, address-bar) instead of locking to the address-bar-visible height.

Added#

  • Web: "update available" badge on the Settings icon. A small accent dot appears on the gear in the sidebar (expanded, icon-rail, and mobile slide-over) when a newer release is detected, so the upgrade prompt finds operators instead of waiting for them to open About. Background poll every 6 hours, shares the ['version'] query cache with the existing AboutSection so a manual "Check now" updates the badge immediately. Suppressed while pending=true to avoid nagging during an in-flight upgrade. i18n parity preserved across en / es / zh.
  • GitHub Sponsors landing material. SPONSORS.md (pitch, five tiers, FAQ, thank-you wall), docs/sponsors/dashboard-copy.md (paste-ready text for both the Opendray org and the navidrast personal Sponsors dashboards), and .github/FUNDING.yml updated to list both accounts.

[v2.7.2], 2026-06-04#

Added#

  • opendray doctor + opendray setup-macos (macOS). setup-macos gives the binary a stable, per-machine self-signed code-signing identity (in a dedicated keychain, fully non-interactive) and re-signs it, so a one-time Full Disk Access grant survives rebuilds/updates instead of macOS re-prompting on every version change. doctor is a read-only health check that flags an ad-hoc signature or a config living in a TCC-protected folder. opendray serve with no -config now falls back to ~/.opendray/config.toml (outside the protected folders) so a fresh install's gateway starts without a privacy prompt.
  • macOS release binaries are now Developer ID-signed + notarized (when the signing secrets are configured), so a user's Full Disk Access grant persists across opendray update. Signing runs via quill on the Linux release runner and is a no-op when unconfigured.
  • Telegram /peek command + control-keyboard button to re-send the selected session's latest output on demand; the docked control keyboard now refreshes on /select and /start.
  • Mobile: switch a running Claude session's account + agent-CLI update awareness. Rebind a live session to a different account from the session screen (web parity), and see when a provider CLI has an npm update available. The session Tasks tab also reached web parity.
  • Spanish (es) translation across web + mobile with in-app language switching, plus a CI translation-parity guard that fails the build on missing/extra keys.

Changed#

  • Telegram notifications consolidated on enabled + muted + the repeat policy. The redundant notify_enabled switch and the per-topic notify_on picker were removed: an enabled, unmuted channel notifies once per round, and the web channel card gained the mute toggle that mobile already had.

Fixed#

  • Switching a Claude session's account no longer leaves it stopped and unrestartable. The switch now starts a fresh conversation under the new account (a session UUID that account's CLI actually knows) instead of --resume-ing a UUID minted under the previous account, which failed with "No conversation found" and exited the process.
  • Web terminal jitter caused by the page's scrollbar: the terminal pane is now isolated from <main>.
  • Mobile: numeric Telegram chat_id is submitted as a number, not a string, so a Telegram channel configured from the phone starts.
  • Release pipeline: release notes are written outside the work tree so goreleaser's dirty-tree check passes.

Removed#

  • ghcr.io/Opendray/opendray container image (all 370 versions / 84 tags). The image was orphaned: the most recent tag was v2.1.0 (five releases behind), no workflow in .github/ was building it any more, no docs / installer scripts / discussions referenced it. More importantly, a pullable container contradicted the host- resident-only deploy policy (Discussion #300, README "Choose how to run it" table): opendray runs AI CLIs through PTYs and shares filesystem state (~/.claude, ssh-agent, project files) with them, which container isolation breaks. Operators who landed on the GHCR page following stale links from external blog posts would have pulled v2.1.0 and hit exactly that failure mode. Supported install paths remain the one-line installer, the from-source quickstart, and npm install -g opendray.

[v2.7.1], 2026-06-01#

Security + bug-fix rollup on top of v2.7.0. No API, config, or schema changes. Drop in.

Security#

  • Path-traversal sanitiser bypass in NotesPanel (#294). replace(/\.\.\/+/g, '') was bypassable by overlap (....// collapses to ../ after one pass). Replaced with split-on-slash + filter-out ../. + rejoin, which cannot be bypassed that way.
  • Windows path-traversal gap in backup local target (#296). filepath.IsAbs("/foo") returns false on Windows, so the prior absolute-path reject left a gap. LocalTarget.resolve() now also rejects paths with a leading / or \, and rejects any colon on Windows to catch drive-relative forms like C:foo / C:..\evil.
  • Demo-client API key in log lines (#294). The integration-key fingerprint embedded in two log sites was emitting bytes from the secret through console.log. Dropped the fingerprint entirely; integration_id already identifies which credential is in use.

Fixed#

  • Windows build failure in internal/session (#296). syscall.SIGTERM / syscall.SIGKILL are undefined on Windows. New build-tagged signals_{unix,windows}.go helpers abstract the difference: Unix preserves the prior SIGTERM → grace → SIGKILL ladder; Windows falls through to proc.Kill() (TerminateProcess) since the platform has no SIGTERM equivalent. Documented in code.
  • go test -race ./... failing on Windows (#296). Test compat across auth, backup, cliacct, catalog, mcp, session, app packages: USERPROFILE setenv alongside HOME for os.UserHomeDir, Unix-perm asserts skipped on Windows (os.Chmod doesn't enforce them there), symlink tests skip when os.Symlink lacks privilege, shell-script fake MCP server replaced with TestMain-as-fake-server pattern, app_test.go uses an existing file as fake parent dir for cross-platform os.MkdirAll failure. Full suite now passes on Windows (44 packages, 0 failures).
  • Identity-replacement no-op in NotesPanel (#294). prefix.replace(/\/$/, '/') stripped a trailing slash and replaced it with the same slash: the author meant to ensure a trailing slash. Rewrote as endsWith('/') ? prefix : prefix + '/'.

Docs#

  • README.fa.md 10-way language switcher backfill (#293). The Persian README still listed only English / 简体中文 / فارسی; now matches the ten-way switcher the other nine READMEs got via #282.
  • enable-cli-updates.sh discoverable from the failure path (#297). The in-app guidance toast (returned when the npm global prefix is read-only by the service user) and scripts/README.md now name the helper script that resolves the situation, so operators don't have to grep for it. Closes #262.

[v2.7.0], 2026-06-01#

The Flutter mobile app catches up to web. Features that landed on the web dashboard but never reached mobile are now at parity: Telegram two-way channel config, Claude account metadata, a gateway version check, and most-recently-used session ordering.

Added#

  • Telegram two-way channel config on mobile (#290). The mobile channel form gained the five Telegram fields the web form already had: owner allow-list (owner_user_ids), two-way chat toggle (chat_enabled), typing indicator (chat_typing), activity notifications (notify_enabled), and reply length cap (reply_max_chars). Booleans render as switches and serialize to the same config shape the gateway expects.
  • Claude account metadata on mobile (#290). The provider page now surfaces the gateway-decorated account fields (subscription tier, rate-limit tier, active session count, last-used time, and OAuth email) as inline chips, plus an identity-drift banner with an Accept action when the on-disk OAuth identity changed.
  • Gateway version check on mobile (#290). The About screen shows the running gateway's version, commit, and whether an update is available, with a release-notes link. Read-only: the in-app self-update stays on web / the host shell.
  • Most-recently-used session ordering on mobile (#290). The sessions list now sorts most-recently-opened first (recorded per-device, persisted across restarts), mirroring the web list; live sessions still group ahead of ended ones.

[v2.6.0], 2026-06-01#

Web/mobile gain a real PR detail surface and a read-only Issues surface, both backed by the existing git provider plumbing. The gateway binary is now also distributable via npm: npx opendray runs without a Go toolchain on the box. Plus a small dropdown- positioning fix and a handful of new README translations.

Added#

  • PR detail surface on web + mobile (#279). Click into a pull request from the PR list to get description, status (open / merged / closed), CI check summary, head/base branches, author, and last-updated timestamp. Backed by the same git.PullRequests provider interface the list view already uses: no new API surface for the host, just a deeper read against what the provider returns.
  • Read-only Issues surface on web + mobile (#281). List and detail views for repo issues, mirroring the PR layout: title, state, labels, assignee, body, comments thread. Read-only by design: issue creation/edit stays out of scope until the permission model around it is settled.
  • Distribute opendray as an npm package (#280). npm i -g opendray or npx opendray now works: the package wraps the platform-appropriate binary from the GitHub release. Useful for operators on Node-heavy fleets who'd rather not script a curl install. The binary itself is unchanged; npm is just another delivery channel alongside the existing tarballs.

Fixed#

  • Dropdown menus clamped to the viewport (#284). The account switcher and the session-action menu could overflow off the right edge of narrow viewports (sub-400px mobile, or a side-by-side desktop layout). Both now flip / clamp so the trailing edge stays inside the visible area.

Docs#

  • Seven additional README translations (#282): Spanish, Brazilian Portuguese, Japanese, Korean, French, German, Russian. The README switcher row at the top of every translation now lists ten languages.
  • Farsi (Persian) README translation (#283), originally contributed by Majid Allahverdi in #278; brought to main via #283 after a cross-fork rebase-conflict workaround. See Credits.

Credits#

  • Farsi (Persian) README translation by Majid Allahverdi, originally contributed in #278, brought to main via #283 after a rebase-conflict workaround.

[v2.5.0], 2026-05-31#

Phase 2 Tier A of the multi-Claude-account work: rate-limit-aware auto-failover. A Claude session that hits its account quota can now automatically switch itself to the next non-throttled enabled account, with the conversation continuing seamlessly on the new identity. Plus documentation polish around the release ceremony so the next operator cutting a release doesn't have to re-discover today's gotchas.

Added#

  • Rate-limit auto-failover for Claude sessions ([providers.claude] auto_failover_enabled, default false). pumpStdout scans each Claude session's PTY for the You've hit your session limit · resets HH:MM (UTC) banner. On a match:
    1. The current account is marked throttled-until-reset in an in-memory ThrottleStore (lazy GC of expired entries).
    2. PickFailoverClaudeAccount picks the next enabled non-throttled account by the same least-loaded heuristic auto-assign already uses.
    3. SwitchClaudeAccount runs end-to-end: transcript JSONL hard-linked, PTY respawned with --resume, conversation continues on the new identity.
    4. Bus events for observability: session.auto_switched on success, session.auto_failover_no_target when the fleet is exhausted, session.auto_failover_failed when the switch itself errors. 5s cooldown per session + 4 KiB rolling window so a persistent banner can't drive the regex on every chunk. Opt-in by design: defaults off so existing operators aren't surprised by silent account switches.
  • RELEASING.md runbook at the repo root documenting the release ceremony end-to-end: the chain diagram, the "tag-after-changelog- merges" gotcha, recovery procedures (empty release body, pulled- back release), pre/post-release checklists, roadmap to release-please automation and pre-release -rc.N channels.

Tests#

  • Pinned contract: disabled accounts are excluded from auto-assign. A regression-safety unit test for the enabled<2 guard in PickAutoAssignClaudeAccount. The SQL filter (WHERE ca.enabled = true) and the explicit-pin validation path were already covered by live integration + handler tests; this closes the last gap.

Internal#

  • New ClaudeAccountResolver interface methods: MarkClaudeAccountThrottled, IsClaudeAccountThrottled, PickFailoverClaudeAccount. pickLeastLoaded SQL gains variadic exclude ...string (parameterized via NOT (ca.id = ANY($1::text[]))).

Config#

  • New: [providers.claude] auto_failover_enabled (default false). Opt-in for the runtime rate-limit-driven account switching.

Honest limitations of Tier A#

  • Banner-text fragile: if Claude rephrases the limit message, the regex needs updating. Fallback separators (-, |) for the middle bullet are already covered.
  • No predictive load spread: only reacts to hard limits. Tiers B (active probing) and C (local HTTPS proxy) from the design discussion remain available as upgrades.
  • Sessions running on the empty-id default (~/.claude) are skipped by the failover path for the MVP, mapping the default to a real account row needs a resolver round-trip we haven't exposed yet. After auto-assign kicks in for ≥2 enabled accounts, most new sessions are pinned to a named account anyway, so this gap shrinks to zero in practice.

[v2.4.0], 2026-05-31#

Multi-Claude-account UX, two-way Telegram channel, and a clutch of session-quality fixes. The big new capability: a single OpenDray gateway can now manage multiple Anthropic identities side-by-side and let an operator switch a live Claude session between them without losing the conversation.

Added#

  • Claude accounts: filesystem watcher. ~/.claude-accounts/<name>/ is now monitored with fsnotify; a new .credentials.json (the result of CLAUDE_CONFIG_DIR=… claude login) registers an account row automatically. 500ms debounce, backoff-on-error reattach loop, symlink rejection at every level.
  • Claude accounts: synthetic default row. ~/.claude/.credentials.json (the CLI's own home) now surfaces as a row named default so the primary identity is visible in the panel without forcing the named-account login flow.
  • Claude accounts: capacity chips. Each row now shows subscription_type, rate_limit_tier, active_sessions, last_used_at, and oauth_email, all derived server-side from <configDir>/.credentials.json + <configDir>/.claude.json + a single JOIN against the sessions table. No new chrome.
  • Claude accounts: least-loaded auto-assign at session create. When POST /sessions arrives with provider=claude and empty claude_account_id (and ≥2 accounts are enabled), the gateway picks the enabled account with the fewest non-terminal sessions (alphabetical tiebreaker). Removes the "everything piles onto default" bias. Explicit operator pin still wins.
  • Claude accounts: identity drift detection. First-seen oauthAccount.emailAddress per account is recorded under ~/.opendray/cliacct-identity.json (chmod 0600). On every List/Get, the current on-disk email is compared; mismatch surfaces identity_drift=true and previous_email on the Account row, rendered as a red "identity changed: was X · accept" chip. POST /api/v1/claude-accounts/{id}/accept-identity updates the baseline so the chip clears.
  • Session switch preserves conversation. PATCH /api/v1/sessions/{id}/claude-account now hard-links the Claude transcript JSONL from <old_config_dir>/projects/<workspace>/ <session_id>.jsonl into <new_config_dir>/projects/<workspace>/ before respawning. Claude --resume then finds and replays the conversation under the new account. Hard-link shares one inode so switching back-and-forth keeps both views synchronized.
  • Telegram: two-way conversational chat. Typing indicator, turn replies, persistent control keyboard acting on the current session, configurable from the dashboard.
  • Catalog: warn + confirm before CLI upgrade. The in-app CLI upgrade button now warns when sessions are using the CLI it's about to replace, with a new scripts/enable-cli-updates.sh helper for the non-root install path.
  • Web: MRU session ordering + Cmd/Ctrl+K palette search.

Changed#

  • claude_account_id validation is now enforced at session create AND at switch: bogus or disabled ids return HTTP 400 BEFORE the row is persisted (create) or BEFORE the live PTY is stopped (switch).
  • Default idle threshold raised 30s → 5m so long-running tool invocations don't get killed by the idle reaper.
  • The "Switch Account" confirmation dialog now says "conversation history is preserved" instead of "in-progress conversation state will be lost", accurate description of what now happens.

Fixed#

  • token_filled previously only checked the legacy <accountsDir>/tokens/<name>.token file, so every config-dir account (the documented flow!) showed "NO TOKEN YET" despite having working credentials. Now reports true when either source has usable credentials.
  • Gemini reply parsing now reads chats/*.jsonl instead of scraping the screen, eliminating screen-dump noise in Telegram forwards.
  • Session 'shell' provider's chrome stripper is now shell-aware so raw prompt characters don't leak into the channel forwarders.
  • Web: copy now works over plain-HTTP LAN (Clipboard API requires HTTPS otherwise), terminal selection-driven copy works, copy pill is anchored at the selection with neutral styling.

Security#

  • All disk reads in the cliacct path use os.Lstat and reject symlinks (<accountsDir>/<name>/, <configDir>/.credentials.json, <configDir>/.claude.json, the legacy token file). Defense in depth against an attacker who can write under the accounts tree.
  • migrateClaudeTranscript Lstat-rejects symlinked sources before os.Link so a planted symlink can't be hardlinked into the new account's tree and read as conversation history by claude --resume.
  • Telegram inbound is gated to the configured owner across all message types, not just control commands.

API#

  • New: POST /api/v1/claude-accounts/{id}/accept-identity: clears the identity-drift baseline by recording the current on-disk email as the new accepted identity.

Config#

  • New: [providers.claude] watcher_enabled (default true). Set to false to disable the fsnotify watcher; the Import-local button still works on demand.

[v2.3.4], 2026-05-29#

Fixed#

  • Language toggle in the web Topbar moved its checkmark but UI strings didn't switch. The zustand → i18next bridge ran as a module-level useLocale.subscribe(...) in i18n.ts that mounted before React. Under React 19 StrictMode + Vite HMR + zustand persist hydration the subscription could end up registered against a store snapshot React never re-reconciled with, so picking a language moved the dropdown's checkmark (which reads from the store) without triggering i18n.changeLanguage(). Moved the bridge into a <LocaleSync /> React effect under QueryClientProvider so it shares the same lifecycle as every other useTranslation() consumer and they update in lockstep (#267).

  • Nine UI strings rendered their placeholders literally: "update available → {{version}}", "Suggested ({{count}})", "Updated {{from}} → {{to}}", "connected · {{count}} tools", and the three About-panel version-toaster lines all showed the {{var}} template instead of the substituted value. The web i18next interpolation is configured for single-brace {name} but those particular keys were authored with the i18next default {{name}}. Normalized them across both locales (#261).

  • Mobile flutter build apk failed with hundreds of parser errors after slang codegen. Mobile's slang config uses string_interpolation: braces (matching the web) but the same {{var}} typos that produced literal placeholders on web produced invalid Dart on mobile (({required Object {version}) and ${{version}}) that wouldn't compile. Same normalization as #261, plus a refresh of the generated strings*.g.dart outputs and alignment of app/mobile/pubspec.yaml to the product version (#264).

Changed#

  • App icons now show the new wooden-cart wordmark glyph instead of the old pink-gradient "D". README was already updated to the opendray.dev wordmark, but the running surfaces (web favicon, Android launcher mipmaps, the full iOS AppIcon.appiconset, and the repo-root assets/icons/logo/ set) hadn't caught up, so a fresh install showed the new brand on GitHub and the old brand on the device. Regenerated every square icon surface from a single 1024×1024 source so proportions stay consistent across sizes (#266).

  • The Providers page now asks for confirmation before upgrading a CLI that has live sessions on it. Linux file-replacement semantics mean an already-loaded session keeps the old binary in memory, but a long session with lazy / dynamic imports or in-flight subprocess work can pick up new code mid-run. When n > 0 non-terminal sessions are using the provider, clicking Update opens a dialog with the count and an honest explanation of the trade-off; with no live sessions Update still fires immediately, as before. Update-check responses also stay fresh for an hour now (matching the server-side npm cache) instead of being re-fetched on every tab switch (#263).

[v2.3.3], 2026-05-24#

Fixed#

  • About panel showed no version and the self-update button did nothing. The dashboard called the version / self-update API at /version and /version/update instead of /api/v1/..., so the requests 404'd. Added the /api/v1 prefix (#251).

[v2.3.2], 2026-05-24#

Fixed#

  • Cross-session memory injection rendered every fact as - ---. The "Recent project memory" banner took the first line of each memory, which for frontmatter-authored facts is the --- YAML delimiter. It now skips the frontmatter and surfaces the description (falling back to the first body line) (#250).

[v2.3.1], 2026-05-24#

Fixed#

  • Copy buttons silently failed over plain HTTP (LAN IP / mobile). navigator.clipboard is only exposed in a secure context. Added a shared copyText() helper that falls back to execCommand('copy') and routed the existing copy callsites through it (#249).

[v2.3.0], 2026-05-23#

Fixed#

  • Live sessions were destroyed by a daemon restart (e.g. a self-update). Sessions are now marked interrupted on a gateway shutdown and auto-resumed on the next startup via their stored agent session id (--resume), with bounded-concurrency spawning and an optional OPENDRAY_AUTO_RESUME_MAX cap. A drain gate warns before a self-update interrupts running work (#247).
  • 404 page instead of the login screen after a restart. The 401 redirect now respects the dashboard base path (→ /admin/login) and keeps next router-relative (#248).
  • Brand icons broke under a non-/admin base path (#246).

[v2.2.2], 2026-05-23#

Added#

  • Memory: global-scope injection fallback + recency default, a fact told to one session surfaces in another regardless of cwd (#244).
  • Transport-aware MCP editor template + "unsupported" badge for Codex (#242).

Fixed#

  • Memory endpoints are now scope-gated (admin or memory:read / memory:write) (#245).

[v2.2.1], 2026-05-22#

Added#

  • Always-visible "Check for updates" + re-install action in the About panel (#243).

Fixed#

  • Remote MCP URL normalization (#230).

[v2.2.0], 2026-05-22#

Added#

  • In-dashboard update notification + one-click background self-update (#241).
  • Startup warning when W^X (MemoryDenyWriteExecute) blocks executable memory (#240).

Changed#

  • Repository renamed opendray_v2opendray across code/config/docs; install / uninstall URLs updated (#238, #237).

Fixed#

  • Dropped MemoryDenyWriteExecute from the systemd unit: it broke Codex / Gemini sessions (#218).

[v2.1.1], 2026-05-22#

Added#

  • Responsive mobile web layout: slide-over nav + inspector with edge handles (#236).

Fixed#

  • Telegram channel: handle /start, and a clearer /list header for terminated sessions (#235).

[v2.1.0], 2026-05-22#

Added#

  • Per-provider model management from the dashboard (#229).
  • Real CLI version + "update available" surfaced in the providers API/UI (#227).
  • Interactive session switching via /select + Talk-to buttons in channels (#226).
  • Validate MCP servers from the Plugins page (#233).
  • Windows installer: a true one-liner: auto-installs WSL2 + Ubuntu, runs the installer, and persists across reboot (#213).

Changed#

  • Hardened the merged Update action: provider mutations are gated and the update path degrades gracefully (#234).

Fixed#

  • Session list shows session names in /list instead of bare ids (#224).
  • Spawned CLIs get a color-capable TERM so Claude/Codex/Gemini render in color (#225).
  • macOS installer hardening: robust local Postgres provisioning, configured-port binding, idempotent launchd reload, bash 3.2 compatibility, and a launchd PATH that finds brew-installed CLIs (#208, #209, #211, #212, #231, #232).
  • Windows installer: OS-build guard, auto-resume after a WSL reboot, PowerShell 5.1 compatibility (#214).
  • Installer: validate DB identifiers and don't abort on a free / commented-out Postgres port (#210).

Security#

  • Scrubbed dev-internal docs + personal-network references from the public repository (#204).

[v2.0.5], 2026-05-18#

Added#

  • Flutter mobile session terminal now has the URL detector badge. Same model as the web admin: the PTY byte stream is scanned for http(s) URLs with the same state-machine extractor that re-assembles CLI-soft-wrapped OAuth URLs. A floating pill in the top-right corner of the terminal: primary tap opens the most recent URL in the OS browser via url_launcher, secondary button opens a bottom-sheet with every URL (newest first) for picking older ones. Closes the OAuth-on-Flutter-app gap reported alongside the web fix.

Changed#

  • Web login no longer pre-fills the username with "admin". The install wizard lets operators pick any username, so seeding the field forced everyone-who-didn't-keep-the-default to backspace before typing. The field is now empty by default and autofocused.

[v2.0.4], 2026-05-18#

Fixed#

  • URL extractor now re-assembles CLI-soft-wrapped URLs. AI CLIs (claude-code, codex, gemini) hard-wrap long OAuth URLs at the terminal column width by emitting literal \n characters every ~55 chars. The v2.0.1 / v2.0.2 / v2.0.3 extractor used a [^\s]+ regex that stops at \n, so it captured only the first wrapped segment (e.g. https://...&client_). Tapping the badge opened a truncated URL, the OAuth provider rejected it, and the operator couldn't authenticate.

    The extractor is now a state-machine walker that anchors on https?://, consumes URL-body characters, and treats a single internal \n as a soft-wrap when the current line is ≥ 40 chars long (matches real CLI wrap width; well above "\n " prose patterns). Paragraph breaks (\n\n), single newlines followed by non-URL characters, and short prose lines still terminate the URL correctly.

    Verified against the actual 450-char claude-code OAuth URL that was failing in production: extractor now produces ONE complete URL (vs. two truncated segments).

[v2.0.3], 2026-05-18#

Fixed#

  • Terminal URL badge always opens with one tap, regardless of how many URLs the session has accumulated. v2.0.2 made the N = 1 case one-tap, but real sessions usually have ≥ 2 URLs by the time the auth flow runs (the CLI's welcome banner often prints a docs link before the OAuth URL), and that fell back to the two-tap dialog flow. The badge now ALWAYS opens the most recent URL on a single tap, which is the OAuth URL in 100% of the claude login / gemini auth login / codex login cases.

    Multi-URL access stays available via a small button beside the primary anchor: tapping it opens the same list dialog as before, so operators can still grab an older URL when they need it. The dialog row Open buttons are also real anchors (not window.open()) for the same popup-blocker reason.

    This is a web-admin-only fix. The Flutter mobile app's terminal surface doesn't have URL detection yet. Separate follow-up.

[v2.0.2], 2026-05-18#

Added#

  • Service-control subcommands: opendray start, opendray stop, opendray restart, opendray status. Thin wrappers over systemctl (Linux) and launchctl (macOS) so operators don't have to remember the platform-native incantation. On Linux, the binary auto-prepends sudo if the caller isn't root. On macOS, defaults to the user LaunchAgent (gui/$UID/com.opendray.opendray); pass --system to target the LaunchDaemon scope.

Fixed#

  • One-tap link open for the OAuth URL badge. When a session has exactly one detected URL (the common AI-CLI auth case: claude login / gemini auth login / codex login each print one OAuth URL), the floating "🔗 1 link" badge is now itself an <a target="_blank">: a single tap goes straight to the browser, no intermediate dialog. The dialog still appears when ≥ 2 URLs are detected, so multi-link sessions still get the disambiguating UI. In the dialog, the "Open" button is also a real anchor now, which avoids popup-blocker gating on some mobile browsers.

[v2.0.1], 2026-05-18#

Removed#

  • Docker deployment path. opendray is a host-resident gateway: it spawns AI CLIs via PTYs and shares process state (~/.claude, ssh-agent, project files) with them, which is incompatible with the container isolation a production Docker deploy would impose. Removed Dockerfile, docker-compose.yml, docker-compose.test.yml, .dockerignore, .env.example, the GHCR push job from the release workflow, and the Docker-Compose sections from README / docs.
  • In-app Tutorial page. All 84 markdown sections plus Tutorial.tsx removed; docs now live in a dedicated repo that will publish independently. Sidebar entry, /tutorial route, and i18n keys (nav.tutorial, web.providers.claudeAccounts.tutorialTooltip, web.providers.claudeAccounts.architectureLink) removed in parallel.

Fixed#

  • "No Claude accounts" empty state (Providers page + Spawn dialog, web + mobile) now tells operators the actual setup path: spawn a session and run claude login in the terminal. The previous wording pointed at the gateway-host shell workflow (works only for SSH- capable operators) and incorrectly implied a system ANTHROPIC_API_KEY fallback. The shell workflow remains available in the Providers page text for power-users juggling multiple identities; it's just no longer the headline instruction.

Changed#

  • Brand: web favicon, docs hero, iOS AppIcon.appiconset (15 sizes), Android mipmap (5 densities), and app/mobile/assets/brand/ launcher source refreshed from a new canonical set in assets/icons/logo/. Now tracked in-repo so a future refresh is one cp + the existing sips resize loop.

Added: install / uninstall / update tooling#

Lifecycle scripts and binary subcommands that grew out of a fresh- LXC end-to-end install test. Everything below is curl | bash– reachable, idempotent, and works on Linux (Ubuntu / Debian) + macOS; Windows is funneled through WSL2.

  • One-line installer wizard (#185 #186)
    • scripts/install.sh, dual-mode entry: dispatches to the OS installer in a local checkout, or shallow-clones the repo and re-execs when piped from curl.
    • scripts/install-linux.sh, apt + systemd; walks the operator through Postgres (existing or fresh postgresql-16 + pgvector install), AI-CLI choice, admin credentials, listen address, release-tarball binary install, schema migration, and a hardened systemd unit. Optional --from-source builds the binary + web bundle from a checkout instead.
    • scripts/install-macos.sh, brew + LaunchAgent (or --launchd-daemon for system-wide), same flow. Detects Apple Silicon vs Intel for the right release asset.
    • scripts/install-windows.ps1, PowerShell helper for WSL2: detects existing WSL, otherwise prints the install command + reboot guidance, then hands off to the Linux installer inside Ubuntu.
  • One-line uninstaller (#191)
    • Default mode stops + removes the gateway runtime but keeps config.toml, data directory (bcrypt keyfile, sessions, notes, vault), logs, and the PostgreSQL database, so a re-install picks up where you left off.
    • --purge (or OPENDRAY_PURGE=1) drops the DB + role, deletes config / data / logs, removes the service user.
    • Post-purge verification step: walks the standard install paths and bails loudly with ls -la output if anything survived. "No trace left" gets checked, not assumed.
  • opendray update subcommand (#194)
    • Fetches the latest GitHub release, picks the goreleaser asset matching this host's GOOS/GOARCH, verifies SHA-256 against the release's SHA256SUMS, then atomically replaces /proc/self/exe via temp+rename.
    • Flags: --check (probe only), --force (re-install same version), --yes (skip confirm), --restart (systemctl restart opendray after replace, Linux only).
    • Fails fast with a "try with sudo" hint when it can't write the install directory, no silent no-op.
  • opendray providers <list|update> (#194)
    • Detects installed AI CLIs (claude, gemini, codex), prints versions + paths.
    • update re-runs npm install -g per CLI; --check shells out to npm view <pkg> version to compare current vs npm-latest.
    • --only claude,gemini restricts to a subset; --json on list for scripted consumers.

Security#

  • Secrets out of config.toml (#192). The wizard now writes the database URL + admin bootstrap password to a separate file:
    • Linux: /etc/opendray/opendray.env (mode 0640 root:opendray), consumed by systemd via EnvironmentFile=.
    • macOS: ~/.opendray/opendray.env (mode 0600), consumed by a tiny launcher wrapper (~/.opendray/bin/opendray-launcher.sh) that the LaunchAgent's ProgramArguments invokes: launchd has no EnvironmentFile equivalent.
    • config.toml is now 0644 and contains only non-secrets (listen, log config, [admin].user, runtime data dir).
    • Existing opendray env-var override layer (OPENDRAY_DATABASE_URL, OPENDRAY_ADMIN_PASSWORD, etc.) does the actual wiring, no Go changes needed.

Fixed (install wizard, all reported during the LXC walkthrough)#

  • curl | bash prompts work, wizard re-attaches stdin to /dev/tty so EOF on the pipe doesn't make every read fail under set -e (#187).
  • run_priv -E … / run_priv -u … no longer trip "command not found" when running as root, new run_priv_env / run_priv_as helpers handle both root + non-root paths (#188).
  • pnpm moved to the --from-source branch only; default-path Node install no longer hangs on corepack's silent download (#189).
  • AI CLI install shows npm's progress bar instead of `--silent

    /dev/null` (so a 90-second download doesn't look like a hang) (#189).

  • Admin login works after install: wizard writes [admin].user in addition to the password; matches opendray's auth contract (#190).
  • Customisable admin username (was hard-coded to "admin") (#190).
  • Final-summary URL resolves the host's LAN IP for 0.0.0.0 listens instead of printing the <this-host> placeholder (#190).
  • Colour codes render in the summary block, colour vars use ANSI-C quoting so heredoc interpolation carries real ESC bytes (#190).
  • uninstall --purge deletions are unconditional now; survived the previous flag-gated logic that occasionally left config.toml on disk (#192).
  • Env-var alternative for the purge flag (OPENDRAY_PURGE=1 bash) that survives bash -s -- --flag paste-newline weirdness (#193).

Documentation#

  • README hero: typographic v2 logo + status / license / CI / GHCR badges + "What is opendray?" five-bullet section + paired EN / ZH README.md / README.zh.md (#180 #181 #182).
  • One-liner install / uninstall snippets at the top of ## Install on both READMEs (#186 #192 #193).
  • docs/getting-started.md (+ .zh.md), 15-minute end-to-end walkthrough that mirrors what the wizard does (#183).
  • docs/operator-guide.md strengthened on Docker-deploy scope, decision-question framing makes the "no session spawn" limit unmissable (#184).
  • scripts/README.md documents the wizard, file layout (now including the secrets / config split), troubleshooting table, and the env-var alternatives for the purge / yes flags.

Branding#

  • Unified launcher icons across web favicon, iOS AppIcon.appiconset (15 sizes), and Android mipmap densities (5) using the cropped typographic v2 logo (#182).

[v2.0.0], 2026-05-17#

Versioning realignment#

  • Re-tagged from the previous v1.0.0 tag (issue #165). The major version now reflects this codebase's identity as the second generation of the opendray product (opendray_v2). The previous v1.0.0 tag was deleted (had three duplicate draft releases on GitHub, all deleted; no published release; no downstream installers depend on it).
  • New VERSIONING.md documents the major-as-generation policy and what triggers future bumps.

Added#

  • Per-session bypass toggle in the Spawn dialog (mobile + web). Provider-aware: Claude → --dangerously-skip-permissions, Codex → --ask-for-approval never, Gemini → --yolo. Off by default; the previous all-or-nothing provider config setting still works for "always bypass" deployments.

Changed#

  • Spawn dialog's Claude account picker now appears immediately on open (mobile + web). Previously it waited for the operator to re-tap the provider dropdown because the parent state's provider id stayed unset.
  • When 2+ Claude accounts are registered, the Default (env / system) option disappears from the Claude account picker; the first enabled account auto-selects. Single-account setups retain the Default option.

Fixed#

  • Release workflow's ghcr job now produces image tags on workflow_dispatch. docker/metadata-action was reading github.ref (a branch when dispatched manually), so type=semver rules emitted zero tags and buildx failed with "tag is needed when pushing to registry". Each rule now passes value=${{ env.TAG }} so the same ruleset works for both push:tags and workflow_dispatch entry points.

Added#

  • Release workflow gains a ghcr job that builds the multi-arch Dockerfile (linux/amd64 + linux/arm64) and pushes to ghcr.io/opendray/opendray on every tag release. Job-scoped packages: write (the parent release job stays at contents+id-token least-privilege). Tag set covers :1.0.0, :1.0, :v1.0.0, plus :latest for non-prerelease semver. SHA-pinned actions throughout, matching the existing release- pipeline pattern.

  • .github/workflows/release.yml, automated release pipeline. Triggers on v* tag push (or manually via workflow_dispatch with a tag input). Produces a goreleaser draft release with:

    • cross-compiled archives (linux/darwin × amd64/arm64) + SHA256SUMS
    • cosign keyless OIDC signatures (SHA256SUMS.sig, SHA256SUMS.pem) via Sigstore Fulcio, no long-lived key
    • SPDX SBOM via anchore/sbom-action Permissions limited to contents: write (release upload) and id-token: write (cosign OIDC). Supply-chain hardening: SHA-pinned cosign-installer, sbom-action, and goreleaser-action; fail-fast tag-format validation on workflow_dispatch.
  • deploy/ directory with reference deploy artefacts:

    • deploy/systemd/opendray.service, production-ready systemd unit with sandboxing (NoNewPrivileges, ProtectSystem=strict, etc.), migrate-then-serve startup, 20s graceful-stop window.
    • deploy/lxc/proxmox-pty-notes.md, Proxmox-specific guide covering privileged vs unprivileged container PTY behaviour, the cgroup + bind-mount config required for unprivileged LXCs, networking + pgvector + pg_dump-version checks, and a pre-go-live checklist.
    • deploy/README.md, index pointing operators at the right artefact for their topology.
    • operator-guide.md "Where to look next" section now links to deploy/.
  • ADR 0016 (Proposed): backup-format v2 design for per-install PBKDF2 salt. Captures the four binding decisions (in-header storage, version-byte bump 1→2, per-Seal salt provenance, indefinite v1 read compat) and the three-PR rollout. Implementation pending.

  • LICENSE file (Apache 2.0), previously declared in README only.

  • SECURITY.md, threat model, default posture, deployment checklist, report channel.

  • CONTRIBUTING.md, dev setup, test commands, PR + commit conventions.

  • CHANGELOG.md, this file.

Changed#

  • internal/backup/cipher.go: 6-line comment on kdfSalt flagging it as a frozen v1 protocol constant and pointing at ADR 0016. No code behaviour change.
  • Renumbered ADR 0011-memory-subsystem.md0014-memory-subsystem.md to resolve the duplicate-0011 collision with 0011-channel-rich-content-and-bridge.md. Updated cross-references in README, ADR 0013, and the embed-onnx stub.

[v1.0.0, retracted], 2026-05-09#

Note. This tag was retracted on 2026-05-17 and the work it covered is folded into v2.0.0 above. See issue #165 and VERSIONING.md for the rationale. Original section preserved verbatim below for historical context.

First stable release. Tagged at commit fe96fd8 on main. Web frontend

  • backend feature-complete; mobile + Slack inbound + automated release workflow deferred to v1.x per the post-v1.0 roadmap. v1 (Opendray/opendray) keeps running in production through this quarter per ADR 0001.

The feature inventory below was originally captured under [v1.0-rc] — 2026-05-05; section was promoted to [v1.0.0] on tag.

Added (since the greenfield start)#

  • M0, composition root: internal/app/, config loader (internal/config/), pgx pool + hand-rolled migration runner (internal/store/), event bus (internal/eventbus/), structured logging via slog.
  • M1, sessions: PTY lifecycle, ring-buffer streaming, WS handler, resume-via-reconnect (per ADR 0003).
  • M2, CLI catalog: provider manifests + per-id user config (internal/catalog/).
  • M2.5, admin auth: bearer tokens with constant-time password compare and 24h TTL (internal/auth/).
  • M3, integrations: external-app registry, /api/v1/proxy/{prefix}/* reverse proxy, integration call log (internal/integration/, ADR 0006, ADR 0010).
  • M4, channels: channel hub + Telegram, Slack, Discord, DingTalk, Feishu, WeChat, WeCom (internal/channel/, ADR 0005, ADR 0011-channel).
  • Memory: built-in pgvector cross-CLI memory layer (internal/memory/, ADR 0014). Three-CLI mirror keeps Claude / Codex / Gemini transcripts aligned. ONNX local-embedding optional via -tags local_onnx.
  • Ambient memory: auto-capture from active sessions + auto-injection on session start (ADR 0013).
  • Backup + export: AES-256-GCM encrypted PostgreSQL dumps, S3/WebDAV/SFTP/rclone targets, admin export/import bundles (internal/backup/, ADR 0012).
  • Web admin (W0–W5): React 19 + Vite + Tailwind v4 + shadcn/ui + TanStack Router/Query + Zustand + xterm.js. Single SPA bundled into the Go binary via go:embed (ADR 0007, ADR 0008).
  • Events stream: admin-bearer-authed /api/v1/integrations/_events WebSocket (ADR 0009).

Deferred to post-v1.0#

  • Mobile (Flutter) client, replaced by responsive web in v2 phase 2.
  • Slack inbound (M5+).
  • Deploy automation (release toolchain, goreleaser, Dockerfile, systemd unit) lands in a follow-up PR.
  • e2e Playwright harness.