Repository navigation
The documented self-location recipe is unexecutable: filters={"is_caller": true} silently returns [] #92
Description
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:
src/libtmux_mcp/_utils.py#L841—_apply_filterscallsitems.filter(**coerced)on theQueryList, then serializes the survivors.
But is_caller is 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_caller
And the lookup silently degrades to False rather 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 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 thatis_calleris not a filterable field. - Control: call
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_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 rowsAlternatively, 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_identityto return a deterministicCallerIdentitypointing 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 withis_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
- is_caller annotation gives false positives across tmux sockets #19 —
is_callerannotation gives false positives across tmux sockets (same annotation, adjacent defect) - Companion issue: add a
whoamiprimitive so the instructions no longer have to advertise this recipe at all.
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.
- The documented self-location recipe is unexecutable: filters={"is_caller": true} silently returns [] #92 — [bug] documented self-location recipe is unexecutable (
filters={"is_caller": true}→[]) - Add a self-location primitive (where_am_i): agents must dump every pane on the server to learn which pane they are in #93 — [feat]
where_am_iself-location primitive + self-first relational routing (root fix) - The caller's tmux socket is advertised in the instructions but never targeted: an MCP inside
tmux -L workqueriesdefault#94 — [bug] caller's socket advertised but never targeted;TMUXenv race vs. the threadpool - The list_* family is unbounded, unscoped, unprojected — and explicitly excluded from the response limiter #95 — [perf]
list_*unbounded, unscoped, unprojected, excluded from the response limiter - Surface invocation context (in tmux? server running? which socket?) and make tmux-server auto-start explicit #96 — [feat] invocation context (in tmux? server running?) + explicit tmux-server auto-start
- No self-location API (TMUX_PANE), and the socket_path derivation in Server.__init__ is unreachable dead code libtmux#704 — upstream: no self-location API (
TMUX_PANE); unreachablesocket_pathderivation
(← this issue: #92)
Activity