April 20, 2026 • Versión: v0.14.x - v1.x

Notificaciones del skill coding-agent descartadas silenciosamente por configuración predeterminada de heartbeat.target - Coding-Agent Skill Notifications Silently Dropped Due to Default heartbeat.target Configuration

Las notificaciones de finalización de tareas en segundo plano del skill coding-agent fallan silenciosamente porque heartbeat.target tiene como valor predeterminado 'none', lo que provoca que las respuestas del LLM se descarten antes de ser entregadas.

🔍 Síntomas

Manifestación principal

Cuando una tarea en segundo plano del agente de codificación se completa, el mensaje de notificación esperado nunca llega a ningún canal (terminal, interfaz de usuario o integración externa).

Salida de error técnico

El latido se activa y el LLM genera una respuesta, pero la entrega se suprime silenciosamente. No se registra ningún error en la consola. La inspección en modo depuración revela:

// Verbose log output (if DEBUG=openclaw:heartbeat is enabled)
[openclaw:heartbeat] Resolving delivery target for system event heartbeat
[openclaw:heartbeat] target config: "none" (default)
[openclaw:heartbeat] Delivering to: NoHeartbeatDeliveryTarget { reason: "target-none" }
[openclaw:heartbeat] Response generated but discarded - no valid delivery target

// Standard log output - nothing appears
// User sees: (silence)

Pasos de reproducción

# 1. Verify default configuration
openclaw config get heartbeat.target
# Output: none

# 2. Trigger a background coding-agent task with completion notification
openclaw exec --background -- coding-agent "Run slow analysis..."

# 3. Wait for completion (task finishes successfully)
# 4. Observe: No "Done: ..." message received
# 5. Check task status
openclaw task status --last
# Output: status: "completed", notifications: []

Indicadores secundarios

  • El manejador maybeNotifyOnExit() para procesos exec en segundo plano también presenta fallos silenciosos
  • La invocación manual de openclaw system event --text "Test" --mode now no produce salida visible
  • La inspección de configuración confirma que no existe anulación de heartbeat.target en el archivo de configuración del usuario

🧠 Causa raíz

Visión general arquitectónica

El flujo de notificaciones involucra tres componentes interconectados:

┌─────────────────────────────────────────────────────────────────────┐
│                    NOTIFICATION FLOW DIAGRAM                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Coding-Agent Skill                                                 │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ openclaw system event --text "Done: ..." --mode now         │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ enqueueSystemEvent({ text, mode: "now" })                  │   │
│  │ Source: pi-embedded-*.js:maybeNotifyOnExit()                │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ requestHeartbeatNow()                                       │   │
│  │ Source: heartbeat system                                    │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ Heartbeat fires → LLM processes system event                │   │
│  │ Source: reply-*.js:heartbeat handler                        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ resolveHeartbeatDeliveryTarget()                            │   │
│  │ Source: reply-*.js:resolveHeartbeatDeliveryTarget()        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│         ┌────────────────────┼────────────────────┐                │
│         ▼                    ▼                    ▼                │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐          │
│  │ target:      │    │ target:      │    │ target:      │          │
│  │ "last"       │    │ "none"       │    │ "session"    │          │
│  ├──────────────┤    ├──────────────┤    ├──────────────┤          │
│  │ DELIVER      │    │ DISCARD      │    │ DELIVER      │          │
│  │ response     │    │ response     │    │ response     │          │
│  │ silently     │    │ silently     │    │ to session   │          │
│  └──────────────┘    └──────────────┘    └──────────────┘          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Secuencia de fallo

Paso 1: Configuración predeterminada La opción de configuración heartbeat.target tiene como valor predeterminado "none" en src/config/defaults.ts:

// src/config/defaults.ts (line ~47)
export const defaultConfig = {
  // ...
  heartbeat: {
    target: "none",  // ← THIS IS THE CULPRIT
    interval: 30000,
    // ...
  },
  // ...
};

Paso 2: Resolución del destino de entrega Cuando se invoca resolveHeartbeatDeliveryTarget(), lee la configuración:

// reply-*.js (line ~26974 in dist, or src/core/reply.ts)
function resolveHeartbeatDeliveryTarget(context) {
  const target = config.heartbeat?.target ?? "none";
  
  switch (target) {
    case "last":
      return buildLastChannelTarget(context);
    case "session":
      return buildSessionTarget(context);
    case "none":
    default:
      return buildNoHeartbeatDeliveryTarget({ reason: "target-none" });
  }
}

Paso 3: Descarte silencioso El objeto NoHeartbeatDeliveryTarget instruye al subsistema de entrega para:

// reply-*.js
function buildNoHeartbeatDeliveryTarget({ reason }) {
  return {
    type: "none",
    reason,
    deliver: (response) => {
      // Silently discard - no logging at INFO level
      debug(`Heartbeat response discarded: ${reason}`);
      return { delivered: false, reason };
    }
  };
}

Paso 4: Rutas de código afectadas

Tanto la habilidad de codificación-agente como los manejadores internos comparten esta ruta de código:

// pi-embedded-*.js (line ~15413)
// maybeNotifyOnExit() - handles background exec process completion
function maybeNotifyOnExit(pid, exitCode, backgroundContext) {
  if (shouldNotify(exitCode, backgroundContext)) {
    enqueueSystemEvent({
      type: "process-exit",
      pid,
      exitCode,
      timestamp: Date.now(),
      sessionId: backgroundContext.sessionId
    });
    requestHeartbeatNow();
  }
}

Por qué no se detectó

  1. El mecanismo de latido fue diseñado principalmente para temporización interna, no para notificaciones visibles al usuario
  2. El valor predeterminado "none" garantiza operación silenciosa para tareas de mantenimiento del latido en segundo plano
  3. La habilidad de codificación-agente se agregó posteriormente sin conocimiento de esta restricción de entrega
  4. No existe advertencia de validación cuando la habilidad usa comandos que activan latidos sin la configuración adecuada

🛠️ Solución paso a paso

Solución A: Configurar heartbeat.target (Recomendado para usuarios)

Paso 1: Verificar la configuración actual

# View current heartbeat configuration
openclaw config get heartbeat
# Expected output (default):
# { "target": "none", "interval": 30000 }

# Or view specific target
openclaw config get heartbeat.target
# Expected output: none

Paso 2: Actualizar la configuración

Para configuración global (~/.config/openclaw/config.yaml):

# Before (default)
# No heartbeat.target entry (or implicit "none")

# After
heartbeat:
  target: "last"
  interval: 30000

Vía CLI:

openclaw config set heartbeat.target last
# Output: Configuration updated successfully

# Verify
openclaw config get heartbeat.target
# Output: last

Para configuración de proyecto (openclaw.yaml en el espacio de trabajo):

# openclaw.yaml
# Before
version: "1"

# After
version: "1"
heartbeat:
  target: "last"

Paso 3: Reiniciar el demonio de OpenClaw (si está en ejecución)

# For Homebrew-installed OpenClaw
brew services restart openclaw

# For npm-installed
openclaw daemon stop
openclaw daemon start

Solución B: Usar destino de sesión (Para configuraciones multiusuario)

Si se ejecuta en un entorno multiusuario o basado en sesiones:

# openclaw.yaml
version: "1"
heartbeat:
  target: "session"  # Delivers to originating session instead of last channel
  interval: 30000

Solución C: Modificar la habilidad de codificación-agente (Para desarrolladores)

Si controlas la habilidad y deseas evitar requerir configuración del usuario:

# skills/coding-agent/SKILL.md
# Modify the Auto-Notify section from:

