⚠ 2026-09-28 合并记录:本插件现在也管"会话那一摊"
起因是一次「合插件」的需求 ——
dsh-session-switch(建新会话、发交接包、写 pending、供界面切过去) 整个搬进了本包,作为switch-session.js与switch-store.js两个模块:
- 内部握手改成直调(
runSwitchDirect()),不再是POST http://127.0.0.1:3080/api/session/switch那条网络往返;- 那四条 HTTP 路由照旧注册(
/api/session/switch|pending|ack|health),自检、手工 curl、老调用方都还认;- 状态目录不变:
~/.dsh/session-switch/(DSH_SESSION_SWITCH_DIR可覆盖);- 会话那一摊要的七个宿主服务改成运行时注入:缺谁只让这一摊缺席,档案那一摊照常(合并前是"整包卡 pending");
- 自检多了一个口:
DSH_SESSION_SWITCH_DIR=/tmp/ss node index.js --switch-selftest(原样 19 项)。撤下前的完整副本在
plugin-src/归档-合并前-20260927-232845/。
dsh-auto-handoff
DSH(DeepSeek Harness)插件:上下文占用到阈值时自动交接。
到阈值那一刻,它做这两件事:
- 拼一份限长的交接包(默认 1200 字,只有它进新会话),请会话那一摊
(2026-09-28 已并入本包的
switch-session.js)开新会话、把交接包当第一条消息送进去; - 记一笔幂等指纹到自己状态目录下的
handoff-fingerprints.json(跨重启有效,封顶 50 条)。
⚠️ 2026-10-16 起:默认 不写档案
以前它每交接一次就往日记里塞一条"上下文到阈值,插件自动做了一次交接"的流水账(外加状态一节、 生长记录一行),一天能塞好几条、删了又长;接手那边还会被提示"再补一条"。现在默认
archiveOnHandoff: false:
- 交接只交接 —— 一个档案字节都不碰;
- 幂等指纹不再寄生在日记条目上,改落
~/.dsh/session-switch/handoff-fingerprints.json;- 日记/状态/生长记录只在人点「日记」或跑「收尾」时才更新;
- 想回到旧行为(写日记 + 状态一节 + 生长记录一行)就把
archiveOnHandoff设成true。到阈值时的默认动作也变了:
autoTrigger: true时它不自己切会话,而是给 AI 塞一条【自动收尾提示】(先做收尾,做完写fire-now举手换会话)—— 免得在人正看着屏幕的时候把对话切走。
本插件不自己建会话。 建会话、切界面、归档原会话都是会话那一摊(已并入本包的 switch-session.js)的活;
两者只靠一个 HTTP 调用握手,叫不动就走降级(把完整待接写到约定位置,由浏览器那半边自己切)。
它管什么 / 不管什么
| 本插件 | dsh-session-switch |
|
|---|---|---|
| 管什么 | 交接包 + 待接记录(archiveOnHandoff: true 时才写日记 / 状态 / 生长记录) |
会话:建、切、归档 |
| 触发 | 上下文到阈值自动;或手动打演练关键词 | 收到 /api/session/switch 才动 |
记忆的看 / 编 / 导入 / 导出是另一个插件(记忆面板)的事,本插件不碰。
安装
- 把这个目录放进你的 DSH 插件源目录(本机示例:
~/.dsh/plugin-src/dsh-auto-handoff)。 - 把
cordis.patch.example.yml里那一条按你的情况改好,插进 profile 的cordis.patch.yml;或者用 Plugin Manager 的install_bundle把它当 bundle 装进 profile。 - 重启宿主。实测:装 bundle、改 bundle 内代码、改 config 都认重启(HMR 不可靠)。
唯一的必填项是档案根:留空则依次尝试
config.root → 环境变量 DSH_MEMORY_ROOT → ~/.dsh/memory → 从当前目录逐级向上找
认根靠特征文件:身份.md + 日记.md + 生长\状态.md 三者齐备才算一份档案;
认不到就不动作(绝不拿一个猜出来的根去写真档案)。
配置
| 键 | 默认 | 说明 |
|---|---|---|
root |
'' |
档案根;留空按上面的顺序找 |
triggerRatio |
0.65 |
占用到这个比例触发交接 |
minTurns |
3 |
会话太短不触发 |
cooldownMs |
180000 |
冷却 3 分钟;过了冷却还能再交一次(不是"一会话一次") |
handoffMaxChars |
1200 |
交接包上限 |
dryRun |
true |
首启只算不写:把"本来会写什么"记日志,一个字节都不落盘 |
archiveOnHandoff |
false |
交接要不要写档案:false = 只出交接包(默认、推荐);true = 还写日记一条 + 状态一节 + 生长记录一行 |
autoTrigger |
false |
true = 到阈值不自己切会话,而是给 AI 塞一条「该收尾了」的提示(见上文 2026-10-16 说明) |
device |
【PC】 |
日记标题与状态节里的设备标签(archiveOnHandoff: true 时才用得上) |
drillKeyword |
#交接演练 |
手动演练关键词(只在用户消息里整句匹配) |
gatewayBase |
http://127.0.0.1:3080 |
dsh-session-switch 的地址 |
legacyDevice / legacyBoundary |
'' |
旧条目设备归属判据;没配就完全不推断 |
占用率的取数口径:优先会话投影 contextPressure(跟页面上那个百分比同源),
拿不到再退回 ctx.tokenMeter.measure() ÷ request/context().contextWindow;
两个都拿不到就返回"不动作" —— 不许瞎猜分母。
手动演练
在对话里整句打 #交接演练:
- 只写演练副本根(
sandboxRoot),真档案一个字节不碰; - 没配
sandboxRoot就只做一次干跑,日志里能看到"本来会写什么"; sandboxRoot落在真档案根里面时硬拒(那样"演练"就是真写)。
Host 侧路由
前缀 /api/handoff/,五条各自注册一次(requestBody 只能是 buffered,写别的会静默失败)。
| 路由 | 方法 | 用途 |
|---|---|---|
pending |
GET | 给客户端轮询的待接(完整交接包 + sourceSession + newSessionId) |
take |
GET/POST | 与 pending 同义(取一份待接) |
ack |
POST | 客户端切完回报,标记"已处理" |
health |
GET | 一眼看配置与管道:阈值 / 冷却 / 上限 / 档案根 / gateway |
status |
GET | 最近 20 行诊断日志 + 上次判定 |
draft-report |
POST | 客户端上报输入框草稿状态("别打断正在打字的人"用) |
fire |
POST | AI 举手请求换会话(只认「收尾」口令 + 每小时 ≤2/每天 ≤8) |
fire-ack |
POST | 客户端替用户打完「换会话」后回来清标志 |
客户端半边(client.js)只做"轮询 + 调导航/归档":不注册 slot、不写 DOM,
出错只 console,绝不抛 —— 客户端入口抛错的代价是"整个页面打不开"。
「收尾后自动换会话」怎么举手(文件信令)
POST /api/handoff/fire 那条路要过宿主鉴权(实测在本机回 401,本机 curl 也一样),
所以 AI 举手改用写一个文件:
: > ~/.dsh/session-switch/fire-now # 路径 = `<状态目录>/fire-now`,内容随便
- 插件每 1 秒看一次这个文件 → 删掉 → 设
fireRequested; - 客户端本来就在轮询
/pending,看到fire就替用户打一条「换会话」,于是走既有那条触发路径 (建新会话、送交接包、切界面),打完回fire-ack清标志; - 闸没松:仍然受 每小时 ≤2/每天 ≤8 限制;
fire那条 HTTP 路由还额外要求"最近一条用户消息就是「收尾」"。 - 典型用法:AI 跑完收尾(日记 → 备份 → 体检)写一下这个文件,人就自动被带到新会话。
写档案的护栏(只在 archiveOnHandoff: true 时用得上)
默认不写档案(见上文),所以这一节只在把 archiveOnHandoff 打开时生效。写盘一律四条:
- 改前备份(整份复制到
<根>\归档-旧版本与记录\写入备份\,.bak-YYYYMMDD-HHMMSS); - 只改定位处(序号 +1、
total_count+1、last_updated、unfinished,别的一字不动); - 写完复核(读回来逐字比对 + "旧内容只能是新内容的前缀/只差一处"的形状复核);
- 外部改动拒写(指纹比对:我们读过之后被改过 → 拒写)。
外加:
- 幂等:同一份交接重复跑不追加第二条 ——
archiveOnHandoff: false时指纹写在<状态目录>/handoff-fingerprints.json,true时写在日记条目里的注释标记; - 写临时文件再 rename,不赌覆盖式写入;
- 行尾/编码:全文件 CRLF、UTF-8 无 BOM。
自检
node index.js --selftest # 在临时目录造假档案,跑全套回归(每项打实际输出)
node test-handoff-sandbox.mjs # 同一套,只打结论
node test-handoff-sandbox.mjs --verbose
自检口在模块层(不是 apply 里)—— 独立跑 node index.js 时 cordis 不会调
apply,写在里面会一声不吭。
已知边界
- 它不保证"交接的内容是这一轮的全文":交接包是压缩后的接手起点,不是复制品 —— 新会话一开就吃满上下文等于把坑搬过去。
- 日记正文里的
event_description由"最近说过的话 + 动过的工具"拼成; 没给userMood时那一栏会照实留白,不替你编心情。 - 跨设备没做:手机侧没有同一套 Web 客户端,切会话要另想办法。
legacyDevice/legacyBoundary只是判据:本插件不会去改写任何旧条目。
License
MIT
No comments yet. Be the first to write one.