Skip to content

Defer sessionId to server for cloud sessions#1479

Merged
stephentoub merged 1 commit into
mainfrom
stephentoub/cleanup1
May 28, 2026
Merged

Defer sessionId to server for cloud sessions#1479
stephentoub merged 1 commit into
mainfrom
stephentoub/cleanup1

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

@stephentoub stephentoub commented May 28, 2026

Summary

When a caller creates a session with cloud set and does not supply a sessionId, the SDK now omits sessionId from the session.create request and lets the CLI/server assign one. The returned id is captured synchronously when the response is parsed (via an inline response callback) so any subsequent session.event notification routes to the registered session without race.

For all other cases (no cloud, or caller-supplied sessionId), the SDK continues to generate a UUID client-side (when none is supplied) and pre-register the session BEFORE issuing the RPC. Pre-registration is required because the CLI may issue session-scoped requests (e.g. sessionFs.writeFile for workspace metadata) DURING session.create processing, before it has sent the response — without pre-registration those requests would have no handler.

A caller-supplied sessionId is always forwarded to the server (even with cloud), so the server can validate it. A mismatched returned id fails the call with a clear error.

In addition, the .NET implementation extracts the session-init local function from CreateSessionAsync into a private InitializeSession helper now shared by both CreateSessionAsync and ResumeSessionAsync, eliminating duplicated handler-wiring code.

Decision matrix

cloud caller sessionId Behavior
set not supplied Server-assigned. Omit sessionId from RPC; register lazily from inline callback.
set supplied Pre-register with caller id; send to server; server validates.
not set not supplied Client-generated UUID; pre-register; send to server.
not set supplied Pre-register with caller id; send to server.

What changed per SDK

The same conditional (useServerGeneratedId = cloud != null && callerSessionId == null) is applied uniformly across all 6 SDKs:

  • .NET (dotnet/src/Client.cs): New private InitializeSession helper shared by CreateSessionAsync and ResumeSessionAsync. CreateSessionAsync either pre-registers (default) or installs an onResponseInline callback (cloud + no id).
  • Python (python/copilot/client.py): create_session either pre-registers via uuid.uuid4() or installs an on_response_inline callback that registers when the response is parsed.
  • Go (go/client.go): CreateSession either pre-registers via uuid.NewString() or installs an inline callback that registers from the read loop the instant the response arrives.
  • Rust (rust/src/session.rs): create_session either pre-registers via uuid::Uuid::new_v4() or installs an InlineResponseCallback that validates the returned id and registers in the router. SessionConfig::into_wire now takes the resolved local session id (or None when the server is generating one).
  • Java (java/src/main/java/com/github/copilot/CopilotClient.java): createSession either pre-registers via UUID.randomUUID() or registers in the .thenCompose continuation (race-free — CompletableFuture sync continuations run on the JSON-RPC reader thread).
  • Node.js (nodejs/src/client.ts): createSession either pre-registers via randomUUID() or lazy-initializes after the response (race-free — vscode-jsonrpc dispatches each message via setImmediate, so microtasks flush before the next message is read).

Why hybrid instead of always-lazy?

An earlier iteration of this PR took the "always lazy-init" path. CI surfaced that the CLI emits sessionFs.writeFile for workspace metadata DURING session.create processing (before the response). For non-cloud sessions the SDK must have a registered handler at that moment, which is only possible if it pre-registers before sending the RPC. Cloud sessions don't have a workspace and so don't hit that path, which is why deferring to the server is safe there.

Testing

All SDK unit tests and lints pass locally. The .NET CreateSessionReKeyEntryTest (Java) was rewritten to assert that mismatched ids are rejected (the previous "re-key on mismatch" behavior is intentionally gone).

E2E suites are validated in CI on the PR.

Copilot AI review requested due to automatic review settings May 28, 2026 03:09
@stephentoub stephentoub requested a review from a team as a code owner May 28, 2026 03:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors session creation across all six SDKs (Node.js, Python, Go, .NET, Rust, Java) to defer sessionId assignment to the CLI/server when the caller doesn't supply one. Each SDK's JSON-RPC layer gains an "inline response callback" hook that runs synchronously in the read loop the instant a successful response is parsed, so the new session can be registered before any subsequent session.event notification is dispatched. This replaces per-SDK pre-generation of UUIDs and pre-registration of sessions, and consolidates the .NET session-init logic into a shared InitializeSession helper.

