* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement
When a cron task's agent returns NO_REPLY, the payload filter strips the
silent token, leaving an empty text string. isLikelyInterimCronMessage()
previously returned true for empty input, causing the cron runner to
inject a forced rerun prompt ('Your previous response was only an
acknowledgement...').
Change the empty-string branch to return false: empty text after payload
filtering means the agent deliberately chose silent completion, not that
it sent an interim 'on it' message.
Fixes#41246
* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement
Fixes#41246. (#41383) thanks @jackal092927.
---------
Co-authored-by: xaeon2026 <xaeon2026@gmail.com>
When the gateway restarts with many overdue cron jobs, they are now
executed with staggered delays to prevent overwhelming the gateway.
- Add missedJobStaggerMs config (default 5s between jobs)
- Add maxMissedJobsPerRestart limit (default 5 jobs immediately)
- Prioritize most overdue jobs by sorting by nextRunAtMs
- Reschedule deferred jobs to fire gradually via normal timer
Fixes#18892
AbortSignal.any() fails in Node.js when signals come from different module
contexts (grammY's internal signal vs local AbortController), producing:
"The signals[0] argument must be an instance of AbortSignal. Received an
instance of AbortSignal".
Replace with manual event forwarding that works across all realms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the gateway receives SIGTERM, runner.stop() stops the grammY polling
loop but does not abort the in-flight getUpdates HTTP request. That request
hangs for up to 30 seconds (the Telegram API timeout). If a new gateway
instance starts polling during that window, Telegram returns a 409 Conflict
error, causing message loss and requiring exponential backoff recovery.
This is especially problematic with service managers (launchd, systemd)
that restart the process immediately after SIGTERM.
Wire an AbortController into the fetch layer so every Telegram API request
(especially the long-polling getUpdates) aborts immediately on shutdown:
- bot.ts: Accept optional fetchAbortSignal in TelegramBotOptions; wrap
the grammY fetch with AbortSignal.any() to merge the shutdown signal.
- monitor.ts: Create a per-iteration AbortController, pass its signal to
createTelegramBot, and abort it from the SIGTERM handler, force-restart
path, and finally block.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On macOS, launchd sets XPC_SERVICE_NAME on managed processes but does
not set LAUNCH_JOB_LABEL or LAUNCH_JOB_NAME. Without checking
XPC_SERVICE_NAME, isLikelySupervisedProcess() returns false for
launchd-managed gateways, causing restartGatewayProcessWithFreshPid()
to fork a detached child instead of returning "supervised". The
detached child holds the gateway lock while launchd simultaneously
respawns the original process (KeepAlive=true), leading to an infinite
lock-timeout / restart loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>