本版的主线是把"谁有权决定"和"谁有权改写"各自收回一处:只有人能拍板的工具成为一条声明而不是三处分支,会话授权按它被问到的那个主体记录,委派运行需要越过写入围栏时向人发问而不是向派它出来的模型发问;同时一个会话被另一个窗口占用时改为只读打开——租约保护的从来是写回,不是阅读。供应商一侧,DeepSeek 目录、视觉能力与官方价格各自收敛为一份声明,九个逐厂商手写的迁移函数随之退役。界面一侧是一轮以实测为准的整理:阅读列、任务页、导航树的键盘可达、上下文折叠点说出是哪条界限并在读到它的地方修改。自 2.14.1 起 67 个提交。
升级路径:无需人工步骤。三平台照旧自更新——Linux 走 .deb,Windows 运行下一个安装器,macOS 替换自身 bundle。DeepSeek 的 flash 模型 id 改为 deepseek-flash,已安装的配置由升级改写而非弃置,价格、推理强度等以旧 id 为键的设置随之迁移。此前"只能由人批准"的会话授权若以裸工具名记录,会在下一次遇到时再问一遍——那条规则不是你给出的答案,是记录它的代码替你放宽的。
新增
权限:只有人能决定的事,是一条声明
- 三处声明合一,并因此补上了前一次修复漏掉的那一半。"这个工具的授权要读它的主体"此前写在三个地方:
subjectRequiresHuman的分支、subjectScopedTools表、以及测试里的一份副本。合并后暴露出一条更早的路径:install_source的apply拿到high:...计划时,分级判定返回 Ask,而 Approver 为 nil 时unattendedAsk直接放行——YOLO 正是以 nil approver 构建的,高风险自我扩展计划(常驻进程、生命周期钩子、外部服务)在该档下被静默批准,没有人看到过那张计划。现在成员资格一处声明同时承载三个后果,加第四个工具只是一条表项。 - 「不再询问」只对它被问到的那个主体生效。
SessionGrantRuleForScope只为 bash 保留主体、把文件工具合并为一条 Edit 授权,其余一律落到裸工具名——而裸工具名规则匹配该工具此后的一切主体。实测:给/ws/internal/foo/bar.go的一次放行同时覆盖~/.ssh/authorized_keys与/ws/.git/hooks/pre-commit;给一份低风险安装计划的放行覆盖高风险计划。 - 旧形态的会话授权不再生效。上一条只停止了新的记录。已记录的那些同样覆盖一切,而它们不是用户写下的句子——用户当时看的是一份计划、一条路径。规则里没有主体,迁移无从谈起,所以判定移到读取侧:主体敏感工具的裸授权被拒绝,问题再问一次。用户自己写在配置里的
allow = ["install_source"]是他自己的句子,不在此列。 - 委派运行可以请求扩大写入围栏,且只有人能回答。
write_paths只能在开工前声明,于是每一次"读了才知道要动哪里"都是终局:工具拒绝,运行自行绕开或放弃。现在它发问——问用户,不问父代理,因为父代理是模型,模型批准自己的触及范围不构成围栏。走的是权限这条轴(扩权是一种能力),在所有 fallback 档位下都回到人,与install_source同一把尺。WriteGrant把 declared 与 granted 分开:只有 declared 那一份能在开工前证明互不重叠,调度读Declared()、审计读Scope(),越出声明集的写入按调用时长向调度器预留(try 而非 wait——持有路径的调用内再等待正是死锁的形状)。
会话与运行时
- 被另一个窗口写着的会话,改为只读打开。租约存在是为了两个窗口不把同一份转录互相覆盖:各自持有内存副本,
Hub.Close与 resume 都会在退出时快照,后关的那个抹掉前一个的产出。那是真实的丢失路径,但它完全是关于写回的。现在面板改为 attach:租约空闲则取得,被活跃运行时持有则以无租约、无写入权限打开。守卫没有移动——回合准入走ensureWriteAuthorityReady,每一次保存走requireWriteAuthorityForSave,没有租约就什么都递不出去。 - 一个活过了自己窗口的内核,不再攥着那个窗口的会话。排查"为什么打不开这个会话、它说被别处占用"时发现:本机有四个 Reasonix 内核存活,其中一个在派生它的应用消失后又活了十天,它们之间握着五个会话租约。宿主本应随 shell 结束——它读 stdin 的 EOF 来判定——但启用这条的判据要求
os.ModeNamedPipe,而 shell 是 Node:libuv 在 Studio 发布的每个平台上给子进程 stdio 的是 socketpair,不是 FIFO。parentLease在每一次真实启动中都返回 nil,看门狗从未武装过。 现在 socket 也算租约(终端与普通文件仍不算:一个永不结束,另一个立刻结束会在启动时带走宿主)。原有测试用os.Pipe()证明机制,那是任何一次启动都不会走的形状,所以它一直绿着而生产侧根本没有看门狗。 - 同时打开的会话数不再有上限。8 是一台机器对另一台机器的猜测,在三处强制、在第四处解释,读者遇到它的形式是一个在本可承载三十个的笔记本上变灰的按钮。整条移除而非调高:
Open/OpenRemote的拒绝、X-Panes-Max头、[desktop] max_panes及其钳制、窗口侧的天花板、新建按钮的守卫、两侧的hub.too_many_panes文案。它保护的成本是真实的(每个面板是一套完整装配,带自己的工具、扩展与 MCP 子进程),那笔成本由人来花。实测打开 fixture 全部会话:十五个面板,无拒绝,无控制台错误。 - 会话已经删除的,就说它已经不在。删掉一个会话后点同项目的另一个,回答是"被别处占用,请先在那里关闭"。
resumeInto返回 (status, error) 且注释写明了为什么,/resume把状态交给writeErr,而Open把它丢进了_,openRuntime用 409 作所有失败的兜底——于是每一个没有自己代码的失败都以 Conflict 抵达,前端渲染为"正被占用"。兜底改为 500,路径拒绝改为携带session.bad_path/session.outside_dir(前端本来就有文案),新增session.open_failed。两个测试成对:已删除的必须说自己不在,真有活跃持有者的必须仍然按占用拒绝。
上下文折叠点
- 说出是两条界限中的哪一条把它放在那里。会话在
compact_ratio与context_soft_limit_tokens(默认 160000)中先到者折叠。1M 窗口下永远是后者胜出,所以compact_ratio = 0.8从未到达它所要求的 800k——会话在 160k 折叠,是同一块面板所称模型容量的 16%,而屏幕上没有任何东西提到另一条界限。宿主早已判定(compactBoundary()自 280dfd8 起随ContextMaintenance传递),只是无人渲染。现在event.Compaction与ContextBreakdown都携带界限及其尺寸,轨道把折叠点刻在容量条上——160k 对 1M 是没人会在读面板时做的除法。同期更正三处"compact_ratio是唯一自动触发"的陈述,其中一处写在向导渲染的每一个 config.toml 里。 - 在读到它的地方就能改。此前脚注解释完为什么 1M 窗口在 160k 折叠,就把读者留在原地:界限本身在两次点击之外的设置页、一个叫「高级」的折叠区里。轨道现在提供与设置页相同的三个选择(默认 / 自定义 / 仅容量保护),
FoldMode与foldModeOf收进ui/foldbound.ts。写入会重建运行时,所以交回的读数是内核自己的port.context(),不是本次回复的算术。 - 条上的刻度就是手柄。它被画成一个刻痕、被读作可以拖的东西——那确实是读者期待的控件。它不能变成的是百分比滑块:这条界限是绝对输入尺寸,同样的 16% 对一个窗口是 160k、对另一个是 20k,按份额计量的轨道会在每次换模型时改写这条设置。四件只有真拖一次才会暴露的事都已修正(轨道按窗口而非 min..max 定位、默认值处的顿感做两步宽、读数与滑块各自的钳制、步进按千而非 2 的幂——否则 160k 落在两个停位之间)。
供应商目录与价格
- DeepSeek 的 flash 模型是
deepseek-flash,它被服务的那个 id 去掉了它从不需要的世代号。已安装的条目被改名而不是被抛下,改名会把每一个以旧 id 为键的字段一起搬走——留在旧键下的价格或强度列表会停止为取代它的模型作答,在下游读起来就是用户改过的设置自己变回了默认值。 - 预设是"它的哪些模型读图"的判据。读名字会漏掉这些预设声明的每一个 Kimi、Qwen、MiniMax 与 Claude 模型,而
deepseek-flash什么也拼不出;拼写退为无人声明过的地址的最后手段,并且仍是可纠正的建议而非断言。那条只为够到一个读图模型而存在的连接,在它旁边的条目以同一把 key 够到同一个模型时折叠掉——除非它的用户配置过它,因为折叠等于用删除来回答用户的编辑。 - 预设改形状通过一份声明抵达已装机:每一代一个声明形状,一个把匹配条目带向前的升级。判据是整个形状——目录、窗口、默认值——因为持有我们发布过的形状的条目是我们的,其余都是它的用户自己整理的。这让九个逐厂商手写的迁移函数退役。
- 官方价目只活在一处(
billing的officialRates,最老的一代在前)。价格变动是追加,不是编辑:被取代的那一档正是"认出这份已安装配置持有的是我们的价格、并把它带向前"的依据,否则它余生都在报一个已退役的价格。config通过billing.CurrentRate与SupersededRates读取、自己不复述——写两遍的表就是会漂移的表,某家厂商的美元价此前只在一侧被更新过正是这么来的。 make pricecheck读各厂商自己的页面与这张表比对,并报出每一条距上一次有人确认过去了多久。它会联网,所以不进 lint、不进 CI。读不到的页面以非零退出:未读是它自己的结论,绝不算作一致。
界面
- 一条阅读列。转录此前没有宽度上限,1920 下一行跑到 159 字符,是可读长度的两倍。此前两次尝试都因同一个理由被退回(右边缘悬空),两次都只收窄了一半——一次收外层列而正文仍保持 68ch,一次收正文而列仍占满。两者同时取才是没有东西悬空的那一版:820px,正是 1512 窗口本来就在排的宽度。
- 导航树可以用键盘走。十六行声明了
role="treeitem"——那是在告诉读屏软件方向键可以走它们——而没有一行能被任何按键到达。Tab 够得到的反而是行内的小按钮,于是键盘可以删掉一个它没办法打开的会话。改为 roving tabindex(树是 Tab 路径上的一站,方向键在其内移动),入口从当前打开的会话起算。实测:走动前后都是 16 行 1 个 tab stop,Left 把 16 行收成 11、Right 还原,重命名输入框保留它自己的方向键。 - 计划只画一次,由有地方画的那一面画。任务视图在侧轨仍在时以全宽铺开步骤,于是同一份清单以两种笔法同时站在一块屏幕上。316px 与 880px 不该要同样的密度,所以两者不合并,只是不再同时出现——存在与否仍是面板自己的契约,顺序表未动。
- 任务页是一页带答案的内容,不是四张等重的卡片。状态与目标成为页头(无框、目标大一档、下面一条发丝线),计划与实时轨迹是正文,轨迹拿到它本该有的整幅宽度。四张卡片各自携带的 520px 径向光晕取消:卡片不是控件,指针进入它时什么都不会发生,而那四种色相里没有一种有含义——「当前任务」与「快速操作」都是琥珀色。
- 上下文条有了自己的调色板,状态色也拿回了它们的槽位。五个部分此前画在 deleg / net / accent / ok / faint 上——它们各自已经表示一件事,没有一个表示"系统提示词"。代价有两重:状态词汇表丢掉五个槽位给了一张图表,而这张图表并没有调色板:分类色集要过的六项检查它不过四项。最要命的是第四项——工具输出(faint,灰)对模型回复(ok,绿)在常规视觉下 ΔE 12.0,低于 15 的底线,而它们是条上第二与第四大的两段。现在五个槽位是自己的令牌,在两个方案里各自分档并作为一个集合校验(相邻 CVD 最差 9.1 浅 / 8.4 深,目标 8;常规视觉 19.6 / 19.3,底线 15)。
- 浅色方案的强调色是那个色相,而不是一种能托住白字的棕。
#A76200不是被选出来的颜色,是从另一头倒推出来的——还能托住白字的最深的琥珀,白字实测 4.4977,连 4.5 都没到。深色方案一直是反着来的:用色相本身,配深色字。现在两个方案都是(浅#E6AC45对深#DDA144,字色实测 7.25:1)。 - 其余整理:会话轨按机器分组且字重不再与颜色相反;指标轨用间距而非发丝线分组,最大的数字跟随位置而非固定给成本块;工具标题行读作三件事而不是五件;移除与新建不再同等分量;正在挡着你的那张卡片是被抬起的那一张;你说的话有多宽取决于你说了多少;答案里的表格是排版而不是表单,其中的行内代码不再穿着"已委派"的色相;空输入框下那行只在 Enter 的含义变化时才说话。
端点
- 一个端点如何携带上下文是声明,不是探测。中转站可以在不存储任何状态的情况下应答 Responses 协议,然后拒绝
previous_response_id而不是忽略它,于是第一轮之后每一轮都失败——报错还点名一个读者无从控制的字段。配置字段一直存在,只是从窗口里够不到,唯一的办法是手改reasonix.toml。现在它是协议目录上的一个位(只有 Responses 能按引用携带一轮),config一处声明优先级(mode 优先于遗留布尔值),列表像携带canSetThinking一样携带canSetContinuation,未知 mode 一律拒绝而非折成 auto。把"任意 400 就退出有状态模式"的方案试过并退回了:一次无关的 400 就会让一个好端点永久掉出有状态模式、掉出前缀缓存——端点拿一个字段做了什么,不是它是否支持该字段的证据。(#9815)
修复
- 标题栏重新可以整条拖动。两种拼写都在说"这一行是拖拽把手",而它们对继承的看法相反:
--wails-draggable是自定义属性,于是 Wails 下整条的每个 span 都继承了拖拽;-webkit-app-region是常规属性,不继承任何东西——在运行页上实测,.chrome计算为可拖,它的九个后代全部计算为不可拖。整条只在什么都没画的地方可拖,而面包屑一个就占 1060px、右侧那组 328px。(#10156) - 长项目名不再跑到标题栏控件底下。
.crumb带着flex:1与min-width:0所以盒子会收缩,但子元素留着overflow:visible,文字照常画出去:1512 下越界 1336px,900 下 1948px,运行药丸和右侧三个按钮被画在一起。裁剪规则本来就存在,只是写在"两侧栏都折起"那一档下——而把标题栏撑爆并不需要等到窄窗。 - 转录不再伸到定位轨底下。
.flow靠收窄自己给轨让位,又用margin-inline:auto居中剩下的部分,于是一条轨的宽度被两侧平分、只有一半落在轨所在的那侧。1512 下实测内容跑到 1077,而轨从 1070 开始;轨整条pointer-events:auto,那些像素对点击的回答是跳转而不是选中。 - 滚动指示器不再在你向上滚时倒着走。没人滚到过的块用自己的占位高度顶替,而那个数字停留在 96px——实际最短的卡片 119、中位 199、均值 216。回滚 300 轮实测:内容在读者脚下长了 18393px(+20%),指示器在向上滚的过程中三次下滑,最差 713px 轨上 48.8px。改为由这份转录自己的块学习(有界窗口内的滑动均值,以实测 216 作种子)后:1113px(−94%)、0 次倒退、最差 0.5px;1200 轮时 1229px、零次。
- 消息轨是鱼眼,不是地图。它按每条消息在整份转录中的位置放刻度,于是画出来的是每一轮产出了多少,而不是"被问了哪些问题"的清单:混合长度的会话里间距从 14px 到 171px,σ=56。现在刻度以固定 9px 挤在中间(300 个刻度落到 2.21px 并保持在轨内),长度与浓淡是自指针挑中的那一个起的余弦衰减。
- MCP 服务器被取消时说得出是谁取消的。首次工具调用才启动的服务器跑在宿主拥有的 context 上;该 context 在子进程起来之前结束时,
exec.CommandContext从Start()返回 "context canceled"——不到一毫秒、没有 stderr,因为进程根本没跑。读者看到的是MCP startup launch failed after 0.7ms: context canceled,读起来像是服务器启动失败。宿主是唯一知道发生了哪一种的一方,而两者要去修的东西不同(MCP 宿主关闭 / 这台服务器被移除、停用或重连),现在两者都作为 cause 携带。 - 不允许在这里编辑的来源,不是连不上的来源。对着不授予 provider 路由的内核做检查会得到 403
provider.editing_disabled,而面板把每一次失败的检查都画成「无法连接」压在内核自己的句子上面,两者互相矛盾。判据读代码而不读状态码——403 也是内核前面的网关会给的答案,那是另一件要去修的事。(#9772) - 一个字段和它的外层不再共用一个名字。
.mfind同时代表弹出菜单里的裸<input>与模型列表里包着字段和计数的<div>,两份样式表都没有作用域,于是双向互相够到:菜单的 sticky 规则把设置容器粘在页面上而它的行在下面滚动。(#9648) - 成本面板说出四个原因里的哪一个;CSRF 守卫此前一直在拒绝的两个 POST 带上了它要求的类型(启动健康确认与等待中提示的重放,一个永远落不了地、又因为沉默而从不说出来的调用);长输出上的渐隐遮罩不再盖住"展开全部";英文界面下三处标签不再为了保住脚注而被截断(设置列表的分区名此前
flex: 1,basis 为零,行永远不算溢出);折叠标记、警告符号、圆角各自只有一种画法。
门禁与工具
- 英文窗口里仍在渲染的中文,改走目录。26 处字符串是直接写进 JSX 而非经由
t()到达屏幕的:登录设备码、安装与 MCP 连接完成时说的话、包管理行、标签页菜单、Ask 卡与 Rewind 卡,以及版本面板里的六处——最后这一处从没有人读到过,因为走查运行中的应用的扫描只看得见一次运行渲染出来的东西,而它的版本目录答的是 404。旁边那个目录测试检查的是t()调用点名的键,所以一个不点名任何键的字面量对它是隐形的。新的jsx.test.ts改读每个组件的语法:JSX 文本、属性、表达式里的中文,除非最近的外层调用会翻译它,否则一律红(会跟随 import 时改名的t)。 - 对比度是量出来的,不是看出来的,而量它的探针自己也被量了一次:
perf/contrast.mjs的脚本回合给出的工具结果没有contextTokens,于是.cost只渲染一个 span,第二个 span 上的规则永远没有元素可匹配——补上一个字段后,它找出了三处低于 AA 的读数(深 3.30 / 浅 3.33 / 深 4.07),其中一处还是一次手工普查漏掉的浅色实例。 - 守卫的守卫改为自己找齐。
selfcheck向每个守卫注入一条恒假断言来看它是否真的会红,而它的 GUARDS 数组漂移了:cards、composer、intrinsic、motion、overflow、press、surface 七个脚本从未被问过能不能失败。现在它在目录里找出每一个以fails表达失败的脚本,新守卫靠存在就加入。22 个全部在注入下变红。同期扫出四条已经不再看着产品的断言,以及三条在它们自己的断言写下之后才变红的守卫(其中一条是产品:审批卡窄档下两个次级按钮高度差一像素)。 - 走查判据不再误报。
offscreen此前把可横向滚动容器里的元素算作出界(最窄窗口下设置导航的每一个按钮都被点名,而那条轨会滚到每一个),unnamed把aria-hidden的装饰按钮算作无名控件——73 条发现,没有一条是缺陷。一个总在喊狼来了的门禁会被当成噪音读,而它旁边两条真的发现正要走这条路。 - repolint 的翻译表豁免指向了现存的目录。它仍然点名已退役桌面存放 locale 的那棵树,自那棵树被删除起就没有豁免过任何东西,而在用的目录正好卡在行数天花板上。
- 取消归因那条守卫改跑平台上存在的命令。它点名
/bin/echo,而它自己的注释写明了为什么命令必须存在:可执行文件的解析发生在读 context 之前,所以一个不存在的命令会更早、以另一个原因失败。Windows 上正是如此——exec: "D:\bin\echo": executable file not found取代了本该被断言的取消原因,这条守卫自写下起在那条腿上一直是红的。 - wire-parity 现在同时约束携带折叠界限的两面镜子:页面读不到的界限,是一次它只能靠猜来解释的折叠。
macOS 已用 Developer ID 签名并公证,打开时不需要清除隔离属性。Windows 包未签名,SmartScreen 会告警:选「更多信息」,再选「仍要运行」。Linux 从 .deb 安装。
三个平台都能自更新:Linux 走自己的包,Windows 运行下一个安装器,macOS 替换自身 bundle。