Skip to content

Commit 9edb21d

Browse files
aseembits93claude
andcommitted
fix: clean up stale worktrees at startup after SIGKILL
Write a .codeflash.pid file when creating a worktree. On startup, scan the worktrees directory and remove any whose owning process is no longer alive — handles the case where a previous run was killed with SIGKILL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0e8befa commit 9edb21d

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

codeflash/code_utils/git_worktree_utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import configparser
4+
import os
45
import shutil
56
import stat
67
import subprocess
@@ -65,6 +66,10 @@ def create_detached_worktree(module_root: Path) -> Optional[Path]:
6566

6667
repository.git.worktree("add", "-d", str(worktree_dir))
6768

69+
# Write PID file so stale worktrees can be detected after SIGKILL
70+
pid_file = worktree_dir / ".codeflash.pid"
71+
pid_file.write_text(str(os.getpid()), encoding="utf-8")
72+
6873
# Get uncommitted diff from the original repo
6974
repository.git.add("-N", ".") # add the index for untracked files to be included in the diff
7075
exclude_binary_files = [":!*.pyc", ":!*.pyo", ":!*.pyd", ":!*.so", ":!*.dll", ":!*.whl", ":!*.egg", ":!*.egg-info", ":!*.pyz", ":!*.pkl", ":!*.pickle", ":!*.joblib", ":!*.npy", ":!*.npz", ":!*.h5", ":!*.hdf5", ":!*.pth", ":!*.pt", ":!*.pb", ":!*.onnx", ":!*.db", ":!*.sqlite", ":!*.sqlite3", ":!*.feather", ":!*.parquet", ":!*.jpg", ":!*.jpeg", ":!*.png", ":!*.gif", ":!*.bmp", ":!*.tiff", ":!*.webp", ":!*.wav", ":!*.mp3", ":!*.ogg", ":!*.flac", ":!*.mp4", ":!*.avi", ":!*.mov", ":!*.mkv", ":!*.pdf", ":!*.doc", ":!*.docx", ":!*.xls", ":!*.xlsx", ":!*.ppt", ":!*.pptx", ":!*.zip", ":!*.rar", ":!*.tar", ":!*.tar.gz", ":!*.tgz", ":!*.bz2", ":!*.xz"] # fmt: off
@@ -119,6 +124,36 @@ def remove_worktree(worktree_dir: Path) -> None:
119124
logger.exception(f"Failed to remove worktree: {worktree_dir}")
120125

121126

127+
def is_process_alive(pid: int) -> bool:
128+
try:
129+
os.kill(pid, 0)
130+
except ProcessLookupError:
131+
return False
132+
except PermissionError:
133+
return True # process exists but we can't signal it
134+
return True
135+
136+
137+
def cleanup_stale_worktrees() -> None:
138+
"""Remove worktrees left behind by killed processes (e.g. SIGKILL)."""
139+
if not worktree_dirs.exists():
140+
return
141+
for entry in worktree_dirs.iterdir():
142+
if not entry.is_dir():
143+
continue
144+
pid_file = entry / ".codeflash.pid"
145+
if pid_file.exists():
146+
try:
147+
pid = int(pid_file.read_text(encoding="utf-8").strip())
148+
except (ValueError, OSError):
149+
pid = None
150+
if pid is not None and is_process_alive(pid):
151+
continue # worktree is still in use
152+
# No PID file or owning process is dead — stale worktree
153+
logger.info(f"Removing stale worktree: {entry}")
154+
remove_worktree(entry)
155+
156+
122157
def create_diff_patch_from_worktree(
123158
worktree_dir: Path, files: list[Path], fto_name: Optional[str] = None
124159
) -> Optional[Path]:

codeflash/optimization/optimizer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from codeflash.code_utils.env_utils import get_pr_number, is_pr_draft
2323
from codeflash.code_utils.git_utils import check_running_in_git_repo, git_root_dir
2424
from codeflash.code_utils.git_worktree_utils import (
25+
cleanup_stale_worktrees,
2526
create_detached_worktree,
2627
create_diff_patch_from_worktree,
2728
create_worktree_snapshot_commit,
@@ -738,6 +739,8 @@ def run_with_args(args: Namespace) -> None:
738739
import atexit
739740
import signal
740741

742+
cleanup_stale_worktrees()
743+
741744
optimizer = None
742745
original_sigterm = signal.getsignal(signal.SIGTERM)
743746
original_sighup = signal.getsignal(signal.SIGHUP)

0 commit comments

Comments
 (0)