Skip to content

Releases: liancha22/dsh-puzzle-mode

v0.27.0 · 熔断之后继续推:报过不再闭嘴,第二次起撤掉「转人工」

Choose a tag to compare

@liancha22 liancha22 released this 04 Oct 19:06

v0.27.0 · 熔断之后继续推:报过不再闭嘴,第二次起撤掉「转人工」

一句话

重复思考熔断以前只报一次,报完就永久沉默——模型被点名后要是没换动作,
后面每一次重复都被静默放行,最后还得人工推一把。现在它会按间隔反复推,
而且第二次起不再允许「把问题抛回用户」。

症状(用户原话)

模型复读了之后它会熔断,然后熔断然后主动去揭发它,就让它继续……熔断了之后
他就没有继续……还是得人工手动。

根因:不是检测问题,是熔断之后的处置问题

检测一直是对的——noteAction 能准确认出「同名工具 + 同参数」连续撞了 3 次。
问题在报完之后:

const alreadyFired = previous?.signature === signature && previous.fired === true
const shouldFire = count >= limit && !alreadyFired

fired 一置位,签名不变就永远不再报。当时的理由是「第 4、5、6 次各报一条
比不报更烦」——防噪音防过了头:一次提醒不是熔断,持续推动才是。

于是模型有两条出路,两条都不需要它真的推进:

出路 后果
继续原样重复 无人提醒,静默空转,每轮都是实打实的成本
挑「停下来说清」 把问题原样抛回用户,熔断变成转人工

修法

① 报过一次不等于闭嘴:升级重报

新增 LOOP_ESCALATE_EVERY = 3:到阈值报第 1 次,之后每 3 次再报一次。
noteAction 多返回一个 level,记录这是这一段连击的第几次提醒。
噪音由间隔控制,不再靠永久沉默。

② 第二次起撤掉「转人工」这个逃逸口

loopBreakText 按 level 分档:

  • 第 1 档(刚撞阈值):维持原样,三选一——换输入 / 换动作 / 停下来说清。
    留着第三条是合理的,确实可能真卡住。
  • 第 2 档起:撤掉「停下来说清」,改成硬指令,按顺序不许跳步:
    1. 先捋事实——把上下文里已经拿到的信息列成几条结论;
    2. 再基于事实下结论 / 动手,不要再调一次工具去确认;
    3. 只有连「基于已有信息的最小下一步」都做不了时,才允许说卡住,
      且必须说清卡在哪一步、缺哪一条具体信息,不许笼统地说「需要你确认」。

为什么必须撤:那条出路原本是给「真缺权限 / 工具坏了」准备的,但实测里它成了
最省力的逃逸口——一被点名就挑它。第一次提醒留着它没问题;第二次还在原地重复,
就说明它不是真卡住,而是没在推进。

③ 文案封顶,但推动不封顶

LOOP_MAX_LEVEL = 3:到顶后不再加码,但仍然按间隔重报。
理由写在常量注释里:沉默比重复更贵——沉默等于放任空转。

④ 提示段同步改口

提示段原先只说「会注入一条提示」,与新的「会反复注入、第二次变硬」两说并存。
补上一句说明它不是只报一次,以及第二次起不要再试图问用户——那正是它要拦的行为。

验收判据

验证项 判据
检测未回归 连续 3 次同签名仍触发第 1 次熔断(shouldFire === true)
间隔内不刷屏 报过之后紧邻的 1–2 次不重复注入(LOOP_ESCALATE_EVERY = 3)
必须再报 没换动作时,每 3 次必须再报一次——这是「熔断后能继续推进」的核心判据
档位升级 第二次提醒 level === 2,文案含「先捋事实」且不再含「三选一」
逃逸口已撤 第 2 档文案含「现在不适用」,且要求「卡在哪一步」
封顶仍推 level 到 LOOP_MAX_LEVEL 后不再加码,但仍持续重报
换动作复位 签名一变,连击与 level 双双归零,重新给温和的第一档
接线未断 钩子层:不改动作时真的重复注入,且第二次是硬指令(只验纯函数会漏掉「没把 level 传下去」)
误报防线未松 参数每次不同 / 无参轮询(job_list)/ 工具被 block,一律不注入
全量 npm test 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5;跨插件握手 18 / 0

新增测试

test/60-loopguard.test.mjs 从 13 项增到 17 项,新增的 4 项正对着这次的回归:
间隔内不刷屏 / 没换动作必须再报 / 封顶后仍重报 / 接线层重复注入且第二次是硬指令。

变异验证(本仓约定:守卫必须能抓到漂移,否则形同虚设):
把 shouldFire 退回 firstTime(即旧的「只报一次」),测试在
「没换动作时每 3 次必须再报一次」那条当场变红;还原后复绿。
删掉这条断言,功能会静默退化回「只报一次」而没人发现——这正是它存在的理由。

装

python3 "$DSH_HOME/plugin-manager.py" github liancha22 dsh-puzzle-mode v0.27.0

或在插件市场点更新。装完重启该 profile,再刷新浏览器页面。

v0.26.1 · 修 op:audit 带源码体检必失败

Choose a tag to compare

@liancha22 liancha22 released this 04 Oct 18:35

v0.26.1 · 修 op:audit 带源码体检必失败

一句话

puzzle_mode{op:'audit'} 只要带源码体检,之前一律失败;现在正常了。
这是 v0.26.0 里就存在的一个返回体形状问题,与文档、与你的代码都无关。

症状

Error: tool "puzzle_mode" returned invalid output: value is not lossless JSON

出现条件:带源码体检(source 默认就是 true,也就是它的主用法)。
绕过方式是显式传 source:false —— 但那等于放弃了源码体检,
而体检正是 op:audit 用来发现「巨函数 / 没分层 / 死导出」的唯一途径。

根因

宿主对工具返回做「无损 JSON」校验:返回值必须能
JSON.parse(JSON.stringify(x)) 原样往返。显式 undefined 过不了这一关——

{ file: undefined }  →  JSON.stringify  →  "{}"  →  JSON.parse  →  {}   // 丢失了 file

lib/index.js 把源码体检的发现并进 findings 时,无条件写了 file: item.file。
而其中两条是项目级发现,本来就没有单个文件:

发现 事实
source_flat 「38 个源码文件全在同一层目录(.)」
source_long_function(截断提示) 「另有 N 个函数也超过阈值,未逐条列出」

它们没有 file,于是 file: item.file 写出了一个显式的 undefined 属性。

修法

字段要么是字符串、要么根本不存在:

...(typeof item.file === 'string' && item.file !== '' ? { file: item.file } : {}),

带 file 的发现照旧带 file(没修成一律不带);项目级的那两条不再带 file 键。

验收判据

验证项 判据
主用法可用 op:audit(带源码体检)成功返回,不再报 not lossless JSON
返回体无损 整个返回能 JSON.parse(JSON.stringify(x)) 深比较相等
项目级发现不带 file 造出「7 个同层文件」时 source:source_flat 命中,且没有 file 键
带 file 的照旧带 所有 source:* 里存在的 file 都必须是非空字符串
没修过头 source:false 仍可用,且不含任何源码发现
全量 npm test 全绿:60 + 47 + 7 + 51 + 13 + 36 + 12 + 5;跨插件握手 18 / 0

新增测试

test/90-audit-tool.test.mjs(5 项)。它不钉某个具体字段,而是断言
整个返回能无损往返 —— 这条判据不管以后哪个字段被写成 undefined 都会红。

为了让 source_flat 真的出现,测试会造出「7 个源码文件、只有一层目录」的形状
(只造 3 个文件不会命中,那样测试就是假的)。

诊断过程(为什么值得单独记)

inspectSource 与 auditOf 单独跑都是可序列化的 ——
所以问题只可能在「组装工具返回」那一段,而那一段只有真调一次工具才覆盖得到。
这就是为什么加的是工具级测试,而不是给 lib/source.js 补单测:
单测会全绿,而真机照旧报错。

装

python3 "$DSH_HOME/plugin-manager.py" github liancha22 dsh-puzzle-mode v0.26.1

或在插件市场点更新。装完重启该 profile,再刷新浏览器页面。

v0.26.0 · 固定收尾问可全局关闭

Choose a tag to compare

@liancha22 liancha22 released this 04 Oct 17:54

v0.26.0 · 固定收尾问可全局关闭

一句话

面板左栏多了一颗 固定收尾问:开 · 点此关闭。关掉之后,所有会话的提问都不再带
「要不要先停下?」——包括提示段、工具返回、拒绝理由与面板提示语四处。

用户原话

提问到最后还要选继续还是停下?的功能加一个全局关闭功能。

关掉之后到底变了什么

位置 关掉前 关掉后
提示段 「提问的最后一项固定问『要不要先停下?』…」 「固定收尾问已全局关闭(用户设置):不要再问…」「问到实质问题问完就结束」
工具返回 askPause:true + pauseQuestion + pauseOptions askPause:false,不带后两个字段
只拼不写的拒绝理由 要求「最后问一次『要不要先停下?』」 「固定收尾问已全局关闭,不要再问」
面板右栏 「每次提问的最后都会问:要不要先停下?」 「固定收尾问已全局关闭:提问不再带…」
面板「提问」模板 模板里塞一段收尾问 不再塞(否则等于用一个按钮把设置又打开一遍)

为什么关掉后要「不带字段」而不是「带 false」:留着 pauseQuestion 比不带更糟——
模型看到收尾问文案就会继续问,「关掉了」于是变成一句空话。字段在不在,本身就是给模型的信号。

默认值与作用范围

  • 默认是开,而且设置文件里缺这个字段也算开。
    这个开关的语义是「默认行为」而不是「新特性」——反过来(缺 = 关)会让所有老用户的
    提问静默变了行为,比功能本身严重得多。
  • 全局:写进 $DSH_HOME/.dsh-puzzle-mode.json,跨会话、跨项目、跨 profile 一致。
  • 它与左栏那颗「关掉本会话的拼图模式」是两个开关,共用同一个文件但互不覆盖
    (任一侧写入都带上另一侧)。这正是本仓记过的「同名不同形的字段会互相盖掉」,所以有断言钉着。

顺手修掉的一处测试脆弱性

10-puzzle / 30-rpc 里的断言会去读运行者真实的 DSH_HOME。用户一旦在面板上关掉
收尾问,npm test 立刻变红——而代码一行没错。测试不该依赖运行者的个人设置。

现在统一走 test/helpers/isolate-home.mjs,并且它必须在其它 import 之前:
ESM 的 import 会被提升,写在文件中间赋值环境变量太晚了(模块体在依赖求值之后才跑)。

实测:把真实设置改成 askPause:false 再跑全套测试,仍然全绿。

验收判据

验证项 判据
默认开 删掉设置文件后 pauseFields() 回 {askPause:true, pauseQuestion, pauseOptions}
缺字段算开 设置文件写 {"disabledSessions":[]} 时 isAskPauseEnabled() 为 true
坏值算开 "false" / 0 / null / [] 一律当开(只有显式布尔 false 才关)
关掉生效 走真实 RPC:method:settings 带 askPause:false 后,state 返回 askPause:false 且无 pauseQuestion
提示段跟着换 关掉后提示段含「固定收尾问已全局关闭」,且不再含「提问的最后一项固定问」
能再打开 askPause:true 后返回与提示段都恢复
互不覆盖 关收尾问后禁用名单仍在;禁用/恢复会话后 askPause 不变
不误伤会话 关收尾问不该让提示段整段消失(那是「禁用会话」的效果)
测试不受影响 把真实 DSH_HOME 设置改成关闭,npm test 仍全绿
全套测试 npm test 全绿:60 + 47 + 7 + 51 + 13 + 36 + 12;跨插件握手 18 / 0

测试

  • test/80-ask-pause.test.mjs(12 项):默认值语义、坏值、幂等、两个开关互不覆盖。
  • test/50-contract.test.mjs +2 条契约锚:默认开 + 关掉后不下发文案 + 互不覆盖。
  • tools/verify-ask-pause.mjs(7 项):走真实 RPC 验「设置 → 工具返回 → 提示段」三处接线。
    纯函数全绿而接线断了,正是本仓反复记过的假绿。
  • test/helpers/isolate-home.mjs:把 DSH_HOME 隔离成 import 顺序问题,不靠注释提醒。

装

python3 "$DSH_HOME/plugin-manager.py" github liancha22 dsh-puzzle-mode v0.26.0

装完重启该 profile,再刷新浏览器页面。

v0.24.2

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 14:02

v0.24.2 · 审查器没跟上「项目规模」——大档项目被全线误报

用户原话:

审查器没有同步规模改动
看看提示词有没有旧的无规模的限制提示词

两处都成立,而且第一处是真 bug:大档项目的审查结果全是假发现。


① 审查器三处写死了中档上限(真 bug)

「项目规模(小 / 中 / 大)」是 v0.23.0 加的:同一个项目写多细由档位决定。
写入侧改对了——一直走 capsOfSize(档位) / limitsOfSize(档位);
审查侧漏了——三处仍拿中档的尺子去量:

位置 原先(写死中档) 改成
条数上限 ENTRY_CAPS = 悬而未决 4 / 已定 10 capsOfSize(state.size)
模块条目字数 中档 20 字 按档位(大档 40 字)
主文档条目字数 中档 50 字 按档位(大档 80 字)

后果:大档项目(要点 40 字、已定 30 条)每一条合法内容都被报成违规。
实测对照(大档项目写 31 字要点 + 12 条已定):

修前:entryIssues limit=20 报「要点超长」
      over_cap 报「已定 12 条,超过上限 10 条」
修后:两条都归零

这正是本仓记过的「两把尺子」:同一份规格,写入侧与审查侧必须同源。
audit.js 里那句注释「上限是硬规则(悬而未决 ≤4 / 已定 ≤10)」也停在规模改动之前,一并改正。

修法是新增 mainEntrySpecOf(size) / moduleEntrySpecOf(size)(entries.js),
moduleEntries 加 size 参数(默认中档,所以旧调用点行为不变)。

② 提示词四处旧上限

位置 问题
文档锁理由 每次拦下 write / edit 都回给模型,最容易被读到——却写死中档值
提示段「文档分工」 写死「一句话 ≤50 字;坑 ≤20 字」
「条目级发现优先」 写死四个中档字数
工具 schema 的 content 说明 写死中档字数

而提示段自己两说并存:一行说死数字,下一行说「别信死数字,看返回里的 limits」。

现在统一改成「随项目规模变,以 op:read / op:size 返回的 limits 为准」。
两处刻意保持静态:

  • 工具 schema——它是提示缓存的一部分,不该随会话变;
  • 工作流的名字 / 步数上限(名字 ≤20 字 / 每步 ≤80 字 / 每块 ≤12 步)——这三个数与规模无关。

验证

新增 1 条断言,双向钉住:

方向 钉住什么
正向 大档的合法要点(中档超长、大档合法)不许被报超长
正向 大档的合法条数(>中档 10、≤大档 30)不许被报超上限
反向 中档项目里已有的超长条目仍须被报出来

反向那条不是凑数:只测正向的话,把上限全改成 Infinity 也能绿。

写这条断言时踩到一个真教训:我最初用 updateModuleSection 造「中档超长条目」,
测试当场报红——因为写入侧会直接拒绝(条目 25 字,超过上限 20 字),数据根本没落盘。
超长条目只可能来自旧文档(迁移不追溯),所以反向构造改成直接写文件——
那正是审查存在的理由。

两处变异验证都真的红了,且报出对应事实:

变异 结果
条数上限改回写死中档 ❌ 大档的 12 条已定不该被报超上限
条目字数改回固定中档 ❌ 大档的 25 字要点不该被报超长

npm test 全绿:60 + 41 + 7 + 49 + 13;跨插件握手 18 / 0。

跨插件契约本次未动(只改上限取值与提示文案),已复核三处段序一致(10120)、
六条分工条款全在、默认段序不撞 DSH 内置表。


验收判据

⚠️ 装完必须重启 profile(patchReload: startup)——本次改的是宿主半,不重启不生效。

重启后:

  1. 大档不再误报(最重要):把一个项目设成大档,写几条大档才合法的要点
    (21–40 字)和超过 10 条的已定,然后跑 op:audit——
    不应出现 entry_issue(超长)或 over_cap(超上限)这两类发现。
    若仍报「超过上限 10 条」或「超 20 字」,就是没修好。
  2. 中档仍会报(防「把上限放开成不限」):在中档项目里,用 read / write 工具
    手工改文件塞一条 25 字以上的要点(写入工具会拒绝,必须直接改文件),
    再跑 op:audit——应该报出 entry_issue。这条不响就说明审查根本没在量。
  3. 提示词不再自相矛盾:让模型读提示段,看「文档分工」一节——
    不应再出现「≤50 字 / ≤20 字」这类死数字,应是「随项目规模变,以 limits 为准」。
  4. 文档锁的理由也换了:在「只拼不写」会话里让模型 write 拼图文档,
    被拦时回的那句理由不应再列具体数字,而应指向 limits。
  5. 回归:op:size 三档切换仍正常;工作流的「名字 ≤20 字 / 每步 ≤80 字」照旧生效。

第 1、2 条都在真机上看;自动化断言只能证明「审查读的是档位上限、且校验真的在跑」,
不能替代真机。

v0.24.1

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 13:29

v0.24.1 · 两处「看着烦」的收拾:接续不翻代码 + 空态删占位

两处都是用户直接点名的观感 / 轮数问题。没有新能力,只有两件该早就做的事。


① 接续会话不再自己翻代码推进度

用户原话:

把接续会话里的分析代码删了吧,这种事交给专门的审查就行了,不然拆来拆去看得我烦。

「接续会话」按钮填进输入框的提示词,第 5 步原先是:

5. 结合代码与文档的**当前状态**判断进度(哪些写完了、哪些还是空壳 / TODO),从断掉的地方接着做

删掉,换成:

5. 从文档里读到的断点直接接着做——**不要从头重做已经做完的部分**。

理由与 v0.21.0 删「报五维最弱项」完全一致:接续会话的读者是干活的人,
不是评审。让它先报分数、或者自己翻代码推进度,都是把「继续做」变成「先做一轮评估」——
而后者更贵,因为它要真的把源码读进来。进度该由 op:audit 的客观发现给(那是专门的审查)。

刻意保留第 4 步「按源码索引跳、不要全仓搜」:那条不是分析代码,是防乱翻的护栏,
删了反而更容易满仓找。断言里专门有一条反向保护钉着它。
多绑定警告里同族的「逐个判断进度」也一并改成「逐个接上」。

② 空态中栏删掉虚线占位卡

用户原话:

把未绑定态面版中间的白色大块占位删了,没用。

删掉空态(未绑定态)中栏那块虚线卡:

删掉前                                删掉后
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│      [收件箱图标]      │
│  本会话还没绑定拼图项目   │  ← 与顶部副标题重复      ──── 全删
│ 新会话默认是空的,不会…  │  ← 但这句是有效信息      ──── 降级成一行提示
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
        ↓
   「或交给 AI」三个按钮        ← 位置前移

删它的两条理由:

  1. 标题与面板顶部副标题「本会话未绑定项目」重复——面板出现在空态本身就说明了这件事;
  2. 它占着中栏最值钱的位置——下面紧跟着才是「或交给 AI」的三个真按钮,那块卡把主操作往下推。

但那行灰字不能一起删(这条是测试拦下来的)

动手删整块时 npm test 当场报红:

AssertionError: 空态要说清不会自动占用别人的项目

卡里那行灰字是回答**「我的项目怎么不见了」**的信息(新会话不会自动占用上一个会话的项目),
来自 v0.14.0「一个会话只绑一个项目」那次大改,有一条断言专门钉着它。

所以最终处理成删占位、留信息:去掉虚线框、图标、重复标题,那行字降级成一行普通提示
(不占额外高度、不带框与图标)。信息不丢,占位没了。

顺带清理

  • 随之成为孤儿的 inbox 图标(16px 圆里一道横)已删——它只被这块卡用;
  • dshpz-empty / -emptyicon / -emptytitle 三个 CSS 类保留:「还没有模块」那块还在用它们,
    删了就破相(删之前查过引用)。

验证

新增 8 条断言,逐条做过变异验证(去掉修复即红):

断言 钉住什么 变异结果
接续模板不含「结合代码与文档」 ① 放回原句 → ❌ 红
接续模板不含「空壳 / TODO」 ① —
接续模板仍含「不要全仓搜」 ① 护栏 拆掉护栏 → ❌ 红
接续模板仍含「不要从头重做」 ① —
多绑定警告不含「逐个判断进度」 ① —
空态无 dshpz-empty 卡 ② 把卡放回去 → ❌ 红
空态无 dshpz-emptyicon ② —
空态无 dshpz-emptytitle ② —
(原有)空态仍含「不会自动占用」 ② 信息不许丢 —

npm test 全绿:59 + 41 + 7 + 49 + 13;跨插件握手 18 / 0。

UI.md 改过 → 已重渲染教程图(只有 04-已绑定态 与长图变化,与正文一致)。


验收判据

⚠️ 装完必须重启 profile(patchReload: startup)——本次改的都是宿主半 / 浏览器半,
不重启不生效。

重启后:

  1. 空态不再有大块占位(最直观):新开一个没绑项目的会话,打开面板——
    中栏第一眼就是「或交给 AI」那三个按钮,上方不应再有虚线框、图标或
    「本会话还没绑定拼图项目」这行标题。
  2. 但那句说明还在:中栏应仍能看到一行「新会话默认是空的,不会自动占用上一个会话的项目。」
    ——它没丢。若这行也没了,就是删过头了。
  3. 接续不再翻代码:点「接续会话」按钮,看填进输入框的提示词——
    不应再出现「结合代码与文档的当前状态判断进度」「空壳 / TODO」这类要求;
    应仍保留「按主文档的源码索引直接跳到源码,不要全仓搜」。
  4. 回归:三个按钮(照现有项目搭文档 / 快速建空壳 / 采访后再建)都还在,点了仍只填提示词、不自动发送。

第 1、3 条在真机上看;自动化断言只能证明「渲染树里没有那个类名」「提示词里没有那句话」,
不能替代真机——它证明不了版面看起来舒服。

v0.24.0

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 12:45

v0.24.0 · 重复思考熔断:模型绕圈时把它推出去

长任务里模型会陷进「同一步反复做同一件事」——反复读同一个文件、反复跑同一条命令、
反复用同样的参数调同一个工具。每一轮都要把整个上下文重发一遍,所以空转的每一轮
都是实打实的成本;而模型自己往往出不来,因为它看不到「我刚做过」(历史每轮重发,
重复的动作混在长上下文里不显眼)。

这一版给宿主加了一道熔断:同一个工具连续 3 次用同样的参数调用,就注入一条提示,
把模型推出去。


判据:工具调用签名,不是推理文本

「重复思考」在动作层有确定性指纹:工具名 + 参数。抓思维链要读模型输出的
reasoning,形状随模型/版本变、拿不稳;动作层的指纹稳定,而且可以纯函数测试。

签名做了两件必要的归一化:

  • 键排序:模型每次生成参数时键序可能不同({a,b} 与 {b,a})。不排序的话
    同一件事会算出两个签名,连击永远数不到 3——熔断就成了永不触发的死代码。
    这是有断言钉住的(变异验证:去掉 .sort() 当场红)。
  • 长文本截断:参数里塞了整个文件内容时,签名只留前缀,别把内存撑爆。

为什么挂在 tools/post-execute

不是 agent/pre-step——这是本仓已经用假绿换过的教训:

agent/pre-step 的 decision.messages 契约是 UserMessage[],
里面根本没有 tool-call。旧测试伪造了一条带 tool-call 的 assistant 消息,
于是「拦截生效」是假绿——真实运行时永远走不到那个分支。

真实的工具调用只在 ToolExecution 上(exec.name / exec.arguments)。
而 post-execute 的 additionalContexts 是唯一能「不拦、不打断、只提醒一句」的
通道(pre-execute 的 allow 分支会忽略 reason 字段)。这条与「工作流触发」同源。

三条误报防线(宁可漏报,不可误伤)

熔断最怕的不是漏报,是误伤正经工作——一个总在乱响的提醒等于没有。

防线 挡住什么
无参调用一律不计 job_list / 截图 / 读状态这类轮询工具,反复调是正常行为
阈值 3 连续 2 次相同调用极常见(改完再跑一次测试),留一步容错
同一段连击只报一次 报过之后要等签名变了才重新武装,否则第 4、5、6 次各报一条,比不报更烦

提示长什么样

