Skip to content

The documented self-location recipe is unexecutable: filters={"is_caller": true} silently returns [] #92

Open

Description

@tony

Summary

The server instructions tell every agent, verbatim, how to answer "which pane am I in?":

Tool results mark is_caller=true; filter list_panes for it to answer 'which pane am I in?' (no whoami tool).

That instruction cannot work. _apply_filters applies filters to the raw libtmux QueryList of Pane objects before serialization, but is_caller is a serialization-time computation — it does not exist as an attribute on libtmux.Pane. libtmux's keygetter returns None for the unknown attribute and filter_lookup then returns False for every row, so the call returns an empty list.

It fails silently: no error, no warning, just []. That is the worst possible failure shape for an LLM — the agent concludes "I'm not in any pane" or, more commonly, falls back to pulling the full unbounded listing and post-filtering in its own context. Which is precisely the expensive path the instruction appears to be steering it away from.

Evidence

The instruction, in _BASE_INSTRUCTIONS:

". Tool results mark is_caller=true; filter list_panes for it to answer "
"'which pane am I in?' (no whoami tool)."

Filters are applied to un-serialized libtmux objects:

But is_caller is only produced during serialization:

And the lookup silently degrades to False rather than raising:

Reproduction

Environment: libtmux-mcp==0.1.0a17, MCP server launched from inside a tmux pane on the default socket.

  1. Call list_panes(filters={"is_caller": true}).
  2. Observed: [] — an empty list, no error.
  3. Expected: exactly one PaneInfo (the caller's own pane); or, failing that, an explicit error stating that is_caller is not a filterable field.
  4. Control: call list_panes() with no filters and observe that exactly one row does carry is_caller: true. The field is real; it just cannot be filtered on.

Suggested fix

Two independent changes; both are worth making.

1. Make the advertised filter actually work. is_caller is a synthetic, serialization-time field, so it must be filtered after serialization. Split the filter set in _apply_filters:

_SYNTHETIC_PANE_FIELDS = {"is_caller"}

def _apply_filters(items, filters, serializer):
    coerced = _coerce_dict_arg("filters", filters)
    if not coerced:
        return [serializer(item) for item in items]
    synthetic = {k: v for k, v in coerced.items() if k in _SYNTHETIC_PANE_FIELDS}
    native = {k: v for k, v in coerced.items() if k not in _SYNTHETIC_PANE_FIELDS}
    rows = [serializer(item) for item in (items.filter(**native) if native else items)]
    for key, want in synthetic.items():
        rows = [r for r in rows if getattr(r, key, None) == want]
    return rows

Alternatively, reject unknown filter keys with an ExpectedToolError rather than returning [] — a silent empty result is strictly worse than a loud failure.

2. Stop routing self-location through a full listing at all. Even a working filters={"is_caller": true} still pays a full list-panes -a server-side; it only saves tokens, not tmux work (_apply_filters materializes the whole QueryList and filters in Python). The right primitive is a dedicated self-location tool (where_am_i) — see the companion issue. Once it exists, delete the filter list_panes for it sentence from _BASE_INSTRUCTIONS entirely.

Tests

In tests/test_pane_tools.py (or tests/test_utils.py):

  1. Monkeypatch _get_caller_identity to return a deterministic CallerIdentity pointing at a known pane on the test server.
  2. Assert list_panes(filters={"is_caller": True}) returns exactly that one pane — this fails today, returning [].
  3. Assert list_panes() (unfiltered) contains exactly one row with is_caller is True, proving the field is populated and the bug is in the filter path only.
  4. Add a regression test that an unknown/synthetic filter key does not silently yield [].

Related

Environment

  • libtmux-mcp: 0.1.0a17
  • libtmux: 0.61.0
  • fastmcp: 3.4.3 / mcp: 1.28.1
  • tmux: 3.7b
  • Python: 3.14.0

Cross-references — filed together from a single audit of v0.1.0a17. The self-location gap is the root cause; the rest are what it exposed.

(← this issue: #92)

Activity

  1. added
    bugSomething isn't working
    on Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions