dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
内容退化检测(思考打转、输出复读)+ 继续/交接成本裁决 + 零模型交接文档。
DeepSeek Harness(DSH)长会话的退化守卫与交接工具。
当 DSH 在长任务中出现思考打转、回答复读,或你准备换新会话继续任务时,本插件从本地会话日志给出可核对的退化信号与交接建议,并生成不依赖模型总结的 Markdown 交接文档。实时守卫检测到疑似思考空转时会中止当前生成;插件不会自动创建新会话,也不会上传日志。
English: A local DSH plugin for long sessions. It detects reasoning loops and repeated output, can stop a suspected runaway turn, and creates a model-free handoff from session logs.
适合:经常运行长会话、遇过模型复读或空转、需要把未完成任务带到新会话的用户。如果只想看 token 占用或费用统计,可先看专门的 dsh-context。
使用前须知:守卫命中时会立即中止当前生成,而且没有"仅提示"或"关闭守卫"的开关 ——
它误判了你的正常推理时,你无法降级,只能改配置或卸载(2026-10-01 本机的首条真实命中就是一次误报,
形态见下方「已知边界」)。命中记录默认在本地保留思考尾部 500 字。设置 DSH_ATTENTION_HEALTH_GUARD_TAIL=0 可减少尾部留存,但仍保存首尾各 40 字及可能含原文的命中原因/样本。详见下方「隐私与威胁模型」。
这个插件读本地会话日志(不联网、不额外调用模型)做三件事:
当前状态:三个阶段均已落地并实测生效(离线工具 → host 插件 projection → Web UI 提示条)。
标准 DSH 插件包(host + 浏览器双面,一个包):
dsh plugin --profile web add -w dsh-attention-health
-w 不能省:profile 自身是 pnpm workspace 根,否则会报 ERR_PNPM_ADDING_TO_ROOT;node_modules/.modules.yaml 记着
创建它的版本;版本不一致会报 ERR_PNPM_VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF);dsh plugin --profile web add -w github:donghangxunlang-cmd/dsh-attention-health;dsh plugin --profile web remove -w dsh-attention-health。装完必须重启 DSH(插件行不会热加载;浏览器半只需刷新页面)。
本机实测:dsh-node.err.log 出现 [attention-health] projection "attentionHealth" registered,
且页面启动图里出现 <包名>/client.js。
在 GitHub 源码副本中装好依赖(Node ≥22.19;npm install --no-audit --no-fund)后:
node tools/demo.mjs
合成样本会依次演示:正常思考放行 → 重复思考调用取消接口 → 建议交接并生成文档 → 检查实际留存字段。
演示调用当前插件代码,使用临时 DSH_HOME,不需要真实会话或模型凭据;宿主与取消动作由脚本模拟。
演示工具只随 GitHub 源码提供,不包含在 npm 插件包内。
完整步骤与真实 DSH 验收见 一分钟演示与首次安装验收。 外部安装成功、交接体验和误报/漏报均可通过 反馈入口 提交; 优先提供版本、现象和合成样本,不要直接上传私人日志或交接文档。
由 AI 主导开发——需求、验收标准与真实场景测试由作者提出,代码、测试与文档由 AI 在长程会话中编写, 并经过多轮独立审查与真实数据校准(五套自动化测试;阈值来自 2,244 个真实思考块与 79 个会话实测)。 它不是随手生成的玩具,也不是专业团队打磨的产品。
维护状态:个人项目,维护能力有限;欢迎 issue 与 PR,但不承诺 SLA。
三个能力:
除守卫外,其余能力都不调用任何模型;守卫本身也只做字符串判据,不额外发起模型请求。
核心设计立场:要交接的时刻,正是模型已经不可靠的时刻。所以交接文档绝不能靠 LLM 写——让退化的模型总结它退化的原因,产出必然是垃圾。会话日志是 append-only 的事件溯源记录,数据本身完好,与模型状态无关。
DSH 生态里已有更成熟的上下文洞察工具,本插件刻意不重复它们:
| 你想要的 | 用哪个 |
|---|---|
| 上下文占用 / 构成 / 趋势、逐请求浏览、成本估算(元) | dsh-context —— 它读官方 token-meter 投影,比本插件自算的更准更细 |
| 一键导出交接文档(LLM 总结式,交互更直接) | dsh-handoff-button 等 |
| 按规模 / 成本 / 缓存判断"要不要新开" | dsh-context-compass 等(已覆盖,UI 更完整) |
| 按内容退化信号(思考打转 / 输出复读 / 守卫命中)判断"要不要交接" + 零模型交接文档 | 本插件(生态里没有替代) |
所以它只做生态不做的三件事:
dsh-context-compass)
主要看规模 / 成本;本插件看的是内容退化(思考打转 / 输出复读 / 守卫命中),
并据此主动给出"该交接了"的结论。成本明细只在结论是交接时作为依据显示,其余情况交给 dsh-context。
# 健康自检:当前工作目录的最新会话
node handoff.mjs --health
# 健康自检:指定会话(支持 ID 前缀)
node handoff.mjs d346d3e8 --health
# 生成交接文档到文件
node handoff.mjs -o HANDOFF.md
# 指定会话 + 限制规模
node handoff.mjs 6938bea6 --max-turns 30 --max-requests 20 -o HANDOFF.md
# 列出会话
node handoff.mjs --list
# 生成 + 打印解析统计
node handoff.mjs --stats -o HANDOFF.md
# 汇总历史交接记录(阈值回溯素材,零 token;见下文「校准留痕」)
node handoff.mjs --calibration
--health 输出示例(2026-09-15 实测,对象是本机真实发生过的退化事故会话):
会话 :session-SAMPLE-e79e963a
健康评分 :50 / 100 — 上下文偏大
上下文规模 :353,386 token
占声明窗口 :35.3%(窗口 1,000,000)
有效占用 :58.9%(有效窗口 600,000,系数 0.6)
思考异常 :1 次(打转 1 / 其他 0),最长同一行重复 69,667 次
发现:
- 已过有效窗口一半(评分 −25)
- 思考过程出现打转:1 次(最长同一行重复 69,667 次)(评分 −25)
上面是 2026-09-16 边界设定后的真实输出。同一条会话在旧系数(0.5)下是 30 分 / 上下文过大, 现在 50 分 / 上下文偏大 —— 因为有效窗口从 500K 变成 600K,分母变大、判定变宽。 这是有意的:Chroma 的 Context Rot 研究给的是「声明窗口的 50%~60%」(见下文第 3 节), 0.5 取的是区间下端、0.6 取上端,同时让"何时劝交接"不再早到离谱(见「双口径」一节)。
那次事故:模型在思考过程里把「好。」重复了 69,667 次,
reasoningTokens打满 256,000 才停 —— 而同一轮的最终回复完全正常,所以只看输出的旧判据一个字都没报。 现在思考退化单列一行,并且参与评分(见下文「退化检测」)。
等级文案(正常 / 留意上下文 / 上下文偏大 / 上下文过大)只描述上下文规模, 不含行动建议;要不要交接,看交接文档里的「维度 A 质量(异常信号)」与命中率成本。 评分为启发式指标(阈值内部校准、未做回溯验证):看同一会话的趋势可以,不代表精确测量。
| 章节 | 内容 | 数据来源 |
|---|---|---|
| 0. 为什么要交接 | 健康评分、上下文规模、风险信号 | usage + 事件分析 |
| 1. 会话标识 | ID、标题、cwd、preset、模型、窗口、时长 | header + request/context |
| 2. 当前目标 | goal 的 objective / phase / 轮次 | goal/change |
| 3. 用户请求主线 | 全部人类输入(含问答回答) | user/message + ask_user_question 结果 |
| 4. 各轮进度概要 | 每轮的请求、结果、工具数、错误数、结束原因 | turn/* + assistant/message |
| 5. 涉及的文件 | 路径 + 操作类型与次数 | tool/call 的 arguments |
| 6. 待办事项 | checkbox 列表 | todo/write |
| 7. 遇到的错误 | 工具失败 + 轮次异常结束 | tool/result.isError + turn/end.reason |
| 8. 工作量统计 | 轮次、步骤、工具分布、token | 全量事件 |
会话里健康提示条展开后有一个「复制交接内容」按钮,点一下:
GET /attention-health/handoff?sessionId=…;实测:当前会话 3ms 出结果、4516 字符;最大会话(9745 事件)也是 4ms 级别。
| 环节 | 用的接口 | 说明 |
|---|---|---|
| 生成内容 | host 半 webServer.register({kind:'prefix', path:'/attention-health', handler}) | 原生 (req,res) 通道。官方 @Remote/ctx.remote 需要 typert 生成链,本机无源码 checkout 无法构建,故不用 |
| 读会话事件 | 活会话 ctx.sessions.get(id).snapshotEvents();冷会话 ctx.sessionQuery.readSession(id) | 活会话优先,拿到的是内存中最新内容 |
| 复制剪贴板 | navigator.clipboard.writeText,兜底 document.execCommand('copy') | 两条都失败时明确报错,不假装成功 |
⚠️ 注册路由必须用 ctx.inject(['webServer'], cb),不能写成 ctx.get('webServer'):
profile 的加载条目是并行初始化的,插件常常在 webserver 之前跑完 apply,
那一刻 ctx.get 只能拿到 undefined,路由就悄悄丢了(浏览器点按钮报 HTTP 404)。
这个坑真实踩过,test/plugin-test.mjs 里有一项专门守着它(mock 的 get('webServer')
故意返回 undefined,只有走 inject 才能注册成功)。
曾经实现过:客户端 uiWorkspace.startSession() 新开会话 + 新会话 slot props 的
inputActions.setDraft(text) 写入草稿。这两个接口本身确实存在(都是官方 contract),
但实测客户端 ctx.get('uiWorkspace') 在 apply 阶段同样拿不到服务(与 host 侧同一个
时序问题),功能不可用。按用户决定改为剪贴板方案:不依赖任何服务时序、一步到位、零副作用。
失败时不会静默降级:按钮旁会显示具体原因(后端错误 / 剪贴板被拒等)。
文档头之后(「给接手方」操作指引 → 零模型声明 → 隐私提示 → 生成时间 / 数据来源):
| 小节 | 内容要点 |
|---|---|
| 为什么交接(健康度) | 评分、上下文规模、有效占用,并附评分机制说明(分数只由单会话上下文规模驱动、与时间无关;质量维度需伴随退化信号才给交接结论:≥70% 有效 + 退化 → 建议交接、≥90% 有效 + 退化 → 立即新开会话;只有规模大、无退化则交给压缩/成本维度) |
| 会话 | ID、最新标题(含曾用标题)、cwd、preset、模型、时间跨度 |
| 最新目标 | 优先最新 goal/change;无 goal 时取最近一条用户请求;首尾差异大时并列「最初目标 / 最新目标」并提示以最新为准 |
| 用户请求主线 | 超长正文(>320 字)自动压缩成摘要,原文进附录 B |
| 关键决策与未决问题 | 从助手文本与问答回答里抽句,去重、按时间排序、标注来源轮次与角色。两级词表:主表是 25 个取舍词(根因 / 结论 / 决定 / 改为 / 放弃 / 必须 / 风险…);当整场会话主表命中不足 3 条时,自动用通用结论词(发现 / 推荐 / 因此 / 最优 / 汇总 / 合计)兜底补抽 —— 咨询、报表、生活对话里的数字结论(如"散件合计约 11900 元")靠这一级补回。文档里会写明本会话是否启用了兜底 |
| 各轮进度 | 最近若干轮的请求 / 结果 / 工具 / 错误 / 结束原因 |
| 涉及的文件 | 文件工具清单(精确)+「可能涉及(来自命令)」(从 pwsh/bash 文本启发式挑出的写文件动作) |
| 待办 | 最后一次 todo/write |
| 遇到的错误 | 按错误码聚合:× N、出现轮次,以及"第 X 轮之后未再出现"或"最近一轮仍出现(未解决)" |
| 下一步 | 规则推导(未闭合轮次、未解决错误、进行中待办…) |
| 统计 | 总量 + 各轮上下文增长表(增量 / 输出 token / 工具结果字符 / 主要来源),并列出增长最快的轮次及成因判定(大工具输出 vs 模型长输出) |
| 附录 A | 最近 1~2 轮完整原文(不截断,总长上限 20,000 字符,超出在尾部截断并标注);自动跳过"继续 / 好了"这类指令型轮次,并在标题注明跳过了哪几轮 |
| 附录 B | 超长用户请求原文(上限:完整档 12,000 字符;精简档只展开第 1 条 ≈900 字符,超出截断并标注) |
全部为本地规则:零模型调用、零 token、不联网。
两个维度各用一个坐标系,运行时并列展示、各自标注口径,互不换算:
| 维度 | 口径 | 阈值 |
|---|---|---|
| A 质量 | 有效窗口(0.6×声明) | 规模且退化才动手:≥90% 有效 且检出退化信号(重复调用 / 输出退化 / 思考打转 / 思考异常)→ 立即新开;≥70% 有效 且检出退化信号 → 建议交接;只有规模大、无退化 → 本维度不给结论(交给 B) |
| B 压缩 | 声明窗口(与官方线同坐标) | <50% 正常;50 |
覆盖规则:退化信号优先于压缩;压缩后仍 >60% 声明、或已发生 ≥2 次模型摘要 → 建议交接。 ⚠️ 这两条覆盖规则都还要过一道成本闸门:按真实价格继续若并不比新开贵,就不改判(只在依据里保留"反复摘要有损"这个事实)。
行动建议只有四档(AG 节 2026-09-22 之后;压缩不是建议 —— 它已退出行动域, 因为手动压缩的一次性成本在官方线到来前摊不回来,而到线官方会自己处理):
| 档 | 含义 | 界面 |
|---|---|---|
| 可以继续 | 没有触发任何判据 | 绿 #22c55e ● |
| 每轮成本偏高 | 省额为正但不值一提(< 门槛的 60%)—— 只是状态,不是建议 | 浅蓝 #38bdf8 · |
| 接近交接阈值 | 钱上快划算了;或钱已划算但会话还早(轮数 < 15)—— 不催促 | 浅黄 #facc15 ○ |
| 建议机械交接 | 钱上划算且会话已成熟;或有退化 / 守卫信号 | 黄 #eab308 ● 加粗 |
为什么有"会话还早"这道软门槛(2026-09-22 用户实测:「对话才 6 轮插件就建议机械交接了」): 一个 7 轮的会话在成本上确实"该换"(每轮十几次请求 × 二十几万存量,到官方线前省 ¥2.5~3), 但任务刚展开就换会话会把任务切成碎片,而"任务连续性 / 重建理解"这些代价不在成本模型里。 所以轮数低于
matureSessionTurns(15)时,成本通道最高只到"接近交接阈值"; 质量通道(退化 / 守卫)不受此限 —— 那是止损,不是打扰。 标定与逐会话对照见work/ag-gradient-scan.mjs(82 个真实会话:4 个从"建议交接"降为"接近阈值")。
为什么质量维度要加「且退化」这个前提(2026-09-16 用扫描矩阵实测出来的): 旧规则里"有效 ≥90% → 立即新开"只看规模,而 90% 有效 = 声明 45% —— 早于官方线(80% 声明)。 结果是声明 45%~100% 全区间都被
drivenBy=quality的 handoff 顶替, 那条「现在手动/compact,否则官方即将自动压缩」的建议一次都没出现过(官方线分支永远不可达)。 而"上下文大"本身不是故障信号,真正不可救的是退化(压缩只腾空间、不修退化); 反过来没有退化,也就没必要为了规模换会话。为什么压缩档位改锚声明窗口:锚在有效窗口(0.5×声明)时,声明 40% 起就全部落进 urgent, 紧急档失去区分度;更要命的是它必须与质量维度共用坐标,质量维度一旦只看规模就会遮蔽整条压缩曲线。
推迟压缩(2026-09-16 用户反馈后新增):档位只说明"风险在升高",该不该现在动手还要看
「回本能否在官方自动压缩线到来之前兑现」—— 因为官方压缩到线时先做零 token 裁剪
(prune;仍会丢弃工具结果中段),只有不够才调模型做有损摘要;而手动 /compact 是立刻调模型、立刻付全价。
于是满足任一条件就不催压缩,改为「可以继续(暂不必压缩)」:
官方线分支排在推迟规则之前:到了官方线就不再谈"值不值"—— 那是控制权问题。 手动压缩与自动压缩都要调模型、都有损,区别只在谁定时机、以及之前能否先用零 token 裁剪争取空间。 (代理线定为 60 而不是 50:50 正好等于档位起点,会让这条代理判据永远不可达,本轮踩过。)
同时修正两句不实文案:不再出现"有效窗口 ≥80% 但离官方线还远,却说否则官方即将自动压缩"; 推荐档不再说"先压缩再继续"(档位本意是"阶段结束点执行")。
cacheFactor(0.02)+未命中率×1) 每轮;
B 先压缩 = 被压缩历史×缓存系数 + 摘要输出×全价 + 缓存重建(1.5 轮),之后 压缩后上下文×同系数 每轮;
C 机械交接 = 交接文档实测长度(两遍渲染回填,不是固定值);新开总额 × 1.5 < 继续总额"
对任何窗口长度都无解 —— 实测 76 个会话里旧判定命中 0 个,成本维度等于被钉死。)handoff.mjs CLI 入口(--list / --health / -o / --stats)
⚠️ 渲染**直接复用** lib/handoff.js 的 buildHandoff
lib/ ← 2026-09-18 标准包化:仓库根 == npm 包根,全部源码都在这里
zstd-frames.mjs Zstandard 多帧扫描与解压
session-log.mjs 会话定位、代际选择、事件解析
extract.mjs 【已弃用】早期的第二套折叠/渲染实现,仅供测试交叉验证
detect.mjs 【离线侧】扫日志后**交给插件的同一套判据与评分**(不再自持实现)
—— 下面是运行时(host + UI)的两半,也是产物质量的**唯一来源** ——
index.js host 半:attentionHealth projection + 交接生成路由
handoff.js barrel:只转发下面 6 个模块的导出(CLI 与插件都 import 这个路径)
handoff-core.js 常量 / 评分 / token 口径(不依赖任何兄弟模块)
handoff-text.js 文本工具(截断 / 清洗 / 切句 / 决策句抽取)
handoff-judge.js 退化判据(全项目唯一实现)
handoff-fold.js 事件流折叠(冷会话日志 → 结构化事实)
handoff-plan.js 计划推导(评分 → 继续 / 压缩 / 交接)
handoff-render.js Markdown 渲染(交接文档)
history.js 交接历史留痕(`--calibration` 的素材;纯本地、零 token)
guard.js 实时思考守卫(复读 / 空转即中止本轮)
guard-log.js 守卫现场落档(尾部 500 字,可减少留存,仍有原文片段)
prices.js 价格数据与峰谷档位(可被 prices.json 覆盖)
jsonl.js JSONL 追加 + 512 KB 轮转
client.js UI 半:健康提示条 + 复制交接内容(浏览器 bundle)
cordis.patch.yml 本包作为 bundle 的挂载行(一行同时装入 host 与 UI 两面)
包形态(2026-09-18 标准包化):
package.json声明main: lib/index.js、exports["./client"]: ./lib/client.js、dsh.bundle.patch: ./cordis.patch.yml、dsh.client.platform: web—— 与生态里同为双面的dsh-context完全同形, 所以别人可以dsh plugin --profile web add dsh-attention-health直接装。 浏览器半的模块 id 必须等于包名(dsh-client-modules用解析出的包名当模块身份)。files白名单只放lib/+handoff.mjs+cordis.patch.yml+README.md+SECURITY.md+LICENSE,开发档案(DEPLOYMENT-NOTES/HANDOFF.md/work//test//*.ps1)不进包。能力声明与 CLI 入口(2026-09-22 生态审计后补):
dsh.seams: ["fs", "web"]显式声明 本插件用到的能力(本地 JSONL 读写 →fs;浏览器半同源取数 →web);bin: { "attention-health-handoff": "./handoff.mjs" }让离线 CLI 成为可声明的入口。 两者正是dsh-vet这类审计工具判断 "声明与实际是否一致"的依据 —— 不声明时它只能按perm.network-client之类的 per-capability 规则报警,声明后降为清单项。
安全审计(2026-09-22):按生态标准
dsh-vet对实际发布的 npm 包扫描, 评级 A(0 critical / 0 high / 0 medium)。剩下两条 info 是预期内的:perm.unreachable-files只点名lib/detect.mjs与lib/extract.mjs—— 它们是 测试与离线对齐专用(见上表与下面的"漂移防护"),不进插件运行时图, 因此从入口做静态可达性分析时确实"不可达";perm.network-client声明webseam 后 降为 info("已声明的能力,仅作清单")。复现方式:npm pack→ 解包 →npx dsh-vet .(要扫 npm 包,不要扫仓库根:仓库里的test/tools/会引入与包无关的告警)。 数据去向、能力边界与漏洞报告渠道见SECURITY.md。
漂移防护(2026-09-15 全量复核 N-1 / N-5):CLI 与插件曾各持一套渲染实现, 第二批修复的 12 项只落到了插件里,于是同一会话会产出两种质量的文档 (表格错位、附录 B 无上限、"最新目标"可能仍是"继续")。 现在
handoff.mjs直接import { buildHandoff } from './lib/handoff.js'—— 一处修改、两处生效,漂移在结构上不可能再发生;lib/extract.mjs的渲染层已标记弃用。 判据与评分同理:唯一实现是handoff.js的replyDegradeReasons()/scanReasoning()/scoreContextHealth(),lib/detect.mjs只负责扫日志、然后调用它们 (plugin-test会逐会话比对热/冷两条路径的 token 数与评分)。2026-09-18 D3:
handoff.js已按「判据 / 折叠 / 计划 / 渲染」拆成 6 个模块, 对外仍是同一个 barrel(导出 17 个名字,一个不多一个不少), 所以上面这些"唯一实现"的说法全部不变 —— 变的只是各自有了独立的文件边界, 改判据时不必再在 3,656 行里找依赖。
zlib.zstdDecompressSync(buffer) 只解第一帧(435KB 文件只解出 207 字节)zlib.createZstdDecompress() 流式报 ZSTD_error_prefix_unknownscanZstdFrames() 移植自 DSH 自带的
@deepseek-ai/dsh-session-persistence-jsonl(MIT),保证与官方实现一致实测:71/71 个会话解析成功,3 秒完成,0 帧失败。
usage.inputTokens 是未命中缓存的输入,不是上下文规模这是最容易踩的坑。实测某个长会话:
inputTokens: 142 ← 只有 142!
cacheReadTokens: 258944
totalTokens: 259196 ← 真实上下文规模
用 inputTokens 会把上下文低估三个数量级。 必须用 totalTokens
(= inputTokens + cacheReadTokens + outputTokens,已实测验证)。
用户环境里 contextWindow 被配成 1,000,000。但 Chroma 的 Context Rot 研究显示,
宣称百万窗口的模型在声明窗口的 50%~60% 处就已明显退化。
因此评分基于「有效窗口 = 声明窗口 × 0.6」,而占声明窗口的百分比仍然如实呈现 ——
事实与判断分开,避免误导。系数的唯一来源是 lib/handoff.js 的
COMPACT_DEFAULTS.effectiveWindowRatio(lib/detect.mjs 那份与它保持同值)。
ask_user_question 的回答不走 user/message它作为工具结果返回:{"answers":[{id, selected, custom}]}。
只统计 user/message 会漏掉这些交互(实测某会话 9 次人类输入中有 3 次是这样来的)。
user/message 的 source.kind 必须过滤实测分布:user × 6、agent-message × 3、subagent-settled × 3、
agent-instructions × 1、plugin × 1、goal × 1。只有 kind === 'user' 是人类输入。
session.jsonl.zstd(v0) / session.v2.jsonl.zstd / session.v3.jsonl.zstd。
v0 的词汇表不同(assistant/chunk、reasoning-chunks 等),且用 seq0/time0 字段名。
实测分布:v0 × 38、v2 × 3、v3 × 30。
todo 投影会在每轮开始被清空DSH 的 dsh-tool-todo 在 turn/start 时把投影重置为 null。所以必须自己折叠日志,
取最后一个 turn/start 之后的最后一条 todo/write。
str_replace_editor / write / edit)
完成的修改。通过 pwsh / bash 直接写文件的操作不会出现在日志中。repeat-tool-reminder
会在 3 次时提醒模型,模型通常会改)。近似重复(改了个路径)检测不到。usage.reasoningTokens ≥ 16,000(真实计量,与字符数无关)。contextWindow 依赖日志里的 request/context 事件。若会话从未记录该事件,
无法计算占用百分比。rc.3):全部阈值与判据是在
一台机器、dsh 0.1.5-rc.2 / 0.1.5-rc.3(两者都实测过)+ cordis 4.0.2 上、
用 79 个真实会话标定与回归的。换机器 / 换模型 / 官方改契约后,结论可能不再成立 ——
遇到不符请按 安全反馈说明 提供版本、现象与可选合成样本。repeat-inline 判据要求"周期单元含字母或数字",因此
命令参数 / 数字参数的连续重复会被当成复读 —— 本机首条真实命中正是此类:模型在正常地写
"连按 N 次同一按键清空输入框"的命令,同一个按键码被连续写出十几次,生成被中止。
(此处只描述形态:命中片段属于私人任务内容,具体命令、思考原文与会话标识都不进公开仓库。)
合成最小复现:work/guard-repro-inline-cmd.mjs(开发用探针,不随 npm 包发布)。
同类可疑形态:重复的纯数字列、连续相同的数值参数、重复的版本号片段。
该误报尚未修复(是否收紧判据属产品取舍,见仓库留档的候选方案与影响面)。prices.json 覆盖),
「每轮」按实测平均请求数换算;官方调价或改缓存规则后需要更新价表。插件全程本地:零模型调用、零 token、不联网、不启动子进程。但它确实会写盘,
位置与上限如下(全部在 $DSH_HOME\attention-health\,可用 DSH_HOME 环境变量整体改道):
| 文件 | 内容 | 上限 |
|---|---|---|
handoff-history.jsonl | 每次交接的一行统计(评分、建议、成本口径,不含对话原文) | 512 KB 后轮转为 .1.jsonl,共两份 |
guard-trips.jsonl | 守卫命中的种类、理由、detail,以及当时的思考原文尾部 | 同上 |
prices.json | 可选的价格覆盖表(没有就用插件内置价) | 手工维护,无轮转 |
守卫现场(唯一会落"原始文本"的地方):
dsh-node.err.log
每次重启覆盖,靠不住);DSH_ATTENTION_HEALTH_GUARD_TAIL=0,tail 字段为空,但仍保存最近尾部片段的
哈希、长度、首尾各 40 字;reason / detail.sample 也可能含原文。这不是零原文模式;120;host 内存中的尾部最多 500 字,设置更大值也不会保留更长。
非法值回落到 500。设置须由 DSH 启动进程继承并重启生效,不会清除已有记录或关闭守卫。复制交接前:精简版与完整版都可能包含用户请求、代码、路径和最近轮次的原文。 本地生成不等于自动脱敏;粘贴到另一模型服务或公开发布前,请先检查内容。 减少守卫留存不会改变交接文档,也不会改变 DSH 自己保存的会话日志。
清理入口:直接删掉 $DSH_HOME\attention-health\ 目录即可(没有索引、没有外部副本,
删了就是删了);插件下次用到时会自己重建。交接历史丢了只影响阈值回溯分析,不影响功能。
威胁模型:插件的 HTTP 路由 GET /attention-health/handoff 是本机通道,把守条件为
四层同时成立 —— Host 是 loopback、sec-fetch-site 非 cross-site、Origin(若带)与 Host 同源、
以及连接来源 remoteAddress 是 loopback。
isTrustedApiRequest 同样不校验(token 校验在 browser-auth 层,
值存在模块私有的 PROCESS_LAUNCH_TOKENS WeakMap 里,插件拿不到),所以这里不发明新机制;
remoteAddress 是唯一不可伪造的一层(Host 头可以伪造),当前只绑 127.0.0.1 时它无副作用,
将来若开放局域网,它能挡住"外部伪造 Host"这一类;~/.dsh/sessions
(交接文档的全部素材都来自那里),所以这条路不额外扩大暴露面。旧判据在全量 73 会话 / 4,185 条回复 / 3,491 个思考块上的实测表现:
| 旧判据 | 真实触发 | 处置 |
|---|---|---|
| 中英严重混杂(全篇中文占比 5%~25%) | 25 次,25/25 全是误报 | 删除 |
| 长段落逐字重复(>60 字) | 1 次 | 阈值降到 25 字(中文一句话常 25~50 字) |
| 围栏不闭合 / 同行复读 / 符号堆砌 | 0 次 | 被全局 length < 80 门槛挡住 → 门槛删除 |
误报的代价是具体的:规则 5 每条扣 6 分、最多扣 30 分,还会让提示条显示"输出异常 N 条" —— 而误报会让用户忽略提示,比不提示更糟(这是插件自己写下的立场)。
另外「符号堆砌」原来会把 Unicode 边框表格(╔══╦══╗)判成退化,现在排除了表格框线字符;
命中原因现在会带进提示条与文档(如"输出异常 2 条(符号堆砌 ×2)"),用户能自己判断提示可不可信。
N-5 排除了框线字符,但漏掉了更大的误报源:代码本身。全量实测 (70 会话 / 3,638 个思考块 + 2,271 个回复块):
| 判据 | 思考侧命中 | 回复侧命中 | 人工判定 |
|---|---|---|---|
| 围栏不闭合 | 0 | 0 | 从未触发(安全网) |
| 同行复读 | 0 | 0 | 从未触发(安全网) |
| 段落复读 | 0 | 1 | 真实重复(同一段代码块逐字出现两次) |
| 符号堆砌 | 21 | 1 | 22/22 全部落在代码里 |
| 思考打转 | 1 | — | 真事故 |
| 思考异常长 | 1 | — | 真事故 |
命中的串清一色是模型正在思考正则和代码:
/^[{}()[\];,.<>|=+\-*/\\_#`~^&%$@!?:"']+$/
`^\*\*(.+)\*\*$`
"((?:[^"\\]|\\.)*)"
('|---|---|---|---|');
也就是说,旧判据等于因为模型思考正则表达式而扣它 12 分(reasoningFlags 每条 −4、上限 −12)。
修复两条:先 stripCodeBlocks() 剥掉围栏代码块与行内代码;再要求命中串以非 ASCII 符号为主
(真实乱码 / 卡图必然是非 ASCII,而长串 ASCII 标点实测 100% 是正则或代码)。
复验:思考侧 21 → 2、回复侧 1 → 0。残留 2 条的形态是 ◆◇■□▲△▼▽…,
来自一个"正在写符号堆砌测试样本"的会话 —— 文本上无法与真乱码区分,属可接受的止损。
受此修复影响,3 个历史会话曾被错误多扣 12 分、另外 3 个多扣 4 分。
旧版只读 type === 'text',思考块一个字都没检查。而实测思考量是最终回复的 7.9 倍
(3,491 块 / 592 万字符 vs 2,284 块 / 75 万字符),用户描述的退化现象恰恰是
"对话长了以后思考过程夹杂一堆无用的东西和符号"。真实事故:
会话 :session-SAMPLE-bfd2b142(本机)
单个思考块:535,355 字符 / 116,152 行 / 只有 56 个唯一行(重复率 0.9995)
最高频行 :「好。」×69,667、「执行。」×23,214、「(执行)」×23,205
usage :reasoningTokens = 256,000(输出预算被打满)
旧判据 :**正常**(漏检) 新判据:思考打转 ×1 → 评分 30/100
新增两条思考判据(阈值由该事故 + 全量 3,491 个思考块零误报共同定出):
| 判据 | 条件 | 阈值敏感性(实测) |
|---|---|---|
thinkLoop 思考打转 | 行数 ≥30 且行重复率 ≥0.7 | 0.6/0.7/0.8/0.9 都只命中那 1 个事故块;0.5 会多带 2 个正常代码块 |
thinkHuge 思考异常长 | 单块 ≥10 万字符 | 实测最长正常块 44,249,事故块 535,355 |
thinkBudget 思考预算打满 | usage.reasoningTokens ≥16,000(真实计量) | 3,534 条带该字段的消息 p50=269 / p90=1,648 / p99=4,237,事故那条 256,000 → 语料上零误报 |
thinkBudget与thinkHuge不是重复:后者只看单个块的字符数, 遇到"多个中等块加起来烧掉预算"就会漏(每块都不到 10 万字符);thinkBudget按消息级真实 token 判定,且不依赖思考正文是否落盘(该字段 81% 的消息都有)。
思考退化参与评分:打转 −25/次(上限 −50)、其他异常 −4/次(上限 −12)—— 比输出退化(−6/条,上限 −30)重,因为一次打转就是几万 token 的输出预算被烧掉。
复核指出:所有阈值(30/50/65/70/80/90、扣分力度、0.6 系数)都是内部校准出来的, 校准目标是"分数与等级自洽",没有做过预测准确性验证。最低成本的改进是留痕:
$DSH_HOME\attention-health\handoff-history.jsonl 追加一行:
时间 / 会话 / 评分 / 等级 / 有效占用 / 压缩次数 / 退化信号 / 文档字符数;node handoff.mjs --calibration 汇总这些记录(评分分布、有效占用中位数、最近 10 次);攒够样本后就能回答复核提的那个问题 —— "评分 55 分时交接,新会话实际能撑多久" —— 用真实数据反推阈值。
--list / --health / 生成文档)实现:lib/index.js。2026-09-20 门 3 起,线上形态是标准插件包:
$DSH_HOME\profiles\web\node_modules\dsh-attention-health(由 dsh plugin add 装入,
层栈记在 profile 的 dsh.profile.bundles)。旧形态(profiles\web\attention-health\ +
profiles\node_modules\attention-health-ui\ 两个手拷目录 + patch 里两条 insert 行)
已退役但保留在磁盘上,作为回退线(快照见 <TOOLS>\DSH\留档\attention-health-发布前快照-20260920\)。
⚠️ 开发时的关键差别:改
lib/不会自动同步到线上。改完要.\install-package.ps1(打包 + 安装)再重启;自测用.\test-all.ps1(自动指向线上那份)。 这两个脚本随仓库公开;deploy.ps1/restart-attention-health.ps1是作者本机的开发工具 (内含本机 DSH 安装布局),不随仓库公开。
export { apply, inject, name },inject: ['sessionProjections']attentionHealth projection(ctx.sessionProjections.register(def)),客户端零代码自动同步apply(state, event) 纯同步折叠,无关事件返回同一引用(严格遵守投影契约)lib/detect.mjs 刻意保持一致,用测试保证不漂移历史验证(下表记录 2026-09-16 的 486 项;当前数量以测试汇总为准):
| 测试 | 规模 | 覆盖 |
|---|---|---|
test/plugin-test.mjs | 84 项 | 加载 / 注册 / schema 契约(含 wire 键集合逐键比对);折叠契约(无关事件返回同一引用、null 安全);6 个真实会话与离线版 token 数 / 评分 / 等级 / 退化计数逐一致;边界(缺 usage 不崩、正确使用 totalTokens、100% 有效占用判为 critical);畸形输入加固回归(null 事件、contextWindow 0/负/NaN、usage NaN/Infinity 均不得产出非法 state);推迟压缩规则;有损压缩的评分封顶;热/冷两条路径的退化判据逐项一致(含思考打转) |
test/ui-build-test.mjs | 100 项 | 客户端包契约、各 health 取值的渲染、"整条提示条只允许一个行动建议"、展开区排版(每项只出现一次、不出现未渲染的 Markdown)、思考异常行与命中原因、"为什么这么建议"与三方案成本对照、缺数据时不许编造结论、host 未重启(缺新字段)时的向后兼容、结论说"别压"时不得出现省钱暗示、回本偏慢的语境、启发式标注 |
test/handoff-test.mjs | 278 项 | 提炼正确性、渲染完整性、边界健壮性、性能、两个输出档位、压缩不该"治愈"评分、退化判据的正/反例(含真实事故形态、旧误报样本、代码里的正则不算退化)、思考预算真实计量判据、上下文规模单一实现(缺 totalTokens 时三项相加、忽略 NaN/Infinity)、缺数据时"继续"与"无法判断"分开 + 界面依据 FINAL_NOTES(无 Markdown、不复述结论)、推迟压缩(回本 vs 官方线)、压缩次数覆盖规则的成本闸门(摘要 ≥2 但钱不支持新开时不得再劝交接;缓存真崩时仍须劝)、边界可达性守卫(网格扫描:四个档位 / 官方线分支 / 结构不变量 / 常量间结构关系)、文档侧不得给出"每轮省"暗示、分级词表、校准留痕、输出体积(附录上限不变式 + 确定性 fixture) |
test/guard-test.mjs | 13 项 | 实时思考守卫(正反例并重):正常推理 / 100 行各不相同 / 短行反复 / 同关键词多句 —— 均不得触发;同行连续 8 次 / 行内片段复读 / 窗口空转 / 真实事故形态 —— 必须抓住;enabled=false 与畸形输入安全;性能(20000 delta ≈ 80 万字符 < 2 秒) |
test/crosscheck-test.mjs | 11 项 | 多方面求证:74 个会话在热路径投影 / 冷路径日志折叠 / 离线 CLI 三条实现上逐项一致;全量不变量(score 区间、level 与阈值一致、计数非负、封顶生效、schema |