t9 telegram: single-id allowlist delivery — notify-once via claim-send-commit, thin bot api client, lifespan-owned poller - #4
Merged
Conversation
…ngle-id allowlist poller in lifespan, notify job with claim-send-commit once semantics on notified_at, html-escaped formatting
…token was landing in ingest_runs.detail), poller survives any exception + acks poisoned updates, shutdown tolerates a crashed poll task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
what
delivery/telegram.py— a hand-rolled thin Bot API client (sendMessage+getUpdates) over the shared connector resilience (request_jsonextended with method/json_body/http_timeout), behind two Protocol halves:Messenger(what the notify job needs) andUpdateSource(what the poller reads). Telegram's HTTP-200-with-ok:falsecounts as failure. This supersedes the old python-telegram-bot pin (D16): two endpoints don't justify a framework that wants to own the event loop — DECISIONS entry to follow post-merge.delivery/formatting.py— pure formatting for HTML parse mode; every dynamic field is html-escaped at this one boundary, because the reasoning is LLM prose over untrusted headlines and must never become markup.delivery/poller.py—UpdatePollerlong-poll loop as a lifespan-owned background task (cancellation is the stop signal, and the only thing that stops it). Default-deny allowlist: chat id and sender id must both equal the single allowlisted id, or the update is dropped in silence — strangers learn nothing. Allowlisted messages get the latest explained anomaly; the reply text arrives through an injected async callable so the poller has no DB dependency.ingest/jobs.py— fifth recurring jobnotify_anomalies(60s tick): queue = explained-but-unnotified anomalies oldest-first; once-semantics = compare-and-set on the (already-migrated)anomalies.notified_at: claim (uncommitted) → send → commit per item. A failed send rolls the claim back for the next tick; Telegram down aborts the batch as an error run. Deliberately at-least-once: a rare duplicate alert beats a silently missing one.store/repositories.py—explained_unnotified,latest_explained,mark_notified(the atomic claim).telegram_bot_token/base_url/chat_id/rate_per_min, required, no defaults;.env.exampleupdated.main.pycomposes the client + poller;create_appgains an optionalpoller(PollerLike).review fixes (subagent)
SourceUnavailabletext is persisted toingest_runs.detail.request_jsonnow takessecrets=(...)and scrubs them at the wrap; applied to Telegram and the pre-existing Finnhub/Massive query-param variant of the same leak. Regression-tested.finally, aborting the rest of teardown. The loop now absorbs any exception (backoff on failed polls, ack-and-skip on poisoned updates — never redelivered into the same crash), and shutdown suppresses a crashed task's exception.tests
<a href>/&reasoning stays literal), respx client tests (payload shape, ok:false, 5xx retry + token-redaction assertion), poller allowlist driven throughrun()with a scripted API (stranger silence, both-ids-must-match, crash-survival with ack).