Skip to content

fix(process_registry): finished processes no longer hold FDs for the TTL window (salvage #75162) - #98930

Merged
teknium1 merged 4 commits into
mainfrom
salvage/75162-process-registry-handle-release
Sep 14, 2026
Merged

teknium1 merged 4 commits into
mainfrom
salvage/75162-process-registry-handle-release

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Summary

Finished background processes now release their OS handles (Popen pipes + PTY masters) immediately instead of holding them for the 30-minute finished-TTL window — heavy background churn can no longer exhaust the gateway's file-descriptor budget.

Salvage of #75162 by @RGerrish (commit cherry-picked, authorship preserved) onto current main, plus a follow-up widening commit. Zed shipped the same fix class this week (zed-industries/zed#62978 — completed agent terminals retained live PTY resources), which is what surfaced this stalled PR in the weekly scout.

Changes

  • tools/process_registry.py: idempotent _release_finished_handles() called from _move_to_finished() (contributor commit) and from both _prune_if_needed() drop paths — TTL expiry + oldest-finished eviction — which the original PR missed (follow-up commit).
  • tests/tools/test_process_registry.py: contributor's TestFinishedHandleRelease + 2 new tests (prune releases handles; still-running session's pipe stays open).

Validation

Result
tests/tools/test_process_registry.py all passed (memory-capped run)
Live E2E spawned real background process from the worktree: Popen stdout closed after finish, output preserved; running session's handles stay open
Attribution audit scripts/audit_pr_attribution.py --fix — all emails mapped

Live repro: contributor's failing-first test red on main (proc.stdout.closed stays False after finish); green on branch; live E2E above confirms on real processes.

Safety notes: send-input paths already guard on session.exited (graceful already_exited), reader threads drain before _move_to_finished runs so handles never close under a live reader, checkpoint-recovered detached sessions carry no local handles.

Infographic

Release finished process handles

@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 3080dad — test(process_registry): trim handle-release tests to invaria

⚠️ Warnings

OSV vulnerability scan · View job

76 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 5m12s vs 5m33s (-6.3%). 8 job(s) slower, 6 faster,

  • Python tests / Run tests: +40.0s
  • OS-specific tests / Windows-only tests: -23.0s
  • Python tests / e2e: -5.0s
  • OSV scan / Emit review status: +5.0s
  • Python lints / ruff enforcement (blocking): +4.0s

@alt-glitch alt-glitch added type/bug Something isn't working comp/tools Tool registry, model_tools, toolsets tool/terminal Terminal execution and process management area/sessions Session lifecycle, resume, persistence, history P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 30, 2026
@Sora-bluesky

Copy link
Copy Markdown
Contributor

Heads-up: #68948 (open, CI green) also touches tools/process_registry.py and tests/tools/test_process_registry.py, on the spawn side (Windows PeekNamedPipe reader, plus dropping the compound-background rewrite from spawn) rather than the finished-TTL/FD-release path changed here. The regions look disjoint, and I'm happy to rebase #68948 if this lands first.

RGerrish and others added 4 commits September 13, 2026 19:44
Finished sessions retained their subprocess.Popen pipe objects (and PTY
masters) until the finished-process TTL (FINISHED_TTL_SECONDS, default 30
minutes) elapsed. Under heavy background churn — deployments, archivers,
watchers — finished-but-unpruned sessions accumulated one open pipe FD
each, exhausting the gateway process's file descriptor budget and
surfacing as a 'file descriptor limit' error on new background spawns.

The registry never rejects spawns (it prunes oldest-finished at
MAX_PROCESSES), so the real defect was the retained-handle leak, not a
registry-cap rejection. The fix closes each finished session's Popen
stdout/stderr/stdin streams and PTY master in _move_to_finished(), right
after the reader loop drains EOF. poll()/wait()/read_log() serve output
from the buffered output_buffer — never from the pipe — so the release is
lossless.

Tests: 4 new cases in TestFinishedHandleRelease — Popen pipes closed,
PTY closed, no-handle sessions safe, and poll() still serves buffered
output after the release. All 4 fail on main (reproduction) and pass
with the fix.
…llow-up)

Widen #75162: _prune_if_needed() drops finished sessions (TTL expiry and
oldest-finished eviction at MAX_PROCESSES) — release their Popen/PTY
handles there too, covering sessions inserted into _finished without
passing through _move_to_finished(). The release helper is idempotent,
so double-close on the normal path is a no-op. Adds two tests: prune
releases handles of dropped sessions, and a still-running session's
pipe stays open.
…ath race

_release_finished_handles read the dataclass fields through getattr
fallbacks and swallowed every exception; use the real attributes, suppress
only OSError/ValueError on the pipe close (an stdin flush can hit EPIPE),
and rely on ptyprocess/pywinpty close() idempotence for the master fd.
The call-site comment claimed the reader had always drained the pipe;
on kill_process/_reconcile_local_exit the reader may still be reading,
so state what actually happens (its next read raises, the loop exits).
…prune fixture

Drop the no-op and still-running change-detectors (4 invariant tests remain:
pipe closed on finish, PTY closed on finish, poll still serves buffered
output, prune releases handles). The accretion-caps fake session now
carries process/_pty like the real dataclass, since prune reads them.
@teknium1
teknium1 force-pushed the salvage/75162-process-registry-handle-release branch from 8b9c150 to 3080dad Compare September 14, 2026 02:55
@teknium1
teknium1 merged commit dcf1763 into main Sep 14, 2026
37 checks passed
@teknium1
teknium1 deleted the salvage/75162-process-registry-handle-release branch September 14, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants