overcode 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
overcode/pid_utils.py CHANGED
@@ -150,23 +150,30 @@ def acquire_daemon_lock(pid_file: Path) -> Tuple[bool, Optional[int]]:
150
150
  return False, None
151
151
 
152
152
 
153
- def count_daemon_processes(pattern: str = "monitor_daemon") -> int:
153
+ def count_daemon_processes(pattern: str = "monitor_daemon", session: str = None) -> int:
154
154
  """Count running daemon processes matching the pattern.
155
155
 
156
156
  Uses pgrep to find processes matching the pattern.
157
157
 
158
158
  Args:
159
159
  pattern: Pattern to search for in process names/args
160
+ session: If provided, only count daemons for this specific session
160
161
 
161
162
  Returns:
162
- Number of matching processes (excluding this check process)
163
+ Number of matching processes
163
164
  """
164
165
  import subprocess
165
166
 
167
+ # Build pattern - if session provided, make it session-specific
168
+ if session:
169
+ search_pattern = f"{pattern} --session {session}"
170
+ else:
171
+ search_pattern = pattern
172
+
166
173
  try:
167
174
  # Use pgrep to find matching processes
168
175
  result = subprocess.run(
169
- ["pgrep", "-f", pattern],
176
+ ["pgrep", "-f", search_pattern],
170
177
  capture_output=True,
171
178
  text=True,
172
179
  timeout=5.0,
overcode/protocols.py CHANGED
@@ -78,6 +78,18 @@ class TmuxInterface(Protocol):
78
78
  """Attach to a tmux session (replaces current process)."""
79
79
  ...
80
80
 
81
+ def select_window(self, session: str, window: int) -> bool:
82
+ """Select a window in a tmux session.
83
+
84
+ Args:
85
+ session: tmux session name
86
+ window: window number to select
87
+
88
+ Returns:
89
+ True if successful, False otherwise
90
+ """
91
+ ...
92
+
81
93
 
82
94
  @runtime_checkable
83
95
  class FileSystemInterface(Protocol):
@@ -88,6 +88,9 @@ class Session:
88
88
  # Statistics
89
89
  stats: SessionStats = field(default_factory=SessionStats)
90
90
 
91
+ # Sleep mode - agent is paused and excluded from stats
92
+ is_asleep: bool = False
93
+
91
94
  def to_dict(self) -> dict:
92
95
  data = asdict(self)
93
96
  # Convert stats to dict
overcode/settings.py CHANGED
@@ -8,6 +8,8 @@ Configuration hierarchy:
8
8
  1. Environment variables (highest priority)
9
9
  2. Config file (~/.overcode/config.yaml)
10
10
  3. Default values (lowest priority)
11
+
12
+ TODO: Make INTERVAL_FAST/SLOW/IDLE configurable via config.yaml
11
13
  """
12
14
 
13
15
  import os
@@ -19,7 +21,7 @@ import os
19
21
  DAEMON_VERSION = 2 # Increment when daemon behavior changes
20
22
  from dataclasses import dataclass, field
21
23
  from pathlib import Path
22
- from typing import Optional
24
+ from typing import Optional, Set
23
25
 
24
26
  import yaml
25
27
 
@@ -328,6 +330,16 @@ def get_supervisor_log_path(session: str) -> Path:
328
330
  return get_session_dir(session) / "supervisor.log"
329
331
 
330
332
 
333
+ def get_web_server_pid_path(session: str) -> Path:
334
+ """Get web server PID file path for a specific session."""
335
+ return get_session_dir(session) / "web_server.pid"
336
+
337
+
338
+ def get_web_server_port_path(session: str) -> Path:
339
+ """Get web server port file path for a specific session."""
340
+ return get_session_dir(session) / "web_server.port"
341
+
342
+
331
343
  def ensure_session_dir(session: str) -> Path:
332
344
  """Ensure session directory exists and return it."""
333
345
  session_dir = get_session_dir(session)
@@ -367,6 +379,9 @@ class TUIPreferences:
367
379
  timeline_visible: bool = True
368
380
  daemon_panel_visible: bool = False
369
381
  view_mode: str = "tree" # tree, list_preview
382
+ tmux_sync: bool = False # sync navigation to external tmux pane
383
+ # Session IDs of stalled agents that have been visited by the user
384
+ visited_stalled_agents: Set[str] = field(default_factory=set)
370
385
 
371
386
  @classmethod
372
387
  def load(cls, session: str) -> "TUIPreferences":
@@ -389,6 +404,8 @@ class TUIPreferences:
389
404
  timeline_visible=data.get("timeline_visible", True),
390
405
  daemon_panel_visible=data.get("daemon_panel_visible", False),
391
406
  view_mode=data.get("view_mode", "tree"),
407
+ tmux_sync=data.get("tmux_sync", False),
408
+ visited_stalled_agents=set(data.get("visited_stalled_agents", [])),
392
409
  )
393
410
  except (json.JSONDecodeError, IOError):
394
411
  return cls()
@@ -407,6 +424,8 @@ class TUIPreferences:
407
424
  "timeline_visible": self.timeline_visible,
408
425
  "daemon_panel_visible": self.daemon_panel_visible,
409
426
  "view_mode": self.view_mode,
427
+ "tmux_sync": self.tmux_sync,
428
+ "visited_stalled_agents": list(self.visited_stalled_agents),
410
429
  }, f, indent=2)
411
430
  except (IOError, OSError):
412
431
  pass # Best effort
@@ -24,8 +24,17 @@ class InstructionPreset:
24
24
 
25
25
  # Default presets - used to generate initial presets.json
26
26
  DEFAULT_PRESETS: Dict[str, InstructionPreset] = {
27
- "DEFAULT": InstructionPreset(
28
- name="DEFAULT",
27
+ "DO_NOTHING": InstructionPreset(
28
+ name="DO_NOTHING",
29
+ description="Supervisor ignores this agent (default)",
30
+ instructions=(
31
+ "Do not interact with this agent at all. Do not approve or reject any prompts. "
32
+ "Do not send any input. Leave the agent completely alone and let it wait for "
33
+ "the human user. This agent is not under supervisor control."
34
+ ),
35
+ ),
36
+ "STANDARD": InstructionPreset(
37
+ name="STANDARD",
29
38
  description="General-purpose safe automation",
30
39
  instructions=(
31
40
  "Approve safe operations within the working directory: file reads/writes/edits, "
@@ -208,11 +217,11 @@ def get_preset_names() -> List[str]:
208
217
  List of preset names
209
218
  """
210
219
  presets = load_presets()
211
- # Return in a consistent order (DEFAULT first, then alphabetical)
220
+ # Return in a consistent order (DO_NOTHING first, then alphabetical)
212
221
  names = list(presets.keys())
213
- if "DEFAULT" in names:
214
- names.remove("DEFAULT")
215
- names = ["DEFAULT"] + sorted(names)
222
+ if "DO_NOTHING" in names:
223
+ names.remove("DO_NOTHING")
224
+ names = ["DO_NOTHING"] + sorted(names)
216
225
  else:
217
226
  names = sorted(names)
218
227
  return names
@@ -17,6 +17,7 @@ STATUS_NO_INSTRUCTIONS = "no_instructions"
17
17
  STATUS_WAITING_SUPERVISOR = "waiting_supervisor"
18
18
  STATUS_WAITING_USER = "waiting_user"
19
19
  STATUS_TERMINATED = "terminated" # Claude Code exited, shell prompt showing
20
+ STATUS_ASLEEP = "asleep" # Human marked agent as paused/snoozed (excluded from stats)
20
21
 
21
22
  # All valid agent status values
22
23
  ALL_STATUSES = [
@@ -25,6 +26,7 @@ ALL_STATUSES = [
25
26
  STATUS_WAITING_SUPERVISOR,
26
27
  STATUS_WAITING_USER,
27
28
  STATUS_TERMINATED,
29
+ STATUS_ASLEEP,
28
30
  ]
29
31
 
30
32
 
@@ -60,6 +62,7 @@ STATUS_EMOJIS = {
60
62
  STATUS_WAITING_SUPERVISOR: "🟠",
61
63
  STATUS_WAITING_USER: "🔴",
62
64
  STATUS_TERMINATED: "⚫", # Black circle - Claude exited
65
+ STATUS_ASLEEP: "💤", # Sleeping/snoozed - human marked as paused
63
66
  }
64
67
 
65
68
 
@@ -78,6 +81,7 @@ STATUS_COLORS = {
78
81
  STATUS_WAITING_SUPERVISOR: "orange1",
79
82
  STATUS_WAITING_USER: "red",
80
83
  STATUS_TERMINATED: "dim", # Grey for terminated
84
+ STATUS_ASLEEP: "dim", # Grey for sleeping
81
85
  }
82
86
 
83
87
 
@@ -96,6 +100,7 @@ STATUS_SYMBOLS = {
96
100
  STATUS_WAITING_SUPERVISOR: ("🟠", "orange1"),
97
101
  STATUS_WAITING_USER: ("🔴", "red"),
98
102
  STATUS_TERMINATED: ("⚫", "dim"),
103
+ STATUS_ASLEEP: ("💤", "dim"), # Sleeping/snoozed
99
104
  }
100
105
 
101
106
 
@@ -114,6 +119,7 @@ AGENT_TIMELINE_CHARS = {
114
119
  STATUS_WAITING_SUPERVISOR: "▒",
115
120
  STATUS_WAITING_USER: "░",
116
121
  STATUS_TERMINATED: "×", # Small X - terminated
122
+ STATUS_ASLEEP: "z", # Lowercase z for sleeping
117
123
  }
118
124
 
119
125
 
@@ -188,3 +194,8 @@ def is_waiting_status(status: str) -> bool:
188
194
  def is_user_blocked(status: str) -> bool:
189
195
  """Check if status indicates user intervention is required."""
190
196
  return status == STATUS_WAITING_USER
197
+
198
+
199
+ def is_asleep(status: str) -> bool:
200
+ """Check if status indicates agent is asleep (paused by human)."""
201
+ return status == STATUS_ASLEEP
@@ -116,6 +116,13 @@ class StatusDetector:
116
116
 
117
117
  last_line = last_lines[-1]
118
118
 
119
+ # Check for spawn failure FIRST (command not found, etc.)
120
+ # This should be detected before shell prompt check because the error
121
+ # message appears before the shell prompt returns
122
+ spawn_error = self._detect_spawn_failure(lines)
123
+ if spawn_error:
124
+ return self.STATUS_WAITING_USER, spawn_error, content
125
+
119
126
  # Check for shell prompt (Claude Code has terminated)
120
127
  # Shell prompts typically end with $ or % and have username@hostname pattern
121
128
  # Also check for absence of Claude Code UI elements
@@ -297,6 +304,37 @@ class StatusDetector:
297
304
  ]
298
305
  return '\n'.join(filtered)
299
306
 
307
+ def _detect_spawn_failure(self, lines: list) -> str | None:
308
+ """Detect if the claude command failed to spawn.
309
+
310
+ Checks for common error messages like "command not found" that indicate
311
+ the claude CLI is not installed or not in PATH.
312
+
313
+ Args:
314
+ lines: All lines from the pane content
315
+
316
+ Returns:
317
+ Error message string if spawn failure detected, None otherwise
318
+ """
319
+ # Check recent lines for spawn failure patterns
320
+ # We check the last 20 lines to catch the error message
321
+ recent_lines = lines[-20:] if len(lines) > 20 else lines
322
+ recent_text = ' '.join(recent_lines).lower()
323
+
324
+ if matches_any(recent_text, self.patterns.spawn_failure_patterns):
325
+ # Find the specific error line for a better message
326
+ for line in reversed(recent_lines):
327
+ line_lower = line.lower()
328
+ if any(p.lower() in line_lower for p in self.patterns.spawn_failure_patterns):
329
+ # Extract just the error part, clean it up
330
+ error_msg = line.strip()
331
+ if len(error_msg) > 80:
332
+ error_msg = error_msg[:77] + "..."
333
+ return f"Spawn failed: {error_msg}"
334
+ return "Spawn failed: claude command not found - is Claude CLI installed?"
335
+
336
+ return None
337
+
300
338
  def _is_shell_prompt(self, lines: list) -> bool:
301
339
  """Detect if we're at a shell prompt (Claude Code has exited).
302
340
 
@@ -115,6 +115,18 @@ class StatusPatterns:
115
115
  # Format: " /command-name Description text"
116
116
  command_menu_pattern: str = r"^\s*/[\w-]+\s{2,}\S"
117
117
 
118
+ # Spawn failure patterns - when the claude command fails to start
119
+ # These indicate the command was not found or failed to execute
120
+ # Checked against pane content to detect failed spawns
121
+ spawn_failure_patterns: List[str] = field(default_factory=lambda: [
122
+ "command not found",
123
+ "not found:", # zsh style: "zsh: command not found: claude"
124
+ "no such file or directory",
125
+ "permission denied",
126
+ "cannot execute",
127
+ "is not recognized", # Windows-style (for future compatibility)
128
+ ])
129
+
118
130
 
119
131
  # Default patterns instance
120
132
  DEFAULT_PATTERNS = StatusPatterns()
@@ -15,10 +15,15 @@ Prerequisites:
15
15
 
16
16
  Architecture:
17
17
  Monitor Daemon (metrics) → monitor_daemon_state.json → Supervisor Daemon (claude)
18
+
19
+ TODO: Add unit tests (currently 0% coverage)
20
+ TODO: Extract _send_prompt_to_window to a shared tmux utilities module
21
+ (duplicated in launcher.py)
18
22
  """
19
23
 
20
24
  import json
21
25
  import os
26
+ import signal
22
27
  import subprocess
23
28
  import sys
24
29
  import tempfile
@@ -28,20 +33,16 @@ from datetime import datetime
28
33
  from pathlib import Path
29
34
  from typing import Dict, List, Optional
30
35
 
31
- from rich.console import Console
32
- from rich.text import Text
33
- from rich.theme import Theme
34
-
36
+ from .daemon_logging import SupervisorDaemonLogger
37
+ from .daemon_utils import create_daemon_helpers
35
38
  from .monitor_daemon_state import (
36
39
  MonitorDaemonState,
37
40
  SessionDaemonState,
38
41
  get_monitor_daemon_state,
39
42
  )
40
43
  from .pid_utils import (
41
- get_process_pid,
42
- is_process_running,
44
+ acquire_daemon_lock,
43
45
  remove_pid_file,
44
- write_pid_file,
45
46
  )
46
47
  from .session_manager import SessionManager
47
48
  from .settings import (
@@ -131,105 +132,6 @@ class SupervisorStats:
131
132
  return cls()
132
133
 
133
134
 
134
- # Rich theme for supervisor logs
135
- SUPERVISOR_THEME = Theme({
136
- "info": "cyan",
137
- "warn": "yellow",
138
- "error": "bold red",
139
- "success": "bold green",
140
- "daemon_claude": "magenta",
141
- "dim": "dim white",
142
- "highlight": "bold white",
143
- })
144
-
145
-
146
- class SupervisorLogger:
147
- """Rich-based logger for supervisor daemon."""
148
-
149
- def __init__(self, log_file: Path = None):
150
- self.log_file = log_file or PATHS.supervisor_log
151
- self.log_file.parent.mkdir(parents=True, exist_ok=True)
152
- self.console = Console(theme=SUPERVISOR_THEME, force_terminal=True)
153
- self._seen_daemon_claude_lines: set = set()
154
-
155
- def _write_to_file(self, message: str, level: str):
156
- """Write plain text to log file."""
157
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
158
- line = f"[{timestamp}] [{level}] {message}"
159
- with open(self.log_file, 'a') as f:
160
- f.write(line + '\n')
161
-
162
- def info(self, message: str):
163
- """Log info message."""
164
- self._write_to_file(message, "INFO")
165
- self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] [info]INFO[/info] {message}")
166
-
167
- def warn(self, message: str):
168
- """Log warning message."""
169
- self._write_to_file(message, "WARN")
170
- self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] [warn]WARN[/warn] {message}")
171
-
172
- def error(self, message: str):
173
- """Log error message."""
174
- self._write_to_file(message, "ERROR")
175
- self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] [error]ERROR[/error] {message}")
176
-
177
- def success(self, message: str):
178
- """Log success message."""
179
- self._write_to_file(message, "INFO")
180
- self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] [success]OK[/success] {message}")
181
-
182
- def daemon_claude_output(self, lines: List[str]):
183
- """Log daemon claude output, showing only new lines."""
184
- new_lines = []
185
-
186
- for line in lines:
187
- stripped = line.strip()
188
- if not stripped:
189
- continue
190
- if stripped not in self._seen_daemon_claude_lines:
191
- new_lines.append(stripped)
192
- self._seen_daemon_claude_lines.add(stripped)
193
-
194
- # Limit set size
195
- if len(self._seen_daemon_claude_lines) > 500:
196
- current_lines = {line.strip() for line in lines if line.strip()}
197
- self._seen_daemon_claude_lines = current_lines
198
-
199
- if new_lines:
200
- for line in new_lines:
201
- self._write_to_file(f"[DAEMON_CLAUDE] {line}", "INFO")
202
- if line.startswith('✓') or 'success' in line.lower():
203
- self.console.print(f" [success]│[/success] {line}")
204
- elif line.startswith('✗') or 'error' in line.lower() or 'fail' in line.lower():
205
- self.console.print(f" [error]│[/error] {line}")
206
- elif line.startswith('>') or line.startswith('$'):
207
- self.console.print(f" [highlight]│[/highlight] {line}")
208
- else:
209
- self.console.print(f" [daemon_claude]│[/daemon_claude] {line}")
210
-
211
- def section(self, title: str):
212
- """Print a section divider."""
213
- self._write_to_file(f"=== {title} ===", "INFO")
214
- self.console.print()
215
- self.console.rule(f"[bold]{title}[/bold]", style="dim")
216
-
217
- def status_summary(self, total: int, green: int, non_green: int, loop: int):
218
- """Print a status summary line."""
219
- status_text = Text()
220
- status_text.append(f"Loop #{loop}: ", style="dim")
221
- status_text.append(f"{total} agents ", style="highlight")
222
- status_text.append("(", style="dim")
223
- status_text.append(f"{green} green", style="success")
224
- status_text.append(", ", style="dim")
225
- status_text.append(f"{non_green} non-green", style="warn" if non_green else "dim")
226
- status_text.append(")", style="dim")
227
-
228
- self._write_to_file(f"Loop #{loop}: {total} agents ({green} green, {non_green} non-green)", "INFO")
229
- self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] ", end="")
230
- self.console.print(status_text)
231
-
232
-
233
135
  class SupervisorDaemon:
234
136
  """Background daemon that orchestrates daemon claude for non-green sessions.
235
137
 
@@ -244,7 +146,7 @@ class SupervisorDaemon:
244
146
  tmux_session: str = None,
245
147
  session_manager: SessionManager = None,
246
148
  tmux_manager: TmuxManager = None,
247
- logger: SupervisorLogger = None,
149
+ logger: SupervisorDaemonLogger = None,
248
150
  ):
249
151
  """Initialize the supervisor daemon.
250
152
 
@@ -252,7 +154,7 @@ class SupervisorDaemon:
252
154
  tmux_session: Name of the tmux session to manage
253
155
  session_manager: Optional SessionManager for dependency injection
254
156
  tmux_manager: Optional TmuxManager for dependency injection
255
- logger: Optional SupervisorLogger for dependency injection
157
+ logger: Optional SupervisorDaemonLogger for dependency injection
256
158
  """
257
159
  self.tmux_session = tmux_session or DAEMON.default_tmux_session
258
160
  self.session_manager = session_manager or SessionManager()
@@ -267,7 +169,7 @@ class SupervisorDaemon:
267
169
  self.log_path = get_supervisor_log_path(self.tmux_session)
268
170
 
269
171
  # Logger with session-specific log file
270
- self.log = logger or SupervisorLogger(log_file=self.log_path)
172
+ self.log = logger or SupervisorDaemonLogger(log_file=self.log_path)
271
173
 
272
174
  # Load persistent supervisor stats
273
175
  self.supervisor_stats = SupervisorStats.load(self.stats_path)
@@ -808,20 +710,30 @@ class SupervisorDaemon:
808
710
  """
809
711
  check_interval = check_interval or DAEMON.interval_fast
810
712
 
811
- # Check for existing supervisor daemon
812
- existing_pid = get_process_pid(self.pid_path)
813
- if existing_pid is not None and existing_pid != os.getpid():
814
- self.log.error(f"Another supervisor daemon is running (PID {existing_pid})")
713
+ # Atomically check if already running and acquire lock
714
+ # This prevents TOCTOU race conditions that could cause multiple daemons
715
+ acquired, existing_pid = acquire_daemon_lock(self.pid_path)
716
+ if not acquired:
717
+ if existing_pid:
718
+ self.log.error(f"Supervisor daemon already running (PID {existing_pid})")
719
+ else:
720
+ self.log.error("Could not acquire daemon lock (another daemon may be starting)")
815
721
  sys.exit(1)
816
722
 
817
- # Write PID file
818
- write_pid_file(self.pid_path)
819
-
820
723
  self.log.section("Supervisor Daemon")
821
724
  self.log.info(f"PID: {os.getpid()}")
822
725
  self.log.info(f"Tmux session: {self.tmux_session}")
823
726
  self.log.info(f"Check interval: {check_interval}s")
824
727
 
728
+ # Setup signal handlers for graceful shutdown
729
+ def handle_shutdown(signum, frame):
730
+ self.log.info("Shutdown signal received")
731
+ self._shutdown = True
732
+
733
+ signal.signal(signal.SIGTERM, handle_shutdown)
734
+ signal.signal(signal.SIGINT, handle_shutdown)
735
+ self._shutdown = False
736
+
825
737
  # Wait for monitor daemon
826
738
  self.log.info("Waiting for Monitor Daemon...")
827
739
  if not self.wait_for_monitor_daemon():
@@ -834,7 +746,7 @@ class SupervisorDaemon:
834
746
  self.status = "active"
835
747
 
836
748
  try:
837
- while True:
749
+ while not self._shutdown:
838
750
  self.loop_count += 1
839
751
 
840
752
  # Cleanup orphaned daemon claudes
@@ -914,64 +826,21 @@ class SupervisorDaemon:
914
826
 
915
827
  time.sleep(check_interval)
916
828
 
917
- except KeyboardInterrupt:
918
- self.log.section("Shutting Down")
919
- self.status = "stopped"
920
- remove_pid_file(self.pid_path)
921
- self.log.info("Supervisor daemon stopped")
922
- sys.exit(0)
829
+ except Exception as e:
830
+ self.log.error(f"Supervisor daemon error: {e}")
831
+ raise
923
832
  finally:
833
+ self.log.info("Supervisor daemon shutting down")
834
+ self.status = "stopped"
924
835
  remove_pid_file(self.pid_path)
925
836
 
926
837
 
927
- def is_supervisor_daemon_running(session: str = None) -> bool:
928
- """Check if the supervisor daemon is running for a session.
929
-
930
- Args:
931
- session: tmux session name (default: from config)
932
- """
933
- if session is None:
934
- session = DAEMON.default_tmux_session
935
- return is_process_running(get_supervisor_daemon_pid_path(session))
936
-
937
-
938
- def get_supervisor_daemon_pid(session: str = None) -> Optional[int]:
939
- """Get the supervisor daemon PID if running.
940
-
941
- Args:
942
- session: tmux session name (default: from config)
943
- """
944
- if session is None:
945
- session = DAEMON.default_tmux_session
946
- return get_process_pid(get_supervisor_daemon_pid_path(session))
947
-
948
-
949
- def stop_supervisor_daemon(session: str = None) -> bool:
950
- """Stop the supervisor daemon if running.
951
-
952
- Args:
953
- session: tmux session name (default: from config)
954
-
955
- Returns:
956
- True if daemon was stopped, False if it wasn't running.
957
- """
958
- import signal
959
-
960
- if session is None:
961
- session = DAEMON.default_tmux_session
962
- pid_path = get_supervisor_daemon_pid_path(session)
963
- pid = get_process_pid(pid_path)
964
- if pid is None:
965
- remove_pid_file(pid_path)
966
- return False
967
-
968
- try:
969
- os.kill(pid, signal.SIGTERM)
970
- remove_pid_file(pid_path)
971
- return True
972
- except (OSError, ProcessLookupError):
973
- remove_pid_file(pid_path)
974
- return False
838
+ # Create PID helper functions using factory
839
+ (
840
+ is_supervisor_daemon_running,
841
+ get_supervisor_daemon_pid,
842
+ stop_supervisor_daemon,
843
+ ) = create_daemon_helpers(get_supervisor_daemon_pid_path, "supervisor")
975
844
 
976
845
 
977
846
  def main():