mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user