Skip to content

Grouped and linked windows crash every pane/window-targeted tool: MultipleObjectsReturned escapes the error mapper #97

Description

@tony

Every tool that resolves a pane_id or window_id raises an opaque Internal error against a grouped session or a linked window. Grouped sessions are ordinary usage — tmux new-session -t <existing> — and are what tmuxp creates.

pane_id is the identifier the server instructions explicitly tell agents to prefer.

Reproduction

s = Server(socket_name="probe")
a = s.new_session(session_name="alpha")
s.cmd("new-session", "-d", "-t", a.session_name, "-s", "grouped")   # ordinary grouped session

s.panes.get(pane_id="%0", default=None)
list-panes -a rows for %0 -> [('%0', '$0'), ('%0', '$1')]

  server.panes.get(pane_id=%0)     -> RAISED MultipleObjectsReturned  (msg='')
  server.windows.get(window_id=@0) -> RAISED MultipleObjectsReturned  (msg='')
     isinstance LibTmuxException? False

Why it escapes everything

tmux list-panes -a / list-windows -a emit one row per holding session, so a grouped or linked window matches twice. QueryList.get() raises MultipleObjectsReturned on more than one match before it consults default, so default=None does not absorb it.

MultipleObjectsReturned subclasses bare Exception, not LibTmuxException. So it misses every arm of _map_exception_to_tool_error and lands in the catch-all, surfacing as ToolError("Unexpected error: MultipleObjectsReturned: ") — with an empty message, expected=False, logged at ERROR.

rg MultipleObjectsReturned across src/ and tests/: zero hits. Never caught, never tested.

Blast radius

Both central resolvers:

  • _resolve_pane_utils.py:716, server.panes.get(pane_id=..., default=None)
  • _resolve_window_utils.py:646, server.windows.get(window_id=..., default=None)

which means send_keys, capture_pane, get_pane_info, get_window_info, select_window, rename_window, move_window, split_window, wait_for_text, and others.

The self-kill guards are on the same path — session_tools.py:264 and window_tools.py:331 both do server.panes.get(pane_id=caller.pane_id, default=None) — so the guard raises instead of guarding.

list_windows also returns duplicate rows for a linked window.

Fix

Route the resolvers through libtmux's point lookups instead of filtering a server-wide listing:

Pane.from_pane_id(server=server, pane_id=pane_id)
Window.from_window_id(server=server, window_id=window_id)

These target the object with -t and let tmux's own cmd_find resolve it, so exactly one row comes back.

Sequencing matters. On the pinned libtmux>=0.61.0 those constructors still scan the whole server with -a and keep the last matching row — ordered by session name — so migrating today would trade a crash for a silently arbitrary session (the session you get would change if someone renamed a session). tmux-python/libtmux#713 rewrites them to ("-t", <id>). Do the migration after that lands and you get tmux's own answer; do it before and you inherit the arbitrary one.

So: this is not blocked on libtmux for the crash (you could catch MultipleObjectsReturned today), but the correct fix wants libtmux#713 released first.

Interim mitigation

If a fix is wanted before that release, MultipleObjectsReturned at minimum needs an arm in _map_exception_to_tool_error so the agent gets an actionable message rather than an empty Internal error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions