Replaces the built-in compaction provider: at a configurable fraction of the context threshold (85% by default) a detached task summarizes everything up to the previous turn, and the next step boundary commits that held summary as a checkpoint with zero model calls, so the swap itself takes milliseconds. /bcompact \[message] starts the same generation on demand and then sends the message. A settings page sets the trigger ratio, the summarization model, and how many consecutive failures stop background generation until a half-open probe gets through. Subagent sessions, a changed prefix or an expired summary fall back to the built-in synchronous path.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-background-compaction
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:OZONGO/dsh-background-compaction
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Summarize history before the context hits the threshold, so the next turn can use the summary — no stall, no waiting.
A DeepSeek Harness (DSH) plugin. It takes over when compaction happens: at 85% of the threshold it spends one nearly-fully-cached model call summarizing everything up to the previous turn in the background; at the next step boundary it commits that summary as a checkpoint with zero model calls. The conversation stays usable the whole time.
- Automatic: background generation starts at 85% of the threshold → applied on the next turn
- Manual:
/bcompact <message>→ answers immediately, applied on the next turn - Settings page: auto trigger ratio, summarization model, failure circuit breaker
- After compaction the model sees: summary + the previous turn verbatim
1. The problem
DSH's built-in compaction (dsh-compaction-basic) measures pressure at agent/pre-step, and once the threshold is crossed it writes the summary synchronously before the turn continues. The user sees a stall.
The slow part is not the input (the prefix hits the provider KV cache) — it is the output. So the only effective optimization is to move generation into a window where nobody is waiting.
Turn N-1 (context reaches 85%)
├─ built-in compaction: below threshold → does nothing
└─ this plugin: detached task summarizes [first node .. end of previous turn]
↓ (30–60s, the user keeps chatting)
Turn N (summary ready)
└─ this plugin: pre-step commits the held summary
├─ compaction/start
├─ summarize() → returns the held summary (0 model calls)
├─ compaction/summary + user/message(surfaceOp:replace)
└─ compaction/end ← milliseconds
this turn sends: system + summary + previous turn + new message
2. Install
2.1 From a local directory (development)
cd ~/.dsh/profiles/desktop
pnpm add file:E:/AI/workspace/dsh-plugin/dsh-background-compaction
2.2 From Git
cd ~/.dsh/profiles/desktop
pnpm add github:OZONGO/dsh-background-compaction
2.3 Register the bundle
Add the package to the profile's package.json:
{
"dsh": {
"profile": {
"bundles": ["...", "dsh-background-compaction"]
}
},
"dependencies": {
"dsh-background-compaction": "file:E:/AI/workspace/dsh-plugin/dsh-background-compaction"
}
}
Then restart DSH. Host code is not hot-reloaded: hmr.config.root defaults to [], and module-level HMR additionally requires --expose-internals.
2.4 Confirm it loaded
The host log (%APPDATA%/DSH Desktop/logs/host/dsh-YYYY-MM-DD.log) should contain:
[background-compaction-engine] background-compaction: engine ready (auto=true, startRatio=0.85, maxFailures=2, state=…)
The settings panel should gain a Background compaction page.
3. Usage
3.1 Automatic
Nothing to do. When the context reaches thresholdTokens × autoTriggerRatio, background generation starts; the next step boundary after the summary is ready applies it.
Where the threshold really is: DSH's
thresholdTokensis notcontextWindow × 0.8. For a 200k window:messageBudget = 200000 - 65536,pressureBudget = 134464 - 65536, sothresholdTokens = min(160000, 68928) = **68928**. The 85% trigger is ~58.6k, not 170k. Every ratio in this plugin is relative tothresholdTokens; it never uses an absolute token constant.
3.2 /bcompact [message]
| Input | Behaviour |
|---|---|
/bcompact keep going |
Background compaction starts; keep going is answered immediately as a normal user message; the next turn uses the summary |
/bcompact |
Triggers compaction only, no turn |
Command handlers run outside any turn, so they cannot compact synchronously (compactRegion requires an open turn). This command starts the detached task and calls agent.followup(message).
3.3 Settings page
Settings → Background compaction, three fields, applied live without a restart:
| Field | Range | Meaning |
|---|---|---|
| Auto trigger ratio | 0.1 – 0.99 | Starts background generation at thresholdTokens × ratio; default 0.85 |
| Summarization model | provider / model, or "follow the conversation" | Defaults to the model the conversation is using |
| Failure circuit breaker | 0 – 10 | Stops background generation after this many consecutive failures; one probe is allowed after the cooldown (5 min by default) and a success clears the count |
Settings live in ~/.dsh/background-compaction/config.json and can be edited by hand:
{
"$schema": 1,
"autoTriggerRatio": 0.85,
"summarizationModel": { "provider": "", "model": "" },
"maxFailures": 2
}
Precedence: settings file > YAML config > built-in defaults. An empty summarizationModel pair means "follow the conversation".
4. Verifying it works
No long conversation required — three steps:
| Step | Action | Expected |
|---|---|---|
| 1 | New session, 2–3 turns (reading one file is plenty — a few hundred to a few thousand tokens) | Normal |
| 2 | Type /bcompact keep going |
Answer arrives immediately; log shows held ready |
| 3 | Send another message | That turn commits the summary at pre-step; log shows swap hit; last-swap.json appears |
/bcompactneeds a non-empty, balanced compactable span (absolute floor 512 tokens); with less history it answers "not enough history to compact". The automatic path has a higher floor: the summarized span must be at least 10% ofthresholdTokens(~6.9k tokens on a 200k window).
Evidence file ~/.dsh/background-compaction/last-swap.json:
{
"origin": "bcompact",
"range": { "start": 4, "end": 88 },
"shadowedTokens": 41230,
"shadowedTokenCount": 40120,
"tailTokens": 4310,
"messages": [
{ "role": "system", "chars": 1180, "head": "You are a coding agent…" },
{ "role": "user", "chars": 1900, "head": "This is an automatically generated checkpoint…" },
{ "role": "user", "chars": 220, "head": "keep going" },
{ "role": "assistant", "chars": 3400, "head": "Sure — let me look at…" }
]
}
messages is session.deriveMessages() taken after the commit. The surface is a pure projection, so this is exactly what the next turn sends.
rangeis the replaced surface span (result.shadowedRange), not the seq of thecompaction/startmarker.shadowedTokensshares its unit withtailTokens(route pricing);shadowedTokenCountis the heuristic count the engine writes to the durable log.
Log markers:
background-compaction: engine ready … plugin loaded
background-compaction: started … background generation started
background-compaction: held ready … summary ready
background-compaction: swap hit … committed with 0 model calls
background-compaction: discarded … discarded (reason=…)
background-compaction: summarize miss … fingerprint missed; real call
5. How it attaches to DSH
Key fact: the Web app puts compaction inside agent presets, not the host plane.
# dsh-web-app/cordis.patch.yml
- id: compaction-basic
disabled: true # disabled in the host plane
# dsh-web-app/presets/standard.patch.yml
- id: compaction
name: cordis:group
group: true
isolate: { compaction: true, toolResultPruner: true }
config:
- id: compaction-basic
name: '@deepseek-ai/dsh-compaction-basic'
- id: command-compact
- id: tool-result-pruner
So each agent's ctx.compaction comes from its preset and is isolated. This plugin mounts in the host plane and works through agent/pre-step (events bubble to the root):
- it commits the summary through the inherited
compactRegionitself — it does not need to be the agent'sctx.compaction; - the preset's built-in engine stays mounted as a fallback for the cases where the summary did not arrive in time, context overflow, and
/compact; - both use the same
tokenMeterand threshold arithmetic, so on the normal path the built-in engine never fires first.
This is deliberate: take over compaction behaviourally, while keeping a fallback that cannot corrupt anything.
5.1 What is overridden
| Member | Override | Why |
|---|---|---|
static Config |
yes | adds the background section |
constructor |
yes | strips background, loads settings, registers hooks |
compactIfNeeded |
yes | prefers a held summary on the pressure path |
summarize |
yes | returns the held summary on a fingerprint hit |
compactNow |
no | it runs with owner: null, while the public compactRegion requires an open turn — it cannot swap |
compactRegion |
no | the durable transaction must stay untouched |
5.2 Invalidation matrix
A summary is only used while the prefix still matches; otherwise it is discarded. Discarding costs one extra call — using a stale one would corrupt the model's view.
| Condition | Result |
|---|---|
state !== ready |
not swapped |
| fingerprint differs from this input | summarize performs a real call |
| routed provider/model changed | discard |
| surface head node changed | discard |
tail exceeds tailRatio × thresholdTokens |
discard |
older than holdTtlMs |
discard |
unmatched compaction/start exists |
do not start |
unmatched compaction/start predates session/end-seed |
stale marker from an earlier process lifecycle — not a lock |
| consecutive failures ≥ limit | stop background generation until failureCooldownMs lets one probe through |
the swap hits another compaction's lock (busy) |
keep the summary and retry at the next step boundary |
In addition, any compaction/start (including /compact and overflow recovery) aborts an in-flight summary immediately.
6. Configuration reference
6.1 YAML (bundle patch or profile patch)
- id: background-compaction
name: 'dsh-background-compaction'
config:
# every BasicCompactionEngine key also works here:
# thresholdRatio, headroomTokens, retainRatio, retainTokens,
# summarizationProvider, summarizationModel, maxTokens,
# compactionRetries, maxOverflowRetries, modelPolicies, auto
background:
startRatio: 0.85 # overridden once the settings page writes autoTriggerRatio
tailRatio: 0.5 # verbatim tail cap, relative to thresholdTokens
minShadowedRatio: 0.1 # lower bound for the summarized span
cacheWarmMs: 300000 # cold-cache gate
holdTtlMs: 900000 # maximum age of a ready summary
maxSessions: 8 # sessions that may hold a summary
maxFailures: 2 # overridden once the settings page writes maxFailures
failureCooldownMs: 300000 # half-open probe delay once the breaker is open
6.2 Subagents
Subagents (delegationDepth > 0) do not precompute in the background; they keep the built-in synchronous compaction. Their contexts are short, so precomputation would cost more than it saves.
7. Compatibility
- Changes only when compaction happens; checkpoint rendering, log format, and pricing rules are untouched.
/compactkeeps the official synchronous semantics.- The tool-result pruner is untouched.
- Context overflow still recovers synchronously through the official path.
auto: falsedisables background work entirely.- Only two methods are overridden; when
@deepseek-ai/dsh-compaction-basicupgrades, runnpm testfirst (it includes a real-Session swap test).
8. Development
npm test # 116 cases: pure functions + real Cordis context + real-Session swap
Highlights:
test/spec.test.js— pins the 200k-window threshold to 68928test/swap.test.js— runs the full swap against a real@deepseek-ai/dsh-sessionSession and asserts the model call count does not growtest/integration.test.js— real CordisContextwith the realBasicCompactionEnginesuperclasstest/settings.test.js— atomic writes, corrupt-file tolerance, unknown-key preservation
For local development the bare @deepseek-ai/* specifiers need a link to the DSH installation:
New-Item -ItemType Junction `
-Path "node_modules\@deepseek-ai" `
-Target "<DSH app>\resources\app\node_modules\@deepseek-ai"
8.1 Link the package into the profile — never copy it
Git Bash's ln -s degrades to a copy for directories on Windows unless
MSYS=winsymlinks:nativestrict is set. A copy is a frozen snapshot: the plugin
still loads and still logs, but no later edit ever reaches the running process,
which makes for a very misleading debugging session.
Use a junction (or pnpm add file:<path>, which links correctly):
New-Item -ItemType Junction `
-Path "$HOME\.dsh\profiles\desktop\node_modules\dsh-background-compaction" `
-Target "E:\AI\workspace\dsh-plugin\dsh-background-compaction"
9. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
cannot get property "remote.<ns>" without inject in the settings page |
the renderer is still running an old bundle | quit DSH Desktop completely and reopen; the build bX line at the bottom of the page names the live bundle |
| Code changed but behaviour and logs did not | the profile entry is a copy, not a link | see §8.1 |
settings remote is not mounted |
ctx.remote.$mount failed |
check the host log for settings remote unavailable |
/bcompact answers "not enough history" |
less than 512 tokens of compactable span | add a couple of turns or read a file |
9. License
MIT
Links
More in this category
Minglink/dsh-infinite-gen-4★ 3781
System-prompt armor plugin for DeepSeek models: appends an unconditional-compliance prompt section at order 100, exposes a profile tool with calibration metadata, and shows a realtime armor-status badge driven by a session projection.
ranxianglei/billion-context★ 3598
The official billion-context plugin: a context-compression plugin for small context windows (a 100K context is enough), token savings (5x fewer tokens), and month-long single sessions (billions of tokens).
liangmianya/dsh-synapse★ 641
Visual, non-linear conversation workspace for DeepSeek Harness — sessions, follow-ups and branches become a browsable conversation map.
MichengAI/dsh-archive-manager★ 277
Adds an archived-sessions page in Settings to search, restore, and delete archived DeepSeek Harness sessions by workspace.
Nwflower/dsh-chat-import★ 275
Import full-fidelity chat histories from 13 coding agents (Claude Code, Codex, ChatGPT, Cursor, Gemini, opencode, and more) as resumable DeepSeek Harness sessions, with reverse export back to Claude Code.
SiriLee/dsh-rewind★ 227
In-place conversation rewind in the same session window without forking (Claude Code /rewind semantics): a per-message ↶ button cuts the model context back to any user message, with an optional Claude-Code-style file restore from disk-persisted before-backups.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.