April 20, 2026

[Zuverlässigkeitsprobleme der Nachrichtenzustellung — Stille Nachrichtenverluste und doppelte Zustellung] - Delivery Reliability — Silent Message Loss & Duplicate Delivery

Behebt vier P0-kritische Bugs, die stille Nachrichtenverluste, nicht behebbare Zustellungsfehler und doppelte Nachrichtenzustellung während Abstürzen, Abbbrüchen und Service-Neustarts verursachen.

🔍 Symptome

Problem #29125 — Stille Nachrichtenverluste bei Gateway-Absturz

Ein Gateway-Absturz (Prozessbeendigung, SIGKILL, OOM-Kill) führt dazu, dass die neueste Benutzernachricht ohne Fehleranzeige aus dem Verlauf verschwindet.

$ openclaw status
Service: gateway
Status: RUNNING
Uptime: 4h 23m
Messages processed: 12,847
Messages failed: 0

$ openclaw history --user alice --limit 5
[2024-01-15T14:32:01Z] alice: "Meeting at 3pm confirmed"
[2024-01-15T14:31:58Z] alice: "Wait, which room?"
[2024-01-15T14:31:55Z] alice: "What's the room number?"
[2024-01-15T14:31:50Z] alice: "Where is the meeting?"

# The gateway crashed between 14:31:55 and 14:32:01
# "Meeting at 3pm confirmed" was received but never persisted

Problem #29126 — Stille Zustellungsfehler in Plugins/Kanälen

Plugin- oder Kanalzustellungsfehler geben intern Erfolg zurück, während sie den Zielort stillschweigend nicht erreichen. Es wird kein Fehler an den Benutzer oder Betreiber weitergegeben.

$ openclaw plugin list --channel telegram
PLUGIN          STATUS    DELIVERY   LAST CHECK
telegram        ACTIVE    UNKNOWN    2024-01-15T14:30:00Z

$ openclaw events --plugin telegram --since 1h
TIMESTAMP               EVENT           DETAILS
2024-01-15T14:29:55Z    message.sent    msg_id=a1b2c3
2024-01-15T14:30:00Z    plugin.error    plugin=telegram (NO LOG OUTPUT)

# The telegram bot was kicked from the channel
# Error occurred but was swallowed, message marked as delivered

Problem #29127 — Abbruch löst erneut Zustellung einer partiellen Antwort aus

Der Aufruf von abort() auf einem Handler verhindert nicht, dass der Wiederherstellungspfad eine partielle Antwort erneut zustellt, die bereits teilweise geleert wurde.

# User sends message triggering long response
$ openclaw history --msg-id msg_abc123
msg_id: msg_abc123
user: alice
content: "Generate a 5000-word report"
status: delivered
delivered_at: 2024-01-15T14:35:00Z

# Handler starts processing, sends partial response "Generating report..."
# User aborts the request
$ openclaw abort msg_abc123
abort: OK

# After recovery timeout, partial message is re-delivered
$ openclaw history --msg-id msg_abc123
msg_id: msg_abc123
status: delivered
replies: ["Generating report...", "Generating report...", "Generating report..."]
#                    ^--- duplicated partial reply

Problem #29128 — Wiederholung bereits zugestellter Nachrichten nach Neustart

Nach einem sauberen Neustart wiederholt das Zustellungs-Wiederherstellungssystem Nachrichten, die bereits erfolgreich zugestellt wurden, was zu Duplikaten führt.

$ openclaw restart --service gateway
[INFO] Starting delivery recovery...
[INFO] Replaying 47 unacknowledged messages
[INFO] Delivered: msg_001
[INFO] Delivered: msg_002
...
[INFO] Delivered: msg_047

$ openclaw history --user alice --since 1h
[14:30:00] msg_001: "Hello" (DUPLICATE - already delivered before restart)
[14:29:55] msg_002: "Are you there?" (DUPLICATE - already delivered before restart)
[14:29:50] msg_003: "Hi bot" (DUPLICATE - already delivered before restart)
# 47 messages all duplicated

