用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Jasonzhangf/zterm --skill terminal-buffer-truth命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | terminal-buffer-truth |
| description | terminal buffer / render / daemon mirror 真源与门禁 |
resource.renderer_window may hand an explicitly immutable projected snapshot to resource.client_sparse_buffer's render-store projection edge only after renderer-side equality/reuse checks complete. This removes duplicate full-window comparison/copy work; it does not change wire payload, buffer truth, visible-range ownership, or daemon behavior.任何 terminal / session / daemon / buffer / renderer 相关开发、修复、重构,必须先完成架构映射,再读代码和修改代码。
固定顺序:
android/docs/architecture.mdandroid/docs/audits/2026-07-02-architecture-boundary-remediation.md
同时读 android/docs/decisions/2026-09-05-runtime-memory-truth.md 区分当前实现与目标;参数/初始化/路由目标不从旧经验段落重建。android/docs/resource-registry.json 与 android/docs/resource-map.md,确认 source/target resource、直接/间接关系、via_resources、禁止直连关系修改前必须明确:
pnpm --dir android run daemon:mirror:close-loop。pnpm --dir mac test -- --reporter dot 与 pnpm --dir mac run type-check,其中必须覆盖 bridge-transport、local-tmux-transport、terminal-runtime、workbench active target。禁止事项:
mux-hello 后,wire 上只允许 TerminalMuxServerFrame / TerminalMuxClientFrame。sessions、session-activity、target error/pong)只走 mux-target-message;session terminal 业务只走 mux-channel-message / mux-channel-binary。BridgeServerMessage。唯一物理 sender contract 必须使用 shared protocol 的 wire-frame union,禁止 as unknown as 绕过类型锁。terminal-mux-channel-runtime.ts 必须删除 channel registry、关闭 subscriber、投影显式 mux-channel-closed error,并停止 attach。isTerminalMuxServerFrame。session-activity 或错误的 mux-channel-message;publish throw 后不得留下 channel、subscriber transport 或继续 attach。tmux truth
-> daemon server
-> client buffer manager
-> client sparse buffer
-> renderer window
-> DOM renderer
-> terminal shell
-> UI shell
四层只允许单向依赖,禁止越层漂移。
client.renderer_window 只拥有 follow / reading / renderBottomIndex / visible range / immutable render snapshot / next-RAF commit。client.dom_renderer 拥有 TerminalView / VisibleRow / TerminalPreviewRow / mirror-fixed zoom-pan / shared cell render / terminal theme 的 DOM 投影,禁止请求 transport、修改 sparse truth 或自己决定 follow/reading/renderBottomIndex。client.terminal_shell 拥有 stage shell、shell skin、status、quickbar assembly、copy menu、keyboard lift,只能消费 renderer/DOM projection 并发出 shell intent。client.renderer_window 直接承担 DOM 投影,也禁止 client.terminal_shell 持有 sparse/render 真源。terminal / daemon / client / renderer 相关任务完成前,先写清本轮影响到哪一层,再跑到对应层级。低层验证不能冒充高层完成。
pnpm --dir android run test:feature-registry -- --reporter dot、pnpm --dir android exec tsc -p tsconfig.json --noEmit --pretty false、平台对应 type-check。pnpm --dir android run daemon:mirror:close-loop。pnpm --dir mac test -- --reporter dot 与 pnpm --dir mac run type-check。bridge-transport、local-tmux-transport、terminal-runtime、workbench active target。sessionBufferStore -> TerminalPageStageShell -> TerminalView 渲染,断言 header/active id/name 与 DOM body marker 同源;同时模拟旧 session 迟到 publish、pause/resume、layout/IME resize,证明旧 body 不会出现在新 active 名字下。pnpm --dir android run build:android。汇报必须给出 versionName、versionCode、APK 路径、sha256;有 online ADB 设备时继续安装/启动/真机 smoke,没有设备时明确 L5 缺口。.app 或唯一 dev Electron 实例,截图/DOM/进程证据,必要时资源采样。{ data, sentAt } 这类 object envelope 直接放进现有 input.payload,旧 daemon 会把 object 写成 [object Object] 进入 tmuxinput_invalid,不得解包执行、不得隐式 stringificationswitchRuntime 布尔语义:
restore-sync
explicit-resume
resumeActiveSessionTransport / ensureActiveSessionFreshexplicit-resume,否则现场会出现“当前 tab 黑屏不刷,切到别的 tab 再切回来才首连首刷”。session.state === connected、terminal page 显示 connected、activeSessionId 命中,都不是 transport freshness 真源SessionContext -> ensureActiveSessionFresh / buildActiveSessionRefreshPlanclosed/error/tmux_session_unavailable 只属于 transport / attach fact,不得被 App 直接映射成 open-tab 物理关闭tmux_session_unavailable 也不得从非 active、非 live session 投影成当前 UI 的错误/重连 banner。抽屉打开、session picker refresh、foreground audit 发现 stale persisted tab 时,只能记录缺失事实并停止该 session 的自动 retry;不能 emit SESSION_STATUS_EVENT(type='error'),不能让缺失的旧 tab 污染当前 active session。closeSessionOPEN_TABS 为唯一真源 materialize;runtime sessions 只补 transport/state。若 persisted open tab 的 runtime shell 缺失,UI 必须保留 closed placeholder;只有用户显式 resume/open 才允许按 persisted tab 重建 runtime shell 和 transport,禁止用 open tabs ∩ runtime sessions 过滤导致“看起来自动关闭 tab”。OPEN_TABS 的物理身份只能是 sessionId;sessionName + daemon/bridge owner 语义 key 不得用于 normalize/upsert/runtime-merge/close 时合并、替换、删除已打开 tab。semantic key 只允许用于 saved-list import 去重与用户显式 close 后的 tombstone。ZTERM_TERMINAL_BACKEND=wezterm,禁止在 Windows 上误走 tmux。Start-Process node server.cjs 只能当 direct smoke,不能当持久服务结论;OpenSSH job 生命周期可能带走子进程。持久运行真源必须是 Windows Scheduled Task ZTermDaemon 或后续明确的服务 owner。$PID/$pid 是只读自动变量,Start-Process 的 stdout/stderr 不能指向同一文件,New-ScheduledTaskSettingsSet 参数集不能按 PowerShell 7 猜。Unexpected token '';Windows runner 必须用 no-BOM UTF-8 写配置,不能依赖 Set-Content -Encoding UTF8 这种默认行为。wezterm.exe 可直接找到,必须显式探测/固化 ZTERM_WEZTERM_EXE 或安装目录。127.0.0.1:<port>、本机 Tailscale IP <100.x>:<port>、远端设备到 <100.x>:<port>;前两者成功不等于 Android/Mac 经 Tailscale 可达。buffer-sync target,并通过 daemon control 精确关闭本轮 session;只验 CLI、create/connect/input,或通过 SSH 绕过 daemon 清理,都不算闭环。WezTermBackendRuntime#closeSession,tmux 才允许 tmux kill-session。禁止让兼容 wire 名 tmux-kill-session 把实现锁死到 tmux。server / daemon 是独立层,只做:
buffer-head-requestbuffer-sync-requesttmux -> daemon mirror writer -> daemon mirror store -> read api -> client
tmux -> mirror storemirror store -> head/range replyhistory capture + visible capture + concat,只允许
single-capture -> canonicalize -> mirror storepane read --source recent --lines N 是
adapter 的 history tail truth,canonical frame 的可见 tail 是 live
overlay。两者只能在 Herdr adapter 内合并成一个 mirror snapshot;mirror
store 层不得再拆第二语义,不得把 1000 行 history 放进 33ms capture loop。top/htop/vim 持续变化会多次重采样并进入 failure backoff,表现为刷新很慢。buffer-head-request / buffer-sync-request 只是读当前 mirrortmux -> daemon mirror truth commit -> daemon push buffer-sync -> client buffer apply -> renderer body repaintmirror body unchanged -> push buffer-head/infomirror body changed -> push buffer-sync diffactive tab / foreground / follow / reading / visible range / viewport / pane layoutdaemon.buffer_publisher 的 per-subscriber pending/flush 消费,不得拖低 mirror capture cadencedaemon.buffer_publisher per-subscriber 处理,不得降低 mirror capture cadence;否则 TUI/status bar 的下一次原地更新会被 polling 先天限速。硬规则:
head/range 请求路径里“先同步 tmux 再回复”bodySubscribed=false、tab 暂停取数不销毁 mirror;仍存在 physical subscriber 时 transport detach 只影响对应 subscribernew-session;远端 tmux session 不存在时只能显式报 tmux_session_unavailable,显式创建唯一走 tmux-create-session。
invalid pane metrics / pane is dead 不是 tmux_session_unavailable;它只表示 mirror capture/pane target 故障,不得 release subscriber / close client runtimesession.statemirror.stateterminalWidthMode / requestedAdaptiveColsresize / terminal-width-mode 做成 daemon 内部状态推进入口logical client sessionclientSessionId 成为 daemon 内部长期状态 ownerreadyTransportIdsession transport token / attach-resume state machineactive tab / foreground / background / pane / viewport / visible range / width modeclientSessionId / sessionTransportToken / session-ticket 的额外冻结:
buffer manager 是独立 worker,不归 daemon、不归 renderer。
它的唯一职责:
补充冻结:
buffer-head 到达时,buffer manager 只更新 metadata / planner 输入cursor 变化也只更新 metadatabuffer-head 携带 cursor / cursorKeys metadata 时,可以更新本地 metadata truth,但不得调用 scheduleSessionRenderCommit() 或发布 render body;否则旧 body 会在真正 buffer-sync apply 前被重新投影,表现为“先闪旧 buffer,再被新 buffer 覆盖”。incomingRevision < localRevision 必须显式 drop、记录 debug、请求当前 tail;同 revision payload 若会改写当前本地已有 non-gap absolute-index 行,也必须显式 drop。只有同 revision 且命中本地 gap 的 payload 才能作为 gap repair 合并。禁止用“先清空 buffer/DOM 再刷新”掩盖旧 payload 污染。local window invalid 只说明“当前工作窗口理解错了”,不说明已有 buffer truth 作废anchor mismatch / head mismatch 也一样;它们只影响下一次 request plan,不影响已有 absolute-index 内容的存在性startIndex/endIndex 为空、lines=[] 的 payload 只能表示“新 buffer 尚未 ready”;client 必须保留上一帧等待非空或明确范围的 buffer-sync,禁止先发布空 buffer 导致黑屏再刷新每次 tick:
补充冻结:
cacheLines 语义混写两者;retention 只表示本地保留跨度,不是拉取目标follow / reading / renderBottomIndexlocal window invalid / anchor mismatch / head mismatch 实现成“先清空已有本地 buffer 再重拉”startIndex/endIndexbuffer-sync span 覆盖当前 authoritative tail,且 span 长度超过当前配置的本地 retentionstartIndex 更老而裁成 head windowincoming.endIndex == nextTailEndIndex 但 nextEndIndex << nextTailEndIndex;这会先发布旧历史窗口,再被下一帧尾部 patch 拉回,表现为旧 buffer 闪屏buffer-sync 是较新 revision、覆盖了当前窗口一部分、但 endIndex < authoritativeTailEndIndexincoming.startIndex < current.startIndex 把窗口重新锚到更老位置previousEndIndex == tail,随后 nextEndIndex < tail,再被同 revision 追加尾部 patch 拉回;这会在底部持续更新时闪旧 buffer[10606,11606) + incoming [10592,11601) + tail 11606,并证明非 tail reading/prepend 窗口仍可移动buffer-sync 只能建立在连续 revision 基线上:
[startIndex,endIndex) 窗口,client 不得把该 sparse diff 合并成本地 body truthrenderer 只看三件事:
local render buffer tail:buffer manager 已提交给 renderer 的本地内容底部renderBottomIndex:当前要显示窗口的底部visible range:当前要画的 absolute rows额外门禁:
buffer-sync applyrevision=N 的 render body 后,任何 revision<N 的 render snapshot 都必须拒绝发布并记录 session.render-store.revision-regression-dropdeleteSession() / 重建 session),不得由迟到旧 snapshot 隐式覆盖U+2580..U+259F(block / shade / quadrant)不能按普通文本 glyph 渲染;这类字符在 TUI/tmux 里经常承担“背景块/色块”语义。fg/bg 生成 fill/gradient/pattern;若直接渲成普通字形,现场会表现成“红绿背景变灰”。color-mix(...) 渲染 ░▒▓;应在 JS 侧直接算出最终 RGB,否则现场可能退成灰/透明。它不关心:
renderer 还必须显式区分两种宽度模式:
adaptive-phone
colsmirror-fixed
1ch / 2ch 当终端列宽真相2 * measuredCellWidthPx 渲染;浏览器 fallback 字体的 ch 不是 tmux 列宽真相zoom
加 renderer 在 paint 前重派生可见行数。禁止把 zoom 直接写进 .term-grid,
禁止改写 tmux/daemon rows/cols,禁止等普通 effect 晚一帧同步行数。scrollTop;.term-grid 保持物理行高与普通 padding,
不得用 translateY 模拟纵向滚动。renderer 的 viewportRows 必须按
clientHeight / (physicalRowHeight * visualScale) 向上取整,visible demand
与 repair range 随之扩大;follow/bottom sync 在 visualScale < 1 时不得被抑制。renderBottomIndex 对齐到最新底部renderBottomIndex只允许三种:
除此之外,live update / 补 gap / 尾部推进,都不能把用户拉回 follow。
UI 只负责容器位置与裁切:
硬规则:
状态浮窗 与 绝对行号 都属于 UI shell observability,但必须 解耦
状态浮窗 只负责连接/刷新/模式观测绝对行号 必须由独立显式开关控制,不能再隐式绑定到状态浮窗visualViewport.offsetTop 不得再被当成第二份 top inset 叠到 header 上cols/rows 当成 tmux viewport 真相带给 daemon;容器变窄/变矮、IME 弹起、safe-area、前后台恢复,都只能影响 shell 裁切与 renderer 可见窗口adaptive-phone 若需要适配手机,最多只改 colsattach / resize / width-mode reconcile 也不得再写第二份 rows;rows 真相只能来自上游 tmux / mirror baseline状态 / ↑ / 键盘 与 ← / ↓ / →状态 只替换老附件位;↑ 与 键盘 保持老位置状态 / 键盘 文案,不能裁切、顶出或超界keyboardInsetPx > 0,QuickBar 必须作为整体容器抬升到键盘上方;同一份 keyboard inset 只能消费一次:terminal stage.bottom = quickBarHeight + keyboardLift,quickbar shell.bottom = keyboardLift,禁止再用 QuickBar 内部 padding/margin 对同一份 inset 二次抬升onMeasuredHeightChange 必须上报自身真实 chrome 高度,不得再扣 keyboardInsetPx;IME lift 已由外层 shell bottom 消费。若测量阶段扣 inset,quickBarHeight 会在键盘弹出时归零,导致 stage 丢失快捷栏预留。以下一律视为错误实现:
ws close -> daemon 推导客户端状态并修改 mirror/tmux 生命周期inactive tab -> close session / close transportreconnect -> new client session semanticsbuffer-head-request / buffer-sync-request 路径里触发 tmux capturetmux kill-sessionmirror-fixed 下把长行本地重排成手机宽度mirror-fixed 下在没有 horizontal pan 手势链的情况下把 tab swipe 禁用,造成无横向交互出口local window invalid -> empty local buffer -> full reanchoranchor mismatch -> clear local truthhead mismatch -> treat local content as lost先落 docs / AGENTS / skill
-> 再补测试
-> 再改代码
-> 再跑真实回环
顺序错了,视为没按真源做。
其中测试与检查清单真源固定为:
android/docs/daemon-mirror-test-plan.mdandroid/docs/terminal-test-loop-checklist.mdtmux truth
-> daemon log
-> renderer declare visible range
-> client buffer manager log
-> renderer commit log
-> Android APK 真实画面
最少覆盖:
buffer-sync 前,terminal 可见内容不得本地直接变化buffer-sync 到达后,renderer 只回显 payload,不得自己再造 prompt/cursor 第二语义char / fg / bg / flagslines 不得因 cursor 改变上述 case 不能只靠人工重试。
必须把问题收敛成:
可复现的本地 case
-> 可失败的自动测试
-> 修复后稳定转绿
-> 纳入每次编译前回归
只要某个 terminal 线上问题还不能被本地自动 case 复现,就不允许说“根因已收敛”。
最低自动回归覆盖:
top / vim / input echo新增门禁精华:
buffer-sync 下行长期几百 KB/s 甚至 MB/s,先直接抓 daemon 回包;若仍返回 legacy lines[].cells[] 而不是 compact i/t/w/s,优先查 daemon service staged runtime 没更新,尤其是 start/restart 只重启 launchd 但没重建 ~/.wterm/daemon-runtime/server.cjs。start/restart 必须重建当前 staged runtime;服务异常必须显式失败,不能 fallback 回 tmux session 掩盖旧 runtime/旧语义。daemon mirror close-loop 必须使用隔离测试端口;禁止复用用户常驻 service 端口(如 3333),否则脚本会误连现场 daemon,出现“自动回归假绿 / 假红”。daemon mirror close-loop 的 client replay harness 也必须服从 revision reset 真相:daemon 重启后若 revision 回到更小值,回放时必须先 reset local buffer 再 apply;否则会把回环假红误报成 daemon/client 主链故障。buffer-head-request;若 daemon 仅 revision 前进而 latestEndIndex 不变,buffer manager 仍必须带一次性 same-end tail refresh demand,确保 head -> sync -> body repaint 闭环成立。session.state 先分叉,否则会把“label stale but transport alive”误杀成重连。visibilitychange / resume / appStateChange,SessionContext active tick 不得再自行读取 document.visibilityState 做第二份停刷判定。SessionContext lifecycle 必须把 active session 送入唯一 explicit-resume 入口,和冷启动恢复共用同一 transport owner,否则会重演“杀进程秒连、后台回来卡死”的双路径分裂。sessionState=reconnecting + ws=null + no pending open intent,优先判定为 stale reconnect bookkeeping,不是“真的还在连”。foreground/explicit refresh 必须允许重新 reconnect;不能让粗粒度 reconnectInFlight=true 永久挡住恢复。看到这些现象,优先判对应层:
pullHz == 0 && renderHz == 0:优先查 active tab 首次激活后是否根本没进入 head-first 主循环connecting 且 hidden tabs 同时在连:优先查 active-only transport gate 是否被破坏ImeAnchor 已经产生日志,但 client 侧出现 session.input.queue 且长期无刷新:先判定为 active transport 已死,不是 IME 问题;active tab 在 resume / switch / input 这三个动作上,只要发现没有 live ws,就必须立即 reconnect,不能只排队等下一次偶然恢复bridgeHost / bridgePort / authToken,再去打 /health、/debug/runtime、WebSocket probeconnect -> head -> input -> head change;如果 direct probe 是几十毫秒,而现场 session 仍是几十秒,就先把 generic daemon 基线排除,转查现场 session / IME / active transport 链路session.buffer.request 已发出、daemon direct probe 也能直接拿到非空 range,但 APK 仍首屏空白/R=0,优先判定为 client 侧 buffer-sync -> local apply -> renderer commit 断链;先补本地结构化证据,不要再回头怪 daemonImeAnchor 的 stale show/focus 状态是否跨前后台遗留;只有显式 keyboard action 才允许 show IMEtargetHeadRevision 变了,必须允许重发buffer-head.cursor 是否被 client 丢弃;head 已经带来的 cursor metadata 必须立刻进入本地 truth,不能等下一次 buffer-sync 才纠正高亮/光标terminal.keyboard_ime normalization owner 归一成文本分隔空格;显式 Enter 继续走 editor action / hardware key 独立路径,禁止 daemon、transport、renderer 再过滤语音换行tmux -> daemon mirror -> client sparse buffer -> immutable render store -> shared TerminalView -> preview DOM。TerminalView 作为 read-only shared renderer 使用:active=false、live=true、无 input/resize/viewport callbacks、allowDomFocus=false、mirror-fixed。terminalCellStyle()/row view-model,按相同 style run 聚合成少量 span,保留 ANSI fg/bg/flags;禁止退回纯文本 theme.foreground 或另写第二套 color parser。sessionId + bridgeHost + bridgePort + sessionName,有 daemonHostId 时也必须匹配;stale target 是失效选择,不是隐式 open/reconnect intent。PARTIAL_WAKE_LOCK(保留 WAKE_LOCK 权限)供现有 control-plane / target-transport owner 做低频心跳;必须保持 bounded lifecycle,sessionCount <= 0 / stop 即 release 并 stopForeground(true) / stopSelf()。它禁止拥有 socket、route、session truth,禁止请求忽略电池优化权限,禁止替 transport owner 开/建连接、做 body/video 订阅或后台保活。TerminalStageShell 替换后继续刷新:选中 session 新 marker 出现在真实 shell DOM,旧 session marker 被排除,物理 socket 不重建,subscribers 恢复 baseline。{ activeSessionId, slotIds, focusSlot }。关闭按钮、右滑退出、Android system Back 都走唯一 cancel owner 并恢复该快照;tile tap activation 必须先清除快照再执行显式 switch。Back listener 只在 preview open 生命周期注册。commitBuffer() 是否把 live buffer 引用直接塞进 store 并被引用短路。wezterm cli 输出不是 daemon truth;ZTerm adapter 必须把 get-text --escapes 转成自己拥有的 absolute mirror snapshot 后才允许进入 buffer-sync 链。get-text --escapes 正文里;Windows backend 必须从 wezterm cli list --format json 读取 cursor_x/cursor_y/cursor_visibility,并作为独立 metadata 进入 mirror snapshot / buffer-head.cursor。wezterm cli --prefer-mux send-text --pane-id <id> --no-paste 只允许通过 stdin 写真实 terminal input,禁止把用户输入塞进 shell args;已验证 Enter / Backspace / arrow escape / raw TUI / Codex TUI text entry。wezterm,非 Windows 默认 tmux,未知 backend 必须显式报错。cmd.exe 子进程;不要宣称完整键盘等价。TerminalSourceAdapter.readSnapshot() 产出的
TerminalSourceMirrorSnapshot;mirror 主链禁止散点执行 tmux/Herdr/WezTerm
source 命令,source 差异全部收口到 adapter。revision 只是 source-side 信息字段;zterm
mirror.revision 由 daemon.mirror_store 的 runtime scheduling owner 唯一
推进,禁止让 source revision 直接变成 zterm revision broadcast,也禁止
daemon.mirror_writer 推进 revision。daemon.mirror_writer 唯一执行;resource.mirror_store 仍是 canonical
mirror truth / revision / runtime scheduling 的唯一 owner。adapter 只是把
source readback 归一成 mirror snapshot,不新增第二真源或第二 writer。pane read <pane_id> --source recent --lines <min(terminalCacheLines,1000)> --format ansi --raw 的 adapter-owned tail
snapshot;canonical frame 仍只提供 live visible rows / cursor / geometry /
cursorKeysApp / alternateScreen。pane get.scroll 的 maxOffsetFromBottom + viewportRows 只作为 source
total-row hint 推进 daemon-owned tail window;禁止把它当成 absolute line
identity,禁止用内容 overlap 推断 index。sourceEndIndex 只单调增长,bufferStartIndex 必须等于
sourceEndIndex - bufferLines.length,availableStartIndex 同
bufferStartIndex;Herdr 0.8.0 只能提供最近 1000 行,禁止本地拼接或
滚动遍历伪造更早历史。pane read 1000 行禁止进入 33ms loop;live frame 到达通过
onLiveActivity 唤醒 mirror live sync,无 body subscriber 时由现有
scheduleMirrorLiveSync 停止,不产生空转。pane read 失败或返回空必须显式失败,禁止 frame-only 24 行兜底;官方
1000 行上限写入显式 capability gap herdr-history-limit-1000。pane get scroll metrics 从每帧同步读取改为低频节流(默认 100ms),
metrics 读取失败不得丢弃 frame。sessionId + visibleRange + tailEndIndex + targetRevision 维护 per-session
ledger:pending -> dispatched -> fulfilled,dispatched 超时未收到完整
visible-window authoritative response 可再次 dispatch,但不能无限放大。pending;send-fail/no-socket 不清 ledger。gapRanges 或 localRevision == daemonHeadRevision 代替行级
freshness;sparse 后续 revision 不得掩盖旧 non-gap visible 行。pending / dispatched 且未超过 stale timeout 才在重发窗口内抑制,否则后续
sparse demand 必须创建新 ledger entry 继续收敛。targetRevision 标记 fulfilled,不创建以 response revision 为
key 的合成 entry;payloadRevision >= entry.targetRevision 且 payload
连续覆盖 entry 的 request range 才满足。daemon.input_queue 唯一 owner 是
src/server/daemon-input-queue-runtime.ts;receive validation、byte cap、
stale/session-required retryable nack、seq dedupe、enqueue/dispose、
backend write ordering 都在该文件加
src/server/terminal-reliable-input-ack.ts。terminal-message-runtime.ts 只把 input 和 plain-text 帧转给 queue
owner;terminal-control-runtime.ts 只向 queue 暴露
writeBackendInputGroup / chunk budget,不再持有 queue internals。daemon-input-queue-runtime.test.ts、
terminal-message-runtime.test.ts、
terminal-control-runtime.input-queue.test.ts、mirror/backpressure/
detached-session 回归。setOnLongClickListener(v -> true) 只管系统 ActionMode / 工具栏,setLongClickable(false) 才会停掉原生 haptic / selection 拦截,让 JS onTouchStart 的长按计时器真正启动。preventDefault / stopPropagation,但不能指望它单独压住 Android 原生长按行为。ShellWorkspace,不要只测未接入入口组件;packaged smoke 必须用进程路径证明启动的是 mac/out/.../ZTerm.app。mirror.rows/cols/bufferStartIndex/bufferLines/cursor 只能由 tmux capture/readback owner 写入。viewportCols 或 active/focus 状态重排 terminal content。attach/resume -> tmux geometry/head -> buffer-head -> buffer-sync -> local apply 首包链路;不要在 renderer 层用 CSS wrap、row background、scrollbar、border、默认 theme、synthetic cols/rows 或 forced layout key 去“修排版”。adaptive-phone 也不是 client 本地排版:正确链路只能是 client 上报 measured cols -> daemon adaptive lease owner 请求 tmux reflow -> tmux capture/readback 更新 mirror truth -> client 固定行高渲染。viewportCols / widthMode 放进 render geometry revision key、无 tmux truth 时补 80 cols、修改 shared renderer row/cell/theme 背景来掩盖旧 buffer 或空 buffer、用 UI border/scrollbar/IME 变化触发 terminal content geometry refresh。buffer-head-request 到达当前 active resource,head 后按 tmux availableStartIndex/latestEndIndex/rows 拉 buffer-sync;renderer 测试只证明消费 fixed mirror truth,不证明或制造排版。codesign --force --deep --sign - <ZTerm.app>;缺少这一步或使用 open -n 启动 unsigned package,会被 Launch Services 在后续启动时重新走 Gatekeeper 并把 bundle 标成“恶意软件/移到废纸篓”。open -n unsigned package。正确启动是直接执行 "<ZTerm.app>/Contents/MacOS/ZTerm" --remote-debugging-port=<port> --user-data-dir=<evidence>/user-data --no-sandbox,并把 stdout/stderr、exit code 落到 evidence。test -d mac/out/mac-arm64/ZTerm.app;缺失时先 pnpm --dir mac run package,禁止继续跑 blackbox gate。spctl --assess rejected 不等于 unsigned internal alpha 启动失败。真实判定必须看进程路径 + CDP /json/version + /json/list 是否返回 ZTerm Mac page target。ps -axo pid,ppid,comm,args | egrep 'ZTerm|--remote-debugging-port=<port>',再按明确 PID 关闭;禁止多实例叠在同一个 port 上。TerminalStageShell -> PaneStage -> .wterm -> ResizeObserver/clientHeight -> renderer viewport demand 的实际尺寸链;shell stage wrapper 必须是可计算高度的 column flex 容器,不能只设置 flex: 1。TerminalView 是 DOM 高度和 viewportRows 的唯一 owner。TerminalPage / pane 编排层禁止读取 buffer.rows、buffer.endIndex 伪造非 active pane 的 viewport demand;该路径会把 buffer truth 和 renderer window 混成第二语义,并可能在 shrink 后黑屏。.wterm clientHeight > 0、renderer 上报的 viewportRows 随高度变化、absolute tail 行连续、没有 data-terminal-line-discontinuous;再恢复原尺寸确认 geometry 和尾行继续收敛。open-tab-intent 这类 core truth 模块不得出现 fallbackActiveSessionId / fallbackSessionIds 式命名;需要保留 active 或选择关闭后的下一个 active 时,必须写成显式 policy(如 preserveActiveSessionId / nextActiveCandidateSessionIds)并由架构 gate 锁住。open-tab-persistence 读写失败不得变成空 truth:存储损坏、读取异常、写入异常必须返回显式 failed/invalid 或 { ok:false, error },调用方至少记录结构化 runtime debug。useSessionOpenActions 这类 session-open owner;App.tsx、TerminalPage、header、drawer 只能传 intent,不能自己构造 Host 或执行 closeSession -> createSession -> switchSession 生命周期序列。adaptive-phone 只能进入唯一 adaptive width lease owner:
adaptive-phone connect/resize 注册当前 physical transport subscriber 的 { cols, heartbeatAt }。cols,并只在 applyAdaptiveTmuxWidth() 内请求 tmux resize-window -x <cols> 让 tmux 自己重排。mirror-fixed、invalid cols 或 heartbeat 过期后必须清理该 subscriber metadata 并重算;最后一个 lease 消失必须在 releaseAdaptiveTmuxWidth() 恢复/释放 tmux 宽度控制权。widthMode、terminalWidthMode、requestedAdaptiveCols 不得写入 daemon 业务真相;resize-window / window-size 只允许出现在 adaptive lease owner 的 apply/release 函数里。mirror.rows/cols;mirror 内容和尺寸仍只能来自 tmux capture/readback。traceId + mirrorRevision + subscriberId 关联独立样本;同 session 的不同 revision 不得被拼成一个伪 latency。完整阶段是 capture -> canonicalize -> mirror commit -> send -> client rx -> buffer apply -> RAF -> render commit,只允许有界 metadata ring 和 p50/p95/p99 summary。bodySubscribed 这一物理事实,不保存 active/inactive/visible/foreground 原因;buffer-sync,不得 close transport、detach mirror、禁用 input/file/schedule 或 explicit head/range read;bodySubscribed 的 physical subscriber demand 驱动;unsubscribe 必须经同一个 scheduler owner 立即停旧 timer,恢复 demand 后恢复 scheduler,不得由 head/range 请求直接 capture。detachSubscriberTransportOnly。该路径禁止杀 tmux;若 detach 后 mirror 已无任何 physical subscriber,只允许由 terminal runtime 释放 daemon-owned mirror/buffer/input/timer 资源;仍有 sibling subscriber 时必须保留 mirror。不得在 Android UI/renderer/buffer 层做退出补偿。buffer-sync,覆盖原 span 的每一行;禁止裁成 live tail,否则超过一屏的大刷新会静默丢掉 source rows;daemon.buffer_publisher(src/server/daemon-buffer-publisher-runtime.ts);terminal-mirror-runtime.ts 只调用 publisher 的 broadcast/flush 接口,不得重放 pending/backpressure/head cache/frame split 语义。buffer-sync-request 的 range response 必须进入 publisher 的独立 per-subscriber FIFO lane;不得复用 live pending-latest 合并语义,必须保留请求范围和 requestSentAt,并受同一 backpressure/frame-split/send-budget 边界约束。pane identity/history_size/rows/cols/alternate/captured count;sendText 必须进入同一 accounting owner,记录 bytes/total/error/buffered-before-after/duration/backpressure transition;禁止为统计重新 stringify terminal payload。buffer-head 只允许更新 head metadata / cursor metadata / planner 输入buffer-sync apply 可以触发正文 body repaintbuffer-head.latestEndIndex 只表示 daemon mirror 的 authoritative tail absolute index(bufferStartIndex + bufferLines.length),不是 row-level body freshness。诊断 input/tail 行旧内容时,禁止用 latestEndIndex 或全局 localRevision == daemonHeadRevision 直接证明可见非 gap 行已新鲜;必须比较 source row / payload row / client sparse row / DOM row。buffer-sync 只覆盖 payload 中的 row/range。若一个可见非 gap 行曾漏刷,后续单行 status/footer sparse patch 可能推进全局 buffer revision 并掩盖该旧行。此类问题必须加“source row cleared once -> client missed non-gap row -> later tiny sparse patch -> DOM still stale”的黑盒 gate,再改 sparse apply / visible-window repaint authority;禁止用 QuickBar、reconnect、header repaint、DOM 清空补偿。bufferStartIndex / 同文本 / 新 revision / 新 cell style 会更新 DOM。terminal-page-shell-ui / TerminalPageDebugOverlay 的单一投影与 fixed overlay,不得改 daemon/buffer/renderer truth 来迎合 chrome。lines[].cells[].flagspane_current_path;client 不得拿 process.env.HOME / 本地 env 冒充远端 cwdclientSessionId 是 client-owned session identitysessionTransportToken / session-ticket 是 attach-only wire materialsessionTransportToken 放进长期 transport runtime store;它只能是 handshake 期间的临时 attach 材料openRequestId = client-local open intent correlationsessionTransportToken = daemon one-shot attach proofclientSessionId 放回 daemon token owner / primary wire correlation 语义server.ts 只保留 transport/http glueserver.tsbuffer-sync 的 in-flight / pull bookkeeping 只是 transport bookkeeping,不是 buffer truth;active tab 重新进入、resume、reconnect 时不得让旧 bookkeeping 永久挡住新的 head-first 请求链lastServerActivityAt 过旧、缺 pong、缺 head、pong-only traffic 都不得让仍为 WebSocket.OPEN 的长连接过期。active tab 恢复 / 重新进入 / tick 只能在原 transport 上 request-head / ping 观测;只有物理 close/error、send 抛错、daemon 不可达、用户显式关闭、或 tmux/session target 事实变更,才允许重建。bridgeHost:bridgePort:authToken 不足以判定 same target;同 daemon 的 Tailscale/WS、rtc-direct、TURN rtc-relay、Relay endpoint candidates、transportMode、daemon/relay identity 必须影响 target key,Relay lastSeenAt 这类 freshness metadata 不得影响 key。否则会复用错误 OPEN socket,出现有上下行速率但可见 body 不更新。clientSessionId、不认 control/session 两级客户端状态机closed / websocket detach 只表示 transport failure,必须进入 retryable reconnect owner;terminal 终态关闭只认显式业务真相,例如 tmux_session_killed。禁止把普通 closed 映射成本地 tab/session closed,否则 WebSocket 断开会卡死不重试。killTmuxSession 后刷新 catalog),禁止把 remote:* id 交给 local open-tab close。capturing -> transferring -> preview-readycapturing / transferring 都必须有显式失败边界;不允许无限 spinnersave / discard文件 = 本地文件选择并上传到当前 session图片 = 本地图片选择并上传到当前 session同步 = 打开远程文件同步页 / FileTransferSheet截图 = 远端截图预览流maxRuns=0 表示无限次,默认 3firedCount / endAt / stop condition 只能由 daemon 维护;client 只编辑和展示follow / reading 模式、拉取/刷新状态ACTIVE_SESSION 是最后激活 tab 的唯一持久化真相ACTIVE_SESSIONACTIVE_SESSION 恢复 active tab,ACTIVE_PAGE 只决定页面种类,不得反向覆盖keyboardInsetPx 清零,否则会被输入法盖住ImeAnchor,则 ImeAnchor editable / composing / selection 必须是单一真相;组合输入期间不得一边让 IME 持有 composing state,一边又由插件自行清空/改写 editable 造成第二语义ImeAnchor 的 InputConnection 也必须服从这条真相:commitText / finishComposingText 不能跳过 super 直接短路返回;否则 framework editable/selection 不更新,真机会出现 输入法底部预编辑光标错位 / caret 乱飞mirror-fixed 下,UI shell 若启用横向查看:
touchmove/touchendmirror-fixed 下若当前客户端并没有独立 horizontal pan 手势链在生效:
adaptive-phone 下若保留左右滑切 tab,该手势也必须属于 UI shell interaction surface:
createSession(connect:false)closed/error/unavailable session,生命周期链只能 skip 或读取当前 live transport;不得自动 reconnect。foreground resume 不再是独立语义,必须映射成 explicit-resume,由唯一 reconnect/open owner 判定是否重新打开 daemon session。adaptive-phone 是 daemon adaptive width lease,不是 renderer 后处理:
resize-window -x 进行真实重排。adaptive-phone attach/resize 的 invalid cols 必须在进入任何 throwing geometry normalizer 前被显式拒绝:缺失 / NaN / <=0 cols 返回 adaptive_width_cols_invalid,daemon 进程必须继续存活。测试 helper 禁止用 cols || default 掩盖真实 daemon 的 strict normalizer;invalid-input 红测必须使用 strict normalizer,并最好补真实 WebSocket probe。mirror-fixed 是 client render crop / pan policy:不得注册 adaptive width lease,不得改变 tmux width。adaptive-phone 的 tmux side effect 必须单点化:只允许 terminal-mirror-runtime.ts 的 adaptive lease owner 执行 resize-window -x 和 final release 的 set-window-option -u window-size;禁止 daemon-start、renderer、UI、foreground/background、普通 resize/attach 分支散落执行。send-keys、create/kill/rename session),但 adaptive width 不是 tmux 请求。请求结果不能被 daemon 预测写入 mirror truth;mirror.rows/cols/bufferStartIndex/bufferLines/cursor 只能由 tmux 回读 / capture owner 写入。assertTmuxSessionExists、control runtime、capture runtime 执行 set-option ... alternate-screen off 或其它 window/session option 变更;若历史 daemon 留下 alternate-screen off,只能作为一次性现场清理 set-window-option -u -t <target> alternate-screen,代码真源必须物理删除副作用并加 gate。sessions[],也必须立刻持久化 OPEN_TABS / ACTIVE_SESSION;不能因为“这次不是 restore 分支”就跳过首次回写,否则下次冷启动恢复会拿到陈旧 tab 真相。buffer-sync 后变化;若是,再直接比 daemon payload 的 prompt/input row char/fg/bg/flags。buffer-sync 明明持续收到,但 localRevision/localEndIndex 长时间不前进、client 反复请求同一 3 屏窗口,优先查 client 侧 incoming buffer-sync apply 阶段;收到即更新本地 buffer truth,不要再叠微任务批处理/延迟 flush 第二语义。reading-repair / visible-gap repair 的 client 判重与 in-flight cover 必须纳入当前 missingRanges / gap 拓扑语义;同一 knownRevision/localWindow/requestWindow 下,只要可见区 gap 变了,就必须允许再次发 repair。否则现场会出现:页面局部空白,手动上下划一下(viewport 改变)后才补刷。reading-repair / missingRanges 的 daemon 响应必须是 连续 authoritative span,禁止返回“外层 request window 很大,但 lines 只包含多个非连续 gap 行”的带洞 payload。若一次请求有多个 gap,响应只能返回从第一个 gap 到最后一个 gap 之间的完整行 span,或未来协议显式拆成多个独立连续 buffer-sync;当前 client apply/render 不接受中间带洞窗口,否则会保留旧行并在刷新时闪旧 buffer。buildTerminalViewportDemandWithRepair;平台 view 只传入 local buffer window + gap ranges,不得复制 missingRanges 计算。demand key 必须纳入非空 missingRanges,无 gap 时不发送空数组以保持旧 payload 形状。env(safe-area-inset-top) 做第二份 safe-area 计算。ACTIVE_SESSION;ACTIVE_PAGE.focusSessionId 只描述页面焦点,不得反向覆盖已恢复的 active session。ws.readyState === OPEN,不得仅因后台静默一段时间就直接重连;必须先用同一条 ws 做 buffer-head-request / ping / 必要的 session 状态查询。只有 socket 物理 close/error、用户显式 reconnect、或当前 session 已无可用 ws 且属于 explicit-resume/open,才允许新建 ws。CONNECTING socket 或 pending open intent,不得因等待预算超时自动 force-replace。继续等待同一条 pending ws,并把等待状态显式投影到 SessionContext;不能创建第二条 session ws。网络通断事件不是 reconnect 依据,只是触发现有 ws 的协议探测。reconnectRuntime.connecting / stale reconnect bookkeeping 只是 client 本地编排观测,不是 transport failure truth;foreground resume / tab re-entry / online 不得因为它返回 reconnect 或创建第二条 ws,只能显式显示等待或在 socket 物理 close/error 后进入真正 open/reconnect owner。force-replace API 不属于 lifecycle/probe/input/foreground/online 恢复路径;禁止把 stale activity、missed pong、pong-only、foreground resume、tab re-entry、online 事件映射成 cleanup old socket -> fresh connect。物理 close/error 进入唯一 reconnect/open owner,不能由 UI 或 buffer 层清理 live socket。buffer-head-request / buffer-sync-request 若允许调用方显式传 ws,都必须先校验:该 ws 仍是当前 session 的 active transport socket;旧 superseded socket 只能被物理关闭或忽略,绝不能继续拿来发 head/range 请求污染当前 transport 真相。onopen/onmessage/onerror/onclose;凡是“旧 ws 回调里继续触发 request/head/probe”的路径,也必须有同样的 active-socket gate,否则 stale transport 仍会在写侧继续推进错误状态。38:2::r:g:b / 48:2::r:g:b / 38:5:n / 48:5:n)的 colon 语法,进入 parser 前必须先规范化到当前唯一支持的 semicolon 语法;否则颜色会退回 default sentinel,现场表现就是红/绿背景丢失或发灰。cleanup old socket -> fresh connectTerminalCell 真相一致;当前 app/runtime 里的默认前景/背景是 256/256,不能在 compact encode/decode 里偷偷改成 ANSI 15/0。bg=256 必须绘制为 theme.background,不能映射为 transparent。row / cell wrap / gap fill 也必须主动 paint terminal theme background,禁止让外层容器或页面背景替代 terminal 背景。bg=256 -> theme.background 修复后真机仍“无变化”,下一步必须用 WebView DevTools 读取 live localStorage['zterm:bridge-settings'].terminalThemeId、.wterm computed background、最近可见 row/cell computed background。若 active preset 自身的 theme.background 是纯黑,继续改 renderer 是错路;应修 shared theme preset 真源,并用 live DOM 证明 computed background 变化。buffer-sync apply -> schedule RAF -> RAF 时读取当前 live buffer 一次;render gate 只做 frame coalescing,不能再消费 network/transport cadence,也不能保存会晚发布的旧 scheduled snapshot。measureTerminalViewport() 的 glyph probe 是否把单个字符测成接近整屏宽;cell 宽度必须拒绝异常整屏测量并回退到字体估算,禁止在页面层补第二份宽度逻辑。online 事件恢复 active tab transport;恢复动作只能复用现有 active resume/audit/follow reset 主线,hidden online 不恢复,禁止扫所有 session。send-keys -l 的 argv 可接受不等于 PTY 长输入稳定;必须用 source SHA-256 与 tmux target file SHA-256 自动比较,并把 byte-exact source/target gate 与 mirror recovery gate 分开stty -echo / sink prelude,并等待 ready marker 后再流式发送 body;禁止把关闭 echo 与大 payload 混在同一 burst 后把回显洪泛误判成 input byte lossSessionBufferState -> SessionRenderBufferSnapshot 必须产出 immutable render snapshotlines / gapRanges / cursor 不得把 live buffer 引用直接交给 renderer/storeframeChunkCount > 1,还必须先审 client buffer owner 是否逐 chunk publish:
generatedAt 只作为 frame identity,不得替代 revision/absolute index 做 freshness 排序resource.client_buffer_frame_assembly truth;repair 未实际写入 wire 时保持 pending,只有 dispatch 成功才改为 dispatched,每个 revision 最多 dispatch 一次0;same-revision interleave 只修原 pending frame range。无合法 revision 时等 authoritative live head;成功 repair 后用有界 per-session revision ledger 跨后续成功 revision 保留 dispatch 事实,迟到 malformed payload 不得对旧 revision 二次 dispatch;新 frame supersede 旧 pending frame 时清掉旧 repair error,但保留 ledgerTerminalView.dynamic-refresh / renderer 回归。禁止先凭截图改代码、再补测试。captureMirrorAuthoritativeBufferFromTmux() 必须实际调用稳定化主线,覆盖 transient half-frame 不发布;同一 mirror 的 totalAvailableLines 必须以当前 mirror end 为单调下界,避免 alternate-screen 短可见窗口把 absolute tail 拉回 pane height。tmux_session_unavailable / 网络短断 / handshake 临时失败只能进入 retryable error/reconnect,禁止发 SESSION_STATUS_EVENT(type='closed'),也禁止触发 open-tab prune;只有明确 terminal close 语义才允许进入 tab close 链。client snapshot source -> collectClientDebugSnapshot -> active session WS debug-snapshot -> daemon store 这一条链;禁止再开第二条 relay/debug transport 或散落页面内临时上报。DebugRegistry duplicate registration 是 fail-fast;测试文件只要 import 真实 client-debug-snapshot 并 render 真实 TerminalPage/App,resetClientDebugSnapshotForTests 与 cleanup 就必须放 file scope。把 hooks 放在某个 describe 内会让后续 describe 残留 producer,报 duplicate debug producer: terminal-page,此时先修测试隔离,不能把生产 fail-fast 改成静默替换。pendingInputTailRefresh 的 buffer-head 请求必须放到 coalesced microtask,后续 burst input 在 pending 清除前合并,禁止每键强制刷 head 或把 head 请求绑回 key event stack。ImeAnchor key 必须直接走 shared terminal keyboard resolver 并写入 active session。plain letter 留给 editable/IME 文本路径,Ctrl/Alt 组合键和方向/Esc 等特殊键走硬件 key path;红测必须让 allowDomFocus=false 时 Ctrl+C 仍到达 terminal input。onResize 改 tmux rows。cursorRowIndex + 1,让首屏显示 prompt 而不是空白下 pane;tail window 有真实内容或 cursor 已靠近 tail 时仍必须保持 tail follow。cursor metadata 不能作为 body repaint 源。client.terminal_shell 的 flex geometry:承载 PaneStage 的 absolute stage wrapper 必须是 display:flex; flex-direction:column; min-height:0,否则 flex:1 不会把稳定高度传给 pane/.wterm,最终表现为 renderer host 高度 0 或过期。Shell memo comparator 必须覆盖所有影响 absolute top/bottom 的 measured inset props;漏掉 terminalChromeTopPx 会让容器位置停在旧值。修复必须留在 shell geometry owner,不能在 buffer manager 伪造 viewport 或加渲染 fallback。ws.readyState === OPEN 或 input 可写只证明写路径可用,不证明 buffer-head -> buffer-sync -> local apply -> render commit 已恢复forceHead + markResumeTail,即使本地已有旧 buffersession-context-activity-runtime、session-context-lifecycle、SessionContext.ws-refresh 和 architecture boundary gateterminal.transport_lifecycle 的 retryable reconnect projection:retryable handshake/control attach failure 和 scheduleReconnectRuntime() 的 retryable reconnect start 都只能保持 reconnecting 并继续 retry,禁止 emitSessionStatus(..., 'error') 投给 UI。只有 nonretryable/auth rejected、auto reconnect explicitly blocked、或显式 retry exhausted 才能投 terminal error;不要在 drawer/UI 加二次过滤。reconnectAttempt 是标准流程内部计数,不是用户可见故障事实。只要状态仍是 reconnecting,顶部只能显示中性进度(如 正在重连 / 正在同步控制通道),不得因 attempt 次数、probe timeout、RTC data channel closed 等中间失败显示错误浮窗。只有 offline、typed terminal error、auth/nonretryable failure 才能进入错误 banner。SessionContext.switchSession(..., { refreshSource:'explicit-resume' })。只在 open-tab 层标 switchRuntime:'explicit-resume' 不够;如果 provider facade 固定转成 active-reentry,同一次用户选择会被拆成两套资源语义。内部 lifecycle active change 才默认 active-reentry。SessionTransportResource.socket 并同步写入;它不是 reconnect/open-intent owner。禁止在 input runtime 里调用 reconnectSession、probeOrReconnectStaleSessionTransport、shouldReconnectQueuedActiveInput 或 stale pending-open 补偿,也禁止 input owner 直接 ws.close() / 替换物理 transport。缺 transport / pending-open / backpressure 必须显式 drop/debug,transport close/replace/reconnect 恢复唯一 owner 是 terminal.transport_lifecycle。buffer-head 若早于 renderer visible range 到达,buffer owner 必须按 daemon head bounds 直接 bootstrap 当前 tail 的 buffer-sync body;默认拉多屏 tail 并受 availableStartIndex/latestEndIndex 约束,避免最后一屏为空白时把健康 session 误渲成黑屏。非 active / 无 visible demand 不拉正文。禁止把 renderer layout 当首包前置条件,也禁止用 hidden cache window 冒充 visible fetch window。buffer-head 只能更新 head/cursor metadata;不得触发正文 render commit。正文 repaint 只来自 buffer-sync apply,否则会把旧 body 重新投影成短暂闪屏。buffer-head 也不得提前清理 pending body request 权威。若 tail-refresh / reading-repair 的 buffer-sync-request 已发出,head 先到、同 revision body 后到是合法顺序;lastSyncRequestAtRef 只能由 body apply/drop 路径消费。否则后到 fresh body 会被误判成 unsolicited same-revision stale overwrite,表现为输入框/可见旧行不刷新。requestedAt、daemon response generatedAt/requestSentAt 可以进 debug 和黑盒 freshness 证据,但不得替代 revision + absolute row index 排序,也不得作为 accept/drop body 的业务真相。adb install -r / 系统安装器更新包后,前台旧 WebView 进程可能继续显示旧 JS/runtime projection;这不是 OPEN_TABS / TERMINAL_LAYOUT 一定复活dumpsys package 的 versionCode/lastUpdateTime、pidof com.zterm.android、dumpsys window focus、UI dump 文本;再 am force-stop 后冷启动对比app_webview / Local Storage / OPEN_TABS 当 workaroundadb install 冷启动只证明新 APK 可运行,不证明 App 内 AppUpdatePlugin.downloadAndInstall() handoff 已闭环;必须单独验证插件路径或明确 L5 缺口ImeAnchor.getState()/keyboardState 的 keyboardVisible,不要用本地 requested flag 或 inset 猜mirror-fixed 横向滑动只能是 renderer projection:.term-grid 可按 session 记住水平 offset 并做 translateX(-offset);禁止把横滑映射成 daemon resize、tmux width change、adaptive lease 或 buffer/mirror truth 修改。adaptive-phone 不响应横向 pan,它的宽度变化只走 daemon adaptive lease owner。mirror-fixed 横向手势归属:只有 offset 已为 0 且起点在左缘热区内的右滑可交给 drawer;positive offset 右滑、非左缘右滑、右侧/中间横滑都必须由 TerminalView 消费并 stopPropagation(),即使 offset 已经被 clamp 到 0、视觉上不能继续移动。禁止只 preventDefault() 后让父层 touchend 解析成 drawer previous。