# 设计细节 中文 | [English](DESIGN.en.md) · [返回 README](README.md) 这份文档记录插件**为什么这样做**。README 讲它是什么、怎么用;这里讲每一处取舍背后的理由,以及那些被试过又否掉的方案。读者假定为想改这个插件、或想在 DSH 上做类似事情的人。 --- ## 目录 1. [两个概念:芯片与信封](#1-两个概念芯片与信封) 2. [什么落盘、什么不落盘](#2-什么落盘什么不落盘) 3. [信封:给 Jev 看什么](#3-信封给-jev-看什么) 4. [两个问题,一条规则](#4-两个问题一条规则) 5. [一轮一决策](#5-一轮一决策) 6. [手动选择的判定](#6-手动选择的判定) 7. [芯片的生命周期](#7-芯片的生命周期) 8. [本会话开关](#8-本会话开关) 9. [Host ↔ 浏览器通道](#9-host--浏览器通道) 10. [为什么不写会话日志](#10-为什么不写会话日志) 11. [失败行为](#11-失败行为) 12. [被否掉的方案](#12-被否掉的方案) 13. [已知的未完成](#13-已知的未完成) --- ## 1. 两个概念:芯片与信封 **芯片**是输入框右侧那个小标签,显示 Jev 决策出来的推理等级:`Jev · High · 87%`。它展示的不是模型,是**这一轮实际使用的思考深度**,以及 Jev 对这个判断有多确定。 **信封**是每轮开始时插件发给 Jev 的那段请求内容。Jev 读完信封,回答"这条消息该用多深的推理"。 两者的关系:**信封 → Jev → 决策 → 芯片**。芯片准不准,取决于信封给的信息够不够。所以这份文档的大半篇幅在讲信封。 --- ## 2. 什么落盘、什么不落盘 这是整个设计的骨架。插件**不拥有任何磁盘存储**,只用两种东西:harness 自己的会话日志(持久),和进程内存(易失)。 | 状态 | 在哪 | 重启后 | 为什么这样 | |---|---|---|---| | 上一轮是什么样(信封的记忆) | 会话日志 → 投影 | ✅ 在 | 决策质量不该因重启下降 | | 芯片显示的决策(等级、置信度、原因) | Host 内存 | ❌ 空 | 见 [§7](#7-芯片的生命周期) | | 本会话开关 | Host 内存 | ❌ 回到全局设置 | 见 [§8](#8-本会话开关) | | 手动选择的判定基准 | Host 内存 | ❌ 第一轮不判定 | 见 [§6](#6-手动选择的判定) | | 本轮决策(供第 2+ 步复用) | Host 内存 | — | 一轮之内的事 | 一个原则贯穿全部:**能从日志重建的,就不自己存;不能从日志重建的,就承认它是易失的,并把易失当作正确语义**,而不是补一套持久化去掩盖。 ### 投影是什么 DSH 的会话日志是只追加的事件流。**投影**是对这条流做一次折叠(fold),得到一个小结果——比如会话标题、待办列表。harness 把折叠结果缓存在 `~/.dsh/storages/session_projcache/sessions/.json`,每个会话一个文件,内部是 KV: ``` record.rows ├── title: { ver:1, seq:2126, val:"…" } ├── todos: { ver:2, seq:2141, val:[…] } ├── jevTurn: { … } ← 本插件,下发浏览器(芯片的刷新信号) └── jevContext: { … } ← 本插件,只在 Host ``` `seq` 记录折到日志第几条。重启后 harness 从 `seq` 往后继续折,不重读全部日志。`ver` 是插件声明的 `stateVersion`:代码改了结构就升版本,harness 只重算这一个 key。 所以"信封的记忆"其实**也**落盘了——落在 harness 的缓存里,含用户消息前 300 字和助手结尾 500 字的明文。它是日志的派生物(日志本身就是明文),但确实多了一份副本。手动删除会话日志时,这份缓存不会跟着消失。 --- ## 3. 信封:给 Jev 看什么 ### 问题 孤立地看,"继续"是一句琐碎的话——Jev 会判 `off`,哪怕上一轮正在设计分布式事务引擎。"扫"更甚:一个字,含义完全取决于助手刚才问了什么。 ### 结构 `useContext` 打开时,插件发一段 system 文本,再跟当前消息: ``` Context: an ongoing conversation with an AI coding assistant. Session topic: 排查代码报错原因 Previous turn: - reasoning effort used: high - user said: "帮我看下为什么 dsh 启动报错" - earlier the user said: "动手吧" - assistant ended with: "…已修复两个会话。要我把剩下 22 个会话也全部扫一遍分帧吗?" - activity: 14 steps, 9 tool calls - previous turn outcome: completed - unfinished todos: 0 ``` 每一行的作用: | 行 | 解决什么 | |---|---| | `reasoning effort used` | 让 Jev 知道上一轮的深度基线 | | `user said` / `earlier the user said` | 两轮的走向比一轮更能看出"在干什么" | | `assistant ended with` | **最值钱的一行**。"扫"、"好"、"删掉"这类一个字的回复,含义完全取决于助手刚问了什么。没有这行,Jev 看到"扫"只能当 trivial | | `activity` | 上一轮是重活还是闲聊 | | `previous turn outcome` | 上一轮是否正常结束。被打断、报错、超长度都算"未完成" | | `unfinished todos` | 上一轮结束时待办里还有没完成的项(未开始或进行中),说明工作没完 | ### 大小 用户消息各截 300 字,助手结尾取尾部 500 字,整体约 500–800 token。这个数字是刻意放宽的:Jev 是分类模型,输入多几百 token 对延迟和费用影响很小,但对一个字的消息,上下文多一点判断就准很多。再往上收益递减——Jev 不是靠读全文取胜的。截断长度集中在 `lib/index.js` 顶部的常量里,方便调。 ### 来源 全部来自 harness 自己写的事件: | 事件 | 提供 | |---|---| | `turn/start` | 轮次边界 | | `user/message`(仅 `source.kind === 'user'`) | 用户原话。插件注入的系统提醒同类型但 `source.kind === 'plugin'`,必须过滤 | | `assistant/message` | 助手结尾 | | `step/start` / `tool/call` | 活动量 | | `turn/end` | `reason.kind`:`completed` / `aborted` / `error` / `max-tokens` / `blocked` | | `request/header` | 实际使用的等级与路由 | | `todo/write` | 这一轮最后一次写的待办里,没完成的项数 | 待办数**不能**读 harness 内置的 `todos` 投影:它在每个 `turn/start` 清空,而 `turn/start` 写进日志早于 `agent/request`。决策那一刻它永远是空的——0.3.11 及以前就是这样读的,这个判据因此从未生效。0.3.12 改为在插件自己的投影里折 `todo/write`,每轮记下它结束时留下的待办;只数"进行中"也不够,最常见的"做完一步、问要不要继续"留下的是"未开始"的项,所以两者都算。 一个折叠细节:`request/header` **只在 config 变化时追加**。所以某一轮可能没有这个事件,它跑在上一轮的 header 值上。折叠时新轮次必须从上一轮**继承** `effort` 和 `route`,否则信封里会出现 `effort: null`。这是回放真实日志时发现的。 ### 当前消息的来源不同 上一轮来自投影;**当前这条消息不能来自投影**。`agent/request`(决策发生的地方)跑在"accepted user batch are committed"之前——harness 契约原文。也就是决策那一刻,当前消息还没写进日志,投影里没有它。 所以插件另有一个 `agent/pre-step` 监听,只干一件事:把本步的用户消息文本递给 `agent/request`。0.3.0 曾把这条也改成读投影,结果每轮都因"当前消息为空"直接放行,Jev 一次没被调用。离线回放日志发现不了这类时序问题,因为回放永远是在事件都写完之后。0.3.1 修正。 ### 首轮 没有上一轮时,信封只有 `Context` 和 `Session topic` 两行,且不问"关系"问题(见下节)。 --- ## 4. 两个问题,一条规则 ### 两个问题 Jev 的接口支持一次问多个问题(`questions` 是对象,多个 key)。同一次调用、不多花时间,问: 1. **`reasoning_effort`**:在这个模型的档位里选一个。criteria 按档位数动态生成。 2. **`relation`**:`continues`(延续、追问、确认、回答助手的问题)还是 `new`(无关的新请求)。 第二个问题是"几点了"和"继续"的分水岭——两句一样短,区别只在是否延续上一个任务。实测四个场景,`relation` 的置信度全部 ≥ 0.98,比等级问题还稳。 ### 一条规则 Jev 回来两个答案后,插件只做一件事: > **如果 `relation === continues`,且上一轮的活还没干完,那就不低于上一轮的等级。其他一切情况,Jev 说几档就几档。** "活没干完"两个判据任一成立:`previous turn outcome ≠ completed`,或 `unfinished todos > 0`。 三个变量,四格,穷尽: | 延续? | 活干完? | 处理 | |---|---|---| | 否 | 完 | 听 Jev | | 否 | 没完 | 听 Jev(新话题,和旧活无关) | | 是 | 完 | 听 Jev | | 是 | **没完** | **不低于上一轮** | ### 为什么规则不看文字 "几点了"、"现在啥时候"、"what time is it"、"继续"、"go on"、"扫"、"好"、"谢谢"……无穷多种说法。穷举不可能也没必要——语言理解是 Jev 的强项。插件里**没有一行代码看消息文字**,只看三个变量:Jev 的两个答案,加日志里的完成状态。criteria 里那几句描述("包括 go on、ok、对助手问题的一个字回答")是帮 Jev 理解类别边界的说明,不是匹配列表。 ### 实测 | 上一轮 | 这一条 | 关系 | 活干完? | 结果 | |---|---|---|---|---| | High,复杂重构 | 几点了 | new 1.00 | — | **Off** | | High,被打断 | 继续 | continues 1.00 | 没 | **High** | | High,干完了 | 谢谢 | continues 0.99 | 完了 | **Off** | | High,助手问"要扫吗" | 扫 | continues 1.00(等级仅 0.16) | 完了 | 低置信取强 → **High** | 第四行值得注意:一个字的"扫",Jev 对等级拿不准,但对"这是延续"判得很死。低置信度规则接手——在前两档里取更强的。这就是两层配合的价值。 ### 低置信度 等级置信度低于 `confidenceThreshold` 时,在概率最高的两档里取**更强**的。关系置信度低时按 `continues` 处理——它触发的锚定只会保深度,不会减,往安全方向错。 ### 升档 任何时候立刻生效,不设限。规则只锁"延续未完成的活时不降档"这一种情况。 --- ## 5. 一轮一决策 Jev 只在每轮第 1 步被调用。之前的实现只改第 1 步的 config,第 2 步起回到 harness 默认——一轮里模型调了 10 次工具,只有第 1 次跑在 Jev 选的等级上。芯片显示 High,实际大半时间不是。 现在第 1 步决策后缓存到"本轮"(`turnDecisions: sessionId → {turn, effort}`),后续每步直接复用。实测一个 10 步的轮次,日志里只有 1 条 `request/header`——harness 只在 config 变化时追加,10 步没漂移正是一致的证据。 **重试也复用。** LLM 请求失败时,harness 会在同一步里重试,而每次重试都会再跑一遍 `agent/request`。0.3.1 之前,第 1 步的重试会重新问一次 Jev:多花 1–2 秒和一次调用;Jev 可能给出另一档,同一步前后两次请求等级不同;而且重试不会写新的 `user/message`,芯片不刷新,显示的和实际用的对不上。现在只要本轮已有决策,不管第几步、第几次尝试,一律复用。 ### 子代理不管 插件挂在宿主上,每个 agent 的请求都会经过它,包括主代理派出的子代理。0.3.12 及以前,子代理的每一轮也会问 Jev、按 Jev 的结果改等级:主代理给子代理指定(或让它沿用)的等级被悄悄覆盖;主对话里关掉了 Jev,它派出的子代理照样被管;每个子代理每轮多一次 1–2 秒的调用。 Jev 判断的是"你发的这条消息"该用多深的推理;子代理的任务是主代理写的,深度也该由主代理决定。所以 0.3.13 起子代理会话直接放行,不问 Jev、不写记录。 识别方式只能看会话头:DSH 把任务作为 `source.kind === 'user'` 的消息交给子代理,消息本身区分不了。dsh-subagent 在子代理的会话头里写了 `origin: 'subagent'`,插件只认这一个。**不能**拿 `parentSession` 判断:用户在界面上分叉(fork)出来的对话也带它,那是用户自己在说话的对话。 --- ## 6. 手动选择的判定 **定义**:你在选择器里动过等级(哪怕选的是它当前显示的那一档),且模型没换 → 手动选择了。这一轮 Jev 不参与;换了模型就正常走 Jev。 **为什么不能拿"选择器的值"和"日志里的值"比**:直觉上,选择器存的是你的选择,日志记的是实际发出的,两者一比就知道你动没动。但选择器**没有自己存值**,它显示的内容本身就是从日志推算出来的(见下一小节)。Jev 把 Low 改成 Medium 发出去之后,选择器也显示 Medium。两边的值来自同一个地方,比不出"你有没有动过"。能证明你动过手的,只有那次点击本身留下的记录。 **实现**:每次在选择器里改等级,harness 都会写一条 `model/selection` 事件。投影数这个事件的次数(`selections`)。插件在每次决策时记下当时的计数(`seenSelections`,内存);下一轮计数增加且路由未变 → 手动。 **换模型不算**:`model/selection` 也会因换模型而增加,但此时 `previous.route ≠ 当前 route`,不判定为手动,Jev 为新模型重新决策。 **重启后**:`seenSelections` 为空,第一轮没有基准,正常走 Jev。 手动选择时,插件原样放行 config,并把决策记为 `reason: 'manual'`,芯片显示你选的等级、悬停"手动选择,Jev 本轮未参与"。手动只管这一轮,下一轮 Jev 照常决策。 ### 模型选择器会跟着 Jev 变 插件从来不碰选择器组件,只改发给 LLM 的那一个参数。但 Jev 开着时,选择器显示的等级会跟着 Jev 的决策变。原理是三件事连在一起: **① 改过的参数会进日志。** 每次请求发出去,harness 把实际发出的 config 写成一条 `request/header`(只在和上次不同时写)。插件的改写发生在发出之前,所以记下的是改写后的值。harness 并不知道这个值是谁改的。 **② 选择器是从日志推算出来的。** 选择器背后是 harness 的 `modelSelection` 投影,只看两种事件: ``` model/selection 你在选择器里点了一下 → pending = 你选的值 request/header 一次请求发出去了 → lastUsed = 实际发出的值 (和 pending 相同时清掉 pending) 选择器显示 = pending ?? lastUsed ``` **③ 串起来:** ``` 你手动选 Low → pending = Low 选择器显示 Low 下一轮发出 Low(手动) → lastUsed = Low,pending 被清掉 选择器显示 Low 再下一轮 Jev 改成 Medium → lastUsed = Medium 选择器显示 Medium ``` **为什么 harness 这样设计**:选择器表示的不是"你想要什么",而是**"这个会话当前在用什么"**。下一次请求就从这里出发(`dsh-agent-loop` 的 `seedConfig = requestProposal(persistedHeader)`),会话会停在最后一次实际使用的配置上,而且这个状态随日志持久化。选择器必须显示它,否则你看到的和下一次实际发出的就对不上了。 **后果**:Jev 的决策会真的变成会话"当前的等级"。所以**关掉 Jev(全局或本会话)后,会话停在 Jev 最后选的那一档**,不会自动回到你之前手动选的等级;想换就在选择器里手动选一下。这是有意保持的现状,替代方案见 [§12](#12-被否掉的方案)。 --- ## 7. 芯片的生命周期 **芯片不持久。** 它显示的是"这个进程里 Jev 最近一次的决策",重启即空,下一轮决策后重现。 理由:芯片的意思是"Jev 刚才选了什么"。重启之后插件自己都不记得上次选了什么、为什么选(那些是内存里的),芯片却还显示着一个等级——这是在展示一个没人记得的决定。0.2.x 的芯片恰好有这个毛病:它折 harness 的 `request/header`,蹭到了持久性,但那个事件里没有置信度、也不知道是谁选的,所以只能显示中性的"思考 High"。 现在芯片和它描述的那个决定**同寿**。代价是重启后老会话在下一轮对话前没有芯片——可接受。 **可见性四态**: | 全局开关 | 本会话开关 | 有决策 | 显示 | |---|---|---|---| | 关 | — | — | 隐藏 | | 开 | 关 | — | `Jev 关`,可点 | | 开 | 开 | 无 | 隐藏 | | 开 | 开 | 有 | `Jev · High · 87%` | **样式**:不管什么状态,`Jev` 都和旁边模型选择器里的模型 ID 同一个样式;后面那个词(等级或 `关`)和选择器里的推理等级同一个样式,只是等级保留颜色(Off/Minimal/Low 灰、Medium 橙、更高红)。`关` 用的是等级的灰色。芯片没有整体变灰的状态——`关` 这个字本身就说明 Jev 没参与(0.3.15 及以前整块变灰)。 全局关时完全隐藏,是 0.2.4 时的选择:当时的问题是关了还显示"思考 High",和右边的模型选择器完全重复,还暗示 Jev 在工作。 **刷新时机**:芯片订阅 `jevTurn` 投影,但不显示它的值,只把它当触发器——它一动,芯片就去 Host 拉最新决策。不轮询。 `jevTurn` 每轮恰好动一次:在 `turn/start` 之后的第一条 `user/message` 写入时。选这个事件是因为它**在决策完成之后**才写入——`agent/request` 返回后,harness 才把本步接收的消息提交进日志。所以浏览器看到它动的时候,本轮决策一定已经在内存里了,不存在竞态。同一轮里后续的 `user/message`(运行时上下文、插件注入的提醒、中途插话)不会再让它动,一轮只拉一次。 旧的触发器是折 `request/header`,看起来最直接,但 harness **只在 config 变化时**才写这条事件。Jev 连续两轮选同一档,第二轮就没有它,芯片不刷新,显示的是上一轮的置信度和原因。偏偏"打断后说继续、保持原等级"就是同一档的典型场景。拿真实测试会话回放:35 轮里有 9 轮没有 `request/header`,按旧触发器这些轮次芯片全是旧的;换成 `jevTurn` 后每轮都恰好触发一次。 **悬停文案**回答的是"谁定的、规则有没有介入"。鼠标在芯片上停 0.4 秒就显示:用的是 DSH 自带的提示气泡(消息点赞按钮用的同一个,0.4 秒也是官方 Agent 预设卡片用的延迟),比浏览器原生的 `title` 快,又不会鼠标一划过就弹出来;点开弹层时气泡收起,因为弹层里也写着原因。0.3.16 及以前用的是 `title`。 | 情况 | 芯片 | 悬停 | |---|---|---| | Jev 的原判(不论新话题还是延续) | `Jev · High · 87%` | 由 Jev 判断 | | 规则拦住了降档 | `Jev · High · 76%` | 延续未完成的任务,保持上一轮的等级 | | 你手动选了 | `Jev · Low` | 手动选择,Jev 本轮未参与 | | Jev 超时 | `Jev 关` | Jev 未响应(超时),沿用当前等级 | | Jev 调用失败 | `Jev 关` | Jev 调用失败,沿用当前等级 | | 没有密钥 | `Jev 关` | 未配置 Jev 密钥,沿用当前等级 | | 没有 API 地址 | `Jev 关` | 未配置 Jev API 地址,沿用当前等级 | 后四行见 [§11](#11-失败行为)。 第二行只在规则**真的改了结果**时出现:Jev 本来想降档,被拦住了。如果信封已经让 Jev 自己判出了原等级,规则没有介入,显示的是第一行。Jev 对"新话题 / 延续"的判断仍然记录在决策里(`relation` 字段),只是不再展示在悬停中——结果都是听 Jev 的,对用户没有区别。 **按会话隔离**:芯片挂在 `conversation.input.right`,会话作用域的 slot;Host 端决策按 `sessionId` 存。A 会话的芯片显示 A 的决策。 --- ## 8. 本会话开关 点芯片弹出一个开关:"本会话启用 Jev 推理选择"。 - 存在 Host 内存,按 `sessionId`,不落盘 - 决策时先看本会话有没有覆盖,没有就用全局设置 - **仅当全局开关为开时可用**。全局是总闸,本会话开关只在总闸开着时有意义;"总闸关了但某个会话偷偷在跑"容易困惑 - 本会话关掉时,芯片不消失,显示 `Jev 关`——否则没地方再点开 - 重启后内存清空,自动回到全局设置 - 关掉后会话停在 Jev 最后选的那一档,不会回到你之前手动选的等级(见 [§6 模型选择器会跟着 Jev 变](#模型选择器会跟着-jev-变)) **切换会话不影响它**。查实 `agent/disposed` 只在进程关闭或 harness 卸载时触发;源码里没有空闲超时回收;切走再切回来,内存里的东西都在。 --- ## 9. Host ↔ 浏览器通道 芯片要显示置信度、点击要能开关,都需要浏览器和 Host 互相传消息。 **官方走法**:`TypertRemoteService` + `@Remote` 装饰器 + TypeScript 编译期生成的 manifest(约 500 行 zod schema)。本插件是手写 JS,没有这条生产线。 **实际走法**:harness 的 gateway 有一条 **SRC fallback**——没有生成产物时,它去活的服务里找带 `@Remote` 标记的方法。这个标记本质上只是原型上的一个普通对象,纯 JS 用 `Object.defineProperty` 就能打上: ```js Object.defineProperty(JevRemote.prototype, '@deepseek-ai/dsh-typert-protocol/remote-methods', { value: { version: 1, methods: [{ method: 'getSessionState', invocation: { kind: 'direct' } }, …] }, }) remote.typertRemote = { service: remote, serviceKey: 'jevEffortSelector', namespace: 'jevEffortSelector' } ctx.provide('jevEffortSelector', remote) ``` 浏览器端 `ctx.remote.*` 那层糖只认生成产物,但它底下的裸 RPC 是开放的: ```js connection.rpc.call('/api', 'jevEffortSelector/getSessionState', { args: { sessionId } }) ``` **代价**:参数校验退化为"按名字传 JSON",没有 zod 那层类型检查。Host 端自己判类型。对三个方法(`getSessionState(sessionId)`、`setSessionEnabled(sessionId, enabled)`、`modelLadders()`)来说够了。 `modelLadders()` 给设置卡片用:列出所有支持至少两档推理的模型、它们广播的档位,以及「自动」会用的档位。自动档位在 Host 端用决策时同一份 `deriveLadder` / `clampLadder` 算出,卡片上高亮的就是 Jev 实际会拿到的那几档,两边不会各算一套。 这条路是先用一个最小探路插件验证通了,才决定不走兜底方案(斜杠命令 `/jev on|off`)的。 --- ## 10. 为什么不写会话日志 插件**不往会话日志里写任何东西**。这是一次事故换来的。 早期版本用 `session.append('jev/effort', …)` 写自定义事件,投影折它。在写它的那个进程里一切正常。但持久化的读路径要求事件类型要么在 harness 词汇表(`KNOWN_SESSION_EVENT_TYPES`)里,要么 envelope 带 `ignorable: true`——否则**拒绝重建整个会话**("历史加载失败")。而 `Session.append()` 的签名根本没有设置该标记的入口。 于是四个会话被写坏,打不开。修复过程中又出了第二次事故:修日志的脚本把多帧 zstd 整体重压成单帧,让 dsh 完全无法启动(详见仓库外的事故记录)。 结论:芯片需要的数据,harness 内置的 `request/header` 里本来就有;置信度和归属虽然不在里面,但走内存通道就够了。**不值得为此碰日志。** --- ## 11. 失败行为 任何一种情况都**原样放行** harness 的 config,等于这一轮插件不存在,用会话当前的等级(也就是选择器此刻显示的那一档,通常是 Jev 上一次选的)。不报错、不阻塞。 ### 失败会显示在芯片上 失败分四类,每类都写一条决策记录,芯片显示 `Jev 关`(和本会话关闭时同一个样子),悬停说明原因: | 失败 | reason | 包括 | |---|---|---| | 超时 | `timeout` | 等满 `timeoutMs` 还没回来 | | 调用失败 | `failed` | 网络不通、HTTP 非 2xx、返回格式不对、Jev 选的等级不在档位里 | | 没有密钥 | `no-key` | 字面量和凭据引用都解析不到 | | 没有 API 地址 | `no-url` | `apiUrl` 为空。它刻意没有默认值:连接到哪个服务应当由用户明确填写 | 记录里的等级是**实际发出去的那一档**,不是 Jev 选的——这一轮 Jev 没有选。所以芯片不再显示这一档:旁边的模型选择器已经显示了当前等级,芯片再写一遍容易被看成是 Jev 选的(0.3.15 及以前显示整块变灰的 `Jev · Medium`)。下一轮只要成功,芯片恢复正常样式。 0.3.2 及之前,失败什么都不记,芯片会继续显示上一次成功的决策:等级碰巧对(harness 停在上一次的等级上),但置信度和"由 Jev 判断"都是假的,而且你看不出这一轮 Jev 出了问题,只会觉得多等了几秒。 **超时和"你点了停止"要分开。** 两者都会中止对 Jev 的请求。插件自己的计时器触发时记一个标记,据此区分:计时器触发的记为 `timeout`;这一轮本身被取消的什么都不记——这一轮根本没有发生。 **失败也缓存到本轮。** LLM 请求重试时会再跑一遍 `agent/request`(见 [§5](#5-一轮一决策))。如果不缓存,Jev 超时之后每次重试都要再等一次 `timeoutMs`。现在失败同样记入本轮,重试直接放行。 ### 不显示为失败的情况 这些不是 Jev 出了问题,而是这一轮 Jev 不适用,插件不写记录,芯片保持上一次的样子: - `agent/pre-step` 没抓到当前消息(空批次、纯插件注入,比如后台任务的通知唤醒了会话) - 当前模型没有推理等级,或者 adapter 暂时描述不了它 - 子代理会话(见 [§5](#5-一轮一决策)) `seenSelections` 不论成败都更新,因为它记录的是选择器,与 Jev 无关。 --- ## 12. 被否掉的方案 记录下来,免得再走一遍。 **每轮最多降一档(迟滞)**。想法是防止"继续"从 High 直接掉到 Off。被"几点了"否掉:复杂重构后问几点,要经过 Medium 才能到 Off,多想了一轮。后来实测"谢谢"也证明它太钝——已完成的活之后说谢谢,会从 High 卡到 Medium。换成"只在延续未完成的活时不降档",一条规则覆盖,且不误伤新问题。 **为"被中断"单独建规则**。上一轮没做完的原因不止中断:报错、超长度、被打断……为每种都写规则是死路。改成读 `turn/end.reason.kind`,`completed` 之外一律算"未完成",一个字段覆盖所有情况。 **手动判定用日志里的 effort 比**。见 [§6](#6-手动选择的判定):日志记的是 Jev 改写后的值,比不出来。 **当前消息也从投影读**。见 [§3](#当前消息的来源不同):决策时它还没进日志。0.3.0 因此静默失效。 **芯片持久化**。见 [§7](#7-芯片的生命周期):会展示一个没人记得的决定。 **插件自有 storage 存决策**。可以让芯片扛重启,但违反"能从日志重建的不自己存、不能重建的承认易失"的原则,且多一套要维护的持久化。 **芯片改成只显示"Jev"不显示置信度**(便宜方案)。能做,但 Jev 超时失败时沿用调用方等级,这时标"Jev"不准确。既然通道能造出来,就直接做完整的。 **斜杠命令 `/jev on|off` 作为本会话开关**。是 SRC 通道造不出来时的兜底。探路成功后作废。 **用 `request/header` 当芯片的刷新信号**。见 [§7](#7-芯片的生命周期):它只在 config 变化时写入,Jev 连续选同一档时芯片不刷新。0.3.1 及之前都有这个问题。 **重试时重新问 Jev**。见 [§5](#5-一轮一决策):多一次调用,同一步可能前后两个等级,而且芯片不知道。 **让选择器始终显示你手动选的等级**。做法是每次请求后,插件往日志里补写一条 `model/selection`,把你的选择重新设成 pending。否掉的原因:插件要写会话日志(虽然是 harness 认识的事件类型,不会写坏,但违背了 [§10](#10-为什么不写会话日志) 的原则);等于替你伪造了"用户手动选择";harness 每次请求都会重新套用它,再被 Jev 改掉,两边一直拉锯;手动判定也得额外区分哪些选择是插件写的;而且选择器显示 Low、实际跑的是 Medium,这本身就是在骗人。 **关掉 Jev 时自动退回你最后手动选的等级**。可行:你最后一次手动选择在日志里有记录(`model/selection`),重启后也在。暂未采用,维持 harness 的原生语义。 --- ## 13. 已知的未完成 - **Jev 调用的延迟**:每轮第 1 步前串行等 1.5–2.4 秒。理论上可以和请求组装并行,未做。 - **全局关时的入口**:现在全局关就完全隐藏芯片,没有地方能看到/打开它。可以改成显示 `Jev 关`,点击提示去设置。 - **Jev 不适用的轮次**:没有用户文字的轮次、模型没有推理等级时,芯片仍显示上一次的决策(见 [§11](#不显示为失败的情况))。后者可以直接隐藏芯片。