xiamubumu/dsh-context-reclaim

dsh-context-reclaim

上下文治理:折叠超大工具结果、收拢工具面、稳住提示词缓存命中,长会话不再越聊越贵。

미분류deepseek-harnessdsh

dsh-context-reclaim

Tool results are reclaimed before they enter history, so the prompt cache never goes cold.

⚠️ DSH 1.5 only (>=0.1.5-rc.2 <0.2.0). Newer DSH releases will be supported in follow-up updates.

🟢 Collision risk: very low. It occupies 5 server-side seams and 0 client slots — measured alongside the 19 same-track plugins among the 44 installed here, with no conflict, and it takes no chain slot (the only kind that makes plugins displace each other). See Compatibility.

English · 简体中文


What this is

A DSH session's context is not blown up by the conversation; it is blown up by tool results: a single read of several hundred lines, a single pwsh dumping a wall of logs, a system prompt repeated verbatim every turn. Once this content has entered the history, the only thing left is for the compactor to operate on it after the fact — and compaction has to rewrite already-committed history, which knocks out the whole server-side prefix cache and makes every compaction more expensive than the last.

dsh-context-reclaim moves to a different position: it handles them before the tool result is appended to history, and it never rewrites content that has already been committed. So the prefix cache is never invalidated because of this plugin.

It hooks into 5 seams:

SeamWhat it doesDefault
tools/post-executePurify a tool result the moment it is produced + bound it by content type, inserting a retrieval pointer at the truncation pointon
system-prompt/assembleFold the tool definition surface, stabilize the order of prefix sections, truncate over-long description lineson
tools/pre-executeDuplicate-edit interception (a second identical edit to the same content of the same file is rejected outright)on
session/eventCompaction-summary integrity audit (warns when a compaction summary is missing sections)on
llm/streamOne cost-attribution line per request, printing route / prompt / cache hit / outputon

1. Tool-result purification and bounding

This is the core. Every time a tool call returns, the plugin first performs lossless purification, then bounding.

Lossless purification

Collapses ANSI escape sequences, strips trailing whitespace, normalizes CR. This step is pure purification and loses no information.

It only acts when the content meets both thresholds: length ≥ 32 characters, and a reduction ratio after purification ≥ 0.2%. Small results are not touched at all.

Turn it off: config.noise = false, or the environment variable DSH_RECLAIM_NOISE=0.

Bounding by content type

Different types of tool results have completely different tolerances. The plugin sorts tools into three classes:

ClassTools coveredHead / tail keptPer-line capTotal character cap
Read-onlyread grep glob codegraph_* lsp_*80 lines / 12 lines512160
Side-effectingpwsh bash docker_* job_output40 lines / 40 lines1024256
Faithfulweb_fetch web_search knowledge_* read_image pilot_snapshotleft exactly as-is——

Tools it does not recognize are treated as faithful — better to save nothing than to cause unintended damage.

Wherever content was truncated, a retrieval pointer is inserted telling you how much of the original is left and how it can be retrieved, rather than dropping it silently.

The trigger threshold for bounding is "saves more than 5%"; if it does not save much, nothing is touched.

Turn it off: config.bounded = false, or the environment variable DSH_RECLAIM_BOUNDED=0.

About oversized results: content above config.maxInlineBytes (default 50000) is handed to the spill mechanism to be written to disk. This requires the spill plugin to be installed in your profile — without it, this threshold has no effect and over-limit content passes through unchanged.

The faithful allowlist

The lists for all three tool classes can be replaced wholesale in the configuration:

- id: reclaim
  config:
    readOnlyTools: ['read', 'grep', 'glob']      # a full-table replacement, not an append
    sideEffectTools: ['pwsh', 'bash']
    faithfulTools: ['web_fetch', 'web_search']

Note that this is a full-table replacement, not a per-field merge — writing only headLines will lose the character caps. Either write the whole table or write none of it.


2. Tool definition surface folding

At the start of every session, DSH stuffs the complete definitions (name, description, parameter schema) of every available tool into the system prompt. With dozens of plugins installed, this block can account for a large proportion of it, while most of those tools will not be used at all in the current turn.

By default the plugin folds this part: it keeps the full definitions of the most frequently used tools and compresses the rest into a one-line list (name + one-line description, no parameter schema).

A folded tool is still callable. The one-line list only removes the schema from the prompt — the tool itself stays registered with the host, so the model can still find it with search_tools, call it straight away with invoke_tool, or make it resident again with load_plugin. There is no load_tools tool; search_tools + invoke_tool is the pair. If you would rather keep every schema in the prompt, turn the folding off (next line).

Turn it off: config.foldTools = false, or the environment variable DSH_RECLAIM_TOOL_FOLD=0.


3. Prefix stabilization

A server-side prefix cache only hits when the prefix is byte-for-byte unchanged. Yet the system prompt inherently carries a pile of things that change every turn: token counts, timestamps, session sequence numbers.

By default the plugin flattens these volatile numbers and reorders the sections so that stable sections come first and volatile ones last. Each reorder prints one [reclaim/section-order] line in the log, telling you which sections were moved.


4. Truncating over-long description lines

The system prompt occasionally contains a single exceptionally long description line (long comments written by some plugins). Lines above the threshold (not exposed in config; the environment variable DSH_RECLAIM_PROMPT_LINE_MAX, default 120 characters) are truncated, and the line numbers that were truncated are printed to the log.


5. Duplicate-edit interception

If the model issues exactly the same modification to the same content of the same file twice in a row, the second one is intercepted and an explanation is returned — such a call is almost certainly a meaningless duplicate that burns a whole round trip for nothing.

Log line: [reclaim/dedup-deny].

Turn it off: the environment variable DSH_RECLAIM_DEDUP_DENY=0.


6. Compaction-summary integrity audit

If the summary produced by the compactor is truncated by the output budget, it silently becomes shorter — you receive no notice at all, the next turn simply starts with a piece of the context missing.

After every compaction the plugin audits whether the summary contains the structural sections it should have, and warns when any are missing:

[reclaim/summary-audit] ⚠️ 摘要缺 3/8 段

It only warns, it does not modify the summary. What actually prevents summaries from being truncated is dsh-aux-degrade; see the interaction section below.


7. Cost attribution

One line is printed at the end of every request:

[reclaim/usage] route=aux prompt=12043 hit=11890 miss=153 out=204 purpose=compaction

hit / miss are the prefix-cache hit and miss character counts, and purpose is the purpose of this call (if it has one). When the cache is broken, an extra [reclaim/cache-break] line is printed.

This log is the fastest entry point for diagnosing "why is my cache hit rate not high?".


8. Writable-request takeover — no dsh-llm patch required

This is the reason you can install the plugin and stop patching files.

On a stock @deepseek-ai/dsh@0.1.5-rc.2 install the main-path conversation request is deep-frozen by the host before it reaches @deepseek-ai/dsh-llm. Its entry point is:

stream(options) { return this.streamWithRegistration(options) }
streamWithRegistration(options, prepared) {
  return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared))
}

The waterfall's next is a closure that ignores its argument and the adapter reads the outer options. So a plugin listening on llm/stream can read the request but cannot rewrite it — writing to a frozen object throws, and passing a new object to next(...) does nothing at all. That is why the desktop build of DSH has always shipped with a local file patch to dsh-llm/lib/index.js whose entire body is six lines: copy the frozen request shallowly, copy the messages array, re-mark it with markAgentLoopRequest, and hand the copy to the waterfall.

Version 1.1.0 does the same thing from the plugin layer. On load, this plugin wraps ctx.llm.streamWithRegistration once:

  • request is frozen → shallow copy + messages array copy + every message object copied, then re-marked through markAgentLoopRequest taken from the host's own dsh-llm module, and the copy enters llm/stream;
  • request is not frozen (host already patched) → passed through untouched, byte for byte. Zero behaviour change;
  • anything unexpected → logged and passed through untouched. This plugin never turns a working session into a broken one.

