package main import ( "encoding/json" "errors" "io" "net/http" "strconv" "strings" ) const ( sessionSnapshotItems = 24 sessionSnapshotBytes = 8 << 20 ) // boundSessionSnapshot caps only the continuity fallback response. Native // WebUI requests remain untouched, including when an older backend ignores // its optional `msg_limit` query parameter. The tenant WebUI answers with // {"session": {..., "messages": [...], "message_count": N}}, so the envelope // is decoded field-by-field: every key other than the message tail is relayed // verbatim rather than re-serialized from a fixed struct, which would silently // drop session metadata the poller and future WebUI releases depend on. func boundSessionSnapshot(response *http.Response) error { request := response.Request if request == nil || response.StatusCode != http.StatusOK || request.URL.Query().Get("hermes_fallback") != "1" || request.URL.Path != sessionFallbackPath { return nil } body, err := io.ReadAll(io.LimitReader(response.Body, sessionSnapshotBytes+1)) if err != nil { return err } _ = response.Body.Close() if len(body) > sessionSnapshotBytes { return errors.New("session snapshot exceeds safe response limit") } body, err = boundSessionSnapshotBody(body) if err != nil { return err } response.Body = io.NopCloser(strings.NewReader(string(body))) response.ContentLength = int64(len(body)) response.Header.Set("Content-Length", strconv.Itoa(len(body))) response.Header.Set("Content-Type", "application/json; charset=utf-8") response.Header.Set("Cache-Control", "no-store") response.Header.Del("ETag") return nil } // boundSessionSnapshotBody trims the message tail of one WebUI session payload // while preserving the total the backend reported. func boundSessionSnapshotBody(body []byte) ([]byte, error) { malformed := errors.New("session snapshot is malformed") var envelope map[string]json.RawMessage if err := json.Unmarshal(body, &envelope); err != nil { return nil, malformed } rawSession, ok := envelope["session"] if !ok { return nil, malformed } var session map[string]json.RawMessage if err := json.Unmarshal(rawSession, &session); err != nil { return nil, malformed } var messages []json.RawMessage if raw, ok := session["messages"]; ok { if err := json.Unmarshal(raw, &messages); err != nil { return nil, malformed } } total := len(messages) if raw, ok := session["message_count"]; ok { var count int if err := json.Unmarshal(raw, &count); err == nil && count > total { total = count } } if len(messages) > sessionSnapshotItems { messages = messages[len(messages)-sessionSnapshotItems:] } trimmed, err := json.Marshal(messages) if err != nil { return nil, err } session["messages"] = trimmed session["message_count"] = json.RawMessage(strconv.Itoa(total)) rawSession, err = json.Marshal(session) if err != nil { return nil, err } envelope["session"] = rawSession return json.Marshal(envelope) }