fix(gateway): order bootstrap cache clear after embedded run wait

Landed from #38873 by @MumuTW.

Co-authored-by: MumuTW <clothl47364@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-07 20:42:33 +00:00
parent 3ec81709d7
commit f17f2f918c
3 changed files with 26 additions and 1 deletions

View File

@@ -272,6 +272,7 @@ Docs: https://docs.openclaw.ai
- Agents/OpenAI WS reconnect retry accounting: avoid double retry scheduling when reconnect failures emit both `error` and `close`, so retry budgets track actual reconnect attempts instead of exhausting early. (#39133) Thanks @scoootscooob.
- Daemon/Windows schtasks runtime detection: use locale-invariant `Last Run Result` running codes (`0x41301`/`267009`) as the primary running signal so `openclaw node status` no longer misreports active tasks as stopped on non-English Windows locales. (#39076) Thanks @ademczuk.
- Usage/token count formatting: round near-million token counts to millions (`1.0m`) instead of `1000k`, with explicit boundary coverage for `999_499` and `999_500`. (#39129) Thanks @CurryMessi.
- Gateway/session bootstrap cache invalidation ordering: clear bootstrap snapshots only after active embedded-run shutdown wait completes, preventing dying runs from repopulating stale cache between `/new`/`sessions.reset` turns. (#38873) Thanks @MumuTW.
## 2026.3.2

View File

@@ -207,14 +207,15 @@ async function ensureSessionRuntimeCleanup(params: {
queueKeys.add(params.sessionId);
}
clearSessionQueues([...queueKeys]);
clearBootstrapSnapshot(params.target.canonicalKey);
stopSubagentsForRequester({ cfg: params.cfg, requesterSessionKey: params.target.canonicalKey });
if (!params.sessionId) {
clearBootstrapSnapshot(params.target.canonicalKey);
await closeTrackedBrowserTabs();
return undefined;
}
abortEmbeddedPiRun(params.sessionId);
const ended = await waitForEmbeddedPiRunEnd(params.sessionId, 15_000);
clearBootstrapSnapshot(params.target.canonicalKey);
if (ended) {
await closeTrackedBrowserTabs();
return undefined;

View File

@@ -23,6 +23,10 @@ const sessionCleanupMocks = vi.hoisted(() => ({
stopSubagentsForRequester: vi.fn(() => ({ stopped: 0 })),
}));
const bootstrapCacheMocks = vi.hoisted(() => ({
clearBootstrapSnapshot: vi.fn(),
}));
const sessionHookMocks = vi.hoisted(() => ({
triggerInternalHook: vi.fn(async () => {}),
}));
@@ -68,6 +72,14 @@ vi.mock("../auto-reply/reply/abort.js", async () => {
};
});
vi.mock("../agents/bootstrap-cache.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/bootstrap-cache.js")>();
return {
...actual,
clearBootstrapSnapshot: bootstrapCacheMocks.clearBootstrapSnapshot,
};
});
vi.mock("../hooks/internal-hooks.js", async () => {
const actual = await vi.importActual<typeof import("../hooks/internal-hooks.js")>(
"../hooks/internal-hooks.js",
@@ -204,6 +216,7 @@ describe("gateway server sessions", () => {
beforeEach(() => {
sessionCleanupMocks.clearSessionQueues.mockClear();
sessionCleanupMocks.stopSubagentsForRequester.mockClear();
bootstrapCacheMocks.clearBootstrapSnapshot.mockReset();
sessionHookMocks.triggerInternalHook.mockClear();
subagentLifecycleHookMocks.runSubagentEnded.mockClear();
subagentLifecycleHookState.hasSubagentEndedHook = true;
@@ -926,6 +939,10 @@ describe("gateway server sessions", () => {
test("sessions.reset aborts active runs and clears queues", async () => {
await seedActiveMainSession();
const waitCallCountAtSnapshotClear: number[] = [];
bootstrapCacheMocks.clearBootstrapSnapshot.mockImplementation(() => {
waitCallCountAtSnapshotClear.push(embeddedRunMock.waitCalls.length);
});
embeddedRunMock.activeIds.add("sess-main");
embeddedRunMock.waitResults.set("sess-main", true);
@@ -947,6 +964,7 @@ describe("gateway server sessions", () => {
["main", "agent:main:main", "sess-main"],
"sess-main",
);
expect(waitCallCountAtSnapshotClear).toEqual([1]);
expect(browserSessionTabMocks.closeTrackedBrowserTabsForSessions).toHaveBeenCalledTimes(1);
expect(browserSessionTabMocks.closeTrackedBrowserTabsForSessions).toHaveBeenCalledWith({
sessionKeys: expect.arrayContaining(["main", "agent:main:main", "sess-main"]),
@@ -1163,6 +1181,10 @@ describe("gateway server sessions", () => {
test("sessions.reset returns unavailable when active run does not stop", async () => {
const { dir, storePath } = await seedActiveMainSession();
const waitCallCountAtSnapshotClear: number[] = [];
bootstrapCacheMocks.clearBootstrapSnapshot.mockImplementation(() => {
waitCallCountAtSnapshotClear.push(embeddedRunMock.waitCalls.length);
});
embeddedRunMock.activeIds.add("sess-main");
embeddedRunMock.waitResults.set("sess-main", false);
@@ -1180,6 +1202,7 @@ describe("gateway server sessions", () => {
["main", "agent:main:main", "sess-main"],
"sess-main",
);
expect(waitCallCountAtSnapshotClear).toEqual([1]);
expect(browserSessionTabMocks.closeTrackedBrowserTabsForSessions).not.toHaveBeenCalled();
const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<