@@ -23,7 +23,8 @@ import {
2323 StreamMessageReader ,
2424 StreamMessageWriter ,
2525} from "vscode-jsonrpc/node.js" ;
26- import { createServerRpc } from "./generated/rpc.js" ;
26+ import { createServerRpc , registerClientApiHandlers } from "./generated/rpc.js" ;
27+ import type { SessionStoreHandler } from "./generated/rpc.js" ;
2728import { getSdkProtocolVersion } from "./sdkProtocolVersion.js" ;
2829import { CopilotSession , NO_RESULT_PERMISSION_V2_ERROR } from "./session.js" ;
2930import { getTraceContext } from "./telemetry.js" ;
@@ -179,8 +180,12 @@ export class CopilotClient {
179180 private negotiatedProtocolVersion : number | null = null ;
180181 /** Per-session store configs, keyed by sessionId. Used to route sessionStore.* RPC requests. */
181182 private sessionStoreConfigs : Map < string , SessionStoreConfig > = new Map ( ) ;
182- /** Temporary store used during listSessions() calls with a session store. */
183- private listSessionsStore : SessionStoreConfig | null = null ;
183+ /**
184+ * Stores used during listSessions() calls. Keyed by an incrementing ID to support concurrent calls.
185+ * The latest registered store is used when the server calls sessionStore.list.
186+ */
187+ private listSessionsStores : Map < number , SessionStoreConfig > = new Map ( ) ;
188+ private listSessionsStoreNextId = 0 ;
184189
185190 /**
186191 * Typed server-scoped RPC methods.
@@ -403,6 +408,8 @@ export class CopilotClient {
403408 }
404409 }
405410 this . sessions . clear ( ) ;
411+ this . sessionStoreConfigs . clear ( ) ;
412+ this . listSessionsStores . clear ( ) ;
406413
407414 // Close connection
408415 if ( this . connection ) {
@@ -487,6 +494,8 @@ export class CopilotClient {
487494
488495 // Clear sessions immediately without trying to destroy them
489496 this . sessions . clear ( ) ;
497+ this . sessionStoreConfigs . clear ( ) ;
498+ this . listSessionsStores . clear ( ) ;
490499
491500 // Force close connection
492501 if ( this . connection ) {
@@ -1005,9 +1014,11 @@ export class CopilotClient {
10051014 ? { filter : filterOrOptions as SessionListFilter }
10061015 : undefined ;
10071016
1008- // Temporarily register the store for the duration of this RPC call
1017+ // Register the store for the duration of this RPC call (supports concurrent calls)
1018+ let storeId : number | undefined ;
10091019 if ( options ?. sessionStore ) {
1010- this . listSessionsStore = options . sessionStore ;
1020+ storeId = this . listSessionsStoreNextId ++ ;
1021+ this . listSessionsStores . set ( storeId , options . sessionStore ) ;
10111022 }
10121023
10131024 try {
@@ -1037,8 +1048,8 @@ export class CopilotClient {
10371048 context : s . context ,
10381049 } ) ) ;
10391050 } finally {
1040- if ( options ?. sessionStore ) {
1041- this . listSessionsStore = null ;
1051+ if ( storeId !== undefined ) {
1052+ this . listSessionsStores . delete ( storeId ) ;
10421053 }
10431054 }
10441055 }
@@ -1505,78 +1516,63 @@ export class CopilotClient {
15051516 } ) : Promise < { output ?: unknown } > => await this . handleHooksInvoke ( params )
15061517 ) ;
15071518
1508- // Register session store RPC handlers (server→client callbacks for event persistence)
1509- this . connection . onRequest (
1510- "sessionStore.load" ,
1511- async ( params : { sessionId : string } ) : Promise < { events : SessionEvent [ ] } > => {
1512- const store = this . sessionStoreConfigs . get ( params . sessionId ) ;
1513- if ( ! store ) {
1514- throw new Error ( `No session store registered for session ${ params . sessionId } ` ) ;
1515- }
1516- const events = await store . onLoad ( params . sessionId ) ;
1517- return { events } ;
1518- }
1519- ) ;
1519+ // Register session store RPC handlers via generated registration function.
1520+ // The handler routes each call to the appropriate SessionStoreConfig based on sessionId.
1521+ registerClientApiHandlers ( this . connection , {
1522+ sessionStore : this . createSessionStoreHandler ( ) ,
1523+ } ) ;
15201524
1521- this . connection . onRequest (
1522- "sessionStore.append" ,
1523- async ( params : { sessionId : string ; events : SessionEvent [ ] } ) : Promise < void > => {
1524- const store = this . sessionStoreConfigs . get ( params . sessionId ) ;
1525- if ( ! store ) {
1526- throw new Error ( `No session store registered for session ${ params . sessionId } ` ) ;
1527- }
1528- await store . onAppend ( params . events , params . sessionId ) ;
1529- }
1530- ) ;
1525+ this . connection . onClose ( ( ) => {
1526+ this . state = "disconnected" ;
1527+ } ) ;
15311528
1532- this . connection . onRequest (
1533- "sessionStore.truncate" ,
1534- async ( params : {
1535- sessionId : string ;
1536- upToEventId : string ;
1537- } ) : Promise < { eventsRemoved : number ; eventsKept : number } > => {
1538- const store = this . sessionStoreConfigs . get ( params . sessionId ) ;
1539- if ( ! store ) {
1540- throw new Error ( `No session store registered for session ${ params . sessionId } ` ) ;
1541- }
1542- return await store . onTruncate ( params . upToEventId , params . sessionId ) ;
1529+ this . connection . onError ( ( _error ) => {
1530+ this . state = "disconnected" ;
1531+ } ) ;
1532+ }
1533+
1534+ /**
1535+ * Create a {@link SessionStoreHandler} that routes each RPC call to the
1536+ * appropriate {@link SessionStoreConfig} registered for the given session.
1537+ */
1538+ private createSessionStoreHandler ( ) : SessionStoreHandler {
1539+ const getStore = ( sessionId : string ) : SessionStoreConfig => {
1540+ const store = this . sessionStoreConfigs . get ( sessionId ) ;
1541+ if ( ! store ) {
1542+ throw new Error ( `No session store registered for session ${ sessionId } ` ) ;
15431543 }
1544- ) ;
1544+ return store ;
1545+ } ;
15451546
1546- this . connection . onRequest (
1547- "sessionStore.list" ,
1548- async ( ) : Promise < {
1549- sessions : Array < { sessionId : string ; mtime : string ; birthtime : string } > ;
1550- } > => {
1547+ return {
1548+ load : async ( params ) => {
1549+ const store = getStore ( params . sessionId ) ;
1550+ const events = await store . onLoad ( params . sessionId ) ;
1551+ // Events are opaque on the wire but typed in the SDK consumer API
1552+ return { events : events as Record < string , unknown > [ ] } ;
1553+ } ,
1554+ append : async ( params ) => {
1555+ const store = getStore ( params . sessionId ) ;
1556+ await store . onAppend ( params . events as SessionEvent [ ] , params . sessionId ) ;
1557+ } ,
1558+ truncate : async ( params ) => {
1559+ const store = getStore ( params . sessionId ) ;
1560+ return store . onTruncate ( params . upToEventId , params . sessionId ) ;
1561+ } ,
1562+ list : async ( ) => {
15511563 // Use the most recently registered store for listing.
1552- // In practice, listSessions() registers a temporary store before the RPC call.
1553- const store = this . listSessionsStore ;
1564+ const store = Array . from ( this . listSessionsStores . values ( ) ) . pop ( ) ;
15541565 if ( ! store ) {
15551566 throw new Error ( "No session store registered for listing" ) ;
15561567 }
15571568 const sessions = await store . onListSessions ( ) ;
15581569 return { sessions } ;
1559- }
1560- ) ;
1561-
1562- this . connection . onRequest (
1563- "sessionStore.delete" ,
1564- async ( params : { sessionId : string } ) : Promise < void > => {
1565- const store = this . sessionStoreConfigs . get ( params . sessionId ) ;
1566- if ( ! store ) {
1567- throw new Error ( `No session store registered for session ${ params . sessionId } ` ) ;
1568- }
1570+ } ,
1571+ delete : async ( params ) => {
1572+ const store = getStore ( params . sessionId ) ;
15691573 await store . onDelete ( params . sessionId ) ;
1570- }
1571- ) ;
1572-
1573- this . connection . onClose ( ( ) => {
1574- this . state = "disconnected" ;
1575- } ) ;
1576-
1577- this . connection . onError ( ( _error ) => {
1578- this . state = "disconnected" ;
1579- } ) ;
1574+ } ,
1575+ } ;
15801576 }
15811577
15821578 private handleSessionEventNotification ( notification : unknown ) : void {
0 commit comments