Repository navigation
fix(proxy): the DAP connect window ends with the adapter's exit or the parent's deadline; the worker's stdout pipe is drained (#963) - #964
Merged
Conversation
…e parent's deadline, and the worker's stdout pipe is drained (#963) `DapConnectionManager.connectWithRetry` was a fixed 60 attempts (~12.5 s) for every adapter, alive or dead, unrelated to the parent's 30 s init ceiling: an adapter that took longer to listen (rdbg on a loaded Windows runner, 2 of 5 CI runs on 2026-10-19) failed the launch with ECONNREFUSED while still booting. The parent's deadline now travels in the init payload (`initializationTimeoutMs`); in spawn mode the loop runs until the adapter process exits (an AbortSignal from its exit handler, the error quoting the exit and stderr tail) or 500 ms past the parent's deadline, whose stage-aware message is the one a slow boot gets. Connect mode and legacy payloads keep the historical window as the default; the cadence is unchanged. Found by the new 14 s integration case: `ProxyManager` spawned the worker with a piped stdout it never read. Outside the server CLI (which silences console output) the worker's console transport filled the 64 KB pipe and the worker blocked solid, deaf to IPC. The parent now drains stdout like stderr, logged at debug and sanitized. The stdio-client test suites log to logs/tests/stdio/, so their proxy logs reach the Windows CI diagnostics artifact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3ULnoVSoCZ5t2Y8d98Ctv
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… mid-connect; state the elapsed time on expiry (#963) Own-PR review follow-ups. The exited-adapter error quoted the worker's whole 20-line stderr tail (up to ~8 KB); it now quotes the parent's documented 10 lines / 2000 characters, most recent first to go, and says the lines are those seen by the time the process exited — the snapshot is taken in the exit handler, before the pipe has settled, because waiting would hand the race to the parent's generic exit message. The expiry message states the elapsed time beside the window (one connect can outlive it: a remote host that drops SYNs answers ETIMEDOUT after the OS's ~21 s). client.disconnect() on the failure path is guarded so it can never mask the reason. The start test pins that the stdout drain's listeners detach with the rest; the stdio test helper sweeps entries older than a day from logs/tests/stdio, which nothing else cleans; the event-management doc sample sanitizes as its comment says. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3ULnoVSoCZ5t2Y8d98Ctv
This was referenced Oct 20, 2026
Collaborator
Author
|
Dev proxy dogfood after the rebuild from this branch (Windows, the dev proxy at |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #963.
What changed
The DAP connect window is bounded by what it waits for, not by a count.
DapConnectionManager.connectWithRetrywas 500 ms + 60 attempts × 200 ms ≈ 12.5 s for every adapter, alive or dead, with no relation to the parent's 30 s initialization ceiling. An adapter that needed more than ~12.5 s to listen failed the launch withFailed to connect DAP client: connect ECONNREFUSEDwhile it was still booting, 17.5 s before the parent would have given up. Now:ProxyInitPayload.initializationTimeoutMs, written byProxyManager, validated by the parser likeadapterPort).connectWithRetry(host, port, { budgetMs, abort })is bounded by wall clock. In spawn mode the worker passes the parent's deadline + 500 ms (the parent's timer was armed first and its stage-aware message —adapter process spawned (PID n) … DAP connection was never established— is the one a slow boot should get) and anAbortSignalthe adapter'sexithandler fires with{ code, signal, stderrTail }, so a dead adapter ends the wait at once and the error quotes the exit. Connect mode (a remote rdbg/debugpy already listening) and legacy payloads keep the historical 12.5 s window as the default. The 500 ms / 200 ms cadence is unchanged.Failed to connect DAP client:prefix.Found on the way, fixed here: the worker's stdout pipe was never read.
ProxyManagerspawns the worker withstdio: ['pipe','pipe','pipe','ipc']and drained only stderr. Outside the server CLI (which setsCONSOLE_OUTPUT_SILENCED=1, inherited by the worker), the worker's winston console transport writes every log line to stdout; once the OS pipe buffer filled (64 KB on Windows) the worker blocked solid — CPU flat, deaf to IPC (terminateignored, the parent force-killed it), its DAP socket unread, no exception anywhere. The new 14 s integration case was the first test to log that much; it stalled three different ways at a fixed ~13.8 s until the cause was found. The parent now drains stdout like stderr ([ProxyManager STDOUT]at debug, sanitized). EveryProxyManagerembedder that is not the CLI was exposed.Diagnostics. The stdio-client suites' servers now log to
logs/tests/stdio/, so their per-session proxy logs land inside the Windows CI diagnostics artifact; before, they went toos.tmpdir()and were lost with the runner — the connect timeline that would have settled #963 was unrecoverable. The adversarial suite's new case prints the proxy log's connect lines in its failure message;ADVERSARIAL_KEEP_TMP=1keeps its temp dir.Measured
ECONNREFUSED, rdbg alive (PID, no exit code); a sibling launch in the same suite succeeded in 12.9 s under the same load; normal boots 3.5–4.8 snpm run build: 308–386 mslistenDelayMs: 14_000, parent deadline 25 sECONNREFUSED; this branch: connects,start()resolves in 15.4 sCONSOLE_OUTPUT_SILENCED=1orlogLevel: warnmade it vanish,UV_THREADPOOL_SIZE=64changed nothingThe runner, not rdbg, is the variable: locally rdbg listens in under half a second even under a build.
Tests
tests/unit/proxy/dap-proxy-connection-manager.test.ts: past 60 attempts while the budget remains; default budget expires with the "nothing accepted a connection … within 12.5s" message; at least one attempt however small the budget; abort ends the wait at once quoting code/signal/stderr and no attempt follows; a pre-aborted signal makes no attempt; a bareabort()(an AbortError, whose owncodeis 20) is not read as an exit code.tests/proxy/dap-proxy-worker.test.ts: spawn mode passes the deadline-derived budget and a signal; a legacy payload passes no budget; connect mode passes neither; an adapterexitduring the connect aborts the signal with the exit and the stderr tail.tests/unit/proxy/dap-proxy-message-parser.test.ts,tests/unit/proxy/proxy-manager.start.test.ts: the payload field; the init command carries the manager's deadline (default and injected); the stdout drain (listener attached, lines logged, secret line redacted, trailing partial flushed on end).tests/integration/adversarial-dap/adversarial-adapter.test.ts: the 14 s listen-delay case (red on main, green here, ~15 s).Verification
npx vitest run --project unittests/integration/adversarial-dap/adversarial-adapter.test.tslogs/tests/stdio/sessions/npm run lint,npm run typecheck:all(ratchet 0/0),node scripts/check-docs.mjs,pnpm changelog:checkOut of scope
The 500 ms / 200 ms cadence; a readiness signal from rdbg's
DEBUGGER:banner; the parent's 30 s default; #875 (whose next sighting now gets a proxy log in the artifact).🤖 Generated with Claude Code
https://claude.ai/code/session_01Y3ULnoVSoCZ5t2Y8d98Ctv