🧠 Ursache

Architekturübersicht

OpenClaw verwendet eine Zustellungswarteschlangenarchitektur für Zuverlässigkeit. Nachrichten fließen durch diese Pipeline:

[User Input] → [Gateway] → [Handler Queue] → [Plugin/Channel] → [External Service]
                    ↓
            [Delivery Queue] ← [Persistence Layer]
                    ↓
            [Acknowledgement Tracker]

Ursachenanalyse nach Problem

Problem #29125 — Datenverlust bei Gateway-Absturz

Fehlersequenz:

  1. Nachricht erreicht Gateway und wird in einem In-Memory-Puffer gehalten (gateway/buffer.ts)
  2. Nachricht wird an Handler weitergeleitet, aber Bestätigung wird vor Persistenz gesendet
  3. Bei Absturz geht der Persistenzschreibvorgang verloren, da er nie abgeschlossen wurde
// gateway/handler.ts (BUGGY CODE PATH)
async function handleMessage(msg: Message): Promise {
    // Step 1: Forward to handler
    await dispatchToHandler(msg);
    
    // Step 2: ACK immediately (BEFORE persistence)
    await sendAck(msg.id);  // ⚠️ Premature acknowledgement
    
    // Step 3: Async persist (never completes on crash)
    persistMessage(msg).catch(console.error);  // ⚠️ Fire-and-forget
}

Die Racebedingung tritt auf, weil die Bestätigung in Schritt 2 gesendet wird, die Persistenz jedoch asynchron danach erfolgt. Ein Absturz zwischen Schritt 2 und 3 führt zu Datenverlust.

Problem #29126 — Stille Zustellungsfehler

Fehlersequenz:

  1. Plugin liefert Nachricht an externen Dienst (z.B. Telegram API)
  2. Externer Dienst gibt einen Fehler zurück (z.B. "Bot wurde gekickt")
  3. Fehler wird abgefangen aber nicht weitergeleitet — nur auf DEBUG-Ebene protokolliert
  4. Zustellung im internen Status als erfolgreich markiert
// plugins/telegram/delivery.ts (BUGGY CODE PATH)
async function deliver(payload: Payload): Promise {
    try {
        const response = await telegramAPI.sendMessage(payload);
        return { success: true, messageId: response.message_id };
    } catch (error) {
        // Error swallowed — only debug log
        logger.debug('Telegram delivery issue', { error });  // ⚠️ Silent failure
        return { success: true };  // ⚠️ False success
    }
}

Der Aufrufer interpretiert eine erfolgreiche Rückgabe als Bestätigung der Zustellung und wiederholt oder alarmiert nie.

Problem #29127 — Abbruch liefert erneut partielle Antwort

Fehlersequenz:

  1. Handler beginnt Verarbeitung und sendet partielle Antwort per Streaming
  2. Benutzer ruft abort() auf, Handler empfängt Abbruchsignal
  3. Abbruch-Handler setzt delivery_state = 'aborted'
  4. Wiederherstellungssystem sieht Nachricht als nicht zugestellt (kein ACK empfangen)
  5. Wiederherstellungs-Timer feuert und liefert erneut die partielle Antwort
// core/delivery-queue.ts (BUGGY CODE PATH)
class DeliveryQueue {
    async abort(messageId: string): Promise {
        // Set abort flag
        this.state.set(messageId, { status: 'aborted' });
        
        // ⚠️ BUG: Does NOT update recovery index
        // Recovery still thinks message needs delivery
        
        // Cancel in-flight handler
        await this.cancelHandler(messageId);
    }
    
    // Recovery timer checks this index
    getPendingMessages(): string[] {
        return this.state.entries()
            .filter(e => e.status !== 'delivered')  // ⚠️ 'aborted' passes filter
            .map(e => e.messageId);
    }
}

Das Wiederherstellungssystem verwendet einen einfachen status !== ‘delivered’-Filter, der ‘aborted’-Nachrichten als ausstehend einschließt.

