Script to kill TraceLens Agent eval jobs - #960
Open
kyle-hoffmeyer wants to merge 3 commits into
Open
Conversation
kyle-hoffmeyer
force-pushed
the
feat/kill_eval_jobs
branch
from
August 24, 2026 20:24
4cc3d99 to
fbd94c1
Compare
kyle-hoffmeyer
marked this pull request as ready for review
August 24, 2026 20:24
kyle-hoffmeyer
requested review from
Ahmedhasssan-aig and
tsrikris
as code owners
August 24, 2026 20:24
kyle-hoffmeyer
force-pushed
the
feat/kill_eval_jobs
branch
from
August 24, 2026 20:43
fbd94c1 to
a06e9c6
Compare
gabeweisz
approved these changes
Aug 25, 2026
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.
Summary
This PR adds a script
kill_eval_jobs.shthat kills all eval jobs spawned byrun_repeatability_parallel.shandgenerate_ref.sh.The analysis eval pipeline (
run_repeatability_parallel.sh, andgenerate_ref.sh) fans out into a large tree of processes per run: the orchestrator scripts, their worker subshells, the Cursoragent/pianalysis and eval calls, thenodeworker each of those spawns, and the scripted-evalpythonprocesses. There was no clean way to stop a run:The processes from a single run don't share one kill handle. Here is a live snapshot of the tree from one
generate_ref.shrun, showing the different kernel IDs:bash …/generate_ref.sh(the script; reparented toinit)bash …/generate_ref.sh(worker subshell)bash …/generate_ref.sh(worker subshell)timeout 1800 agent …(starts a new group)agent …(Cursor CLI)node …/cursor-agent/…(the LLM worker)bash -c …(agent tool-call;setsid→ own group and session)initand keep going, and the pipeline's retry loop respawns new agents.kill -- -PGID) can't cover the run in one call, because a single run is spread across multiple process groups: thetimeoutwrapper around each agent call starts a new group, so thetimeout/agent/nodeworker chain sits outside the script's group.setsidand leave the session entirely, so there's no single session that contains the whole tree.This PR tags each process with an inherited environment marker and adds a
kill_eval_jobs.shscript that that reliably kills an entire eval run by killing jobs with this environment marker.Motivation
The core difficulty is identifying which processes belong to an eval run. Matching by command name is unreliable (the real workers are generic
nodeandpythonprocesses), and matching by parentage or process group fails because of reparenting and the agent CLI's own process group. What every process in a run does have in common is that it descends from one of the two entrypoint scripts. An inherited environment marker survives reparenting, process-group changes, and renames.Solution
Both entrypoints export
TRACELENS_EVAL_JOB=1, which is inherited by every descendant.kill_eval_jobs.shthen finds the whole run by scanning/proc/<pid>/environfor that marker and signalling those PIDs.Implementation
agent_evals/Analysis/eval_scripts/kill_eval_jobs.sh(new)A standalone cleanup tool. It scans
/proc/<pid>/environforTRACELENS_EVAL_JOB=1(guarding on file ownership so it only ever touches the current user's processes) and signals the matches. Because the marker is unique to the pipeline, unrelatedagent,claude, orpythonsessions are never touched.Two subtleties drove the design:
sleep, or a fresh agent — in the window between the scan and the kill; that child inherits the marker but wasn't in the snapshot. So the tool runs a bounded rescan loop: scan, signal, repeat until a scan comes back empty (orMAX_PASSESis reached). Because the orchestrator scripts are themselves tagged (see below), they die on the first pass and stop respawning, and later passes just mop up orphaned stragglers.SIGTERMto let processes shut down cleanly; every pass after that sendsSIGKILL. If anything is still alive after the last pass (e.g. stuck in uninterruptible I/O), it prints a warning with an inspection command and exits non-zero.It also supports
--list(show what would be killed, kill nothing) and-9(SIGKILLfrom the first pass).agent_evals/Analysis/eval_scripts/generate_ref.shandrun_repeatability_parallel.shEach entrypoint gains a small guard at the top that sets the marker and re-execs itself once:
The re-exec is needed since a process's
/proc/<pid>/environis fixed when it starts. Exporting an environment variable in the script tags its children but not the script itself. Without the re-exec, the scan would find the agents but not the orchestrator scripts, and a kill would leave those scripts alive to respawn the agents.Tests
Manual verification for both entrypoints:
generate_ref.shand, separately,run_repeatability_parallel.sh, let each spin up its agent tree, and confirmed via/proc/<pid>/environthat the orchestrator script process itself, its worker subshells, thetimeout/agentlaunchers, thenodeworker, and tool-call subprocesses were all tagged.kill_eval_jobs.sh --listand confirmed it enumerated the full tree (script + subshells + agent tree).kill_eval_jobs.shand independently verified afterward (a fresh/procscan pluspgrep) that zero marked processes remained and nothing respawned.agent/pythonprocess is left untouched, and a marked but generically-named process (e.g. a plainsleep) is still found and killed — demonstrating the match is by environment marker, not command name.environbehavior directly: a runtime-onlyexportdoes not appear in the exporting process's own/proc/environ, while the re-exec form does — validating why the re-exec is necessary.