Skip to content

TS, Python, Go, .NET, Java SDKs: add createCloudSession#1395

Draft
tclem wants to merge 18 commits into
tclem/verbose-memefrom
tclem/tclem-ts-create-cloud-session
Draft

TS, Python, Go, .NET, Java SDKs: add createCloudSession#1395
tclem wants to merge 18 commits into
tclem/verbose-memefrom
tclem/tclem-ts-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 the Rust SDK's Client::create_cloud_session contract (PR #1394) to the remaining five SDKs: TypeScript, Python, Go, .NET, and Java.

What each SDK gains

A new entry point — createCloudSession / create_cloud_session / CreateCloudSession / CreateCloudSessionAsync — that creates a cloud-managed session whose ID is assigned by the runtime. Caller-supplied sessionId or provider is rejected up front. session.create is sent over the wire without a sessionId field. The non-cloud createSession path now rejects cloud configs so the two paths are clearly separated. Java's existing rekey-on-mismatch behavior on createSession is preserved for the non-cloud path (potentially load-bearing for older CLIs); the new contract applies to createCloudSession only.

The pending-routing buffer

Because the runtime may emit session.event notifications and inbound JSON-RPC requests addressed to the new session ID before the session.create response returns, each SDK now buffers/parks those messages while a cloud create is in flight and flushes them into the registered session afterwards. Per-session cap is 128 in every SDK. Cross-SDK invariants:

  • Notification overflow logs a warning and drops oldest (notifications have no response id).
  • Request overflow rejects the oldest parked waiter with "pending session buffer overflow"; the SDK's JSON-RPC layer converts the rejection into a -32603 error response so the runtime isn't left hanging on the request id.
  • When the pending guard drops without a successful registration (e.g. session.create failed mid-RPC), all parked request waiters are rejected with the distinct message "pending session routing ended before session was registered" — separate phrasing so the two failure modes can be told apart in logs.

Doc rule

Adds a "cross-SDK features ship in a single PR" rule to .github/copilot-instructions.md. The sdk-consistency-review workflow auto-reviews any PR touching nodejs/, python/, go/, or dotnet/, so splitting per-language guarantees a "missing in N languages" flag on each.

Tests

Per SDK, mirrors of the six Rust integration tests plus the two follow-up overflow/guard-drop tests (8 each, roughly; exact count varies by language harness shape). All unit suites pass locally.

Known limitations

  • TypeScript: the generated sessionFs.* handlers in nodejs/src/generated/rpc.ts use a synchronous getHandlers(sessionId) callback, so early sessionFs.* requests will throw immediately rather than being parked. Documented inline; proper fix requires a codegen update.
  • Go: no select on ctx.Done() while parked in waitForSession (pre-existing behavior in the branch).

Related runtime issues

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

- CreateSession now rejects config.Cloud with an error pointing to
  CreateCloudSession
- CreateCloudSession validates config (requires Cloud, rejects
  caller-supplied SessionID and Provider), omits sessionId from the
  session.create wire payload so the runtime assigns one
- Pending-routing support: a refcounted beginPendingSessionRouting
  guard buffers session.event notifications (bounded drop-oldest, 128
  per id) and parks inbound request handlers (userInput.request,
  exitPlanMode.request, autoModeSwitch.request, hooks.invoke,
  systemMessage.transform) until the runtime-assigned session id is
  registered; waiters are rejected with a clear error if pending mode
  ends without registration
- TOCTOU race fixed: after acquiring pending.mu, both handleSessionEvent
  and waitForSession re-check c.sessions so a notification/request that
  races with flushPendingForSession is dispatched directly rather than
  buffered and abandoned
- Waiter buffer is also bounded at 128; oldest waiter is rejected on
  overflow
- README and inline godoc updated

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tclem and others added 2 commits May 22, 2026 19:28
Port create_cloud_session to the Python SDK, following the Rust (PR #1394)
and TypeScript (PR #1395) reference implementations.

Key additions:

- CopilotClient.create_cloud_session(): creates a Mission Control-backed
  cloud session. The runtime owns the session ID (omitting sessionId from
  the wire payload); the caller must not set session_id or provider.

- Pending-routing infrastructure: session.event notifications and inbound
  JSON-RPC requests that arrive before the session is fully registered are
  buffered (up to _PENDING_SESSION_BUFFER_LIMIT = 128) and replayed once
  the session is ready. _PendingSessionRoutingGuard + _begin_pending_session_routing
  + _flush_pending_for_session + _resolve_session implement this.

- CopilotSession.remote_url property: exposes the remoteUrl returned in the
  session.create response for cloud sessions.

- create_session() now rejects cloud= being set; callers must use
  create_cloud_session() instead.

- test_cloud_session.py: 7 new unit tests covering wire shape, validation,
  early notification buffering, and parked inbound request handling.

- Updated test_client.py: test_create_session_forwards_cloud_options
  updated to test create_cloud_session instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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>
Port CreateCloudSessionAsync from the Rust (PR #1394) and TypeScript (PR #1395) SDKs.

- CreateCloudSessionAsync sends session.create without a caller-supplied sessionId, so
  the cloud server assigns one. The server-assigned sessionId is set on the returned
  session object alongside a new RemoteUrl property.
- CreateSessionAsync now rejects configs that include a Cloud block; callers must use
  CreateCloudSessionAsync for cloud sessions.
- A pending-routing buffer handles the window between sending session.create and receiving
  the response: incoming session.event notifications are buffered per-sessionId (bounded at
  128 with drop-oldest semantics) and replayed once the session is registered; inbound
  RPC requests (userInput.request, permission.request, etc.) park on a TaskCompletionSource
  and are unblocked atomically when the session is registered.
- Added RemoteUrl property to CopilotSession.
- 9 unit tests cover the core behaviours: serialization, guard/buffer mechanics, early
  notification replay, and inbound request parking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tclem and others added 11 commits May 22, 2026 19:51
Two symmetric fixes ported from Rust SDK (commits 491b442, e0ff254)
and TS SDK (commit c167bc3, PR #1395):

1. Pending request buffer overflow: when _resolve_session would push the
   waiter list past _PENDING_SESSION_BUFFER_LIMIT (128), evict the oldest
   future and call set_exception(ValueError('pending session buffer
   overflow')). The JSON-RPC dispatch layer (_dispatch_request) catches
   the raised exception and sends a proper -32603 error response so the
   runtime doesn't hang on the request id.

2. Guard drop without registration: _PendingSessionRoutingGuard.dispose()
   already called set_exception on parked waiters, but used a generic
   message. Changed to the cross-SDK canonical message 'pending session
   routing ended before session was registered' so the two failure paths
   are distinguishable in logs and error messages.

Notifications retain warn-and-drop-oldest behaviour (no response needed
— they carry no id).

Add two unit tests:
- TestPendingRequestBufferOverflow: 129 concurrent waiters; oldest
  raises with the overflow message; remaining 128 resolve after
  registration.
- TestPendingRequestGuardDropWithoutRegistration: session.create fails;
  parked request raises with the routing-ended message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Carries the Rust SDK PR #1394 follow-up review fixes into the Go port:

1. Cap the per-session parked-waiter list at 128. When exceeded, reject
   the oldest waiter with errPendingSessionBufferOverflow ('pending
   session buffer overflow'). The handler returns a *jsonrpc2.Error with
   code -32603 via the new pendingRoutingRPCError helper, so the runtime
   receives a proper error response instead of hanging on the request id
   until its own timeout. Mirrors Rust commit 491b442 and TS commit
   c167bc3.

2. When the last pending-routing guard drops without RegisterSession
   (e.g. session.create failed mid-RPC), signal all parked waiters with
   errPendingSessionRoutingEnded ('pending session routing ended before
   session was registered'). Distinct phrasing from the overflow path so
   debugging can tell the two cases apart. Mirrors Rust commit e0ff254
   and TS commit c167bc3.

Adds pendingRoutingRPCError helper that routes sentinel errors to
-32603 while unknown-session errors keep -32602.

Adds two tests:
- TestPendingRouting_OverflowEmitsError: 129 parked waiters, oldest
  gets -32603 overflow error, remaining 128 resolve normally after
  registration.
- TestPendingRouting_GuardDropDistinctMessage: parks a request, drops
  the guard without registration, verifies exact routing-ended message
  and -32603 code.

Updates TestPendingRouting_RejectsWaitersOnDispose to assert the new
exact message and code instead of the old 'dropped' substring check.

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

1. Cap the per-session inbound-request parked-waiter list at 128. When
   exceeded, reject the oldest waiter with 'pending session buffer
   overflow'. The custom JsonRpc dispatcher translates the thrown
   exception into a JSON-RPC error response (-32603) back to the runtime
   so the request id isn't left hanging — silently dropping it would
   leave the runtime waiting on the response until its own timeout.
   Mirrors Rust commit 491b442 and TS commit c167bc3.

2. Use a distinct message ('pending session routing ended before
   session was registered') when the pending guard drops without
   registration. Lets debugging tell the overflow path from the
   create-failed path. Also fixes the pre-existing bug where the
   waiter-fault loop ran inside _pendingLock despite the comment saying
   otherwise — moved it outside the lock so TCS continuations can't
   deadlock against the lock. Mirrors Rust commit e0ff254.

Adds two tests:
- overflow path: 129 early inbound requests → oldest gets error with
  'pending session buffer overflow', remaining 128 resolve after
  registration
- guard-drop path: session.create fails with parked request → error
  response with 'pending session routing ended before session was
  registered'

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>
Ports the cloud-session contract from the Rust SDK (PR #1394) to
the TypeScript SDK.

- createSession({ cloud }) now throws — the runtime rejects this
  shape, and the existing path silently sent cloud + sessionId
  which RemoteSessionManager.cloud() rejects with a generic error.
- New createCloudSession(config) omits sessionId from the wire
  payload, lets the runtime assign the Mission Control session id,
  and registers the resulting session under that id.
- Pending-routing buffer (bounded, drop-oldest) holds session.event
  notifications and inbound JSON-RPC request handlers
  (userInput / exitPlanMode / autoModeSwitch / hooks /
  systemMessage) that arrive while session.create is in flight,
  replaying them after the runtime-assigned id is registered.
- Rejection guards for caller-provided sessionId, caller-provided
  provider, and missing cloud config.
- Post-response setup is wrapped in try/catch so a setupSessionFs
  failure rolls back the partial registration and disposes the
  pending-routing guard.

Inbound sessionFs.* requests still use the generated synchronous
handler shim and are not pending-buffered. This is unlikely in
practice (runtime does not initiate sessionFs before the create
response) and is documented as a known limitation; the fix is a
codegen update to make registerClientSessionApiHandlers accept an
async session resolver.

Tests cover wire-shape parity with Rust, rejection guards, early
notification buffering, and early inbound-request parking.

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

1. Cap the per-session inbound-request parked-waiter list at 128. When
   exceeded, reject the oldest waiter with 'pending session buffer
   overflow'. vscode-jsonrpc translates the rejection into a JSON-RPC
   error response (-32603) back to the runtime so the request id isn't
   left hanging — silently dropping it would leave the runtime waiting
   on the response until its own timeout. Mirrors Rust commit 491b442.

2. Use a distinct message ('pending session routing ended before
   session was registered') when the pending guard drops without
   registration. Lets debugging tell the overflow path from the
   create-failed path. Mirrors Rust commit e0ff254.

Adds two tests:
- overflow path resolves to overflow error for the oldest waiter,
  remaining 128 resolve normally after registration
- guard-drop path rejects parked waiters with the distinct message
@tclem tclem force-pushed the tclem/tclem-ts-create-cloud-session branch from c167bc3 to f4752b6 Compare May 23, 2026 03:45
@tclem tclem changed the title TS SDK: add createCloudSession TS, Python, Go, .NET, Java SDKs: add createCloudSession May 23, 2026
@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1395 · ● 22.4M

Comment thread go/client.go
Comment thread nodejs/src/client.ts
Comment thread java/src/main/java/com/github/copilot/sdk/CopilotClient.java
Python (session.remote_url) and .NET (Session.RemoteUrl) already exposed
the runtime-assigned remote URL returned from session.create; this brings
the TypeScript, Go, and Java SDKs to parity per the sdk-consistency-review
feedback on PR #1395.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Apply Spotless formatting after adding the remoteUrl record component.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

SDK Consistency Review ✅

This PR adds createCloudSession across all five SDKs (TypeScript, Python, Go, .NET, Java), complementing the Rust reference implementation from #1394. Cross-SDK consistency looks good:

Aspect Status
Pending-routing buffer limit (128) ✅ Consistent across all SDKs
Drop-oldest overflow behavior ✅ Consistent across all SDKs
Overflow vs guard-drop error message distinction ✅ Consistent across all SDKs
Validation rules (cloud required; sessionId/provider rejected) ✅ Consistent across all SDKs
Method naming conventions ✅ Language-idiomatic (createCloudSession / create_cloud_session / CreateCloudSession / CreateCloudSessionAsync)
infiniteSessions support in cloud path ✅ Present in all SDKs

The two documented known limitations (sessionFs.* not pending-buffered in TypeScript; no ctx.Done() select while parked in Go) are called out inline and appear intentional/pre-existing — no cross-SDK gap there.

No consistency issues found. 🎉

Generated by SDK Consistency Review Agent for issue #1395 · ● 7.6M ·

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