personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

cortex: add pgid teardown for talent wrappers

Start talent wrapper subprocesses in their own process groups and terminate the group when cortex stops a run, so the 2026-04-26 21:00-22:05 cortex meltdown failure mode cannot leave Gemini CLI descendants behind after wrapper shutdown. Talent main now installs asyncio-integrated SIGTERM/SIGINT cancellation handlers and removes them during cleanup; the live dev-host Gemini smoke remains deferred until API credentials are available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+35 -2
+22 -2
think/cortex.py
··· 20 20 import json 21 21 import logging 22 22 import os 23 + import signal 23 24 import subprocess 24 25 import sys 25 26 import threading ··· 58 59 59 60 if self.process.poll() is None: 60 61 # First try SIGTERM for graceful shutdown 61 - self.process.terminate() 62 + try: 63 + self.process.terminate() 64 + except ProcessLookupError: 65 + pass 66 + self._signal_process_group(signal.SIGTERM) 62 67 try: 63 68 self.process.wait(timeout=10) # Give more time for graceful shutdown 64 69 except subprocess.TimeoutExpired: 65 70 logging.getLogger(__name__).warning( 66 71 f"Talent {self.use_id} didn't stop gracefully, killing" 67 72 ) 68 - self.process.kill() 73 + self._signal_process_group(signal.SIGKILL) 74 + try: 75 + self.process.kill() 76 + except ProcessLookupError: 77 + pass 69 78 self.process.wait() # Ensure zombie is reaped 79 + 80 + def _signal_process_group(self, sig: int) -> None: 81 + try: 82 + pgid = os.getpgid(self.process.pid) 83 + except ProcessLookupError: 84 + return 85 + try: 86 + os.killpg(pgid, sig) 87 + except ProcessLookupError: 88 + return 70 89 71 90 72 91 class CortexService: ··· 321 340 env=env, 322 341 bufsize=1, 323 342 cwd=subprocess_cwd, 343 + start_new_session=True, 324 344 ) 325 345 326 346 # Send input and close stdin
+13
think/talents.py
··· 18 18 import json 19 19 import logging 20 20 import os 21 + import signal 21 22 import sys 22 23 import traceback 23 24 from datetime import datetime ··· 1239 1240 1240 1241 app_logger = setup_logging(args.verbose) 1241 1242 event_writer = JSONEventWriter(None) 1243 + loop = asyncio.get_running_loop() 1244 + main_task = asyncio.current_task() 1245 + registered_signals: list[signal.Signals] = [] 1246 + if main_task: 1247 + for sig in (signal.SIGTERM, signal.SIGINT): 1248 + try: 1249 + loop.add_signal_handler(sig, main_task.cancel) 1250 + registered_signals.append(sig) 1251 + except (NotImplementedError, RuntimeError): 1252 + LOG.debug("Signal handler registration unavailable for %s", sig) 1242 1253 1243 1254 def emit_event(data: Event) -> None: 1244 1255 if "ts" not in data: ··· 1301 1312 emit_event(err) 1302 1313 raise 1303 1314 finally: 1315 + for sig in registered_signals: 1316 + loop.remove_signal_handler(sig) 1304 1317 event_writer.close() 1305 1318 1306 1319