From 8319c740fc8f213031a0aeac1b0f57aaed505499 Mon Sep 17 00:00:00 2001 From: Belal Taher Date: Sat, 27 Jun 2026 23:46:22 -0400 Subject: [PATCH] Forward internalCorrelationIds through session create/resume The runtime already accepts an `internalCorrelationIds` param on `session.create`/`session.resume` and stamps each entry onto every emitted telemetry event as an `sdk_correlation_` property. The SDK, however, builds its wire bag by explicitly enumerating fields, so the value never reached the runtime and could not be set by first-party hosts (e.g. the Copilot cloud agent) that need to join the runtime's CLI telemetry stream back to their own job/repo identifiers. Add an `@internal` `internalCorrelationIds` field on `SessionConfigBase` (stripped from the published type declarations via `stripInternal`, so third-party integrators do not see it) and forward it in both the `session.create` and `session.resume` payloads, mirroring the existing `expAssignments` passthrough. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/client.ts | 2 ++ nodejs/src/types.ts | 21 +++++++++++++ nodejs/test/client.test.ts | 64 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 53686a6ca..73e8476c1 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1448,6 +1448,7 @@ export class CopilotClient { remoteSession: config.remoteSession, cloud: config.cloud, expAssignments: config.expAssignments, + internalCorrelationIds: config.internalCorrelationIds, }); const { @@ -1646,6 +1647,7 @@ export class CopilotClient { remoteSession: config.remoteSession, openCanvases: config.openCanvases, expAssignments: config.expAssignments, + internalCorrelationIds: config.internalCorrelationIds, }); const { workspacePath, capabilities, openCanvases } = response as { diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index e354bd821..baa3f0d82 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2165,6 +2165,27 @@ export interface SessionConfigBase { * @internal */ expAssignments?: Record; + + /** + * First-party session correlation IDs forwarded verbatim to the runtime, + * which stamps them onto every telemetry event emitted for the session + * (each entry becomes an `sdk_correlation_` property). Intended for + * trusted hosts (e.g. the Copilot cloud agent) that need to join the + * runtime's CLI telemetry stream back to their own job/repo identifiers; + * `session_id` alone only correlates within a single invocation. + * + * Keys must match `^[a-z0-9][a-z0-9_]{0,63}$`; the runtime drops malformed + * keys, empty/oversized values, and anything beyond its per-session limit + * (fail-open). Applies to both session creation and resume. + * + * Deliberately not part of the public, typed SDK surface: it is stripped + * from the published type declarations so third-party integrators do not + * see it, while first-party hosts can still set it (via a cast) and have it + * threaded through to the runtime. + * + * @internal + */ + internalCorrelationIds?: Record; } /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 96d7da30c..ecd6792d0 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -251,6 +251,70 @@ describe("CopilotClient", () => { expect(resumePayload.expAssignments).toBeUndefined(); }); + it("forwards internalCorrelationIds in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const correlationIds = { + cca_job_id: "job-123", + owner_id: "1", + repo_id: "2", + }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + internalCorrelationIds: correlationIds, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + internalCorrelationIds: correlationIds, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.internalCorrelationIds).toEqual(correlationIds); + expect(resumePayload.internalCorrelationIds).toEqual(correlationIds); + }); + + it("omits internalCorrelationIds from session.create and session.resume when unset", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.internalCorrelationIds).toBeUndefined(); + expect(resumePayload.internalCorrelationIds).toBeUndefined(); + }); + it("forwards capi options in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start();