package gateway import ( "context" ) // wsReplayLimit is the number of most-recent messages replayed to a connecting // client. It is intentionally small so a reconnect is cheap; the frontend pulls // the full active-session history via REST (design/16 §4.1) and the replay is // only there to give a brand-new connection immediate context. const wsReplayLimit = 50 // replayHistory sends the most recent messages for sessionID to a single, // freshly-registered connection as a ws "history" event. // // The replay carries the main session's message bodies, so it is owner/admin only // (design/11 §7.2 A, decided by the owner on 2026-10-03). ownerAdminClaims is the // same predicate the proactive push uses, so 「who may be handed the owner's // conversation unasked」 is answered in one place instead of per caller. A viewer // connection completes the handshake with no replay at all: its read-only window // is the REST monitoring surface (design/11 §7.1 「语义澄清」), not a push. // // The gate lives here rather than at the register/replay call site in // ws_hub_conn.go on purpose: this is the only function that emits the replay, so // no future caller can reach it without the check. A role the owner later wants // to serve the replay to has to be registered in design/11 §7.2 first, exactly // like an inbound frame (see ws_route.go). // // It is best-effort: a nil store or a read error skips the replay silently. // Thinking is NOT included (design/16 §6), so the frame never leaks // chain-of-thought even to owner/admin. func (h *hub) replayHistory(c *wsConn, sessionID string) { if !ownerAdminClaims(c.claims) { return } if h.history == nil { return } msgs, err := h.history.Recent(context.Background(), sessionID, wsReplayLimit) if err != nil { h.log.Warn("history replay", "err", err, "session", sessionID) return } out := make([]historyMessage, 0, len(msgs)) for _, m := range msgs { wire := toHistoryMessage(m) wire.Thinking = "" out = append(out, wire) } c.enqueueJSON(wsEvent{Type: "history", SessionID: sessionID, Data: map[string]any{"messages": out}}) }