DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Session Relocate — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins

dsh-session-relocate

Session Relocate

Move DSH conversations across workspaces: move a Session’s log files and workspace association to another workspace as a unit, with automatic registration of the new workspace. Uses safe transactions with log-level crash recovery and safely releases sessions that are open but idle first—so currently

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add dsh-session-relocate@0.2.2
READMECompatibilityVersions

Description

Move DSH conversations across workspaces: move a Session’s log files and workspace association to another workspace as a unit, with automatic registration of the new workspace. Uses safe transactions with log-level crash recovery and safely releases sessions that are open but idle first—so currently open conversations can also be moved directly without restarting DSH.

Compatibility and provenance

Session Relocate is published as dsh-session-relocate and currently resolves to version 0.2.2. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
npm
Registry updated
10/19/2026

Versions

0.2.2stable
10/19/2026
0.2.1stable
10/19/2026
0.2.0stable
10/18/2026
Show 1 more versionCollapse versions
0.1.0stable
10/17/2026

Related plugins

Loading related plugins…

Latest
0.2.2
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
241.7 kB
Files
13
Surface
web
License
MIT
Source
npm
GitHub
★ 0
Weekly downloads
0
Last push
10/19/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in productivity-workflow.

Acp App@deepseek-ai/dsh-acp-appThe dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-baseExperimental Schedule Bundle@deepseek-ai/dsh-experimental-schedule-bundleOptional bundle that adds Schedule, its task page, and time context to the Web compositionAb Ocrdsh-ab-ocr--- description: "The out-of-tree ocr tool: recognizes a scanned PDF or image page by page, merges the pages in order only after the last one, and saves one Markdown document with the heading levels the model decided." kind: "package-reference" ---Scriptor@linfengqaqtat/dsh-scriptorWebnovel Writer: DSH Long-Form Web Novel Writing Workspace

README

dsh-session-relocate

让 DSH 的对话能跨工作区搬家 —— 连正在回答的那个也能搬。

装完之后,你在侧边栏里就能把一个对话从「工作区 A」搬到「工作区 B」:它在 A 里消失、在 B 里出现, 而且接着继续聊 —— 它的文件沙箱根、工具工作目录、后续读写全部变成 B。


三步用起来

1. 装

# 从 npm 装:
dsh plugin --profile web add dsh-session-relocate

# 或者从源码装(把路径换成你 clone 下来的 checkout):
dsh plugin --profile web add <你 clone 下来的 dsh-session-relocate 目录>

2. 重启 dsh web

主机侧是 Node 模块,必须重启才会加载。

3. 确认它活了

  • 侧边栏里任意一个对话的 「…」菜单里会多出一项 移动到;
  • 或者直接问它一句:
curl http://127.0.0.1:3080/api/dsh-session-relocate/status

返回 {"ok":true, ...} 就是在线。


怎么用

方式一:菜单

  1. 鼠标移到侧边栏里某个对话上,点 …;
  2. 点 移动到 —— 会列出除「它当前所在的工作区」以外的所有工作区;
  3. 点目标工作区。列表立刻刷新,这个对话出现在目标工作区下;
  4. 打开它继续聊:它现在就运行在新目录里。

想搬到一个还不存在的工作区:点 其它目录…,输入一个绝对路径 —— 目录会被创建, 并自动注册成一个新工作区。

方式二:直接拖

把侧边栏里的对话拖到另一个工作区上,松手就搬,不用点菜单。

投放目标是整块工作区分组,不是那个 34px 的文件夹行:

  • 拖到目标工作区的文件夹行上 —— 可以;
  • 拖到它下面任意一条会话行上 —— 也可以;
  • 拖到分组里的空隙、或「展开其余 N 个会话」上 —— 一样可以。

松手前,整块目标分组会亮起蓝框。

多选也能拖。 在多选模式里勾好几条后,抓住其中任意一条拖到目标工作区,松手就是整批搬(逐条文件事务 + 最后一次性写归属),进度显示「正在移动 x/N…」;拖的如果是一条没被勾中的行,就只搬它自己。

组内上下拖动仍然是官方的排序功能,插件一点不抢。 判断依据是"指针落在哪个分组里": 在别的分组里才接管并搬走;在它自己所在的那个分组里就完全让路。

批量搬(多选)

侧边栏某个对话的「…」菜单里点 多选,每行左侧就会出现勾选圈;点整行即勾选(不会打开对话),底部出现操作条 已选 N 个 / 取消 / 移动。选好目标工作区后逐条搬,进度实时显示「正在移动 x/N…」,失败的那条原地不动、保留选中方便重试。

  • 装了 dsh-delete-session 的话,它的「多选」底栏会多出一个 移动 按钮 —— 两个插件共用同一套选择模式,不会出现两个「多选」入口。
  • 多选状态下直接拖也行:抓一条选中的行拖到目标工作区,整批一起搬(见上一节)。
  • 目标列表会直接标出「将移动 M 个 / 跳过 K 个」;全部已在目标工作区的项会置灰,点了也不会发请求。
  • 批量不是把 N 次单搬简单循环:文件事务逐条做(进度、失败隔离),工作区归属最后一次性写入。按实测的单条固定开销推算,3 条会话的总耗时从约 0.96 秒降到约 0.6 秒,条数越多差距越大。
  • 中途崩了也不怕:每条会话的迁移日志还在,下次启动(或下一次桥接调用)会按文件状态幂等补完归属。

连同子对话(默认开)

子智能体的会话(origin: subagent,侧边栏里是隐藏的)默认会跟着父对话一起搬:父搬到哪,它的子、孙会话也搬到哪里,cwd 和工作区归属一起改。每条子会话独立走事务,失败按条隔离,不会拖累父对话的回执。

想只搬父对话:Plugins → dsh-session-relocate → 配置 里把「连同子对话一起移动」关掉(存在浏览器本地,默认开)。关掉后子会话留在旧工作区 —— 旧工作区先别删,子会话日志还在里面。

正在回答的对话也能搬

包括正在回答、正跑子代理、正跑后台任务的那个。 插件会先拿到它的写所有权,再动文件:

  • 打开着但空闲:先把它从 DSH 内存里安全释放,再搬;
  • 正在跑活:就地接管 —— 换掉写句柄的 header 并在写链内搬文件,不打断这一轮回答。

一句诚实的话:正在跑的那一轮会继续跑完,但它手里的工具工作目录仍然是旧目录 (会话在内存里的 header 是冻结的,改不了);文件和归属都已经搬走,这个对话下次打开时才真正 在新目录里跑。想要"正在跑活就拒绝"的保守行为,把 moveLive 设为 false。

如果你搬的正是屏幕上打开的那个对话

页面需要跳转/刷新一次再打开它(客户端手里那份是旧的)。搬别的对话则完全无感。


出问题怎么办

症状多半是怎么办
「…」菜单里没有 移动到装完没重启,或插件没加载重启 dsh web;再 curl .../status 看是不是 200
拖动一点反应都没有DSH 改了侧边栏的 DOM 约定,或 dragMove 被关看 dragMove(设回 true);菜单那条路不受影响,先用它
搬完打开是空的 / 还在旧工作区搬的正是当前打开的那个对话刷新页面,再打开它
提示"归属会在下次启动自动补齐"这个 DSH 版本的注册表没有可用的刷新入口重启一次 DSH,归属会自动补上
提示"这个对话正在跑"moveLive 被设成 false等它跑完,或把 moveLive 设回 true
提示"打开着的会话搬不了"releaseLive 被设成 false先切到别的对话,或把 releaseLive 设回 true
搬完这一轮的工具还在旧目录设计如此(内存 header 冻结)不用管;下次打开就在新目录了

它是怎么做到的

为什么 DSH 自己搬不了

DSH 发行版里没有任何跨工作区移动会话的能力(moveSession / relocateSession / changeCwd 零命中), 因为「对话属于哪个工作区」被编码在几个互相约束的地方:

