






















@@ -36,6 +36,10 @@ export interface PendingDeliveryDrainDecision {
3636bypassBackoff?: boolean;
3737}
383839+export type ActiveDeliveryClaimResult<T> =
40+| { status: "claimed"; value: T }
41+| { status: "claimed-by-other-owner" };
42+3943const MAX_RETRIES = 5;
40444145/** Backoff delays in milliseconds indexed by retry count (1-based). */
@@ -90,20 +94,19 @@ function releaseRecoveryEntry(entryId: string): void {
9094entriesInProgress.delete(entryId);
9195}
929693-/**
94- * Claim an entry id against the shared in-memory recovery set so a concurrent
95- * reconnect/startup drain will skip it while the owning caller is mid-flight.
96- * Returns `false` if the id is already claimed. Callers must pair a successful
97- * claim with {@link releaseActiveDelivery} in a `finally`. The claim is
98- * process-local and intentionally does not survive a crash, so crash-replay
99- * paths still recover fresh entries whose owning process died.
100- */
101-export function tryClaimActiveDelivery(entryId: string): boolean {
102-return claimRecoveryEntry(entryId);
103-}
97+export async function withActiveDeliveryClaim<T>(
98+entryId: string,
99+fn: () => Promise<T>,
100+): Promise<ActiveDeliveryClaimResult<T>> {
101+if (!claimRecoveryEntry(entryId)) {
102+return { status: "claimed-by-other-owner" };
103+}
104104105-export function releaseActiveDelivery(entryId: string): void {
106-releaseRecoveryEntry(entryId);
105+try {
106+return { status: "claimed", value: await fn() };
107+} finally {
108+releaseRecoveryEntry(entryId);
109+}
107110}
108111109112function buildRecoveryDeliverParams(entry: QueuedDelivery, cfg: OpenClawConfig) {
@@ -246,15 +249,8 @@ export async function drainPendingDeliveries(opts: {
246249const now = Date.now();
247250const deliver = opts.deliver;
248251const matchingEntries = (await loadPendingDeliveries(opts.stateDir))
249-.map((entry) => ({
250- entry,
251-decision: opts.selectEntry(entry, now),
252-}))
253-.filter(
254-(item): item is { entry: QueuedDelivery; decision: PendingDeliveryDrainDecision } =>
255-item.decision.match,
256-)
257-.toSorted((a, b) => a.entry.enqueuedAt - b.entry.enqueuedAt);
252+.filter((entry) => opts.selectEntry(entry, now).match)
253+.toSorted((a, b) => a.enqueuedAt - b.enqueuedAt);
258254259255if (matchingEntries.length === 0) {
260256return;
@@ -264,7 +260,7 @@ export async function drainPendingDeliveries(opts: {
264260`${opts.logLabel}: ${matchingEntries.length} pending message(s) matched ${opts.drainKey}`,
265261);
266262267-for (const { entry, decision } of matchingEntries) {
263+for (const entry of matchingEntries) {
268264if (!claimRecoveryEntry(entry.id)) {
269265opts.log.info(`${opts.logLabel}: entry ${entry.id} is already being recovered`);
270266continue;
@@ -280,6 +276,12 @@ export async function drainPendingDeliveries(opts: {
280276continue;
281277}
282278279+const currentDecision = opts.selectEntry(currentEntry, Date.now());
280+if (!currentDecision.match) {
281+opts.log.info(`${opts.logLabel}: entry ${currentEntry.id} no longer matches, skipping`);
282+continue;
283+}
284+283285if (currentEntry.retryCount >= MAX_RETRIES) {
284286try {
285287await moveToFailed(currentEntry.id, opts.stateDir);
@@ -296,7 +298,7 @@ export async function drainPendingDeliveries(opts: {
296298continue;
297299}
298300299-if (!decision.bypassBackoff) {
301+if (!currentDecision.bypassBackoff) {
300302const retryEligibility = isEntryEligibleForRecoveryRetry(currentEntry, Date.now());
301303if (!retryEligibility.eligible) {
302304opts.log.info(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。