@@ -74,6 +74,10 @@ import {
7474 requestedCursorToolUseCount ,
7575} from "./tool-definitions" ;
7676import type { CursorNativeToolDeps } from "./native-exec-tools" ;
77+ import {
78+ terminateBackgroundShellsForSession ,
79+ type BackgroundShellTerminationReport ,
80+ } from "./native-exec-shell" ;
7781import type { CursorClientMessage , CursorRunRequest , CursorServerMessage } from "./types" ;
7882import type { CursorTransport , CursorTransportFactoryInput } from "./transport" ;
7983
@@ -418,6 +422,7 @@ class LiveCursorTransport implements CursorTransport {
418422 private mcpPrepared ?: Promise < void > ;
419423 private releaseMcpObservation ?: ( ) => void ;
420424 private blobRequestScope ?: CursorBlobRequestScopeToken ;
425+ private shellCleanup ?: Promise < BackgroundShellTerminationReport > ;
421426 // Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
422427 // close; safe to read after a stream failure because open() owns the only writer before run().
423428 private turnStartedAt = 0 ;
@@ -437,7 +442,11 @@ class LiveCursorTransport implements CursorTransport {
437442 this . activeClientToolFinalizeGraceMs = this . clientToolFinalizeGraceMs ;
438443 // Desktop (computer-use / record-screen) executors are available even with no MCP servers.
439444 this . desktopDeps = desktopDepsFromConfig ( input . provider . desktopExecutor ) ;
440- this . execContext = { ...this . desktopDeps , unsafeAllowNativeLocalExec : effectiveCursorNativeExecAllow ( input . provider , input . requestDeclaresFullAccess === true ) } ;
445+ this . execContext = {
446+ ...this . desktopDeps ,
447+ sessionId : this . sessionId ,
448+ unsafeAllowNativeLocalExec : effectiveCursorNativeExecAllow ( input . provider , input . requestDeclaresFullAccess === true ) ,
449+ } ;
441450 const servers = resolveMcpServers ( input . provider ) ;
442451 if ( servers . length > 0 ) {
443452 this . mcpManager = new CursorMcpManager ( servers , {
@@ -470,6 +479,7 @@ class LiveCursorTransport implements CursorTransport {
470479 ...this . desktopDeps ,
471480 ...mcpDepsFromManager ( this . mcpManager ! ) ,
472481 mcpToolDefs,
482+ sessionId : this . sessionId ,
473483 unsafeAllowNativeLocalExec : effectiveCursorNativeExecAllow ( this . input . provider , this . input . requestDeclaresFullAccess === true ) ,
474484 } ;
475485 } catch ( err ) {
@@ -648,7 +658,11 @@ class LiveCursorTransport implements CursorTransport {
648658 }
649659 }
650660
651- close ( ) : void {
661+ private startShellCleanup ( ) : Promise < BackgroundShellTerminationReport > {
662+ return this . shellCleanup ??= terminateBackgroundShellsForSession ( this . sessionId ) ;
663+ }
664+
665+ async close ( ) : Promise < void > {
652666 if ( this . heartbeat ) clearInterval ( this . heartbeat ) ;
653667 this . clearPendingFinalize ( ) ;
654668 this . clearFirstFrameTimer ( ) ;
@@ -658,6 +672,7 @@ class LiveCursorTransport implements CursorTransport {
658672 this . releaseMcpObservation ?.( ) ;
659673 this . releaseMcpObservation = undefined ;
660674 void this . mcpManager ?. dispose ( ) ;
675+ await this . startShellCleanup ( ) ;
661676 }
662677
663678 private cancelCursorRun ( ) : void {
@@ -675,6 +690,7 @@ class LiveCursorTransport implements CursorTransport {
675690 this . releaseMcpObservation ?.( ) ;
676691 this . releaseMcpObservation = undefined ;
677692 void this . mcpManager ?. dispose ( ) ;
693+ void this . startShellCleanup ( ) . catch ( ( ) => { /* close() observes the same cleanup promise */ } ) ;
678694 }
679695
680696 private releaseBlobRequestScope ( ) : void {
@@ -850,16 +866,17 @@ class LiveCursorTransport implements CursorTransport {
850866 return ;
851867 }
852868 const previousPayloadBytes = connectBufferedPayloadBytes ( pending ) ;
853- // Transfer the retained-byte lease from the aggregate pending buffer to decoded frame and
854- // residual owners. Charging both copies transiently would cut the 32 MiB frame contract in half.
855- this . releaseTransportBytes ( previousPayloadBytes ) ;
869+ // The decoder materializes payload and residual copies. Keep the aggregate pending owner
870+ // charged until every replacement has been admitted and committed; a failed admission must
871+ // leave that predecessor lease intact for deterministic cleanup.
856872 const decoded = decodeAvailableConnectFrames (
857873 pending ,
858874 CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES ,
859875 availableSlots ,
860876 reservePayloadCopy ,
861877 ) ;
862878 pending = decoded . remainder ;
879+ this . releaseTransportBytes ( previousPayloadBytes ) ;
863880 for ( const frame of decoded . frames ) {
864881 this . pendingTransportFrames += 1 ;
865882 this . updateTransportFlowControl ( ) ;
@@ -882,18 +899,26 @@ class LiveCursorTransport implements CursorTransport {
882899 debugProviderDiagnostic ( "cursor" , "first-frame" , { latencyMs : this . firstFrameAt - this . turnStartedAt } ) ;
883900 }
884901 const bytes = typeof chunk === "string" ? new TextEncoder ( ) . encode ( chunk ) : new Uint8Array ( chunk . buffer , chunk . byteOffset , chunk . byteLength ) ;
902+ let incomingPayloadBytes = 0 ;
903+ let incomingCharged = false ;
904+ let replacement : ReturnType < typeof reservePayloadCopy > | undefined ;
885905 try {
886906 const previousPendingPayloadBytes = connectBufferedPayloadBytes ( pending ) ;
907+ const nextPendingPayloadBytes = connectBufferedPayloadBytesAcross ( pending , bytes ) ;
908+ incomingPayloadBytes = Math . max ( 0 , nextPendingPayloadBytes - previousPendingPayloadBytes ) ;
909+ this . reserveTransportBytes ( incomingPayloadBytes ) ;
910+ incomingCharged = true ;
911+ replacement = reservePayloadCopy ( nextPendingPayloadBytes ) ;
887912 const nextPending = concatBytes ( pending , bytes ) ;
888- const nextPendingPayloadBytes = connectBufferedPayloadBytes ( nextPending ) ;
889- // The incoming HTTP/2 chunk is externally owned. Replace the prior pending lease with the
890- // concatenated buffer lease instead of charging old + new simultaneously.
891- this . releaseTransportBytes ( previousPendingPayloadBytes ) ;
892- const replacement = reservePayloadCopy ( nextPendingPayloadBytes ) ;
893913 pending = nextPending ;
894914 replacement . commitRetained ( ) ;
915+ replacement = undefined ;
916+ this . releaseTransportBytes ( previousPendingPayloadBytes + incomingPayloadBytes ) ;
917+ incomingCharged = false ;
895918 drainPendingFrames ( ) ;
896919 } catch ( err ) {
920+ replacement ?. release ( ) ;
921+ if ( incomingCharged ) this . releaseTransportBytes ( incomingPayloadBytes ) ;
897922 failAndClear ( err instanceof Error ? err : new Error ( String ( err ) ) ) ;
898923 }
899924 } ) ;
@@ -1135,6 +1160,27 @@ function connectBufferedPayloadBytes(input: Uint8Array): number {
11351160 return payloadBytes ;
11361161}
11371162
1163+ /** Payload-byte count for a virtual concatenation, without allocating the concatenated buffer. */
1164+ function connectBufferedPayloadBytesAcross ( first : Uint8Array , second : Uint8Array ) : number {
1165+ const totalBytes = first . byteLength + second . byteLength ;
1166+ const byteAt = ( index : number ) : number => index < first . byteLength
1167+ ? first [ index ] !
1168+ : second [ index - first . byteLength ] ! ;
1169+ let offset = 0 ;
1170+ let payloadBytes = 0 ;
1171+ while ( totalBytes - offset >= 5 ) {
1172+ const length = ( byteAt ( offset + 1 ) * 0x1000000 )
1173+ + ( byteAt ( offset + 2 ) << 16 )
1174+ + ( byteAt ( offset + 3 ) << 8 )
1175+ + byteAt ( offset + 4 ) ;
1176+ const available = Math . min ( length , totalBytes - offset - 5 ) ;
1177+ payloadBytes += available ;
1178+ if ( available < length ) break ;
1179+ offset += 5 + length ;
1180+ }
1181+ return payloadBytes ;
1182+ }
1183+
11381184/** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
11391185function cursorHostLabel ( baseUrl : string ) : string {
11401186 try {
0 commit comments