fix(discord-bridge): zgomot in bot.log si doua tipuri de stream necunoscute

Trei lucruri observate in logurile de productie dupa restartul precedent.

1. Ecoul propriilor mesaje umplea bot.log cu WARNING. Propriile mesaje au si
   ele `author.bot == True`, iar verificarea generica de bot venea INAINTEA
   celei pe `self_id` — deci raspunsurile botului se jurnalizau ca "bot
   strain", la fiecare mesaj. Verificarea pe `self_id` trece prima (motivul e
   acum precis), iar refuzurile de rutina — propriile mesaje si ceilalti boti —
   merg la DEBUG. Guild / canal / utilizator strain si webhook raman WARNING:
   alea chiar sunt semnal de securitate si erau inecate in zgomot.

2. `tool_progress` (heartbeat la 30s cat timp o unealta ruleaza) devine
   eveniment `ToolProgress`. Mesajul live arata acum "⏳ ruleaza de 2m30s" sub
   unealta curenta — singurul semn ca un tur lung lucreaza si nu a inghetat.

3. `rate_limit_event` devine eveniment `RateLimit` si apare in `/status` la
   randul `utilizare`. Cum nu exista plafon de cost (abonament, nu API),
   fereastra de utilizare e singura limita reala; se avertizeaza in log o data
   per schimbare de stare, nu la fiecare eveniment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
Claude Agent
2026-08-31 07:46:51 +00:00
parent 1cc1a572d9
commit a89cf201ab
6 changed files with 243 additions and 8 deletions

View File

@@ -107,6 +107,11 @@ class Decision:
ACCEPTED = Decision(True)
# Refuzuri de rutina: botul isi vede propriile mesaje si pe ale altor boti la
# fiecare raspuns. Nu sunt semnal de securitate, deci merg la DEBUG — altfel
# ineaca in bot.log refuzurile care chiar conteaza (guild/canal/utilizator strain).
ROUTINE_REJECTS = ("propriul mesaj", "bot")
def _attr(obj, name, default=None):
return getattr(obj, name, default)
@@ -141,11 +146,13 @@ def check_message(message, self_id: str | None = None) -> Decision:
author = _attr(message, "author")
if author is None:
return Decision(False, "fara autor")
if bool(_attr(author, "bot", False)):
return Decision(False, "bot")
# Verificarea `self_id` sta INAINTEA celei generice de bot: propriile mesaje au
# si ele `author.bot == True`, iar altfel ar fi jurnalizate ca "bot strain".
uid = str(_attr(author, "id", ""))
if self_id is not None and uid == str(self_id):
return Decision(False, "propriul mesaj")
if bool(_attr(author, "bot", False)):
return Decision(False, "bot")
guild = _attr(message, "guild")
gid = _attr(guild, "id") if guild is not None else _attr(message, "guild_id")
@@ -285,6 +292,16 @@ def rss_mb(pid: int | None) -> float:
return 0.0
def _reset_text(resets_at: int) -> str:
"""Momentul resetarii ferestrei, in ora locala. 0 / valoare aiurea -> „?"."""
try:
if not resets_at:
return "?"
return time.strftime("%H:%M", time.localtime(int(resets_at)))
except (TypeError, ValueError, OSError):
return "?"
def _tool_line(ev) -> str:
if isinstance(ev, stream_mod.ToolUse):
inp = ev.input or {}
@@ -294,6 +311,13 @@ def _tool_line(ev) -> str:
return ""
def _progress_line(ev) -> str:
"""`tool_progress` -> „ruleaza de 2m30s". Doar asta arata ca turul e viu."""
secs = max(0, int(ev.elapsed_s or 0))
human = f"{secs}s" if secs < 60 else f"{secs // 60}m{secs % 60:02d}s"
return f"⏳ ruleaza de {human}"
# --------------------------------------------------------------------- Bridge
class Bridge:
"""Logica adaptorului, fara nicio dependinta de discord.py."""
@@ -323,6 +347,10 @@ class Bridge:
self.last_result = None # ultimul `result` primit (diagnostic)
self.steered = 0
self.approvals_wired = False
# Ultimul `rate_limit_event` din stream. Cum nu exista plafon de cost
# (abonament, nu API), fereastra de utilizare e singura limita reala.
self.rate_limit = None
self._rate_limit_warned: str | None = None
# ------------------------------------------------------------ iesire
async def _edit(self, target, text: str) -> None:
@@ -455,7 +483,9 @@ class Bridge:
if not decision.ok:
# Refuz TACUT: nu raspundem, nu reactionam. Doar log local.
self.rejected.append(decision.reason)
log.warning(
level = logging.DEBUG if decision.reason in ROUTINE_REJECTS else logging.WARNING
log.log(
level,
"mesaj respins (%s): guild=%s canal=%s user=%s",
decision.reason,
_attr(_attr(message, "guild"), "id"),
@@ -627,6 +657,11 @@ class Bridge:
f"in coada : {'da' if self.limits.queued(thread_id) else 'nu'}",
f"procese : {self.runner.live_count()} vii, {self.limits.free_slots} sloturi libere",
f"cost fir : ${float(rec.get('cost_usd_total') or 0.0):.4f}",
f"utilizare : " + (
"in regula" if (self.rate_limit is None or self.rate_limit.ok)
else f"⚠️ {self.rate_limit.status} ({self.rate_limit.limit_type or '?'}), "
f"reset {_reset_text(self.rate_limit.resets_at)}"
),
f"cost azi : ${self.limits.cost_today():.4f}"
+ (f" / ${cap:.2f}" if self.limits.cap_enabled else " (fara plafon)")
+ (" ⛔ PLAFON ATINS" if self.limits.stopped() else ""),
@@ -638,6 +673,19 @@ class Bridge:
lines.append("stderr (ultimele linii):\n```\n" + body + "\n```")
return "\n".join(lines)
def note_rate_limit(self, ev) -> None:
"""Retine fereastra de utilizare; avertizeaza o data per schimbare de stare."""
self.rate_limit = ev
if ev.ok:
self._rate_limit_warned = None
return
if self._rate_limit_warned != ev.status:
self._rate_limit_warned = ev.status
log.warning(
"limita de utilizare: status=%s tip=%s reset=%s",
ev.status, ev.limit_type or "?", _reset_text(ev.resets_at),
)
async def cmd_permisiuni(self, message, tid: str, cmd: Command) -> None:
await self.say(message.channel, self.permissions_text(tid, "revoca" in cmd.args))
@@ -729,22 +777,30 @@ class Bridge:
placeholder = await channel.send("⏳ lucrez…")
chunks: list[str] = []
tools: list[str] = []
progress = "" # ultimul heartbeat al uneltei curente, sters cand incepe alta
loop = self.render.loop_for(str(tid))
async def on_event(ev) -> None:
nonlocal progress
if isinstance(ev, stream_mod.SystemInit) and ev.session_id:
proc.sid = ev.session_id
elif isinstance(ev, stream_mod.AssistantText):
chunks.append(ev.text)
elif isinstance(ev, stream_mod.ToolUse):
tools.append(_tool_line(ev))
progress = ""
elif isinstance(ev, stream_mod.ToolProgress):
progress = _progress_line(ev)
elif isinstance(ev, stream_mod.RateLimit):
self.note_rate_limit(ev)
return
elif isinstance(ev, runner_mod.SessionRestarted):
chunks.append(f"_{ev.text}_\n")
else:
return
live = "".join(chunks)[-LIVE_TAIL:]
if tools:
live = (live + "\n" + tools[-1]).strip()
live = (live + "\n" + tools[-1] + (f" · {progress}" if progress else "")).strip()
loop.queue(placeholder, live or "⏳ lucrez…")
started = time.time()