Changes:

  • Add inline response callback plumbing in each SDK's JSON-RPC client (jsonrpc.rs/JsonRpc.cs/_jsonrpc.py/jsonrpc2.go/vscode-jsonrpc microtask-based / CompletableFuture.thenApply).
  • Always lazy-initialize the session from the session.create response, removing client-side UUID/randomUUID generation and the cloud-only branching.
  • Validate that the returned sessionId matches a caller-supplied id (mismatch → error), and ensure failure paths only unregister sessions actually registered.
Show a summary per file
File Description
rust/src/wire.rs Make SessionCreateWire.session_id optional with skip-if-none.
rust/src/types.rs Update SessionConfig::into_wire and tests to take Option<SessionId>.
rust/src/session.rs Defer session registration to inline callback after session.create response; remove pre-registration.
rust/src/lib.rs Add call_with_inline_callback; delete obsolete pending-registration cancel test.
rust/src/jsonrpc.rs Add InlineResponseCallback + PendingRequest; run callback synchronously in read loop.
python/copilot/_jsonrpc.py Add on_response_inline kwarg, store + invoke synchronously in reader thread.
python/copilot/client.py Replace pre-generated UUID and pre-registration with lazy _initialize_session via inline callback.
python/test_client.py Update mocks to accept **kwargs and invoke the inline callback.
nodejs/src/client.ts Lazy-init session after response; throw on mismatch; remove randomUUID() path.
nodejs/test/{client,toolSet}.test.ts Mocks fall back to "session-id" when caller doesn't supply one.
go/internal/jsonrpc2/jsonrpc2.go Add RequestWithInlineResponse; run callback in read loop with panic recovery.
go/client.go Defer session registration to inline callback; clean up only registered sessions on error.
java/src/main/java/com/github/copilot/CopilotClient.java Initialize session inside thenApply; remove pre-registration and rekey-on-mismatch branch; throw on mismatch.
dotnet/src/JsonRpc.cs Add onResponseInline parameter; invoke synchronously after parsing result.
dotnet/src/Client.cs Extract InitializeSession helper shared by Create/Resume; defer create to inline callback; misc unrelated refactors (field ordering, lazy _modelsCacheLock, lock-on-collection).
dotnet/test/Unit/{JsonRpcTests,ClientSessionLifetimeTests}.cs Pass null for new param; fall back to a generated id when caller doesn't supply one.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 0

Comment thread dotnet/src/Client.cs Fixed
Comment thread dotnet/src/Client.cs Fixed
Comment thread dotnet/src/JsonRpc.cs Dismissed
@stephentoub stephentoub force-pushed the stephentoub/cleanup1 branch from d81d4d4 to 6dbb5e8 Compare May 28, 2026 03:17
@github-actions github-actions Bot mentioned this pull request May 28, 2026
@stephentoub stephentoub force-pushed the stephentoub/cleanup1 branch 2 times, most recently from 333d5b9 to f3e65ee Compare May 28, 2026 03:48
Comment thread dotnet/src/Client.cs Dismissed
Comment thread dotnet/src/Client.cs Dismissed
@github-actions

This comment has been minimized.

@stephentoub stephentoub force-pushed the stephentoub/cleanup1 branch from f3e65ee to 42b26b1 Compare May 28, 2026 03:59
@github-actions

This comment has been minimized.

@stephentoub stephentoub force-pushed the stephentoub/cleanup1 branch from 42b26b1 to 544ef71 Compare May 28, 2026 04:08
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

All six SDKs (Node.js, Python, Go, .NET, Java, Rust) implement the same logic consistently in this PR:

SDK useServerGeneratedId condition Mismatch validation
Node.js config.cloud != null && callerSessionId == null ✅ throws Error
Python cloud is not None and session_id is None ✅ raises RuntimeError
Go config.Cloud != nil && config.SessionID == "" ✅ returns error
.NET config.Cloud != null && string.IsNullOrEmpty(config.SessionId) ✅ throws InvalidOperationException
Java config.getCloud() != null && (callerSessionId == null || callerSessionId.isEmpty()) ✅ throws
Rust config.cloud.is_some() && caller_session_id.is_none() ✅ returns SessionIdMismatch error

The extracted initializeSession local-function pattern is also uniformly applied across all SDKs.


Note on PR description accuracy