## Auto-Notify on Completion
When the agent finishes a background task, it will automatically notify via:
\`\`\`bash
openclaw system event --text "Done: {summary}" --mode now
\`\`\`

# To a mechanism that doesn't depend on heartbeat delivery:

## Auto-Notify on Completion
When the agent finishes a background task, it will automatically notify via
the message tool:

1. Use the built-in message tool to send directly to the current session
2. Format: `message(to="session", content="Done: {summary}")`
3. This bypasses heartbeat delivery entirely
\`\`\`bash
# This approach is deprecated - relies on heartbeat.target config
# openclaw system event --text "Done: {summary}" --mode now
\`\`\`

Solución D: Agregar advertencia de validación al inicio (Para mantenedores del framework)

Agregar una verificación en el cargador de habilidades para advertir a los usuarios cuando falta la configuración requerida:

// src/skills/skill-loader.ts
function validateSkillRequirements(skill, config) {
  const requirements = skill.configRequirements || [];
  
  for (const req of requirements) {
    if (req.key === "heartbeat.target" && config.heartbeat?.target === "none") {
      logger.warn(
        `Skill "${skill.name}" requires heartbeat notifications but ` +
        `heartbeat.target is set to "none". Add "heartbeat.target: last" to your config.`
      );
    }
  }
}

🧪 Verificación

Prueba 1: Configuración aplicada correctamente

# Verify config is set
openclaw config get heartbeat.target
# Expected: "last"

# Verify full heartbeat config
openclaw config get heartbeat
# Expected: { "target": "last", "interval": 30000 }

Prueba 2: Entrega manual de eventos del sistema

# Enable debug logging (optional)
export DEBUG=openclaw:heartbeat

# Send a test system event
openclaw system event --text "Test notification" --mode now

# Expected debug output:
# [openclaw:heartbeat] Resolving delivery target for system event heartbeat
# [openclaw:heartbeat] target config: "last"
# [openclaw:heartbeat] Delivering to: LastChannelTarget { channelId: "..." }
# [openclaw:heartbeat] Response delivered successfully

# Expected visible output in terminal:
# Test notification

Prueba 3: Notificación de tarea en segundo plano de codificación-agente

# Start a background task with coding-agent
openclaw exec --background -- coding-agent "sleep 2 && echo 'Analysis complete'"

# Get the task ID
TASK_ID=$(openclaw task list --json | jq -r '.[0].id')

# Wait for completion (with timeout)
timeout 30 bash -c 'while openclaw task get '$TASK_ID' --json | jq -e ".status != \"completed\"" > /dev/null; do sleep 1; done'

# Check if notification was delivered
openclaw task get $TASK_ID --json | jq '.notifications'
# Expected: [ { "type": "system-event", "delivered": true, ... } ]

# Check logs for delivery confirmation
openclaw logs --tail 50 | grep -i "heartbeat.*delivered"
# Expected: [openclaw:heartbeat] Response delivered successfully

Prueba 4: Script de prueba de integración

#!/bin/bash
# test-notification.sh - Run after applying fix

set -e

echo "=== Testing Heartbeat Notification Fix ==="

# Check config
TARGET=$(openclaw config get heartbeat.target)
if [ "$TARGET" != "last" ] && [ "$TARGET" != "session" ]; then
  echo "FAIL: heartbeat.target is '$TARGET', expected 'last' or 'session'"
  exit 1
fi
echo "PASS: heartbeat.target is '$TARGET'"

# Send test event
RESULT=$(openclaw system event --text "Test $(date +%s)" --mode now --json)
DELIVERED=$(echo "$RESULT" | jq -r '.delivered // .success // false')

if [ "$DELIVERED" = "true" ]; then
  echo "PASS: Test notification delivered successfully"
else
  echo "FAIL: Test notification was not delivered"
  echo "Raw result: $RESULT"
  exit 1
fi

echo "=== All tests passed ==="
exit 0

Códigos de salida esperados

PruebaCódigo de salida en éxitoCódigo de salida en fallo
Verificación de configuración01
Evento manual01
Tarea en segundo plano01

⚠️ Errores comunes

Error común 1: Prioridad de ubicación del archivo de configuración

OpenClaw lee la configuración desde múltiples ubicaciones. La ubicación incorrecta hace que los cambios se ignoren.

# Config priority (highest to lowest):
# 1. Project config: ./openclaw.yaml
# 2. User config: ~/.config/openclaw/config.yaml  (Linux)
#                 ~/Library/Preferences/openclaw/config.yaml  (macOS)
# 3. Environment: OPENCLAW_HEARTBEAT_TARGET=last
# 4. Default: "none"

# Verify which config is active
openclaw config show --source
# Output: /Users/you/.config/openclaw/config.yaml

# Check for conflicting project config
cat ./openclaw.yaml 2>/dev/null || echo "No project config"

Error común 2: Anulación de variable de entorno en Docker/Contenedor

En despliegues Docker, las variables de entorno pueden sombrear los archivos de configuración.

# Wrong - env var may be set but config shows different
$ echo $OPENCLAW_HEARTBEAT_TARGET
# (empty)

$ openclaw config get heartbeat.target
# none

# The default is coming from compiled defaults, not explicit config
# Fix: Either set the env var or create a config file

Ejemplo de Docker Compose:

# docker-compose.yaml - Correct approach
services:
  openclaw:
    image: openclaw/openclaw:latest
    environment:
      - OPENCLAW_HEARTBEAT_TARGET=last  # Must be set for notifications
    volumes:
      - ./openclaw.yaml:/app/openclaw.yaml:ro  # Or use config file

Error común 3: Demonio no reiniciado después del cambio de configuración

Los cambios de configuración requieren reiniciar el demonio para que surtan efecto.

# WRONG: Config changed but daemon still running with old config
openclaw config set heartbeat.target last
openclaw system event --text "Test" --mode now  # Still uses old config

# CORRECT: Restart daemon
openclaw config set heartbeat.target last
openclaw daemon restart
sleep 2
openclaw system event --text "Test" --mode now  # Now uses new config

Error común 4: Servicio Homebrew de macOS no reiniciado

# Homebrew-managed services require explicit restart
brew services restart openclaw

# Verify service status
brew services list | grep openclaw
# Expected: openclaw started ... /Users/.../Library/LaunchAgents/...

# Check actual running config
openclaw config show | grep -A2 heartbeat

Error común 5: Entrega en sesión no interactiva

Cuando se ejecuta en modo no interactivo, "last" puede entregar a un canal diferente al esperado.

# CI/CD environments or detached processes
# "last" target resolves to whatever channel was last active
# which may be stale or non-existent

# Solution: Use "session" target for predictable delivery
# Or ensure session context is explicitly passed

Error común 6: Habilidades en conflicto que sobrescriben la configuración

Algunas habilidades pueden establecer programáticamente heartbeat.target en "none" para sus propios propósitos.

# Check if any skill modifies heartbeat config
grep -r "heartbeat.target" skills/
# or
grep -r "config.set.*heartbeat" ~/.local/share/openclaw/skills/

Error común 7: Discrepancia de versión

Las opciones "last" y "session" se agregaron en una versión específica. Usar una versión anterior ignora silenciosamente la configuración.

# Check OpenClaw version
openclaw --version
# v0.14.x or earlier: "last"/"session" may not be available
# v0.15.x+: Full heartbeat target options supported

# Upgrade if needed
npm update -g openclaw
# or
brew upgrade openclaw

🔗 Errores relacionados

Códigos de error conectados e Issues históricos

  • HEARTBEAT_NO_TARGET — Error interno cuando el latido se activa pero no existe un destino de entrega. Se manifiesta como fallo silencioso en configuraciones predeterminadas.
  • ENQUEUESYSTEM_EVENT_DROPPED — Ocurre cuando los eventos del sistema se encolan durante el apagado del demonio o cuando el sistema de latidos está deshabilitado.
  • BACKGROUND_EXEC_NOTIFY_FAILED — Relacionado con el fallo de `maybeNotifyOnExit()` para entregar notificaciones de finalización. Comparte la misma causa raíz que este problema.
  • SKILL_NOTIFICATION_TIMEOUT — Los agentes que esperan notificaciones de finalización pueden agotarse si `heartbeat.target` es `"none"`, causando que la ejecución de la habilidad parezca colgada.
  • Issue #1847 — "Background task notifications not working in v0.14.2" — Reporte original de esta clase de problema.
  • Issue #2103 — "maybeNotifyOnExit silently fails when heartbeat.target is none" — Confirmado upstream.
  • Issue #2256 — "Documentation does not mention heartbeat.target default value" — Issue de seguimiento de brecha en documentación.
  • Issue #2389 — "Coding-agent skill unusable without manual config" — Solicitud de característica para corregir comportamiento predeterminado.

Opciones de configuración relacionadas

# These related options may also affect notification behavior
heartbeat:
  target: "last"      # Required for notifications (the fix)
  interval: 30000     # How often heartbeat fires passively
  mode: "auto"        # When "manual", only explicit requests fire
  suppressOnIdle: true # May suppress notifications when no activity

system:
  eventBufferSize: 100  # Events dropped if buffer full and daemon busy

Referencias de documentación

  • docs/gateway/heartbeat.md — Documenta la arquitectura de latidos pero omite el impacto del valor predeterminado `"none"` en las notificaciones
  • skills/coding-agent/SKILL.md — Referencia el comando `openclaw system event` sin indicar el requisito de configuración
  • docs/config/reference.md — Lista las opciones de `heartbeat.target` pero no explica las implicaciones de notificación

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.