# Inbound telegram

Last verified: 06-08-2026 @ c61b0b3

V-637, under V-628. Reads with `22-correcting-a-turn.md`.

## What was missing

Telegram was a reach and nothing else. `telegramsink` pushed an away message and the chat
had no way to answer, so the correction gesture reached the web and voice only.

That skews the labels. V-546 fits routing heads on them, and a sample drawn from wherever
the owner happens to be sitting is the wrong sample.

## Long-poll, not a webhook

The box takes no inbound connections and reaches api.telegram.org through a relay, so the
connection has to open outward. `getUpdates` with a 25 second hold, one goroutine in the
daemon's WaitGroup.

A failed poll waits 15 seconds and retries without escalating. The relay going down is the
normal cause and it comes back on its own.

## The backlog is dropped on start

Telegram keeps undelivered updates for 24 hours. A daemon that was down overnight would
otherwise wake and answer every queued message in order.

That is worse than missing them. A question asked eight hours ago has been answered
already. A reminder set from it lands at the wrong time. So the first call moves the offset
past whatever is queued and acts on none of it.

## One chat

`ChatID` is the only accepted sender, and it is the same chat the push half already sends
to. A message from anywhere else is dropped with no reply, because a reply confirms the bot
exists and whose it is.

Chat ids are not guessable. They are also not secret, since they travel in every forwarded
message. So this is the whole authorisation and it is an allowlist of one.

## The gesture

Two taps at most. The reply carries one button, `не то`. Tapping it writes nothing and opens
the seven intents plus `просто неверно`. The untargeted negative stays reachable, because he
may have opened the row without meaning to name anything.

Callback data carries the trace id and the target, under telegram's 64 byte cap. It comes
off the wire. So an id that will not parse is dropped, and so is a target that is not one of
the seven. A label nothing can score is worse than no label.

A failed write says so on the button and leaves the keyboard up. A successful one takes the
keyboard off, because a live keyboard on an answered turn invites correcting it twice.

## The seam

`NewPoller` takes two functions and no daemon type. `cmd/mavend/telegramintake.go` fills
them from `ipc.CoreAPI`: `Chat` returns the reply and the trace id it collected off the
context, and `CorrectTurn` writes the label. So a chat turn takes the path
`POST /api/chat` already takes, and nothing in `internal/delivery` knows what a handler is.

## What is not done

The turn source is still `tap:text`, which telegram shares with the web. Provenance cannot
tell a chat turn from a typed one, so a label's `source` column cannot either.
That matters the first time someone asks whether corrections given in the chat differ from
corrections given at the desk.

Voice messages are ignored. The poller reads `message.text` and nothing else, so a voice
note in the chat does not reach `mavsttd`.
