dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Infix rerun for DeepSeek Harness: re-run any earlier turn in place - the reply is regenerated from the same prompt while every later turn survives, so subsequent calls read A B C1 D E F G. Official seams only; the append-only session log is never rewritten.
DeepSeek Harness 的中缀重跑插件 —— 点某条模型回答旁的「重跑这一轮」,这一轮会用同一句提示词重新生成,而它之后的每一轮原样留在模型上下文里:后续调用读到的历史是 A B C1 D E F G。 重跑由官方 surface-replace 契约 + 一次逐事件重放组成(把被遮蔽的后续轮次以官方事件追加回来),append-only 会话日志一个字节都不改写。
中文 · English
DSH 的模型上下文是每次调用从会话 surface 重新推导的。官方只有「回退到末尾」式的编辑(dsh-edit-turn 的默认行为是改一条消息、丢掉它之后的一切),没有「重跑中间那一轮、后面照旧」这条路——因为在 surface-replace 契约里,模型回答永远不能充当替换载体(assistant/message 被格式明令禁止携带 sourceEventSeqs),所以一条回答无法原地换掉。
这个插件把它做出来:
surfaceOp: { op: 'replace', startSeq, endSeq } 替换事件(与 /compact 同一契约);重放是普通的 append 事件。本插件仍然不写自定义事件类型。宿主 0.2.1-rc.2 已有官方的 appendPluginRecord + registerMessageProjection(dsh-edit-turn 0.3.0 用它做回复块的就地补丁);重跑没有采用,原因见下面的「为什么重跑仍然必须重放」。source.kind = plugin:dsh-rerun-turn),它不开轮次、不开步骤:格式里只有 user/message 不需要打开中的 turn/step。它的内容形状由装载时对已安装适配器的一次探测决定(0.1.27):只有证明「当前适配器会丢掉空 user 消息」时才写空数组(deepseek 适配器天然如此;pi-ai 需要本地手补丁 dsh-delete-turn:skip-empty-user),否则写一个零宽空格——未打补丁的 pi-ai 会把 content: [] 变成 { role: 'user', content: '' },provider 直接 HTTP 400 user message must have content 并中断整轮(2026-10-07 桌面版真机事故)。0.1.14-0.1.25 用的是"空 system/message + 合成簿记轮",那一版每重跑一次就花掉一个轮号:对话视图里留下一条空的「已完成」条,轨迹视图轮号出现空洞(0.1.26 修)。tool/call、tool/result(sourceEventSeqs 重映射到副本)、TOOL_NOT_STARTED 修复结果(保留其"无 sourceEventSeqs"的精确形状);丢弃 usage(防统计翻倍)、内嵌 stream、系统消息(系统提示词由循环自己调和)与全部纯记账事件。rerunId,fold 记住它遮蔽了哪些节点,每个副本带 originalSeq 标记;"哪些副本还没写"永远可以从日志单独算出来。/state 默认自动续传(会话活着、空闲、收件箱为空时),半写的括号孤儿会被识别并续写而不是误判为忙。body[data-ds-dark-theme])。Host 与 Origin。dsh plugin --profile web add dsh-rerun-turn从本地目录安装(开发用):
dsh plugin --profile web add /path/to/dsh-rerun-turnpatchReload: live 的 profile 会热加载;否则重启该 profile 对应的 DSH 进程。
默认一次点击即执行;设置页勾上「重跑时询问」(或配置 confirm: true)后先弹确认框。
- id: dsh-rerun-turn
config:
confirm: false # 默认 false:一次点击即执行
autoResume: true # 默认 true:中断的重放在会话空闲时自动续传
idleTimeoutMs: 600000 # 等待新回答落地的最长时间
devTools: false # 默认 false:端到端验证用的 /dev/* 回环路由字段 · 默认 · 含义 confirm · false · 客户端是否先弹一次确认框。设置页的「重跑时询问」勾选会覆盖它(存在浏览器本地,未勾选过时跟随这里的值)。 autoResume · true · /state 是否在会话活着、空闲、收件箱为空时自动完成被中断的重放(不花模型调用)。 idleTimeoutMs · 600000 · 等待新回答闭合的最长时间;超时后重放留给续传。 devTools · false · 是否挂载验证用回环路由 /dev/scratch、/dev/derived(会发真实模型调用)。
插件管理页里 dsh-rerun-turn 那一行有配置入口,里面一个勾选:
dsh-delete-turn 的删除确认同一个位置/尺寸/样式),说明这一轮会怎么重放;取消勾选则点一下直接重跑。选择存在浏览器本地(dsh-rerun-turn:prefs),没有存过时跟随 profile 里的 confirm。一次重跑是三组官方操作,按序落地:
① 遮蔽(SHADOW)。 从目标轮次的提示词到 surface 末尾,是一条连续的窗口;追加一条带 surfaceOp: { op: 'replace', startSeq, endSeq } 的不占轮次的 user/message 载体,sourceEventSeqs 完整列出被遮蔽的每一个节点。载体没有任何可读文本、source.kind = plugin:dsh-rerun-turn:plugin: 类型让平台不会把它当人类提问(dsh-session-turn-outline 只认 source.kind === 'user' 的人类提问),内容形状按装载时的适配器探测二选一(见下面 0.1.27 的说明)。
载体内容为什么是两态(0.1.27):0.1.26 断言「两个官方适配器都会丢掉空 user 消息」,因此载体写死content: []。这条断言只在宿主装载的 pi-ai 带本地手补丁 `dsh-delete-turn:skip-empty-user` 时成立——补丁只在 npx 缓存副本里,DSH 桌面版(app.asar内置运行时)没有。真机session-5c3c4c12(2026-10-07 16:27:33)就是这样断的:载体content: []→ 未打补丁的 pi-ai 转成{ role: 'user', content: '' }→ provider 返回400 {"message":"user message must have content","param":"messages.93.content"},整轮turn/end → reason.error中止。同一天对真实 provider(https://api.cline.bot/api/v1,cline-pass/deepseek-v4.1-flash)实测:content: ""→ 失败;content: []→ 失败;content: "\u200b"→ 通过;content: "ok"→ 通过。所以 0.1.27 把形状改成能力相关:装载时从宿主入口解析@deepseek-ai/dsh-llm-pi-ai并读源码,只有读到补丁标记才写空数组,否则写一个零宽空格(代价是几个启发式 token,用户不可见,provider 一定接受)。失败的默认方向是安全的那一侧:解析不到、读不到、异常 → 都当"没有证明" → 零宽空格。如果你要把载体改回空的,必须同时证明当前安装的适配器会丢掉它——否则就是上面那次 400。
为什么载体不能开轮次(0.1.1 的事故、0.1.14 的倒退、0.1.26 的定案):0.1.0 用"空 developer/message + 自开轮次"当载体。它通过了追加校验,但代理循环的轮号计数器是进程内局部的——只统计循环自己开的轮,永不重读日志里的最大轮号。于是循环为再生成开轮时复用了载体占过的号,日志从此冷读失败(turn/start does not open the expected turn)。同理,重放写下的轮次也会被循环的下一次开轮撞上。0.1.1 的修复有两件:载体彻底不占轮次;重放完成后把循环的空闲计数器同步到日志真实最大轮号(syncLoopTurn,带形状守卫、失败降级并写诊断——DSH 没有官方重同步 API,这是本插件唯一一处触及循环状态的地方)。
0.1.14 为了"载体完全不进模型输入"改回开轮次的形状(空system/message+ 合成簿记轮):模型确实看不到了,但每重跑一次就多一个没有内容的轮次——对话视图里是一条空的「已完成,用时 N 秒」,轨迹视图里轮号出现空洞(真实会话session-5ce30467的 turn 4/6/8/10/12 就是这样)。0.1.26 回到不占轮次的载体(0.1.27 起内容形状随宿主适配器能力二选一)。格式义务仍由同一处满足——只有user/message能在没有打开中的 turn/step 时被读取(system/message/developer/message是 step 事件、assistant/message走同一检查、替换型tool/result需要打开中的轮次),所以不占轮次的载体只可能是 user 消息这一种形状。
② 再生成(REGENERATE)。 走官方准入路径 ctx.sessionController.prompt({ mode: 'queue', content: 原提示词 })。模型看到的正是 A B + 提示词,用会话自己的模型与工具重新回答。后台任务通过 agent.whenIdle()(失败时轮询日志的 turn/end)等它闭合。
③ 重放(REPLAY)。 从目标轮之后的下一轮开始(目标轮自己的旧回答是被替换掉的,不重放),逐事件复制到日志尾部:
turn/start / step/start / step/end / turn/end —— 轮号重排为紧接生成轮之后的连续编号(格式的 Relationships 要求 turn/start 恰好打开 nextTurn);user/message / assistant/message / tool/result —— 新 id、新轮号/步骤、原文内容、sourceEventSeqs 重映射;消息 source 加 { rerunBy, rerunId, originalSeq } 标记;tool/call —— 与结果成对才复制;悬空调用连同助手内容里的工具调用块一起剥掉(格式拒绝带着未决调用的轮次收尾);TOOL_NOT_STARTED 修复结果保留"无 sourceEventSeqs"的形状(校验器只接受这种修复形状);为什么重跑仍然必须重放? 先区分两种操作:
dsh-edit-turn 的回复编辑不调用模型,只是把已有回答的某个块改成用户写好的文字。宿主 0.2.1-rc.2 的 appendPluginRecord + registerMessageProjection 正好能做这件事:surface 不动,后面的轮次一个不用重放。有没有办法绕过? 有三种,都有代价,本插件不做:
因此重跑保留"遮蔽 → 官方 prompt 再生成 → 逐事件重放"。重放本身已经做过压缩:内嵌 stream、usage、系统消息和纯记账事件都不复制;只复制仍在 surface 上的后续节点及其 tool 配对,所以日志增长是尾部内容的线性副本,不是整份会话或前缀的重复。
崩溃恢复。 承载事件源里的 rerunId/promptRequestId/replayFrom/rerunTurnEnd + fold 的 shadowed 列表 + 每个副本的 originalSeq 标记,让"哪些副本缺失"完全可从日志推导。/state 报出未完成的重跑;续传只写缺失的后缀,并用"尾部前缀匹配"跳过崩溃前已经落地的括号写(避免重复 turn/start 让日志读不出来)。
插件接口
类型 · 名称 · 说明 路由 · GET /dsh-rerun-turn/state?sessionId= · 可重跑的作答、hidden 账本、重跑记录(含 complete/missing)、忙碌/进度 路由 · POST /dsh-rerun-turn/apply · { sessionId, seq \ · messageId } → 遮蔽 + 后台再生成/重放;响应含 started/rerunId/shadowed 路由 · POST /dsh-rerun-turn/replay · { sessionId, rerunId? } → 为中断的重跑补写重放 路由 · GET /dsh-rerun-turn/debug · 只读请求记录(诊断"点了没反应") 路由 · GET/POST /dev/scratch、GET /dev/derived · devTools: true 才挂载;端到端验证用 前端 · conversation.chat.assistant-actions · 回答动作条里的 ↻ 入口(order 6,编辑铅笔之后) 前端 · conversation.input.overlay · 隐藏被退役的行、跟随后台重跑、显示确认框(官方 Modal)/错误
重放会带上 dsh-edit-turn 的回复补丁(0.1.28):edit-turn 0.3.0 的回复编辑是 plugin:dsh-edit-turn/patch 投影,不改写原始回答事件。如果先 edit 了后面某条回答、再 rerun 前面一轮,本插件在复制那条被保留下来的回答时,会把该 seq 上最新的 text / reasoning patch 应用到副本内容里(reasoning 会去掉旧签名);因此被重跑保留的后续轮上的编辑不会因为一次更早的重跑而消失。同一轮自身的重跑仍然以新生成的结果为准——重跑的目标回答不会被复制,编辑被覆盖是重跑的定义。记录是只读的跨插件契约:版本不是 1、字段畸形或目标块类型不符的 patch 一律跳过;即使 edit-turn 暂时不在,记录也会被本插件读取并带到副本里。
给兄弟插件的契约:每次重跑落一条没有可读文本的 `user/message` 替换载体(source.kind === 'plugin:dsh-rerun-turn'、rerunBy、rerunId,不占轮次);其 content 在 0.1.27 起为「空数组(证明适配器会丢)或一个零宽空格」,不要按内容判形状。每个重放副本的 message.source 带 { rerunBy, rerunId, originalSeq }。按 surface 校验的入口可以沿 /state 的 reruns[].shadowed 或副本的 originalSeq 找到活节点。识别约定:source.kind 以 plugin: 开头、且内容里没有可读文本(空数组,或只有零宽/空白字符的文本块)的 user 消息是静默载体,不是提示词——本插件的规划器(resolveRerunPrompt)据此拒绝"重发一条已被删除/已被替换掉的提示词";dsh-edit-turn 的就地改写(kind: 'user' + editedBy,且带新文本)不受影响,仍然可以重跑。历史形状(0.1.0-0.1.13 的 developer/user 载体、0.1.14-0.1.25 的 system/message 载体 + 簿记轮)继续被识别,旧日志的账本、隐藏集与 markerTurns 都照旧正确。
本插件在 DSH 0.1.7-rc.2(npx 实例)上完成以下验证:
验证 · 命令 · 结果 单测(窗口规划、重放改写、标记、恢复匹配、计数器同步、载体不占轮次、载体两种内容形状,+ 客户端 DOM stub 的注入/隐藏/重装用例) · npm test · 60 项通过 官方校验器契约(真实 Session + sessionFormatCatalog strict 冷读往返) · npm run verify:contract · 29 项通过:完整重跑后的日志(含重跑后新增的普通轮)通过 v4 词汇表/关系/生命周期 + Session.fromRestore;派生上下文恰好 A B C1' D' E'(线上视图去掉静默载体);遮蔽不买轮号、全日志没有任何"只装载体"的轮次;载体形状与本机适配器探测一致(无证明时不得为空);"同一轮连点三次 ↻"的真机形状;链式重跑;崩溃半写后续传;准入失败回退;TOOL_NOT_STARTED 修复保真 客户端静态检查 + 运行中实例下发字节 · npm run verify:client / npm run verify:live -- · 7 项通过:模块可加载、槽位/字典/版本/错误码一致;3080 实例下发的模块组里就是本插件的当前字节 真实沙箱端到端(独立 DSH_HOME + 独立端口,真实模型调用) · npm run verify:e2e · 通过:3 轮 scratch 会话 → /apply 重跑中间轮 → 后台生成+重放完成 → 追加第 4 轮提示 → 读实时派生上下文:提示词保序、中间回答是新生成的、后一轮是带 originalSeq 标记的重放副本;随后用 tools/repair-session.mjs 对沙箱写出的日志做严格冷读校验:0 broken 运行中实例挂载探针 · npm run probe:loaded [端口] · 通过:/state 返 400、/apply 返 405
尚未验证的一环(诚实说明):浏览器里的交互渲染(按钮落位、点击、隐藏行)还没有自动化冒烟——本机没有可用的浏览器驱动接线;客户端半部的加载、槽位注册与真实下发已由 verify:client/verify:live 覆盖,宿主行为由端到端覆盖。首次使用请在浏览器里刷新一次 DSH 页面确认按钮出现。
user/message 提示词(人类提问或注入的用户消息都算);系统提示词头不可重跑。attachments-unsupported):prompt 准入要求文件走上传收据,durable 引用无法直接重送;图片可以。usage、内嵌 stream 被丢弃(防 token 统计翻倍);系统消息与 deliverable/workspace/todo 等纯记账事件不复制;被重放轮次里的工具不会重新执行(结果照抄)。dsh-delete-turn 0.1.x 删除模型回答时会落一条 system/message 替换载体(source 为 plugin:dsh-delete-turn),而格式只允许 system-prompt 来源的 system 消息——含这种事件的会话在冷读时报 seed system/message ... must have system-prompt source、历史打不开。修复方向与 dsh-edit-turn 0.2.4 相同(载体改为不占轮次的 user/message,或空 developer/message + 合成轮)。本仓库的修复工具能用 --force 截断这类坏日志。npm run probe:loaded [端口]——404 说明插件没挂载;400/405 说明挂载正常,问题在浏览器(刷新页面、看控制台)。GET /dsh-rerun-turn/debug——没有 apply 记录 = 点击丢在浏览器半边;有 apply 且 ok:false = 宿主拒绝,code 是原因。GET /state 看 reruns[].complete/missing 与 progress;interrupted 时默认会自动续传。node tools/repair-session.mjs ~/.dsh/sessions --dry-run # 先看
node tools/repair-session.mjs ~/.dsh/sessions # 修复(先备份)工具只自动修复带 plugin:dsh-rerun-turn 标记的坏日志;其他坏日志只报告不动。修复后重开会话,待处理的提示词会被循环自动补答。
0.2.5 —— 设置卡导出给组合包;两个补法只留两个字,说明收成一句,下拉不再撑满一行。
exports.ConfigCard 对外导出,聚合包 dsh-as-aistudio 在自己页面上渲染的是这一张卡,不是另画一份;单独装本插件时那张卡仍在本插件自己的行上。0.2.4 —— 缺失 O 的可见轮也能点重跑:顺补 / 自补两种补法可选。
Q O,所以 QOQOQO 是三轮各有一问一答。注意 `tool/result`(工具的结果)不是模型输出,不算 O —— 它是工具/宿主产生的,只是恰好和 T 挨着。实测一份两万条的日志:assistant 消息 2931 条、内容块只有 tool-call ×2855 / reasoning ×2205 / text ×195,没有第四类。buildReplayWrites 逐条 appendWrite,全程没有模型调用;只有思维链的签名会去掉,因为签名签的是旧文本)。副作用是日志追加一份尾巴、被抄的轮次拿到新 seq(界面上看起来一样)。只有被作用的那一轮本来就是最末一轮时,才是「一次请求、后面一概不动」。planRerun 本来就接受「提示词目标」,只对「自己没有任何可重跑回复的那一轮」放行(停止 / 失败 / 刚发出还没回答都算),并拒绝把「有 A 的轮次的 Q」当目标 —— 所以「点这个 Q 去重跑下面那一轮」这种映射由客户端决定发哪个 seq,宿主不接受这种地址。被隐藏的 A 不算可见:默认模式会跳过它继续往下找,找不到就退回重发最新那条没回答的提示词。npm test 86/86(宿主侧提示词目标只 shadow 提示词自己、回放窗口为空,且只广播「带 O」的回答 —— 空内容 / 零宽文本不算 O,纯工具调用算 O;客户端侧两种模式各自的作用对象与按钮文案、隐藏的 A 被跳过、回答被删的轮次仍带按钮且不落到别人的 A 上、卡片里能切模式并落盘)。0.2.3 —— 修掉「重跑已中断」提示关不掉;命名会话不再被拒。
setProgress() 把每次阶段更新合并进上一条记录,从不清理旧字段。一次 interrupted 写下的 error 会跟着 replaying、done 一路活下来,/state 于是一直报告"已中断",前端也就一直画那条提示。progressAfter(current, patch);阶段变化 = 新记录,不再继承上一阶段的 error / promptError;同一阶段内的增量 patch 仍然合并。setProgress() 改走它。phase === 'interrupted' | 'failed' 的 error(旧宿主合并出来的 done + error 不再显示);提示条右侧加关闭按钮,点整条也能关;关闭状态按「阶段 + rerunId + 文案」记忆,新错误仍会重新出现。bili-whale 这类命名会话(~/.dsh/storages//sessions/bili-whale.json)是完全合法的会话 —— 于是对它的每个请求都被 400 拒掉,面板上就是「状态加载失败,稍后重试。」。现在按 DSH 自己的语法(宿主侧 schema 就是 z.string().min(1))放宽为 /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,仍然挡住 /、`、..`、空白与控制字符。npm test 81/81(新增 3 条 progressAfter 用例、5 条 DOM 用例:stale error 不显示、关闭按钮、点整条关闭、dismissed key、promptError 仍显示;以及命名会话 / 空状态的契约用例)。0.2.2 —— 设置页多一个「重跑时询问」,确认框换成官方 Modal。
dsh-rerun-turn 这一行的配置入口新增「重跑时询问」(默认关)。勾上后每次点重跑先弹确认框,说明这一轮会怎么重放;取消勾选则点一下直接重跑。选择存在浏览器本地(dsh-rerun-turn:prefs),没有存过时跟随 profile 里的 confirm。Modal + Button,位置/宽度(380)/圆角/阴影/按钮行与 dsh-delete-turn 的删除确认完全一致;正文 14/22,第二段说明用三级文字 13/20。0.2.1 —— 重跑一开始就退掉旧回答;轮次导航不再留空档。
order:2 留在下面。代价是最多一帧的空位(宿主挂上 process 行即填满)。0.2.0 —— 与 dsh-edit-turn 0.5 的整轮保存对齐,并把重跑按钮的尺寸修准。
plugin:dsh-edit-turn/patch 记录,targets 可能跨多条消息。重放现在有测试覆盖这条形状:被保留的后续轮里,每条消息都按自己的 target 取到新文本,原记录不被改写。.dsrr-action 补上 box-sizing:border-box(此前 width/height:28px 叠加 padding:6px 实际渲染成 40×40),并去掉 hover 的 inset 描边,只保留浅色圆底;现在与官方 action 图标一致:28×28、15px SVG、stroke-width:1.2、颜色 #81858c。0.1.28 —— 重放带上 dsh-edit-turn 的回复补丁:先编辑后面某条回答、再重跑前面一轮时,编辑不再消失。
editPatchesOf(events) 读取 plugin:dsh-edit-turn/patch 记录,按 seq + blockIndex 取每个块最新的 text / reasoning 补丁;记录版本不是 1、字段畸形、目标块类型不符的一律跳过。buildReplayWrites 在复制被保留下来的后续回答时,用 applyEditPatches 把补丁应用到副本内容;思维链补丁同时去掉旧 signature / thinkingSignature。工具调用、其他块、轮次结构不动;原始事件不改。npm test 63/63、npm run verify:contract 29 项、npm run verify:client 7 项全绿。0.1.27 —— 修复真机中断事故:载体写死空数组,在没有手补丁的宿主上让整个 provider 请求 400,整轮中止。
session-5c3c4c12-e803-492a-b614-9a0f4c5ea6a3,2026-10-07 16:27:33):turn 187 中止,turn/end → reason.error: Failed to create stream ... 400 {"message":"user message must have content","param":"messages.93.content"};派生消息下标 93 正是本插件的空载体(seq 10968,content: [],source.kind = plugin:dsh-rerun-turn)。/Applications/DeepSeek Harness.app,运行时打在 app.asar 里,同一包内没有这一行)会把 content: [] 转成 { role: 'user', content: '' },provider 拒绝整个请求。https://api.cline.bot/api/v1,cline-pass/deepseek-v4.1-flash):content: "" → stream_initialization_failed;content: [] → 同样失败;content: "\u200b" → 通过;content: "ok" → 通过。process.argv[1] 所在目录)解析 @deepseek-ai/dsh-llm-pi-ai 并读源码,读到补丁标记才写空数组(保住 0.1.26 的零 token 行为),否则写一个零宽空格;解析失败/读失败/异常一律按"没有证明"处理,即零宽空格。判定是纯函数 + 装载时算一次(adapterSourceDropsEmptyUserContent / silentCarrierContent / ADAPTER_DROPS_EMPTY_USER_CONTENT),形状经 buildShadowWrites(plan, rerunId, promptRequestId, dropsEmptyUserContent?) 注入,测试不依赖本机 npx 缓存与 process.argv。与兄弟仓库 dsh-delete-turn 的同名设计一致(同一补丁标记、同一保守默认)。isSilentPluginCarrier 由「内容为空」放宽为「没有可读文本」(空数组,或只有零宽/空白字符的文本块)——否则零宽载体在链式重跑里会被当成人类提示词重发;/dev/derived 只在有证明时才把空 user 消息从报告里去掉(未证明时那条零宽消息就是适配器真正发出去的东西),并在响应里多一个 carrier: { shape, adapterDropsEmptyUserContent } 让现场一眼看出走的是哪一支。syncLoopTurn、resolveRerunPrompt 的 'carrier' 拒绝、isRerunCarrier 的账本跳过照旧(形状判定只看 source 标记,不看内容)。/tmp 副本把探测强制为"无证明"后同样全绿(证明两支都受支持);/tmp 副本只还原载体行为(保留新用例)→ 恰好 2 条新用例转红(零宽回退、零宽载体不是可重发提示词),再把探测强制为"无证明"→ 契约校验器在「无证明时不得写空载体」这条断言上转红。0.1.26 —— 修复真机可见缺陷:载体占了独立的轮次,屏幕上留下空的「已完成」空壳(对话视图)+ 轮号空洞(轨迹视图),两者同一个根因。
session-5ce30467):用户对同一条提问反复点 ↻,日志里出现 turn 4/6/8/10/12——每个只有一条空 system/message 载体、turn/start → step/start → 载体 → step/end → turn/end,屏幕上就是「已完成,用时 1 秒」下面什么都没有,轨迹视图轮号 2,3,5,7,9,11,13 跳号。system/message/developer/message 只能在打开中的 turn+step 里被读(requireStep)、assistant/message 走同一检查、替换型 tool/result 需要打开中的轮次、turn/start 又必须等于 nextTurn(已关闭的轮次回不去)——所以只要载体是这几种类型之一,就必须开一个轮次。user/message(source.kind = plugin:dsh-rerun-turn,content: [])。格式只对 user/message 不要求打开中的 turn/step;"模型看不到"当时按"两个官方适配器都在发请求前丢掉空 user 消息"论证(dsh-llm-deepseek:role === "user" && content.length === 0 → continue;pi-ai:同款跳过,注释里叫 dsh-delete-turn:skip-empty-user)。同一个形状也是 dsh-delete-turn 一直在用的静默载体,不是新发明的写法。0.1.27 更正:这条论证对 pi-ai 只成立于带本地手补丁的构建——桌面版内置运行时没有这一行,空数组会被转成 content: '' 并被 provider 以 400 拒绝(session-5c3c4c12 真机事故),所以 0.1.27 起内容形状改为按适配器探测二选一。resolveRerunPrompt 不再把"站在已退役提示词位置上的静默载体"当作可重发的提示词(空内容也过不了 prompt 准入);重放不再复制本插件自己的载体(记账不是对话,副本只会变成空节点);/dev/derived 报的是适配器真正发出的请求(去掉空 user 消息),不再把派生列表直接当成模型输入。宿主不再需要 syncLoopTurn 去迁就载体轮——载体的那次调用整个删掉了(重放之后的那次保留)。markerTurns/retiredTurns 的轮号判定除了 key 之外也读平台自己的 data-chat-turn——DSH 现在把过程/尾巴行按节点 kind 作 key(turn-tail、["turn-tail","response"]),只从 key 里抠轮号会让旧日志(以及被退役的轮)的空条留在屏幕上。/tmp 副本还原改动 → 恰好 4 条新增用例转红(2 条载体形状 + 2 条客户端隐藏),契约校验器在第一条新断言就红;真实会话 `session-5ce30467` 全程用真机校验器(0.2.0-rc.2)验证:原日志照旧严格冷读通过、账本输出与 0.1.25 逐字节一致;在该日志上跑一次新形状重跑(真 Session 追加 + 重新冷读)通过,遮蔽买的轮号数为 0(只有再生成轮 +1)。0.1.25 —— 没有定稿作答的轮次也能重跑(点它的提示词行):rerunnableReplies 末尾为"有活提示词、但没有可重跑回答"的轮次补一条 { seq: 提示词 seq, messageId: null },规划器只在这类轮次接受提示词寻址。
0.1.24 —— 补齐 0.1.23 的一个漏网形状:提示词被兄弟插件替换成非用户消息时,界面仍会给出 ↻。
session-e81f9424,turn 31):dsh-delete-turn 先把提示词「机组」替换成自己的用户载体,又把它替换成自己的 `system/message` 载体——最终活节点仍是 system 消息。作答还在界面上,但已经没有可重发的提示词。user/message——两侧判据仍有一处没有共享。现在两侧共同调用同一个 resolveRerunPrompt(bracket 归属 + 链解析 + 类型检查 + 头部检查全在里面),广告与承诺不可能再分叉。check-rerun-plans.mjs 对 8 个真实会话(含 10953 事件的大会话)全部 M=0 / 广告全部可规划;新增回归测试「提示词 → 兄弟用户载体 → system 载体」钉死该形状。0.1.23 —— 修复「这条回答不支持重跑」:作答所属的轮次可能被拆成多个 bracket,规划器原先取同号的第一个,于是一个界面上明明可见、且自己 bracket 里有提示词的作答被拒。
events[seq] 并不等于「seq 等于它的那条事件」。同一个会话里这个错位会把提示词读成一条 step/start,报「the turn prompt is not a user message」。所有「按 seq 取事件」的读取现在都走 seq 索引。/state 的 replies[] 与规划器共用同一个 bracket 判据 —— 没有提示词(或提示词已不在界面上)的应答不再出现在列表里,界面不会给出一个点了必然失败的按钮。0.1.22 —— 修复「重新 apply 时注入节点不清理 → 幽灵按钮堆积」:行内注入前先复用已有宿主(不再删旧的再造新的),fiber dispose 时按命名空间属性清扫本插件的注入节点。
[data-dsrr-action-host="1"] 查一次行内已有宿主,找到就复用同一个节点(WeakMap 重新指向它),绝不新建第二个;重复的旧副本(本插件自己的节点)才删。data-dsrr-* 命名空间 / 本插件 id 的 style),宿主与兄弟插件的节点一律不动。0.1.21 —— 互操作加固(AI Studio 组件契约):输入框浮层槽位号 8 → 10、隐藏归属守卫、注入节点带命名空间。