From cbaf858227985e004d7c06e2386e7cf216d58f6a Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 03:28:26 +0000 Subject: [PATCH] fix: retry config snapshot after rejection (#83944) Summary: - This PR clears the cached CLI config snapshot promise when a read rejects, adds a reject-retry-cache regression test, and adds an Unreleased changelog entry. - Reproducibility: yes. Current main clearly caches the first snapshot-read promise, and the source PR supplied a focused reject, recover, cached-success probe; I did not rerun it in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: fix: retry config snapshot after rejection Validation: - ClawSweeper review passed for head a46b5ec5c7b07a88f4970e2f5ed86cbfad36ef8f. - Required merge gates passed before the squash merge. Prepared head SHA: a46b5ec5c7b07a88f4970e2f5ed86cbfad36ef8f Review: https://github.com/openclaw/openclaw/pull/83944#issuecomment-4484051060 Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/cli/program/config-guard.test.ts | 25 +++++++++++++++++++++++++ src/cli/program/config-guard.ts | 10 +++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac9c988cfc8..b06b2d87d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/subagents: skip stale embedded-run wake probes for dormant completion requesters, so late subagent completions go straight to requester-agent/direct handoff instead of producing `reason=no_active_run` queue noise. (#82964) Thanks @galiniliev. +- CLI: retry config snapshot reads after a transient failure so one rejected read no longer poisons later commands in the same process. (#83931) Thanks @honor2030. - WhatsApp: drain pending outbound deliveries on a 30s periodic timer in addition to the reconnect handler, so messages enqueued while the provider is already connected no longer wait for the next reconnect to send. (#79083) Thanks @Oviemudiaga. ## 2026.5.19 diff --git a/src/cli/program/config-guard.test.ts b/src/cli/program/config-guard.test.ts index 2f033a25ed3..3462b39f82c 100644 --- a/src/cli/program/config-guard.test.ts +++ b/src/cli/program/config-guard.test.ts @@ -130,6 +130,31 @@ describe("ensureConfigReady", () => { ); }); + it("retries the cached config snapshot after a read rejection", async () => { + const originalVitest = process.env.VITEST; + process.env.VITEST = "false"; + const transientError = new Error("temporary config read failure"); + const recoveredSnapshot = makeSnapshot(); + readConfigFileSnapshotMock + .mockRejectedValueOnce(transientError) + .mockResolvedValueOnce(recoveredSnapshot); + + try { + await expect(runEnsureConfigReady(["status"])).rejects.toThrow(transientError); + await expect(runEnsureConfigReady(["status"])).resolves.toBeDefined(); + await expect(runEnsureConfigReady(["status"])).resolves.toBeDefined(); + } finally { + if (originalVitest === undefined) { + delete process.env.VITEST; + } else { + process.env.VITEST = originalVitest; + } + } + + expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(2); + expect(setRuntimeConfigSnapshotMock).toHaveBeenCalledWith(undefined, undefined); + }); + it("exits for invalid config on non-allowlisted commands", async () => { setInvalidSnapshot(); const runtime = await runEnsureConfigReady(["message"]); diff --git a/src/cli/program/config-guard.ts b/src/cli/program/config-guard.ts index 8b099fab732..770059f0b1a 100644 --- a/src/cli/program/config-guard.ts +++ b/src/cli/program/config-guard.ts @@ -30,7 +30,15 @@ async function getConfigSnapshot() { if (process.env.VITEST === "true") { return readConfigFileSnapshot(); } - configSnapshotPromise ??= readConfigFileSnapshot(); + if (!configSnapshotPromise) { + const pendingSnapshot = readConfigFileSnapshot(); + configSnapshotPromise = pendingSnapshot; + pendingSnapshot.catch(() => { + if (configSnapshotPromise === pendingSnapshot) { + configSnapshotPromise = null; + } + }); + } return configSnapshotPromise; }