Session-visibility replay pages by the recorded session_meta.daemonId, so a new holder never receives the tightened gates.
What is wrong
The live push path was converted and says why:
// Resolved, not read: a pool agent's gate belongs on whichever member holds it, and
// `agent.daemonId` names no machine for one — the whole session set would silently skip.
const agent = await this.deps.repos.agent.getUnscoped(s.agentId)
byAgent.set(s.agentId, agent ? await this.placement().servingDaemon(agent) : null)
(packages/control-plane/src/orchestrator/visibilityPush.ts:130)
The reconnect replay behind it was not. replayTo → replayPages reads visibilitySnapshotForDaemon and countUnackedVisibility, both of which page by the column (packages/control-plane/src/persistence/repositories/session.repo.ts:1617 and :1638):
FROM "session_meta"
WHERE "daemonId" = ${daemonId}::uuid
SessionMeta.daemonId is the daemon that first reported the session, and it is onDelete: SetNull (prisma/schema.prisma:1091), so after a rollout it either names a retired member or is NULL.
The failing scenario
A user marks a session private. The CP bumps visibilityRev and pushes to whichever member is serving it — correct today. Now that member is rolled away and the duty moves. The new holder connects and the CP replays the visibility snapshot to converge it, which is, in this file's own words, "what ultimately closes the bypass" for anything missed while a member was offline. The query returns zero rows, because no session_meta row names the new member. The new holder keeps the session's default org memory-capture gate and can extract from a session the user marked private.
The unacked counter has the same predicate, so nothing reports that the convergence did not happen: countUnackedVisibility returns 0 for the new member, and the tightening looks delivered.
Smallest correct fix
Page by the agents the daemon serves rather than by the recorded column: resolve the daemon's served agent set (placement ∪ duties it holds — the union orchestrator/placement.ts already builds for the reconcile roster) and select session_meta rows for those agents, keeping the existing "unacked first, then newest-active" ordering.
Same shape as #1019 (transcript reads routed to the session's recorded daemon), on the visibility path instead of the content path. Part of the class tracked by #955.
Session-visibility replay pages by the recorded
session_meta.daemonId, so a new holder never receives the tightened gates.What is wrong
The live push path was converted and says why:
(
packages/control-plane/src/orchestrator/visibilityPush.ts:130)The reconnect replay behind it was not.
replayTo→replayPagesreadsvisibilitySnapshotForDaemonandcountUnackedVisibility, both of which page by the column (packages/control-plane/src/persistence/repositories/session.repo.ts:1617and:1638):SessionMeta.daemonIdis the daemon that first reported the session, and it isonDelete: SetNull(prisma/schema.prisma:1091), so after a rollout it either names a retired member or is NULL.The failing scenario
A user marks a session private. The CP bumps
visibilityRevand pushes to whichever member is serving it — correct today. Now that member is rolled away and the duty moves. The new holder connects and the CP replays the visibility snapshot to converge it, which is, in this file's own words, "what ultimately closes the bypass" for anything missed while a member was offline. The query returns zero rows, because nosession_metarow names the new member. The new holder keeps the session's defaultorgmemory-capture gate and can extract from a session the user marked private.The unacked counter has the same predicate, so nothing reports that the convergence did not happen:
countUnackedVisibilityreturns 0 for the new member, and the tightening looks delivered.Smallest correct fix
Page by the agents the daemon serves rather than by the recorded column: resolve the daemon's served agent set (placement ∪ duties it holds — the union
orchestrator/placement.tsalready builds for the reconcile roster) and selectsession_metarows for those agents, keeping the existing "unacked first, then newest-active" ordering.Same shape as #1019 (transcript reads routed to the session's recorded daemon), on the visibility path instead of the content path. Part of the class tracked by #955.