Telegram: stop bot on polling teardown

This commit is contained in:
liuxiaopai-ai
2026-03-02 09:30:26 +08:00
committed by Peter Steinberger
parent 666a4763ee
commit 042d06a19b
3 changed files with 33 additions and 1 deletions

View File

@@ -122,6 +122,7 @@ Docs: https://docs.openclaw.ai
- Security/Subagents sandbox inheritance: block sandboxed sessions from spawning cross-agent subagents that would run unsandboxed, preventing runtime sandbox downgrade via `sessions_spawn agentId`. Thanks @tdjackey for reporting.
- Security/Workspace safe writes: harden `writeFileWithinRoot` against symlink-retarget TOCTOU races by opening existing files without truncation, creating missing files with exclusive create, deferring truncation until post-open identity+boundary validation, and removing out-of-root create artifacts on blocked races; added regression tests for truncate/create race paths. This ships in the next npm release (`2026.3.1`). Thanks @tdjackey for reporting.
- Control UI/Cron editor: include `{ mode: "none" }` in `cron.update` patches when editing an existing job and selecting “Result delivery = None (internal)”, so saved jobs no longer keep stale announce delivery mode. Fixes #31075.
- Telegram/Restart polling teardown: stop the Telegram bot instance when a polling cycle exits so in-process SIGUSR1 restarts fully tear down old long-poll loops before restart, reducing post-restart `getUpdates` 409 conflict storms. Fixes #31107.
- Security/Node metadata policy: harden node platform classification against Unicode confusables and switch unknown platform defaults to a conservative allowlist that excludes `system.run`/`system.which` unless explicitly allowlisted, preventing metadata canonicalization drift from broadening node command permissions. Thanks @tdjackey for reporting.
- Plugins/Discovery precedence: load bundled plugins before auto-discovered global extensions so bundled channel plugins win duplicate-ID resolution by default (explicit `plugins.load.paths` overrides remain highest precedence), with loader regression coverage. Landed from contributor PR #29710 by @Sid-Qin. Thanks @Sid-Qin.
- Discord/Reconnect integrity: release Discord message listener lane immediately while preserving serialized handler execution, add HELLO-stall resume-first recovery with bounded fresh-identify fallback after repeated stalls, and extend lifecycle/listener regression coverage for forced reconnect scenarios. Landed from contributor PR #29508 by @cgdusek. Thanks @cgdusek.

View File

@@ -59,6 +59,10 @@ const { createTelegramBotErrors } = vi.hoisted(() => ({
createTelegramBotErrors: [] as unknown[],
}));
const { createdBotStops } = vi.hoisted(() => ({
createdBotStops: [] as Array<ReturnType<typeof vi.fn<() => void>>>,
}));
const { computeBackoff, sleepWithAbort } = vi.hoisted(() => ({
computeBackoff: vi.fn(() => 0),
sleepWithAbort: vi.fn(async () => undefined),
@@ -111,6 +115,8 @@ vi.mock("./bot.js", () => ({
if (nextError) {
throw nextError;
}
const stop = vi.fn<() => void>();
createdBotStops.push(stop);
handlers.message = async (ctx: MockCtx) => {
const chatId = ctx.message.chat.id;
const isGroup = ctx.message.chat.type !== "private";
@@ -128,7 +134,7 @@ vi.mock("./bot.js", () => ({
api,
me: { username: "mybot" },
init: initSpy,
stop: vi.fn(),
stop,
start: vi.fn(),
};
},
@@ -179,6 +185,7 @@ describe("monitorTelegramProvider (grammY)", () => {
registerUnhandledRejectionHandlerMock.mockClear();
resetUnhandledRejection();
createTelegramBotErrors.length = 0;
createdBotStops.length = 0;
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
});
@@ -382,6 +389,22 @@ describe("monitorTelegramProvider (grammY)", () => {
expect(runSpy).toHaveBeenCalledTimes(2);
});
it("stops bot instance when polling cycle exits", async () => {
const abort = new AbortController();
runSpy.mockImplementationOnce(() =>
makeRunnerStub({
task: async () => {
abort.abort();
},
}),
);
await monitorTelegramProvider({ token: "tok", abortSignal: abort.signal });
expect(createdBotStops.length).toBe(1);
expect(createdBotStops[0]).toHaveBeenCalledTimes(1);
});
it("surfaces non-recoverable errors", async () => {
runSpy.mockImplementationOnce(() =>
makeRunnerStub({

View File

@@ -270,6 +270,13 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
});
return stopPromise;
};
const stopBot = () => {
return Promise.resolve(bot.stop())
.then(() => undefined)
.catch(() => {
// Bot may already be stopped by runner stop/abort paths.
});
};
const stopOnAbort = () => {
if (opts.abortSignal?.aborted) {
void stopRunner();
@@ -309,6 +316,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
} finally {
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
await stopRunner();
await stopBot();
}
};