Skip to content

feat: responses api server-side state management#87

Merged
bzp2010 merged 2 commits intomainfrom
bzp/feat-server-side-state-management
May 4, 2026
Merged

feat: responses api server-side state management#87
bzp2010 merged 2 commits intomainfrom
bzp/feat-server-side-state-management

Conversation

@bzp2010
Copy link
Copy Markdown
Collaborator

@bzp2010 bzp2010 commented May 4, 2026

Summary by CodeRabbit

  • New Features

    • Optional server-side session persistence for conversation history
    • Configurable session storage via a new builder option
    • Deterministic ordering of sessions when timestamps tie, preserving insertion order
    • Validation handling when referenced prior responses are not found
  • Tests

    • Expanded coverage for saving/restoring sessions, tie-breaking order, and validation scenarios

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Gateway gains optional server-side session persistence: a SessionStore trait and in-memory implementation, wiring to load prior messages into outgoing Responses API chat requests and persist hub responses back into the store, plus tests and helper logic to handle previous_response_id and conversation-id lookups.

Changes

Server-Side Session Persistence

Layer / File(s) Summary
Data Shape
src/gateway/session.rs
StoredSession adds pub insertion_index: u64. SessionStore trait signature and stored session fields (response_id, conversation_id, messages, model, created_at, metadata) are present.
In-Memory Store Implementation
src/gateway/session.rs
InMemorySessionStore adds next_insertion_index: AtomicU64. put_session sets/retains insertion_index; get_by_conversation_id sorts by (created_at, insertion_index).
Gateway Surface
src/gateway/gateway.rs, src/gateway/mod.rs
Gateway gets session_store: Option<Arc<dyn SessionStore>> and pub fn with_session_store(...). pub mod session; exported.
Control Flow Integration
src/gateway/gateway.rs
For ResponsesApiFormat in generic chat, resolve_session(...) splices stored messages into outgoing hub ChatCompletionRequest (respecting synthetic responses-instructions), and after a non-native hub response save_session(...) persists a StoredSession derived from request/response and OpenAIResponsesExtras.
Helpers / Validation
src/gateway/gateway.rs
Helpers to load session by previous_response_id (returning GatewayError::Validation when not found), by conversation id via OpenAIResponsesExtras, detect/drop synthetic responses-instructions when building stored history, and compute stored-session metadata.
Tests
src/gateway/session.rs, src/gateway/gateway.rs
Session store unit tests updated to set insertion_index; new tests for saving/resolving sessions via previous_response_id, dropping synthetic responses-instructions from stored history, and validation on unknown previous_response_id.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Gateway
    participant SessionStore
    participant Hub
    Client->>Gateway: send Responses API chat request (maybe with previous_response_id)
    alt previous_response_id or conversation_id present
        Gateway->>SessionStore: get_by_response_id / get_by_conversation_id
        SessionStore-->>Gateway: StoredSession(s) (messages, metadata)
        Gateway->>Gateway: splice stored messages into hub ChatCompletionRequest
    end
    Gateway->>Hub: forward ChatCompletionRequest
    Hub-->>Gateway: Hub ChatCompletionResponse (id, model, created, messages)
    alt non-native hub response
        Gateway->>SessionStore: put_session(StoredSession derived from request+response)
        SessionStore-->>Gateway: ack
    end
    Gateway-->>Client: aggregated response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Security Check ❌ Error PR introduces critical security vulnerabilities: plaintext storage of sensitive conversation data, missing authorization checks allowing unauthorized session access, and absent multi-tenancy isolation. Implement encryption at rest, add explicit ownership validation in SessionStore methods, include user_id/tenant_id fields in StoredSession, and enforce authorization checks before returning sessions.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: responses api server-side state management' accurately summarizes the main change—adding server-side session persistence to the responses API via SessionStore integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
E2e Test Quality Review ✅ Passed PR includes comprehensive E2E tests with real HTTP servers, session persistence validation, and message history reconstruction. Production code exhibits zero unwrap() calls, proper error handling via Result<>, explicit error scenario testing, and concurrency safety through RwLock and AtomicU64.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bzp/feat-server-side-state-management

Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/gateway/gateway.rs (1)

89-103: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Persist streamed Responses API turns before returning the stream.

