READMESource: master@d3376c37
dsh-session-manager
DeepSeek Harness(DSH)会话管理增强插件:归档会话可恢复、支持删除会话、AI 自动重命名、非人为中断自动续跑、手动释放活跃会话、会话分组(Workspace)、会话导入导出(zip 合并式恢复 + 导入后自动续跑/修复联动)、用大白话解释内置菜单。
针对 DSH 0.1.0-rc.6 内置会话菜单的痛点:
| 痛点 | 内置行为 | 本插件 |
|---|---|---|
| 归档会话是单向陷阱 | 归档后从侧边栏隐藏,没有任何查看/恢复入口(官方 README 自认 "No Session deletion or unarchive control") | 侧边栏新增「会话管理」面板,「已归档」页可查看并恢复(取消归档) |
| 没有删除会话能力 | 内置界面完全无删除入口 | 面板支持删除会话(拒绝删除运行中会话 + 二次确认) |
| 标题跟不上对话 | 只有「首条消息自动起名」一次性的;主题变了标题不变 | 每会话可勾选自动重命名:AI 分析最近对话,主题明显偏离时自动改标题 |
| 中断了不会自己继续 | 回合因崩溃/限流/超时中断后,只能人工重新打开会话再发消息 | 读取会话记录识别非人为中断,自动让 AI 继续完成原任务 |
| 活跃会话只增不减 | 会话一旦被打开就一直挂在内存,没有关闭/释放入口 | 置为不活跃 / 释放全部空闲:把空闲会话拆回冷状态,记录保留、可自动恢复挂载 |
| 菜单选项看不懂 | 「分叉会话」「归档会话」无任何解释 | 面板「说明」页用大白话解释 重命名 / 分叉会话 / 归档会话 分别是什么意思 |
内置菜单到底是什么意思
- 重命名:改会话标题,不改内容。
- 分叉会话:以当前会话「最后一个已完成的回合」为起点派生子会话——继承这段对话历史,自动命名「原标题 (N)」并打开;原会话完全不动。类似 git 分支:想换个方向探索又不破坏原会话时用它。
- 归档会话:把会话从侧边栏所有分组中隐藏(相当于收进抽屉),数据保留在磁盘上。但内置菜单归档后界面上没有找回入口——本插件的「已归档」页可以恢复。
功能
- 会话管理面板:侧边栏底部(设置上方)新增「会话管理」入口,浮层面板三个视图:
- 启动即自动加载 + 长效保存(v1.10.0):插件挂载时自动预取会话列表(不依赖打开面板),每次刷新结果写入 localStorage——面板重开/页面刷新秒显历史数据,无需等待
- 全部会话:按工作区(分组)分节展示——会话自动归入其 cwd 的最深祖先分组(如
move-test子目录会话归move-test组、其余归workspace根组),分组标题显示会话数、可点击折叠/展开,0 会话分组不显示,未匹配任何分组的会话归入「未分组」节(host 无分组 API 时自动回退平铺)。每条会话显示 标题 / 目录 / 最后活动时间 / 状态徽标(运行中、已归档、已中断),可 归档 / 删除;每条会话有自动重命名、自动续跑勾选框(自动重命名勾上后出现「分析」按钮可立即手动触发一次;被中断的会话出现「继续」按钮可立即手动续跑;live 空闲会话出现「置为不活跃」按钮),面板顶部有释放全部空闲 - 已归档:列出被归档的会话,可 恢复 / 删除
- 说明:解释内置三个菜单项 + 本插件新增的能力
- 另见「会话分组」功能(设置 → 插件配置 → 会话分组卡片)
- 自动重命名(AI 分析主题偏移):
- 给会话勾选「自动重命名」后,每次对话回合结束(
turn/end),插件延迟几秒分析一次 - 分析 = 折叠出最近最多 12 条用户消息 + 当前标题,调用 LLM(复用会话自身的模型路由
request/header里的 provider/model)判断「主题/方向是否已明显偏离标题」 - 明显偏离 → 生成 ≤20 字新标题,通过
sessionTitle.rename()落盘(标题会 pin 住,内置首条消息自动起名不会覆盖) - 标题带实时状态后缀:「(运行中)」= 回合进行中、「(已中断)」= 崩溃/错误中断待续跑;回合开始/结束自动更新(主题由 LLM 生成、后缀由状态机维护,两者分离;可用
config.titleStateSuffix: false关闭) - 成本控制:同会话分析最小间隔 5 分钟、至少新增 3 条用户消息才再分析、全局最多 2 个分析并发
- 只对 live 会话生效(正在聊天/刚打开的会话);冷会话的勾选状态会持久化,等你打开它聊天后自动生效
- 给会话勾选「自动重命名」后,每次对话回合结束(
- 自动续跑(非人为中断自动继续,核心新增):
- 识别非人为中断:读取会话记录,只看最后一次回合边界——
turn/end reason.kind === "interrupted":DSH 崩溃修复写入的合成闭合(进程被杀/崩溃)turn/end reason.kind === "error"且错误码 ∈ {RATE_LIMIT, SERVER, TIMEOUT, EMPTY_RESPONSE}:可重试的基础设施错误turn/end reason.kind === "aborted"且非用户/目标主动取消:系统原因中断- 末尾是未闭合 turn/start(open turn):崩溃中断
- 不算非人为中断:用户手动点「停止」、目标暂停(goal/parent)、普通完成、blocked、max-tokens、非可重试错误——这些不会自动续跑,但被中断的会话可手动点「继续」
- 自动继续:live 会话直接
agent.followup(续跑提示);冷会话ctx.agents.resume()(沿用会话最近的 provider/model + 原 agent preset 组合,保证工具齐全)→ 发送续跑提示(source=plugin,不伪造用户消息)→ 等回合结束 → flush →dispose()释放 agent,活跃会话数回到基线 - 触发时机:插件启动后首扫(15s)+ 周期扫描(默认 5 分钟)+ 每次回合结束事件判断
- 防护(防死循环与资源堆积):每会话串行锁、全局并发上限(默认 2)、活跃(attached)会话上限(默认 12,达到后自动续跑暂停、手动仍可用)、同会话冷却(默认 15 分钟)、每会话自动续跑总次数上限(默认 3)、续跑回合超时取消(默认 20 分钟)
- 识别非人为中断:读取会话记录,只看最后一次回合边界——
- 任务完成汇报(task-completion-report 管道,合并自 dsh-task-completion):
- 约定注入:插件启动时把「任务结束必须用 ═ 分隔线 + ✅ 任务完成 + 交付/验证/遗留 三要素收尾」注入每个 agent 的系统提示(
systemPrompt.section)——所有会话所有 AI 生效,无需用户反复叮嘱 - 工具(agent 结束前可调用):
task_completion_render渲染标准收尾块;task_completion_check自检文本是否满足格式,返回缺失项 - API:
/api/task-completion/{status,render,check}(curl 可验证) - 管道逻辑(渲染/校验)是纯函数(
lib/core.js),「任务做完了吗/交付了什么」的判断仍留在 agent
- 约定注入:插件启动时把「任务结束必须用 ═ 分隔线 + ✅ 任务完成 + 交付/验证/遗留 三要素收尾」注入每个 agent 的系统提示(
- 会话导入导出(合并自 dsh-session-transfer v0.1.0,设置 → 插件配置 → 「会话导入导出」卡片):
- 导出:会话日志(
sessions/)+ 工作区注册表(storages/workspace.json)+ 投影缓存(session_projcache.json)+ 用户级技能(skills/*.md)打包 zip;可选含工作区文件(跳过node_modules/.git/extracted等可再生大目录 + 单文件 >100MB / 总量 >1GB 跳过) - 导入(合并式恢复):会话日志重映射到现网路径键、workspace.json 合并(现网保留 + 追加去重 + 孤儿引用跳过留痕)、归档状态保持、projcache 按 id 合并(fail-soft)、技能拷贝、工作区同名默认跳过(可配覆盖)、权限收紧 + 全量比对验证(注册表无悬空引用);导入前现网注册表自动留底到
<dshHome>/session-transfer/backups/<时间戳>/,可回退 - 联动:导入完成后自动触发「自动续跑扫描」(可续会话立即进入续跑队列)+「损坏会话修复」(jsonl 格式问题自动修)——会话恢复走 session-manager 一条链路,git-rescue 的会话恢复联动也因此受益
- 安全:zip 条目路径穿越防护(
../绝对路径/盘符/反斜杠/NUL 全拒)、非法会话 id 拒绝、上传大小上限、ZIP64 明确报错 - API 前缀:
/api/session-manager/transfer/*(status/export/list/download/remove/import/backups)
- 导出:会话日志(
- Host API(
/api/session-manager/):GET /list— 全部会话:id、标题(折叠自日志)、cwd、创建/最后活动时间、live / archived / running / autoRename / interruption(中断判定)/ autoContinue / continueRunning 状态;损坏的日志会带inspectError字段POST /archive{sessionId}— 归档(原生workspaceRegistry.archiveSession)POST /unarchive{sessionId}— 取消归档POST /delete{sessionId}— 删除(拒绝运行中 → detach → 取消归档 → workspace 记账清理 → 删除磁盘日志)POST /auto-rename{sessionId, enabled}— 开关自动重命名;开启时立即分析一次POST /analyze{sessionId}— 立即手动分析一次POST /continue{sessionId}— 手动续跑(异步执行,立即返回 accepted,列表用continueRunning反映进行中)POST /auto-continue{sessionId, enabled}— 逐会话开关自动续跑(覆盖全局默认)POST /scan— 立即扫描一遍全部会话并自动续跑(测试/手动触发用)POST /detach{sessionId}— 置为不活跃:释放一个 live 空闲会话(拒绝运行中 / subagent 拥有的会话;日志保留,可重新挂载)POST /detach-all— 释放全部空闲:释放所有 live 空闲会话,返回{released[], skipped[]}(跳过运行中 / subagent / 续跑中)- 高峰期省钱模式(
/api/session-manager/peak-*,v0.7.0):GET /peak-status—{enabled, isPeakHour, peakHours, peakHoursText, sessions:{id:{notifiedAt,ackedAt,paused}}}POST /peak-pause{sessionId}— 高峰期暂停:detach 释放 + 记账paused(运行中/subagent 拒绝 409)POST /peak-resume{sessionId}— 清除暂停标记(重新打开/发消息自动挂载)POST /peak-ack{sessionId}— 确认「继续跑」:本次高峰不再提醒、自动续跑放行
- 任务完成汇报(
/api/task-completion/):GET /status(插件状态)、POST /render{delivered, verified, remaining, status}(渲染收尾块)、POST /check{text}(校验收尾格式) - 会话导入导出(
/api/session-manager/transfer/,v0.8.0,原 dsh-session-transfer 合并):GET /status— 状态(home/workspace/会话数/技能数/导出数)POST /export{includeSkills?, includeWorkspace?}— 生成导出包GET /list— 导出历史列表GET /download?id=— 下载导出包(zip)POST /remove{id}— 删除导出包POST /import(raw body zip)— 合并式恢复;导入后自动联动续跑扫描 + 会话修复GET /backups— 导入留底列表
- 会话分组(
/api/session-manager/group/,原 dsh-session-group 合并):GET /status— 分组数 / groupRoot / workspaceRoot / profile / dshHome /blockGroupNewSessionGET /list— 全部分组(workspaceId / path / title / sessionIds / createdAt / updatedAt)POST /create{title? | path?}— 创建分组:mkdir(如缺)+workspaceRegistry.create采纳目录;默认在<dshHome>/Group/<title>建软链接 → workspace 项目目录POST /delete{workspaceId}— 删除分组引用(目录与会话保留)POST /rename{workspaceId, title}— 分组改名POST /move{workspaceId, sessionId, handoff?}— 移动 = 归档原会话 + 分组下新建承接会话 + 交接(用户约定,2026-08-18):归档原会话(可还原)→ 在分组下新建会话(cwd=分组 path 自动归属,默认开启自动重命名)→ 把交接说明(默认说明原会话已归档 + 承接会话行为约束;可传handoff覆盖)作为首条消息发给新会话;返回{oldSessionId, archived, newSessionId, handoffAccepted};归档被 DSH 拒绝时 → 409archive-failed(实测运行中会话可正常归档交接)POST /new-session{workspaceId?, handoff?}— 在分组下新建会话(cwd=分组 path 自动归属,默认开启自动重命名);不带workspaceId时默认落在.dsh上一层workspace工作区(不存在则自动新建文件夹);blockGroupNewSession: true时拒绝 401;带handoff时创建后把交接说明作为首条消息发出)POST /set-config{blockGroupNewSession}— 运行时热改「禁止分组下直接新建会话」(仅内存生效,持久化需改 patch config)
- 会话分组(原 dsh-session-group 插件合并):在 设置 → 插件配置 → 「会话分组」卡片 管理 Workspace 分组——
- 创建分组:给分组名(默认在
<dshHome>/Group/<title>建软链接,项目本体留在 workspace 根)或绝对路径;分组即侧边栏的一个归类容器 - 分组列表:分组名 / path / 会话数,可重命名、删除(引用删除、目录与会话保留)
- 移动会话:输入会话 ID + 目标分组。移动 = 工作移交(用户约定):原会话先归档(可还原),再在分组下新建一个承接会话,并把交接说明作为首条消息发给新会话(可传自定义
handoff)——因为 DSH 机制不允许已有会话跨 cwd 移动,这是唯一可靠的「把会话归到某分组」的方式;归档失败时才返回 409(实测运行中会话可正常归档交接) - 在分组下新建会话:cwd 自动取分组 path → 自动归属该分组;可带
handoff把当前工作交接给新会话(首条消息即交接说明) - 承接会话行为约束(用户约定 2026-08-18):move / new-session 的交接说明自动附带「行为约定」——承接会话只简单了解情况 + 把已知信息总结发给用户,不直接开始写代码(即使发现原理/代码有 bug 也不动手,先汇报情况等用户指示)
- 承接会话默认开启自动重命名(用户约定 2026-08-18):move / new-session 创建的承接会话自动打开「会话管理」面板的自动重命名开关(主题变化自动改标题),可在面板随时关闭
- 「禁止在分组下直接新建会话」开关:开启后隐藏侧边栏每个分组行的「+」按钮并在服务端拒绝
- 创建分组:给分组名(默认在
- 高峰期省钱模式(DeepSeek 2026-08-17 起峰谷分时定价:高峰时段价格翻倍):
- 高峰期暂停自动续跑(默认开启
peakPauseAutoContinue):处于高峰时段时,自动续跑不启动(省钱核心);用户点「高峰继续」(peak-ack)可放行单个会话,或过了高峰时段自动恢复 - 高峰期提醒(
peakRemindEnabled):回合结束时若在高峰时段且会话未确认过,记录「高峰期待确认」标记,面板显示高峰期横幅 + 会话行「高峰」徽标,提醒用户可用「置为不活跃」暂停非必要会话 - 手动暂停/恢复:会话行「高峰继续」= 确认该会话本次高峰放行;「继续」= 清除暂停标记(被暂停的会话重新打开/发消息自动挂载)
- 高峰时段可配置
peakHours(默认官方 9:00-14:00;支持多段与跨天,如夜间 21:00-2:00 用[[21,2]])
- 高峰期暂停自动续跑(默认开启
配置(cordis.patch.yml 的 config,全部有默认值)
- insert:
- id: dsh-session-manager
name: dsh-session-manager
config:
enabled: true # 自动续跑总开关
defaultAutoContinue: true # 单会话默认开启(面板可逐会话覆盖)
maxConcurrent: 2 # 全局同时续跑会话数上限
maxAttached: 12 # 活跃(attached)会话上限,达到后自动续跑暂停
cooldownMs: 900000 # 同会话自动续跑最小间隔(15 分钟)
maxContinuesPerSession: 3 # 每会话自动续跑总次数上限(防死循环)
turnTimeoutMs: 1200000 # 单回合续跑最长等待(20 分钟),超时取消
scanIntervalMs: 300000 # 周期扫描间隔(5 分钟)
groupEnabled: true # 会话分组功能总开关
groupRoot: "" # 分组根目录,默认 <dshHome>/Group(.dsh 下)
workspacePath: "" # 项目根目录,默认取默认 workspace 的 path
blockGroupNewSession: false # 禁止在分组下直接新建会话(默认 false = 不屏蔽;新建会话默认落 workspace 工作区)
peakHours: [[9,14]] # 高峰期时段(DeepSeek 官方 9:00-14:00 价格翻倍;支持多段/跨天,如 [[9,14],[21,24]]、[[21,2]])
peakRemindEnabled: true # 高峰期提醒总开关(回合结束提醒用户可暂停)
peakPauseAutoContinue: true # 高峰期暂停自动续跑(省钱核心;用户 ack「继续」或过高峰后恢复)
当前实例活跃会话数可用内置 RPC
host.describe查询(attachedSessions字段),作为设置maxAttached的参考。
安装
开发/验证请使用隔离测试环境(主实例 ~/.dsh 是只读的),以本仓库内的干净环境脚本为例:
cd /vol1/@appshare/DeepSeekHarness/workspace
# 1) 复制插件源码
mkdir -p <env>/profiles/web/node_modules_local/dsh-session-manager
cp -r dsh-session-manager/{package.json,cordis.patch.yml,lib} <env>/profiles/web/node_modules_local/dsh-session-manager/
# 2) 补软链(镜像型 node_modules 必须)
ln -sfn ../node_modules_local/dsh-session-manager <env>/profiles/web/node_modules/dsh-session-manager
# 3) 注册三要素
# cordis.patch.yml 追加:
# - insert:
# - id: dsh-session-manager
# name: dsh-session-manager
# package.json dependencies 追加:
# "dsh-session-manager": "file:./node_modules_local/dsh-session-manager"
# 4) 重启实例
验证:
curl -s http://127.0.0.1:<port>/ | grep -o 'dsh-session-manager[^"]*' # boot 含插件
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/plugins/dsh-session-manager/client.js # 200
curl -s http://127.0.0.1:<port>/api/session-manager/list
实现说明(内部变通,升级 DSH 需复核)
自动重命名部分:
- 开关与分析记账持久化在独立存储域
dsh_session_manager(<DSH_HOME>/storages/dsh_session_manager.json),用 DSH 官方 storage-domain 机制(defineDomain+ctx.storageDomain.open),非手写文件 - LLM 调用复用官方
dsh-session-title-llm的模式:ctx.llm.stream+BlockAssembler+createUserMessage,模型路由取会话自身request/header里的{provider, model} - 改标题走公开 API
sessionTitle.rename(session, title)(与内置重命名同一入口),会把标题 pin 为 user 来源——副作用是内置「首条消息自动起名」不再覆盖它,这正是期望行为 - 分析逻辑(prompt 构造 / 判别结果解析)导出为纯函数并有单测:
node test-auto-rename.mjs(12 项断言,mock LLM 流)
自动续跑部分:
- 中断判定:崩溃中断的冷会话,
sessionPersistence.inspect()会先套用 DSH 的interruptedTurnClosers崩溃修复(合成turn/end reason.kind="interrupted"),插件直接读修复后的事件流即可;判定函数interruptionInfo导出为纯函数(node test-interruption.mjs,21 项断言) - 续跑机制参考
dsh-headless直驱写法:ctx.agents.resume({resumeSessionId, agentOptions, setup})加载持久化会话并挂 agent →agent.followup()发送续跑提示 → 回合结束sessions.flush()→handle.dispose()释放(活跃会话数回落)。setup 里installModelSelection安装会话最近模型路由、agentPresets.mount挂载原 preset 组合(保证工具齐全) - 记账:
autoContinue[sessionId] = {lastContinuedSeq, lastContinuedAt, continueCount},lastContinuedSeq >= 中断 turn/end 的 seq视为已续过,配合冷却与总次数上限防死循环(node test-auto-continue.mjs,12 项断言) - 全链路 e2e:
bash e2e-crash-continue.sh(真实创建会话 → SIGKILL 模拟崩溃 → 重启 → 插件首扫自动续跑 → 验证中断清除 + 记账落盘)
手动释放部分:
- DSH 没有「关闭/释放会话」的公开入口:
host.describe的attachedSessions就是ctx.agents.list().length,会话一旦被 UI 打开就常驻。detachSessionAgent复刻 dsh-agent-loop 生命周期 dispose 的拆卸顺序:agent.cancel(keepInbox)→whenIdle→agent.scope.dispose()(卸载 agent 作用域 fiber 的注册项)→ agents 注册表移除 +agents.emitDisposed(entry)(复刻detachEntered)→sessions.flush→sessions.store 条目 detach()(触发session/disposed→ 持久化协调器 retire 落盘) - 守卫:运行中(open turn)拒绝;subagent 拥有的会话(
hasApiRemoteSubagentOwner,即header.origin==="subagent"或父代理拥有)拒绝;冷会话幂等返回 not-live - 非破坏:日志完整保留;再次打开/发消息时 apiproxy 的
ensureSession会从持久化自动重新挂载(排队中的 inbox 消息以agent/inbox/spliced事件持久化,重新挂载后由 Inbox 重放) - 单测:
node test-detach.mjs(5 项断言);e2e:bash e2e-detach.sh(创建 2 会话 → 单条 detach → attached 2→1 → detach-all → 0 → 重新发消息自动挂载 → 1)
DSH 0.1.0-rc.6 没有公开的 unarchive / session-delete / session-detach API,本插件用到几处内部实现,均在 lib/index.js 注释标注:
- 取消归档:调用
dsh-workspaceWorkspaceRegistry的state/setState(原型方法,非#私有)。setState与原生archiveSession走完全相同的domain.global.set → domain/changed持久化路径,host-apiproxy 监听到变更后自动向所有客户端推送host/archived-sessions-changed帧——前端无需额外刷新。 - 删除 live 会话:调用
dsh-sessionSessionStore内部store条目({sessionId, seq...}索引)的detach()——与 agent 拆卸会话同一路径,触发session/disposed→ 前端收host/session-removed。 - 自动续跑:依赖 dsh-agent-loop 的
ctx.agents.resume()/agent.followup()/handle.dispose()(公开 API,见dsh-headless直驱示例)。 - 手动释放:
agent.cancel()/whenIdle()公开;agent.scope.dispose()、agents.store条目删除 +agents.emitDisposed()、sessions.store条目detach()为内部实现(与 agent-loop 自身 dispose 同一路径、同一顺序)。
删除流程:拒绝运行中(最后一个 turn 边界未闭合)→ live detach → 若已归档先取消归档 → 从 workspace 记账 detach(detachSession 公开方法)→ 用 sessionPersistence.listArtifacts() 定位并 fs.rm 会话目录(含 session.jsonl.zstd)→ sqlite 查询索引自动对账清理。先 detach 再删文件,避免持久化协调器把会话写回磁盘。
会话分组部分(lib/group.js,源码 + 实测三重印证):
- 会话归属分组 = 会话 header 的 cwd 硬绑定:只有 cwd 恰好等于分组 path 的会话才能进该分组(
Workspace.attachSession强校验realpath(cwd)===path);session.create带workspaceId时 cwd 自动设为分组 path → 自动归属 - 已有会话无法跨组移动(机制事实):
attachSession未暴露公开 RPC;insertSessionBefore拒绝 "not accounted";GUI 拖拽只做组内排序;直接改workspace.json无效(注册表内存快照无文件 watcher,且 sessionIds getter 按sessionPath(id)===path过滤)。因此 move 走「归档原会话 + 分组下新建承接会话 + handoff 交接」(用户约定,2026-08-18):原会话归档(可还原)→session.create {workspaceId}在分组下新建 →session.prompt {mode:'queue'}发交接说明 + 行为约束;归档失败(DSH 拒绝)→ 409archive-failed不新建(实测运行中会话可正常归档) - 分组目录布局:默认分组引用建在
<dshHome>/Group/<title>(软链接 → workspace 项目目录),项目本体留在 workspace 根,不把项目塞进.dsh - new-session 走 HTTP 回环:host 侧没有公开的 session 创建服务方法,转发公开 RPC
session.create {workspaceId}(与 GUI 分组行「+」按钮同一路径);handoff交接说明再转发session.prompt {mode:'queue'}作为首条消息 - 单测:
node test-group.mjs(mock ctx + 内存 workspaceRegistry 假实现,10 项断言)
已知边界
unarchive/ 删除 live 会话 / 自动续跑 / 手动释放依赖 DSH 内部实现,DSH 升级后需复核(见上)- 自动重命名只对 live 会话生效:
sessionTitle.rename要求会话在 store 中(sessions.get(id)非空);冷会话(重启后未打开)勾选后不立即分析,等打开聊天、turn/end触发时才生效。面板中可点「分析」手动触发 - 自动重命名需要可用的 LLM provider(会话自身的模型路由);调用失败会在 API 响应和实例日志中如实报错(
llm-failed) - 自动续跑只对「非人为中断」生效:可重试错误码集合固定为 RATE_LIMIT/SERVER/TIMEOUT/EMPTY_RESPONSE,其他错误请手动「继续」;自动续跑使用会话最后使用的模型路由,若该模型已不可用,续跑回合会失败(计入每会话次数上限,不会死循环)
- 自动续跑在活跃会话数达到
maxAttached(默认 12)时暂停(手动「继续」不受限);续跑完成的冷会话会被释放(dispose),活跃数回落 - 手动释放只对 live 空闲会话生效:运行中 / subagent 会话拒绝;被释放的会话若正被浏览器打开,会收到
host/session-removed帧(从侧边栏消失),再次打开/发消息自动恢复挂载 - 删除是硬删除:日志与记录一并清除,不可恢复(面板强制二次确认)
- 删除后附件等旁路数据可能残留孤儿文件(不影响会话从 DSH 消失)
- 面板 UI 需在浏览器人工确认(React 渲染不在 curl 验证范围内)
- 会话分组:move 是「归档原会话 + 新建承接会话」的工作移交(原会话可还原,不会丢),不是把已有会话挂进分组——DSH 机制不允许跨 cwd 移动;归档失败才返回 409(实测运行中会话可正常归档交接);「在分组下直接新建会话」默认不屏蔽(
blockGroupNewSession: false),new-session不带workspaceId时默认落在.dsh上一层workspace工作区(无则自动新建,2026-08-19 用户约定);set-config仅内存热改,持久化需改 patch config;new-session/move依赖 HTTP 回环(PORT/TEST_DSH_PORT环境变量定位自身端口,多端口部署需复核) - 会话分组客户端在 设置 → 插件配置 的「会话分组」卡片,React 渲染需浏览器人工确认
目录结构
dsh-session-manager/
├── package.json # dsh.bundle.patch + dsh.client.platform/inject
├── cordis.patch.yml # insert 注册 + 可选 config
├── lib/
│ ├── index.js # host:list/archive/unarchive/delete/auto-rename/analyze/continue/auto-continue/scan/detach/detach-all/repair-sessions/peak-*
│ ├── group.js # host:会话分组路由 /api/session-manager/group/*(原 dsh-session-group 合并)
│ ├── peak.js # host:高峰期判定纯函数(isPeakHour/normalizePeakHours)
│ ├── core.js # 任务完成汇报 纯函数(render/check)
│ ├── repair.js # 会话日志损坏扫描/修复(纯函数)
│ └── client.js # client:sidebar.footer.action「会话管理」入口 + 面板 + 设置页「会话分组」卡片
├── test-interruption.mjs # 中断判定纯函数单测(22 项断言)
├── test-auto-rename.mjs # 自动重命名分析逻辑单测(12 项断言)
├── test-auto-continue.mjs # 自动续跑流程单测(mock ctx,12 项断言)
├── test-detach.mjs # 手动释放单测(mock ctx,5 项断言)
├── test-repair.mjs # 会话修复单测
├── test-task-completion.mjs # 任务完成汇报单测
├── test-group.mjs # 会话分组单测(mock ctx,13 项断言)
├── test-peak.mjs # 高峰期判定单测(22 项断言)
├── e2e-crash-continue.sh # 崩溃→重启→自动续跑 全链路 e2e
├── e2e-detach.sh # 手动释放(置为不活跃)e2e
└── README.md
No comments yet. Be the first to write one.