# Fix "'ClusterPipeline' object has no attribute 'raise_first_error'" with OpenAI Agents SDK RedisSession on Redis Cluster

*Disclosure: I'm Mohammed, founder of MemorySync. I drafted this with AI assistance from the public GitHub issue and the SDK source. Code status: on Oct 25, 2026 I installed openai-agents 0.24.0 and redis 8.1.0 (Python 3.13). I started a local 3-node Redis Cluster (Redis 8.0.2), confirmed that `ClusterPipeline` lacks the hooked attributes, reproduced the new `TypeError` from `add_items` and `clear_session` with a `RedisCluster` client, and ran the custom session below against that cluster, including two `Runner.run` turns with a stand-in model (no OpenAI key, so no real LLM call). I checked the hash-slot math with redis-py's `key_slot`.*

## The error

With the OpenAI Agents SDK's Redis-backed session and a cluster client (`redis.asyncio.cluster.RedisCluster`), storing history fails:

```text
File "agents/extensions/memory/redis_session.py", line 483, in _write_items_attempt
    raise_first_error = pipe.raise_first_error
AttributeError: 'ClusterPipeline' object has no attribute 'raise_first_error'
```

This is [openai/openai-agents-python#5322](https://github.com/openai/openai-agents-python/issues/5322), still open as of Oct 25, 2026. The reporter saw it from 0.20.0 through 0.23.1. Per the issue, `add_items` and (on latest) `clear_session` fail, `get_items` and `pop_item` work, and 0.14.5 worked for all four.

On **0.24.0** you'll get a clearer error instead. I reproduced this against a local Redis Cluster:

```text
TypeError: RedisSession does not support transactions with RedisCluster clients. Use redis.asyncio.Redis connected to a standalone Redis server, or implement a custom Session backend for Redis Cluster.
```

In 0.24.0 the check sits in the write path, not in `__init__`. Constructing the session succeeds, and the first write raises. That comes from PR #5361 ("scope Redis Cluster rejection to transactional writes"), which the issue links and which was merged on Oct 14, 2026. On the cluster, `get_items` and `pop_item` still worked on 0.24.0, and `add_items` and `clear_session` raised the `TypeError`.

## Why it happens

The issue identifies two causes:

1. **Pipeline internals.** The write hooks `pipe.raise_first_error` and `pipe.parse_response`, which exist on the standalone `redis.asyncio.client.Pipeline` but not on `ClusterPipeline`. I checked: `hasattr(ClusterPipeline, "raise_first_error")` is `False`, and it's `True` for `Pipeline`.
2. **Cross-slot transaction.** The write is a `WATCH`/`MULTI` transaction over three keys: `{prefix}:{id}`, `{prefix}:{id}:messages` and `{prefix}:{id}:counter`. Those keys hash to different cluster slots, and Redis Cluster only runs a transaction within one slot. So adding the missing hooks wouldn't be enough. With redis-py's `key_slot`, `repro:s1` and `repro:s1:messages` landed in slots 7598 and 11344.

## Fix options

**A. Point `RedisSession` at a standalone (non-cluster) Redis.** This is what the SDK's error message suggests. It's simplest if your provider offers a non-cluster mode.

**B. Pin `openai-agents==0.14.5`.** The issue reports that it worked on a cluster, and on my local cluster (with redis 8.1.0) add, get, pop and clear all ran. You'd give up every later fix, though. Use this only as a stopgap.

**C. Write a small cluster-safe Session.** The SDK's sessions are a four-method interface (`get_items`, `add_items`, `pop_item`, `clear_session`). Use one list key with a Redis Cluster **hash tag** (`{...}`), so everything for a session lives in one slot and needs no transaction:

```python
import json
from agents.memory import SessionABC


class ClusterRedisSession(SessionABC):
    """Session history on Redis Cluster: one list key, hash-tagged by session id."""

    def __init__(self, session_id: str, redis_client, key_prefix: str = "agents"):
        self.session_id = session_id
        self._redis = redis_client
        # {...} is a hash tag: every key for this session maps to one slot.
        self._key = f"{key_prefix}:{{{session_id}}}:messages"

    async def get_items(self, limit: int | None = None):
        start = -limit if limit else 0
        raw = await self._redis.lrange(self._key, start, -1)
        return [json.loads(r) for r in raw]

    async def add_items(self, items):
        if items:
            await self._redis.rpush(self._key, *[json.dumps(i) for i in items])

    async def pop_item(self):
        raw = await self._redis.rpop(self._key)
        return json.loads(raw) if raw else None

    async def clear_session(self):
        await self._redis.delete(self._key)
```

Use it like any other session:

```python
from redis.asyncio.cluster import RedisCluster
from agents import Agent, Runner

client = RedisCluster(host="your-cluster-host", port=6379)
session = ClusterRedisSession("user-123:chat-1", client, key_prefix="agents")
result = await Runner.run(agent, "Hi again", session=session)
```

What I verified on the local 3-node cluster: add/get (including `limit=1` returning the newest item), pop and clear all behaved as expected. Two `Runner.run` turns with a stand-in model stored both user and assistant items, and the second turn's model input included the first turn's history. `repro:{s1}:messages` and `repro:{s1}:counter` both hash to slot 15224. I didn't make a real LLM call or test with concurrent writers.

Trade-offs: `RPUSH` is atomic for a single call, but this skips the SDK's WATCH-based protections and its `session_settings` handling. Concurrent writers to the *same* session interleave in arrival order. The SDK docstring says third-party libraries should implement the `Session` protocol rather than subclass `SessionABC`. Subclassing is fine in your own app code. `SessionABC` declares `session_settings` with a default of `None`, so this class doesn't need to set it.

## A note on what a session is (and isn't)

A session replays one conversation's history. It doesn't give a user memory across different sessions. That's a separate layer, whether you use the SDK's other memory features or a hosted store. MemorySync has an [OpenAI Agents SDK guide](https://docs.memorysync.io/guides/openai-agents?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_openai-agents-redissession-redis-cluster-error) for that cross-session layer. It doesn't replace your Redis session store.

## Common questions

**Will upgrading fix it?** As of 0.24.0, no. You get a clear `TypeError` instead of the `AttributeError`. Watch #5322 for real cluster support. The reporter proposed hash-tagged keys plus a single-node WATCH/MULTI.

**Why do `get_items` and `pop_item` still work?** According to the issue, only the transactional write paths use the hooked pipeline.

**Can I just hash-tag the prefix I pass to `RedisSession`?** The key layout is `{prefix}:{id}...`. A tag in the prefix would put all sessions in one slot, which concentrates load on one node. It also wouldn't fix the missing pipeline attributes, because 0.24.0 rejects any `RedisCluster` client on write.

**Managed Redis only offers cluster mode. Is option C safe for production?** It's a small, readable store, but test it under your real concurrency on a staging cluster first.

Curious what cross-session memory looks like next to session history? **[Try MemorySync in the browser](https://memorysync.io/try?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_openai-agents-redissession-redis-cluster-error)**. No signup is needed.

## Sources

- Issue #5322: https://github.com/openai/openai-agents-python/issues/5322
- OpenAI Agents SDK, Sessions: https://openai.github.io/openai-agents-python/sessions/
- Redis Cluster spec, hash tags: https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/
- MemorySync OpenAI Agents SDK guide: https://docs.memorysync.io/guides/openai-agents
