Skip to content

Agent browser daemons are never reclaimed: socket dies with the per-process scratch dir (~10.7 GB stranded, oldest 4 days) #5986

Closed

Description

@peterhieuvu

What

A playwright-cli browser daemon started by an agent is never reclaimed. It outlives
its agent process, reparents to pid 1, and keeps a Chromium tree alive indefinitely. On one
long-running gateway host, measured directly:

  • 19 cliDaemon processes, all reparented to pid 1, oldest 4 days 8 hours old
  • the CLI's registry knows only 5 of them (default, lnkchk, oesweep, tg, tgv)
  • 14 of 19 have a TMPDIR that no longer exists
  • total resident set across the daemon trees: ~14.8 GB, median 0.81 GB per tree —
    of which roughly 10.7 GB belongs to daemons no live session claims

Root cause

The CLI writes its control socket under TMPDIR. KiroCrew's agent_scratch points
TMPDIR at a per-process scratch directory, so a session's .session entry records
something like:

<scratch>/runtime-<id>/pw-<hash>/cli/<workspacehash>-<name>.sock

When that agent process dies, sweep_dead_scratch reclaims the directory. The daemon keeps
running, but its socket is gone, so:

  1. nothing can talk to it again — it is unreachable, not merely untracked;
  2. a later process gets a different TMPDIR, so even reopening the same session name
    cannot find or stop the old daemon. It just overwrites the registry entry and spawns a
    new one.

Two independent facts confirm the mechanism: the 14 daemons whose TMPDIR is gone, and
three different scratch dirs on the same host each containing their own pw-<hash>/.

Why the obvious fix does not work

A reaper that releases via the CLI (playwright-cli -s=<name> close) cannot reclaim
these
. close resolves the name through the registry to a socketPath and talks to it.
A stray has no .session entry at all — 5 entries for 19 daemons — so close answers
"not open" and the daemon survives. Such a reaper only ever reclaims a browser whose owner
is dead but whose registry entry and socket still resolve, which on this evidence is
close to never. It would walk past every byte of the 10.7 GB above.

This was implemented and then removed from #5952's PR for exactly this reason.

Nothing else covers it

  • The orphan sweep in session_pid.py gates on MCP launcher shapes (@playwright/mcp,
    mcp start-server); a cliDaemon.js cmdline matches neither.
  • kill_process_tree on session teardown cannot reach it — the daemon is reparented to
    pid 1 and is not in the ACP process tree.
  • The agent cannot clean up after itself unattended: close / close-all / kill-all are
    deliberately fail-closed in the browser-CLI approval gate.
  • There is no TTL or idle timeout. A 4-day-old daemon with no live session is not waiting
    on anything.

Today the only routine reclamation is an operator running playwright-cli close-all by
hand, which also takes down live sessions.

Directions worth considering

  1. Reclaim by process tree, not by CLI verb. A cliDaemon reparented to pid 1 whose
    session name (available as argv[1]) has no live registry entry is structurally
    identifiable without any state we have to store — and therefore without anything an
    agent could forge. Killing the tree does not need the lost socket.
  2. Stop putting the socket somewhere KiroCrew reclaims. If the CLI's socket dir lived
    outside per-process scratch, a daemon would stay reachable across its owner's death and
    an ordinary close would work. This may be the more correct fix; it needs a decision
    about whether TMPDIR containment should carve out this case.
  3. Reap before reclaiming the scratch dir. sweep_dead_scratch knows the owner is
    dead and still has the socket at that moment — the one point in time where a graceful
    close would succeed.

Direction 1 is available today; 2 and 3 need a design call about scratch containment.

Notes

  • Human-triggered rather than timer-driven reclamation avoids a separate problem found in
    review: a gateway that releases browsers unattended, based on on-disk state, converts an
    agent-writable file into an unattended close — laundering a verb the approval gate
    fails closed on. A verb an operator runs does not have that property.
  • Per-session browser naming (Agent sessions share one playwright-cli browser, so concurrent sessions steal and close each other's pages #5952) neither causes nor cures this: the strand happens per
    session process regardless of naming. It changes only how many browsers are live at once.

Activity

  1. added
    area: agentsACP runtime, sub-agents, session lifecycle
    area: coreCore runtime utilities, config/paths, process singletons
    bugSomething is not working
    on Aug 26, 2026
  2. github-actions commented on Aug 26, 2026

    @github-actions
    Contributor

    Automated triage summary — generated from the issue text alone, before any maintainer has read it. Treat every line as a starting point, not a verdict.

    Playwright-cli browser daemons spawned by agent sessions outlive their agent, reparent to pid 1, and are never reclaimed because their control socket lived in the per-process scratch dir that gets swept, leaving them unreachable and untracked by the CLI registry. On one long-running gateway host the reporter measured 19 such daemons (oldest 4d8h) holding ~10.7 GB with no live session claiming them.

    Possibly already reported

    If one of these is the same problem, say so and we will close this as a duplicate.

    Posted by issue-summary.yml. The model had no access to the repository — it read only this issue and the titles of recent ones.

  3. added
    needs-triageAwaiting automated triage
    needs-investigationTriage: requires deep analysis before a fix
    and removed
    needs-triageAwaiting automated triage
    on Aug 26, 2026
  4. CrysisDeu commented on Aug 26, 2026

    @CrysisDeu
    Contributor

    🤖 Kiro Crew Auto-Pipeline [operator: CrysisDeu#0c98c3a2]: Routing to needs-investigation. The leak mechanism is fully diagnosed (control socket lives under per-process scratch TMPDIR; sweep_dead_scratch reclaims the dir, stranding an unreachable pid-1 daemon that no CLI verb can release), but the fix requires a design decision between the two directions the report itself lays out: (1) a reap-by-process-tree sweep keyed on cliDaemon argv[1] vs live registry entries — mechanically derivable but it is a process-killing reaper with forgeability/safety review needs, and an equivalent was deliberately removed from PR #5952; or (2) relocating the CLI socket dir outside per-process scratch, which is likely more correct but changes TMPDIR-containment semantics and needs an owner's call. A human should decide the direction (or both: relocation for reachability + a conservative tree-reaper for existing strays); after that decision the implementation is dispatchable.

  5. Tiger-0512 commented on Aug 26, 2026

    @Tiger-0512
    Contributor

    Implemented direction 1 (reclaim by process identity, no CLI verb, no registry trust) in #6083.

    Verified the mechanism on a live host before writing code: a pid-1-reparented cliDaemon.js here carries the exec-time KIROCREW_SPAWNED=1 marker and a TMPDIR pointing at a since-swept managed scratch dir — evidence an agent cannot forge for another process, and enough to reclaim without touching the CLI or its registry. The kill pins the process start token across validation and signals only a self-led process group, so a recycled PID or an attached operator browser is never a target; Windows fails closed.

    Directions 2 and 3 (socket relocation out of per-process scratch / reap at scratch-sweep time) stay open here as the design-call half — this closes the standing leak without changing TMPDIR containment semantics.

  6. added 4 commits that reference this issue on Aug 27, 2026
    3fa1d31
    029d2c7
    8c7de92
    79563d9
  7. Tiger-0512 commented on Aug 27, 2026

    @Tiger-0512
    Contributor

    Update on #6083 after the review rounds: the automatic process-table reaper from my earlier comment has been fully withdrawn.

    The reviews correctly established that cross-process SIGKILL cannot be authorized safely from scratch metadata (agent-writable), a process-local live set (misses concurrent same-home managers), or a token-only numeric PID/group check on macOS (no pidfd-equivalent identity pin). The PR now contains no automatic close or process signal.

    The replacement scope is the smaller safe part of direction 2: for Kiro-Crew-generated per-process browser sessions only, keep PWTEST_SOCKETS_DIR and PWTEST_DAEMON_SESSION_DIR outside agent scratch. The daemon remains reachable through ordinary playwright-cli list / -s=<name> close after scratch is reclaimed. Operator session locations are untouched. Crash-orphan reclamation remains open in this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: agentsACP runtime, sub-agents, session lifecyclearea: coreCore runtime utilities, config/paths, process singletonsbugSomething is not workingneeds-investigationTriage: requires deep analysis before a fix

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions