dsh-context-rescue

sessiontested: passnpmMIT

Recovers a session the host already refused for exceeding the context window: it re-checks the gateway message the built-in classifier misses, takes the real window out of that message, and folds the oldest span into one checkpoint without a model call (tool-result pruning, the host compaction engin

下载量 / Downloads
813
依赖体积 / node_modules
0.55 MB
测试时间 / Tested
2026-11-07
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-context-rescue

Sandbox test results

StageResult
1 install✓
2 list✓
3 mount✓
4 boot—

Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).

README

dsh-context-rescue

中文 · [English](README.en.md)

给"上下文爆窗"的 DeepSeek Harness 会话叫救护车。

会话里每条消息都回 400,因为某一次请求超过了模型真正能吃的窗口。/compact 也救不回来

—— 它沿着同一条超限的投影重发;如果压缩被代理层接管,还会被直接拒绝。这个插件是急救车:

识别爆窗、把最老的历史折叠成结构化摘要、把同一步重发一次,会话就活了。

400 the request exceeds the model's context window
  └─ context-rescue: overflow detected (gw-prose, score 0.95)
     ├─ T1 裁剪工具输出     → 宿主未提供
     ├─ T2 宿主压缩引擎     → 代理拒绝,本进程内该车道永久关闭
     ├─ T3 机械折叠         → 956 节点 → 169 节点,991,512 → 298,595 tokens
     └─ T4 超大单条消息     → 仅当历史只有一条巨消息时:保留它的开头与结尾
  └─ 重试同一步 → 200 OK

为什么需要它

这条失效链在上游有完整记录(DSH 讨论 #10138),它是三段叠加,不是单点 bug:

1. 网关用自然语言拒绝(payload core text alone is ~N bytes … no upstream can accept it),

宿主的分类器认不出来,于是这次失败没有被打上 CONTEXT_WINDOW_EXCEEDED;

2. 错误码不对,宿主自带的上下文溢出分支就不会执行;

3. 没有任何东西缩小投影,重发出去的请求字节级相同,于是再失败一次 —— 无限循环。

现有插件都是事前预防(滚动换窗、裁剪工具输出、代理侧压缩)。没有一个在会话已经卡死之后

动手。本插件平时什么都不做,只在请求失败时介入,因此可以和上面任何一款共存。

安装

# 直接从本仓库装
dsh plugin --profile <profile> add git+https://github.com/Lequait/dsh-context-rescue.git#master

# 本地目录
dsh plugin --profile <profile> add /path/to/dsh-context-rescue

# 从 npm 安装(0.2.5 已发布)
dsh plugin --profile <profile> add dsh-context-rescue

分支形式跟随 master;市场条目则是钉死提交(git+…#<commit>),列表指向的构建不会在脚下挪动。

包内声明了 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },以 context-rescue

为 id 插入。**运行时它不 import 任何 @deepseek-ai/* 包**:surfaceOp、compaction/* 事务、

配对规则、模型窗口解析全部按宿主的线上结构在本地构造。宿主升级不会打断它的 import,

它也不声明任何宿主 peer 依赖(也就没有"peer 缺失装不上"这回事)。装完重启一次(新 bundle 需要宿主重载)。

上架条目

market/ 放图标和生成脚本,产出的是格式 1 的插件市场索引条目。校验用的是宿主自己的

parseMarketIndex —— 也就是 Discover 页读索引时跑的那套代码:

# 校验一条以本仓库某个 commit 作为 Git 源的条目
node scripts/make-market-index.mjs \
  --repo https://github.com/OWNER/dsh-context-rescue \
  --commit "$(git rev-parse HEAD)" \
  --out market/index.json

# 或者一步发布:先推送,再把条目钉到真正推上去的那个提交
npm run publish:remote -- --repo OWNER/dsh-context-rescue --dry-run

它会打印解析出的安装 spec(git+https://…git#<commit>;加 --npm 则是 name@version),

任何一条规则不通过就带 JSON 路径非零退出。校验器位置可用 DSH_APP_ASAR 覆盖。

DSH 只读用户自己配置的索引:市场 bundle 是可选的、默认 indexes: [],所以"能被发现"只差一步。

本仓库已经发布,用户有两条路:

# 1. 直接从 git 装 —— 不需要任何市场,钉住你复核过的 commit
dsh plugin --profile <profile> add git+https://github.com/Lequait/dsh-context-rescue.git#<commit>
# 2. 通过 Discover 页
添加索引源   https://github.com/Lequait/dsh-context-rescue/raw/master/market/index.json
            (Discover → 索引源 / "Add index source";官方市场 bundle
             @deepseek-ai/dsh-plugin-market 是可选的,不是每个 profile 都有)
选中条目     dsh-context-rescue —— 它装的是条目里钉住的 commit,不是分支

仓库同时带了 dsh-plugin topic,按 topic 扫描的列表能自动发现它;条目也已投给两个策展列表

(PR 记录在 docs/PUBLISH.md)。

两个不同的计数

measure() 数的是 surface 节点;真正要上游接受的却是请求,也就是

session.deriveMessages() 生成的那批消息。这两个数并不相同:按前者看还装得下的会话,

按后者看可能照样被拒。/rescue-status 两个都报,宿主闸门断言的也是后者——每次折叠之后,

派生出的请求必须比折叠前小,而且必须在窗口以内。

救不回来的时候

有些会话救不回来:光是系统提示词就已经超过窗口。插件会把每一档都试一次,然后把这条会话记为「已无解」,不再每个请求都重新测量一遍——否则那条会话的每个请求都要付一次完整测量、然后再以同样方式失败。这一步照常继续,请求照常发出,失败原样交回宿主。/rescue(或 context_rescue 的 action: "rescue")总会再试一次;任何一次真正生效的救援都会让正常路径重新打开。

这个「无解」结论属于当时那个窗口和当时那份历史形状,而不是永久属于这个会话:换到窗口更大的模型,或者投影在这之后又长大了,结论都会被丢弃,失败路径与每步预检都会重新测量一次。否则「换个模型试试」——卡住时最自然的动作——反而会让插件继续装看不见。

检查点里留下什么

摘要不调用模型,所以它留的是事实而不是文笔:错误与失败、任务清单、跑过的操作(一行一次,

重复的合并计数)、用户的请求、最近的结论,以及这次工作触及的文件与路径。路径无论是存放在

参数树里还是存放在宿主实际写入的那一条 JSON 文本里,都会被读出来——一次把路径全丢掉的折叠,

会让请求变小、让模型失去线索。

请求发出之前

等到 400 才救,代价是一次白走的重试;而有些会话一次都救不回来(issue #10138)。所以同一套急救

也挂在 agent/pre-step 上,早一步发生:

1. 先量循环即将发出的投影;

2. 如果它已经超过模型窗口,这个请求根本不可能被接受 —— 就地折叠:先 T1 裁剪,再走零模型检查点;

3. 如果只是落在压力带(fitRatio × 窗口 到窗口之间),一律不动:那是普通压缩策略,归宿主管。

窗口依次来自模型注册表(用会话头里的 provider)、会话自身、windowFallback,并按目标缓存,

一个 turn 不会每步都去解析一次。这里故意不走 T2:宿主引擎有自己的 pre-step 钩子,借它会为

同一次超窗压缩两遍。

量一份大投影才是急救里最贵的一步:本机实测整条阶梯中位数 1.1 秒,几乎都花在给节点计价上。pre-step

每一步都会调用,所以它一个 turn 只量一次,之后只数「这之后新追加了什么」——同一 turn 的第二次检查

耗时可忽略,但仍然会发现大段粘贴(任何新消息超过 4,000 字符就强制重新测量;投影节点数与缓存不符也

强制重测,那说明别人折叠过)。node scripts/dev/rescue-bench.mjs 会打出本机最大几份日志的耗时分布。

请求失败之后它做什么

挂在 agent/request-error 瀑布上 —— 这条钩子对任何失败都会触发,跟宿主给的是什么错误码

无关,因此正好补住分类器的漏。

1. 判定:强证据(网关原文、model's context window、no upstream can accept、

预算与窗口的算术关系)+ 结构化错误码;顺带抽出 requestTokens / windowTokens。

失败里观测到的窗口优先于适配器声明的窗口。

2. 计量:走宿主 token meter,没有就本地估算。

3. 规划:用宿主自己的配对规则算安全切点(工具调用是 assistant/message 里的

tool-call block,结果是 tool/result,镜像事件不动余额)。保留钉住的头部和最近尾部;

遇到未闭合的 compaction 事务直接拒绝出计划。

4. 四档阶梯:

  • T1 用宿主暴露的 pruner 裁剪工具输出(最便宜,零模型);
  • T2 请宿主的压缩引擎做它本该做的事:compactIfNeeded(agent, 'context-overflow', signal);

借来的引擎只有把投影真的变小才算救活,什么都没做的 no-op 会继续落到 T3;

若被代理拒绝(dsh_compaction_refused),记下来,本进程内不再问第二次;

  • T3 写检查点:compaction/start → compaction/prune → user/message(surfaceOp: replace)

→ compaction/end,正文是折叠区间的结构化摘要。

  • T4 当整段超窗的其实只有一条消息(比如刚贴进来的一整个文件)时,没有区间可折:

把那条消息本身换成「开头 + 结尾」的摘录,并在检查点里写明原文长度与被丢弃的中间部分。

只对删掉它仍能保持工具调用配对完整的文本消息生效;孤儿 tool/result 之类一律不碰。

当超窗的其实是一次工具输出时,那次调用和它的结果会一起被换成摘录——单独删掉任一半,

另一半就成了孤儿。

系统提示词永远不会被摘录:把指令换成首尾摘录,等于让请求合法地丢掉这一轮该做什么,所以当窗口里

超出的只剩它时,阶梯报告失败而不是假装成功。一次贴进多段内容也能一趟处理完——折叠先吃掉旧区间,

留在尾部的那条由 T4 截断。而在剩下的内容里,旧历史会先被摘录:这一轮刚打进去的那条消息就是这一轮

要做的事,只有当更早的地方没有足够大的东西能把请求压回窗口时,才会动它。

5. 只重试一次:返回 { kind: 'retry' },让循环用变小后的投影重发同一步。全档失败或

超出每轮预算就 next() 交出控制权,绝不让真实错误被吞掉。

摘要是结构化的,不是复述:错误与失败、涉及的文件与路径、任务清单、关键操作、用户意图、

最近结论。长会话会丢细节,但保留骨架和线索。

工具

工具 · 用途

context_rescue · action: "status" 报告占用、能力与上次急救;action: "rescue" 立刻走一遍阶梯、不等失败——即使投影目前还装得下也会折,因为这是一次显式请求

context_rescue_estimate · 分类一段贴进来的报错文本(可选参数 text),给出从中提取的尺寸,外加本会话的占用情况

context_rescue_fold · 立刻折叠最旧的一段历史(没有参数,与 /rescue 走同一个机械折叠),并报告 token 变化

三个工具的结果在返回前都过一遍 lossless 化:宿主要求工具输出是 lossless JSON,一个 undefined 字段就会让整条结果被丢弃(issue #2)。

工具不抛异常,统一返回带 ok、reason 和计数的报告对象。

斜杠命令

只要宿主提供了命令注册表(/compact 用的那个槽),插件就会注册两个命令。它们是可选的:没有该注册表的 p

…(truncated)

Rendered from the README published on npm. Formatted only, not edited.

Versions

Published on npm::8,最后更新 2026-11-05

Links

GitHub · Homepage · npm

Same category