diff --git a/CHANGELOG.md b/CHANGELOG.md index ee2d18b865c..56087d37cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/shutdown: cancel delayed post-ready maintenance during close and suppress maintenance/cron startup after quick restarts, preventing orphaned background timers. Thanks @vincentkoc. - Agents/generated media: treat attachment-style message tool actions as completed chat sends, preventing duplicate fallback media posts when generated files were already uploaded. - Control UI/sessions: show each session's agent runtime in the Sessions table and allow filtering by runtime labels, matching the Agents panel runtime wording. Thanks @vincentkoc. - Discord/streaming: show live reasoning text in progress drafts instead of a bare `Reasoning` status line. diff --git a/src/gateway/server-import-boundary.test.ts b/src/gateway/server-import-boundary.test.ts index 2da1e5db2df..f3cd2ee56e0 100644 --- a/src/gateway/server-import-boundary.test.ts +++ b/src/gateway/server-import-boundary.test.ts @@ -56,9 +56,24 @@ describe("gateway startup import boundaries", () => { const closeStart = serverImpl.indexOf("close: async (opts)"); const hookStart = serverImpl.indexOf("runGlobalGatewayStopSafely", closeStart); const markStart = serverImpl.indexOf("markClosePreludeStarted();", closeStart); + const markHelperStart = serverImpl.indexOf("const markClosePreludeStarted = () => {"); + const markHelperEnd = serverImpl.indexOf("};", markHelperStart); + const postReadyStart = serverImpl.indexOf("scheduleGatewayPostReadyMaintenance({"); + const postReadyEnd = serverImpl.indexOf("});", postReadyStart); + const postReadyBlock = serverImpl.slice(postReadyStart, postReadyEnd); expect(closeStart).toBeGreaterThan(-1); expect(markStart).toBeGreaterThan(closeStart); expect(markStart).toBeLessThan(hookStart); + expect(markHelperStart).toBeGreaterThan(-1); + expect(serverImpl.slice(markHelperStart, markHelperEnd)).toContain( + "clearPostReadyMaintenanceTimer();", + ); + expect(postReadyStart).toBeGreaterThan(-1); + expect(postReadyBlock).toContain("isClosing: () => closePreludeStarted"); + expect(postReadyBlock).toContain("if (closePreludeStarted)"); + expect(postReadyBlock).toContain( + "shouldStartCron: () => !closePreludeStarted && !gatewayCronStartHandled", + ); }); }); diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index aa13bd722ac..b8c870df5b9 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1524,14 +1524,28 @@ export async function startGatewayServer( onStarted: () => { postReadyMaintenanceTimer = null; }, - startMaintenance: earlyRuntime.startMaintenance, + startMaintenance: async () => { + if (closePreludeStarted) { + return null; + } + return earlyRuntime.startMaintenance(); + }, applyMaintenance: (maintenance) => { + if (closePreludeStarted) { + clearInterval(maintenance.tickInterval); + clearInterval(maintenance.healthInterval); + clearInterval(maintenance.dedupeCleanup); + if (maintenance.mediaCleanup) { + clearInterval(maintenance.mediaCleanup); + } + return; + } runtimeState.tickInterval = maintenance.tickInterval; runtimeState.healthInterval = maintenance.healthInterval; runtimeState.dedupeCleanup = maintenance.dedupeCleanup; runtimeState.mediaCleanup = maintenance.mediaCleanup; }, - shouldStartCron: () => !gatewayCronStartHandled, + shouldStartCron: () => !closePreludeStarted && !gatewayCronStartHandled, markCronStartHandled: () => { gatewayCronStartHandled = true; },