fix(session): archive old transcript on daily/scheduled reset to prevent orphaned files (#35493)

Merged via squash.

Prepared head SHA: 0d95549d75
Co-authored-by: byungsker <72309817+byungsker@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Byungsker
2026-03-06 04:52:23 +09:00
committed by GitHub
parent edc386e9a5
commit 709dc671e4
3 changed files with 61 additions and 1 deletions

View File

@@ -1457,6 +1457,61 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
archiveSpy.mockRestore();
});
it("archives the old session transcript on daily/scheduled reset (stale session)", async () => {
// Daily resets occur when the session becomes stale (not via /new or /reset command).
// Previously, previousSessionEntry was only set when resetTriggered=true, leaving
// old transcript files orphaned on disk. Refs #35481.
vi.useFakeTimers();
try {
// Simulate: it is 5am, session was last active at 3am (before 4am daily boundary)
vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0));
const storePath = await createStorePath("openclaw-stale-archive-");
const sessionKey = "agent:main:telegram:dm:archive-stale-user";
const existingSessionId = "stale-session-to-be-archived";
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(),
},
});
const sessionUtils = await import("../../gateway/session-utils.fs.js");
const archiveSpy = vi.spyOn(sessionUtils, "archiveSessionTranscripts");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const result = await initSessionState({
ctx: {
Body: "hello",
RawBody: "hello",
CommandBody: "hello",
From: "user-stale",
To: "bot",
ChatType: "direct",
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
},
cfg,
commandAuthorized: true,
});
expect(result.isNewSession).toBe(true);
expect(result.resetTriggered).toBe(false);
expect(result.sessionId).not.toBe(existingSessionId);
expect(archiveSpy).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: existingSessionId,
storePath,
reason: "reset",
}),
);
archiveSpy.mockRestore();
} finally {
vi.useRealTimers();
}
});
it("idle-based new session does NOT preserve overrides (no entry to read)", async () => {
const storePath = await createStorePath("openclaw-idle-no-preserve-");
const sessionKey = "agent:main:telegram:dm:new-user";

View File

@@ -328,7 +328,6 @@ export async function initSessionState(params: {
sessionStore[retiredLegacyMainDelivery.key] = retiredLegacyMainDelivery.entry;
}
const entry = sessionStore[sessionKey];
const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
const now = Date.now();
const isThread = resolveThreadFlag({
sessionKey,
@@ -354,6 +353,11 @@ export async function initSessionState(params: {
const freshEntry = entry
? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy }).fresh
: false;
// Capture the current session entry before any reset so its transcript can be
// archived afterward. We need to do this for both explicit resets (/new, /reset)
// 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).
const previousSessionEntry = (resetTriggered || !freshEntry) && entry ? { ...entry } : undefined;
if (!isNewSession && freshEntry) {
sessionId = entry.sessionId;