with one click
terminal-buffer-truth
terminal buffer / render / daemon mirror 真源与门禁
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
terminal buffer / render / daemon mirror 真源与门禁
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
zterm Android 客户端开发工作流 - 基于 Capacitor + @jsonstudio/wtermmod-react,含完整开发闭环
zterm Windows Electron 客户端与 WezTerm daemon 开发闭环,覆盖共享核心边界、packaged preload、真实 Windows CDP/source-to-DOM gate 和精确资源清理。
zterm Mac 客户端开发工作流 - Electron 壳、terminal renderer、local/remote tmux、自闭环验证
wterm-mobile Android 终端应用开发工作流 - 基于 Capacitor + @wterm/react,含完整开发闭环
| name | terminal-buffer-truth |
| description | terminal buffer / render / daemon mirror 真源与门禁 |
任何 terminal / session / daemon / buffer / renderer 相关开发、修复、重构,必须先完成架构映射,再读代码和修改代码。
固定顺序:
android/docs/architecture.mdandroid/docs/audits/2026-07-02-architecture-boundary-remediation.mdandroid/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。禁止事项:
tmux truth
-> daemon server
-> client buffer manager
-> renderer
-> UI shell
四层只允许单向依赖,禁止越层漂移。
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。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 生命周期序列。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 + concattop/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 layoutadaptive-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。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 repaintlines[].cells[].flagspane_current_path;client 不得拿 process.env.HOME / 本地 env 冒充远端 cwd硬规则:
head/range 请求路径里“先同步 tmux 再回复”revision -> 1 / latestEndIndex 回退,不是 tmux 变了,而是 daemon 自己把 absolute truth 丢了new-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 的额外冻结:
clientSessionId 是 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 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 语义混写两者;1000 行只表示本地保留上限,不是拉取目标follow / reading / renderBottomIndexlocal window invalid / anchor mismatch / head mismatch 实现成“先清空已有本地 buffer 再重拉”startIndex/endIndexbuffer-sync span 覆盖当前 authoritative tail,且 span 长度超过本地 retention(默认 1000 行)[tail-1000, tail)),不得因为 incoming startIndex 更老而裁成 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 truthbuffer-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 事实变更,才允许重建。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。renderer 只看三件事:
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 列宽真相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 丢失快捷栏预留。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:
以下一律视为错误实现:
ws close -> daemon 推导客户端状态并修改 mirror/tmux 生命周期inactive tab -> close session / close transportreconnect -> new client session semanticsbuffer-head-request / buffer-sync-request 路径里触发 tmux capturemirror-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 永久挡住恢复。createSession(connect:false) 做 cold restore / sessionId remap,但不得自动打开 daemon session。真正的 daemon open 只能来自显式用户 open/import/resume 动作。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。看到这些现象,优先判对应层:
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 再过滤语音换行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/storeTerminalView.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 或散落页面内临时上报。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。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 加二次过滤。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 补偿。缺 transport / pending-open / backpressure 必须显式 drop/debug,恢复交给 terminal.transport_lifecycle。buffer-head 若早于 renderer visible range 到达,buffer owner 必须按 daemon head bounds 直接 bootstrap 当前 tail 的 buffer-sync body;非 active / 无 visible demand 不拉正文。禁止把 renderer layout 当首包前置条件,也禁止用 hidden cache window 冒充 visible fetch window。buffer-head 只能更新 head/cursor metadata;不得触发正文 render commit。正文 repaint 只来自 buffer-sync apply,否则会把旧 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。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。sessionId + bridgeHost + bridgePort + sessionName,有 daemonHostId 时也必须匹配;stale target 是失效选择,不是隐式 open/reconnect intent。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 子进程;不要宣称完整键盘等价。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,不证明或制造排版。