fix(notifications): HTML-safe card data render — fix <1м injection freezing the tracker (ORCH-095) #107

Merged
admin merged 7 commits from feature/ORCH-095-bug-html-1-render-task-tracker into main 2026-06-10 00:21:49 +03:00
Owner

Summary

render_task_tracker sends/edits the live tracker card with parse_mode=HTML. _fmt_minutes returns the literal <1м for a sub-minute stage; interpolated raw into the HTML text, Telegram parsed <1м as an opening tag → editMessageText 400 can't parse entities: Unsupported start tag "1м"edit_telegram returns EDIT_FAILEDupdate_task_tracker early-returns (anti-duplicate ORCH-087) → the card froze (incident ORCH-093, message_id 18854).

The root class is wider than <1м: every interpolated data value (durations, status label, model, effort, token/cost metrics) was inserted raw; only the title (esc_title) and the issue-link href/label were escaped.

Change (per ADR-001)

  • New module-local _esc(x) = html.escape(str(x)) (never-raise → "") wraps every DATA slot exactly once at the render boundary in render_task_tracker/_stage_line: durations (_fmt_minutes/_capped_review_str), status label (_card_status_label), model (short_model_name), effort (_run_effort), tokens/cost (fmt_tokens/fmt_cost).
  • Source functions stay HTML-agnostic — _fmt_minutes still returns <1м; escape on the boundary renders it visually identical (&lt;1м), so the visible format is unchanged.
  • MARKUP slots (num_html/plane_issue_link, link_for, _done_link, already-escaped esc_title) are not escaped → the issue number stays a clickable <a> tag; no double-escaping.
  • Frozen cards (within the Telegram window) auto-recover on the next stage transition — a fresh safe render edits in place (200). No new code; edit_telegram/update_task_tracker/orphan ledger untouched → ORCH-087 anti-duplicate invariant preserved.

STAGE_TRANSITIONS / QG_CHECKS / check_* / notification transport / DB schema — untouched.

Tests

New tests/test_tracker_html_escape.py (TC-01..TC-11): boundary escape of sub-minute durations, never-raise _fmt_minutes/_esc, render without raw <1м, title special chars without double-escape, escaped status/model/effort, safe token/cost metrics, clickable-<a> + _done_link markup regression, parse-safe edit payload, edit-in-place + transient-fail anti-duplicate, never-raise on broken inputs.

Full pytest tests/ -q green (1437); ruff check clean.

Docs

Architecture golden-source (docs/architecture/internals.md §7, README.md) already carry the ORCH-095 HTML-safety invariant; CHANGELOG.md updated under [Unreleased].

Refs: ORCH-095

🤖 Generated with Claude Code

## Summary `render_task_tracker` sends/edits the live tracker card with `parse_mode=HTML`. `_fmt_minutes` returns the literal `<1м` for a sub-minute stage; interpolated **raw** into the HTML text, Telegram parsed `<1м` as an opening tag → `editMessageText` `400 can't parse entities: Unsupported start tag "1м"` → `edit_telegram` returns `EDIT_FAILED` → `update_task_tracker` early-returns (anti-duplicate ORCH-087) → **the card froze** (incident ORCH-093, `message_id 18854`). The root class is wider than `<1м`: every interpolated **data** value (durations, status label, model, effort, token/cost metrics) was inserted raw; only the title (`esc_title`) and the issue-link href/label were escaped. ## Change (per ADR-001) - New module-local `_esc(x) = html.escape(str(x))` (never-raise → `""`) wraps every **DATA** slot exactly once **at the render boundary** in `render_task_tracker`/`_stage_line`: durations (`_fmt_minutes`/`_capped_review_str`), status label (`_card_status_label`), model (`short_model_name`), effort (`_run_effort`), tokens/cost (`fmt_tokens`/`fmt_cost`). - Source functions stay HTML-agnostic — `_fmt_minutes` still returns `<1м`; escape on the boundary renders it visually identical (`&lt;1м`), so the visible format is **unchanged**. - **MARKUP** slots (`num_html`/`plane_issue_link`, `link_for`, `_done_link`, already-escaped `esc_title`) are **not** escaped → the issue number stays a clickable `<a>` tag; no double-escaping. - Frozen cards (within the Telegram window) **auto-recover** on the next stage transition — a fresh safe render edits in place (`200`). No new code; `edit_telegram`/`update_task_tracker`/orphan ledger untouched → ORCH-087 anti-duplicate invariant preserved. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / notification transport / DB schema — **untouched**. ## Tests New `tests/test_tracker_html_escape.py` (TC-01..TC-11): boundary escape of sub-minute durations, never-raise `_fmt_minutes`/`_esc`, render without raw `<1м`, title special chars without double-escape, escaped status/model/effort, safe token/cost metrics, clickable-`<a>` + `_done_link` markup regression, parse-safe edit payload, edit-in-place + transient-fail anti-duplicate, never-raise on broken inputs. Full `pytest tests/ -q` green (1437); `ruff check` clean. ## Docs Architecture golden-source (`docs/architecture/internals.md` §7, `README.md`) already carry the ORCH-095 HTML-safety invariant; `CHANGELOG.md` updated under `[Unreleased]`. Refs: ORCH-095 🤖 Generated with [Claude Code](https://claude.com/claude-code)
admin added 6 commits 2026-06-10 00:17:27 +03:00
render_task_tracker sends/edits the live card with parse_mode=HTML. _fmt_minutes
returns the literal "<1м" for a sub-minute stage; interpolated raw into HTML text
Telegram parsed "<1м" as an opening tag -> editMessageText 400 can't parse
entities -> edit_telegram EDIT_FAILED -> update_task_tracker early return
(anti-duplicate ORCH-087) -> the card froze (incident ORCH-093, message_id 18854).

Close the whole "unescaped data in HTML text" class per ADR-001: a module-local
_esc(x)=html.escape(str(x)) (never-raise) wraps every DATA slot (durations, status
label, model, effort, token/cost metrics) exactly once at the render boundary in
render_task_tracker/_stage_line. Source functions stay HTML-agnostic (_fmt_minutes
still returns "<1м"; escape on the boundary renders it visually identical as
&lt;1м, so the visible format is unchanged). Intentional MARKUP slots (num_html /
link_for / _done_link / already-escaped esc_title) are NOT escaped, so the issue
number stays a clickable <a> tag and nothing is double-escaped.

A previously-frozen card auto-recovers on the next stage transition (a new safe
render edits in place, 200) — no new code, no touch to edit_telegram /
update_task_tracker / the orphan ledger, so the ORCH-087 anti-duplicate invariant
is preserved (a transient edit failure still does not spawn a new card).

STAGE_TRANSITIONS / QG_CHECKS / check_* / notification transport / DB schema are
untouched. New tests/test_tracker_html_escape.py (TC-01..TC-11); full suite green.

Refs: ORCH-095

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tester(ET): auto-commit from tester run_id=530
All checks were successful
CI / test (push) Successful in 41s
CI / test (pull_request) Successful in 41s
cdc5e5c548
admin force-pushed feature/ORCH-095-bug-html-1-render-task-tracker from efe8fd6383 to cdc5e5c548 2026-06-10 00:17:27 +03:00 Compare
admin merged commit 8c2fa5de6d into main 2026-06-10 00:21:49 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/orchestrator#107