Skip to content

Commit 709dc67

Browse files
Byungskerjalehman
andauthored
fix(session): archive old transcript on daily/scheduled reset to prevent orphaned files (#35493)
Merged via squash. Prepared head SHA: 0d95549 Co-authored-by: byungsker <72309817+byungsker@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman
1 parent edc386e commit 709dc67

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
2828

2929
- 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.
3030
- 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.
31+
- 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.
3132
- 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.
3233
- 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.
3334
- 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.

src/auto-reply/reply/session.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,61 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
14571457
archiveSpy.mockRestore();
14581458
});
14591459

1460+
it("archives the old session transcript on daily/scheduled reset (stale session)", async () => {
1461+
// Daily resets occur when the session becomes stale (not via /new or /reset command).
1462+
// Previously, previousSessionEntry was only set when resetTriggered=true, leaving
1463+
// old transcript files orphaned on disk. Refs #35481.
1464+
vi.useFakeTimers();
1465+
try {
1466+
// Simulate: it is 5am, session was last active at 3am (before 4am daily boundary)
1467+
vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0));
1468+
const storePath = await createStorePath("openclaw-stale-archive-");
1469+
const sessionKey = "agent:main:telegram:dm:archive-stale-user";
1470+
const existingSessionId = "stale-session-to-be-archived";
1471+
1472+
await writeSessionStoreFast(storePath, {
1473+
[sessionKey]: {
1474+
sessionId: existingSessionId,
1475+
updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(),
1476+
},
1477+
});
1478+
1479+
const sessionUtils = await import("../../gateway/session-utils.fs.js");
1480+
const archiveSpy = vi.spyOn(sessionUtils, "archiveSessionTranscripts");
1481+
1482+
const cfg = { session: { store: storePath } } as OpenClawConfig;
1483+
const result = await initSessionState({
1484+
ctx: {
1485+
Body: "hello",
1486+
RawBody: "hello",
1487+
CommandBody: "hello",
1488+
From: "user-stale",
1489+
To: "bot",
1490+
ChatType: "direct",
1491+
SessionKey: sessionKey,
1492+
Provider: "telegram",
1493+
Surface: "telegram",
1494+
},
1495+
cfg,
1496+
commandAuthorized: true,
1497+
});
1498+
1499+
expect(result.isNewSession).toBe(true);
1500+
expect(result.resetTriggered).toBe(false);
1501+
expect(result.sessionId).not.toBe(existingSessionId);
1502+
expect(archiveSpy).toHaveBeenCalledWith(
1503+
expect.objectContaining({
1504+
sessionId: existingSessionId,
1505+
storePath,
1506+
reason: "reset",
1507+
}),
1508+
);
1509+
archiveSpy.mockRestore();
1510+
} finally {
1511+
vi.useRealTimers();
1512+
}
1513+
});
1514+
14601515
it("idle-based new session does NOT preserve overrides (no entry to read)", async () => {
14611516
const storePath = await createStorePath("openclaw-idle-no-preserve-");
14621517
const sessionKey = "agent:main:telegram:dm:new-user";

src/auto-reply/reply/session.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,6 @@ export async function initSessionState(params: {
328328
sessionStore[retiredLegacyMainDelivery.key] = retiredLegacyMainDelivery.entry;
329329
}
330330
const entry = sessionStore[sessionKey];
331-
const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
332331
const now = Date.now();
333332
const isThread = resolveThreadFlag({
334333
sessionKey,
@@ -354,6 +353,11 @@ export async function initSessionState(params: {
354353
const freshEntry = entry
355354
? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy }).fresh
356355
: false;
356+
// Capture the current session entry before any reset so its transcript can be
357+
// archived afterward. We need to do this for both explicit resets (/new, /reset)
358+
// and for scheduled/daily resets where the session has become stale (!freshEntry).
359+
// Without this, daily-reset transcripts are left as orphaned files on disk (#35481).
360+
const previousSessionEntry = (resetTriggered || !freshEntry) && entry ? { ...entry } : undefined;
357361

358362
if (!isNewSession && freshEntry) {
359363
sessionId = entry.sessionId;

0 commit comments

Comments
 (0)