282 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Fixed
- Android session rows stay neutral when optional live activity is unavailable or still loading. Directory refreshes no longer restore a persistent Checking state, and full-row activity borders are reserved for actual Starting or Working turns.
- Returning from parent settings keeps Supervised Chat rendered. Parent access now relocks without rebuilding the active navigation graph, and full Settings keeps a prominent shortcut back to Supervised Mode controls.
[Android 1.13.1] - 2026-08-25
Fixed
- Android session activity now follows live Hermes runtime truth. Working, Starting, Needs input, Idle, Checking, Unavailable, and Background work no longer come from the Dashboard's five-minute recency hint, and only complete, unambiguously resolved live snapshots clear stale state.
[Android 1.13.0] - 2026-08-25
Added
- Provider usage and limits are available from top-level Settings. Codex credential pools, Nous balances, and OpenCode Go account windows share one provider-neutral screen with Summary, Expanded, and Hidden presentation modes. Provider credentials remain on the Hermes host.
- Android Bot Mode provides one messenger-style workspace across saved Hermes gateways. Bots and read-only group rooms aggregate without changing the foreground connection, Bot Chats retain exact gateway/profile ownership, and unavailable gateways keep clearly marked last-known roster entries.
- Android Assistant screen context. Compatible unlocked assistant-button invocations can open Hermes, begin listening, and include bounded visible text plus an available screenshot in the first Standard voice turn. Ordinary wake and keyguard invocations remain screen-context free.
- Android Supervised Mode presents a parent-controlled, profile-pinned chat surface. Parents can limit attachments, Standard voice, generated media, conversation history, actions, and technical metadata while device authentication protects full settings. Hermes-Relay can identify and revoke a paired supervised client without becoming the policy enforcement boundary.
Changed
- Android releases and review candidates use clear public product names. Stable builds use
Hermes-Relay Android, while isolated review installs useHR Candidatewithout changing package identities or update contracts. - Review candidates are explicit and source-pinned. Maintainers can opt a PR into a matched Android and Relay bundle with checksums, expiry, source SHA, and bounded review instructions.
Fixed
- Unlabeled PR updates no longer receive false candidate-failure comments. The trusted reporter ignores skipped review-bundle workflow shells before reading artifacts or writing to a PR.
- Android chats no longer retain a stale busy composer. A completed Gateway bubble settles automatically when its exact session has no live or detached turn, new-chat navigation clears stale visible ownership, and Stop remains an immediate escape hatch. (#416, #418)
- README and Google Play onboarding now match the Dashboard-first product path. Public setup copy names the two separate Dashboard QR actions, treats the API server as an advanced fallback, explains the encouraged Hermes-Relay extension without implying Play includes Device Control, and ships one current deterministic Android screenshot set.
- The Android Sphere remains gently animated while visibly idle. New chats and the ambient Sphere behind messages now use a low-cost layer breath, while hidden/backgrounded and motion-disabled surfaces stay still and active agent/voice states retain their full procedural animation.
- Android retries Windows-hosted
MEDIA:attachments through Relay's by-path route. A document deferred on cellular no longer treatsC:\...as an opaque media token and reports it as expired.
[Plugin 1.10.0] - 2026-08-25
Added
- Relay provides normalized provider usage without exposing credentials. The authenticated Dashboard route resolves the active Codex pool entry, structured Nous balances, and OpenCode Go windows on the Hermes host; explicitly enabled paired clients receive the same provider-neutral schema.
Changed
- Plugin releases use the
Hermes-Relay Pluginpublic name. The display name is aligned with Android and CLI+UI while theserver-v*compatibility tag remains unchanged.
Fixed
- Relay profile discovery follows
HERMES_HOMEby default. Custom Hermes installations surface their real default profile and persist Relay sessions beside the active config while retaining the explicitRELAY_HERMES_CONFIGoverride.
[0.4.0-beta.5] - 2026-08-25
Added
- Desktop releases now include a Linux ARM64 CLI artifact. The one-line installer, updater, checksums, release publication, architecture validation, and platform documentation all recognize the same
linux-arm64binary. - The public site now shows the real Windows CLI UI and guides each surface through first use. Deterministic public-safe screenshots cover connection, host access, activity, computer control, and updates.
Changed
- Desktop releases use the
Hermes-Relay CLI+UIpublic name. The beta keeps its existingdesktop-v*tag and updater contract.
Fixed
- Desktop install and update discovery remains reliable in a multi-surface release repository. Every resolver paginates GitHub releases before choosing the SemVer maximum, Windows cooperative updates clean their released backup, unsigned preview installers retain the normal SmartScreen warning, and release smoke tests preserve real exit codes.
- Desktop daemon connections recover instead of exiting after an interrupted Relay socket. Healthy daemons retry through Relay restarts and repeated failed reconnect attempts, oversized desktop-tool results fail within a bounded response instead of closing the shared WebSocket, and terminal failures leave an accurate stopped status for the tray.
- Desktop computer control follows Hermes' current CUA Driver contract. CUA Driver 0.20 and newer are accepted when their manifest, daemon/MCP arguments, required tools, and canonical path remain compatible, and Windows sessions use the manifest-declared direct standard-mode runtime instead of a potentially stale machine-wide daemon. Current 0.21 installations no longer fall back solely because of an obsolete upper version pin or daemon contract.
[Android 1.12.1] - 2026-08-22
Fixed
- Android shares open as complete reviewable drafts. Shared links and text now survive fresh-chat draft restoration, while single or multiple shared images and files enter the same composer attachment flow. Mixed text-and-file shares are supported and nothing is sent automatically.
- Adding or renewing an Android connection no longer stalls during local preparation. Pair setup keeps its allocated target exact, performs an explicit validated handoff when renewing an existing connection, and continues with that connection's scoped authentication state.
- Unavailable Android chat routes now fail visibly. Send attempts with no usable Gateway or API fallback expose a retryable failure, while required profile-scoped history reads report an error instead of treating the wrong or missing history as an empty conversation.
- Android Diagnostics reports secure-storage degradation and recovery without exposing credentials. Keystore fallback, encrypted-store self-healing, and temporary in-memory storage are recorded with secret-free recovery guidance.
[Android 1.12.0] - 2026-08-21
Added
- Android can create and save custom themes. The Custom workshop provides a live chat preview, editable Background, Surface, Accent, and Text roles, Light or Dark ownership, saved Soft/Balanced/Sharp shape, and bounded rename, duplicate, and delete actions. Up to 20 presets remain local to the device.
- Maintainers can build matched Android and Relay review candidates without cutting a release. Candidate artifacts share exact source provenance and checksums, install beside stable builds with isolated data, and remain excluded from stable update prompts.
Changed
- Appearance shape now applies consistently across the app. Soft, Balanced, and Sharp styling reaches chat, settings, sheets, dialogs, terminal, voice, Bridge, and other shared surfaces, while accent and shape changes apply immediately. (#385)
- Selecting an All Profiles session now activates its owning agent. Header identity, avatar, transcript, drafts, routing, and persistence move together. Merely browsing All Profiles changes nothing, and a profile lock hides All Profiles and rejects cross-profile opens.
Fixed
- Language changes preserve the active profile and session. Activity recreation retains the exact connection, agent, session, and All Profiles browser state without replacing them with stale persisted values. The persistent connection notification also relocalizes without reconnecting. (#381)
- Gateway chats recover when a terminal frame is missed. An authoritative idle state settles the active turn, retains its durable session, and reconciles history without resubmitting through fallback transport. (#365)
- Relay endpoint forms normalize to the correct sibling routes. Saved base,
/ws, and/healthURLs resolve idempotently without producing paths such as/relay/ws/health; malformed or ambiguous routes still fail closed. (#380)
[Server 1.9.0] - 2026-08-21
Added
- Reconnect-delivered phone messages carry explicit backlog context. Relay marks messages flushed from its bounded offline queue and emits one ordered completion event so compatible clients can label delayed messages and summarize the batch without generating one banner per item.
- Phone status reports granular Bridge capability grants. Human-readable status and the
android_phone_statustool distinguish permanent, timed, and unlimited capabilities while retaining the existing Android permission and safety state.
[1.11.0] - 2026-08-20
Added
- Sideload Bridge access is explicitly capability-scoped. Read-only, read-and-confirm, and custom presets grant only selected powers for the active connection. Screen inspection and control can be allowed for a bounded period or explicitly left unlimited, and Relay status reports the resulting permanent, timed, and unlimited grants.
Changed
- The sideload Bridge screen is a summary-first access cockpit. Agent access, unattended mode, selected Android requirements, and advanced safety controls are separated clearly while the complete permission matrix and power-user controls remain available one tap deeper.
Fixed
- Android keeps failed session resumes visible and in context. Continuing a stored Gateway session no longer falls through to a fresh session when Hermes rejects or mis-scopes the resume. Failed turns remain error-marked and expose a composer-adjacent recovery panel with route-aware details, explicit retry/dismiss actions, and sanitized Diagnostics evidence.
- Software-keyboard Return inserts a newline across both common Android IME paths. Keyboards that commit text directly and keyboards that synthesize
KEYCODE_ENTERnow keep multiline composition separate from physical-keyboard Send behavior. (#367) - Cancelled answer recovery retains its Stopped status. Empty recovery placeholders with a persistent status badge are no longer discarded during stream finalization.
- Android screen-on idle no longer continuously redraws the ASCII sphere. Idle holds a stable frame while thinking, streaming, and voice states retain full-rate motion; inactive voice waveforms and closed session drawers also stop their frame loops.
- Android capture and audio effects release power-sensitive resources at their actual lifecycle boundaries. Screen capture attaches its MediaProjection surface only for a requested frame, unattended Bridge wake locks release when the command finishes, and barge-in AEC/noise suppression attach to the microphone capture session instead of playback.
- Experimental wake-word listening reuses its PCM normalization buffer. Continuous opt-in listening no longer allocates a new float frame for every inference call.
[1.10.0] - 2026-08-18
Added
- Android preserves composer drafts across app restarts. Text, quote/edit context, and pending attachments remain scoped to their exact connection, profile, and session in bounded app-private no-backup storage, and successful sends remove the saved draft.
- Android can turn large pastes into reviewable text attachments. The default-on Chat setting converts inserts of at least 5,000 characters into a compact attachment while preserving surrounding text; Gateway uploads the file through upstream Hermes and fallback transports retain the pasted content as text.
- Android renders Markdown incrementally while replies stream. The native streaming parser retains stable message, selection, and AST identities from the first token through completion, including provisional paragraphs, lists, links, fenced code, and tables.
Fixed
- The Android software keyboard exposes Return in the multiline composer. The dedicated composer button sends, while physical Enter, Shift+Enter, and caret-arrow behavior remain unchanged. (#367)
- Open chats reattach after Android returns to the foreground. Gateway reconnect restores the visible session subscription and reconciles missed work without requiring the user to leave and reopen the conversation. (#365)
- Imported credentials fail closed before network or secure-state mutation. Control characters and malformed values are rejected before header construction or encrypted-state replacement without logging credential material.
- Streaming follow remains stable through completion. Deliberate scrollback stays untouched, bottom-follow uses one bounded owner, and Markdown, voice actions, timestamps, and token metadata settle without rebuilding the bubble or resetting its scroll anchor. (#341)
[1.9.1] - 2026-08-16
Added
- Android adopts Hermes-owned profile creation, shared avatars, and animated pets. Current Gateways provide the profile roster, explicit shared/copied/isolated authentication choices, partial create outcomes, validated avatar upload/fetch/clear, and profile-scoped pet selection that follows the agent across supported Hermes clients. Older hosts retain authenticated Dashboard creation plus Relay/local presentation fallbacks, and profile deletion remains Dashboard-only.
- Android identifies proactive messages delivered after reconnect. Relay marks messages flushed from its bounded offline queue, Thread bubbles label them as received “While away,” and Android shows one accessible localized summary for the completed batch.
- Android can create finite recurring schedules from Manage. The native editor uses the authenticated Hermes Gateway
cron.managecontract, optionally stops after 1–999 runs, and rejects invalid counts rather than silently creating unlimited work. - Chat resets retain content-free local evidence. New-chat and Thread transitions save a bounded app-private checkpoint for user-reviewed Diagnostics without prompts, message text, IDs, profile names, paths, URLs, media, tool payloads, secrets, or telemetry.
- Android surfaces host resource risk before chat state is lost. Current Hermes Dashboard memory and disk pressure signals render as a persistent, capability-gated warning; older hosts remain unchanged and no telemetry is added.
- Android honors Hermes model-selection safeguards. Every Gateway model transition, including fresh-chat and Server-default choices, now avoids raw session overrides; picks requiring cost or data-training consent show Hermes' exact warning and apply only after a confirmed second request.
Changed
- Profile identity sources are explicit in Agent Passport. Server-owned static avatars and upstream pets follow the Hermes profile, while phone picks, Relay-host imports, phone-only animated icons, and Sphere skins remain separate local presentation choices.
- Interactive Gateway asks remain resolver-bound. Android continues to use upstream clarify, approval, sudo, and secret response RPCs; connector-only prompt/reaction operations are not copied into Relay cards as a second approval protocol.
Fixed
-
Shared avatar picks now persist from Android's filesystem picker. The app accepts any image Android can decode, applies display orientation, and safely resizes or re-encodes it to the upstream PNG/JPEG/WebP and 2,000,000-byte contract. Successful writes update the local shared cache immediately, and upload failures remain visible beside the control.
-
Nous-hosted Android sign-in follows the official native broker contract. The gateway now selects its native provider exactly as Hermes Desktop does, callback attempts retain the upstream five-minute window, and post-callback failures explain whether the one-time code, hosted gateway, network, response, or secure storage prevented session creation without exposing auth material.
-
Android edit-and-regenerate fails closed on incomplete durable history. Mixed Gateway transcripts now require the selected message's durable row identity instead of attempting an ordinal-only rewind, while older Hermes histories with no row identities remain editable.
-
Android fails closed when a Gateway does not confirm the selected profile. Named-profile session creation and recovery now require Hermes to echo the exact owning profile, preventing stale or older gateways from silently running the launch profile under another agent's identity. Profile inspection also keeps read-only Gateway data available when
profiles.configureis unsupported while disabling further write attempts without discarding drafts. -
Android attachment sends are bounded and fail closed. Picked files are size-limited while streaming into the encoder, cold and queued Gateway sends upload only after the exact session is ready, and an unsupported or interrupted document upload no longer falls through to a text-only route while its file card implies delivery. Every attachment type retains the same compact collapse/expand affordance.
[0.4.0-beta.4] - 2026-08-15
Fixed
- The Windows management UI remains available while the daemon is stopped. Missing, stale, malformed, or temporarily unavailable daemon status now resolves to an explicit stopped state instead of trapping the tray on its loading screen, so configuration, diagnostics, host management, and daemon controls remain accessible.
[1.9.0] - 2026-08-14
Added
- Android session browsing matches Hermes Desktop's recent organization model. The primary session drawer can toggle between the active profile and all profiles, group by recency, project, status, or profile, order by supported session metrics, and narrow rows by status, project, profile, or pull-request state without collapsing duplicate IDs across profile stores. Named profiles receive stable identity-color badges with locally persisted color overrides.
- Android can edit current Hermes profiles through the standard Gateway. The Profile Inspector capability-gates
profiles.describeandprofiles.configure, keeps Relay-only memory editing and older-Hermes fallback intact, and reports partial section saves without discarding failed drafts. - Android sessions show their coding context when Hermes supplies it. Session rows can display repository, Git branch, and the current state of the pull request created by that session while older hosts remain unchanged.
- Android Manage can now finish host-owned backup workflows, edit or remove learning nodes with explicit recovery guidance, configure and activate memory providers, and complete profile-scoped WhatsApp QR onboarding through the authenticated upstream Dashboard contracts.
Fixed
- Android network clients shut down safely during route changes. Replacing an authenticated Dashboard client now moves OkHttp connection-pool eviction off the main thread, preventing a live TLS socket close from crashing the app with
NetworkOnMainThreadException. (#334) - Android preserves authoritative Gateway outcomes. Protected-file cards cannot offer forbidden persistent scopes, compression no-ops show the server result, bounded resume failures do not create context-free replacement sessions, and edit/regenerate retains durable row identities across consecutive rewinds.
- Android routes and uploads against live upstream truth. Multiplex API fallback trusts
served_profilesinstead of installed profiles, and generic documents carry the Gateway-issued@file:reference into ordinary and queued prompts. - Android clarify cards preserve upstream decision semantics. Multi-select prompts keep independent selections and submit one exact list, while server expiry events—not an invented local deadline—retire unanswered cards.
- Android keeps profile management and retained automation truthful. Custom Endpoint list and mutation routes now follow the selected Hermes profile, while completed one-shot cron jobs show their retained outcome and expose only valid Runs/Delete actions.
- Android and Relay recover more generated media reliably. Android accepts upstream-valid wrapped, punctuated, adjacent, spaced, and Windows
MEDIA:markers without consuming fenced examples, and Relay translates Docker-visible workspace, home, cache, and configured-mount paths before applying its existing credential, sandbox, and size checks. - Android keeps cross-profile sessions with their owning agent. Opening a session from All Profiles hydrates, resumes, sends, and renders with that session's profile without changing the global profile selection; New Chat from that view starts with the default profile.
- Android reactions and standard voice follow the active conversation. Reactions resolve durable rows for both user and assistant messages, while Vanilla Hermes voice remains on the authenticated Gateway instead of requiring the optional API fallback.
- Android session navigation behaves predictably. The drawer closes on outside taps, uses an ungrouped recent-session list by default, retains project grouping as an explicit option, and exposes secondary actions in All Profiles mode.
[0.4.0-beta.3] - 2026-08-14
Fixed
- Windows tray polling can no longer accumulate unbounded helper processes. Grant discovery now uses lightweight local state, management refreshes are single-flight and visibility-aware, and child probes have hard timeouts, bounded output, tree cleanup, caching, and backoff. A dedicated bounded
tray.logrecords sanitized operational failures without mixing them into daemon logs. - Concurrent Desktop lifecycle requests cannot start duplicate daemons. Cross-process lifecycle and runtime ownership locks serialize startup and recovery while preserving stale-owner cleanup.
[1.8.0] - 2026-08-14
Added
- Official Hermes Desktop can surface Relay through its supported runtime Plugin SDK. The unified plugin package now includes an opt-in, profile-scoped Desktop pane for Relay status, paired devices, bridge activity, media, pairing, revocation, and remote-access management. Loading, startup, reconnects, profile changes, and updates never open it; only labeled sidebar, status-bar, or command-palette actions register and reveal the movable native pane.
[0.4.0-beta.2] - 2026-08-14
Added
- Desktop Activity now keeps inspectable local evidence. Commands, files, devices, connection lifecycle, and computer control share a truthful event stepper with dedicated failure details; screenshot events can retain bounded local PNG evidence and open it in a larger borderless viewer. Settings controls retention as Off, 1 day, 7 days, or 30 days and shows local file usage.
Fixed
- Tunnel state stays responsive through interruption and retry. The CLI UI distinguishes connected, reconnecting, and stopped states, exposes retry attempt/timing and a Retry now action, records connection failures and recovery in Activity, and shows compact connection cards only while the main UI is hidden.
- Windows CUA readiness no longer depends on the flaky whole-desktop health scan. Hermes-Relay verifies the canonical runtime, manifest, required tools, daemon, and safe permission mode before starting structured sessions, while accessibility health remains an explicit CLI/UI diagnostic that can be rechecked without forcing the compatibility backend. This temporary workaround is scoped to the upstream fixed-timeout issue and keeps individual actions fail-closed.
[0.4.0-beta.1] - 2026-08-14
Added
- CUA Driver is the preferred Windows structured-control engine. New local settings prefer a verified CUA runtime for window-targeted background actions, fresh snapshot tokens, and optional per-session animated agent cursors without moving the physical pointer; Windows Input is the explicit compatibility backend and backend choice is fixed for each control session. Full-display observation remains on the read-only system capture path. CLI and UI can explicitly install, check, or update the canonical CUA package after verifying the upstream release manifest and installer checksum; nothing is bundled or updated automatically, driver telemetry stays off for Hermes sessions, and activity records contain only bounded, redacted control metadata.
Fixed
- Windows bundle updates fail closed when installed processes retain a binary lock. Setup waits for the invoking CLI, quiesces the tray and its short-lived CLI children, checks every payload extraction before writing release metadata, preserves custom install directories, and returns a failure instead of reporting a mixed-version installation.
- CUA readiness follows the published driver contract. Hermes accepts the documented
okhealth state, distinguishes an installed-but-degraded runtime from a missing installation, and constructs trusted Windows installer paths consistently across verification environments.
[1.7.0] - 2026-08-13
Added
- Hermes Secure Link provides self-hosted pinned TLS ingress. Relay, API, and Dashboard namespaces share one operator-owned TLS endpoint while retaining their native authentication boundaries, QR-carried certificate continuity, explicit rotation, and fail-closed route validation.
- Hermes Reach is available for explicit experimentation. The optional self-hosted rendezvous broker carries opaque Secure Link TLS records over outbound-only connections with bounded multiplexing, hashed credentials, replay protection, persistence, revocation, and no access to Hermes payloads.
- Remote-access management exposes supported reachability clearly. Dashboard status and pairing metadata distinguish Tailscale reachability, Secure Link transport protection, direct routes, and experimental Reach without presenting the broker as a replacement for authentication.
Changed
- Tailscale is the recommended remote route. Pairing, Dashboard, documentation, and public site guidance present Tailscale as the easiest supported remote-access path; Reach remains disabled by default, advanced, and lower priority than supported routes.
- Relay voice custom transports follow upstream provider security options. Relay-owned OpenAI/xAI realtime and TTS clients honor custom headers, custom CA bundles, standard CA environment precedence, and an explicitly warned development-only verification override.
- Voice Lab xAI sign-in uses device authorization. The standalone login shows a verification URL and user code and polls for approval without requiring a loopback callback.
Fixed
- Phone delivery remains compatible with strict Hermes targets. Version-tolerant parser and validator hooks retain older-host registration and exactly-once standalone delivery.
- Profile-owned Relay registrations stay isolated. Current Hermes uses profile-scoped ownership and context-local profile homes while legacy hosts retain a guarded compatibility path.
- Phone is discoverable before its first historical session. The Relay phone adapter publishes its configured home destination through Hermes' standard channel directory.
[0.4.0-alpha.8] - 2026-08-13
Added
- Windows management separates each Relay host from this PC. Host detail owns identity, pairing, access, capabilities, authorized clients, re-pairing, and guarded removal; Settings owns local daemon lifecycle, startup, privilege, terminal, logs, diagnostics, updates, and Help & About.
- Desktop access uses clear host-scoped presets and capabilities. Restricted, Ask Every Time, Standard, Full Access, and Custom remain explicit across commands, files, screen/input, USB, microphone, and camera controls.
- Activity drilldown preserves bounded execution evidence. Overview shows the latest three events and detail views expose request, output, result, exit, duration, and truncation metadata without copying sensitive inputs.
- Connection presentation shows the live Agent-to-PC path. Host selection, bidirectional packet motion, transition feedback, route details, and connection testing stay compact, responsive, and reduced-motion aware.
Changed
- Connect and disconnect remain responsive during daemon work. Lifecycle calls and snapshot collection run outside the UI thread, transition status polls quickly without overlapping probes, and progress remains visible until authoritative daemon state arrives.
- Tailscale is recommended for remote access. Secure Link and direct TLS routes remain supported, while Hermes Reach is visibly experimental and lower priority.
Fixed
- Connection tests classify legacy private routes correctly. A saved generic role is inferred from its actual endpoint, so LAN and Tailscale routes no longer appear as Custom VPN; results include reachability, latency, security, endpoint, and route count.
- Ask-mode approval cards show the requested action. A bounded preview appears in the compact card with full context and an Open in UI action.
- Mixed capability policies are labeled Custom. Overview no longer claims a preset when individual capability controls differ.
- Tray placement follows the notification-area monitor and DPI. Responsive popup geometry stays anchored above the tray icon across compact and high-DPI desktops.
- PowerShell success output is complete and self-describing. Scalar, pipeline, JSON, native stdout/stderr, exit status, and truncation metadata survive the desktop RPC response.
[1.6.4] - 2026-08-12
Added
- Desktop tools support explicit host targeting. Every client-routed desktop tool accepts a stable device ID or unambiguous computer name, and
/desktop/healthenumerates connected targets and their advertised tools. - USB operations retain both routing scopes. Raw USB and ADB tools use
deviceto select the desktop PC, while ADB operations continue to useserialto select hardware attached to that PC.
Fixed
- Multiple desktop clients remain connected simultaneously. The Relay no longer replaces the previous desktop when another heartbeat arrives; concurrent requests are bound to their selected WebSockets, responses from another PC are ignored, and an untargeted call fails closed when several desktops are online.
- Pairing another desktop preserves existing credentials. Legacy placeholder device identifiers are treated as absent instead of shared ownership, preventing an unrelated PC from revoking the first desktop's session.
[1.6.3] - 2026-08-11
Fixed
- Relay diagnostics distinguish a prior clean stop from a crash. Doctor and
/relay/infoexpose only bounded clean, unclean, or unknown gateway-exit state with an optional suspected out-of-memory hint, without returning raw log evidence. - Relay reconnects spread out after shared gateway restarts. Ordinary exponential reconnect delays use full jitter while explicit reconnects and server-directed retry timing retain their exact behavior.
[0.4.0-alpha.7] - 2026-08-11
Fixed
- Installer lifecycle validation uses an isolated Windows PATH fixture. Release smoke tests now verify add/remove cleanup against a fixed registry value and restore the runner's original value afterward, independently of the temporary profile used for session-preservation checks.
[0.4.0-alpha.6] - 2026-08-11
Fixed
- Installer cleanup validation compares the unexpanded Windows PATH. Release smoke tests now read the raw user registry value, ensuring
%USERPROFILE%entries are verified without temporary-profile expansion changing their apparent value.
[0.4.0-alpha.5] - 2026-08-11
Fixed
- Installer cleanup validation handles expandable Windows PATH entries. Release smoke tests restore the original profile environment before comparing user PATH, avoiding false failures when unchanged
%USERPROFILE%entries are expanded inside an isolated test profile.
[0.4.0-alpha.4] - 2026-08-11
Fixed
- Windows release validation waits for installer processes. The packaged install/uninstall lifecycle smoke now captures GUI-subsystem process exit codes reliably before validating installed files, preserved sessions, registry state, and cleanup.
[0.4.0-alpha.3] - 2026-08-11
Added
- Windows tray provides focused remote-access management. The compact host-aware popup covers connection state, per-host Ask/Trusted/Full Access, pending grant dialogs, authorized-client revocation, activity, daemon controls, and settings without adding chat, terminal, plugin, voice, or session surfaces.
- Desktop access policy is isolated per Hermes host.
hermes-relay hostslists and selects local pairings and stores fail-closed access modes independently for each canonical relay URL. - Windows CLI installations can add or open the management UI directly.
hermes-relay ui install|open|statusand the installed UI shim provide a supported lifecycle for optional UI setup, discovery, and activation.
Changed
- Daemon connectivity no longer requires a tool grant. Ask mode can keep an authenticated daemon connected with zero desktop tools attached; Trusted enables command/file tools with task-scoped screen/input grants, while Full Access removes those task prompts only for the selected host.
- Windows bundle updates preserve the desktop lifecycle. The CLI and tray coordinate one verified installer launch, restore the daemon and UI after setup, and permit same-version UI add or repair without silently downgrading a newer CLI.
Fixed
- Background daemon start reports real readiness. Detached startup now waits for the spawned process to authenticate and connect, and returns actionable log evidence for configuration, authentication, early-exit, and timeout failures.
- Local and release tray builds embed the packaged UI. Development installs use Tauri's production protocol instead of attempting to load a missing localhost development server, and release CI exercises a silent install/uninstall lifecycle.
- Windows-trusted certificates work in the desktop CLI. The packaged Windows binary and newer Node runtimes add the Windows certificate store without dropping bundled or operator-supplied roots, while TLS verification and Relay certificate pinning remain enforced.
[1.6.2] - 2026-08-11
Fixed
- Paired sessions use recognizable device identities. Relay sessions preserve a client-provided hostname as the primary name, retain model and platform details, and enrich valid reconnects without requiring users to pair again.
- Long-lived session expiry is readable. The Dashboard presents paired-session lifetime in days or weeks with the exact local deadline available in the detail view instead of accumulating hundreds of hours.
[Android 1.8.1] - 2026-08-09
Fixed
- Android preserves complete long-session transcripts. API-server and profile-scoped Dashboard history reads now use explicit bounded pagination, retain compatibility with older unpaginated responses, and keep edit, retry, sharing, and recovery anchors stable beyond Hermes' latest-500 default window.
- Android follows authoritative Gateway turn contracts. Submit rejections retain the server's message without silently falling through to SSE, event envelopes reconcile consistently, and edit-and-regenerate requests send the required truncation confirmation.
[Android 1.8.0] - 2026-08-09
Added
- Android chat keeps work in context and makes live turns easier to read. Draft text, edits, quotes, and attachments stay with their connection, profile, and session; conversation search and prompt-turn navigation jump by stable message identity; message actions reveal smoothly on tap; quoted replies use linked previews without placing markup in the composer; assistant replies retain their compact high-contrast bubbles; and pending attachments support preview, removal, and accessible reordering.
- Android reasoning and tool activity use a quieter transcript. Live thinking opens as an inline disclosure and settles to a collapsed Thought row, while consecutive routine reads, searches, commands, browser actions, and device actions share one live activity ticker or concise completed summary. Approvals, failures, generated media, file changes, output risks, and delegated work keep their own visible lifecycle surfaces even when ordinary tool progress is hidden.
- Android Profile Shelf makes agent switching immediate without mixing conversations. The Chat header expands a compact, accessible shelf with ordered profile avatars, a subtle Server-default home badge on the resolved identity, last-session restoration, display hiding, lock controls, and one full switcher shared with Agent Passport.
- Android accepts shared text as a new Chat draft. Hermes Relay now appears in the system sharesheet for text, opens the active profile in a fresh conversation, and fills the composer for review without sending automatically.
Changed
- Android appearance controls are more expressive and easier to preview. Theme presets, accent and shape customization, imported Sphere skins, and custom pet creation share one live-preview workflow while preserving separate agent, background, and companion identities.
Fixed
- Android restores complete Gateway activity and makes settled replies speakable. Successful Gateway turns reconcile structured persisted tool calls even when an upstream server omits live tool lifecycle events, and a configured voice can read a completed assistant reply from its message actions without requiring Voice Mode. While that narration is active, the same message actions expose Stop without cancelling an unrelated chat turn.
- Android chat matches standard keyboard, scrolling, and photo behavior. Sentence capitalization is enabled, physical Enter can send or insert a newline according to a device-level setting, Ctrl/Command+Enter always submits, directional keys stay with the text caret, expanded thinking and tool content retains bottom-follow until the user scrolls away, and portrait attachments honor their EXIF orientation in previews and message viewers.
- Android distinguishes live-turn corrections from queued follow-ups. The composer names its current action with visible text and accessible state, successful gateway redirects show a correction lifecycle marker, and attachment-bearing follow-ups always enter the session-owned queue because the upstream redirect operation is text-only.
- Android visibly explains quiet startup work without an empty chat bubble. The full-size thinking animation now sits directly in the conversation lane with a stable reviewable status until the first answer text arrives, while recovery keeps its explicit reconnecting state.
- Android keeps pets and screen chrome inside safe interaction bounds. Floating companions avoid agent identity rows and controls during scrolling, remain touchable for their menu, and settings headers respect edge-to-edge system insets.
[Android 1.7.1] - 2026-08-08
Fixed
- Android chat follows a growing live reply. Bottom-owned conversations now observe each replacement of the streaming message list, keeping newly added lines visible while preserving the reader's position after a manual scroll away.
- Hosted Hermes onboarding completes through the official Dashboard sign-in path. Android recognizes hosted account addresses, uses the system-browser native PKCE flow, and resumes the verified Dashboard session after its loopback callback.
- Live Android tool cards remain expandable while a run is active. Streaming Gateway updates preserve stable card identity and merge tool arguments and result previews into the existing row, so details can be opened before the session finishes.
- Completed Android replies format Markdown immediately. Live assistant text keeps its stable plain renderer only while incomplete, then the same owned row transitions to rich code blocks, lists, emphasis, and links without leaving or reopening the session.
- Android approval cards require an explicit labeled decision. Reading or scrolling a guarded command, navigating away, backgrounding, recomposition, later turn activity, and card dismissal cannot submit or locally resolve it; pending requests remain bound to their owning profile and session until an explicit response or authoritative upstream expiry.
- Android Agent Passport controls are readable and easy to dismiss. Safety and speed choices use full-width accessible targets with plain-language selected-state explanations, while a persistent close action and boundary-aware downward swipe make the sheet reliably dismissible without stealing nested content scrolling.
- Android queued messages stay with their originating chat. Follow-ups now retain their exact connection, profile, session, run, route, attachments, and voice context across concurrent Gateway session switches instead of following whichever session is visible when a run finishes.
- Android model pickers reject duplicate catalog identities before rendering. Repeated provider/model rows from cached or refreshed inventories are merged at the provider boundary, while identical model IDs under different providers remain distinct choices with provider-aware reasoning capabilities.
- Android session pins and archives survive app restarts. The session drawer now reads and updates the owning Hermes profile's durable session metadata, rolls failed changes back, and makes unpinned stars clearly distinct in light theme.
[1.6.1] - 2026-08-08
Fixed
- The Dashboard plugin hands hosted Hermes connections to Android reliably. Mobile setup exposes the canonical Dashboard address and keeps dialog focus handling contained, so system-browser authentication can return to the correct connection without disrupting the Dashboard.
[Android 1.7.0] - 2026-08-06
Added
- Android exposes provider-aware reasoning controls. The effort drawer consumes exact upstream or optional Relay capability metadata for each provider/model identity, while unmodified or older Hermes installations retain a fail-soft standard fallback including
maxandultra. - Android support information is local, redacted, and reviewable. Fatal crashes and handled failures share a bounded on-device record, Diagnostics can copy or share the exact reviewed text, and nothing is uploaded automatically.
Fixed
- Android chat chrome follows its active interaction state. Opening the session drawer dismisses the composer keyboard, refreshed sessions keep their newest row visible, and floating pets wait for measured chat terrain, sit flush on supported rails, and treat the complete scroll-to-bottom control as forbidden space.
- Android pets and optional model discovery initialize quietly. Floating companions wait for a measured overlay before taking their home position, and background API model-inventory failures retain actionable local diagnostics without interrupting chat with a generic notice.
- Android chat and Voice stay precisely bottom-pinned through replies, restores, and layout changes. The active tail keeps its stable live renderer until another row takes ownership, restored sessions follow late composer and message measurement without overriding a reader, and bottom-owned transcripts settle to the exact list boundary after replies and keyboard animations instead of leaving a small hidden remainder.
- Android Focus voice controls remain responsive. The modal click-through guard now sits behind the voice UI instead of consuming pointer events from the mic, close, expansion, and panel controls.
- Android diagnostics explain what failed and what to try next. Relay, route, WebSocket, and API checks distinguish the saved route from the redacted request they actually attempted, name the operation, and provide targeted guidance for connection, DNS, timeout, TLS, authentication, rate-limit, and server failures.
- Android chat and Voice keep one render identity through recovery. Checkpoint restore, streamed callbacks, server-ID adoption, and replay now resolve the same owned transcript row before publication, preventing recurring Compose duplicate-key crashes.
- Android crash reports retain actionable release context. Reports identify the Android surface, avoid exposing hosts and credentials, migrate earlier local crash records, and release automation retains exact Play and sideload R8 mappings for retrace.
[1.6.0] - 2026-08-06
Added
- Relay supplies exact provider/model reasoning capabilities when providers expose them. The bounded, profile-aware overlay resolves dynamic catalogs for OpenAI Codex, Copilot, LM Studio, and Ollama Cloud, keeps provider credentials on the host, and leaves unknown or unavailable catalogs on the advisory fallback.
[Android 1.6.1] - 2026-08-03
Fixed
- Voice capture waits for the microphone to be released. Manual recording no longer races barge-in teardown, and AudioRecord startup failures now explain how to free or permit the microphone before retrying.
- Android text selection stays stable as streamed replies finish. Chat resets an active selection when live text becomes rich Markdown, preventing selection-handle drags from retaining removed text nodes.
- Android session history follows the upstream page-size contract. The drawer keeps its 200-session window through bounded 100-row requests, avoiding HTTP 422 errors from current dashboard servers while preserving active-profile isolation.
- Android no longer mistakes optional-surface auth failures for expired Relay pairing. Background session refreshes stay out of the global snackbar, Dashboard and API authorization errors name their owning credential, and Relay-only surfaces use consistent Optional, Ready, Reconnecting, Unavailable, and Needs re-pair states. Foreground recovery retries ordinary Relay backoff immediately while preserving server rate limits, and recovery prioritizes Dashboard or host session management while retained credentials are labeled as stored details instead of active pairing.
- Voice controls no longer collide with new-chat coaching. The clean-view hint yields while Voice owns the composer so it cannot cover the expanding Voice drawer.
[1.5.1] - 2026-08-03
Fixed
- Re-pairing repairs one device instead of accumulating duplicate sessions. An explicit host-approved pair replaces older sessions and refresh credentials for the same device, while the Dashboard and
/relay revoke <token-prefix>remain available for operator cleanup.
[Android 1.6.0] - 2026-08-02
Added
- Hermes can be selected as Android’s default Digital Assistant. The opt-in system role supports background and locked-screen invocation, while the separate experimental “Hey Hermes” listener keeps pre-activation audio on the phone and exposes an ongoing Stop control.
- Installed Hermes plugins can contribute native Android pages. Android renders a bounded declarative schema instead of plugin code, keeps write access off until the user grants it, and supports approval-gated agent-created previews through Relay 1.5.0.
- Pets can stay with you across the Android app without replacing the agent. Petdex and imported companions live in an app-level overlay, can be held and dragged, and optionally roam across live-measured chat and settings surfaces without reserving message space. (#267)
- Petdex browsing and one-tap installation are built into Appearance. Search results use lightweight previews, full atlases download only after Install, creator attribution remains visible, and installed pets stay available offline. (#267)
- Android can be used in Russian. Both product flavors include an AI-assisted Russian catalog, language picker support, localized plurals, and refreshed translations for the 1.6 feature set.
Changed
- Assistant and floating Voice surfaces use compact, expandable controls. Opening full Voice continues the same turn and microphone owner instead of restarting the session.
- Voice interruption covers generation and playback. Barge-in follows upstream RMS calibration and timing, exact stop phrases can end an active voice chat, and interrupted spoken context remains private to the next Standard turn.
- Profile identity, the Sphere, and pets are separate appearance choices. Agent avatars identify messages, background visualization controls ambient art, and Floating pet controls the companion independently. (#267)
- The Agent Passport exposes more profile state and safer controls. Profile configuration, skills, routing, reasoning, and scoped API access remain visibly distinct from the active session identity.
Fixed
- Voice output recovers when a streaming renderer produces no audio. Android falls back to basic synthesis after a bounded first-audio timeout, and long Standard Voice uploads no longer retain duplicate encoded audio buffers.
- Relay route failover avoids competing reconnect loops. Route changes settle through one generation-aware reconnect owner instead of rapidly switching between LAN and remote candidates.
- Live chat rows keep stable UI identity while upstream state reconciles. Streamed messages and process rows no longer collide or restart merely because a server identity arrives later.
- Floating pets recover from invalid or scrolling terrain. Roaming uses measured bubble edges, avoids the jump-to-latest control and text overlap, resumes after drag or scrolling, and preserves locomotion, held, drop, and fallback animation states.
- Hermes appears and activates in OEM Android assistant pickers. Required Assist, Voice, recognition-service, and single-microphone lifecycle metadata now agree.
- Experimental wake detection handles completed sherpa results and empty speech cleanly. Tests use the real local microphone/model path, and no-speech activation returns to ready state instead of surfacing a fatal server error.
[Android 1.5.3] - 2026-07-31
Fixed
- Voice transcripts retain stable rows after chat-history reconciliation. Focus mode uses the same stable Compose identity as the main conversation, preventing duplicate-key crashes when live rows adopt persisted server IDs.
[1.5.0] - 2026-08-02
Added
- Realtime Agent sessions can speak only settled answers. Clients may enable an optional per-session
final_answer_onlypolicy that suppresses routine acknowledgements, progress narration, and intermediate commentary while preserving spoken approvals, confirmation questions, blocking failures, and the final Hermes answer. - Agents can draft native Android plugin pages through Relay. New tools store bounded declarative JSON pages under the authenticated Relay plugin namespace, while Android retains control of enablement, publication, write grants, and persistent removal. Generated pages cannot include executable code, arbitrary network calls, Android intents, or backend action requests.
[Android 1.5.2] - 2026-07-28
Fixed
- Dashboard sign-in completes across supported providers and network routes. Self-hosted OIDC stays on the dashboard cookie flow, while Nous Portal opens in the system browser and completes standards-compatible PKCE through HTTPS, private-LAN, or Tailscale dashboard routes.
- Replayed chat updates no longer destabilize the conversation list. Duplicate upstream message identifiers are coalesced before Compose renders them.
[Android 1.5.1] - 2026-07-26
Added
- Voice supports focused and conversational layouts. Focus keeps spoken turns, Markdown, tools, media, and actions in a compact voice surface, while Conversation opens the full Chat renderer without leaving the active voice session.
- Voice can speak only settled answers. A global Voice setting keeps tool progress, service updates, and intermediate commentary visual while supported voice paths wait to speak the final Hermes answer.
Changed
- Chat answers are easier to read in every theme. Primary assistant text now uses the theme's full-contrast foreground, and chat prose uses a 15sp size with 21sp line height.
- Google Play builds target Android 16. The app now targets API level 36 while retaining its existing minimum-device support.
Fixed
- Completed streamed answers render their formatting without losing the reading position. Markdown headings, lists, emphasis, and code blocks replace the live text renderer only after completion, then the measured trailing edge remains anchored at the bottom.
- Standard Voice speaks completed assistant replies again. Session and message fences no longer suppress a valid final answer during the handoff from generation to narration.
- Realtime background work no longer blocks the active voice controls. A promoted task releases the foreground spinner and microphone while its progress, tools, cancellation, and final result remain available in the owning chat.
[Server 1.4.3] - 2026-07-22
Added
- Relay diagnostics describe upstream Gateway compatibility. Doctor and
/relay/inforeport optional Gateway health, configuration-route, and capability signals so clients can distinguish an older upstream install from a Relay failure.
Fixed
- Relay trust boundaries are enforced across privileged interfaces. Pairing policy is host-authorized, Android bridge and terminal dispatch require active grants, ordinary sessions can only reduce their own policy, remote profile config is restricted to a public schema, and voice callers cannot redirect host provider credentials.
- Plugin bootstrap work no longer blocks the Gateway event loop. Database initialization and compatibility-state inspection run off the async request path while preserving older upstream bootstrap behavior.
- Starting Relay no longer terminates a running Hermes gateway on Windows. Profile discovery now checks gateway PIDs through non-signalling process APIs, including during periodic rescans.
[Android 1.5.0] - 2026-07-25
Added
- Voice settings are organized around Standard and Realtime paths. Provider, model, and voice choices use a cleaner card layout with upstream-aware discovery, useful descriptions, inline previews, waveform feedback, loading skeletons, and an expandable scrolling voice browser.
- Standard Hermes speech streams while replies are generated. Android plays completed speech segments as they arrive, interrupts prior playback before starting another preview or reply, and stops audio when leaving voice mode.
- Manage and diagnostics expose more upstream Gateway controls. Android consumes health hints, follows canonical redirects, compresses larger RPC payloads, scopes diagnostics by profile, and surfaces compatibility information without requiring Relay-only behavior.
- Chat shows richer upstream state and media. One-turn model selection, approval policies, advisor progress, queued-recovery and project labels, collapsible attachments, persisted images, interim Gateway events, and a theme-aware image-generation animation make active work easier to follow.
- The Agent Passport makes the active agent controllable. The chat drawer now combines live connection and session context with profile switching, personality, model, reasoning, approval, and speed controls in one focused surface.
- Android onboarding finishes with a permission setup step. After connecting, users can enable background chat alerts with one deliberate Android prompt, review optional feature permissions individually, or continue immediately without granting phone access.
- Image generation stays visible when upstream tool progress is hidden. A paired Relay can expose read-only image-tool activity from Hermes session state so Android shows and completes its existing generation animation during Standard Gateway turns; native Gateway lifecycle events remain authoritative and Relay remains optional.
- Background work stays actionable. User-started turns remain protected until every active session settles, while privacy-safe notifications reopen the correct conversation for approvals, questions, elevated permissions, and secure responses.
Fixed
- Voice settings and active-turn correction remain usable across supported languages. New voice controls are localized and correction copy accurately describes the turn being replaced.
- Chat reconnects preserve the running Gateway turn without duplicating it. Android reactivates the original live session after a socket loss, avoids resubmitting a prompt when its acknowledgement was lost, and de-duplicates session rows before they reach the drawer.
- Relay pairing preserves Tailscale and other fallback routes. Adding Relay to an existing Standard connection now keeps every signed QR route, restores older per-device endpoints hidden by the connection upgrade, and gives remote Dashboard routes their API fallback. When a host-scoped Dashboard sign-in is still required, Chat shows the route-specific sign-in action instead of loading indefinitely.
- Remote routes move every Hermes surface together. Android uses
GET /healthinstead of misclassifying the API server's405 Method Not Allowedresponse toHEAD, and the selected Tailscale route now carries Dashboard/Gateway, sessions, Manage, and Standard Voice with API and Relay instead of leaving them pinned to the saved LAN host. Manage also distinguishes host-side Nous provider authentication from Dashboard sign-in. - Hosted Manage and direct-chat compatibility stay bounded and secure. OAuth state remains tied to the selected dashboard, inline image memory is capped, and session reset and queued-recovery boundaries follow upstream contracts.
- Dashboard sign-in is secure and route-aware. Browser-based authorization is scoped and serialized to the selected host, while cold start no longer activates a temporary localhost API fallback or reports a missing key before stored connection state is ready.
- Background and promoted voice work retain their owning chat rows. Completing an initial spoken handoff no longer removes an otherwise empty assistant bubble that still owns a running task, and concurrent turns remain reachable without requiring an always-on idle connection.
- Self-hosted rendering is safer. Android accepts deliberately installed user certificate authorities without bypassing chain, hostname, or Relay-pin verification, and malformed syntax-highlighting ranges no longer crash Markdown rendering.
- Developer Options reflect current product behavior. The obsolete Relay feature toggle is removed, version-tap unlock and explicit relock persist correctly, and backup, import, reset, and completion messages now report their actual results.
[1.4.9] - 2026-07-19
Changed
- Hermes connections now use the Dashboard/Gateway as their standard surface. Chat, sessions, Manage, and voice share one upstream sign-in; the API server is an optional automatic fallback or headless compatibility path, while Relay remains optional for power features.
- Connection management and onboarding now explain each path clearly. Nearby and remote dashboard setup, Tailscale and custom ports, Relay pairing, startup preference, route details, and security posture are presented in dedicated flows.
Fixed
- Server default consistently displays Hermes' pinned active profile. Chat, session drawers, agent details, settings, voice, diagnostics, and profile inspection now use the active profile identity while preserving server-default routing semantics.
- Discovered connections show useful host identity. Successful local dashboard probes resolve and retain a hostname without overwriting a user-supplied connection label.
[1.4.8] - 2026-07-18
Fixed
- The Google Play privacy-policy URL is permanently available. The canonical policy now lives on hermes-relay.dev, the historical GitHub Pages URL serves the complete policy for compatibility, and Android release automation blocks publication if either public page is unavailable.
- Android opens the hosted privacy policy directly. The About screen no longer sends users to a repository source file.
[1.4.7] - 2026-07-18
Added
- Android adds German, Brazilian Portuguese, and Japanese. Complete AI-assisted catalogs cover both product flavors, with language-picker integration and freshness validation against the canonical English resources.
Fixed
- Long streamed replies grow smoothly and remain at the latest text. Android frame-paces bursty token delivery, expands the active bubble within clipped bounds, preserves bottom-following through completion, and avoids replacing the visible live transcript while readers who intentionally scroll up remain undisturbed.
[Android 1.4.6] - 2026-07-15
Added
- Profile display order and visibility are customizable per connection. The profile manager can reorder every profile, including Server default, selectively hide inactive profiles, restore hidden active profiles, and reset the saved presentation without changing server configuration.
- Agent icons can come from the phone or paired host. The profile manager offers the Android document picker and can import conventional host files such as
avatar.pngorprofile.jpg, storing a per-connection/profile copy on the phone.
Fixed
- Profile image import reports host compatibility accurately. Android now distinguishes an older Relay without the optional avatar endpoint from a profile that genuinely has no conventional image, and presents the system file picker as a clear fallback.
- Server-default chats use one profile session scope. Android resolves the Server default row through Hermes' sticky active profile before Gateway create/resume and dashboard session operations, so the drawer, transcript, writes, and agent no longer split across different profile databases when the dashboard was launched under another profile.
[Plugin 1.4.2] - 2026-07-15
Added
- Profile avatars are available to paired clients. Relay discovers conventional direct-child profile images such as
avatar.pngandprofile.jpg, validates their type, size, and profile boundary, and serves them through an authenticated profile route.
Fixed
- Relay follows Hermes' sticky active profile. The advertised Server default identity, model, SOUL, profile metadata, and avatar now come from the profile selected by Hermes'
active_profilemarker instead of always describing the root profile.
[1.4.5] - 2026-07-15
Fixed
- Running Android chats survive session switching. On the upstream Gateway path, opening another chat, profile, draft, or Thread now detaches the visible stream without interrupting Hermes. Each running session keeps its own durable UI checkpoint, reconnects the shared event socket across route loss, and reattaches through
session.activate/session.resumewhen selected again. SSE fallback remains intentionally single-stream and cancels on navigation. - Expired Gateway prompts no longer remain actionable. Android collapses matching secret and sudo cards when Hermes emits their expiry events, recognizes late expired responses, and is ready for an upstream session-scoped approval-expiry contract without guessing the server timeout.
- Provider wait notices stay transient. Canonical Hermes provider-wait, reconnect, and continuation notices now use Chat's live status line instead of accumulating in the assistant reasoning transcript.
[0.4.0-alpha.2] - 2026-07-13
Added
- Desktop chat can use Relay typed streaming over WSS. The opt-in
--relay-chatmode sendschat.send, renders typedstream.eventv1 assistant/tool/artifact/memory/skill/error lifecycles, de-duplicates reconnect events, and preserves the existing gateway chat path as the default. - Pending computer-use grants are manageable from the CLI.
hermes-relay grantslists and interactively approves or rejects local grant-bridge requests, with explicitapprove,reject, and JSON forms for scripts. - Desktop use has a durable CLI control plane.
hermes-relay computer-usepersists enablement, reports daemon and grant state, and cancels active task-scoped grants through the local daemon bridge.
Changed
- The optional Windows systray is a native context menu for the CLI. The WebView dashboard, embedded terminals, overlays, chat, sessions, plugins, voice, and settings windows were removed. The sub-megabyte tray now invokes the single installed CLI for TUI, pairing, daemon control, grants, audit, and logs.
- Systray daemon controls are state- and privilege-aware. The menu cross-checks PID liveness, identifies User versus Administrator daemons, disables invalid lifecycle actions, shows pending-grant counts and version metadata, toggles sign-in startup, and requests UAC only for an explicit elevated daemon start or restart.
- Systray desktop-use controls preserve safety across restart and elevation. The menu enables or disables the persistent capability, displays active grant mode and expiry, raises a native pending-approval alert, supports immediate cancellation, and warns while Administrator input authority is active.
- CLI and tray releases use one synchronized version contract. A single npm lifecycle keeps package, compiled CLI, Cargo, and installer metadata aligned; local verification and tag CI reject drift, off-main release tags, and untested CLI changes before publishing.
Fixed
- Compiled CLI diagnostics report the physical executable.
hermes-relay doctorno longer mistakes Bun's virtual embedded path for the installed binary, so PATH and install-directory checks describe the executable that actually launched.
1.4.4 - 2026-07-12
Added
- Android adds AI-assisted Spanish. A repeatable translation harness and freshness checks keep catalogs structurally complete while tracking fluent review separately.
- Diagnostics exposes the Relay contract. A manual refresh reports the installed plugin version, protocol version, capability count, profile enablement state, and last-check time; shared issue reports include sanitized Android and device metadata.
- What’s New links to complete release history. The polished modal now provides direct access to every bundled version, with large-text screenshot coverage.
Fixed
- Profile operations stay inside the selected Hermes profile. Session list, history, rename, delete, and in-flight recovery no longer fall through to the default database after a scoped failure; optimistic writes roll back and repeated recovery failures stop cleanly.
1.4.3 - 2026-07-11
Added
- Language switching is available inside the app. Settings → Appearance now offers System default, English, and Simplified Chinese, stays synchronized with Android's per-app language setting, and persists the choice on Android 12 and lower.
Fixed
- Release builds reject unsupported collection APIs. CI now scans Kotlin sources and final minified APK bytecode for Java 21 list endpoint calls that can crash on Android versions before API 35.
1.4.2 - 2026-07-11
Added
- Android now supports Simplified Chinese. Chat, Manage, Voice, connection setup, settings, diagnostics, notifications, accessibility labels, and both product flavors follow the device language, with Android per-app language discovery on supported versions.
- Localization is contributor-ready. CI enforces resource, plural, and format-argument parity; translated README and VitePress entry points establish a repeatable path for adding languages without duplicating fast-moving technical references.
Fixed
- Connection scan and queued-message counts use proper plurals. Count formatting no longer depends on English-only suffix arguments and cannot fail when a locale needs a different plural structure.
1.4.1 - 2026-07-11
Added
- Background work is visible in Standard Chat. A live process strip opens a mobile process sheet with running or recent state, output, elapsed time, Stop, and Dismiss controls. It remains compatible with older Hermes servers that do not expose process details.
- Background work has a clearer Chat home. Realtime work appears as a titled task card with working, waiting, delivery, and completion states, queued work, and an expandable tool timeline.
- Multi-image messages open as galleries. Adjacent images render in a compact grid and open at the selected image in a swipeable viewer while preserving sensitive-media reveal and original-file actions.
- Voice gains commands and presets. Spoken commands can stop speech, cancel background work, pause or resume listening, repeat a result, or start Standard voice chat. Hands-free, Low latency, Careful tools, and Quiet presets tune existing interaction settings.
Changed
- Streaming Chat content stays steadier and more readable. Settled prose and headings adopt final Markdown styling during generation, wide tables scroll with readable columns, the thinking indicator respects system motion and TalkBack settings, and the jump-to-bottom control counts unread messages.
- Offline Demo mode no longer starts Voice. The mic action now explains locally that a Hermes connection is required.
Fixed
- An in-flight Chat turn survives reopening the app. Session-backed replies restore partial text, live reasoning, lifecycle status, tool/subagent cards, background-task state, and unanswered approval or clarification cards. Current Hermes gateways reattach to the same running turn; older or finished sessions reconcile from history without duplicating the prompt or losing the final answer.
- Realtime Agent delivery is protected. Hermes results use exact provider speech where supported, delivery validation, generation-safe confirmation, and a single relay-TTS fallback if the provider closes or rejects delivery. Voice commands no longer leave synthetic cancellation turns or mute a later background answer.
- Standard Chat receives background-process completions automatically. When Hermes completes detached work and starts a follow-up turn on the originating Gateway session, Android shows the unsolicited assistant stream in the open conversation and reconciles history after a cold reconnect. The synthetic process prompt is rendered as a compact process notice rather than a user-authored message.
1.4.0 - 2026-07-09
Added
- Android model pickers can refresh the server catalog. Chat's model sheet and Manage's main/profile model dialogs now expose upstream's explicit Refresh Models action, so dynamic/custom provider model lists can be reloaded on demand without making every picker open probe providers.
- Server-backed session cleanup plumbing. The dashboard client now supports single-session export, the upstream
/api/sessions/pruneroute with a mandatory dry-run preview before destructive apply, plus soft archive/restore helpers and anarchivedsession-list filter for the Manage surface. - Notification triggers MVP. Settings → Notifications now has explicit opt-in proactive rules for the Notification companion: match by app package plus optional title/text filters, post a safe local "Ask Hermes?" prompt, show the latest trigger activity, and pause everything instantly with a kill switch.
- Android bridge: multi-device targeting. The relay can keep multiple Android bridge clients connected at once, route commands by
deviceselector (phone,pixel,fold,boox,note,notemax,tablet, or device ID), expose/bridge/devicesand/bridge/select-active, and advertise an optionaldeviceargument on theandroid_*tool schemas. - Voice: a second long request gets queued, not refused. Ask for another long task while one is already running in the background and it's now queued (up to three) and starts automatically when the current one finishes — with a short spoken transition. The task card shows "+N queued", and cancelling the current task clears the queue.
- Voice: background answers start speaking sooner and can never be silently lost. The spoken summary now streams as it's generated (it used to be held until fully complete — a noticeable dead gap, then the whole answer at once). Delivery is verified two ways: the summary must actually reflect the answer's content (not just avoid known filler phrases), and if no spoken delivery lands within 30 seconds the answer is posted as text instead of vanishing.
- Voice: tap the finished-task card to hear the answer again. After a background task's card settles to "finished," tapping it replays the delivered answer. The card also now shows in the compact voice view (it previously existed only in the full-screen layout), a "Drafting the answer…" status appears as the reply is being composed, and leaving voice mode with a task still running leaves a note in chat so the work stays visible.
- Voice: quick questions answered while a background task runs. Realtime voice used to refuse any second request while a long task ran in the background — even a two-second lookup. A quick second ask is now answered inline on a side session (within the same few-second window that decides backgrounding); anything that turns out to be long still gets the "a task is already running" answer, and the running task is never disturbed.
- Voice: the background-task card no longer vanishes mid-answer. The card used to disappear the instant the spoken answer started (exactly when the waveform returned), reading as the task being lost. It now settles to a "Background task finished." state, lingers for a few seconds while the answer plays, then dismisses itself — and its ✕ during that settled state just dismisses the card instead of sending a cancel.
- Voice: the "Thinking" pill no longer spins forever. The server streams its drafting text as an internal pseudo-tool that never reports completion, and the app rendered it as a live tool pill — which then ran indefinitely in both chat and the voice overlay. Internal tool events no longer become pills (their text still feeds the thinking trace).
- Voice: background-task answers can't be lost to a stray cancel. Tapping cancel/stop after a background task had already finished used to mark the finished run "cancelled" — losing the answer that was about to be spoken. Cancel now only cancels a run that's actually still running; stopping the current speech works as before.
- Voice: no more spoken run IDs or phantom queue state. The realtime voice model no longer reads 32-character run IDs aloud after starting a background task (identifiers stay out of everything it's asked to speak), no longer claims a request was queued unless the relay accepted it, and a completed task's answer is spoken directly — deferral filler like "one moment while I look that up" in place of a finished result now triggers the fallback that speaks the real answer.
- Voice: finished-task answers keep the realtime voice. A completed background task's answer is now spoken by the same realtime voice you've been talking to — read word for word from the authoritative Hermes answer — instead of switching to the standard TTS voice mid-conversation. The answer always lands: if the realtime model goes off-script or the provider connection drops, standard TTS speaks it, and if you start talking mid-delivery it's posted as text instead of interrupting you. The "When the answer is ready" setting keeps its four modes (Exact / Summary / Notify / Show), now explained behind an info icon in Voice Settings.
- Voice: realtime models refreshed. OpenAI realtime now defaults to
gpt-realtime-2.1(with the cheapergpt-realtime-2.1-miniselectable), the versionedgrok-voice-think-fast-1.0pin is available alongside xAI'sgrok-voice-latestalias, and session logs record which model the provider actually served — so provider-side alias moves no longer happen invisibly. - Voice: session logs clean up after themselves. Realtime voice session logs are swept after 14 days by default (
realtime_voice.run_retention_days, 0 disables), and the per-response TTS audio capture is now opt-in debug tooling (debug_audio_tap) instead of an always-on multi-MB tap. - Voice: one-command delivery health report.
python -m plugin.relay.realtime_agent.reportsummarizes recent voice deliveries — how many were spoken by the realtime voice vs fell back to TTS or text, and why — for quick health checks after live testing.
Changed
- Bootstrap compatibility layer slimmed to true gaps. The optional compatibility hook no longer injects session CRUD/messages or the legacy skills list — current Hermes serves those natively; it now covers only surfaces with no native replacement yet (session search, memory, legacy skill detail/toggle, config, available-models, and the slash-command middleware). Older pre-session-API Hermes builds degrade to the standard completions/runs chat paths.
- Dependency floor: aiohttp ≥ 3.14.1. Raised from 3.9 across plugin requirements and package metadata to the patched line covering the 2026 aiohttp security advisories.
Fixed
- Realtime voice recovers after background route loss. A recorded turn now waits for a relay-confirmed resumed socket, retains unacknowledged follow-up PCM for replay, and reports transport rejection instead of sitting on a dead persistent connection. Resume handshakes are coalesced, and the relay requires a valid resume claim before replacing the active phone socket, so a slower stale connection cannot detach background-result delivery. Long-lived sessions start their bounded retry window when the route actually drops instead of at voice-mode entry, and a bare socket open cannot reset it. Late callbacks from a retired session are ignored. Exiting voice mode clears its detached reconnect and confirmation state before another session opens; rejected or unacknowledged cancels no longer leave an undismissable background-task pill. Provider transcription no longer impersonates active microphone capture, Stop settles the local turn even when the route is gone, and provisional
Listening.../Still working...rows cannot remain stuck in chat. - xAI exact background answers bypass model deferral. Non-structured Exact deliveries now use xAI's provider-native forced speech event, preserving the selected realtime voice and normal assistant history while speaking the authoritative Hermes answer without asking the model to follow a read-verbatim prompt. Structured results and summary modes still use natural model summarization, and the validator plus standard-TTS fallback remain as safety nets.
- Background voice handoffs no longer repeat themselves. If the realtime provider already spoke an acknowledgement before calling Hermes, promotion keeps that first line and suppresses the redundant "running in the background" follow-up; silent tool calls still receive the configured spoken handoff. Provider protocols that report both response creation and output-item creation now also produce one client
response.startedevent instead of two. - Realtime voice model and voice picks now apply to the next session. Voice Settings persists the selected Realtime Agent model and voice per connection/profile and sends both when opening a session, so choosing a pinned model immediately controls the next session instead of requiring Save realtime agent to rewrite the relay config. The active voice UI reflects the override, changing it retires any prewarmed session, and the choice survives an app restart.
- Fresh realtime sessions emit one ready event. Android's required
session.startacknowledgement no longer causes the relay to send a secondvoice.session.ready, avoiding duplicate event IDs and duplicate session-ready telemetry on every new voice conversation. - Relay media can no longer serve credential files.
/media/by-pathnow always blocks paths that resolve into credential or system locations (~/.hermes/.env,auth.json,config.yaml, OAuth/MCP token stores,pairing/,~/.ssh, and similar) even in the default permissive mode — mirroring upstream Hermes' media-delivery hardening — so a prompt-injectedMEDIA:marker can't deliver live secrets to a paired phone. Symlinks are resolved before the check, and the relay's own QR-signing secret and session-token store are covered too. - Long agent turns no longer die or duplicate at the transport. Gateway chat (Android and the desktop CLI) now gives
prompt.submitup to 30 minutes to acknowledge — matching upstream desktop and the server's own turn ceiling — instead of short generic RPC timeouts that could falsely fall back to SSE (duplicating the turn on Android) or kill a legitimately long deep-reasoning turn. Turn liveness is governed by idle-progress watchdogs (no events at all for a stretch), never a hard cap while output is still streaming. - Manage → Models keeps providers that still need keys. Newer Hermes hides unconfigured providers from the model catalog unless a management UI opts in; Android Manage now opts in and keeps rendering greyed provider rows with their key-setup guidance on both old and new servers. In-chat model picking is unchanged (configured providers only).
- Phone-local context actually reaches the server on fallback chat paths. The sessions/runs streaming payloads carried voice-intent traces, card dispatches, and attachments in fields the server never reads — silently dropping them. That context now rides channels the server actually consumes (a per-turn context digest, real history fields where they exist, inline images on the completions path), and any attachment with no supported channel is reported instead of silently discarded.
- Relay plugin works under the native
hermes plugins installpath. The plugin's runtime imports assumed the repo's editable layout, so upstream's native installer (which loads plugins under its own package namespace) brokehermes relay startandhermes pairwithModuleNotFoundError: No module named 'plugin'. All runtime imports are now package-relative, the dashboard module boots correctly when the upstream web server loads it standalone, andhermes relay doctornow exercises the real import chain so this class of breakage can't pass doctor again. (#165) - Installer handles modern venv layouts.
install.shnow autodetects the classic venv, uv-managed.venv, and containerized layouts — and everything it generates (the systemd unit and all four command shims) points at the interpreter it actually detected instead of a hardcoded classic path. On immutable container images it steers to the native install path with a clear message instead of dying mid-run. (#165) - Doctor catches dashboard URLs pointed at the wrong Hermes surface.
hermes relay doctornow distinguishes the dashboard/Manage surface from an API-server/headless backend URL and tells operators to usehermes dashboardwhen a configured dashboard URL is actually pointing athermes serve/ the API server. - Doctor and installer catch stale duplicate plugin copies. The gateway plugin loader picks a discovered plugin by manifest name, so a second directory declaring
name: hermes-relay(a leftover backup copy or a stray extra install) could win and make the gateway load stale code — silently ignoring every later deploy.hermes relay doctornow warns when more than one directory under the plugins dir declares the same plugin name, andinstall.shremoves any such duplicate so only the canonical plugin symlink remains. - Crash-safety on Android 14 and earlier. Built against SDK 35, Kotlin's
removeFirst()/removeLast()resolve to the new JavaListmethods that don't exist below Android 15, crashing older devices. All such calls in the app are nowremoveAt(...), and Tink (pulled in by encrypted storage) is pinned ahead of the transitive version whoseHybridConfigtripped the same Google Play pre-launch check. - No crash when a relay address is malformed. A corrupt or hand-edited pairing address with an invalid host could crash the app the moment it opened the relay connection (the connection is built on a background thread, so the error escaped uncaught). A bad relay address is now handled as a normal connection failure — shown as disconnected with a "re-pair to refresh" note — instead of crashing. The same guard now also covers the relay's media, session, and voice HTTP calls. (relay half of #131)
- Voice: cleaner error recovery. A failed or timed-out voice turn no longer shows the same error twice (the top overlay banner and a duplicate bottom banner) and can now be dismissed, not just retried — so a stuck error state can't block the screen.
- Voice: fallback-spoken answers no longer play into a frozen overlay. When an answer is delivered by the standard TTS fallback (or replayed from the finished-task card), the voice screen now shows the waveform and the answer text while it speaks — previously it sat on "Thinking" with no visuals even though audio was playing.
- Voice: a quiet realtime session no longer dies with a raw provider error. xAI ends a realtime conversation after 900 seconds of inactivity, and no keepalive traffic resets that timer — so a voice session left open through a long background task (or simply left open) died with a raw provider error. That provider timeout is now treated as routine expiry: the session ends cleanly with no error banner, and your next voice turn transparently opens a fresh provider conversation that picks up from the same durable Hermes chat session.
- No crash when a malformed server address reaches a chat send. The three streaming chat paths built their HTTP request before any error handling, so a corrupt or hand-edited API URL could throw instead of failing the turn gracefully. They now surface "Invalid server address — edit the connection's API URL or re-pair" through the normal in-chat error channel (closes the remaining #131 crash-class gap).
- Demo mode: typing a message now gets an honest reply. Sending a message in the offline demo used to do nothing (the composer silently ignored it, reading as broken). The demo now echoes your message and answers with a short notice explaining it's an offline sample, pointing at the Connect action to chat for real.
- Voice: realtime conversations reliably reach your chat history. Turns the realtime voice model answers directly (without calling Hermes) are folded into the chat session on your next message — but on the default gateway connection that hand-off could be deferred indefinitely, so the agent never learned what was said in voice. The turn that carries them now routes so the sync actually lands. Synced voice turns also render cleanly when a chat reloads: a quiet "Realtime Agent" chip instead of a raw provenance footnote, and no more duplicated voice exchange after the sync.
[1.3.0] - 2026-07-06
Added
- Voice settings: edit your server's voice engine. Voice settings now has a Server voice config section that reads and writes the host's text-to-speech and speech-to-text settings — provider, voice, model, language, and per-provider options — over the dashboard, the same config the official desktop app edits. It includes an ElevenLabs voice picker that lists the voices available on your server's ElevenLabs key (and tells you when no key is set). Works on the no-plugin (Standard) path; sign in to Manage to use it.
- Desktop CLI:
hermes-relay audit. Shows what the remote agent has actually run on this machine through the desktop tools — tool, status, and a short detail per call — read from a local log, no network or auth. Answers "what did the agent just do?" at a glance. - Desktop CLI:
hermes-relay relay. Inspect the relay server itself:relay info(version, uptime, sessions — on the relay host),relay security(runtime auth toggles),relay context(audit the system-prompt context the relay injects into the agent, which works from a remote machine with your session), andrelay queue(list — or--clear/--cancel <id>— the messages your agent queued for an offline phone; on the relay host). - Desktop CLI: background daemon.
hermes-relay daemon startruns the headless tool router in the background (no console window, survives closing the terminal), withdaemon stopanddaemon statusto manage it.daemon statusreports state, uptime, relay, and advertised-tool count; baredaemonstill runs in the foreground. Logs go to~/.hermes/daemon.log. - Desktop CLI: per-command help. Every subcommand now answers
--help, anddevices/sessions/plugins/voice/relayprint their own usage (sub-commands, flags, examples) instead of a terse "unknown sub-verb". - Desktop CLI: startup banner. A slim "Hermes Relay" wordmark shows atop
--help, the first-run welcome, and the chat REPL — andhermes-relay logoprints it on demand. Suppressed for piped/--json/--no-coloroutput. - Animated "thinking" indicator. While a reply streams, the in-bubble working indicator can now be a small dot-matrix animation instead of the three dots. Pick a motion (Wave, Pulse, Bounce, Sparkle) and a color (match-text or a brand accent) in Chat settings, with a live preview. It follows light/dark and your app theme, and goes static when animations are turned off.
- Proactive messages from the agent to your phone. Your Hermes agent can reach out to the paired phone on its own — via
send_message target=phoneor a crondeliver=phone. Messages surface as a system notification, collect in a dedicated Hermes inbox, and can be injected into the active chat to continue the conversation (selected per message). Off by default and gated on pairing: nothing is pushed unless you enable it on the server (PHONE_ENABLED) and opt in on the phone ("Let Hermes message me"). Delivered over the existing relay connection through the upstream platform-plugin API (no fork). - Reply to your agent's messages (two-way). A proactive message is now a conversation, not a one-way ping: reply straight from the notification (inline Reply) or from the Hermes inbox, and your answer goes back to the agent and continues the same thread. The phone behaves like any other Hermes messaging platform — the reply arrives as an inbound message the agent processes and answers. Rides the same paired relay connection; no extra setup beyond the proactive opt-in above. If your phone is offline when the agent answers, the message is queued and delivered when you reconnect — not lost.
- Pick your font. A Font picker in Appearance sets the app-wide typeface — Inter (the new default), Nunito, or your system font — each previewed in its own face and applied instantly across the app, no restart. Code and timestamps stay monospaced. (Bundled faces are SIL OFL.)
- Quick Controls in Settings. A Quick Controls card at the top of Settings groups the switches you flip most often — Persistent connection and Turn-complete alerts — so they're one tap from the Settings root instead of buried in a sub-screen.
- Connections: a cleaner list and a tabbed detail. Settings → Connections is now a scannable list — each server shows an Active badge and an at-a-glance capability summary (API · Dashboard · Voice · Relay) — and tapping a server opens a focused detail screen with Overview, Routes, Advanced, and Security tabs. Rename / re-pair / revoke / remove moved into the detail's ⋮ menu, and relay sessions (review and revoke the phones paired with that server) get a clear home under Security.
- Keep connected through deep sleep (sideload). When Persistent connection is on, Settings offers a one-tap "Allow unrestricted battery" prompt so the connection survives Android's deep-sleep (Doze) — without it, the OS pauses background networking after the screen's been off a while even with a foreground service. (Sideload only; Google Play restricts this permission.)
Changed
- Reporting a diagnostic now files the right kind of issue. The Report button on a diagnostics entry used to turn routine log lines into "[Bug]" GitHub issues with an empty template. Now informational entries first ask "what were you expecting to happen?" and file as a "[Diagnostic]" question, error entries keep the direct bug flow, and every report carries the connection mode you were actually on instead of a placeholder line. (#155, #154, #146)
- Simpler release downloads. Each Android release on GitHub now attaches just two files — the tap-to-install sideload APK and the Play Store upload bundle — plus checksums, with the release notes leading with the one file most people want. The extra "parity/testing" artifacts are gone from the release page (still reproducible from the tag via CI). (#144)
- Clearer, snappier voice capture and playback. Voice now engages the device's echo-cancellation and noise-suppression while recording (matching the desktop's microphone setup), and requests audio focus before the first reply so the opening words aren't clipped on a cold start. Listening timing also matches the official desktop: auto-stop ~1.25s after you stop speaking (was 3s), give up after 12s with no speech, and cap a turn at 60s.
- Refreshed chat look. Message bubbles are wider and denser, each assistant turn shows a small Hermes avatar to its left (once per group), and code blocks are richer — a language label, a copy button, and a clearer inset so fenced code and inline
codeno longer blend into the bubble. - Desktop CLI: visual + ergonomics refresh. A single color theme across the CLI, aligned tables for
devices/sessions, status dots for on/off states, and progress spinners for slow operations (the multi-endpoint pairing probe and the gateway connect) so nothing looks hung. Errors now suggest the fix (e.g. re-pair on auth failure). - Desktop CLI: smoother pairing. The multi-endpoint probe shows per-endpoint progress and latency; a near-expiry session warns before it fails and prints the exact re-pair command; and a bare
ws://host(no port) defaults to:8767. - Desktop CLI: voice + consent transparency.
voicenow surfaces enhanced-voice capabilities (Gemini tone tags / persona, xAI speech tags); the desktop-tool consent prompt is clear that it persists per relay and points athermes-relay audit; and computer-use's observe → grant → act flow is documented in--help. - Persistent connection (was "keep chat connected"). The background keep-alive and its notification are reframed from a "chat connection" to your overall connection to Hermes — it holds the app's connection open in the background so messages and live features stay responsive, and for relay-paired setups also keeps device control and notification mirroring reachable. The toggle moved out of Chat settings into the new top-level Quick Controls card.
- Chat is the home; simpler top-level navigation. The Chat / Manage / Bridge mode strip is gone — Chat is now full-height, and Manage and Bridge are reached from Settings (Settings → Hermes management / Bridge), each with a back arrow to Chat. Terminal and Settings remain quick icons in the chat top bar.
- Gentler reconnects when your server is unreachable. After the server has been unreachable for a while, the app stops retrying every ~15 seconds and drops to a slower poll — easier on the battery — and still reconnects immediately the moment the network changes or the server comes back.
- Connection status stays out of your way. Connection feedback now sits exactly where it matters and never covers the nav or shifts the screen. Your agent's connection shows in the header subtitle under the agent name — it reads Reconnecting… / Connecting… / Disconnected and crossfades back to the model when it recovers, the same place messaging apps put it. The relay link (bridge / terminal / voice) shows only as a small amber Reconnecting… cue in the bottom status strip, since it doesn't block chat. Returning to the app from the background is now fully silent instead of flashing a misleading "connection changed" for the same connection re-handshaking.
- Realtime voice: quieter progress. The periodic spoken status updates during a long task ("Using cronjob…") are now off by default — the agent speaks at the milestones that matter (task started in background, finished, or failed) and the visual progress chip covers the in-between. A server setting brings the timed narration back if you prefer it.
- Realtime voice: a live background-task chip. The "working on it" chip in voice mode now actually shows what's happening: the current step ("Running command"), how many steps have finished, and a running timer — with a pulse so you can tell it's alive. It also reads the connection honestly ("Reconnecting — your task is still running" during a blip, "Done — delivering the answer…" while the reply queues up), and a ✕ on the chip cancels the task outright.
- Realtime voice: snappier long-task handoffs and first turns. When a clearly long-running tool starts (cron, desktop, browser work), the agent hands the task to the background right away instead of waiting out the full grace period — and the voice session now warms up when you open voice mode, so the first turn skips the connection setup it used to pay.
Removed
- Two voice controls that did nothing. The disabled "Auto-TTS" toggle and the "STT language" picker under "Coming soon" in Voice settings are gone: the official desktop doesn't read every typed message aloud, and speech-to-text language is a server-side setting now editable in the new Server voice config section.
Fixed
- Realtime voice: you can keep talking while a background task runs. Progress updates from a background task were flipping the voice UI back into "Thinking" with a Stop button on every tick, so the mic never came back until the task finished. Progress now feeds only the task chip; the conversation stays open the whole time.
- Realtime voice: leaving voice mode no longer cancels a running task. Exiting (or tapping Stop to interrupt speech) used to kill an in-flight background task and could overwrite its already-delivered answer with "Cancelled." in the chat. Exit now detaches — the task keeps running and the result arrives on your next session or as a notification — and a delivered answer always keeps its text (a Stopped badge marks a genuine cancel). The chip's ✕ remains the one deliberate way to cancel.
- Long answers are no longer lost when the connection drops mid-turn. On slow local models (or skills that delegate long background work), the phone could drop the stream mid-turn — the server finishes and saves the answer, but the chat sat on "Still working…" forever. The app now detects the dropped stream and quietly re-checks the conversation until the finished answer arrives, then completes the turn normally (with the usual done-notification if you've backgrounded the app). Switching chats or sending something new cancels the wait. (#166)
- Onboarding slides fit every screen. Intro slide text could run past the bottom of the screen with no way to scroll on short displays or large font sizes. Slides now scroll when needed and compact their artwork on short viewports, so no setup guidance is unreachable. (#145)
- Docs: fixed stale setup labels and broken links. The setup guide referenced a "Vanilla Hermes" button the app hasn't shown since v1.2.2 (it's labeled "Hermes"), several deep links into the getting-started page were dead, and the README under-counted the available phone tools. (docs site)
- Back button on Manage and Bridge now works. The back arrow on the Manage ("Hermes management") and Bridge screens did nothing — it tried to jump to Chat in a way that silently no-op'd. Back now reliably returns to the screen you opened it from.
- Dropped relay connections from a status-report race. The phone's periodic device-status report could occasionally be sent to the relay before the connection had finished authenticating, which made the relay reject the whole connection and forced a reconnect. The app now holds every message until the connection is authenticated, so the handshake always completes first.
- Fewer needless connection re-checks when switching apps. Returning to the app after a quick glance at another app no longer triggers a full connection re-probe (and the brief "checking…" flash) when the connection was already healthy — it only re-checks after a longer absence or if something actually looks off.
- No more scary "server isn't accepting connections" pop-up on first load. A bare bottom message could flash on cold start while the app was still establishing its first connection (the background session-list load failing before the server was reachable). That state is now shown only by the themed connection banner at the top — the redundant pop-up is suppressed for cold-start/reconnect bootstrapping, while real failures while you're using the app still surface normally.
- Reconnect loop on remote (Tailscale) connections. Connecting from off your home network could make chat loop — repeatedly reconnecting before it finally settled — because a brief route-probe miss flipped the active route back to the (unreachable) home address and rebuilt the chat connection against it. The app now keeps the last working route through a transient miss, tolerates a slow first handshake on remote links, and absorbs VPN-interface churn, so a remote connection settles quickly instead of thrashing.
- Realtime voice: background tasks survive a brief disconnect. Asking the voice agent to run a longer task in the background no longer loses the result to a momentary network drop — the server keeps the run alive across the reconnect and delivers the answer once you're back, and a task that runs too long is now stopped cleanly instead of hanging silently.
- Realtime voice: the spoken answer is no longer dropped when a background task finishes. When the agent completed a longer background task, a harmless internal provider notice was being treated as a fatal error and closed the voice session right as the reply was about to be spoken (surfacing an "xAI realtime error" toast with Retry). Those transient notices no longer end the turn, so the answer is actually spoken.
- Realtime voice: the answer waits for you instead of playing to a dead connection. If a background task finishes while your phone is disconnected, the spoken summary is now held and delivered when the voice session reconnects — and the phone keeps retrying that reconnect for several minutes instead of giving up after one attempt. If the voice session is gone for good, the result arrives as a notification instead (the full answer is always in the chat).
- Realtime voice: asking for a second task while one is running no longer breaks the first. The agent now tells you the earlier task is still in progress (wait, check status, or cancel) instead of silently losing its result.
[1.2.6] - 2026-06-27
Added
- Session drawer refresh. A refresh button in the session drawer re-pulls the chat list on demand, so a title the server generates a moment after a turn shows up without waiting for the next reload.
Changed
- Calmer connection status. Transient connection status — reconnecting, checking, LAN↔Tailscale handoffs — now renders as a thin banner at the top that takes its own space (the screen slides down) instead of a card floating over the chat. The floating alert is reserved for persistent errors. Frequent confirmations (copied, profiles updated, profile/personality switches) moved to the same top banner instead of the bottom pop-up.
Fixed
- Chats stuck showing "Untitled". The session drawer no longer overwrites a chat's first-message preview with a blank title when the server hasn't auto-named it yet (and the SSE path never does), so chats stop reading "Untitled"; titles also reconcile once the turn settles. (#133)
- Rename on a non-default agent profile. Renaming a chat while a non-default profile is active now persists to that profile's own store instead of the shared one — matching the earlier session-delete fix.
[1.2.5] - 2026-06-27
Added
- Demo mode. A "Try the demo" option on the setup / Connect screen — and on the empty chat screen if you skip setup — opens an offline preview of the real Chat UI: a sample conversation with Markdown, a tool-progress card, and a rich card, with zero setup and zero network (works in airplane mode). A persistent "Demo mode — sample data, not connected" banner offers a one-tap Connect that opens the real setup wizard; other tabs show a friendly "connect your Hermes server" empty state. Lets a first-run user — or a Play reviewer with no server — see what the app does before connecting.
Fixed
- Crash when a non-address is entered as a server URL. Typing or pasting non-URL text (for example a label, or a line copied from the docs) into the API server or Dashboard URL field could force-close the app on the Manage / sign-in screen: the value was handed to the networking layer as a host, which rejected it with an uncaught error on the main thread. The setup fields now reject anything that isn't a valid host or
http(s)://URL with an inline error, and the dashboard and voice request paths treat a malformed address as "unreachable" instead of ever crashing. (#131, #132)
[1.2.4] - 2026-06-25
Added
- Connection security indicator. The chat status chip, the connection card, and the route picker now show at a glance whether your connection is encrypted — 🔒 Encrypted · TLS, 🛡️ Encrypted · Tailscale (both secure), 🛡️ Mixed routes, or ⚠️ Not encrypted — and tapping it opens a per-transport breakdown (chat, API, relay tools). A Tailscale/WireGuard route is now correctly shown as encrypted rather than implied insecure. Adds a new "Is my connection secure?" docs page explaining the difference between TLS and overlay (WireGuard) encryption.
Fixed
- Crash when a dashboard connection drops mid-check. A transient network blip on the dashboard session check (e.g. a pooled connection aborting or timing out over Tailscale) could close the app: the check returned a result type but re-threw the network error instead of reporting it, and it surfaced on the main thread. The check now reports the failure cleanly, and the connection probe degrades gracefully instead of ever crashing. (#129)
[1.2.3] - 2026-06-23
Fixed
- Crash on connect over TLS / Tailscale. Connecting to a server over an encrypted link (Tailscale Serve or public HTTPS) could hard-close the app with
NetworkOnMainThreadException. Tearing down an HTTP client closed live SSL sockets on the main thread, and a TLS socket close performs a network write — which Android forbids on the main thread. Client shutdown now always closes sockets off the main thread, so connecting over a secured link no longer crashes. (#118, #124; likely the v1.1.0 / Tailscale crash in #70)
[1.2.2] - 2026-06-22
Added
- Diagnostics: status timeline. Diagnostics now opens full-screen and leads with a top-to-bottom list of subsystem health checks — network, API server, chat transport, pairing, relay, and voice — each with a clear pass / warning / fail state and, when something's wrong, the reason why; tap a failing check for full detail. The recent-activity log stays below it.
Changed
- Connections wording simplified. The default connection is now just "Hermes" (previously "Vanilla" / "Standard Hermes"), and the optional power features are labelled "Relay" / "Relay plugin", across the connection setup, switcher, voice, and permissions screens.
- Clean chat mode shows more text. The distraction-free chat view gives its text a noticeably taller, scrollable area instead of capping it near a third of the screen.
Fixed
- Deleting a session on a non-default profile now sticks. Removing a chat while a non-default agent profile was active could leave it on the server, so it reappeared after the list refreshed; the delete is now scoped to the active profile.
- Session drawer opens on the right profile from a cold start. When launching with a non-default profile selected, the session list could briefly show the default profile's chats and then snap to the correct ones; it now waits for the profile to resolve and loads the right list directly.
[1.2.1] - 2026-06-21
Added
- Profile lock. Settings → Profile lock pins the app to a single agent profile and hides the rest from the pickers; the lock screen stays the one place that lists every profile, with a clear notice if the locked profile isn't on the current server.
- In-app What's New & changelog. A new Settings entry shows the current and past release notes any time — not just the post-update popup.
- Diagnostics: tap for detail + report. Logged errors now carry clean titles and open a detail view with Copy / Share / Create-GitHub-issue (the same flow as crash reports); classified errors across voice, chat, and connection are captured centrally.
- Update-available nudge. A dismissable in-app banner when a newer version is live — Google Play In-App Update on Play installs, GitHub Releases on sideload. Per-version dismissal, throttled, never nags.
Changed
- Crash reports can be shared without GitHub. The crash dialog now has a Share action alongside Copy and Report, handing the full report to the system share sheet (email, chat apps, notes, Drive). This covers users without a GitHub account and sideload installs that Play vitals never sees. Every outbound path stays user-initiated — nothing is sent automatically.
Fixed
- Voice override applies in Auto mode. A chosen per-profile/enhanced voice now takes effect when the engine is on Auto with the relay paired — previously only "Relay" mode applied it. Per-profile voice settings are also namespaced by connection.
- Realtime voice "Stop" stops immediately. Tapping Stop while the agent is speaking now halts realtime playback at once; over-chatty spoken status is throttled; and long background tasks no longer time out the turn (relay keeps the session alive while the task runs).
- Realtime Agent: brokered Hermes turns no longer fail (relay). When the Realtime Agent reached back to Hermes for context or tool work, a session-namespace mismatch could make the API Server reject the turn with
session_not_found. The relay now mints or reuses a valid API Server session and retries once, and reads the API Server's current nested create-session response. Provider-native turns are unaffected. - Hold-to-talk no longer releases on accidental drift. The mic button holds until the finger genuinely lifts, instead of cancelling when it drifts off the button.
- Voice overlay is readable. The voice dropdown panel and its status bubbles are opaque (no bleed-through), and the Focus/Overlay/Exit labels no longer wrap to two lines; invalid engine/route combinations are no longer selectable.
- Connection status overlay clears faster. Resolved (error/warning) connection toasts auto-dismiss within ~5s instead of lingering.
[1.2.0] - 2026-06-20
Added
- Sensitive-media classification (relay). The relay teaches the agent — server-side, via a removable system-prompt block — to mark private/NSFW media so the phone blurs it per your setting. On by default for relay installs (installing the relay is itself the opt-in); reversible from the "Agent context" toggle in the Relay dashboard, or
RELAY_AGENT_CONTEXT_ENABLED=0. The exact injected instruction is visible in the chat "What the agent sees" sheet under "Relay context (server-side)". No on-device or relay-side classifier — sensitivity stays model-emitted. Vanilla upstream (no plugin) is unaffected. Seedocs/plans/2026-06-20-relay-enhancement-layer.md. - Transport path is visible (chat). The chat status strip now shows which streaming path is actually in use — ⚡ Gateway (live thinking), 📡 Sessions, Completions, or Runs — instead of a generic "api online", and Chat Settings adds a basic→best tier ladder explaining the active path and its fallback.
- Injected-context audit (chat). Tap the context-usage meter in chat to open a "What the agent sees" sheet showing the exact extra context prepended to your next turn — persona/profile, phone status, and any per-turn (voice) hint. On the gateway path it notes the persona is applied server-side, so the audit is honest about what the phone does and doesn't send.
- Spoken-turn badges (chat). Voice-mode replies now carry a "Voice" chip and realtime replies a "Realtime Agent" chip — both with a speaker glyph — so spoken turns are distinguishable from typed ones in the scrollback.
- App themes. A new theme picker in Settings → Appearance ships eight looks: the signature Hermes Relay brand (with full light/dark) plus ports of the Nous Hermes baselines — Hermes Teal, Nous Blue (light), Midnight, Ember, Mono, Cyberpunk, and Rosé. The whole app — brand chrome, accents, and chat background — follows the chosen theme. Light/Dark/Auto applies to themes that ship both modes; fixed-mode themes show their own complete look.
- Hot-swappable agent sphere. The orb is now a pluggable "skin": an Adaptive skin that recolors to match your theme, built-in Classic / Aurora / Solar / Mono looks, and support for user-authored skins loaded from a small JSON spec. Each skin declares which live signals it reacts to (voice, tool bursts, activity), shown as capability badges in the picker. See
docs/sphere-spec.md. - Connections separate features from routes (Android). Connection settings now distinguish what a connection can do (a Features section) from how this phone reaches Hermes (a Route section), so you can enable Relay features over whichever transport you prefer. The optional plugin-provided Hermes Secure Link route is surfaced alongside LAN, Tailscale, public, and custom routes. The standard direct-to-upstream path is unchanged and still needs no plugin. See
docs/plans/2026-06-18-native-secure-routes.md. - Enhanced voice control (Gemini & xAI). When the relay uses a Gemini or xAI voice provider, Voice Settings can now steer it: pick a Gemini voice and model and turn on expressive tone tags (with optional natural-language voice direction), or set an xAI voice with expressive speech tags. Expressive tags also apply to xAI on the streaming voice-output renderer. Standard (no-plugin) voice stays configured server-side.
- Voice render-path visibility. Voice Settings shows which path is rendering speech (streaming vs. basic), and Diagnostics records it each session, making voice issues easier to troubleshoot.
- Agent pets — a living, swappable avatar. The orb can be replaced with an animated "pet" that reacts to what the agent is doing: idle / thinking / writing / speaking / listening states, a distinct working pose during tool calls, one-shot greet / celebrate reactions, and a loop that quickens as output streams. Add or remove pets right in Settings → Appearance (no
adbneeded), with a live state preview, a playback-speed slider, and optional frame auto-stabilization; capability badges (Voice · Tools · Activity) show honestly what each pet actually reacts to. Pets are pure data — an AI authoring kit and a JSON schema let you generate one from sprite art. Seedocs/pet-spec.mdand the custom-avatars guide. - Per-profile agent icon + single-image avatars. Each agent profile can wear its own small icon beside its name (client-side, never sent to Hermes), shown in chat, the agent sheet, the top bar, and Settings. Importing an avatar now also accepts a single image (auto-wrapped as a one-frame pet) — no animated pack required.
- In-app crash reporting. If the app ever force-closes, the next launch shows a clean dialog with the stack trace — Copy it, or Report to open a pre-filled GitHub issue from the bug template. The report persists until you acknowledge it, and the handler re-raises so the OS still records the crash in Play vitals.
- Clean text-flow mode (chat). A distraction-free chat layout where your sent text slides up into a continuous flow, paired with the swappable-avatar/pet system.
- Permissions review screen. A central page makes the permission model explicit — standard Chat and Manage need no phone-control permissions, while voice, camera, notifications, and sideload Device Control stay opt-in — reading the same live grants Bridge does.
- In-app attachment previews + richer capture. Attachments preview inline before sending, sensitive media is blurred per your setting, and the capture flow is richer.
Changed
- Much faster cold start. The app was building several hardware-keystore-encrypted stores at launch, which serialize on a process-global lock and stalled the chat header (model, personality, approvals) for seconds. It now builds a single keyset and the dashboard cookies share it, cutting measured time-to-connected from ~2.9 s to ~1 s after first frame, with the keystore lock contention gone. Existing sign-ins are migrated automatically on first launch.
- Honest loading, never stale, never hidden. Model, personality, and approvals now show a brief "checking…" state and fade in once the server confirms them, instead of popping in or showing a possibly-wrong value. Standard upstream controls (Model, YOLO, Fast, reasoning effort) are no longer hidden while loading or when unavailable — they always appear: a live control when ready, "checking…" while a value loads, or a cleanly disabled control with the reason (e.g. "available over the gateway transport") when this connection can't use them. The chat composer's reasoning-effort chip now shows alongside the model chip instead of lagging seconds behind the gateway check, and picker lists (models, personalities) show a brief, bounded "loading…" cue. The same fade-in is applied to the context meter, session drawer, and Manage panels.
- Tidier chat header. The LAN/Tailscale chip was dropped from the top bar (the bottom status strip already shows the route, and is now tappable to open Connections), and a
nonepersonality is no longer shown — leaving more room for the model name. - Connection toast reads like the cold-start screen. The floating connection status toast now shows a live checklist — Route / API / Relay each with a spinner, ✓, or ✕ as the checks land — instead of flat text, matching the splash screen's stepper. Swiping it up now tracks your finger (slide + fade) rather than snapping, and connection problems get an explicit "Open Connections →" link at the bottom so the path to the detailed view is obvious.
- Tidier chat header. The "approvals off" warning moved out of the agent subtitle into a single amber ⚡ icon in the top bar (tap for the full explanation in the agent sheet), and Share folded into a ⋮ overflow menu — so the personality · model subtitle no longer gets clipped by the trailing action icons.
- Voice replies are formatted for listening. In voice mode the assistant is now guided to answer in short, conversational sentences without markdown, emoji, or raw URLs — without changing what is stored in chat history.
- Leaner terminal screen (Android). The extra-keys bar scrolls horizontally with compact, fully-legible keys (no more clipped "CTRL"), the header is a single compact row showing one inline connection-status dot plus state, and the tab strip is hidden for single-tab sessions — the new-tab "+" moves into the header — reclaiming vertical space for the terminal.
- Relay terminals run on an isolated, TUI-tuned tmux. Sessions now use a dedicated tmux server/socket with its own config — instant ESC (
escape-time 0), truecolor$TERM, mouse and focus events on, and no status bar — so editors and full-screen tools behave correctly, without touching the user's personal tmux. - "Standard" is now "Vanilla Hermes" throughout. The user-facing name for the no-plugin upstream path is now Vanilla Hermes, so it's clear the default path runs on a plain Hermes agent.
- QR pairing degrades gracefully on unusual cameras. On foldables and devices where the camera can't initialize, the scanner now shows a "camera unavailable — pair manually" card instead of force-closing.
- Image & attachment viewers rotate to landscape. The full-screen image / attachment viewers can rotate to landscape even though the rest of the app stays portrait-locked.
Fixed
- Clearer error when a feature needs a newer relay. Toggling a setting an older relay plugin doesn't recognize (e.g. xAI expressive speech tags) now shows "Relay update needed" instead of a generic HTTP 400 with a dead Retry button. Genuine input errors are unaffected.
- Connection status toast is no longer see-through. The floating connection-lost/switching toast renders fully opaque so content behind it no longer bleeds through and hurts legibility.
- Provenance badges survive the post-turn history reload. "Voice", "Realtime Agent", "Stopped", and "Error" chips are now preserved when the conversation reloads after a turn, instead of silently vanishing.
- Chat and Manage no longer stay dark in Light mode. Brand-styled surfaces bypassed the theme and were effectively hardcoded dark; they now follow the selected theme and light/dark mode, and the glow/border flourishes key off the active theme rather than the system setting.
- Realtime voice no longer drops the conversation mid-session with some providers. A normal end-of-turn signal was being rejected on certain voice providers, ending the session every turn.
- Relay voice synthesis no longer leaves temporary audio files behind on the server.
- Clearer voice errors and an oversize-recording guard. Standard voice now rejects an over-long recording before uploading it and shows a helpful message for audio the server can't read, instead of a generic HTTP error.
- Terminal paste no longer auto-runs multi-line text. The key-bar PASTE now uses bracketed paste, so multi-line content lands intact in shells and editors instead of executing line by line.
- Terminal on-screen arrows behave inside TUIs. Arrow/Home/End keys follow the running app's cursor-key mode (application vs. normal), so they work correctly in vim, less, and fzf.
- Terminal footer spacing. A small gap now keeps the last terminal row clear of the key bar (it could previously look like the footer overlapped it), and a redundant navigation-bar inset that left empty space below the keys was removed.
- In-chat model picker now actually applies on a new chat. Picking a model and provider in the chat composer (e.g. Grok 4.3 via your xAI subscription) is bound to the new conversation, so the agent runs on the picked model instead of silently falling back to the account's global default. Switching profiles retires an explicit pick so the profile's own model takes over, and the picker label updates immediately instead of lagging a round-trip.
- Server-generated images render in chat when paired to the relay. An assistant image that points at a server-side file path is now fetched through the relay's media route and shown inline (tap to zoom), instead of degrading to an "image is on the server" notice. On the SSE chat path the agent is also told it can surface images and files by path when a relay route is configured (visible in the chat "What the agent sees" sheet). Standard (no-plugin) connections are unchanged.
- Smoother profile switching. Switching profiles no longer blanks the conversation to an empty/"Loading…" state before the new history loads; the previous transcript is held and cross-fades to the new one.
- In-chat model switch now applies mid-conversation, not just on new chats. Picking a model in an already-started chat switches the live session in place — the same path the desktop/TUI
/modeluses — instead of racing into a global-default write, so the turn runs the model you picked. - Server-side turn errors always surface. A failed turn (e.g. a provider rejecting the request) now stays on screen as an error bubble with the message, instead of appearing for a moment and then vanishing when the conversation reconciled after the turn.
- The model shown in chat matches the live session. The chat header and the agent detail sheet now show the model the current session is actually running (reflecting a mid-session switch) rather than the profile/global default, and the agent sheet no longer pairs the global default model name with the session's provider — it now also names the host's "Server default" when the session runs something different.
- Server steering markers no longer appear as chat bubbles. The "[System: the active model/personality changed]" notes the server injects into history for the agent's benefit are hidden from the transcript by default (matching the desktop/TUI); a new "Show system messages" debug toggle in Chat Settings can reveal them.
- Per-reply token counts (and other per-message details) survive the post-turn reload. The input/output token subtext, provenance badges, tapped-card state, and voice/realtime sync traces are now preserved when the conversation reconciles against the server after a turn — previously a normal reply lost its token line once the turn finished (the error bubble kept it only because errored turns skip that reload). The reloader now preserves client-only message details by default instead of dropping any it doesn't re-derive from the server.
- PDF viewer no longer crashes when the document closes mid-render. A PDF preview that was torn down during a layout pass could read a closed renderer and throw
IllegalStateException: Document already closed; the renderer is now guarded so it returns nothing instead of crashing. - No crash opening a chat with a server-local image. Rendering a relay-fetched image could throw
ClassCastException: kotlin.Result cannot be cast to byte[]because asuspendfunction returnedkotlin.Result(which collides with the coroutine machinery's own wrapper); a purpose-built result type fixes it. - Side-loaded avatars and sphere skins are reachable again. Both loaders read internal storage while the docs (correctly) pointed
adb pushat external app-scoped storage, so a side-loaded pet or skin never appeared. Both now resolve through one external-preferred location, so the documented install path works. - Reopened chats paint the session's real model (not the profile/global default), the model-picker "Server default" caption shows the true default rather than the active override, and a chat's media badge shows only when paired — with the underlying server-image fetch-failure reason surfaced when a fetch fails.
[1.1.0] - 2026-06-16
Added
- Automated Play Console upload on release. When a
PLAY_SERVICE_ACCOUNT_JSONsecret is configured, pushing a stableandroid-v*tag uploads thegooglePlayApp Bundle to the Production track as a draft (a human still starts the rollout). Prereleases are skipped, and thesideloadflavor is structurally blocked from ever publishing to Play. Without the secret, the release builds publish to GitHub Releases exactly as before. - Desktop UI preview harness (
:ui-preview). A non-shipped Compose for Desktop module renders presentational composables in a window on the PC with Compose Hot Reload, for fast UI iteration without a device build/install loop. It reuses the shared sphere algorithm as its single source of truth. - Plugin: guided env-key setup. The relay plugin declares its optional voice-provider keys (
XAI_API_KEY,OPENAI_API_KEY,ELEVENLABS_API_KEY) in its manifest, sohermes plugins installprompts for them (masked, with a "get yours" link) instead of hand-editing.env. The standard no-plugin path needs none. - Plugin: native install path. Tools-only setups can install via
hermes plugins install Codename-11/hermes-relay/plugin; the full relay still uses the curlinstall.sh. /relayslash commands.relay status · devices · pairusable mid-conversation from any platform (CLI / Discord / TUI).- Dashboard relay-status widget. A
Relay · connected / offline / unpairedbadge in the dashboard header, visible on every page. - Session-start relay health check. A minimal, fully-guarded
on_session_starthook records relay reachability without slowing the gateway.
Changed
- Release names normalized by surface. Future GitHub Releases are named
Hermes-Relay-Android,Hermes-Relay-Plugin, andHermes-Relay-CLI, with future tags onandroid-v*,plugin-v*, andcli-v*. The CLI installer and updater still understand historicaldesktop-v*prereleases during the migration. - Per-surface release notes. Plugin and CLI GitHub Releases now use hand-written
PLUGIN_RELEASE_NOTES.md/CLI_RELEASE_NOTES.mdfiles (Summary + Added/Changed/Fixed + Install/Verify) — the same format as Android'sRELEASE_NOTES.md— instead of static boilerplate baked into the workflow. The release workflows substitute the version into the install commands automatically. - Settings screen overhaul (Android). Status pills are now exception-only — they appear only when a surface needs attention and stay quiet when healthy. The Power tools section shows a single state-aware Plugin active / required / offline badge instead of an identical "Relay paired" chip on every card. Connections moved to the top (above the Hermes section), Diagnostics + Developer options moved into the App section, the status chips were restyled to match the app's translucent-bordered language, and the brand blue was deepened.
Fixed
- Force-close on connect when the stored credential keyset was corrupt. A corrupt encrypted token store (which can happen after an app upgrade or device restore) threw during construction and crashed the app right after a successful pair, on both standard and relay connections. The token store now heals a corrupt keyset on the spot, and credential storage degrades to a re-pair instead of crashing if the device keystore is unusable.
- Dashboard plugin: unreadable button labels. Solid buttons in the relay dashboard panel inherited the container text colour, which matched their background. Solid button variants now keep their proper contrast colour.
- Installer failed on uv-managed Hermes hosts.
install.shassumedpiplived in the hermes-agent virtualenv, but environments created byuv(the upstream default) ship nopipmodule, so the editable install aborted at step 2. The installer now bootstrapspipviaensurepip, or falls back touv pip, so the plugin installs cleanly on uv-managed cores. - Chat settings (Android). The streaming-endpoint picker no longer wraps "Gateway"/"Sessions" onto a second line, and the system-prompt preview now reflects the enabled context toggles (foreground app, battery, safety rails) with representative placeholder values instead of looking inert.
- Dashboard plugin: buttons rendered as blank boxes. The host dashboard's Nous design-system
Button/Badgeuse boolean variant flags (outlined/ghost/invert) and atoneprop — not the shadcn-stylevariantprop the plugin passed — so every button collapsed to a solid near-white fill with an invisible label. The plugin now translates its props to the design-system contract via an adapter, and drops a label-hiding CSS reset.
1.0.0 - 2026-06-14
Added
-
Relay plugin diagnostics and install guidance.
hermes relay doctornow reports standard upstream API/dashboard reachability, Relay loopback state, dashboard plugin presence, plugin-manager layout, and whether the legacy bootstrap monkeypatch is installed. The plugin manifest now advertises its Android and desktop tools, andafter-install.mdgives the upstream plugin manager a first-run handoff. -
Plugin-owned compatibility hook lifecycle.
hermes relay compat status/install/removenow owns the optionalhermes_relay_bootstrap.pthstartup hook, so the monkeypatch can be inspected, added, or removed without rerunning the legacy installer. The standard v1.0.0 path does not require this hook. -
Legacy cleanup alignment. The legacy installer now installs the optional
.pthhook through the plugin compat lifecycle, and the uninstaller removes every shell shim it creates (hermes-pair,hermes-status,hermes-relay,hermes-relay-update,hermes-relay-tailscale) while delegating hook cleanup tohermes relay compat removewhen available. -
Gateway chat transport with live thinking. Chat can ride the upstream dashboard
/api/ws(thetui_gatewaysurface the official hermes-desktop client speaks) — the only vanilla-upstream path that streams reasoning live, so the Thinking block and sphere light up during generation. "Auto" prefers it when the dashboard is reachable and Manage is signed in, and falls back to the SSE endpoints per turn. -
Gateway desktop parity. Native image/PDF/file attachments (with an in-chat notice when a turn falls back to a transport that can't carry files), mid-turn steering, edit & resend, interactive approval / clarify / sudo / secret cards, live subagent lanes, a context-window meter, server slash commands in autocomplete, and turn-complete notifications when the app is backgrounded.
-
Gateway warm-start + Keep connected in background. Pre-warming the gateway on foreground moves the cold session-setup cost off the send path. An opt-in foreground-service toggle (both flavors;
specialUse, off by default) holds the socket open in the background so a long-backgrounded conversation resumes instantly. -
Switch agent profiles from chat. Pick a different agent — model, SOUL, personality, and skills — per conversation. The selection is ephemeral (bound to the session like the official desktop; it never changes the server's default agent for other clients). The session drawer scopes to the active profile and loads that profile's history, and the right agent is restored on cold start. The Manage tab's server-wide Activate Profile action now confirms first.
-
Manage parity with the desktop dashboard. Change models from the full provider catalog, manage provider keys (write-only, masked, reveal), create/edit profiles and SOUL.md, and browse/install/update skills. Manage data is cached to disk for an instant cold launch.
-
Open & save chat images and attachments. Tap an image for a full-screen viewer (pinch-zoom, double-tap, Share/Save); non-image attachments gain an Open/Share/Save menu. Saves land in
Pictures/Download/Hermes-Relaywith no permission on Android 10+, preserving the original bytes. -
Persistent Realtime Agent voice + background runs (ADR 33). The realtime engine keeps one session across turns (follow-ups retain context); a long Hermes run is promoted to a tracked background task and spoken when ready, so the conversation stays responsive.
-
Redesigned chat input bar. A Telegram-clean pill field with one trailing button that morphs between Send / Voice / Stop / Steer / Queue; the slash button is gone (typing
/still opens autocomplete). -
Routes card reachability verdicts ("Reachable", or the specific failure reason) and per-turn latency tracing (
TurnLatency, durations only) for diagnosing transport speed.
Changed
-
Relay plugin/server version aligned to v1.0.0. The Python package, plugin manifest, dashboard manifest, and relay runtime now use the same
1.0.0line as the stable Android release so a retagged source checkout describes one product version. -
The standard (no-plugin) path is first-class. Chat, Manage, and voice all work against an unmodified upstream Hermes agent; standard voice rides the dashboard audio surface (
/api/audio/*) with the Manage sign-in, and relay-paired voice is the profile-aware fallback. The relay plugin is now purely additive. -
Seamless connection UX. LAN↔Tailscale handoffs and reconnects no longer reload the chat; connection and update status are now in-theme slide-down toasts over the content instead of banners that pushed the UI around.
-
Editable, roaming routes. Add/edit/remove routes in Settings → Connections; bare-host URLs default their scheme and port (and preview what will be saved); remote-access (Tailscale) is surfaced in the main setup flow with a "Remote" readiness line.
-
Faster Manage. A shared auth preamble plus concurrent payloads cut a full load from ~40 round trips to ~12; a process-lifetime cache and startup pre-warm render the last-seen data instantly, and Manage now names which dashboard URL it's talking to.
-
Faster, calmer cold start. Key-less connections skip the multi-second keystore decrypt; the startup sphere is now the actual loading screen with narrated check lines, and the OS splash blends into it.
-
Docs + branding. The docs site was rechromed to the app theme and repositioned around the two-path story; the README and Play listing were refreshed standard-first; product-name copy normalized to Hermes-Relay.
-
Quality-of-life. Quote-in-reply, share-conversation-as-Markdown, ambient mode as a long-press gesture, a floating status pill, decluttered Manage cards, back buttons on pushed screens, and a softer active-connection card.
Fixed
-
No "Connect to Hermes" flash on cold start. The empty-state now distinguishes "still hydrating from disk" from "nothing configured" (
ConnectionStore.isHydrated→chatConnectState), showing a quiet "Connecting to Hermes…" spinner until ready and the connect CTA only once hydration confirms no connection exists. -
In-app What's New renders cleanly — parsed into a version subtitle, bold section headers, and real bullets instead of raw text with literal
*. -
App-start UI freeze from Keystore lock contention. The encrypted cookie store built its StrongBox-backed prefs eagerly in its constructor (1–4 s under a process-global lock) from several code paths at once; it now builds lazily on an I/O thread and is shared per connection.
-
Standard connections now follow LAN↔Tailscale changes, standard voice follows the resolved route (not the persisted URL), and a stale probe cache can no longer pin a dead route after a handoff or resume.
-
Editing a URL no longer wipes fallback routes (edits merge with stored extras instead of rebuilding from the edited URL alone); "Re-check" / "Use now" no longer fail silently (the probe always publishes its outcome and per-route failure reasons); and a network change can no longer resurrect a deliberately disconnected relay socket.
0.8.1 - 2026-05-26
Fixed
- Voice mode crash with barge-in on legacy TTS playback. When barge-in was enabled and the relay served audio over the legacy
/voice/synthesize(Media3) path, the first agent sentence played for ~2 syllables and then the app crashed withIllegalStateException: Player is accessed on the wrong thread. The barge-in listener'sDispatchers.IOreader was readingExoPlayer.getAudioSessionId()(a thread-confined accessor) to attach the echo canceller.VoicePlayer.audioSessionIdnow serves a@Volatilecache populated from main-thread Media3 callbacks, so it is safe to read from any thread.
0.8.0 - 2026-05-23
Added
-
Provider-native Realtime Agent voice. Android can opt into a Realtime Agent voice engine where Android streams mic PCM to the relay, xAI or OpenAI owns realtime speech recognition and speech generation, and Hermes remains the governed authority for tools, memory, profiles, confirmations, current-data checks, side effects, and durable transcript context.
-
Hermes-brokered realtime tool timeline. Realtime Agent turns now mirror transcript, assistant speech, Hermes task state, concise tool-status rows, confirmation state, path badges, and compact result provenance into chat/voice UI without dumping raw tool output aloud.
-
Connection diagnostics and activity logs. Settings now includes a Diagnostics surface with sanitized recent API, relay, session, endpoint, and voice activity. API / Relay / Session detail drawers also tail the relevant recent activity so hung or unreachable relays are visible without ADB first.
-
Realtime and Voice Settings active-engine layout. Voice Settings now separates Voice Engine from global voice controls, shows only the selected engine's provider card, keeps fallback TTS visible as a global safety-net card, and provides Test Current Engine: stable voice plays the saved Voice Output sample, while Realtime Agent opens a provider-native
/voice/realtime-agent/*test session and plays streamed realtime audio. -
Voice Lab text and mic demos. The realtime voice test screen now offers two clearly separated demos: a Text demo that plays raw provider TTS, and a Mic demo that exercises the full agent path — real speech recognition, Hermes brokering, and a spoken reply — with tap-to-record / tap-to-stop capture. A
scripts/realtime-voice-lab-smoke.ps1smoke script accompanies the lab. -
Realtime playback diagnostics. Playback now records a time-to-first-audio metric, logs requested-vs-actual AudioTrack buffer sizes, runs a first-frame watchdog, and cross-checks playback drain drift so cold-start and underrun regressions surface in the Diagnostics log instead of as silent dead air.
Changed
-
Google Play Bridge Core split. The Google Play Android track keeps relay pairing, chat, profiles, voice, terminal/TUI, media, notification companion, relay sessions, diagnostics, and status while removing AccessibilityService-backed Device Control declarations and permissions. Sideload remains the track for screen reading, gestures, screenshots, SMS/calls, contacts/location, overlays, wake locks, and unattended control.
-
Release lanes now use explicit product tags and names. Future Android releases use
android-v*, plugin/Python releases useserver-v*, and CLI releases continue ondesktop-v*. GitHub Release names now publish asHermes-Relay-Android vX.Y.Z,Hermes-Relay-Plugin vX.Y.Z, andHermes-Relay-CLI vX.Y.Z; the old relay-named server scripts remain compatibility shims. -
Realtime voice instructions are provider-neutral. Realtime providers receive active interface context, local date/time, provider/model/voice/profile metadata, and guidance to ask Hermes for current facts, research, device/desktop state, project context, precise/versioned data, and any requested checks instead of guessing from model knowledge.
-
Play/user docs now match the actual artifact. Release-track docs, feature matrix, getting-started copy, privacy/security references, and Play listing copy now say Google Play has no AccessibilityService, screen reading, gestures, screenshots, or phone-control utility permissions.
Fixed
-
Silent / choppy first-turn realtime voice playback. The AudioTrack deep-buffer cold-start was parking the playback head at zero so the first turn dropped or stuttered. The streaming buffer was shrunk from 4000ms to 700ms, the low-latency prebuffer threshold retuned, and a preroll force-start removed, giving reliable low-latency playback from the first frame. Confirmed on-device.
-
Voice Lab waveform now tracks the playback cursor. The waveform is driven by
RealtimePcmPlayer.playbackAmplitude()at the playback position instead of socket-arrival time, so the visual matches what is actually being heard. -
Realtime Hermes calls no longer depend on the phone's saved Hermes API key. Provider-native Hermes tool calls are brokered by the relay with its server-side Hermes credential, so a phone can be paired for realtime voice without exposing or misusing its saved API bearer.
-
Hung relay voice turns fail visibly. Voice turns run relay health preflight and shorter realtime/session timeouts so Settings and Voice mode surface unreachable relay state instead of sitting indefinitely on Thinking.
-
OpenAI realtime is no longer treated as render-after-Hermes fallback.
openai_realtimeis registered as a native Realtime Agent provider path alongside xAI, with provider-native audio events normalized through the same broker contract. -
Local release signing no longer falls back to debug when
local.propertiesuses a repo-root relative keystore path. The Android Gradle signing config now resolves relative keystore paths from the repo root, matching the documentedrelease.keystoresetup.
0.7.0 - 2026-05-19
Added
-
Profile-aware Hermes sessions and voice settings. Android now treats Hermes profiles as first-class connection state: profile selection resolves against the active server, profile-specific chat sessions are persisted separately, default/Victor display is normalized, and per-profile voice provider/model/voice settings can be read and saved through server-owned endpoints without depending on Hermes config mutations.
-
Realtime voice playground and provider lab. The relay now includes standalone OpenAI/xAI/ElevenLabs-oriented voice lab tooling, provider adapters, provider option discovery routes, realtime playground routes, and generated WAV/JSONL artifact ignores for iterative voice quality testing outside production Hermes routes.
-
Streaming voice output routes. server-owned
/voice/output/*, realtime playground, profile voice config, and provider option endpoints support provider-neutral TTS rendering, dynamic voice/model option surfaces, and profile-scoped voice configuration for Android. -
Experimental Android realtime voice overlay. Android adds a richer voice overlay with tap-to-talk, continuous mode controls, optional system overlay mode, compact mode, realtime waveform visualization, playback controls, and an experimental badge around barge-in instead of treating all voice as experimental.
-
Experimental realtime Hermes voice-agent plan.
docs/plans/2026-05-19-realtime-hermes-voice-agent.mdrecords the next architecture step: provider-native realtime speech with Hermes-brokered profiles, sessions, tools, confirmations, and transcript mirroring. The stable Hermes chat + voice-output path remains the default. -
Desktop tray pairing and consent flow. The desktop surface gained Tauri tray pairing, QR/consent affordances, sidecar preparation, and computer-action approval polish so desktop and Android pairing flows are closer to parity.
-
Shared relay/Quest scaffolding. Experimental
relay-core,relay-ui, and Quest prototype modules were added for shared pairing, terminal, transport, voice, and morphing-sphere work without changing the Android phone app's default route. -
Desktop Chat tab with first-run route setup. The Tauri tray dashboard now has a Chat tab inspired by the Hermes Desktop chat-first flow. It streams through the saved paired relay when
~/.hermes/remote-sessions.jsonhas an active session, or through a direct Hermes gateway/API URL when relay pairing is not available. The tab supports stop, retry, new chat, clear, current-session transcript history, and a setup panel that offers relay pairing or direct WebAPI configuration without saving the optional API key. -
First-class desktop TUI tab. The Tauri tray dashboard now gives the embedded xterm/PTY Hermes session its own sidebar tab instead of nesting it under Terminal / CLI. Terminal remains the external launcher, shim-state, and copyable-command surface, while plugin embeds route into the same TUI tab.
-
Desktop surface plugins. The desktop CLI and Tauri tray now register built-in terminal surface plugins, starting with Herm (
herm-tui) fromliftaris/herm. Users can inspect plugin status, install or update Herm, launch a fresh dashboard, resume withherm -c, or embed the plugin in the tray's xterm/PTY surface withbunx/npxfallback when thehermbinary is not installed. -
Relay server release track. Relay server and Python package releases now use
relay-v*tags, validate relay-owned version metadata, build wheel/sdist artifacts, generate checksums, and publish through.github/workflows/release-relay.yml. This lets Relay server fixes ship independently from Android appversionCodebumps and desktop CLI alphas. -
Dashboard plugin CI.
.github/workflows/ci-dashboard.ymlbuilds the dashboard plugin, runs the dashboard API tests, and verifies the plugin-owned QR modal CSS markers are present in the built bundle. -
Upstream integration sync reference.
docs/upstream-integration-sync.mdnow tracks which Hermes-Relay surfaces use upstream-supported extension points, which pieces are server-owned compatibility layers, and what has to be checked before changing relay, Android, desktop, dashboard, bootstrap, or user-doc surfaces. -
Relay version sync verifier.
scripts/check-relay-version-sync.pyvalidates the relay package version against plugin metadata and dashboard metadata so release and dashboard surfaces cannot silently drift.
Changed
-
Stable voice is now the main Android voice path. Voice mode defaults to Hermes chat streaming plus relay-managed voice output, with realtime-provider work kept as a standalone lab/testbench and future experimental mode instead of replacing Hermes session/tool authority.
-
Realtime voice output uses balanced coalescing. Normal assistant speech is batched into more natural chunks while tool/status speech stays immediate, reducing provider render resets and tone/volume variation during voice replies.
-
Voice settings are profile-scoped and option-aware. Android can fetch provider/model/voice options from relay endpoints, show profile context in voice settings, save voice choices per Hermes profile, and expose advanced manual entry when provider metadata is incomplete.
-
Voice UI state is synchronized with chat state. Voice mode now reuses more of the chat session/profile state, preserves live transcript and tool timeline visibility, and improves overlay exit/minimize behavior for hands-free use.
-
Release versioning is split by surface. Android app releases remain on
v*and usegradle/libs.versions.toml; Relay releases userelay-v*and keeppyproject.toml,plugin/relay/__init__.py,plugin/plugin.yaml, and dashboard plugin metadata in lockstep; desktop remains ondesktop-v*anddesktop/package.json.scripts/bump-version.shis now a backward-compatible Android alias, with new explicitscripts/bump-android-version.shandscripts/bump-relay-version.shhelpers. -
Upstream voice imports are isolated. Relay voice routes now call upstream Hermes STT/TTS helpers through
plugin.relay.upstream_voice, keeping private upstream voice helper imports in one adapter module until Hermes exposes a stable HTTP voice API. -
CI paths and release actions tightened. Relay CI now watches Relay-owned paths instead of all
plugin/**, validates Relay version metadata during syntax checks, uses explicit timeouts, and runs the focused route/auth/session test slice instead of broad test discovery. Release workflows now usesoftprops/action-gh-release@v3.
Fixed
-
Profile switching no longer silently falls back to the wrong local API host. Profile API URL resolution now handles per-profile Hermes API servers, default/Victor compatibility, and relay-managed profile metadata so selecting a profile does not try to create sessions against
localhostfrom the phone. -
Non-default profile names remain visible in chat. Agent display metadata is normalized so selected profile names persist above finalized assistant messages instead of disappearing back to the default label after stream completion.
-
Voice waveform and playback state are better aligned to real audio. The output waveform waits for audio playback, handles processing separately, and avoids returning to the microphone too early at the end of an assistant response.
-
Continuous voice mode no longer starts a session just because auto mode is enabled. Auto/continuous remains a preference, while explicit voice start/stop controls decide when a voice session is active.
-
Android voice mode no longer 403s when paired over plain-LAN
ws://with a Hermes API key saved. Symptom: tap the mic in Voice mode → red banner "Voice access expired — extend or re-pair with voice grants" even though the Connections card shows API Server / Relay / Session all green. Root cause:RelayVoiceClientpreferred the saved Hermes API key over the paired Relay session token; the relay's_request_is_secure_enough_for_api_bearercorrectly rejects API-bearer auth on/voice/*over plaintext outside loopback/Tailscale, returning a generic 403 that the client flattened to "expired." Fix: invert bearer precedence so paired devices use the session token first (no transport guard — it's the credential the QR/pair handshake already established), with the API key as fallback for chat+voice-only installs that never paired.describeHttpErrornow also reads the server's text/plain response body when present so future 403s show the relay's actual reason instead of a one-size-fits-all string.
[0.6.1] - 2026-05-06
Added
-
Android bridge media sharing and MMS handoff. New
android_share_mediaandandroid_send_mmstools expose full file/attachment support through the relay media registry and AndroidFileProvidercontent://grants. Host-local paths are registered with/media/register, phones fetch bytes with their paired relay session, and the sideload app opens Android's native share or MMS compose UI after on-device confirmation. Relay HTTP now includes/share_mediaand/send_mms, and docs spell out that directandroid_send_smsremains text-only{to, body}. -
Relay voice endpoints accept Hermes API bearer tokens.
/voice/config,/voice/transcribe, and/voice/synthesizenow accept either a Relay session token with explicitvoice:*grants or the existing Hermes API bearer token. API bearer validation is voice-only, uses the configured Hermes API server's protected/v1/modelsendpoint with a short positive cache, and rejects non-loopback plaintext by default unless a trusted HTTPS proxy header or the explicit dev escape hatch is configured. Existing Relay sessions are backfilled with voice grants so paired phones do not need to re-pair. -
Relay CLI can toggle plain-LAN API-key voice auth without restart.
hermes relay insecure-api-key status|on|offcalls the running relay's loopback-only/relay/securityendpoint and flips the runtimeallow_insecure_api_bearerflag immediately. This keeps HTTPS as the default for API-key voice auth while making Android phone LAN smoke tests possible without exporting env vars or restarting the service. -
Desktop CLI alpha.14 —
Ctrl+A ?chord re-displays the chord-help banner. The attach-time banner scrolls off as soon as anything writes to the terminal, so users mid-session forgot the verb list and had to detach + re-attach (or guess). NewCtrl+A ?(andCtrl+A hsynonym) reprints the banner to stderr without leaving the session. Banner text refactored into a singleCHORD_HELPconstant so the attach-time print, the?chord, and the unknown-chord hint can't drift out of sync. Unknown-chord hint now also lists?as one of the known verbs. -
Desktop CLI alpha.13 —
Ctrl+A vchord inhermes-relay shellfor in-session paste. Reported gap: "...we have to exit hermes-relay shell to runhermes-relay paste. Can we leverage a tmux hook?" Tmux runs on the Linux server with no path back to the Windows clipboard, so server-side hooks can't help — but the existing client-side chord state machine (Ctrl+A .detach,Ctrl+A kkill,Ctrl+A Ctrl+Aliteral) is the right place. AddedCtrl+A v: client reads its own clipboard image (samecaptureClipboardImage()path as the/pasteREPL command), POSTs to/clipboard/inboxvia the new sharedstageClipboardImageToInbox(url, token)helper exported fromcommands/paste.ts, then types/paste\rinto the PTY so the upstream Hermes TUI consumes it in the same flow the user would have typed by hand. Status line goes to stderr so it doesn't pollute the PTY stream:[shell] pasted 1920×1080 (245 KB) → /paste. Reentrancy guard prevents double-stage on a fast double-press. Banner help and chord doc-comment updated to list the new verb.
Fixed
-
Android bridge tool/route contract drift. The active plugin import now uses
plugin.tools.android_toolas the single source of truth, while top-levelplugin/android_tool.pyremains as a compatibility shim. The relay now registers/return_to_hermes, matching the documented and phone-side command, and bridge status gating checks/bridge/statusso tools are hidden unless a phone is actually connected. -
Android CI/release gate no longer hangs on the broad Gradle test aggregate. The Android CI and
v*release workflows now run the stable sideload pairing/connection regression slice with explicit timeouts while the deferred full JVM test-suite cleanup remains tracked separately. -
Android connection/profile state no longer leaks across switches. Connection switches now clear the outgoing profile object immediately, load the destination connection's saved profile name only after that connection is active, and resolve it against the destination server's current profile list. The default local relay URL is now
ws://localhost:8767, and auto-managed relay URLs are derived from the active API URL before reconnecting. -
Desktop CLI alpha.12 — install scripts truncated the prerelease suffix in the "upgrading X → Y" line. A user saw
existing install detected: 0.3.0-alpha.9 — upgrading to 0.3.(literally truncated mid-token). Root cause:normalize_pinned_version(bash) andGet-NormalizedPin(PowerShell) stripped everything after the first-, including-alpha.N. Comment claimed this was "for comparison against the bare semver the binary reports" — but since alpha.4, the binary's--versionreports the FULL semver (via the embeddedgen:versionconstant), so the strip is no longer defensive, just lossy. Removed the suffix-strip from both normalizers; both now produce0.3.0-alpha.11fromdesktop-v0.3.0-alpha.11. The equality compare at line 138 still works because both sides include the prerelease tail. -
Desktop CLI alpha.11 —
hermes-relay update(and the install one-liners) saw the wrong "latest" release. On alpha.9,hermes-relay update --checkexpected to see alpha.10 but reported "Up to date." Root cause: GitHub's/repos/.../releasesAPI returns rows ordered by the release object'screated_at, NOT by SemVer of the tag — andcreated_atshifts whenever the row is touched (re-tag, manual edit, asset replacement). When alpha.9's release row got touched after alpha.10 was tagged, the API listed alpha.9 first and all three of our resolvers blindly took[0]. Fix: pick the SemVer-max from all desktop-v* tags explicitly. (1)desktop/src/updater.ts—desktop.reduce((max, r) => compareVersions(r.tag_name, max.tag_name) > 0 ? r : max). (2)desktop/scripts/install.sh—sort -V | tail -1(zero new deps; bash + sort is sufficient). (3)desktop/scripts/install.ps1— customSort-Objectcomparator that packs (Major, Minor, Patch, PrereleaseRank, PrereleaseNum) into a zero-padded sortable string with alpha=1, beta=2, rc=3, stable=999. Live-verified against the real API: all three now returndesktop-v0.3.0-alpha.10instead ofalpha.9. -
Desktop CLI alpha.10 —
hermes-relay pastealways returned "No image on clipboard" on Windows even when an image was present. Root cause: the PowerShell invocation incaptureClipboardWindows(src/chatAttach.ts) was missing the-STAflag.powershell.exe -Commanddefaults to MTA (Multi-Threaded Apartment), and[System.Windows.Forms.Clipboard]::GetImage()only returns a valid image from STA threads — from MTA it silently returns null, indistinguishable from "no image present." Also affects thechatREPL's/pastecommand which routes through the same Windows code path. Fix: added-STAto the powershell args list (now['-NoProfile', '-NonInteractive', '-STA', '-Command', ps]). Live verification: empty clipboard returns null; a cyan 100×80 PNG placed via[System.Windows.Forms.Clipboard]::SetImagereturns the expected 305-byte capture with correct dimensions. Affectsdesktop-v0.3.0-alpha.7throughdesktop-v0.3.0-alpha.9.
Changed
-
Android voice no longer requires Relay pairing when a Hermes API key is saved. The phone now resolves voice auth from the saved Hermes API key first, then falls back to the paired Relay session for
/voice/config,/voice/transcribe, and/voice/synthesize. Chat+voice-only setups can use manual/API-key configuration without the full pairing-code flow; bridge, terminal, media, clipboard, profile writes, and Android-control routes remain paired-session-only. -
Relay grant labels are now human-readable in Android and dashboard management UI. Relay session grant chips still preserve the server keys internally, but user-facing lists now sort the known grant set and render labels such as
Voice STT/Voice TTSinstead of rawvoice:stt/voice:tts. Privacy and configuration docs now reflect that Voice mode uses runtime microphone permission and split voice grants. -
Desktop CLI alpha.8 —
/screenshotis multi-monitor aware by default. The alpha.6/alpha.7screenshotHandler/captureScreenshotcaptured only the primary display on Windows and treateddisplayas a number-only param. alpha.8 changes the default to-1(all monitors stitched) and accepts string aliases so both the agent tool call and the/screenshotslash command can say'all'/'primary'/'1'/'2'etc. Windows path usesSystem.Windows.Forms.SystemInformation.VirtualScreenfor the union rect (handles negative coordinates when monitors are arranged left-of-primary). macOS path usesscreencapture -D Nfor 1-indexed per-display capture. Linux path relies on grim/scrot/import's inherent whole-X-screen behavior. REPL/screenshotdefaults to all monitors;/screenshot primaryor/screenshot 0 | 1 | 2narrow. Live smoke on a multi-monitor Windows box: all = 1.6 MB stitched, primary = 405 KB — 4× size ratio confirms virtual-screen path. Zero server changes;image.attach.bytesRPC consumes whatever bytes the client sends.
Added
-
Desktop CLI alpha.7 — native image paste in
hermes-relay chat.desktop-v0.3.0-alpha.7. Plan:docs/plans/2026-04-23-desktop-alpha-7-native-paste.md. Users now type/paste(system clipboard),/screenshot(primary display), or/image <path>(file on disk) inside thechatREPL, get a one-line feedback echo ([📎 clipboard 1920×1080, 234 KB — attached to next message]), and the NEXTprompt.submitships with the image attached so the vision-capable model sees it in the same turn. Parity with Claude Desktop's paste behavior — minus OS-level Ctrl+V, which terminals fundamentally don't deliver image bytes through. Spans two repos: the client half is newdesktop/src/chatAttach.ts(captureClipboardImage / captureScreenshot / readImageFile — platform-shelled like the alpha.6 clipboard handler: Windows PowerShellGet-Clipboard -Format Image+System.Drawing.Bitmap.CopyFromScreen, macOSpngpaste/screencapture -x -t png, Linux Wayland-firstwl-paste --type image/png/grimwith X11xclip/scrotfallbacks) plus new slash-command branches indesktop/src/commands/chat.ts; the server half is ONE new@method("image.attach.bytes")RPC handler on the fork'stui_gateway/server.py(Codename-11/hermes-agentbranchfeat/image-attach-bytes→ merged toaxiom) that accepts{session_id, format, bytes_base64, filename_hint?}, validates magic bytes (PNG89 50 4E 47/ JPEGFF D8 FF/ WEBPRIFF....WEBP) to prevent content-type laundering, decodes to~/.hermes/images/remote_<ts>_<rand6>.<ext>, and appends tosession["attached_images"]. The fork's existing_enrich_with_attached_imagespipeline already handles the hard part — multimodal payload plumbing, session-scoped image state, vision-model routing — so this release is almost entirely about bridging client-captured bytes to the server-side state that's been there for months. Thetuirelay channel is a transparent RPC forwarder; zero relay changes. Fallback when hermes-host hasn't been updated yet: client'simage.attach.bytesRPC call getsmethod not found, client catches it specifically and prints[attach failed: method not found — server may need axiom rollout]to stderr, REPL stays alive, user can still send text — no crash, and the exact error points the operator at the fix. Non-goals locked for this release: no Ctrl+V terminal keybinding (terminals don't pipe image bytes to stdin — that's OS-level), no Kitty/iTerm2 inline image protocols (defer to alpha.10+), no PTY shell-mode support (the remotehermesCLI has its own paste handling), no multimodalprompt.submitpayload extension (the attach-then-submit pattern is cleaner and matches the existing server state model). -
Desktop CLI alpha.6 — seamless-local dev pass. Nine features across six parallel agent workstreams delivered in one integration. Plan:
docs/plans/2026-04-23-desktop-alpha-6-seamless-local.md. (1) Workspace-awareness envelope (#1+#8) — newsrc/workspaceContext.tsdetectscwd/git_root/git_branch/git_status_summary/repo_name/hostname/platform/arch/active_shellvia parallelgit rev-parse/git status --porcelain=v1 --branchcalls under a 2 s total budget;RelayTransportauto-sends adesktop.workspaceenvelope after firstauth.ok(guarded against reconnect re-send); server-sideplugin/relay/channels/desktop.py::DesktopChannelstashes per-ws as ephemeral session metadata. Active-editor hints (src/activeEditor.ts) poll tmux (display-message -p "#{pane_current_path}:#{pane_current_command}") or detect VSCode/Cursor via$VSCODE_IPC_HOOK_CLI+TERM_PROGRAM; dedupes envelopes so only actual changes fire. Newhermes-relay workspacesubcommand prints the context;doctoroutput gains aworkspace:block. Gated client-side by--watch-editorfor the poller; envelope itself is always-on. (2)hermes-relay updateself-update (#2) — newsrc/updater.ts+src/commands/update.ts. Polls GitHub Releases API (the same prerelease-aware resolver the installer uses), semver-compares toVERSION, downloads asset with SHA256 verification, and atomic-swaps on POSIX (fs.rename— running process's inode stays live so the daemon keeps running; next invocation picks up new binary). Windows can't replace a running.exe, so the updater writes to<bin>.new.exeandfinalizePendingUpdate()runs at the top ofmain()on every subsequent invocation to rename it into place.--checkdry-runs;--yesskips confirm;--jsonemits machine-readable status. (3+4) Editor tool + interactive patch approval (#3+#4) — newsrc/tools/handlers/editor.tsfordesktop_open_in_editor(path, line?, col?, wait?)with launcher detection ($VISUAL→$EDITOR→PATH probe forcode/cursor/subl/nvim/vim→platform fallback);-ginjection for GUI editors supports:line:col.desktop_patchnow routes throughsrc/tools/patchApproval.tsin interactive mode — renders unified diff with ANSI (green/red/cyan, NO_COLOR/isTTY aware), promptsy/n/e/rvia readline on stderr;eopens the patch in$EDITORand re-reads on close. Non-interactive modes (daemon, piped stdin) auto-reject with structured reason; never auto-accepts. Router (src/tools/router.ts) carries aninteractiveflag set at construct time (stdin.isTTY && HERMES_RELAY_DAEMON !== '1'). (5) Conversation picker on connect (#5) — newsrc/sessionPicker.tscalls tui_gateway'ssession.listJSON-RPC (same RPC upstream Ink TUI uses), renders a numbered list with human-readable age + first-prompt preview.shell.tsinjects after banner / before PTY attach, appending--resume '<id>'to the hermes exec when a session is picked.chat.tsinjects before the chat loop.--session <id>(chat: legacy alias for--conversation; shell: tmux session name — distinct),--conversation <id>and--newbypass the picker. Graceful degradation: 404 / "method not found" returns empty list silently, picker falls through to'new'. (9+12) Clipboard + screenshot handlers (#9+#12) —src/tools/handlers/clipboard.tsand.../screenshot.ts. Clipboard: Windowspowershell Get-Clipboard -Raw/$input | Set-Clipboard(strips trailing CRLF); macOSpbpaste/pbcopy; Linux Wayland-first (wl-paste/wl-copyvia$WAYLAND_DISPLAY), xclip fallback. 5 s timeout, 10 MB cap both directions. Screenshot: Windows writes a temp.ps1usingSystem.Drawing.Bitmap.CopyFromScreen(honors multi-monitor viaScreen.AllScreens[display]); macOSscreencapture -x -t png; Linuxgrim→scrot→importfallback chain.save_tokeeps the file; otherwise base64 + tempfile delete. 10 s timeout, 50 MB cap. All three wired intoshell.ts/chat.ts/daemon.tsrouter handler map (9 handlers advertised now, up from 5). (13)hermesalias (#13) —install.shcreates a POSIX symlink~/.hermes/bin/hermes → hermes-relay;install.ps1drops a universal.cmdshim (no admin required — avoids Windows symlink Developer-Mode requirement). Collision-safe: only creates if nothing else lives at that name. Uninstall scripts remove the alias only when it points at our binary (preserves an unrelated upstream hermes-agent install). -
Dev-iteration additions.
npm run smokeexpanded from 4 to 5 assertions (addedworkspace); still runs locally in ~1 s post-build. CI workflow already runs the equivalent 5-command smoke on the Linux binary before publishing.
Fixed
- desktop CLI binary was a no-op on alpha.3 — installed cleanly, exited 0, produced zero stdout/stderr, wasn't "recognized" as a CLI. Root cause: cli.ts guarded its entry-point invocation with
fileURLToPath(import.meta.url) === process.argv[1], which is a valid Node idiom but fails in Bun-compiled binaries because the entry module has a synthetic URL that doesn't match the.exepath — the check evaluated false,main()was never called, binary exited 0 silently. Replaced withimport.meta.main(cross-runtime: Bun, Node 20.11+, tsx) which is true in the entry module regardless of compile mode. All four invocation paths stay correct (Bun --compile binary,bin/hermes-relay.jsshim,tsx src/cli.ts, test imports). Caught by adding a localnpm run smoketarget that runs the compiled Windows binary against--version/--help/doctorand verifies each produces output. Same smoke runs inrelease-cli.ymlon the Linux target so future regressions of this class are caught pre-publish. Affectsdesktop-v0.3.0-alpha.3; fix ships asdesktop-v0.3.0-alpha.4. hermes-relay --versionprinted0.0.0in compiled binaries.readVersion()tried to readpackage.jsonvia__dirname + '../package.json', which doesn't resolve in a Bun--compilebinary (no real filesystem layout). Replaced with a build-time-generatedsrc/version.tsmodule (npm run gen:versionwrites the version from package.json before every build and everybuild:bin:*).readVersion()now just returns the embedded constant. Works identically in tsx / Node / Bun.- desktop CLI binary segfaulted at startup on Bun 1.3.13 Windows x64 (
panic(main thread): Segmentation fault at address 0x100000D9C). Root cause identified as Bun's experimental--bytecodeflag; attempted fix in alpha.2 only editeddesktop/package.json's build scripts while the release workflow's inlinebun buildcommands silently kept--bytecode, so alpha.2 shipped with the same crash. alpha.3 fixes the workflow two ways: (1) dropped--bytecodefrom the CLI release workflow, and (2) refactored the four build steps to delegate tonpm run build:bin:*so the package.json scripts are the single source of truth for compile flags. Added abun --versiondiagnostic step to the workflow for future triage. Versions affected:desktop-v0.3.0-alpha.1anddesktop-v0.3.0-alpha.2. Fix ships asdesktop-v0.3.0-alpha.3. - Installer couldn't find alpha-only releases. GitHub's
/releases/latest/download/URL deliberately skips prereleases, so the defaultcurl | sh/irm | iexone-liner failed against alpha.1 with "maybe no Windows release for this version yet?" Bothinstall.shandinstall.ps1now query the Releases API directly (GET /repos/.../releases, filter todesktop-v*tags, take first) whenHERMES_RELAY_VERSION=latest. Pinned versions unchanged.
Added
-
Pre-release hardening: uninstall, doctor, first-run prompts, version-aware install. Four parallel workstreams that close the "feels like a dev preview" gap before tagging
desktop-v0.3.0-alpha.1. (1) Uninstall scripts — newdesktop/scripts/uninstall.{sh,ps1}matching install one-liners, 3-tier: default--binary-only(removes binary + PATH entry, preserves~/.hermes/remote-sessions.json),--purge(also wipes the shared session store with a loud cross-surface warning about Ink TUI + Android tooling dependencies),--service(stub for when daemon service installers ship — prints canonical systemd/launchd/sc.exe paths without acting). iex-pipe safety: Windows falls back toHERMES_RELAY_UNINSTALL_{PURGE,SERVICE}env vars since$argsdrops throughirm | iex. Shell rc files deliberately untouched (mirrors install.sh philosophy). (2)hermes-relay doctorsubcommand — local-only diagnostic report (225 lines,src/commands/doctor.ts); human format uses!!prefix for warnings + hint line at bottom,--jsonfor support-paste / scripts. Fields: version / binary_path / install_dir / on_path / sessions file + size + count + summaries (no tokens — total omission, not even prefix) / daemon detection (stat of canonical service unit file paths) / platform + node version. Case-insensitive PATH comparison on Windows. (3) Interactive first-run fallback — newsrc/relayUrlPrompt.ts(~180 lines) withpromptForRelayUrl()(readline on stderr,^wss?:\/\/\S+$validation, 3 retries) andresolveFirstRunUrl()(auto-picks single stored session, numbered picker for multiple, first-run banner for zero). Wired intoconnectAndAuthinshell.ts/chat.ts/tools.tsandresolvePairTargetinpair.ts, replacing the hardNo relay URLerror. Fresh-install UX: barehermes-relaynow printsWelcome to hermes-relay. No stored sessions yet — let's pair with a Server.→ URL prompt → pairing code prompt → drops into shell.--non-interactivestill fails fast. Daemon command deliberately untouched — headless binaries must never prompt; fails closed on missing credentials/consent as before. (4) Version-aware install —install.{sh,ps1}now read$target --versionbefore download and print one ofupgrading X → Y,reinstalling X,will replace (could not read version), orinstalling fresh(no prior install); post-install readback re-invokes the new binary to confirm. Pinned-version mismatches (HERMES_RELAY_VERSION=desktop-v0.3.0-alpha.1) print a non-fatal WARN rather than failing (pre-release version-name drift is expected). 5s timeout on the version call (wheretimeout(1)available); all diagnostic failures fall through to the "could not read version" path. Cross-version normalizer stripsdesktop-v/vprefix +-alpha.N/-beta.N/-rc.Nsuffix for matching. All structural flow (SHA256 verify, tmp cleanup, PATH injection, quarantine note) preserved additively. Type-check + build green; live smoke:doctorboth modes,daemonfails-closed without credentials, help text includes all new surfaces. -
hermes-relay daemon— headless WSS + tool router, lifts the "tools only work while a shell is open" ceiling. Newdesktop/src/commands/daemon.tssubcommand that opens a persistent relay connection and attachesDesktopToolRouterwithout a TTY. The agent can now reach the user's machine any time of day — first step toward "feels-local" parity. Fails closed on missing credentials (no stored session + no--token→ exits 1) and on missing consent (notoolsConsented: trueon the stored record → exits 1 unless--allow-toolsis passed alongside an explicit--token); a headless binary must never be the thing that first grants tool access. InheritsRelayTransport's reconnect state machine as-is — exp backoff 1s → 30s (5min on 429), reconnect listeners persistent across close/reconnect cycles becausechannelListenersis a Map on the transport (not wiped on socket close), so the router'sattach()fires exactly once. Structured logging defaults to JSON-line on stderr (parseable by journald / log shippers / jq), auto-switches to human-readable when stderr is a TTY, or force either with--log-json/--log-human. Lifecycle events:starting→authed(includesserver_version,transport) →ready(withadvertised_toolslist) →reconnecting(attempt + delay_ms) /reconnected→shutdownon SIGTERM/SIGINT/SIGHUP →transport_exitedwhen the transport exhausts reconnects (exits 1 so the service manager restarts fresh). Live smoke againstws://192.168.1.100:8767:starting→authed(server 0.6.0) →ready(5 tools advertised) in ~120ms. New BOOLEAN_FLAGS entries:log-human,log-json,allow-tools. Service installers for Windowssc.exe/ systemd user unit / macOS launchd plist are the obvious follow-up; the daemon binary is runnable standalone today viahermes-relay daemon --remote <url>. -
Desktop CLI v0.2 — PTY shell, local tool routing, multi-endpoint pairing, reconnect + TOFU, devices, contextual banner. The
@hermes-relay/clipackage atdesktop/grew from a chat-only scripting surface into a full Hermes-experience thin client. Barehermes-relaynow drops intoshellmode (interactive PTY pipe through the existing relayterminalchannel →tmux new-session -A+ post-attachexec hermes→ the full localhermesbanner/skin/session id verbatim, zero server changes).Ctrl+A .detaches preserving tmux;Ctrl+A kdestroys it. Newdevicessubcommand drives the relay'sGET/DELETE/PATCH /sessionsHTTP endpoints for listing, revoking, and extending server-side paired-device tokens. Status now surfacesgrants:(per-channel expiry) andexpires:(session TTL) pulled from theauth.okhandshake the transport already received —RemoteSessionRecordgainedgrants,ttlExpiresAt,endpointRole,toolsConsented(additive, back-compat preserved via aSaveSessionOptions | string | nulloverload onsaveSession). Contextual connect banner (Connected via LAN (plain) — server 0.6.0) replaces the flatConnected (server X)line acrosschat+shell. Multi-endpoint pairing (ADR 24):--pair-qr <payload>/HERMES_RELAY_PAIR_QRaccepts a full v3 QR payload (compact JSON or base64), decodes theendpoints[]array, probes each candidate with strict-priority-within-tier racing (Promise.any+AbortSignal.any, 4 s per-candidate timeout, 60 s reachability cache), and auto-selects the first reachable — role propagates into the banner + stored record. Reconnect-on-drop:RelayTransportgained aReconnectStatemachine (idle|connecting|connected|reconnecting), exponential backoff (1 s → 30 s, 5 min on 429),reconnectGatere-checked both at schedule time and post-backoff (matches Android's mid-sleep purge-race lesson),'reconnecting'+'reconnected'events, and bufferedEvents-cleared-on-reconnect. TOFU cert pinning: TLS probe runs before the WebSocket opens onwss://, extracts peer-cert SPKI sha256 (sha256/<base64>, OkHttp-compatible), compares against the stored pin or captures it first-time; mismatches error out with a human-readable "re-pair to reset" pointer. Client-side tool routing (Phase B): newdesktoprelay channel on the server (plugin/relay/channels/desktop.py+plugin/tools/desktop_tool.pyregisteringdesktop_read_file/desktop_write_file/desktop_terminal/desktop_search_files/desktop_patch) forwards tool calls from Hermes to the connected Node CLI; client-sideDesktopToolRouterdispatches to in-process handlers (fs,terminal,search) under a 30 s AbortController, 30 s heartbeat advertising the tool names. Gated behind a one-time per-URL consent prompt (toolsConsentedon the session record) +--no-toolskill-switch; non-TTY stdin fails closed. New files on the client:src/banner.ts,src/endpoint.ts,src/pairingQr.ts,src/certPin.ts,src/commands/devices.ts,src/tools/router.ts,src/tools/consent.ts,src/tools/handlers/{fs,terminal,search}.ts. New files on the server:plugin/relay/channels/desktop.py,plugin/tools/desktop_tool.py,docs/relay-protocol.md §3.5. Still zero runtime deps on the client (Node ≥21 globalWebSocket+fetch+tls.connect+node:cryptoX509Certificate +AbortSignal.any). Build clean; live smoke passed forstatus/tools/devices; interactiveshell+ tool-call smoke pending user walk-through. Delivered as four parallel implementation agents (multi-endpoint, reconnect+TOFU, server-side desktop, client-side tool handlers) + one synthesis-and-integration pass; theconnectAndAuth → {relay, url, endpointRole}return-shape refactor inchat.ts/shell.ts/tools.tsunifies how--pair-qr's winning-endpoint URL overrides--remoteacross every subcommand. -
Desktop thin-client CLI (
@hermes-relay/cli) v0.1 underdesktop/. Node ≥21 package — installable vianpm install -g @hermes-relay/cli,npx @hermes-relay/cli, or the newscripts/install.sh/install.ps1curl+iwr one-liners. Onehermes-relaybinary with four subcommands:chat(REPL + one-shot + piped-stdin, default),pair(one-time handshake → persists session token),status(local read of~/.hermes/remote-sessions.json),tools(tools.listRPC → enabled/available toolsets on the server). Credential precedence matches the Ink TUI exactly:--token→HERMES_RELAY_TOKEN→--code→HERMES_RELAY_CODE→ stored session → interactive readline prompt. Reuses the same~/.hermes/remote-sessions.jsonstore as the TUI, so a user paired via either surface sees the other work with no re-pair. Zero server changes: the CLI consumes the existing relaytuiWSS channel +tui_gatewaysubprocess events (message.delta,tool.start/complete,thinking.delta,status.update,error,approval.request, …) and renders them as plain lines to stdout, with decorated tool arrows on stderr. Flags:--remote <url>,--code <CODE>,--token <TOKEN>,--session <id>,--json(event-per-line forjq),--verbose,--quiet,--no-color,--non-interactive,--reveal-tokens(opt-in full-token output onstatus --json— default redacts). Transport, gateway types, session storage, graceful-exit, and rpc helpers are vendored verbatim fromhermes-agent-tui-smoke/ui-tui/src/(feat/tui-transport-pluggable) with a header note; the CLI and TUI stay in lockstep on the envelope protocol (docs/relay-protocol.md §3.7) until the shared surface can be lifted into a@hermes-relay/corepackage post-stabilization. SIGINT during a turn callssession.interruptvia a per-turn{ promise, cancel }handle — the REPL's cancellation state lives and dies with the turn so a late-arrivingerrorevent for a cancelled turn can't be misread by the next turn's handler. Smoke-tested end-to-end againstws://192.168.1.100:8767(hermes-relay 0.6.0, hermes-agent 0.10.0): connect/auth/session.create/prompt.submit/tools.list/--json/piped-stdin all clean. Not yet wired: interactive approval/clarify/sudo/secret request response (renderer logs a warning; out of scope for v0.1). Upstream PR candidate once the sibling Ink TUI stabilizes — seedesktop/README.mdand vaultDesktop Client.mdfor the broader thin-client roadmap.
Changed
- Transport Security badge is now role-aware — "Plain (on LAN)" instead of "Insecure (network unknown)". The previous badge derived its label from
PairingPreferences.insecureReason, which only got populated when the user toggled "Allow insecure connections" ON via the Ack dialog and picked a reason. If a user paired directly from a plain-ws://LAN QR, they never had to toggle that flag — the connection was alreadyws://— so the reason stayed blank and the badge degraded to the alarming"Insecure (network unknown)"even though the multi-endpoint resolver was trackingactiveEndpointRole = "lan"in real time. Fix:insecureReasonLabelnow accepts an optionalactiveRole: String?and prefers the live role over the stored ack reason (Plain (on LAN)/Plain (on Tailscale)/Plain (on public URL)). Neutral fallback when both role and reason are unknown is"Plain (no TLS)"— matches the new "Plain / Secure" vocabulary, drops the scary "Insecure" adjective. Binary-booleanTransportSecurityBadge(isSecure, reason, ...)overload gains an optionalactiveRoleparam with defaultnullso existing call sites compile unchanged.ConnectionViewModel.applyPairingPayloadauto-stampsPairingPreferences.insecureReasonat pair time based on the selected endpoint's role (lan→lan_only,tailscale→tailscale_vpn,public/unknown → leave blank so the user thinks); clears any stale reason when upgrading to a secure endpoint. Only overwrites blank values — never clobbers a user-selected reason. Two user-visible "Insecure" strings inside the Advanced section's insecure-toggle subsection also rewritten to "Plain" for consistency ("Plain connection — traffic is not encrypted","Allow plain (unencrypted) connections").
Added
-
Bridge destructive-verb "Don't ask again" per verb.
BridgeSafetyManagernow consults a newtrustedDestructiveVerbs: Flow<Set<String>>inBridgeSafetyPreferencesand short-circuits the confirmation overlay when the incoming verb is in the set (logging the auto-approval to the activity log so the trail is preserved). TheDestructiveVerbConfirmDialoggets aDon't ask again for "{verb}"checkbox — off on every dialog open, so the user has to actively opt in per-action. Deny path never persists trust (denying a command is not consent). Kill-switch precedence is preserved and strictly ordered: master-disable wins over blocklist wins over per-verb trust. A trusted verb in a blocklisted app still 403s.BridgeScreensurfaces aTrusted actions · N actions bypass confirmationrow with aResetbutton under the existing safety section so a user who changes their mind can find the escape hatch without deep-linking to developer options. Addresses the confirmation-fatigue trap where approvingsend_sms50 times trains the user to click through without reading the 51st. -
AllInsecure pairing — one-time acknowledgment gate. When every endpoint in a scanned QR is plain
ws:///http://(no secure sibling to fall back to),ConnectionWizard.ConfirmStepnow renders an"I understand this pairing sends traffic in plain text — visible to anyone on the network."checkbox that gates the Pair button. Per-install via newPairingPreferences.allInsecurePairAckSeen— once the user has acknowledged it, subsequent AllInsecure pairs pair one-tap. Mixed and AllSecure pairings are ungated (the amber "Mixed — secure fallback available" warning on Mixed is sufficient because the secure route exists). Matches theInsecureConnectionAckDialogprecedent of per-install Tier-1 consent and complements the UX pass's explicit "subtle warning for Tier-2, forced confirm for Tier-1 absolute boundaries" philosophy documented in DEVLOG 2026-04-22.
Changed
- Connection UX self-narration pass — Route / Relay sessions vocabulary + section headers + per-route security chips. Three linked problems shipped as one commit: (1) pairing step 2 read as "you're stuck with insecure" for any multi-endpoint QR with LAN first, because the security badge + warning card were both computed from
endpoints[0]alone — never acknowledging a secure Tailscale fallback in the same list; (2) the post-refactor active card had the right structure but no narration — sections stacked without headers, no captions explaining what Routes / Advanced / Security are for, Advanced surfaced manual URLs with no "most people don't need this" framing; (3) "Paired Devices" sounded like Bluetooth to anyone outside the project — the actual concept is server-side relay sessions with per-channel grants. Fix: introduce a shared vocabulary (Route for network path, Active/Fallback for state, Secure/Plain for transport, Relay sessions for server records) used consistently acrossConnectionWizard.ktConfirmStep,ActiveConnectionSections.kt(all three body sections),EndpointsCard.kt, andPairedDevicesScreen.kt. NewTransportSecurityStatetri-state (AllSecure/Mixed/AllInsecure) drives a context-aware pairing badge — the Mixed case now reads "LAN is plain ws:// — fine at home or the office, not on public Wi-Fi. Tailscale is encrypted (wss://) and the app uses it automatically when LAN is unreachable. You're safe on any network." — so users see they have a secure fallback without needing to understand the candidate-list mental model. Active card gains four labelMedium section headers (Connection health / Routes (N) / Advanced / Security) each with a one-line bodySmall caption above the section body. Endpoint rows in both surfaces carry per-row Secure/Plain chips (green 🔒 / amber 🔓, not scary red) so each route's security is visible at a glance; ordinal labels are humanized (1st choice/Fallback/Fallback 2on pairing step 2;Active/Fallbackon the active card — different framings because pre-connection the commitment is ordinal and post-connection what matters is state).PairedDevicesKotlin identifier and deep-link route string stay — only the user-visible labels change — so nav deep links are unaffected. New intro paragraph on the Relay sessions screen explains that rows are sessions (not Bluetooth pairings), and a tap-for-info icon on "Channel grants" opens a dialog explaining that chat/bridge/voice are per-feature permissions with independent expiries. Delivered as three parallelgeneral-purposeimplementation agents (one per surface, isolated file ownership) plus a post-implementationcode-reviewersweep that caught seven leftoverendpoint/Paired Devicesstrings acrossConnectionInfoSheet.kt,SessionTtlPickerDialog.kt,EndpointsCard.kt,SettingsScreen.kt, and theScreen.PairedDevicesnav title — all corrected before commit.
Fixed
- Add-Connection navigation now fires on the tap instead of waiting for placeholder persistence. Pre-fix,
RelayApp.kt'sonAddConnectionlambda awaitedbeginAddConnection().join()before callingnavController.navigate(Screen.Pair)— so three serialized DataStore writes (addConnection / persistUrls / setActiveConnection) blocked the QR scanner appearing. On a warm device this was ~15-50 ms; on a cold / flash-pressured device it spiked to 100-150 ms, a visible freeze on every FAB tap. Fix pre-allocates the placeholder UUID synchronously on the UI thread, firesnavController.navigate(Screen.Pair.route(connectionId = id, autoStart = "scan"))immediately, and runsconnectionViewModel.beginAddConnection(preAllocatedId = id)in a fire-and-forget background coroutine.ConnectionViewModel.beginAddConnectiongains an optionalpreAllocatedId: String? = nullparam — when provided, skips UUID generation, does an existence check (idempotent re-entry on double-tap / recomposition), and falls through to the existing mutex-guarded placeholder-build path. PairScreen's existing reactivecollectAsStateonconnectionStore.connections/activeConnectionIdpicks up the placeholder milliseconds later — the user is still framing the QR. Critical path drops from three DataStore writes to zero; the writes still happen, just off the critical path. Zero behavior change forpreAllocatedId == nullcallers (the legacy placeholder-reuse scan path is preserved byte-for-byte).
Added
relayReadysignal gates voice + bridge surfaces. NewConnectionViewModel.relayReady: StateFlow<Boolean>composes three inputs — WSSConnectionState.Connected,AuthState.Paired, AND non-blankrelayUrl— into a single "WSS is actually functional" truth. ChatScreen's mic button dims + Toasts "Voice mode unavailable — relay not connected" instead of launching an overlay that would immediately fail on/voice/transcribe. BridgeScreen surfaces an error-container banner at the top of the scroll region so the user doesn't enable the master toggle expecting commands to flow. Soft-gate semantics — neither surface hard-disables, matching the existing Chat-send / Terminal-Refresh patterns; BridgeScreen intentionally still lets the user pre-configure permissions and safety rails before a relay pairs. Three-input (rather than the simpler two-inputchatReadyform) because the Case-C teardown edge — last connection removed,_apiServerUrl/_relayUrlblanked — can leave a stalePairedtoken alive alongside a dead URL; without the URL check the banner would never surface in that state.
Changed
- Connection settings unified — one screen, one mental model. The pre-refactor app had two near-identically-named screens (
ConnectionSettingssingular,ConnectionsSettingsplural) reached from two different Settings-top surfaces (Active Connection quick-look card vs. "Connections" category row), each covering overlapping functionality. Everything the singular screen did — pair QR entry, manual URL config, insecure toggle, manual pairing code fallback, 3 tappable status rows — now folds inline onto the active card of the plural screen as expandable body sections. The singularConnectionSettingsscreen (1429 lines), its route, itsScreenenum entry, itsonNavigateToConnectionSettingsparam chain, and the Active Connection quick-look card on Settings have all been removed. New active-card structure: Status rows (always visible) → Endpoints expander → Advanced expander (manual URL / insecure toggle / manual pairing code) → Security posture strip (transport badge + Tailscale chip + hardware keystore badge + Paired Devices row). Non-active cards stay flat. Navigation path throughout the user docs updates fromSettings → Connection → XtoSettings → Connections → [active card] → X(or→ Advanced → X). New fileui/components/ActiveConnectionSections.kt(~650 lines) owns the active-card bodies;ui/screens/ConnectionsSettingsScreen.ktis rewritten (~580 lines) with screen-scope hoisting for info sheets + the insecure-Ack dialog soLazyColumnitem disposal can't silently dismiss them mid-scroll. Team-delivered: three parallelfeature-dev:code-exploreragents produced the full feature inventory + integration map + caller trace in under 2 minutes, which made the synthesis + implementation mechanical.
Fixed
-
Voice-exit chime firing on every Add-connection tap.
ConnectionSwitchCoordinator.switchConnectionfires thevoiceStopCallbackunconditionally at step 3 (correct for connection-to-connection switches while voice is active), butbeginAddConnectionalso routes throughswitchConnectionto bind the placeholder Connection's auth store before the pair wizard runs — andVoiceViewModel.exitVoiceMode()was playingsfxPlayer.playExit()regardless of whether voice mode was actually on. Logcat confirmed the chime on every Add-connection FAB tap. Fix adds an idempotence guard at the top ofexitVoiceMode(): early-return when_uiState.value.voiceModeis already false. Teardown is still safe to skip because every inner statement is null-guarded + try/catch-wrapped and would be a no-op on an already-stopped voice session; the only meaningful line is theplayExit()SFX, which is what we're silencing. -
500 ms freeze on every Add-connection tap.
ConnectionSwitchCoordinator.switchConnectionruns awithTimeoutOrNull(AUTH_HYDRATE_TIMEOUT_MS = 500L)block at step 10 to wait for the freshly-boundAuthManagerto flipAuthStatefromLoadingtoPaired. The comment acknowledged Add-connection is the common path and the 500 ms was meant to be "imperceptible," but on-device it wasn't — the user perceived the delay (and the voice chime masking it) on every tap. The placeholder Connection created bybeginAddConnectionhaspairedAt == nulland an empty EncryptedSharedPreferences store, soAuthStatewill NEVER reachPaired— the 500 ms is pure stall. Fix short-circuits the hydrate wait whentarget.pairedAt == null: skipwithTimeoutOrNullentirely for placeholders and log at DEBUG instead of the misleading "auth hydrate timeout" INFO. Real paired-to-paired switches still run the full hydrate wait because both sides havepairedAt != null. -
KDoc nested-comment trap in
ConnectionViewModel.relayReadydoc block. A literal/voice/*path pattern inside therelayReadyKDoc opened a nested block comment (Kotlin supports nested/* */, Java does not) whose*/then closed only the nested level — leaving the outer/**open for the remaining ~2200 lines of the file. Symptom:MainActivity.kt:67"Unresolved reference 'isReady'" plus ~50 cascading "Cannot infer type" errors acrossPairedDevicesScreen,SettingsScreen,TerminalScreen. Real errors (Missing '},Unclosed comment) were the last two lines of./gradlew compileGooglePlayDebugKotlinoutput, easy to miss. Fix was a two-character rewrite: path patterns now wrapped in backticks AND/*→/...so the glob-looking character isn't in a block-comment position. Lesson logged inDEVLOG.md2026-04-21; worth a sweep of other KDoc blocks for shell/regex-looking patterns before the next large diff. -
Orphan placeholder connections from abandoned Add-connection flows. The
beginAddConnectionpath pre-creates a placeholder Connection and switches to it before the pair wizard runs — soapplyPairingPayloadlands the token in the right auth store. Previously, cleanup of the placeholder was wired only to the explicit Cancel button and TopAppBar back arrow. System back (gesture back / predictive back) bypassed that branch, leaving the placeholder in the connection list forever. Two-part fix: (a)PairScreennow installs aBackHandlerthat routes system back through the sameonCancel→discardPlaceholderConnectionbranch the explicit back arrow uses; (b)ConnectionViewModel.initsweeps for any existing orphans (tuple:pairedAt == null && apiServerUrl.isBlank() && label == PLACEHOLDER_LABEL) on cold start and removes them — the tuple cannot be produced by any real pairing, so the sweep is safe without a dry-run. If the active connection at startup points at an orphan, the sweep switches to the first surviving real connection before deleting. Fixes the "why does my chip say 'New connection…'" symptom on devices that were affected pre-fix. -
Pair flow now auto-starts the camera on Add connection.
ConnectionWizardgains anautoStart: String?param (currently only"scan"is honored). The Add-connection FAB onConnectionsSettingsScreenpasses it so the wizard fires the camera permission launcher on first composition instead of forcing users through the Method chooser — one obvious next step, one-tap flow. Re-pair surfaces intentionally leaveautoStartnull so the full Scan / Enter code / Show code chooser stays available there. The deep-link arg is plumbed throughScreen.Pair's route (pair?connectionId=...&autoStart=...) andPairScreen's newautoStartparam; unrecognized values fall through to the default Method step so future builds can add more targets without breaking old ones.
Changed
- Top-bar connection chip → inline switcher in the Agent sheet. The app-wide
ConnectionChiprow that used to sit above every primary tab has been removed. Multi-connection switching now renders as a radio list inside the existing Agent sheet's Connection section (matching the visual pattern of the Profile and Personality sections above it), visible only when ≥2 connections are paired. Tapping a non-active connection firesswitchConnection+ a confirmation toast. Reasons: the chip duplicated the Agent sheet's Connection metadata, ate vertical space above every screen, and exposed the placeholder'sNew connection…label whenever an orphan existed (the root cause of the double-pair confusion). Dead code removed: theConnectionChipimport, theconnectionSheetVisiblestate, theConnectionSwitcherSheetrender block at the bottom ofRelayApp, and theconnectionChipVisible/activeConnectionvals.ConnectionSwitcherSheet.ktitself is kept for future programmatic callers.
Added
- Card-dispatch → server session sync (completes ADR 26). Every [HermesCardDispatch] now carries a
syncedToServeridempotency flag; on the next chat send,CardDispatchSyncBuildersynthesizes unsynced dispatches into OpenAI-formatassistant+toolpairs under a namespaced synthetic tool namehermes_card_actionand splices them into the request body alongside the existing voice-intent synthetic messages.ChatHandler.markCardDispatchesSyncedcommits the flag after the API client accepts the request — same post-handoff timing as voice intents, so a thrown request-building exception leaves both streams retryable. Guarantees the LLM sees prior card interactions ("you approved theRun shell command?card") across server restarts and reconnects, includingopen_urldispatches that never go throughsendMessage. Unit-tested underCardDispatchSyncBuilderTest(pure-function JVM tests, no Android deps). - Rich cards in chat via
CARD:{json}inline markers (ADR 26). Assistant messages can now surface structured Material 3 cards — skill results, approval prompts, link previews, calendar entries, weather — emitted as a single-lineCARD:{...}alongside prose text. Follows the same streaming-endpoint-agnostic marker recipe asMEDIA:, so it works unchanged on/v1/runs,/api/sessions/{id}/chat/stream, and/v1/chat/completions. NewHermesCarddata class (@Serializable,ignoreUnknownKeys=trueso newer agent schemas don't crash older phone builds) carriestitle/subtitle/body(markdown) /fields/actions/footer/accent(info/success/warning/danger). Built-in types:skill_result,approval_request,link_preview,calendar_event,weather; unknown types render via a generic fallback.approval_requestintentionally mirrors Slack's exec-approval pattern (Allow / Deny with primary/danger button styles) so upstream Phase B adapter parity is a translation exercise, not a data-model rethink. Action dispatch (send_textdefault,slash_command,open_url) routes throughChatViewModel.dispatchCardAction, which stamps aHermesCardDispatchon the owning message before forwarding so the card collapses into a "Chose: X" confirmation even if the side effect fails. Renderer isHermesCardBubble.kt— accent stripe + Icon + Title/Subtitle + markdown body + fields table + FlowRow of action buttons. Cards render between the assistant's prose and any attachments inMessageBubble. - CI test jobs advisory on
dev, strict onmain. Both.github/workflows/ci-android.yml(test) and.github/workflows/ci-server.yml(unit-tests) now carrycontinue-on-error: ${{ github.ref != 'refs/heads/main' && github.base_ref != 'main' }}— tests still run on every dev push/PR and surface annotations and reports, but they no longer red-gate the merge. Lint stays strict on both branches (deliberate: lint debt should still block). The release-merge PR fromdev→mainflips tests back to strict, so nothing sneaks through to a tagged release. - MorphingSphere on the docs site. New
SphereMark.vuecomponent (inuser-docs/.vitepress/theme/components/) renders a 58×34 sphere directly above the "Install in 30 seconds" block — mounted in thehome-hero-afterslot alongsideInstallSectionfor a hero → sphere → install stack. Importspreview/web/sphere.jsdirectly soMorphingSphereCore.ktremains the single source of truth across app / preview / docs. The cursor reactivity is eye-only — the sphere body stays anchored while the bright-spot gaze tracks the pointer (no canvas translate / body bounce). Gaze composition: scroll-tracking is the always-on baseline — the eye anchors to the Install section's top edge (via.install-sectionDOM query), not to the viewport center.installGap = installRect.top − viewportHis the runway until install enters view; as it shrinks below 50 % viewport-height,scrollVyramps linearly to 1, so by the time install's top crosses into the viewport the eye is already looking straight down at it. Before that runway, the eye sits forward (scrollVy = 0). Cursor-tracking is a soft overlay — inside a rectangular detection band (full viewport width × container height, linear falloff over 1.0 × container height past the top/bottom edges) the cursor's unit-vector direction crossfades into the scroll target viacursorWeight. The eye always has one coherent target — no mode switching, no fbm drift fighting the cursor at the band boundary, no eye-flip between modes. Palette retarget Idle ↔ Listening is gated oncursorWeight(0.2 / 0.5 hysteresis) so the sphere reads as calmly watching at the scroll baseline and attentive on direct hover. A tiny fbm wander (±0.07 on top of the target) keeps the eye breathing when both scroll and cursor are stationary. Fallback when the install element isn't on the page: viewport-center reference preserves the gaze-follows-scroll feel without the anchor. Pointer inputs pass through a per-frame EMA low-pass (180 ms direction / 280 ms proximity time constants) before any math runs — stops the per-event jitter frompointermove's big discrete jumps; asin/acos inputs are capped at ±0.9 so we stay off the infinite-slope end of the inverse-trig curves. Canvas is square (aspect-ratio: 1 / 1,clamp(280px, 48vw, 420px)) so the sphere fills the frame at the algorithm's natural 0.60-envelope sizing — no dead space between the phone video and the Install block. Respectsprefers-reduced-motion(zeroes the gaze blend so the eye stops tracking but the ambient animation continues), pauses drawing while scrolled off-screen viaIntersectionObserver, and resizes viaResizeObserveron the container. SSR-safe without a<ClientOnly>wrapper —sphere.jshas no side-effectful imports and all DOM access lives insideonMounted, which Vue 3 never runs on the server. SphereFramegaze-bias fields inMorphingSphereCore.kt(mirrored insphere.js). NewlightAngleBiasX,lightAngleBiasY,lightAngleBlend(all default 0f / 0) let callers aim the sphere's bright spot at a specific direction without touching the sphere body. The light-angle computation blends between the naturalt * lightSpeedX + noiserotation (blend = 0) and the caller-supplied bias (blend = 1). Defaults preserve byte-identical behavior for every existing caller — AndroidMorphingSphere.ktcomposable, the parity test, and the JS parity harness all stay green because they never set the new fields. First consumer:SphereMark.vueon the docs site, which uses the bias to make the sphere's eye track the reader's cursor without bouncing the canvas.SphereFrame.shadowStrength(mirrored insphere.js, default 0f / 0). DarkensdistBrightnesson the hemisphere facing away from the light, scaling it by(1 − shadowStrength · (1 − directionalLight))— the lit side is untouched, the shadow side dims proportionally. At 0 the legacy uniform "pearl" shading is preserved byte-for-byte. Docs-siteSphereMark.vueuses 0.6 so the eye reads clearly against the unlit half of the sphere; Android composable doesn't set it and stays on legacy shading.MorphingSphereCore.kt— pure, platform-agnostic sphere algorithm. Extracted fromMorphingSphere.ktas the single source of truth for the sphere going forward. Uses onlykotlin.math— no Android, no Compose, noPaint— so the same math can back a terminal TUI (Hermes CLI), the codename-11.dev user site, or a Compose Desktop port without visual drift between surfaces.preview/web/— zero-dep browser harness for the sphere.sphere.jsis a line-for-line JS mirror ofMorphingSphereCore.kt(Math.imul+|0for KotlinIntoverflow, floored modulo for.mod(),Math.truncfor.toInt()).index.htmlexposes live panel controls for state / voice / layout (cols, rows, fill%, aspect, char size) + aphone 9:16preset matching Compose@Preview(widthDp=360, heightDp=640). Serve viapython3 -m http.server --directory preview/web.- Runtime parity harness for the sphere.
preview/web/parity-check.mjs+ JVMMorphingSphereCoreParityTestrender the 8 Compose@Previewfixtures on both sides and emit FNV-1a 32-bit checksums. 8/8 structural checksums (over discrete(row, col, char)tuples) and 8/8 zone histograms match between JS and Kotlin; 6/8 full (color/alpha-inclusive) checksums match — the 2 voice-modulated fixtures drift at the 3rd decimal due to Float (Kotlin) vs Double (JS) precision in compound expressions, sub-perceptible. - Multi-endpoint pairing QR (ADR 24). A single pairing now carries an ordered list of endpoint candidates (
lan/tailscale/public/ operator-defined) so the same phone works seamlessly across LAN, Tailscale, and a public reverse-proxy URL. The phone picks the highest-priority reachable candidate at connect time and re-probes reachability on everyConnectivityManagernetwork change with a 30s per-candidate cache. Strict-priority semantics — reachability only breaks ties among equal priorities, never promotes a lower priority over a higher one. Newplugin/pair.pyCLI flags--mode {auto,lan,tailscale,public}(default auto) and--public-url <url>drive candidate emission. Seedocs/remote-access.md. - First-class Tailscale helper (ADR 25). New
plugin/relay/tailscale.py+hermes-relay-tailscaleCLI shim fronts the loopback-bound relay withtailscale serve --bg --https=<port>so the port is reachable over the tailnet with managed TLS + ACL-based identity. Safe to call unconditionally — no-ops with structured-dict failure when thetailscalebinary is absent.install.shgains an optional step [7/7] offering Tailscale enablement; skipped silently when the binary is missing, whenTS_DECLINE=1, or under non-interactive shells withoutTS_AUTO=1. Auto-retires when upstream PR #9295 merges. - Remote Access dashboard tab (in the dashboard plugin). Operators can enable/disable the Tailscale helper, mint multi-endpoint pairing QRs, and inspect which endpoint modes are currently active — all from the hermes-agent web UI.
- Reachability probe + network-change re-probe in the Android client.
ConnectionManager.resolveBestEndpoint()doesHEAD /healthagainst each API candidate with a 2s timeout + 30s cache;NetworkCallback.onAvailable/onLosttriggers a re-probe.RelayUiStategainsactiveEndpointRoleso the UI can render which endpoint (LAN / Tailscale / Public) is currently serving. - Opt-in terminal sessions. Fresh terminal tabs no longer auto-attach — each tab shows a centered Start session overlay and spawns the tmux-backed shell only after the user taps it. Tabs that have already been started still auto-reattach on reconnect. Removes the previous behavior of creating persistent server-side shells just by opening the Terminal tab.
terminal.killenvelope — hard-destroy a session. The relay runstmux kill-session -t <name>out-of-band before tearing down the PTY so the background shell (and any running commands) die with it. Closing a tab now opens a confirmation dialog with explicit Detach (preserve tmux session) vs Kill (destroy it) choices; the session info sheet also gains an error-tinted Kill session button.- Touch-scroll + scrollback buttons for the terminal. A vertical swipe on the terminal surface now moves xterm.js's scrollback (with a 12 px deadzone so long-press-to-select still works); the extras toolbar gains ⇑ / ⇓ / ⇲ buttons for ten-line scroll up, ten-line scroll down, and jump-to-bottom. Scrollback depth is unchanged at 10 000 lines.
- Friendly names for terminal tabs. The session info sheet now has an inline rename field that persists a cosmetic name (up to 40 chars) keyed on the wire-side
session_name. Names survive app restart and re-pair; cleared on Kill but preserved on Detach. The tab chip renders1 · buildwhen named. --prefer <role>priority override on every pair surface (hermes-pair --prefer tailscale, the/hermes-relay-pairskill, and the dashboard Remote Access tab's "Prefer role" dropdown). Open-vocab role string — promotes the named role to priority 0 with the rest renumbered in natural order. Unknown role emits a stderr warning and keeps the natural order. Case-insensitive matching; role string preserved verbatim for HMAC round-trip.- Active-endpoint chip in the Chat top bar. Compact tappable chip (e.g. "LAN" / "Tailscale" / "Public" / "Custom VPN (…)") rendered next to the ambient-mode button when the resolver has picked an endpoint. Tap jumps to the Connections screen so the user can probe / override / re-pair without leaving chat. Hidden for single-endpoint legacy pairings — the existing Settings row already spells the host out.
- Re-pair hint on single-endpoint connections. When the active connection has exactly one endpoint (legacy single-URL pair), the Connections list card shows a tertiary-container info strip suggesting "Re-pair with Mode = Auto to get LAN + Tailscale + Public in one QR" with an inline Re-pair button. Silent when zero or ≥2 endpoints are stored.
- Tailscale Funnel auto-detect for the public candidate.
plugin.relay.tailscale.funnel_url(port)probestailscale serve status --jsonforAllowFunnelflags and returns thehttps://<hostname>/URL when the relay port is funneled.plugin/pair.pybuild_endpoint_candidatescalls it as a fallback whenevermode=autoormode=publicis picked without an explicit--public-url— removes the "pin the public URL on Remote Access tab" step when Funnel is already publishing. Soft-fail on every error path; missing CLI / non-funneled port / unparseable JSON all return None.
Changed
- Install-command copy buttons stay pinned. The copy buttons on the docs home's "Install in 30 seconds" commands used to scroll out of view with long one-liners because
.install-codehad bothposition: relativeandoverflow-x: auto— the button's absolute coordinates anchored to the scrolling content box, not the visible viewport. Split into.install-code(positioning context, no overflow) wrapping a new.install-code-scroll(padding + horizontal overflow). Button now overlays the code as a proper static copy affordance. - Docs hero (mobile). VitePress's default
.image-containeris a fixed 320×320 square on mobile (designed for round illustrations) with negative margins on.imagethat overlap.main. On a 9:16 phone-frame video this caused the frame to overflow the square and the text/CTAs to sit on top of the video.custom.cssnow overrides the container toheight: autoand zeroes the negative margins below 960 px, andHeroDemo.vueswaps three breakpoint widths (280/240/200 px) for oneclamp(180px, 62vw, 280px)rule with amax-height: 70vhsafety rail so the frame can't dominate the fold on tall narrow viewports. MorphingSphere.ktis now a thin Compose renderer that delegates all math toMorphingSphereCore. Public@ComposableAPI is unchanged (same params, same defaults); call sites inVoiceModeOverlayand the chat empty state need no updates. Renderer also swapped legacyandroid.graphics.Paint+Typeface+nativeCanvas.drawTextfor Compose'srememberTextMeasurer()+drawText, dropping allandroid.graphics.*imports.- Pairing QR now carries the
hermes: 3schema when endpoints are emitted.plugin/pair.py→build_payload(endpoints=...)bumps the version only when theendpointsarray is present; pairs without endpoint candidates continue to emithermes: 2.canonicalize()inplugin/relay/qr_sign.pypreserves array order and role strings verbatim (no case/whitespace normalization) so HMAC signatures round-trip across Python / Kotlin. - Paired Devices screen renders per-endpoint rows. Each paired device now shows one row per
(device, endpoint)pair, with a styled chip per role (LAN / Tailscale / Public / Custom VPN). Settings and Paired Devices both read from the newPairingPreferencesper-device endpoint store. - Terminal session info sheet is vertically scrollable — tall phones in landscape with the new Start / Reattach / Kill action rows no longer clip the Done button.
- Connections list subtitle shows role names, not count. Active card's subtitle was "hostname • Connected • LAN • 2 endpoints" — accurate but opaque (users couldn't tell which endpoints the QR carried without expanding). Now shows "hostname • Connected • Active: LAN • LAN + Public" — role set on display, not count. Non-active cards unchanged.
- Looser resolver probe timing. Per-candidate HEAD
/healthtimeout raised from 2s → 4s and cache TTL from 30s → 60s. ADR 24's 2s was tight enough that LTE hand-off and slow hotel Wi-Fi routinely got marked unreachable spuriously; 4s preserves fast-fail-on-real-outage while surviving the flaky-network case. NetworkCallback still invalidates the cache on real network changes, so the longer cache is functionally equivalent but saves battery.
Backward compatible
- Old v1 / v2 QRs keep parsing unchanged. The Android parser's
ignoreUnknownKeys = trueplus the nullableendpointsfield means pre-v3 QRs work on new phones (the phone synthesizes a single priority-0role: lancandidate from the top-level fields, promoted torole: tailscalewhen the host matches100.64.0.0/10/.ts.net), and v3 QRs work on v0.6.x and earlier clients (they ignoreendpointsand use the top-level fields). No forced re-pair for existing installs.
Fixed
- Profile
PUTendpoints restored. The ADR 24 commit collaterally deleted ~479 lines ofhandle_profile_soul_put/handle_profile_memory_putwhile adding multi-endpoint passthrough to the pairing handlers.PUT /api/profiles/{name}/soulandPUT /api/profiles/{name}/memory/{filename}are back at their canonical positions; atomic-write semantics and loopback-or-bearer auth unchanged. - Stray terminal errors no longer poison the wrong tab. Server-level error envelopes without a
session_name(e.g. "Unknown terminal message type" from an older relay) previously fell through to the active tab and flashed an error overlay on whichever tab the user happened to be looking at. Errors without session scope now log only. - Dashboard-minted QRs now show the correct 10-minute expiry.
handle_pairing_mintwas returningexpires_at = now + 60whenever the caller didn't pin a session TTL (every dashboard mint), which conflated the pairing-code window with the future session's lifetime and made the dashboard dialog count down from ~1 minute even though the underlying code was valid for 10. Now stampsexpires_at = now + _PAIRING_CODE_TTLexplicitly — the pairing-code TTL is what the UI cares about. Session TTL continues to ride the QR payload'sttl_secondsfield for the phone's TTL picker. - PairDialog: multi-endpoint aware, Authelia-trap guardrail. The dashboard Management tab's "Pair new device" button was still minting legacy single-endpoint QRs (no
endpoints[], nomode, noprefer) while the Remote Access tab had been on the modern path for months. Swapped tomintPairingWithModewithMode+Prefer roledropdowns as primary inputs; the legacy host/port/tls fields moved under a collapsed "Advanced · API-server override" section with a warning that triggers when the typed host looks like a forward-auth-gated FQDN (the root cause of "relay pairs but phone drops config" reports: e.g.wss://hermes.example.comfronted by Authelia gets pinned into the QR's API block, relay WSS succeeds over LAN, then API probes return 401 and the wizard cleans up). Modal widened frommax-w-mdtomax-w-xlto fit the endpoints receipt without horizontal scroll. - PairDialog: proxy-fronted override now requires explicit consent. Previously the Advanced warning was purely informational — the dialog still auto-minted a QR the phone would fail to use. Now the auto-mint is gated: when the pinned host matches the proxy-fronted heuristic, the dialog pauses and shows "Mint anyway / Clear override" instead of proceeding. Consent is per-host — changing the host resets
proxyConfirmedso a new host triggers a fresh confirm step.
[0.6.0] — 2026-04-18
Added
- Pair with multiple Hermes servers and switch in one tap. A new Connection chip on the left of the Chat top bar opens a switcher sheet with a health indicator for each paired server — tap one to cancel in-flight chat, disconnect the old relay, rebind to the new server, and reload sessions + personalities + profiles. The chip is hidden automatically when you only have one Connection. Existing single-server installs migrate transparently on first launch of this version — zero re-pair, zero token migration. See
docs/decisions.md§19. - Connections management screen at Settings → Connections. Each paired server is a card with inline rename, re-pair (reuses the QR onboarding flow), revoke, and remove. Add a new Connection from the same screen. Per-connection state kept separate: sessions, memory, personalities, skills, profiles, relay URL + cert pin, voice endpoints, last-active session. Theme, bridge safety preferences, and TOFU cert-pin map stay global.
- Agent Profiles — the relay now auto-discovers upstream Hermes profiles by scanning
~/.hermes/profiles/*/(plus a synthetic "default" for the root config) and advertises them in theauth.okpayload. On chat send with a profile selected, the phone overlays the request'smodelandsystem_messagewith the profile'smodel.default+SOUL.md. Selection is ephemeral and clears on Connection switch. Gated byRELAY_PROFILE_DISCOVERY_ENABLED=1(default on) — operators can set it tofalseto keep the picker empty. Seedocs/decisions.md§21. - Consolidated agent sheet on the Chat top bar. Tap the agent name in the middle of the top bar to open a scrollable bottom sheet holding Profile selection, Personality selection, and session info + analytics (message count, tokens in/out, avg TTFT). Replaces the separate top-bar chips from intermediate v0.5.x builds. Toast confirmations fire on Profile and Personality switches.
- "Active agent" card at the top of Settings — summarizes the current Connection / Profile / Personality. Tap navigates to Chat with the agent sheet auto-opened via the
openAgentSheetnav arg, giving Settings-originating users a one-tap path to change agent context. - Three-layer agent model formalized: Connection (server) → Profile (agent directory) → Personality (system-prompt preset). Documented in
docs/spec.md,docs/decisions.md§8 / §19 / §21, anduser-docs/features/{connections,profiles,personalities}.md. - Pair wizard URL scheme cross-validation — an inline hint fires when the API field is given a
wss://URL (or any obviously-wrong scheme), so misplaced values surface before the pair attempt instead of after. - Pair-stamp on the active Connection — successful auth now stamps the active Connection's pairing metadata (paired-at, transport hint, expiry) in place, so a re-pair from Settings doesn't leave stale state on the card.
- Live WSS state on the active Connection row in the Connections list — the active card now reflects Connected / Reconnecting… / Stale in real time instead of a static "Paired N minutes ago" timestamp. A Stale state also surfaces an inline Reconnect action button (promoted above Rename) tinted to signal "attention."
- Reconnect taps get explicit feedback. Every Stale-recovery affordance (the Relay row, the Reconnect button in Connection Settings, and the new Reconnect action in the Connections list) now shows a snackbar / toast "Reconnecting to relay…" so users know the tap registered even during the sub-second before the row flips to Connecting.
Changed
- Unified relay status across screens.
SettingsScreen,ConnectionSettingsScreen, and the Connections list used to resolve relay status independently (each with its own ad-hoc stale / auto-reconnect / probing combinator), which let them disagree on what state the relay was in — e.g. the Settings card said Disconnected red while the Connection sub-screen said Reconnecting… amber for the same moment. State resolution now lives onConnectionViewModel.relayUiState: StateFlow<RelayUiState>with five well-defined cases (NotConfigured/Connected/Connecting/Stale/Disconnected) and a 5 s grace window before a Paired-but-Disconnected pose is promoted toStale— every screen maps the single source of truth onto the existingConnectionStatusRowAPI. - Settings "Connection" card → "Active Connection". Title renamed, and the current Connection's label now renders as the card subtitle so installs with multiple servers can see at a glance which one the status rows describe. Fresh
reconnectIfStale()tick on first compose so the Relay row doesn't flash red before the lifecycle observer's resume path lands. - Status-badge UX polish.
ConnectionStatusBadgetop-aligns cleanly on multi-line rows (was vertically centered and drifted off-center when the label wrapped). The Settings screen now treats a paired Connection with a briefly-down relay as Connecting (amber) instead of Disconnected (red) — avoids scare-red during the few seconds around a relay restart. - Top-bar chip layout.
ProfilePicker.ktandPersonalityPicker.ktas standalone top-bar chips are gone; their selection now lives inside the consolidated agent sheet.
Fixed
POST /pairing/mintemits the correct wire format. Dashboard-minted QRs were unscannable — the relay endpoint put the freshly-minted pairing code in top-levelkeyand defaulted the top-level port to the relay's own8767(itsserver.config.port) instead of the Hermes API server's8642. The Android scanner reads top-levelhost:portas the API server URL and expects the minted code insiderelay.code, so phones sawserverUrl=http://host:8767(wrong port, no API reachable) and an emptyrelayblock —applyServerIssuedCodeAndResetbailed on the empty code and the WSS never handshook. Silent fail. Thehermes-pairCLI and/hermes-relay-pairskill were unaffected because they go throughpair.py's CLI path which builds the payload correctly; only the dashboard's "Pair new device" flow hit the bug.handle_pairing_mintnow mirrorspair.py:762— top-levelhost/port/key/tlsdefault fromRelayConfig.webapi_url(resolved to a LAN-routable IP via_resolve_lan_ip) withhost/port/tls/api_keybody overrides, and therelayblock carriesurlfrom_relay_lan_base_url(server.config.host, server.config.port, ...)plus the mintedcode. Shape now matchesdocs/spec.md§3.3.1 andQrPairingScanner.kt. Regression test atplugin/tests/test_pairing_mint_schema.py(8 cases) pins the payload shape against what the Android parser expects so the two sides can't drift silently again.- Dashboard Relay Management tab no longer crashes on paired-session list.
RelayManagement.jsx:172wrapped a dict-shapeds.grants({chat, terminal, bridge}) in a 1-element array and rendered each entry as a React child, tripping minified React error #31 ("objects are not valid as a React child"). Now usesObject.keys(s.grants)when the value is dict-shaped so Badge children are always strings; existing array path preserved for future callers. Rebuilt bundle atplugin/dashboard/dist/index.js— the hermes-agent dashboard loads that file verbatim so source changes require a rebuild.
Deferred
- True per-profile isolation on a single Connection (memory + sessions +
.envshared today; use separate Connections for full isolation). - Persisted Profile selection per Connection across app restarts.
- Gateway-running probe (hermes-desktop-inspired) on the Connection health indicator.
[0.5.x] — Unreleased feature work
Added — Voice silence auto-stop (2026-04-18)
- Silence-based auto-stop for Listening turns.
VoiceViewModel.startListening()now arms asilenceWatchdogJobthat pollsVoiceRecorder.amplitudeevery 150 ms and callsstopListening()after the user's configuredsilenceThresholdMsof continuous silence following at least one above-floor frame. Uses the existingRESUME_SILENCE_THRESHOLD = 0.08ffloor (already tuned to reject mic hiss / room tone while catching whispered speech). Cancelled on manual stop,interruptSpeaking, andonCleared. Skipped inInteractionMode.HoldToTalk— the physical release is the authoritative stop there. Closes the previously-deadVoiceSettings.silenceThresholdMspreference, which was persisted- exposed via a Settings slider but never consumed by any code path.
Fixed — Bootstrap crash when wrapping command middleware (2026-04-18)
hermes_relay_bootstrap/_command_middleware.py—maybe_install_middleware()was replacingapp._middlewares(an aiohttpFrozenList) with a plain tuple via(*existing, middleware). WhenAppRunner.setup()later calledapp._middlewares.freeze(), tuples have no.freeze()method and the gateway crashed on startup with'tuple' object has no attribute 'freeze'. Switched to in-placeapp._middlewares.append(middleware)— the FrozenList is still mutable at middleware-install time. 31/31 tests intest_command_middleware.pypass.
Added — Dashboard plugin
- Hermes-agent dashboard plugin at
plugin/dashboard/— surfaces relay state in the gateway's web UI via four tabs. Relay Management lists paired devices + health + Server version; Bridge Activity renders the in-memory ring buffer of recent bridge commands (method / path / decision, with safety-railexecuted/blocked/confirmed/timeout/errorfilters); Push Console ships as a stub with an "FCM not configured" banner until FCM lands; Media Inspector lists activeMediaRegistrytokens with live TTL countdowns and basename-only file names (absolute paths never leave the server). Frontend is a pre-built React IIFE atplugin/dashboard/dist/index.js(~16 KB) loaded verbatim by the dashboard shell; backend is a thin FastAPI proxy atplugin/dashboard/plugin_api.pymounted at/api/plugins/hermes-relay/*. - Three new loopback-only relay routes feeding the plugin —
GET /bridge/activity(ring buffer;?limit=N, max 500),GET /media/inspect(token list;?include_expired=trueto include evicted entries), andGET /relay/info(aggregate{version, uptime_seconds, session_count, paired_device_count, pending_commands, media_entry_count, health}). Plus a loopback-exempt branch on the existingGET /sessionsso the plugin proxy doesn't need to mint a bearer. BridgeCommandRecordring buffer onBridgeHandler(deque(maxlen=100)) — recordsrequest_id,method,path, redactedparams,sent_at,response_status,result_summary,error, anddecision. Commit777a06awires append/update intohandle_command()/handle_response()without changing external behaviour; timeouts flipdecision=timeout, phone-side safety denials flipblocked. Params are redacted for keys in{password, token, secret, otp, bearer}.MediaRegistry.list_all(include_expired=False)— lock-guarded snapshot method returning{token, file_name, content_type, size, created_at, expires_at, last_accessed, is_expired}dicts sorted newest-first. Absolute paths are never included. Commit2212fbc.- Pairing workflow from the dashboard — new
POST /pairing/mintrelay route (loopback-only) generates a random 6-char A-Z/0-9 code, registers it with the existingPairingManager, and returns the signed QR payload built viaplugin.pair.build_payload. The dashboard backend exposes it atPOST /api/plugins/hermes-relay/pairing. A new PairDialog in the Management tab renders the QR (via theqrcodenpm lib bundled into the IIFE), shows the code + expiry countdown, and lets the operator override Host / Port / TLS in the QR payload — useful for Traefik-fronted deploys where the phone needswss://relay.example.com:443even when the dashboard itself is served at a different hostname. Settings persist per-browser in localStorage. - Functional session revocation — loopback-exempt branch on
DELETE /sessions/{token_prefix}plus a proxy route atDELETE /api/plugins/hermes-relay/sessions/{prefix}. The Revoke button on the Management tab now confirms via native dialog, calls the proxy, and auto-reloads the session list on success.
Added — Installer
--dashboard-plugin=yes|noflag oninstall.sh(defaultyes; also viaHERMES_RELAY_DASHBOARD_PLUGINenv var). Passingnorenamesplugin/dashboard/manifest.json→manifest.json.disabledso the hermes-agent dashboard loader skips the plugin entirely. Re-running with the opposite flag flips it back — no config lives anywhere else.- Live dashboard rescan in both
install.shanduninstall.sh— parseshermes-dashboard.serviceExecStart for--host/--portand GETs/api/dashboard/plugins/rescan, falling back to loopback and common ports. The relay tab appears/disappears without a dashboard restart. Silent no-op when the dashboard isn't running.
Fixed
- Dashboard plugin UI uses plain tab buttons instead of Radix
<Tabs>: Radix'sTabscontainer expectsTabsContentchildren (not exposed in the SDK whitelist) and its internal context blew up at first render aso is not a functionafter minification. - Install banner no longer claims "Phase 3 — Bridge channel + status tool" (stale since v0.2.x). Phase-agnostic copy now.
Added — Sideload in-app update check
- In-app update banner on the
sideloadflavor. On cold start (at most once every 6h) the app queries the GitHubreleases/latestendpoint and, if it's behind, shows a slimUpdateBannerat the top of the scaffold with the current and latest versions. Tap Update → opens the-sideload-release.apkasset URL directly in the browser; Android's DownloadManager fetches it and hands it to the OS installer. Tap the X to dismiss for this version — the banner reappears automatically on the next release. - "Updates" row in About → About card — manual "Check" button
with the same plumbing. After a successful check shows either
"You're on the latest release" or "Update available — v0.x.y" with
a Download CTA. The row is hidden on the
googlePlayflavor (Play Store owns update delivery there). - No new permissions — the app never installs APKs itself; it
only opens the asset URL via
ACTION_VIEW. The Android download + install path is unchanged from what sideload users already use. - Files:
update/UpdateChecker.kt,UpdatePreferences.kt,UpdateModels.kt,SemverCompare.kt;viewmodel/UpdateViewModel.kt;ui/components/UpdateBanner.kt; wire-up inui/RelayApp.kt+ui/screens/AboutScreen.kt.
Added — v0.4.1 Bridge page polish pass
UnattendedGlobalBanner— thin 28dp amber strip at the top ofRelayApp's scaffold, visible on every tab when master + unattended are both on (sideload only). Pulsing amber dot, copy "Unattended access ON — agent can wake and drive this device", chevron → navigates to the Bridge tab. Theme-aware colours (amber-on-dark in dark mode, dark-amber-on-pale-amber in light). Pairs with the existingBridgeStatusOverlayChip— banner handles the app-foregrounded case, the overlay chip handles the app-backgrounded case.PhoneSnapshotagent-awareness fields —unattendedEnabled,credentialLockDetected,screenOn.PhoneStatusPromptBuilder.buildBridgeLine()now appends explicit guidance so the LLM knows upfront whether commands will land on the device while the user is away, instead of finding out reactively viakeyguard_blockederror responses.MASTERpill next to the master-toggle title, and leading "Master switch —" subtitle copy, so the parent-gate role of the toggle is legible without reading a wall of helper text.
Changed — v0.4.1 Bridge page polish pass
- Bridge tab card order rewritten with a clear hierarchy: Master →
Permission Checklist → [Advanced divider] → Unattended Access → Safety
Summary → Activity Log. The previous standalone
BridgeStatusCardwas dropped from the layout because its device / battery / screen / current-app rows already render inline inside the master toggle card. (The component file remains in-tree and is still unit-testable; it's just not rendered byBridgeScreenany more.) - Unattended Access gated on the master toggle. The Switch inside
UnattendedAccessRowis nowenabled = masterEnabledand the subtitle reads "Requires Agent Control — enable the master switch above first." when master is off. The standaloneKeyguardDetectedChipcard was inlined as aKeyguardDetectedAlertSurface band inside the Unattended Access card so the credential-lock warning lives next to the thing that triggers it (same concern, one card). - Persistent-notification copy corrected. The unattended one-time scary dialog no longer implies the unattended toggle owns the "Hermes has device control" notification — explicitly attributes it to the master switch. The master-toggle info dialog gained a matching paragraph naming the persistent notification.
Fixed — v0.4.1 Bridge page polish pass
- Master toggle silent no-op when Accessibility Service isn't
granted. Tapping the disabled Switch used to do nothing (stock
Android disabled-switch behavior); now it surfaces a snackbar —
"Accessibility Service must be enabled first." — with an "Open
Settings" action that deep-links to
Settings.ACTION_ACCESSIBILITY_SETTINGS. - Permission checklist Optional pill wrapped on narrow titles
(e.g. "Notification Listener"). Switched the row layout to
FlowRowand forcedsoftWrap=falseon the pill's text so the pill renders as a single unbroken element. - Runtime-permission rows silently no-opped after permanent denial.
Mic / Camera / Contacts / SMS / Phone / Location rows now fall back
to
Settings.ACTION_APPLICATION_DETAILS_SETTINGSwhen the user has selected "Don't ask again", taking the user straight to the app's permission page instead of consuming the tap.
Added — v0.4.1 Bridge fast-follows (in progress)
- Tiered permission checklist on the Bridge tab — the previously-flat
4-row layout is now four explicit sections (Core bridge, Notification
companion, Voice & camera, Sideload features). Each runtime dangerous
permission gets its own row with a
RequestPermissionlauncher that re-probes status on grant. Optional rows render an "Optional" Material 3 pill so users don't perceive them as urgent. Sideload-only rows (Contacts, SMS, Phone, Location) are hidden on the googlePlay flavor via the existingBuildFlavor.isSideloadgate. - JIT permission-denied surfacing for the Tier C agent-tool wrappers
(
android_search_contacts,android_send_sms,android_call,android_location). When the phone reports a missing runtime permission, the wrapper upgrades the bridge response to a structured envelope carryingcode: "permission_denied"+permission: "android.permission.READ_CONTACTS"+ a deterministic LLM-readable explanation that names the exact Settings deep-link path. The LLM no longer has to guess from a free-text error string why the tool failed. - Voice-mode JIT chip — when a voice intent dispatch returns
permission_denied, a tappable errorContainer-coloured chip surfaces above the mic button with copy like "I need Contacts to Send SMS here. Tap to open Settings." Tapping deep-links toSettings.ACTION_APPLICATION_DETAILS_SETTINGSfor the running package's permission page. Cleared on tap or on the next mic tap. ResolveResulttyped-union inplugin/tools/resolve_result.py—Found(value)/NotFound(detail)/PermissionDenied(permission, reason)dataclass hierarchy with afrom_bridge_responseclassifier. Reads both the canonical wire keys (code/permission, v0.4.1) and the legacy aliases (error_code/required_permission, pre-v0.4.1) for forwards/backwards compatibility across the v0.4.x APK rollout.- 17 new Python unit tests in
plugin/tests/test_resolve_result.py.
Changed — v0.4.1 Bridge fast-follows (in progress)
BridgeCommandHandler.respondFromResultnow emits the canonicalcode+permissionwire keys alongside the legacyerror_code+required_permissionon permission-failure bridge responses. Existing consumers that read the legacy keys keep working unchanged.BridgePermissionStatusextended withmicrophonePermitted,cameraPermitted,contactsPermitted,smsPermitted,phonePermitted,locationPermitted.refreshPermissionStatus()probes each on everyLifecycle.Event.ON_RESUME.
Added — v0.4.1 unattended access mode (sideload-only)
Opt-in "Unattended Access" toggle on the Bridge tab that lets the
agent wake the screen and dismiss the keyguard while the user is
away from the phone. Sideload-only — the googlePlay flavor never
sees the toggle, never installs the wake lock, and never invokes
requestDismissKeyguard.
UnattendedAccessManager— sideload-only singleton holding the screen-bright wake lock and orchestrating theKeyguardManager. requestDismissKeyguardcall.acquireForAction()is invoked from the bridge command dispatcher pre-gate for any non-read-only route and returns one ofSuccess/SuccessNoKeyguardChange/KeyguardBlocked/Disabled. The wake lock usesSCREEN_BRIGHT_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP | ON_AFTER_RELEASEwith a 30 s hard timeout per acquire.- One-time scary opt-in dialog — fires the first time the user
flips the unattended toggle ON. Explains the security model
("agent can drive your phone while you're away"), the credential-
lock limitation ("Android won't let us dismiss PIN / pattern /
biometric locks"), and the three disable paths (toggle off, auto-
disable timer expiry, relay disconnect). Latched via
BridgeSafetySettings.unattendedWarningSeenso it never re-appears after dismissal. - Persistent keyguard-detected chip — when unattended is ON
and the device has
KeyguardManager.isDeviceSecure == true, an error-tinted Card on the Bridge tab warns the user that the screen will wake but stop at the lock screen. - Amber "Unattended ON" status-overlay chip — when unattended
is on, the existing
BridgeStatusOverlayChipswitches from the red-dot "Hermes active" variant to an amber-dot "Unattended ON" variant so the user (or anyone glancing at the device) can tell at a glance that the agent is permitted to wake the screen. Forced visible whenever unattended is on, even if the user has the regular status-overlay preference disabled. keyguard_blockedstructured error — when the wake fires but the keyguard refuses to dismiss, the bridge dispatch short- circuits with HTTP 423 anderror_code = "keyguard_blocked"before invoking the action. The LLM's tool wrapper sees the classification and can tell the user to change their lock screen to None / Swipe rather than blindly retrying.ActionExecutoralso classifies dispatch failures against the live keyguard state via the newclassifyGestureFailure()helper, so the sameerror_codesurfaces if a gesture fails on a locked device with unattended OFF.- Manifest:
DISABLE_KEYGUARDdeclared inapp/src/sideload/AndroidManifest.xml. WAKE_LOCK was already declared in the main manifest for the bridge gesture wake-lock scope and is reused. - Lifecycle wiring:
MainActivity.onResumeregisters the host activity forrequestDismissKeyguard,onPauseclears it. Master-toggle-off and relay-disconnect both callUnattendedAccessManager.release()so the screen-bright lock drops immediately and the screen returns to its natural timeout.
Decisions documented during implementation, not re-litigated:
- No WiFi-disconnect failsafe — rejected because Tailscale / VPN invalidates the "leaving WiFi = leaving LAN" assumption. Rely on existing relay-disconnect detection plus auto-disable timer.
- Default auto-disable timer stays as-is (30 minutes). No special unattended-mode default.
- Credential lock cannot be dismissed by third-party apps — surfaced
via the one-time warning dialog, the persistent chip, and the
keyguard_blockederror code rather than worked around.
Added — Voice intent → server session sync (v0.4.1 fast-follow)
- Voice actions now reach the server-side LLM's session memory. Previously, phone-local voice intents (
open Chrome,text Sam saying hi, etc.) ran in-process viaBridgeCommandHandler.handleLocalCommandand appended local-only trace bubbles to the chat scroll. The Hermes API server's session never learned about them, so a follow-up text question like "did that work?" hit the LLM with no context and returned hallucinated answers (per a 2026-04-14 on-device repro). - Implementation. Each phone-local voice intent now records a structured
VoiceIntentTrace(tool name, JSON args, success, JSON result envelope) on the post-dispatch chat-trace bubble it produces.VoiceIntentSyncBuilderwalks the chat history before eachPOST /v1/runs/POST /api/sessions/{id}/chat/streamcall and synthesizes OpenAI-formatassistant(withtool_calls) +tool(withtool_call_id) message pairs from any unsynced traces. The synthesized array rides under the existing payload's newmessagesfield — additive, ignored by older servers, picked up by anything OpenAI Chat Completions–shaped. Idempotency: traces flip tosyncedToServer=truethe moment the API client takes ownership of the request, so subsequent turns don't re-emit them. - Zero server changes. Frontend-only, no hermes-agent edits needed.
- Files.
data/ChatMessage.kt(newvoiceIntent: VoiceIntentTrace?field),voice/VoiceIntentSyncBuilder.kt(pure-function builder + helpers),network/HermesApiClient.kt(optionalvoiceIntentMessagesparameter on both stream methods),viewmodel/ChatViewModel.kt(build + sync + flag flip instartStream),viewmodel/VoiceViewModel.kt(extended dispatch callback wires the structured trace into the chat-trace bubble),voice/VoiceBridgeIntentHandler.kt(newandroidToolName+androidToolArgsJsononIntentResult.Handled), sideloadVoiceBridgeIntentHandlerImpl.ktpopulates them per intent, sideload + googlePlayVoiceBridgeIntentFactory.kttypealias updates. Tests intest/voice/VoiceIntentSyncBuilderTest.kt(12 cases — empty input, single success, failure with error_code, idempotency, chronological order, prefix gate, blank-args gate, call-id pairing, helpers) andtest/network/handlers/ChatHandlerTest.kt(4 new cases for trace storage +markVoiceIntentsSynced).
Added — Barge-in (interrupt the agent)
Voice mode can now be interrupted by speaking while the agent is replying — the same turn-taking pattern ChatGPT, Siri, and Google Assistant use. Stops the current TTS response the moment your voice is detected, flips to Listening, and hands the mic back to you without you needing to tap anything. If you then stay quiet for ~600 ms, the agent resumes from the next sentence of the response you interrupted — so a quick breath or pause won't throw away its answer.
- Duplex audio + Silero VAD. A new
BargeInListenerruns a continuousAudioRecord(16 kHz mono PCM,VOICE_COMMUNICATIONsource) alongside TTS playback, feeding 32 ms frames to a bundled Silero voice-activity-detection model viacom.github.gkonovalov: android-vad:silero.AcousticEchoCanceler+NoiseSuppressorare attached to the ExoPlayer audio session so the VAD doesn't trip on our own TTS output. A second hysteresis layer on top of the library (2–3consecutive speech frames depending on sensitivity) rejects isolated false-positive frames. - Two-stage ducking → cutoff. A single raw speech frame fires a
maybeSpeechevent → TTS volume ducks to 30 % as a soft acknowledgement (user hears the shift, knows we heard something). If hysteresis passes → hardbargeInDetected→interruptSpeaking()fires (same path V4 wired for user-initiated interrupts in the voice-quality-pass — cancels synth/play workers, deletes pending cache files). If no follow-up detection within 500 ms, a watchdog un-ducks so a single stray frame doesn't leave playback quieted. - Resume-from-next-sentence.
VoiceViewModeltracks the list of sentence chunks the play worker has spoken plus the index the user interrupted at. After an interrupt, a 600 ms watchdog listens toVoiceRecorder.amplitude— if the user keeps speaking past the threshold, the new turn proceeds normally and the interrupted response is dropped. If silence wins, remaining chunks re-enqueue onto the TTS queue and playback resumes from the sentence after the cut. Controlled by the "Resume after interruption" sub-toggle (default on). - Settings UI. New "Barge-in" section in Voice Settings: master
toggle (default off), sensitivity segmented button (
Off / Low / Default / High— inverted from the library'sModeenum so higher user-facing value = more sensitive), resume sub-toggle, and a compatibility warning badge that shows on devices whereAcousticEchoCanceler.isAvailable() == false("Your device may have limited echo cancellation. Barge-in quality will vary."). Preferences live inBargeInPreferencesDataStore following the existingBridgeSafetyPreferencesshape. - Shipped default-off. AEC quality varies widely across Android
OEMs — Pixel is solid, many mid-tier and older devices aren't. The
feature ships disabled by default; users opt in from Voice Settings
and see the compatibility badge if their device has no AEC. A
useExoPlayerVoiceflavor-safe architecture from the voice-quality- pass already exposedVoicePlayer.audioSessionId, which is what AEC binds against. - Live settings reactivity. Toggling the feature on or off
mid-conversation works without restarting voice mode; the
coordinator observes
BargeInPreferences.flowand starts/stops the listener on each emission.
Tests: 7 new VoiceViewModelBargeInTest cases covering the
interrupt path, resume-vs-keep-talking branches, the ducking
watchdog, and live prefs-change reactivity. Plus unit tests for each
new subsystem (VAD engine, duplex listener with AudioFrameSource
seam for non-instrumented tests, ducking helpers, DataStore).
Changed — Voice output quality pass
Addresses four symptom classes that surfaced in on-device voice testing
after v0.4.0: voice output switching between crisp and muffled, volume
drifting between sentences, audible pauses between chunks, and occasional
jumbled-letter spell-outs when the agent emitted markdown, URLs, or
tool-annotation tokens. Root-caused across five compounding layers and
fixed end-to-end in a single agent-team session on
feature/voice-quality-pass.
- Text sanitization, both ends. A new
plugin/relay/tts_sanitizermodule strips markdown (code fences, links, URLs, bold/italic, inline code, headers, list markers, horizontal rules), Hermes tool-annotation tokens (`💻 terminal`,`🔧 android_foo`, etc.), and a conservative standalone-emoji set before/voice/synthesizehands text to the upstreamtext_to_speech_tool. The same regex set is mirrored client-side inVoiceViewModel.sanitizeForTtsand applied per delta before the sentenceBuffer sees the text, with multi-delta code-fence deferral so unclosed fences don't leak orphaned backticks to the chunker. Kills the "jumbled letters" symptom — ElevenLabs no longer reads URLs character-by-character or speaks backtick+emoji wrappers aloud. - Coalescing chunker. The old
MIN_SENTENCE_LEN=6chunker emitted every tiny acknowledgement ("Sure.","Okay.") as its own TTS call, guaranteeing audible inter-chunk variance. NewMIN_COALESCE_LEN=40+MAX_BUFFER_LEN=400secondary-break escape merges short runs into one synthesize call, splits run-on sentences at the last comma/semicolon/em-dash inside the 400-char window, and preserves thee.g./U.S.abbreviation lookahead. An 800 ms silent-delta timer force-flushes buffered text so trailing fragments on an abrupt stream-end don't strand in the buffer. - Prefetch pipelining.
VoiceViewModel.startTtsConsumerwas previously a strictly serialsynthesize → play → awaitCompletionloop — every sentence boundary cost one full network round-trip. Now split into twosupervisorScope-rooted coroutines joined by a boundedChannel<File>(capacity=2): the synth worker runs up to one sentence ahead of the play worker, so N+1's audio is already on disk when N's playback finishes. Synth failures on N+1 no longer stall N's playback. Cancellation paths (stopVoice,interruptSpeaking,exitVoiceMode) cancel the scope and delete any unplayedvoice_tts_<ts>.mp3cache files; afinally-scoped cleanup catches any late-arriving synth results that beat the cancel signal. - Gapless ExoPlayer playback.
VoicePlayerswapped from recreating aMediaPlayerper file to a single persistent Media3 ExoPlayer +addMediaItemqueue. Appending is non-blocking;awaitCompletion()now returns when the queue is drained AND the player is idle (documented semantic change). Kills the codec-reset pop between sentences and composes naturally with the prefetcher — the play worker appends without blocking the synth worker. Visualizer attaches once against the ExoPlayer audio session (deferred to the firstonIsPlayingChanged(true)since some OEMs initialize the session id lazily) and degrades gracefully if attach fails. Ships behind aFeatureFlags.useExoPlayerVoicehook as a safety net; noMediaPlayerfallback is currently wired. - ElevenLabs model flipped to
eleven_flash_v2_5. Operator change applied to~/.hermes/config.yamlon hermes-host ahead of the code work.eleven_multilingual_v2is expressive but re-interprets prosody per call — wrong model for a chunked pipeline.eleven_flash_v2_5is the streaming-optimized model (~75 ms per-request latency, lower per-call variance, designed exactly for sentence-scale pipelines) and is net-cheaper per character. Voice id unchanged. This single flip accounts for the bulk of the perceived "clear↔muffled switching" reduction; the code units below reduce what remained.
Deferred: upstream PR exposing VoiceSettings (stability /
similarity_boost / use_speaker_boost) in
hermes-agent/tools/tts_tool.py::_generate_elevenlabs. Useful once
merged — default stability is a hair too low for consistent chunked
output — but not blocking; the flash model already solves most of what
the settings would.
Tests: 33 new relay sanitizer tests, 4 new client test files covering sanitization parity, chunking semantics, prefetch pipelining timing + cancellation cleanup, and ExoPlayer queue behavior.
[0.4.0] - 2026-04-14
Added — Bridge feature expansion (the big one)
v0.4 roughly triples the bridge surface. The agent can now do everything
v0.3 could do, plus long-press, drag, full clipboard access, system-wide
media control, raw Android Intents, an accessibility-event stream, app
launching, app listing, multi-window screen reads, filtered node search,
screen-hash change detection, stable per-node IDs, three-tier
tap_text fallback, a batched macro dispatcher, wake-lock-guarded
gesture dispatch, and a per-app skill playbook for common flows. The
sideload track additionally ships direct SMS, contact lookup, one-tap
dialing, and location awareness.
Read surface
/long_press(A1) — long-press gesture by coordinate or node ID, covering context menus, text selection, and widget rearranging/drag(A2) — drag gesture from point A → point B over a configurable duration/find_nodes(A3) — filtered accessibility-tree search (text, clickable flag, class name, resource ID) instead of returning the whole tree/describe_node(A4) — full property bag for a stable node ID, plusnodeIdwiring for/tapand/scrollso the agent can hand IDs forward without re-resolving coordinates/screen_hash+/diff_screen(A5) — cheap SHA-256 screen fingerprint and diff tools for "wait until this screen changes" loops without re-downloading the full accessibility tree/events+/events/stream(B1) — accessibility-event stream. In-memoryEventStorebuffers recentAccessibilityEventobjects so the agent can poll for UI events or wait for a specific trigger instead of hammering/screen. Toggle capture on/off via/events/stream.- Multi-window
ScreenReader(P1) —/screennow walks every accessibility window (system UI, popups, notification shade) instead of only the active app's window
Act surface
/clipboard(A6) — bidirectional system clipboard read/write/media(A7) — system-wide playback control (play, pause, next, previous, volume) viaMediaSessionManager/send_intent+/broadcast(B4) — raw Android Intent / broadcast escape hatch for apps that expose deep-link actions- Three-tier
tap_textcascade (A9) — exact match → clickable ancestor walk → substring fallback, fixes apps that wrap labels in non-clickable parents android_macro(A10) — batched workflow dispatcher runs a sequence of bridge commands as one call with configurable pacing, no round-trip per stepWakeLockManager(A8) —PARTIAL_WAKE_LOCKscope wrapper around gesture dispatch so commands still land on dim or idle screens. Scoped try/finally semantics, never a stale hold.
Tier C — sideload-only phone utilities
/location(C1) — GPS last-known-location read for "where am I?" and location-scoped commands/search_contacts(C2) — contact lookup by name → phone number for voice intents like "text Mom"/call(C3) — direct call viaACTION_CALL, with anACTION_DIALfallback where the flavor can't holdCALL_PHONE/send_sms(C4) — direct SMS send viaSmsManagerwith send-result confirmation (no dialer bounce)
Docs + skills
skills/android/SKILL.md(A11) — per-app playbook with reusable flows for common apps, agent-discoverable via the Hermes skills systemdocs/spec.md+docs/decisions.md— v0.4 bridge surface documented, Phase 3 status marked shipped, 15-item spec rot pass
Fixed
- Missing Kotlin handlers for
/open_app,/get_apps,/apps, and/setup— latent v0.3.0 regression. The Python relay side had the routes and the plugin tools were calling them, but the in-appBridgeCommandHandlerhad never wired the correspondingwhen (path) ->branches. Commands silent-dropped until this release. - Android 11+ package visibility for
/get_apps— added a<queries>element to the main manifest soPackageManager.queryIntentActivities(ACTION_MAIN + CATEGORY_LAUNCHER)returns the full launchable app list. Without this, the tool returned an empty list on modern Android targets.
Docs
user-docsexpansion — added the full 27-route bridge HTTP inventory toreference/relay-server.md, rewrotearchitecture/security.mdaround the five-stage safety gate + Tier 5 rails, added ADR-9 through ADR-13 (bridge safety gate, wake-scope, event stream, MediaProjection FGS type, build flavors), and retired all remaining "Bridge :8766" references now that the bridge is unified on:8767.
[0.3.0] - 2026-04-13
Added
Bridge channel (the big one) — the agent can now read the phone's screen, tap, type, swipe, and take screenshots. Gated behind a deliberate in-app master toggle, per-channel session grants, Android Accessibility Service permission, MediaProjection consent, and the safety rails system (blocklist, destructive-verb confirmation modal, idle auto-disable timer, optional persistent status overlay).
HermesAccessibilityService— AndroidAccessibilityServicesubclass that reads the active window's UI tree, dispatches taps / types / swipes / scrolls / key presses viaGestureDescriptionandACTION_SET_TEXT, and caches the foregrounded packageScreenCapture.kt—MediaProjection→VirtualDisplay→ImageReader→ PNG bytes, uploaded to the relay via/media/uploadBridgeCommandHandler— routes inboundbridge.commandenvelopes to the executor, with the three-stage safety check (blocklist → destructive-verb confirmation → auto-disable reschedule)BridgeSafetyManager— process-wide safety enforcement singleton with DataStore-backed blocklist (30 default banking/payments/2FA apps), destructive verb list (send/pay/delete/transfer/etc.), auto-disable timer, and confirmation timeoutBridgeForegroundService— persistent "Hermes has device control" notification with Disable + Settings actions, deep-linked to the Bridge Safety settings screenBridgeStatusOverlay—WindowManageroverlay host for the destructive-verb confirmation modal and optional floating status chip- Bridge UI — new Bridge tab with master toggle, permission checklist (accessibility / screen capture / overlay / notification listener), status card, activity log, and safety summary card
- Bridge Safety settings screen — blocklist editor (searchable package picker), destructive verb editor, auto-disable timer slider, status overlay toggle, confirmation timeout slider
- 18
android_*plugin tools routed through the new unified bridge (14 baseline + send_sms, call, search_contacts, return_to_hermes added in v0.4.0) channel (migrated from the legacy standaloneandroid_relay.py) android_navigate— vision-driven close-the-loop navigation tool (sideload track only)android_phone_status— agent-callable introspection tool that reports live bridge state (device,bridgepermissions,safetyconfig) via the new/bridge/statusrelay endpoint
Voice mode — real-time voice conversation via relay TTS/STT:
- Tap the mic in the chat bar to enter voice mode with the ASCII morphing sphere, layered-sine-wave waveform visualizer, and streaming sentence-level TTS playback
- Three interaction modes (Tap-to-talk, Hold-to-talk, Continuous)
- Sphere voice states — Listening (blue/purple), Speaking (green/teal)
- Voice settings screen (interaction mode, silence threshold, provider info, Test Voice)
- New relay endpoints —
POST /voice/transcribe,POST /voice/synthesize,GET /voice/config - Voice-to-bridge intent routing (sideload track only) — spoken commands like "text Mom saying on my way" route to the bridge channel instead of the chat channel, with destructive-verb confirmation flow
Notification companion — HermesNotificationCompanion
(NotificationListenerService) reads posted notifications and forwards
them to the relay over a new notifications channel for agent
summaries. Opt-in via the standard Android notification-access grant.
Self-setup skill — /hermes-relay-self-setup and
skills/devops/hermes-relay-self-setup/SKILL.md. Single-source agent
install recipe — raw URL fetch for pre-install users, Hermes skill
discovery for post-install users. Zero drift.
Manual pairing fallback — hermes-pair --register-code ABCD12
pre-registers an arbitrary 6-char code with the relay for the rare
"no camera available" case (SSH-only, single-device pair). Phone-side
"Manual pairing code (fallback)" card in Settings → Connection walks
the user through the three-step workflow with a real Connect button.
Per-channel grant revoke — each device on the Paired Devices screen now has tappable per-channel grant chips (chat / voice / terminal / bridge) with an inline x icon. Revoking a single channel leaves the other channels' expiries intact.
hermes-status and hermes-relay-update shell shims — alongside
hermes-pair, these three shims give full discoverable CLI coverage for
pair / status / update.
/health + /bridge/status relay endpoints — loopback-only
structured phone-status endpoint (device, bridge permissions, safety
state) that backs hermes-status, android_phone_status(), and the
/hermes-relay-status skill.
ConnectionWizard + onboarding unification — shared three-step pairing wizard (Scan → Confirm → Verify) used by both first-run onboarding and re-pair from Settings. Eliminates the "half-paired" state where onboarding configured the API side but dropped the relay block.
Lifecycle-aware health checks — ConnectionViewModel.revalidate()
fires on ON_RESUME and on ConnectivityObserver Available
transitions, with a new Probing tri-state and a new gray pulsing
ConnectionStatusBadge pose. Kills the "foreground lag flash" where
badges showed stale Connected/Disconnected for 30s after foregrounding.
Two build flavors — googlePlay (Play Store track, conservative
Accessibility use case) and sideload (.sideload applicationId
suffix, full feature set including voice-to-bridge intents and
android_navigate). sideload shows as "Hermes Dev" in the launcher
for side-by-side disambiguation.
TOFU cert pinning, Android Keystore session token storage
(StrongBox-preferred with EncryptedSharedPreferences fallback),
transport security badge, session TTL picker dialog (1d / 7d /
30d / 90d / 1y / never), Paired Devices screen with full revoke
flow, Tailscale detector, insecure-mode ack dialog with reason
picker.
Changed
install.shTUI polish — ANSI colors (TTY-aware,NO_COLORrespected), boxed banner, unicode step bullets, spinner for the long pip install, polished closing message with structured Pair / Update / Manage / Uninstall sectionsinstall.shrestart semantics — the restart-relay path now uses explicitsystemctl --user restartinstead ofenable --now(the latter is a no-op on already-active services and silently left editable-install code refreshes stranded)install.shstep 6b — offer (don't force) hermes-gateway restart so new plugin tools re-import. Interactive prompt (default no), env var opt-in viaHERMES_RELAY_RESTART_GATEWAY=1, or flag opt-out- Connection settings card rename — "Bridge pairing code" → "Manual pairing code (fallback)" with a walkthrough UX instead of a bare code display. The old label implied bridge-specific 2FA; it's actually the auth fallback for the whole handshake.
- Sideload flavor strings —
app_name→Hermes Dev,a11y_service_label→Hermes-Bridge Dev, notification companion label →Hermes Dev notification companion. Disambiguates side-by-side installs in launcher / recents / Settings → Apps. - Google Play flavor a11y label →
Hermes-Bridge(with hyphen) for consistency with the sideload naming BridgeStatusReporter— pushed envelope now includes the full nesteddevice/bridge/safetycontract instead of four flat keys, with a newpushNow()method for out-of-band emission on master toggle flipshermes-relay.servicesystemd unit — runs the relay on port 8767 with--no-ssl --log-level INFO, loads~/.hermes/.envvia_env_bootstrap.pyat import time (noEnvironmentFile=needed)
Fixed
- Android 14 MediaProjection grant evaporation — on API 34+,
getMediaProjection()returned projections the system auto-revoked within frames becauseBridgeForegroundServicewas declared asspecialUseonly. AddedmediaProjectionto the FGS type slot, updatedstartForeground()to OR both type constants, and gatedrequestScreenCapture()on the master toggle so the FGS is guaranteed running before consent fires. - Master toggle gate broken end-to-end —
cachedMasterEnabledwas never written because nothing calledupdateMasterEnabledCache. The cache was permanentlyfalseandBridgeCommandHandler403'd every command except/pingand/current_app. Service now owns a coroutine that observes the DataStore flow and feeds the cache. - MediaProjection consent flow never wired —
MediaProjectionHolder. onGrantedexisted but noActivityResultLauncherwas registered.MainActivitynow registers a launcher and a newScreenCaptureRequesterprocess-singleton bridges non-Activity callers (BridgeViewModel.requestScreenCapture()). - Manifest dedupe — duplicate
HermesAccessibilityServiceentry in Android Settings caused by a stub<service>block in the flavor manifests that pointed at a class that didn't exist - Gradle deprecation —
android.dependency. excludeLibraryComponentsFromConstraints=truecollapsed intouseConstraints=false - Version drift —
pyproject.tomlhad speculatively bumped to0.5.0andplugin/relay/__init__.py::__version__was stuck at0.2.0. Both synced to0.3.0via the newbump-version.shscript.
Docs
hermes-relay-self-setup,hermes-relay-pair, andhermes-relay-statusskills — agent-readable setup / pair / status recipes via the Hermes skills systemRELEASE.md— expanded with the three-source version contract, feature-branch workflow,--no-ffmerge style, branch protection policy, andbump-version.shrecipeCLAUDE.md— updated Git section with the new branching policy, added file-table entries forhermes-relay-update,register_code_command, and the expandedinstall.shTODO.md— captures open research questions around proper Hermes plugin/skill/tool distributionuser-docsvitepress site — new "For AI Agents" copy-paste block on the home view, Feature Matrix component, two-track explainer, manual-pair workflow walkthrough in configuration.md
[0.2.0] - 2026-04-12
Added
- Voice mode — real-time voice conversation via relay TTS/STT endpoints. Tap the mic in the chat bar to enter voice mode with the sphere, waveform visualizer, and streaming sentence-level TTS playback
- Three interaction modes (Tap-to-talk, Hold-to-talk, Continuous)
- Streaming TTS with sentence-boundary detection
- Interrupt: tap stop during Speaking to cancel TTS + SSE stream
- Sphere voice states: Listening (blue/purple), Speaking (green/teal)
- Voice settings screen (interaction mode, silence threshold, provider info, Test Voice)
- Relay endpoints:
POST /voice/transcribe,POST /voice/synthesize,GET /voice/config - 6 TTS + 5 STT providers via hermes-agent config
- Voice messages appear as normal chat messages in session history
- Reactive layered-sine waveform — three overlapping waves with amplitude-driven phase velocity (
withFrameNanosticker), pill-shaped edge merge (geometricsin(πt)taper +BlendMode.DstIngradient mask), color-keyed to voice state - Enter/exit voice chimes — synthesized 200ms PCM sweeps via AudioTrack (440→660 Hz enter, mirror exit)
- Terminal (preview) — tmux-backed persistent shells with tabs, scrollback search, and session info sheet
- Session TTL picker — choose 1d / 7d / 30d / 90d / 1y / Never at pair time
- Per-channel grants — control terminal/bridge access per paired device
- Android Keystore token storage — StrongBox-preferred hardware-backed encrypted storage with TEE fallback
- TOFU certificate pinning — SHA-256 SPKI fingerprints per host:port, wiped on re-pair
- Paired Devices screen — list all paired devices with metadata, extend sessions, revoke access
- Transport security badges — three-state visual indicator (secure / insecure-with-reason / insecure-unknown)
- HMAC-SHA256 QR signing — pairing QR codes signed via host-local secret
- Insecure connection acknowledgment dialog — first-time consent with threat model explanation + reason picker
- Inbound media pipeline — agent-produced files via relay
MediaRegistrywith opaque tokens, Discord-style rendering for image/video/audio/PDF/text/generic attachments /media/by-pathendpoint — LLM-emittedMEDIA:/pathmarkers fetched directly by the phone- Settings refactor — category-list landing page with dedicated sub-screens (Connection, Chat, Voice, Media, Appearance, Paired Devices, Analytics, Developer)
- Global font-scale preference — applies to both chat and terminal
- RelayErrorClassifier — converts any
Throwableinto a user-facingHumanError(title, body, retryable, actionLabel)with context-aware titles - Global SnackbarHost —
LocalSnackbarHostCompositionLocal at RelayApp scope so any screen can surface classified errors - Mic permission banner — rebuilt with "Open Settings" action button instead of a toast
- Relay
.envautoload —plugin/relay/_env_bootstrap.pyloads~/.hermes/.envat Python import time, matching the gateway pattern - systemd user service —
install.shstep [6/6] installs and enableshermes-relay.serviceautomatically - Save & Test health probe — relay connection verification with classified error feedback
- Gradle logcat task —
silenceAndroidViewLogsauto-runsadb shell setprop log.tag.View SILENTafter every install to suppress Compose Android 15 VRR spam - App screenshots in
assets/screenshots/
Fixed
- Voice replying to wrong turn —
ignoreAssistantIdbaseline prevents the stream observer from replaying the previous turn's response as TTS for the new question - Waveform flatline between sentences — TTS consumer restructured from
forloop towhile+tryReceivesomaybeAutoResumeonly fires when the queue is actually drained, not after every sentence - Stop button during Speaking —
interruptSpeaking()now cancels the SSE stream viachatViewModel.cancelStream(), drains TTS queue, and returns to Idle (previously only paused playback) - Waveform unresponsive to speech — perceptual amplitude curve at the source (noise-floor subtraction + speech-ceiling rescale + sqrt boost), attack/release envelope follower (0.75/0.10 at 60Hz), killed Compose spring double-smoothing
- Stop button color — hardcoded vivid red
Color(0xFFE53935)for Listening + Speaking (Material 3 darkcolorScheme.errorresolved to pale pink) - NaN amplitude propagation — guards in VoicePlayer.computeRms and VoiceViewModel.sanitizeAmplitude (IEEE 754:
Float.coerceInsilently passes NaN) - Relay voice 500 on restart —
.envnot loaded into relay process when started via nohup/systemd without shell sourcing - Rate-limit block on re-pair —
/pairing/registerclears all rate-limit blocks on success - Paired devices JSON unwrap — permissive
/media/by-pathsandbox - Settings status flicker — unified relay status as "Reconnecting..." on Settings entry
Changed
- Smart-swap trailing input button (empty → Mic, text → Send) replacing the floating Mic FAB
- Voice overlay is fully opaque surface (was 0.95 alpha — "phantom pencil" bleed-through from chat)
- Bottom nav hidden during voice mode
- Scrollable response text in voice overlay (long responses no longer clip)
install.shis now 6 steps (was 5) — new step [6/6] for systemd user service- Relay restart is
systemctl --user restart hermes-relay(nohup era ended)
0.1.0 - 2026-04-07
Added
- ASCII morphing sphere — animated 3D character sphere on empty chat screen (pure Compose Canvas,
. : - = + * # % @characters, green-purple color pulse, 3D lighting) - Ambient mode — toggle in chat header hides messages and shows sphere fullscreen; tap to return to chat
- Animation behind messages — sphere renders at 15% opacity behind chat message list as subtle background (toggleable)
- Animation settings — Settings > Appearance section with "ASCII sphere" and "Behind messages" toggles
- File attachments — attach files via
+button; images, documents, PDFs sent as base64 in the Hermes APIattachmentsformat - Attachment preview — horizontal strip above input shows thumbnails (images) or file badges (other types) with remove button
- Message queuing — send messages while the agent is streaming; queued messages auto-send when the current response completes
- Queue indicator — animated bar above input shows queued count with clear button
- Configurable limits — expandable Limits section in Chat settings for max attachment size (1–50 MB) and message length (1K–16K chars)
- Stats for Nerds enhancements — reset button, tokens per message average, peak TTFT, slowest completion, seconds subtext on all ms values
- Feature gating —
FeatureFlagssingleton with compile-time defaults (BuildConfig.DEV_MODE) and runtime DataStore overrides - Developer Options — hidden settings section, tap version 7 times to unlock (same pattern as Android system Developer Options)
- Relay feature toggle — Server settings and pairing sections gated behind developer options in release builds
- Dynamic onboarding — terminal, bridge, and relay pages excluded from onboarding when relay feature disabled
- Parse tool annotations — experimental annotation parsing for Sessions mode (marked with badge, disabled for Runs mode)
- Privacy policy link — accessible from Settings → About
- MCP tooling docs —
docs/mcp-tooling.mdreference for android-tools-mcp + mobile-mcp development setup - Dev scripts — added
release,bundle,versioncommands toscripts/dev.bat - MIT LICENSE — added project license file
Fixed
- Empty bubbles — messages with blank content and no tool calls are now hidden from chat
- App icon — adaptive icon foreground scaled to 75% via
<group>transform for proper safe zone padding - Token tracking — usage data now extracted before SSE event type resolution, fixing 0 token counts when server sends OpenAI-format events
- Token field compatibility —
UsageInfoaccepts bothinput_tokens/output_tokens(Hermes) andprompt_tokens/completion_tokens(OpenAI) - Keyboard gap — removed Scaffold content window insets that stacked with ChatScreen's IME padding
- Session drawer highlight — active session now properly highlighted (background color was computed but not applied)
- Privacy doc — added CAMERA permission, corrected network security description
- CHANGELOG URLs — fixed comparison links to use correct GitHub repository
- FOREGROUND_SERVICE — removed unused permission from AndroidManifest
- Plugin refs — updated from raulvidis to Codename-11
Changed
- Version bumped from
0.1.0-betato0.1.0for Google Play release - Input bar shows both Stop and Send buttons during streaming (previously only Stop)
- Onboarding page flow now uses enum-based dynamic list instead of hardcoded indices
0.1.0-beta - 2026-04-06
MVP release — native Android companion app for Hermes agent with direct API chat, session management, and full Compose UI.
Added
Core Chat
- Direct API chat — connects to Hermes API Server via
/api/sessions/{id}/chat/streamwith SSE streaming - HermesApiClient — full session CRUD + SSE streaming, health checks, cancel support
- Dual connection model — API Server (HTTP) for chat, Server (WSS) for bridge/terminal
- API key auth — optional Bearer token stored in EncryptedSharedPreferences
- Cancel streaming — stop button to cancel in-flight chat responses
- Error retry — retry button in error banner re-sends last failed message
Session Management
- Session CRUD — create, switch, rename, delete chat sessions via Sessions API
- Session drawer — slide-out panel listing all sessions with title, timestamp, message count
- Message history — loads from server when switching sessions
- Auto-session titles — first user message auto-titles the session (truncated to 50 chars)
- Session persistence — last session ID saved to DataStore, resumes on app restart
Chat UI
- Markdown rendering — assistant messages render code blocks, bold, italic, links, lists (mikepenz multiplatform-markdown-renderer)
- Reasoning display — collapsible thinking block above assistant responses (toggle in Settings)
- Token tracking — per-message input/output token count and estimated cost
- Personality picker — dynamic personalities from server config (
config.agent.personalities), agent name on chat bubbles - Message copy — long-press any message to copy text to clipboard
- Enriched tool cards — tool-type icons, completion duration tracking
- Responsive layout — bubble widths adapt to phone, tablet, and landscape
- Input character limit — 4096 character limit with counter
- Haptic feedback — on send, stream complete, error, and message copy
App Foundation
- Jetpack Compose scaffold — bottom nav with Chat, Terminal (stub), Bridge (stub), Settings
- WSS connection manager — OkHttp WebSocket with auto-reconnect and exponential backoff
- Channel multiplexer — typed envelope protocol for chat/terminal/bridge/system
- Auth flow — 6-character pairing code with session token persistence
- Material 3 + Material You — dynamic theming with light/dark/auto
- Onboarding — multi-page pager with feature overview and connection setup
- Settings — API Server + Server config, theme, reasoning toggle, data export/import/reset
- Offline detection — banner shown when network connectivity is lost
- What's New dialog — shown automatically when app version changes
- Splash screen — branded splash via core-splashscreen API
- Network security — cleartext restricted to localhost only
Infrastructure
- Server — Python aiohttp WSS server for bridge/terminal channels
- CI/CD — GitHub Actions for lint, build, test, and tag-driven releases
- Claude Code automation — issue triage, PR fix, chat, and code review workflows
- Dependabot — weekly Gradle + GitHub Actions dependency updates with auto-merge
- Dev scripts — build, install, run, test, relay via scripts/dev.bat
- ProGuard rules — okhttp-sse, markdown renderer, intellij-markdown parser