feat(gateway): watched sessions in presence with viewer facepiles#111225
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b821e587a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| upsertPresence(client.presenceKey, { | ||
| watchedSessions: watchedSessions.length > 0 ? watchedSessions : undefined, | ||
| }); |
There was a problem hiding this comment.
Redact watched session keys from unscoped presence
Presence broadcasts are intentionally delivered to operator.pairing and node-role connections (src/gateway/server-broadcast.ts:52), while session discovery and subscription require operator.read (src/gateway/methods/core-descriptors.ts:194-198). Adding the raw keys here bypasses that boundary: a pairing-only client or node can observe watched keys such as agent:main:whatsapp:direct:+1555..., exposing agent, channel, group, and peer identifiers it cannot otherwise list. Emit watchedSessions only in a read-scoped/redacted presence projection rather than the globally visible snapshot.
Useful? React with 👍 / 👎.
| protected presenceViewersForSession(sessionKey: string): readonly PresenceViewer[] { | ||
| return this.onlinePresenceViewers.filter( | ||
| (viewer) => | ||
| viewer.id !== this.selfPresenceUserId && viewer.watchedSessions.includes(sessionKey), | ||
| ); |
There was a problem hiding this comment.
Match scoped global subscription keys to sidebar rows
When the selected session is global, the gateway records the subscription as agent:<agentId>:global (src/gateway/server-methods/sessions-subscriptions.ts:28-31) but returns and lists the canonical row key as global. This exact includes(sessionKey) comparison therefore never matches for global-session sidebar rows, so other viewers of a global session are absent from its facepile even though their presence advertises the scoped key. Normalize or compare these keys with the selected agent scope before filtering.
Useful? React with 👍 / 👎.
|
Codex review: needs real behavior proof before merge. Reviewed July 19, 2026, 5:46 AM ET / 09:46 UTC. Summary PR surface: Source +549, Tests +156, Other +4. Total +709 across 21 files. Reproducibility: yes. from source inspection: connect a pairing-only or node-role recipient, have another client subscribe to a sensitive session key, and observe the presence snapshot; the current changed path inserts and broadcasts that key without a shown recipient read-scope projection. Review metrics: 1 noteworthy metric.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Project Do we have a high-confidence way to reproduce the issue? Yes from source inspection: connect a pairing-only or node-role recipient, have another client subscribe to a sensitive session key, and observe the presence snapshot; the current changed path inserts and broadcasts that key without a shown recipient read-scope projection. Is this the best way to solve the issue? No. Subscription membership is not a reliable active-view signal, and a globally broadcast raw key is not the narrowest safe representation; scoped presence projection plus explicit active-view semantics is safer. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against d4ed08994d31. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +549, Tests +156, Other +4. Total +709 across 21 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (3 earlier review cycles)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85ba6a0c2c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const presenceKey = shouldTrackPresence | ||
| ? role === "node" | ||
| ? (device?.id ?? instanceId ?? connId) | ||
| : connId |
There was a problem hiding this comment.
Preserve device connectivity across reconnect presence rows
When a paired non-node client reconnects or has multiple tabs, keying presence by connId leaves several rows with the same deviceId, including disconnect rows retained for five minutes. listSystemPresence() returns newest-first, but both ui/src/pages/nodes/nodes-page.ts:59-70 and ui/src/components/app-sidebar-session-catalog-live.ts:286-300 overwrite device state while iterating, so an older disconnect row can win over the live row. The reconnect then appears to have no connectivity transition, preventing the immediate device/node and session-catalog refresh and leaving the UI stale until polling. Preserve a device-level aggregate or make every consumer explicitly reduce multiple connection rows so any live connection wins.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 887e7024ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const presenceWatchedSessions = (connId: string): string[] => { | ||
| // Presence snapshots stay small even if a long-lived client accumulates | ||
| // subscriptions. Keep only the 32 most recently subscribed session keys. | ||
| return [...sessionMessageSubscribers.getForConnection(connId)].slice(-32).toSorted(); |
There was a problem hiding this comment.
Track the active native session instead of every subscription
On trusted-proxy or Tailscale gateways where native iOS/macOS clients receive a user identity, this projects every accumulated message subscription as actively watched. Both native setActiveSessionKey implementations only call subscribe and never unsubscribe the previous key (apps/ios/Sources/Chat/IOSGatewayChatTransport.swift:300-305, apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift:625-633), so after switching sessions the user remains in facepiles for every session visited during that connection, up to this 32-key cap. Track an explicit active key or update the native subscription lifecycle before treating these keys as viewer presence.
Useful? React with 👍 / 👎.
| instanceId: role === "node" ? (device?.id ?? instanceId) : instanceId, | ||
| ...(authenticatedUserId | ||
| ? { user: { id: authenticatedUserId, email: authenticatedUserId } } | ||
| : {}), |
There was a problem hiding this comment.
Broadcast presence when an identified connection comes online
When an identified Control UI client connects directly to a non-chat route, existing clients never learn that it is online: this connect block updates the presence map but only increments the version, while the sidebar consumes hello and presence events and message subscription broadcasts occur only after the chat page calls sessions.messages.subscribe. Consequently the new footer facepile stays stale until that user visits chat or an unrelated presence change happens; broadcast the connect-time snapshot after inserting the per-connection row.
Useful? React with 👍 / 👎.
887e702 to
a6d0efb
Compare
a6d0efb to
5fecb08
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fecb08fa1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <openclaw-viewer-facepile | ||
| .presencePayload=${this.presencePayload} | ||
| .selfInstanceId=${this.presenceInstanceId} | ||
| .sessionKey=${session.key} | ||
| .maxVisible=${3} | ||
| variant="session" | ||
| ></openclaw-viewer-facepile> |
There was a problem hiding this comment.
Show viewers on the main-session Home row
This adds the session facepile only to renderRecentSession, but selectedAgentSessionRows deliberately removes the main session from that list (ui/src/components/app-sidebar-session-navigation.ts:560-571) and renderHomeRow has no equivalent facepile. Consequently, when another identified user watches the agent's main session—the default and most prominent conversation—the sidebar shows no viewers on Home even though presence advertises them. Render the same facepile on the Home row using mainKey.
Useful? React with 👍 / 👎.
|
Merged via squash.
|
…enclaw#111225) * feat(gateway): expose watched sessions in presence * fix(gateway): resolve concurrent watched-session rollbacks via in-flight state * fix(ui): viewing CI conformance — lazy facepile, bindings, dead exports * fix(ui): move viewer presence projection out of the startup chunk * fix(ui): capture sessionKey before facepile filter closure
What Problem This Solves
On a shared gateway you cannot see who else is online or which session they are looking at — collaboration is blind. The gateway already knows both facts: presence entries carry identity (#111179), and per-session event subscriptions tell it what each connection watches; neither was exposed.
Part of the multiplayer-identity program: #111133 (who's-viewing PR of the train).
Why This Change Was Made
watchedSessionsarray (sorted for deterministic snapshots), maintained from the existing session-scoped event subscription lifecycle — no new client→gateway messages. Recency-capped at 32 sessions per connection to keep snapshots small.viewer-facepilecomponent renders (a) up to 3 overlapping viewer avatars ++Non each sidebar session row for other identified viewers, and (b) a global online facepile in the sidebar footer bar (Zone 5) showing everyone identified online — one circle per person regardless of connection count (grouped byuser.id), self included, initials + deterministic color fallback,avatarUrlwins when set.User Impact
Shared-gateway teams see who is online (footer facepile) and who is viewing which session (row avatars), Google-Docs style. Single-user gateways look exactly as before.
Evidence
tbx_01kxwace801n9pche7p953svgp: 25 gateway, 3 protocol, 134 sidebar tests green; core/UI production + test type gates green.node scripts/run-vitest.mjs src/gateway/server-chat-state.test.ts src/gateway/session-message-events.test.ts src/gateway/server-methods/sessions.messages-subscribe-approvals.test.ts— green locally, including both concurrent-rollback failure orders, mixed failure/success orders, cap-at-32 recency, unsubscribe/disconnect cleanup, anonymous reporting.