触发时注入一条以 【拼图模式 · 重复思考熔断】 开头的消息:先把事实摆出来
(「你已经连续 3 次用同样的参数调用 X——这一步没有产生新信息」),再给三条具体出路:

  1. 换输入——参数里有什么不同,让这次调用真的带回新东西;
  2. 换动作——要的信息已经拿到了,直接下结论 / 动手改,不要再确认一遍;
  3. 停下来说清——确实卡住了(缺权限 / 缺信息 / 工具坏了),把卡点讲给用户。

语气刻意是陈述事实 + 给出口,不是训斥:模型不是「不听话」,是它看不到自己刚做过。
只说「别重复」等于没给信息,所以三条出路缺一不可(有断言钉住)。

提示段同时补了第二道(hook 判定 + 提示段兜底),与「首轮自动判定」同一条设计。


验证

新增 13 条断言(test/60-loopguard.test.mjs),按本仓约定断言引常量
(阈值取 LOOP_REPEAT_THRESHOLD,不写死 3),并且每条都覆盖接线与误报两侧:

组 钉住什么
纯函数 键序不敏感 / 工具名与参数敏感 / 无参不计 / 连击计数 / 只报一次 / 换签名重新武装 / 按会话隔离 / 空 sessionId 安全降级
提示文本 点名工具与次数 / 带标记 / 三条出路齐全
接线 真驱动 tools/post-execute:连续重复后真的注入了提示,且内容正确
误报自证 参数每次不同 → 一条都不注入;无参轮询 → 不注入;工具失败(block)→ 不插话

两处变异验证都真的红了(本仓规矩「守卫须能抓到漂移否则形同虚设」):

变异 结果
拆掉熔断钩子(接线断开) ❌ 必须有一个钩子...注入熔断提示
去掉签名的键排序 ❌ 键序不同 → 同一个签名

还原后复跑全绿,无残留。

npm test 全绿:59 + 41 + 7 + 49 + 13;跨插件握手 18 / 0。


验收判据

⚠️ 装完必须重启 profile(patchReload: startup)——本次改的是宿主半,
不重启的话钩子不会加载,熔断完全不生效(不会有任何报错,只是安静地不工作)。

重启后:

  1. 熔断会响:让模型连续三次用同样参数调同一个工具
    (最省事的造法:让它反复读同一个文件,或反复跑同一条命令)。
    第 3 次之后,下一轮应看到一条以 【拼图模式 · 重复思考熔断】 开头的提示,
    里面点名了工具名与次数(「连续 3 次」)。
  2. 不误伤:让模型连续调同一个工具但每次参数都不同(读不同文件)——
    不应出现那条提示。
  3. 轮询不算绕圈:反复调 job_list / 截屏这类无参工具——不应出现提示。
  4. 不打断:熔断不拦工具、不弹审批框。第 3 次调用本身应照常成功,
    提示只是附加在结果后面的上下文。若某次调用被拒或被要求审批,那就是 bug。
  5. 同段只报一次:继续原样再调第 4、5 次,不应每轮都报一条。

第 1、2 条都在真机上看;自动化断言只能证明「接线没断、签名算得对」,
不能替代真机——它证明不了模型真的会因此改道。

v0.23.3

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 11:57

v0.23.3 · 修「项目规模」这条线上的三个 bug(其中一个是静默删数据)

三个 bug 都长在「项目规模」这条线上。第一个会静默删掉你的文档条目,
而且是本插件在自己的文档上真的发生过。


① 重写 front-matter 漏透传 规模: → 静默删除条目(严重)

现象:往主文档追加一条「坑」(或写一次工作流、改一次会话绑定)之后,
规模: 这一行从 front-matter 里消失了。

后果不是「显示回中档」这么轻:

规模: 大   →(写一条坑,重写 front-matter 时漏透传)→   字段被抹掉
           →  下次读按「中档」算  →  中档的 pit 上限更严  →  下一次写入静默删掉超出的条目

实测代价(本插件自己的文档):主文档「坑」从 149 条被砍到 60 条,丢了 93 条。
被删条目只出现在工具返回值 droppedEntries 里——界面上没有任何提示,你不看返回值就不知道。

范围:三条路径都漏了,不只一条:

路径 触发动作
updateMainSection 写坑 / 写工作流 / 写主文档任何小节
writeWorkflowDoc 写工作流归档
writeSessionList 改会话绑定

setMainFields(改模式 / 改源码根那条路)本来就有这一行,注释里也写明了这个后果——
另外三条路当初漏了。三处已补齐。

这条也解释了为什么「改个模式就掉档」这类报告难复现:丢数据的动作与设置档位的动作不是同一个,
中间隔着一次无关的写入。

② 大档的「坑」比中档更严——切档反而收紧

SIZE_CAPS.中.pit = null(不限),而 大.pit = 60。实测(真驱动写入函数):

档位 写 70 条坑 结果
中 落盘 71 条 全留
大 落盘 60 条 被砍掉 10 条

这恰好违反分档的初衷(大项目更不该丢内容),而「坑」是只增不减的踩坑记录——
决策类被取代还有理由删,坑被删就是纯粹的信息损失。

改成 大.pit = null,恢复单调性:小 10 ≤ 中不限 = 大不限。
客户端 FALLBACK_LIMITS 同步——契约测试「与宿主逐字段相等」当场抓到这一处,正是它该干的活。

③ 切项目时面板沿用上一个项目的模式

用户报「执行模式怎么继承到下一个打开的面板了」。

不是数据被写串了(宿主侧两个项目的 模式: 各写各的,单独验过),
而是切项目时只改了「当前是哪个」的标记、没换 mode,而重读是异步的:

① 当前 A(只拼不写)   高亮 = 只拼不写
② 在 A 点「边拼边写」   高亮 = 边拼边写
③ 切到 B(回包未到)    高亮 = 边拼边写   ← 这是 A 的模式,就是「继承」
④ 回包到达              高亮 = 写后再拼   ← 最终才对

改成从胶囊就地取目标项目的 mode / health——胶囊本来就带这些字段,
所以这一步不需要任何额外请求。权威值仍以随后的重读为准,这里只是把窗口期填对。

modules 刻意没动:胶囊里只有模块个数、没有列表,填不对;清空又会多一次
「模块区闪一下空」的抖动。所以窗口期内模块区可能还是上一个项目的——这是已知的、
刻意留下的
同族残留,注释里写明了,不要以为它也被修了。


验证

新增 3 条断言,逐条做过变异验证(去掉修复即红):

断言 钉住什么
规模档位在所有重写 front-matter 的路径上都被透传 ①(7 条路径逐个过,不只测踩到的那条)
切项目:面板立刻换成目标项目的模式 ③
(20-client 已有的规模 / 模式两条延续) ②

第一条刻意写成「7 条路径逐个过一遍」(写坑 / 写工作流 / 写模块 / 改模式 / 改源码根 /
写会话 / 写归档),因为同一形状的坑在本仓已经出现过多次——只测踩到的那条,
下次换个入口又中。

又一次假绿(本轮第三次,成因各不相同)

切项目那条断言最初写成 onClick() 之后 await setTimeout(0) 再读渲染树。
但本地更新是在 current 请求的 .then 回调(微任务)里跑的,而 setTimeout 是宏任务:
回调还没跑就读树,看到的是「点了没反应」那一态——断言与修复无关,删掉修复照样绿。

改成排空微任务(await Promise.resolve())才咬住。三种假绿成因已全部写进注释:

  1. 宏任务越过回包(setTimeout 排在微任务链之后)
  2. 假 React 的 useEffect 每次渲染都跑,面板 effect 里的 load() 把乐观更新冲掉
  3. 微任务里的本地更新没排空

npm test 全绿:59 + 41 + 7 + 49;跨插件握手 18 / 0。


验收判据

⚠️ 装完必须重启 profile(patchReload: startup)——本次改的是宿主半,
不重启的话修复不生效,旧代码仍会砍你的文档。

重启后:

  1. 档位不再丢(最重要):把某个项目设为「大」,然后写一条「坑」或写一次工作流,
    再 op:read 看 size —— 应仍是「大」,front-matter 里也应仍有 规模: 大 那一行。
    若它变回「中」,就是本 bug 没修好(并且此时再写大段内容会开始删条目)。
  2. 大档不再比中档严:op:read 看 limits.entryCaps.pit —— 大档应是 null(不限)。
    实测口径:写 70 条坑,一条都不该被删(dropped 为空)。
  3. 切项目不残留:绑两个项目、模式各不相同。在 A 上点一个模式,再点 B 的胶囊——
    高亮立刻应是 B 自己的模式,不是 A 的。
  4. 回归:切换条一直在;规模三档的「当前上限」仍是三个不同的数字。

第 1、2 条都在真机上看;自动化断言只能证明「接线没断、字段没丢」,不能替代真机。

如果你已经在旧版本上丢过条目:被删的条目名与原文只出现在那次工具调用的返回值
droppedEntries 里,界面上没有痕迹。建议对重要项目先做一次外部备份(本插件
不做自动备份)。

v0.23.2

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 10:37

v0.23.2 · 修两个面板显示 bug:胶囊模式「弹回去」+ 规模切换「假态 / 延时」

两个都是真机报出来的,而且都不是「界面崩了」——是显示的值不对。
所以这版新增的断言一律写成「断言渲染出来的文本 / 高亮」,而不是断言内部字段:
读错字段时界面照常渲染,不报错、不空白,只有渲出来的字能抓住它。


① 写操作返回缺绑定组 → 胶囊上的模式「点了又自己弹回去」

现象:点「边拼边写」,按钮亮了,但切换条上那个项目的胶囊仍显示旧模式,
看着就像「点了又自己弹回去」。

根因:mode / size / current / workflow 四个写操作的返回原先只有
summarize(readState(...)),里面没有 bindings;而客户端 mergePanelData 的规则是
「新值没给这个键就沿用旧的」。

这条规则本身是对的(它防的是「切着切着胶囊没了」),但只防得住「值没了」,防不住「值过期」:
胶囊的悬停提示直接渲染 item.mode(健康性同理由 % 渲染),旧数组原样留着,
于是那个字段停在改动前。

修法:抽 bindingPanel(...) 一处算绑定组,state 与四个写操作共用同一份——
而不是让每个写操作各自拼一遍(那正是「两把尺子」类漂移的温床)。
客户端那层合并保留,降级场景(老版本宿主、返回被截断)仍然需要它。

② 项目规模切换「假态」+「延时切换」

假态:那行「当前上限:悬而未决 ≤x · 已定 ≤y · 工作流 ≤z · 坑 …」原先读
limits.sizeCaps——那是「小 / 中 / 大三档的整张表」,不是当前这一档。
于是 caps.pending 恒为 undefined,靠 undefined === undefined ? 4 : … 的兜底
永远显示中档数字:

你点了 那行显示 应该显示
小 已定 ≤10 已定 ≤6
中 已定 ≤10 已定 ≤10
大 已定 ≤10 已定 ≤30

真值在 limits.entryCaps(= capsOfSize(当前档))。改读它,并留两级回退。

延时:按钮只在回包后才变色(写盘 + 重读 + 回包)。
这三颗是「我点了一个开关」的直接反馈,必须按下即亮——改成乐观更新。
同一处纪律也补给了执行模式按钮(它原先同样要等回包)。

两处的失败分支都改成「回宿主重读」:乐观更新已经把界面翻过去了,
不拉回来面板就会停在一个没写进文档的假态上——正是这个 bug 的形状。


验证

新增 4 条断言,逐条做过变异验证(把对应修复去掉,断言即红):

断言 钉住什么
RPC:mode / size / current 都回完整绑定组且值是新的 ① 的根因(接线断了)
规模:三档渲染出三行不同的「当前上限」 ② 的假态
规模:扣住回包时点「大」也立刻翻档 ② 的延时
模式:扣住回包时点一下也立刻高亮 延时的同一处纪律

第 1 条刻意驱动真实 RPC 路由而不是只测 bindingPanel 纯函数——
这个 bug 的本质是接线断了(bindingPanel 自己怎么写都对,错在分支根本没调它),
纯函数断言全绿也照漏。

一个写进注释的坑:第一版断言是假绿的

「按下即亮」的断言,我第一版是 onClick() 之后 await 一个宏任务再读渲染树。
它把乐观更新整个删掉照样绿。 两个原因叠在一起:

  1. setTimeout(0) 是宏任务,排在微任务链之后——fetch → json → then 早已跑完,
    读到的其实是回包之后的状态;
  2. 测试用的假 React 的 useEffect 是每次渲染都跑的(真实 React 只在挂载 / 依赖变化时跑),
    而面板 effect 里有 load(sessionId)——那次 state 拉取会把乐观更新冲掉。

修法是「扣住回包 + 同步读树」:给写操作加一道闸门扣住响应,onClick() 之后
同步读树,此刻 store 里只可能是乐观更新写进去的那份。

这条值得单独说:断言绿 ≠ 断言有效。变异验证(去掉修复看它红不红)才是那把尺子。

npm test 全绿:58 + 41 + 7 + 49;跨插件握手 18 / 0(无限五代不在场,互校跳过)。


验收判据

刷新页面后(宿主半改动需重启 profile,纯客户端改动刷新即可):

  1. 规模:点「规模」区的小 / 中 / 大 ——
    • 按钮按下立刻亮(不等回包);
    • 下面「当前上限」那行三档显示三个不同的数字:小 =「已定 ≤6」、
      中 =「已定 ≤10」、大 =「已定 ≤30」。若三档都显示「已定 ≤10」,就是本 bug 没修好。
  2. 执行模式:点「写后再拼」/「边拼边写」—— 高亮立刻切过去。
  3. 胶囊:绑两个项目后点模式按钮,把鼠标悬停在那个项目的胶囊上,
    提示里应是新模式(形如「当前项目 · aaa(边拼边写 · 健康性 62% · 3 个模块)」),
    而不是改动前那个。
  4. 回归:切换条仍然一直在(不会切几次就只剩一个胶囊)。

以上 4 条都在真机上看;自动化断言只能证明「接线没断、渲染出的字跟着档位走」,
不能替代真机。

v0.23.0

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 07:17

v0.23.0 · 项目规模(小 / 中 / 大)+ 删掉面板那颗审查按钮

三件事:① 项目分档位、条目上限随档放宽或收紧,并让「照现有项目搭文档」自动判档;
② 删掉面板「看项目」区那颗「审查 / 真实值」按钮与它配套的显示块;
③ 查一遍缓存与提示段稳定性,修掉查出来的真问题。


① 项目规模:小 / 中 / 大

为什么要它:固定上限对两头都不合适——小项目(几百行脚本)的「已定 10 条」根本写不满;
大项目(几万行、十几个模块)四条「悬而未决」一上午就顶满,于是最旧的决策被静默挤掉,
而那正是文档存在的意义。

档位 悬而未决 已定 工作流 坑(主文档) 字数上限
小 4 6 3 10 与中档相同
中(默认) 4 10 5 不限 与升级前相同
大 12 30 12 60 要点/已定/悬而未决/可复用 40 字、详细记录 80 字、主文档条目 80 字

三个设计取舍:

取舍 为什么
存主文档 front-matter 的 规模: 跟着项目走——换会话、换机器都还在;且一个项目里的模块不该各写各的
中档 = 升级前的全部值 没写过 规模: 的存量文档一个都不用改,行为与升级前逐字节一致
写「中」时删掉那一行 默认值写出来是噪音;读侧认不出就当「中」

改档位:面板「规模」三颗按钮,或 puzzle_mode{op:'size', size:'大'}。

顺带修掉一个真问题(缓存检查查出来的)

提示段 POLICY_BODY 是模块级常量(为了缓存稳定——每轮注入的字符串必须逐字节相同,
否则提示缓存每步都 miss)。但它原先用 ENTRY_CAPS.pending(中档固定值)告诉模型
「悬而未决 ≤4 条」——而大档实际能写 12 条。

后果:模型照提示段写就少写;照写入侧写又「违反」提示段。两边数字不一致。

修法(两半,缺一不可):

  • 提示段不再写死数字,只说「各节上限随项目规模变化,动手前先 op:'read' 看返回里的 limits」;
  • receipt 下发的 limits 改成按当前档算(limitsFor(state.size)),与写入侧同一个函数。

这样提示段保持常量(缓存不破),真实上限由每轮返回携带。

「照现有项目搭文档」自动判档

adoptTemplate 加了一步:让模型按第 1 步真读到的数字自判档位并 op:size 写下来——
小(≲2k 行、≲3 模块)/ 中(≲20k 行、≲8 模块)/ 大(更多)。
说不准就写「中」(默认档,写错无额外后果)。

② 删掉面板的「审查 / 真实值」按钮

删了两样(用户裁定「看项目的审查没什么用」):

  • 「看项目」区的 「审查 / 真实值」按钮(它拉 op:audit 的 RPC);
  • 配套的 「真实值(声明 → 实测)」显示区——没按钮拉数据,它就永远是空的。

审查能力本身没动:op:audit 还在,「让 AI 动手」区的「审查(交给 AI)」仍可用;
state.findings 那块「审查 · 客观发现」也照旧(它随 state 下发,与那颗按钮无关)。

③ 缓存检查:查了什么、结论如何

检查项 结论
提示段是否随会话/时间变化 ✅ 稳定:跨会话、20 次调用逐字节相同(6684 字符),无时间戳 / 随机 / 会话 ID
提示段是否含项目名或绝对路径 ✅ 不含(用 <工作区> 占位),跨项目可复用同一份缓存
提示段是否写死条目上限 ❌ 是(见 ① 的修复)
写后立即读(同进程) ✅ 正确:setMode / updateMainSection 写后立刻读到新值
同毫秒连续追加 ✅ 正确:连续 5 次追加,条数 2 → 7 累积无丢失
外部直接改文件(不经 atomicWrite) ✅ 正确:mtime+size 指纹发现变化并重读
工具描述是否随会话变化 ✅ 常量(模块级),其中的 ${ORDER} 是进程内固定值(环境变量不会中途变)

验证

项 结果
npm test(10/20/30/40/50 + 握手) 58 + 40 + 7 + 49 全绿,握手 18 通过 / 0 失败
新增断言 4 条(规模中档=旧值 / 大档放宽小档收紧 / 提示段不写死上限 / receipt 是当前档真值)
逐条验红 改回去就红(实测:中档改 5 → 红;提示段写死 ENTRY_CAPS.pending → 红)

