agent-Specialization/server/context/reaper.py
JOJO e5808ea5be refactor(server,web): remove Socket.IO entirely, unify on REST polling
- task event stream (_append_event) is now the sole realtime channel for web and CLI
- status snapshots: idle 5s polling of /api/status (was status_update push); operation initiator gets state from REST responses
- terminal panel: REST polling (list 5s, output 1.5s, prefix-matched incremental xterm writes)
- multi-tab passive sync deliberately degrades to polling-based perception
- delete socket_handlers/broadcast/useLegacySocket (2103-line dead file); extensions.py keeps only run_background
- remove socket token chain (/api/socket-token, prune/consume_socket_token, pending_socket_tokens)
- app.run(threaded=True) replaces socketio.run; reapers switched to plain threading
- drop flask-socketio/socket.io-client/websockets dependencies
- docs: AGENTS.md section 12.5 (replacement map + hard constraints), plus CLI rewrite companion doc updates

Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
2026-09-11 17:28:47 +08:00

161 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""系统状态重置与对话级 terminal 24h TTL 回收器。"""
from __future__ import annotations
import os
import time
from typing import Optional
from core.web_terminal import WebTerminal
from server import state
from server.utils_common import debug_log
def reset_system_state(terminal: Optional[WebTerminal]):
"""完整重置系统状态"""
if not terminal:
return
try:
if hasattr(terminal, 'current_session_id'):
terminal.current_session_id += 1
debug_log(f"重置会话ID为: {terminal.current_session_id}")
web_attrs = ['streamingMessage', 'currentMessageIndex', 'preparingTools', 'activeTools']
for attr in web_attrs:
if hasattr(terminal, attr):
if attr in ['streamingMessage']:
setattr(terminal, attr, False)
elif attr in ['currentMessageIndex']:
setattr(terminal, attr, -1)
elif attr in ['preparingTools', 'activeTools'] and hasattr(getattr(terminal, attr), 'clear'):
getattr(terminal, attr).clear()
debug_log("系统状态重置完成")
except Exception as e:
debug_log(f"状态重置过程中出现错误: {e}")
import traceback
debug_log(f"错误详情: {traceback.format_exc()}")
# ====== 对话级 terminal 24h TTL 回收器 ======
# 对话级 terminalkey 为 username::workspace_id::conversation_id 三段)常驻内存,
# 仅当「超过 TTL 无活动 且 该对话无运行中工作」时回收;工作区级服务 terminal 不回收。
CONVERSATION_TERMINAL_TTL_SECONDS = float(os.environ.get("CONVERSATION_TERMINAL_TTL_SECONDS", str(24 * 3600)))
CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS = float(os.environ.get("CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS", "600"))
_conversation_terminal_reaper_started = False
def _conversation_terminal_has_running_work(
username: str,
workspace_id: str,
conversation_id: str,
terminal: WebTerminal,
) -> bool:
"""判定对话是否仍有运行中的工作(主任务/子智能体/后台命令/多智能体)。
判定失败时保守返回 True不回收
"""
try:
from server.tasks import task_manager
active_statuses = {"pending", "running", "cancel_requested"}
for rec in task_manager.list_tasks(username, workspace_id):
if rec.conversation_id == conversation_id and rec.status in active_statuses:
return True
status = task_manager.get_conversation_running_status(terminal, conversation_id)
if any(bool(v) for v in (status or {}).values()):
return True
except Exception as exc:
debug_log(f"[ConvTerminalReaper] 运行状态判定失败 {conversation_id}: {exc}")
return True
return False
def reap_idle_conversation_terminals(now: Optional[float] = None) -> int:
"""回收超过 TTL 且无运行任务的对话级 terminal返回回收数量可测试"""
now = now or time.time()
reaped = 0
for term_key, terminal in list(state.user_terminals.items()):
parts = term_key.split("::")
if len(parts) < 3:
continue # 工作区级服务 terminal 不回收
username, workspace_id = parts[0], parts[1]
conversation_id = "::".join(parts[2:])
try:
last_active = float(getattr(terminal, "last_activity_at", None))
except (TypeError, ValueError):
last_active = None
if last_active is None:
# 无时间戳实例(旧版本创建):补上当前时间,下轮再判定
try:
terminal.last_activity_at = now
except Exception:
pass
continue
if now - last_active < CONVERSATION_TERMINAL_TTL_SECONDS:
continue
if _conversation_terminal_has_running_work(username, workspace_id, conversation_id, terminal):
continue
# 竞态防护:判定到关闭之间存在窗口,期间新请求可能拿到该实例并建任务。
# 先打关闭标记get_user_resources 见到标记会原地重建新实例),
# 并二次确认活动时间/运行工作未变化,最后 pop 时校验实例身份。
try:
terminal._reaper_closing = True
except Exception:
pass
aborted = False
latest_active = float(getattr(terminal, "last_activity_at", 0) or 0)
if latest_active > last_active:
debug_log(f"[ConvTerminalReaper] 关闭前检测到新活动,取消回收: {term_key}")
aborted = True
elif _conversation_terminal_has_running_work(username, workspace_id, conversation_id, terminal):
debug_log(f"[ConvTerminalReaper] 关闭前检测到运行任务,取消回收: {term_key}")
aborted = True
if aborted:
try:
terminal._reaper_closing = False
except Exception:
pass
continue
try:
cm = getattr(terminal, "context_manager", None)
# 与 __del__ 同理:空 history 保存会把磁盘上非空对话覆盖为空
if cm and getattr(cm, "current_conversation_id", None) and getattr(cm, "conversation_history", None):
cm.save_current_conversation()
except Exception as exc:
debug_log(f"[ConvTerminalReaper] 保存对话失败 {conversation_id}: {exc}")
try:
tm = getattr(terminal, "terminal_manager", None)
if tm:
tm.close_all()
except Exception as exc:
debug_log(f"[ConvTerminalReaper] 关闭 shell 失败 {conversation_id}: {exc}")
try:
mcp = getattr(terminal, "mcp_client_manager", None)
if mcp:
mcp.close_all_clients()
except Exception as exc:
debug_log(f"[ConvTerminalReaper] 关闭 MCP 失败 {conversation_id}: {exc}")
# 仅当缓存里仍是本实例时才移除(可能已被请求侧原地重建)
if state.user_terminals.get(term_key) is terminal:
state.user_terminals.pop(term_key, None)
reaped += 1
debug_log(f"[ConvTerminalReaper] 已回收对话级 terminal: {term_key} (idle {int(now - last_active)}s)")
return reaped
def _conversation_terminal_reaper_loop():
"""后台循环:定期扫描回收空闲对话级 terminal。"""
while True:
try:
reap_idle_conversation_terminals()
time.sleep(CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS)
except Exception as exc:
debug_log(f"[ConvTerminalReaper] 后台循环异常: {exc}")
time.sleep(CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS)
def start_conversation_terminal_reaper():
"""幂等启动对话级 terminal TTL 回收后台线程。"""
global _conversation_terminal_reaper_started
if _conversation_terminal_reaper_started:
return
_conversation_terminal_reaper_started = True
threading.Thread(target=_conversation_terminal_reaper_loop, daemon=True).start()