mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(sessions): preserve pinned sessions during maintenance (#122990)
This commit is contained in:
@@ -1660,6 +1660,77 @@ describe("sqlite session normalization", () => {
|
||||
).toEqual(["agent:main:newer", "agent:main:newest"]);
|
||||
});
|
||||
|
||||
it("preserves pinned SQLite entries and transcripts during write-triggered capping", async () => {
|
||||
vi.mocked(getRuntimeConfig).mockReturnValue({
|
||||
session: {
|
||||
maintenance: {
|
||||
mode: "enforce",
|
||||
pruneAfter: "365d",
|
||||
maxEntries: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
const env = { ...process.env, OPENCLAW_STATE_DIR: paths.stateDir };
|
||||
const scopeFor = (sessionKey: string) => ({
|
||||
agentId: "main",
|
||||
env,
|
||||
sessionKey,
|
||||
storePath: paths.sqlitePath,
|
||||
});
|
||||
const pinnedKey = "agent:main:pinned-dashboard";
|
||||
const pinnedSessionId = "pinned-dashboard-session";
|
||||
const pinnedTranscriptEvent = {
|
||||
id: "pinned-event",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: "metadata",
|
||||
};
|
||||
|
||||
await patchSessionEntryCore(
|
||||
scopeFor(pinnedKey),
|
||||
() => ({ sessionId: pinnedSessionId, updatedAt: 1, pinnedAt: 2 }),
|
||||
{
|
||||
fallbackEntry: { sessionId: pinnedSessionId, updatedAt: 1, pinnedAt: 2 },
|
||||
replaceEntry: true,
|
||||
skipMaintenance: true,
|
||||
},
|
||||
);
|
||||
await appendTranscriptEvent(
|
||||
{ ...scopeFor(pinnedKey), sessionId: pinnedSessionId },
|
||||
pinnedTranscriptEvent,
|
||||
);
|
||||
await patchSessionEntryCore(
|
||||
scopeFor("agent:main:recent-dashboard"),
|
||||
() => ({ sessionId: "recent-dashboard-session", updatedAt: 3 }),
|
||||
{
|
||||
fallbackEntry: { sessionId: "recent-dashboard-session", updatedAt: 3 },
|
||||
replaceEntry: true,
|
||||
skipMaintenance: true,
|
||||
},
|
||||
);
|
||||
|
||||
await patchSessionEntryCore(
|
||||
scopeFor("agent:main:maintenance-trigger"),
|
||||
() => ({ sessionId: "maintenance-trigger-session", updatedAt: 4 }),
|
||||
{
|
||||
fallbackEntry: { sessionId: "maintenance-trigger-session", updatedAt: 4 },
|
||||
replaceEntry: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(loadSessionEntry(scopeFor(pinnedKey))).toMatchObject({
|
||||
pinnedAt: 2,
|
||||
sessionId: pinnedSessionId,
|
||||
});
|
||||
await expect(
|
||||
loadTranscriptEvents({
|
||||
agentId: "main",
|
||||
env,
|
||||
sessionId: pinnedSessionId,
|
||||
storePath: paths.sqlitePath,
|
||||
}),
|
||||
).resolves.toEqual([pinnedTranscriptEvent]);
|
||||
});
|
||||
|
||||
it("preserves an admitted SQLite session when another session triggers maintenance", async () => {
|
||||
vi.mocked(getRuntimeConfig).mockReturnValue({
|
||||
session: {
|
||||
|
||||
@@ -428,8 +428,8 @@ export function shouldPreserveMaintenanceEntry(params: {
|
||||
entry: SessionEntry | undefined;
|
||||
preserveKeys?: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
// Archived sessions are user-shelved; only an explicit sessions.delete may remove them.
|
||||
if (params.entry?.archivedAt !== undefined) {
|
||||
// Archived and pinned sessions are user-retained; only an explicit user action may release them.
|
||||
if (params.entry?.archivedAt !== undefined || params.entry?.pinnedAt !== undefined) {
|
||||
return true;
|
||||
}
|
||||
// A model lock is durable harness ownership, not merely a UI restriction.
|
||||
|
||||
@@ -142,6 +142,20 @@ describe("pruneStaleEntries", () => {
|
||||
expect(pruneStaleEntries(store, 30 * DAY_MS)).toBe(1);
|
||||
expect(store.archived).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves pinned entries until they are unpinned", () => {
|
||||
const now = Date.now();
|
||||
const store = makeStore([
|
||||
["pinned", { ...makeEntry(now - 31 * DAY_MS), pinnedAt: now - DAY_MS }],
|
||||
]);
|
||||
|
||||
expect(pruneStaleEntries(store, 30 * DAY_MS)).toBe(0);
|
||||
expect(store).toHaveProperty("pinned");
|
||||
|
||||
delete store.pinned?.pinnedAt;
|
||||
expect(pruneStaleEntries(store, 30 * DAY_MS)).toBe(1);
|
||||
expect(store.pinned).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveQuotaSuspensionEntryMaintenance", () => {
|
||||
@@ -710,6 +724,20 @@ describe("capEntryCount", () => {
|
||||
expect(store.old).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves pinned sessions when capping", () => {
|
||||
const now = Date.now();
|
||||
const store = makeStore([
|
||||
["pinned", { ...makeEntry(now - 10 * DAY_MS), pinnedAt: now - 5 * DAY_MS }],
|
||||
["recent", makeEntry(now)],
|
||||
["old", makeEntry(now - DAY_MS)],
|
||||
]);
|
||||
|
||||
expect(capEntryCount(store, 2)).toBe(1);
|
||||
expect(store).toHaveProperty("pinned");
|
||||
expect(store).toHaveProperty("recent");
|
||||
expect(store.old).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves runtime-provided pending subagent sessions when capping", () => {
|
||||
const now = Date.now();
|
||||
const childKey = "agent:main:subagent:child";
|
||||
|
||||
Reference in New Issue
Block a user