Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,7 @@ export class CopilotClient {
remoteSession: config.remoteSession,
cloud: config.cloud,
expAssignments: config.expAssignments,
internalCorrelationIds: config.internalCorrelationIds,
});

const {
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2165,6 +2165,27 @@ export interface SessionConfigBase {
* @internal
*/
expAssignments?: Record<string, unknown>;

/**
* 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_<key>` 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<string, string>;
}

/**
Expand Down
64 changes: 64 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading