feat(infra): exponer pane_id (%N) estable en el JSON de la flota

El orquestador identificaba cada agente por el campo tmux_window (@N), pero
el window_id de tmux cambia cuando un pane entra/sale de windows (el focus de
la flota usa break-pane + join-pane, que recrean windows). El pane_id (%N) en
cambio es estable durante toda la vida del pane: es el identificador correcto.

- claude_fleet.go: nuevo campo ClaudeFleet.PaneID `json:"pane_id"`. Se mantiene
  TmuxWindow (lo necesita el focus internamente); esto AÑADE pane_id, no lo
  reemplaza.
- resolve_pane_ids.go (+ .md, .go test): nueva función del registry
  ResolvePaneIDs(socket, pids) -> map[pid]pane_id. Lista los panes del socket
  (tmux -L <socket> list-panes -a) y para cada PID sube por el árbol de procesos
  (PPID en /proc) hasta dar con un pane_pid. Reutiliza runTmux y procPPID del
  paquete infra. Best-effort: tmux/socket caído o PID sin pane -> "" sin crash.
  Núcleo testeable con inyección de la salida tmux y del resolvedor de PPID.
- list_claude_fleet.go: ListClaudeFleet() puebla PaneID resolviendo cada PID
  vivo contra $FLEET_SOCKET (default "fleet"). Solo la entrada pública lo hace;
  ListClaudeFleetFrom() queda intacta (cero coste tmux en tests y en el bucle
  de render de fleetview).

Tag de grupo: orchestration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:19:55 +02:00
parent 8e16202935
commit fb76b53c17
6 changed files with 275 additions and 6 deletions
+36 -2
View File
@@ -42,13 +42,47 @@ type runtimeFile struct {
// ListClaudeFleet scans the current user's ~/.claude directory and returns the
// fleet of Claude Code sessions known to the machine. It is a thin wrapper over
// ListClaudeFleetFrom resolving the home directory.
// ListClaudeFleetFrom resolving the home directory, plus it populates each
// member's PaneID ("%N") by resolving it against the fleet tmux socket.
//
// The socket comes from $FLEET_SOCKET, defaulting to "fleet". Resolution is
// best-effort: if tmux/the socket is unavailable, every PaneID is left "" and
// the fleet is still returned. PaneID is only populated here (the public
// registry entry point), not in ListClaudeFleetFrom, so consumers that call the
// core directly in a hot loop (and the unit tests) pay no tmux cost.
func ListClaudeFleet() ([]ClaudeFleet, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("resolve home dir: %w", err)
}
return ListClaudeFleetFrom(filepath.Join(home, ".claude"))
fleet, err := ListClaudeFleetFrom(filepath.Join(home, ".claude"))
if err != nil {
return nil, err
}
populatePaneIDs(fleet)
return fleet, nil
}
// populatePaneIDs resolves each alive member's pane_id ("%N") against the fleet
// tmux socket ($FLEET_SOCKET, default "fleet") and writes it into PaneID. It
// mutates fleet in place. Best-effort: tmux/socket down -> every PaneID stays ""
// (ResolvePaneIDs returns an empty map), no crash. Only alive PIDs are queried;
// a dead PID has no pane to resolve.
func populatePaneIDs(fleet []ClaudeFleet) {
socket := os.Getenv("FLEET_SOCKET")
if socket == "" {
socket = "fleet"
}
pids := make([]int, 0, len(fleet))
for _, f := range fleet {
if f.Alive {
pids = append(pids, f.PID)
}
}
byPID := ResolvePaneIDs(socket, pids)
for i := range fleet {
fleet[i].PaneID = byPID[fleet[i].PID]
}
}
// ListClaudeFleetFrom scans claudeDir (e.g. ~/.claude) and returns the fleet of