fix(telegram): keep polling watchdog active for wedged runner

This commit is contained in:
Vincent Koc
2026-04-25 02:18:49 -07:00
committed by GitHub
parent f6a3b42cfa
commit ceace83556
5 changed files with 56 additions and 13 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys.
- Telegram: remove the startup persisted-offset `getUpdates` preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar.
- Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave `getUpdates` silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb.
- Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.
- Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser.
- Browser/Chrome: stop passing redundant `--disable-setuid-sandbox` when `browser.noSandbox` is enabled; `--no-sandbox` remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger.

View File

@@ -31,7 +31,6 @@ describe("TelegramPollingLivenessTracker", () => {
expect(
tracker.detectStall({
thresholdMs: POLL_STALL_THRESHOLD_MS,
runnerIsRunning: true,
}),
).toBeNull();
@@ -45,7 +44,6 @@ describe("TelegramPollingLivenessTracker", () => {
now = 120_001;
const stall = tracker.detectStall({
thresholdMs: POLL_STALL_THRESHOLD_MS,
runnerIsRunning: true,
});
expect(stall?.message).toContain("Polling stall detected (no completed getUpdates");
expect(stall?.message).toContain("inFlight=0 outcome=not-started");
@@ -54,7 +52,6 @@ describe("TelegramPollingLivenessTracker", () => {
expect(
tracker.detectStall({
thresholdMs: POLL_STALL_THRESHOLD_MS,
runnerIsRunning: true,
}),
).toBeNull();
});
@@ -69,7 +66,6 @@ describe("TelegramPollingLivenessTracker", () => {
now = 120_001;
const stall = tracker.detectStall({
thresholdMs: POLL_STALL_THRESHOLD_MS,
runnerIsRunning: true,
});
expect(stall?.message).toContain("active getUpdates stuck");

View File

@@ -89,14 +89,7 @@ export class TelegramPollingLivenessTracker {
this.#inFlightGetUpdates = Math.max(0, this.#inFlightGetUpdates - 1);
}
detectStall(params: {
thresholdMs: number;
runnerIsRunning: boolean;
now?: number;
}): TelegramPollingStall | null {
if (!params.runnerIsRunning) {
return null;
}
detectStall(params: { thresholdMs: number; now?: number }): TelegramPollingStall | null {
const now = params.now ?? this.#now();
const activeElapsed =
this.#inFlightGetUpdates > 0 && this.#lastGetUpdatesStartedAt != null

View File

@@ -387,6 +387,60 @@ describe("TelegramPollingSession", () => {
}
});
it("forces a restart when the runner task is pending but reports not running", async () => {
const abort = new AbortController();
const firstRunnerStop = vi.fn(async () => undefined);
const secondRunnerStop = vi.fn(async () => undefined);
createTelegramBotMock.mockReturnValue(makeBot());
let firstTaskResolve: (() => void) | undefined;
const firstTask = new Promise<void>((resolve) => {
firstTaskResolve = resolve;
});
let cycle = 0;
runMock.mockImplementation(() => {
cycle += 1;
if (cycle === 1) {
return {
task: () => firstTask,
stop: async () => {
await firstRunnerStop();
firstTaskResolve?.();
},
isRunning: () => false,
};
}
return {
task: async () => {
abort.abort();
},
stop: secondRunnerStop,
isRunning: () => false,
};
});
const watchdogHarness = installPollingStallWatchdogHarness();
const log = vi.fn();
const session = createPollingSession({
abortSignal: abort.signal,
log,
});
try {
const runPromise = session.runUntilAbort();
const watchdog = await watchdogHarness.waitForWatchdog();
watchdog?.();
await runPromise;
expect(runMock).toHaveBeenCalledTimes(2);
expect(firstRunnerStop).toHaveBeenCalledTimes(1);
expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected"));
} finally {
watchdogHarness.restore();
}
});
it("honors a custom polling stall threshold", async () => {
const abort = new AbortController();
const botStop = vi.fn(async () => undefined);

View File

@@ -295,7 +295,6 @@ export class TelegramPollingSession {
const stall = liveness.detectStall({
thresholdMs: this.#stallThresholdMs,
runnerIsRunning: runner.isRunning(),
});
if (stall) {
this.#transportState.markDirty();