Add shared session watch APIs by toliaqat · Pull Request #2415 · github/copilot-sdk · GitHub
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
23 changes: 23 additions & 0 deletions nodejs/README.md
92 changes: 86 additions & 6 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import type {
SessionUpdateOptionsParams,
} from "./generated/rpc.js";
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
import { CopilotSession } from "./session.js";
import { CopilotSession, SharedSessionWatch } from "./session.js";
import type { FfiRuntimeHost } from "./ffiRuntimeHost.js";
import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js";
import { createCopilotRequestAdapter } from "./copilotRequestHandler.js";
Expand Down Expand Up @@ -486,6 +486,7 @@ export class CopilotClient {
private actualHost: string = "localhost";
private state: "disconnected" | "connecting" | "connected" | "error" = "disconnected";
private sessions: Map<string, CopilotSession> = new Map();
private sharedSessionWatches: Map<string, SharedSessionWatch> = new Map();
private stderrBuffer: string = ""; // Captures CLI stderr for error messages
/** Resolved connection mode chosen in the constructor. */
private connectionConfig: InternalRuntimeConnection;
Expand Down Expand Up @@ -1029,6 +1030,19 @@ export class CopilotClient {
async stop(): Promise<Error[]> {
const errors: Error[] = [];

const activeWatches = [...this.sharedSessionWatches.values()];
for (const watch of activeWatches) {
try {
await watch.close();
} catch (error) {
errors.push(
new Error(
`Failed to close shared-session watch ${watch.sessionId}: ${error instanceof Error ? error.message : String(error)}`
)
);
}
}

// Disconnect all active sessions with retry logic
const activeSessions = [...this.sessions.values()];
// TEMPORARY: over the in-process (FFI) transport the runtime shares this
Expand Down Expand Up @@ -1078,6 +1092,7 @@ export class CopilotClient {
session._markDisconnected();
}
this.sessions.clear();
this.sharedSessionWatches.clear();
this.githubTokenProviders.clear();

// Ask SDK-owned runtimes to flush and clean up before we tear down
Expand Down Expand Up @@ -1261,6 +1276,7 @@ export class CopilotClient {
session._markDisconnected();
}
this.sessions.clear();
this.sharedSessionWatches.clear();
this.githubTokenProviders.clear();

// Force close connection. Suppress writer failures first so teardown
Expand Down Expand Up @@ -1773,6 +1789,54 @@ export class CopilotClient {
return session;
}

/**
* Watch a session shared with the authenticated user.
*
* The returned handle exposes canonical history and live events but no
* interactive session operations. Authentication and lane routing remain
* entirely inside the runtime. Register a `session.disconnected` lifecycle
* handler before calling this method if terminal connection loss must not
* be missed.
*
* @param sessionId - The owner's shared session ID.
*/
async watchSharedSession(sessionId: string): Promise<SharedSessionWatch> {
if (!this.connection) {
await this.start();
}

const result = await this.rpc.sessions.watch({ sessionId });
if (result.readOnly !== true) {
await this.rpc.sessions.close({ sessionId: result.sessionId });
throw new Error("Runtime returned an interactive shared-session watch");
}

const routedSession = new CopilotSession(
result.sessionId,
this.connection!,
undefined,
this.onGetTraceContext
);
const closeWatch = async (): Promise<void> => {
try {
await this.rpc.sessions.close({ sessionId: result.sessionId });
} finally {
routedSession._markDisconnected();
this.sessions.delete(result.sessionId);
this.sharedSessionWatches.delete(result.sessionId);
}
};
const watch = new SharedSessionWatch(
result.sessionId,
result.metadata,
routedSession,
closeWatch
);
this.sessions.set(result.sessionId, routedSession);
this.sharedSessionWatches.set(result.sessionId, watch);
return watch;
}

/**
* Resumes an existing conversation session by its ID.
*
Expand Down Expand Up @@ -3102,11 +3166,18 @@ export class CopilotClient {
};
}

const event = {
type: raw.type,
sessionId: raw.sessionId,
metadata,
} as SessionLifecycleEvent;
const event = (
raw.type === "session.disconnected"
? {
type: raw.type,
sessionId: raw.sessionId,
}
: {
type: raw.type,
sessionId: raw.sessionId,
metadata,
}
) as SessionLifecycleEvent;

// Dispatch to typed handlers for this specific event type
const typedHandlers = this.typedLifecycleHandlers.get(event.type);
Expand All @@ -3128,6 +3199,15 @@ export class CopilotClient {
// Ignore handler errors
}
}

if (
event.type === "session.disconnected" &&
this.sharedSessionWatches.has(event.sessionId)
) {
this.sessions.get(event.sessionId)?._markDisconnected();
this.sessions.delete(event.sessionId);
this.sharedSessionWatches.delete(event.sessionId);
}
}

private async handleUserInputRequest(params: {
Expand Down
40 changes: 40 additions & 0 deletions nodejs/src/generated/rpc.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
export { CopilotClient } from "./client.js";
export { DisableBypassPermissionsModes, RuntimeConnection } from "./types.js";
export { BuiltInTools, ToolSet } from "./toolSet.js";
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
export {
CopilotSession,
SharedSessionWatch,
type AssistantMessageEvent,
type SharedSessionMetadata,
} from "./session.js";
export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js";
export {
Canvas,
Expand Down Expand Up @@ -158,6 +163,7 @@ export type {
SessionHooks,
SessionCreatedEvent,
SessionDeletedEvent,
SessionDisconnectedEvent,
SessionUpdatedEvent,
SessionForegroundEvent,
SessionBackgroundEvent,
Expand Down
90 changes: 90 additions & 0 deletions nodejs/src/session.ts
Loading
Loading