diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57767511f8a..c20a8f58c53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- Dashboard: constrain exec approval modal overflow on desktop so long command content no longer pushes action buttons out of view. (#67082) Thanks @Ziy1-Tan.
- Agents/CLI transcripts: persist successful CLI-backed turns into the OpenClaw session transcript so google-gemini-cli replies appear in session history and the Control UI again. (#67490) Thanks @obviyus.
- Discord/tool-call text: strip standalone Gemma-style `...` tool-call payloads from visible assistant text without truncating prose examples or trailing replies. (#67318) Thanks @joelnishanth.
+- WhatsApp/web-session: drain the pending per-auth creds save queue before reopening sockets so reconnect-time auth bootstrap no longer races in-flight `creds.json` writes and falsely restores from backup. (#67464) Thanks @neeravmakwana.
## 2026.4.15-beta.1
diff --git a/extensions/whatsapp/src/connection-controller.test.ts b/extensions/whatsapp/src/connection-controller.test.ts
index cf5c4c73767..d767dfecdcc 100644
--- a/extensions/whatsapp/src/connection-controller.test.ts
+++ b/extensions/whatsapp/src/connection-controller.test.ts
@@ -1,20 +1,35 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js";
import { WhatsAppConnectionController } from "./connection-controller.js";
-import { createWaSocket, waitForWaConnection } from "./session.js";
+import {
+ createWaSocket,
+ waitForCredsSaveQueueWithTimeout,
+ waitForWaConnection,
+} from "./session.js";
vi.mock("./session.js", async () => {
const actual = await vi.importActual("./session.js");
return {
...actual,
createWaSocket: vi.fn(),
+ waitForCredsSaveQueueWithTimeout: vi.fn(async () => {}),
waitForWaConnection: vi.fn(),
};
});
const createWaSocketMock = vi.mocked(createWaSocket);
+const waitForCredsSaveQueueWithTimeoutMock = vi.mocked(waitForCredsSaveQueueWithTimeout);
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
+function createListenerStub(messageId = "ok") {
+ return {
+ sendMessage: vi.fn(async () => ({ messageId })),
+ sendPoll: vi.fn(async () => ({ messageId })),
+ sendReaction: vi.fn(async () => {}),
+ sendComposingTo: vi.fn(async () => {}),
+ };
+}
+
describe("WhatsAppConnectionController", () => {
let controller: WhatsAppConnectionController;
@@ -66,6 +81,26 @@ describe("WhatsAppConnectionController", () => {
expect(controller.getActiveListener()).toBeNull();
});
+ it("flushes pending creds saves before opening a socket", async () => {
+ const callOrder: string[] = [];
+ waitForCredsSaveQueueWithTimeoutMock.mockImplementationOnce(async () => {
+ callOrder.push("wait");
+ });
+ createWaSocketMock.mockImplementationOnce(async () => {
+ callOrder.push("create");
+ return { ws: { close: vi.fn() } } as never;
+ });
+ waitForWaConnectionMock.mockResolvedValueOnce(undefined);
+
+ await controller.openConnection({
+ connectionId: "conn-flush-first",
+ createListener: async () => createListenerStub() as never,
+ });
+
+ expect(waitForCredsSaveQueueWithTimeoutMock).toHaveBeenCalledWith("/tmp/wa-auth");
+ expect(callOrder).toEqual(["wait", "create"]);
+ });
+
it("keeps the previous registered controller until a replacement listener is ready", async () => {
const liveController = new WhatsAppConnectionController({
accountId: "work",
@@ -83,12 +118,7 @@ describe("WhatsAppConnectionController", () => {
maxAttempts: 5,
},
});
- const liveListener = {
- sendMessage: vi.fn(async () => ({ messageId: "live-msg" })),
- sendPoll: vi.fn(async () => ({ messageId: "live-poll" })),
- sendReaction: vi.fn(async () => {}),
- sendComposingTo: vi.fn(async () => {}),
- };
+ const liveListener = createListenerStub("live");
createWaSocketMock.mockResolvedValueOnce({ ws: { close: vi.fn() } } as never);
waitForWaConnectionMock.mockResolvedValueOnce(undefined);
await liveController.openConnection({
diff --git a/extensions/whatsapp/src/connection-controller.ts b/extensions/whatsapp/src/connection-controller.ts
index fa37bbf7a95..4545cd797af 100644
--- a/extensions/whatsapp/src/connection-controller.ts
+++ b/extensions/whatsapp/src/connection-controller.ts
@@ -347,6 +347,7 @@ export class WhatsAppConnectionController {
let sock: WaSocket | null = null;
let connection: WhatsAppLiveConnection | null = null;
try {
+ await waitForCredsSaveQueueWithTimeout(this.authDir);
sock = await createWaSocket(false, this.verbose, {
authDir: this.authDir,
});