feat(infra): detect_fleet_context — contexto de flota por $TMUX (no $FLEET_SOCKET)

Funcion nueva detect_fleet_context_bash_infra (tag orchestration). Deriva
socket/session de $TMUX (senal fiable que todo proceso dentro de tmux tiene
siempre), con fallback a $FLEET_SOCKET/$FLEET_SESSION. Devuelve JSON
{in_fleet,in_tmux,socket,session,source}. Causa raiz del bug: $FLEET_SOCKET
(exportada con tmux set-environment -g por launch_fleetclaude) a veces viene
vacia en un claude resumido/relanzado pese a vivir en la flota, y el modo
orquestador caia al fallback kitty. .md self-doc (Ejemplo + Cuando usarla +
Gotchas).
This commit is contained in:
agent
2026-06-21 21:50:32 +02:00
parent cbefc82c02
commit 3c9e909eda
2 changed files with 197 additions and 0 deletions
@@ -0,0 +1,98 @@
---
name: detect_fleet_context
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: "detect_fleet_context() -> JSON {in_fleet,in_tmux,socket,session,source}"
description: "Detecta de forma robusta si el proceso corre dentro de una flota tmux FleetView, derivando socket y sesion de $TMUX (senal fiable) en vez de $FLEET_SOCKET (fragil, a veces vacia en un claude resumido/relanzado). Salida JSON con in_fleet/in_tmux/socket/session/source."
tags: [orchestration, fleet, tmux, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: []
tested: false
file_path: "bash/functions/infra/detect_fleet_context.sh"
params:
- name: "(ninguno)"
desc: "No recibe argumentos. Lee el entorno ($TMUX, con fallback a $FLEET_SOCKET/$FLEET_SESSION) y consulta el servidor tmux."
output: "JSON en stdout: {\"in_fleet\":bool, \"in_tmux\":bool, \"socket\":str, \"session\":str, \"source\":\"tmux|fleet_socket|none\"}. in_tmux=true basta para lanzar una window; in_fleet es la senal semantica de 'estoy en una flota'."
---
# detect_fleet_context
Detecta el contexto de flota del proceso actual sin depender de `$FLEET_SOCKET`.
## Por que existe
La deteccion de "estoy en una flota FleetView" dependia de la variable de
entorno `$FLEET_SOCKET`, que `launch_fleetclaude` exporta con
`tmux set-environment -g`. Esa variable solo llega a los procesos que tmux
arranca **despues** de setearla: un `claude` relanzado o resumido a mano puede
no heredarla y `$FLEET_SOCKET` queda vacia, aunque ese claude SI viva en una
window de la flota. Cuando eso pasa, el modo orquestador cae al fallback kitty
(`launch_claude_agent_kitty`) y lanza ejecutores en terminales sueltas en vez de
como windows de la flota.
La senal **fiable** es `$TMUX`: todo proceso dentro de tmux la tiene SIEMPRE, con
el formato `/tmp/tmux-<uid>/<socket>,<server_pid>,<client_id>`. De ahi se extrae
el socket (basename del path antes de la primera coma) y, con
`tmux -L <socket> display-message -p '#{session_name}'`, la sesion actual.
## Salida
```json
{"in_fleet":true,"in_tmux":true,"socket":"fleet3","session":"fleet3","source":"tmux"}
```
| Campo | Significado |
|---|---|
| `in_fleet` | Heuristica de "estoy en una flota". `true` si en tmux Y (socket/sesion casan `fleet`, O hay window `fleetview`, O la sesion tiene >= 2 windows). |
| `in_tmux` | `true` si el proceso esta dentro de tmux. Basta para lanzar una window (mejor que caer a kitty). |
| `socket` | Socket tmux derivado de `$TMUX` (o de `$FLEET_SOCKET` en fallback). |
| `session` | Sesion tmux actual resuelta con `display-message` (fallback a `$FLEET_SESSION` o al socket). |
| `source` | `tmux` (derivado de `$TMUX`), `fleet_socket` (fallback), o `none`. |
## Ejemplo
```bash
# Dentro de una window de la flota fleet3:
bash bash/functions/infra/detect_fleet_context.sh
# {"in_fleet":true,"in_tmux":true,"socket":"fleet3","session":"fleet3","source":"tmux"}
# Fuera de tmux, sin FLEET_SOCKET:
env -u TMUX -u FLEET_SOCKET bash bash/functions/infra/detect_fleet_context.sh
# {"in_fleet":false,"in_tmux":false,"socket":"","session":"","source":"none"}
# Parsear el socket con jq para pasarlo a spawn_fleet_agent:
ctx=$(bash bash/functions/infra/detect_fleet_context.sh)
sock=$(printf '%s' "$ctx" | jq -r .socket)
```
## Cuando usarla
Antes de lanzar un ejecutor de la flota: llama a esta funcion para saber si
estas dentro de una flota tmux. Si `in_tmux=true`, lanza con `spawn_fleet_agent`
(que ya la usa para auto-detectar el socket); NUNCA caigas a kitty. Tambien la
usa el hook `hook_fleet_state_inject.sh` para recordarle al orquestador el socket
de su flota cada turno.
## Gotchas
- Es **impura**: consulta el servidor tmux (`display-message`, `list-windows`).
No modifica estado.
- `in_fleet` es **heuristico** a proposito. Para LANZAR basta `in_tmux=true`
(lanzar una window en cualquier tmux supera a una kitty suelta). `in_fleet` es
solo la senal semantica que consume el hook y la doctrina.
- Fallback `source=fleet_socket`: si `$TMUX` no esta pero `$FLEET_SOCKET` si,
devuelve `socket`/`session` de esas vars con `in_tmux=false`. Un
`tmux -L <socket> new-window` puede seguir funcionando si el servidor existe,
aunque el caller no este attached.
- No requiere `jq` ni python: emite el JSON con `printf`, para poder ser el
detector base que invocan hooks y otras funciones bash.
- Si `tmux` no esta instalado y `$TMUX` esta seteada (raro), `socket` se deriva
igual de `$TMUX` pero `session` cae al fallback y `in_fleet` no se puede afinar
por windows.
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# detect_fleet_context — detecta de forma robusta si el proceso actual corre
# dentro de una sesion tmux de una flota FleetView, derivando el socket y la
# sesion de la variable de entorno $TMUX (senal fiable) en vez de depender de
# $FLEET_SOCKET (que a veces viene vacia en el entorno de un claude resumido o
# relanzado, aunque ese claude SI viva en una window de la flota).
#
# Por que $TMUX y no $FLEET_SOCKET:
# launch_fleetclaude exporta FLEET_SOCKET/FLEET_SESSION con `tmux
# set-environment -g`. Esa variable solo llega a los procesos que tmux arranca
# DESPUES de setearla; un claude relanzado o resumido a mano puede no heredarla
# y entonces $FLEET_SOCKET queda vacia. En cambio, todo proceso que corre
# dentro de tmux tiene SIEMPRE $TMUX seteada, con el formato:
# /tmp/tmux-<uid>/<socket>,<server_pid>,<client_id>
# De ahi se extrae el socket (basename del path antes de la primera coma) y,
# con `tmux -L <socket> display-message -p '#{session_name}'`, la sesion
# actual. Eso identifica el contexto fleet sin depender de $FLEET_SOCKET.
#
# Salida: JSON en stdout con los campos:
# in_fleet : true|false — heuristica de "estoy en una flota" (ver criterio).
# in_tmux : true|false — estoy dentro de tmux (basta para lanzar una window).
# socket : nombre del socket tmux derivado ("" si no hay).
# session : nombre de la sesion tmux actual ("" si no se resuelve).
# source : "tmux" | "fleet_socket" | "none" — de donde se derivo el contexto.
#
# Criterio de "flota reconocible" (in_fleet): estar en tmux (in_tmux) Y que se
# cumpla al menos uno, de mas fiable a menos:
# 1. el socket o la sesion casan el patron de flota (contienen "fleet"), o
# 2. existe una window llamada "fleetview" (la TUI de la flota), o
# 3. la sesion tiene >= 2 windows (una flota agrupa varios agentes en windows).
# Es heuristico a proposito: para LANZAR un ejecutor basta con in_tmux (lanzar
# una window en cualquier tmux es mejor que caer a una kitty suelta); in_fleet es
# la senal semantica que consume el hook del orquestador y la doctrina.
#
# Funcion IMPURA: lee el entorno y consulta el servidor tmux (display-message,
# list-windows). No modifica estado. Degrada limpio: si tmux no esta o falla
# cualquier consulta, devuelve los campos que pueda y nunca aborta con error.
set -euo pipefail
IFS=$' \t\n'
detect_fleet_context() {
local socket="" session="" source="none"
local in_tmux="false" in_fleet="false"
if [[ -n "${TMUX:-}" ]]; then
in_tmux="true"
source="tmux"
# $TMUX = /tmp/tmux-<uid>/<socket>,<server_pid>,<client_id>
# Socket = basename del path antes de la primera coma.
local tmux_path="${TMUX%%,*}"
socket="$(basename "$tmux_path" 2>/dev/null || true)"
# Sesion actual: tmux resuelve el cliente via $TMUX. -L fija el socket.
if command -v tmux >/dev/null 2>&1 && [[ -n "$socket" ]]; then
session="$(tmux -L "$socket" display-message -p '#{session_name}' 2>/dev/null || true)"
fi
# Fallback de sesion si display-message no resolvio nada.
[[ -z "$session" ]] && session="${FLEET_SESSION:-$socket}"
elif [[ -n "${FLEET_SOCKET:-}" ]]; then
# No estamos en tmux pero hay FLEET_SOCKET exportada: usarla como ultimo
# recurso (un claude que perdio $TMUX pero conserva la env del perfil).
in_tmux="false"
source="fleet_socket"
socket="${FLEET_SOCKET}"
session="${FLEET_SESSION:-$socket}"
fi
# Heuristica in_fleet: solo tiene sentido si estamos en tmux.
if [[ "$in_tmux" == "true" && -n "$socket" ]]; then
local sl="${socket,,}" sesl="${session,,}"
if [[ "$sl" == *fleet* || "$sesl" == *fleet* ]]; then
in_fleet="true"
elif command -v tmux >/dev/null 2>&1; then
# Construir el target de sesion sin trucos de expansion fragiles.
local -a tgt=()
[[ -n "$session" ]] && tgt=(-t "$session")
# window "fleetview" presente => flota.
if tmux -L "$socket" list-windows "${tgt[@]}" \
-F '#{window_name}' 2>/dev/null | grep -qx 'fleetview'; then
in_fleet="true"
else
# >= 2 windows => agrupacion tipo flota.
local nwin
nwin="$(tmux -L "$socket" list-windows "${tgt[@]}" \
-F x 2>/dev/null | wc -l | tr -d ' ')"
[[ "${nwin:-0}" -ge 2 ]] && in_fleet="true"
fi
fi
fi
# JSON sin dependencias (jq/python no requeridos: este es el detector base).
printf '{"in_fleet":%s,"in_tmux":%s,"socket":"%s","session":"%s","source":"%s"}\n' \
"$in_fleet" "$in_tmux" "$socket" "$session" "$source"
return 0
}
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
detect_fleet_context "$@"
fi