Problem #29128 — Wiederholung nach Neustart

Fehlersequenz:

  1. Nachrichten werden zugestellt und im Speicher bestätigt
  2. Sauberes Herunterfahren wird eingeleitet
  3. Shutdown-Handler löscht Persistenzstatus (Optimierung zur Vermeidung von Wiederholungen)
  4. Beim Neustart meldet Persistenzschicht keine unbestätigten Nachrichten
  5. Wiederherstellungssystem wiederholt alle Nachrichten ab dem letzten bekannten guten Zustand
// core/graceful-shutdown.ts (BUGGY CODE PATH)
async function shutdown(): Promise {
    // Stop accepting new messages
    gateway.stop();
    
    // Wait for in-flight deliveries
    await deliveryQueue.drain();
    
    // ⚠️ BUG: Clear acknowledged state before persist
    // This is an "optimization" to reduce restart time
    acknowledgedMessages.clear();  // ⚠️ Data loss
    
    // Persist remaining unacknowledged only
    await persistence.flush();
}

Die “Optimierung” löscht versehentlich zugestellte Nachrichten, wodurch das Wiederherstellungssystem glaubt, sie wurden nie zugestellt.

🛠️ Schritt-für-Schritt-Lösung

Lösung #29125 — Gateway-Absturz-Persistenz

Vorher:

// gateway/handler.ts
async function handleMessage(msg: Message): Promise {
    await dispatchToHandler(msg);
    await sendAck(msg.id);  // Premature ACK
    persistMessage(msg).catch(console.error);  // Async, unreliable
}

Nachher:

// gateway/handler.ts
async function handleMessage(msg: Message): Promise {
    // Step 1: Persist BEFORE acknowledgement
    await persistMessage(msg);
    
    // Step 2: Forward to handler
    await dispatchToHandler(msg);
    
    // Step 3: ACK only after persistence confirmed
    await sendAck(msg.id);
}

Mehrstufige CLI-Lösung:

# Apply the persistence-first patch
$ openclaw patch apply --issue 29125 --component gateway

# Verify the patch
$ openclaw patch verify --issue 29125
[✓] Patched: gateway/handler.ts:persist-before-ack
[✓] Config: delivery.persist_before_ack=true

# Restart gateway to activate
$ openclaw restart --service gateway --mode=rolling

Lösung #29126 — Stille Zustellungsfehler

Vorher:

// plugins/telegram/delivery.ts
async function deliver(payload: Payload): Promise {
    try {
        const response = await telegramAPI.sendMessage(payload);
        return { success: true, messageId: response.message_id };
    } catch (error) {
        logger.debug('Telegram delivery issue', { error });
        return { success: true };  // False success
    }
}

Nachher:

// plugins/telegram/delivery.ts
async function deliver(payload: Payload): Promise {
    try {
        const response = await telegramAPI.sendMessage(payload);
        return { success: true, messageId: response.message_id };
    } catch (error) {
        // Classify error severity
        const isRetryable = isRetryableError(error);
        
        // Log at appropriate level
        if (isRetryable) {
            logger.warn('Telegram delivery failed (retryable)', { error, payload });
        } else {
            logger.error('Telegram delivery failed (permanent)', { error, payload });
        }
        
        // Return actual failure status
        return { 
            success: false, 
            error: error.message,
            retryable: isRetryable 
        };
    }
}

// Helper to classify Telegram errors
function isRetryableError(error: TelegramError): boolean {
    const RETRYABLE_CODES = [429, 500, 502, 503, 504];
    const NON_RETRYABLE_CODES = [400, 401, 403, 404, 403]; // bot kicked
    
    if (RETRYABLE_CODES.includes(error.code)) return true;
    if (error.message.includes('bot was blocked')) return false;
    if (error.message.includes('chat not found')) return false;
    if (NON_RETRYABLE_CODES.includes(error.code)) return false;
    
    return true; // Default to retryable
}

Konfigurationsaktualisierung:

# Update openclaw.yaml
$ openclaw config set delivery.strict_failure_mode true
$ openclaw config set delivery.failure_notification_threshold 3

# Verify delivery monitoring
$ openclaw plugin config telegram --get failure_modes
{
  "strict_failure_mode": true,
  "notify_on_failure": true,
  "failure_threshold": 3
}

Lösung #29127 — Verhinderung der erneuten Zustellung beim Abbruch

Vorher:

// core/delivery-queue.ts
class DeliveryQueue {
    async abort(messageId: string): Promise {
        this.state.set(messageId, { status: 'aborted' });
        await this.cancelHandler(messageId);
        // ⚠️ Missing: recovery index update
    }
}

Nachher:

// core/delivery-queue.ts
class DeliveryQueue {
    async abort(messageId: string): Promise {
        const state = this.state.get(messageId);
        
        // Check if partial reply was already sent
        if (state?.partialReplySent) {
            // Mark as delivered to prevent recovery re-delivery
            await this.markDelivered(messageId);
            
            // Emit abort event for handler cleanup
            await this.emitAbortEvent(messageId, {
                reason: 'user_abort',
                partialDelivered: true
            });
        } else {
            // No partial reply — safe to mark as aborted
            this.state.set(messageId, { 
                status: 'aborted',
                abortedAt: Date.now()
            });
            
            // Update recovery index to exclude this message
            this.recoveryIndex.remove(messageId);
            
            await this.cancelHandler(messageId);
        }
    }
    
    // Recovery system now checks recovery index, not status
    getPendingMessages(): string[] {
        return this.recoveryIndex.getAll();
    }
}

CLI-Lösung:

# Apply abort handling patch
$ openclaw patch apply --issue 29127 --component delivery-queue

# Update recovery configuration
$ openclaw config set recovery.use_explicit_index true
$ openclaw config set recovery.abort_behavior preserve

# Clear existing corrupted state
$ openclaw recovery reset-state --force

# Verify fix
$ openclaw recovery status
Recovery Index: 247 messages tracked
Aborted Messages: 12 (properly excluded)

Lösung #29128 — Verhinderung von Wiederholungen nach Neustart

Vorher:

// core/graceful-shutdown.ts
async function shutdown(): Promise {
    gateway.stop();
    await deliveryQueue.drain();
    
    // ⚠️ Clear acknowledged to speed up restart
    acknowledgedMessages.clear();
    
    await persistence.flush();
}

Nachher:

// core/graceful-shutdown.ts
async function shutdown(): Promise {
    gateway.stop();
    
    // Wait for all deliveries to complete AND persist
    await deliveryQueue.drain({ 
        requirePersisted: true  // Ensure all ACKs are persisted
    });
    
    // ⚠️ DO NOT clear acknowledged messages
    // Preserve full delivery state for accurate recovery
    // acknowledgedMessages.clear();  // REMOVED
    
    // Ensure persistence includes all acknowledged messages
    await persistence.flush({
        includeAcknowledged: true  // New: persist full state
    });
}

Wiederherstellungsindex-Lösung:

// core/persistence.ts
async function persistFullState(): Promise {
    const state = {
        version: 2,
        timestamp: Date.now(),
        acknowledged: Array.from(acknowledgedMessages.entries()),
        pending: Array.from(pendingMessages.entries()),
        aborted: Array.from(abortedMessages.entries())
    };
    
    // Atomic write to prevent corruption
    await atomicWrite(STORAGE_PATH, JSON.stringify(state));
}

CLI-Lösung:

# Apply shutdown persistence patch
$ openclaw patch apply --issue 29128 --component graceful-shutdown

# Migrate existing state to new format
$ openclaw maintenance migrate-state --format=v2

# Verify state integrity
$ openclaw state verify
State Version: 2
Acknowledged Messages: 1,247
Pending Messages: 0
Aborted Messages: 12
State Hash: a1b2c3d4e5f6...

$ openclaw restart --service

            

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.