dsh-context-ops
A DeepSeek Harness (dsh) plugin for context management:
handoff, compaction decision and diagnosis skills, a self-contained pressure tool, three slash commands, and a pressure toast in the Web Client.
> 中文版见 [README.md](README.md)。
---
## Why this plugin
| Problem | What it does |
|---|---|
| Sessions get slower but you don't know when to compact | "Should I compact?" → reads `context_pressure`, applies the ladder, gives one prescription |
| Compaction loses constraints / fails, without a clear reason | "Context diagnosis" → layer-by-layer check + hard rule + three failure causes |
| You re-read old chats across days or sessions | "Handoff" → a 5-line brief written to `CHECKPOINT.md` |
| Readings depend on a machine-specific external script | `context_pressure` reads DSH's built-in `tokenMeter`, so it works anywhere |
| Switching models then compacting still uses the old model | The diagnosis skill says: run one normal turn first, then `/compact` |
## Features
| Capability | Name | Implementation | Notes |
|---|---|---|---|
| Handoff skill | `context-handoff` | `lib/skills/handoff.js` | Writes a 5-line brief (goal / status / next / open / evidence) to `CHECKPOINT.md` |
| Decision skill | `context-decision` | `lib/skills/decision.js` | Reads numbers → applies the ladder → gives **one** prescription |
| Diagnosis skill | `context-diagnose` | `lib/skills/diagnose.js` | One check-up, layer by layer, **full report** |
| Pressure tool | `context_pressure` | `lib/index.js` | Self-contained; also reports window evidence (`windowSource` / `routeKey`) |
| History tool | `context_history` | `lib/index.js` | Peak / last input-side tokens, compaction attempts / successes / failures |
| Slash commands | `/handoff` `/pressure` `/diagnose` | `lib/index.js` | Explicit triggers (skipped when no `commands` service) |
| Pressure toast (opt-in) | `enableReminder` | `client.js` + projections | Web Client corner badge: suggests `/compact` at `remindAt` and handoff at `handoffAt`; auto-hides after 60 s, instantly on compaction |
| Compaction model | `compaction-basic` config | documented below | Native DSH fields; **no hard-coded provider/model** |
## Quick start
```sh
# 1) Install (replace with yours, e.g. desktop / web)
dsh plugin --profile add dsh-context-ops
# 2) Restart DSH (profile changes take effect on restart)
# 3) Try a trigger
# "handoff" / "should I compact?" / "context diagnosis"
# or the slash commands: /handoff /pressure /diagnose
```
Local development install:
```sh
dsh plugin --profile add "file:/absolute/path/to/dsh-context-ops"
```
## Skills
### 1) Handoff `context-handoff`
**Triggers**: handoff, wrap up, archive, checkpoint, resume project, continue last night's project.
**Output**: a five-line `CHECKPOINT.md` table — goal, status, next steps, open questions, evidence.
**When not to use**: 1–2 turn tasks, pure Q&A with no project state, or when the user explicitly says no brief.
### 2) Context decision `context-decision`
**Triggers**: context management, should I compact, should I hand off, what about the context, how much pressure, how much room left.
**Flow**: call `context_pressure` for real numbers, apply the ladder, and give one prescription (what + why + which number); when compaction is possible, **copy `/compact` to the clipboard** (`pbcopy` on macOS, otherwise prompt the user to type it), and when it is not, recommend a handoff.
### 3) Context diagnosis `context-diagnose`
**Triggers**: context diagnosis, diagnose, context check-up, check-up, ctx-check, take a look at my context.
**Output**: a diagnosis report (status / band / can-compact / should-compact / warnings); copies `/compact` to the clipboard when compaction is possible, recommends a handoff otherwise.
## The `context_pressure` tool
Called with no arguments, it returns:
```json
{
"pressureTokens": 82341,
"surfaceTokens": 71022,
"window": 800000,
"declaredWindow": 1048576,
"realWindow": 800000,
"windowInflated": true,
"windowSource": "override",
"routeKey": "zicode/deepseek-v4-pro",
"pct": 10.3,
"cacheHitRate": 0.995,
"provider": "zicode",
"model": "deepseek-v4-pro",
"note": ""
}
```
| Field | Meaning |
|---|---|
| `pressureTokens` | Total request-body tokens (the real compaction signal) |
| `surfaceTokens` | Surface-only token count |
| `window` | Effective window = `min(declaredWindow, realWindow)`; `null` when unknown |
| `declaredWindow` | Window declared by the route / profile; `null` when unknown |
| `realWindow` | Route limit: **the configured measured value wins**, otherwise the built-in nominal value; `null` when both are unknown |
| `windowInflated` | The declared window exceeds the limit the plugin believes in (`declaredWindow > realWindow`); the meaning depends on `windowSource`, see below |
| `windowSource` | Evidence for the number: `override` measured config / `nominal` built-in nominal / `declared` declaration only / `unknown` |
| `routeKey` | Current route key `provider/model`, the key to use in `realWindows` |
| `pct` | Usage percent = `pressureTokens / window * 100`; `null` when unknown |
| `cacheHitRate` | Read-cache share of input cache buckets (0–1) = `cacheReadTokens / (cacheReadTokens + cacheWriteTokens + uncachedInputTokens)`; `null` when unknown |
| `provider` / `model` | Current route; empty string when unknown |
| `note` | Error text when the reading failed; empty otherwise |
> **The plugin does not decide whether a provider is official or a relay.** `provider` is only a profile key; the plugin cannot see the base URL and must not guess. The same model can have different capacity on different routes (manual §1.5). `windowSource=nominal` only means the number is a built-in nominal value and has not been measured on this route.
>
> To make `pct` use measured capacity, add a `provider/model` entry to `realWindows` using the needle test from manual §1.6 and a safe value around 80% of the measured pass (manual §1.2). Unconfigured routes fall back to the old nominal-table behavior.
> A high `cacheHitRate` (near 1) means the prefix is being reused — cheaper and faster first token. A persistently low rate means the context keeps being rewritten (or the provider does not cache), a signal to consider a fresh session.
> It is a **session cumulative** figure: the first cold request is always 0% and dilutes as the session grows.
> `windowInflated` means the declared window exceeds the limit the plugin is using, but **interpret it with `windowSource`**:
>
> - `windowSource=nominal`: the declaration exceeds the model's nominal value. The host still uses the declared window for auto-compaction, so it may never trigger; fix the profile or add a route-specific measurement.
> - `windowSource=override`: the declaration exceeds the **measured safe value** for this route. `pct` uses the measured value, but **host auto-compaction still uses the declaration**, so its trigger is later than the measured safety boundary. To fix host behavior too, set the profile `contextWindow` to the measured value; `realWindows` alone only corrects this plugin's reading.
> - The opposite case is legitimate: deliberately capping a large-window model (1M → 128k) yields `windowInflated=false` and `window` stays at the smaller value.
>
> **Two rulers:** `pct` divides by `window` (which a measured value can narrow), while host auto-compaction divides by `declaredWindow`. When `window` is narrowed, do not infer that auto-compaction is broken just because `pct` passed 80% and nothing was compacted.
> `windowInflated = null` means either the model is not in the built-in table, or this session has no routed window yet. **Both mean "undecidable" — never read it as false/safe.**
> Use `pressureTokens ÷ window` for compaction decisions, **not** the `projectedTokens` shown in the UI (it under-reports).
## History tool `context_history`
Takes no arguments; returns session-level trend and compaction records:
| Field | Meaning |
|---|---|
| `peakTokens` / `lastTokens` | Peak / last-request **input-side tokens** (input + cacheRead + cacheWrite, excluding output) |
| `compactionCount` | Compaction attempts (`compaction/start`) |
| `compactionSuccess` | Successful compactions (`compaction/summary`) |
| `compactionError` | Failed compactions (`compaction/end` carrying `error`) |
> `peakTokens` and `context_pressure.pressureTokens` are **not the same ruler**: the latter includes output budget plus tools/system prompt, so it is systematically larger. Compare trends, never subtract one from the other.
> Why failures are counted that way: the host only persists `compaction/start` / `summary` / `end`, and a failure is recorded as `compaction/end.data.error`. `compaction/summary-error` is merely an in-memory waterfall hook name that never reaches the log — counting it would always yield 0.
## Slash commands
| Command | Equivalent trigger | Effect |
|---|---|---|
| `/handoff` | "handoff" | Starts the 5-line brief flow |
| `/pressure` | "should I compact?" | Calls `context_pressure` and gives a prescription |
| `/diagnose` | "context diagnosis" | Runs the full check-up |
## Decision ladder

**Experience line (not a success guarantee)**: when no additional failure evidence exists, `pressureTokens < window × 0.95` means compaction is worth attempting; it does not guarantee success. Summary-model limits, output budget, gateway body limits, timeouts, or a non-smaller summary can still fail.
**⚠ This ladder is not the host's threshold.** DSH's own `compaction-basic` auto-compacts at
`min(contextWindow × 0.8, contextWindow − reserved output − 65536)` — roughly **838k (80%)** for a 1M
window; for a 131k window, the 65,536-token headroom alone caps the threshold at **~65,536**, before any reserved output. In normal automatic mode the "85% → prepare a handoff" band is
usually unreachable: by the time you see 85%, compaction already happened. The ladder here is
**advice** and does not change host behaviour; to move the trigger, configure `thresholdRatio` /
`headroomTokens` on `compaction-basic` in your profile.
**Three failure causes**:
1. `context overflow` → switch to a larger-window model and retry, or hand off;
2. `stream idle timeout` → do not retry; hand off;
3. `summary is not smaller` → already at the limit; ignore; only a new session can reduce it.
## Architecture
```mermaid
flowchart LR
subgraph DSH["DSH Runtime"]
S[skills registry]
T[tools registry]
C[commands]
M[tokenMeter]
PR[contextPressure projection]
end
subgraph W["Web Client"]
SL[conversation.input.dock slot]
BN["pressure toast (client.js)"]
end
subgraph P["dsh-context-ops"]
H["context-handoff"]
D["context-decision"]
G["context-diagnose"]
PT["context_pressure"]
HT["context_history"]
CM["/handoff · /pressure · /diagnose"]
RC["contextOpsReminderConfig projection"]
CS["contextOpsCompactionSignal projection"]
end
H --> S
D --> S
G --> S
PT --> T
HT --> T
PT --> M
CM --> C
RC --> PR
CS --> PR
BN --> SL
BN -.reads.-> RC
BN -.reads.-> CS
```
```mermaid
flowchart TD
A["User: should I compact / diagnose"] --> B["Call context_pressure"]
B --> C{"Can compact?
pressureTokens < window × 0.95"}
C -- Yes --> D["Apply the ladder, give a prescription"]
C -- No --> E["Hand off / switch to a larger window"]
D --> F{"Is the prescription an action?"}
F -- Yes --> G["Copy /compact to the clipboard (or prompt to type it)"]
F -- No --> H["Conclusion only"]
G --> I["User pastes /compact or triggers a handoff"]
```
## Plugin config
| Key | Default | Meaning |
|---|---|---|
| `enablePressureTool` | `true` | Register `context_pressure` + `context_history` (needs `tokenMeter`; skipped if missing) |
| `enableCommands` | `true` | Register `/handoff` `/pressure` `/diagnose` (needs `commands`; skipped if missing) |
| `enableReminder` | `false` | Pressure toast: a corner badge in the Web Client suggesting `/compact` or handoff (see below) |
| `remindAt` | `85` | Reminder threshold percent (50–95) |
| `handoffAt` | `92` | Handoff confirmation prompt percent (50–95, must be ≥ `remindAt`); at or above it pauses and requests confirmation before writing `CHECKPOINT.md` |
| `realWindows` | `{}` | Measured safe windows keyed by `provider/model`; see below |
Unknown keys raise an error (`unknown config key`); known keys of the wrong type (e.g. `enableReminder: "true"`, `remindAt: "70"`) also raise, instead of silently inverting or falling back.
### Measured windows `realWindows` (optional)
The built-in table holds **vendor nominal values**: the same model served through a different route or gateway may have a different usable capacity, so a nominal value does not prove your route accepts that much. To make `pct` reflect measured capacity, add an entry per `provider/model`:
```yaml
- id: context-ops
name: 'dsh-context-ops'
config:
enablePressureTool: true
enableCommands: true
enableReminder: false
remindAt: 85
handoffAt: 92
realWindows:
zicode/deepseek-v4-pro: 800000
zicode/glm-5.3: 600000
deepseek-account/deepseek-v4-pro: 1048576
```
Rules:
- **Keys must include the provider** (`provider/model`). A bare model key raises an error: the same model differs per route, so a model-only override would apply one route's measurement to another, which is worse than not configuring it.
- You can copy the key straight from `context_pressure`'s `routeKey`.
- Use roughly **80% of the measured passing value**, not the measured ceiling (manual §1.2); the plugin rounds to an integer.
- The effective window is still `min(declaredWindow, measured)`, so a smaller declared value wins and a measured value never enlarges the window.
- Invalid values (0, negative, a string, a bare model key) fail at load time instead of silently taking effect.
- Routes without an entry keep the previous behavior (nominal table), so upgrading does not change an existing profile.
> ⚠ DSH patches **replace the whole `config` of a matching id**; they do not merge. When overriding the `context-ops` row in your profile, repeat every key, otherwise the others snap back to defaults.
### Pressure toast (optional)
With `enableReminder` on, the Web Client shows a pressure badge (toast) in the **top-right corner of the current session**:
- **Two tiers**:
- usage ≥ `remindAt` (default 85%) → yellow: "suggest /compact";
- usage ≥ `handoffAt` (default 92%) → red: "suggest handoff now".
- **Auto-hides after 60 s**; the hidden state is stored per `sessionId + band` in localStorage, so remounting the component or restarting the app does not restart the timer, and a deadline that elapsed while the app was closed does not resurrect the toast.
- **Hides instantly when a compaction starts** (`compaction/start` signal via projection) — no waiting for the 60-second timer. A compaction-driven downgrade does not re-pop another toast; it re-arms only after a valid reading actually falls back.
- **Usage number**: prefers the host's own `projectedTokens` (next-request estimate, falls back right after compaction); falls back to `pressureTokens`.
- **Nothing written to the session**: the toast is driven by projections (`contextPressure` plus the plugin's own `contextOpsReminderConfig` / `contextOpsCompactionSignal`); it appends no session events and fakes no user messages.
- **No Client, no problem**: without the Web Client (pure CLI / headless) the toast simply never shows; skills and tools are unaffected.
- **Off by default**: only active with `enableReminder: true`. Thresholds (`remindAt` / `handoffAt`) are recomputed from the current profile config each time, so old session caches never pin stale thresholds.
## Robustness
| Situation | Behavior |
|---|---|
| `tokenMeter` missing | Both tools are skipped (no data source); all three skills still register |
| `commands` missing | Only the slash commands are skipped; skills and tools are unaffected |
| Web Client / projections missing | Toast never shows; skills and tools keep working; a dismissed toast is not resurrected when readings recover |
| Tool called with no session | Empty values + `note: no session` (`pressureTokens` stays 0, so **the note matters**) |
| Reading fails | Empty values + a `note`; a reading failure never becomes a conversation failure |
| Reminder evaluation fails | Toast not shown, plus a `warn` log — code errors are not swallowed silently |
## Compaction model (optional)
This plugin **does not hard-code** a provider/model. To route `/compact` summaries to a specific model, configure `compaction-basic` (native DSH fields) in your own profile `cordis.patch.yml`:
```yaml
- id: compaction-basic
config:
summarizationProvider: deepseek-account # replace with your provider
summarizationModel: deepseek-v4-pro # replace with your model
```
Provide both fields or neither; otherwise `compaction-basic` refuses to load.
## Project structure
```
dsh-context-ops/
├── lib/
│ ├── index.js entry: skills + both tools + slash commands + opt-in reminder
│ ├── policy.js shared constants (ladder / host threshold / hard rule / failure causes / windows)
│ └── skills/
│ ├── handoff.js handoff skill
│ ├── decision.js context decision skill
│ └── diagnose.js context diagnosis skill
├── client.js Web Client entry: pressure toast (conversation.input.dock slot)
├── locale/ bilingual metadata (en/zh)
├── assets/ README images (hero, ladder)
├── test/ node --test unit tests
├── cordis.patch.yml self-registration entry
├── icon.svg plugin icon
├── CHANGELOG.md changelog
├── package.json dsh.bundle + dsh.client manifest, depends on zod
└── README.md / README.en.md
```
## Compatibility
| Item | Requirement |
|---|---|
| Node.js | `>= 20` |
| DSH | 0.2.x (uses `ctx.skills.register` / `ctx.tools.register` / `ctx.commands.register` / `tokenMeter`) |
| Runtime dependencies | 1 (`zod`, projection schema validation) |
| Storage changes | None (no DSH storage schema changes; reversible) |
## Development
```sh
# Unit tests (Node's built-in test runner, no extra dependencies)
npm test
# Syntax check
node --check lib/index.js
# Install into a profile locally
dsh plugin --profile add "$(pwd)"
```
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
## License
[MIT](LICENSE) © 廷宇