uu88s/dsh-session-handoff ↗★ 0
@uu88s/dsh-session-handoff
复制 DSH 会话 id,或把整个会话交接给 codex:在目标 agent 的会话库里写下它能 resume 的记录并登记进它的索引。 适合需要将当前会话无缝迁移、恢复至Codex Agent继续对话的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:uu88s/dsh-session-handoff@uu88s/dsh-session-handoff — 会话交接
DSH Web 插件。做两件事:
- 复制会话 id:在侧边栏会话行右侧的 hover 图标或
…菜单里,一键复制 DSH 会话 id。 - 交接给 codex:把整个 DSH 会话(对话文本 + 工具调用 + 工具输出 + 文件变更清单)转写成一个 codex 能
resume的会话,写进 codex 的会话库并登记进它的索引,最后给出可粘贴的恢复命令codex resume。
只复制 id 是不够的:codex 的
resume只认自己库里的会话。交接 = 读源会话 → 展平成中间表示 → 写成目标 agent 的原生记录 → 登记进目标索引 → 给出恢复凭据。
安装
下面的写法都实测过,任选其一:
# 跟随 main
dsh plugin --profile web add github:uu88s/dsh-session-handoff
# 锁定版本标签
dsh plugin --profile web add github:uu88s/dsh-session-handoff#v0.1.0
# 源码归档 —— 含 test/ 与 docs/,可本地跑验收
dsh plugin --profile web add https://codeload.github.com/uu88s/dsh-session-handoff/tar.gz/refs/tags/v0.1.0
# Release 资产 —— 只含发布白名单里的 15 个文件
dsh plugin --profile web add https://github.com/uu88s/dsh-session-handoff/releases/download/v0.1.0/uu88s-dsh-session-handoff-0.1.0.tgz
或者先下载再本地安装(plugin_manager 的 install_bundle,target 传绝对目录):
curl -L -o handoff.tar.gz https://codeload.github.com/uu88s/dsh-session-handoff/tar.gz/refs/tags/v0.1.0
tar -xzf handoff.tar.gz
dsh plugin --profile web add /绝对路径/到/dsh-session-handoff-0.1.0
装完在 dsh --profile web --dump-config 的组合树里能看到 - id: session-handoff / name: '@uu88s/dsh-session-handoff'。
包内容
package.json 的 files 白名单(共 15 个文件):
index.js client.js package.json cordis.patch.yml README.md LICENSE
lib/{zstd-frames,dsh-session,ir,codex-rollout,codex-store,handoff-log,handoff}.mjs
locale/{zh,en}.json
test/ 与 docs/ 不进包,但都在仓库里,克隆源码后可直接跑。
源码:;问题反馈:。
安装后 client 半边改动可免刷新生效(依赖 dsh-client-hmr 的轮询);host 半边改 JS 需要重启 DSH。
用法
| 入口 | 位置 | 作用 |
|---|---|---|
| 会话行 hover 图标 | 侧边栏会话行右侧 | 交接给 codex |
会话行 … 菜单 | 同上 | ① 复制 DSH 会话 id ② 交接给 codex |
| 会话头部「交接」按钮 | 会话标题右侧 | 交接给 codex |
/handoff 命令 | 输入框 | [] [--dry-run] [--undo [--force]] |
handoff_session 工具 | 模型可调用 | {sessionId?, dryRun?, codexHome?} |
交接是两步式:先预演(列出将新建的 rollout 文件与将插入的索引行),确认后才写入。完成后 Toast 上给三个按钮:复制恢复命令、撤销这次交接、关闭。
安全性
- 只增不改:永不覆盖已存在的文件(
flag: 'wx'),永不改写索引里已有的行;同一会话只对应一个新的目标会话。 - 预检:写入前校验 codex 家目录、
state_*.sqlite、threads表、必需列、schema 版本(_sqlx_migrations不得高于已核实的 57),并用BEGIN IMMEDIATE探测库是否被占用;预检失败直接报错,不会悄悄建目录。 - 可撤销:撤销 = 删掉刚建的 rollout 文件 + 删掉刚插入的
threads行(按id+rollout_path双条件),并在交接记录里留痕。文件被外部改动过则拒绝撤销(需force)。 - 留痕:每次交接/撤销都追加到
$DSH_HOME/session-handoff/handoffs.jsonl,用于撤销与重复交接检测。
保真度(v1)
| 内容 | 是否转写 |
|---|---|
| 用户 / 助手对话文本 | ✅(模型上下文 + 界面转录两条通道都写) |
| codex 界面里显示的历史 | ✅ 用户 / 助手消息;工具活动不进界面(不伪造 codex 的工具条目,见 ADR-0003) |
| 工具调用(名称 + 参数,参数超 2 KB 截断并标注) | ✅ |
| 工具输出(单条超 8 KB 截断,标注原 DSH 会话 id) | ✅ |
| 文件变更清单(从工具参数里抽取的路径) | ✅(汇总进前言 + 计划) |
| reasoning / 思考过程 | ❌ |
| 附件图片、系统提示快照 | ❌(附件仅以绝对路径占位一行) |
交接会话的 cwd 默认等于源会话的 cwd(同一工作区继续干活);originator 标记为 dsh-session-handoff,cli_version 标记为 0.1.0-dsh-handoff,便于日后区分来源。
交接是快照:源会话后来长了怎么办
交接在写入那一刻固化源会话当时的内容,之后源会话继续加消息,目标线程不会跟着变长(目标是一个独立会话,codex 不会回读 DSH 日志)。
要拿到后来的内容,重新交接一次:每次交接都新造一个目标会话 id,不会覆盖或追加到旧目标线程(见 ADR-0001)。旧目标线程可以留着继续用,也可以 /handoff --undo 撤掉。
实测(源会话 session-e181bce7-…):
| 交接时刻 | 目标线程 | 写入规模 | 界面转录 |
|---|---|---|---|
| 17:16 | 2e61001f-… | 783 行 / 1.3 MB | 8 轮 / 32 条 |
| 18:05(源会话已变长) | 40c65068-… | 1053 行 / 1.75 MB | 11 轮 / 39 条 |
第一次交接之后才在源会话里出现的文字在 2e61001f 的转录里查不到,在重新交接出来的 40c65068 里查得到——两条通道(模型上下文 / 界面转录)都带上了新内容。
适配器范围
v1 只实现 codex(本机唯一可 resume 的 CLI agent)。适配器接缝只有四件事:读源会话 / 展平成中间表示 / 写目标记录 / 给出恢复凭据;zcode、codebuddy 等属于 v2。
反方向(外部会话 → DSH)由官方 dsh-chat-import 负责,本插件不重复实现。
测试与验收
test/ 不进发布包,这几个脚本都不需要浏览器,也不需要正在运行的 DSH:
| 脚本 | 做什么 | 写盘? |
|---|---|---|
node test/smoke.mjs [sessionId] | 读本机会话日志 → 中间表示 → rollout 行(逐条校验行类型、配对、时间戳、arguments 可解析)→ 目标库预检 → 预演 | 只读 |
node test/e2e.mjs [sessionId] | 在 node 里装一个最小 React + 渲染器,真渲染客户端注册的组件、按真实路径点击按钮(复制 id / 开始写入 / 复制恢复命令 / 撤销 / 错误态)→ 宿主路由 → 写一次真会话 → 再撤销 | 写一次并撤销 |
node test/acceptance.mjs [--dry|--undo] | 真写一次并独立校验 rollout 首行与索引行(含两条通道与 history_mode),打印 codex resume | 写一次(--dry 不写) |
node test/probe-transcript.mjs … | 用 codex 自己的 app-server 问「这条会话的转录里有什么」(轮次 + 可见条目)——界面通道的验收判据 | 只读 |
node test/probe-schema.mjs | 只读打开 state_5.sqlite:迁移版本、threads 列、必需列、触发器、本插件不认识的列及原生取值 | 只读 |
已实测(2026-09-27,codex-cli 0.157.1):
codex exec resume --json "…"返回{"type":"thread.started","thread_id":""}与turn.completed input_tokens=306867—— codex 把转写出来的三十万 token 历史读了进去并正常回答(模型上下文通道)。node test/probe-transcript.mjs返回historyMode paginated、8 轮 / 32 条可见条目,内容就是源会话里的用户/助手消息(界面转录通道)。
已知限制
- 界面转录只还原用户 / 助手消息:codex 的工具条目(
TurnItem::CommandExecution等)枚举取值只能靠猜,而 rollout 是整行反序列化的,猜错一条会让整个会话在resume时直接崩——所以工具调用只留在模型上下文里,界面里不显示。reasoning同样不写。 - 写入依赖 codex 的
TurnItem载荷形状与history_mode语义(ADR-0003)。升级 codex 后除了用probe-schema复核threads表,还要用probe-transcript复核界面转录;形状对不上时宁可停下报错,不硬写。 - 目标侧必须存在可用的 codex 状态库;若目标 agent 正在运行,登记可能遇到
SQLITE_BUSY(预检会先探测)。 - 撤销不会动 DSH 源会话,也不会动
~/.codex/session_index.jsonl(它只是名字索引,不是 resume 入口)。
许可
MIT