mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 18:31:10 +00:00
Cron: fix 1/3 timeout on fresh isolated CLI runs (openclaw#30140) thanks @ningding97
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: ningding97 <17723822+ningding97@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -95,12 +95,14 @@ vi.mock("../../agents/subagent-announce.js", () => ({
|
||||
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
const runCliAgentMock = vi.fn();
|
||||
vi.mock("../../agents/cli-runner.js", () => ({
|
||||
runCliAgent: vi.fn(),
|
||||
runCliAgent: runCliAgentMock,
|
||||
}));
|
||||
|
||||
const getCliSessionIdMock = vi.fn().mockReturnValue(undefined);
|
||||
vi.mock("../../agents/cli-session.js", () => ({
|
||||
getCliSessionId: vi.fn().mockReturnValue(undefined),
|
||||
getCliSessionId: getCliSessionIdMock,
|
||||
setCliSessionId: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -494,4 +496,82 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
|
||||
expect(runWithModelFallbackMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI session handoff (issue #29774)", () => {
|
||||
it("does not pass stored cliSessionId on fresh isolated runs (isNewSession=true)", async () => {
|
||||
// Simulate a persisted CLI session ID from a previous run.
|
||||
getCliSessionIdMock.mockReturnValue("prev-cli-session-abc");
|
||||
isCliProviderMock.mockReturnValue(true);
|
||||
runCliAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: "output" }],
|
||||
meta: { agentMeta: { sessionId: "new-cli-session-xyz", usage: { input: 5, output: 10 } } },
|
||||
});
|
||||
// Make runWithModelFallback invoke the run callback so the CLI path executes.
|
||||
runWithModelFallbackMock.mockImplementationOnce(
|
||||
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => {
|
||||
const result = await params.run("claude-cli", "claude-opus-4-6");
|
||||
return { result, provider: "claude-cli", model: "claude-opus-4-6", attempts: [] };
|
||||
},
|
||||
);
|
||||
resolveCronSessionMock.mockReturnValue({
|
||||
storePath: "/tmp/store.json",
|
||||
store: {},
|
||||
sessionEntry: {
|
||||
sessionId: "test-session-fresh",
|
||||
updatedAt: 0,
|
||||
systemSent: false,
|
||||
skillsSnapshot: undefined,
|
||||
// A stored CLI session ID that should NOT be reused on fresh runs.
|
||||
cliSessionIds: { "claude-cli": "prev-cli-session-abc" },
|
||||
},
|
||||
systemSent: false,
|
||||
isNewSession: true,
|
||||
});
|
||||
|
||||
await runCronIsolatedAgentTurn(makeParams());
|
||||
|
||||
expect(runCliAgentMock).toHaveBeenCalledOnce();
|
||||
// Fresh session: cliSessionId must be undefined, not the stored value.
|
||||
expect(runCliAgentMock.mock.calls[0][0]).toHaveProperty("cliSessionId", undefined);
|
||||
});
|
||||
|
||||
it("reuses stored cliSessionId on continuation runs (isNewSession=false)", async () => {
|
||||
getCliSessionIdMock.mockReturnValue("existing-cli-session-def");
|
||||
isCliProviderMock.mockReturnValue(true);
|
||||
runCliAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: "output" }],
|
||||
meta: {
|
||||
agentMeta: { sessionId: "existing-cli-session-def", usage: { input: 5, output: 10 } },
|
||||
},
|
||||
});
|
||||
runWithModelFallbackMock.mockImplementationOnce(
|
||||
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => {
|
||||
const result = await params.run("claude-cli", "claude-opus-4-6");
|
||||
return { result, provider: "claude-cli", model: "claude-opus-4-6", attempts: [] };
|
||||
},
|
||||
);
|
||||
resolveCronSessionMock.mockReturnValue({
|
||||
storePath: "/tmp/store.json",
|
||||
store: {},
|
||||
sessionEntry: {
|
||||
sessionId: "test-session-continuation",
|
||||
updatedAt: 0,
|
||||
systemSent: false,
|
||||
skillsSnapshot: undefined,
|
||||
cliSessionIds: { "claude-cli": "existing-cli-session-def" },
|
||||
},
|
||||
systemSent: false,
|
||||
isNewSession: false,
|
||||
});
|
||||
|
||||
await runCronIsolatedAgentTurn(makeParams());
|
||||
|
||||
expect(runCliAgentMock).toHaveBeenCalledOnce();
|
||||
// Continuation: cliSessionId should be passed through for session resume.
|
||||
expect(runCliAgentMock.mock.calls[0][0]).toHaveProperty(
|
||||
"cliSessionId",
|
||||
"existing-cli-session-def",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -463,7 +463,14 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
throw new Error(abortReason());
|
||||
}
|
||||
if (isCliProvider(providerOverride, cfgWithAgentDefaults)) {
|
||||
const cliSessionId = getCliSessionId(cronSession.sessionEntry, providerOverride);
|
||||
// Fresh isolated cron sessions must not reuse a stored CLI session ID.
|
||||
// Passing an existing ID activates the resume watchdog profile
|
||||
// (noOutputTimeoutRatio 0.3, maxMs 180 s) instead of the fresh profile
|
||||
// (ratio 0.8, maxMs 600 s), causing jobs to time out at roughly 1/3 of
|
||||
// the configured timeoutSeconds. See: https://github.com/openclaw/openclaw/issues/29774
|
||||
const cliSessionId = cronSession.isNewSession
|
||||
? undefined
|
||||
: getCliSessionId(cronSession.sessionEntry, providerOverride);
|
||||
return runCliAgent({
|
||||
sessionId: cronSession.sessionEntry.sessionId,
|
||||
sessionKey: agentSessionKey,
|
||||
|
||||
Reference in New Issue
Block a user