Skip to content

fix: iOS chat broken — node role unauthorized + session key mismatch causes messages to vanish #6767

@echennells

Description

@echennells

Summary

The iOS (and Android) embedded chat is broken due to two server-side bugs. Node-role clients (mobile apps) cannot call any chat methods, and when that's fixed, messages vanish from the UI after the agent responds because chat.send and chat.history use different session store keys.

There is also a minor iOS-side bug where the main screen permanently shows "Connecting…" despite the gateway being connected.

Bug 1: Node role missing chat method authorization

File: src/gateway/server-methods.ts

Mobile apps connect with role: "node". The authorizeGatewayMethod function only allows node-role clients to call three methods (node.invoke.result, node.event, skills.bins). Chat methods like chat.history, chat.send, chat.abort, sessions.list, and health are all rejected with unauthorized role: node.

Fix: Add a NODE_CHAT_METHODS set and allow node-role clients to call them:

 const NODE_ROLE_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]);
+const NODE_CHAT_METHODS = new Set([
+  "health",
+  "chat.history",
+  "chat.send",
+  "chat.abort",
+  "sessions.list",
+]);
   if (role === "node") {
+    if (NODE_CHAT_METHODS.has(method)) return null;
     return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
   }

Bug 2: Session key mismatch — chat.send writes raw key, chat.history reads canonical key

File: src/gateway/server-methods/chat.ts

The iOS app sends sessionKey: "main". The chat.history handler canonicalizes this to "agent:main:main" via loadSessionEntry → resolveSessionStoreKey. But chat.send passes the raw "main" into MsgContext.SessionKey, so the agent run writes to the session store under the raw key "main", creating a separate store entry.

When the agent finishes and the iOS client calls chat.history to refresh, it reads from the canonical "agent:main:main" key — which points to a stale/different session. The UI replaces its messages with the wrong session's data, making messages appear to vanish.

Fix: Use the canonical key from loadSessionEntry for the agent dispatch, and register the chat run with the raw key so client-facing broadcasts still match:

-    const { cfg, entry } = loadSessionEntry(p.sessionKey);
+    const { cfg, entry, canonicalKey } = loadSessionEntry(p.sessionKey);
-        SessionKey: p.sessionKey,
+        SessionKey: canonicalKey,
           onAgentRunStart: () => {
             agentRunStarted = true;
+            context.addChatRun(clientRunId, {
+              sessionKey: p.sessionKey,
+              clientRunId,
+            });
           },

Bug 3: iOS reconnect loop clobbers "Connected" status

File: apps/ios/Sources/Model/NodeAppModel.swift

The reconnect loop in connectToGateway() runs every 1 second. At the top of each iteration it unconditionally resets gatewayServerName = nil and gatewayStatusText = "Connecting…". On the first pass, onConnected sets "Connected", but the next iteration immediately resets it. Since the channel is already connected, onConnected doesn't fire again, so the UI permanently shows "Connecting…".

Fix: Guard the status reset:

             while !Task.isCancelled {
                 await MainActor.run {
+                    guard !self.gatewayConnected else { return }
                     if attempt == 0 {
                         self.gatewayStatusText = "Connecting…"

Testing

All three fixes verified on a live gateway + iOS simulator. Type-check and existing tests pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions