HTTP transport: drop WS server, offline send queue + dead-stream watchdog
CI / Gateway plugin tests (push) Successful in 5m9s
CI / Kotlin tests (android host + desktop) (push) Successful in 6m55s

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
This commit is contained in:
ARIA committed 2026-08-22 20:10:05 +02:00
1 parent 2349a95dd4
commit e6015033b6
22 files changed
+2804 -2439

No files matched your search

+4 -22
View File
@@ -26,11 +26,8 @@ PROTOCOL_VERSION = 1
# ---------------------------------------------------------------------------
# Pairing / lifecycle
TYPE_HELLO = "hello"
TYPE_HELLO_ACK = "hello.ack"
TYPE_ERROR = "error"
TYPE_PING = "ping"
TYPE_PONG = "pong"
# Chat
TYPE_MESSAGE = "message"
@@ -82,12 +79,8 @@ TYPE_SYNC_DONE = "sync.done"
TYPE_HISTORY = "history"
# Media (M4)
TYPE_MEDIA_UPLOAD_START = "media.upload.start"
TYPE_MEDIA_UPLOAD_END = "media.upload.end"
TYPE_MEDIA_UPLOAD_ACK = "media.upload.ack"
TYPE_MEDIA_OFFER = "media.offer"
TYPE_MEDIA_PULL = "media.pull"
TYPE_MEDIA_PULL_END = "media.pull.end"
# Push / notifications (M5)
TYPE_NOTIFICATION = "notification"
@@ -749,7 +742,8 @@ def media_offer(
thread_id: str | None = None,
message_id: str | None = None,
) -> Frame:
"""Event: the agent produced media the app can fetch via ``media.pull``.
"""Event: the agent produced media the app can fetch via
``GET /v1/media/{media_id}`` (docs/19 §19.15).
``message_id`` (optional) associates the offer with the assistant message
it belongs to (the app falls back to the lane's last assistant message).
@@ -766,14 +760,9 @@ def media_offer(
return Frame(type=TYPE_MEDIA_OFFER, chat_id=chat_id, thread_id=thread_id, payload=payload)
def media_pull_end(ok: bool, *, id: int | None = None) -> Frame:
"""Terminal frame of a ``media.pull`` binary stream."""
return Frame(type=TYPE_MEDIA_PULL_END, id=id, payload={"ok": ok})
def media_upload_ack(ok: bool, media_ref: str, *, id: int | None = None) -> Frame:
"""Response to ``media.upload.end``: the ref is cached and may be used in
a ``message.send`` ``media_refs``. Failures use ``error`` frames instead."""
"""Response to ``POST /v1/media``: the ref is cached and may be used in a
``message.send`` ``media_refs``. Failures use ``error`` frames instead."""
return Frame(
type=TYPE_MEDIA_UPLOAD_ACK,
id=id,
@@ -783,10 +772,3 @@ def media_upload_ack(ok: bool, media_ref: str, *, id: int | None = None) -> Fram
def error(code: str, message: str, *, id: int | None = None) -> Frame:
return Frame(type=TYPE_ERROR, id=id, payload={"code": code, "message": message})
def pong(ts: int | None = None) -> Frame:
payload: dict[str, Any] = {}
if ts is not None:
payload["ts"] = ts
return Frame(type=TYPE_PONG, payload=payload)