事实说明
目录名由 cwd 推导会话存在 ~/.dsh/sessions/<由 cwd 编码的项目目录>/<会话目录>/session.vN.jsonl.zstd,项目目录名是 cwd 的编码(分隔符折叠成 -,非 ASCII 变 ~XXXX)。
cwd 住在日志第一行每个 generation 文件的第一行就是 header,含 cwd;header 的 cwd 与物理位置不一致会被判定成 corrupt session log。
归属 = 注册表 + 校验工作区记录在 ~/.dsh/storages/workspace.json 的 sessionIds 里,但 attachSession 还会校验 header 的 cwd 必须等于工作区 path。
不能出现「两份」findLog(id) 一旦发现同一个 id 出现在两个项目目录里就直接抛错 —— 这个会话会永久打不开。

所以「移动」必须同时做三件事,而且中间态必须不伤源数据: 改 header 的 cwd → 把会话目录搬到新项目目录 → 改注册表归属。

移动是一个可恢复事务,不是一次 rename

  1. 校验:会话存在、目标工作区存在、源 ≠ 目标、日志读得出来;正在跑活时确认能就地接管;
  2. 如果这个对话正打开着:空闲的走 release、正在跑的走 handoff(见下两节);
  3. 写迁移日志(~/.dsh/session-move/<opId>.json);
  4. 把整个会话目录复制到暂存区 <sessionsRoot>/_session-move-staging/<opId>/<会话目录>/。 暂存区比 DSH 认的会话目录深一层,所以后端的扫描器永远看不到它;
  5. 在暂存区里改每个 generation 的 header cwd,并逐字节自校验:除第一行外, 明文必须与源文件完全一致、行数也必须一致 —— 不满足就整体放弃;
  6. rename 到目标目录(同一存储根下,是原子操作);
  7. 删源目录;
  8. 刷新注册表的 header 索引 → attachSession 到目标 → 从源 detachSession;
  9. 失效该会话的投影缓存条目,删掉迁移日志。

关键取舍:改写发生在暂存区,源目录在改名之前一个字节都没被动过。所以任何一步失败都能干净回滚; 即使进程在改名后被杀死,也只会留下「目标已完整 + 源还在」这一种中间态,由启动时的恢复流程 (recoverPendingMoves)幂等推完。

A.「先释放再搬」—— 打开着但空闲的对话

会话日志是单写者的:写所有权由 agent-loop 的写句柄持有(它调 sessionPersistence.open(id, 'write')),只在 DSH 进程退出时才释放。不释放就搬文件, 那个句柄会继续往旧路径写、并在旧位置重建一个空壳目录 —— 这就是生态里其它迁移插件 要么直接拒绝、要么排队等重启的原因。

释放的缝是存在的(SessionStore 每个条目自带 detach()),同一个 DSH 版本里 dsh-delete-session 已经在生产里用它。本插件的顺序:

  1. sessions.flush(session) —— 持久化屏障,把缓冲的事件落盘;
  2. sessions.store.get(id).detach() —— 从 live store 摘掉,派发 session/disposed;
  3. 持久层收到 session/disposed 会 close() 那个写句柄,从而释放 session.lock 租约 (它是 fire-and-forget 的异步动作);
  4. 探针确认:反复尝试 sessionPersistence.open(id, 'write') —— 能拿到就证明已经没有活跃写者, 立刻 close() 归还;
  5. 探针通过后才开始文件事务。

任何一步失败都会整体放弃且不碰任何文件(release-failed)。想恢复旧行为就把 releaseLive 设为 false:打开中的会话直接拒绝。

第 8 步里「刷新 header 索引」是必需的:注册表的 header 缓存是启动时建的,不刷新的话它会拿旧 cwd 去比目标路径、然后自己把 attach 拒掉。这个入口是包私有方法,所以做了 feature-detect —— 探测不到时磁盘操作照样完成,归属会在下次启动时自动补齐(菜单会告诉你)。

B.「就地接管」—— 正在跑的对话也能搬

写句柄的写入路径是每次从它手上的 header 现算的,而这个 header 是可写槽位(被冻结的是 那个对象,不是这个属性)。所以顺序是:

  1. 从后端的 tracker.writers 取到这个会话的 live 写句柄(取不到就整体放弃, 绝不静默搬 —— 那会留下「文件已搬、句柄还指着旧路径」的坏状态);
  2. await handle.drainLive() + await handle.flush() —— 把缓冲事件落盘。 drainLive 自己会往写链上排队,所以必须在进入临界区之前调用,否则死锁;
  3. await handle.enqueueChain(...) —— 进入该句柄唯一的串行点。此后所有 append / flush 都排在我们后面;
  4. 在临界区里:释放内核写锁并置空 → handle.header = { ...header, cwd: 目标 } → 跑文件事务(暂存 / 改写 / 原子改名 / 删源)。失败就把 header 与锁状态一起回滚;
  5. 归属绕开 attachSession:live 会话在内存里的 header 是 deep-frozen 的旧 cwd, 注册表的 cwd 校验必然失败。改为直接走注册表自己的写路径(entity.mutate → table.update),并先把路径索引指到新 cwd(mutate 内部会用它过滤 sessionIds, 顺序反了会把刚加进去的 id 自己滤掉)。

写锁的形态值得单独记一句:Windows 上是由路径派生的命名内核信号量,根本没有锁文件; POSIX 上才是会话目录里的 session.lock(flock)。两种情况下锁的名字/对象都跟路径绑定, 所以「先释放、让下一次写入在新路径重取」是必需且正确的。


配置

cordis.patch.yml 里这一行的 config(profile 层可覆盖):

