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

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction safeguard pre-check: skip embedded compaction before entering the Pi SDK when a session has no real conversation messages, avoiding unnecessary LLM API calls on idle sessions. (#36451) thanks @Sid-Qin.
- iMessage/cron completion announces: strip leaked inline reply tags (for example `[[reply_to:6100]]`) from user-visible completion text so announcement deliveries do not expose threading metadata. (#24600) Thanks @vincentkoc.
- Sessions/daily reset transcript archival: archive prior transcript files during stale-session scheduled/daily resets by capturing the previous session entry before rollover, preventing orphaned transcript files on disk. (#35493) Thanks @byungsker.
- Feishu/group slash command detection: normalize group mention wrappers before command-authorization probing so mention-prefixed commands (for example `@Bot/model` and `@Bot /reset`) are recognized as gateway commands instead of being forwarded to the agent. (#35994) Thanks @liuxiaopai-ai.
- Agents/context pruning: guard assistant thinking/text char estimation against malformed blocks (missing `thinking`/`text` strings or null entries) so pruning no longer crashes with malformed provider content. (openclaw#35146) thanks @Sid-Qin.
- Agents/schema cleaning: detect Venice + Grok model IDs as xAI-proxied targets so unsupported JSON Schema keywords are stripped before requests, preventing Venice/Grok `Invalid arguments` failures. (openclaw#35355) thanks @Sid-Qin.

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;