com um clique
terminal-buffer-truth
terminal buffer / render / daemon mirror 真源与门禁
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Menu
terminal buffer / render / daemon mirror 真源与门禁
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
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,含完整开发闭环
Baseado na classificação ocupacional SOC
| 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,不证明或制造排版。