Skip to content

Java SDK: add CopilotClient.createCloudSession#1398

Closed
tclem wants to merge 2 commits into
tclem/verbose-memefrom
tclem/java-create-cloud-session
Closed

Java SDK: add CopilotClient.createCloudSession#1398
tclem wants to merge 2 commits into
tclem/verbose-memefrom
tclem/java-create-cloud-session

Conversation

@tclem
Copy link
Copy Markdown
Member

@tclem tclem commented May 23, 2026

Stacked on #1394 — will be retargeted to main when that merges.

Ports createCloudSession to the Java SDK, following the same contract as the Rust (#1394) and TypeScript (#1395) implementations.

What changed

CopilotClient

  • createCloudSession(SessionConfig) — new public API; sends session.create without a caller-supplied sessionId, letting the runtime assign one (cloud-managed routing); returns CompletableFuture<CopilotSession>
  • createSession now rejects SessionConfig that has a non-null cloud field with IllegalArgumentException
  • createCloudSession rejects caller-supplied sessionId or provider

PendingRoutingState (new class)

  • Thread-safe buffer for events and parked request futures that arrive before cloud session registration completes
  • Bounded at 128 entries per session id, drop-oldest when full
  • registerAndFlush() atomically registers the session and drains buffered events/waiters inside the same lock, closing the TOCTOU race

RpcHandlerDispatcher

  • Accepts a PendingRoutingState (nullable; null = non-cloud, fast path unchanged)
  • All 7 inbound server-push handlers (tool.call, permission, user-input, exit-plan-mode, auto-mode-switch, hooks.invoke, system-message-transform) now call resolveSessionForRequest, which parks the handler on the pending state for up to 60 s when the session is still in flight
  • handleSessionEvent buffers events via pendingState.tryBufferNotification during the pending window

SessionRequestBuilder

  • buildCloudCreateRequest(SessionConfig) — omits sessionId and provider from the wire payload

Tests

10 new tests in CloudSessionTest covering all 7 contract scenarios:

  1. createCloudSession rejects caller sessionId
  2. createCloudSession rejects caller provider
  3. createCloudSession rejects null cloud
  4. createSession rejects cloud config
  5. Wire format: sessionId absent in JSON payload
  6. Pending-routing flush: events buffered before registration are delivered after
  7. Guard disposal before registration completes all waiters exceptionally

Existing RpcHandlerDispatcherTest (27 tests) updated for new 4-arg constructor; all pass.

  Generated via Copilot (Claude Sonnet 4.6) on behalf of @tclem

tclem and others added 2 commits May 22, 2026 19:35
- Add createCloudSession(SessionConfig) to CopilotClient that sends a
  session.create request without a caller-supplied sessionId, letting
  the runtime assign one (cloud-managed routing)
- Add PendingRoutingState: thread-safe buffer for events and parked
  request futures that arrive before cloud session registration; bounded
  at 128 entries per id, drop-oldest; flush on registerAndFlush()
- Add SessionRequestBuilder.buildCloudCreateRequest that omits sessionId
  and provider from the wire payload
- Update RpcHandlerDispatcher to accept PendingRoutingState and route
  all inbound server requests (tool.call, permission, user-input,
  exit-plan-mode, auto-mode-switch, hooks.invoke,
  system-message-transform) through resolveSessionForRequest so they
  park on the pending state when the session is still in flight
- Guard createSession against being called with cloud config; guard
  createCloudSession against caller-supplied sessionId or provider
- Preserve existing non-cloud rekey-on-mismatch logic unchanged
- 10 new tests in CloudSessionTest covering all 7 contract scenarios;
  fix RpcHandlerDispatcherTest constructor call for new signature

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Carries forward the Rust SDK PR #1394 review feedback into the Java port:

1. Cap the per-session inbound-request parked-waiter list at 128
   (BUFFER_LIMIT). When exceeded, evict the oldest waiter via
   completeExceptionally("pending session buffer overflow"). The
   RpcHandlerDispatcher thread blocked in waiter.get() wakes up, catches
   ExecutionException, and resolveSessionForRequest calls
   rpc.sendErrorResponse(-32603, ...) so the runtime isn't left waiting
   on a request id that will never get a reply. Mirrors Rust commit
   491b442 and TS commit c167bc3 on PR #1395.

2. decrementGuard now completes all stale waiters internally with a
   distinct message ("pending session routing ended before session was
   registered") instead of returning them for callers to complete with
   ad-hoc strings. A single canonical message lets debugging tell the
   overflow-eviction path from the create-failed path. Mirrors Rust
   commit e0ff254 and TS commit c167bc3.

3. Fix isDone() fast path in resolveSessionForRequest: the existing
   catch-all "fall through" swallowed ExecutionException from an
   overflow-evicted waiter, sending a generic -32602 "Unknown session"
   error instead of -32603. Now explicitly catches ExecutionException in
   the isDone() branch and sends the same -32603 error as the blocking
   path.

Adds two new unit tests in CloudSessionTest:
- parkedRequestWaiterBuffer_overflow_evictsOldestWithOverflowMessage:
  parks 129 waiters, verifies oldest completes with "pending session
  buffer overflow", remaining 128 resolve normally after registration.
- parkedRequestWaiter_guardDropMessage_isDistinctFromOverflowMessage:
  parks a request via the full handler path, drops the guard without
  registration, verifies the JSON-RPC error response contains "routing
  ended before session was registered" but not "buffer overflow".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tclem
Copy link
Copy Markdown
Member Author

tclem commented May 23, 2026

Consolidating into #1395 so the sdk-consistency-review workflow sees the full cross-SDK matrix in one PR. Commits from this branch are merged into that PR's branch.

  Generated via Copilot (Claude Opus 4.7) on behalf of @tclem

@tclem tclem closed this May 23, 2026
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