Session Takeover: When a Second Device Connects to the Same PTY
If you’re connected to a Mac from your iPhone and then open the same connection from your iPad, what happens? The PTY session can only have one active “primary” attachment at a time. TermOnMac handles this with explicit takeover state.
The isTakenOver Flag
Each TerminalSession carries a flag indicating whether it has been taken over by another device:
// ios_remote_dev_ios_app/RemoteDevApp/Models/SessionStore.swift
struct TerminalSession: Identifiable {
let id: String
var name: String
var isReady: Bool = false
// ...
var isTakenOver: Bool = false
}
When set to true, the UI shows that the session is no longer the active receiver of PTY output — another device has taken control.
The onSessionTakenOver Callback
iOSRelayConnection exposes a callback for takeover events:
// ios_remote_dev_ios_app/RemoteDevApp/Network/iOSRelayConnection.swift
var onSessionTakenOver: ((String, Bool) -> Void)? // (sessionId, isTakenOver)
When the Mac informs the iOS that a session was taken over (or release back), this callback fires and the UI updates accordingly.
Reset on Detach
When a session is detached, the takeover flag is cleared:
// ios_remote_dev_ios_app/RemoteDevApp/Models/SessionStore.swift
func detachSession(id: String) {
guard sessions.count > 1,
let idx = sessions.firstIndex(where: { $0.id == id }) else { return }
var session = sessions.remove(at: idx)
session.isTakenOver = false
draftInputTexts.removeValue(forKey: id)
availableSessions.append(session)
}
A detached session is no longer “owned” by this device, so it cannot be in a takeover state with respect to this device.
Available Sessions on Reconnect
When the iOS app reconnects after a disconnect, the Mac sends a list of currently available sessions:
var onPtySessions: (([PTYSessionInfo]) -> Void)? // reconnect: session list
The iOS app populates availableSessions with this list. The user can then choose which sessions to attach. Sessions that are currently being viewed by another device may also appear in this list — the user can attach them, which triggers a takeover.
Multiple Devices on the Same Account
The room secret is the authorization, not the device. Multiple iOS devices logged into the same account, with the same room secret, can each attempt to connect to the same room. The relay enforces a single iOS socket per room:
// relay_server/src/room.ts (handleJoin)
// Close old iOS socket if different (allows iOS to reconnect / replace)
if (this.iosSocket && this.iosSocket !== ws) {
console.warn("[room] replacing old iosSocket with new connection");
// Eagerly clean up old socket's storage entries (same rationale as handleRegister)
const oldTags = this.state.getTags(this.iosSocket);
if (oldTags.length > 0) {
await this.state.storage.delete(`role:${oldTags[0]}`);
await this.state.storage.delete(`user:${oldTags[0]}`);
await this.state.storage.delete(`ip:${oldTags[0]}`);
await this.state.storage.delete(`clientRole:${oldTags[0]}`);
}
this.declaredRoleBySocket.delete(this.iosSocket);
try { this.iosSocket.close(4001, "replaced by new connection"); } catch {}
}
this.iosSocket = ws;
The previous iOS socket is closed with code 4001. The replaced iOS app sees a disconnect and (depending on its state) may either show “disconnected” or attempt to reconnect, which would itself trigger another replacement.
This means: at any moment, exactly one Mac and one iOS socket are connected to a given room. Multiple iOS devices on the same account can connect, but only one at a time.