The PR title ("Always defer sessionId to CLI") and description ("removes useServerGeneratedId/presetSessionId branching") don't match what the code actually does. The diff shows that useServerGeneratedId is introduced by this PR (it was not present in any SDK's main branch baseline), and UUID generation is retained for non-cloud sessions. The actual behavior after this PR is:

  • Cloud sessions with no caller-supplied ID → defer session-id assignment to the CLI, lazy-register after the response
  • Non-cloud sessions (or caller-supplied ID) → generate UUID client-side, register before the RPC (same as before this PR)

This is a reasonable design (non-cloud CLIs may issue sessionFs.writeFile during session.create, requiring the session to be pre-registered), but the PR summary overstates the scope. Worth updating the PR description to match the actual behavior to avoid confusion for future readers.

Generated by SDK Consistency Review Agent for issue #1479 · ● 5.1M ·

@stephentoub stephentoub changed the title Always defer sessionId to CLI and lazy-init session from response Lazy-init session from response sessionId May 28, 2026
When a caller creates a session with `cloud` set and does not supply a
`sessionId`, the SDK now omits `sessionId` from the `session.create`
request and lets the CLI/server assign one. The returned id is captured
synchronously when the response is parsed (via an inline response
callback) so that any subsequent `session.event` notification routes to
the registered session without race.

For all other cases (no `cloud`, or caller supplied `sessionId`), the
SDK continues to generate a UUID client-side (when none is supplied)
and pre-register the session BEFORE issuing the RPC. Pre-registration
is required because the CLI may issue session-scoped requests (e.g.
`sessionFs.writeFile` for workspace metadata) during `session.create`
processing, before it has sent the response — without pre-registration
those requests would have no handler. A caller-supplied `sessionId` is
always passed to the server so the server can validate it; mismatched
returned ids fail the call with a clear error.

In addition, the .NET implementation extracts the session-init local
function from CreateSessionAsync into a private InitializeSession
helper that is now shared by both CreateSessionAsync and
ResumeSessionAsync, eliminating duplicated handler-wiring code.

Per-SDK summary:

.NET (dotnet/src/Client.cs): New private InitializeSession helper
shared by CreateSessionAsync and ResumeSessionAsync. CreateSessionAsync
branches on (Cloud != null && SessionId == null): the cloud/no-id path
registers the session lazily from an `onResponseInline` callback; all
other paths pre-register before the RPC. Validates that the server-
returned id matches a caller-supplied id.

Python (python/copilot/client.py): create_session uses the same
conditional. Cloud+no-id installs an `on_response_inline` callback that
registers the session as soon as the response is parsed. Otherwise the
session is pre-registered via uuid.uuid4() (or the caller-supplied id).

Go (go/client.go): createSession uses the same conditional. Cloud+no-id
installs an inline callback that registers from the read loop the
instant the response arrives. Otherwise the session is pre-registered
via uuid.NewString() (or the caller-supplied id).

Rust (rust/src/session.rs): create_session uses the same conditional.
Cloud+no-id installs an InlineResponseCallback that validates the
returned id and registers in the router. Otherwise the session is
pre-registered via uuid::Uuid::new_v4() (or the caller-supplied id).
SessionConfig::into_wire now takes the local session id (or None when
the server is generating one).

Java (java/src/main/java/com/github/copilot/CopilotClient.java):
createSession uses the same conditional. Cloud+no-id defers
registration to the .thenCompose continuation (race-free because
CompletableFuture sync continuations run on the JSON-RPC reader
thread). Otherwise the session is pre-registered via UUID.randomUUID()
(or the caller-supplied id) before the RPC.

Node.js (nodejs/src/client.ts): createSession uses the same
conditional. Cloud+no-id lazy-initializes after the response (race-free
because vscode-jsonrpc dispatches each message via setImmediate, so
microtasks flush before the next message is read). Otherwise the
session is pre-registered via randomUUID() (or the caller-supplied id).

Co-authored-by: Copilot <[email protected]>
@stephentoub stephentoub changed the title Lazy-init session from response sessionId Defer sessionId to server for cloud sessions May 28, 2026
@stephentoub stephentoub force-pushed the stephentoub/cleanup1 branch from 544ef71 to 9e7841f Compare May 28, 2026 04:38
@stephentoub stephentoub merged commit bdedbd7 into main May 28, 2026
39 checks passed
@stephentoub stephentoub deleted the stephentoub/cleanup1 branch May 28, 2026 04:39
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.

2 participants