Commite0a04619Recorded29 Jun 2026Repositorycourier

Bound Telegram poll + send reads with request-timeout

Message

Pass a 25s request-timeout (below the 30s poller stale threshold) into the poller's make-tg-bot and into the leader-side send-message tg-client, so a half-open/blackholed TLS read raises instead of blocking. The poller's poll-loop guard then logs, backs off, and reconnects; the leader-side send fails cleanly instead of freezing the whole leader (the likeliest cause of needing a manual /mcp re-init).

Consumes the new sigil-telegram request-timeout (which threads through to sigil-http 0.16.3's opt-in timeout: keyword). The media-upload client also sets it for consistency, but uploads read over raw TLS in sigil-telegram's upload path, so that bound is not yet effective there (noted inline as a follow-up).

Changed
 src/courier/poller.sgl   | 10 +++++++++-
 src/courier/telegram.sgl | 22 ++++++++++++++++++++--
 2 files changed, 29 insertions(+), 3 deletions(-)
Diff
src/courier/poller.sglmodified
@@ -66,6 +66,13 @@
66
(define *error-backoff-max* 60)
67
(define *heartbeat-slice* 5) ;; max seconds between heartbeats
68
+69
;; HTTP read timeout for each poll's getUpdates request. Kept BELOW
+70
;; *poller-stale-threshold* (30s) so a stalled/half-open read raises
+71
;; (caught by the poll-loop guard -> log + backoff + reconnect)
+72
;; before the supervisor watchdog would SIGKILL the child. Without
+73
;; it a blackholed TLS read blocks past the threshold every time.
+74
(define *poll-request-timeout* 25) ;; seconds; must be < stale threshold
+75
76
;; Supervisor: wedge detection and restart backoff
77
(define *poller-stale-threshold* 30) ;; seconds of child silence
78
(define *watchdog-interval* 10) ;; seconds between checks
@@ -161,7 +168,8 @@
168
(: courier-config? -> void?)
169
(let* ((token (courier-config-telegram-token config))
170
(allowed (courier-config-allowed-senders config))
164
(bot (make-tg-bot token: token)))
+171
(bot (make-tg-bot token: token
+172
request-timeout: *poll-request-timeout*)))
173
174
(poller-emit-line!
175
(json-encode `((type . "hello") (pid . ,(process-id)))))
src/courier/telegram.sglmodified
@@ -24,6 +24,14 @@
24
format-size)
25
(begin
26
+27
;; HTTP read timeout (seconds) for leader-side Telegram sends. A
+28
;; blackholed send-response read on the leader's blocking TLS read
+29
;; would otherwise freeze the whole leader (MCP servicing, watchdog,
+30
;; poller supervision) until the OS TCP timeout -- the likeliest
+31
;; cause of needing a manual /mcp re-init. Bounding the read makes a
+32
;; stalled send raise promptly so the tool call fails cleanly.
+33
(define *send-request-timeout* 25)
+34
35
;; ============================================================
36
;; Send Message Tool
37
;; ============================================================
@@ -63,7 +71,10 @@
71
default-chat-id)))
72
(if (and token chat-id)
73
(begin
66
(tg-send-message (tg-client token: token) chat-id text)
+74
(tg-send-message
+75
(tg-client token: token
+76
request-timeout: *send-request-timeout*)
+77
chat-id text)
78
(log-info "Telegram message sent" chat-id: chat-id)
79
"Message sent.")
80
"Error: Telegram not configured (missing token or chat ID)"))))))))))
@@ -140,7 +151,14 @@
151
"Error: no chat ID (set COURIER_TELEGRAM_CHAT_ID or pass 'to')"
152
(let* ((kind (resolve-media-type type-arg path))
153
(size (file-size path))
143
(client (tg-client token: token)))
+154
;; NOTE: media uploads go through tg-api-call/upload,
+155
;; which reads over a raw TLS connection (not
+156
;; sigil-http's http-post/json), so this timeout is
+157
;; NOT yet enforced on the upload read -- set for
+158
;; consistency/future-proofing. Bounding uploads needs
+159
;; a timeout on sigil-telegram's upload path (follow-up).
+160
(client (tg-client token: token
+161
request-timeout: *send-request-timeout*)))
162
(upload-by-kind client chat-id kind path caption-arg)
163
(log-info "Telegram media sent"
164
chat-id: chat-id kind: kind