dsh-compact-agents
DeepSeek Harness 会话上下文强制压缩插件(模型可调用的 compact_agents)
强制压缩忽略自动阈值 · 覆盖进程内所有活会话(主会话 / 普通子代理 / AgentTeams 成员一视同仁) · 没有子代理时主会话也能压自己 · 忙的目标自动排队、本轮结束立即补压 · 逐目标回报被遮蔽节点数与估算 token 数 · 只有顶层 agent 能扫描、子代理越权被拒 · 工具调用串行不并发 · 压缩过程在对话区可见 · 被输出上限截断时自动续写 · 压缩阈值等参数可在设置里自成一页直接改 · 零网络、零依赖、浏览器 half 手写无构建步骤
v0.8.1:参数表单回到"设置里自成一页" —— 设置 → 「压缩与自动续写」,与「内置插件」同级。起因是 DSH 0.1.6 把插件配置搬去侧栏「插件」页、并退役了老槽位 settings.plugin.item,我们那张卡片先是凭空消失(slots.inject 对没人声明的槽位是静默的),随后即便把表单挂进插件页,也得"进插件页 → 找到本包 → 进详情"才能改参数。现在用官方的 settings.section 槽位自成一页,同时保留插件页与旧设置页两处(三处共用同一份正文与同一个控制器)。上一版还顺带修掉了第二处静默失败:只在 dsh.profile.bundles 里、不在 profile dependencies 里的包会被插件页整条过滤。详见设计说明 §8.5 与 §8.7。
English | 中文
功能总览
| 能力 | 入口 / 参数 | 说明 |
|---|---|---|
| 强制压缩(忽略自动阈值) | compact_agents(scope) |
走 ctx.compaction.compactNow,契约原文是*"Explicitly compact useful history even below automatic pressure thresholds"*;自动压缩要等阈值,本工具不等 |
| 覆盖所有活会话 | scope: "others"(默认) / "all" |
覆盖范围是 ctx.agents.list()——主会话、普通子代理、AgentTeams 成员一视同仁,不是只挑团队成员 |
| 主会话压自己 | scope: "self" |
没有子代理时也有效:调用者永远在 turn 中,必然走排队路径,本轮结束自动补压 |
| 指定目标 | scope: "ids" + ids: [...] |
只压列出的会话;找不到的 id 单独回报,不静默吞掉 |
| 忙则排队 | whenBusy: "queue"(默认) |
目标正在跑 turn 时不放弃:监听其 agent/status → idle,这一轮一结束就补压;"skip" 则直接报 busy 不动它 |
| 逐目标回报 | 返回值 results[] |
每个目标给出 compacted / queued / noop / busy / error,以及被遮蔽的节点数与估算 token 数 |
| 越权保护 | — | 只有顶层 agent 能扫描他人;子代理只能 scope: "self",成员无法互压或压队长 |
| 串行调度 | — | 注册为 fail-closed 的 exclusive,一次扫描不会和另一个可能压同一会话的调用并行;超时 30 分钟(排队部分不计入,它在 turn 之后跑) |
| 自动压缩阈值(配套) | compaction-basic 配置 |
本插件不改自动策略;安装脚本顺带核对 thresholdRatio(DSH 默认 0.8×1M=800K 等于永不触发;默认 0.35,即 350K 触发) |
| 压缩过程在对话区可见 | 默认开启;notice: false 关闭 |
订阅 session/event,compaction/start 一落地就往会话尾追加一条插件来源的 user/message,客户端渲染成「上下文注入 · dsh-compact-agents」折叠行:压缩中显示 正在压缩上下文…(当前 213,400 tokens)· 触发线 ×0.35,结束时显示 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点。compaction/start 是在摘要模型调用之前写的,这段提示正好盖住原本什么都看不见的等待。提示里会报出本会话实际生效的触发线;万一热同步没成功,折叠行与正文会直接点明两个值与"新开一条对话才生效" |
| 被输出上限截断时自动续写 | 默认开启;maxAutoContinues(默认 2,0/false 关闭) |
一轮以 turn/end{reason: 'max-tokens'} 结束时,替用户发一句"继续"(agent.followup,与人在界面上发言同一条路),让对话自己走下去。连续次数有上限,任一轮正常结束即清零,避免无止境烧 token |
| 设置界面里能改 | 默认开启;settings: false 关闭 |
注册 settings 命名空间 compact-agents,浏览器 half 在侧栏「插件」页(DSH ≤ 0.1.5 是「设置 → 插件」)里提供表单:压缩触发阈值、保留比例、受控阶段输出预算、压缩提示开关、自动续写次数,五项都能在界面上改,不用再去编辑 preset 的 YAML |
为什么需要它
DSH 的手动压缩入口只有一个人机命令 /compact(@deepseek-ai/dsh-command-compact 用 ctx.commands.register 注册,只服务交互式 UI 适配器)。于是:
- headless 的子代理 / AgentTeams 成员没有命令面,执行不了
/compact; - 队长也没有任何工具能替成员压缩——
packages/compaction/下没有任何registerTool; - 而自动压缩(
compaction-basic)是按阈值触发的:阈值没到就不压。
结果就是"会话越跑越贵"过去只能靠换人(退役成员、新建成员)解决。本插件补上这个缺失的模型侧入口。
真实教训:一个成员会话曾以每次调用重发 约 40 万 tokens 的上下文跑到 348 次调用,累计 1.4 亿 cacheRead、¥10.87;而它只是"没人能替它压缩"。
安装
需求:Node.js ≥ 20 + DeepSeek Harness(带
agent-presets的版本)。
一键安装(推荐)
git clone https://github.com/zhuto666/dsh-compact-agents.git
cd dsh-compact-agents
node scripts/install.mjs --dry-run # 先预览要改什么(不改盘)
node scripts/install.mjs # 确认后执行(profile 默认 web,可用 --profile 指定)
脚本做这几件事,且幂等:
- 建三个目录联接(junction),让插件能解析到
@deepseek-ai/dsh-tools、@deepseek-ai/cordis与@deepseek-ai/schemastery——本项目刻意零依赖(不装 node_modules),Node 会把 junction 解析到真实路径,因此拿到的是和宿主同一个模块实例,没有双实例问题; - 往 preset 的
compaction隔离组里追加一行挂载(compact_agents工具、压缩提示、自动续写都靠它):
- id: compact-agents
name: '/absolute/path/to/dsh-compact-agents/index.js' # 安装脚本会自动填成你的真实绝对路径
把本插件登记进宿主组成(浏览器 half、那张配置表单靠它):在
<DSH_HOME>/profiles/<profile>/node_modules/下建一个指向本仓库的dsh-compact-agents联接,并把dsh-compact-agents加进该 profilepackage.json的dsh.profile.bundles—— 宿主 Loader 于是多出一行compact-agents-client-host(由本包的dsh.bundle.patch→cordis.patch.yml注入,入口client-host.js)。同时把它写进同一个package.json的dependencies(link:<本仓库绝对路径>):新版插件页只列installed || optional || error的 bundle,而installed的判据就是包名在不在dependencies里 —— 少了这一步,插件页里会整条看不到它,且毫无报错。profile 用--profile <name>指定,默认web;改这个package.json前会留一份<package.json>.bak-compact-agents。这一步会先试官方通道
dsh plugin add <本仓库绝对路径> --profile <profile>(它把依赖、bundle 登记与实际安装一次做完),失败或加了--no-cli时回落到上面的手写登记。
装完请重启一次 dsh:宿主组成变了(profile 多了一个 bundle),重启后侧栏「插件」页里才会出现 dsh-compact-agents 与那张配置表单(DSH ≤ 0.1.5 在「设置 → 插件」里)。之后只改 preset 里的参数就不必重启(见「生效」)。
自动发现 $DSH_HOME/.agent-presets/*/agent.cordis.yml 与 $DSH_HOME/profiles/*/node_modules/@linxin666/*/presets/*/agent.cordis.yml;也可以用 --preset <file> 指定要处理的 preset。改文件前会留 .bak 备份,没有 compaction 组的 preset 直接跳过。
DSH 检出位置同样是自动探测的,脚本和文档里不写死任何盘符,依次尝试:
--dsh <checkout>/$DSH_CHECKOUT/$DSH_HARNESS;- 本项目
node_modules里已有的联接目标(装过一次就连带记下了检出在哪儿); - PATH 上
dsh启动器的真实入口(包管理器生成的 shim 里写着dsh在哪儿)—— 全新克隆、什么线索都没有时,靠的就是这一条; - 各 profile 的
node_modules; - 家目录下的常见克隆位置。
全都落空才报错,并提示用 --dsh 指定。
那行路径不是写死的,是安装时算出来的 —— install.mjs 用自己所在目录推导 PLUGIN_ENTRY,所以:
- 你在哪儿克隆/放这个项目,那行就指向哪儿;
- 项目被移动或改名后,重跑一次
install.mjs就会自动改正:发现已有行指向一个不存在的路径时直接repaired;若旧路径仍然有效(例如另存了一份副本),则只WARN不动手,加--force才重新指向。
node scripts/install.mjs --dry-run # 预览(不改盘)
node scripts/install.mjs # 执行;自动修复失效路径
node scripts/install.mjs --force # 旧路径还有效时也强制重新指向
为什么不能像普通包那样只写包名? 这不是偷懒,是 DSH 的既定语义:preset 行里的裸包名是从 harness 安装位置解析的(
agent-presets/src/mount.ts的PresetTree.import注释原文 "a package name resolves from the harness base"),不是从用户目录;装在工作区/用户目录的包根本解析不到。所以第三方插件在这里只有两条路——写绝对路径,或把插件文件放进 preset 目录跟着走。本项目选前者:单一真源,不给每个 preset 留副本。
为什么要挂两处(宿主组成 + preset)
两个挂载点缺一不可,各管一半:
| 挂载点 | 载体 | 负责 |
|---|---|---|
| preset 行 | <DSH_HOME>/.agent-presets/*/agent.cordis.yml 里 compaction 组的 compact-agents 行(绝对路径指向本仓库 index.js) |
compact_agents 工具、压缩进度提示、被输出上限截断后的自动续写。必须待在 compaction realm 内:cordis 的隔离按服务名生效,realm 外解析不到 ctx.compaction |
| 宿主组成里的 bundle 行 | profile 的 dsh.profile.bundles 登记本包 → 本包 dsh.bundle.patch 指向 cordis.patch.yml → 插入 id: compact-agents-client-host / name: 'dsh-compact-agents/client-host'(入口 client-host.js) |
让 DSH 的客户端模块表扫到本包的 dsh.client 声明(从而把 lib/client.js 下发给浏览器),并在宿主根上注册 settings 命名空间 |
只挂 preset 是不够的:DSH 的 ClientModuleRegistry(packages/client/modules/src/index.ts) 只遍历宿主 Loader 的 entries 来决定给浏览器下发哪些客户端 bundle —— 它监听 internal/plugin 的那段里有一行 const entryName = fiber.entry?.options.name; if (entryName === undefined) return,注释明说"fiber.entry 为空的是子插件或手动挂载",直接丢弃;构造时也只做 for (const entry of ctx.loader.entries())。而 preset 里的行是 agent-presets 用 internal.import 手动挂载的、不是 loader 行。所以只挂在 preset 里,浏览器 half 永远不会被下发,症状是界面上既没有命名空间也没有表单、且毫无报错。根因与源码位置见设计说明 §8.4。
登记成 bundle 也还不够(v0.8.0 起):新版插件页(packages/client/ui-plugin-manager)只列 pkg.installed || pkg.optional || pkg.error 的 bundle,而 installed 的判据是 packages/boot/plugin-manager/src/index.ts 的 listBundles() 里那句 dependencies.includes(name) —— 只写 dsh.profile.bundles、不写 dependencies 的包会被整条过滤掉,同样毫无报错。install.mjs 现在两处都写;只写 bundle 的旧安装会表现为"插件页里根本没有这个插件"。见设计说明 §8.5。
client-host.js 这个根入口刻意 inject = []:宿主根上没有 compaction 服务(它由 preset realm 内的 compaction-basic 提供),声明依赖只会让这一行永远 pending。它只做两件事:让客户端模块表扫到本包、在宿主根上注册 settings 命名空间;两处入口都调用 registerSettings,靠模块级缓存保证进程级只注册一次(真实的 SettingsProvider.register 对重复命名空间会抛错)。
这不是自创形态:已装的站外插件
@a9i5k4/dsh-auto-memory同样声明了dsh.bundle.patch、dsh.client与exports['./client'],patch 内容就是- insert: [ { id, name } ]。本插件采用与它相同的形态。
生效
改动落在哪一层,决定它怎么生效:
| 改了什么 | 生效方式 |
|---|---|
宿主组成(首次安装新增的 bundle 行、client-host.js、cordis.patch.yml、package.json 的 dsh.* 声明) |
必须重启 dsh —— 重启后「插件」页里才会出现它,设置页(旧版)里才会出现那张卡片 |
| preset 的压缩阈值 / 保留比例 | 保存即生效:本插件把新值热同步进正在运行的会话(下一次步边界就用新值),同时写进 preset 文件供新会话读取 |
preset 的受控阶段输出预算(tool-bootstrap)与挂载行 |
新开一条对话即可,不必重启 |
插件本体 .js |
必须重启 dsh(ESM 模块缓存,理由见「开发者本地调试」) |
preset 改动靠 standing mount 的文件戳热重载(戳 = stat 的 mtimeMs + size):戳变了,下一条新会话重新挂载一代,就带上工具;已经 composed 的会话因 agent-preset/locked 拿不到,属预期。
但阈值与保留比例不必等那一步:本插件订阅设置面的保存事件,读回 preset 里的新值后直接写进正在运行的 compaction-basic 实例 —— 该实例的 config 是普通自有属性(对象本身被冻结,但引用可换),而压力判定每次调用都重新读它,所以下一次步边界就按新阈值判,不用新开对话、更不用重启。bootstrapMaxTokens 属于另一个插件(tool-bootstrap.mjs 在 apply() 里把值捕获进闭包),改不动,仍然只对新会话生效。
压缩提示里会报出本会话实际生效的触发线;只有热同步真的进不去时(配置形状变了/写不进去),才会多一行说明两个值与出路:
⚠️ preset 文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。
行配置 livePresetParams: false 可关掉热同步(那就回到"新会话生效"的老行为)。
它还会自我反证(v0.7.1):光"写进去了"不等于"引擎吃了"。热同步之后,插件拿策略自己决定的那次压缩反过来验算 —— 达线判定是 token 数 >= 窗口 × 阈值,所以把阈值从 0.2 抬到 0.5 之后,若 1M 窗口下仍在 250K 就压,说明引擎还按 0.2 判。这时它不再自称生效,而是按 0.2 报触发线,并给出"新开一条对话"这条正路。手动压缩(/compact,或 compact_agents 工具触发的那次)在任意 token 数上都可能发生,一律不当作证据,免得冤枉引擎。
想让现有的成员也被压,就别重启 DSH —— 重启会丢掉所有成员/子代理会话(
ctx.agents.list()只覆盖活着的会话),那就没东西可压了。新开一条对话不影响它们。于是首次安装有个次序问题:"看到设置卡片"和"别丢成员会话"不能同时满足 —— 先压完再重启,或者重启后重新开成员。
compact_agents工具本身不依赖这次重启:preset 行装好,新开一条对话就能用。
校验安装
node scripts/validate-presets.mjs
逐份报告挂载行位置、引用的文件是否存在、以及 compaction-basic 的 thresholdRatio / retainRatio。期望输出:
.agent-presets/liangshen/agent.cordis.yml
rows = compaction-basic,command-compact,compact-agents,tool-result-pruner
thresholdRatio = 0.35 retainRatio = 0.05
compact-agents -> /absolute/path/to/dsh-compact-agents/index.js (存在)
README.md: 版本徽章 0.8.0 == package.json
profile web: dependencies["dsh-compact-agents"] = link:E:/dsh-compact-agents(也在 dsh.profile.bundles 里)
ALL OK (4 preset mounted)
最后一行是 v0.8.0 新加的守卫:登记在 dsh.profile.bundles 里却不在 dependencies 里会直接判失败并给出修法 —— 那正是"插件页里看不见我们"的静默状态(原因见上一节)。随包发行的 preset(路径里带 node_modules 的那些)被它自己的插件升级重写、丢掉我们的挂载行时,也会在这里失败,并提示重跑 node scripts/install.mjs。
表单将显示哪些初值,可以用只读脚本核对(一个文件都不写):
node scripts/inspect-presets.mjs
更新 / 卸载
git -C dsh-compact-agents pull # 更新:拉取后重新执行 install.mjs(幂等)
node scripts/uninstall.mjs --dry-run # 卸载:先预览
node scripts/uninstall.mjs # 移除挂载行 + 摘掉 profile bundle 登记 + 删除自己建的 junction
卸载只删自己加的东西:preset 的注释、!!js 表达式、其它行一律不动(实测安装→卸载后文件逐字节回到原状),profile package.json 里的 bundle 登记同样按行摘除、保留原排版。插件目录与 .bak / .bak-compact-agents 备份不删,自行处理。卸载同样改了宿主组成,所以重启 dsh 后设置卡片才会消失。
开发者本地调试
node scripts/integration-test.mjs # 真机加载测试(真 cordis Context + 真 ToolRuntime)
node scripts/deferred-test.mjs # 忙→排队→idle 补压 的行为测试(假 ctx)
node scripts/selftest.mjs # 模块导入 + defineTool 规格自检
改完 index.js 后必须重启 dsh 才能真正生效 —— 这一点很容易踩坑:
- Node 的 ESM 模块缓存按 URL 命中,preset 重新挂载不会清它(DSH 自己的 HMR 插件是靠显式清
internal.loadCache才能热重载的,而它在 profile 里默认disabled); - 所以"新开一条对话"只会重新挂载 preset,插件的模块本身仍是进程里已缓存的旧代码;
scripts/*.mjs是每次直接执行的脚本,不受影响,改完立即是新的。
只有改了 preset 时,新开对话就够了;改了插件
.js(含client-host.js/settings.js)或宿主组成相关的文件(cordis.patch.yml、package.json的dsh.*声明)都必须重启dsh。
用法
模型侧调用(不是人机命令):
compact_agents(scope = "others" | "all" | "self" | "ids", ids?: string[], whenBusy = "queue" | "skip")
| scope | 含义 |
|---|---|
others(默认) |
除调用者以外的所有活会话 |
all |
所有活会话,含调用者自己 |
self |
只压调用者自己 —— 没有子代理时也有效 |
ids |
只压 ids 里列出的会话 |
典型说法:
- 「把其他会话都压一遍」→
scope: "others" - 「我这条对话也一起压」→
scope: "all"(你自己会被排队,本轮结束补压) - 「只压我自己」→
scope: "self"
模型没调用它? 工具描述里已写明"用户要求压缩上下文时立即调用、不要反问",但仍可能遇到模型选择先确认一下。最稳的说法是把工具名说出来:
调用 compact_agents,scope=all,把所有会话压一遍工具本身与模型行为无关——只要它在工具清单里,点名调用必定执行。
返回每个目标一行,例如:
compact_agents: 3 compacted, 1 queued, 0 skipped, 0 failed (of 4 selected).
- sess_ab12: compacted, ~213,400 → ~49,800 tokens — shadowed surface 12-107
- sess_cd34: compacted, ~52,100 → ~9,040 tokens — shadowed surface 3-33
- sess_ef56: noop, ~0 tokens shadowed — nothing safely compactable (empty session, or one oversized retained unit)
- sess_gh78: queued — mid-turn; queued and will be compacted as soon as it goes idle
beforeTokens / afterTokens 是压缩前后用 ctx.tokenMeter 实测的表面估算;测不到时为 -1。
对话区的压缩提示
工具之外,任何压缩(包括阈值触发的自动压缩)都会在对话区留下一条可见提示 —— 形态就是框架自己 注入上下文时用的那种折叠行:
▸ 上下文注入 · dsh-compact-agents · 正在压缩上下文…(当前 213,400 tokens) · 触发线 ×0.35
▸ 上下文注入 · dsh-compact-agents · 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点
compaction/start 是在摘要模型调用之前写入会话日志的,所以第一条提示正好出现在原本那段
什么都看不见的等待里。行配置 notice: false 可关闭(工具不受影响)。
折叠行末尾那个 触发线 ×0.35 是本会话实际生效的值。正常情况下它总是跟 preset 文件一致 ——
保存设置时插件会把新值热同步进运行中的会话;只有热同步进不去时才会多一行:
⚠️ preset 文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。
被输出上限截断时自动续写
一轮以 turn/end{reason: {kind: 'max-tokens'}} 结束时(DeepSeek 的 finish_reason: 'length'),
插件会替用户发一句"继续",让对话自己走下去,而不是停在那里等人手动发:
▸ 上下文注入 · dsh-compact-agents · 上一轮被输出上限截断,已自动续写(1/2)
继续 ← 插件以用户身份发出的(和用户手打的一样)
- 次数上限由行配置
maxAutoContinues控制,默认 2;0或false关闭。用满后不再续写, 改为提示"已停止自动续写",避免"截断 → 续写 → 又截断"无止境烧 token。 - 任一轮正常结束即清零,所以额度是"连续"次数,不会长期耗尽。
- 发出的是标准
user/message(source.kind: 'user'+ 冻结 + 唯一 id),走的就是人在界面上发消息 的同一条路(agent.followup)—— 不是往会话表面塞一条不唤醒模型的消息。
为什么需要它:压缩后 preset 常会把下一个请求的输出预算压到很小的窗口来"重新锚定"。 若模型开着高推理,思考 token 与正文共享这份预算,很容易整份被思考吃光 → 正文 0 字被判截断。 详见设计说明 §7。
在界面里改这些参数
首选:设置里自成一页。 打开 设置 → 侧栏里找 「压缩与自动续写」 —— 它和「通用设置 / 模型 / 内置插件 / Agent 预设」是同级的一项,点进去就是全部五个字段。这一处与 DSH 版本无关(0.1.5 与 0.1.6 都在),也不要求你先进插件页找到本包。
第二处:侧栏「插件」页里本包的详情页(DSH ≥ 0.1.6)。上游把插件配置搬到了这里,所以我们也在:侧栏「插件」→ 找到 dsh-compact-agents → 点进详情,描述与组件行之间是同一份表单(页自己带标题与面包屑,表单只有字段本身);列表里那一行还带一句摘要,写着当前阈值与保留比例。
第三处:老设置页(DSH ≤ 0.1.5):设置 → 插件 → 可配置 里一张可折叠的「压缩与自动续写」卡片。

上图是 0.1.5 及更早的界面。浏览器 half 现在同时注册三处(
settings.section自成一页、plugins.bundle.config键 = 包名、settings.plugin.item键 = settings 命名空间),正文只有一份、控制器只有一个,三处不会漂移;注册各自独立,一处失败另两处照旧。旧图暂未替换 —— 取新截图需要重启 GUI。
这个表单是纯参数表单:本插件在界面上没有自己的按钮 —— 压缩与自动续写都是自动发生的, 框架自带的「重置」只是还原入口,不会触发压缩。 DSH 里唯一的手动压缩入口是自带的人机命令
/compact(不是本插件);本插件提供的是模型侧工具compact_agents,由模型调用,不是给人点的。
界面按生效时机分成三组,字段名就是卡片上那几个字:
| 字段 | 含义 | 生效时机 |
|---|---|---|
| 压缩提示播报 | 是否在对话区播报「正在压缩上下文…/压缩完成」,以及遮蔽节点数与估算 token 数 | 立即生效 |
| 自动续写次数上限 | 被输出上限截断时最多自动发几次"继续"(0 = 关闭) | 立即生效 |
| 压缩触发阈值比例 | 0.35 = 上下文用到 350K 就自动压缩 |
保存即热同步(同时写进预设,供之后新建的会话) |
| 压缩后保留比例 | 压缩后按该比例留下最近的历史 | 保存即热同步(同上) |
| 受控阶段输出预算 | 每次压缩后会重新进入的"受控阶段"里,单个请求的输出预算 | 新建会话生效 |
表单里还会标出哪些字段是你覆盖过的(可以单独"重置"回 preset 里的值)。
旧版那张卡片长什么样,见上面那张标注图;图里的编号与右侧图例一一对应。docs/images 记录了这批图的来源与做法(真实界面截图 + 标注,不含任何个人信息),将来若要补别的截图,命名与插入位置也在那里写清。
表单(三处中的任何一处)能出现的前提是宿主组成里有那一行(本包作为 profile bundle 被登记、进而插进宿主 Loader):只在 preset 里挂载的话,浏览器根本收不到 lib/client.js,症状是界面上既没有命名空间也没有表单、且毫无报错。另外它还得在 profile 的 dependencies 里,否则新版插件页会把它整条过滤掉。所以首次安装后、以及任何改动宿主组成之后,都要重启 dsh(根因见设计说明 §8.4 与 §8.5)。
设计要点(为什么分成三组):
- 立即生效的那两个值是本插件自己的,会话事件发生时才读,所以改完立刻算数。
- 中间那两个(阈值 / 保留比例)写的是 preset 文件,但保存时会热同步给正在运行的会话: 新值直接进运行中实例,下一次步边界就用;热同步万一进不去,压缩提示里会自己点明"本会话仍是旧值、 新开一条对话才生效",不会假装成功(见对话区的压缩提示)。
- 最后一个(受控阶段输出预算)属于 preset 里的另一个插件(
tool-bootstrap),插件没法替它改 运行时策略,所以只改 preset 文件本身:preset 的挂载会记录文件 stamp,stamp 变了就给之后新建的 会话开新一代 —— 不用重启 DSH,但已经在跑的会话不受影响。写盘前会落一份<preset>.bak-compact-agents备份,并用临时文件 + rename 原子替换;只改目标那一行,preset 里的注释与排版原样保留。
settings: false可以整体关掉这个设置面(工具与提示不受影响)。
每个参数到底在管什么
上面那张表只说了"字段是什么意思"。这一节说清这个参数到底在管什么、往哪边调会怎样。 机制层面的理由(为什么这样设计)见设计说明 §9。
先看一次压缩从头到尾发生了什么(以 1M 窗口 ≈ 100 万 tokens、preset 用 0.35 / 0.05 为例):
压之前 系统提示 1 万 + 历史 34 万 = 35 万
└─ 撞到 thresholdRatio 0.35 的触发线 → 开始压缩
压缩中 把"最近 5 万"以外的部分【遮蔽】掉(不是删除,是移出发送内容)
└─ 换成一段摘要;压缩提示播报里报的"遮蔽节点数"就是它
压之后 系统提示 1 万 + 摘要 0.3 万 + 最近原文 5 万 ≈ 6.3 万
└─ 这"最近 5 万"由 retainRatio 0.05 × 窗口 100 万 决定
随后 进入【受控阶段】:接下来每次请求最多输出 bootstrapMaxTokens tokens
三个数各管一段,互不重叠:thresholdRatio 管什么时候压,retainRatio 管压完留下多少原文,
bootstrapMaxTokens 管压完那几轮最多让它说多少。没有一个参数管"摘要写得好不好"——那是摘要模型的职责。
把上面这条链路画成图,就是一次压缩的全过程(数值与文字版一致):
flowchart TD
A["压缩前:系统提示 1 万 + 历史 34 万<br/>≈ 35 万 tokens"] --> B["撞到 thresholdRatio 0.35 的触发线<br/>(1M 窗口 × 0.35 = 35 万)"]
B --> C["压缩:把「最近 5 万」以外的部分<br/>遮蔽成一段摘要(不删除,只移出发送内容)"]
C --> D["压缩后:系统提示 1 万 + 摘要 0.3 万 + 最近原文 5 万<br/>≈ 6.3 万 tokens"]
D --> E["「最近 5 万」= retainRatio 0.05 × 1M 窗口"]
D --> F["进入受控阶段:<br/>此后每次请求最多输出 bootstrapMaxTokens"]
主线只有一条:达线 → 遮蔽成摘要 → 进入受控阶段。下面按段拆开讲。
压缩后保留比例 retainRatio
本质是保真的边界:这条线以内的内容原文一字不差保留,线以外只剩摘要。
- 它的单位是窗口比例,不是消息条数:
0.05× 1M = 保留最近 5 万 tokens 原文。 - 为什么必须留一块原文:摘要一定丢细节;而最近发生的内容恰恰最可能马上要用(刚贴的代码、刚提的 需求、刚纠正的错误)。被总结掉就会出现"我刚说过的它当没看见"。
- 调小的症状:
0.01= 只留 1 万,一个几百行的代码文件差不多就 1 万 tokens,压完立刻被总结掉 —— 模型转头就"忘"。 - 调大的症状:
0.2= 留 20 万,压完还剩约 25 万,很快又撞触发线 → 反复压缩、反复打断。 - 建议:一般对话
0.05够用;经常贴大文件可调到0.08~0.1。
受控阶段输出预算 bootstrapMaxTokens
它是输出预算,不是输入预算:压缩结束后那一小段时间里,每次请求最多让模型输出多少 tokens。
- 最容易踩的坑:
max_tokens把思考(reasoning)token 也算在内。 所以1024时可能光思考就 吃满预算、正文一个字都出不来 —— 表现为"空回复"或话说到一半硬截断。(这正是本插件存在的起因之一: 此前观测到的 4 次截断全是outputTokens=1024、正文 0 字。) - 为什么要"受控":刚压缩完,模型拿到一段崭新摘要,很容易一口气写一大篇,把刚清出来的空间又塞满, 前一次压缩就白做了。所以 preset 在每次压缩结束后把会话打回受控状态、强制卡住输出;等会话"晋升" (解除受控)后恢复模型本身的大预算。它是压缩之后的一段临时限流,不是永久设置。
- 调小:
16384 → 1024省,但极易截断。 - 调大:
32768不容易截断,但受控阶段每轮都可能很贵。 - 与「自动续写」配套:预算太小 → 回复被截断 → 自动续写自动发一句"继续"把话说完,所以现象是
锯齿状输出(说一半 → 自动继续 → 接着说)。
16384是折中值。 - 建议:常出现断在半句或空回复 → 调到
32768;压完那几轮输出长得离谱 → 保持16384或更小。
速查表
| 你想要的 | 怎么调 |
|---|---|
| 更省钱、更快 | 阈值调小(早压)+ 保留比例调小 |
| 少打断、记得住刚说的 | 保留比例调大(0.08~0.1) |
| 受控阶段老被截断(反复自动续写) | 输出预算调大(32768) |
| 压缩太频繁、嫌它老在总结 | 阈值调大(0.3~0.4) |
生效时机与恢复默认
- 前三个(压缩触发阈值、保留比例、受控阶段输出预算)写进 preset 文件,所以只对之后新建的会话生效, 已经在运行的会话不受影响。
- 后两个(压缩进度提示、自动续写次数)是本插件自己的,改完立即生效。
- 想恢复默认:各字段旁边的「重置」按钮回到 preset 里的值;
thresholdRatio填0.8即 DSH 出厂值 (约等于不触发)。
工作原理
对每个目标调用 ctx.compaction.compactNow(agent, signal),然后把结果翻译成报告行。
不绕过契约自身的约束:
| 约束 | 原因 |
|---|---|
| 目标必须 idle 才能立即压 | compactNow 走 agent.runMaintenance,agent 正在跑 turn 时会同步抛 ManualCompactionError('busy') |
| 调用者自己永远 busy | 它此刻正在执行这个工具调用 —— 所以走 queue,在本轮结束时补齐 |
| 只有顶层 agent 能扫描 | 防止成员互相压、或压队长;子代理只能用 scope: "self" |
| 一次一个摘要模型调用、串行 | 每个目标都要真实调一次模型;排队的那部分在 turn 之后跑,不计入工具超时 |
| 只覆盖活着的会话 | 已结束/已归档的会话没有 live agent,compactNow 需要 Agent 句柄;压死会话也不省 token |
| 压缩不会修好"单个超大单元" | 契约明确:单个过大的保留单元或请求信封无法靠表面压缩修复,这种情况回报 noop |
细节(契约出处、事件语义、踩过的坑)见设计说明。
状态与副作用
- 零持久化:不写任何文件、不建账本、不改配置;压缩结果由 DSH 自身的会话日志记录。
- 零网络:只有一次摘要模型调用(由
compaction-basic经宿主 LLM 通道发出),插件本身不出站。 - 不改变自动策略:
compaction-basic的阈值与保留比例由 preset 决定,本插件不覆盖。 - 可在任何时候卸载:删掉挂载行与宿主组成里的 bundle 登记即可(
uninstall.mjs两处都还原),不留残留状态;宿主侧同样要重启dsh才生效。 - 提示会进入会话(含模型上下文):它是一条
user/message,所以模型下一轮也能看到 —— 这是 有意的(让模型知道上下文刚被压过)。代价是每次压缩多几十个 token,且下一次压缩会把它一并遮蔽。 不想要就用notice: false关掉。 - 自动续写会以"你"的身份发言:那条
继续的source.kind是user,在对话区就是一条普通 用户气泡(也有提示行说明是插件自动发的)。这是刻意的 —— 用 plugin 来源可能被 preset 的messageSources白名单过滤出模型表面。不想要就用maxAutoContinues: 0关掉。
架构
dsh-compact-agents
├── index.js # preset 行的入口:注册 compact_agents 工具 + 会话监听(压缩提示/自动续写)
├── client-host.js # 宿主组成那一行的入口:只做两件事(下发浏览器 half + 注册设置命名空间)
├── cordis.patch.yml # dsh.bundle.patch:往宿主组成里插 compact-agents-client-host 行
├── settings.js # 设置面:settings 命名空间 + preset 参数读写(两个入口共用)
├── lib/client.js # 浏览器 half:插件页的配置表单 + 旧设置页的折叠卡片(同一份正文,手写无构建)
├── package.json # ESM 包声明(main → index.js;dsh.client → lib/client.js;dsh.bundle.patch → cordis.patch.yml)
├── scripts/
│ ├── lib/presets.mjs # 各脚本共用:路径常量 + preset 发现(只有一处定义)
│ ├── install.mjs # 一键安装/修复:junction + preset 行 + profile bundle 与 dependencies(幂等,带 .bak)
│ ├── uninstall.mjs # 卸载:移除挂载行 + 删除自己建的 junction / bundle 与依赖登记
│ ├── validate-presets.mjs # 校验挂载行/阈值/路径/README 徽章/profile 依赖
│ ├── inspect-presets.mjs # 只读自检:真实 preset 里读到的生效值是多少
│ ├── compose-test.mjs # 组装验证:真 FileSettingsProvider + 复刻 preset 隔离(只碰临时夹具)
│ ├── settings-test.mjs # 设置面测试(真 schemastery + preset 文本手术)
│ ├── client-test.mjs # 浏览器 half 测试(假 __ModuleLoader__ + 桩 require)
│ ├── integration-test.mjs # 真机加载测试(真 Context + 真 ToolRuntime)
│ ├── deferred-test.mjs # 排队补压行为测试(假 ctx)
│ ├── dryrun-test.mjs # `--dry-run` 守卫:预演绝不真删(沙箱 DSH_HOME)
│ └── selftest.mjs # 模块与 defineTool 规格自检
├── docs/
│ └── design.md # 设计说明:契约出处、约束、踩过的坑、测试矩阵
└── node_modules/@deepseek-ai/ # install.mjs 建的 junction(不进版本库)
├── dsh-tools -> <dsh checkout>/packages/core/tools
├── cordis -> <dsh checkout>/vendor/cordis
└── schemastery -> <dsh checkout>/vendor/schemastery
此外 install.mjs 还会在该 profile 下建一个联接 <DSH_HOME>/profiles/<profile>/node_modules/dsh-compact-agents -> <本仓库>,并把包名登记进 profile 的 dsh.profile.bundles(见下)。
两处挂载点,缺一不可:
| 挂载点 | 怎么来的 | 负责 |
|---|---|---|
| preset 行 | install.mjs 往 <DSH_HOME>/.agent-presets/*/agent.cordis.yml 的 compaction 组追加 |
compact_agents 工具、压缩进度提示、自动续写。必须留在 compaction realm 内,否则解析不到 ctx.compaction |
| 宿主组成里的 bundle 行 | install.mjs --profile:profile 的 dsh.profile.bundles 登记本包 → 本包 dsh.bundle.patch(cordis.patch.yml)插入 id: compact-agents-client-host / name: 'dsh-compact-agents/client-host' |
被 ClientModuleRegistry 扫到(下发 lib/client.js)、在宿主根注册 settings 命名空间 |
client-host.js 刻意 inject = [] —— 宿主根上没有 compaction 服务(它由 preset realm 内的 compaction-basic 提供),声明依赖只会让这一行永远 pending。它只做两件事:让客户端模块表扫到本包、在宿主根上注册 settings 命名空间。
为什么 preset 一处不够:ClientModuleRegistry(packages/client/modules/src/index.ts)只遍历宿主 Loader 的 entries,而它的 internal/plugin 监听里有 const entryName = fiber.entry?.options.name; if (entryName === undefined) return —— fiber.entry 为空的"子插件或手动挂载"被直接丢弃;preset 行正是 agent-presets 用 internal.import 手动挂载的,不是 loader 行。结果就是:只挂 preset 时浏览器 half 永远不下发,设置页里既没有命名空间也没有卡片、且毫无报错。详见设计说明 §8.4。
插件本体只 import 两个东西:defineTool(来自 @deepseek-ai/dsh-tools),以及动态 import 的 @deepseek-ai/schemastery(用来声明设置的 schema)。
后两个 junction 只有设置面与测试需要;缺了 schemastery 只会少一个设置面,不会让插件挂掉(动态 import 失败只记一条 warn)。
为什么 preset 行必须写绝对路径:agent-presets/src/specifier.ts 的分类函数对绝对盘符路径走 pathToFileURL(注释写明"专为 Windows 盘符路径所必需"),变成 file: 行;而裸包名在 preset 里是从 harness 解析的,指向用户目录的包会解析失败。
开发与验证
node --check index.js # 语法自检
node scripts/selftest.mjs # 模块导入 + defineTool 规格 + 参数枚举
node scripts/deferred-test.mjs # 忙→排队→下次 idle 补压(假 ctx,不起 DSH)
node scripts/integration-test.mjs # 真机:真 Context + 真 ToolRuntime,全链路
node scripts/compose-test.mjs # 组装验证:真 FileSettingsProvider + 复刻 preset 隔离
node scripts/inspect-presets.mjs # 只读:表单将显示的初值
node scripts/validate-presets.mjs # preset 挂载行与阈值 + README 徽章 + profile 依赖
node scripts/install.mjs --dry-run # 安装预演(不改盘)
integration-test.mjs 覆盖假 ctx 测不到的东西,且零模型调用、零成本(只给 systemPrompt / compaction / agents 三个最小桩服务):inject 真的解析、ctx.tools.register(defineTool(...)) 真的被注册表接受、ctx.tools.get() 查得到、ctx.tools.executionMode() 真的判成 exclusive、端到端 execute() 返回值真的过 output schema、render() 真的产出文本块、非顶层 sweep 真的被拒。断言清单见设计说明。
compose-test.mjs 则用检出里真实的 FileSettingsProvider(写到临时文件)与复刻的 preset isolate 语义,验证设置命名空间在真实服务 + 真实隔离作用域下也注册得上;它还会刻意先挂宿主组成那一行、后挂 settings 服务,证明 ctx.inject 的等待路径真的在服务到场后完成注册。
⚠️ 测试隔离教训:
compose-test.mjs会走真实的update → watch → 写回链路,必须先用setPresetFilesForTest()把 preset 读写指向临时夹具 —— 第一版漏了这一步,于是这个"验证"脚本真的改写了用户的 preset(把thresholdRatio写成了 0.42)。现在脚本开头会拍下真实 preset 的全文快照、结尾逐字节比对:只要有一个字被改动就立即失败(npm test会跑到它,也可以单独node scripts/compose-test.mjs)。判据刻意不是"阈值必须是某个数字"——thresholdRatio本来就是给人调的旋钮,硬编码期望值会把"用户调过参"误报成"测试污染了配置"。凡是会写盘的测试,夹具必须显式指向临时文件,不能依赖"我以为它不会写"。
已知限制
- 只覆盖活着的会话:已结束/已归档的会话拿不到 Agent 句柄;历史会话无法事后压缩(压了也不省 token);
- 压缩不是免费的:每个目标要真实调一次摘要模型,本身有 token 成本——目标是拿它换掉后续每次调用重发的巨额前缀;
- 单个超大保留单元无法修复:契约明确表面压缩修不了这种情况,会回报
noop; scope: "self"是异步的:工具立刻返回queued,真正的压缩发生在本轮 turn 结束之后,结果写进 DSH 日志(ctx.logger.info)而不是工具返回值;- 已 composed 的会话拿不到新工具:preset 改动只对新会话生效,老会话需新开(或重启 DSH,但那会丢成员会话);
- 不做孤儿数据/状态清理:本插件无状态,无需清理。
- 只支持 DSH 开发检出布局:安装脚本要求检出里同时有
packages/core/tools与vendor/cordis;npm i -g全局安装的布局未验证(全局安装下这两个包的落点不同,需要另行适配)。 - 插件页的启用/停用开关只管浏览器 half:插件页上关掉本插件,动的是宿主组成里那一行(
compact-agents-client-host)—— 表单不再出现;而compact_agents工具由 preset 行提供,与这个开关无关,仍然照常挂载。要整体停用,用node scripts/uninstall.mjs。 - 随包发行的 preset 会被它自己的插件升级重写:
<profile>/node_modules/@linxin666/*/presets/*/agent.cordis.yml里那一行是安装时插进去的,插件升级会整份重写掉它(实测撞上过:@linxin666/dsh-liangshen升级后默认 preset 里就没有compact-agents行了,而用户自建的~/.dsh/.agent-presets/liangshen又被随包同名 preset 遮蔽——agent-presets的 roots 顺序是"随包最先、用户目录最后",同 id 前者胜)。升级后重跑一次node scripts/install.mjs即可,node scripts/validate-presets.mjs会先告诉你少了哪一份。
更新历史
v0.8.2 — 文档更正(无代码改动):界面是按生效时机分三组的(立即生效 / 写入预设并热同步 / 新建会话生效),而「在界面里改这些参数」下面那张表还停在 v0.7.0 之前的说法 —— 它把阈值与保留比例写成"新建会话生效"(其实保存时就热同步进运行中的会话了),字段名也和卡片上的字不完全一样。现在表里的分组与字段名(
压缩提示播报、自动续写次数上限)与卡片逐字对齐,"为什么分成两种生效时机"那段也重写成三组:本插件自己的两个值在事件发生时读取、改完立刻算数;阈值与保留比例写预设文件的同时热同步;受控阶段输出预算属于另一个插件,只写预设文件、供之后新建的会话读取。v0.8.1 — 参数表单回到"设置里自成一页":新增官方
settings.section槽位的注册(id: compact-agents、order: 26、侧栏那一行文字「压缩与自动续写」),于是 设置 → 「压缩与自动续写」 与「通用设置 / 模型 / 内置插件 / Agent 预设」同级 —— 这个位置与宿主版本无关(0.1.5、0.1.6 都在),也不必先钻进插件页找到本包。三处注册共用同一份正文(formBody)与同一个控制器,各自try/catch,一处失败不影响另外两处。起因见 v0.8.0:上游把插件配置搬去插件页后,老槽位settings.plugin.item已无渲染方,用户明确要求"和插件同级",而不是挂在插件页里面。v0.8.0 — 跟上 DSH 0.1.6 的插件页。上游
90af3110b7 feat(web): host plugin configuration on the Plugins page把插件配置从「设置」搬到侧栏新的「插件」页(ui-plugin-manager),老槽位settings.plugin.item的渲染方ConfigurablePluginsTab.tsx被删掉 —— 我们那张卡片于是凭空消失、毫无报错(ctx.slots.inject对没人声明的槽位是静默的,注册上去既不抛错也不显示)。现在同一份正文同时注册进两个槽位:新版plugins.bundle.config(键是包名)画表单、旧版settings.plugin.item(键是 settings 命名空间)画折叠卡片;两边共用同一个控制器与同一份正文,不会漂移;一个槽位注册失败也不会连累另一个。第二处静默失败一并修掉:新版插件页只列installed || optional || error的 bundle,而installed的判据是包名在不在 profile 的dependencies里 —— 只登记dsh.profile.bundles的旧安装会被整条过滤掉,所以install.mjs现在两处都写(优先走官方dsh plugin add,失败或--no-cli时回落到行级手写登记,写前留.bak-compact-agents、写坏自动回滚),validate-presets.mjs把"在 bundles 里却不在 dependencies 里"直接判成失败。client-test.mjs相应覆盖:两个槽位的注册、表单形状(不套卡片外壳)、列表项一行摘要、命名空间未就绪时的降级,以及槽位互不连累。v0.7.4 — README 顶部的版本徽章和 package.json 对齐,并加进 npm test 的校验里。起因是查市场收录时顺手发现:徽章还写着 0.4.0,而 package.json 已经 0.7.x —— 市场抓的正是 README,别人在 dsh.market 上看到的版本号就是那个过时的数字。手写的徽章靠自觉必然再忘一次,所以让 validate-presets 在两者不一致时直接失败。
v0.7.3 — 改预设参数时顺手把行尾那句带数字的注释也刷新掉。起因是真实撞上的一幕:卡片把 retainRatio 从 0.05 改成 0.02 之后,文件里留着
retainRatio: 0.02 # 压缩后保留最近 5% 窗口 = 50K 原文—— 值与解释自相矛盾,而这文件正是用户唯一能直接看到的地方。现在规则很窄:注释里出现数字的(说明它是照着旧值算出来的)换成按新值生成的规范说明;没有数字的注释是用户自己的话(为什么这么设),一律原样保留;本来没注释的行也不替他加。v0.7.2 — 界面文案统一用中文:卡片的「写入 preset 并热同步」改成「写入预设并热同步」,提示里的「preset 文件里现在是 ×…」改成「预设文件里现在是 ×…」,字段说明里的「写进 preset,新会话生效」也一并更正(阈值与保留比例早就是热同步,不再是新会话生效)。术语取自 DSH 官方中文界面(Agent 预设);
token一词保留原样,因为 DSH 自己的中文文案就是这么写的(“已压缩 N 条历史记录(约 X tokens)”),换成“词元”反而不一致。v0.7.1 — 给热同步加一道效果校验:写进去了不代表引擎吃了。补丁落下时留一个待验证(并预先查好上下文窗口 —— 压缩发生时那条路径是同步的,来不及查),拿策略自己决定的下一次压缩反算:达线判定是
tokens >= 窗口 × 阈值,所以把阈值从 0.2 抬到 0.5 后若仍在 250K(1M 窗口)就压,就说明引擎还在按 0.2 判。判定成立就不再自称生效,触发线按 0.2 报并给出新开一条对话这条正路。手动压缩(/compact与compact_agents工具触发的那次)在任意 token 数上都可能发生,一律不采信,免得冤枉引擎;反证顺序也必须是先验旧的、再同步新的,否则会拿按旧阈值判出来的本次压缩当新阈值的证据。v0.7.0 — 阈值与保留比例不必再等新会话:设置卡片保存后,插件把新值热同步进正在运行的
compaction-basic实例(它的config是普通自有属性 —— 对象虽被deepFreeze,引用可换 —— 而压力判定每次调用都重新读它),下一次步边界就按新阈值判:历史会话直接生效,不用新开对话、更不用重启。只碰这两个旋钮:bootstrapMaxTokens属于tool-bootstrap,它在apply()里把值捕获进闭包,改不动,仍只对新会话生效;配置里带modelPolicies时只改命中那条(与引擎resolveTargetPolicy同款匹配)。写进去后会读回校验;读不到/写不进去就退回老行为,并由压缩提示点明两个值与出路。行配置livePresetParams: false关闭。设置卡片因此多出一组「写入 preset 并热同步」。v0.6.1 — 压缩提示报出本会话实际生效的触发线(
· 触发线 ×0.35),并在热同步进不去时直接写明两个值与"新开一条对话才会用上新值"。起因是一个必然会撞上的困惑:preset 里的压缩参数在会话建立时读进compaction-basic并深冻结,而旧提示只报 token 数,看不出用的是哪一档阈值。同时把设置卡片里那句会误导人的 hint 改成显式说明"默认 0.35"——它此前写成"(0.35 = 350K tokens)",紧挨着一个用户自己设的 0.5,读起来像当前值。v0.6.0 — 压缩触发阈值默认值由
0.2(200K) 调成0.35(350K):默认值偏小会让压缩来得太早、把还没用上的空间提前总结掉,0.35更接近"快满了再压"。同时把compose-test.mjs的真实 preset 守卫从"硬编码thresholdRatio必须是 0.2"改成"与开头全文快照逐字节一致",修掉用户把阈值调成别的值后守卫恒失败的假阳性。v0.4.0 — 参数搬进「设置」界面:
- 新增设置面:宿主侧注册 settings 命名空间
compact-agents(模式与官方一致 —— slot 契约原话是 "Keying on the namespace is what lets a plugin distributed outside this repository contribute a card"),浏览器 halflib/client.js在settings.plugin.item槽里注册同一命名空间的卡片,于是「设置 → 插件」里多出一张「压缩与自动续写」; - 五项可改:压缩触发阈值比例、压缩后保留比例、受控阶段输出预算、压缩进度提示、自动续写次数。 前三项属于 preset 里的其它插件,所以由本插件写进 preset 文件(写前备份 + 原子替换 + 只改目标行,注释与排版保留),新建会话时自动开新一代;后两项是本插件自己的,立即生效;
- 浏览器 half 是手写的单文件 bundle(DSH 的客户端模块系统就是惰性 CJS 表,不需要打包器),
只 require 种子模块
react与@deepseek-ai/dsh-client-store,因此本项目仍然零 npm 依赖; - 新增两个测试:
settings-test.mjs(真 schemastery + 真 cordis Context + preset 文本手术, 全程用临时文件,不碰真实配置)与client-test.mjs(假__ModuleLoader__+ 桩 require, 真 React 渲染断言); install.mjs多建一个@deepseek-ai/schemastery联接(缺了只会少一个设置页,插件照常工作)。
- 新增设置面:宿主侧注册 settings 命名空间
v0.3.0 — 被输出上限截断时自动续写:
- 新增自动续写:一轮以
turn/end{reason: 'max-tokens'}结束时,替用户发一句"继续" (agent.followup,与人在界面上发言同一条路),让对话自己走下去。行配置maxAutoContinues控制连续次数(默认 2,0/false关闭),用满后改为提示,任一轮正常结束即清零; - 实机定位到一个典型诱因并写进设计说明 §7:压缩后 preset 会把下一个请求的
输出预算压到很小的窗口来"重新锚定",若模型开着高推理,思考 token 与正文共享该预算,很容易
整份被思考吃光 → 正文 0 字被判截断。同一会话日志里 4/4 次截断都出现在
compaction/end之后 9~12 条记录,全部reasoning=1024/1024、正文 0 字。
- 新增自动续写:一轮以
v0.2.0 — 压缩过程不再静默:
- 新增对话区可见的压缩提示。原本
compaction/start到compaction/end之间对话区没有任何节点 —— 自带自动压缩也一样(只有"轨迹"面板显示正在压缩上下文…)—— 观感就是卡住。现在插件订阅session/event,在生命周期两端各追加一条插件来源的user/message,渲染形态与框架自己的 "上下文注入"完全一致,可用行配置notice: false关闭; - 工具回执补上压缩前后 token 数:
beforeTokens/afterTokens(-1表示计量服务不可用), 渲染成~213,400 → ~49,800 tokens。
- 新增对话区可见的压缩提示。原本
v0.1.0 — 首个版本:
compact_agents工具(4 种 scope、忙则排队)、一键安装/卸载/校验脚本、四层验证(自检 / 行为测试 / 真机集成测试 / preset 校验)。- 安装脚本支持失效路径自愈(项目移动/改名后重跑即修正)与
--force重新指向; - DSH 检出位置改为自描述探测(显式参数 → 既有联接 → PATH 上的
dsh启动器 → profile → 家目录),脚本与文档中不含任何本机盘符; uninstall.mjs只移除指向本项目的挂载行,避免克隆两份时误拆别人的安装。
- 安装脚本支持失效路径自愈(项目移动/改名后重跑即修正)与
No comments yet. Be the first to write one.