linanbuan/dsh-session-retry ↗★ 0
@local/dsh-session-retry
会话重试 · Session retry — 助手消息动作条上的「重新回答」按钮:先把这一轮的旧回答从模型上下文里移除,再把该轮的用户提问重新提交给同一个会话。 A DSH plugin that adds an "answer again" action to every assistant turn strip, dropping the previous answer from the model context first. 支持移除当前轮次旧回答并重新提交提问,适合需要无污染重试对话的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:linanbuan/dsh-session-retry重新回答 · dsh-session-retry
在助手消息的动作条上加一个 重新回答 按钮:把该轮回答对应的用户提问重新提交给 同一个会话,并先让这一轮的旧回答退出模型上下文 —— 重答时模型看不到自己上一次 在这一轮里答了什么(日志与界面上的历史消息不动)。
按钮落在复制与分支之间(conversation.chat.assistant-actions,order: 20),样子与原生动作条按钮一致:方形图标按钮、悬停出深色提示气泡、与那条动作条一起悬停/聚焦显隐(data-clock=end 那一轮尺寸跟着原生图标走)。图标与提示气泡都是插件自带的:插件实践要求第三方插件的浏览器半边不 require Harness 客户端包,所以这里只 require('react')。
- 行 id:
session-retry - 位置:助手消息动作条(
conversation.chat.assistant-actions) - 依赖:无(宿主半边注入
connection与agents,浏览器半边只依赖react)
它做什么
-
宿主半边(index.js)在共享的
/api通道上注册两条路由:POST /api/session-retry/context { sessionId } (只读) → { ok, sessionId, latestTurn, cached, anchors: [ { messageId, turn, text, promptCount, omittedParts, promptSeq, anchorSeq } ] } POST /api/session-retry/regenerate { sessionId, messageId } (登记改写) → { ok, turn, promptSeq, runs, shadowed, kept, eventSeqs, deferred: true }前者一遍扫过整条会话日志,把「动作条挂在哪条助手消息上」与「该轮的用户提问」配对,结果按会话缓存 1.5 秒。后者只认活会话:从
sessions.get(id)拿会话、用snapshotEvents()现场重算锚点(与/context同一套retryTargets规则),再把该轮的可见面节点替换成空节点 —— 但不在点按钮的当场写,而是登记一份影子计划,等下一次请求真的打开一个步骤时补写(原因见下一节)。 -
浏览器半边(client.js)按会话注册一个动作条条目:取一次上面的表,每条动作条拿自己的
messageId查表 —— 命中就可点,没命中就置灰并说明原因。点击后先 POST/regenerate,成功才用session.prompt([{ type: 'text', text }], 'queue')提交提问(与ui-conversation的conversation.send走同一条路径)—— 两步都成了才提示「已重新回答(旧回答已退出上下文…)」;改写没成就只转述原因、不提交,否则等于在旧回答还在上下文时把提问问第二遍。
旧回答是怎么消失的
改写的是可见面(surface),不是日志:会话记录、界面上的历史消息、标题与统计都原样保留,只有送给模型的消息列表少掉这一段 —— 模型能看到什么由 dsh-session 的 surface 折叠与 deriveMessages() 决定。
用法是 surface 自己的替换通道:追加一条 system/message 事件,带 surfaceOp: { op: 'replace', startSeq, endSeq } 与 sourceEventSeqs(被遮蔽节点必须列全,应用会严格校验)。载体必须是空 system 节点:应用自己的规则是「空 system/developer 节点保留可见面位置但不产生线缆消息」(dsh-session 的 deriveEventMessage),而空 user/message 会作为一条空 user 消息真的发出去。
写这条事件的时机有硬约束:system/message 属于 V4 的「步骤事件」,校验要求它的 turn/step 必须等于当前打开的那一对(dsh-session-format-v3-to-v4 的 STEP_EVENT_TYPES + requireStep)。点按钮时会话是空闲的 —— 上一轮已经 turn/end、下一轮还没 turn/start,没有任何打开的步骤 —— 此刻带着上一轮的 turn/step 写进去,日志当场就坏:
SessionFormatError: system/message does not match an open turn and step
→ stored log is corrupt ...
所以 /regenerate 只登记影子计划(deferred: true,可见面当场不动),等客户端提交的提问真的打开一个步骤时,在 agent/request 钩子里用当时打开的 turn/step 补写。这个位置既合法(步骤确实开着),又足够早 —— dsh-agent-loop 的 step() 先 step/start,然后 prepareRequest→agent/request(hook),最后才 buildRequest→deriveMessages(),所以用户这一次请求就已经看不见旧回答了。会话在补写前被关掉也没事:计划带 TTL,过期即弃,日志里什么都没写过。
影子范围怎么算:每个可见面节点取一个「内容区间终点」—— 追加型节点是自己本身就是新内容,终点是它自己的 seq;替换型节点继承被替换段的位置(压缩摘要就是这样占住早期位置的),终点是被替换段的末端。终点早于本轮提问的节点属于「提问之前的记忆」(例如提问前产生的压缩摘要),保留;终点不早于提问的节点都是这一轮的产物,影掉。要影的节点在可见面里未必连续,所以按可见面下标切成连续段,一段一次 append(每段留一个空节点占位)。这段判定是纯函数 shadowPlan(),真实日志与合成事件都能直接喂进去测。
判定规则(与界面严格对齐)
动作条由 ui-chat 的轮次尾部渲染:每轮一条,挂在该轮最后一个产出文本的步骤里最后一条 assistant/message 上(closing = finalized.findLast(hasText))。本插件的锚点就是这条消息,所以:
- 该轮没有产出文本的步骤(例如中途被打断)→ 界面本来就不渲染动作条,自然也没有按钮;
- 该轮的提问 =
turn/start之后、锚点之前的人工消息(source.kind === 'user')。注入的上下文(time-context、skill-catalog、插件通知)与压缩检查点都不是提问;替换操作(surfaceOp为对象,压缩复写旧消息)也不是追加,不参与判定。
一轮里不止一句提问:用户在模型回答途中发话(steering / 排队)时,那句话会被接进同一个轮次。此时按顺序把该轮全部提问合并成一段重发 —— 只挑其中一句会丢掉另一句,合并则既不丢内容也只提交一次。提示语会如实写明「合并 N 条提问」。
刻意做出的取舍
- 影掉而不是删除:宿主没有截断历史或回滚轮次的公开接口,已经写进日志的事件也删不掉,所以这是「让模型看不见」。想要真正另起一段历史,用动作条上的分支按钮(另开会话)。
- 重答更早的轮次 = 回退到那一轮重来:影子范围从该轮提问起一直覆盖到可见面末尾,所以那一轮之后的轮次也会退出上下文(否则模型会带着「后面已经发生的事」去答前面那轮)。
- 只服务活会话:
/regenerate要拿到内存里的会话对象才能改写;会话不在应用里打开时返回NOT_LIVE。会话正在回答时返回RUNNING—— 边答边改可见面属于压缩那种受控场景,这里不做(提示语会直接说「先停止再重新回答」)。 - 非文字内容不重发:只重发文字块;原消息里的图片、附件只计数,并在提示语里说明「N 个非文字内容没有重发」。
- 一次请求答完整条会话:动作条是每轮一条,一场长会话有几十条同时挂在页面上;逐条询问就是几十趟日志回放。所以客户端按会话取一次表,同一提交里挂载的动作条共用一趟请求(后挂载的动作条会补一趟,保证看到的是它挂载之后的判定)。
工程约束
- 浏览器半边只依赖
react:dsh.client.inject只声明@deepseek-ai/dsh-client-ui-chat(插槽的属主),immediately: true让前端在启动时就预取这个包。图标(RefreshIcon,几何照抄原生刷新图标)与提示气泡(纯 CSS 显隐,不 portal、不写document.body)都在 client.js 里自带。 - 宿主半边注入
connection与agents:agents是agent/request钩子(补写那一步)必需的;sessionQuery与sessions都不注入,缺失时按不可用降级回答 —— 没挂sessionQuery的组合里/context答UNAVAILABLE,没挂sessions的组合里/regenerate也答UNAVAILABLE,都不会把插件拖垮。 - 空节点写在打开的步骤里:补写只发生在
agent/request(步骤已打开、消息还没装配),所以空节点的turn/step一定等于当时打开的那一对,日志不会被写坏;没有打开的步骤时(例如钩子被别的组合提前调用)就什么也不写,计划留到下一步。
安装
本插件是一个标准 DSH bundle:package.json 里声明了 dsh.bundle.patch 与 dsh.client,装进任何 profile 都走同一条路。先装好 DSH 桌面端(deepseek.com/download),然后下面任选一种。
方式一 · 在 DSH 桌面端里装(推荐)
- 侧栏点 插件 → 添加插件(Add plugin);
- 在输入框里填下面任意一种 spec:
- 本仓库地址:
https://github.com/linanbuan/dsh-session-retry - 本仓库的压缩包链接(GitHub 页面的
Source code (zip)) - 已经克隆到本机的目录绝对路径
- 包名(若将来发布到 npm)
- 本仓库地址:
- 点 安装:Harness 先读出这个 spec 指向什么(包名、简介、版本),确认后开始装;
- 装完点 立即启用。
整个安装由 Harness 自己的插件管理器完成:把这个包写进当前 profile 的 dsh.profile.bundles,并挂上宿主半边。桌面端装插件不需要命令行,也不需要手改 profile 文件。
方式二 · 让 Agent 自己装
把仓库地址或本机目录直接交给任意会话里的 Agent,它会调用 plugin_manager 工具(action: install_bundle)走同一条路。这是官方给第三方插件推荐的安装方式。
方式三 · 命令行
装了 dsh CLI 的机器上:
dsh plugin --profile
add
`` 与方式一相同(包名、Git 地址、压缩包链接或本地路径);参数以 dsh plugin --help 的实际输出为准。
方式四 · 手工装进某个 profile(排障用,不推荐)
profile 目录默认在 ~/.dsh/profiles/ 。把包放进去,并在 profile 自己的 package.json 里登记两处:
"dependencies": { "@local/dsh-session-retry": "link:" },
"dsh": { "profile": { "bundles": [ "…", "@local/dsh-session-retry" ] } }
手改 profile 文件要过权限审批,写坏会让 Harness 起不来,所以正常安装请走前面三种。
一个坑:在 WSL 里 ln -s 出来的链接是 Linux 符号链接,Windows 侧跑的 Electron 解析不了它,插件页会报 dsh: cannot resolve profile bundle "@local/dsh-session-retry"。要真链接就让 Windows 侧的 pnpm 去建(方式一/二),否则就用目录副本。
装上之后
激活与改完代码之后怎么让它生效是两件事:
- 侧栏 插件 → 找到
@local/dsh-session-retry; - 点那一行的开关 停用再启用(一次热挂载);
- 组件状态从「已关闭」变「运行中」=宿主半边挂在启动图里了;
- 浏览器半边会跟着
/plugins/events的 graph 帧重新拉取client.js,不用刷新页面。
但宿主半边改了代码必须重启应用。 cordis 的插件加载器用裸 import() 加载宿主模块,Node 的 ESM 缓存按文件 URL 命中,停用再启用只会重跑缓存里那份旧模块的 apply() —— 新注册的路由不会出现。表现是:按钮是新版本,点下去却提示「宿主那半还是旧版本(404),重启应用后再试」(客户端把这条 404 专门翻译成能照着做的一句话)。所以:
- 只改 client.js:热挂载即可,页面不用刷新;
- 改了 index.js 或 package.json:重启应用。
停用 / 卸载
- id: session-retry
disabled: true
或者直接点插件页那一行的开关;彻底移除用插件页 → 这个包 → 卸载。
注意事项
- 本插件不是官方插件:与 DeepSeek AI 无任何关联,不提供服务承诺。它依赖 DSH 内部的 surface 折叠与步骤事件契约,这些不是稳定 API:Harness 升级后按钮可能置灰或改写失败,此时插件只转述原因、不提交任何东西,不会破坏会话日志。
- 它会改写当前会话的可见面:这是插件的全部目的,但它确实往会话里追加了事件(
system/message+surfaceOp)。原始日志与界面历史不动,可随时用「分支」另开会话保留原样。请勿在需要严格留证的一次性会话里使用。 - 0.3.0 之前写过坏日志:早期版本在会话空闲时当场追写步骤事件,会被 V4 校验器拒绝(
system/message does not match an open turn and step),受影响的会话日志需要单独修复;1.0.0 起改为「登记影子计划 + 在打开的步骤里补写」,不再有这个问题。若你手上是旧版本,请升级。 - 运行中的会话不能重答:返回
RUNNING,提示语会说「先停止再重新回答」。 - 非文字内容不重发:图片与附件只计数、不重新提交,提示语会写明数量。
- 安装后不会自动更新:官方插件管理器目前不支持自动更新,升级=先卸载再装新版;替换已安装的包之后宿主半边要重启应用。
- 兼容性:在 DSH 0.1.7-rc.2(Windows 桌面端)上开发与验证。宿主半边注入
connection与agents(后者供agent/request钩子用),sessionQuery/sessions缺失时降级为UNAVAILABLE而不是崩溃。 - 隐私:插件不联网、无遥测。它在宿主侧读会话日志与内存中的会话对象,浏览器半边只拿到这一轮要用的锚点表(消息 id、轮次、提问文本)。
- 权限:插件自身不申请任何额外权限;安装时由 Harness 的插件管理器走它自己的审批流程。
- 本仓库的隐私:仓库内不含任何密钥、凭据、账号标识、会话日志或本机路径(文档里的路径一律是
~/.dsh/...形式或 ``)。
测试
node test/retry-targets.mjs # 判定与影子范围的单元测试(合成事件,零依赖)
node test/client-contract.mjs # 浏览器半边在替身环境里的契约测试(零依赖)
retry-targets 覆盖锚点判定与影子范围:一轮一会话、多轮取最大轮次、一轮多句提问合并计数、提问只有非文字内容时不给锚点、该轮没有产出文本的步骤时不给锚点、注入上下文与插件通知不算提问、锚点之后的提问不算本轮、空日志/畸形事件/缺 turn/end 安全降级、影子范围的边界与不连续切段。
client-contract 按宿主客户端的加载契约搭一个最小替身(window.__ModuleLoader__ + react + slots/locale/sessions 服务),驱动真模块走完「注册 → 挂载 → 取上下文 → 点击提交 → 提示语」的全过程。替身故意不提供任何 Harness 客户端包 —— 一旦 client.js 又去 require 一个,就在那里当场失败。它验证的是模块自己的逻辑与注册形状;「按钮在真实界面里的位置与样式」不在它能证明的范围内。
真机实证(用应用自己的 Session 实现与真实会话日志跑端到端,含「补写出来的日志能被 V4 校验器读回来」这条)不在本仓库内,属于本机验证材料。
结构
package.json bundle 清单(dsh.bundle.patch + dsh.client)
cordis.patch.yml 一行 insert:装载宿主半边
index.js 宿主半边:两条路由 + retryTargets/shadowPlan
client.js 浏览器半边:动作条按钮(手写 lazy CJS,只用 react)
test/retry-targets.mjs 判定与影子范围单元测试
test/client-contract.mjs 浏览器半边契约测试
icon.svg 插件图标
致谢与第三方许可
- 提示气泡与刷新图标照抄 DSH 官方
@deepseek-ai/dsh-client-ui-primitives的外观(13px 文字、深色底、圆角、下方 6px 的摆放,以及IconRefreshOutlineRegular的几何),只取外观与几何,不加载该包、不复制其代码:官方包的许可为 MIT,本条按 MIT 的要求标注来源。 - 与界面判定对齐的规则(动作条挂在「该轮最后一个产出文本步骤的最后一条助手消息」上)来自 DSH 官方
ui-chat的实现口径,属于接口兼容性的对齐,不含官方代码副本。 - surface 替换通道(
surfaceOp的replace语义、空 system 节点不产生线缆消息)是 DSH 官方dsh-session自带契约,本插件只按契约写入,不包含其代码。 - 本仓库没有引用或改编任何第三方插件的代码。
License
MIT © 2026 linanbuan