mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
Fix cron sessionFile persistence for isolated runs (#65203)
Merged via squash.
Prepared head SHA: c45230d2ed
Co-authored-by: samrusani <14844597+samrusani@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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: "" });
|
||||
|
||||
@@ -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<string, { sessionId?: string; label?: string }>;
|
||||
const store = JSON.parse(raw) as Record<
|
||||
string,
|
||||
{ sessionId?: string; label?: string; sessionFile?: string }
|
||||
>;
|
||||
return store[key];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -83,6 +83,7 @@ export function resolveCronSession(params: {
|
||||
lastAccountId: undefined,
|
||||
lastThreadId: undefined,
|
||||
deliveryContext: undefined,
|
||||
sessionFile: undefined,
|
||||
}),
|
||||
};
|
||||
return { storePath, store, sessionEntry, systemSent, isNewSession };
|
||||
|
||||
Reference in New Issue
Block a user