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.session-identity.test.ts b/src/cron/isolated-agent.session-identity.test.ts index f2f345378a7..36df45280f4 100644 --- a/src/cron/isolated-agent.session-identity.test.ts +++ b/src/cron/isolated-agent.session-identity.test.ts @@ -15,6 +15,9 @@ import { runCronTurn, withTempHome, } from "./isolated-agent.turn-test-helpers.js"; +import { setupRunCronIsolatedAgentTurnSuite } from "./isolated-agent/run.suite-helpers.js"; + +setupRunCronIsolatedAgentTurnSuite(); describe("runCronIsolatedAgentTurn session identity", () => { beforeEach(() => { @@ -101,6 +104,22 @@ describe("runCronIsolatedAgentTurn session identity", () => { }); }); + it("passes sessionFile to isolated cron runs", async () => { + await withTempHome(async (home) => { + await runCronTurn(home, { + jobPayload: DEFAULT_AGENT_TURN_PAYLOAD, + }); + const call = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] as { + sessionFile?: string; + }; + + expect(call?.sessionFile).toContain( + path.join(home, ".openclaw", "agents", "main", "sessions"), + ); + expect(call?.sessionFile?.endsWith(".jsonl")).toBe(true); + }); + }); + it("starts a fresh session id for each cron run", async () => { await withTempHome(async (home) => { const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" }); diff --git a/src/cron/isolated-agent.turn-test-helpers.ts b/src/cron/isolated-agent.turn-test-helpers.ts index 18de2530ac7..8bdb413f2e2 100644 --- a/src/cron/isolated-agent.turn-test-helpers.ts +++ b/src/cron/isolated-agent.turn-test-helpers.ts @@ -60,7 +60,10 @@ export function expectEmbeddedProviderModel(expected: { provider: string; model: export async function readSessionEntry(storePath: string, key: string) { const raw = await fs.readFile(storePath, "utf-8"); - const store = JSON.parse(raw) as Record; + const store = JSON.parse(raw) as Record< + string, + { sessionId?: string; label?: string; sessionFile?: string } + >; return store[key]; } diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index 625fa73d1a1..172aa2f2d01 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -67,10 +67,13 @@ export function createCronPromptExecutor(params: { abortSignal?: AbortSignal; abortReason: () => string; }) { - const sessionFile = resolveSessionTranscriptPath( - params.cronSession.sessionEntry.sessionId, - params.agentId, - ); + 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; + } const cronFallbacksOverride = resolveCronFallbacksOverride({ cfg: params.cfg, job: params.job, diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 593a9a94b83..bba5adbd9d8 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -18,6 +18,7 @@ import { } from "./helpers.js"; import { resolveCronModelSelection } from "./model-selection.js"; import { buildCronAgentDefaultsConfig } from "./run-config.js"; +import { resolveSessionTranscriptPath } from "./run-execution.runtime.js"; import { executeCronRun, type CronExecutionResult } from "./run-executor.js"; import { createPersistCronSessionEntry, @@ -272,6 +273,9 @@ async function prepareCronRunContext(params: { forceNew: input.job.sessionTarget === "isolated", }); const runSessionId = cronSession.sessionEntry.sessionId; + if (!cronSession.sessionEntry.sessionFile?.trim()) { + cronSession.sessionEntry.sessionFile = resolveSessionTranscriptPath(runSessionId, agentId); + } const runSessionKey = baseSessionKey.startsWith("cron:") ? `${agentSessionKey}:run:${runSessionId}` : agentSessionKey; 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 };