tool.start gains an optional cosmetic 'emoji' field, resolved server-side
through hermes' own display layer (active-skin overrides, then the tool
registry's per-tool emoji) so icons track the user's hermes theme and
new/plugin tools get their glyph for free. Omitted for unknown tools so
the app falls back to its default wrench.
- protocol.py: tool_start(emoji=...) kwarg, payload field when set
- adapter.py: _tool_emoji() helper (lazy import, None on unknown/failure)
- frames.schema.json + docs/04: field documented
- app: ToolStartPayload.emoji -> ToolItem.emoji -> ToolCard header
- tests: frame shape, resolution/fallback, end-to-end tool.start emoji
Gateway (docs/19):
- Remove ws_server.py; frame dispatch factored into dispatch.py
- http_server: media upload/pull, pairing over HTTP
- protocol: media frames mirrored; tests + ws_probe updated for HTTP
App:
- HttpGateway: postFrame/uploadMedia/pullMedia no longer throw on
network failure (PostResult ok=false / Result.failure) — uncaught
SocketTimeoutException on Dispatchers.Default crashed the app
- GatewayClient: dead-stream watchdog (health probe every 10s, 2
failures -> redial in ~20s instead of the 45s SSE read timeout);
state flips to Reconnecting when the stream dies, restored from the
last hello.ack on long-poll success; poke() + backoff reset on app
resume (MainActivity.onResume)
- Offline sends: composer enabled while disconnected; a send with no
response (status 0) stays queued (Pending) and is auto-resent on the
next (re)connect after a 2s outbox-replay grace; gateway 4xx
rejections fail the bubble (tap to retry, no auto-loop)
- ChatStore: echo-replace and thread-relocate also match Failed
bubbles (POST response lost in a network drop); loadHistory dedupes
local failed bubbles the server already has; failMessage()
- MainActivity: poke() on resume so a backgrounded app reconnects
promptly instead of waiting out the backoff
- e2e.py: scenario 13 (http fallback) — drives a full turn over the
HTTP leg (health + POST /v1/frame + SSE /v1/events, no WS) and
asserts the user echo lands on the SSE stream in < 1.5 s.
- ws_probe.py --http: prints '== user echo in X.XXs' (the docs/19
sendable-in-fallback timing assertion) alongside the existing
health/POST/SSE output; same assertion flags as the WS leg.
- docs: 19 status flipped to implemented; 09-pairing-security §9.4
cross-reference (second door, same lock: token + device allowlist,
64 KiB cap, rate limit, optional TLS, unauthenticated /v1/health);
13-testing manual scenario 15 + automated pointers.
Second, short-lived-connection transport next to the WS: same frames,
same outbox/cursor, same token, served over plain HTTP (stdlib
ThreadingHTTPServer bridged into the asyncio loop; zero new deps).
- http_server.py: /v1/health (unauthenticated), POST /v1/frame
(accept-and-ack; validation rejections as 4xx error frames), SSE
/v1/events (outbox catch-up with id=cursor, event: hello, 15s
heartbeat, bounded-queue backpressure), long-poll /v1/poll (25s hold).
Bearer token + X-Iris-Device (same allowlist as WS hello), 64 KiB body
cap, per-device rate limit, optional TLS, non-fatal bind failure.
- ws_server.py: inbound dispatch chain extracted to shared
dispatch_frame() used by both transports.
- adapter.py: ANDROID_HTTP_PORT/CERT/KEY config; start/stop next to the
WS; delivery counting in _broadcast_or_log (an SSE subscriber is a
live subscriber -> no push, docs/19 19.8); _reply() routes
point-to-point replies into the in-flight HTTP response (reply sink)
or broadcasts when the device has no live WS (19.7); status/typing/
channel events fan out to both transports.
- ws_probe.py: --http mode (health + POST + SSE turn drive, same
assertion flags); tests/README updated.
- Tests: hermes-agent/tests/gateway/test_android_http.py (23 tests,
incl. the 19.8 delivery-counting regression); test_android.py (74)
still green.