Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Development

#### `capture_since` reads its cursor from libtmux

The cursor machinery behind the `capture_since` tool — anchor arithmetic,
history-trim re-anchoring, the stable double-read, and the serialized cursor
format — now comes from libtmux's `Pane.capture_since()` instead of living
here. The tool keeps the part that is an MCP concern: bounding a response with
`max_lines` and `max_bytes` so one observation cannot blow an agent's context
window.

The cursor wire format is unchanged, so cursors issued by earlier versions
still decode. A cursor that no longer describes its pane now raises libtmux's
`CaptureCursorError` family, which maps to an agent-facing error advising a
fresh cursor rather than the generic tmux-error wording.

### Documentation

#### opencode joins the install picker
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ lint = [
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv.sources]
# TEMPORARY: tracks the libtmux branch that adds Pane.capture_since().
# Drop this block and the matching uv.lock entry once that lands in a
# released libtmux. https://github.com/tmux-python/libtmux/pull/741
libtmux = { git = "https://github.com/tmux-python/libtmux.git", branch = "capture-since" }

[tool.uv.exclude-newer-package]
# git-pull packages release in lockstep with their workspaces, so a
# fresh release blocking on the 3-day cooldown blocks every
Expand Down
11 changes: 11 additions & 0 deletions src/libtmux_mcp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,17 @@ def _map_exception_to_tool_error(fn_name: str, e: BaseException) -> ToolError:
f"Pane not found: {e}",
suggestion="Call list_panes to discover valid pane ids.",
)
if isinstance(e, exc.CaptureCursorError):
# Not a tmux failure — the cursor the agent replayed no longer
# describes the pane it was taken from, so the generic "tmux
# error:" prefix would point at the wrong thing to fix.
return ExpectedToolError(
str(e),
suggestion=(
"Call capture_since without a cursor to start a fresh "
"observation of this pane."
),
)
if isinstance(e, exc.LibTmuxException):
return ExpectedToolError(f"tmux error: {e}")
logger.exception("unexpected error in MCP tool %s", fn_name)
Expand Down
4 changes: 4 additions & 0 deletions src/libtmux_mcp/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@ async def on_call_tool(
libtmux_exc.ObjectDoesNotExist,
libtmux_exc.MultipleObjectsReturned,
libtmux_exc.PaneNotFound,
# Covers InvalidCaptureCursor and PaneLifecycleChanged. A cursor that
# does not describe its pane describes it no better on a second look,
# and a respawned or dead pane does not un-respawn.
libtmux_exc.CaptureCursorError,
libtmux_exc.NoWindowsExist,
libtmux_exc.BadSessionName,
libtmux_exc.TmuxSessionExists,
Expand Down
Loading
Loading