Filed unassigned by the dev seat working #8893 (console pin bump). ⛔ Not fixed there — different file, out of that card's scope. No domain:* / target:* set.
What happened, measured
scripts/gen-sdui-manifest.sh starts the console dev server in the background and arms a cleanup trap:
pnpm --filter @object-ui/console exec vite dev --port 5180 > /tmp/sdui-dump-dev.log 2>&1&
DUMP_DEV_PID=$!trap'kill "$DUMP_DEV_PID" 2>/dev/null || true' EXIT
On 2026-08-17T22:29:53Z the manifest dump failed (Playwright browser revision mismatch — the documented docs/releases-maintenance.md case) and the script exited 1, printing its own failure message. The trap ran. The background process was still alive 20 minutes later:
PID ELAPSED CMD
4605 20:12 node /opt/node22/bin/pnpm --filter @object-ui/console exec vite dev --port 5180
Two things make this more than an untidy process:
- It was no longer serving anything.
ss -ltn showed nothing on 5180 and the vite child was gone; only the pnpm wrapper node process survived. kill "$DUMP_DEV_PID" evidently reached something other than the process that outlives the run — the wrapper re-execs, so $! is not the pid that matters. - It held this container's shared heavy-verify flock. The run was wrapped, per agent discipline, in
flock -E 99 -w 540 /tmp/os-heavy-verify.lock -c '...'. Background children inherit the lock fd, so the orphan inherited it and never released it:
$ fuser -v /tmp/os-heavy-verify.lock
USER PID ACCESS COMMAND
root 4605 f.... node
$ ls -l /proc/4605/fd | grep -c os-heavy-verify
1
Every subsequent acquisition in the container times out at exit 99. Two later attempts (540s and 300s) both queued out against this zombie. Any parallel agent doing a build or test run in this container is blocked by it for as long as it lives, with no signal other than a lock timeout.
Why it matters beyond one bad run
The failing path is not exotic — it is the documented one. docs/releases-maintenance.md records that in an agent dispatch container the Playwright build revision routinely does not match after a pin bump, and the script itself prints that pointer on failure. So the failure branch that leaks the server is the branch a dispatch container is expected to take, and the leak is invisible: the script's last words are its own tidy failure message.
The lock interaction turns a per-run annoyance into a container-wide stall, and the agent that caused it is not necessarily able to clear it (this seat's kill 4605 was refused by its permission layer, so the orphan is still running as this is filed).
Suggested shape (not a decision)
- Kill the process group, not
$!: start the server with setsid and kill -- -$PGID on EXIT, or track the pid that actually binds the port. The current kill reaches a wrapper that is not the survivor. - Have the trap verify the process is gone (poll briefly, then SIGKILL) and say so when it cannot. A cleanup that cannot fail silently is the whole point here.
- Consider having the script open no inherited fds into background children (
... & with 0<&- ...), so an orphan can never hold a caller's flock even if the kill misses. This is the part that converts a leak into a container-wide outage, and it is fixable independently of the kill.
Reproduction is cheap and does not need a broken Playwright: run the script with a PLAYWRIGHT_BROWSERS_PATH pointing at an empty directory, under flock, then check fuser -v on the lock file.
Filed unassigned by the dev seat working #8893 (console pin bump). ⛔ Not fixed there — different file, out of that card's scope. No
domain:*/target:*set.What happened, measured
scripts/gen-sdui-manifest.shstarts the console dev server in the background and arms a cleanup trap:On 2026-08-17T22:29:53Z the manifest dump failed (Playwright browser revision mismatch — the documented
docs/releases-maintenance.mdcase) and the script exited 1, printing its own failure message. The trap ran. The background process was still alive 20 minutes later:Two things make this more than an untidy process:
ss -ltnshowed nothing on 5180 and the vite child was gone; only the pnpm wrapper node process survived.kill "$DUMP_DEV_PID"evidently reached something other than the process that outlives the run — the wrapper re-execs, so$!is not the pid that matters.flock -E 99 -w 540 /tmp/os-heavy-verify.lock -c '...'. Background children inherit the lock fd, so the orphan inherited it and never released it:Every subsequent acquisition in the container times out at exit 99. Two later attempts (540s and 300s) both queued out against this zombie. Any parallel agent doing a build or test run in this container is blocked by it for as long as it lives, with no signal other than a lock timeout.
Why it matters beyond one bad run
The failing path is not exotic — it is the documented one.
docs/releases-maintenance.mdrecords that in an agent dispatch container the Playwright build revision routinely does not match after a pin bump, and the script itself prints that pointer on failure. So the failure branch that leaks the server is the branch a dispatch container is expected to take, and the leak is invisible: the script's last words are its own tidy failure message.The lock interaction turns a per-run annoyance into a container-wide stall, and the agent that caused it is not necessarily able to clear it (this seat's
kill 4605was refused by its permission layer, so the orphan is still running as this is filed).Suggested shape (not a decision)
$!: start the server withsetsidandkill -- -$PGIDon EXIT, or track the pid that actually binds the port. The currentkillreaches a wrapper that is not the survivor.... &with0<&- ...), so an orphan can never hold a caller's flock even if the kill misses. This is the part that converts a leak into a container-wide outage, and it is fixable independently of the kill.Reproduction is cheap and does not need a broken Playwright: run the script with a
PLAYWRIGHT_BROWSERS_PATHpointing at an empty directory, underflock, then checkfuser -von the lock file.