You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
js launch: post-attach 'initialized' race stalls child adoption 3s — CDP function-breakpoint bridge attaches after the entry pause, pre-launch function breakpoints silently never arm #529
The e2e test mcp-server-smoke-js-function-bp.test.ts > "honors conditions evaluated in the callee scope" failed in a full-suite run with expected null not to be null — no pause was ever observed. Root cause: a child-adoption step can miss the child's post-attach initialized DAP event, stall adoption for its full 3 s timeout, and push the CDP function-breakpoint bridge attach past the (forced) entry pause. The pre-launch function breakpoint then silently never arms and the debuggee runs to completion with zero stops.
This is a long-latent race (present since the JS adapter policy refactor), not a recent regression. The test's assertions match the documented contract and need no changes.
Timeline of the failing run
Session 67ff0834, 2026-08-27T17:41Z. Server log debug-mcp-server-587064.log, proxy per-pid log debug-mcp-server-587243.log (pre-#526, so the [CdpFnBpBridge] / [ChildSessionManager] lines are in the per-pid file).
t (17:41:xx)
event
17.954
child attach request sent
18.055
attach response
18.056
child's post-attach initialized event — same TCP chunk as the response
18.056→21.05
handlePostAttachInit waits waitForEvent(child, 'initialized', 3000) — misses the event, times out in full
18.094
CDP entry pause hits; bridge: holding 'entry' stop up to 300ms for its CDP pause record
18.394
bridge: no CDP pause record for 'entry' stop — forwarding unchanged (it isn't attached to the CDP proxy yet, so no record can ever arrive)
18.396
server auto-continues the entry stop (stopOnEntry: false)
18.709/18.711
compute called with 3 / compute called with 7 — run through, nothing armed
21.058
adoption finally proceeds: requestCDPProxy sent
21.145
[CdpFnBpBridge] attached to CDP proxy — 3 s too late; target is running, sticky Debugger.paused replay has nothing to replay
21.9
compute called with 99 + FUNCTION_BP_FIXTURE_DONE; exit 22.07 — zero function-breakpoint stops
In the contrasting passing run (session 38b6a2ec, 17:49Z), the initialized event landed in a later chunk, the wait caught it in ~150 ms, the bridge attached at 52.518 — before the entry stop was forwarded at 52.615 — the sticky pause replay provided the record, and compute armed (armed 'compute' as 7:1 at 52.62). Both condition-gated stops then landed exactly as the test asserts.
Mechanism
MinimalDapClient processes a socket chunk synchronously: the attach response resolves its promise (a microtask), then the initialized event in the same chunk is emitted immediately. Only afterwards does attachChild's continuation run and handlePostAttachInit (src/proxy/child-session-manager.ts:874) register its listener — the event is already gone.
waitForEvent (child-session-manager.ts:993) has no latch; it only observes future events. So the wait burns its whole 3 s.
Adoption is strictly sequential (initializeChild → configureChild → attachChild → handlePostAttachInit → cdpBridge.attachToChild → ensureChildStopped, :539-582), so the bridge attach inherits the stall.
The bridge can only resolve an ESM module-scoped name like compute via Debugger.evaluateOnCallFrame at a pause. The one designed pause is the forced entry stop (Forcing stopOnEntry=true in the js-debug launch config (pending CDP function breakpoints, issue #295)). Miss it and the entry is left "will retry at the next pause" — but the only thing that could produce a next pause is the breakpoint that failed to arm. Silent zero-stop run.
Side effects of the same miss
When the 3 s wait times out, handlePostAttachInit also skips its mirror re-send of exception breakpoints and stored line breakpoints to the child (child-session-manager.ts:878-895).
Residual fragility even on the good path: the held entry stop is released when the CDP pause record arrives, not when arming completes. In the passing run, arming (52.62) beat the resume (52.668) by only ~48 ms. A slow Debugger.setBreakpointOnFunctionCall round trip can still lose to the auto-continue.
Proposed fix
Latch child initialized events, mirroring the existing sawChildStop latch in the same file (:179, set at :947; same shape as the fix(#512): latch a late init ACK instead of losing it between retry windows #515 "latch a late init ACK" fix): count initialized events from wireChildEvents onward; initializeChild consumes the first; handlePostAttachInit checks the latch before falling back to waitForEvent.
Hardening: in cdp-function-breakpoint-bridge.tsprocessStoppedEvent, when entries are pending and a pause record is present, await the in-flight arming attempt (bounded) before releasing the held stop, so the auto-continue cannot resume V8 before Debugger.setBreakpointOnFunctionCall lands.
Summary
The e2e test
mcp-server-smoke-js-function-bp.test.ts > "honors conditions evaluated in the callee scope"failed in a full-suite run withexpected null not to be null— no pause was ever observed. Root cause: a child-adoption step can miss the child's post-attachinitializedDAP event, stall adoption for its full 3 s timeout, and push the CDP function-breakpoint bridge attach past the (forced) entry pause. The pre-launch function breakpoint then silently never arms and the debuggee runs to completion with zero stops.This is a long-latent race (present since the JS adapter policy refactor), not a recent regression. The test's assertions match the documented contract and need no changes.
Timeline of the failing run
Session
67ff0834, 2026-08-27T17:41Z. Server logdebug-mcp-server-587064.log, proxy per-pid logdebug-mcp-server-587243.log(pre-#526, so the[CdpFnBpBridge]/[ChildSessionManager]lines are in the per-pid file).attachrequest sentattachresponseinitializedevent — same TCP chunk as the responsehandlePostAttachInitwaitswaitForEvent(child, 'initialized', 3000)— misses the event, times out in fullholding 'entry' stop up to 300ms for its CDP pause recordno CDP pause record for 'entry' stop — forwarding unchanged(it isn't attached to the CDP proxy yet, so no record can ever arrive)stopOnEntry: false)compute called with 3/compute called with 7— run through, nothing armedrequestCDPProxysent[CdpFnBpBridge] attached to CDP proxy— 3 s too late; target is running, stickyDebugger.pausedreplay has nothing to replaycompute called with 99+FUNCTION_BP_FIXTURE_DONE; exit 22.07 — zero function-breakpoint stopsIn the contrasting passing run (session
38b6a2ec, 17:49Z), theinitializedevent landed in a later chunk, the wait caught it in ~150 ms, the bridge attached at 52.518 — before the entry stop was forwarded at 52.615 — the sticky pause replay provided the record, andcomputearmed (armed 'compute' as 7:1at 52.62). Both condition-gated stops then landed exactly as the test asserts.Mechanism
MinimalDapClientprocesses a socket chunk synchronously: theattachresponse resolves its promise (a microtask), then theinitializedevent in the same chunk is emitted immediately. Only afterwards doesattachChild's continuation run andhandlePostAttachInit(src/proxy/child-session-manager.ts:874) register its listener — the event is already gone.waitForEvent(child-session-manager.ts:993) has no latch; it only observes future events. So the wait burns its whole 3 s.initializeChild → configureChild → attachChild → handlePostAttachInit → cdpBridge.attachToChild → ensureChildStopped,:539-582), so the bridge attach inherits the stall.computeviaDebugger.evaluateOnCallFrameat a pause. The one designed pause is the forced entry stop (Forcing stopOnEntry=true in the js-debug launch config (pending CDP function breakpoints, issue #295)). Miss it and the entry is left "will retry at the next pause" — but the only thing that could produce a next pause is the breakpoint that failed to arm. Silent zero-stop run.Side effects of the same miss
handlePostAttachInitalso skips its mirror re-send of exception breakpoints and stored line breakpoints to the child (child-session-manager.ts:878-895).Debugger.setBreakpointOnFunctionCallround trip can still lose to the auto-continue.Proposed fix
initializedevents, mirroring the existingsawChildStoplatch in the same file (:179, set at:947; same shape as the fix(#512): latch a late init ACK instead of losing it between retry windows #515 "latch a late init ACK" fix): countinitializedevents fromwireChildEventsonward;initializeChildconsumes the first;handlePostAttachInitchecks the latch before falling back towaitForEvent.cdp-function-breakpoint-bridge.tsprocessStoppedEvent, when entries are pending and a pause record is present, await the in-flight arming attempt (bounded) before releasing the held stop, so the auto-continue cannot resume V8 beforeDebugger.setBreakpointOnFunctionCalllands.