April 21, 2026

[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

  1. Warteschlangenmodus als steer konfigurieren:
    # config.yaml
    messages:
      queue:
        mode: "steer"
    heartbeat:
      interval: 3s
    
  2. Warten, bis ein Heartbeat-Lauf ausgelöst wird (in Logs sichtbar als HEARTBEAT_RUN)
  3. Eine Benutzernachricht innerhalb des Heartbeat-Fensters senden
  4. 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)
    
  5. 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

  1. 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 – steer priorisiert Latenz über Isolation.
  2. 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
    
  3. 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
    
  4. Vorzeitige Unterdrückung: Da HEARTBEAT_OK vorhanden ist, wird der gesamte Lauf als HEARTBEAT_NOOP markiert, 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)
    
  5. Benutzerantwort verloren: Die TEXT-Antwort (die gültiger benutzerorientierter Inhalt ist) wird verworfen, weil sie sich einen Lauf mit HEARTBEAT_OK teilt.

Code-Speicherort-Referenz

KomponenteDateiProblem
Steer Injectionsrc/queue/steer.tsInjiziert Benutzernachrichten in aktive Heartbeat-Läufe
Run Classificationsrc/runs/classifier.tsKlassifiziert gesamten Lauf basierend auf Präsenz von HEARTBEAT_*
Outbound Deliverysrc/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

  1. Konfiguration sichern
    cp config.yaml config.yaml.backup
  2. Fix anwenden
    # If using Option 1 or 2:
    vim src/runs/classifier.ts   # or src/outbound/delivery.ts
    npm run build
  3. Dienst neu starten
    docker-compose down && docker-compose up -d
    # Or for systemd:
    sudo systemctl restart openclaw
  4. 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 (nicht Discarded)
  • Sitzungstranskript zeigt sowohl HEARTBEAT_OK als auch TEXT-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 Discarding fü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

UmgebungFalleGegenmaßnahme
DockerContainer-Uhrenabweichung kann dazu führen, dass Heartbeat-Timing unzuverlässig wird, was die Race-Bedingung zwischen Heartbeat-Läufen und Benutzernachrichten-Injection verschlimmertNTP-Synchronisation sicherstellen: docker run --cap-add=SYS_TIME openclaw:latest
VPS/CloudNetzwerklatenz zwischen Telegram-API und Server kann das Timing-Problem maskierenMit 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 werdenSystemschlaf während des Testens deaktivieren: caffeinate -s
Windows (Entwicklung)Zeilenendungs-Unterschiede (\r\n vs \n) in Konfigurationsdateien können Parsing-Probleme verursachenUnix-Zeilenenden verwenden: set FILE_OPTS=-o nowrap im Editor

Timing-Empfindlichkeit

Der Bug ist stark timing-abhängig. Das Race-Fenster existiert zwischen:

  1. Heartbeat-Lauf startet (HEARTBEAT_RUN geloggt)
  2. Heartbeat-Bestätigung generiert (HEARTBEAT_OK geloggt)
  3. Benutzernachricht eingeht und injiziert wird
  4. Benutzerantwort generiert wird
  5. 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

  • steer mit force verwechseln:
    # 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 messages
    

    Better for testing

    heartbeat.interval: 3s

  • Fehlende kanalspezifische Warteschlangeneinstellungen:
    # Some channels may override global queue mode
    telegram:
      queue_mode: "collect"  # ← May override steer setting
    

    Use 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 limited statt Discarded
  • Fehlende Antwort aufgrund von Agent-Stille: Agent generiert nie eine Antwort, Log zeigt kein TEXT im 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_TIMEOUT
    
  • HEARTBEAT_SKIP — Heartbeat-Lauf aufgrund aktiver Konversation übersprungen; Konfiguration heartbeat.pause_on_active: true
    [heartbeat] Skipping HEARTBEAT_RUN - active conversation detected
    
  • STEER_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 keyword
    
  • RATE_LIMIT_EXCEEDED — Telegram-API-Ratenlimit erreicht; Antworten für Wiederholung in Warteschlange
    [telegram] Rate limit exceeded (30/30), queuing response for retry in 60s
    
  • QUEUE_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

Belege & Quellen

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