Changelog

The project should feel alive

Readable release notes from GitHub, designed for humans.

Latest release

v2.7.2

Jun 4, 2026

See CHANGELOG.md for the full release notes.

opendray update
View release
v2.7.2 Current

v2.7.2

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.7.1 Patch

v2.7.1

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

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.
View details

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.
Open on GitHub
v2.7.0 Stable

v2.7.0

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.
View details

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.
Open on GitHub
v2.6.0 Stable

v2.6.0

Web/mobile gain a real PR detail surface and a read only Issues surface, both backed by the existing git provider plumbing.

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.

Distribution channels in this release

  • GitHub release tarballs — built and signed by goreleaser (this page).
  • npm[email protected] was not published in this release: the Publish to npm job in release.yml failed with NODE_AUTH_TOKEN not set. The NPM_TOKEN repository secret needs to be configured (see #280 for the workflow). The npm publish can be retried by running the workflow_dispatch on Publish to npm once the secret is set, or it will publish automatically on the next tag. Run-of-shame: https://github.com/Opendray/opendray/actions/runs/26753088400
View details

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.

Distribution channels in this release

  • GitHub release tarballs — built and signed by goreleaser (this page).
  • npm[email protected] was not published in this release: the Publish to npm job in release.yml failed with NODE_AUTH_TOKEN not set. The NPM_TOKEN repository secret needs to be configured (see #280 for the workflow). The npm publish can be retried by running the workflow_dispatch on Publish to npm once the secret is set, or it will publish automatically on the next tag. Run-of-shame: https://github.com/Opendray/opendray/actions/runs/26753088400
Open on GitHub
v2.5.0 Stable

v2.5.0

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 conver

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.
View details

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.
Open on GitHub
v2.4.0 Stable

v2.4.0

Multi Claude account UX, two way Telegram channel, and a clutch of session quality fixes.

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.

Improved

  • 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.
View details

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.
Open on GitHub
v2.3.4 Patch

v2.3.4

[v2.3.4] — 2026 05 29 Fixed Language toggle in the web Topbar moved its checkmark but UI strings didn't switch.

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).

Improved

  • 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).

View details

[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).

Open on GitHub
v2.3.3 Patch

v2.3.3

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.3.2 Patch

v2.3.2

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.3.1 Patch

v2.3.1

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.3.0 Stable

v2.3.0

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.2.2 Patch

v2.2.2

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.2.1 Patch

v2.2.1

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.2.0 Stable

v2.2.0

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.1.1 Patch

v2.1.1

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.1.0 Stable

v2.1.0

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.0.5 Patch

v2.0.5

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.0.4 Patch

v2.0.4

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.0.3 Patch

v2.0.3

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
v2.0.2 Patch

v2.0.2

See CHANGELOG.md for the full release notes.

See CHANGELOG.md for the full release notes.

Open on GitHub
NextUpcoming

Operator polish

Operator polish, reconnect stability, memory improvements, and clearer docs.