Skip to content

Lazily initialize sessions on session.created lifecycle events#1452

Draft
stephentoub wants to merge 1 commit into
mainfrom
stephentoub/lazysessions
Draft

Lazily initialize sessions on session.created lifecycle events#1452
stephentoub wants to merge 1 commit into
mainfrom
stephentoub/lazysessions

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

Summary

For cloud session creation, defer CopilotSession construction until the session.created lifecycle event arrives with the server-assigned session ID. This avoids registering a session under a provisional client-generated ID that will immediately be replaced by the server, and ensures the session object is keyed by its real ID from the start.

Changes

All SDKs (Node, .NET, Go, Python, Rust):

  • Extract session configuration helperconfigureSession / CreateConfiguredSession / equivalent consolidates the repeated tool/hook/handler registration logic used by both create and resume paths.
  • Add pending cloud session creates map — tracks deferred session factory functions keyed by the provisional client session ID.
  • Lazy initialization on lifecycle event — when a session.created lifecycle event arrives with a clientSessionId, the pending factory is invoked to materialize the session under the real server-assigned ID.
  • Surface clientSessionId on SessionLifecycleEvent types across all languages.
  • Mark cloud session APIs as experimental where applicable (.NET [Experimental] attribute, Python docstring annotations).

Motivation

Cloud-created sessions receive their final session ID from the server, which may differ from the client-generated provisional ID. Previously, the session was eagerly constructed and registered under the provisional ID, then had to be re-keyed. This lazy approach is cleaner: the session is only constructed once it's known what ID it should have.

For cloud session creation, defer CopilotSession construction until the
session.created lifecycle event arrives with the server-assigned session ID.
This avoids registering a session under a provisional client-generated ID
that will be replaced by the server, and ensures the session object is
immediately keyed by its real ID.

Changes across all SDKs (Node, .NET, Go, Python, Rust):
- Extract a configureSession/CreateConfiguredSession helper for session setup
- Add pendingCloudSessionCreates map to track deferred session factories
- On session.created lifecycle event with a clientSessionId, invoke the
  pending factory to materialize the session under the real ID
- Surface clientSessionId on SessionLifecycleEvent types
- Mark cloud session APIs as experimental where applicable

Co-authored-by: Copilot <[email protected]>
Copilot AI review requested due to automatic review settings May 27, 2026 11:32
@stephentoub stephentoub requested a review from a team as a code owner May 27, 2026 11:32
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

The PR description says "All SDKs (Node, .NET, Go, Python, Rust)" — and that's correct — but the Java SDK is not included and is now out of sync.

Missing Java changes

1. clientSessionId field on SessionLifecycleEvent

All five updated SDKs add a clientSessionId / client_session_id / ClientSessionID field to their lifecycle event type. The Java equivalent (java/src/main/java/com/github/copilot/rpc/SessionLifecycleEvent.java) is missing this field:

// Should be added (marked `@Experimental` analogous to other SDKs):
`@JsonProperty`("clientSessionId")
private String clientSessionId;

public String getClientSessionId() { return clientSessionId; }
public void setClientSessionId(String clientSessionId) { this.clientSessionId = clientSessionId; }

2. handleLifecycleEvent doesn't extract clientSessionId

RpcHandlerDispatcher.handleLifecycleEvent manually deserializes the params but never reads clientSessionId from them. Other SDKs now pass this through to the event object and use it to trigger lazy session materialization.

3. Lazy initialization for cloud sessions

CopilotClient.createSession in Java uses the old eager approach (creates the CopilotSession before the RPC call and re-keys it on mismatch). The PR introduces a cleaner lazy approach in every other SDK: register a factory in a pendingCloudCreates map and materialize the session only when the session.created lifecycle event arrives with the real server-assigned ID.

Java has CloudSessionOptions / setCloud(...) in SessionConfig, so cloud session creation is a supported path — the bug/inconsistency is real, not just cosmetic.

Summary

SDK clientSessionId on event Lazy cloud init
Node.js ✅ added ✅ added
Python ✅ added ✅ added
Go ✅ added ✅ added
.NET ✅ added ✅ added
Rust ✅ added ✅ added
Java ❌ missing ❌ missing

Suggest following up with a Java PR that mirrors the same changes (or including Java in this PR if it's still feasible).

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

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

This PR updates all SDKs to support deferring (“lazy”) session construction for cloud session creation until a session.created lifecycle event provides the server-assigned session ID, avoiding provisional-ID registration and adding clientSessionId correlation on lifecycle events.

Changes:

  • Add clientSessionId to session lifecycle event types across SDKs and mark cloud session APIs as experimental.
  • Introduce per-client “pending cloud create” registries to materialize sessions once the created lifecycle event arrives.
  • Refactor session setup/registration into shared helpers (where applicable) to reduce duplication between create/resume paths.
Show a summary per file
File Description
rust/src/wire.rs Makes sessionId optional in the session.create wire payload.
rust/src/types.rs Adds clientSessionId to lifecycle events; marks cloud options/config as experimental; updates wire creation call sites.
rust/src/session.rs Implements pending cloud-create materialization and runtime-part extraction/cleanup helpers for lazy initialization.
rust/src/router.rs Hooks lifecycle notifications to trigger pending cloud session materialization on session.created.
rust/src/lib.rs Adds pending cloud-create storage to ClientInner and wires it into router startup.
python/copilot/client.py Adds client_session_id to lifecycle events; tracks pending cloud creates and attempts to re-key sessions.
nodejs/src/types.ts Adds experimental annotations for cloud options/config and clientSessionId on lifecycle events.
nodejs/src/client.ts Adds pending cloud-create map and a shared session configuration helper; materializes sessions on lifecycle created.
go/types.go Adds experimental docs for cloud options/config; adds clientSessionId to lifecycle events.
go/client.go Adds pending cloud create materialization and lifecycle-triggered initialization for cloud sessions.
dotnet/src/Types.cs Marks cloud session APIs experimental and adds ClientSessionId to lifecycle event type.
dotnet/src/Client.cs Adds pending cloud-create initialization and shared session configuration helper; lifecycle-triggered materialization.

Copilot's findings

  • Files reviewed: 12/12 changed files
  • Comments generated: 10

Comment thread rust/src/session.rs
Comment on lines +812 to +814
if is_cloud_create {
wire.session_id = None;
}
Comment thread rust/src/session.rs
Comment on lines +964 to +973
if is_cloud_create && materialized_cloud_session.lock().is_none() {
self.inner.pending_cloud_creates.lock().remove(&session_id);
}

let Some(parts) = materialized_cloud_session.lock().take() else {
self.inner.pending_cloud_creates.lock().remove(&session_id);
return Err(Error::InvalidConfig(
"cloud session was not registered".to_string(),
));
};
Comment thread nodejs/src/client.ts
Comment on lines +838 to 841
if (isCloudCreate && config.sessionId) {
// In cloud mode this is a provisional client-side ID, not the final server ID.
}

Comment thread nodejs/src/client.ts
Comment on lines +935 to +943
if (isCloudCreate) {
session = this.sessions.get(createResponse.sessionId);
if (!session) {
throw new Error(
`Cloud session was not registered: ${createResponse.sessionId}`
);
}
this.pendingCloudSessionCreates.delete(sessionId);
}
Comment thread go/client.go
Comment on lines +811 to +819
if isCloudCreate {
c.sessionsMux.Lock()
session = c.sessions[response.SessionID]
delete(c.pendingCloudCreates, sessionID)
c.sessionsMux.Unlock()
if session == nil {
return nil, fmt.Errorf("cloud session was not registered: %s", response.SessionID)
}
}
Comment thread go/client.go
Comment on lines +1318 to +1320
if _, err := materialize(event.SessionID); err != nil {
fmt.Printf("Error materializing cloud session %s: %v\n", event.SessionID, err)
}
Comment thread dotnet/src/Client.cs
Comment on lines +624 to +635
if (session is null)
{
_sessions.TryGetValue(response.SessionId, out session);
if (session is null)
{
throw new InvalidOperationException($"Session was not registered: {response.SessionId}");
}
}
if (isCloudCreate)
{
_pendingCloudCreates?.TryRemove(sessionId, out _);
}
Comment thread python/copilot/client.py
Comment on lines +1899 to +1903
self._pending_cloud_creates.pop(actual_session_id, None)
raise RuntimeError(
f"Cloud session was not registered: {response_session_id}"
)
self._pending_cloud_creates.pop(actual_session_id, None)
Comment thread go/client.go
Comment on lines 775 to 789
result, err := c.client.Request("session.create", req)
if err != nil {
c.sessionsMux.Lock()
delete(c.sessions, sessionID)
delete(c.pendingCloudCreates, sessionID)
materializedMux.Lock()
if materializedSessionID != "" {
delete(c.sessions, materializedSessionID)
}
materializedMux.Unlock()
if session != nil {
delete(c.sessions, session.SessionID)
} else if sessionID != "" {
delete(c.sessions, sessionID)
}
c.sessionsMux.Unlock()
Comment thread dotnet/src/Client.cs
Comment on lines 641 to 645
catch (Exception ex)
{
session.RemoveFromClient();
_pendingCloudCreates?.TryRemove(sessionId, out _);
session?.RemoveFromClient();
if (ex is not OperationCanceledException)
@stephentoub stephentoub marked this pull request as draft May 27, 2026 15:32
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