fix(agent): 收敛超时与取消后的副作用生命周期 (#6388)

This commit is contained in:
InfinityPacer
2026-08-22 12:11:15 +08:00
committed by GitHub
parent 7b6353337d
commit ed3c0541c7
16 changed files with 952 additions and 72 deletions
+104 -8
View File
@@ -128,6 +128,11 @@ class _TerminalSessionManager:
"""初始化会话表和并发保护锁。"""
self._sessions: dict[str, _TerminalSession] = {}
self._lock = asyncio.Lock()
self._close_lock = asyncio.Lock()
self._closed = False
self._starting = 0
self._starts_idle = asyncio.Event()
self._starts_idle.set()
@staticmethod
def _normalize_bool(value: Any, default: bool = True) -> bool:
@@ -211,20 +216,55 @@ class _TerminalSessionManager:
should_use_pty = self._normalize_bool(use_pty, default=True) and os.name == "posix"
async with self._lock:
if self._closed:
raise RuntimeError("终端会话管理器已关闭")
self._cleanup_finished_sessions_locked()
if self._active_session_count_locked() >= TERMINAL_CONCURRENCY_LIMIT:
if (
self._active_session_count_locked() + self._starting
>= TERMINAL_CONCURRENCY_LIMIT
):
raise RuntimeError(
f"后台终端会话数已达到上限 {TERMINAL_CONCURRENCY_LIMIT}"
)
self._starting += 1
self._starts_idle.clear()
session = (
await self._start_pty_session(command, normalized_cwd, normalized_env)
if should_use_pty
else await self._start_pipe_session(command, normalized_cwd, normalized_env)
)
session: Optional[_TerminalSession] = None
reject_session = False
session_registered = False
session_released = False
try:
session = (
await self._start_pty_session(command, normalized_cwd, normalized_env)
if should_use_pty
else await self._start_pipe_session(
command, normalized_cwd, normalized_env
)
)
async with self._lock:
self._sessions[session.session_id] = session
async with self._lock:
reject_session = self._closed
if not reject_session:
self._sessions[session.session_id] = session
session_registered = True
if reject_session:
await self._terminate_session(session)
session_released = True
raise RuntimeError("终端会话管理器已关闭")
except BaseException:
if session is not None and not session_registered and not session_released:
cleanup_task = asyncio.create_task(self._terminate_session(session))
try:
await asyncio.shield(cleanup_task)
except asyncio.CancelledError:
await cleanup_task
raise
finally:
async with self._lock:
self._starting -= 1
if self._starting == 0:
self._starts_idle.set()
logger.info(
"启动后台终端会话: session_id=%s, pid=%s, use_pty=%s, command=%s",
@@ -473,6 +513,62 @@ class _TerminalSessionManager:
return self._session_payload(session, output="", output_truncated=False)
async def close(self) -> None:
"""停止所有后台终端会话并释放 PTY、读取任务和会话记录。"""
async with self._close_lock:
async with self._lock:
self._closed = True
await self._starts_idle.wait()
async with self._lock:
sessions = list(self._sessions.values())
await asyncio.gather(
*(self._terminate_session(session) for session in sessions),
return_exceptions=True,
)
async with self._lock:
for session in sessions:
session.close_pty()
self._sessions.clear()
async def _terminate_session(self, session: _TerminalSession) -> None:
"""以有限等待停止进程,并在必要时升级为 SIGKILL。"""
if session.status == "running":
session.kill_requested = True
self._send_signal(session, signal.SIGTERM)
wait_task = session.wait_task
if wait_task and not wait_task.done():
try:
await asyncio.wait_for(
asyncio.shield(wait_task),
timeout=TERMINAL_KILL_GRACE_SECONDS,
)
except asyncio.TimeoutError:
force_signal = getattr(signal, "SIGKILL", signal.SIGTERM)
self._send_signal(session, force_signal)
try:
await asyncio.wait_for(
asyncio.shield(wait_task),
timeout=TERMINAL_KILL_GRACE_SECONDS,
)
except asyncio.TimeoutError:
logger.error(
"终端会话关闭超时: session_id=%s, pid=%s",
session.session_id,
session.pid,
)
for task in session.reader_tasks:
if not task.done():
task.cancel()
if session.reader_tasks:
await asyncio.gather(*session.reader_tasks, return_exceptions=True)
session.close_pty()
def get_session(self, session_id: str) -> _TerminalSession:
"""按 ID 获取会话,不存在时抛出清晰错误。"""
session = self._sessions.get(session_id)