After some investigation and assistance from crazy_stewie, we managed to determine the cause of the issue.

First, we disabled the render server using RenderingServer.render_loop_enabled = false when we started the transition and drew the next frame using RenderingServer.force_draw.call_deferred(). This helped us confirm that the frame was being rendered incorrectly for the next frame, sometime between the transition starting and when the camera position was set.

Adding a breakpoint, we were also able to confirm that the camera position was set correctly during that frame as well, but it was still rendering in the incorrect position.

We also used the following code to see when the frames were drawn:

func init()
	RenderingServer.frame_pre_draw.connect(frame_pre_draw)
	RenderingServer.frame_post_draw.connect(frame_post_draw)
 
func frame_pre_draw():
	print("Pre Draw")
 
func frame_post_draw():
	print("Post Draw")

This, coupled with other debugging prints helped point out the fact that there was a frame being rendered with the position set to the correct value, but still rendering in the incorrect place.

As it turns out, as crazy_stewie explained, the camera can be thought of as having two different representations; the camera is just a fancy interface to a viewport in the RenderingServer, which does the actual rendering. The function to force the camera to use the new position is align(). Calling this function after setting the position fixes the issue.

Inspecting the code, it appears that align() takes the global position into account when updating its internal representation before calling update_scroll() which updates the viewport. Adding a debugging print for the update_scroll function also reveals that it is called before the frame is rendered, but since it doesn’t take the global position into account, the viewport is not updated correctly.

Calling align() fixes the problem. Consider the following code:

func update_position():
	if target:
		global_position = target.global_position + Vector2(0, -100)
		print("Setting camera position to ", global_position)
		print("Smoothing enabled? ", position_smoothing_enabled, "; Target position: ", get_target_position())
		align()
		print("Smoothing enabled? ", position_smoothing_enabled, "; Target position: ", get_target_position())
 
func _init():
	RenderingServer.frame_pre_draw.connect(frame_pre_draw)
	RenderingServer.frame_post_draw.connect(frame_post_draw)
 
func frame_pre_draw():
	print("Pre Draw")
 
func frame_post_draw():
	print("Post Draw")
 
func _notification(what: int):
	# This notification will call `update_scroll()`. No other notification calls `align()`
	# See https://github.com/godotengine/godot/blob/a8453cb3337c2e27c061a385f9c772cf670e38e0/scene/2d/camera_2d.cpp#L230-L234
	if what == NOTIFICATION_TRANSFORM_CHANGED:
		print("update_scroll")

This results in the following (abridged) log:

Setting camera position to (-120, -73)
Smoothing enabled? false; Target position: (136, -49)
Smoothing enabled? false; Target position: (-120, -73)
update_scroll
Pre Draw
Post Draw

As you can see, the target position isn’t updated unless we call align even though we set the position and update_scroll, which updates the viewport in the RenderingServer, will use the incorrect position (the target position) if we don’t call align.