Repository navigation
fix(webui): the native acceptance names the step it stopped at, and keeps our preload out of node-pty's helper - #31
Merged
Conversation
added 2 commits
November 11, 2026 13:04
Node-pty's Windows kill path forks a helper that lists the processes attached to the shell's console, and `child_process.fork` hands that child this process's `execArgv`. Whatever this process was started with is therefore loaded inside the helper, and the helper has no use for our arguments. Under the acceptance host that meant tsx's loader ran inside node-pty's helper. The failing Windows run of PR #29 shows it: the helper crashed on `AttachConsole` with tsx's transformer in the stack, twice, and the kill then fell back to node-pty's five-second timer. That is the shape microsoft/node-pty#974 describes, and the amplifier is ours because the preload is ours. The kill is synchronous and the helper needs none of our flags, so the flags are put aside for the length of the kill and put back afterwards. In a process started without anything unusual — which is the desktop app — this changes nothing at all.
The native acceptance's teardown hook waits for the host to say it closed. On Windows it sometimes waited the full thirty seconds and then reported a bare timeout, because the host had stopped answering inside its own service close and had nothing to say about it. The one thing the log did carry was the host's step trace, and that is in the hook's error text rather than in the failure itself. The host now gives the service close a deadline of its own, and when that deadline passes it reports the step it stopped at before it goes. The hook then fails with the step in the message and the host's own trace beside it, instead of a timeout that names nothing. Nothing is loosened: the hook's thirty-second budget is unchanged and a stall still fails the suite. It fails sooner, and it says where. The terminals are killed before the service close waits on anything else, so a service close that stalls has already released them and the host can go once it has said where it stopped.
The service close awaits every owner it owns — the terminal manager, the websocket connections, the websocket server, the browser bridge, the Mini App proxies, the HTTP listener, the harness port — and every one of those awaits is unbounded. The host could therefore only say that "the service close" had not finished, which is one level above the question that matters: which of them is stuck. The service now announces each owner before it awaits it, and the host passes that announcement on. A stall names the owner it stalled at, and the hook's own budget sits below Playwright's hook timeout, so the message that carries the host's trace is the message that is said. The close signature gains an optional callback, so a caller that does not want the progress does not get it. The close order, the owners, and what a failure does are all unchanged.
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.
Change
The native-terminal acceptance on the Windows runner fails intermittently in its teardown. The hook waits for the native host to say it closed; on some runs the host stopped answering inside its own service close and had nothing to say about it.
This is the failure recorded in #30. It is not #22's five-second drag, and it is not what #17 fixed either: #17 made the host go once it had reported, and here the host never reaches the report.
Where it stops. Since #17 the host reports each step of its own shutdown, so a shutdown that does not finish says which step stopped answering. The failing run's last line is:
It never reaches
terminals exited. The runtime's own shutdown lines (Turn controller shutdown,All MCP connections closed, the sandbox lifecycle changes) follow, and then nothing.What the evidence does not say. The host's captured log also carries two crashes inside node-pty's console-list helper, with this repository's tsx loader in the stack:
That is the shape microsoft/node-pty#974 describes — the console-list agent inherits
process.execArgv, so the host's preload runs inside it — and it is ours because the preload is ours. It is not, however, what makes this stall last thirty seconds, and this change does not claim it is:kill()returns synchronously and the promise is a side branch — and the kills run in parallel, so it accounts for about five seconds, not thirty;The thirty seconds are still unexplained.
WebuiService.close()awaits every owner it owns — the terminal manager, the websocket connections, the websocket server, the browser bridge, the Mini App proxies, the HTTP listener, the harness port — and each of those awaits is unbounded. A shutdown that stalls is a silence, and this is that silence.What this change does about it. It makes the silence say which owner it is in, so the next occurrence names the step instead of only naming the service close:
packages/webui/src/server/service.ts—close()takes an optional callback and announces each owner before it awaits it. The close order, the owners, and what a failure does are unchanged; a caller that does not want the progress does not receive it.packages/webui/test/browser/support/native-browser-host.ts— the host gives the service close a deadline of its own. When that deadline passes it reports the owner it stopped at, and goes. The terminals are killed before the service close waits on anything else, so a service close that stalls has already released them.packages/webui/test/browser/native-runtime.spec.mjs— the hook's budget sits below Playwright's own hook timeout, so the message that carries the host's trace is the message that is said, and the teardown's second complaint explains itself when the body failed earlier.packages/webui/src/runtime/terminal.ts—disposeBySession()putsprocess.execArgvaside for the length of the kill and puts it back afterwards, so node-pty's helper stops loading this repository's preload. The kill is synchronous and the helper needs none of our flags. The development server starts under tsx as well (scripts/run-webui-server.mjs), so the same exposure is in the product, not only in the test.Nothing is loosened. The hook's thirty-second budget is unchanged, and a stall still fails the suite; it fails sooner, and it says where. No test is skipped, no retry is added, and no expected value changed.
Validation
packages/webui/test/unit/webui-service.test.tsandpackages/webui/test/unit/terminal-process-manager.test.ts: 118 tests pass.packages/webui/test/browser/workspace-panel-actions.spec.mjs("the workspace review stays responsive with 70 expanded diff files" reported "workspace panel drag produced 54 ms long tasks" -- a long-task budget in a client-side spec this change does not touch); it was rerun and passed on the second attempt. All three platforms are green.perf:fulllabel: the change is outside the paths the performance rules inCONTRIBUTING.mdlist, and it is not a performance optimization.service.close(), and nothing in the chain said which owner.Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable. — no source file was added or removed, sorelease/public-source.jsonis unchanged; no vitest file was added, so no suite declaration is needed.packages/webui/docs/runtime/boot.mdis unchanged by this change, so that document still describes what the code does.Maintainer handoff
Publication scope or license changes (if any): none.
Shared-source port: not needed. The change is inside
packages/webui, which does not exist upstream; perpackages/webui/docs/agents/issue-tracker.mdnothing in this repository is published to upstream. The node-pty side is already open upstream as microsoft/node-pty#952, #1007 and #974, so no new upstream issue is needed.