Interop gap — can't share a Redis between TS and Python Chat SDK deployments.
Current state
src/chat_sdk/state/redis.py generates lock tokens with a fixed prefix: redis_<ms>_<hex> (see redis.py:30 and the _generate_token() helper).
Upstream Vercel Chat TS ships its Redis state adapter on top of the ioredis npm package and generates tokens with the prefix ioredis_<ms>_<hex>. This is the prefix that any existing TS Chat SDK deployment will have written into Redis.
Today, if a consumer wants to migrate from a TS Chat SDK deployment to this Python port — or run both side-by-side against a shared Redis during a migration — the two sides can't identify each other's tokens. Release semantics still work (tokens compare by full string equality), but:
- There's no way for our port to reject a release attempt that targets a TS-issued lock using a Python-issued token, because we only compare opaque strings — that part happens to be safe.
- Observability suffers: looking at
KEYS chat:lock:* in Redis and seeing a mix of redis_... and ioredis_... tokens gives no signal about which runtime issued each lock.
- Migrations can't do the graceful "let TS drain, then flip Python on" dance with certainty that every in-flight TS lock has a matching TS-prefixed token.
Proposed fix
Parameterize the prefix on RedisStateAdapter, and ship a thin IoRedisStateAdapter subclass for the ioredis interop case:
# src/chat_sdk/state/redis.py
class RedisStateAdapter:
def __init__(self, *, token_prefix: str = "redis", ...) -> None:
self._token_prefix = token_prefix
...
def _generate_token(self) -> str:
return f"{self._token_prefix}_{int(time.time() * 1000)}_{secrets.token_hex(16)}"
class IoRedisStateAdapter(RedisStateAdapter):
"""Use when sharing a Redis with a TypeScript Chat SDK deployment that uses ioredis.
Identical to RedisStateAdapter except lock tokens carry the ``ioredis_`` prefix,
matching the TS port byte-for-byte so tokens are origin-identifiable at a glance.
"""
def __init__(self, **kwargs: Any) -> None:
kwargs.setdefault("token_prefix", "ioredis")
super().__init__(**kwargs)
Everything else is unchanged — same Lua scripts, same key layout ({prefix}:lock:{thread_id}, {prefix}:queue:{thread_id}, …), same SET NX PX acquisition, same PEXPIRE extend, same cache semantics.
Estimate: 10–15 lines of net new code plus the subclass. Zero behavior change for anyone not opting into IoRedisStateAdapter.
What this does not solve
- Wire format of queue entries is still "message serialized via
to_json()", which differs from TS's JSON.stringify(entry) — tracked at docs/UPSTREAM_SYNC.md under Serialization differences. Cross-runtime queue sharing needs a separate effort. This issue is scoped to lock-token prefix only.
- Cross-runtime subscription storage uses the same Redis set membership on both sides and is already compatible.
Acceptance
Interop gap — can't share a Redis between TS and Python Chat SDK deployments.
Current state
src/chat_sdk/state/redis.pygenerates lock tokens with a fixed prefix:redis_<ms>_<hex>(seeredis.py:30and the_generate_token()helper).Upstream Vercel Chat TS ships its Redis state adapter on top of the
ioredisnpm package and generates tokens with the prefixioredis_<ms>_<hex>. This is the prefix that any existing TS Chat SDK deployment will have written into Redis.Today, if a consumer wants to migrate from a TS Chat SDK deployment to this Python port — or run both side-by-side against a shared Redis during a migration — the two sides can't identify each other's tokens. Release semantics still work (tokens compare by full string equality), but:
KEYS chat:lock:*in Redis and seeing a mix ofredis_...andioredis_...tokens gives no signal about which runtime issued each lock.Proposed fix
Parameterize the prefix on
RedisStateAdapter, and ship a thinIoRedisStateAdaptersubclass for theioredisinterop case:Everything else is unchanged — same Lua scripts, same key layout (
{prefix}:lock:{thread_id},{prefix}:queue:{thread_id}, …), sameSET NX PXacquisition, samePEXPIREextend, same cache semantics.Estimate: 10–15 lines of net new code plus the subclass. Zero behavior change for anyone not opting into
IoRedisStateAdapter.What this does not solve
to_json()", which differs from TS'sJSON.stringify(entry)— tracked atdocs/UPSTREAM_SYNC.mdunder Serialization differences. Cross-runtime queue sharing needs a separate effort. This issue is scoped to lock-token prefix only.Acceptance
RedisStateAdapter(..., token_prefix="ioredis")worksIoRedisStateAdaptersubclass re-exports fromchat_sdk.statedocs/ARCHITECTURE.md(or a new section) documents when to pickIoRedisStateAdapterdocs/UPSTREAM_SYNC.mdnoting the compat variant exists