Review round on #758. bash.run() only promises to resolve for nonzero exits, timeouts, and aborts, and bash.resolve() can reject on policy grounds, so either could escape the serial agent/step listener and abort the model turn — contradicting the plugin's documented failed-query no-op contract. Contain both and log a warning instead; the location is optional context. The Agent Note claimed an unchanged location suppresses the query. It does not: only the interval floor is checked before the query, while change suppression compares state the query returned. Corrected in both languages and re-recorded the i18n pairs.
8.4 KiB
Agent Note:tmux 位置上下文
Status: implemented
English | 中文
问题
运行在 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/ 的可选启用型函数插件,与其他既不定义工具也不定义服务的有界请求上下文增强并列。随附示例不挂载它,因为 tmux 位置披露及其 token 成本属于部署策略。
在每轮的第一个 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(在插件加载时手动校验为非负安全整数)会额外抑制距最近一次注入不足该窗口的注入。
文本
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 布局,并在位置变化时按轮次更新。部署方通过 cordis.yml 选择启用;默认 spine 与随附示例保持沉默。在真实 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(),而非检查一条独立关系。它能报出的每种失败都必须先修改本包,而这些本包的管线测试已经覆盖。仅当出现插件自身并不计算的关系时才重新引入伴生检查——例如读数将来具备可被其他包破坏的跨轮次顺序或包裹义务。