Skip to content

Commit 3d11cff

Browse files
feat: add session data store support to TypeScript SDK
- Add sessionDataStore option to CopilotClientOptions - Extend codegen to generate client API handler types (SessionDataStoreHandler) - Register as session data storage provider on connection via sessionDataStore.setDataStore RPC - Add E2E tests for persist, resume, list, delete, and reject scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3bcca2a commit 3d11cff

18 files changed

+705
-16
lines changed

nodejs/src/client.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
StreamMessageReader,
2525
StreamMessageWriter,
2626
} from "vscode-jsonrpc/node.js";
27-
import { createServerRpc } from "./generated/rpc.js";
27+
import { createServerRpc, registerClientApiHandlers } from "./generated/rpc.js";
2828
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
2929
import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
3030
import { getTraceContext } from "./telemetry.js";
@@ -46,6 +46,7 @@ import type {
4646
SessionListFilter,
4747
SessionMetadata,
4848
SystemMessageCustomizeConfig,
49+
SessionDataStoreConfig,
4950
TelemetryConfig,
5051
Tool,
5152
ToolCallRequestPayload,
@@ -216,6 +217,7 @@ export class CopilotClient {
216217
| "onListModels"
217218
| "telemetry"
218219
| "onGetTraceContext"
220+
| "sessionDataStore"
219221
>
220222
> & {
221223
cliPath?: string;
@@ -238,6 +240,8 @@ export class CopilotClient {
238240
private _rpc: ReturnType<typeof createServerRpc> | null = null;
239241
private processExitPromise: Promise<never> | null = null; // Rejects when CLI process exits
240242
private negotiatedProtocolVersion: number | null = null;
243+
/** Connection-level session data store config, set via constructor option. */
244+
private sessionDataStoreConfig: SessionDataStoreConfig | null = null;
241245

242246
/**
243247
* Typed server-scoped RPC methods.
@@ -307,6 +311,7 @@ export class CopilotClient {
307311

308312
this.onListModels = options.onListModels;
309313
this.onGetTraceContext = options.onGetTraceContext;
314+
this.sessionDataStoreConfig = options.sessionDataStore ?? null;
310315

311316
const effectiveEnv = options.env ?? process.env;
312317
this.options = {
@@ -399,6 +404,13 @@ export class CopilotClient {
399404
// Verify protocol version compatibility
400405
await this.verifyProtocolVersion();
401406

407+
// If a session data store was configured, register as the storage provider
408+
if (this.sessionDataStoreConfig) {
409+
await this.connection!.sendRequest("sessionDataStore.setDataStore", {
410+
descriptor: this.sessionDataStoreConfig.descriptor,
411+
});
412+
}
413+
402414
this.state = "connected";
403415
} catch (error) {
404416
this.state = "error";
@@ -1069,7 +1081,9 @@ export class CopilotClient {
10691081
throw new Error("Client not connected");
10701082
}
10711083

1072-
const response = await this.connection.sendRequest("session.list", { filter });
1084+
const response = await this.connection.sendRequest("session.list", {
1085+
filter,
1086+
});
10731087
const { sessions } = response as {
10741088
sessions: Array<{
10751089
sessionId: string;
@@ -1562,6 +1576,13 @@ export class CopilotClient {
15621576
await this.handleSystemMessageTransform(params)
15631577
);
15641578

1579+
// Register session data store RPC handlers if configured.
1580+
if (this.sessionDataStoreConfig) {
1581+
registerClientApiHandlers(this.connection, {
1582+
sessionDataStore: this.sessionDataStoreConfig,
1583+
});
1584+
}
1585+
15651586
this.connection.onClose(() => {
15661587
this.state = "disconnected";
15671588
});

nodejs/src/generated/rpc.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@ export interface AccountGetQuotaResult {
179179
};
180180
}
181181

182+
export interface SessionDataStoreSetDataStoreResult {
183+
/**
184+
* Whether the data store was set successfully
185+
*/
186+
success: boolean;
187+
}
188+
189+
export interface SessionDataStoreSetDataStoreParams {
190+
/**
191+
* Opaque descriptor identifying the storage backend (e.g., 'redis://localhost/sessions')
192+
*/
193+
descriptor: string;
194+
}
195+
182196
export interface SessionModelGetCurrentResult {
183197
/**
184198
* Currently active model identifier
@@ -1050,6 +1064,78 @@ export interface SessionShellKillParams {
10501064
signal?: "SIGTERM" | "SIGKILL" | "SIGINT";
10511065
}
10521066

1067+
export interface SessionDataStoreLoadResult {
1068+
/**
1069+
* All persisted events for the session, in order
1070+
*/
1071+
events: {
1072+
[k: string]: unknown;
1073+
}[];
1074+
}
1075+
1076+
export interface SessionDataStoreLoadParams {
1077+
/**
1078+
* The session to load events for
1079+
*/
1080+
sessionId: string;
1081+
}
1082+
1083+
export interface SessionDataStoreAppendParams {
1084+
/**
1085+
* The session to append events to
1086+
*/
1087+
sessionId: string;
1088+
/**
1089+
* Events to append, in order
1090+
*/
1091+
events: {
1092+
[k: string]: unknown;
1093+
}[];
1094+
}
1095+
1096+
export interface SessionDataStoreTruncateResult {
1097+
/**
1098+
* Number of events removed
1099+
*/
1100+
eventsRemoved: number;
1101+
/**
1102+
* Number of events kept
1103+
*/
1104+
eventsKept: number;
1105+
}
1106+
1107+
export interface SessionDataStoreTruncateParams {
1108+
/**
1109+
* The session to truncate
1110+
*/
1111+
sessionId: string;
1112+
/**
1113+
* Event ID marking the truncation boundary (excluded)
1114+
*/
1115+
upToEventId: string;
1116+
}
1117+
1118+
export interface SessionDataStoreListResult {
1119+
sessions: {
1120+
sessionId: string;
1121+
/**
1122+
* ISO 8601 timestamp of last modification
1123+
*/
1124+
mtime: string;
1125+
/**
1126+
* ISO 8601 timestamp of creation
1127+
*/
1128+
birthtime: string;
1129+
}[];
1130+
}
1131+
1132+
export interface SessionDataStoreDeleteParams {
1133+
/**
1134+
* The session to delete
1135+
*/
1136+
sessionId: string;
1137+
}
1138+
10531139
/** Create typed server-scoped RPC methods (no session required). */
10541140
export function createServerRpc(connection: MessageConnection) {
10551141
return {
@@ -1067,6 +1153,10 @@ export function createServerRpc(connection: MessageConnection) {
10671153
getQuota: async (): Promise<AccountGetQuotaResult> =>
10681154
connection.sendRequest("account.getQuota", {}),
10691155
},
1156+
sessionDataStore: {
1157+
setDataStore: async (params: SessionDataStoreSetDataStoreParams): Promise<SessionDataStoreSetDataStoreResult> =>
1158+
connection.sendRequest("sessionDataStore.setDataStore", params),
1159+
},
10701160
};
10711161
}
10721162

@@ -1188,3 +1278,40 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
11881278
},
11891279
};
11901280
}
1281+
1282+
/**
1283+
* Handler interface for the `sessionDataStore` client API group.
1284+
* Implement this to provide a custom sessionDataStore backend.
1285+
*/
1286+
export interface SessionDataStoreHandler {
1287+
load(params: SessionDataStoreLoadParams): Promise<SessionDataStoreLoadResult>;
1288+
append(params: SessionDataStoreAppendParams): Promise<void>;
1289+
truncate(params: SessionDataStoreTruncateParams): Promise<SessionDataStoreTruncateResult>;
1290+
list(): Promise<SessionDataStoreListResult>;
1291+
delete(params: SessionDataStoreDeleteParams): Promise<void>;
1292+
}
1293+
1294+
/** All client API handler groups. Each group is optional. */
1295+
export interface ClientApiHandlers {
1296+
sessionDataStore?: SessionDataStoreHandler;
1297+
}
1298+
1299+
/**
1300+
* Register client API handlers on a JSON-RPC connection.
1301+
* The server calls these methods to delegate work to the client.
1302+
* Methods for unregistered groups will respond with a standard JSON-RPC
1303+
* method-not-found error.
1304+
*/
1305+
export function registerClientApiHandlers(
1306+
connection: MessageConnection,
1307+
handlers: ClientApiHandlers,
1308+
): void {
1309+
if (handlers.sessionDataStore) {
1310+
const h = handlers.sessionDataStore!;
1311+
connection.onRequest("sessionDataStore.load", (params: SessionDataStoreLoadParams) => h.load(params));
1312+
connection.onRequest("sessionDataStore.append", (params: SessionDataStoreAppendParams) => h.append(params));
1313+
connection.onRequest("sessionDataStore.truncate", (params: SessionDataStoreTruncateParams) => h.truncate(params));
1314+
connection.onRequest("sessionDataStore.list", () => h.list());
1315+
connection.onRequest("sessionDataStore.delete", (params: SessionDataStoreDeleteParams) => h.delete(params));
1316+
}
1317+
}

