diff --git a/CHANGELOG.md b/CHANGELOG.md index a28745bb73c..7f61d985116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Infra/net: fix multipart FormData fields (including `model`) being silently dropped when a guarded runtime fetch body crosses a FormData implementation boundary, restoring OpenAI audio transcription requests that failed with HTTP 400. (#64349) Thanks @petr-sloup. - Plugins/memory: restore cached memory capability public artifacts on plugin-registry cache hits so memory-backed artifact surfaces stay visible after warm loads. Thanks @sercada and @vincentkoc. - Gateway/cron: preserve requested isolated-agent config across runtime reloads so subagent jobs and heartbeat overrides keep the right workspace and heartbeat settings when the hot-loaded snapshot is stale. Thanks @l0cka and @vincentkoc. +- Cron/isolated sessions: persist the right transcript path for each isolated run, including fresh session rollovers, so cron runs stop appending to stale session files. Thanks @samrusani and @vincentkoc. ## 2026.4.11 diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index 9c84e79c351..172aa2f2d01 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -70,6 +70,7 @@ export function createCronPromptExecutor(params: { const sessionFile = params.cronSession.sessionEntry.sessionFile?.trim() || resolveSessionTranscriptPath(params.cronSession.sessionEntry.sessionId, params.agentId); + // Fallback for callers that bypass prepareCronRunContext before persisting retries. if (!params.cronSession.sessionEntry.sessionFile?.trim()) { params.cronSession.sessionEntry.sessionFile = sessionFile; } diff --git a/src/cron/isolated-agent/session.test.ts b/src/cron/isolated-agent/session.test.ts index f52c0bd3923..8b050ce53b4 100644 --- a/src/cron/isolated-agent/session.test.ts +++ b/src/cron/isolated-agent/session.test.ts @@ -167,6 +167,24 @@ describe("resolveCronSession", () => { expect(clearBootstrapSnapshot).toHaveBeenCalledWith("webhook:stable-key"); }); + it("clears stale sessionFile when forceNew rolls to a fresh session", () => { + const result = resolveWithStoredEntry({ + entry: { + sessionId: "existing-session-id-456", + updatedAt: NOW_MS - 1000, + sessionFile: "/tmp/stale-session.jsonl", + modelOverride: "sonnet-4", + }, + fresh: true, + forceNew: true, + }); + + expect(result.sessionEntry.sessionId).not.toBe("existing-session-id-456"); + expect(result.isNewSession).toBe(true); + expect(result.sessionEntry.sessionFile).toBeUndefined(); + expect(result.sessionEntry.modelOverride).toBe("sonnet-4"); + }); + it("clears delivery routing metadata and deliveryContext when forceNew is true", () => { const result = resolveWithStoredEntry({ entry: { diff --git a/src/cron/isolated-agent/session.ts b/src/cron/isolated-agent/session.ts index ae20dfdab20..da33148943c 100644 --- a/src/cron/isolated-agent/session.ts +++ b/src/cron/isolated-agent/session.ts @@ -83,6 +83,7 @@ export function resolveCronSession(params: { lastAccountId: undefined, lastThreadId: undefined, deliveryContext: undefined, + sessionFile: undefined, }), }; return { storePath, store, sessionEntry, systemSent, isNewSession };