[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 persistedProblem #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 deliveredProblem #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 replyProblem #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:
- Nachricht erreicht Gateway und wird in einem In-Memory-Puffer gehalten (
gateway/buffer.ts) - Nachricht wird an Handler weitergeleitet, aber Bestätigung wird vor Persistenz gesendet
- 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:
- Plugin liefert Nachricht an externen Dienst (z.B. Telegram API)
- Externer Dienst gibt einen Fehler zurück (z.B. "Bot wurde gekickt")
- Fehler wird abgefangen aber nicht weitergeleitet — nur auf DEBUG-Ebene protokolliert
- 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:
- Handler beginnt Verarbeitung und sendet partielle Antwort per Streaming
- Benutzer ruft
abort()auf, Handler empfängt Abbruchsignal - Abbruch-Handler setzt
delivery_state = 'aborted' - Wiederherstellungssystem sieht Nachricht als nicht zugestellt (kein ACK empfangen)
- 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:
- Nachrichten werden zugestellt und im Speicher bestätigt
- Sauberes Herunterfahren wird eingeleitet
- Shutdown-Handler löscht Persistenzstatus (Optimierung zur Vermeidung von Wiederholungen)
- Beim Neustart meldet Persistenzschicht keine unbestätigten Nachrichten
- 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=rollingLö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