Repository navigation
Releases
dsh-gui-last-session v0.1.9 — 文档重构
dsh-gui-last-session v0.1.8 — 审计修复
审计修复版。详见 GUI 仓库同批次提交(含逐条证据)。
要点:Command Code 半边此前因未定义标识符整条死掉;V4.1 守卫读错层,会给从没配置过该路由的
用户写入 30 条模型列表;会话头补丁在认不出目标主机时失败开放;未设 \ 时把缓存写进
当前工作目录。
dsh-gui-last-session v0.1.7 — 恢复失败会说话;惰性解析 opener;目标迟到会重试
fix: 恢复会话的失败现在会说话(并把「目标还没进列表」改成会重试)0.1.7
现状:记录侧已经确认工作(指针跟着当前会话走),但启动后仍然落在新会话上。而恢复那条路
一行日志都没有 —— 「没指针」「目标一直没进列表」「这台引擎没有打开会话的 API」「打开后又被
引擎自己导航走了」这几种情况在日志里长得一模一样,所以只能靠猜。这一版先把这件事变成可诊断的,
同时修掉最可能的两个原因。
1. 诊断:宿主侧新增 /gui-last-session/report
浏览器半边每次尝试恢复都会发一行结论给宿主半边,宿主记进应用日志(引擎 stdout 会被外壳合并):
reopen: no usable pointerreopen: target <session-id>reopen: open() called for <session-id>reopen: FAILED — the session never appeared in the list (N attempts)reopen: retried successfully for <session-id>reopen: no way to open a session on this enginereopen: giving up (the target never became listable)
为什么不直接打浏览器日志:页面 console 只有在外壳开了 page-debug 标志时才会进应用日志 ——
靠它等于什么都没有。这条路由和已经工作的指针写入走同一套信任围栏。
2. 修:uiWorkspace 改成惰性解析
原来在 apply() 里查一次 ctx.get('uiWorkspace')。但 uiWorkspace 由另一个客户端插件提供,
可能在本插件 fiber 激活时还没挂上 —— 那样 opener 就是 null,插件会直接把自己关掉,而且一关
就是一整个会话。现在改成真正要打开时才解析(并额外兜底 ctx.uiWorkspace)。
3. 修:目标还没进列表时会重试
会话列表是走网络拿的、而且按工作区划分,所以第一次尝试时目标 id 常常根本不在列表里;引擎自己的
启动导航也可能发生在我们之后。现在失败的恢复会在兜底复读里重试(有 5 分钟上限,避免很久以后
把用户硬拽回去),一旦目标出现就打开并记一行 retried successfully。
测试
新增用例:目标不在列表里时不打开、且发出报告;目标随后出现时重试成功、并再次报告。
连同既有用例共 22 项通过。
怎么用它
重启 GUI(会装上 0.1.7)之后,应用日志里会出现 [gui-last-session] reopen: … 这一行 ——
它直接说明恢复卡在哪一步。若显示 open() called for <id> 却仍然是新会话,那就是打开之后被引擎
自己的启动导航盖掉了,下一步就从那里入手。
dsh-gui-last-session v0.1.6 — 用 uiWorkspace.openSession 打开会话(引擎已移除 sessions.open)
fix: 找回「打开上次会话」的现代 API —— 引擎 0.2.0 已移除 sessions.open()(0.1.6)
现象:装上插件、记录也正常了(指针已经跟着当前会话走),但启动 DSH 后还是打开一个新会话。
根因(在引擎源码里确认):引擎 0.2.0 把客户端会话服务上的 open(id) 删掉了 ——
该服务的公开方法只有 retain / using / create / fork / scope / refresh / …,全引擎搜索没有任何
地方定义 sessions.open。而「打开某个会话」在 0.2.0 里的入口是:
ctx.uiWorkspace.openSession(id) // 侧边栏/日程等 UI 都是这么切会话的
本插件的 record 修好之后,卡住的是 reopen 那一半:它调用的 sessions.open(id) 每次都抛
TypeError,被循环里的 try/catch 吞掉、傻等 30 秒(200 次 × 150ms)后放弃 —— 于是页面留在引
擎自己新建的会话上,表现就是「启动后还是新会话」。
顺带一个平台层发现:引擎自己 dsh-client-ui-conversation 里那段「记住上次会话并恢复」的
代码,读的是 sessions.list.getSnapshot().current(0.2.0 的列表状态里没有这个字段),
调用的也是 sessions.open(...)(已不存在)—— 所以引擎自带的这条恢复路径在 0.2.0 上同样
失效。这一点我改不了,只能修我们的插件,但值得反馈给引擎。
修法:
- 新增
resolveSessionOpener({ uiWorkspace, sessions }):优先用uiWorkspace.openSession(id)
(0.2.0 及以后),退回到旧的sessions.open(id)(老引擎 / 仍暴露它的宿主);两者都没有时返回
null; openWhenReady()改为接受注入的 opener;- 没有 opener 时立刻放弃并记一条明确的警告(不再空转 30 秒);
- 旧文档里「未知会话按契约会大声失败」的重试语义保持不变。
测试:新增 2 条用例 —— ①opener 的选择顺序(workspace API 优先、旧 API 兜底、都没有则为
null);②reopen 使用注入的 opener,且在没有 opener 时不做无意义轮询。共 21 项通过。
怎么确认生效:下次启动 GUI 后应当直接落在最近那次对话上;引擎日志里会先出现
[gui-last-session] pointer -> <id>(记录),页面则会用它打开该会话。
dsh-gui-last-session v0.1.5 — 找回真正的根因:引擎不再提供 current
fix: 会话指针真正的原因 —— 引擎 0.2.0 的列表状态里根本没有 current(0.1.5)
这次找到了确凿的根因,不再靠推断。
引擎 0.2.0 的客户端会话列表状态是(dsh-api-session-controller/lib/client.js 的
projectList() 最后一行):
this.list.set({ ids, byId, phase, projectionsBySession })
没有 current,而且全文件里 current: 从来没有任何一处写入。
而本插件的记录路径第一行就是:
const current = state?.current
if (!isSessionId(current)) return // ← current 恒为 undefined,这里永远 return
所以:插件加载正常、路由正常、记录永远不发生,而且一行日志都不打 —— 指针从引擎升到
0.2.0 那天起就没有再动过。上一版(0.1.4)我改的是「行是否被用过」的判断,但代码根本走不到
那里,所以没解决问题。
(顺带:引擎自己 dsh-client-ui-conversation 里有一段几乎一样的「记住上次会话」逻辑,
读的也是 sessions.list.getSnapshot().current,所以这条也是平台层的回归;我只能修我们的插件。)
修法:current 存在时仍优先用它;不存在时,从列表里推导「上次所在的对话」——
取 lastPromptAt(用户最后一次提问)最新的那一个,其次按 updatedAt,并跳过未使用
(blank)的引导会话与 subagent 行。投影元数据从两处读(row.projectionValues 与
projectionsBySession[id].values.sessionListMetadata),因为 projectList() 只在 manager
已附带时才把它复制到行上。
回归测试:新增 3 条用例,其中一条用的就是上面那个真实的 0.2.0 状态形状(没有
current) —— 这条如果早写,就不会有这次返工。另两条覆盖「元数据只在 state 级投影里」
与「跳过 subagent / 有 current 时优先用它」。共 19 项通过。
可诊断性:宿主半边现在每次指针写入都记一行
[gui-last-session] pointer -> <session-id> —— 这行会进 GUI 自己的日志(外壳会把引擎 stdout
合并进去)。此前整条记录路径完全静默,「没记录」和「记录了没生效」无法区分,这正是它能悄悄坏掉
一周的原因。
dsh-gui-last-session v0.1.4 — 记录路径不再静默卡死
修:记录路径静默卡死
现象:重启 GUI 后回到的不是最近那次对话。指针文件 <DSH_HOME>/last-session.json
停在 2026-09-29,而这期间用户一直在用多个会话。
排查(有证据):插件装着、启用着;宿主半边在跑(路由注册成功);客户端半边也在跑
(把页面在 Electron 里真打开,能看到它请求 /gui-last-session 并得到 200,启动清单里的条目
与注入声明都正常、模块图已就绪)。也就是说读指针正常、写指针停了。
两处改动:
- 记录闸门不再依赖那个「保守为真」的展示位。 引擎 0.2.0 的行字段
blank是展示语义
(可复用的空白 New-Session 槽位),客户端在 host 的sessionListMetadata.blank === false
到达之前保守保持 true,而该投影只在第一次turn/start时翻转 —— 于是「用户明明在用的
会话」也可能被判成空白、永远不记录。现在改用同一份投影里的lastPromptAt(第一次提交
user/message时写入,不依赖那个位是否对齐):判据是「host 说非空白 或 该会话已有关联的
用户提问」。真正的空白引导会话两个条件都不满足,依旧跳过(有回归用例守着)。 - 加了 15s 兜底复读。 记录此前只挂在会话列表订阅上:通知一旦没送到,指针就永远冻着,而且
不报任何错。复读在快照未变时立即返回,成本只有一次快照读取。
顺带:指针真的移动时现在会记一行日志([gui-last-session] remembered <id>)—— 之前这条路径
完全静默,「没记录」和「记录了但没生效」在支持场景里无法区分。
测试:新增 2 条用例(① host 说已提问时即使展示位仍空白也要记录;② 订阅完全没通知时复读也能
记录),连同既有用例 16 项通过。
环境要求
DSH >= 0.2.0-rc.2、Node >= 20、零运行时依赖。
升级
dsh plugin --profile web add github:itchenshi/dsh-gui-last-session或本 Release 的 tarball(dsh-gui-last-session-0.1.4.tgz)。用 DSH Ready GUI 的话,随包版本已抬高
到 0.1.4,下次启动 GUI 会自动换过来,之后指针就会跟着你当前所在的会话走。
dsh-gui-last-session v0.1.3 — DSH 兼容性声明(范围 + 在跑的引擎版本)
只改清单元数据:把 DSH 兼容性声明补成「范围 + 在跑的引擎版本」
DSH STORE 的固定 commit 自动检查对同类插件报过一条
「DSH compatibility is not explicitly declared」。查下来原因是清单里只写了:
"dshReleases": { "0.1.5-rc.2": "compatible" }而本插件实际跑的引擎是 0.2.0-rc.2 —— 也就是说,清单里没有任何一条声明对应
「正在被审查的那个引擎版本」。
现在改为:
"compatibility": {
"node": ">=20",
"dsh": ">=0.1.5-rc.2",
"dshReleases": { "0.1.5-rc.2": "compatible", "0.2.0-rc.2": "compatible" }
}dsh是范围(商城建议的写法):>=0.1.5-rc.2语义上覆盖 0.2.0-rc.2 及之后;dshReleases是逐版本证据,两端都有实测:0.1.5-rc.2 是此前长期在跑的引擎,
0.2.0-rc.2 是现在在跑的(verify:builtin第 4 幕就是在它上面启动引擎、四个插件
都在 bundle 列表里、ALL CHECKS PASSED)。不是猜的。
运行时代码一行没动,行为与前一版完全一致。版本号照常抬高,是为了让「声明的兼容范围」
能对应到一个确定的版本 —— 同版本改内容会让这个声明无从归属。
本插件的能力边界(便于静态审查)
- 运行时依赖:无(
dependencies为空)。 - 凭据 / 命令 / 原生制品 / 生命周期脚本:无。
- 详见 README 的「权限与边界」一节。
安装
dsh plugin --profile web add github:itchenshi/dsh-gui-last-session或本 Release 的 tarball(dsh-gui-last-session-0.1.3.tgz)。
dsh-gui-last-session v0.1.2
Reopens the conversation you were last in after restarting DeepSeek Harness.
- Keeps a pointer file under the DSH home directory instead of patching engine files
- Guards against the engine's own startup navigation creating an empty session, which would otherwise overwrite the pointer
- Enabled by default
Install: dsh plugin --profile web add github:itchenshi/dsh-gui-last-session