键默认含义
bridgePrefix/api/dsh-session-relocate主机桥接前缀。改动后客户端自动跟随(通过引导全局下发)。
journalsDir<dsh home>/session-move迁移日志目录。
sessionsRoot后端的 root,退化为 <dsh home>/sessions会话存储根。一般不用设。
home$DSH_HOME 或 ~/.dshDSH home。
moveLivetrue正在跑活的对话是否走「就地接管」。设 false 则正在跑活的会被拒绝。
dragMovetrue侧边栏拖动搬移(整块工作区分组都是投放目标)。依赖官方的 DOM 行属性与分组结构;DSH 改了约定时可一键关掉(菜单那条路不受影响)。
releaseLivetrue打开着但空闲的对话,搬之前是否自动释放它。设 false 则打开中的直接拒绝。
releaseRetries / releaseRetryDelayMs24 / 150释放后探测写租约的重试次数与间隔。前 5 次几乎即时(0/10/25/50/100 ms),之后按 150 ms 退避;回执里会带上 releaseAttempts。
verifyauto改写后的自校验强度。auto:压缩后 ≤ fullVerifyLimit 的日志全文解码,更大的只做帧级比对 + 解头解尾(回执标 verification: 'sampled')。full:一律全文解码。
fullVerifyLimit2097152(2 MB)verify: auto 下的分界点。
selftestfalse打开后多挂几个 __selftest/* 路由,用来在真实进程里造/清一个测试会话、施加写入压力、验证接管。

桥接 API

方法路径说明
POST<prefix>/describe{ sessionId? } → 工作区列表;带 sessionId 时额外给出 currentWorkspaceId、live、willHandoff(正在跑,会就地接管)、willRelease(空闲,会先释放)、activity、canMove、reason。
POST<prefix>/move{ sessionId, targetWorkspaceId } 或 { sessionId, targetPath } → 迁移回执(applied: 'live' | 'pending'、released、releaseAttempts、handedOff、verification: 'full' | 'sampled')。带 deferRegistry: true 时只做文件事务、保留迁移日志。
POST<prefix>/move-commit{ sessionIds } → 把一批 defer 过的迁移的归属一次性写进注册表(每个目标/源工作区各一次 mutate),返回逐条结果。
GET<prefix>/status版本/配置(含 moveLive/releaseLive/verify/bulkDefer)/待收尾的迁移列表;调用时会顺带尝试恢复。
POST<prefix>/__selftest/create-live仅 selftest: true 时存在。造一个「已打开 + 持有写租约」的会话({ workspacePath, pressure? })。
POST<prefix>/__selftest/state仅自测时存在。写入压力的采样时间戳 + 每次接管的窗口 + 写句柄现状。
POST<prefix>/__selftest/ensure-lease仅自测时存在。强制写句柄重取内核锁,用来证明它把路径算到了新目录。
POST<prefix>/__selftest/drop仅自测时存在。释放 + 摘归属 + 删目录 +(可选)注销工作区。

失败一律是 { ok: false, code, message },code 形如 session-live、session-active、 already-there、corrupt-log。

测试与验证

node --test

覆盖:路径编码与真实 ~/.dsh/sessions 目录名逐一比对、对真实会话日志做 header 外科手术的逐字节校验、 完整迁移 / 回滚 / 崩溃恢复、桥接端到端(假 cordis ctx)、客户端状态机(假 module loader + react 替身)、 以及拖动的分组归属规则。全部用合成数据,不碰你的真实会话。

三条真机验证(都不需要额外装包):

脚本验证什么
scripts/e2e.mjs live在临时 DSH_HOME 里造一个持有真实写租约 + 持续写入压力的会话(等价于"正在回答"),边跑边搬:断言 header 换成目标、内核锁释放、没有任何一次写入插进接管窗口、搬完还活着、旧目录绝不被重建。
scripts/drag-verify.mjsCDP 驱动 headless Chrome 打开真实 DSH 页面:客户端半真的执行、引导全局带上 drag: true、合成拖放事件后会话真的落到了目标工作区。
scripts/drag-live-check.mjs对着你正在跑的那个 DSH 做只读检查:分组契约在真实 DOM 上成立、拖到别组会话行被接管并高亮、拖回自己分组完全让路。它只合成事件、绝不投放,所以永远不会搬动任何会话。

已知限制

  • 拖动搬移依赖官方的 DOM 约定:行属性(data-row-key="session:<id>" / "workspace:<workspaceId>") 加上分组结构(官方 renderGroup 把每个工作区渲染成一个「恰好只含一个工作区行」的容器)。 它读的是官方渲染出来的东西,不是未公开的拖拽载荷;DSH 若改掉这些,拖动会分级退化: 分组读不到 → 退化成「只认文件夹行」;连行属性也没有 → 完全不介入。 菜单那条路始终不受影响,dragMove: false 可以一键全关。
  • 嵌套工作区(工作区套工作区)时,父分组里若渲染了子分组,父分组的会话行不再算投放目标 —— 这是保守取舍:宁可少接管,也不猜错工作区。
  • 正在跑活的那一轮不会换工作目录:会话在内存里的 header 是 deep-frozen 的,改不了; 文件与归属都搬走了,但那一轮的工具仍然用旧目录,下次打开才真正在新目录里跑。
  • 正在跑活的接管依赖私有内部结构(tracker.writers、写句柄的 header 槽位、enqueueChain、 以及注册表的 entity.mutate)。DSH 换 rc 版本时这些都可能变,所以每一处都 feature-detect: 探测不到就整体放弃(handoff-failed),绝不半途而废。
  • 多进程共用一个 DSH home 时:接管期间会短暂释放内核写锁,理论上另一个进程可能抢进来。 单进程部署(默认)没有这个问题。
  • 搬「当前屏幕上打开的那个」之后,页面要跳转/刷新一次再打开它(客户端手里那份是旧的)。
  • 只移动这一个会话。由它 fork 出来的子会话是自包含的(继承事件已物理写入子日志),留在原处不会坏; 子代理子会话同样留在原处。
  • 一个会话同时只能属于一个工作区,这是 DSH 的模型,不是插件的选择。
  • 主机侧假设会话存储根下可以放一个 _session-move-staging 暂存目录(对 DSH 不可见,用完即清)。