# Agent Note：tmux 位置上下文

Status: implemented

[English](2026-07-27-tmux-location-context.md) | 中文

## 问题

运行在 tmux 内的 agent 无法告诉模型自己身在何处：进程占据哪个 session、window、pane，以及 window 如何布局。当用户操作多个 pane 时，希望模型能对自身位置有所定位，从而让"下方的 pane""这个 window"之类的指令得以解析。位置必须以持久、可重建的上下文形式送达模型，而非在原地被改写的系统提示值，并且当位置未变化时不产生任何成本。

tmux 无需守护进程即可暴露这些信息：`$TMUX_PANE` 标识进程所在 pane，`tmux display-message -t "$TMUX_PANE" -p '<format>'` 可打印任意 pane/window/session 字段。待决问题在于如何观测——在每次准备时拉取，还是由 tmux hook 推送——以及如何避免逐步骤 token 成本与隐藏的进程内状态。

## 决策

`@deepseek-ai/dsh-tmux-context` 是位于 `packages/context/tmux-context/` 的可选启用型函数插件，与其他既不定义工具也不定义服务的有界请求上下文增强并列。已交付的 TUI 会挂载它，因为终端复用器上下文是该界面特有的；`dsh-agent-spine-demo` 与 Web／无头界面保持沉默。

**在每轮的第一个 step 拉取，而非 tmux 推送。** 插件前置注册一个 `agent/step` 监听器，仅在 `step === 1` 时动作。拉取模型无需后台进程、无需在用户的 tmux 中安装 hook、也无需清理；它每轮重新读取当前状态，因此被移动、改名或重新布局的 pane 都会被自然感知。以第一个 step 为门槛使读数按轮次生成：位置在一轮内是稳定的，逐步骤重复查询只会增加成本而不带来新信息。轮次中途移动的 pane 会在下一轮反映，这是换取更简单设计所接受的取舍。

**通过 `ctx.bash` seam 读取，绝不用裸 `child_process`。** 监听器通过 `ctx.bash` 运行 tmux／`ps` 只读命令，从而应用部署方的沙箱与策略，插件不拥有任何子进程代码。`ctx.bash` 缺失、tmux 环境缺失、字段数不符或 pane id 为空，都会使本次尝试成为空操作，与 `workspace-context` 在无 `fs` provider 时的空操作一致。

**以 tty 判定真实 pane，而非仅凭 `$TMUX_PANE`。** `$TMUX_PANE` 会被继承：从 tmux shell 启动的终端（VS Code 集成终端、桌面启动器）会从该祖先进程带上 `$TMUX`／`$TMUX_PANE`，即使进程并不位于那个 pane 中，否则就会注入一个陈旧且错误的位置。命令用 `ps -o tty= -p <pid>`（在进程内传入 agent 自身的 pid）解析本进程的控制终端，并与 pane 的 `#{pane_tty}` 比较；只有匹配时才输出字段。真正的 pane 拥有本进程的 tty；继承而来的环境指向的是另一个 pane 的 tty，因而被读作"不在 tmux 中"。改为检查 `$TMUX` 也无济于事——它同样会被继承。这是决定性的判别依据，且无需维护终端模拟器名单。

**仅自身位置与布局。** 查询字段为 session name、window index/name、pane index/id、window/pane 活动标志以及 `window_layout`。省略 pane 与 window 像素尺寸（布局树已传达结构；尺寸嘈杂且每次终端缩放都会变化）。从不采集相邻 pane 内容（`capture-pane`），使读数保持小巧，并避免抓取无关、可能敏感的输出。

**仅在变化时注入，并可选间隔下限。** 需要时，插件调用 `agent.inject()` 注入一条来源为 `{ kind: 'plugin', plugin: 'tmux-context' }` 的 `user/message`。变化抑制将渲染出的状态块（轮次前缀行之后的全部内容）与该来源的最近一次注入比较，后者通过扫描原始持久会话事件获得——因此调度可跨压缩与进程恢复存续，无需进程内缓存。可选的 `refreshIntervalMs`（在插件加载时手动校验为非负安全整数）会额外抑制距最近一次注入不足该窗口的注入。

### 文本

```text
tmux location (turn <turn>):
session <session>, window <index> "<name>", pane <index> <pane-id>
window active=<0|1>, pane active=<0|1>, layout <window-layout>
```

