---
name: drain_fleet_events
kind: function
lang: py
domain: infra
version: "1.1.0"
purity: impure
signature: "def drain_fleet_events(events_path: str | None = None, cursor_path: str | None = None, advance: bool = True) -> dict"
description: "Drena la cola JSONL de eventos de la flota desde un cursor persistente. Lee los eventos nuevos (desde la linea del cursor hasta el final), los parsea saltando lineas en blanco o JSON invalido, los agrupa por su campo `to` (RECLAMA, MAL_LANZADO, DICE_TERMINADO, ESTANCADO, TRABAJANDO, GONE), aisla los urgentes y avanza el cursor para no reprocesar. Pensado para que el orquestador-Claude de flota consuma eventos nuevos cada vez que despierta."
tags: [fleet, claude-fleet, jsonl, cursor, queue, drain, watcher, orchestrator, orchestration]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["os", "json"]
params:
  - name: events_path
    desc: "ruta a la cola JSONL append-only de eventos. Default: ~/.claude/fleet/events.jsonl"
  - name: cursor_path
    desc: "ruta al archivo de cursor (numero de lineas ya procesadas, entero en texto plano). Default: ~/.claude/fleet/cursor"
  - name: advance
    desc: "si True avanza el cursor al total de lineas para no reprocesar; si False hace peek sin mover el cursor (inspeccion)"
output: "dict con total_new (int), events (lista de eventos nuevos en orden), by_classification (dict agrupado por campo `to`), urgent (lista de eventos con urgent==True), cursor (nueva posicion) y opcionalmente reset==True si la cola estaba truncada/rotada y el cursor se reinicio a 0"
tested: true
tests:
  - "cola con eventos cursor cero devuelve todos y avanza"
  - "segunda llamada cero nuevos"
  - "advance false no mueve cursor"
  - "archivo ausente vacio sin crash"
  - "json invalido se salta"
  - "agrupacion por to y urgent"
  - "cursor mayor que lineas reinicia"
  - "normaliza flag advance posicional de fn run"
  - "peek flag posicional no mueve cursor"
test_file_path: "python/functions/infra/drain_fleet_events_test.py"
file_path: "python/functions/infra/drain_fleet_events.py"
---

## Ejemplo

```python
from infra.drain_fleet_events import drain_fleet_events

# El orquestador despierta y consume los eventos nuevos de la flota.
drained = drain_fleet_events()  # usa ~/.claude/fleet/events.jsonl y ~/.claude/fleet/cursor

print(f"{drained['total_new']} eventos nuevos")
for ev in drained["by_classification"].get("RECLAMA", []):
    print("RECLAMA:", ev["session_id"], ev["goal"])

# Eventos urgentes primero.
for ev in drained["urgent"]:
    print("URGENTE:", ev["to"], ev["session_id"])

# Peek sin avanzar el cursor (no consume): util para inspeccionar antes de actuar.
peek = drain_fleet_events(advance=False)
```

Tambien desde la CLI, sin escribir codigo. El drenado canonico consume y avanza
el cursor; el peek inspecciona sin moverlo:

```bash
# Drenado canonico: consume los eventos nuevos y avanza el cursor.
./fn run drain_fleet_events

# Peek (no mueve el cursor): inspeccionar la cola sin consumirla.
./fn run drain_fleet_events --advance false
```

## Cuando usarla

Usala en el orquestador-Claude de flota cada vez que despierta y necesita ver
las transiciones de estado producidas DESDE la ultima vez sin reprocesar las ya
vistas. Tambien para diagnostico con `advance=False` cuando quieres mirar la
cola sin consumirla.

## Gotchas

- **Impura**: lee la cola JSONL del disco y, con `advance=True`, sobrescribe el
  archivo de cursor. Pasa `advance=False` para peek puro sin efectos en cursor.
- **Cursor por lineas, no por bytes**: el cursor cuenta lineas procesadas. Las
  lineas en blanco e invalidas tambien avanzan el cursor (se cuentan en el
  total de lineas) aunque no produzcan eventos, de modo que nunca se re-leen.
- **Cola rotada/truncada**: si el cursor guardado es mayor que el numero de
  lineas actual (la cola se trunco o rota), el drain reinicia desde 0 y marca
  `reset: True` en el dict de salida — devolvera de nuevo todos los eventos
  presentes. Diseñado asi para no perder eventos silenciosamente.
- **JSON invalido o lineas en blanco**: se saltan sin romper el drain. Solo los
  objetos JSON validos entran en `events`.
- **`by_classification`**: agrupa por el campo `to`. Un evento sin campo `to`
  (raro) entra en `events` pero no en ningun grupo.
- **Fallo al persistir el cursor**: si `advance=True` pero no se puede escribir
  el cursor (permisos, FS), el drain sigue siendo valido pero la proxima llamada
  reprocesara estos eventos (at-least-once, no at-most-once).
- **Peek por `fn run`**: `fn run` mapea los argumentos POSICIONALMENTE y no
  parsea flags `--nombre valor`. Para que el peek documentado
  `./fn run drain_fleet_events --advance false` funcione, la funcion renormaliza
  ese patron internamente (`_normalize_fn_run_flags`): detecta `--advance` como
  primer posicional y lo interpreta como el booleano `advance`, devolviendo las
  rutas a su default. Una llamada normal por kwargs no se ve afectada. Acepta
  `--advance false|true|0|1|no|yes`; `--advance` sin valor equivale a `true`.

## Capability growth log

- v1.1.0 (2026-06-21) — el peek `./fn run drain_fleet_events --advance false`
  ahora funciona directo desde la CLI. Antes `fn run` colaba el flag como
  `events_path="--advance"` (ruta inexistente) y devolvia un falso
  `{total_new:0, cursor:0}`. Se añade `_normalize_fn_run_flags` para renormalizar
  el patron sin tocar el runner de Go. Elimina el gotcha de invocacion que
  silenciaba la cola al orquestador (reports 0011/0012).
