Compare commits

...
Author SHA1 Message Date
Bailey Dixon 1a6f9cc10b Merge pull request #580 from Codename-11/dev
release(android): 1.16.1
2026-09-12 09:54:40 -04:00
Bailey Dixon c7f54321ad Merge pull request #579 from Codename-11/release/android-1.16.1
release(android): android-v1.16.1
2026-09-12 09:26:59 -04:00
Bailey Dixon e6b9933d46 release(android): android-v1.16.1 2026-09-12 09:17:51 -04:00
Bailey Dixon c27ee439ef Merge pull request #578 from Codename-11/fix/android-gateway-directory-bootstrap
fix(android): release Gateway directory bootstrap
2026-09-12 08:55:33 -04:00
Bailey Dixon 68fce1b1da fix(android): release gateway directory bootstrap 2026-09-10 20:07:41 -04:00
Bailey Dixon 2865456761 Merge pull request #577 from Codename-11/dev
docs(release): reconcile 1.16.0 and 1.11.2 dates
2026-09-10 10:00:16 -04:00
Bailey Dixon 4547031bba Merge pull request #576 from Codename-11/docs/release-date-2026-09-10
docs(release): correct 1.16.0 and 1.11.2 dates
2026-09-10 09:52:19 -04:00
Bailey Dixon 41e9acf4c2 docs(release): correct 1.16.0 and 1.11.2 dates 2026-09-10 09:43:58 -04:00
Bailey Dixon a5f671c06c Merge pull request #575 from Codename-11/dev
release: Android 1.16.0 and Plugin 1.11.2
2026-09-10 09:33:47 -04:00
Bailey Dixon 5372fc1fef Merge pull request #574 from Codename-11/release/android-1.16.0
release(android): android-v1.16.0
2026-09-09 22:44:43 -04:00
Bailey Dixon 75feb55adf Merge pull request #540 from Codename-11/dev
release: Android 1.15.1 and CLI+UI 0.4.0-beta.7
2026-09-02 22:02:09 -04:00
Bailey Dixon f4b366389b release: Android 1.15.0, Plugin 1.11.1, CLI+UI 0.4.0-beta.6 (#525) 2026-08-31 23:45:58 -04:00
Bailey Dixon 2ec7b7aac9 release: Android 1.14.0 and Plugin 1.11.0 (#490) 2026-08-30 23:50:41 -04:00
13 changed files with 168 additions and 59 deletions
+8 -2
View File
@@ -6,7 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
## [Unreleased]
## [Android 1.16.0] - 2026-09-09
## [Android 1.16.1] - 2026-09-12
### Fixed
- Android Dashboard-only connections start the profile-scoped session directory before Gateway readiness, so a cold launch no longer leaves both the directory and passive Gateway socket waiting on each other. (#495, #528)
## [Android 1.16.0] - 2026-09-10
### Fixed
@@ -20,7 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- Chat distinguishes session preparation from response streaming and retains initialization errors that arrive before the session acknowledgement. Long-press the agent header to open a live session-diagnostics drawer.
- Delegated-agent activity survives parent replies and leaves compact history entries for later read-only review. The activity strip appears only while work runs; historical process views cannot stop or dismiss live work. (#447)
## [Plugin 1.11.2] - 2026-09-09
## [Plugin 1.11.2] - 2026-09-10
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
# Hermes-Relay Plugin v__VERSION__
**Release Date:** September 9, 2026
**Release Date:** September 10, 2026
## Summary
+7 -19
View File
@@ -1,10 +1,10 @@
# Hermes-Relay Android v1.16.0
# Hermes-Relay Android v1.16.1
**Release Date:** September 9, 2026
**Release Date:** September 12, 2026
## Download
> Installing on your phone? Download `hermes-relay-1.16.0-sideload-release.apk` and tap it for the full feature set, or install the conservative build from [Google Play](https://play.google.com/store/apps/details?id=com.axiomlabs.hermesrelay).
> Installing on your phone? Download `hermes-relay-1.16.1-sideload-release.apk` and tap it for the full feature set, or install the conservative build from [Google Play](https://play.google.com/store/apps/details?id=com.axiomlabs.hermesrelay).
The `.aab` file is a Play Console upload bundle and cannot be installed by tapping it on a phone.
@@ -12,28 +12,16 @@ Verify the download against `SHA256SUMS.txt`. See the [sideload guide](https://h
## Summary
This release makes startup and connection switching safer, prevents a network-change crash, and keeps Bot Mode and delegated work stable across multiple Hermes gateways. Chat feedback, attachment failures, and session preparation are also easier to understand and review.
## Changed
- Delegated-agent activity remains available after parent replies as compact, bounded, read-only history. The live strip appears only while work is active, and historical views cannot control a running process.
- Android feedback uses themed banners and action cards. A long-press on the agent header opens session diagnostics, and Developer settings can preview message surfaces locally.
This patch fixes a remaining cold-start path that could leave a Dashboard-only connection waiting for Gateway readiness until the app resumed or its network route changed.
## Fixed
- An authenticated Gateway chat opens on the first foreground launch instead of waiting for a background-and-resume cycle.
- Network changes can invalidate route probes without racing the endpoint cache or crashing Android.
- Saved Dashboard sign-ins stay bound to their owning connection when switching gateways; an outgoing route cannot invalidate another connection's session.
- Dashboard sign-in removes pasted line breaks from username and password fields while preserving every other credential character.
- Bot Mode and Active Now keep connection identity when different gateways expose the same profile name, and progress opens only on the selected bot.
- Missing attachments keep their error and Retry action in the attachment card without repeated global messages.
- Chat distinguishes session preparation from response streaming and retains initialization errors received before session acknowledgement.
- Dashboard-only connections now start the exact profile-scoped session directory before Gateway readiness, so the directory and passive Gateway socket no longer wait on each other during a cold launch.
## Install / Verify
- App version: **1.16.0** (versionCode **55**).
- App version: **1.16.1** (versionCode **56**).
- Standard Chat, sessions, profiles, Manage, voice, and ordinary media use current upstream Hermes. Speech-to-text still requires a configured provider on the host.
- Hermes-Relay Plugin **1.11.2** is the matching optional release for Relay tools; the Android connection, Bot Mode, and delegated-activity fixes do not require the plugin.
- Already-erased or revoked Dashboard credentials still require a legitimate sign-in; this release prevents cross-connection invalidation going forward.
- Hermes-Relay Plugin **1.11.2** remains the matching optional release for Relay tools; this Gateway startup fix does not require it.
- Explicit Direct API/API-only connections remain supported and are not used as silent failover for Dashboard-owned chats.
- Granular Device Control and the system Voice Focus overlay remain sideload-only.
@@ -31,9 +31,11 @@ import com.hermesandroid.relay.network.upstream.GatewayChatClient
import com.hermesandroid.relay.network.upstream.GatewayConnectionState
import com.hermesandroid.relay.network.upstream.HermesApiClient
import com.hermesandroid.relay.network.upstream.models.MessageItem
import com.hermesandroid.relay.network.upstream.models.SessionItem
import com.hermesandroid.relay.ui.components.GatewayBackgroundProcessStrip
import com.hermesandroid.relay.ui.components.SubagentPreviewVisibility
import com.hermesandroid.relay.ui.screens.shouldOwnVisibleGateway
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -222,6 +224,75 @@ class GatewayForegroundRecoveryInstrumentedTest {
}
}
@Test
fun dashboardOnlyColdLaunch_waitsForExactDirectoryThenOpensObservationSocket() {
viewModel.setChatVisible(false)
viewModel.updateGatewayClient(null)
gatewayClient.shutdown()
gatewayScope.cancel()
val directoryStarted = CompletableDeferred<Unit>()
val directoryResult = CompletableDeferred<Result<List<SessionItem>>>()
viewModel.setProfileSessionLister { profile ->
assertEquals(PROFILE_NAME, profile)
directoryStarted.complete(Unit)
directoryResult.await()
}
handler.setSessionId(null)
viewModel.switchProfileContext(
AgentDisplay.profileContextKey("fixture-connection", PROFILE_NAME),
STORED_SESSION_ID,
)
val controlMethods = setOf(
"session.resume",
"session.activate",
"prompt.submit",
"session.interrupt",
)
val baseline = controlMethods.associateWith(fixture::rpcCount)
val ticketMintsBefore = fixture.requestsTo("/api/auth/ws-ticket")
gatewayScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val okHttp = OkHttpClient()
gatewayClient = GatewayChatClient(
initialDashboardClient = DashboardApiClient(
baseUrl = fixture.server.url("/").toString().trimEnd('/'),
okHttpClient = okHttp,
),
okHttpClient = okHttp,
callbackDispatcher = { block -> Handler(Looper.getMainLooper()).post(block) },
scope = gatewayScope,
reconnectJitterUnit = { 0.0 },
)
viewModel.setChatTurnCheckpointStore(null)
viewModel.updateGatewayClient(gatewayClient)
viewModel.setChatVisible(true)
// This is the production binder's Dashboard/profile hydration edge.
// The socket must stay passive and closed until the exact-owner REST
// directory publishes, then open without a lifecycle bounce.
viewModel.refreshSessions()
compose.waitUntil(5_000) { directoryStarted.isCompleted }
assertEquals(ticketMintsBefore, fixture.requestsTo("/api/auth/ws-ticket"))
directoryResult.complete(
Result.success(listOf(SessionItem(id = STORED_SESSION_ID, title = "Fixture session"))),
)
compose.waitUntil(5_000) {
gatewayClient.connectionState.value == GatewayConnectionState.Ready
}
serverSocket = fixture.awaitServerSocket()
assertEquals(ticketMintsBefore + 1, fixture.requestsTo("/api/auth/ws-ticket"))
controlMethods.forEach { method ->
assertEquals(
"directory-gated cold observation sent $method",
baseline.getValue(method),
fixture.rpcCount(method),
)
}
}
@After
fun tearDown() {
viewModel.updateGatewayClient(null)
@@ -1,3 +1,3 @@
v1.16.0 - Safer startup, connections, and activity
v1.16.1 - Dashboard-only cold starts recover
Gateway chat now opens reliably on a cold launch. Saved Dashboard sign-ins stay bound to the correct connection, pasted credentials ignore accidental line breaks, and network changes no longer race the route cache. Bot Mode supports duplicate profile names across gateways, while delegated work, session setup, attachment errors, and feedback remain visible and easier to review.
Dashboard-only connections now prepare the selected profile before Gateway readiness, fixing a remaining cold-start path that could stay on waking or waiting for Gateway until the app resumed or its network route changed.
+22 -1
View File
@@ -1,10 +1,31 @@
{
"schema": 3,
"versions": [
{
"version": "1.16.1",
"title": "Dashboard-only cold starts recover",
"date": "2026-09-12",
"summary": "Dashboard-only connections can now prepare the selected profile and open Gateway chat without waiting for a background, resume, or network-route change.",
"changes": [
{
"id": "gateway-directory-bootstrap",
"kind": "fixed",
"title": "Open Gateway chat from a Dashboard-only cold start",
"summary": "The selected profile's session directory starts before Gateway readiness, so it cannot wait on the same passive socket that depends on its result.",
"highlight": true
}
],
"compatibility": [
"Standard Dashboard and Gateway chat continue to use current upstream Hermes without requiring the optional Hermes-Relay Plugin.",
"Hermes-Relay Plugin 1.11.2 remains the matching optional release for Relay tools."
],
"playNotes": "Dashboard-only connections now prepare the selected profile before Gateway readiness, fixing a remaining cold-start path that could stay on waking or waiting for Gateway until the app resumed or its network route changed.",
"sections": []
},
{
"version": "1.16.0",
"title": "Safer startup, connections, and activity",
"date": "2026-09-09",
"date": "2026-09-10",
"summary": "Gateway chat opens reliably from a cold launch, saved sign-ins stay with the correct connection, and network changes no longer race the route cache. Bot Mode and delegated-work feedback also remain stable across multiple gateways and later review.",
"changes": [
{
+5 -18
View File
@@ -1,24 +1,11 @@
v1.16.0 - Safer startup, connections, and activity
v1.16.1 - Dashboard-only cold starts recover
Summary
* Gateway chat opens reliably from a cold launch, saved sign-ins stay with the correct connection, and network changes no longer race the route cache. Bot Mode and delegated-work feedback also remain stable across multiple gateways and later review.
* Dashboard-only connections can now prepare the selected profile and open Gateway chat without waiting for a background, resume, or network-route change.
Highlights
* Open Gateway chat on the first launch — An authenticated Gateway wakes and opens from a cold foreground start instead of waiting for the app to background and resume.
* Keep saved sign-ins with their connection — Switching gateways cannot reuse an outgoing resolver route to invalidate another connection's saved Dashboard session.
* Recover safely when the network changes — Route-probe completion and endpoint-cache invalidation are serialized so Wi-Fi, mobile-data, VPN, or Tailscale changes do not trigger the reported crash.
* Review delegated work after it finishes — Compact, bounded activity receipts survive parent replies and remain available read-only, while the live strip appears only during active work.
Improved
* Keep feedback with the surface that owns it — Themed banners and action cards replace platform popups, global actions stay clear of the composer, and local Developer previews make feedback states inspectable.
* See session preparation and initialization failures — Chat distinguishes session preparation from response streaming, retains early initialization errors, and opens session diagnostics from the agent header.
Fixed
* Open same-named bots from multiple gateways — Bot Mode and Active Now keep connection and profile identity together, avoiding duplicate list keys and opening progress on the selected bot.
* Retry missing attachments in place — A missing attachment keeps its error and Retry action in the attachment card without producing repeated global messages.
* Paste Dashboard credentials without hidden line breaks — Username and password fields remove pasted carriage returns and line feeds while preserving every other credential character.
* Open Gateway chat from a Dashboard-only cold start — The selected profile's session directory starts before Gateway readiness, so it cannot wait on the same passive socket that depends on its result.
Compatibility
* Standard Chat, sessions, profiles, Manage, voice, Bot Mode, and delegated activity continue to use current upstream Hermes without requiring the optional Hermes-Relay Plugin.
* Hermes-Relay Plugin 1.11.2 is the matching optional release for Relay tools. Existing erased or revoked Dashboard credentials still require a legitimate sign-in.
* Granular Device Control and the system Voice Focus overlay remain sideload-only.
* Standard Dashboard and Gateway chat continue to use current upstream Hermes without requiring the optional Hermes-Relay Plugin.
* Hermes-Relay Plugin 1.11.2 remains the matching optional release for Relay tools.
@@ -351,14 +351,18 @@ internal class HermesRuntimeBinder(
connection.activeConnectionId,
connection.effectiveSessionProfileName,
connection.lastSessionId,
connection.activeEndpoint,
) { ready, connectionId, profileName, sessionId, activeEndpoint ->
// The session directory belongs to the standard Dashboard
// route. connection.activeEndpoint is the optional Relay
// socket's selected candidate and stays null on a valid
// Dashboard-only LAN connection.
connection.effectiveDashboardUrl,
) { ready, connectionId, profileName, sessionId, dashboardUrl ->
ProfileContextInputs(
ready,
connectionId,
profileName,
sessionId,
dashboardRouteResolved = activeEndpoint != null,
dashboardUrl = dashboardUrl,
)
}
combine(
@@ -374,7 +378,7 @@ internal class HermesRuntimeBinder(
)
}.collectLatest { inputs ->
profileContextReady.value = false
if (!shouldRefreshSessionDirectory(inputs.chatReady, inputs.dashboardRouteResolved)) {
if (!shouldRefreshSessionDirectory(inputs.chatReady, inputs.dashboardUrl)) {
return@collectLatest
}
if (!inputs.profileSelectionSettled) {
@@ -600,7 +604,7 @@ internal class HermesRuntimeBinder(
val connectionId: String?,
val profileName: String?,
val sessionId: String?,
val dashboardRouteResolved: Boolean,
val dashboardUrl: String,
val profileSelectionSettled: Boolean = false,
val profileLocked: Boolean = false,
val hiddenSources: Set<String> = emptySet(),
@@ -632,12 +636,13 @@ internal class HermesRuntimeBinder(
/**
* Session browsing is Dashboard HTTP state, not Gateway-socket state. API-only
* connections still use chat readiness; Dashboard connections can refresh once
* the resolver has selected a live route, after the profile-settle fence.
* their persisted/resolved Dashboard origin publishes, after the profile-settle
* fence. The optional Relay endpoint is deliberately not part of this decision.
*/
internal fun shouldRefreshSessionDirectory(
chatReady: Boolean,
dashboardRouteResolved: Boolean,
): Boolean = chatReady || dashboardRouteResolved
dashboardUrl: String,
): Boolean = chatReady || dashboardUrl.isNotBlank()
internal fun assistantCanTransmitScreenContext(engineMode: VoiceEngineMode): Boolean =
engineMode == VoiceEngineMode.HermesVoiceOutput
@@ -6,23 +6,29 @@ import org.junit.Test
class HermesRuntimeBinderSessionDirectoryPolicyTest {
@Test
fun `session directory can refresh from either standard route owner`() {
fun `session directory starts from dashboard publication before Gateway readiness`() {
assertTrue(
shouldRefreshSessionDirectory(
chatReady = true,
dashboardRouteResolved = false,
dashboardUrl = "",
),
)
assertTrue(
shouldRefreshSessionDirectory(
chatReady = false,
dashboardRouteResolved = true,
dashboardUrl = "https://dashboard.example.test",
),
)
assertFalse(
shouldRefreshSessionDirectory(
chatReady = false,
dashboardRouteResolved = false,
dashboardUrl = "",
),
)
assertFalse(
shouldRefreshSessionDirectory(
chatReady = false,
dashboardUrl = " ",
),
)
}
+2 -2
View File
@@ -91,9 +91,9 @@ This app is a community project and is not affiliated with or endorsed by NousRe
Paste into Play Console → **What's new** (≤500 characters):
```
v1.16.0 - Safer startup, connections, and activity
v1.16.1 - Dashboard-only cold starts recover
Gateway chat now opens reliably on a cold launch. Saved Dashboard sign-ins stay bound to the correct connection, pasted credentials ignore accidental line breaks, and network changes no longer race the route cache. Bot Mode supports duplicate profile names across gateways, while delegated work, session setup, attachment errors, and feedback remain visible and easier to review.
Dashboard-only connections now prepare the selected profile before Gateway readiness, fixing a remaining cold-start path that could stay on waking or waiting for Gateway until the app resumed or its network route changed.
```
## Category
+2 -2
View File
@@ -1,6 +1,6 @@
[versions]
appVersionName = "1.16.0"
appVersionCode = "55"
appVersionName = "1.16.1"
appVersionCode = "56"
agp = "9.4.0"
kotlin = "2.4.10"
compose-bom = "2026.08.00"
@@ -247,6 +247,18 @@ class FixtureTestCase(unittest.IsolatedAsyncioTestCase):
async def test_cold_start_observer_opens_socket_without_control_rpc(self) -> None:
_, base_url = await self.start("cold_start_observation")
# Android's cold-start barrier hydrates the profile-scoped Dashboard
# directory before it opens the passive Gateway observation socket.
# The REST read is independent of gateway.ready and must not create or
# attach a live runtime.
async with self.session.get(
f"{base_url}/api/sessions",
params={"profile": "default", "limit": 50, "offset": 0},
) as response:
self.assertEqual(200, response.status)
self.assertEqual([], (await response.json())["sessions"])
observer, _ = await self.connect(base_url)
await self.rpc(observer, 1, "session.active_list")
active = (await observer.receive_json())["result"]["sessions"]
@@ -33,6 +33,7 @@ class GatewayFixture:
[
web.post("/api/auth/ws-ticket", self._ticket),
web.get("/api/ws", self._websocket),
web.get("/api/sessions", self._sessions),
web.get("/api/sessions/{session_id}/messages", self._history),
web.get("/__fixture__/state", self._state),
web.get("/__fixture__/evidence", self._evidence),
@@ -289,6 +290,18 @@ class GatewayFixture:
{"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}},
)
async def _sessions(self, request: web.Request) -> web.Response:
profile = request.query.get("profile")
if profile not in (None, self.scenario.profile):
raise web.HTTPNotFound(text="profile not found")
self.evidence.add("directory", outcome="listed")
return web.json_response(
{
"sessions": [],
"pagination": {"limit": 50, "offset": 0, "returned": 0},
},
)
async def _history(self, request: web.Request) -> web.Response:
if request.match_info["session_id"] != self.scenario.stored_session_id:
raise web.HTTPNotFound(text="session not found")