中文EN

@2jumpsina/dsh-context-guard

session已测通过npmMIT

长会话越跑越贵,平台到点还会把上下文压掉。这个插件做三件事:在会话标题旁显示当前上下文占了多少,鼠标停上去能看到这个数是怎么来的;过 45% 时往工作目录的 HANDOVER.md 写一份交接草稿,记下改过哪些文件、花了多少钱、最近几轮在聊什么;到 60% 提醒你一次该收尾了。平台压缩发生时,它把压缩前的数字记一行,事后还查得到。不调用模型,也不改动对话内容。它不阻止压缩,拿不回已经被压掉的部分。

下载量 / Downloads
2,742
依赖体积 / node_modules
1.12 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

安装

dsh plugin --profile web add @2jumpsina/dsh-context-guard

沙箱测试结果

阶段结果
1 安装✓
2 列装✓
3 挂载✓
4 启动—

四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。

README

dsh-context-guard

npm 包名:@2jumpsina/dsh-context-guard · [English](README.md)

会话太长就提醒你收尾换会话的 DeepSeek Harness 插件。

消费官方 dsh-token-meter 已经算好的 contextPressure 投影,在一轮结束之后按双档阈值提示:

「先把交接写进文档,再开新会话」。它会顺着帮你把这句提示变成动作 —— 跨过阈值时自动往会话工作目录起草一份交接草稿。

上下文税 = 0:不注册任何 model-facing 工具、不注入 prompt 段落、不额外发请求。宿主算数,UI 说话。