轮次前缀是易变的首行；其下的两行状态块才是变化抑制所比较的单元，因此重新注入由 tmux 状态驱动，而非循环位置。

### 持久性与请求重建

每条读数在被压缩遮蔽前都是普通表层节点；插件对系统提示装配毫无贡献，`request/header` 也不携带任何 tmux-context 文本。读数记录的是一次准备尝试，而非已提交的 step：由于前置监听器最先运行，当后续 `agent/step` 监听器取消或失败时其追加可能仍会保留，只追加的日志不做回滚。

发布的 `./invariant` 伴生插件不注册任何运行时检查：读数是外部 tmux 状态的按轮快照，会话中不存在需要校验的跨事件关系，调度与格式由本包的管线测试固定。

## 后果

启动于 tmux 内的 agent 现在会以持久、带来源标记的上下文收到自身的 session/window/pane 位置及 window 布局，并在位置变化时按轮次更新。已交付的 TUI 选择启用；自定义部署可直接组合该插件。在真实 tmux pane 之外——包括仅继承了 `$TMUX`／`$TMUX_PANE` 的终端——或没有 `ctx.bash` 执行器时，插件保持惰性且不报错，因此在任何地方组合它都安全。由于读数是一条持久的 `user/message`，它作为普通历史经受压缩，对系统提示装配与请求头毫无贡献，且每个发生变化的轮次至多花费一条两行消息。拉取模型在每个到期轮次的第一个 step 增加一次 `tmux display-message` 子进程（经沙箱化的 bash seam）。可选的间隔下限在查询之前检查，因此同时抑制查询与注入；而位置未变化只能通过比较查询返回的状态得知，因此它只抑制注入，查询开销仍会付出。

## 测试

单元测试固定了：首个 step 的注入及来源/表层元数据；以 `$TMUX_PANE` 为键的命令（含其 `#{pane_tty}` 与 `ps -o tty=` 的比对守卫）；step 门槛；跨轮次的变化抑制与 pane 移动时的重新注入；正间隔抑制与阈值；每条空操作路径（无 bash、非零退出、字段数不符、pane id 为空、信号已取消，以及 `resolve()` 或 `run()` 抛出的执行器拒绝被兜住并记录警告而非使该轮失败）；前置排序先于普通 `agent/step` 监听器；对损坏的历史读数（非文本块、单行文本）的容错；以及配置对负值与非整数间隔的拒绝。逐文件覆盖率为 100%。

## 考虑过的替代方案

- **由 tmux hook / 后台监视器推送**——否决：需要在用户的 tmux 中安装 hook，并引入带清理的后台进程，只为换取按轮次上下文并不需要的步内新鲜度。
- **每个 step 都运行**——否决：位置在一轮内稳定；重复查询只增加 token 成本而无新信息。以 `step === 1` 为门槛得到按轮次读数。
- **裸 `child_process`**——否决：绕过沙箱/策略 seam，并手写 `ctx.bash` 执行器已拥有的子进程代码。
- **包含 pane/window 像素尺寸**——否决：尺寸每次缩放都变动、徒增噪声；布局树已传达结构。
- **用 `capture-pane` 抓取相邻 pane**——否决：庞大、嘈杂且涉及隐私；超出"自身位置"范围。
- **动态系统提示区块**——否决：替换某个值会抹去支撑先前推理的历史读数且不可重建；单条持久且带来源的消息在每个位置变得可见时予以记录。
- **信任 `$TMUX_PANE`（或 `$TMUX`）存在即可**——否决：两者都会被从 tmux shell 启动的终端（VS Code 集成终端）继承，于是非 pane 进程会注入陈旧位置。pane 的 `#{pane_tty}` 与本进程控制终端的比对才是决定性检查。
- **对已知终端模拟器设黑名单（如 `TERM_PROGRAM=vscode`）**——否决：名单不完整且会不断增长，仍会漏掉其他启动器；tty 比对精确且与启动器无关。
- **用运行时 invariant 校验每条读数的轮次、位置与格式**——最初随包发布，随后移除：它从日志中重新推导生产者自身的调度，并对同一个包刚刚渲染出的文本断言正则，因此只是重述 `apply()`，而非检查一条独立关系。它能报出的每种失败都必须先修改本包，而这些本包的管线测试已经覆盖。仅当出现插件自身并不计算的关系时才重新引入伴生检查——例如读数将来具备可被其他包破坏的跨轮次顺序或包裹义务。
