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.
- Call
list_panes(filters={"is_caller": true}).
- Observed:
[] — an empty list, no error.
- Expected: exactly one
PaneInfo (the caller's own pane); or, failing that, an explicit error stating that is_caller is not a filterable field.
- 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):
- Monkeypatch
_get_caller_identity to return a deterministic CallerIdentity pointing at a known pane on the test server.
- Assert
list_panes(filters={"is_caller": True}) returns exactly that one pane — this fails today, returning [].
- 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.
- 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)
Summary
The server instructions tell every agent, verbatim, how to answer "which pane am I in?":
That instruction cannot work.
_apply_filtersappliesfiltersto the raw libtmuxQueryListofPaneobjects before serialization, butis_calleris a serialization-time computation — it does not exist as an attribute onlibtmux.Pane. libtmux'skeygetterreturnsNonefor the unknown attribute andfilter_lookupthen returnsFalsefor 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:src/libtmux_mcp/server.py#L208Filters are applied to un-serialized libtmux objects:
src/libtmux_mcp/_utils.py#L841—_apply_filterscallsitems.filter(**coerced)on theQueryList, then serializes the survivors.But
is_calleris only produced during serialization:src/libtmux_mcp/_utils.py#L977—is_caller=_compute_is_caller(pane)inside_serialize_panesrc/libtmux_mcp/_utils.py#L153—_compute_is_callerAnd the lookup silently degrades to
Falserather than raising:libtmux/_internal/query_list.py#L535—keygetterreturnsNonefor an unknown attribute;filter_lookupthen evaluatesFalsefor every pane.Reproduction
Environment:
libtmux-mcp==0.1.0a17, MCP server launched from inside a tmux pane on thedefaultsocket.list_panes(filters={"is_caller": true}).[]— an empty list, no error.PaneInfo(the caller's own pane); or, failing that, an explicit error stating thatis_calleris not a filterable field.list_panes()with no filters and observe that exactly one row does carryis_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_calleris a synthetic, serialization-time field, so it must be filtered after serialization. Split the filter set in_apply_filters:Alternatively, reject unknown filter keys with an
ExpectedToolErrorrather 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 fulllist-panes -aserver-side; it only saves tokens, not tmux work (_apply_filtersmaterializes the wholeQueryListand filters in Python). The right primitive is a dedicated self-location tool (where_am_i) — see the companion issue. Once it exists, delete thefilter list_panes for itsentence from_BASE_INSTRUCTIONSentirely.Tests
In
tests/test_pane_tools.py(ortests/test_utils.py):_get_caller_identityto return a deterministicCallerIdentitypointing at a known pane on the test server.list_panes(filters={"is_caller": True})returns exactly that one pane — this fails today, returning[].list_panes()(unfiltered) contains exactly one row withis_caller is True, proving the field is populated and the bug is in the filter path only.[].Related
is_callerannotation gives false positives across tmux sockets (same annotation, adjacent defect)whoamiprimitive so the instructions no longer have to advertise this recipe at all.Environment
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.filters={"is_caller": true}→[])where_am_iself-location primitive + self-first relational routing (root fix)tmux -L workqueriesdefault#94 — [bug] caller's socket advertised but never targeted;TMUXenv race vs. the threadpoollist_*unbounded, unscoped, unprojected, excluded from the response limiterTMUX_PANE); unreachablesocket_pathderivation(← this issue: #92)