
























@@ -10,6 +10,8 @@ import {
1010MAX_RETRIES,
1111type RecoveryLogger,
1212recoverPendingDeliveries,
13+releaseActiveDelivery,
14+tryClaimActiveDelivery,
1315} from "./delivery-queue.js";
1416import {
1517createRecoveryLog,
@@ -413,4 +415,35 @@ describe("drainPendingDeliveries for reconnect", () => {
413415414416expect(deliver).not.toHaveBeenCalled();
415417});
418+419+it("skips entries that an in-flight live delivery has actively claimed", async () => {
420+// Regression for openclaw/openclaw#70386: a reconnect drain that runs
421+// while the live send is still writing to the adapter must not re-drive
422+// the same entry. The live delivery path holds an in-memory active claim
423+// for `queueId` across its send; drain honors that claim via the same
424+// `entriesInProgress` set used for startup recovery.
425+const log = createRecoveryLog();
426+const deliver = vi.fn<DeliverFn>(async () => {});
427+428+const id = await enqueueDelivery(
429+{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
430+tmpDir,
431+);
432+433+expect(tryClaimActiveDelivery(id)).toBe(true);
434+try {
435+await drainAcct1DirectChatReconnect({ deliver, log, stateDir: tmpDir });
436+expect(deliver).not.toHaveBeenCalled();
437+expect(log.info).toHaveBeenCalledWith(
438+expect.stringContaining(`entry ${id} is already being recovered`),
439+);
440+} finally {
441+releaseActiveDelivery(id);
442+}
443+444+// Once the live delivery path releases its claim (success or failure), a
445+// later reconnect drain is free to pick the entry up again.
446+await drainAcct1DirectChatReconnect({ deliver, log, stateDir: tmpDir });
447+expect(deliver).toHaveBeenCalledTimes(1);
448+});
416449});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。