mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:39:35 +00:00
fix(gateway): invalidate bootstrap cache on session rollover (openclaw#38535)
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: yfge <1186273+yfge@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -225,6 +225,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- ACP/console silent reply suppression: filter ACP `NO_REPLY` lead fragments and silent-only finals before `openclaw agent` logging/delivery so console-backed ACP sessions no longer leak `NO`/`NO_REPLY` placeholders. (#38436) Thanks @ql-wade.
|
- ACP/console silent reply suppression: filter ACP `NO_REPLY` lead fragments and silent-only finals before `openclaw agent` logging/delivery so console-backed ACP sessions no longer leak `NO`/`NO_REPLY` placeholders. (#38436) Thanks @ql-wade.
|
||||||
- Feishu/reply delivery reliability: disable block streaming in Feishu reply options so plain-text auto-render replies are no longer silently dropped before final delivery. (#38258) Thanks @xinhuagu.
|
- Feishu/reply delivery reliability: disable block streaming in Feishu reply options so plain-text auto-render replies are no longer silently dropped before final delivery. (#38258) Thanks @xinhuagu.
|
||||||
- Agents/reply MEDIA delivery: normalize local assistant `MEDIA:` paths before block/final delivery, keep media dedupe aligned with message-tool sends, and contain malformed media normalization failures so generated files send reliably instead of falling back to empty responses. (#38572) Thanks @obviyus.
|
- Agents/reply MEDIA delivery: normalize local assistant `MEDIA:` paths before block/final delivery, keep media dedupe aligned with message-tool sends, and contain malformed media normalization failures so generated files send reliably instead of falling back to empty responses. (#38572) Thanks @obviyus.
|
||||||
|
- Sessions/bootstrap cache rollover invalidation: clear cached workspace bootstrap snapshots whenever an existing `sessionKey` rolls to a new `sessionId` across auto-reply, command, and isolated cron session resolvers, so `AGENTS.md`/`MEMORY.md`/`USER.md` updates are reloaded after daily, idle, or forced session resets instead of staying stale until gateway restart. (#38494) Thanks @LivingInDrm.
|
||||||
|
|
||||||
## 2026.3.2
|
## 2026.3.2
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ export function clearBootstrapSnapshot(sessionKey: string): void {
|
|||||||
cache.delete(sessionKey);
|
cache.delete(sessionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearBootstrapSnapshotOnSessionRollover(params: {
|
||||||
|
sessionKey?: string;
|
||||||
|
previousSessionId?: string;
|
||||||
|
}): void {
|
||||||
|
if (!params.sessionKey || !params.previousSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBootstrapSnapshot(params.sessionKey);
|
||||||
|
}
|
||||||
|
|
||||||
export function clearAllBootstrapSnapshots(): void {
|
export function clearAllBootstrapSnapshots(): void {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import * as bootstrapCache from "../../agents/bootstrap-cache.js";
|
||||||
import { buildModelAliasIndex } from "../../agents/model-selection.js";
|
import { buildModelAliasIndex } from "../../agents/model-selection.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import type { SessionEntry } from "../../config/sessions.js";
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
@@ -850,11 +851,18 @@ describe("initSessionState RawBody", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("initSessionState reset policy", () => {
|
describe("initSessionState reset policy", () => {
|
||||||
|
let clearBootstrapSnapshotOnSessionRolloverSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
clearBootstrapSnapshotOnSessionRolloverSpy = vi.spyOn(
|
||||||
|
bootstrapCache,
|
||||||
|
"clearBootstrapSnapshotOnSessionRollover",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
clearBootstrapSnapshotOnSessionRolloverSpy.mockRestore();
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -881,6 +889,10 @@ describe("initSessionState reset policy", () => {
|
|||||||
|
|
||||||
expect(result.isNewSession).toBe(true);
|
expect(result.isNewSession).toBe(true);
|
||||||
expect(result.sessionId).not.toBe(existingSessionId);
|
expect(result.sessionId).not.toBe(existingSessionId);
|
||||||
|
expect(clearBootstrapSnapshotOnSessionRolloverSpy).toHaveBeenCalledWith({
|
||||||
|
sessionKey,
|
||||||
|
previousSessionId: existingSessionId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats sessions as stale before the daily reset when updated before yesterday's boundary", async () => {
|
it("treats sessions as stale before the daily reset when updated before yesterday's boundary", async () => {
|
||||||
@@ -1057,6 +1069,10 @@ describe("initSessionState reset policy", () => {
|
|||||||
|
|
||||||
expect(result.isNewSession).toBe(false);
|
expect(result.isNewSession).toBe(false);
|
||||||
expect(result.sessionId).toBe(existingSessionId);
|
expect(result.sessionId).toBe(existingSessionId);
|
||||||
|
expect(clearBootstrapSnapshotOnSessionRolloverSpy).toHaveBeenCalledWith({
|
||||||
|
sessionKey,
|
||||||
|
previousSessionId: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
parseTelegramChatIdFromTarget,
|
parseTelegramChatIdFromTarget,
|
||||||
} from "../../acp/conversation-id.js";
|
} from "../../acp/conversation-id.js";
|
||||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||||
|
import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js";
|
||||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -358,6 +359,10 @@ export async function initSessionState(params: {
|
|||||||
// and for scheduled/daily resets where the session has become stale (!freshEntry).
|
// and for scheduled/daily resets where the session has become stale (!freshEntry).
|
||||||
// Without this, daily-reset transcripts are left as orphaned files on disk (#35481).
|
// Without this, daily-reset transcripts are left as orphaned files on disk (#35481).
|
||||||
const previousSessionEntry = (resetTriggered || !freshEntry) && entry ? { ...entry } : undefined;
|
const previousSessionEntry = (resetTriggered || !freshEntry) && entry ? { ...entry } : undefined;
|
||||||
|
clearBootstrapSnapshotOnSessionRollover({
|
||||||
|
sessionKey,
|
||||||
|
previousSessionId: previousSessionEntry?.sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isNewSession && freshEntry) {
|
if (!isNewSession && freshEntry) {
|
||||||
sessionId = entry.sessionId;
|
sessionId = entry.sessionId;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { listAgentIds } from "../../agents/agent-scope.js";
|
import { listAgentIds } from "../../agents/agent-scope.js";
|
||||||
|
import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js";
|
||||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||||
import {
|
import {
|
||||||
normalizeThinkLevel,
|
normalizeThinkLevel,
|
||||||
@@ -144,6 +145,11 @@ export function resolveSession(opts: {
|
|||||||
opts.sessionId?.trim() || (fresh ? sessionEntry?.sessionId : undefined) || crypto.randomUUID();
|
opts.sessionId?.trim() || (fresh ? sessionEntry?.sessionId : undefined) || crypto.randomUUID();
|
||||||
const isNewSession = !fresh && !opts.sessionId;
|
const isNewSession = !fresh && !opts.sessionId;
|
||||||
|
|
||||||
|
clearBootstrapSnapshotOnSessionRollover({
|
||||||
|
sessionKey,
|
||||||
|
previousSessionId: isNewSession ? sessionEntry?.sessionId : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const persistedThinking =
|
const persistedThinking =
|
||||||
fresh && sessionEntry?.thinkingLevel
|
fresh && sessionEntry?.thinkingLevel
|
||||||
? normalizeThinkLevel(sessionEntry.thinkingLevel)
|
? normalizeThinkLevel(sessionEntry.thinkingLevel)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
|
||||||
vi.mock("../../config/sessions.js", () => ({
|
vi.mock("../../config/sessions.js", () => ({
|
||||||
@@ -8,6 +8,16 @@ vi.mock("../../config/sessions.js", () => ({
|
|||||||
resolveSessionResetPolicy: vi.fn().mockReturnValue({ mode: "idle", idleMinutes: 60 }),
|
resolveSessionResetPolicy: vi.fn().mockReturnValue({ mode: "idle", idleMinutes: 60 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/bootstrap-cache.js", () => ({
|
||||||
|
clearBootstrapSnapshot: vi.fn(),
|
||||||
|
clearBootstrapSnapshotOnSessionRollover: vi.fn(({ sessionKey, previousSessionId }) => {
|
||||||
|
if (sessionKey && previousSessionId) {
|
||||||
|
clearBootstrapSnapshot(sessionKey);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { clearBootstrapSnapshot } from "../../agents/bootstrap-cache.js";
|
||||||
import { loadSessionStore, evaluateSessionFreshness } from "../../config/sessions.js";
|
import { loadSessionStore, evaluateSessionFreshness } from "../../config/sessions.js";
|
||||||
import { resolveCronSession } from "./session.js";
|
import { resolveCronSession } from "./session.js";
|
||||||
|
|
||||||
@@ -40,6 +50,10 @@ function resolveWithStoredEntry(params?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("resolveCronSession", () => {
|
describe("resolveCronSession", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(clearBootstrapSnapshot).mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves modelOverride and providerOverride from existing session entry", () => {
|
it("preserves modelOverride and providerOverride from existing session entry", () => {
|
||||||
const result = resolveWithStoredEntry({
|
const result = resolveWithStoredEntry({
|
||||||
sessionKey: "agent:main:cron:test-job",
|
sessionKey: "agent:main:cron:test-job",
|
||||||
@@ -100,6 +114,7 @@ describe("resolveCronSession", () => {
|
|||||||
expect(result.sessionEntry.sessionId).toBe("existing-session-id-123");
|
expect(result.sessionEntry.sessionId).toBe("existing-session-id-123");
|
||||||
expect(result.isNewSession).toBe(false);
|
expect(result.isNewSession).toBe(false);
|
||||||
expect(result.systemSent).toBe(true);
|
expect(result.systemSent).toBe(true);
|
||||||
|
expect(clearBootstrapSnapshot).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates new sessionId when session is stale", () => {
|
it("creates new sessionId when session is stale", () => {
|
||||||
@@ -121,6 +136,7 @@ describe("resolveCronSession", () => {
|
|||||||
expect(result.sessionEntry.modelOverride).toBe("gpt-4.1-mini");
|
expect(result.sessionEntry.modelOverride).toBe("gpt-4.1-mini");
|
||||||
expect(result.sessionEntry.providerOverride).toBe("openai");
|
expect(result.sessionEntry.providerOverride).toBe("openai");
|
||||||
expect(result.sessionEntry.sendPolicy).toBe("allow");
|
expect(result.sessionEntry.sendPolicy).toBe("allow");
|
||||||
|
expect(clearBootstrapSnapshot).toHaveBeenCalledWith("webhook:stable-key");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates new sessionId when forceNew is true", () => {
|
it("creates new sessionId when forceNew is true", () => {
|
||||||
@@ -141,6 +157,7 @@ describe("resolveCronSession", () => {
|
|||||||
expect(result.systemSent).toBe(false);
|
expect(result.systemSent).toBe(false);
|
||||||
expect(result.sessionEntry.modelOverride).toBe("sonnet-4");
|
expect(result.sessionEntry.modelOverride).toBe("sonnet-4");
|
||||||
expect(result.sessionEntry.providerOverride).toBe("anthropic");
|
expect(result.sessionEntry.providerOverride).toBe("anthropic");
|
||||||
|
expect(clearBootstrapSnapshot).toHaveBeenCalledWith("webhook:stable-key");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears delivery routing metadata and deliveryContext when forceNew is true", () => {
|
it("clears delivery routing metadata and deliveryContext when forceNew is true", () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
evaluateSessionFreshness,
|
evaluateSessionFreshness,
|
||||||
@@ -58,6 +59,11 @@ export function resolveCronSession(params: {
|
|||||||
systemSent = false;
|
systemSent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearBootstrapSnapshotOnSessionRollover({
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
previousSessionId: isNewSession ? entry?.sessionId : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const sessionEntry: SessionEntry = {
|
const sessionEntry: SessionEntry = {
|
||||||
// Preserve existing per-session overrides even when rolling to a new sessionId.
|
// Preserve existing per-session overrides even when rolling to a new sessionId.
|
||||||
...entry,
|
...entry,
|
||||||
|
|||||||
Reference in New Issue
Block a user