nodejs/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export type {
5656
SessionListFilter,
5757
SessionMetadata,
5858
SessionUiApi,
59+
SessionDataStoreConfig,
60+
SessionDataStoreHandler,
61+
ClientApiHandlers,
5962
SystemMessageAppendConfig,
6063
SystemMessageConfig,
6164
SystemMessageCustomizeConfig,

nodejs/src/types.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@
1010
import type { SessionEvent as GeneratedSessionEvent } from "./generated/session-events.js";
1111
export type SessionEvent = GeneratedSessionEvent;
1212

13+
// Re-export generated client API types
14+
export type {
15+
SessionDataStoreHandler,
16+
SessionDataStoreLoadParams,
17+
SessionDataStoreLoadResult,
18+
SessionDataStoreAppendParams,
19+
SessionDataStoreTruncateParams,
20+
SessionDataStoreTruncateResult,
21+
SessionDataStoreListResult,
22+
SessionDataStoreDeleteParams,
23+
ClientApiHandlers,
24+
} from "./generated/rpc.js";
25+
26+
import type { SessionDataStoreHandler } from "./generated/rpc.js";
27+
1328
/**
1429
* Options for creating a CopilotClient
1530
*/
@@ -171,6 +186,14 @@ export interface CopilotClientOptions {
171186
* ```
172187
*/
173188
onGetTraceContext?: TraceContextProvider;
189+
190+
/**
191+
* Custom session data storage backend.
192+
* When provided, the client registers as the session data storage provider
193+
* on connection, routing all event persistence through these callbacks
194+
* instead of the server's default file-based storage.
195+
*/
196+
sessionDataStore?: SessionDataStoreConfig;
174197
}
175198

176199
/**
@@ -1318,6 +1341,20 @@ export interface SessionContext {
13181341
branch?: string;
13191342
}
13201343

1344+
/**
1345+
* Configuration for a custom session data store backend.
1346+
*
1347+
* Extends the generated {@link SessionDataStoreHandler} with a `descriptor`
1348+
* that identifies the storage backend for display purposes.
1349+
*/
1350+
export interface SessionDataStoreConfig extends SessionDataStoreHandler {
1351+
/**
1352+
* Opaque descriptor identifying this storage backend.
1353+
* Used for UI display (e.g., `"redis://localhost/sessions"`).
1354+
*/
1355+
descriptor: string;
1356+
}
1357+
13211358
/**
13221359
* Filter options for listing sessions
13231360
*/

0 commit comments

Comments
 (0)