fix(ladder): занятость уровня в срезе из running-задач, а не cycle_state.busy (t-260929170039-aad557)

busy в read model всегда False — истинная занятость живёт в памяти
планировщика, событий для неё нет. Срез показывал «ожидает», пока
часовой уровень держал running-задачу. TaskClaimed закрепляет уровень
за задачей в проекции — теперь срез показывает «занят (running-задача)».
This commit is contained in:
simbiot-harness 2026-09-29 21:07:12 +00:00
parent 78ba888bb9
commit 9cd10256c4

View file

@ -176,17 +176,29 @@ def notify_chat(text: str):
def ladder_snapshot() -> str:
"""Срез лестницы для Telegram: уровни, занятость, задачи, эскалации."""
"""Срез лестницы для Telegram: уровни, занятость, задачи, эскалации.
Занятость: busy в read model всегда False (истинное состояние живёт
в памяти планировщика, событий для него нет), поэтому выводим её из
running-задач: TaskClaimed закрепляет уровень за задачей в проекции.
Уровень без задачи (чистый дайджест-тик) здесь выглядит как «ожидает» —
приемлемо: дайджест-тик короткий, а взятие задачи — главный кейс.
"""
model = es.run_projections()
cs = model.get("cycle_state") or {}
tasks_all = list((model.get("tasks") or {}).values())
running_levels = {t.get("level") for t in tasks_all if t.get("status") == "running"}
order = ["sec", "min", "hour", "day"]
lines = []
for name in order:
st = cs.get(name) or {}
busy = "🔧 занят" if st.get("busy") else "ожидает"
if name in running_levels:
busy = "🔧 занят (running-задача)"
else:
busy = "ожидает"
lines.append(f"{'🔹' if name != 'sec' else '⚡'} {name}: {busy}, тиков {st.get('ticks', 0)}"
+ (f", ошибок {st['errors']}" if st.get("errors") else ""))
tasks = list((model.get("tasks") or {}).values())
tasks = tasks_all
tasks.sort(key=lambda t: t.get("created") or "", reverse=True)
board = [t for t in tasks if t.get("status") in ("ready", "running")]
lines.append(f"📋 готово/в работе: {sum(1 for t in tasks if t['status'] == 'ready')}/"