-
-
Notifications
You must be signed in to change notification settings - Fork 52.6k
Description
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.