For ResponsesApiFormat with stream: true, this branch returns at Line 94 before save_session() at Lines 101-102 can run. Streamed turns therefore never enter SessionStore, so a later previous_response_id lookup for a streamed response will fail even though the non-streaming path works.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/gateway.rs` around lines 89 - 103, For ResponsesApiFormat with
stream=true the code returns the BridgedStream before persisting the turn, so
streamed turns never get saved; fix by ensuring save_session is invoked for
streamed ResponsesApiFormat before returning the ChatResponse::Stream.
Concretely, in the stream branch (where call_chat_hub_stream, BridgedStream::new
and ChatResponse::Stream are used) detect F::name() ==
ResponsesApiFormat::name() and call the same persistence logic (reuse
save_session or introduce a small helper that accepts the stream/metadata) prior
to the return, or else arrange for BridgedStream to invoke save_session on the
first response chunk; reference call_chat_hub_stream, BridgedStream,
ChatResponse::Stream, save_session and F::name()/ResponsesApiFormat to locate
where to add the persistence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gateway/session.rs`:
- Around line 38-48: get_by_conversation_id currently sorts only by created_at
so ties leave order nondeterministic; fix by adding a deterministic tie-breaker
or returning the latest session directly. Add a monotonic insertion/order key
(e.g., insertion_index: u64) to StoredSession and increment it when sessions are
inserted (update the session creation/insert path where sessions are added),
then in get_by_conversation_id sort by (created_at, insertion_index) or use
max_by_key((created_at, insertion_index)) to pick latest; alternatively
implement a helper latest_by_conversation that finds the max_by_key((created_at,
insertion_index)) and use that in gateway.rs instead of assuming the last
element. Ensure you update any constructors and places that clone/create
StoredSession to populate the new insertion_index.

---

Outside diff comments:
In `@src/gateway/gateway.rs`:
- Around line 89-103: For ResponsesApiFormat with stream=true the code returns
the BridgedStream before persisting the turn, so streamed turns never get saved;
fix by ensuring save_session is invoked for streamed ResponsesApiFormat before
returning the ChatResponse::Stream. Concretely, in the stream branch (where
call_chat_hub_stream, BridgedStream::new and ChatResponse::Stream are used)
detect F::name() == ResponsesApiFormat::name() and call the same persistence
logic (reuse save_session or introduce a small helper that accepts the
stream/metadata) prior to the return, or else arrange for BridgedStream to
invoke save_session on the first response chunk; reference call_chat_hub_stream,
BridgedStream, ChatResponse::Stream, save_session and
F::name()/ResponsesApiFormat to locate where to add the persistence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 933d373f-26bf-4634-b09d-ab9daeee4b81

📥 Commits

Reviewing files that changed from the base of the PR and between 342afdd and 11ac38f.

📒 Files selected for processing (3)
  • src/gateway/gateway.rs
  • src/gateway/mod.rs
  • src/gateway/session.rs

Comment thread src/gateway/session.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/gateway/gateway.rs (1)

90-104: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Responses streaming bypasses session persistence.

At Line 90 the stream branch returns early, so save_session (Line 102+) is never reached for streamed Responses requests. That makes server-side history incomplete and can cause previous_response_id continuity to fail after streamed turns.

🧭 Possible short-term safety guard (until stream persistence is implemented)
         if stream {
+            if F::name() == ResponsesApiFormat::name() && self.session_store.is_some() {
+                return Err(GatewayError::Validation(
+                    "responses streaming with server-side session state is not yet supported"
+                        .into(),
+                ));
+            }
             let hub_stream = self.call_chat_hub_stream(instance, &hub_request).await?;
             let (usage_tx, usage_rx) = oneshot::channel();
             let bridged_stream = BridgedStream::<F>::new(hub_stream, ctx, usage_tx);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/gateway.rs` around lines 90 - 104, The stream branch currently
returns early from call_chat_hub_stream so save_session is never called for
streamed Responses; fix by making the bridged stream surface the final hub
response and persisting it when the stream completes: change BridgedStream
(constructed in call_chat_hub_stream) to accept/emit a completion channel (e.g.,
oneshot or mpsc) that yields the final hub_response, then in the gateway before
returning ChatResponse::Stream spawn a background task that awaits that
completion channel and calls self.save_session(&ctx, &hub_request,
&hub_response). Keep the existing call_chat_hub path unchanged (it continues to
call save_session directly) and ensure the new task only runs when F::name() ==
ResponsesApiFormat::name().
🧹 Nitpick comments (1)
src/gateway/session.rs (1)

14-38: ⚡ Quick win

Add API docs and make ordering semantics explicit for SessionStore.

SessionStore::get_by_conversation_id is consumed as an ordered list (latest inferred by taking the last item in src/gateway/gateway.rs), but that contract is not documented on the public trait. Please add /// docs on the public trait/structs and explicitly define expected ordering for returned sessions.

📝 Proposed doc contract update
 #[allow(dead_code)]
 #[async_trait]
+/// Persists and retrieves chat sessions for server-side conversation state.
 pub trait SessionStore: Send + Sync + 'static {
     async fn get_by_response_id(&self, response_id: &str) -> Result<Option<StoredSession>>;
+    /// Returns all sessions for a conversation in ascending order by recency
+    /// (oldest first, newest last).
     async fn get_by_conversation_id(&self, conv_id: &str) -> Result<Vec<StoredSession>>;
     async fn put_session(&self, session: &StoredSession) -> Result<()>;
     async fn delete_session(&self, response_id: &str) -> Result<()>;
 }
 
 #[allow(dead_code)]
 #[derive(Debug, Clone, Default)]
+/// Stored conversation snapshot keyed by provider response id.
 pub struct StoredSession {

As per coding guidelines **/*.rs: Use /// for doc comments on public items in Rust.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/session.rs` around lines 14 - 38, Add /// doc comments to the
public SessionStore trait and its methods (especially get_by_conversation_id),
and to the StoredSession and InMemorySessionStore types, explaining the
behaviour and ordering contract: get_by_conversation_id must return sessions in
chronological order from oldest to newest (so the last element is the latest
session as relied on by the gateway), document what StoredSession fields
represent (response_id, conversation_id, messages, model, created_at,
insertion_index, metadata) and note that insertion_index is monotonic for
tie-breaking; update InMemorySessionStore docs to state it preserves that
ordering semantics. Keep comments short and use /// doc style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/gateway/gateway.rs`:
- Around line 90-104: The stream branch currently returns early from
call_chat_hub_stream so save_session is never called for streamed Responses; fix
by making the bridged stream surface the final hub response and persisting it
when the stream completes: change BridgedStream (constructed in
call_chat_hub_stream) to accept/emit a completion channel (e.g., oneshot or
mpsc) that yields the final hub_response, then in the gateway before returning
ChatResponse::Stream spawn a background task that awaits that completion channel
and calls self.save_session(&ctx, &hub_request, &hub_response). Keep the
existing call_chat_hub path unchanged (it continues to call save_session
directly) and ensure the new task only runs when F::name() ==
ResponsesApiFormat::name().

---

Nitpick comments:
In `@src/gateway/session.rs`:
- Around line 14-38: Add /// doc comments to the public SessionStore trait and
its methods (especially get_by_conversation_id), and to the StoredSession and
InMemorySessionStore types, explaining the behaviour and ordering contract:
get_by_conversation_id must return sessions in chronological order from oldest
to newest (so the last element is the latest session as relied on by the
gateway), document what StoredSession fields represent (response_id,
conversation_id, messages, model, created_at, insertion_index, metadata) and
note that insertion_index is monotonic for tie-breaking; update
InMemorySessionStore docs to state it preserves that ordering semantics. Keep
comments short and use /// doc style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84bbb68c-a594-43bf-a8b7-68495a86d7d0

📥 Commits

Reviewing files that changed from the base of the PR and between 11ac38f and 25ffca7.

📒 Files selected for processing (2)
  • src/gateway/gateway.rs
  • src/gateway/session.rs

@bzp2010
Copy link
Copy Markdown
Collaborator Author

bzp2010 commented May 4, 2026

Responses streaming bypasses session persistence.

@coderabbitai Create an issue for this: we should implement a stream accumulator to ensure that messages in streaming responses can also be managed by the server.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@bzp2010 bzp2010 merged commit 0359219 into main May 4, 2026
3 checks passed
@bzp2010 bzp2010 deleted the bzp/feat-server-side-state-management branch May 4, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant