Commit Graph

3 Commits

Author SHA1 Message Date
kshitijk4poor eb4772ec2f fix(gateway): guard empty task_id reaps and prefer the finished worker's result
Two follow-ups from review of the salvaged fix:

- _reap_gateway_turn_processes now returns 0 for a blank task_id.
  ProcessSession.task_id defaults to empty for sessionless callers, so a
  blank turn id would match (and kill) every unrelated empty-task process
  instead of the turn's own.

- The asyncio poll loop checks executor completion BEFORE the watchdog's
  timeout flag. When both race in the same window, the completed run has
  already persisted its real reply to session history; surfacing the
  'agent inactive' diagnostic would contradict the stored transcript.
  This matches _abandon_timed_out_gateway_turn's own worker-done-wins
  tiebreak.
2026-08-02 14:23:55 +05:30
joaomarcos 1b886822de test(gateway): cover the run_generation guard and API-server disconnect reap
dbbb10d39 shipped without direct test coverage for its own new logic
— the same gap teknium's review flagged on the competing PR. Close it:

- _reap_gateway_turn_processes: skips when is_still_current() is
  False, proceeds when True, fails open (reaps) if the check itself
  raises rather than silently disabling the leak fix.
- _abandon_timed_out_gateway_turn: still marks the turn abandoned
  (interrupt fires) even when the reap itself is skipped.
- api_server._reap_disconnected_agent_processes: reaps the
  baseline-diff for an owned turn, no-ops when the agent never
  recorded ownership markers.
- APIServerAdapter._run_agent: markers are populated with the right
  task_id/baseline during the turn and cleared once it completes,
  closing the same race window fixed in gateway/run.py for this
  separate agent-lifecycle surface.
2026-08-02 14:23:55 +05:30
joaomarcos 80e4fb5995 fix(gateway): reap only the background processes an abandoned turn created
An agent turn can spawn a long-running background subprocess (e.g.
`next build`) and later be abandoned via inactivity timeout, /stop,
/new, or a client disconnect. Before this fix the gateway interrupted
the agent loop but never touched the subprocess: it kept running
inside the gateway's cgroup, unbounded, until memory pressure starved
the event loop and made every platform/cron look hung (#76115).

The process registry already knew how to kill a process tree — the
missing piece was per-turn ownership: nothing distinguished a process
that predates the turn (must survive), a process the turn started and
finished successfully (must survive), and a process an abandoned turn
left running (must be reaped).

- tools/process_registry.py: snapshot_running_ids() captures a turn's
  starting baseline; kill_started_since() reaps only IDs created after
  it, scoped to one task_id.
- gateway/turn_context.py: TurnContext carries process_task_id +
  process_baseline so the timeout/interrupt paths can reach them.
- gateway/run.py: baseline is snapshotted right before the turn's
  executor task starts; the inactivity-timeout path and the explicit
  /stop|/new|disconnect interrupt path both reap via the same helper.
  A daemon-thread watchdog backs up the asyncio-based timeout poll,
  since a starved event loop is exactly the failure mode this bug
  causes. The turn's own worker clears its ownership markers the
  instant it finishes, closing a race where a /stop landing right
  after normal completion could reap a background process the turn
  deliberately left running.

Related but insufficient on their own: #37454 (cgroup ExecStopPost
reaper only fires on service restart) and #68915 (orphaned-pipe
grandchild detection, a registry bug not a turn-lifecycle gap).
Neither ties process cleanup to turn abandonment.
2026-08-02 14:23:55 +05:30