验收判据

  1. npm test → 退出码 0,末尾 跨插件握手校验: 18 通过 / 0 失败。
  2. 面板打开一个已绑定项目 → 五维下面出现**「规模」三颗按钮**(小 / 中 / 大),
    当前档高亮;下方一行写着当前各节上限(坑在「中」档显示「不限」)。
  3. 点「大」→ 按钮切换;再点「仅迁移格式」不相关;op:read 返回的 limits.entryCaps
    变成 {pending:12, decided:30, workflow:12, pit:60}。
  4. 切「中」→ 主文档 front-matter 里没有 规模: 那一行(默认值不留噪音)。
  5. 大档下写第 11 条「已定」→ 成功(中档会被删到 10 条);切回中档再写 → 只留 10 条。
  6. 小档下写 15 条「坑」→ 只剩 10 条(最旧的 5 条被删)。
  7. 面板「看项目」区不再有「审查 / 真实值」按钮,也不再有「真实值(声明 → 实测)」区块;
    「让 AI 动手」区的「审查(交给 AI)」仍在。
  8. 老项目(没写过 规模:)打开面板 → 显示「中」,条目行为与 v0.22.0 完全一致。

装法

见 README 的「下载与安装」。
附件 dsh-puzzle-mode-0.23.0.tgz 可直接用插件管理器的「本地压缩包」入口装。

v0.22.0

Choose a tag to compare

@liancha22 liancha22 released this 03 Oct 06:25

v0.22.0 · 与「无限五代」同装时按契约共存

本版把跨插件共存从复刻仓并回主线:与 dsh-infinite-gen-5(无限五代)同装时,
两边不再各说一套,而是段序可协商 + 六条分工条款 + 双向核对。


问题:两个插件都自认「最后一段」

两个插件都会往系统提示里插一段常驻规则,而它们对同一批动作给过相反的要求:

冲突点 本插件原先说 无限五代说
提问额度 一轮最多 10 问、每题 3–6 个岔路 同一轮最多一问、2–5 个互斥选项
整批题在场 照常采访、提问 批量合同优先:不采访、不回问、不中停
段序 写死 order = 10500(自认最后一段) 真末位锚点 order = 10150(自认最后一段)

两边都「自认最后一段」——同装时那句话必然有一边是假的。规则打架时模型只能猜。

改法一:段序从写死改为可协商

const DEFAULT_SECTION_ORDER = 10120
const ORDER = Number.parseInt(process.env.PUZZLE_SECTION_ORDER ?? '', 10) || DEFAULT_SECTION_ORDER
export const SECTION_ORDER_VALUE = ORDER
  • 默认 10120:小于无限五代末位锚点 10150,于是它的「本载荷末位」成立;
  • 刻意避开 DSH 内置段序表的全部取值——内置表里有 WEB_SURFACE: 10100,
    同号时段序相同时按段名比较,位置就不再由协商决定;
  • 需要恢复旧行为:PUZZLE_SECTION_ORDER=10500 dsh ...。

复刻仓原先默认 10100,正好撞上内置的 WEB_SURFACE: 10100。并入时改成 10120。

改法二:六条分工条款写进提示段

两份载荷的规则落在同一份提示里,不靠外部约定:

规则 谁说了算
域划分 交付物内容与形态 → 无限五代;拼图文档与采访节奏 → 本插件(不重叠,第一顺位)
提问额度 采访轮 / 审查确认轮 → 本插件额度;其余场景 → 无限五代口径
批量优先 整批题在场 → 无限五代批量合同优先;拼图文档只在该轮结束后幂等回写
工具形态 拼图文档只走 puzzle_mode;其它工具的 schema 不受「参数扁平」约束
停下语义 只停动作:本轮不写,但已交付的产物 / 结论不回退
末位让位 无限五代的末位锚点只声明「本载荷的末位」,不声明整份提示的最后一段

改法三:把「会不会漂」变成能自动发现

  • compat.json:双方同一组数字的契约(ig5-puzzle-coexist/1);
  • tools/verify-cross-plugin.mjs(npm run verify:cross,已挂在 npm test 链尾):
    自证 + 装了对方时双向互校——对方记的拼图段序 == 本仓默认、对方记的末位锚点 == 本仓契约、关系成立;
  • .github/workflows/verify.yml:CI 跑全链 + 契约可解析 + 包内容门禁;
  • test/50-contract.test.mjs 新增 2 条钉字面量断言(共存数字改了必须红)。

对方不在场时走「互校跳过」分支并标注,不判失败——CI 上没有那个仓库,
这一分支必须能跑通,否则 CI 会红在本机永远走不到的那条路上。

验证

项 结果
npm test(10/20/30/40/50 + 握手) 58 + 37 + 7 + 45 全绿,握手 18 通过 / 0 失败
新增契约断言 2 条,逐条验过「改回去就红」
段序环境覆盖 实测 10120(默认)/ 10500 / 10000 / 非法值回退默认
包内容 npm pack --dry-run 含 compat.json、COMPAT.md、tools/verify-cross-plugin.mjs

两条断言的验红方式(都是真删真改,不是看着对):

  • 把 DEFAULT_SECTION_ORDER 改回 10100 → 红(同时撞内置段序、与 compat.json 不符);
  • 真删「批量优先」那条整行 → 红(政策文本缺条款:batch-first)。

验收判据

  1. npm test → 退出码 0,末尾出现
    跨插件握手校验: 18 通过 / 0 失败;无限五代不在场时带「互校跳过」标注。
  2. PUZZLE_SECTION_ORDER=10500 node -e "import('./lib/index.js').then(m=>console.log(m.SECTION_ORDER_VALUE))"
    → 打印 10500;不带环境变量 → 10120。
  3. 装了两边插件时 npm run verify:cross → 六条文本条款与两个段序数字双向核对通过;
    任一侧改数字 → 两边都红并指出改哪一处。
  4. npm pack --dry-run → 输出里有 compat.json、COMPAT.md、tools/verify-cross-plugin.mjs。
  5. 真机:同装无限五代时,拼图提示段排在它的末位锚点之前(10120 < 10150),
    两边不再抢「最后一段」。

来源

这套兼容层最初由 @SunsetRNE 在复刻仓
SunsetRNE/dsh-puzzle-mode-2 做出来(「上游管功能,本仓管兼容」),
与无限五代 SunsetRNE/dsh-infinite-gen-5 的 data/arbitration.mjs 逐次互校。
本版并入主线,此后上游每次发版自带这层,不再依赖复刻仓逐版追赶。

装法

见 README 的「下载与安装」。
附件 dsh-puzzle-mode-0.22.0.tgz 可直接用插件管理器的「本地压缩包」入口装。