Because of this, an unpatched install gets a writable request without a single file being touched. Measured A/B on a stock runtime that carries no patches at all (dsh --profile headless, real request, [reclaim/wire] line):

# takeover on  (default)
[reclaim/wire] 入口探测:messages=5 条 · options 冻结=false · messages 冻结=false · route=deepseek-official|deepseek-flash|main|ba8a6419
# takeover off (DSH_RECLAIM_WRITABLE_REQUEST=0)
[reclaim/wire] 入口探测:messages=5 条 · options 冻结=true  · messages 冻结=true  · route=deepseek-official|deepseek-flash|main|d7d22e75

Turn it off with DSH_RECLAIM_WRITABLE_REQUEST=0. The startup log tells you exactly what happened:

[reclaim/writable] 已接管 llm.streamWithRegistration:冻结的主请求先浅拷贝再进 llm/stream ⇒ 不再需要给官方 dsh-llm 打文件补丁(可用 dsh-llm 副本 2 份)

How the host's own module is found

markAgentLoopRequest writes into a WeakSet that lives inside one module instance, and Node creates a separate instance for every distinct module URL. Marking a copy with a foreign instance is useless: the object simply is not in the set your other plugins check. So this plugin

  1. derives candidate locations from process.argv (the real …/@deepseek-ai/dsh/lib/bin.js — on the desktop shell it sits in argv[2], not argv[1]), from createRequire resolution of the host entry, and from $DSH_HOME/profiles{/*}/node_modules;
  2. imports each candidate and round-trip tests it against the actual request: the first copy whose isAgentLoopRequest(options) returns true is the host's own instance and is used;
  3. if none matches, it warns once and takes over anyway with the first copy. Honest limitation: in that case context rewriting still works, but @deepseek-ai/dsh-session-title may stop auto-titling sessions, because it checks the same WeakSet. This has been observed on one npm-style install layout and not on the desktop layout. Nothing else in DSH consumes that marker.

The takeover is skipped automatically if an invariants service is present (the dsh-agent-loop frozen-contract assertion would otherwise abort your session). No shipping 1.5 profile loads it, but the plugin steps aside rather than breaking a session if it ever appears.

The startup patch check — it reports, it never patches

On load the plugin inspects the official patch targets it can locate and prints exactly one line:

[reclaim/patch-check] 官方补丁体检(只读,本插件不会自动改官方文件):7 个目标里未检测到 7 条 —— dsh-llm 可写请求、agent-loop 快照回收 / 系统提示收敛、…

It is deliberately read-only. This plugin is not a patcher: modifying files inside your DSH installation is your decision, not a side effect of installing a plugin. The check is a heuristic (it looks for the *.orig backups a patch script leaves next to the file it touched), so it says "not detected" rather than "not patched", and it stays completely silent when it cannot work out your install layout. Silence it with DSH_RECLAIM_PATCH_CHECK=0.

When something is missing and you do want it, do not hunt for a script — hand the job to your own agent. The file shipped inside this package, tutorial-install-and-official-patches.md (Chinese original: tutorial-install-and-official-patches.zh-CN.md), contains a ready-to-paste prompt describing each patch, its target, and its acceptance check; your agent reads it and patches your machine. That is the recommended route, because your agent can see your actual installation while a hard-coded script inside a plugin cannot.

The patch library — all ten official patches, published separately → https://github.com/xiamubumu/dsh-patch-library The patches themselves, an adaptive applier, five ways to apply them and a one-command revert. node apply.mjs --check is a read-only health check, --apply backs every file up and runs node --check on each one, --revert puts everything back.

The other official patches: what a plugin can and cannot replace

Stated plainly, because installing these plugins does not make the other file patches unnecessary:

Official 1.5 file patchEditsCan a plugin replace it?
@deepseek-ai/dsh-llm — writable requestlib/index.js✅ Yes — done by this plugin, see above
@deepseek-ai/dsh-client-ui-sidebar — flex-wrap on the footer slotlib/lib-client.js✅ Yes (a client plugin can inject CSS); not shipped here
@deepseek-ai/dsh-agent-loop — runtime-snapshot reclaim, system-prompt convergencelib/index.js❌ No — needs the loop's private projection class and its commit point
@deepseek-ai/dsh-llm-pi-ai — splitSystemPrompt collects every leading system messagelib/index.js❌ No, and it is pointless without the dsh-agent-loop patch
@deepseek-ai/dsh-compaction-tool-result-pruner — result pruning + load-bearing guardlib/index.js❌ No — the host instantiates the exported class directly; there is no registration seam
@deepseek-ai/dsh-client-ui-renderer — conversation.chat.turnTail chain electionlib/client.js❌ No — the official client bundle itself must be edited
@deepseek-ai/dsh-session-format-v1-to-v2 — turn/start repairlib/index.js❌ No — runs inside the session restore path
@deepseek-ai/dsh-session-persistence-jsonl — the same, persistence sidelib/worker.cjs❌ No — runs in a worker thread
@earendil-works/pi-ai — max_tokens floordist/api/simple-options.js❌ No — third-party dist, its ESM namespace is read-only

In short: the one patch that these plugins had to depend on is gone. The rest are independent improvements that change how DSH's own machinery behaves, and they remain optional. To apply any of them, use the patch library: https://github.com/xiamubumu/dsh-patch-library.


9. The official-patch notice: one dialog, naming the patch and how to apply it

The official patches this plugin benefits from are announced by this plugin itself, in one dialog. No other plugin speaks for it, and it never speaks for another.

  • Which patches: each row gives the patch id, the package and file it edits, and why this plugin wants it (marked required or recommended);
  • How to apply them (any one of three): A press "Copy the prompt" and hand the whole block to your own agent (it runs a read-only health check first, applies one patch at a time, and stops to ask you if a hunk does not match); B use the patch library script, node apply.mjs --check then node apply.mjs --apply --only <id>; C apply by hand, cd <package> && git apply -p1 <library>/patches/<id>/change.diff;
  • The patch library: https://github.com/xiamubumu/dsh-patch-library (ten self-contained patches, an adaptive applier, five ways to apply them);
  • Detection: judged from the *.orig backup next to each target file plus a content hash against the library manifest (pristine / patched). When neither can decide, it says "cannot tell" outright and never guesses "applied";
  • Once applied, it stops appearing. The dialog offers "Later" (this once) and "Do not remind me again" (permanent, recorded per patch id — a patch added later still gets announced).

The same dialog also lists the other official patches (owned by no plugin — apply them by symptom), so nothing goes unseen.

Configuration

The configuration file is the profile's cordis.patch.yml:

- id: reclaim
  config:
    enabled: true
    noise: true
    bounded: true
    stripLines: false
    noOpGuardRatio: 0.05
    maxInlineBytes: 50000
    attribution: true
    trimSections: false
    foldTools: true
    activePlugins: []
KeyTypeDefaultEffect
enabledbooleantrueMaster switch (!== false counts as on)
noisebooleantrueLossless purification (ANSI / trailing whitespace / CR)
boundedbooleantrueBounding by content type + retrieval pointer
stripLinesbooleanfalseStrip line-number prefixes
noOpGuardRationumber0.05The "only act if it saves enough" threshold for bounding, 5%
maxInlineBytesnumber50000Above this byte count, content goes to spill (spill plugin required)
readOnlyToolsstring[]see the table aboveRead-only tool list, a full-table replacement
sideEffectToolsstring[]see the table aboveSide-effecting tool list, a full-table replacement
faithfulToolsstring[]see the table aboveFaithful tool list, a full-table replacement
attributionbooleantruePrint the [reclaim/usage] attribution line
trimSectionsbooleanfalseSection trimming (note: the real gate is the environment variable DSH_RECLAIM_SECTION_TRIM, see below)
foldToolsbooleantrueFold the tool definition surface
activePluginsstring[][]Plugin list participating in section-ownership computation

⚠️ The value of the trimSections option does not actually open the gate — the gate in the code only honours the environment variable DSH_RECLAIM_SECTION_TRIM, and it additionally requires activePlugins to be non-empty. The old documentation got this one wrong; it is stated accurately here.

Common environment variables

There are more than 40 environment variables in total, covering every internal threshold. Grouped by function, the most commonly used ones are:

VariableDefaultEffect
DSH_RECLAIM_NOISE1Set to 0 to turn off lossless purification
DSH_RECLAIM_BOUNDED1Set to 0 to turn off bounding
DSH_RECLAIM_STRIP_LINES0Set to 1 to strip line numbers
DSH_RECLAIM_TOOL_FOLD1Set to 0 to turn off tool definition surface folding
DSH_RECLAIM_PROMPT_LINE_MAX120Truncation threshold for a single system-prompt line
DSH_RECLAIM_DEDUP_DENY1Set to 0 to turn off duplicate-edit interception
DSH_RECLAIM_SECTION_TRIM0Set to 1 to turn on section trimming (works together with activePlugins)
DSH_RECLAIM_STABILIZE1Set to 0 to turn off prefix stabilization
DSH_RECLAIM_WRITABLE_REQUEST1Set to 0 to stop taking over llm.streamWithRegistration (see section 8); the official dsh-llm patch is only needed if you turn this off
DSH_RECLAIM_PATCH_CHECK1Set to 0 to silence the one-line "official patch check" notice printed at startup
DSH_RECLAIM_LOG1Set to 0 to turn off this plugin's log output
DSH_RECLAIM_DUMP_SYSTEM0Set to 1 to dump the assembled system prompt to a temporary directory, for diagnosis

The remaining threshold variables (sampling line counts, budget observation, pollution-detection window, archive limits, reasoning stripping, etc.) each have a per-item comment at the top of the source.


📚 Tutorials

DocumentWhat it covers
Install & official patches (English)How to install (two verified routes) and what to do about the official patches — including prompts that let your own agent apply them the patches themselves ship as a library: https://github.com/xiamubumu/dsh-patch-library
安装与官方补丁(中文)the same, in Chinese 补丁本体已开源成补丁库:https://github.com/xiamubumu/dsh-patch-library

🔗 The rest of the family

Each plugin installs on its own; together they save the most. The other three:

  • dsh-aux-degrade — Auxiliary-call degradation: summaries and titles stop burning their budget on reasoning
  • dsh-capability-composer — Capability composer: switchable tool/skill/MCP sets instead of one bloated surface
  • dsh-composer-launcher — Launcher: one panel for compositions, plugins and MCP, plus the extension walkthrough

All of them are DSH 1.5 only; newer DSH releases will be supported in follow-up updates. Plugins that need official patches announce them in the UI once you install them — nothing goes unseen.

Installation

dsh plugin --profile web add github:xiamubumu/dsh-context-reclaim

One command. DSH registers it into the profile's dsh.profile.bundles by itself; no config to hand-edit.

From a local tarball (air-gapped machines)

Download dsh-context-reclaim-1.1.0.tgz from Releases, then:

dsh plugin --profile web add /path/to/dsh-context-reclaim-1.1.0.tgz

From the DSH plugin marketplace / npm (not yet possible)

The marketplace submission (an awesome-dsh-plugin PR) has not been filed and nothing is published on npm yet ⇒ neither route works today. Once it is listed/published, installing from the marketplace, or dsh plugin --profile web add dsh-context-reclaim, becomes valid.

⚠️ Known issue: do not feed the release asset's tarball URL to dsh plugin add. dsh plugin --profile web add https://github.com/.../releases/download/v1.1.0/....tgz fails on the pnpm DSH bundles (10.34.5) with ERR_PNPM_MISSING_TARBALL_INTEGRITY (pnpm#14351, fixed upstream only in pnpm ≥ 12.5.1). Installing a second plugin that way always fails — use the two routes above instead.

After installing you need to restart DSH — host-side plugins are only loaded when DSH starts.

This plugin has no runtime dependencies at all; peerDependencies only declares the official @deepseek-ai/dsh-tools. Installation will not stuff any third-party package into your profile.


Compatibility

Coexistence: does it fight other plugins? (measured 2026-10-15)

**This plugin hook