Skip to content

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

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    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