Skip to content

app-server: Replay pending item requests on thread/resume#12560

Open
euroelessar wants to merge 4 commits intomainfrom
ruslan/thread-resume-requests
Open

app-server: Replay pending item requests on thread/resume#12560
euroelessar wants to merge 4 commits intomainfrom
ruslan/thread-resume-requests

Conversation

@euroelessar
Copy link
Contributor

@euroelessar euroelessar commented Feb 23, 2026

Replay pending client requests after thread/resume and emit resolved notifications when those requests clear so approval/input UI state stays in sync after reconnects and across subscribed clients.

Affected RPCs:

  • item/commandExecution/requestApproval
  • item/fileChange/requestApproval
  • item/tool/requestUserInput

Motivation:

  • Resumed clients need to see pending approval/input requests that were already outstanding before the reconnect.
  • Clients also need an explicit signal when a pending request resolves or is cleared so stale UI can be removed on turn start, completion, or interruption.

Implementation notes:

  • Track pending client requests in ThreadState and replay them after thread/resume attaches the connection.
  • Reuse the original JSON-RPC request id for replays and resend prerequisite notifications like item/started for pending file change approvals.
  • Emit item/commandExecution/approvalResolved, item/fileChange/approvalResolved, and item/tool/requestUserInputResolved when pending requests are answered or cleared by lifecycle cleanup.
  • Update the app-server protocol schema, generated TypeScript bindings, and README docs for the replay/resolution flow.

High-level test plan:

  • Added automated coverage for replaying pending command execution and file change approval requests on thread/resume.
  • Added automated coverage for resolved notifications in command approval, file change approval, request_user_input, turn start, and turn interrupt flows.
  • Verified schema/docs updates in the relevant protocol and app-server tests.

Manual testing:

  • Tested reconnect/resume with multiple connections.
  • Confirmed state stayed in sync between connections.

@etraut-openai etraut-openai added the oai PRs contributed by OpenAI employees label Feb 23, 2026
@euroelessar euroelessar force-pushed the ruslan/thread-resume-requests branch from d0a1dc7 to 593d7d9 Compare February 26, 2026 02:06
Replay pending client requests after `thread/resume` and emit resolved notifications when those requests clear so approval/input UI state stays in sync after reconnects and across subscribed clients.

Affected RPCs:
- `item/commandExecution/requestApproval`
- `item/fileChange/requestApproval`
- `item/tool/requestUserInput`

Motivation:
- Resumed clients need to see pending approval/input requests that were already outstanding before the reconnect.
- Clients also need an explicit signal when a pending request resolves or is cleared so stale UI can be removed on turn start, completion, or interruption.

Implementation notes:
- Track pending client requests in `ThreadState` and replay them after `thread/resume` attaches the connection.
- Reuse the original JSON-RPC request id for replays and resend prerequisite notifications like `item/started` for pending file change approvals.
- Emit `item/commandExecution/approvalResolved`, `item/fileChange/approvalResolved`, and `item/tool/requestUserInputResolved` when pending requests are answered or cleared by lifecycle cleanup.
- Update the app-server protocol schema, generated TypeScript bindings, and README docs for the replay/resolution flow.

High-level test plan:
- Added automated coverage for replaying pending command execution and file change approval requests on `thread/resume`.
- Added automated coverage for resolved notifications in command approval, file change approval, request_user_input, turn start, and turn interrupt flows.
- Verified schema/docs updates in the relevant protocol and app-server tests.

Manual testing:
- Not run.
@euroelessar euroelessar force-pushed the ruslan/thread-resume-requests branch from 593d7d9 to 60ee51e Compare February 26, 2026 05:32
CommandExecutionApprovalResolved => "item/commandExecution/approvalResolved" (v2::CommandExecutionApprovalResolvedNotification),
FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification),
FileChangeApprovalResolved => "item/fileChange/approvalResolved" (v2::FileChangeApprovalResolvedNotification),
ToolRequestUserInputResolved => "item/tool/requestUserInputResolved" (v2::ToolRequestUserInputResolvedNotification),
Copy link
Collaborator

Choose a reason for hiding this comment

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

ooc, why do we need these? can't clients resolve things based on receiving item/completed?

Copy link
Collaborator

Choose a reason for hiding this comment

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

i.e. server will send:

  • item/started notification for file change / command execution / etc.
  • item/commandExecution/requestApproval request
  • a client responds
  • item/completed notification

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. we don't have item/started/item/completed events for user input at all, as there is no corresponding item, iiuc
  2. in case of file changes, they are synthesized, but not always? e.g. I don't think we never emit item/completed in cases like turn interruption and such (which could be a bug); also harness is allowed to send multiple requests for the same item, so there is no one-to-one mapping between requests & items for file changes approvals in particular, it's many-to-one

due to points above it's just easier (from protocol perspective & client-side consumption) and more consistent to have dedicated events

as a side note, it should be fine to merge these events into just single one if you prefer, I've only split them to mirror having multiple independent rpcs

Copy link
Collaborator

@owenlin0 owenlin0 Feb 26, 2026

Choose a reason for hiding this comment

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

ah ok, we might want to add an item for representing the user input tool call. I had to do that recently for dynamic tool calls: #12732

but you're right, we're going to move to a world soon where there are multiple approvals for a CommandExecution so we can't rely on item/completed for that either. proceed :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

naming nit: since our requests are like item/commandExecution/requestApproval should we name these notifications item/commandExecution/requestApproval/resolved?

Copy link
Collaborator

Choose a reason for hiding this comment

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

basically just appending /resolved to the corresponding server method name

notifications_before_request: &[ServerNotification],
) -> bool {
// Hold the callback map lock until the replay is queued so an already-resolved
// request cannot be replayed to a resumed client.
Copy link
Contributor

Choose a reason for hiding this comment

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

stresses me out - prefer to make this not needed for correctness, or use try_send
I don't fully understand what race this prevents - even if we send an already-resolved request event that should be ok right? since the client will observe the resolution as well right after (due to ordering guarantee provided by the thread listener task)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I agree, we already serialize resolution properly so should be fine, simplified

Copy link
Contributor

@maxj-oai maxj-oai left a comment

Choose a reason for hiding this comment

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

awesome, ty!

} = event;
match msg {
EventMsg::TurnStarted(_) => {
abort_pending_client_requests(conversation_id, &thread_state, &outgoing).await;
Copy link
Collaborator

@owenlin0 owenlin0 Feb 26, 2026

Choose a reason for hiding this comment

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

interesting, is this just out of abundance of caution that we're doing this? why is this necessary here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

btw there's also a cancel_requests_for_thread method on OutgoingMessageSender, can we reuse that?

pub(crate) struct ThreadState {
pub(crate) pending_interrupts: PendingInterruptQueue,
pub(crate) pending_rollbacks: Option<ConnectionRequestId>,
pending_client_requests: Vec<ServerRequest>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to track this here? we already have tracking on OutgoingMessageSender request_id_to_callback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

oai PRs contributed by OpenAI employees

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants