Skip to content

Commit 3d58565

Browse files
HCLclaude
andcommitted
fix: address clawsweeper P2+P3 on #77385
P2: add timeout and unknown recovery tests to prove cleared binding + fresh retry behavior, matching the existing session_expired test pattern. Also tighten comment in attempt-execution.ts to clarify the retry logic. P3: fix changelog to reference #77385 (this PR) and #77089 (bug), not the closed unmerged #77141. 19/19 tests pass. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 21076eb commit 3d58565

3 files changed

Lines changed: 88 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Docs: https://docs.openclaw.ai
1717
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.
1818
- Google Meet: preserve `realtime.introMessage: ""` so realtime Chrome joins can stay silent instead of restoring the default spoken intro. Thanks @vincentkoc.
1919
- OpenAI/Codex media: advertise Codex audio transcription in runtime and manifest metadata and route active Codex chat models to the OpenAI transcription default instead of sending chat model ids to audio transcription. Thanks @vincentkoc.
20-
- Agents/failover: clear the stale CLI session binding in the session store for any `FailoverError` (not just recoverable session-expiry/timeout/unknown reasons), so non-recoverable errors from auth or billing cannot leave a dead session ID that the next request would try to resume. Recoverable reasons still retry with a fresh session; non-recoverable reasons rethrow immediately after clearing. (#77141)
20+
- Agents/failover: clear the stale CLI session binding in the session store for any `FailoverError` (not just recoverable session-expiry/timeout/unknown reasons), so non-recoverable errors from auth or billing cannot leave a dead session ID that the next request would try to resume. Recoverable reasons still retry with a fresh session; non-recoverable reasons rethrow immediately after clearing. Fixes #77089. (#77385)
2121
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
2222
- Cron CLI: add `openclaw cron list --agent <id>`, normalize the requested agent id, and include jobs without a stored agent id under the configured default agent while keeping `cron list` unfiltered when no agent is supplied. Fixes #77118. Thanks @zhanggttry.
2323
- Status: show compact Gateway process uptime and host system uptime in `/status`, making restart and host-lifetime checks visible from chat. Thanks @vincentkoc.

src/agents/command/attempt-execution.cli.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,88 @@ describe("CLI attempt execution", () => {
289289
expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
290290
});
291291

292+
it.each([
293+
{ reason: "timeout" as const, status: 504 },
294+
{ reason: "unknown" as const, status: 500 },
295+
])(
296+
"clears stale CLI session and retries fresh after $reason FailoverError (#77089)",
297+
async ({ reason, status }) => {
298+
const sessionKey = `agent:main:subagent:cli-${reason}-fail`;
299+
const homeDir = path.join(tmpDir, "home");
300+
const projectsDir = path.join(homeDir, ".claude", "projects", "demo-workspace");
301+
process.env.HOME = homeDir;
302+
await fs.mkdir(projectsDir, { recursive: true });
303+
await fs.writeFile(
304+
path.join(projectsDir, `stale-cli-session-${reason}.jsonl`),
305+
`${JSON.stringify({
306+
type: "assistant",
307+
message: { role: "assistant", content: [{ type: "text", text: "old reply" }] },
308+
})}\n`,
309+
"utf-8",
310+
);
311+
const sessionEntry: SessionEntry = {
312+
sessionId: `session-cli-${reason}-789`,
313+
updatedAt: Date.now(),
314+
cliSessionIds: { "claude-cli": `stale-cli-session-${reason}` },
315+
};
316+
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
317+
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
318+
319+
runCliAgentMock
320+
.mockRejectedValueOnce(
321+
new FailoverError(`${reason} error`, {
322+
reason,
323+
provider: "claude-cli",
324+
model: "opus",
325+
status,
326+
}),
327+
)
328+
.mockResolvedValueOnce(makeCliResult(`recovered from ${reason}`));
329+
330+
await runAgentAttempt({
331+
providerOverride: "claude-cli",
332+
originalProvider: "claude-cli",
333+
modelOverride: "opus",
334+
cfg: {} as OpenClawConfig,
335+
sessionEntry,
336+
sessionId: sessionEntry.sessionId,
337+
sessionKey,
338+
sessionAgentId: "main",
339+
sessionFile: path.join(tmpDir, "session.jsonl"),
340+
workspaceDir: tmpDir,
341+
body: `${reason} retry test`,
342+
isFallbackRetry: false,
343+
resolvedThinkLevel: "medium",
344+
timeoutMs: 1_000,
345+
runId: `run-cli-${reason}`,
346+
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
347+
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
348+
spawnedBy: undefined,
349+
messageChannel: undefined,
350+
skillsSnapshot: undefined,
351+
resolvedVerboseLevel: undefined,
352+
agentDir: tmpDir,
353+
onAgentEvent: vi.fn(),
354+
authProfileProvider: "claude-cli",
355+
sessionStore,
356+
storePath,
357+
sessionHasHistory: false,
358+
});
359+
360+
// Two calls: stale session first, then fresh retry
361+
expect(runCliAgentMock).toHaveBeenCalledTimes(2);
362+
expect(runCliAgentMock.mock.calls[0]?.[0]?.cliSessionId).toBe(`stale-cli-session-${reason}`);
363+
expect(runCliAgentMock.mock.calls[1]?.[0]?.cliSessionId).toBeUndefined();
364+
// Binding cleared in store
365+
expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
366+
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
367+
string,
368+
SessionEntry
369+
>;
370+
expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
371+
},
372+
);
373+
292374
it("does not pass --resume when the stored Claude CLI transcript is missing", async () => {
293375
const sessionKey = "agent:main:direct:claude-missing-transcript";
294376
const homeDir = path.join(tmpDir, "home");

src/agents/command/attempt-execution.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -515,10 +515,11 @@ export function runAgentAttempt(params: {
515515
params.sessionStore &&
516516
params.storePath
517517
) {
518-
// Clear stale binding for any FailoverError so the next turn cannot
519-
// resume a dead CLI session (#77089). Retry fresh only for recoverable
520-
// reasons; for non-recoverable reasons (auth, billing, rate_limit) clear
521-
// and rethrow so the caller can surface the real error.
518+
// Clear stale binding for any FailoverError (#77089) so the next turn
519+
// cannot resume a dead CLI session. For recoverable reasons
520+
// (session_expired, timeout, unknown) retry once with a fresh session;
521+
// for non-recoverable reasons (auth, billing, rate_limit) clear and
522+
// rethrow so the caller can surface the real error without retrying.
522523
log.warn(
523524
`CLI session failed (reason=${err.reason}), clearing from session store: provider=${sanitizeForLog(cliExecutionProvider)} sessionKey=${params.sessionKey}`,
524525
);

0 commit comments

Comments
 (0)