iOS: Tapping "new session" creates the thread on the host but the Litter app shows nothing (no error, no new thread in the list)
Summary
On the Litter iOS app, tapping "new session" with the kittylitter (alleycat) host appears to do nothing — the app does not show a new thread and does not show an error. The thread is in fact created on the host side (alleycat creates the opencode session and writes the JSON-RPC response), the response is written to the iroh QUIC stream, but the Litter iOS app's UI never reflects the new thread.
Environment
- Litter iOS app: v0.3.4 (TestFlight, current shipping build)
- Host:
npx -y kittylitter@0.3.4 serve (v0.3.4 of the npm wrapper; underlying alleycat binary version 0.1.0)
- Bridge agent:
opencode (wire=jsonl)
- Host OS: macOS 26 (Apple Silicon)
- Phone node id (constant across re-pairs):
104547c0e9a06d5e651850b969fce9f0c134c68b8d2226e090bfbd2d4f98b81e
- Host node id (re-paired for this report):
2bcefae63c49551a319363f78d32cca372624484420957d543f9441e4cdb6cb2
Repro steps
- On a Mac, run
npx -y kittylitter@0.3.4 so the daemon is up. Pair the Litter iOS app to it (the host's iroh node id and token).
- In the Litter iOS app, tap the "+" / "New session" button.
- Wait a few seconds.
- Expected: a new thread appears in the home list and the Litter app navigates into it.
- Actual: nothing happens. No new thread in the home list, no error toast, no spinner that resolves into a thread. The user must back out and look at the home list to confirm.
Evidence — the thread IS created on the host
The thread/start JSON-RPC request is received, dispatched, and the response is written to the iroh stream in well under a second. The thread is persisted in both the bridge's threads.json and in the upstream opencode /session API.
Bridge log (cat 18:39 — log entries immediately after the user tapped "new session"):
09:41:17.903Z INFO alleycat_bridge_core::server: drainer: spawned, beginning replay phase session=0xb47231690 backlog_len=0
09:41:17.903Z INFO alleycat_bridge_core::server: json-rpc request method=initialize id=initialize
09:41:48.455Z INFO alleycat_bridge_core::server: json-rpc request method=model/list id=43
09:41:50.622Z INFO alleycat_bridge_core::server: json-rpc request method=thread/start id=45
09:41:50.622Z INFO alleycat_opencode_bridge::handlers: opencode handle_thread_start: entered cwd=/Users/aleksandrsdaniilsjolkins/Documents/My Workspace title=None permission=Some("on-request")
09:41:50.777Z INFO alleycat_opencode_bridge::handlers: opencode handle_thread_start: completed threadId=24cf73fc-b6bb-ebd3-44d8-9abbb8ec78de sessionId=ses_13030fe5fffeLip8ElOb0v80QR
thread/start id=45 was handled in 155 ms. The response is enqueued via the bridge-core's NotificationSender::send_message → Session::enqueue, which pushes to the replay ring and forwards to the live drainer.
Bridge-core trace (added during this investigation; same v0.1.0 binary as the shipping npm artifact):
DEBUG alleycat_bridge_core::server: drainer: writing live frame seq=N bytes=<response body>
DEBUG alleycat_bridge_core::server: drainer: wrote live frame seq=N
The drainer reports the JSON-RPC response was written to the iroh stream. No write error.
Bridge state after the call (~/Library/Application Support/com.sigkitten.kittylitter/host.toml and ~/var/folders/.../alleycat-opencode-bridge/threads.json):
threads.json has the new binding:
{
"threadId": "24cf73fc-b6bb-ebd3-44d8-9abbb8ec78de",
"sessionId": "ses_13030fe5fffeLip8ElOb0v80QR",
"directory": "/Users/username/Documents/My Workspace",
"archived": false,
"name": "New session - 2026-06-16T09:41:50.624Z"
}
- opencode upstream
/session returns the same ses_13030fe5fffeLip8ElOb0v80QR with the same timestamp. So the new session exists in the upstream agent, the bridge has the binding, and the bridge wrote the JSON-RPC response that points at both.
Evidence — the Litter iOS app did not pick it up
- The home list never shows the new thread. Pull-to-refresh does not show it.
- The Litter iOS app does not call
thread/list or thread/read for the new thread after the thread/start response — the connection drops 20 s after the thread/start (well within the iroh default 30 s idle window, so it's the iOS app closing the connection, not a server timeout). A separate thread/resume id=34 is sent ~24 s later from a reconnected Litter session with attached=Resumed and current_seq=4, suggesting the iOS app noticed the new thread exists but does not render it.
- The same observable behavior reproduces after a complete re-pair (host
host.key deleted, fresh node id, fresh token, fresh host.toml).
- The same observable behavior reproduces when the Litter app is kept foregrounded the entire time.
Likely root cause (hypothesis)
The Litter iOS app sends thread/start and the bridge correctly writes the response to the iroh stream. The Litter iOS app's RemoteAppServerClient (in shared/rust-bridge/codex-mobile-client) appears to either (a) drop the response before its internal Conversation model updates, or (b) update the model but not push the new Thread into the SwiftUI home list. Either way, no error is surfaced to the user.
Strongest hint: the iOS app opens two iroh QUIC connections back-to-back — one for agent=codex and one for agent=opencode, both attached=Fresh (no resume cursor). The thread/start is then sent on the opencode connection, the response is written there, and the Litter app does not send a follow-up thread/list to pull the new thread into the home list. The home list is never refreshed with the thread that was just created.
Workaround
Use the upstream opencode web UI directly: opencode web listens on 127.0.0.1:4096 by default; the Litter user can hit it from the Mac (or via Tailscale on the phone). The bridge's OPENCODE_BRIDGE_BACKEND_URL=http://127.0.0.1:4096 env var makes the bridge share that same opencode process, so the threads created via "new session" in the Litter app DO appear in the opencode web UI — they're not lost, just invisible to the Litter iOS app.
Related
iOS: Tapping "new session" creates the thread on the host but the Litter app shows nothing (no error, no new thread in the list)
Summary
On the Litter iOS app, tapping "new session" with the kittylitter (alleycat) host appears to do nothing — the app does not show a new thread and does not show an error. The thread is in fact created on the host side (alleycat creates the opencode session and writes the JSON-RPC response), the response is written to the iroh QUIC stream, but the Litter iOS app's UI never reflects the new thread.
Environment
npx -y kittylitter@0.3.4 serve(v0.3.4 of the npm wrapper; underlying alleycat binary version 0.1.0)opencode(wire=jsonl)104547c0e9a06d5e651850b969fce9f0c134c68b8d2226e090bfbd2d4f98b81e2bcefae63c49551a319363f78d32cca372624484420957d543f9441e4cdb6cb2Repro steps
npx -y kittylitter@0.3.4so the daemon is up. Pair the Litter iOS app to it (the host's iroh node id and token).Evidence — the thread IS created on the host
The
thread/startJSON-RPC request is received, dispatched, and the response is written to the iroh stream in well under a second. The thread is persisted in both the bridge'sthreads.jsonand in the upstream opencode/sessionAPI.Bridge log (cat 18:39 — log entries immediately after the user tapped "new session"):
thread/start id=45was handled in 155 ms. The response is enqueued via the bridge-core'sNotificationSender::send_message→Session::enqueue, which pushes to the replay ring and forwards to the live drainer.Bridge-core trace (added during this investigation; same v0.1.0 binary as the shipping npm artifact):
The drainer reports the JSON-RPC response was written to the iroh stream. No write error.
Bridge state after the call (
~/Library/Application Support/com.sigkitten.kittylitter/host.tomland~/var/folders/.../alleycat-opencode-bridge/threads.json):threads.jsonhas the new binding:{ "threadId": "24cf73fc-b6bb-ebd3-44d8-9abbb8ec78de", "sessionId": "ses_13030fe5fffeLip8ElOb0v80QR", "directory": "/Users/username/Documents/My Workspace", "archived": false, "name": "New session - 2026-06-16T09:41:50.624Z" }/sessionreturns the sameses_13030fe5fffeLip8ElOb0v80QRwith the same timestamp. So the new session exists in the upstream agent, the bridge has the binding, and the bridge wrote the JSON-RPC response that points at both.Evidence — the Litter iOS app did not pick it up
thread/listorthread/readfor the new thread after thethread/startresponse — the connection drops 20 s after thethread/start(well within the iroh default 30 s idle window, so it's the iOS app closing the connection, not a server timeout). A separatethread/resume id=34is sent ~24 s later from a reconnected Litter session withattached=Resumedandcurrent_seq=4, suggesting the iOS app noticed the new thread exists but does not render it.host.keydeleted, fresh node id, fresh token, freshhost.toml).Likely root cause (hypothesis)
The Litter iOS app sends
thread/startand the bridge correctly writes the response to the iroh stream. The Litter iOS app'sRemoteAppServerClient(inshared/rust-bridge/codex-mobile-client) appears to either (a) drop the response before its internalConversationmodel updates, or (b) update the model but not push the newThreadinto the SwiftUI home list. Either way, no error is surfaced to the user.Strongest hint: the iOS app opens two iroh QUIC connections back-to-back — one for
agent=codexand one foragent=opencode, bothattached=Fresh(no resume cursor). Thethread/startis then sent on theopencodeconnection, the response is written there, and the Litter app does not send a follow-upthread/listto pull the new thread into the home list. The home list is never refreshed with the thread that was just created.Workaround
Use the upstream opencode web UI directly:
opencode weblistens on127.0.0.1:4096by default; the Litter user can hit it from the Mac (or via Tailscale on the phone). The bridge'sOPENCODE_BRIDGE_BACKEND_URL=http://127.0.0.1:4096env var makes the bridge share that same opencode process, so the threads created via "new session" in the Litter app DO appear in the opencode web UI — they're not lost, just invisible to the Litter iOS app.Related