mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:31:37 +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.
|
- 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.
|
- 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.
|
- 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/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.
|
- 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();
|
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 () => {
|
it("idle-based new session does NOT preserve overrides (no entry to read)", async () => {
|
||||||
const storePath = await createStorePath("openclaw-idle-no-preserve-");
|
const storePath = await createStorePath("openclaw-idle-no-preserve-");
|
||||||
const sessionKey = "agent:main:telegram:dm:new-user";
|
const sessionKey = "agent:main:telegram:dm:new-user";
|
||||||
|
|||||||
@@ -328,7 +328,6 @@ export async function initSessionState(params: {
|
|||||||
sessionStore[retiredLegacyMainDelivery.key] = retiredLegacyMainDelivery.entry;
|
sessionStore[retiredLegacyMainDelivery.key] = retiredLegacyMainDelivery.entry;
|
||||||
}
|
}
|
||||||
const entry = sessionStore[sessionKey];
|
const entry = sessionStore[sessionKey];
|
||||||
const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const isThread = resolveThreadFlag({
|
const isThread = resolveThreadFlag({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@@ -354,6 +353,11 @@ export async function initSessionState(params: {
|
|||||||
const freshEntry = entry
|
const freshEntry = entry
|
||||||
? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy }).fresh
|
? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy }).fresh
|
||||||
: false;
|
: 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) {
|
if (!isNewSession && freshEntry) {
|
||||||
sessionId = entry.sessionId;
|
sessionId = entry.sessionId;
|
||||||
|
|||||||
Reference in New Issue
Block a user