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:
StingNing
2026-03-02 09:34:18 +08:00
committed by GitHub
parent 949200d7cb
commit ca770622b3
4 changed files with 170 additions and 3 deletions

View File

@@ -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",
);
});
});
});

View File

@@ -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,