🆕 当前版本已发布到 npm latest(0.7.x):45% 只静默起草交接、60% 才提醒一次;
压缩点在角标 tooltip 与 banner 正文里给出(按 min(窗口×0.8, 窗口−预留−64K) 实时推算,标来源;读不到
平台压缩配置时标「推算」);平台压缩那一刻把压缩前读数抢存进 HANDOVER.md 的压缩账。
v0.5.0 起删掉了微信外推与跨会话趋势(相关配置键已在 v0.7.2 从 schema 退场,旧残留值被安全忽略)。
详见 HANDOVER.md §46–§48。
⚠️ 别敲 npm i dsh-context-guard —— 那个不带 scope 的名字是第三方的包(作者 greenlv)。
本插件的包名是 scoped 的:@2jumpsina/dsh-context-guard,已发布到 npm(见[安装](#安装))。
🙂 用起来了?回我一条吧 —— 我想知道还有哪些可以改善的地方。
一句话就够:它在你那儿做对了什么、哪里别扭。

⚠️ 它会写你的工作目录(装之前先看这一段)

  • 默认开启(handoverOnWarn: true)。当某个会话的占用率跨过 warnRatio(默认 45%)时,

插件会在该会话的工作目录下创建/改写 handoverPath(默认 HANDOVER.md),此后水位每涨 5 个点刷新一次。

  • 写入的内容是机器事实:会话 id、标题、工作目录、占用率、轮数/调用数、账本花费、

本次会话写过的文件清单、最近几轮的用户诉求与回复摘要,以及一段留给 agent 的「待补」空槽。

⇒ 它可能包含敏感内容(文件路径、你的提问与回复片段),也可能被 git 提交。

  • 建议:把 HANDOVER.md 加进你的 .gitignore;或把 handoverPath 指到一个不在仓库里的路径;

或直接 handoverOnWarn: false。

  • 边界:目标父目录不存在 ⇒ 拒绝写入;目标已存在但不是文本类文件(.json/.yml/…)⇒ 拒绝改写;

目标是指向别处的符号链接 ⇒ 拒绝;写入使用原子替换(临时文件 + rename)并收紧权限(0o600)。

任何失败都只记录状态,绝不影响徽标 / banner / 推送。

为什么需要它

一句话:长会话不是技术故障,而是每一轮都在涨的账单。这个插件存在的意义,就是在你还能行动的那一刻

把这张账单摆到面前 —— 一轮已经结束、下一次请求还没发出的那几秒。

这个问题是可测量的

成本 ≈(平均 prompt 大小)×(调用次数)×(单价)。长会话会把前两个因子同时推上去,而单价本身也随占用率上升。

拿标定这台机器自己的账本(本地 session 投影缓存,84 个会话 / 14 天,自报 ¥252.29)对着官方价表核:

测量项 · 数值

自检(价表 × contextTimeline.cost,对账本总额) · ¥254.29 vs ¥252.29 ⇒ 偏差 0.8%;逐请求金额覆盖总额 85.4%

单价随占用率 · 0–10% ¥0.011/次 → 70–80% ¥0.045/次(4.2×)

历史花费落在哪里 · ≥40% 占用时的调用吃掉 37.4% 的花费;≥60% 吃掉 20.0%

会话峰值 · 观测到的 75/75 个会话都不超过 ~80%;真正的压缩点由公式决定(本机 deepseek-flash ≈ 67.8%)

打扰成本(线画在 40% / 50% / 60%) · 每天 0.78 / 0.57 / 0.50 次

再举一次真实会话 —— 就发生在开发这个插件的仓库里:**584 次调用、2.74 亿 token、¥31.66,其中 98.6% 花在

重读上下文,模型输出只占 0.17%。** 这正是它要对付的失败形态:不崩溃、不报错,只是一张发票。

为什么它是这笔交易里便宜的那一半

上面那个公式有个值得写明的形状。一个会话的总额是

总额 ≈ Σ_轮 ( prompt_i × 单价(占用_i) )

而两个因子是同向动的:prompt_i 随每一轮单调增长,单价本身也随占用率上升(上面实测过:

0–10% 时 ¥0.011/次 → 70–80% 时 ¥0.045/次)。所以一个会话不是「跑得越久、贵得越线性」,而是

超线性地变贵 —— 这就是为什么长会话的最后三分之一,明明轮数一样,却可能比头三分之一贵得多。

这正是这个插件值得装的全部理由:它的边际成本严格为零。

  • 不注册 model-facing 工具、不注入 prompt 段落、不额外发请求 ⇒ 它不往任何请求里加 token,

账本上根本查不到它。

  • 它唯一让你付出的是结尾那份交接 —— 而那不是付给插件的费用,是你本来就要做的事

(而且通常做得更糟:凭记忆,在一个已经被放弃的会话之后)。

  • 它只在轮边界出声。轮内的提示没有用 —— 那一轮的 prompt 已经发出去、已经付过钱;

晚两轮的提示是一张你已无法回避的发票。

所以这笔交易不是「花钱省钱」,而是:在一张你已经在付的账单上,免费装一个读数 ——

而且装在数字还来得及改变决定的那一刻。

为什么别的机制接不住它

  • 平台会压缩,但不会跟你商量。 自动压缩点是 min(窗口 × 0.8, 窗口 − 输出预留 − 65,536)

(本机 deepseek-flash ≈ 67.8%),到了就压、不问。压缩是会话的

安全网,不是给你的通知 —— 等它触发时,你已经为整段爬升付过钱了。

  • 模型自己当不了这个计量器。 projectedTokens 由 dsh-token-meter 从真实请求算出,模型看不到自己的 prompt

有多大;而提示只有在一轮结束之后才有意义(那一轮的 prompt 已经发出去了)。何况任何「让模型自己注意上下文」

的做法都要注入 prompt 段落或注册工具 —— 那等于给一个上下文税问题再加一笔上下文税。

  • 你手上没有读数。 占用率可以看见,但「该收尾了」不是一次读数能定的:它需要迟滞、冷却、边沿触发、跨会话计数,

而且要在你点进一个已经满了的历史会话时就响。没人一边干活一边盯百分比。

它做了什么 —— 以及刻意不做什么

  • 上下文税 = 0:不注册 model-facing 工具、不注入 prompt 段落、不发额外请求。防上下文膨胀的东西,自己不能吃上下文。
  • 只消费官方计量器已经算好的数字:不重复计量、不估算、不猜。
  • 每档只响一次,且只在一轮结束时:双档 + 迟滞 + 边沿触发 + 冷却。
  • fail-closed:没有分母就完全不判定,徽标显示 ctx — 而不是假装 0%。
  • 把一句话变成动作:跨过 warnRatio 就地起草一块机器事实草稿(会话、水位、账本花费、本会话写过的文件、

最近几轮的诉求 / 回应预览,外加「待补」空槽)写进会话工作目录 —— 零 LLM、零额外请求。

  • 本插件不联网(v0.5.0 起删除了微信外推);能力清单只剩 fs-read / fs-write。

这一节不声称什么

  • 省下的钱是反事实上界,不是承诺。 拿本机账本重放:≥40% 占用时发生的那 2741 次调用,若都发生在 0–20%,

可省 ¥59(23%);≥60% 的那批可省 ¥34(14%)。收尾不是免费的 —— 它恰好要付一次交接的成本。

这里只声称一件事:把数字摆在面前时,这个决定能做得更好。

  • 阈值是标定值,不是最优解。 0.45 / 0.60 来自那 84 个会话;它们是默认值,且每一个都可配置。
  • 样本是单机、单 provider、两周。 价格与压缩行为都会变。
  • 它不判断你的任务值不值得做完。 它只说一句:从这里继续,每轮比重新开始贵好几倍 —— 交接草稿在这里。

它做什么

半边 · 做什么

host(lib/index.js) · 订阅 ctx.sessionProjections.onChanged,把 contextPressure 喂给纯策略;在 turn/end 结算一轮;持有设置;session/created(新建或从存储恢复)时立刻判一次;compaction/end、request/header 参与判定;把判定注册成 contextGuard wire 投影给浏览器半边;注册 GET /api/context-guard/state(只读调试镜像);按设置把结论外推到微信;跨过阈值时起草交接草稿

client(lib/client.js,手写单文件 bundle) · 会话头徽标(实时占用率,只显示数字、不出声)+ 跨档时一次性 banner(占比 + 下一步 + 指向交接文档);档位/轮次/冷却一律 useProjection("contextGuard") 只读宿主投影 —— 没有本地状态机、不读阈值、不发额外请求(见下面「架构」一节)

行为要点:

  • 判定时机 = 投影变化时(不是 turn/end):contextPressure 在每次请求后更新,插件随即判一次;

turn/end 只做轮计数与冷却衰减,compaction/end 与 request/header 参与判定。

⚠️ 早期本节写的是「绝不 mid-turn」,与实现不符。2026-10-17 代码审查后定案保持实现、改文档,

理由是风险不对称:改成只在 turn/end 判,一旦该事件没送到就完全不提示(漏报);

投影即判最坏只是提示出现得早一点。详见 HANDOVER.md §33。

  • 只响一次:双档 + 迟滞(回落到 warn − hysteresis 才重新武装)+ 边沿触发 + 冷却 N 轮。
  • 跨档时给你两个选择:写好交接,我复制(宿主立刻写一次机器事实草稿,并把可复制的开场文本摆出来)与 知道了(纯关闭,没有附带动作)。
  • fail-closed:没有分母(contextWindow 缺席)就不判定,徽标显式显示 ctx —,不假装 0%。

架构(v0.4.3):宿主判,浏览器只读(一份判定)

判定只有一处、显示只有一处,中间靠一条 wire 投影连着:

层 · 在哪 · 负责什么

判定 · lib/engine.mjs(只在宿主) · 状态机是纯折叠(init + apply(state, event)):档位 / 武装 / 轮次 / 冷却 / 迟滞只算一遍,compaction/* 折算进状态

投影 · lib/index.js(宿主) · 把判定注册成 wire 投影 contextGuard:{ level, armed, turn, cooldown, ratio },扁平 JSON,未变化时保持同一引用

显示 · lib/client-source.js(浏览器) · useProjection("contextGuard") 只读显示(徽标 + 一次性 banner);没有阈值、没有状态机、不发额外请求

调试镜像 · GET /api/context-guard/state · 只读镜像:宿主当前配置 / 最近判定 / 压缩账计数 / 交接统计。浏览器半边不再从它抄阈值

这样

…(truncated)

以上内容来自 npm 上的 README,我们只做格式化,不改内容。

版本

npm 上共发布:15,最后更新 2026-10-25

链接

GitHub · Homepage · npm

同类插件