diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index e46a7a888..4bd5b5b93 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -2766,6 +2766,10 @@ public sealed class SessionMetadata
///
public string? Summary { get; set; }
///
+ /// Identifier of the client driving the session.
+ ///
+ public string? ClientName { get; set; }
+ ///
/// Whether the session is running on a remote server.
///
public bool IsRemote { get; set; }
diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs
index c1e406104..72685b1f2 100644
--- a/dotnet/test/Unit/SerializationTests.cs
+++ b/dotnet/test/Unit/SerializationTests.cs
@@ -171,6 +171,30 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio
Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString());
}
+ [Fact]
+ public void SessionMetadata_CanRoundTripClientName_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var original = new SessionMetadata
+ {
+ SessionId = "session-1",
+ StartTime = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
+ ModifiedTime = DateTimeOffset.Parse("2025-01-01T01:00:00Z"),
+ Summary = "loaded session",
+ ClientName = "my-app",
+ IsRemote = false
+ };
+
+ var json = JsonSerializer.Serialize(original, options);
+ using var document = JsonDocument.Parse(json);
+ var root = document.RootElement;
+ Assert.Equal("my-app", root.GetProperty("clientName").GetString());
+
+ var deserialized = JsonSerializer.Deserialize(json, options);
+ Assert.NotNull(deserialized);
+ Assert.Equal("my-app", deserialized.ClientName);
+ }
+
[Fact]
public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions()
{
diff --git a/go/client_test.go b/go/client_test.go
index 39358a72a..efd41146b 100644
--- a/go/client_test.go
+++ b/go/client_test.go
@@ -402,6 +402,33 @@ func TestResumeSessionRequest_ClientName(t *testing.T) {
})
}
+func TestSessionMetadata_ClientName(t *testing.T) {
+ t.Run("round-trips clientName in JSON", func(t *testing.T) {
+ var metadata SessionMetadata
+ if err := json.Unmarshal([]byte(`{
+ "sessionId":"s1",
+ "startTime":"2025-01-01T00:00:00Z",
+ "modifiedTime":"2025-01-01T01:00:00Z",
+ "summary":"loaded session",
+ "clientName":"my-app",
+ "isRemote":false
+ }`), &metadata); err != nil {
+ t.Fatalf("Failed to unmarshal: %v", err)
+ }
+ if metadata.ClientName == nil || *metadata.ClientName != "my-app" {
+ t.Fatalf("Expected clientName to be my-app, got %v", metadata.ClientName)
+ }
+
+ data, err := json.Marshal(metadata)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %v", err)
+ }
+ if !strings.Contains(string(data), `"clientName":"my-app"`) {
+ t.Fatalf("Expected marshaled JSON to include clientName, got %s", string(data))
+ }
+ })
+}
+
func TestCreateSessionRequest_Agent(t *testing.T) {
t.Run("includes agent in JSON when set", func(t *testing.T) {
req := createSessionRequest{Agent: "test-agent"}
diff --git a/go/types.go b/go/types.go
index 52fd27eee..8144659e9 100644
--- a/go/types.go
+++ b/go/types.go
@@ -1426,6 +1426,7 @@ type SessionMetadata struct {
StartTime time.Time `json:"startTime"`
ModifiedTime time.Time `json:"modifiedTime"`
Summary *string `json:"summary,omitempty"`
+ ClientName *string `json:"clientName,omitempty"`
IsRemote bool `json:"isRemote"`
Context *SessionContext `json:"context,omitempty"`
}
diff --git a/java/src/main/java/com/github/copilot/rpc/SessionMetadata.java b/java/src/main/java/com/github/copilot/rpc/SessionMetadata.java
index 90207b9c7..e18163e77 100644
--- a/java/src/main/java/com/github/copilot/rpc/SessionMetadata.java
+++ b/java/src/main/java/com/github/copilot/rpc/SessionMetadata.java
@@ -46,6 +46,9 @@ public class SessionMetadata {
@JsonProperty("summary")
private String summary;
+ @JsonProperty("clientName")
+ private String clientName;
+
@JsonProperty("isRemote")
private boolean isRemote;
@@ -130,6 +133,25 @@ public void setSummary(String summary) {
this.summary = summary;
}
+ /**
+ * Gets the identifier of the client driving the session.
+ *
+ * @return the client name, or {@code null} if not available
+ */
+ public String getClientName() {
+ return clientName;
+ }
+
+ /**
+ * Sets the client identifier for the session.
+ *
+ * @param clientName
+ * the client name
+ */
+ public void setClientName(String clientName) {
+ this.clientName = clientName;
+ }
+
/**
* Returns whether this session is stored remotely.
*
diff --git a/java/src/test/java/com/github/copilot/ModelInfoTest.java b/java/src/test/java/com/github/copilot/ModelInfoTest.java
index b4936d1cc..e9252cb8f 100644
--- a/java/src/test/java/com/github/copilot/ModelInfoTest.java
+++ b/java/src/test/java/com/github/copilot/ModelInfoTest.java
@@ -60,8 +60,11 @@ void sessionMetadataGettersAndSetters() {
assertNull(meta.getStartTime());
assertNull(meta.getModifiedTime());
assertNull(meta.getSummary());
+ assertNull(meta.getClientName());
assertFalse(meta.isRemote());
+ meta.setClientName("my-app");
+ assertEquals("my-app", meta.getClientName());
meta.setRemote(true);
assertTrue(meta.isRemote());
}
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 11e6131cb..d9913aee8 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -1316,6 +1316,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
+ clientName?: string;
isRemote: boolean;
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
}>;
@@ -1354,6 +1355,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
+ clientName?: string;
isRemote: boolean;
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
};
@@ -1371,6 +1373,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
+ clientName?: string;
isRemote: boolean;
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
}): SessionMetadata {
@@ -1380,6 +1383,7 @@ export class CopilotClient {
startTime: new Date(raw.startTime),
modifiedTime: new Date(raw.modifiedTime),
summary: raw.summary,
+ clientName: raw.clientName,
isRemote: raw.isRemote,
context: context
? {
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 333079e1f..dab04de86 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -2033,6 +2033,8 @@ export interface SessionMetadata {
startTime: Date;
modifiedTime: Date;
summary?: string;
+ /** Identifier of the client driving the session */
+ clientName?: string;
isRemote: boolean;
/** Working directory context (working directory, git info) from session creation */
context?: SessionContext;
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index a4554550e..77fd4f2a1 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -240,6 +240,80 @@ describe("CopilotClient", () => {
spy.mockRestore();
});
+ it("maps clientName from session.list responses", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ vi.spyOn((client as any).connection!, "sendRequest").mockImplementation(
+ async (method: string) => {
+ if (method === "session.list") {
+ return {
+ sessions: [
+ {
+ sessionId: "session-1",
+ startTime: "2025-01-01T00:00:00Z",
+ modifiedTime: "2025-01-01T01:00:00Z",
+ summary: "test session",
+ clientName: "my-app",
+ isRemote: false,
+ },
+ ],
+ };
+ }
+ throw new Error(`Unexpected method: ${method}`);
+ }
+ );
+
+ const sessions = await client.listSessions();
+
+ expect(sessions).toEqual([
+ expect.objectContaining({
+ sessionId: "session-1",
+ clientName: "my-app",
+ summary: "test session",
+ }),
+ ]);
+ expect(sessions[0].startTime).toBeInstanceOf(Date);
+ expect(sessions[0].modifiedTime).toBeInstanceOf(Date);
+ });
+
+ it("maps clientName from session.getMetadata responses", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ vi.spyOn((client as any).connection!, "sendRequest").mockImplementation(
+ async (method: string) => {
+ if (method === "session.getMetadata") {
+ return {
+ session: {
+ sessionId: "session-1",
+ startTime: "2025-01-01T00:00:00Z",
+ modifiedTime: "2025-01-01T01:00:00Z",
+ summary: "loaded session",
+ clientName: "my-app",
+ isRemote: false,
+ },
+ };
+ }
+ throw new Error(`Unexpected method: ${method}`);
+ }
+ );
+
+ const metadata = await client.getSessionMetadata("session-1");
+
+ expect(metadata).toEqual(
+ expect.objectContaining({
+ sessionId: "session-1",
+ clientName: "my-app",
+ summary: "loaded session",
+ })
+ );
+ expect(metadata?.startTime).toBeInstanceOf(Date);
+ expect(metadata?.modifiedTime).toBeInstanceOf(Date);
+ });
+
it("forwards enableSessionTelemetry in session.create request", async () => {
const client = new CopilotClient();
await client.start();
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 4386adb08..dd56feb50 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -780,6 +780,7 @@ class SessionMetadata:
modified_time: datetime # Timestamp when session was last modified
is_remote: bool # Whether the session is remote
summary: str | None = None # Optional summary of the session
+ client_name: str | None = None # Identifier of the client driving the session
context: SessionContext | None = None # Working directory context
@staticmethod
@@ -795,6 +796,7 @@ def from_dict(obj: Any) -> SessionMetadata:
f"startTime={start_time}, modifiedTime={modified_time}, isRemote={is_remote}"
)
summary = obj.get("summary")
+ client_name = obj.get("clientName")
context_dict = obj.get("context")
context = SessionContext.from_dict(context_dict) if context_dict else None
return SessionMetadata(
@@ -803,6 +805,7 @@ def from_dict(obj: Any) -> SessionMetadata:
modified_time=_parse_session_timestamp(modified_time),
is_remote=bool(is_remote),
summary=summary,
+ client_name=client_name,
context=context,
)
@@ -814,6 +817,8 @@ def to_dict(self) -> dict:
result["isRemote"] = self.is_remote
if self.summary is not None:
result["summary"] = self.summary
+ if self.client_name is not None:
+ result["clientName"] = self.client_name
if self.context is not None:
result["context"] = self.context.to_dict()
return result
diff --git a/python/test_client.py b/python/test_client.py
index 757322fa2..634fc64ea 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -22,6 +22,7 @@
ModelInfo,
ModelLimits,
ModelSupports,
+ SessionMetadata,
)
from copilot.session import PermissionHandler
from e2e.testharness import CLI_PATH
@@ -602,6 +603,23 @@ async def mock_request(method, params):
finally:
await client.force_stop()
+
+class TestSessionMetadataParsing:
+ def test_session_metadata_round_trips_client_name(self):
+ metadata = SessionMetadata.from_dict(
+ {
+ "sessionId": "session-1",
+ "startTime": "2025-01-01T00:00:00Z",
+ "modifiedTime": "2025-01-01T01:00:00Z",
+ "summary": "loaded session",
+ "clientName": "my-app",
+ "isRemote": False,
+ }
+ )
+
+ assert metadata.client_name == "my-app"
+ assert metadata.to_dict()["clientName"] == "my-app"
+
@pytest.mark.asyncio
async def test_create_session_forwards_provider_headers(self):
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
diff --git a/rust/src/types.rs b/rust/src/types.rs
index 6f5c826c6..7d863e49a 100644
--- a/rust/src/types.rs
+++ b/rust/src/types.rs
@@ -3362,6 +3362,9 @@ pub struct SessionMetadata {
/// Agent-generated session summary.
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option,
+ /// Identifier of the client driving the session.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub client_name: Option,
/// Whether the session is running remotely.
pub is_remote: bool,
}
diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs
index bb4e602e0..e4c7cc4bf 100644
--- a/rust/tests/session_test.rs
+++ b/rust/tests/session_test.rs
@@ -597,6 +597,7 @@ async fn list_sessions_returns_typed_metadata() {
"startTime": "2025-01-01T00:00:00Z",
"modifiedTime": "2025-01-01T01:00:00Z",
"summary": "test session",
+ "clientName": "my-app",
"isRemote": false,
}]
},
@@ -607,6 +608,7 @@ async fn list_sessions_returns_typed_metadata() {
assert_eq!(sessions.len(), 1);
assert_eq!(sessions[0].session_id, "s1");
assert_eq!(sessions[0].summary, Some("test session".to_string()));
+ assert_eq!(sessions[0].client_name.as_deref(), Some("my-app"));
}
#[tokio::test]
@@ -1100,6 +1102,7 @@ async fn get_session_metadata_returns_typed_metadata() {
"startTime": "2025-01-01T00:00:00Z",
"modifiedTime": "2025-01-01T01:00:00Z",
"summary": "loaded session",
+ "clientName": "my-app",
"isRemote": false,
}
},
@@ -1110,6 +1113,7 @@ async fn get_session_metadata_returns_typed_metadata() {
let metadata = metadata.expect("server returned a session");
assert_eq!(metadata.session_id, "s1");
assert_eq!(metadata.summary.as_deref(), Some("loaded session"));
+ assert_eq!(metadata.client_name.as_deref(), Some("my-app"));
}
#[tokio::test]