package gateway // 本文件是 WS 下行帧角色门的唯一归属地(design/11 §7.1「WS 下行帧的 // 角色门」,主人于 2026-10-03 决策)。它是判定一个下行帧可以触达哪些 // 连接的唯一位置: // // - 类型列在 frameReachesAll 中的帧属于「状态机信号」:不含内容, // 因此每个已认证连接都能收到; // - 其余所有帧类型 —— 回复正文(message.*)、思考、工具调用与 // 结果、持久化/编辑/删除的消息、图谱更新、子代理与任务 // 流量、角色私有状态/设备状态 —— 只发 owner + admin。 // // 分类是 fail-closed 的:新帧类型在有人把它显式登记到此处、并且在 // design/11 §7.1 登记之前一律返回 false,因此漏分类某个类型 // 永远不会泄露内容。没有任何调用点自行决定可见性(按调用点传 // 标志正是「漏一处即成泄露」的来源),这也是 hub.broadcast // 从收到的帧反查类别的原因。 // // 本门只决定向谁 PUSH。数据可见性仍归 REST 门: // viewer 依然只能通过 viewerAuth 读取它的 12 个只读监控端点 // (design/11 §7.1「语义澄清」)。 // frameReachesAll 报告某个下行帧类型是否可投递给每个已认证 // 连接。只有 design/11 §7.1 白名单中的「状态机信号」符合条件: // session_busy / pending_ack / pending_drain / session_idle / // session_interrupted / pending_full / session_read。它们只报告 // 会话忙闲状态发生了变化,因此只读监控端可以跟随对话的 // 活跃度,而不必接收对话本身。 // // 扩大这个名单属于契约变更:先在 design/11 §7.1 登记理由 // (「不含内容」),并在 ws_frame_scope_test.go 中同步。 func frameReachesAll(typ string) bool { switch typ { case EventSessionBusy, EventPendingAck, EventPendingDrain, EventSessionIdle, EventSessionInterrupt, EventPendingFull, wsSessionRead: return true default: return false } }