fix(telegram): warn on stale polling status

This commit is contained in:
Peter Steinberger
2026-04-29 12:58:34 +01:00
parent 39f810911c
commit 4f540c703f
4 changed files with 189 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import type {
ChannelAccountSnapshot,
ChannelStatusIssue,
} from "openclaw/plugin-sdk/channel-contract";
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import {
appendMatchMetadata,
asString,
@@ -9,10 +10,19 @@ import {
resolveEnabledConfiguredAccountId,
} from "openclaw/plugin-sdk/status-helpers";
const TELEGRAM_POLLING_CONNECT_GRACE_MS = 120_000;
const TELEGRAM_POLLING_STALE_TRANSPORT_MS = 30 * 60_000;
type TelegramAccountStatus = {
accountId?: unknown;
enabled?: unknown;
configured?: unknown;
running?: unknown;
connected?: unknown;
mode?: unknown;
lastStartAt?: unknown;
lastTransportActivityAt?: unknown;
lastError?: unknown;
allowUnmentionedGroups?: unknown;
audit?: unknown;
};
@@ -38,11 +48,82 @@ function readTelegramAccountStatus(value: ChannelAccountSnapshot): TelegramAccou
accountId: value.accountId,
enabled: value.enabled,
configured: value.configured,
running: value.running,
connected: value.connected,
mode: value.mode,
lastStartAt: value.lastStartAt,
lastTransportActivityAt: value.lastTransportActivityAt,
lastError: value.lastError,
allowUnmentionedGroups: value.allowUnmentionedGroups,
audit: value.audit,
};
}
function asFiniteNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function appendTelegramRuntimeError(message: string, lastError: unknown): string {
const error = asString(lastError);
return error ? `${message}: ${error}` : message;
}
function collectTelegramPollingRuntimeIssues(params: {
account: TelegramAccountStatus;
accountId: string;
issues: ChannelStatusIssue[];
now: number;
}) {
const { account, accountId, issues, now } = params;
if (account.running !== true || asString(account.mode) !== "polling") {
return;
}
const lastStartAt = asFiniteNumber(account.lastStartAt);
const lastTransportActivityAt = asFiniteNumber(account.lastTransportActivityAt);
const fix = `Run: ${formatCliCommand("openclaw channels status --probe")} (or restart the gateway). Check the bot token, proxy/network settings, and logs if it persists.`;
if (account.connected === false) {
const withinStartupGrace =
lastStartAt != null && now - lastStartAt < TELEGRAM_POLLING_CONNECT_GRACE_MS;
if (!withinStartupGrace) {
issues.push({
channel: "telegram",
accountId,
kind: "runtime",
message: appendTelegramRuntimeError(
"Telegram polling is running but has not completed a successful getUpdates call since startup",
account.lastError,
),
fix,
});
}
return;
}
if (account.connected === true && lastTransportActivityAt != null) {
if (lastStartAt != null && lastTransportActivityAt < lastStartAt) {
const lifecycleAgeMs = Math.max(0, now - lastStartAt);
if (lifecycleAgeMs <= TELEGRAM_POLLING_STALE_TRANSPORT_MS) {
return;
}
}
const ageMs = now - lastTransportActivityAt;
if (ageMs > TELEGRAM_POLLING_STALE_TRANSPORT_MS) {
issues.push({
channel: "telegram",
accountId,
kind: "runtime",
message: appendTelegramRuntimeError(
`Telegram polling transport is stale (last successful getUpdates ${Math.max(0, Math.floor(ageMs / 60_000))}m ago)`,
account.lastError,
),
fix,
});
}
}
}
function readTelegramGroupMembershipAuditSummary(
value: unknown,
): TelegramGroupMembershipAuditSummary {
@@ -94,6 +175,13 @@ export function collectTelegramStatusIssues(
continue;
}
collectTelegramPollingRuntimeIssues({
account,
accountId,
issues,
now: Date.now(),
});
if (account.allowUnmentionedGroups === true) {
issues.push({
channel: "telegram",

View File

@@ -73,6 +73,105 @@ describe("collectTelegramStatusIssues", () => {
expect(issues[0]?.message).toContain("channels.telegram.groups");
});
it("reports polling runtime state that never completed getUpdates after startup grace", () => {
const issues = collectTelegramStatusIssues([
{
accountId: "main",
enabled: true,
configured: true,
running: true,
mode: "polling",
connected: false,
lastStartAt: Date.now() - 121_000,
lastError: "network timeout",
} as ChannelAccountSnapshot,
]);
expect(issues).toEqual([
expect.objectContaining({
channel: "telegram",
accountId: "main",
kind: "runtime",
message: expect.stringContaining("has not completed a successful getUpdates call"),
}),
]);
expect(issues[0]?.message).toContain("network timeout");
expect(issues[0]?.fix).toContain("channels status --probe");
});
it("does not report polling startup before the connect grace expires", () => {
const issues = collectTelegramStatusIssues([
{
accountId: "main",
enabled: true,
configured: true,
running: true,
mode: "polling",
connected: false,
lastStartAt: Date.now() - 60_000,
} as ChannelAccountSnapshot,
]);
expect(issues).toEqual([]);
});
it("reports stale polling transport activity after successful getUpdates stops refreshing", () => {
const issues = collectTelegramStatusIssues([
{
accountId: "main",
enabled: true,
configured: true,
running: true,
mode: "polling",
connected: true,
lastStartAt: Date.now() - 60 * 60_000,
lastTransportActivityAt: Date.now() - 31 * 60_000,
} as ChannelAccountSnapshot,
]);
expect(issues).toEqual([
expect.objectContaining({
channel: "telegram",
accountId: "main",
kind: "runtime",
message: expect.stringContaining("polling transport is stale"),
}),
]);
});
it("does not report inherited stale transport activity during a fresh polling lifecycle", () => {
const issues = collectTelegramStatusIssues([
{
accountId: "main",
enabled: true,
configured: true,
running: true,
mode: "polling",
connected: true,
lastStartAt: Date.now() - 60_000,
lastTransportActivityAt: Date.now() - 2 * 60 * 60_000,
} as ChannelAccountSnapshot,
]);
expect(issues).toEqual([]);
});
it("does not report webhook accounts with polling-only runtime diagnostics", () => {
const issues = collectTelegramStatusIssues([
{
accountId: "main",
enabled: true,
configured: true,
running: true,
mode: "webhook",
connected: false,
lastStartAt: Date.now() - 10 * 60_000,
} as ChannelAccountSnapshot,
]);
expect(issues).toEqual([]);
});
it("ignores accounts that are not both enabled and configured", () => {
expect(
collectTelegramStatusIssues([