dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
按「闲置时长」触发的上下文压缩插件:会话上下文涨过设定的体量下限、并且安静了设定这么久,就自动压一次。 下次回到这个会话,看到的是已收敛的历史,而不是每次请求都拖着一条没人再看的长尾巴。
DSH 内置的自动压缩只认两个触发条件:请求压力 pressure 与提供方确认的上下文溢出。它不看「这个会话
多久没人用了」。本插件补的就是这一维 —— 闲置时长和上下文体量,两个都得过线才动手。
compaction seam 与 tokenMeter 的 DeepSeek Harness(在 0.1.2-alpha.3 上验证)。@deepseek-ai/* 全是 import type,编译后消失,lib/index.js 是一个自包含的 ESM 文件。git clone https://github.com/QuanhuZeYu/dsh-idle-compactor.git
dsh plugin --profile web add ./dsh-idle-compactor
# pnpm 9 往 workspace root 加依赖需要显式 -w:
dsh plugin --profile web add -w ./dsh-idle-compactor
装完重启 profile:patch 层是热加载的,bundle 挂载不是。
bundle 自带已提交的 lib/ 产物,所以无论 git 还是路径安装都不触发构建脚本,也就不需要 allowBuilds 授权。
dsh plugin add 做的事就是把 bundle 追加进 profile 的层栈,它的 cordis.patch.yml 才会生效。想手工配,就往
~/.dsh/profiles/web/package.json 里写这两处:
{
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-idle-compactor"] } },
"dependencies": { "dsh-idle-compactor": "link:../../dsh-idle-compactor" }
}
每 scanMs(默认 30 秒)扫一遍当前进程里的活体 agent,全部满足才压:
| 条件 | 依据 |
|---|---|
agent 处于 idle(没有回合、没有维护任务在跑) | agent.status |
会话最新一条日志事件距今超过 idleMs | session.events.at(-1).time(durable,重启不会把老会话误判成新会话) |
实测请求压力 ≥ thresholdTokens | ctx.tokenMeter.measure(session).totalTokens |
模型可见表面也 ≥ thresholdTokens | 同一次 measure 的 surfaceTokens |
| 上次压缩之后确实又有新内容 | 每会话 compactedAt 水位 |
| 冷却 / 失败退避已到期 | cooldownMs、retryBackoffMs |
| 该会话没有被归档 | ctx.workspaceRegistry.archivedSessionIds |
| 该会话没有还在等的后台工作 | 自己名下未结的 ctx.jobs、自己收件箱、以及它作为父会话的活体子会话 |
归档只是把会话从所有一级分组表面隐藏掉,并且故意留下活体 agent 继续跑。所以扫描必须显式点名归档 id: 隐藏一段对话不等于授权改写它给模型看的内容。归档集合还在加载中的会话同样跳过 —— 分类不明的会话一律不动。
totalTokens 是请求压力:锚在上一次成功调用的 provider usage 上,只有下一次调用才会重新定价。
surfaceTokens 才是压缩能动的那部分 —— 模型可见表面的实际内容。刚压完的会话会呈现「压力仍高达 33 万、
表面只剩 7 千」的状态;此时再压只是把已经很短的上下文又摘要一遍,白丢细节。所以两个数都要过线才动手
(这种「压力过线、表面不够」的跳过会记在 debug 级日志里)。
这也是为什么界面上的上下文数字在压缩之后可能还是很高:它显示的是压力锚点,要等下一次请求才重新定价。
执行走标准 seam ctx.compaction.compactNow(agent, signal),也就是 /compact 用的同一个入口:轮次之间的
maintenance 事务,选区与保留策略仍由压缩后端决定。插件自己不选区、不写摘要。
父代理派完子代理、自己结束回合之后,按 loop 的判定它就是 idle;而这段等待多久只取决于子代理,跟
idleMs 无关 —— 一次委托可能跑十分钟,也可能跑三小时。在等待期间压缩父会话,等于把它马上要回去接续
的那份上下文先改写掉。所以扫描还会查三本账:
ctx.jobs.list(agent),只算 ownerSession 就是本会话的那些;因此一个跑很久
的后台 pwsh 构建同样会把住它);第三本账不是重复的。continuable 子代理(以及被 send_message 唤醒的子代理)在 harness 里根本不登记
job,而子代理 driver 在「已接受 prompt」和「真的开回合」之间那一段还会报 idle;所以只要某个后代在
安静窗口之内产生过任何日志,就算作在飞的活 —— 包括仍在创建窗口里的。已经安静满 idleMs 的子会话不再
把住父会话;不在本进程里的冷子会话本来就管不住,因为没有东西会替它唤醒父会话。
waitWarnMs 只做通报不做放行:一段等待超过它,就在这一段里警告一次,然后继续等。
Agent preset 把 compaction-basic 挂在 isolate realm 里,而 dsh-web-app 关掉了 host 平面那一份,host 光纤
读不到 realm 实例。所以本 bundle 的 patch 层把 host 那一份重新启用并设 auto: false:它只应答闲置 compactNow(),
不注册自己的压力压缩,压力压缩仍完全归 preset 那份后端管。附带好处:minimal 模式的会话本来根本没有压缩
后端,现在也能被压。
写在 profile 的 cordis.patch.yml 里(后层会覆盖整份 config,要保留的键请一并重写):
- id: idle-compactor
config:
thresholdTokens: 131072 # 128K
idleMs: 600000 # 10 分钟
| 键 | 默认 | 含义 |
|---|---|---|
enabled | true | 关掉就完全不注册扫描。 |
thresholdTokens | 131072 | 上下文体量下限;要十进制 128K 就写 128000。 |
idleMs | 600000 | 闲置窗口。 |
scanMs | 30000 | 扫描间隔,决定闲置达标后最多再等多久动手。 |
includeSubagents | false | 是否也压 origin: subagent 的子会话。 |
excludeArchived | true | 跳过 workspace registry 标记为归档的会话。 |
skipAwaitingWork | true | 有未结束后台工作(自己的或活体后代的)时一律不压。 |
waitWarnMs | 10800000 | 一段等待把会话把住这么久就警告一次(每段只警告一次)。 |
cooldownMs | 60000 | 同一会话两次压缩的最小间隔。 |
retryBackoffMs | 300000 | 失败或「无可选区」后的退避。 |
maxPerScan | 1 | 单轮最多压几个会话,限制并发摘要请求。 |
dryRun | false | 只记日志不真压。 |
未知键名或越界值在装载时报错,不会静默兜底。
thresholdTokens 的会话测一次就转入下一轮,不会每 tick 重复测量。日志永远不被重写。一次压缩追加 compaction/start、compaction/summary、compaction/end,再用一条带
surfaceOp: replace 的 user/message 替换选中区段;shadowedSeqs 记录了被移出模型可见表面的每一条事件,
原文仍在会话日志里,消费方可以读回。存储索引和归档标记都不变。
includeSubagents 默认关掉也是
同一个考虑:后台子会话本来就归父会话的生命周期管。agent.status === "idle",且 compactNow 是轮次之间的维护任务。
它的另一半 —— 回合之间但还在等子代理 —— 由 skipAwaitingWork 兜住:等待没有上限的父会话,只会在等待
结束之后才被压。settings.section 是下一步。npm install # typescript + vitest 作为 devDependency
npm run link-dsh -- <dsh-checkout 路径> # 从 checkout 解析 harness 包
npm run build # src/index.ts -> lib/index.js
npm test # 12 个集成测试
npm run typecheck:tests # 连测试一起类型检查(tsconfig.check.json)
源码依赖的多数 @deepseek-ai/* 包并不在公共 registry 上,所以 scripts/link-checkout.mjs 是把一个本地
DeepSeek Harness checkout 链接进本项目自己的(已被 gitignore 的)node_modules。它不会往那个 checkout 里写
任何东西;同级存在多个 checkout 时它会停下来让你指定,不会替你猜版本。npm install 不是必需的:没有它
脚本会借用 checkout 自带的 typescript 和 vitest,而 npm scripts 是按文件路径而不是按 bin 垫片调用它们的,
所以借来的和装的一样能用。
tests/idle-compactor.spec.ts 跑在真实 agent loop + 真实会话日志 + 真实 tokenMeter + 真实
BasicCompactionEngine(只脚本化它的 summarize)之上,断言的是日志里出现的 compaction/summary 事件,
不是 mock 调用次数。默认测的就是已构建产物 —— 和 profile 加载的是同一份;设 DSH_IDLE_COMPACTOR_BUNDLE
可以指向别的构建。
覆盖:闲置超阈值恰好压一次且实测上下文下降;同一安静期不重复压;有新活动后重新武装;未达阈值不动;
压力过线但表面已低于阈值时不动;归档会话不动(归档集合还在加载时同样不动);dryRun 不写任何东西;
坏配置直接抛。等待这块正反都测:名下有未结 job 的、子会话正在跑回合的,都不压,等工作结束后才压;
而「无关会话在跑」和「job 属于别的会话」两种情况照点压 —— 说明起作用的是父子关联与 job 归属,不是
场上有没有别的动静。
src/index.ts 插件本体(纯类型导入,零运行时依赖)
lib/ 已提交的构建产物 —— profile 实际加载的东西
cordis.patch.yml bundle 层:插件行 + host 平面压缩后端行
tests/ 在真实组装的 harness 上跑的集成测试
scripts/link-checkout.mjs 仅开发用:把 checkout 的包只读链接进来
vitest.config.mjs 独立测试根,不依赖任何 checkout 配置
tsconfig.json 只输出可擦除语法的 ESM,声明文件进 lib/types
tsconfig.check.json 测试平面类型检查:src + specs,带 Node 环境类型
MIT,见 LICENSE。