[Gesteuerte Benutzernachricht bei HEARTBEAT_OK verloren] - Steered User Message Swallowed When Heartbeat Run Produces HEARTBEAT_OK
Im Steer-Queue-Modus gehen Benutzernachrichten, die in Heartbeat-Runs eingefügt werden, verloren, wenn der Agent HEARTBEAT_OK produziert. Dies liegt an der Run-Level-Antwortunterdrückungslogik, die den gesamten Run als Heartbeat-No-Op behandelt.
🔍 Symptome
Primäre Manifestation
Wenn ein Benutzer eine Nachricht auf Telegram sendet, während ein aktiver Heartbeat-Lauf im steer-Warteschlangenmodus läuft, erhält der Benutzer keine Antwort, obwohl der Agent eine korrekte Antwort generiert hat.
Exakte Reproduktionssequenz
- Warteschlangenmodus als
steerkonfigurieren:# config.yaml messages: queue: mode: "steer" heartbeat: interval: 3s - Warten, bis ein Heartbeat-Lauf ausgelöst wird (in Logs sichtbar als
HEARTBEAT_RUN) - Eine Benutzernachricht innerhalb des Heartbeat-Fensters senden
- Folgendes in den Anwendungslogs beobachten:
[heartbeat] HEARTBEAT_RUN triggered at 2024-01-15T10:30:01.234Z [steer] Injecting user message "What's the weather?" into heartbeat run [agent] Processing run #42 (heartbeat + steered message) [agent] Run #42 produced responses: - HEARTBEAT_OK (heartbeat ack) - TEXT: "The weather is sunny, 72°F" [outbound] Discarding run #42 responses (HEARTBEAT_OK detected) [channel] Delivered: HEARTBEAT_OK ack only (no user message) - Benutzer erhält nichts – die TEXT-Antwort wird niemals an Telegram gesendet
Diagnostische Belege
Die benutzerorientierte Antwort existiert im Sitzungstranskript mit korrekten Generierungszeitstempeln, erreicht aber niemals die Kanalebene:
$ openclaw session show --id session-abc123
Session Transcript:
[10:30:01.234] ← HEARTBEAT (scheduled)
[10:30:02.456] ← USER: "What's the weather?" (steered into heartbeat run)
[10:30:02.789] → HEARTBEAT_OK
[10:30:03.012] → TEXT: "The weather is sunny, 72°F" ← EXISTS IN TRANSCRIPT
[10:30:03.100] ← Delivered: HEARTBEAT_OK only ← MISSING TEXT
Workaround-Verifizierung
Ein Wechsel zu collect-Modus umgeht das Problem:
# config.yaml
messages:
queue:
mode: "collect" # ← Workaround: queues as separate turn
Im collect-Modus wird die Benutzernachricht unabhängig in die Warteschlange eingereiht und erhält ihren eigenen Lauf mit vollständiger Zustellung.
🧠 Ursache
Architekturanalyse: Unterdrückung auf Laufebene
Das Problem entstammt einer grundlegenden Designannahme in der Heartbeat-Handling-Logik: Ein Lauf, der HEARTBEAT_OK enthält, wird als „Heartbeat No-Op" klassifiziert und die gesamte ausgehende Zustellung des Laufs wird unterdrückt.
Fehlersequenz
- Steer-Modus-Injection: Im
steer-Modus werden eingehende Benutzernachrichten, die während eines aktiven Heartbeat-Laufs eingehen, in diesen Lauf injiziert, anstatt einen neuen zu erstellen. Dies ist beabsichtigt –steerpriorisiert Latenz über Isolation. - Mehrfachantwort-Laufgenerierung: Der Agent verarbeitet den kombinierten Heartbeat + Benutzernachricht-Kontext und erzeugt mehrere Antworten sequenziell:
Response 1: HEARTBEAT_OK // Agent bestätigt Heartbeat ohne Aktion Response 2: TEXT // Agent antwortet auf die tatsächliche Frage des Benutzers - Lauf-Klassifizierungslogik: Die Lauf-Klassifizierungslogik scannt Antworten nach beliebigen
HEARTBEAT_*-Markern:// Simplified classification pseudocode function classifyRun(responses): for response in responses: if response.type.startsWith("HEARTBEAT_"): return RUN_TYPE.HEARTBEAT_NOOP // ← Triggers suppression return RUN_TYPE.NORMAL - Vorzeitige Unterdrückung: Da
HEARTBEAT_OKvorhanden ist, wird der gesamte Lauf alsHEARTBEAT_NOOPmarkiert, was die Verwerflogik in der ausgehenden Zustellebene auslöst:// Outbound delivery pseudocode function deliverResponses(run): if run.classification === RUN_TYPE.HEARTBEAT_NOOP: return // ← Entire delivery skipped, including user response deliverToChannel(run.responses) - Benutzerantwort verloren: Die
TEXT-Antwort (die gültiger benutzerorientierter Inhalt ist) wird verworfen, weil sie sich einen Lauf mitHEARTBEAT_OKteilt.
Code-Speicherort-Referenz
| Komponente | Datei | Problem |
|---|---|---|
| Steer Injection | src/queue/steer.ts | Injiziert Benutzernachrichten in aktive Heartbeat-Läufe |
| Run Classification | src/runs/classifier.ts | Klassifiziert gesamten Lauf basierend auf Präsenz von HEARTBEAT_* |
| Outbound Delivery | src/outbound/delivery.ts | Überspringt Zustellung für HEARTBEAT_NOOP-Läufe |
Warum Collect-Modus funktioniert
Im collect-Modus wird die Benutzernachricht als separater Folgeturn in die Warteschlange eingereiht. Sie erhält ihren eigenen unabhängigen Lauf, der nur benutzerorientierte Antworten enthält – keine HEARTBEAT_*-Marker – sodass die Klassifizierungslogik sie korrekt als NORMAL identifiziert und zustellt.
🛠️ Schritt-für-Schritt-Lösung
Option 1: Antworten vor Klassifizierung filtern (Empfohlen)
Ändern Sie die Lauf-Klassifizierungslogik, um HEARTBEAT_OK-Antworten bei der Bestimmung des Laufstyps zu ignorieren, sodass benutzerorientierte Antworten im selben Lauf zugestellt werden können.
Vorher
// src/runs/classifier.ts
function classifyRun(responses: Response[]): RunType {
for (const response of responses) {
if (response.type.startsWith("HEARTBEAT_")) {
return RUN_TYPE.HEARTBEAT_NOOP;
}
}
return RUN_TYPE.NORMAL;
}
Nachher
// src/runs/classifier.ts
function classifyRun(responses: Response[]): RunType {
const hasHeartbeatAction = responses.some(
r => r.type.startsWith("HEARTBEAT_") && r.type !== "HEARTBEAT_OK"
);
const hasUserFacingResponse = responses.some(
r => !r.type.startsWith("HEARTBEAT_") && r.type !== "CONTROL"
);
if (hasHeartbeatAction && !hasUserFacingResponse) {
return RUN_TYPE.HEARTBEAT_NOOP;
}
return RUN_TYPE.NORMAL;
}
Option 2: Ausgehende Zustellung ändern, um Benutzerantworten zu extrahieren
Wenn die Klassifizierung nicht geändert werden kann, ändern Sie die ausgehende Zustellebene, um Nicht-Heartbeat-Antworten auch aus HEARTBEAT_NOOP-Läufen zu extrahieren und zuzustellen.
// src/outbound/delivery.ts
function deliverResponses(run: Run): void {
if (run.classification !== RUN_TYPE.HEARTBEAT_NOOP) {
deliverToChannel(run.responses);
return;
}
// Extract user-facing responses from heartbeat no-op runs
const userResponses = run.responses.filter(
r => !r.type.startsWith("HEARTBEAT_") && r.type !== "CONTROL"
);
if (userResponses.length > 0) {
deliverToChannel(userResponses);
}
}
Option 3: Konfigurationsbasierte Problemumgehung
Wenn Codeänderungen nicht sofort möglich sind, konfigurieren Sie das System, um die Bedingung zu vermeiden:
# config.yaml
messages:
queue:
mode: "collect" # Avoids steered injection into heartbeat runs
heartbeat:
interval: 60s # Reduces chance of user message arriving during heartbeat
# Or disable heartbeat during active conversations:
pause_on_active: true
Bereitstellungsschritte
- Konfiguration sichern
cp config.yaml config.yaml.backup - Fix anwenden
# If using Option 1 or 2: vim src/runs/classifier.ts # or src/outbound/delivery.ts npm run build - Dienst neu starten
docker-compose down && docker-compose up -d # Or for systemd: sudo systemctl restart openclaw - Konfiguration verifizieren
openclaw config show | grep -A5 "queue:" openclaw status
🧪 Verifizierung
Testfall 1: Zugesteuerte Nachrichtenzustellung
Zweck: Verifizieren, dass Benutzernachrichten, die in Heartbeat-Läufe injiziert wurden, zugestellt werden.
# 1. Configure steer mode with short heartbeat
openclaw config set messages.queue.mode steer
openclaw config set heartbeat.interval 3s
openclaw restart
# 2. Monitor logs in one terminal
openclaw logs --follow | grep -E "(HEARTBEAT|steer|deliver)"
# 3. Send user message during heartbeat window
# Wait for log line: [heartbeat] HEARTBEAT_RUN triggered
# Then immediately send: "Testing steer delivery"
# 4. Verify delivery
# Expected: User receives response on Telegram
# Expected log: [outbound] Delivered: TEXT response to user
Erfolgskriterien:
- Benutzer erhält die Antwort auf Telegram
- Log zeigt
Delivered: TEXT(nichtDiscarded) - Sitzungstranskript zeigt sowohl
HEARTBEAT_OKals auchTEXT-Antworten
Testfall 2: Reiner Heartbeat wird weiterhin unterdrückt
Zweck: Sicherstellen, dass echte HEARTBEAT_NOOP-Läufe (ohne Benutzernachrichten) weiterhin unterdrückt werden.
# 1. Wait for heartbeat to fire with NO user interaction
# Monitor logs for a clean heartbeat run
# 2. Expected log behavior
[heartbeat] HEARTBEAT_RUN triggered
[heartbeat] HEARTBEAT_OK generated (no user message)
[outbound] Discarding run (HEARTBEAT_NOOP)
# 3. Verify: User should NOT receive any notification from this run
Erfolgskriterien:
- Heartbeat-only-Läufe erzeugen keine Benutzerbenachrichtigung
- Log zeigt
Discardingfür reine Heartbeat-Läufe
Testfall 3: Sitzungstranskript-Validierung
# Get session ID from a recent conversation
openclaw session list --limit 5
Show detailed transcript
openclaw session show –id <SESSION_ID> –verbose
Verify structure contains both response types
Expected output should show:
[timestamp] → HEARTBEAT_OK
[timestamp] → TEXT: “response content”
[timestamp] ← Delivered: HEARTBEAT_OK, TEXT ← Both delivered
Regressionstests
# Run existing test suite
npm test -- --grep "heartbeat"
Run steer mode specific tests
npm test – –grep “steer”
Expected: All tests pass including:
- steer mode message injection
- heartbeat response classification
- outbound delivery filtering
Exit-Code-Verifizierung
# After fix deployment
openclaw health check; echo "Exit code: $?"
# Expected: 0 (healthy)
Check service logs
docker-compose logs openclaw 2>&1 | tail -20
Expected: No ERROR level entries related to delivery
⚠️ Häufige Fehler
Umgebungsspezifische Fallen
| Umgebung | Falle | Gegenmaßnahme |
|---|---|---|
| Docker | Container-Uhrenabweichung kann dazu führen, dass Heartbeat-Timing unzuverlässig wird, was die Race-Bedingung zwischen Heartbeat-Läufen und Benutzernachrichten-Injection verschlimmert | NTP-Synchronisation sicherstellen: docker run --cap-add=SYS_TIME openclaw:latest |
| VPS/Cloud | Netzwerklatenz zwischen Telegram-API und Server kann das Timing-Problem maskieren | Mit lokalem Bot-Webhook statt Long-Polling testen, um Variablen zu reduzieren |
| macOS (Entwicklung) | Heartbeat-Timer können aufgrund von System-Ruhezuständen weniger zuverlässig ausgelöst werden | Systemschlaf während des Testens deaktivieren: caffeinate -s |
| Windows (Entwicklung) | Zeilenendungs-Unterschiede (\r\n vs \n) in Konfigurationsdateien können Parsing-Probleme verursachen | Unix-Zeilenenden verwenden: set FILE_OPTS=-o nowrap im Editor |
Timing-Empfindlichkeit
Der Bug ist stark timing-abhängig. Das Race-Fenster existiert zwischen:
- Heartbeat-Lauf startet (
HEARTBEAT_RUNgeloggt) - Heartbeat-Bestätigung generiert (
HEARTBEAT_OKgeloggt) - Benutzernachricht eingeht und injiziert wird
- Benutzerantwort generiert wird
- Lauf-Klassifizierung erfolgt
Empfehlung: Verwenden Sie ein Heartbeat-Intervall von 3s oder 5s für zuverlässige Reproduktionstests. Intervalle unter 1s können das Race-Fenster zu eng machen, um zuverlässig ausgelöst zu werden.
Konfigurationsfehler
steermitforceverwechseln:# Wrong - force mode ignores queue entirely messages.queue.mode: "force"Correct - steer mode uses intelligent injection
messages.queue.mode: “steer”
- Heartbeat-Intervall zu lang, was das Problem maskiert:
# This interval may never overlap with user messages heartbeat.interval: 300s # 5 minutes - unlikely to catch user messagesBetter for testing
heartbeat.interval: 3s
- Fehlende kanalspezifische Warteschlangeneinstellungen:
# Some channels may override global queue mode telegram: queue_mode: "collect" # ← May override steer settingUse channel-agnostic config
messages.queue.mode: “steer”
Fehldiagnose: Symptome verwechseln
Diese Probleme können ähnlich erscheinen, haben aber unterschiedliche Ursachen:
- Fehlende Antwort aufgrund von Rate Limiting: Benutzer erhält nichts, aber Log zeigt
Rate limitedstattDiscarded - Fehlende Antwort aufgrund von Agent-Stille: Agent generiert nie eine Antwort, Log zeigt kein
TEXTim Antwortarray - Fehlende Antwort aufgrund von Telegram-Zustellfehler: Log zeigt
Delivered, aber Benutzer erhält nichts; dies ist ein Telegram/API-Problem
Wichtigstes Unterscheidungsmerkmal: Dieser Bug zeigt Discarding run in den Logs mit sowohl HEARTBEAT_OK als auch TEXT im Transkript.
Fallstricke bei partiellen Fixes
Wenn Sie Option 2 (Outbound-Extraktion) anwenden, stellen Sie sicher, dass:
- Control-Antworten (z.B.
HANDOVER,TRANSFER) ebenfalls angemessen gefiltert werden - Analytics/Tracking-Aufrufe weiterhin das vollständige Antwortarray erhalten
- Webhook-Nutzlasten den tatsächlich zugestellten Inhalt widerspiegeln, nicht den ursprünglichen Lauf
🔗 Zugehörige Fehler
HEARTBEAT_TIMEOUT— Heartbeat-Lauf hat die maximale Dauer überschritten; kann Sitzungsbereinigung auslösen, wenn aufeinanderfolgende Timeouts den Schwellenwert überschreiten[heartbeat] Run exceeded 30s timeout, forcing HEARTBEAT_TIMEOUTHEARTBEAT_SKIP— Heartbeat-Lauf aufgrund aktiver Konversation übersprungen; Konfigurationheartbeat.pause_on_active: true[heartbeat] Skipping HEARTBEAT_RUN - active conversation detectedSTEER_INJECT_FAILED— Steer-Modus konnte Nachricht nicht in aktiven Lauf injizieren; fällt zurück auf Warteschlange[steer] Failed to inject message: no active heartbeat run in progress [steer] Falling back to queue for message "..."DELIVERY_FILTERED— Antwort wurde absichtlich durch Kanalrichtlinie gefiltert (Spam-Erkennung, Inhaltsfilterung)[outbound] DELIVERY_FILTERED: response contains blocked keywordRATE_LIMIT_EXCEEDED— Telegram-API-Ratenlimit erreicht; Antworten für Wiederholung in Warteschlange[telegram] Rate limit exceeded (30/30), queuing response for retry in 60sQUEUE_MODE_CONFLICT— Widersprüchliche Warteschlangenmodus-Einstellungen zwischen globaler und kanalspezifischer Konfiguration[config] Warning: telegram.queue_mode conflicts with messages.queue.mode
Historischer Kontext
Dieses Problem stellt eine Regression dar, die eingeführt wurde, als das Lauf-Klassifizierungssystem aus Effizienzgründen vereinheitlicht wurde. Zuvor hatte jeder Antworttyp eine unabhängige Zustelllogik, aber die Konsolidierung in die Klassifizierung auf Laufebene führte das Unterdrückungsverhalten für Mehrfachantwort-Läufe mit HEARTBEAT_OK ein.
Siehe auch
- OpenClaw Warteschlangenmodus-Dokumentation — Detaillierte Erklärung von
steervscollectvsforce - Heartbeat-Systemarchitektur — Technischer Tiefeneinblick in Heartbeat-Planung und Antwort-Handling
- Telegram-Kanal-Adapter — Kanalspezifische Zustellungsüberlegungen