Skip to content

fix(T-31): stop dropping failure alerts, and stop saving blank KPI summaries - #24

Merged
DamengRandom merged 1 commit into
masterfrom
fix/T-31-telegram-alerts-and-kpi-summary
Aug 1, 2026
Merged

fix(T-31): stop dropping failure alerts, and stop saving blank KPI summaries#24
DamengRandom merged 1 commit into
masterfrom
fix/T-31-telegram-alerts-and-kpi-summary

Conversation

@DamengRandom

Copy link
Copy Markdown
Owner

Closes #21
Closes #23

Two bugs from a full-codebase scan. Both were confirmed against the live APIs, not just reasoned about.

1. Failure alerts were silently dropped (#21)

notifyError built an HTML message but never escaped the error text, and never checked whether Telegram accepted it. Telegram's HTML mode rejects any tag outside its whitelist with a 400, so an error carrying markup lost the alert entirely — and because response.ok was never read, nothing was logged either. Every other Telegram builder in the repo already escapes; this one, the one handling arbitrary third-party text, did not.

Confirmed against the real bot. Both of these reach notifyError from live code paths — a Tavily gateway 502 returns an HTML body (ai-news-search.tool.ts:53), and the default reasoning model emits <think> into the curator retry error (curator.graph.ts:40):

Bad Request: can't parse entities: Unsupported start tag "html" at byte offset 109
Bad Request: can't parse entities: Unsupported start tag "think" at byte offset 163

The same errors after this change, plus the worst cases:

case sent result
HTML gateway body 326 chars 200 ok=true
<think> from curator retry 284 200 ok=true
5,000 quote chars (worst-case expansion) 3,760 200 ok=true
mixed & and <> in a DB error 246 200 ok=true

Ordering matters here. The raw text is cut before escaping, never after — escaping a cut string is always safe, but cutting an escaped one can split an entity into &qu, which Telegram also rejects. 600 raw chars stay inside the 4096 limit even if every character expands sixfold ("& quot;). The alert carries a LangSmith link for the full trace.

A rejected alert is now logged instead of discarded. It is the last line of defence, so there is nowhere left to escalate to.

2. github_summary was always empty (#23)

Every GitHub-only KPI day wrote a blank summary. github.tool.ts:56 hardcodes summary: '', and GITHUB_PROMPT tells the agent to "return the full result as-is. Do not summarize or modify the data." — so nothing ever filled it.

Confirmed end-to-end against the live GitHub API and LLM, on a real day:

commits returned      : 4
pullRequests returned : 6
summary field         : ""     ← tool output
...
github_summary        : ""     ← what reached the kpi table

The summary is now built from the counts: "4 commits, 6 PRs on GitHub".

One judgement call worth reviewing. Payloads that never carried the arrays (unparseable, or {}) stay blank rather than getting synthesized text. kpi-record.test.ts:58 argues the summary is the only thing separating a real quiet day from a broken payload, and describing both would destroy that. So a genuine zero day reads "0 commits, 0 PRs on GitHub", a broken one stays "".

Also worth knowing

The LangSmith link appears on every alert, but only 3 of the 14 notifyError call sites are LangChain runs it can trace — the other 11 are fetch and pg errors with no trace to point at. The full error is in the job logs either way. Showing the line only for agent contexts is a reasonable follow-up if it proves annoying.

Verification

  • pnpm tsc, pnpm format:check, pnpm test, pnpm test:coverage — all exit=0
  • 71 tests pass (9 new), kpi-record.ts at 100% coverage
  • Both digests sent for real end-to-end: AI news (4 stories via Tavily) and GitHub trending (27 scraped → top 8 curated), both delivered and saved
  • All Telegram test alerts were deleted afterwards

🤖 Generated with Claude Code

…mmaries

Two bugs found by a full-codebase scan, both confirmed against the live APIs.

notifyError built an HTML message but never escaped the error text, and never
checked whether Telegram accepted it. Telegram's HTML mode rejects any tag
outside its whitelist with a 400, so an error carrying markup lost the alert
entirely and left no trace — verified live: a Tavily gateway 502 (HTML body)
and a <think> tag from the curator retry path both returned

  400 Bad Request: can't parse entities: Unsupported start tag "html"

The error is now escaped, and a rejected alert is logged rather than
discarded. The raw text is cut before escaping, never after: escaping a cut
string is safe, but cutting an escaped one can split an entity ("&qu") and
Telegram rejects that too. 600 raw chars stay inside the 4096 limit even at
the sixfold worst case. The alert links to LangSmith for the full trace.

github_summary was empty on every GitHub-only KPI day. The tool hardcodes
summary: '' and GITHUB_PROMPT tells the agent to relay the output verbatim,
so nothing ever filled it — confirmed end-to-end against the live GitHub API
and LLM: 4 commits and 6 PRs stored with "". The summary is now built from
the counts. Payloads that never carried the arrays stay blank, which keeps a
broken payload distinguishable from a genuine zero-activity day.

Closes #21
Closes #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5CmYPdQcMeLzzJc3iDQ2u
@DamengRandom
DamengRandom merged commit b53509a into master Aug 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant